Pegjs: 完整的 Unicode 支持,即 BMP 之外的代码点

创建于 2018-10-22  ·  15评论  ·  资料来源: pegjs/pegjs

问题类型

  • 错误报告:
  • 功能要求:有点
  • 问题:
  • 不是问题:

先决条件

  • 你能重现这个问题吗?:是的
  • 您是否搜索了存储库问题?:
  • 你看论坛了吗?:是的
  • 您是否进行了网络搜索(谷歌、雅虎等)?:是的

描述

如果没有一些自定义样板,JavaScript 无法正确处理BMP之外的 Unicode 字符/代码点,即编码需要超过 16 位的字符/代码点。

这个限制似乎延续到 PEG.js,如下面的例子所示。

特别是,我希望能够指定诸如[\u1D400-\u1D419] (目前变成[ᵀ0-ᵁ9] )或等效的[𝐀-𝐙] (抛出“无效字符范围”)等范围错误)。 (并且使用新的 ES6 符号[\u{1D400}-\u{1D419}]导致以下错误: SyntaxError: Expected "!", "$", "&", "(", ".", character class, comment, end of line, identifier, literal, or whitespace but "[" found. 。)

可能有一种方法可以使这项工作不需要更改 PEG.js 吗?

重现步骤

  1. 从下面给出的语法生成解析器。
  2. 用它来尝试解析表面上符合的东西。

示例代码:

这个语法:

//MathUpper = [𝐀-𝐙]+
MathUpperEscaped = [\u1D400-\u1D419]+

预期行为:

从给定语法生成的解析器成功解析,例如“𝐀𝐁𝐂”。

实际行为:

解析错误: Line 1, column 1: Expected [ᵀ0-ᵁ9] but " (或者,在取消注释其他规则时,会出现“无效字符范围”错误。)

软件

  • PEG.js: 0.10.0
  • Node.js:不适用。
  • NPM 或纱线:不适用。
  • 浏览器:我测试过的所有浏览器。
  • 操作系统: macOS Mojave。
  • 编辑器:我测试过的所有编辑器。
feature need-help task

所有15条评论

老实说,除了更新对PEG.js 语法解析器JavaScript 示例的Unicode 支持之外,我对 Unicode 知之甚少,因此目前无法解决此问题(这两个语法中都明确说明:_Non -BMP 字符完全被忽略_)。

目前,在处理更紧急的个人和工作相关项目(包括 _PEG.js 0.x_)时,我将继续等待更了解 Unicode 的人提供一些 PR 😆,或者最终在 _PEG 之后解决它。 js v1_,对不起,伙计。

仅供参考,代理对似乎有效。 语法
start = result:[\uD83D\uDCA9]+ {return result.join('')}
解析 💩 是 u+1F4A9。 请注意 result.join('') 将代理对重新组合在一起,否则您将获得['\uD83D','\uDCA9']而不是 hankey。 范围会有问题。
更多关于代理对: https ://en.wikipedia.org/wiki/UTF-16#U +010000_to_U+10FFFF

这绝不能取代 OP 的要求。

@drewnolan谢谢提醒👍

不幸的是,该语法也解析\uD83D\uD83D

对于遇到此问题的其他人:我很幸运,因为我只需要处理 BMP 之外的一小部分代码点,所以我最终在解析和反转此映射之前将它们映射到 BMP 的专用区域.

这个解决方案在一般情况下显然充满了问题,但它在我的问题域中运行良好。

@futagoza - 我会尽力解释。 您在这里面临几个问题。

  1. Unicode 具有编码、符号和范围

    1. 在这里,重要的是“范围” - 支持哪些字符 - 和“符号” - 它们是如何书写的

    2. 这些会随着时间而改变。 Unicode 会定期添加新字符,例如添加表情符号或音符时

  2. Unicode 有“符号”。 这些是诸如utf-16ucs4等的东西。 这些是作为预期数据的codepoints被编码为字节的方式。 通过示例utf-16-le可以将大多数字母编码为称为code units两字节对,但使用代码单元组来表达高达0x10ffff高价值字符。

    1. 关键理解:这还不够高。 很多有趣的东西,比如表情符号,大段的中国历史,以及这篇文章的基本问题(黑板数学字符)都在这条线之上



      1. ISO 和 Unicode Consortium 已经明确表示:他们永远不会修复它。 如果您想要更高的字符,请使用比 utf-16 更大的编码。



    2. 关键理解 #2:他妈的 javascript正式是 utf16



      1. 这意味着存在 Javascript 字符串类型无法表示的 unicode 字符(很多)


      2. OP 要求您解决此问题


      3. 这是可能的,但并不容易 - 你必须实现 Unicode 解析算法,这是著名的老鼠窝



