Prettier Markdown 换行行为详解:从 break 测试用例看 proseWrap 与 printWidth 的底层机制
Prettier Markdown 换行行为详解从 break 测试用例看 proseWrap 与 printWidth 的底层机制【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettierPrettier 是一款有主见的代码格式化器opinionated code formatter对 Markdown 的支持是其内置能力之一而 Markdown 格式化的核心难题在于换行line break的处理哪些换行要保留、哪些要折叠成空格、哪些要按行宽重新打断。本文以仓库中的 break 测试目录 及其两个测试输入文件 simple.md 与 wrap.md 为骨架结合 proseWrap 选项定义 与 Markdown 打印器的源码实现系统讲解 Prettier 在proseWrap: always下对普通段落、硬换行反斜杠续行、列表项以及超长单词的折叠与重排规则。读完本文你将能预测任意 Markdown 片段在给定printWidth与proseWrap组合下的输出结果并能在自己的项目配置中准确选择换行策略。一、测试夹具与运行方式break目录是一个标准的 Prettier 格式化测试夹具fixture结构如下simple.md8 行输入覆盖普通段落、硬换行、列表项三种基础场景wrap.md包含超长单词与硬换行组合的极端场景format.test.js驱动测试的入口只有一行核心调用snapshots/format.test.js.snap快照文件记录输入与格式化输出。其中 format.test.js 的内容是runFormatTest(import.meta, [markdown], { proseWrap: always });它等价于在命令行执行prettier --parser markdown --prose-wrap always tests/format/markdown/break/simple.md也就是说break目录下的所有输入文件统一使用proseWrap: always而printWidth采用默认值80。快照文件头部也明确标出了这一点options parsers: [markdown] proseWrap: always printWidth: 80 (default) |这是理解后续所有格式化结果的前提所有重排行为都发生在行宽 80 字符 总是换行这一组参数之下。二、simple.md三种基础换行场景simple.md 的输入只有 8 行123 456 123\ 456 - 123 123它刻意构造了三个互不相同的换行场景普通段落中的软换行123与456之间是普通换行符\n硬换行hard break123\与456之间是反斜杠 换行符CommonMark 规范中反斜杠结尾的换行表示强制换行列表项中的缩进续行- 123之后另起一行、缩进两格书写123这属于列表项段落内部的软换行。格式化输出原样保留对照 快照文件 中记录的输出输入被完整保留123 456 123\ 456 - 123 123三处换行全部原样保留没有一处被折叠成空格或重新打断。为什么proseWrap: always下这些换行依然不变因为换行决策并非无脑重排而是由打印器中的isBreakable与lineBreakCanBeConvertedToSpace两个判定函数共同决定具体见下文第三节的分析。三、底层机制换行到底由谁决定Markdown 打印器的换行逻辑集中在 src/language-markdown/print/whitespace.js 中它是理解break测试结果的钥匙。整个判定链如下1.printWhitespace最终裁决printWhitespace 是换行输出的唯一出口function printWhitespace(path, value, proseWrap, isLink, options) { if (proseWrap preserve value \n) { return hardline; } const canBeSpace value || (value \n lineBreakCanBeConvertedToSpace(path, isLink)); if (isBreakable(path, value, proseWrap, isLink, options)) { return canBeSpace ? line : softline; } return canBeSpace ? : ; }它的决策矩阵是若proseWrap preserve且原文就是\n直接输出hardline硬换行原样保留否则先判断这个位置能否变成空格canBeSpace再判断这个位置是否允许打断isBreakable允许打断且可变空格 → 输出line可折叠为空格也可按行宽打断允许打断但不可变空格 → 输出softline行宽足够时显示为空超宽时打断不允许打断 → 能变空格就输出 否则输出直接删除。line与hardline等文档构建器定义在 src/document/builders 目录下它们是 Prettier 文档模型Doc IR的基本积木line在组内可折叠/可打断hardline永远换行。2.lineBreakCanBeConvertedToSpace换行能否变成空格lineBreakCanBeConvertedToSpace 处理原文中的\n是否可以安全地视为一个普通空格。核心规则链接内部isLink为真总是可以非 CJK/韩文与相邻非 CJK/韩文字符之间可以韩文按拉丁词处理见源码注释引用的 issue #6516中文字符与中文字符之间不可以中日文不用空格分词\n必须保留相邻字符是CJK 标点KIND_CJK_PUNCTUATION不可以CJK 与 ASCII 标点之间可以但若一侧带前导/尾随标点hasLeadingPunctuation/hasTrailingPunctuation则不可以。3.isBreakable这个位置允许打断吗isBreakable 决定一个空格位置是否可以作为折行点它是break测试原样保留结果的关键function isBreakable(path, value, proseWrap, isLink, options) { if ( proseWrap ! always || path.hasAncestor( (node) SINGLE_LINE_NODE_TYPES.has(node.type) || (node.type heading (options.parser mdx || !isSetextHeading(node))), ) ) { return false; } // ... }规则要点只有proseWrap always时才可能打断never和preserve直接返回false位于tableCell、link、wikiLink这类SINGLE_LINE_NODE_TYPES节点内部禁止打断避免破坏链接语法位于标题heading内部默认禁止打断MDX 的 heading 或非 Setext 标题因为标题不参与正文折行相邻任一侧为 CJK 字符previous.isCJ || next.isCJ禁止打断避免在中文中间强行折行其余位置允许打断。4. 回到 simple.md为什么全部保留对三个场景逐一定位场景 1123\n456\n两侧都是普通 ASCII 字符lineBreakCanBeConvertedToSpace返回true非 CJK 之间可以变空格isBreakable返回truealways下普通段落允许打断于是输出line。line在printWidth: 80下整行只有 7 个字符远未超宽因此保持原始折行位置输出123\n456场景 2123\硬换行反斜杠 换行在解析后仍是空白节点同样输出line在未超宽时保留原文折行——反斜杠本身是 CommonMark 的硬换行语法Prettier 不会删除它因为它承载语义场景 3列表缩进续行列表项的段落由 printList 处理续行使用align对齐到列表标记之后换行逻辑与普通段落一致未超宽时原样保留。5. 段落与句子的文档组装换行决策得出的line/softline/ 最终被组装成文档。流程是paragraph节点由 printParagraph 打印其中的sentence节点由 printSentence 处理——后者把所有子节点word与whitespace交替出现收集进fill()构建器return fill(parts);fill 构建器 的作用是当整行超过printWidth时在line/softline标记处自动折行并尽量填满每一行未超宽时则保持紧凑。这正是printWidth不是硬上限而是期望宽度这一设计见 docs/options.md的实现基础。四、wrap.md超长单词的强制打断wrap.md 检验的是极端场景——由连字符拼接的超长单词a very-very-very-very-very-very-very-very-very-very-long-word very-very-very-very-very-very-very-very-very-very-long-word very-very-very-very-very-very-very-very-very-very-long-word \ word very-very-very-very-very-very-very-very-very-very-long-word very-very-very-very-very-very-very-very-very-very-long-word注意第一段以a开头、第二行带前导空格第三行是一个孤立的\硬换行标记之后是word加两个超长单词。对照 快照输出为a very-very-very-very-very-very-very-very-very-very-long-word very-very-very-very-very-very-very-very-very-very-long-word very-very-very-very-very-very-very-very-very-very-long-word \ word very-very-very-very-very-very-very-very-very-very-long-word very-very-very-very-very-very-very-very-very-very-long-word可以观察到三个明确行为前导空格被剥除第二行的首空格被删除段落统一从行首对齐超长单词在空白处折行每个very-very-...单词约 63 字符一行放下一个单词加空格刚好贴近 80 列放不下第二个时就在单词之间的空白处打断fill行为而不是在单词内部切开孤立的\被保留反斜杠 换行是硬换行语法即使单独成行也原样输出word后的折行同样发生在两个超长单词之间。这里印证了printWidth是期望宽度而非硬上限单个very-very-...单词本身长 63 字符任何单行都必然超长Prettier 不会强行在单词中间插入换行而是在允许的空白点line标记处打断宁可让某一行略超printWidth。五、proseWrap 的三种模式与配置方式break测试固定使用proseWrap: always但换行行为的全貌需要放在三种模式下理解。选项定义位于 src/common/common-options.evaluate.js并由 src/language-markdown/options.js 注册给 Markdown 语言值默认行为对应 CLI/APIpreserve✅按原文保留换行v1.9.0 起可用--prose-wrap preserve/proseWrap: preservealways—超宽时按printWidth折行--prose-wrap always/proseWrap: alwaysnever—强制把段落压成单行--prose-wrap never/proseWrap: never官方文档 docs/options.md 给出了三模式对比示例给定段落The quick brown\nfox jumps over the lazy dog.与printWidth: 20always→ 重新折行至约 20 列never→ 合并为单行The quick brown fox jumps over the lazy dog.preserve→ 原样保留The quick brown\nfox jumps over the lazy dog.。默认值是preserve其设计原因在 docs/options.md 中说明部分服务如 GitHub 评论、BitBucket使用对换行敏感的渲染器擅自重排会改变显示效果。这与 printWhitespace 中preserve \n → hardline的硬编码路径完全对应——preserve模式下原文换行一律硬保留不做任何折叠。配置方式与所有 Prettier 选项一致{ proseWrap: always, printWidth: 80 }或命令行prettier --prose-wrap always --print-width 80 **/*.mdprintWidth的默认值同样为 80其含义在 docs/options.md 中有明确说明它不是 ESLintmax-len那样的硬上限而是期望行宽Prettier 会尽量贴近它但允许个别行更长。六、扩展到其他语法heading、inlineCode 与 MDXbreak测试只覆盖了纯文本场景但换行机制在打印器中是全局统一的同一套isBreakable规则还延伸到了其他节点类型标题heading默认不参与折行见isBreakable中的heading分支避免破坏标题的单行性Setext 风格标题下划线/---形式是例外行内代码inlineCode在 mdast.js 中proseWrap preserve时保留代码内的换行否则把代码内的\n替换为空格强调/粗体emphasis/strong由 printWord 处理涉及*、_的转义与1*2*3这类边界情况的判定链接与脚注位于SINGLE_LINE_NODE_TYPESlink、wikiLink内部的空白禁止打断防止把链接语法拦腰截断MDX走printWordLegacyword.js与printListLegacylist.js两条旧路径行为与其他 Markdown 方言有细微差异。这些分支都集中在 mdast.js 的 switch 分发 中感兴趣可以顺着这条调用链继续深入。七、小结如何预测 Markdown 的换行输出把break测试与源码逻辑合并可以得到一套可操作的预测规则看proseWrappreserve原样保留全部\nnever把所有软换行折叠为空格并压成单行反斜杠硬换行除外always进入第 2 步在always下逐个空白位置调用isBreakable标题内部、链接/表格单元内部、CJK 字符之间均不可打断可打断位置输出line由fill在printWidth附近自动折行不可打断且原文为\n时若两侧为可空格化字符则折叠为空格否则删除或保留超长单词绝不从内部切开只在单词间的空白处折行允许个别行略超printWidth反斜杠硬换行\ 换行是语法语义任何模式下都不被删除。这套规则正是 simple.md 与 wrap.md 两份快照背后的一行行代码。如果你想验证更多场景可以直接修改输入文件后运行yarn jest tests/format/markdown/break或对任意 Markdown 文件执行prettier --prose-wrap always观察输出再对照本文给出的源码路径即可完整掌握 Prettier Markdown 的换行行为。【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

