D0010: String Literals and Raw Syntax
本文记录 2026-07-14 对 Notist Function call、String literal 与 Raw syntax 的统一设计。它取代旧的 #name![...]! Raw Call;现行严格规则见 vault::grammar,面向写作者的说明见 vault::intro。
最终决定
Notist 采用一个普通 Function call 模型:
#name(arguments)
#name(arguments)[trailing Content]
#name[trailing Content]#name(args)是完整调用,不要求 trailing body。[...]始终表示由宿主递归求值的 Content。Trailing Content 绑定到 Function signature 指定的
trailing_content参数,例如heading.body,不是普通位置参数。Function 需要外部源码时接收普通 String 参数。
不再存在 Raw Call、RawSource body、CallMode::Raw 或 bang delimiter level。
例如:
#callout(kind="warning")[Content]
#raw(text=r#"fn main() {}"#, lang="rust")
#diagram::mermaid(
source=r#"""
graph TD
A --> B
"""#,
theme="neutral",
)Parser 不根据 Function 名称或签名切换 body grammar。它只建立普通 arguments、可选 trailing Content 和稳定的 literal range;analysis 再完成名称解析、类型检查与 signature binding。
String Literal Matrix
四种 literal 都产生 String:
Syntax | Form | Style | Type |
|---|---|---|---|
| Inline | Escaped | String |
| Multiline | Escaped | String |
| Inline | Raw | String |
| Multiline | Raw | String |
Raw form 至少包含一个 #,并允许任意正数 hash level:
#raw(text=r##"contains "# without closing"##)
#raw(
text=r###"""
contains """## without closing
"""###,
lang="text",
)Closing delimiter 必须使用与 opener 完全相同的 hash count。Raw style 不处理反斜杠;Escaped style 支持 \"、\\、\n、\r、\t。Inline form 不允许换行。
只有 triple quote 后立即出现 LF 或 CRLF 时才选择 Multiline。该 required opening framing newline 不属于 String value;closing delimiter 前紧邻的一个 framing newline 也被裁掉。除此之外不裁空白,不执行 dedent:
#raw(
text=r#"""
first
second
"""#,
lang="text",
)传给 raw 的文本是 first\n second。
Opening line break 也是 raw inline/multiline 的消歧标记。没有换行时,第一个 quote 仍是 Inline opener:
r#""# -> Inline, value = ""
r#"""# -> Inline, value = "\""
r#"""text"""# -> Inline, value = "\"\"text\"\""因此提高 hash level 仍能保证 Inline Raw String 可表达任意不含换行的 quote 内容;lexer 无需向后搜索 closing delimiter 再决定 form。
Form/style/range 是 literal provenance,不是公开的细分类型。Raw String 与 escaped String 可以绑定到同一个 String parameter,也不能按 form/style 做普通 Function overload。
Explicit Raw Function
显式构造器签名:
raw(text: String, lang: String? = none) -> Content直接 String literal 的 form 决定 Raw Element 是 inline 还是 block:
#raw(text="inline escaped")
#raw(text=r#"inline raw"#)
#raw(text="""
block escaped
""")
#raw(text=r#"""
block raw
"""#)Escaped/Raw style 只控制 String 解码,不控制 Element layout。Literal provenance 可以保存在 Expression 或 BoundArgument 中,不需要把它加入 Value::String 的类型身份。
Built-in Backtick and Fence
日常写作中的 backtick 与 fence 是 host Content syntax:
Run `cargo test` before publishing.
```rust
fn main() {}
```Inline backtick 直接产生 inline Raw Element。
Fence 直接产生 block Raw Element,opening line 的 info tag 直接成为 language。
它们不产生 String expression,不能放进 Function arguments。
它们不查找动态名称
raw,用户或插件定义同名 Function 不会改变核心语法。它们与显式
raw(...)共享底层 Raw Element constructor,而不是共享动态调用路径。
Inline payload 需要包含 backtick 时,提高相同 delimiter run 的长度:
`ordinary`
``contains ` inside``Fence opener 至少三个 backtick;closing fence 至少与 opener 一样长。Opening 后和 closing 前的一对 framing newline 不属于 payload。
Why Remove Raw Call
旧方案写成:
#name![raw body]!
#name!![body containing ]!]!!它的优点是 parser 在 opener 处就能确定 opaque body,并可通过 bang level 避免 terminator 冲突。但它带来更高的系统成本:
Function call 被拆成 Content Call 与 Raw Call 两种 grammar。
Signature 需要特殊 body mode,runtime 需要 CallBody/RawSource 分支。
![]!与编辑器自动补出的]冲突,closing!不便输入。同一份外部源码不能自然作为普通参数参与统一 binding。
Attributes、unknown call recovery、completion 和 Hover 都需要传播 CallMode。
把 ! 挪到方括号内部,例如 [!raw!] 或使用 [``raw``],虽然更容易输入,却会抢占合法 Content body 的开头,迫使 parser 根据 Function signature 或向后搜索消歧。这与 unknown Function、插件 schema、增量解析和稳定语言注入冲突。
Language References
本次设计吸收了几类成熟做法:
Typst:
[...]是 Content value,并可作为 trailing argument;高频 raw markup 是核心语法糖。Rust:raw String 使用可升级 hash delimiter,在 opener 处确定 escape mode。
Python:raw/triple prefix 仍产生普通 String,说明词法 style 不必制造新类型。
CommonMark:inline code span 与 fenced block 使用 backtick run,较长 delimiter 容纳较短 run。
JavaScript tagged template:说明“前方构造器应用于后方源码”可用,但 template 是 source-sensitive 特殊调用;Notist 选择把一般插件源码收回普通 String 参数,仅保留核心 host sugar。
Lua long bracket:说明精确匹配的可升级 delimiter 可以避免修改 payload,本设计把这一原则用于 hash raw String。
对应官方文档名称:Typst Reference Syntax / Raw Text,CommonMark Spec Code spans / Fenced code blocks,The Rust Reference Raw string literals,Python Language Reference String and Bytes literals,MDN Template literals,Lua 5.4 Reference Manual Lexical Conventions。
Notist 额外要求 triple opener 后立即换行。原因不是沿用某一门语言的表面写法,而是 r#"..."# 已允许未转义 quote:若同一行的 r#"""text"""# 同时表示 Multiline,就会与值为 ""text"" 的 Inline Raw String 重叠。把 line break 纳入 opener 可以在局部前瞻内稳定消歧,并保留 raw inline 的完整表达能力。
Tooling Consequences
Syntax parser 不需要 Function registry 即可识别 Call、String、Content 与 Raw syntax 边界。
Completion 在 arguments 中依据 signature 提供参数名;trailing Content 内继续提供 Notist completion。
插件语言注入可以结合静态 schema、String 参数名和 literal payload range 完成,不改变宿主 parse tree。
裸 fence 直接使用 info tag 做 language injection。
Hover 展示
trailing_content绑定目标,而不再展示 Raw/Content body mode。UnresolvedCall 保存 arguments、可选 trailing Content、Attributes 与 range,不再保存 RawSource 分支。
Computed String 的布局
raw 可以从 direct String literal 的 Inline/Multiline provenance 决定布局;computed String 没有这样的 source form。未来的选择可以是显式 block 参数、raw.inline / raw.block 构造器,或为非 literal String 定义默认 form。无论选择哪一种,布局都不应偷偷成为 String 的公开类型身份,因为相同字符串值不该仅因求值来源不同而具有不同类型。
此外,裸 Raw syntax 是否可以直接附加 Attributes、String multiline 是否未来增加可选 dedent,均应作为独立语法提案处理,不影响本次核心模型。