@Doc

Inline Syntax Specification

@Doc Inline Syntax Specification v1.4

⚠️ この文書は AI による翻訳です。誤りが含まれる可能性があります。 正確な内容を確認したい場合は、繁體中文版 または English 版 をご参照ください。

0. 目次


1. 設計思想

@Doc は次を採用する:

Only Known Commands Trigger Parsing

既知の命令だけが構文上の意味を持つ。

未知の命令は常に通常のテキストとして扱われる。

この設計の目標:

  • 学習コストを下げる
  • Email、Mention システムとの衝突を避ける
  • AI のパース安定性を高める
  • エディタの耐障害性を高める
  • DSL の拡張性を保つ
  • 安定して予測可能な AST を構築する

2. Lexer の動作定義

コマンド解析規則

Lexer が @ をスキャンしたとき、以下の優先順位で処理すべきである:

  1. 直後が @@ の場合
    • 単一のプレーンテキスト @ としてパースする
  2. 直後が登録済みの命令名に一致する場合
    • 対応する構文解析フローに入る
  3. どの既知の命令にも一致しない場合
    • 全体を通常のテキストとして出力する

入力結果
@mark[hello]mark ノードとしてパースされる
@@mark@mark を出力
[email protected]プレーンテキスト
@GitHubプレーンテキスト
@unknownプレーンテキスト

3. 曖昧性解消ルール

@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 不透明
   ドメイン を参照。 *)
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 ルール
   (/\{[^}]*\}/、1 行ずつの比較)ももともと複数行をサポートしていない
   ため、Lexer は "}"、行末、"["(コンテンツスロットの開始)のうち
   最初に現れた位置でスキャンを停止し、行末と "[" の 2 つのケースは
   未閉合として扱う。詳細は src/Lexer.ts の scanStylesEnd() を参照。 *)
styles =
    "{" ,
        { text-char - "}" - newline - "[" } ,
    "}" ;

key =
    { text-char - "]" } ;

(* @color の {styles} の内容に対する意味論的な制約 —— 完全な検証規則
   (/^#[0-9a-fA-F]{6}$/ に一致しなければならない)は §7 を参照。終端記号
   自体は構文レベルのものにすぎず、桁数や大文字小文字の厳密な検証は
   意味論レベルであり、下記の `styles` と同じ切り分けである。 *)
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 は inline-node よりも優先度が低い。

   Lexer は常に、プレーンテキストにフォールバックする前に
   既知のコマンド認識を試みなければならない(MUST)。
*)

plain-text-char =
    any-unicode-char ;

text-char =
    any-unicode-char ;

letter =
    Unicode Letter ;

symbol =
    Unicode Symbol ;

5. エスケープ規則

構文

@@

出力

@

用途

ユーザーが構文キーワード自体を出力したい場合に使用する。

これはグローバルなエスケープ規則であり、通常の inline-stream の コンテキストに適用される。 @raw の内部には独立したエスケープ規則があり、詳細は 9. @raw 不透明ドメイン を参照。


入力:

@@mark

出力:

@mark

入力:

@@bold[hello]

出力:

@bold[hello]

入力:

Email: test@@example.com

出力:

Email: [email protected]

この書き方は正当だが、次の理由により:

example

は既知の命令ではないため、実際には直接次のように書ける:

Email: [email protected]

エスケープする必要はない。


6. 未知コマンドのフォールバック

@ の後ろが正当な命令名でない場合、パーサーは必ずプレーンテキストモードにフォールバックしなければならない。

例:

@github

出力:

@github

[email protected]

出力:

[email protected]

@my_custom_tag

出力:

@my_custom_tag

この規則は、次のものとの衝突を効果的に回避できる:

  • Email
  • ソーシャルアカウント
  • Discord Mention
  • GitHub Username
  • Chat Mention System

7. @mark / @color / @bordered スタイル意味論

@mark は任意の styles 修飾構文をサポートする:

@mark{style}[content]

ここで:

  • style はカンマ区切りのスタイルトークン文字列(style token list)である。
  • content はマークされるテキスト内容である。
  • styles任意(optional)の構文であり、省略した場合は純粋なハイライトマークと同じになる:
@mark[重要な内容]

Style Token の意味論

