首页
/ Code Blocks

Code Blocks

2026-09-04 13:45:25作者:邓越浪Henry

Indent Code Block

This line won't *have any markdown* formatting applied.
I can even write <b>HTML</b> and it will show up as text.
This is great for showing program source code, or HTML or even
Markdown. <b>this won't show up as HTML</b> but
exactly <i>as you see it in this text file</i>.

Within a paragraph, you can use backquotes to do the same thing. This won't be *italic* or **bold** at all.

Fence Code Block

#include <iostream>

int main(int argc, const char* argv[]) {
  std::cout << "C++ code block test" << std::endl;
  return 0;
}
This is a code block without language identifier.

它用最小篇幅覆盖了 CommonMark 中代码语法的三个关键分支:

1. **缩进代码块(Indent Code Block)**:以 4 个空格缩进的连续段落。块内 `*have any markdown*`、`<b>HTML</b>`、`<i>...</i>` 均按 CommonMark 规则作为纯文本呈现,不做任何 Markdown/HTML 解析。这是对“块内内容原样保留”这一语义的直接验证。
2. **行内反引号(backquotes)**:段落内用单反引号包裹的 `` `This won't be *italic* or **bold** at all.` ``,验证行内代码与块级代码两条独立路径互不干扰。
3. **围栏代码块(Fence Code Block)**:一个带语言标识 ```` ```cpp ```` 的围栏块(info string 为 `cpp`),一个不带语言标识的裸围栏块(info string 为空)。前者验证 info string 保留与序列化还原,后者验证空 info string 的往返。

## 三、解析侧:Markdown 到状态树

往返测试的“去程”由 [markdownToState.ts](https://gitcode.com/gh_mirrors/ma/marktext/blob/43bd8b77795fb27b1a9512737c000f7362031ea0/packages/muya/src/state/markdownToState.ts?utm_source=gitcode_repo_files) 中的 `MarkdownToState` 完成,代码块相关的核心是私有方法 `_buildCodeState`(约 L412-L464)。关键逻辑有四条:

**(1)info string 原样保留,语言取首词。** 解析后得到标记 token 的 `infoString`(即围栏后的完整信息串),方法先 `trim` 再用 `firstWordOfInfo()` 提取首个词作为高亮语言——这与 CommonMark §4.5 对 info string 的定义一致。注释明确写着:“Keep the whole info string; the language for highlighting / diagram detection is its first word”。状态中存储的 `meta.lang` 是完整 info string 原文(例如 `js title="x"` 或 Pandoc 的 `{...}` 块),而不是截取后的单词;`[types.ts](https://gitcode.com/gh_mirrors/ma/marktext/blob/43bd8b77795fb27b1a9512737c000f7362031ea0/packages/muya/src/state/types.ts?utm_source=gitcode_repo_files)` 中 `ICodeBlockState` 的注释重申了这一点:“The full fenced info string, verbatim ... The language for highlighting is its first word — derive via `firstWordOfInfo()`, never assume a single word.”

**(2)空行裁剪是可选行为。** 构造 `MarkdownToState` 时可传入 `trimUnnecessaryCodeBlockEmptyLines`(默认 `false`),开启后代码块首尾的多余空行会被 `replace(/\n+$/, '').replace(/^\n+/, '')` 裁掉,注释标注这是历史 issue #1265 的修复。往返测试恰好使用默认值 `false`,保证不改变输入语义。

**(3)图语言直通 diagram 状态。** 若首词命中 `^(mermaid|vega-lite|plantuml|flowchart|sequence)$`,不会构造 `code-block` 状态,而是直接生成 `diagram` 状态(`vega-lite` 映射为 `json`,其余映射为 `yaml`)。`CodeBlocks.md` 中的 `cpp` 不在此列,因此走普通代码块路径。

**(4)fenced / indented 二值分类与 fenceLength 记录。** 解析时通过 `walkTokens` 给 token 写入 `codeBlockStyle`:围栏块为 `'fenced'`,缩进块保留 `'indented'`。`_buildCodeState` 据此写出 `meta.type`;此外当围栏长度大于 3 时还会记录 `fenceLength`,供序列化时还原原始围栏长度。注意解析侧的一处细节:缩进代码块的文本会先 `text.replace(/\n$/, '')` 去掉 marked 附加的尾部换行,围栏块则原样保留。

## 四、状态模型:ICodeBlockState

两类代码块在 muya 中共享同一个状态类型,定义于 [types.ts](https://gitcode.com/gh_mirrors/ma/marktext/blob/43bd8b77795fb27b1a9512737c000f7362031ea0/packages/muya/src/state/types.ts?utm_source=gitcode_repo_files#L28-L39):

```ts
export interface ICodeBlockState {
    name: 'code-block';
    meta: {
        type: string; // "indented" | "fenced";
        // 完整围栏 info string 原文(如 `js`、`js title="x"` 或 Pandoc 的 `{…}`);
        // 高亮语言是其首词——用 firstWordOfInfo() 派生,勿假设单词形态。
        lang: string;
        fenceLength?: number;
    };
    text: string;
}
```

夹具中的缩进块对应 `{ type: 'indented', lang: '' }`,`cpp` 围栏块对应 `{ type: 'fenced', lang: 'cpp' }`,裸围栏块对应 `{ type: 'fenced', lang: '' }`。`text` 字段承载代码原文(不含围栏本身)。这一“结构 + 元数据 + 纯文本”的三要素模型也是 muya 所有块级状态的统一范式,使得代码块可以像其他块一样参与 JSON 状态编辑(`editOperation`)、历史回滚与跨进程同步。

## 五、序列化侧:状态树回到 Markdown

“回程”由 [stateToMarkdown.ts](https://gitcode.com/gh_mirrors/ma/marktext/blob/43bd8b77795fb27b1a9512737c000f7362031ea0/packages/muya/src/state/stateToMarkdown.ts?utm_source=gitcode_repo_files) 的 `_serializeCodeBlock`(约 L352-L369)完成,分两条路径,恰好覆盖夹具的两类样本:

- **围栏路径**(`type === 'fenced'`):计算围栏长度 `this._codeFenceLength(text, meta.fenceLength)`——即取原始 `fenceLength` 与“能安全包住正文中反引号串”所需长度的较大值,用 `` `'`.repeat(fence) `` 生成围栏;info string 非空则围栏行写作 `` ```lang ``,为空则只写 `` ``` ``;正文按行输出,末尾再补一道闭合围栏。
- **缩进路径**:每行前缀 4 个空格(`${indent}    ${text}`),即 CommonMark 的缩进代码块规则。

外层 `_serializeSimpleBlock` 对 `code-block` 的统一处理是:先 `_insertLineBreak` 插入与前一块之间的空行,再调用 `_serializeCodeBlock`,保证代码块在文档中前后都有空行分隔——这正是往返输出里 `## Fence Code Block` 与 `` ```cpp `` 之间出现空行的来源。

## 六、断言策略:为什么只验证“二趟稳定”

[roundTrip.spec.ts](https://gitcode.com/gh_mirrors/ma/marktext/blob/43bd8b77795fb27b1a9512737c000f7362031ea0/packages/muya/test/spec/roundTrip.spec.ts?utm_source=gitcode_repo_files) 对 `CodeBlocks.md` 这类夹具**不**要求与原文逐字节一致,而是断言往返收敛性:

```ts
function isStableUnderRoundTrip(markdown: string): boolean {
    const once = roundTrip(markdown);
    const twice = roundTrip(once);
    return normalise(once) === normalise(twice);
}
```

其中 `normalise` 只做两件事:CRLF→LF 归一、去掉尾部换行。注释给出了两个明确的工程理由:其一,序列化器会把结尾规范化、输入可能带 CRLF,直接比原始字符串会被这些无关差异干扰;其二,**故意不**逐行去除行尾空白——CommonMark §6.7 中两个行尾空格是硬换行标记,若折叠掉会掩盖真实的往返不稳定。

而 `CodeBlocks.md` 之所以只进“稳定性”断言、不进“逐字节恒等”断言,是因为测试文件里单独维护了一个 `identityFixtures` 白名单(Images、Escapes、GFM Basic Text Formatting、GFM Tables 四个首轮输出即与原文一致的夹具),`Code Blocks` 不在其中。注释解释:严格的字节一致“过于严格,且 marktext 早期对这些夹具本来就是非确定性的——列表缩进与 ExportMarkdown 的规范化选择就不同”。对代码块而言,典型差异点就是空行布局:解析时缩进块去掉了尾部换行、序列化时又统一补空行与换行,首趟输出与原文在空行上可能略有出入,但第二趟必然收敛。

## 七、运行时视角:状态如何变成可编辑的 DOM 块

状态树最终由 [codeBlock/index.ts](https://gitcode.com/gh_mirrors/ma/marktext/blob/43bd8b77795fb27b1a9512737c000f7362031ea0/packages/muya/src/block/commonMark/codeBlock/index.ts?utm_source=gitcode_repo_files) 的 `CodeBlock` 块挂载为 `<pre>` DOM 节点,类名 `mu-code-block` 与 `mu-${meta.type}-code`(即 `mu-indented-code` / `mu-fenced-code`)——这也解释了为什么类型字符串必须保持这两个取值。两个与本文主题直接相关的运行时行为:

1. **语言 setter 的类型提升**:`set lang(value)` 中,如果当前 `meta.type !== 'fenced'`,会将其改写为 `'fenced'` 并通过 `jsonState.editOperation(path, diffToTextOp(diffs))` 下发 JSON 状态编辑,同时把 DOM 类名从 `mu-indented-code` 切换为 `mu-fenced-code`。也就是说,用户在缩进代码块顶部输入语言标识,块会自动“升级”为围栏代码块——这是夹具中 indented/fenced 两种 `type` 在编辑路径上的真实交汇点。
2. **高亮语言按需加载**:setter 用 `firstWordOfInfo(value)` 取首词,经 `loadLanguage()` 动态加载 Prism 语法,按 `loaded` / `noexist` / `cached` 三种状态决定是否重渲染;无语言、未知语言或缩进块则不加载任何语法——与序列化侧对空 info string 的处理保持一致。

## 八、如何运行这些测试

在 muya 包内用 vitest 运行往返测试即可复现本文全部结论:

```bash
cd packages/muya
npx vitest run test/spec/roundTrip.spec.ts
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
983
503
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384