// c31 — types for sx-doc: tdd.md's typed rich-content format. // // Why a typed tree instead of HTML strings: // • Editor saves a structured shape, not a string blob — block-level // ops (move, transform, AI-edit) operate on typed nodes, not regex. // • Round-trippable: htmlToSx(sxToHtml(doc)) ≈ doc (whitespace modulo). // • Compact JSON: single-letter keys (`t`, `c`, `v`, `m`) keep the // SQLite + git-sidecar payloads small. // // SAMA placement: c31 because this file is pure types/registry — no I/O, // no logic. Parser/renderer live in c32_sxdoc_parse + c32_sxdoc_render // where the deterministic transforms (and their sibling tests) belong. // // Scope-omission: podman's typed marketing blocks (hero, feature-card, // feature-grid, stats-row, steps-grid, use-case-card, cta-band) are // deliberately skipped — tdd.md content has no marketing-landing-page // shape; skipping saves ~600 LOC across server + client. export const SX_DOC_VERSION = 1; export interface SxDocument { v: typeof SX_DOC_VERSION; blocks: SxBlock[]; } export type SxBlock = | SxParagraph | SxHeading | SxList | SxListItem | SxQuote | SxCodeBlock | SxImage | SxDivider | SxHtml | SxShortcode; export interface SxParagraph { t: "p"; c: SxInline[]; } export interface SxHeading { t: "h"; level: 1 | 2 | 3 | 4 | 5 | 6; c: SxInline[]; } export interface SxList { t: "ul" | "ol"; // Each item is an array of blocks so a list item can hold paragraphs, // nested lists, etc. items: SxBlock[][]; } // Separate type so renderers can special-case loose list-items. Lists // store items as SxBlock[][] directly; SxListItem only appears when an // isolated
  • reaches the parser without a parent list. export interface SxListItem { t: "li"; c: SxBlock[]; } export interface SxQuote { t: "quote"; c: SxBlock[]; } export interface SxCodeBlock { t: "code"; // Language hint — e.g. "ts", "py". May be empty. lang?: string; // Raw source code. Newlines preserved verbatim. src: string; } export interface SxImage { t: "img"; src: string; alt?: string; caption?: string; // Intrinsic dimensions if known — used for layout-shift prevention. w?: number; h?: number; } export interface SxDivider { t: "hr"; } // Escape hatch for HTML we don't (yet) model — preserves the source // verbatim so round-tripping is lossless. New element kinds should land // as proper SxBlock variants over time, not as `html` blobs. export interface SxHtml { t: "html"; src: string; } // `[[sx:name arg=value ...]]` shortcode lifted out of source. We store // the name + args structurally so renderers and queries don't need to // understand the wire syntax. export interface SxShortcode { t: "shortcode"; name: string; args: Record; } // ─── inline ────────────────────────────────────────────────────────────── export type SxInline = SxText | SxLink; // Text run with optional marks. Marks are single-character flags: // b=bold i=italic u=underline s=strikethrough c=inline-code // Storage order doesn't matter; renderers nest them deterministically // (see MARK_ORDER in c32_sxdoc_render). export interface SxText { t: "text"; v: string; m?: SxMark[]; } export type SxMark = "b" | "i" | "u" | "s" | "c"; export interface SxLink { t: "a"; href: string; c: SxInline[]; } // ─── helpers ───────────────────────────────────────────────────────────── // Type guard — useful at renderer and storage boundaries. export const isBlock = (node: unknown): node is SxBlock => { if (!node || typeof node !== "object") return false; return "t" in node && typeof (node as { t: unknown }).t === "string"; }; // Sentinel for new posts that haven't been parsed yet. export const emptyDocument = (): SxDocument => ({ v: SX_DOC_VERSION, blocks: [], }); // Row-shape returned by c13_database.listDocuments. Defined here in // Layer 0 (Pure) per SAMA v2 §1.1 so c51 render code can reference // the type without importing from Layer 2 (Adapter). The Adapter // (c13_database) imports this type to type its own return value. export interface SxDocumentSummary { id: number; slug: string; type: "page" | "post"; title: string; status: "published" | "draft"; primaryTag: string | null; updatedAt: number; }