/** * markup.ts — ONE markup grammar for every board write and every board read. * ============================================================================ * * Every Jira description/comment used to be posted as ONE ADF paragraph per * line of raw text, so the Markdown the fleet's writers already emit (`###`, * `**bold**`, `` `code` ``, `- ` lists, `1.` lists, `---`) rendered as literal * punctuation. Vikunja received the same raw text and its Tiptap editor showed * it verbatim. This module is the fix, and it is the ONLY place the grammar * lives: * * Markdown ──parseMarkup──▶ Block[] ──markupToAdf──▶ ADF doc (Jira) * ──markupToHtml──▶ HTML (Vikunja) * ADF doc ──adfToBlocks──▶ Block[] ──blocksToText──▶ Markdown (readers) * HTML ──htmlToBlocks─▶ Block[] ──blocksToText──▶ Markdown (readers) * * WHY THE GRAMMAR IS DELIBERATELY *NOT* CommonMark — three properties the * fleet's own parsers depend on, each of which CommonMark would break: * * 1. A LINE IS A BLOCK. A single newline ends a paragraph; blank lines are * dropped, never rendered as empty paragraphs. The readers therefore hand * back one line per block, joined by `\n` — byte-for-byte what the old * one-paragraph-per-line writer produced, so `parseQuestion` (line 2 of a * comment) and every `Key: value` field the fleet reads off a body keep * working. Nothing ever emits a `hardBreak`. * 2. BLOCK SYNTAX STARTS AT COLUMN 0. A line that begins with whitespace is * a paragraph, whatever follows the whitespace. `draft.js sanitizeBlock` * and `create-node neutralizeChildProse` neutralise a hostile heading or * field line by PREFIXING A SPACE (lossless, visible); CommonMark allows * up to three leading spaces before `###`, which would re-open exactly the * injection those sanitizers close. So a leading space is inert here. * 3. THE MACHINE LINE STAYS PLAIN. `[MAGNUM] verdict exec=… outcome=… * pr=https://…` must come back off the board as the identical first line. * `[x]` without a following `(url)` is text; `_` and `*` open emphasis * only at a word boundary (so `worker=my_worker exec=a_b` has no italics); * a bare URL autolinks only when preceded by whitespace or `(` — so * `pr=https://…` is text while `**PR:** https://…` is a link. A link whose * text equals its href reads back as the bare href, never `[u](u)`. * * SUPPORTED: paragraphs, `#`–`######` headings, `**bold**`/`__bold__`, * `*em*`/`_em_`, `~~strike~~`, `` `code` ``, fenced code blocks, `- `/`* ` * bullets, `1.`/`1)` ordered lists, `- [ ]`/`- [x]` task items, `[text](url)` * + bare URLs, `---` rules, `> ` blockquotes, GitHub-style alert panels * (`> [!NOTE]` / `[!TIP]` / `[!IMPORTANT]` / `[!WARNING]` / `[!CAUTION]` → * ADF panel info / success / note / warning / error), and simple GFM tables. * Anything else is text. NOTHING HERE THROWS: every entry point catches and * degrades to the plain one-paragraph-per-line shape, because a render failure * that lost a comment would be strictly worse than the ugliness it fixes. * * The panel syntax is GitHub's alert extension (a real, documented dialect), * chosen over an invented one so the same Markdown reads correctly on GitHub, * GitLab, Linear and in a plain editor. */ export type Inline = { t: 'text'; v: string; } | { t: 'br'; } | { t: 'code'; v: string; } | { t: 'strong'; c: Inline[]; } | { t: 'em'; c: Inline[]; } | { t: 'strike'; c: Inline[]; } | { t: 'link'; href: string; c: Inline[]; }; export type PanelKind = 'info' | 'success' | 'note' | 'warning' | 'error'; export type Block = { t: 'paragraph'; c: Inline[]; } | { t: 'heading'; level: number; c: Inline[]; } | { t: 'bullet'; items: Block[][]; } | { t: 'ordered'; start: number; items: Block[][]; } | { t: 'task'; items: Array<{ checked: boolean; c: Inline[]; }>; } | { t: 'code'; lang: string; v: string; } | { t: 'quote'; c: Block[]; } | { t: 'panel'; kind: PanelKind; c: Block[]; } | { t: 'rule'; } | { t: 'table'; header: Inline[][] | null; rows: Inline[][][]; }; /** Markdown text → blocks. Line-oriented; never throws. */ export declare function parseMarkup(source: unknown): Block[]; export declare function parseInline(source: unknown): Inline[]; export type AdfNode = { type: string; version?: number; attrs?: Record; content?: AdfNode[]; text?: string; marks?: Array<{ type: string; attrs?: Record; }>; }; /** * The shape every board write used before this module existed — one paragraph * per line, no marks. It is the FALLBACK of `markupToAdf`, and what a caller * retries with when the API rejects the rich document. Exported so the two * cannot drift. */ export declare function plainTextToAdf(text: unknown): AdfNode; /** Markdown → an ADF `doc`. Never throws; degrades to `plainTextToAdf`. */ export declare function markupToAdf(source: unknown): AdfNode; /** Markdown → HTML for Tiptap-backed trackers (Vikunja). Never throws. */ export declare function markupToHtml(source: unknown): string; /** * Blocks → Markdown, ONE LINE PER BLOCK (property 1). Every block ends in `\n`; * nested blocks (quotes, panels) prefix `> `; list items are `- `/`1. `/`- [ ] `. * This is what every reader hands the fleet's parsers. */ export declare function blocksToText(blocks: Block[]): string; /** An ADF doc (or its `content` array) → blocks. Never throws. */ export declare function adfToBlocks(adf: unknown): Block[]; /** * An ADF doc → the Markdown the fleet's parsers read. One line per block, * trimmed. A doc the old writer produced (one plain paragraph per line) comes * back byte-for-byte as the text that was written, blank lines included as * empty paragraphs → empty lines. */ export declare function adfToMarkup(adf: unknown): string; /** HTML (Tiptap/Vikunja) → blocks. Never throws. Plain text without tags is read as Markdown. */ export declare function htmlToBlocks(html: unknown): Block[]; /** HTML → the Markdown the fleet's parsers read (one line per block, trimmed). */ export declare function htmlToMarkup(html: unknown): string;