style の内容はカンマ区切りの色トークン(Color Token)文字列であり、ハイライトの色を表す。Renderer は意味論に従って実際の色値に対応付ける。2 種類の書き方をサポートしており、どちらか一方を選んで使用できる:

  • named token(Renderer が実際の色値を独自に定義する):
    yellow / red / green / blue / orange / purple / gray
  • 16 進数 Hex Token# で始まり、6 桁の 16 進数、大文字・小文字どちらでも可。Renderer は指定された値を MUST でそのまま使用しなければならず、再マッピングしてはならない):
    #ff0000 / #3366FF / #00c896
    /^#[0-9a-fA-F]{6}$/ の形式に一致しない token(例えば #f00#gggggg)は正当な hex token とはみなされず、一般規則に従って Unknown Command Fallback の寛容な精神で処理される(下記の Renderer の挙動を参照)。

変更履歴:以前のバージョンでは underlinestrikethroughbordered という 3 つの修飾 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 と同じ 7 つの named color token(yellowredgreenblueorangepurplegray)を受け付け、単一の 16 進数 hex token(/^#[0-9a-fA-F]{6}$/)も受け付ける。両者は構文上同じ token 名の集合を共有しているが、対応する実際の色値はそれぞれ独立している@mark の色調は薄いハイライト背景向けに調整されており、そのままテキストの前景色として使うとコントラストが不足して読みにくくなる。そのため Renderer は通常、@color 専用の、より濃い色調の対照表を(@mark のものを再利用するのではなく)別に維持する。Renderer は形式に合わない値や認識できない値を MUST で無視し、エラーを投げるのではなく何らかのデフォルト値へフォールバックしなければならない:

@color{not-a-color}[この文字列は色を指定していないため、優雅にデフォルト色へフォールバックします]

@bordered — 文字の外枠

@bordered は文字に外枠を付ける。@color とまったく同じ {styles} フィールドを共有しており——同じ括弧、同じく任意、同じ 7 つの named token に加えて hex も使え、同じ色対照表も共有する(実装上は @color の resolver をそのまま再利用できる)。ただし適用先が文字色ではなく外枠である点だけが異なる:

@bordered[デフォルトの外枠]
@bordered{blue}[青色の外枠]
@bordered{#3366ff}[16進数の外枠色]

{styles} を省略した場合や、認識できない値が与えられた場合、Renderer はエラーを投げるのではなく、デフォルトの外枠スタイルへフォールバックする。これは §6 Unknown Command Fallback の寛容な精神と呼応している。このノードは、@mark の以前の {styles} にあった bordered 修飾 token を置き換え、独立した第一級ノードとなったものであり、underline(すでに @underline がある)、strikethrough(すでに @del がある)と同じ役割を果たす。


Renderer の挙動

  • Renderer は、少なくとも styles 省略時のデフォルトのハイライトスタイル(@mark)/デフォルトの外枠スタイル(@bordered)を MUST でサポートしなければならない。
  • Renderer は、各 named color token に対応する実際の色値を自由に決定してよい(MAY)(例えばダークモードとライトモードで yellow が異なってもよい。@mark@color@bordered もそれぞれ異なる対照表を維持してよく、通常はそうすべきである。理由は上記参照)。16 進数 hex token については、指定された値を MUST でそのまま使用し、再マッピングしてはならない。
  • Renderer は認識できない token(形式に合わない hex token を含む)を MUST で無視しなければならず、エラーを投げるのではなく何らかのデフォルト値へ SHOULD でフォールバックすべきである——この挙動は 6. 未知コマンドのフォールバック における「未知の命令はプレーンテキストにフォールバックする」という寛容の精神と一致するが、その作用範囲は styles の内部に限定され、@mark[content]@color[content]@bordered[content] 自体は通常どおり対応するノードとしてパースされる。fallback の具体的な見た目は Renderer が自由に決定してよい:「追加の色を付けない」(デフォルトの文字色/外枠色を維持する)でもよいし、@markstyles を省略したときのデフォルトのハイライト色を再利用してもよい(3 つのノードは同じフィールドの形を共有しているため、こうしても視覚的に自然である)。
  • token 同士の区切り文字は半角カンマ , に固定されており、前後には任意の数の空白を置くことができる(Parser が自動的に trim すべきである)。この規則は @markstyles に適用される。@color@bordered{} の中は単一の token(color token または hex 値)のみが許可され、カンマ区切りは使用しない。

EBNF 補足説明

以下は 4. 完全なEBNF文法定義 の中の、次の部分に対応する:

(* Lexer 側の追加的な制約:`text-char` 自体は改行を含むため、文字どおりに
   解釈すると「閉じられていない "{" は文書の後方にあるどの "}" までも
   一気に飲み込める」ことになってしまう —— 作者がまだ入力中の "{" が、
   途中にあるすべてのノード(例えば下記の @code ブロック内の、波括弧より
   前にあるものすべて)を styles の中に飲み込み、AST から黙って消えて
   しまう。styles の実際の意味論は、数個のカンマ区切り token であり、
   複数行にまたがる例は一つもない。エディタの Monarch ルール
   (/\{[^}]*\}/、1 行ずつの比較)ももともと複数行をサポートしていない
   ため、Lexer は "}"、行末、"["(コンテンツスロットの開始)のうち
   最初に現れた位置でスキャンを停止し、行末と "[" の 2 つのケースは
   未閉合として扱う。詳細は 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. 設計思想 にある「DSL の拡張性を保つ」という目標に合致している。

@link は正当な URI、または URI らしき識別子(URI-like Identifier)を受け入れる。

@link(uri)[content]

ここで:

  • uri はターゲットとなるリソースの識別子である。
  • content は表示されるテキストである。

Renderer による URI 推論

Renderer は uri の内容から URI Scheme を自動的に推論してもよい(MAY)。

例えば:

入力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 で指定された値をそのまま使用しなければならず、推論や変更を行ってはならない。


サポートされる URI の例

以下はすべて正当な 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 不透明ドメイン

@raw が属するのは:

Opaque Domain

パーサーが次に入ると:

@raw[

その後:

  • 内部の構文は一切解析しない。
  • @mark@bold@link などのキーワードはすべてプレーンテキストとして扱われる。
  • グローバルな @@ エスケープ規則(5. エスケープ規則)はもはや適用されず、raw 域内には独立したローカルな規則のセットがある(下記参照)。

終了規則:角括弧の深さカウントであり、「最初の ] に出会うこと」ではない

@raw[...] の実装モデルは角括弧の深さカウントである。これはまず明確にしておく必要がある。なぜなら、それがエスケープ規則をいつ使うべきか、いつ使うべきでないかを直接決定するからだ:

  • [ は深さを +1 し、] は深さを -1 する。深さがゼロに戻ったその ] こそが、本当の終端である。
  • 言い換えれば、角括弧が対応してさえいれば、そのまま書けばよく、エスケープはまったく不要である——@raw[@mark[hello]] の中の @mark[hello] は、それ自体の左右の括弧が対応しているため、Parser は正しく一番外側の ] で終了し、@mark[hello] をそのままのテキストとして出力する。
  • エスケープ規則が存在するのは、対応していない角括弧のために特化してのことである——例えば、単独の字面 ] だけを書きたい場合や、それ自体の括弧が不均衡な内容の断片を引用したい場合である。もともと対応していた角括弧に余計にエスケープを加えてしまうと(例えば @mark[hello] の終わりを @mark[hello@] と書いてしまうと)、エスケープによって消費された ] は深さカウントをゼロに戻さない。そのため、前にある @mark[[ によって生じた深さ +1 は、対応する ] で相殺されることが永遠になく、Parser はそのまま先へ先へと探し続け、外側のより多くの内容(文書全体すら)を飲み込んでからようやくエラーになる。対応が取れている括弧はそのまま書く。対応が取れていない括弧は必ずエスケープする。エスケープ文字は深さカウントに参加しない。

エスケープ規則は対称的である——][ はそれぞれ「単一文字のエスケープ」と「『@』自体の後にその文字が続くもののエスケープ」の 2 種類を持ち、合計 4 つの規則がある。スキャン時には以下の優先順位で比較される(長く、より明確なシーケンスが優先される):

優先順位入力出力説明
1@@]@]リテラルの @] 2 文字を出力
2@@[@[リテラルの @[ 2 文字を出力
3@]]対応していないリテラルの ] を出力(深さカウントに影響しない)
4@[[対応していないリテラルの [ を出力(深さカウントに影響しない)

入力:

@raw[@mark[hello]]

出力:

@mark[hello]

説明:@mark[hello] 自体は括弧が対応しており、深さカウントは 1→2→1 と変化し、一番外側の ] でようやく深さがゼロに戻り @raw が終了する。エスケープはまったく不要——これが最も一般的な使い方である(raw の内容の中で、完全で括弧の対応が取れた @Doc の構文の例を示す)。


入力:

@raw[@@]

出力:

@@

説明:ここでは @@ の直後に raw-content の終端 ] が続くため、@@] の特例(@@] は連続する 3 文字でなければならない)は発火せず、次のように分解される:通常の文字 @@(グローバルなエスケープが無効化されているためそのまま出力される)+ 終端の ]


