@Doc

Inline Syntax Specification

@Doc Inline Syntax Specification v1.4

0. Table of Contents


1. Design Philosophy

@Doc 採用:

Only Known Commands Trigger Parsing

只有已知指令具有語法意義。

未知指令永遠視為普通文字。

此設計目標:

  • 降低學習成本
  • 避免與 Email、Mention 系統衝突
  • 提高 AI 解析穩定性
  • 提高編輯器容錯能力
  • 保持 DSL 的可擴充性
  • 建立穩定且可預測的 AST

2. Lexer 行為定義

指令解析規則

當 Lexer 掃描到 @ 時,應依照以下優先順序處理:

  1. 若後續為 @@
    • 解析為單一純文字 @
  2. 若後續符合已註冊之指令名稱
    • 進入對應語法解析流程
  3. 若不符合任何已知指令
    • 整段視為普通文字輸出

範例

輸入結果
@mark[hello]解析為 mark 節點
@@mark輸出 @mark
test@example.com純文字
@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: test@example.com

雖然此寫法合法,但由於:

example

並非已知指令,因此實際上可直接寫:

Email: test@example.com

而不需要跳脫。


6. Unknown Command Fallback

@ 後方並非合法指令名稱,解析器必須退回純文字模式。

範例:

@github

輸出:

@github

test@example.com

輸出:

test@example.com

@my_custom_tag

輸出:

@my_custom_tag

此規則能有效避免與:

  • Email
  • 社群帳號
  • 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 行為)。

變更記錄:先前版本另定義了 underlinestrikethroughbordered 三個修飾 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(yellowredgreenblueorangepurplegray),也接受單一 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)。此規則適用於 @markstyles@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(例如未來加入 italicbold 等)不需要修改 EBNF 語法定義本身。
  • 不同 Renderer 可自行擴充或裁剪其支援的 token 集合,符合 1. Design Philosophy 中「保持 DSL 的可擴充性」的目標。

@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(test@example.com)[聯絡我]mailto:test@example.com
@link(+886912345678)[客服電話]tel:+886912345678

uri 已明確指定 Scheme:

@link(https://example.com)[官方網站]
@link(mailto:test@example.com)[聯絡我]
@link(tel:+886912345678)[客服電話]

Renderer MUST 直接使用指定值,不得進行推導或修改。


Supported URI Examples

以下皆屬合法 uri

https://example.com
mailto:test@example.com
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
  • PDF
  • 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 完全不需要、也不會區分作者實際輸入的是哪一種寫法。

CanonicalAlias
@bold@b
@italic@i
@underline@u

(Block Syntax 的 @heading/@paragraph 別名 @h/@p 定義在 Block Syntax Specification §11。)