Prettier 中日韩(CJK)Markdown 文本格式化:splitCjkText 测试集与空白处理机制全解析
Prettier 中日韩CJKMarkdown 文本格式化splitCjkText 测试集与空白处理机制全解析【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier本篇指南以 Prettier 仓库中的 Markdown 测试夹具 tests/format/markdown/splitCjkText/chinese-japanese.md 为切入点深入剖析 Prettier 在处理中文、日文以及韩文Markdown 文本时的切词、空白归一化与换行决策规则。读完你将理解为什么纯中文/日文段落不会被 Prettier 强制断行、为什么中日文之间不会凭空插入空格、全角空格 U3000 为何被原样保留以及proseWrap、printWidth等选项在 CJK 场景下的真实作用边界并掌握在仓库中运行与扩展此类格式测试的方法。一、夹具在 Prettier 测试体系中的角色tests/format/markdown/splitCjkText/目录是 Prettier 针对 Markdown 中「中日韩文本切分split CJK text」行为的专门测试集合与src/language-markdown/下的实现一一对应chinese-japanese.md中文 日文混合边界夹具本文核心han-kana-alnum.md汉字与假名、字母数字混排korean.md韩文谚文与 Latin 混排space.md、symbolSpaceNewLine.md、link.md、mixed.md空白、符号、链接等专项场景format.test.js测试驱动脚本通过runFormatTest以proseWrap: always运行上述所有夹具snapshots/format.test.js.snap快照文件固化输入与输出快照中chinese-japanese.md用例的output 与 input 完全一致见 format.test.js.snap这本身就宣告了一个核心结论对于这类纯中日文文本Prettier 的格式化结果是不增删任何空白的「原样保留」。下面逐段拆解这个结论背后的 7 类边界行为。二、chinese-japanese.md 逐段解读七类边界行为夹具共 13 行、7 个段落每一段都对应一种独立的字符学特征1. 纯中文长段落字与字之间不插入空格、不制造换行点這是一段很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長的段落该段完全由汉字组成、没有任何空白字符。格式化后逐字原样输出。原因在于中文、日文不使用 U0020 空格分词字符之间天然「无缝」Prettier 不会在汉字之间插入空格也不会把「字间」当作可换行点源码注释明确写道Chrome 与 Safari 现在会把这类字符之间的\n替换为空格因此 Prettier 同样不在中日文字符之间断行见 whitespace.js。2. 全角空格 U3000 行被当作「标点」原样保留全 形 空白全 形 空白全 形 空白全 形 空白全 形 空白全 形 空白全 形 空白全角空格Ideographic SpaceU3000没有被当作普通空白归一化而是被归入「CJK 标点」类别在 constants.evaluate.js 中U3000 被显式加入PUNCTUATION_REGEXP字符集注释还引用了 Firefox 的 bug 跟踪编号说明其动机在 format.test.js 中同样注明「Fullwidth Space but should be treated like punctuation」。因此它像标点一样「粘」在相邻汉字之间数量与位置完全保留。3. 汉字 半角空格 全角空格混排既有空格被精确保留空白全形空白全形空白全形空白 空白全形空白全形空白全形空白 空白全形空白全形空白全形空白 空白全形空白全形空白全形空白半角空格 U0020 被识别为whitespace节点位于 CJ 字符与 CJK 标点全角空格之间属于「不可断行」的空白直接以空格形式输出全角空格则按第 2 条规则整体保留。两者交织出现时输出与输入一致。4. 日文长句含浊音/半浊音假名连字与标点整体粘合何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。纯日文句含拗音「ゃ」、浊音点等组合形式格式化后逐字保留证明由平假名、片假名构成的连续序列内部不存在任何格式化的插入点。5. 片假名 浊音符/半浊音符组合用记号「カ゚」「キ゚」不被打散カ゚キ゚ク゚ケ゚コ゚でガギグゴ「゚」U309A 半浊点与「゙」U3099 浊点属于 Unicode General Category 中的Nonspacing_Mark组合用记号被 constants.evaluate.js 的 CJK 字符集显式收录。因此CJK_REGEXP会把「カ゚」作为一个整体字符含可选的可变选择符匹配组合用记号绝不会与基字符分离。6. Latin 词 片假名无空格混排「nasal」后不补空格nasalカ゚キ゚ク゚ケ゚コ゚カ゚キ゚ク゚ケ゚コ゚カ゚キ゚ク゚ケ゚コ゚カ゚キ゚ク゚ケ゚コ゚カ゚キ゚ク゚ケ゚コ゚カ゚キ゚ク゚ケ゚コ゚カ゚キ゚ク゚ケ゚コ゚カ゚キ゚ク゚ケ゚コ゚カ゚キ゚ク゚ケ゚コ゚カ゚キ゚ク゚ケ゚コ゚这是全文最有信息量的一段ASCII 单词nasal与片假名之间没有任何空格且整句内 CJ 与 Latin 之间不存在任何空格usesCJSpaces为false因此 Prettier不会在nasal与「カ」之间凭空插入空格整个超长段落也因为没有任何可断行的空白而保持单行不折行。7. 历史假名叙述 方括号 [v] 标注CJK 与 ASCII 符号的空格风格保持かつてはワ行のワ、ヰ、ヱ、ヲに濁点を付して [v] 音を表現することワ゛、ヰ゛、ヱ゛、ヲ゛も行われたが、一般的にはならなかった。句内「付して [v] 音を表現」中ASCII 方括号[v]两侧原本有半角空格。由于[v]属于非 CJK 词、两侧都是 CJ 字符Prettier 判定其不可断行、保留原有空格而「ワ゛」「ヰ゛」等带浊点的历史假名组合与全角括号、日文逗号都按第 4、5 条规则整体粘合。三、底层原理一splitText 如何把中日文切分为词节点上述行为全部建立在一个核心函数之上splitTextutilities.js。它的工作分两步第一步按空白切分。文本先按/[\t\n ]/切成「空白 token」与「词 token」的交替序列空白统一归约为两种取值含换行的记为\n否则记为 。第二步按 CJK 正则再切分。每个词 token 再用CJK_REGEXP拆成「单字符 CJK 词」与「非 CJK 连续串」的交替序列并给每个word节点打上kind标签共四类WordKind判定条件isCJ典型示例KIND_NON_CJK非 CJK 连续字符串falsenasal、[v]、TestKIND_CJ_LETTER单个 CJK 表意/音节字符true漢、カ、ニKIND_K_LETTER单个谚文Hangul字符false한、글KIND_CJK_PUNCTUATION命中标点正则的 CJK 字符true、。以及 U3000其中CJK_REGEXP定义在 constants.evaluate.js由cjk-regex的完整 CJK 字符集与unicode-regex的Script_Extensions: Han/Katakana/Hiragana/Hangul/Bopomofo及Other_Letter、Letter_Number、Other_Symbol、Modifier_Letter、Modifier_Symbol、Nonspacing_Mark等 General Category 合并而成并追加了可变选择符Variation Selectors区块——这正是组合用记号第 5 类行为能被一并匹配的原因。关键的「假空白」插入逻辑utilities.js相邻两个word节点之间会插入一个值为的「假空白」节点但有两个例外——非 CJK 词与 CJK 标点相邻时不插入任一侧值含全角空格 U3000 时不插入。这些空白代表「字符直接相邻」的位置是后续换行决策的重要输入。一个值得注意的设计韩文谚文被当作 Latin 类处理kind: KIND_K_LETTER, isCJ: false。utilities.js 中的注释解释了原因韩文像拉丁文一样用空格分词而中文与日文不使用空格分词因此「Korean should be treated like non-CJK」——这对应仓库中引用的历史 issue 6516。四、底层原理二printWhitespace 的空白与换行决策切分完成后所有空白节点统一交给 whitespace.js 中的printWhitespace渲染。它依次回答两个问题问题一这个空白能否渲染为空格由lineBreakCanBeConvertedToSpacewhitespace.js判定本身就是 可以是\n仅当满足以下条件之一可转为空格——两侧都是非 CJK 或韩文一侧韩文一侧汉字换行一侧紧邻 ASCII 标点!#$%()*,-./:;?[\]^_\{|}~ 之一绝不转换的情况换行两侧任一侧是 CJK 标点或两侧都是 CJ 字符中日文不用空格分词换行不应变成空格或一侧 CJ 一侧非 ASCII 标点如「〜」U301C、「……」等其余情况CJ 与非 CJK 字母之间取决于该句子的整体风格usesCJSpaces。问题二这个空白能否渲染为换行软换行softline/line由isBreakablewhitespace.js判定proseWrap ! always时一律不可断行表格单元格、链接、wiki 链接、标题MDX 或非 Setext 标题内部不可断行值为假空白且任一侧为 CJ 字符时不可断行——这是「中日文之间不断行、不插空格」的最终保证韩文与汉字之间的例外地允许断行与浏览器行为保持一致两侧均为非 CJK 时允许断行。句级风格投票isInSentenceWithCJSpaceswhitespace.js是第 6 类行为的关键Prettier 会统计当前句子中「CJ 与非 CJK 字符之间」已有空格与无空格的次数多数派胜出并缓存在sentenceNode.usesCJSpaces。nasalカ゚…整句 CJ 与 Latin 之间零空格多数派是「无空格」于是\n不会变成空格、也不会变成空格整个段落既不补空格也不断行。最终组合逻辑为可断行时输出line可换行空格或softline不可断行时输出 或。中文/日文文本因此获得「空格不被增删、标点保持粘合」的确定性输出。五、proseWrap 与 printWidth 在 CJK 场景下的真实作用边界Markdown 的三个 prose 换行选项见 options.md 中 Markdown 相关章节对 CJK 行为的影响如下proseWrap: always本测试夹具使用的模式允许在「可断行的空白」处按printWidth折行。但对纯中文/日文段落而言字符之间是假空白且不可断行因此即使段落视觉宽度超过printWidth: 80如「nasalカ゚…」段也不会被强制折行——中文、日文没有词边界强行断行会破坏可读性。proseWrap: preserve保留原文中的硬换行\n直接输出为hardline见 whitespace.js其余规则不变。proseWrap: never所有空白不可断行仅做空格归一化。换句话说printWidth只对「存在可断行空白」的文本生效中日文连续串内部永远不构成断行点这是设计上对东亚排版习惯的尊重而非能力缺失。六、配套动态测试格式化结果的额外验证除了静态夹具format.test.js 还内置了两个动态构造的用例同样以proseWrap: always运行汉字行尾单空格忽略构造 6 行「文」重复序列奇数行尾带一个空格、偶数行不带验证「汉字 空格 换行 汉字」场景下行尾空格与换行会被正确处理为「无空格直接相连」最终 6 行合并为一段连续汉字快照注释说明该用例是为 Chrome/Safari 相关行为修复预留的。CJK 标点类周围的换行移除把 U3000全角空格、U301C波浪号Pd 类真标点、UFF5E全角波浪号Unicode 类别却是Sm、U1F221 等标点类字符放在行首验证 Prettier 会删除这些标点周围的换行使标点紧贴前文输出为code.replace(/\n/g, )的结果。这两个用例与静态夹具互为印证把「标点粘合」「无空格连接」两条核心规则覆盖到了动态生成的输入上。七、实践建议与已知边界不要期待 Prettier 为中日文自动加空格Prettier 的哲学是「尊重并保持」句内既有空白风格多数投票而不是像部分排版工具那样强制「CJK 与 Latin 之间加空格」。如需统一风格应在源文本层面先规范再交给 Prettier 保持。全角空格请按标点对待U3000 在 Prettier 内部被归类为 CJK 标点不会被折叠或归一化为半角空格若你希望「消灭全角空格」需自行预处理Prettier 不会代劳。存在未来行为变更源码注释明确提示whitespace.js「CJK 字符与其他字符之间的空格可互换为换行」的规则将在未来做出破坏性变更且 CJK 标点与韩文之间的换行行为正逐步与 Firefox 对齐。升级 Prettier 大版本时建议用本测试集回归验证。如何运行与扩展在仓库根目录执行yarn jest tests/format/markdown/splitCjkText即可运行该目录全部用例新增夹具只需在splitCjkText/下放入.md文件如 chinese-japanese.md 这样format.test.js会通过runFormatTest自动拾取并生成快照。快照中chinese-japanese.md的输出与输入逐字一致正是「中日文文本原样保留」这一核心行为的最直接证据。通过本文的剖析可以看到Prettier 对中文/日文 Markdown 的处理不是简单的「不格式化」而是一套基于 Unicode 脚本分类、词节点切分、句级空格风格投票与浏览器行为对齐的精密机制——理解splitText与printWhitespace这两层实现就掌握了东亚文本格式化的全部关键决策点。【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考