入力:

@raw[今日は@]が検出されるのが怖い]

出力:

今日は]が検出されるのが怖い

説明:ここでの @]対応していないリテラルの ](前に対応する [ がない)であり、必ずエスケープしなければならない。そうしないと、これが @raw 自体の終端として扱われてしまい、後ろの「が検出されるのが怖い」が raw の内容の外に出てしまう。


入力:

@raw[今日は@@]が検出されるのが怖い]

出力:

今日は@]が検出されるのが怖い

入力(エスケープを使うべきでない場所で使ってしまった例——反例):

@raw[ここの @mark[hello@] はそのまま保持される]

レンダリング意味論:インラインコード

上記の規則が定義するのは Parser の挙動(内容を解析せず、そのまま保持する)です。レンダリング側の対応する意味論はインラインコード——Markdown のバッククォート `code` と同じものです:

  • Renderer は @raw を等幅(monospace)のインライン要素として出力すべきです(SHOULD)。HTML Route では <code> を使用します。
  • これは @code とは区別されます:@codeブロック(HTML では <pre><code>)であり、@rawインラインです。
  • @kbd とも区別されます:@kbd は物理キーを表し、慣例として枠線とキーキャップ風の外観を持ちます。@raw はコードテキストであり、等幅フォントと背景色のみで十分です。
  • これにより Renderer が内容を解析してよいことにはなりません——レンダリング意味論の追加は opaque domain の解析規則を変更しません。