我也想要这个,但实际上,这不会发生

天啊,大约一年前有人提交了一个完整的字符串解析器替换,他们认识到了开销,所以他们通常让我们使用标准的 JS 字符串

为什么没有合并

@StoneCypher我爱你心中的火焰! 但是为什么要给当前的维护者带来困难呢? 没有人欠下任何东西。 为什么不维护自己的fork呢?

当前没有维护者。 接手 PEG 的人从未发布过任何东西。 他为下一个未成年人工作了三年,然后说他不喜欢它的外观,正在扔掉所有peg.js ,并从他用另一种语言从头开始写的东西重新开始,与不兼容的AST。

该工具已经失去了一半的用户群,他们在这个人身上等了三年才能提交其他人编写的一行修复程序,例如 es6 模块支持、打字稿支持、箭头支持、扩展的 unicode 等。

有十几个人要求他合并,他一直说“不,这是我现在的爱好项目,我不喜欢它”

很多人都有基于这个解析器的公司。 他们完全搞砸了。

这个人答应做一个极其重要的工具的维护者,并没有做任何维护。 现在是时候让其他人保持这个图书馆的良好信誉了。

为什么不维护自己的fork呢?

我已经三年了。 我的peg修复了几乎三分之一的问题跟踪器。

我不得不克隆它,重命名它,并制作一个新的叉子来修复大小问题以尝试提交它,因为我的已经漂移太多了

是时候让其他所有人以及自 2017 年以来一直位于跟踪器中的修复程序接收这些修复程序了。

这家伙没有保持联系; 他让它死去。

是时候改变了。

@drewnolan - 所以,我不确定这是否有趣,但是,代理对实际上不起作用。 只是,巧合的是,他们通常会这样做。

为了理解潜在的问题,您必须考虑编码级别的位模式,而不是表示级别的第一。

也就是说,如果你的 unicode 值为 240,大多数人会认为“哦,他的意思是0b1111 0000 。 但实际上,Unicode 并不是这样表示 240 的; 超过 127 用两个字节表示,因为最高位是标志位,而不是值位。 所以 Unicode 中的 240 实际上是0b0000 0001 0111 0000存储(除了在 utf-7 中,它是真实的而不是打字错误,事情变得非常奇怪。是的,我知道维基百科说它没有被使用。维基百科是错误的.这是发送短信的内容;它可能是总流量中最常见的字符编码。)

所以问题来了。

如果你写一个字节 STUV WXYZ,然后在 utf16 中,从 ucs4 数据,如果你的东西被切成两半,通常你可以把它装回去。

对于超过两个字节编码的字符本机,您不能使用 128 次。 (听起来像是一个非常具体的数字,不是吗?)

因为当使用第二个字节位置的最高位时,将其切成两半会在本应为 1 的地方添加一个零。 将它们并排装在一起作为二进制数据并不会再次消除价值概念。 解码后的值不等于编码后的值,您附加的是解码,而不是编码。

碰巧大多数表情符号都在这个范围之外。 然而,大部分语言都不是,包括罕见的中文、大多数数学和音乐符号。

诚然,您的方法对于几乎所有表情符号以及每种人类语言都足够好,可以通过 Unicode 6 进入,这是对现状的巨大改进

但是这个 PR 真的应该合并,一旦它的正确性和意外性能问题都经过充分测试(记住,unicode 性能问题是 php 死的原因)

似乎. (dot character)表达式也需要 Unicode 模式。 相比:

const string = '-🐎-👱-';

const symbols = (string.match(/./gu));
console.log(JSON.stringify(symbols, null, '  '));

const pegResult = require('pegjs/')
                 .generate('root = .+')
                 .parse(string);
console.log(JSON.stringify(pegResult, null, '  '));

输出:

[
  "-",
  "🐎",
  "-",
  "👱",
  "-"
]
[
  "-",
  "\ud83d",
  "\udc0e",
  "-",
  "\ud83d",
  "\udc71",
  "-"
]

