Gutenberg 块序列化规范解析器(@wordpress/block-serialization-spec-parser)完全指南:从 PEG 文法到双端解析
2026/9/17 3:20:03 网站建设 项目流程

Gutenberg 块序列化规范解析器(@wordpress/block-serialization-spec-parser)完全指南:从 PEG 文法到双端解析

【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg

导读

本文围绕 Gutenberg 项目中@wordpress/block-serialization-spec-parser包展开,它承载着 WordPress 块编辑器的“序列化规范”——用一份 PEG(Parsing Expression Grammar,解析表达式文法)文件描述块注释标记的合法语法,并据此在构建期生成浏览器端 JavaScript 与 WordPress 服务端 PHP 两套解析器。读完本文,你将理解块注释格式(<!-- wp:... -->)的完整文法规则、parse()的返回数据结构、双端生成的构建流程,以及该包与@wordpress/blocksserializeRawBlock之间的上下游关系,并能把示例直接跑起来验证。


一、这个包解决什么问题

在 Gutenberg 中,一篇文档/一篇文章被序列化为“HTML 注释包裹的块标记”与普通 HTML 的混合体,例如:

<!-- wp:core/more --><!--more--><!-- /wp:core/more -->

解析器要做的是:把这样的混合内容还原成结构化的块对象数组,供编辑器恢复每个块的名称、属性与嵌套关系。而@wordpress/block-serialization-spec-parser的特殊之处在于,它不是手写解析逻辑,而是先定义一份规范文法grammar.pegjs),再用 PEG 解析器生成器产出真正的解析器。正如包 README 所述:

This library contains the grammar file (grammar.pegjs) for WordPress posts which is a block serializationspecificationwhich is used to generate the actualparserwhich is also bundled in this package.

即:文法是“规范”,生成的解析器是“实现”,两者同装在该包内。相关背景概念可参考 PEG.js 与 Parsing expression grammar(PEG 文法:每个规则按顺序尝试子规则,取第一个成功匹配)。


二、安装

包名发布在 npm 上,安装命令(见 README.md):

npm install @wordpress/block-serialization-spec-parser --save

从 package.json 可以看到版本与环境要求:

  • 当前版本:5.55.0
  • 运行时要求:node >=18.12.0npm >=8.19.2
  • 主入口:parser.js(即构建生成的 JS 解析器)
  • 依赖:pegjs ^0.10.0(生成 JS 解析器)与phpegjs ^1.0.0-beta7(生成 PHP 解析器)
  • 开发依赖:vitest ^5.0.0(测试框架)
  • sideEffects: false,可被 tree-shaking 安全处理
  • 额外导出:./shared-tests(JS/PHP 共享测试用例)、./package.json

三、基础用法

README 给出的最小示例:

import { parse } from '@wordpress/block-serialization-spec-parser'; parse( '<!-- wp:core/more --><!--more--><!-- /wp:core/more -->' ); // [{"attrs": null, "blockName": "core/more", "innerBlocks": [], "innerHTML": "<!--more-->"}]

注意 README 示例中attrs显示为null,这是文档撰写时的输出快照;按当前文法与测试(shared-tests.js)的实现,未携带属性时会回退为空对象{}(详见下文数据结构说明)。实际使用时以你自己环境里parse()的返回为准。

更完整的调用形态(测试中大量使用的输入)包括:

parse( '<!-- wp:void /-->' ); // → [{ blockName: 'core/void', attrs: {}, innerBlocks: [], innerHTML: '', innerContent: [] }] parse( '<!-- wp:my/bus { "is": "fast" } /-->' ); // → [{ blockName: 'my/bus', attrs: { is: 'fast' }, innerBlocks: [], innerHTML: '', innerContent: [] }] parse( '<!-- wp:block -->Before<!-- wp:void /--><!-- /wp:block -->' ); // → [{ blockName: 'core/block', attrs: {}, innerBlocks: [/* 内层块 */], // innerHTML: 'Before', innerContent: [ 'Before', null ] }] parse( '<p>Break me</p><!-- wp:block /-->' ); // → [{ blockName: null, attrs: {}, innerHTML: '<p>Break me</p>', ... }, // { blockName: 'core/block', ... }]