LangChain 里 ChatTongyi 想换模型通道,改走 TaoToken 行不行?

LangChain 里 ChatTongyi 想换模型通道,改走 TaoToken 行不行?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/21 3:21:25 阅读更多 →
Laravel学习路径:从PHP基础到高级开发实战

Laravel学习路径:从PHP基础到高级开发实战

1. Laravel 学习路径全解析作为一名使用 Laravel 开发过 20 项目的 PHP 工程师,我经常被问到"如何系统学习 Laravel"。今天我就把自己多年积累的学习路线和经验分享给大家,这是一条经过实战验证的有效路径。Laravel 作为目前最流行的 PHP 框架…

2026/9/19 22:01:54 阅读更多 →
深入解析 ik_llama.cpp PR 446:MMVQ 内核中隐藏的 MoE 崩溃 bug 修复

深入解析 ik_llama.cpp PR 446:MMVQ 内核中隐藏的 MoE 崩溃 bug 修复

人工智能大模型推理引擎本地部署模型量化模型优化 【免费下载链接】ik_llama.cpp llama.cpp fork with additional SOTA quants and improved performance 项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp 点击查看 免费下载 本文基于 ik_llama.cp…

2026/9/19 22:01:54 阅读更多 →

最新新闻

FinRL-Meta 基准评测体系指南:统一绩效指标、基线策略与回测实战