我最近在做这方面的工作,使用 #616 作为基础并将其修改为使用 ES6 语法\u{hhhhhhh} ,我将在几个小时内创建一个 PR。

计算由代理分割的 UTF-16 正则表达式范围有点复杂,我为此使用了https://github.com/mathiasbynens/regenerate ; 这将是 pegjs 包的第一个依赖项,我希望这是可能的(还有用于 Unicode 属性的 polyfills 可以作为依赖项添加,参见 #648)。 如果您不知道 UTF-16 代理,请参阅维基百科

为了使 PEG.js 与整个 Unicode 兼容,有多个级别:

  1. 在 BMP 之上添加一个对 Unicode 字符进行编码的语法,由 #616 或我的 ES6 语法版本修复,
  2. 识别常量字符串,上点直接提供,
  3. 修复 SyntaxError 报告可能显示 1 或 2 个代码单元以显示真正的 Unicode 字符,
  4. 准确计算 BMP 和/或星体代码点的正则表达式类 - 仅此一项是行不通的,请参阅下一点,
  5. 管理游标增量,因为正则表达式类现在可以是 (1)、(2) 或(1 或 2,具体取决于运行时),请参阅下面的详细信息,
  6. 实现规则点.以捕获 1 或 2 个代码单元。

对于大多数点,我们可以设法向后兼容并生成与旧的解析器非常相似的解析器,除了点 5,因为解析的结果可能取决于点规则是否捕获了一个或两个代码单元。 为此,我建议添加一个运行时选项,让用户在两个或三个选项之间进行选择:

  1. 点规则只捕获一个 BMP 代码单元,
  2. 点规则捕获一个 Unicode 代码点(1 或 2 个代码单元),
  3. 点规则捕获一个 Unicode 代码点或一个单独的代理。

可以在解析器生成期间静态分析 Regex 类,以检查它们是否具有固定长度(以代码单元数表示)。 有 3 种情况:1. 只有一个 BMP 或单个代码单元,或 2. 只有两个代码单元,或 3. 一两个代码单元,具体取决于运行时。 现在,字节码假定正则表达式类始终是一个代码单元(请参阅此处)。 通过静态分析,对于前两种情况,我们可以将此字节码指令的此参数更改为 1 或 2。 但是对于第三种情况,我想应该添加一个新的字节码指令,以便在运行时获取匹配的代码单元数并相应地增加光标。 没有新字节码指令的其他选项将是: 1. 始终计算匹配的代码单元的数量,但在仅 BMP 解析器的解析过程中这是性能损失,因此我不喜欢此选项; 2. 计算当前代码单元是否是高代理,然后是低代理以递增 1 或 2,但这将假设语法始终具有格式良好的 UTF-16 代理,而无法编写具有单独代理的语法(见下一点),这也是 BMP-only 解析器的性能损失。

存在单独代理的问题(高代理没有低代理,或者低代理没有高代理)。 我对此的看法是,正则表达式类应该是唯一的:要么带有单独的代理,要么带有格式良好的 UTF-16 Unicode 字符(BMP 或高代理后跟低代理),否则存在语法作者不知道的危险UTF-16 的微妙之处混合了两者,无法理解结果,想要管理自己的 UTF-16 代理的语法作者可以使用 PEG 规则来描述特定高低代理之间的联系。 我建议在解析器生成期间添加一个执行此规则的访问者。

总而言之,在 PEG 中处理单独代理的问题可能比在正则表达式中更容易,因为 PEG 解析器总是在前进,因此要么识别下一个代码单元要么不识别,与正则表达式相反,其中可能有一些回溯可能关联或将高代理与低代理分离,从而更改匹配的 Unicode 字符数等。

用于星形 Unicode 字符的 ES6 语法的 PR 是基于 #616 的 #651,类的开发是https://github.com/Seb35/pegjs/commit/0d33a7a4e13b0ac7c55a9cfaadc16fc0a5dd5f0c实现了我上面的评论中的第 2 点和第 3 点,光标增量的快速破解(第 4 点),现在没有规则点. (第 5 点)。

我目前关于这个问题的开发基本完成,更高级的工作在https://github.com/Seb35/pegjs/tree/dev-astral-classes-final。 上面提到的所有五点都得到了处理,并且全局行为试图模仿关于边缘情况的 JS 正则表达式(并且有很多)。

