Special Nodes
@Doc Special Nodes — 語義參考文件
本文件是 Inline Syntax Specification 的語義說明文件——
@n與@@在第 4 節的inline-node選項中出現,但真正決定@@行為的規則散落在第 1 節(設計哲學)、第 2 節(Lexer 行為定義)、第 3 節(Ambiguity Resolution Rule)、第 5 節(Escape Rule)與第 9 節(@rawOpaque Domain)之中。本文件的主要目的,就是把這些散落的規則整理在同一個地方——因為@@很容易被誤讀成「只是眾多行內節點之一」,但實際上並非如此。在 Block Syntax Specification 第 2 節 的 Document AST Structure 圖中,這兩者被歸類為「Special Nodes」。
0. 目錄
- 1. 設計哲學
- 2. Special Nodes 的由來
- 3. 結構形狀比較
- 4. 節點說明
- 5. Lexer 優先順序——為什麼
@@不是查表比對 - 6. 轉義的作用範圍——全域規則 vs.
@raw例外 - 7. AST 表示
- 8. Renderer 獨立性
- 9. AI 生成穩定性
- 10. 設計原則
1. 設計哲學
@n 與 @@ 被歸在一起稱為「Special Nodes」,是因為兩者都不帶括號——沒有 content、沒有 modifier、沒有 styles。但相似之處也僅止於此:
@n → 一個語義標記(強制換行)
@@ → 一個 Lexer 層級的轉義機制@n 像其他所有行內節點一樣,命名了某件事——「這個位置是強制換行」。@@ 則不對文件的語意命名任何東西;它存在的唯一目的,是讓作者在後面的文字原本會被誤判為指令時,能夠輸出字元 @ 本身。EBNF(第 4 節)把兩者都列為 inline-node 的選項之一,這只是文法層級的呈現方式——並不代表 Lexer 在執行時會用同樣的方式處理它們。見 第 5 節。
2. Special Nodes 的由來
換行。 Markdown 的強制換行機制惡名昭彰地脆弱——行尾兩個空格,在大多數編輯器中不可見,也經常被格式化工具、其他工具或複製貼上動作直接吃掉。HTML 的 <br> 雖然明確,卻把原始碼綁死在特定渲染目標上。@n 讓同樣的意圖有了一個可見、不依賴位置的 token,能撐過任何會正規化空白字元的工具。
轉義。 Inline Spec 第 1 節設計哲學 確立了「只有已知指令具有語法意義」——無法辨識的 @word 本來就會自動退回純文字(第 6 節),因此大多數字面上的 @ 用法(電子郵件、提及、未知標籤)根本不需要轉義。@@ 存在的目的是更窄的情境:作者想輸出的字面文字,剛好就是一個已註冊的指令名稱——例如在一段介紹 @Doc 自身語法的句子裡,想打出 @mark 這四個字元,卻不希望觸發 mark 節點。
語意(Meaning)
↓
AST Node
↓
Renderer Adapter
↓
Output Target這兩個節點都讓意圖在原始碼中維持明確,而不是仰賴不可見的空白(@n)或依賴上下文的猜測(@@)。
3. 結構形狀比較
| 節點 | 槽位 | 備註 |
|---|---|---|
@n | 無——裸 @n,完全不帶括號 | 零槽位,與區塊層級的 @hr 相同(見 Structural Blocks:Horizontal Rule) |
@@ | 無——裸 @@,完全不帶括號 | 零槽位,但這兩個字元的序列本身就是整個節點;不像 @n、@mark 等節點有「觸發符號+識別碼」的分離結構 |
兩者都是行內層級版本的 @hr:純粹的標點符號,不攜帶任何資料。兩者都無法附加 modifier、styles 或 content——文法中沒有 @n{...},也沒有 @@[...]。
4. 節點說明
Line Break — @n
第一行@n第二行br = "@n" ;在文字中的精確位置插入一個強制換行——不依賴行首/行尾空白,也沒有不可見字元。實際渲染成什麼,取決於目標平台的換行原件(Web 上是 <br>,純文字/terminal 輸出是 \n,DOCX 中是段落內部的換行)。
@n 的辨識方式與其他具名指令相同——它會經過與 @mark、@bold、@sup 等相同的 Command Registry 比對流程(第 2 節步驟 2,見下方 第 5 節)。它只是恰好是目前唯一一個「文法完全是空的」已註冊指令。
Escape — @@
@@markescape = "@@" ;輸出:一個字面的 @ 字元,後面接續的內容則從該處開始正常解析。
輸入:@@mark
輸出:@mark輸入:@@bold[hello]
輸出:@bold[hello]完整的範例集與設計理由請見 Inline Syntax Specification 第 5 節;本文不重複列出每一個案例。值得在此重申的一個細節是:@@ 並不是「把後面整個指令都轉義掉」——它單純只是「把眼前這兩個字元收斂成一個字面的 @」,僅此而已。後面接續的內容會完全獨立地重新進行 Lexing,這也是為什麼 @@bold[hello] 輸出的是字面文字 @bold[hello],而不是「一個套用了跳脫樣式的粗體節點」——因為在輸出那個字面 @ 之後,bold[hello] 就只是一段前面沒有 @ 的純文字,不會觸發任何東西。
5. Lexer 優先順序——為什麼 @@ 不是查表比對
EBNF(第 4 節)把 escape 列為 inline-node 底下眾多選項之一:
inline-node =
mark
| bold
| ...
| br
| escape ;單看這段文法,會讓人以為 @@ 在結構上與 @mark 或 @n 完全相同——都只是 Parser 拿去比對的其中一個具名選項。但事實並非如此,Inline Syntax Specification 第 2 節 明確定義了實際的處理順序:
當 Lexer 掃描到 @ 時,應依照以下優先順序處理:
1. 若後續為 @@ → 解析為單一純文字 @
2. 若後續符合已註冊之指令名稱 → 進入對應語法解析流程
3. 若不符合任何已知指令 → 整段視為普通文字輸出@@ 是在步驟 1 被檢查的,純粹依靠字元樣式判斷(下一個字元是不是也是 @?)——這發生在 Lexer 去查詢 Command Registry 之前。@n、@mark 以及其他所有具名節點,都是在步驟 2 被比對的,這需要該識別碼確實存在於 registry 中。換句話說:
@@的辨識完全不需要 registry 條目。就算把語言中所有具名指令都移除,@@依然能正常運作。@n則必須是已註冊的名稱。如果n有一天被取消註冊,@n就會落到步驟 3,被當成字面文字@n輸出,就像今天的@unknown一樣(第 6 節)。
這正是本文件想要凸顯的細節:EBNF 中扁平的 inline-node 選項列表,只是文法呈現上的方便寫法,並不代表 Lexer 實作順序也是如此。請將 escape 視為在詞法層級就享有優先權、凌駕於該列表其他成員之上的存在,而不是與它們平起平坐的同儕。
6. 轉義的作用範圍——全域規則 vs. @raw 例外
全域 @@ 規則(Inline Spec 第 5 節)適用於一般的 inline-stream 上下文——@paragraph、@quote、@bold 內部皆是如此。但它不適用於 @raw 內部。Inline Syntax Specification 第 9 節 明確寫道:
全域
@@轉義規則(第 5 節)不再適用。
在 @raw[...] 內部,只存在兩條局部、彼此無關的例外規則:
| 輸入 | 輸出 | 說明 |
|---|---|---|
@] | ] | 輸出字面 ] |
@@] | @] | 輸出字面的兩個字元 @] |
這並不是全域 @@ 規則被重新套用——而是另一套獨立、範圍更窄的機制,目的只是讓 raw 內容能夠包含一個字面的 ](否則會被誤判為 raw 區塊的結尾),而不需要中斷 raw 域。在 @raw 內部,如果一個裸 @@ 後面沒有緊接著 ],它不會被收斂成單一 @——它就只是兩個字面的 @ 字元,因為全域轉義已經停用,而任何局部例外規則也都對不上。規格書自己的範例正好說明了這一點:
輸入:@raw[@@]
輸出:@@這裡的 @@ 緊接在 raw 區塊的結尾 ] 之前。乍看之下似乎應該觸發三字元的 @@] 局部例外規則,但那條例外規則要求內容裡確實存在三個字面字元——這裡卻只有兩個 @ 字元,後面接的是區塊的結尾符號 ],並不是第三個內容字元。因此應該拆解為:字面的 @@(未被轉義,因為全域轉義已停用) + 結尾的 ](根本不算在輸出內容裡)。結果:@@。
需要牢記的重點:「在 @raw 內部」與「在一般 inline-stream 中」是兩套彼此獨立、只是剛好都借用 @ 作為標記符號的轉義系統。若假設全域 @@ 的語意也適用於 @raw 內部,或假設 @raw 的 @]/@@] 例外規則在 @raw 之外也成立,兩種假設都會產生錯誤的輸出。這兩套系統彼此不會互相 fallback。
7. AST 表示
範例:
第一行@n第二行,這裡示範 @@mark 這個寫法本身。Document
└── BlockNodes
└── ParagraphNode
├── TextNode
│ └── "第一行"
├── BrNode
├── TextNode
│ └── "第二行,這裡示範 "
├── TextNode
│ └── "@mark 這個寫法本身。"注意 @@mark 在樹狀結構中並不會產生 EscapeNode 或 MarkNode——當 Lexer 處理到它的時候,它早已被收斂成字面文字 @mark,在 AST 中與直接手打(不帶 @@ 前綴)的同一段字串完全無法區分。轉義機制運作在 Lexer 階段,發生在節點被建立之前——AST 中根本不存在 EscapeNode 這種東西。
相對地,BrNode 是一個真正的 AST 節點——Renderer 需要知道「這裡發生了一次強制換行」這件事本身,而不只是看到混在文字裡的一個 \n 字元(這種字元很可能會被會壓縮空白的 Renderer 直接忽略)。
8. Renderer 獨立性
原始碼:
地址:台北市@n信義區Web:
<p>地址:台北市<br>信義區</p>Terminal/純文字:
地址:台北市
信義區DOCX:段落內部的換行,而不是開啟一個新段落。
@@ 本身沒有任何面向 Renderer 的輸出——內容抵達 AST 的當下,@@mark 已經與直接手打的 @mark 文字無法區分。Renderer 完全不需要為它寫任何特判邏輯。
9. AI 生成穩定性
依賴行尾空白的換行,正是生成式模型——以及它周圍的整套工具鏈(格式化工具、修剪空白、diff 工具)——最容易在毫無可見訊號的情況下悄悄搞丟的東西。@n 用四個可見字元取代兩個不可見的空格,這四個字元能撐過管線中任何地方的空白正規化處理。
@@ 補上的是另一種缺口:如果沒有它,模型就沒有確定性的方式可以輸出字面字串 @mark(例如撰寫一份介紹 @Doc 自身語法的文件時),而不冒著被解析成真實節點的風險——尤其考慮到 第 6 節 Unknown Command Fallback 已經讓大多數 @word 序列本來就能安全地當成純文字,這反而讓少數不安全的情況(剛好是已註冊的指令名稱)變成一個容易被忽略的邊角案例。@@ 給模型一個明確、無條件的方法,能夠不論後面的字詞是否剛好被註冊,都直接關閉指令辨識。
10. 設計原則
Special Nodes 遵循與 @Doc 其餘部分相同的原則:
Semantic First, Layout Later@n 不是格式化工具可能會順手刪掉的兩個行尾空格。@@ 也不是從其他語言整套搬過來的反斜線轉義慣例——它的作用範圍受限、有明確的優先順序,並且刻意在它要解決的那個歧義之外的所有地方保持完全不作用。兩者都是由它們在 Lexer 層級實際做了什麼來定義的,而不是由某個 Renderer 今天剛好怎麼畫一個換行、或某個工具剛好怎麼吃掉一個 @ 字元來定義的。
@Doc