D0002: Scope and Attributes

Notist 将“表达式产生什么内容”与“作者如何描述这段源码”视为两个正交维度:

#[important concept]@concept,#knowledge,.highlight,status=draft

#quote[Keep references stable.]@principle,#design,owner="Alice"

第一行的 ... 是嵌入文档的 Content expression;第二行是调用 quote Function。二者都可以在完整 expression 后附加 Attributes。Function argument 决定调用语义,Content 决定文档产生什么,Attributes 则建立 source Annotation。它们不需要共享一套动态“processor”机制。

Content Expression

文档默认处于 Markup mode,# 开始一个 EmbeddedExpression 并切换到 Code。Code 中的 [...] 是 Content literal,括号内部再切回 Markup:

#[Plain content.]

#[Outer #[nested]@inner content.]@outer

因此 ... 不是一种额外的 TransparentScope grammar。它只是把 Content literal 作为表达式嵌入外层文档;没有 Attributes 时,它与直接写出相同 Markup 的语义内容一致。

Markup
-> "#" CodeExpression Attributes?
-> evaluate CodeExpression
-> insert result into outer Content

Content literal 可以包含文字、Reference、Raw syntax、Function call 以及嵌套 EmbeddedExpression。它可以自然跨越段落和 block 边界,因为范围来自匹配的 [ 与 ],而不是事后在 Element tree 上挑一对节点。

顶层 #expression 没有右侧 delimiter,因此它只消费一个 atomic expression,随后立即恢复 Markup。于是 #kbd[A] - B 的减号是正文,#raw(text="<u>")*word* 的星号仍开始 inline strong;需要复合 expression 时写成 #(...)。Arguments、ContentBlock 和 User Function body 已有自己的 closing delimiter,可以在内部正常使用完整 expression。这条规则让 parser 只看局部 token 就能结束 EmbeddedExpression,不需要询问 Function registry 或猜测后续文字的类型。

Function Call

Function call 使用同一个 Code expression 模型:

#heading(level=2)[Typed evaluation]

#quote(attribution=[Alice])[
The body is ordinary Notist Content.
It may contain [[self::references]] and #[annotations]@example.
]

调用先解析 qualified name 和普通 arguments,再接受零个或多个 ContentBlock;公开 signature 至多声明一个 trailing Content parameter,因此 binder 只绑定零个或一个 trailing block,多余 block 产生 diagnostic。

Trailing [...] 只是普通 Content argument 的紧凑写法:

#quote[Quoted Content.]
#quote(body=[Quoted Content.])

二者进入相同的 signature binding 和 Function 执行路径。Function 不重新调用 Notist parser,也不能通过注册名字改变宿主 grammar。

Raw 内容也不需要第二种 call mode。它通过 String、backtick 或 fenced Raw syntax 显式产生,再传给 raw、code、math 等 Function:

#raw(text="fn main() {}", lang="rust")
#raw(text=`fn main() { println!("hello"); }`, lang="rust")

这样 parser 可以只根据局部语法确定语言边界,不需要知道函数签名,也不再需要 RawSource body、bang delimiter 或 BodyForm。

Attributes

Attributes 是 EmbeddedExpression 的统一 postfix:

#[content]@id,#tag-a,#tag-b,.class-a,.class-b,key=value,owner="Alice"
#quote[content]@citation,#source,author="Alice"

列表包含四类条目:

  • 第一个 bare name 是可选 ID,例如 @intro;

  • #name 是可重复 Tag;

  • .name 是可重复 Class;

  • key=value 是可重复 structured property。

可以跳过 ID,直接从 @#tag@.class@key=value 开始。ID 只能出现一次并位于第一个位置,因为它承担 module-local identity;Tag、Class 和 property 则可组合用于查询、样式与扩展分析。

一个有 Attributes 的 expression 同时保留两个 range:

EmbeddedExpression
├─ scope_range       # "#" 与完整 CodeExpression
├─ attributes_range  # "@" 与 attribute list
└─ metadata

Annotation 的 scope 覆盖完整 EmbeddedExpression,而不是任意 token 片段。Attributes 自身拥有独立 range,使 rename、hover 和格式化能够只操作 metadata。显式 scope 必须正确嵌套或彼此分离;非嵌套交叉无法由语法树稳定表达,也会破坏增量编辑后的 identity。

Lowering Content 时,Attributes 不生成容器 Element,也不改变 expression 的 Value:

#[A [[target]] B]@concept

Content:    Text("A "), Reference("target"), Text(" B")
Annotation: scope_range + { id: "concept" }

因此同一段 Content 可以通过嵌套 expression 获得多层 Annotation,而 renderer 不必把每个语义范围强制变成 DOM 父子容器。

参数与 Attributes

参数描述“函数怎样构造结果”,Attributes 描述“这次源码表达式是什么”:

#heading(level=2)[Title]@introduction,#public

level=2 参与 name resolution、类型检查、signature binding 和 Heading Element 构造。@introduction,#public 不传给 heading,而进入独立的 Annotation index。删去 Attributes 不应改变函数求值;修改参数则可能改变生成的 Content。

这条边界也避免了两个常见问题:

  • 用 Attribute 选择函数会让 parser 和 tooling 依赖动态 registry;

  • 把 ID、Tag 当作普通 Function 参数,会让全局索引只能理解特定 builtin。

Function 可以通过显式 API 查询周围 Annotation,但这属于 analysis/evaluation context,不改变基本 ownership。

Parser and Tooling

语法边界只依赖局部 token:

#[...]                 -> Embedded Content literal
#name(args)[...]       -> CallExpression + trailing Content
#name(args)@...        -> CallExpression + source Annotation
inline/fenced raw      -> RawSyntax

parser 应稳定产出 mode-aware syntax tree,并让 analysis 分别投影:

Parse
├─ references
├─ embedded_expressions
├─ call_expressions
├─ raw_syntax
└─ syntax_errors

Analysis
├─ annotations
├─ resolved_calls
├─ diagnostics
└─ source ranges

未知 Function 不改变 parse。analysis 可以产生 diagnostic,lowering 则保留可见的 partial Content 和准确 range。Tree-sitter、formatter 和 editor injection 也无需先加载插件才能判断一个 [ 是 Markup Content 还是 raw host construct。

Evaluation Model

CodeExpression 先按类型求值,再通过 Content insertion 规则进入外层文档。Content 原样插入,String 转为 Text,None 产生空 Content;其他类型若不能插入则产生 diagnostic。

Function call 的路径是:

arguments + trailing Content
-> expression evaluation
-> signature binding
-> FunctionInput
-> FunctionOutput(Content)

Attributes 在这条路径之外,由 syntax/analysis 保存 source ownership。这样 evaluator 可以专注于 Value 与 Content,索引器可以专注于 Annotation,renderer 只消费结构化 Content,而不必共同维护一种万能 Scope/Processor 节点。

结果

Notist 最终只有一条组合规则:Markup 中的 # 嵌入 Code expression,Code 中的 [...] 创建 Content literal,完整 EmbeddedExpression 后可以附加 Attributes。Function、Content 与 Annotation 各自拥有清楚的职责,同时仍能用紧凑语法组合。