Basic Text Formatting
Basic Text Formatting
Strong text Also Strong
strike and inline code
under line and 43 H2O
this is in italic and so is this
this is in bold and so is this
this is bold and italic and so is this
this will be bold
this will be bold
this is strike through text
So a single word followed _b_y _a_nother
So a single word followed __b__y __a__nother
Some markdown extensions
This is emoji :man:
This is inline math
Paragraph
A two trailing spaces and a new line
makes a line break.
Two new lines make a new paragraph.
它对行内语法的覆盖是刻意的"全量枚举":
- **强调(emphasize)**:`**`/`__` 加粗、`*`/`_` 斜体、`***`/`___` 加粗+斜体三种叠加形式;
- **GFM 扩展**:`~~删除线~~`、`:man:` 表情短代码;
- **Muya 私有扩展**:行内数学 `$a \ne b$`;
- **原始 HTML**:`<u>`、`<sup>`、`<sub>`、`<b>`、`<i>`、`<s>`;
- **边界用例**:`_b_y`、`__b__y` 这类"下划线嵌在单词中间"的 CommonMark 边界规则;
- **段落与换行**:两个尾部空格触发的强制换行(hard line break)与空行触发的新段落。
## 往返测试:为什么只断言"收敛"而不要求"逐字节相等"
这份夹具由 marktext 旧版测试 `test/unit/specs/markdown-basic.spec.js` 迁移(backport)而来,迁移逻辑写在 [roundTrip.spec.ts](https://gitcode.com/gh_mirrors/ma/marktext/blob/43bd8b77795fb27b1a9512737c000f7362031ea0/packages/muya/test/spec/roundTrip.spec.ts?utm_source=gitcode_repo_files) 中。测试流程是:
1. 用 `MarkdownToState` 把夹具文本解析为状态树,参数为 `footnote: false, math: true, isGitlabCompatibilityEnabled: true, frontMatter: true`;
2. 再用 `StateToMarkdown({ listIndentation: 1 })` 重新序列化回 Markdown;
3. 断言**第二轮往返的输出与第一轮输出完全相同**(收敛性),而不是要求第一轮输出与原文逐字节一致——注释里明确说明,后者过于严格且旧实现本身就不确定(例如列表缩进与导出器的规范选择不同)。
```ts
function isStableUnderRoundTrip(markdown: string): boolean {
const once = roundTrip(markdown);
const twice = roundTrip(once);
return normalise(once) === normalise(twice);
}
比较前有一个 normalise 归一化步骤,它只做两件事:把 CRLF 统一为 LF、去掉末尾的换行。一个值得注意的细节是:它刻意不去掉每行行尾的空格,因为 CommonMark §6.7 规定行尾两个空格就是强制换行标记,折叠掉尾部空白会掩盖真实的往返不稳定。这条设计直接对应夹具最后一段 A two trailing spaces and a new line 行尾的两个空格。
在这 11 个夹具中,common/BasicTextFormatting.md 并不在"恒等往返"白名单里(白名单为 Images、Escapes、GFM BasicTextFormatting、GFM Tables 四份)——因为其中的 HTML 标签、表情等语法在序列化器侧有自身的规范化选择,逐字节保持原文无法保证,只需保证稳定收敛。
逐语法拆解:夹具中每个记号对应的 muya 实现
muya 的内联解析器规则集中定义在 rules.ts,按来源分为 commonMarkRules、gfmRules 和 inlineExtensionRules 三组。下面按夹具出现顺序逐一映射。
加粗与斜体:strong / em 规则及嵌套
strong: /^(\*\*|__)(?=\S)([\s\S]*?[^\s\\])(\\*)\1(?!(\*|_))/, // can nest
em: /^(\*|_)(?=\S)([\s\S]*?[^\s*\\])(\\*)\1(?!\1)/, // can nest
**Strong**、__Also Strong__、*this is in italic*、***this is bold and italic*** 全部命中这两条规则:***x*** 会被 strong 规则先匹配出外层 **...**,其内容 *x* 再递归匹配 em,从而渲染出 <strong><em>x</em></strong>。渲染端由 strong.ts 通过共用的 delEmStrongFac 工厂生成节点。
夹具中 So _a_ single _word_ followed _b_y _a_nother 一行是 CommonMark 下划线强调的边界用例:下划线只有在"flanking"(前后边界符合条件)时才能构成强调,因此 _b_y 中间的 _ 不会生效,只有 _a_ 和 _word_ 是真正的斜体;__b__y 同理,只有 __b__ 是加粗。词法器在匹配到候选后,会调用 validateEmphasize(见 lexer.ts 中 tryStrongEm 的调用)按左右 flanking 规则做最终校验,而不依赖正则本身。
删除线与表情:GFM 扩展
del: /^(~{2})(?=\S)([\s\S]*?\S)(\\*)\1/, // can nest
emoji: /^(:)([a-z_\d+-]+)\1/,
~~strike~~ 命中 del 规则,::man: 命中 emoji 规则。表情规则有一个词边界保护:在 lexer.ts 的 tryChunks 中,若 : 前一个字符是 \w(例如 12:00-14:00 里的冒号),则拒绝匹配,避免把时间格式误解析成表情(对应上游 issue #1677)。
行内代码
inline_code: /^(`{1,3})([^`]+|.{2,})\1/,
`inline code` 被解析为不可再嵌套的原子 token,其内容 to[2] 直接作为 content,不再递归 tokenize。
原始 HTML:<u>、<sup>、<sub>、<b>、<i>、<s>
html_tag: /^(<!--[\s\S]*?-->|(<([a-z][a-z\d-]*)[^\n<>]*>)(?:([\s\S]*?)(<\/\3 *>))?)/i, // raw html
<u>under line</u>、4<sup>3</sup>、H<sub>2</sub>O 等全部走 html_tag 规则。渲染端 htmlTag.ts 有两个关键处理:
- 块级标签名或经
sanitize()判定危险的标签会被降级为span(selector = BLOCK_TYPE6.includes(tag) || !sanitize(<${tag}>) ? 'span' : tag),保证段落行内结构不被破坏; - 开闭标签被包在可被导出的 marker 中,token 的
range与raw保留在dataset里,这是序列化回 Markdown 时能原样还原<u>...</u>的依据——即"收敛往返"中 HTML 标签逐字保留的底层机制。
行内数学:muya 私有扩展
inline_math: /^(\$)((?:[^$\\]|\\.)+)(\\*)\1(?!\1)/,
$a \ne b$ 命中 inlineExtensionRules.inline_math。测试中 MarkdownToState 显式开启 math: true,序列化时按相同标记原样输出,因此往返稳定。
强制换行与新段落
soft_line_break: /^(\n)(?!\n)/,
hard_line_break: /^( {2,})(\n)(?!\n)/,
夹具末段 A two trailing spaces and a new line␠␠\nmakes a line break. 行尾两个空格命中 hard_line_break(捕获组 1 保存空格、捕获组 2 保存换行),序列化时原样写回两个空格加换行;而 Two new lines make a new paragraph. 前的空行则使状态树生成两个独立的 paragraph 块,由块级解析(markdownToState.ts)而非内联层处理。
词法器:规则优先级是一份"契约"
上面所有规则的尝试顺序固化在 lexer.ts 的 INLINE_HANDLERS 数组中:
const INLINE_HANDLERS: ReadonlyArray<(state: ILexState) => boolean> = [
tryBacklash, // 转义 \x
tryStrongEm, // strong / em(含嵌套校验)
tryChunks, // inline_code / del / emoji / inline_math
trySuperSubScript, // ^sup^ / ~sub~(可选项)
tryFootnote,
tryImage,
tryLink,
tryReferenceLink,
tryReferenceImage,
tryHtmlEscape, // & 等 HTML 转义
tryAutoLinkExtension,
tryAutoLink,
tryHtmlTag,
trySoftLineBreak,
tryHardLineBreak,
tryTailHeader,
];
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00