syntaxai/tdd.md · main · src / a31_sxdoc.ts

a31_sxdoc.ts 157 lines · 4306 bytes raw
// 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 <li> 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<string, string>;
}

// ─── 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;
}