ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Prettier 对 Markdown 强调符号(emphasis)的规范化:以 issue-15032 测试用例解析 `*` 与 `_` 的选取与转义规则

Prettier 对 Markdown 强调符号(emphasis)的规范化:以 issue-15032 测试用例解析 `*` 与 `_` 的选取与转义规则 开发工具格式化CLI【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址https://gitcode.com/gh_mirrors/pr/prettier点击查看免费下载导读本文围绕 Prettier 仓库中的测试用例 tests/format/markdown/emphasis/issue-15032.md 展开深入讲解 Prettier 格式化 Markdown 强调emphasis与加粗strong语法时如何统一强调符号风格、如何规避与代码段inline code中反引号内容冲突的问题。读完本文你将掌握 Prettier Markdown 打印器选择*还是_的决策逻辑、proseWrap选项对格式化行为的影响以及如何通过快照测试验证这类边角场景。一、测试用例本身4 行输入、2 处改写issue-15032.md文件内容极为精简只有 4 个独立段落1: The **exponentiation (**)** operator. 2: The *exponentiation (*)* operator. 3: The _exponentiation (_)_ operator. 4: The __exponentiation (__)__ operator.每一行都在求幂运算符exponentiation operator这个普通短语中用代码段inline code包裹了一个数学运算符符号同时整段文字又被强调/加粗语法包裹。这里的难点在于强调符号与代码段内部的反引号内容字符相同——**内部包含***内部包含*_内部包含___内部包含__。如果格式化器处理不当很容易破坏原有的强调结构或产生语义歧义。在 tests/format/markdown/emphasis/format.test.js 中该目录下所有用例统一通过runFormatTest(import.meta, [markdown], { proseWrap: always })运行即使用 markdown 解析器并强制开启proseWrap: always始终按printWidth换行。对应的快照文件 tests/format/markdown/emphasis/snapshots/format.test.js.snap 中issue-15032.md一节的期望输出为1: The **exponentiation (**)** operator. 2: The _exponentiation (*)_ operator. 3: The _exponentiation (_)_ operator. 4: The **exponentiation (__)** operator.对比输入与输出可以提炼出两条关键规则行 2*exponentiation (\)→exponentiation (*)单星号强调被改写成单下划线强调行 4__exponentiation (\)→exponentiation (__)双下划线加粗被改写成双星号加粗行 1、行 3**...**与_..._保持原样。也就是说Prettier 对单层强调默认偏好_对加粗默认偏好**但这一偏好存在例外正是这些例外构成了本用例的考察核心。二、强调符号风格选择的源码逻辑Prettier 的 Markdown 打印器在处理 mdast 节点时emphasis节点的打印逻辑位于 src/language-markdown/print/mdast.jscase emphasis: { let style; if (isAutolink(node.children[0])) { style options.originalText[node.position.start.offset]; } else { const hasPrevOrNextWord prevOrNextWord(path); // 1*2*3 is considered emphasis but 1_2_3 is not const inStrongAndHasPrevOrNextWord // 1***2***3 is considered strong emphasis but 1**_2_**3 is not path.callParent( ({ node }) node.type strong prevOrNextWord(path), ); style hasPrevOrNextWord || inStrongAndHasPrevOrNextWord || path.hasAncestor((node) node.type emphasis) ? * : _; } return [style, printChildren(path, options, print), style]; }strong节点则固定输出**见同文件 mdast.jscase strong: return [**, printChildren(path, options, print), **];由此可以还原出强调符号的决策树若强调内容的第一个子节点是自动链接autolink则保留原文使用的符号读取options.originalText中起始偏移处的字符避免破坏 URL 语义否则默认风格是_单下划线当满足以下任一条件时改用*单星号强调内容紧邻前后单词hasPrevOrNextWord强调位于strong内部且紧邻前后单词inStrongAndHasPrevOrNextWord该强调节点嵌套在另一个emphasis节点内部hasAncestor。2.1prevOrNextWord判定函数上述三个例外都依赖辅助函数prevOrNextWord其实现位于 src/language-markdown/print/mdast.jsfunction prevOrNextWord(path) { const { previous, next } path; const hasPrevOrNextWord (previous?.type sentence previous.children.at(-1)?.type word !previous.children.at(-1).hasTrailingPunctuation // https://spec.commonmark.org/0.31.2/#unicode-whitespace-character !/[\p{Space_Separator}\t\n\f\r]$/u.test( previous.children.at(-1).value, )) || (next?.type sentence next.children[0]?.type word !next.children[0].hasLeadingPunctuation !/^[\p{Space_Separator}\t\n\f\r]/u.test(next.children[0].value)); return hasPrevOrNextWord; }其语义是前一个 sentence 的最后一个子节点或后一个 sentence 的第一个子节点是否为真正的单词——要求是word类型、不携带尾随/前导标点、且不以 Unicode 空白字符收尾/开头。这与 CommonMark 规范中强调分隔符的 flanking 规则左/右环绕紧密相关1*2*3在 CommonMark 下会被识别为强调而1_2_3由于下划线在中英文数字间的特殊处理则不会被识别为强调所以此时必须使用*才能保住强调语义。2.2 为什么行 2 与行 4 的输出不同把源码决策树套用到本用例行 2*exponentiation (\)是孤立段落中的强调节点前后无单词邻居也不在strong或外层emphasis内部因此走默认分支输出_得到exponentiation (*)。行 4__exponentiation (\)在 mdast 中是strong节点strong固定输出**因此改写为exponentiation (__)。行 1、行 3**...**已是规范形态_..._本就是默认偏好均无需改动。同时由于代码段inline code中的**、*、_、__是反引号包裹的纯文本内容属于代码段节点而非强调分隔符因此在改写外层强调符号时不会被二次转义或破坏这正是本用例名称issue-15032想要回归验证的行为——格式化不得因为符号雷同而损坏代码段。三、强调/加粗内部的符号转义word.js 的兜底保护仅仅统一外层符号还不够。当强调/加粗内容本身以*或_开头或内部出现可能被 CommonMark 解析为打开/关闭强调的分隔符时Prettier 还必须做转义。这部分逻辑在 src/language-markdown/print/word.js 中实现。3.1 首字符转义在printWord中若当前 word 是强调/加粗内的第一个子节点且文本以*或_开头则先加反斜杠转义见 word.js// escape leading * or _ if its the first character in an emphasis/strong if ( path.isFirst (text.startsWith(*) || text.startsWith(_)) path.callParent(() path.isFirst) path.grandparent emphasisOrStrong ) { text \\${text}; }3.2 内部可开闭分隔符转义随后对内部所有可能被解析为强调分隔符的星号/下划线连续段做替换见 word.js// escape internal * or _ that can open or close emphasis/strong text text.replaceAll( /(\\|^|.)(\*|_)($|.)/g, (match, preceding, delimiterRun, following) { if ( [...preceding].every((c) c \\) preceding.length % 2 1 ) { // already escaped return match; } if ( canOpenOrCloseStrongOrEmphasis( preceding.at(-1) || path.previous?.value.at(-1), delimiterRun, following[0] || path.next?.value[0], ) ) { return ${preceding}\\${delimiterRun}${following}; } return match; }, );其中canOpenOrCloseStrongOrEmphasisword.js直接对应 CommonMark 0.31.2 规范中emphasis and strong emphasis一节的 flanking 规则分别判断分隔符前后是否为空白符\p{Space_Separator}\t\n\f\r或标点PUNCTUATION_REGEXP据此推导 left-flanking / right-flanking 状态从而判定该段分隔符是否具备打开或关闭强调的能力。若具备则在其前插入反斜杠进行转义若前面已经是奇数个反斜杠已被转义则保持不变。这些转义规则与 issue-15032 的用例互为补充本用例考察的是外层强调符号的风格统一而 word.js 保证的是统一之后内部文本仍然安全、不产生新的强调歧义。四、proseWrap 选项与测试运行方式本测试用例运行在proseWrap: always之下这是 Prettier Markdown 格式化的重要前置条件。根据官方选项文档 docs/options.mdproseWrap的取值与含义如下取值行为CLI 参数always将散文按printWidth宽度换行--prose-wrap alwaysnever每个散文块合并为单独一行--prose-wrap neverpreserve保持原有换行不变默认值v1.9.0 起可用--prose-wrap preserve默认值是preserve因为 GitHub 评论、Bitbucket 等平台对换行敏感。在 src/language-markdown/options.js 中Markdown 语言的proseWrap直接复用了通用选项commonOptions.proseWrap因此该选项与 JS/HTML 等语言共享同一套参数定义。对强调符号规范化而言proseWrap: always会先把散文段落按宽度重排、把句子拆分成 sentence 序列再由prevOrNextWord基于重排后的相邻关系判断单词邻接最终影响*/_的选取。这也是快照测试必须显式指定proseWrap: always的原因——不同换行策略下相邻关系可能不同输出也会不同。五、同类用例强调符号规范化的完整证据链tests/format/markdown/emphasis/目录下的其他用例共同构成了强调符号处理的完整回归测试矩阵建议组合阅读asterisk.md输入*123*快照输出_123_验证单星号强调默认改写为下划线的通用规则underscore.md输入_123_保持原样验证下划线是默认偏好complex.md**Do you want to request a *feature* or report a *bug*?**中的内层*feature*、*bug*被改写为_feature_、_bug_验证默认偏好_同样作用于strong内部的单层强调——注意此时并未命中inStrongAndHasPrevOrNextWord例外因为内部强调前后紧邻普通文本而非独立单词issue-3837.md5 * 10、a * b * c这类普通乘法表达式中的星号不会被误判为强调验证了 word.js 中基于 flanking 规则的转义/识别边界issue-6112.md链接文本与路径中含下划线时的强调改写*Italic link*→_..._验证_偏好不会破坏链接语法issue-16160.mdstyle\_name *dplr*中已转义的下划线保持不变*dplr*统一为_dplr_验证转义与偏好规则的叠加issue-17286.md{NO\_PROPAGATE}这类含转义下划线的文本与_response_强调并存时保持稳定的快照nbsp.md含不间断空格的场景下_REPORTED_、_NEEDSINFO_等保持原样special.md0*1*2、!*1*2、1***2***3等符号紧贴数字、感叹号、全角标点的极端组合验证 flanking 判定与1 ***2*** 3改写为1 _**2**_ 3的嵌套加粗行为。六、总结如何验证与复现通过 issue-15032 及其配套用例可以看到Prettier 对 Markdown 强调符号的规范化遵循一套清晰、可解释的规则单层强调默认用_加粗固定用**当强调内容紧邻单词、位于 strong 内部且紧邻单词、或嵌套于外层 emphasis 中时改用*依据 CommonMark flanking 规则保证语义不被破坏自动链接内部的强调符号保留原文word.js 负责对内部可能开闭强调的分隔符做反斜杠转义代码段内容不受影响。要在本地验证可先安装依赖并运行该目录下的专项测试yarn jest tests/format/markdown/emphasis或在命令行直接体验格式化效果echo The *exponentiation (*)* operator. | npx prettier --parser markdown --prose-wrap always输出应为The _exponentiation (\*)_ operator.与快照 [tests/format/markdown/emphasis/__snapshots__/format.test.js.snap](https://link.gitcode.com/i/4a190c0f82a762506ecdc4543ca7e54a) 完全一致。若需深入了解解析与打印的完整链路可继续阅读 [src/language-markdown/print/mdast.js](https://link.gitcode.com/i/a78218e7f5dbcc4c00d8ab4a5cb3d046) 与 [src/language-markdown/print/word.js](https://link.gitcode.com/i/8c4e5af1c0092071bd132fba2e0d3ce0)若需调整 Markdown 换行策略请参考 [docs/options.md](https://link.gitcode.com/i/19493c2c9ff18954c7caebe4919c98c2) 中proseWrap 一节的完整说明。赞分享开发工具格式化CLI【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址https://gitcode.com/gh_mirrors/pr/prettier点击查看免费下载相关推荐marked 强调Emphasis解析深度剖析以 em_2char 规范测试用例为切入点marked 强调Emphasis解析深度剖析以 em_2char 规范测试用例为切入点 导读 本文以 marked 仓库中的规范测试用例 test/sp前端Prettier Markdown 强调语法Emphasis格式化与保真回归测试解析从 issue-17286 看转义下划线、字面花括号与 _response_ 的处理Prettier Markdown 强调语法Emphasis格式化与保真回归测试解析从 issue 17286 看转义下划线、字面花括号与 _respon开发工具格式化CLIPrettier Markdown 段落中的 CJK 格式化以 cjk.md 测试用例解析中文字符排版规则Prettier Markdown 段落中的 CJK 格式化以 cjk.md 测试用例解析中文字符排版规则 本篇文章以 Prettier 仓库中的格式化测试用开发工具格式化CLI创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表