四、解析输出的数据结构

parse()始终返回一个数组,数组中的每个元素是一个块对象,字段如下(由文法中的辅助函数与测试共同确认):

字段类型含义
blockNamestring \| null块的完整名称,如core/moremy/bus自由 HTML(freeform)片段为null
attrsobject开块注释中 JSON 编码的属性对象;无属性时为空对象{}
innerBlocksarray嵌套在该块内部的子块数组
innerHTMLstring该块内部的原始 HTML 字符串(不含开/闭注释)
innerContentarray内部内容的分段序列:HTML 片段原样保留,子块位置以null占位

innerContent的设计是让上层可以精确重建原始内容:遍历该数组,遇到字符串直接拼接,遇到null则替换为对应位置的子块序列化结果。这正是 serialize-raw-block.ts 所做的事——它把innerContent中非null的片段与null处的innerBlocks[childIndex++]交错重排,再经getCommentDelimitedContent重新生成注释标记:

const content = innerContent .map( ( item ) => item !== null ? item : serializeRawBlock( innerBlocks[ childIndex++ ], options ) ) .join( '\n' ) .replace( /\n+/g, '\n' ) .trim();

该文件注释明确将@wordpress/block-serialization-spec-parser列为“合法解析器返回的块节点格式”的权威参考之一,可见innerContent结构是整个块序列化体系的事实契约。


五、文法深度解析:grammar.pegjs

核心文件是 grammar.pegjs。文件顶部注释说明:这是 Gutenberg 文档的官方规范文法,以顶层规则Block_List为入口;文法中嵌入了尽量少的“代码”(辅助函数 + 每条规则的动作),解析器生成器据此产出两套解析器——浏览器端 JavaScript 版与 WordPress 的 PHP 版。

文法文件同时支持 JS 与 PHP 输出:动作代码中用/** <?php ... ?> **/注释块标注 PHP 版本实现,其外是 JS 版本实现,phpegjs 在生成 PHP 解析器时会取出 PHP 分支。

5.1 顶层规则Block_List

Block_List = pre:$(!Block .)* bs:(b:Block html:$((!Block .)*) { return [ b, html ] })* post:$(.*) { return joinBlocks( pre, bs, post ); }

语义:内容被切分为「块前自由 HTMLpre→ 一系列「块 + 块后 HTML 片段」→ 尾部自由 HTMLpost」,最终交给joinBlocks组装。joinBlocks会把每段非空 HTML 包装成blockName: null的 freeform 块,夹在真正的块之间——这正是“HTML 汤(HTML soup)也能被解析成自由块”的机制。

5.2 块的两类形态:Block_VoidBlock_Balanced

Block规则按顺序尝试两种子规则:

  • 自闭合(void)块
Block_Void = "<!--" __ "wp:" blockName:Block_Name __ attrs:(a:Block_Attributes __ {...})? "/-->"

<!-- wp:名称 [属性] /-->,返回innerHTML: ''innerContent: []attrs: attrs || {}

  • 成对(balanced)块
Block_Balanced = s:Block_Start children:(Block / $((!Block !Block_End .)+))* e:Block_End

<!-- wp:名称 [属性] --><!-- /wp:名称 -->之间的任意内容,children递归匹配内层Block或普通文本;最终由processInnerContent把 children 拆成[ innerHTML, innerBlocks, innerContent ]三元组。

5.3 开块/闭块标记:Block_StartBlock_End

Block_Start = "<!--" __ "wp:" blockName:Block_Name __ attrs:(...)? "-->" Block_End = "<!--" __ "/wp:" blockName:Block_Name __ "-->"