FinRL-Meta 基准评测体系指南:统一绩效指标、基线策略与回测实战

FinRL-Meta 基准评测体系指南:统一绩效指标、基线策略与回测实战 【免费下载链接】FinRL FinRL: Financial Reinforcement Learning. 🔥 项目地址: https://gitcode.com/gh_mirrors/fi/FinRL-Library 导读 本文以 FinRL-Meta 的 Benchmark 文档&…

2026/9/21 3:23:54 阅读更多 →
低功耗Bandgap设计实战:结构、启动电路与验证方法

低功耗Bandgap设计实战:结构、启动电路与验证方法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/21 3:22:53 阅读更多 →
国产替代Simulink的工程评估:从建模、代码生成到工具链的差距与可行路径

国产替代Simulink的工程评估:从建模、代码生成到工具链的差距与可行路径

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/21 3:22:53 阅读更多 →
上千篇笔记一键整理:Foam标签、Query查询与智能文件夹进阶指南

上千篇笔记一键整理:Foam标签、Query查询与智能文件夹进阶指南

上千篇笔记一键整理:Foam标签、Query查询与智能文件夹进阶指南 【免费下载链接】foam A personal knowledge management and sharing system for VSCode 项目地址: https://gitcode.com/gh_mirrors/fo/foam Foam 是基于 VSCode 的个人知识管理与笔记分享系统…

