# SDOC Specification v0.2 @sdoc-spec { # Meta @meta { type: doc sdoc-version: 0.2 } # About @about { The formal SDOC v0.2 specification. Defines syntax for scopes, lists, tables, code blocks, inline formatting, references, the meta scope, scope types, data blocks, and comments. Includes the formal EBNF grammar. Read for edge cases and parser behaviour questions. For a friendlier user-facing reference, see \`docs/reference/syntax.sdoc\`. } # Overview @overview { SDOC ("Simple/Smart Documentation") is a plain text documentation format with explicit scoping. # Goals @goals { {[.] - Explicit scoping with braces; whitespace and indentation are cosmetic - Minimal syntax, easy to parse and render - Automatic heading styles based on nesting depth - Easy lists and easy references - No hidden metadata: only user-provided IDs exist } } # Core Concepts @core-concepts { {[.] - A document is a tree of scopes - A scope has a heading line and either a brace-delimited block (`{ ... }`) or braceless content terminated by the next heading, `}`, or EOF - Headings are explicit and always start with `#` - A scope may have an optional human ID (e.g., `@overview`) - References use `@id` in text - Paragraphs are separated by blank lines or by a new scope at the same level - Whitespace (spaces/tabs) is ignored except where noted } } } # Syntax @syntax { # Heading Line @heading-line { ``` # Title text @id # Title text @id :type # Title text :type @id # Title text :type ``` {[.] - The line must start with `#` (after optional indentation) - Multiple `#` characters are allowed but do not affect depth. Depth comes only from scope nesting - The optional `@id` must appear at the end of the line (or before `:type`), separated by whitespace - The optional `:type` assigns a scope type (see @scope-types). Both `@id :type` and `:type @id` orderings are valid - `:type` requires whitespace before the colon, so `# Note: Important` is NOT a scope type — the colon is part of the title - If no `@id` is present, the scope has no ID - If you need a literal `@` in the title, escape it (`\@`) } } # Scope Block @scope-block { ``` # Title { ... } ``` `{` opens a scope block and `}` closes it. Indentation is cosmetic. # Braceless Leaf Scopes @braceless-scopes { A heading not followed by `{` or a block opener creates a braceless scope. The scope's content runs until the next `#` heading at the same level, a closing `}`, or end of file: ``` # Section A This is content of Section A. It can span multiple lines. # Section B Content of Section B. ``` {[.] - Braceless scopes can contain paragraphs, code blocks, blockquotes, implicit lists, horizontal rules, tables, and headingless scopes - Braceless scopes cannot contain child `#` headings; encountering one ends the scope (the heading becomes a sibling) - A closing `}` also terminates a braceless scope (used when braceless scopes appear inside an explicit parent) - Braceless and explicit scopes can be freely mixed in the same document } } # K&R Style Brace Placement @knr-braces { The opening brace (or list/table opener) may appear at the end of a heading or list-item line instead of on its own line: ``` # Title { ... } # My List {[.] - Item 1 - Item 2 } # Data {[table] Name | Age Alice | 30 } # Title @id { ... } ``` {[.] - The opener must be the last token on the line (trailing whitespace is allowed) - Applies to `{`, `{[.]`, `{[#]`, `{[table]`, and `{[table ]` - Also works on list-item shorthand lines (e.g., `- Item {`) - Table options (e.g., `{[table borderless]`, `{[table 60% center]`) work in K&R style: `# Data {[table borderless]` - Escaped braces (`\{`) are not treated as openers - The closing `}` must still appear on its own line - Inline blocks (`{ content }`) are not affected; a line ending with `}` is not treated as K&R - Headingless scopes (bare `{` on its own line) are not affected } } # Headingless Scopes @headingless-scopes { A bare `{` block without a preceding heading creates a headingless scope: ``` { Grouped content here. } ``` {[.] - Headingless scopes render as sections without a heading element - They inherit the same nesting/indentation as headed scopes - Useful for grouping paragraphs or creating visual indentation without a title - Can be nested arbitrarily } } # Inline Blocks @inline-blocks { For simple content, blocks can be written on a single line: ``` # Name { John Doe } # Count { 42 } ``` {[.] - Inline blocks must not contain unescaped `{` or `}` characters - Multi-line blocks are always supported and sometimes clearer for longer content } } } # Lists @lists { Lists are declared by a list block type: ``` # Bulleted List {[.] # Item 1 { ... } # Item 2 { ... } } # Numbered List {[#] # Item 1 { ... } # Item 2 { ... } } ``` {[.] - `{[.]` creates a bulleted list - `{[#]` creates a numbered list - List items are scopes inside the list block - Commas between list items are allowed but ignored } # List Item Shorthand @list-shorthand { Inside a list block, `- Title text` is shorthand for a list item scope: ``` {[.] - Item 1 - Item 2 } ``` {[.] - A shorthand item may optionally be followed by a block (`{ ... }` or list opener) - If no block follows, the item has no body - Outside list blocks, leading list-item lines form implicit lists (see @implicit-lists) } Numbered shorthand is also supported: ``` {[#] 1. Item 1 2. Item 2 } ``` # Multi-line List Item Shorthand @multi-line-list-items { Inside an explicit list block (`{[.]` or `{[#]`), a shorthand item's title may span multiple lines. Lines that follow the item marker and are not themselves a command token (heading, list marker, brace, fence, blockquote, horizontal rule, or blank line) are treated as continuation lines and joined to the item title with a single space: ``` {[.] - This is a long list item that continues on the next line - Short item } ``` {[.] - Continuation lines are joined with a single space - Continuation stops at: blank lines, list markers (`- `, `1. `), headings (`#`), block openers (`{`, `{[.]`, `{[#]`, `{[table]`), block closers (`}`), code fences, blockquotes, and horizontal rules - A block (`{ ... }` or list/table opener) may still follow the completed multi-line title - Multi-line continuation is only supported in explicit list blocks, not in implicit lists - Indentation of continuation lines is cosmetic } } } # Implicit Lists @implicit-lists { Inside a normal scope block, a run of list-item lines is treated as a list: ``` # Simple bulleted list { - Item 1 - Item 2 } # Simple numbered list { 1. First 2. Second } ``` {[.] - The list type is chosen by the first item marker (`-` for bulleted, `1.` / `1)` for numbered) - Mixed markers end the implicit list - Each item may optionally be followed by a block (`{ ... }` or list opener) } } # Anonymous List Items @anonymous-list-items { Inside a list block, a list item can start with a block directly: ``` {[.] { This item has no heading line. } } ``` The block contents are rendered as the list item body. } # Task Lists @task-lists { Inside a list block, task items use Markdown-style checkboxes: ``` {[.] - [ ] Pending task - [x] Completed task } ``` Works in both bulleted and numbered list blocks. } } # Tables @tables { Tables are declared with `{[table]`: ``` {[table] Name | Age | City Alice | 30 | NYC Bob | 25 | LA } ``` {[.] - The first row is always the header (unless `headerless` is specified) - Columns are separated by `|` - Each row is a single line - Cell contents support inline formatting - Leading and trailing whitespace in cells is trimmed } # Table Options @table-options { Optional flags after `table` in the block opener control table rendering: ``` {[table borderless] Feature | Status Parser | Complete Renderer | Complete } {[table headerless] Alice | 30 | New York Bob | 25 | Los Angeles } {[table borderless headerless] ![](a.png =100%) | ![](b.png =100%) ![](c.png =100%) | ![](d.png =100%) } ``` {[.] - `borderless` removes all table borders and row striping - `headerless` treats the first row as data (no header row) - `auto` sets width to shrink-to-content - A percentage value (e.g. `60%`, `33.3%`) sets an explicit percentage width - A pixel value (e.g. `400px`) sets an explicit pixel width - `center` centers the table (auto margins) - `right` right-aligns the table (margin-left: auto) - `left` left-aligns the table (default, no extra styles) - Default width is 100%; default alignment is left - All flags can be combined in any order - Works with both Allman and K&R brace styles } ``` {[table 60% center] Endpoint | Status /v2/weather | Active } {[table auto right borderless] Key | Value Version | 2.0 } {[table 400px] Name | Age Alice | 30 } ``` } # Table Formulas @table-formulas { Table cells whose text begins with `=` (but not `==`) are evaluated as formulas. The result replaces the cell text in rendered output; the original formula is preserved as a tooltip. ``` {[table] Item | Qty | Price Widget | 10 | 5.00 Gadget | 5 | 12.50 **Total** | =SUM(B1:B2) | =SUM(C1:C2) } ``` # Cell References { {[.] - References use spreadsheet-style A1 notation: column letter (A-Z) followed by row number (1-based) - Row numbering starts at 1 for the first data row; headers are excluded from numbering - A range is two references separated by a colon: `A1:B3` } } # Supported Functions { {[.] - `=SUM(range)` — sum of all values in the range - `=AVG(range)` — arithmetic mean of the range - `=COUNT(range)` — number of cells in the range } Function names are case-sensitive (uppercase only). Multiple ranges can be separated by commas: `=SUM(A1:A3,B1:B3)`. } # Arithmetic Expressions { Cells may also contain arithmetic expressions using `+`, `-`, `*`, `/`, parentheses, and cell references: ``` =B1+B2 =B1*3 =(A1+A2)/2 =-B1 ``` Standard operator precedence applies (multiplication and division before addition and subtraction). Unary minus is supported. } # Percentage Values { Cells ending in `%` are stored internally as decimals (e.g. `25%` = 0.25). When all values in a `SUM` or `AVG` are percentages, the result displays as a percentage. Arithmetic between percentages and plain numbers produces a plain number. } # Error Values { {[.] - `#DIV/0!` — division by zero - `#VALUE!` — unresolvable reference (e.g. reference to a text cell or out-of-bounds) - `#REF!` — invalid cell reference syntax - `#NAME!` — unknown function name - `#SYNTAX!` — malformed formula expression - `#CIRCULAR!` — circular dependency between formula cells } Error cells are styled distinctly (red italic) and display the original formula as a tooltip. } # Escaping { To display a literal `=` at the start of a cell, escape it: `\=SUM(...)` renders as plain text `=SUM(...)`. } } } # Paragraphs @paragraphs { {[.] - Consecutive text lines are joined into a single paragraph - A blank line ends the paragraph - A new scope or list at the same level also ends the paragraph } } # References @references { {[.] - A reference is `@id` in text (unescaped) - References are document-local: `@id` resolves to a scope within the same file only. Cross-document reference syntax may be introduced in a future version but is not part of v0.2 - To link to a section in another file, use a standard link with a fragment: `[Label](./other-file.sdoc#section-id)` - References link to the scope with that ID - ID uniqueness is strongly recommended; tooling may warn on duplicates - References are parsed inside link labels — use `\@` to include a literal `@` in a link label without triggering a reference } } # Links @links { Markdown-style links with absolute URLs or relative file paths: ``` [label](https://example.com) [other doc](./other-file.sdoc) [parent doc](../guide/intro.sdoc) ``` {[.] - Absolute URLs (any scheme) open externally - Relative paths are resolved from the document's directory - Fragments (`./file.sdoc#section`) and query strings are stripped for file resolution - Tooling may warn on broken relative links (target file does not exist) } } # Autolinks @autolinks { Angle-bracket links: ``` ``` Only `http`, `https`, and `mailto` schemes are recognised. Bare URLs starting with `http://`, `https://`, or `mailto:` are also auto-linked without angle brackets. Bare email addresses (e.g. `hello@example.com`) are automatically detected and rendered as `mailto:` links. The detection requires at least one character before and after the `@`, with a domain containing a dot. Email auto-detection does not apply inside code spans, links, or angle-bracket autolinks. } # Images @images { Markdown-style images: ``` ![Alt text](https://example.com/image.png) ``` # Image Width and Alignment @image-width { An optional size and alignment suffix can follow the URL, separated by a space and introduced with `=`: ``` ![Alt](image.png =50%) ![Alt](image.png =200px) ![Alt](image.png =50% center) ![Alt](image.png =35% left) ![Alt](image.png =35% right) ``` {[.] - `=%` or `=px` sets the image width - An optional alignment keyword follows the width: `center`, `left`, or `right` - `center` uses auto margins for horizontal centering - `left` floats the image left with text wrapping on the right - `right` floats the image right with text wrapping on the left - Without an alignment keyword, images display inline (side by side when adjacent) - Two images on the same line with explicit widths sit side by side naturally } } } # Blockquotes @blockquotes { ``` > A quoted line. > Another line in the same quote. ``` {[.] - Consecutive `>` lines form a single blockquote - A blank `>` line breaks paragraphs within the blockquote } } # Horizontal Rules @horizontal-rules { A line of three or more `-`, `*`, or `_` characters: ``` --- ``` } # Inline Formatting @inline-formatting { {[.] - Emphasis: `*em*` - Strong: `**strong**` - Strikethrough: `~~strike~~` - Inline code: `` `code` `` - Inline math: `$x^2$` (rendered via KaTeX) - Display math: `$$E = mc^2$$` (centered block) - Positive marker: `{+text+}` (green highlight) - Neutral marker: `{=text=}` (blue highlight) - Note marker: `{^text^}` (amber highlight) - Caution marker: `{?text?}` (dark amber highlight) - Warning marker: `{!text!}` (orange highlight) - Negative marker: `{-text-}` (red highlight) - Highlight: `{~text~}` (yellow highlight) } Inline math requires non-whitespace immediately after the opening `$` and before the closing `$`. A plain `$` followed by a digit (e.g. `$100`) does not trigger math mode because there is no closing `$`. } # Escaping @escaping { In normal text (including headings and paragraphs), a backslash escapes: `\\` `\{` `\}` `\@` `\[` `\]` `\(` `\)` `\*` `\~` `\#` `\!` `\<` `\>` `\$` `\+` `\=` `\-` `\^` `\?` `\|` and `` \` ``. Escapes are processed before reference detection. If a line begins with `\#`, it is treated as a normal paragraph line (rendered with a literal `#`). If a line begins with `\>`, it is treated as a normal paragraph line (rendered with a literal `>`). Use `\$` to prevent a dollar sign from starting math mode. Use `\|` to include a literal pipe character inside a table cell without it being treated as a column delimiter. } # Code Blocks @code-blocks { Fenced blocks for code or raw text: ````` ```lang raw text here { # @ } is not parsed ``` ````` {[.] - The opening and closing fences must be on their own lines - Anything inside is treated as raw text (no parsing, no escapes) - Optional language tag after the opening fence - The special language tag `math` renders the block as a display equation via KaTeX (no copy button) - The special language tag `mermaid` renders the block as an SVG diagram via the Mermaid library } # Include by Link @code-includes { A code fence can reference an external file using `src:` metadata on the opening fence line. The code block body is replaced by the file contents: ````` ```json src:./data.json ``` ```json src:./data.json lines:3-5 ``` ```src:https://example.com/schema.json ``` ````` {[.] - `src:` references a file path (relative to the document) or URL - `lines:-` optionally limits to a line range (1-based, inclusive) - The language tag is optional and goes before `src:` - Any body text in the code block is replaced by the resolved file contents - If the file cannot be read, an error message is shown in the code block - URL sources are fetched and cached; the cache is cleared on document save } } } # Scope Types @scope-types { A scope type annotation provides semantic meaning to a scope. The type is specified with `:typename` on the heading line, after optional `@id`: ``` # User Authentication @auth :requirement { The system shall authenticate users via OAuth 2.0. } # OAuth Flow :specification @oauth-flow { Implements @auth using the authorization code flow. } # Important :warning { This API is deprecated and will be removed in v3.0. } ``` {[.] - Syntax: `# Title @id :type` or `# Title :type @id` or `# Title :type` — both orderings of `@id` and `:type` are supported - The colon in `:type` requires whitespace before it, so `# Note: Important` is NOT a scope type — the colon is part of the title text - Any string is valid as a type name - Well-known types: `schema`, `example`, `requirement`, `specification`, `definition`, `note`, `warning`, `test`, `task`, `api`, `config`, `deprecated`, `comment` - The type is stored on the AST node and available to renderers and tooling - Scope types enable AI agents to filter and navigate by semantic meaning (e.g., "show all requirements", "find tests for this spec") - Works with K&R brace style: `# Title :warning {` } # Comment Scopes @comment-scopes { The `:comment` scope type creates a scope that is present in the AST but not rendered in the document output. This is useful for editorial annotations, AI agent instructions, and internal notes: ``` # TODO :comment { Rewrite this section after the API stabilises. @alice please review the error handling. } # Agent Instructions :comment { When summarising this document, focus on the requirements and skip the implementation details. } ``` {[.] - Comment scopes use the standard scope type system — no special syntax beyond `:comment` - The scope is parsed into the AST (agents and tooling can extract it) - The scope is not rendered in HTML, PDF, or other visual outputs - Comment scopes can contain any valid SDOC content (paragraphs, lists, code blocks, nested scopes) - Useful for structured annotations that are too complex for line comments } } } # Data Blocks @data-blocks { A code fence with the `:data` flag causes the parser to parse the content as structured data and attach it to the AST node: ````` ```json :data { "name": "SDOC", "version": "0.2", "features": ["scopes", "lists", "tables"] } ``` ````` {[.] - Syntax: ` ```json :data ` — the `:data` flag follows the language tag on the opening fence line - The parser parses the JSON content and stores the result on the AST node - Invalid JSON produces a parse error - JSON is the only supported data format in v0.2 - Without the `:data` flag, JSON code blocks are treated as raw text (existing behaviour) - Data blocks render visually the same as regular code blocks, but the parsed data is available to tooling and agents } } # Line Comments @line-comments { A line starting with `//` (after optional indentation) is a line comment. The line is skipped entirely — it does not appear in the AST and is not rendered: ``` # Configuration @config { // TODO: add validation rules The config file uses JSON format. // This paragraph is hidden from output // but visible in the source file. See @setup for installation steps. } ``` {[.] - The `//` must be at the start of the line (after optional whitespace) - Comment lines are discarded during parsing — they are not present in the AST - Inside code blocks: `//` has no special meaning (code block content is raw) - Mid-line `//` has no special meaning — URLs like `https://example.com` are safe - Use line comments for quick annotations; use comment scopes (@comment-scopes) for structured non-rendered content } } } # Styles, Header, and Footer @styling { # Hierarchical Config @hierarchical-config { Place `sdoc.config.json` in any folder. When rendering a file, configs are merged from the workspace root down to the file's folder. ```json { "style": "styles/sdoc.custom.css", "styleAppend": "styles/overrides.css", "header": "My Project Docs", "footer": "© 2026 My Company" } ``` {[.] - The closest config to the file overrides parent configs - `style` replaces the default stylesheet - `styleAppend` is appended after the base stylesheet (string or array) - `header` and `footer` are plain text with inline formatting supported - Paths in `style` and `styleAppend` are resolved relative to the config file } A starter stylesheet template is provided at `examples/sdoc.template.css`. } # Per-File Overrides (Meta Scope) @meta-scope { A reserved meta scope overrides header/footer and styles: ``` # Meta @meta { # Style { styles/sdoc.custom.css } # StyleAppend { styles/overrides.css } # Header { My *custom* header } # Footer { Page-specific footer text } } ``` {[.] - The `@meta` scope is not rendered in the document body - The meta scope should appear at the top level of the document - `Style` and `StyleAppend` are treated as file paths (relative to the SDOC file) - `Header` and `Footer` render their scope contents at the top/bottom of the page - Per-file meta settings override the merged `sdoc.config.json` values - `@meta` is reserved and should not be used for normal references } # Key:Value Meta Syntax @kv-meta { Inside a `@meta` scope, paragraph lines matching `Key: value` are parsed as metadata. This provides a lighter-weight alternative to sub-scopes for simple values: ``` # Meta @meta { style: styles/custom.css header: My Header footer: My Footer author: Jane Smith date: 2026-02-09 version: 1.0 status: Draft sdoc-version: 0.2 } ``` {[.] - Key matching is case-insensitive - The pattern requires at least one space after the colon (`key: value`, not `key:value`) - Well-known keys: `style`, `styleappend`/`style-append`, `header`, `footer`, `sdoc-version` - `sdoc-version` identifies the SDOC format version the document targets (current: `0.2`). A parser warning is emitted when this key is missing. - All other keys are stored as custom properties (e.g., `author`, `date`, `version`, `status`, `tags`) - Sub-scope syntax takes precedence: if both `# Style { path }` and `style: path` exist, the sub-scope value wins - Key:value and sub-scope syntax can be mixed freely in the same meta scope - Each key:value pair must be on its own paragraph line (separated by blank lines from other pairs) } } } } # Interactive Preview @interactive-preview { The VSCode extension provides an interactive preview with the following features. These are preview-only behaviours and do not affect the SDOC format or static HTML export. # Theme Support @theme-support { The preview automatically follows the user's VSCode colour theme. Light, dark, and high-contrast themes are all supported. {[.] - Theme detection is CSS-driven: VSCode adds \`vscode-dark\`, \`vscode-light\`, \`vscode-high-contrast\`, or \`vscode-high-contrast-light\` classes to the webview body element - Dark mode overrides the \`--sdoc-*\` CSS custom properties and hardcoded \`rgba()\` colours (table headers, code blocks, confidential notices, error blocks) - Mermaid diagrams use the \`dark\` theme when in a dark colour theme and \`neutral\` otherwise - Theme changes take effect immediately (CSS-driven) and trigger a full preview rebuild (for Mermaid re-initialisation) - Exported HTML follows the user's OS colour preference via \`@media (prefers-color-scheme: dark)\`. Print output always uses light theme colours - Custom stylesheets (\`sdoc.config.json\` or \`@meta style\`) that use \`--sdoc-*\` variables inherit dark values automatically. Hardcoded colours in custom styles are not overridden } } # Collapsible Scopes @collapsible-scopes { Scope headings that have children display a toggle triangle, visible on hover. Clicking the triangle collapses the scope's children (hides the content below the heading). Clicking again expands them. {[.] - The triangle points right when collapsed, down when expanded - Collapse state is preserved across preview refreshes using webview state - Scopes are identified by their `@id` if present, otherwise by source line number - Only scopes with children show the toggle } } # Click-to-Navigate @click-to-navigate { Clicking any rendered element in the preview (heading, paragraph, code block, blockquote, list item, table, horizontal rule) navigates the editor cursor to the corresponding source line in the SDOC file. {[.] - Each rendered element carries a `data-line` attribute with its 1-indexed source line number - Clicking on a collapse toggle or a hyperlink does not trigger navigation } } } # Document Formatting @document-formatting { The VSCode extension provides a built-in document formatter accessible via Format Document (Shift+Option+F). The formatter reindents the document based on brace depth. # Formatting Rules @formatting-rules { The formatter operates line-by-line, tracking brace depth to compute indentation: {[.] - Blank lines are preserved as empty lines with no indentation - Code block content (between ``` fences) is passed through raw with no reindentation - Code fence lines are indented at the current depth - Closing braces `}` decrement depth before indenting - Standalone openers (`{`, `{[.]`, `{[#]`, `{[table]`) indent at current depth, then increment - Inline blocks (`{ content }`) indent at current depth with no depth change - K&R lines (heading or list item ending with an opener) indent at current depth, then increment - All other lines (headings, paragraphs, list items, blockquotes, HRs) indent at current depth } } # Behaviour @formatting-behaviour { {[.] - The formatter respects the user's VS Code tab size and spaces/tabs preference - Formatting is idempotent: formatting an already-formatted document produces identical output - Structure is never changed; only indentation is adjusted - The formatter does not reflow paragraph text or reorder content } } } # Implicit Root Scope @implicit-root { If the first non-blank line of a document is a `#` heading and the next non-blank line after it is NOT `{`, an inline block, or a list/table opener, the document is in implicit root mode: ``` # My Document # Section A Content of Section A. # Section B { Content of Section B. } ``` {[.] - The first heading becomes the root scope title - Everything after it until EOF becomes the root scope's children - Works with both braceless and explicit child scopes - If the first heading IS followed by `{` or a block opener, the document uses explicit root mode (existing behavior) - K&R brace style on the first heading also triggers explicit mode } } # Parsing Rules @parsing-rules { Command tokens are recognised only at the start of a line (after optional indentation): {[.] - `#` heading line - `{` scope open - `}` scope close - `{[.]` / `{[#]` list open - `{[table]` / `{[table ]` table open - `>` blockquote line - `---` / `***` / `___` horizontal rule - `` ``` `` code fence - `//` line comment (discarded, not in AST) } Blank lines are allowed anywhere and are ignored. This line-start recognition rule, combined with explicit brace scoping, ensures that every SDOC document has exactly one valid parse tree. This determinism is a security property: there is no structural ambiguity that could be exploited to produce different interpretations in different processing pipelines. } # Formal Grammar @grammar { Informal EBNF grammar: ``` document = implicit_root | block_body ; implicit_root = heading block_body ; scope = heading ws? block | heading ws? braceless_body | heading_with_opener block_body "}" ; heading = "#" { "#" } ws title ((ws id)? (ws scope_type)? | (ws scope_type)? (ws id)?) ; heading_with_opener = "#" { "#" } ws title ((ws id)? (ws scope_type)? | (ws scope_type)? (ws id)?) ws block_opener ; id = "@" ident ; scope_type = ":" ident ; (* id and scope_type may appear in either order *) block_opener = "{" | "{[.]" | "{[#]" | table_open ; block = "{" ws? block_body "}" ; braceless_body = { paragraph | code_block | data_block | blockquote | implicit_list | horizontal_rule | headingless_scope | list_scope | table_scope | bare_directive | line_comment | blank } ; bare_directive = "@" ("meta" | "about") (ws block | braceless_body) ; block_body = { blank | line_comment | paragraph | scope | headingless_scope | list_scope | table_scope | implicit_list | blockquote | horizontal_rule | code_block | data_block | bare_directive | comma_sep } ; headingless_scope = "{" ws? block_body "}" ; list_scope = list_open ws? list_body "}" ; list_open = "{[.]" | "{[#]" ; table_scope = table_open ws? table_body "}" ; table_open = "{[table" { ws table_flag } "]" ; table_flag = "borderless" | "headerless" | "auto" | percentage | pixels | "left" | "center" | "right" ; percentage = digit { digit } [ "." digit { digit } ] "%" ; pixels = digit { digit } "px" ; table_body = table_row { table_row } ; table_row = cell { "|" cell } ; list_body = { blank | comma_sep | scope | list_item_shorthand | anonymous_item } ; implicit_list = list_item_shorthand { list_item_shorthand } ; list_item_shorthand = bullet_item | numbered_item ; bullet_item = "-" ws title { continuation_line } [ws? block]? | "-" ws title ws block_opener block_body "}" ; numbered_item = number ("." | ")") ws title { continuation_line } [ws? block]? | number ("." | ")") ws title ws block_opener block_body "}" ; continuation_line = text_line ; (* only in explicit list blocks *) anonymous_item = "{" ws? block_body "}" ; number = DIGIT { DIGIT } ; paragraph = text_line { ws? text_line } ; text_line = line_not_starting_with_command ; blockquote = quote_line { quote_line } ; quote_line = ">" text_line ; horizontal_rule = "---" | "***" | "___" ; code_block = fence_open raw_text fence_close ; data_block = data_fence_open raw_text fence_close ; fence_open = "```" [lang] [ws "src:" path] [ws "lines:" range] newline ; data_fence_open = "```" lang ws ":data" newline ; fence_close = "```" newline ; line_comment = "//" { any_char } newline ; (* discarded, not in AST *) comma_sep = "," ; blank = newline ; ident = (ALPHA | "_") { ALPHA | DIGIT | "_" | "-" } ; ``` {[.] - `title` is the remainder of the heading line, excluding the optional trailing `@id` and `:type` - `scope_type` and `id` may appear in either order after the title - `line_comment` lines are discarded during parsing and do not appear in the AST - `data_block` content is parsed as JSON; invalid JSON produces a parse error - If a line starts with a command token, it is not a paragraph line - The grammar is line-oriented; practical parsers should operate on lines } } # Open Questions @open-questions { {[.] - Duplicate ID resolution (error vs warning vs nearest-scope) - Additional list types (alpha, roman) - Additional inline formatting (underline) - Additional data block formats beyond JSON (YAML, TOML) } } }