wikimedia / wikipeg
JavaScript 和 PHP 的解析器生成器
Requires
- php: >=7.4.3
- ext-json: *
- ext-mbstring: *
Requires (Dev)
- mediawiki/mediawiki-codesniffer: 41.0.0
- mediawiki/mediawiki-phan-config: 0.14.0
- mediawiki/minus-x: 1.1.1
- ockcyp/covers-validator: 1.6.0
- php-parallel-lint/php-console-highlighter: 1.0.0
- php-parallel-lint/php-parallel-lint: 1.3.2
- phpunit/phpunit: 9.6.16
- wikimedia/update-history: ^1.0
README
WikiPEG 是一个用于 Node.js 的递归下降解析器生成器,主要用于支持 Parsoid 的复杂需求。它是 PEG.js 的分支,拥有新的后端。
特性
- 简单且易于表达的语法语法
- 集成词汇和句法分析
- 解析器自带出色的错误报告功能
- 基于 解析表达式语法 形式——比传统的 LL(k) 和 LR(k) 解析器更强大
安装
Node.js
要使用 wikipeg
命令,全局安装 WikiPEG
$ npm install -g wikipeg
要使用 JavaScript API,本地安装 WikiPEG
$ npm install wikipeg
如果您需要同时使用 wikipeg
命令和 JavaScript API,请两种方式都安装 WikiPEG。
生成解析器
WikiPEG 从描述预期输入的语法生成解析器,并可以指定解析器返回的内容(使用语义动作处理输入的匹配部分)。生成的解析器本身是一个具有简单 API 的 JavaScript 对象。
命令行
要使用您的语法生成解析器,请使用 wikipeg
命令
$ wikipeg arithmetics.pegjs
这会将解析器源代码写入与语法文件同名但扩展名为 “.js” 的文件。您也可以显式指定输出文件
$ wikipeg arithmetics.pegjs arithmetics-parser.js
如果您省略了输入和输出文件,则使用标准输入和输出。
默认情况下,解析器对象被分配给 module.exports
,这使得输出成为一个 Node.js 模块。您可以通过传递变量名使用 -e
/--export-var
选项将其分配给另一个变量。如果您想在浏览器环境中使用解析器,这可能很有用。
您可以使用几个选项调整生成的解析器
--cache
— 使解析器缓存结果,避免在病态情况下指数级解析时间,但使解析器速度变慢--allowed-start-rules
— 解析器将允许从逗号分隔的规则列表开始解析(默认:语法中的第一个规则)--plugin
— 使 WikiPEG 使用指定的插件(可以多次指定)--extra-options
— 传递给PEG.buildParser
的附加选项(JSON 格式)--extra-options-file
— 包含传递给PEG.buildParser
的附加选项(JSON 格式)的文件--trace
— 使解析器跟踪其进度--header-comment-file
— 包含格式良好的注释的文件,用于自定义生成的文件顶部的注释
JavaScript API
在 Node.js 中,导入 WikiPEG 解析器生成器模块
var PEG = require("wikipeg");
在浏览器中,使用 <script>
标签将 WikiPEG 库包含在您的网页或应用程序中。API 将在 PEG
全局对象中可用。
要生成解析器,调用 PEG.buildParser
方法并传递您的语法作为参数
var parser = PEG.buildParser("start = ('a' / 'b')+");
此方法将返回生成的解析器对象或其源代码字符串(取决于 output
选项的值——见下文)。如果语法无效,它将抛出异常。异常将包含有关错误的更多详细信息的 message
属性。
您可以通过将一个带有选项对象的第二个参数传递给 PEG.buildParser
来调整生成的解析器。以下选项受支持
cache
— 如果设置为true
,将使解析器缓存结果,避免在异常情况下指数级解析时间,但会使解析器变慢(默认:false
)allowedStartRules
— 解析器将允许从这些规则开始解析(默认:语法中的第一个规则)output
— 如果设置为"parser"
,该方法将返回生成的解析器对象;如果设置为"source"
,则返回解析器源代码作为字符串(默认:"parser"
)optimize
— 在生成的解析器优化解析速度("speed"
)或代码大小("size"
)之间进行选择(默认:"speed"
)plugins
— 要使用的插件
使用解析器
使用生成的解析器很简单 — 只需调用其 parse
方法,并将输入字符串作为参数传递。该方法将返回解析结果(确切值取决于构建解析器的语法)或者在输入无效时抛出异常。异常将包含包含有关错误更多详细信息的 location
、expected
、found
和 message
属性。
parser.parse("abba"); // returns ["a", "b", "b", "a"]
parser.parse("abcd"); // throws an exception
您可以通过将一个包含选项对象的第二个参数传递给 parse
方法来调整解析器的行为。以下选项受支持:
startRule
— 要从其开始解析的规则名称tracer
— 要使用的跟踪器
解析器还可以支持它们自己的自定义选项。
语法语法和语义
语法语法类似于 JavaScript,因为它不是面向行的,并且忽略了标记之间的空白。您还可以使用 JavaScript 风格的注释(// ...
和 /* ... */
)。
让我们看看一个示例语法,它可以识别类似于 2*(3+4)
的简单算术表达式。从这个语法生成的解析器计算它们的值。
start
= additive
additive
= left:multiplicative "+" right:additive { return left + right; }
/ multiplicative
multiplicative
= left:primary "*" right:multiplicative { return left * right; }
/ primary
primary
= integer
/ "(" additive:additive ")" { return additive; }
integer "integer"
= digits:[0-9]+ { return parseInt(digits.join(""), 10); }
在顶层,语法由 规则 组成(在我们的示例中,有五个规则)。每个规则都有一个 名称(例如 integer
),用于标识该规则,以及一个 解析表达式(例如 digits:[0-9]+ { return parseInt(digits.join(""), 10); }
),它定义了一个与输入文本匹配的模式,并可能包含一些 JavaScript 代码,该代码确定当模式成功匹配时会发生什么。规则还可以包含用于错误消息的 人类可读名称(在我们的示例中,只有 integer
规则有一个人类可读名称)。解析从第一个规则开始,这也称为 起始规则。
规则名称必须是 JavaScript 标识符。它后面跟着一个等号(“=”)和一个解析表达式。如果规则有一个人类可读名称,则它是以 JavaScript 字符串的形式写入名称和分隔等号之间的。规则只需要通过空白(它们的开始很容易识别)进行分隔,但解析表达式后面的分号(“;”)是允许的。
第一个规则之前可以有一个 初始化器 — 用花括号(“{”和“}”)括起来的一段 JavaScript 代码。这段代码在生成的解析器开始解析之前执行。初始化器中定义的所有变量和函数都可以在规则动作和语义谓词中访问。初始化器代码内部可以使用 parser
变量访问解析器对象,使用 options
变量访问传递给解析器的选项。初始化器代码中的花括号必须是平衡的。让我们看看上面示例语法中使用简单初始化器的示例。
{
function makeInteger(o) {
return parseInt(o.join(""), 10);
}
}
start
= additive
additive
= left:multiplicative "+" right:additive { return left + right; }
/ multiplicative
multiplicative
= left:primary "*" right:multiplicative { return left * right; }
/ primary
primary
= integer
/ "(" additive:additive ")" { return additive; }
integer "integer"
= digits:[0-9]+ { return makeInteger(digits); }
规则中的解析表达式用于将输入文本与语法匹配。有各种类型的表达式 — 匹配字符或字符类,指示可选部分和重复等。表达式还可以包含对其他规则的引用。请参阅下面的详细说明。
当运行生成的解析器时,如果表达式成功匹配文本的一部分,它将生成一个 匹配结果,这是一个 JavaScript 值。例如
- 匹配文本字面量的表达式会产生一个包含匹配输入部分的 JavaScript 字符串。
- 匹配某些子表达式的重复出现会产生一个包含所有匹配的JavaScript数组。
当在表达式中使用规则名称时,匹配结果会通过规则传播,直到起始规则。生成的解析器在解析成功时返回起始规则的匹配结果。
解析表达式的一个特殊情况是 解析动作 —— 一个大括号(“{”和“}”)内的JavaScript代码,它接受前面某些表达式的匹配结果并返回一个JavaScript值。这个值被认为是前面表达式的匹配结果(换句话说,解析动作是一个匹配结果转换器)。
在我们的算术示例中,有许多解析动作。考虑表达式 digits:[0-9]+ { return parseInt(digits.join(""), 10); }
中的动作。它接受表达式 [0-9]+ 的匹配结果作为参数,该表达式是一个包含数字的字符串数组。它将数字连接起来形成一个数字,并将其转换为JavaScript number
对象。
解析表达式类型
存在几种类型的解析表达式,其中一些包含子表达式,从而形成递归结构。
"文字"
'文字'
匹配精确的文字字符串并返回它。字符串语法与JavaScript相同。在文字后追加 i
使匹配不区分大小写。
.
匹配一个字符并作为字符串返回它。
[字符]
匹配集合中的一个字符并作为字符串返回它。列表中的字符可以像在JavaScript字符串中一样进行转义。字符列表还可以包含范围(例如,[a-z]
表示“所有小写字母”)。在字符前加上 ^
会反转匹配集合(例如,[^a-z]
表示“除了小写字母以外的所有字符”)。在右括号后追加 i
使匹配不区分大小写。
规则
递归地匹配一个规则的解析表达式并返回其匹配结果。
( 表达式 )
匹配一个子表达式并返回其匹配结果。
表达式 *
匹配表达式的零次或多次重复并返回一个数组中的匹配结果。匹配是贪婪的,即解析器尽量多地匹配表达式。
表达式 +
匹配表达式的零次或多次重复并返回一个数组中的匹配结果。匹配是贪婪的,即解析器尽量多地匹配表达式。
表达式 ?
尝试匹配表达式。如果匹配成功,返回其匹配结果,否则返回 null
。
& 表达式
尝试匹配表达式。如果匹配成功,仅返回 undefined
并不前进解析器位置,否则认为匹配失败。
! 表达式
尝试匹配表达式。如果匹配不成功,仅返回 undefined
并不前进解析器位置,否则认为匹配失败。
& { 谓词 }
谓词是一段JavaScript代码,它被执行时就像在函数内部一样。它接收前面表达式中标记表达式的匹配结果作为参数。它应该使用 return
语句返回一些JavaScript值。如果返回值在布尔上下文中评估为 true
,则仅返回 undefined
并不前进解析器位置;否则认为匹配失败。
谓词内的代码可以访问语法开始时初始化器中定义的所有变量和函数。
谓词内的代码还可以使用 location
函数访问位置信息。它返回一个类似于这样的对象
{
start: { offset: 23, line: 5, column: 6 },
end: { offset: 23, line: 5, column: 6 }
}
《start》和《end》属性都指代当前的解析位置。《offset》属性包含一个以零为基础的索引,而《line》和《column》属性则包含以一为基础的行和列索引。
谓词内部的代码也可以通过《parser》变量访问解析器对象,并通过《options》变量访问传递给解析器的选项。
请注意,谓词代码中的大括号必须是平衡的。
! { predicate }
谓词是一段JavaScript代码,它被当作函数内部代码执行。它接收前面表达式标签表达式的匹配结果作为其参数。它应该使用《return》语句返回某些JavaScript值。如果返回的值在布尔上下文中评估为《false》,只需返回《undefined》并不要前进解析器位置;否则认为匹配失败。
谓词内的代码可以访问语法开始时初始化器中定义的所有变量和函数。
谓词内的代码还可以使用 location
函数访问位置信息。它返回一个类似于这样的对象
{
start: { offset: 23, line: 5, column: 6 },
end: { offset: 23, line: 5, column: 6 }
}
《start》和《end》属性都指代当前的解析位置。《offset》属性包含一个以零为基础的索引,而《line》和《column》属性则包含以一为基础的行和列索引。
谓词内部的代码也可以通过《parser》变量访问解析器对象,并通过《options》变量访问传递给解析器的选项。
请注意,谓词代码中的大括号必须是平衡的。
$ expression
尝试匹配表达式。如果匹配成功,返回匹配的字符串而不是匹配结果。
label : expression
匹配表达式,并在给定的标签下记住其匹配结果。标签必须是一个JavaScript标识符。
标签表达式与动作结合使用非常有用,其中保存的匹配结果可以通过动作的JavaScript代码访问。
expression1 expression2 ... expressionn
匹配一系列表达式,并以数组的形式返回它们的匹配结果。
expression { action }
匹配表达式。如果匹配成功,运行动作,否则认为匹配失败。
动作是一段JavaScript代码,它被当作函数内部代码执行。它接收前面表达式标签表达式的匹配结果作为其参数。动作应该使用《return》语句返回某些JavaScript值。这个值被认为是前面表达式的匹配结果。
为了指示错误,动作内部的代码可以调用《expected》函数,这会使解析器抛出异常。该函数接受一个参数——描述当前位置期望的内容。这个描述将用作抛出异常的消息的一部分。
动作内部的代码也可以调用《error》函数,这也会使解析器抛出异常。该函数接受一个参数——一个错误消息。这个消息将被用作抛出异常的消息。
动作内部的代码可以访问语法开头初始化器中定义的所有变量和函数。动作代码中的大括号必须是平衡的。
动作内部的代码也可以使用《text》函数访问由表达式匹配的字符串。
动作内部的代码也可以使用《location》函数访问位置信息。它返回一个类似这样的对象
{
start: { offset: 23, line: 5, column: 6 },
end: { offset: 25, line: 5, column: 8 }
}
《start》属性指向表达式的起始位置,而《end》属性指向表达式结束后的位置。《offset》属性包含一个以零为基础的索引,而《line》和《column》属性则包含以一为基础的行和列索引。
动作内部的代码也可以使用《parser》变量访问解析器对象,并通过《options》变量访问传递给解析器的选项。
请注意,动作代码中的大括号必须是平衡的。
expression1 / expression2 / ... / expressionn
尝试匹配第一个表达式,如果它不成功,尝试第二个,等等。返回第一个成功匹配的表达式的匹配结果。如果没有表达式匹配,则认为匹配失败。
规则参数语法
WikiPEG支持向规则传递参数。这是与PEG.js相比的一个扩展。
语法中引用的所有参数都有一个初始值,可以在首次赋值之前使用。
参数在编译时检测类型:布尔型、整数、字符串或引用。每种类型的初始值如下:
- 布尔型:false
- 整数:0
- 字符串:""
- 引用:null
参数命名空间是全局的,但在分配规则引用终止后,参数的值将恢复到其上一个值。
语法如下
& < 参数 >
断言参数 "x" 为真或非零
! < 参数 >
断言参数 "x" 为假或零
规则 < 参数 = true >
递归匹配规则的解析表达式,并将 参数 在 规则 及其调用者中赋值为布尔值真。
规则 < 参数 = false >
将参数 "x" 赋值为假。
规则 < 参数 >
规则<参数=true>的快捷方式。
规则 < 参数 = 0 >
整数赋值。
规则 < 参数 ++ >
将 x = x + 1。
规则 < 参数 = "字面量" >
字符串赋值。
规则 < & 参数 = 1 >
创建一个引用(输入/输出)参数并给它一个初始值1。
请注意,使用非引用语法为引用参数赋值是不合法的。
变量 : < 参数 >
在JS动作或谓词代码中将参数 "x" 的值暴露为变量 "v"。
可以通过这种方式将引用参数的值暴露给JS作为普通rvalue:对它的赋值将不会在相关的动作之外产生影响。
变量 : < & 参数 >
在JS中,这将暴露引用参数 "r" 为一个具有 r.set()、r.get() 方法的对象。在PHP中,它将是一个原生引用,例如 {$r = 1;} 将设置声明作用域中的引用值。
要求
- Node.js 6或更高版本
开发
开发在MediaWiki的Gerrit的 "wikipeg" 项目中进行。
错误应报告给MediaWiki的Phabricator
WikiPEG是David Majda的PEG.js的一个衍生产品。