Inline Syntax Specification
@Doc Inline Syntax Specification v1.4
0. Table of Contents
- 1. Design Philosophy
- 2. Lexer 行为定义
- 3. Ambiguity Resolution Rule
- 4. 完整 EBNF 语法定义
- 5. Escape Rule
- 6. Unknown Command Fallback
- 7. @mark / @color / @bordered Styles Semantics
- 8. @link URI Semantics
- 9. @raw Opaque Domain
- 10. Nested Parsing
- 11. Parser Recovery Strategy
- 12. Architecture
- 13. Core Principle
- 14. Simplified Syntax Aliases
1. Design Philosophy
@Doc 采用:
Only Known Commands Trigger Parsing
只有已知指令具有语法意义。
未知指令永远视为普通文本。
此设计目标:
- 降低学习成本
- 避免与 Email、Mention 系统冲突
- 提高 AI 解析稳定性
- 提高编辑器容错能力
- 保持 DSL 的可扩充性
- 创建稳定且可预测的 AST
2. Lexer 行为定义
指令解析规则
当 Lexer 扫描到 @ 时,应依照以下优先级处理:
- 若后续为
@@- 解析为单一纯文本
@
- 解析为单一纯文本
- 若后续符合已注册之指令名称
- 进入对应语法解析流程
- 若不符合任何已知指令
- 整段视为普通文本输出
范例
| 输入 | 结果 |
|---|---|
@mark[hello] | 解析为 mark 节点 |
@@mark | 输出 @mark |
[email protected] | 纯文本 |
@GitHub | 纯文本 |
@unknown | 纯文本 |
3. Ambiguity Resolution Rule
由于 @Doc 采用:
Known Command Recognition
因此 Lexer 必须先尝试辨识已知指令,再退回普通文本模式。
换句话说:
inline-node的优先权高于plain-text-char。
Lexer 必须遵循:
@ 开头
↓
是否为 @@ ?
↓
是否存在于 Command Registry ?
↓
是 → Inline Node
否 → Plain Text
因此:
@mark[hello]
必须解析为:
InlineNode(mark)
而不是:
Text('@')
Text('m')
Text('a')
Text('r')
Text('k')
...
4. 完整 EBNF 语法定义
(* ==========================================================================
Entry Point
========================================================================== *)
inline-stream =
{ inline-node | plain-text-char } ;
inline-node =
mark
| color
| bordered
| bold
| italic
| underline
| del
| raw
| sup
| sub
| fn
| defn
| kbd
| link
| br
| escape ;
(* ==========================================================================
Inline Nodes
========================================================================== *)
mark = "@mark" , [ styles ] , content ;
color = "@color" , [ styles ] , content ;
(* @bordered shares @color's exact {styles} slot and swatch (see §7),
applied as a text border instead of a foreground color. *)
bordered = "@bordered" , [ styles ] , content ;
bold = ( "@bold" | "@b" ) , content ;
italic = ( "@italic" | "@i" ) , content ;
underline = ( "@underline" | "@u" ) , content ;
del = "@del" , content ;
raw = "@raw" , raw-content ;
sup = "@sup" , content ;
sub = "@sub" , content ;
(* Footnotes:
fn = 正文中的引用点(角标),只带编号
defn = 脚注定义本体,带编号与实际内容
*)
fn = "@fn" , "[" , integer , "]" ;
defn = "@defn" , modifier , content ;
kbd = "@kbd" , "[" , key , "]" ;
link = "@link" , uri , content ;
br = "@n" ;
escape = "@@" ;
(* ==========================================================================
Shared Components
========================================================================== *)
content =
"[" ,
{ content-element } ,
"]" ;
content-element =
inline-node
| plain-text-char ;
(* raw-content 的实际终止规则是「方括号深度计数」,不是「遇到第一个未跳脱
的 ]」——balanced-bracket-group 用递归产生式表达「内部只要左右括号成对,
可以随意嵌套,完全不需要跳脱」;只有真正不成对的方括号才需要跳脱。
详见 9. @raw Opaque Domain。*)
raw-content =
"[" , { raw-unit } , "]" ;
raw-unit =
escaped-at-close-bracket (* "@@]" → 字面 "@]" *)
| escaped-at-open-bracket (* "@@[" → 字面 "@[" *)
| escaped-close-bracket (* "@]" → 字面 "]"(仅用于不成对的 ]) *)
| escaped-open-bracket (* "@[" → 字面 "["(仅用于不成对的 [) *)
| balanced-bracket-group (* 成对、可嵌套的字面方括号,内容不受限 *)
| raw-char ;
balanced-bracket-group =
"[" , { raw-unit } , "]" ;
escaped-at-close-bracket = "@@]" ;
escaped-at-open-bracket = "@@[" ;
escaped-close-bracket = "@]" ;
escaped-open-bracket = "@[" ;
raw-char =
any-unicode-char - "]" - "[" ;
uri =
"(" ,
{ text-char - ")" } ,
")" ;
modifier =
"(" ,
{ text-char - ")" } ,
")" ;
(* Lexer 额外收敛:`text-char` 本身包含换行,字面解读等于「未闭合的 "{" 可以
一路吞到文档后面任何一个 "}"」——作者还在打字的 "{" 会把中间所有节点
(例如底下 @code 区块里的大括号之前的一切)整段吃进 styles,从 AST 静默
消失。styles 实际语意是一小串逗号分隔 token,没有任何范例跨行,编辑器的
Monarch 规则(/\{[^}]*\}/,逐行比对)本来也不支持跨行,因此 Lexer 在
"}"、行尾、"["(内容槽开始)三者中最先出现的位置停止扫描,行尾与 "[" 两种
情况视为未闭合。见 src/Lexer.ts 的 scanStylesEnd()。 *)
styles =
"{" ,
{ text-char - "}" - newline - "[" } ,
"}" ;
key =
{ text-char - "]" } ;
(* @color's semantic constraint on its {styles} content — see §7 for the
full validation rule (must match /^#[0-9a-fA-F]{6}$/); the terminal itself
is grammar-level only, exact digit-count/case validation is semantic-level,
same split as `styles` below. *)
hex-color =
"#" , hex-digit , hex-digit , hex-digit , hex-digit , hex-digit , hex-digit ;
hex-digit =
digit
| "a" | "b" | "c" | "d" | "e" | "f"
| "A" | "B" | "C" | "D" | "E" | "F" ;
integer =
digit ,
{ digit } ;
digit =
"0" | "1" | "2" | "3" | "4"
| "5" | "6" | "7" | "8" | "9" ;
(* ==========================================================================
Character Sets
========================================================================== *)
(* Note:
plain-text-char has lower precedence than inline-node.
The lexer MUST always attempt known command recognition
before falling back to plain text.
*)
plain-text-char =
any-unicode-char ;
text-char =
any-unicode-char ;
letter =
Unicode Letter ;
symbol =
Unicode Symbol ;
5. Escape Rule
语法
@@
输出
@
用途
当用户需要输出语法关键字本身时使用。
此为全域转义规则,适用于一般 inline-stream 上下文。
@raw内部有独立的转义规则,请参见 9. @raw Opaque Domain。
范例
输入:
@@mark
输出:
@mark
输入:
@@bold[hello]
输出:
@bold[hello]
输入:
Email: test@@example.com
输出:
Email: [email protected]
虽然此写法合法,但由于:
example
并非已知指令,因此实际上可直接写:
Email: [email protected]
而不需要跳脱。
6. Unknown Command Fallback
若 @ 后方并非合法指令名称,解析器必须退回纯文本模式。
范例:
@github
输出:
@github
[email protected]
输出:
[email protected]
@my_custom_tag
输出:
@my_custom_tag
此规则能有效避免与:
- 社群帐号
- Discord Mention
- GitHub Username
- Chat Mention System
发生冲突。
7. @mark / @color / @bordered Styles Semantics
@mark 支持可选的 styles 修饰语法:
@mark{style}[content]
其中:
style为以逗号分隔的样式标记字符串(style token list)。content为被标记的文本内容。styles为可选(optional)语法,省略时等同纯粹的高亮标记:
@mark[重要内容]
Style Token 语意
style 内容为以逗号分隔的颜色 Token(Color Token)字符串,代表高亮色彩,Renderer 依语意对应到实际颜色值。支持两种写法,可择一使用:
- 具名 Token(Renderer 自订实际色值):
yellow / red / green / blue / orange / purple / gray - 16 进位 Hex Token(
#开头、6 位十六进位数字,大小写皆可,Renderer MUST 直接使用指定值,不得再映射):
格式不符#ff0000 / #3366FF / #00c896/^#[0-9a-fA-F]{6}$/的 token(例如#f00、#gggggg)不视为合法 hex token, 依一般规则走 Unknown Command Fallback 的容错精神(见下方 Renderer 行为)。
变更记录:先前版本另定义了
underline/strikethrough/bordered三个修饰 Token,已移除。underline与@underline节点语意重复;strikethrough与@del节点语意重复;bordered已升格为独立节点@bordered(见下方)。style现在只承载颜色语意,不再混用修饰语意。
范例
@mark[缺省高亮]
@mark{yellow}[黄色高亮]
@mark{red}[红色高亮]
@mark{#3366ff}[16 进位背景色]
@color — 文本改色
@mark 改变的是背景(高亮),无法改变文本本身的颜色。@color 补上这个能力:
@color{#ff0000}[这段文本是红色的]
@color 与 @mark 共用同一个 {styles} 字段(见上方 EBNF),本身为选填—— 省略时 Renderer 退回缺省色,行为与 @mark[content] 省略 {styles} 时相同。
@color{blue}[这段文本是深蓝色的]
@color 接受与 @mark 相同的七个具名 color token(yellow/red/green/blue/ orange/purple/gray),也接受单一 16 进位 hex token(/^#[0-9a-fA-F]{6}$/)。 两者语法上共用同一组 token 名称,但对应的实际色值各自独立:@mark 的色阶是为 浅色高亮背景调校的,直接当作文本前景色会对比度不足、难以阅读,因此 Renderer 通常会维护一份色调较深、专门给 @color 用的对照表(而不是重用 @mark 那份)。 Renderer MUST 忽略格式不符或无法识别的值并以某种默认值作为 fallback,而非抛出错误:
@color{not-a-color}[这段没有指定颜色,优雅地退回缺省色]
@bordered — 文本外框
@bordered 为文本加上外框,与 @color 共用完全相同的 {styles} 字段——同样的括号、同样选填、同样的七个具名 token 加 hex,也共用同一份色票对照表(实作上可直接重用 @color 的 resolver),只是套用在外框而非文本色:
@bordered[缺省外框]
@bordered{blue}[蓝色外框]
@bordered{#3366ff}[16 进位外框色]
省略 {styles} 或给出无法识别的值时,Renderer 以其缺省外框样式作为 fallback,而非抛出错误,呼应 §6 Unknown Command Fallback 的容错精神。此节点取代了 @mark 先前 {styles} 中的 bordered 修饰 token,成为独立的第一级节点,与 underline(已有 @underline)、strikethrough(已有 @del)的角色一致。
Renderer 行为
- Renderer MUST 至少支持
styles省略时的缺省高亮样式(@mark)/缺省外框样式(@bordered)。 - Renderer MAY 自行决定各具名 color token 对应的实际色值(例如深色模式与浅色模式下的
yellow可不同;@mark与@color/@bordered也可以、通常也应该各自维护不同的对照表,理由见上);16 进位 hex token 则 MUST 直接使用指定值,不得重新映射。 - Renderer MUST 忽略无法识别的 token(包含格式不符的 hex token),并 SHOULD 以某种默认值作为 fallback,而非抛出错误——此行为与 6. Unknown Command Fallback 中「未知指令退回纯文本」的容错精神一致,但作用范围限定在
styles内部,@mark[content]/@color[content]/@bordered[content]本身仍会被正常解析为对应节点。fallback 的具体样子由 Renderer 自行决定:可以是「无额外颜色」(沿用缺省文本色/外框色),也可以比照@mark省略styles时的缺省高亮色再利用一次(三者共用同一个字段形状,这么做在视觉上是自然的)。 - Token 之间的分隔符固定为半角逗号
,,前后允许任意数量空白(Parser 应自动 trim)。此规则适用于@mark的styles;@color/@bordered的{}内只允许单一 token(color token 或 hex 值),不使用逗号分隔。
EBNF 补充说明
对应 4. 完整 EBNF 语法定义 中:
(* Lexer 额外收敛:`text-char` 本身包含换行,字面解读等于「未闭合的 "{" 可以
一路吞到文档后面任何一个 "}"」——作者还在打字的 "{" 会把中间所有节点
(例如底下 @code 区块里的大括号之前的一切)整段吃进 styles,从 AST 静默
消失。styles 实际语意是一小串逗号分隔 token,没有任何范例跨行,编辑器的
Monarch 规则(/\{[^}]*\}/,逐行比对)本来也不支持跨行,因此 Lexer 在
"}"、行尾、"["(内容槽开始)三者中最先出现的位置停止扫描,行尾与 "[" 两种
情况视为未闭合。见 src/Lexer.ts 的 scanStylesEnd()。 *)
styles =
"{" ,
{ text-char - "}" - newline - "[" } ,
"}" ;
styles 本身在词法层级仅定义为「花括号包裹的任意字符串行」,实际的 token 切分(以逗号分隔、辨识 color token 与 modifier token)属于语意层级(semantic level)的处理,非 Lexer/Parser 的语法层责任,而是留给 Renderer 或后续语意分析阶段完成。这样设计可确保:
- 添加 style token(例如未来加入
italic、bold等)不需要修改 EBNF 语法定义本身。 - 不同 Renderer 可自行扩充或裁剪其支持的 token 集合,符合 1. Design Philosophy 中「保持 DSL 的可扩充性」的目标。
8. @link URI Semantics
@link 接受任何合法 URI 或 URI-like Identifier。
@link(uri)[content]
其中:
uri为目标资源识别符。content为显示文本。
Renderer URI Inference
Renderer MAY 根据 uri 的内容自动推导 URI Scheme。
例如:
| 输入 | Renderer 实际 URI |
|---|---|
@link(example.com)[官方网站] | https://example.com |
@link([email protected])[联系我] | mailto:[email protected] |
@link(+886912345678)[客服电话] | tel:+886912345678 |
若 uri 已明确指定 Scheme:
@link(https://example.com)[官方网站]
@link(mailto:[email protected])[联系我]
@link(tel:+886912345678)[客服电话]
Renderer MUST 直接使用指定值,不得进行推导或修改。
Supported URI Examples
以下皆属合法 uri:
https://example.com
mailto:[email protected]
tel:+886912345678
ftp://example.com/file.zip
discord://channel/123
vscode://file/path
file:///tmp/test.txt
@Doc 本身不限制 URI 类型。
URI 的实际支持能力由 Renderer 决定。
9. @raw Opaque Domain
@raw 属于:
Opaque Domain
解析器进入:
@raw[
之后:
- 不解析任何内部语法。
- 所有
@mark、@bold、@link等关键字皆视为纯文本。 - 全域
@@转义规则(5. Escape Rule)不再适用,raw 域内是独立的一套局部规则(见下方)。
终止规则:方括号深度计数,不是「遇到第一个 ]」
@raw[...] 的实作模型是方括号深度计数,这点必须先讲清楚,因为它直接决定了跳脱规则什么时候该用、什么时候不该用:
[让深度 +1,]让深度 -1;深度归零的那个]才是真正的结尾。- 换句话说,只要方括号成对,可以直接照抄,完全不需要跳脱——
@raw[@mark[hello]]里的@mark[hello]本身左右括号成对,Parser 会正确在最外层那个]结束,输出@mark[hello]原样文本。 - 跳脱规则的存在,是专门给不成对的方括号用的——例如只想写一个单独的字面
]、或引用一段本身括号不平衡的内容片段。如果对一个本来就成对的方括号多加跳脱(例如把@mark[hello]的结尾写成@mark[hello@]),跳脱消耗掉的]不会让深度计数归零,于是前面@mark[那个[造成的深度 +1 永远找不到对应的]抵销,Parser 只能继续往后找,直到把外层更多内容(甚至整份文档)都吞下去才会出错。平衡的括号直接写;不平衡的括号一律跳脱;转义字符不参与深度计数。
跳脱规则是对称的——] 和 [ 各自都有「单字符跳脱」与「跳脱『@』本身后面接该字符」两种,共四条,扫描时依下列优先序比对(长的、更明确的串行优先):
| 优先序 | 输入 | 输出 | 说明 |
|---|---|---|---|
| 1 | @@] | @] | 输出字面 @] 两个字符 |
| 2 | @@[ | @[ | 输出字面 @[ 两个字符 |
| 3 | @] | ] | 输出不成对的字面 ](不影响深度计数) |
| 4 | @[ | [ | 输出不成对的字面 [(不影响深度计数) |
范例
输入:
@raw[@mark[hello]]
输出:
@mark[hello]
说明:
@mark[hello]本身括号成对,深度计数会 1→2→1,最外层那个]才让深度归零并结束@raw。不需要任何跳脱——这是最常见的用法(在 raw 内容里示范一段完整、括号平衡的 @Doc 语法)。
输入:
@raw[@@]
输出:
@@
说明:此处
@@后方紧接的是 raw-content 的结尾], 因此并未触发@@]特例(@@]须为连续三字符), 应拆解为:一般字符@@(原样输出,因全域转义已停用)+ 结尾]。
输入:
@raw[今天我怕@]被侦测]
输出:
今天我怕]被侦测
说明:这里的
@]是一个不成对的字面](前面没有对应的[),必须跳脱,否则它会被当成@raw自己的结尾,让后面的「被侦测」跑到 raw 内容之外。
输入:
@raw[今天我怕@@]被侦测]
输出:
今天我怕@]被侦测
输入(跳脱用在不该用的地方——反例):
@raw[这里的 @mark[hello@] 保持原样]
渲染语义:行内代码
以上规则定义的是 Parser 行为(内容不解析、原样保留)。渲染端的对应语义是行内代码——与 Markdown 的反引号 `code` 是同一件事:
- Renderer SHOULD 将
@raw输出为等宽(monospace)行内元素。HTML Route 使用<code>。 - 这与
@code区分开来:@code是区块(HTML 为<pre><code>),@raw是行内。 - 也与
@kbd区分开来:@kbd表示实体按键,惯例上带边框与键帽外观;@raw是代码文本,只需等宽与底色。 - Renderer 不得因此解析内容——渲染语义的加入不改变 opaque domain 的解析规则。
为何是
@raw而不是另立节点:raw-escaped是全语言唯一具备正规转义机制(@]/@[加方括号深度计数)的内容模式,而「内容不得被解析」正是行内代码的定义性需求。两者的使用情境几乎完全重叠——本节上方每一个示例都在展示代码。
10. Nested Parsing
由于:
content-element =
inline-node
| plain-text-char ;
因此 @Doc 支持完整递归嵌套。
例如:
@bold[
这是粗体,
里面有
@mark{yellow}[重要高亮]
与
@underline[底线]
]
其 AST 结构为:
Bold
├── Text
├── Mark
└── Underline
11. Parser Recovery Strategy
当 Parser 遇到未闭合结构时:
@bold[hello
或:
@mark{red}[hello
建议提供两种模式:
Strict Mode
直接抛出语法错误:
Unexpected EOF while parsing @bold
Editor Mode
允许编辑器自动补全缺失闭合符号:
]
以提升即时编辑体验。
12. Architecture
推荐解析流程:
Source Text
↓
Lexer
↓
Token Stream
↓
Parser
↓
AST
↓
Renderer
Renderer 可以自由输出:
- HTML
- React
- DOCX
- Markdown
- Discord
- Terminal
- Custom UI
13. Core Principle
@Doc 的核心目标并非取代 Markdown。
而是创建:
Human Editable Machine Deterministic AI Friendly Cross Platform
的新一代文档中介格式。
14. Simplified Syntax Aliases
@bold/@italic/@underline 提供简化别名 @b/@i/@u——纯粹是输入时的简写,Parser 会将其范式为正典名称后才创建 AST 节点(node.type 永远是正典名称),Renderer 完全不需要、也不会区分作者实际输入的是哪一种写法。
| Canonical | Alias |
|---|---|
@bold | @b |
@italic | @i |
@underline | @u |
(Block Syntax 的 @heading/@paragraph 别名 @h/@p 定义在 Block Syntax Specification §11。)
@Doc