Notist
On this page
  1. Builtin Content Functions & Sugar Grammar
  2. text
  3. paragraph
  4. heading
  5. section
  6. strong
  7. emph
  8. strike
  9. underline
  10. raw
  11. math
  12. link
  13. embed
  14. list
  15. item
  16. callout
  17. divider
  18. table
  19. row
  20. cell
  21. node

Builtin Content Functions & Sugar Grammar

记法见 Grammar Notation,共用词法、上下文、正文分派、调用及变换见 Syntax,类型与字面量见 Types。每个函数一节,分别定义签名、包含结果构造的语法糖与必要的恢复;MD.* 是 Markdown 前端的 AST 模式。

签名列出已知字段、默认值与 children 契约;已知字段的错误类型、值域及缺少必填字段产生 type 诊断,恢复树保留原值。未知具名字段与多余位置参数产生诊断。实现对应关系见 Syntax。

text

text(text: String) -> InlineContent
lex Escapable -> [`=` `*` `_` `~` `\` `[` `]`
                   `(` `)` `|` `#` `$` U+0060 `!`];
lex Escape -> `\` Escapable;
syntax PlainText -> value:TEXT | value:ESCAPE
  => { if value.kind == Escape then text(text: source(value)[1..]) else text(text: source(value)) };
ast LiteralText -> LiteralToken(value) => text(text: source(value));
ast MD_Text -> MD.Text(s) => text(text: decode_once(s));

LiteralToken 是其他语法的 recover 返回的原始标记;相邻文字按正文空白规则合并。text 参数使用具名写法。

paragraph

paragraph()[children: InlineContent] -> BlockContent
syntax ContentFragment(ctx) -> body:InlineRun(ctx, block_end, None)
  => { if lower(body) != [] then Fragment(lower(body)) else [] };
syntax ExplicitBreak -> BACKSLASH NL
  => ε;
ast SkipNewline -> NL => ε;
ast MD_Paragraph -> MD.Paragraph(body) => Fragment(lower(body));

平坦 CST 中的连续内容由空行、显式分段或块语法边界划为 Fragment,没有独立 Paragraph CST 节点。Lower 只保留分组;Resolve 确定类别后,全 Inline 的片段生成一个 paragraph,单个 Block 表达式直接保留。混合类别或多个 Block 表达式要求硬分隔,Shape 不自动拆段。显式 paragraph 调用始终按签名检查。单 NL 不插入空格,行末与续行开头空白丢弃;Markdown 双空格换行归一为单 NL,反斜杠换行分段。

heading

heading<level: Int = 1>()[children: InlineContent] -> BlockContent
require level > 0
syntax Heading(ctx) ->
  marker:EQ WS body:InlineRun(ctx, always_true, None)
  => { heading<level: size(marker)>()[lower(body)] };
ast MD_Heading -> MD.Heading(level, body) => { heading<level: level>()[lower(body)] };

always_true 使当前行的 NL 结束正文,NL 留给外层。原生块分派在块起点识别 Heading;EQ 是整个连续 = run。Markdown 对应 ATX 与 Setext 标题。

section

section<C: Type = BlockContent>()[children: C] -> BlockContent

无独立源码语法糖;对块序列应用以下 shape 规则:

sectionize(forest) {
  root = []
  stack = []
  for node in forest {
    if ctor(node) == heading {
      k = node.level
      while stack != [] and stack.last.level >= k { close_section() }
      stack.push({ level: k, children: [node] })
    } else {
      append node to stack.last.children if stack != [] else root
    }
  }
  while stack != [] { close_section() }
  return root
}

close_section() {
  entry = stack.pop()
  result = section()[entry.children]
  result.span = hull(entry.children)
  append result to stack.last.children if stack != [] else root
}

两个前端的普通块序列使用同一规则。

strong

strong()[children: InlineContent] -> InlineContent
syntax MatchedInline(k, ctx) ->
  @{ can_open(cursor, ctx) }
  mate:probe FlankedClose(k; ctx)
  @{ mate.close > cursor + 1 }
  open:TokenKind(k)
  body:InlineRun(ctx, block_end, Flanked(k))
  close:TokenKind(k)?;

syntax Strong(ctx) -> matched:MatchedInline(Star, ctx)
  => strong()[lower(matched.body)];
scanner FlankedClose(k; ctx) -> { end, close } | Miss {
  let depth = 0
  for p from cursor + 1 to ctx.limit {
    if kind(p) == Newline and block_end(p, ctx) { return Miss }
    if kind(p) == Backslash and kind(p + 1) == Newline { return Miss }
    if kind(p) == k {
      if can_close(p, ctx) {
        if depth == 0 { return { end: p + 1, close: p } }
        depth -= 1
      } else if can_open(p, ctx) {
        depth += 1
      }
    }
  }
  return Miss
}
ast MD_Strong -> MD.Strong(body) => strong()[lower(body)];
recover MatchedInline on Miss when kind(rule_start) == k {
  consume = 原始起点的一个界定符 token
  cst = LiteralToken(原始 token)
  diagnostic = none
}

探测按 token 进行;Escape 不作为界定符。真正消费正文仍由 InlineRun 决定,可能先遇到内容槽闭括号等停止条件,因此收尾 token 可选。配对仅检查空白,不检查词内边界;Escape 不参与配对。Markdown 对应 **...** / __...__。

emph

emph()[children: InlineContent] -> InlineContent
syntax Emph(ctx) -> matched:MatchedInline(Underscore, ctx)
  => emph()[lower(matched.body)];
ast MD_Emphasis -> MD.Emphasis(body) => emph()[lower(body)];

匹配和恢复使用 strong 节定义的 MatchedInline。Markdown 对应 *...* / _..._。

strike

strike()[children: InlineContent] -> InlineContent
syntax Strike(ctx) -> matched:MatchedInline(Tilde, ctx)
  => strike()[lower(matched.body)];
ast MD_Strikethrough -> MD.Strikethrough(body) => strike()[lower(body)];

匹配和恢复使用 MatchedInline。Markdown 对应 ~...~ / ~~...~~。

underline

underline()[children: InlineContent] -> InlineContent

为行内内容添加下划线,写作 #underline[需要下划线的文字],HTML 输出带下划线样式的 span。_..._ 表示 emph。

raw

raw<C: Type = InlineContent>(text: String, lang?: String) -> C

text 可使用第一个位置参数。

syntax RawInline(ctx) ->
  open:BACKTICK tail:scan RawTail(size(open); ctx)
  => raw(text: source(tail.payload));

syntax RawBlock(ctx) ->
  open:BACKTICK @{ size(open) >= 3 } ^
  tag:scan LineTail(ctx)
  NL?
  tail:scan FenceTail(size(open); ctx)
  => { raw<BlockContent>(text: strip_container_indent(source(tail.payload)), lang: nonempty(trim(source(tag)))) };

syntax BlockBacktick(ctx) -> RawBlock(ctx) | RawInline(ctx);
scanner RawTail(n; ctx) -> { end, payload, close } | Miss {
  let begin = cursor
  for p from begin to ctx.limit {
    if kind(p) == Newline { return Miss }
    if kind(p) == Backtick and size(token(p)) == n {
      return { end: p + 1, payload: tokens[begin..p], close: token(p) }
    }
  }
  return Miss
}

scanner LineTail(ctx) -> { end } {
  let p = cursor
  while p < ctx.limit and kind(p) != Newline { p += 1 }
  return { end: p }
}

scanner FenceTail(n; ctx) -> { end, payload, close } | Miss {
  let begin = cursor
  let p = begin
  while p < ctx.limit {
    let q = p
    if kind(q) == Whitespace
       and display_width(source(token(q))) == ctx.block_indent {
      q += 1
    }
    if kind(q) == Backtick and size(token(q)) >= n {
      let stop = next_newline_or_limit(q, ctx)
      return { end: stop, payload: tokens[begin..p], close: token(q) }
    }
    p = after_next_newline_or_limit(p, ctx)
  }
  return Miss
}

next_newline_or_limit 返回第一个 NL 的下标或 limit;after_next_newline_or_limit 在存在 NL 时返回其后一个下标,否则返回 limit。display_width 的 tab 到下一个四列边界。原生块分派优先识别 RawBlock;闭围栏行尾 NL 不消耗。

ast MD_CodeSpan -> MD.CodeSpan(s) => raw(text: s);
ast MD_CodeBlock -> MD.CodeBlock(s, info) => { raw<BlockContent>(text: s, lang: nonempty(trim(info))) };
recover RawInline on Miss when kind(rule_start) == Backtick {
  consume = 原始起点的一个 BACKTICK token
  cst = LiteralToken(原始 token)
  diagnostic = none
}
recover RawBlock on Broken {
  consume = 从原始起点至 ctx.limit
  cst = Error(Raw(原始 token 序列))
  diagnostic = syntax, 全部消耗范围, "unclosed raw block"
}

Error 包装的未闭合 raw 块在 desugar 时跳过。正文保留末尾换行;nonempty 将空字符串转换为 none,省略 lang 字段。trim 去掉首尾空白。

strip_container_indent 按行移除最多 ctx.blockindent 列的前导空格 / tab;tab 跨过边界时,剩余列补为空格。strip_continuation_indent 使用同一算法,但保留第一行。它们处理容器缩进,不移除 payload 自己的额外缩进。

math

math<C: Type = InlineContent>(text: String) -> C

text 可使用第一个位置参数;C 为编译期类别,默认 InlineContent。$x$ 对应 math,$ x $ 对应 C 为 BlockContent 的同一个 math;块级公式必须独占一个内容片段,与前后正文用空行或显式分段分隔。

syntax Math(ctx) ->
  open:DOLLAR tail:scan MathTail(ctx)
  => { math<C: if tail.block then BlockContent else InlineContent>(text: math_text(tail)) };
scanner MathTail(ctx) -> { end, payload, block } | Miss {
  let begin = cursor
  let block = kind(begin) in { Whitespace, Newline }
  for p from begin to ctx.limit {
    if not block and kind(p) == Newline and block_end(p, ctx) { return Miss }
    if kind(p) == Dollar and
       (kind(p - 1) in { Whitespace, Newline }) == block {
      if trim(source(tokens[begin..p])) == "" { return Miss }
      return { end: p + 1, payload: tokens[begin..p], block }
    }
  }
  return Miss
}
ast MD_Math -> MD.Math(s, block) => { math<C: if block then BlockContent else InlineContent>(text: s) };
recover Math on Miss when kind(rule_start) == Dollar {
  consume = 原始起点的一个 DOLLAR token
  cst = LiteralToken(原始 token)
  diagnostic = none
}

payload 不透明。开后的留白确定类别,随后第一个闭前留白与之匹配的美元符号关闭公式:两端内侧都非空白时为行内数学,都为空白时为块级数学。不匹配的美元符号属于 payload;扫描范围内没有匹配闭符号、payload 为空或全为空白时,开美元符号保留为普通文字。行内形式允许跨软换行,不跨空行或其他块边界。原生块级形式扫描至匹配的闭美元符号或容器 limit;内部的空行、管道符和类似标题或列表的文字都归公式源码。

mathtext 先按 raw 小节的 stripcontinuation_indent 去掉容器缩进,再在块级形式中 trim 首尾布局空白;行内 payload 原样保留。语法糖直接选择 BlockContent 或 InlineContent 实例。Markdown 使用相同的留白匹配规则,但 payload 仍受 Markdown 块范围限制,不跨空行或其他块边界;其段落解析会去掉续行的前导空白。代码内容与转义的美元符号不触发数学解析。

link<T: Type>(target: T, title?: String, query?: String, fragment?: String)[children: InlineContent] -> InlineContent
where T == Path || T == String

target 可使用第一个位置参数。

syntax Link(ctx) -> &LBRACKET parsed:LinkBody(ctx, false)
  => link(target: destination(parsed))[lower(parsed.label)];
syntax WikiLink(ctx) -> &(`[` `[`) parsed:WikiBody(ctx)
  => link(target: wiki_target(parsed))[lower(parsed.label)];
scanner LinkFrame(embed; ctx) -> { end, label_close, destination_close } | Miss {
  open = cursor + (1 if embed else 0)
  要求 kind(open) == LBracket
  p = open + 1;depth = 1
  逐 token 扫描至 ctx.limit:
    NL 且 block_end,或 BACKSLASH NL:返回 Miss
    Backtick / Dollar:若 OpaqueClose(p, ctx) 成功,跳至该 close 后
    LBracket:depth += 1
    RBracket:depth -= 1;depth == 0 时记为 label_close 并停止
    其他:前进一个 token
  未找到 label_close 或其后不是 LParen:返回 Miss
  从 label_close + 2 扫描目的地:
    parens = 1;quote = none;escaped = false;previous_space = false
    NL 或 limit:返回 Miss
    逐源码字符处理当前 token:
      escaped 为 true:清除 escaped,置 previous_space=false,继续
      反斜杠:置 escaped=true、previous_space=false,继续
      quote 已设置:同类引号清除 quote
      否则:前一个字符为空白时的单 / 双引号设置 quote
            "(" 令 parens += 1,")" 令 parens -= 1
      previous_space = 当前字符是否为空白
    parens == 0:返回 { end: p + 1, label_close, destination_close: p }
    前进一个 token
}

scanner OpaqueClose(p, ctx) -> { close } | Miss {
  要求 kind(p) 为 Backtick 或 Dollar
  Dollar:使用 MathTail 的留白匹配、非空 payload 与对应类别的边界规则
          成功则返回对应闭美元符号下标,失败返回 Miss
  从 p + 1 逐 token 扫描至 ctx.limit:
    遇 NL:返回 Miss
    同类 Backtick 且 run 等长:返回该下标
  返回 Miss
}

syntax algorithm LinkBody(ctx, embed) {
  frame = probe LinkFrame(embed; ctx);失败则返回 Miss
  若 embed,消耗 BANG;消耗 LBRACKET
  label = InlineRun(ctx, 外层 stop, Single(RBracket))
  消耗当前位置的 RBRACKET 与其后 LPAREN
  消耗目的地 token 至 frame.destination_close,再消耗 RPAREN
  返回 { label, destination: 目的地原始源码 }
}

scanner WikiFrame(ctx) -> { end, target_end, pipe } | Miss {
  要求前缀为 LBRACKET LBRACKET
  从 cursor + 2 逐 token 扫描至 ctx.limit:
    NL:返回 Miss
    首个 Pipe:记下 pipe
    连续两个 RBracket:返回 { end: p + 2, target_end: pipe 或 p, pipe }
  返回 Miss
}

syntax algorithm WikiBody(ctx) {
  frame = probe WikiFrame(ctx);失败则返回 Miss
  消耗两个 LBRACKET,再消耗 target 至 frame.target_end;label = []
  若有 pipe,消耗 PIPE,再以 InlineRun(ctx, 外层 stop, Pair(RBracket)) 解析 label
  消耗当前位置的两个 RBRACKET
  返回 { target: 已消耗的 target token, label }
}

LinkBody 与 WikiBody 的 stop 继承调用它们的 InlineRun。预探测的位置不作为 label 的新 limit;实际 label 消费仍使用正文分派与 active 停止条件。Escape token 不作为括号界定符。

ast MD_Link -> MD.Link(target, body, _) => link(target: decode_once(target))[lower(body)];

destination(parsed) 为 destinationparts 的 target;普通链接丢弃语法中的 title,显式调用传入的 title 字段保留。wiki_target 从两个开括号之后依序取得原始 token,遇首个 PIPE 或 RBRACKET 停止,拼接后去掉首尾空白,不对标点转义再解码。无正文的 WikiLink 降为无 children 的 link。

transform algorithm destination_parts(parsed) {
  s = trim(parsed.destination)
  从左到右扫描字符,反斜杠屏蔽下一个字符
  找到首个前面是空白的单 / 双引号 q,位置为 i
  若 s 末尾也是 q 且引号之间存在范围:
    target = trim_end(s[..i]);title = s[i + 1..末尾引号]
  否则 target = s;title = none
  若 target 首尾为 "<" 与 ">",去掉这层包装
  target 与 title 各从左到右去掉 ASCII 标点前的反斜杠
  返回 { target, title }
}
recover Link or WikiLink on Miss when kind(rule_start) == LBracket {
  consume = 原始起点的一个 LBRACKET token
  cst = LiteralToken(原始 token)
  diagnostic = none
}

Markdown 的行内、引用式链接均匹配 MD.Link。

embed

embed(target: ResourceTarget, description?: String, title?: String, query?: String, fragment?: String) -> InlineContent

target 可使用第一个位置参数。

syntax Embed(ctx) -> &(`!` `[`) parsed:LinkBody(ctx, true)
  => embed(target: destination(parsed), description: description(parsed.label), title: embed_title(parsed));
ast MD_Image -> MD.Image(target, body, title) => embed(target: decode_once(target),
                                     description: plain_text(lower(body)),
                                     title: decode_optional(title));

embed_title 取 link 小节的 destinationparts.title。description 对 label 先 lower,再按文档顺序递归:Literal 追加其字符串值;Call 优先追加字符串 text 字段,缺少时追加字符串 description 字段,再遍历 children。Markdown 的 plaintext 对 lower 后的 Call 使用同样的字段优先顺序与 children 递归,忽略 Literal;空描述为 "",可选标题保留。

embed 表示资源引用;降糖不读取资源或按扩展名推断类型,具体呈现由渲染器决定。ResourceTarget 接受 Path 或 String;本地字符串在 Resolve/Evaluate 的 embed 构造边界绑定资源身份,远程 URL 保持 String,解析与 lowering 仍只依赖文本。

#embed(p"./image.svg", fragment: "detail") 的 Path 不包含 URL 后缀语义:文件名内的 ?、#、% 不拆分、不解码。query 与 fragment 使用独立参数;本地字符串与两种前端的图片语法按 URL 拆分后缀,路径和 fragment 百分号解码一次。重复提供同一后缀产生诊断。宿主发布时选择、编码公开 URL 并附加后缀,资源身份本身不授予访问或发布权限。

recover Embed on Miss when kind(rule_start) == Bang {
  consume = 原始起点的一个 BANG token
  cst = LiteralToken(原始 token)
  diagnostic = none
}

Markdown 行内、引用式图片均匹配 MD.Image。

list

list(ordered: Bool = false, start: Int = 1)[children: BlockContent] -> BlockContent
require structural_children(children) 的构造器全为 item
syntax List(ctx) ->
  let m = { kind(cursor) }
  let i = { column(cursor) }
  @{ m in {Minus, Plus} }
  first:Item(m, i, ctx)
  (gap:scan ListGap(m, i; ctx) rest:Item(m, i, ctx))*
  => list(ordered: m == Plus, start: 1)[lower(first) map(lower, rest)];
scanner ListGap(m, i; ctx) -> { end } | Miss {
  if kind(cursor) != Newline { return Miss }
  let p = cursor + 1
  loop {
    while kind(p) == Whitespace { p += 1 }
    if kind(p) != Newline { break }
    p += 1
  }
  if kind(p) == m and kind(p + 1) == Whitespace and column(p) == i {
    return { end: p }
  }
  return Miss
}
ast MD_List -> MD.List(ordered, start, body) => list(ordered: ordered,
                                    start: if ordered then start else 1)[lower(body)];

gap 的空白属于 List CST;同缩进切换标记结束该 list。不保存 Markdown tight / loose 标记。structural_children 见 Syntax。Markdown 的 lazy continuation 由其前端识别;.not 的内容归属由 item 小节的缩进规则决定。

item

item<C: Type = BlockContent>()[children: C] -> BlockContent
syntax Item(m, i, ctx) ->
  @{ column(cursor) == i }
  marker:TokenKind(m) WS
  let c = { column(cursor) }
  extent:probe ItemExtent(c; ctx)
  body:within(extent.end,
    ItemBody(ctx with block_indent=c, body_start=cursor))
  => item()[blocks(body)];
scanner ItemExtent(c; ctx) -> { end } {
  let p = cursor
  while p < ctx.limit {
    if kind(p) != Newline { p += 1; continue }
    let boundary = p
    let q = p + 1
    loop {
      while kind(q) == Whitespace { q += 1 }
      if kind(q) != Newline { break }
      q += 1
    }
    if q >= ctx.limit or column(q) < c { return { end: boundary } }
    p = q
  }
  return { end: ctx.limit }
}
ast MD_ListItem -> MD.ListItem(body) => item()[lower(body)];

marker 与 WS 的消费前缀固定正文起始列;BlankLine 不单独结束 item。ItemBody 使用 Syntax 的 BlockRun(ctx, Item);缩进与范围使用上述 ItemExtent。空项没有 children。

callout

callout(kind?: String)[children: BlockContent] -> BlockContent

.not 无语法糖。

ast MD_Blockquote -> MD.Blockquote(body) => callout(kind: "quote")[lower(body)];

MD.Blockquote 由 Markdown 的 > 引用块语法产生。

divider

divider() -> BlockContent
syntax Divider(ctx) ->
  @{ block_start(cursor, ctx) }
  MINUS MINUS MINUS &EndAfterWhitespace(ctx)
  => divider();

syntax EndAfterWhitespace(ctx) -> WS? End(ctx);
ast MD_ThematicBreak -> MD.ThematicBreak => divider();

原生 Divider CST 只包含三个 MINUS,尾部空白和 NL 留给外层。MD.ThematicBreak 对应 Markdown 的 --- / *** / ___ 等。

table

table(align?: Array<String>)[children: BlockContent] -> BlockContent
require structural_children(children) 的构造器全为 row
        每个 align 值属于 {"none", "left", "center", "right"}
syntax Table(ctx) ->
  @{ table_start(cursor, ctx) }
  header:scan TableLine(ctx)
  NL delimiter:scan TableLine(ctx)
  (!@{ block_end(cursor, ctx) } NL rows:scan TableLine(ctx))*
  => table(align: alignments(delimiter))[
    table_row(header, true, width(delimiter))
    map(r => table_row(r, false, width(delimiter)), rows)
  ];
predicate table_start(p, ctx) {
  要求 block_start(p, ctx),且 kind(p) not in {EOF, Whitespace, Newline}
  从 p probe TableLine(ctx) 得到 header;要求其 end 处为 NL
  从 header.end + 1 probe TableLine(ctx) 得到 delimiter
  要求 header.has_pipe or delimiter.has_pipe
  要求 header.cells 与 delimiter.cells 等长
  要求每个 delimiter cell 的原始源码满足 AlignCell
  返回 true;任一要求不成立则返回 false
}

表头和 delimiter 等宽,至少一行 has_pipe;AlignCell 在字符层匹配整个单元格。

lex AlignCell -> [` ` U+0009]* `:`? `-`+ `:`? [` ` U+0009]*;
ast MD_Table -> MD.Table(align, rows) => table(align: align)[lower(rows)];

width(frame) 返回 cells 的数量。pad_to(n, values, empty) 在 values 少于 n 项时补入 empty;take 保留前 n 项,map(f, values) 对各项调用变换 f。辅助 ast 规则通过参数取得输入,ε 表示不额外匹配节点。

alignments 逐 delimiter cell 去掉首尾空白;首 / 尾 / 两端冒号分别映射为 left / right / center,其余为 none。

row

row(header?: Bool)[children: BlockContent] -> BlockContent
require structural_children(children) 的构造器全为 cell
syntax TableRow(ctx) -> frame:scan TableLine(ctx);
scanner TableLine(ctx) -> { end, cells, has_pipe } | Miss {
  若 ctx.in_body 且首 token 为 RBracket ⇒ Miss
  从 cursor 按逻辑行扫描至外层 NL 或 ctx.limit:
    STR / comment 是不透明 token,内部换行和管道符不参与分割
    HASH 处完整 CodeCall 的探测成功 ⇒ 越过整个调用,内部 NL / Pipe 不参与分割
    Backtick / Dollar 处 OpaqueClose 的探测成功 ⇒ 越过整个 raw / math payload
      内部 NL / Pipe 不参与分割,HASH 保持字面含义
    其余 Pipe token 记录下标
  去掉行首 / 行尾 Whitespace token
  空行 ⇒ Miss
  去掉首 / 尾各一个 Pipe token(存在时)
  在余下 Pipe token 处分割 cell 区间,保留空区间
  return { end: 行末 NL 或 limit, cells: 区间序列, has_pipe: 是否出现 Pipe }
}
ast table_row(frame, header, width) -> ε => row(header: header)[
  { pad_to(width, take(width, map(table_cell, frame.cells)), cell()) }
];
ast MD_TableRow -> MD.TableRow(body, header) => row(header: header)[lower(body)];

原生语法糖按逻辑行解析:最外层 NL 结束表格行,最外层 Pipe 分隔单元格。完整 raw、math、字符串、注释和调用内部的管道符归内部内容;块级 math、字符串、注释和调用内部的换行也不结束行。raw / math 的源码保持自身的反斜杠,普通 Markup 中的字面 | 使用 \|。调用沿用 CodeCall 的边界规则;未闭合的调用或不透明界定符不保护管道符或换行。闭合公式之后的换行已在最外层,末尾管道符写在同一行,例如 $ |。Markdown 表格沿用 GFM 的物理行规则,其行内 raw / math 中的管道符仍须按 GFM 转义。

需要多段内容、列表或嵌套表格时,用 #node[ ... ] 包裹单元格内容,两端内侧留白以使用 BlockContent 文法。多行字符串使用 """...""" 或 r#"""..."""#,开引号后须立即换行。示例见 Cook Book。header true 为表头、false 为表体。显式 row 省略 header 时不补入字段。

cell

cell<C: Type = BlockContent>()[children: C] -> BlockContent
syntax TableCell(range, ctx) ->
  body:within(range.end,
    InlineRun(ctx with in_body=false, always_true, None));

TableCell 从 TableLine 提供的 range.start 开始匹配;界定符扫描和消费都受 range.end 限制。

ast table_cell(range) -> ε => cell()[
  { if table_inline(range) != [] then Fragment(table_inline(range)) else [] }
];
ast MD_TableCell -> MD.TableCell(body) => cell()[lower(body)];

table_inline 在 range.start 解析 TableCell 并 lower,沿用正文的转义规则,不对 raw / math payload 额外解码。空区间产生空 cell。

node

node<C: Type = InlineContent>()[children: C] -> C

显式调用 #node[...] 保留边界与注解,类别由输入推导。node 的公开结构透明契约允许结构槽验证其 children;源码不提供匿名调用入口。