全局行为由选项unicode类似于 JS 正则表达式中的标志unicode :光标根据实际文本(例如[^a]匹配文本“💯”,光标增加2个代码单元)。 When the option unicode is false the cursor is always increased of 1 code unit.

关于输入,我不确定我们是否以与 JS 正则表达式相同的方式设计 PEG.js:我们是否应该在非 Unicode 模式的语法中授权[\u{1F4AD}-\u{1F4AF}] (相当于[\uD83D\uDCAD-\uD83D\uDCAF] 我们可以区分“输入 Unicode”和“输出 Unicode”:

  • input Unicode 是关于授权字符类中的所有 Unicode 字符(内部计算为固定的 2-code-unit 或固定的 1-code-unit)
  • 输出 Unicode 是结果解析器的光标增量:1 个代码单元或 1 个 Unicode 字符用于规则“点”和“倒置字符类”——唯一没有明确列出字符的规则,我们需要从语法中做出决定作者

就我个人而言,我想我更希望我们永久授权输入 Unicode,或者使用带有默认true的选项,因为没有显着的开销,默认情况下这将为每个人启用这种可能性,但输出 Unicode 应保持false因为生成的解析器的性能更好(游标增量始终为 1)。

关于这个问题(以及关于输出 Unicode 默认为false ),我们应该记住,已经有可能在我们的语法中编码 Unicode 字符,代价是理解 UTF-16 的功能:

// rule matching [\u{1F4AD}-\u{1F4AF}]
my_class = "\uD83D" [\uDCAD-\uDCAF]

// rule matching any Unicode character
my_strict_unicode_dot_rule = $( [\u0000-\uD7FF\uE000-\uFFFF] / [\uD800-\uDBFF] [\uDC00-\uDFFF] )

// rule matching any Unicode character or a lone surrogate
my_loose_unicode_dot_rule = $( [\uD800-\uDBFF] [\uDC00-\uDFFF]? / [\u0000-\uFFFF] )

因此,想要快速解析器并能够识别她/他的语法特定部分中的 Unicode 字符的语法作者可以使用这样的规则。 因此,这个问题仅仅是关于简化 Unicode 管理,而不是深入研究 UTF-16 内部。


关于实现,我在第一次尝试时考虑到语法文本是用 Unicode 字符编码的,而 PEG.js 解析器的“点”规则可以识别 Unicode 字符。 第二次也是最后一次尝试恢复了这一点(点规则总是 1 个代码单元以便更快地解析)并且访问者 prepare-unicode-classes.js 中有一个小算法来重建字符类中的拆分 Unicode 字符(例如[\uD83D\uDCAD-\uD83D\uDCAF]在语法上被识别为[ "\uD83D", [ "\uDCAD", "\uD83D" ], "\uDCAF" ]并且该算法将其转换为[ [ "\uD83D\uDCAD", "\uD83D\uDCAF" ] ] )。 我想在语法本身中写这个,但它会很长,更重要的是有多种方法来编码字符(“💯”,“uD83DuDCAF”,“u{1F4AF}”),所以写起来更容易在访客中。

我在第二次尝试中添加了两个操作码:

  • MATCH_ASTRAL 类似于 MATCH_ANY 但匹配一个 Unicode 字符(input.charCodeAt(currPos) & 0xFC00) === 0xD800 && input.length > currPos + 1 && (input.charCodeAt(currPos+1) & 0xFC00) === 0xDC00
  • MATCH_CLASS2 与 MATCH_CLASS 非常相似,但匹配接下来的两个代码单元而不是一个classes[c].test(input.substring(currPos, currPos+2)
    然后,根据我们匹配的是 2-code-unit 字符还是 1-code-unit 字符,使用操作码ACCEPT_N将光标增加 1 或 2 个代码单元,并且字符类被分成两部分固定长度的正则表达式(1 或 2 个代码单元)。

我对“匹配”功能进行了一些优化,在生成过程中根据模式(Unicode 与否)和字符类消除了“死代码”路径。

另请注意,此实现中的正则表达式始终为正:反转的正则表达式返回相反的结果,从而产生字节码。 这更容易避免代理周围的边缘情况。

我写了一些文档,但我可能会添加更多(也许是一个指南,可以快速解释选项的详细信息 unicode 和带有自制 Unicode 点规则的片段)。 我将在将其作为 PR 提交之前添加测试。

此页面是否有帮助?
0 / 5 - 0 等级