开标记可携带可选的 JSON 属性段,闭标记只校验名称。注意这里Block_End并不强制与Block_StartblockName一致——文法层面的容错由上层逻辑负责,文法只保证“能解析”。

5.4 块命名规则:Block_Name

Block_Name = Namespaced_Block_Name / Core_Block_Name Namespaced_Block_Name = $( Block_Name_Part "/" Block_Name_Part ) Core_Block_Name = type:$( Block_Name_Part ) { return 'core/' + type; } Block_Name_Part = $( [a-z][a-z0-9_-]* )
  • 带命名空间的名称a/b原样保留(如my/moremy/more);
  • 无命名空间的裸名称自动补全为core/前缀(如morecore/more);
  • 名称片段必须以小写字母开头,随后可包含小写字母、数字、_-

这正是测试'blockName is namespaced string (except freeform)'所验证的行为(见 shared-tests.js)。

5.5 JSON 属性:Block_Attributes

Block_Attributes = attrs:$("{" (!("}" __ """/"? "-->") .)* "}") { return maybeJSON( attrs ); }

属性是开块注释内的一对花括号包裹的 JSON。规则用否定前瞻排除提前遇到闭标记的情形,再交给maybeJSONJSON.parse;解析失败时返回null(JS 端maybeJSON的 catch 分支)。测试覆盖了带空格、换行、嵌套对象、长字符串等变体,例如:

parse( '<!-- wp:void { "value" : true } /-->' )[0].blockName === 'core/void' parse( '<!-- wp:void {\n\t"value" : true\n} /-->' )[0].blockName === 'core/void'

5.6 空白:__

__ = [ \t\r\n]+

用于标记名、属性等之间的空白分隔,至少 1 个空格/制表符/换行。因此<!-- wp:block / -->(自闭合标记/后有空格)不符合Block_Void语法,会被当作自由 HTML 解析——这也是 shared-tests.js 中 “invalid block comment syntax” 用例验证的行为。

5.7 内嵌的辅助函数

文法文件顶部声明了一组最小辅助函数,生成解析器时会内嵌进去:

  • freeform(s):把一段 HTML 包装成blockName: null的自由块;
  • joinBlocks(pre, tokens, post):把「前部 HTML + 块序列 + 后部 HTML」拼接成完整块数组;
  • maybeJSON(s):安全解析 JSON,失败返回null(仅 JS 需要,PHP 用json_decode语义等价替代);
  • processInnerContent(list):把 children 混合列表拆分为[innerHTML, innerBlocks, innerContent](字符串进 HTML/内容,块进子块/null占位)。

对应的 PHP 版本(peg_empty_attrspeg_process_inner_contentpeg_join_blocks)以/** <?php ... ?> **/形式写在函数体内,供 PHP 生成器提取。另外注意 PHP 版对“空属性”做了专门处理:peg_empty_attrs()json_decode('{}', true)缓存空对象,避免 PHP 空数组与空列表的序列化歧义。


六、双端构建:一份文法生成 JS 与 PHP 两套解析器

package.json 中的构建脚本:

"scripts": { "prelint:js": "npm run build:js", "build": "concurrently \"npm run build:js\" \"npm run build:php\"", "build:js": "pegjs --format commonjs -o ./parser.js ./grammar.pegjs", "build:php": "node bin/create-php-parser.js" }
  • JS 解析器:直接用pegjsCLI 把grammar.pegjs编译为 CommonJS 模块parser.js,即包的主入口;
  • PHP 解析器:走 bin/create-php-parser.js,调用pegjs.generate并注入phpegjs插件,产出parser.php
const parser = pegjs.generate( peg, { plugins: [ phpegjs ], phpegjs: { parserNamespace: null, parserGlobalNamePrefix: 'Gutenberg_PEG_', mbstringAllowed: false, }, } );

生成出的 PHP 解析器类名为Gutenberg_PEG_Parser,从 test/test-parser.php 可以看到其用法:

require_once __DIR__ . '/../parser.php'; $parser = new Gutenberg_PEG_Parser(); echo json_encode( $parser->parse( file_get_contents( 'php://stdin' ) ) );

即:PHP 解析器从标准输入读取文档,parse()json_encode输出——与 JS 版保持相同的数据结构契约,这也是 WordPress 服务端能还原同一篇块文档的根本保证。


七、测试体系:一套用例同时跑 JS 与 PHP

shared-tests.js 定义了jsTesterphpTester两个导出:

  • jsTester直接调用传入的parse函数,覆盖以下分组:
    • 输出结构:始终返回数组;blockNameattrsinnerBlocksinnerHTML的类型与取值;
    • 通用行为:多个可复用块({"ref":313})、自闭合与空成对块等价、块前后 HTML soup 的捕获;
    • innerContent 占位符:字符串原样保留、子块位置为null、前后片段与相邻子块的组合;
    • 攻击向量:10 万字符的 JSON 属性段不抛异常、<!-- wp:block / -->这类带多余空格的“伪 void”被当作自由文本(blockName === null)。
  • phpTester在检测到系统装有phpNODE_ENV === 'test'时通过spawnSync('php', ['-r', 'echo 1;'])探测)时,把同一套jsTester用例喂给 PHP 解析器:通过php -f test-parser.php传 stdin 输入、读 stdout 结果;因为 PHPjson_encode会把空关联数组序列化成[],测试做了一次"attrs":[] → "attrs":{}的正则替换以对齐 JS 输出(测试框架层面的归一化,并非解析器差异)。

入口 test/index.js 使用 Vitest,同时注册 JS 与 PHP 两套描述块,做到“同一份断言、双端验证”。


八、在 Gutenberg 生态中的位置

该包是块序列化体系的“规范实现”层。与之对照:

  • @wordpress/block-serialization-default-parser提供默认解析器(非 PEG 生成);
  • @wordpress/block-serialization-spec-parser提供文法驱动、可双端生成的解析器;
  • 上层@wordpress/blocks的 serialize-raw-block.ts 负责把解析器产出的原始块节点重新序列化为注释标记,其注释明确引用本包与 default-parser 作为“合法解析器输出格式”的权威说明。

因此整条链路是:grammar.pegjs(规范)→parser.js/parser.php(解析)→ 块节点对象(blockName/attrs/innerBlocks/innerHTML/innerContent)→serializeRawBlock(再序列化)。理解这份文法,就等于理解了 WordPress 块标记的底层语法约束:命名必须小写开头、属性必须 JSON、自闭合与成对标记的边界、以及任意 HTML 都会被兜底为自由块。


九、参与贡献

该包是 Gutenberg monorepo 的一部分(packages/下的自包含包,独立发布到 npm,被 WordPress 核心及其他项目使用)。若想贡献,可参考项目主 CONTRIBUTING.md 以及包内的 CHANGELOG.md;本地开发时注意先执行构建(npm run build)以生成parser.jsparser.php,再通过 Vitest 运行双端测试。


十、要点小结

  1. 文法即规范grammar.pegjs是 WordPress 块序列化的官方 PEG 规范,从顶层Block_List开始逐规则定义合法标记;
  2. 两类块形态:自闭合<!-- wp:name attrs /-->与成对<!-- wp:name attrs -->…<!-- /wp:name -->
  3. 命名归一:裸名自动加core/前缀,命名空间名原样保留,片段须以小写字母开头;
  4. 输出契约:每个块对象含blockName / attrs / innerBlocks / innerHTML / innerContent五个字段,自由 HTML 的blockNamenull,子块位置在innerContent中以null占位;
  5. 双端同构:pegjs + phpegjs 从同一份文法生成 JS 与 PHP 两套解析器,共享同一套测试用例,确保浏览器端与 WordPress 服务端解析结果一致。

【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询