なぜ別ノードではなく @raw なのか:raw-escaped は言語全体で唯一、正規のエスケープ機構(@] / @[ と角括弧の深度カウント)を備えた内容モードであり、「内容が解析されてはならない」ことはまさにインラインコードの定義的要件だからです。両者の利用場面はほぼ完全に重なります——本節の上記の例はすべてコードを示しています。


10. ネストされたパース

次の理由により:

content-element =
      inline-node
    | plain-text-char ;

@Doc は完全な再帰的ネストをサポートしている。

例えば:

@bold[
    これは太字で、
    中には
    @mark{yellow}[重要なハイライト]
    と
    @underline[下線]
    がある
]

その AST 構造は次のとおりである:

Bold
├── Text
├── Mark
└── Underline

11. パーサーのリカバリー戦略

Parser が閉じられていない構造に遭遇したとき:

@bold[hello

あるいは:

@mark{red}[hello

2 つのモードを提供することが推奨される:

Strict Mode

直接構文エラーを投げる:

Unexpected EOF while parsing @bold

Editor Mode

エディタが不足している閉じ記号を自動補完することを許可する:

]

これによりリアルタイムの編集体験を向上させる。


12. アーキテクチャ

推奨されるパースの流れ:

Source Text
    ↓
Lexer
    ↓
Token Stream
    ↓
Parser
    ↓
AST
    ↓
Renderer

Renderer は自由に次を出力できる:

  • HTML
  • React
  • PDF
  • DOCX
  • Markdown
  • Discord
  • Terminal
  • Custom UI

13. コア原則

@Doc の中核的な目標は Markdown を置き換えることではない。

そうではなく、次のものを作ることである:

Human Editable Machine Deterministic AI Friendly Cross Platform

という、次世代の文書中間フォーマットを。


14. 簡易構文エイリアス

@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 で定義されている。)