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/blocks、serializeRawBlock之间的上下游关系,并能把示例直接跑起来验证。
一、这个包解决什么问题
在 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.0,npm >=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()始终返回一个数组,数组中的每个元素是一个块对象,字段如下(由文法中的辅助函数与测试共同确认):
| 字段 | 类型 | 含义 |
|---|---|---|
blockName | string \| null | 块的完整名称,如core/more、my/bus。自由 HTML(freeform)片段为null |
attrs | object | 开块注释中 JSON 编码的属性对象;无属性时为空对象{} |
innerBlocks | array | 嵌套在该块内部的子块数组 |
innerHTML | string | 该块内部的原始 HTML 字符串(不含开/闭注释) |
innerContent | array | 内部内容的分段序列: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_Void与Block_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_Start与Block_End
Block_Start = "<!--" __ "wp:" blockName:Block_Name __ attrs:(...)? "-->" Block_End = "<!--" __ "/wp:" blockName:Block_Name __ "-->"开标记可携带可选的 JSON 属性段,闭标记只校验名称。注意这里Block_End并不强制与Block_Start的blockName一致——文法层面的容错由上层逻辑负责,文法只保证“能解析”。
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/more→my/more); - 无命名空间的裸名称自动补全为
core/前缀(如more→core/more); - 名称片段必须以小写字母开头,随后可包含小写字母、数字、
_、-。
这正是测试'blockName is namespaced string (except freeform)'所验证的行为(见 shared-tests.js)。
5.5 JSON 属性:Block_Attributes
Block_Attributes = attrs:$("{" (!("}" __ """/"? "-->") .)* "}") { return maybeJSON( attrs ); }属性是开块注释内的一对花括号包裹的 JSON。规则用否定前瞻排除提前遇到闭标记的情形,再交给maybeJSON做JSON.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_attrs、peg_process_inner_content、peg_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 定义了jsTester与phpTester两个导出:
jsTester直接调用传入的parse函数,覆盖以下分组:- 输出结构:始终返回数组;
blockName、attrs、innerBlocks、innerHTML的类型与取值; - 通用行为:多个可复用块(
{"ref":313})、自闭合与空成对块等价、块前后 HTML soup 的捕获; - innerContent 占位符:字符串原样保留、子块位置为
null、前后片段与相邻子块的组合; - 攻击向量:10 万字符的 JSON 属性段不抛异常、
<!-- wp:block / -->这类带多余空格的“伪 void”被当作自由文本(blockName === null)。
- 输出结构:始终返回数组;
phpTester在检测到系统装有php(NODE_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.js与parser.php,再通过 Vitest 运行双端测试。
十、要点小结
- 文法即规范:
grammar.pegjs是 WordPress 块序列化的官方 PEG 规范,从顶层Block_List开始逐规则定义合法标记; - 两类块形态:自闭合
<!-- wp:name attrs /-->与成对<!-- wp:name attrs -->…<!-- /wp:name -->; - 命名归一:裸名自动加
core/前缀,命名空间名原样保留,片段须以小写字母开头; - 输出契约:每个块对象含
blockName / attrs / innerBlocks / innerHTML / innerContent五个字段,自由 HTML 的blockName为null,子块位置在innerContent中以null占位; - 双端同构: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),仅供参考