Notist Grammar
本文定义 Notist 的目标语法模型。语言包含 Markup 与 Code 两种模式:文档默认处于 Markup,# 将一个 Code expression 嵌入当前内容,Code 中的 [...] 创建 Content literal 并暂时切回 Markup。概念说明见 vault::intro,类型与求值规则见 vault::types。
规范状态
本文是 parser、analysis 与 evaluator 的现行规范。当前实现已支持本文描述的 mode-aware syntax tree 与一般 EmbeddedExpression,以及 Function call、trailing Content、String literal、Wiki Reference、Raw syntax 和 Attributes。
本规范确定 Markup 与 Code 两种模式,以及当前实现使用的 literal、Function call、User Function、名称、括号与二元运算 expression。Array、Dict、一般变量声明和控制流尚未进入宿主语法;以后加入它们时也不能改变 Content literal、顶层 EmbeddedExpression 与 Function call 的模式边界。
记号
"text"表示固定字符。A B表示顺序连接,A | B表示选择。A?、A*、A+分别表示零或一次、零或多次、一次或多次。SameHashes、SameBackticks 和括号配对等上下文相关规则由正文定义。
源文件与 Document
源文件是 UTF-8 文本,换行符可以是 LF 或 CRLF。源码位置使用半开 byte range:start 包含,end 不包含。
Document = Markup ;
Markup = MarkupItem* ;
MarkupItem = Text
| WikiLink
| RawSyntax
| EmbeddedExpression ;Document 是一个隐式的 Markup sequence。普通 Markup、Reference、Raw syntax 和 EmbeddedExpression 的输出按源码顺序连接,整篇 Document 的静态类型与求值结果均为 Content。
未被其他 Markup 结构占用的源码是 Text。在顶层 Markup 中,普通 [ 与 ] 是文字;只有 Code mode 中的 [ 才开始 Content literal。
Markup 与 Code Mode
Notist 只有两种语法模式:
Mode | 作用 | 进入方式 |
|---|---|---|
Markup | 书写普通文档、Reference 和 Raw syntax | Document 默认;Code 中使用 |
Code | 书写 literal、Function call 和其他 expression | Markup 中使用 |
EmbeddedExpression = "#" CodeExpression Attributes? ;
ContentBlock = "[" Markup "]" ;
ContentBlock 内未转义的 ] 结束当前 block;需要文字 ] 时使用 \]。Markup 中由 EmbeddedExpression 引入的嵌套 ContentBlock 按各自 delimiter 正确配对。
# 不是一个普通 unary operator。它在 Markup 中开始一个 EmbeddedExpression,使 parser 切换到 Code;expression 结束后恢复 Markup。Code expression 的结果必须能够插入 Content,具体类型规则见 vault::types。
顶层 EmbeddedExpression 只消费一个 atomic expression,随后立即恢复 Markup。这条边界保证 Function call 后的正文标点和相邻 inline markup 不会被继续吞入 Code:
#kbd[A] - B // ` - B` 是正文
#raw(text=("a" + "b")) // 括号内仍是完整 Code expression需要在嵌入位置书写复合 expression 时使用括号。Function argument、括号和 User Function body 已经有明确的 Code delimiter,不受这条顶层边界限制。
Code 中的 ContentBlock 使用 [...] 创建 Content literal。[ 后进入 Markup,匹配的 ] 结束该 literal 并恢复外层 Code。
Plain [brackets] stay markup text.
#[This is a Content literal embedded into the document.]
#quote[The trailing argument is the same kind of Content literal.]因此 #[body] 不再是独立的 TransparentScope grammar。它是 # 嵌入一个 ContentBlock;无 Attributes 时,其求值效果等同于直接插入 body Content。有 Attributes 时,它同时建立一个可供 analysis 查询的显式源码 scope。
Mode 可以递归嵌套:
#[Outer markup with #[inner markup]@inner.]@outer解析模式依次为 Markup → Code → Markup → Code → Markup。
Code Expression 子集
本阶段目标 Code grammar 为:
CodeExpression = BinaryExpression ;
BinaryExpression = AtomicExpression (BinaryOperator AtomicExpression)* ;
AtomicExpression = NoneLiteral
| BoolLiteral
| IntLiteral
| FloatLiteral
| StringLiteral
| ContentBlock
| CallExpression
| NameExpression
| ParenthesizedExpression
| UserFunctionDefinition ;
ParenthesizedExpression = "(" CodeExpression ")" ;
BinaryOperator = "+" | "-" | "*" | "/" ;二元运算遵循乘除高于加减的优先级;括号可以显式改变结合。User Function 使用 let name(parameters) -> Result = expression,其参数、default 和 body 都处于有明确 delimiter 的 Code context。Array、Dict、一般变量声明、unary operator 和控制流以后可以加入 CodeExpression,但不能改变 # 与 ContentBlock 的模式规则。
在 Code mode 内不重复书写 #。例如 Function arguments 本身已经处于 Code:
#raw(text="cargo test", lang="text")这里两个 String 都是 Code literal,而不是 Markup text。
标识符与名称
Identifier = (UnicodeAlphanumeric | "_" | "-")+ ;
QualifiedName = Identifier ("::" Identifier)* ;:: 两侧不允许空白。当前目标保留 Identifier 可由数字开头的行为;以后引入一般变量与 let 前,应重新确认 identifier start 规则,避免数字 literal 与 Name expression 重叠。
Function Call
Function Call 是 CodeExpression,而不是独立的 Markup 节点:
CallExpression = QualifiedName (Arguments ContentBlock* | ContentBlock+) ;
Arguments = "(" Whitespace* ArgumentList? Whitespace* ")" ;
ArgumentList = Argument (Whitespace* "," Whitespace* Argument)*
(Whitespace* ",")? ;
Argument = NamedArgument | PositionalArgument ;
NamedArgument = Identifier Whitespace* "=" Whitespace* CodeExpression ;
PositionalArgument = CodeExpression ;当前 Function signature 最多声明一个 trailing Content parameter,因此现阶段实现只需接受零或一个 trailing ContentBlock;grammar 使用 * 是为了保留一般 Function call 的统一形态,是否允许多个由 signature binding 决定。
由于 ContentBlock 本身是 CodeExpression,Content 也可以显式写在普通 argument list 中:
#quote(body=[Quoted Content.])
#heading(level=2, body=[Title])Trailing ContentBlock 是该普通 Content argument 的紧凑调用形式,不是另一种 body type 或 parser mode。
以下写法共享同一 CallExpression 模型:
#pagebreak()
#raw(text="cargo test")
#quote[Quoted Content.]
#heading(level=2)[Title]#name(args)[body] 的解析过程是:
Markup
-> # enters Code
-> parse Function Call
-> parse ordinary arguments in Code
-> [body] enters Markup and produces Content
-> bind Content as a trailing argument
-> insert Function result into outer MarkupParser 不读取 Function signature 来决定 [...] 的语法。它始终是 Content literal;analysis 再检查该 Function 是否接受 trailing Content。
Argument Literal
NoneLiteral = "none" ;
BoolLiteral = "true" | "false" ;
IntLiteral = "-"? ASCII_DIGIT+ ;
FloatLiteral = "-"? (ASCII_DIGIT+ "." ASCII_DIGIT*
| ASCII_DIGIT* "." ASCII_DIGIT+) ;Float 必须包含一个 . 且至少包含一个 digit,因此 1.0、1.、.5 和 -.5 合法,. 与 -. 非法。
Arguments 允许空白、换行和 trailing comma。Named argument 之后不允许 positional argument。Trailing ContentBlock 是 Function call 的 trailing argument,不受该限制。
Backtick 与 fenced Raw 属于 Markup syntax,不是 CodeExpression;需要把源码传入 Function 时使用 String literal。
String Literal
String literal 只在 Code mode 中成立。在 Markup 中,普通 quote 是 Text;例如 "abc" 会保留 quote 字符,而 #"abc" 会创建 String value 并将其作为文字插入 Content。
四种 String form 都产生同一个 String 类型:
StringLiteral = EscapedInlineString
| EscapedMultilineString
| RawInlineString
| RawMultilineString ;
EscapedInlineString = '"' EscapedInlineCharacter* '"' ;
EscapedMultilineString = '"""' Newline EscapedMultilineCharacter* '"""' ;
RawInlineString = "r" Hashes '"' RawInlineCharacter* '"' SameHashes ;
RawMultilineString = "r" Hashes '"""' Newline RawMultilineCharacter* '"""' SameHashes ;
Hashes = "#"+ ;
Newline = LF | CRLF ;Inline String 不允许 LF 或 CRLF。只有 triple quote 后立即出现 LF 或 CRLF 时才选择 Multiline form;opening framing newline 不属于 value,closing delimiter 前紧邻的一个 LF 或 CRLF 同样被裁掉。除此之外不裁剪空白,也不执行 dedent。
Escaped String 支持 \"、\\、\n、\r、\t。其他或不完整的 escape 产生 SyntaxError。Raw String 至少包含一个 #,不处理反斜杠,closing delimiter 必须使用与 opener 完全相同的 hash count。
Inline/Multiline 与 Escaped/Raw 是 literal provenance,不是不同的公开类型。
Wiki Reference
Wiki Reference 是 Markup syntax:
WikiLink = "[[" WikiReferenceBody "]]" ;
WikiReferenceBody = ModulePart ("#" Label)? ;
ModulePart = ModuleReference | EmptyWhenLabelExists ;
ModuleReference = AbsoluteReference
| ParentReference
| SelfReference
| RelativeReference ;
AbsoluteReference = "vault" ("::" ModuleSegment)* ;
ParentReference = "super" ("::" "super")* ("::" ModuleSegment)* ;
SelfReference = "self" ("::" ModuleSegment)* ;
RelativeReference = ModuleSegment ("::" ModuleSegment)* ;ModulePart 和 Label 两侧会 trim whitespace。引用最多包含一个 #;Label 不能为空,只有带 Label 时 ModulePart 才能为空。
ModuleSegment 不能包含 control character、/、\、#、[ 或 ]。按 :: 分段后每段会 trim,空段非法。vault、super、self 是只允许出现在路径开头的保留段。
显式 ref(target: String) 使用同一个 parse_wiki_reference 入口,target 不包含外围 [[ / ]]。因此 [[vault::guide]] 与 #ref("vault::guide") 直接产生同一种 Reference Element;analysis 也会索引 String literal 形式的显式调用。
Inline Raw 与 Fenced Raw
Backtick syntax 只在 Markup 中构造内置 Raw Element,不是 String,也不是 CodeExpression。
RawSyntax = InlineRaw | FencedRaw ;
InlineRaw = Backticks InlineRawBody SameBackticks ;
FencedRaw = FenceOpen Newline FencedRawBody Newline FenceClose ;
Backticks = "`"+ ;
FenceOpen = BackticksAtLeast3 InfoTag? ;
FenceClose = BackticksAtLeastOpeningLength ;InlineRaw 使用完全相同长度的 backtick run 关闭。FencedRaw opener 至少三个 backtick;opening line 的非空文本经 horizontal trim 后成为 info tag。Closing fence 可以缩进,长度不得小于 opener,之后只能有 horizontal whitespace 或行尾。Opening 后和 closing 前各一个 framing newline 不属于 payload。
Raw syntax 内不扫描 Notist Markup 或 Code。即使 payload 包含 #、[[...]]、[...] 或 @...,也保持原文。
源码注释
注释是 lexer/parser trivia,不产生 Content,也不通过 FunctionRegistry 解析:
Comment = LineComment | BlockComment ;
LineComment = "//" LineCharacter* ;
BlockComment = "/*" (BlockComment | BlockCommentCharacter)* "*/" ;BlockComment 可以嵌套。Raw syntax 和 String literal 内不识别注释;Markup 中 URL 的 https:// 也不能把其中的 // 误判为 LineComment。
Attributes 与源码 Annotation
Attributes 是 EmbeddedExpression 的统一 postfix:
Attributes = "@" FirstAttribute ("," AttributeItem)* ;
FirstAttribute = Id | AttributeItem ;
AttributeItem = Tag | Class | KeyValue ;
Id = Identifier ;
Tag = "#" Identifier ;
Class = "." Identifier ;
KeyValue = Identifier "=" AttributeValue ;
AttributeValue = Identifier | QuotedAttributeValue ;
QuotedAttributeValue = '"' AttributeCharacter* '"' ;Attributes 中不允许空白。可选 ID 只能是第一个 bare Identifier;Tag、Class 和 KeyValue 可以重复并保持源码顺序。
所有 Attributes 都只建立独立的 source annotation:
#[arbitrary Content]@concept,#knowledge,.highlight
#quote[quoted Content]@citation,source="book"
#raw(text="cargo test")@command,#verified它们统一附着到前一个 EmbeddedExpression 的 source scope,不进入 Function arguments、Content、Value、静态类型或 FunctionOutput。删除或忽略 Annotation 不得改变 expression 的类型与求值结果:
eval(erase_annotations(document)) == eval(document)第一个 bare ID 同时声明该 module 内的 label;它仍然不改变求值结果。[[#id]] 指向当前 module,[[path#id]] 指向目标 module,#ref("#id") 与 #ref("path#id") 完全等价。analysis 会索引 declaration、报告重复或未解析的 label,并为跳转提供 ID 的源码范围;站点构建把声明 ID 赋给其 scope 中第一个 HTML semantic element,使 module URL 的 #id fragment 可导航。
Annotation scope 与其他显式语法结构必须正确嵌套或分离,不允许两个范围形成非嵌套交叉。一个 Annotation 可以包含多个完整 inline Element、Function Call、Paragraph 或 block,但不能从另一个显式结构内部开始并在其外部结束。
文本、拼接与段落
Markup 中相邻的 Text、Reference、Raw 和 EmbeddedExpression 输出按源码顺序连接为 Content。普通文本直接生成与 text(value) 函数相同的 Text Element;实现可以在 lowering 后合并相邻 Text Element,但不得改变 source provenance。
普通单换行属于 Text。一个或多个空行产生 Parbreak;structuring 将 Parbreak 之间相邻的 inline Element 组合成与 paragraph(body) 相同的 Paragraph Element。LF 与 CRLF 必须得到相同语义,framing CR 或多余 newline 不进入相邻 Text。
当前核心 Markup sugar 为:= heading、- list item、+ enum item、- [ ] task、inline/fenced backtick、$math$、*strong*、_emphasis_、__underline__、~~strike~~、Wiki Reference、裸 HTTP(S) URL、bare email、管道表格和行末反斜线换行。Quote、Callout 与 Details 只使用显式 Function。图片、命名链接、分隔线和分页同样使用显式 Function。
这些语法在 lowering 阶段产生统一的 Content/Element。Backtick/fence 与显式 code 共享 Raw Element;相邻的普通、有序或任务 item 分别组合成与显式 list、enum、task 相同的容器 Element。列表缩进增加时建立子列表,缩进回落到当前 span 起点以下时结束该 span,不能把一个未消费的 outdent 反复交回同一 lowering 过程。
管道表格允许单元格包含 Raw、Wiki Reference 与 EmbeddedExpression;分隔器扫描会跳过反引号、String、括号和 Content block 内部的 |。表格列数取同一连续 pipe-table 中最宽的一行,较短行在尾部补空 TableCell,因此缺少尾格仍产生规则网格。每个语法糖 TableCell 使用 colspan=1、rowspan=1;跨列或跨行通过显式 table::cell 表达,并与显式表格共享 rowspan-aware 网格算法。
> quote、!!! callout、??? details、定义列表、footnote、citation、abbreviation、insert、spoiler、highlight、super/sub、video/audio 和 %% comment %% 均不是当前宿主语法。Setext 标题、* item、数字列表、Markdown [label](URL)、 与 --- thematic break 也保持普通文本。
Markup 转义
Markup 中的 \ 后接 ASCII punctuation 时,二者共同产生一个 literal punctuation Text;该 punctuation 不再参与 Code、inline delimiter、URL、Wiki Reference 或表格分隔器识别。常见例子是:
\#tag
A \- B
\*literal asterisks\*
\[[literal brackets\]]
| A \| B | C |\# 用于正文 hashtag,因为未转义的 # 会进入 Code。\- 通常不是必需的;它可用于强调字面减号,或兼容旧文档中 EmbeddedExpression 后可能含混的边界。\ 后紧接单个 newline 产生强制 line break。偶数个连续反斜线会让最后一个反斜线失去 escape 作用,例如表格中的 \\| 仍开始新 cell。
错误与恢复
未闭合或非法的 Wiki Reference、ContentBlock、Arguments、String、Raw syntax 和 Attributes 产生 SyntaxError。Code expression 缺失或无法识别时必须产生 diagnostic,不得静默丢弃后回退到 Function default。
未知 Function 不是 syntax error。analysis 负责名称解析、signature binding 和静态类型诊断;evaluator 可以保留 UnresolvedCall 以支持预览和工具恢复。
识别到候选 block sugar、但其完整结构不成立时,lowering 只允许降级一次,并在禁用 block sugar 的 fragment 中保留其 inline 内容;不得对同一 source range 重新识别同一种 sugar。无效或不规则输入必须成为普通文本、容错后的结构或 diagnostic,不能通过递归恢复耗尽调用栈。
Markup、Code 和 Raw 的边界必须由 parser 在本地确定,不依赖 Function registry 或插件 runtime。
非本阶段语法
以下能力不属于当前双 Mode 迁移:
一般变量声明与可变状态。
一般 unary expression。
Array、Dict、record 和 Function value。
控制流与 import。
插件修改宿主 grammar。
Annotation 任意非嵌套交叉。
完整 formatter trivia model。
这些能力以后应扩展 CodeExpression 或 analysis,而不是改变 #、ContentBlock 与 Markup 的基本模式语义。