2026/9/21 3:22:53 阅读更多 →
在 Chrome 打包应用中集成 AppEngine Channel API:WebView 代理桥接架构实战

在 Chrome 打包应用中集成 AppEngine Channel API:WebView 代理桥接架构实战

在 Chrome 打包应用中集成 AppEngine Channel API:WebView 代理桥接架构实战 【免费下载链接】chrome-extensions-samples Chrome Extensions Samples 项目地址: https://gitcode.com/gh_mirrors/ch/chrome-extensions-samples 本篇技术指南围绕 chrome-exte…

2026/9/21 3:22:53 阅读更多 →
Deep Research 对话标题

Deep Research 对话标题

AI 应用前端 【免费下载链接】voyager Enhancement suite for Gemini, AI Studio, Claude & ChatGPT — plus a prompt manager for any websites, DeepSeek Harness included. / 面向 Gemini、AI Studio、Claude 与 ChatGPT 的增强套件;其中的提示词管理器可用…

2026/9/21 3:22:53 阅读更多 →

日新闻

agents-generator 决策矩阵全解析:从项目检测到 AGENTS.md 规则生成的 16 步判定流程

agents-generator 决策矩阵全解析:从项目检测到 AGENTS.md 规则生成的 16 步判定流程

agents-generator 决策矩阵全解析:从项目检测到 AGENTS.md 规则生成的 16 步判定流程 【免费下载链接】agentic-awesome-skills AAS Core is the local, agent-first control plane for complete catalog discovery, agent-owned selection, stack validation, and …

2026/9/21 0:00:01 阅读更多 →
gin-vue-admin 前端工具函数全景指南:src/utils 复用规范与源码级解析

gin-vue-admin 前端工具函数全景指南:src/utils 复用规范与源码级解析

gin-vue-admin 前端工具函数全景指南:src/utils 复用规范与源码级解析 【免费下载链接】gin-vue-admin 🚀ViteVue3Gin拥有AI辅助的基础开发平台,企业级业务AI开发解决方案,内置mcp辅助服务,内置skills管理,…

2026/9/21 0:00:01 阅读更多 →
Wox 全功能插件开发实战指南:基于 Python / Node.js 宿主与 WebSocket 的持久化插件体系

Wox 全功能插件开发实战指南:基于 Python / Node.js 宿主与 WebSocket 的持久化插件体系

桌面应用AI 应用插件系统 【免费下载链接】Wox A cross-platform launcher that simply works 项目地址: https://gitcode.com/gh_mirrors/wo/Wox 点击查看 免费下载 全功能插件(Full-featured Plugin)是 Wox 三类插件实现方式中能力最完整的…

2026/9/21 0:00:01 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/21 3:13:20 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/21 2:19:36 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/20 0:00:46 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/19 23:01:36 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/19 17:50:38 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/19 23:35:34 阅读更多 →