┌─ TS ───────────────────────────────────────────────────────────────────────┐
│ // Markdown ? ASCII chrome, applied at build time. Fenced code blocks │
│ // and tables become framed boxes, [!NOTE]-style blockquotes become │
│ // labeled callouts, lone images become numbered figures, and each <h1> │
│ // gets a head-rule. Everything emits both the desktop and mobile │
│ // variants in the same tree; CSS toggles visibility per viewport. │
│ import type { Root, Element, ElementContent, RootContent, Properties } from "hast" │
│ ; │
│ import { G, checkGrid, len, wrap, type PadSide } from "./ascii"; │
│ import { buildAsciiTable, buildTransposedTable, type Cell } from "./ascii-table"; │
│ import { FRAME_W, MOBILE_FRAME_W } from "./config"; │
│ import { mermaidToAscii } from "./mermaid"; │
│ │
│ const FN_RE = /\b([a-zA-Z_]\w*)(?=\s*\()/g; │
│ const CALLOUT_RE = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*/; │
│ │
│ function visitElements(node: Root | RootContent, fn: (el: Element) => void): void │
│ { │
│ if ((node as Element).type === "element") fn(node as Element); │
│ const children = (node as { children?: RootContent[] }).children; │
│ if (children) for (const c of children) visitElements(c, fn); │
│ } │
│ │
│ function hasClass(el: Element, cls: string): boolean { │
│ const c = (el.properties as Properties | undefined)?.className; │
│ return Array.isArray(c) && c.includes(cls); │
│ } │
│ │
│ function textContent(node: RootContent): string { │
│ if (node.type === "text") return node.value; │
│ const kids = (node as { children?: RootContent[] }).children; │
│ return kids ? kids.map(textContent).join("") : ""; │
│ } │
│ │
│ function frame(value: string): Element { │
│ return { │
│ type: "element", │
│ tagName: "span", │
│ properties: { className: ["code-frame"], "aria-hidden": "true" }, │
│ children: [{ type: "text", value }], │
│ }; │
│ } │
│ │
│ function blockSpan(cls: string, variant: string, children: ElementContent[]): Elem │
│ ent { │
│ return { │
│ type: "element", │
│ tagName: "span", │
│ properties: { className: [cls, variant], "aria-hidden": "true" }, │
│ children, │
│ }; │
│ } │
│ │
│ const copyBtn: Element = { │
│ type: "element", │
│ tagName: "button", │
│ properties: { type: "button", className: ["code-copy"], "aria-label": "Copy code │
│ " }, │
│ children: [{ type: "text", value: "[ COPY ]" }], │
│ }; │
│ │
│ // ---------------------------------------------------------------- code │
│ │
│ /** Line class for ```diff fences; null renders as plain code. */ │
│ export function diffLineClass(line: string): string | null { │
│ if (line.startsWith("+++") || line.startsWith("---") || line.startsWith("diff ") │
│ || line.startsWith("index ")) return "diff-meta"; │
│ if (line.startsWith("@@")) return "diff-hunk"; │
│ if (line.startsWith("+")) return "diff-add"; │
│ if (line.startsWith("-")) return "diff-del"; │
│ return null; │
│ } │
│ │
│ function buildBoxChildren(text: string, lang: string, frameW: number): ElementCont │
│ ent[] { │
│ const inner = frameW - 4; │
│ const lines = text.replace(/\n+$/, "").split("\n").flatMap((line) => { │
│ // Classified before wrapping so continuation chunks keep the class. │
│ const cls = lang === "diff" ? diffLineClass(line) : null; │
│ const cps = [...line]; │
│ if (cps.length <= inner) return [{ text: line, cls }]; │
│ const chunks: { text: string; cls: string | null }[] = []; │
│ for (let i = 0; i < cps.length; i += inner) chunks.push({ text: cps.slice(i, i │
│ + inner).join(""), cls }); │
│ return chunks; │
│ }); │
│ │
│ // Top reserves 8 chars for [ COPY ] plus one trailing ─ before the corner. │
│ const label = lang ? ` ${lang.toUpperCase()} ` : ""; │
│ const topFill = label │
│ ? `${G.TL}${G.H}${label}${G.H.repeat(frameW - 12 - label.length)}` │
│ : `${G.TL}${G.H.repeat(frameW - 11)}`; │
│ const bot = `${G.BL}${G.H.repeat(frameW - 2)}${G.BR}`; │
│ │
│ const children: ElementContent[] = [ │
│ frame(topFill), │
│ copyBtn, │
│ frame(G.H + G.TR), │
│ { type: "text", value: "\n" }, │
│ ]; │
│ │
│ for (const { text: line, cls } of lines) { │
│ checkGrid(line, inner, "code block"); │
│ const padLen = Math.max(0, inner - len(line)); │
│ children.push(frame(`${G.V} `)); │
│ if (cls) { │
│ children.push({ │
│ type: "element", │
│ tagName: "span", │
│ properties: { className: [cls] }, │
│ children: [{ type: "text", value: line }], │
│ }); │
│ } else { │
│ let last = 0; │
│ for (const m of line.matchAll(FN_RE)) { │
│ if (m.index! > last) children.push({ type: "text", value: line.slice(last, │
│ m.index) }); │
│ children.push({ │
│ type: "element", │
│ tagName: "strong", │
│ properties: {}, │
│ children: [{ type: "text", value: m[0] }], │
│ }); │
│ last = m.index! + m[0].length; │
│ } │
│ if (last < line.length) children.push({ type: "text", value: line.slice(last │
│ ) }); │
│ } │
│ children.push(frame(`${" ".repeat(padLen)} ${G.V}`)); │
│ children.push({ type: "text", value: "\n" }); │
│ } │
│ │
│ children.push(frame(bot)); │
│ return children; │
│ } │
│ │
│ function makeCodePre(text: string, lang: string, frameW: number, extraClass: strin │
│ g): Element { │
│ return { │
│ type: "element", │
│ tagName: "pre", │
│ properties: { │
│ className: ["ascii", "code-block", extraClass], │
│ "data-raw-code": text.replace(/\n+$/, ""), │
│ }, │
│ children: [{ │
│ type: "element", │
│ tagName: "code", │
│ properties: {}, │
│ children: buildBoxChildren(text, lang, frameW), │
│ }], │
│ }; │
│ } │
│ │
│ // --------------------------------------------------------------- table │
│ │
│ function tableCells(tbl: Element): { rows: Cell[][]; headerRows: number } { │
│ const rows: Cell[][] = []; │
│ let headerRows = 0; │
│ visitElements(tbl, (el) => { │
│ if (el.tagName !== "tr") return; │
│ const cells: Cell[] = []; │
│ let isHeader = false; │
│ for (const c of el.children) { │
│ if (c.type !== "element" || (c.tagName !== "td" && c.tagName !== "th")) cont │
│ inue; │
│ if (c.tagName === "th") isHeader = true; │
│ const a = (c.properties as Properties | undefined)?.align; │
│ const align: PadSide = a === "right" ? "start" : a === "center" ? "center" : │
│ "end"; │
│ cells.push({ text: textContent(c).replace(/\s+/g, " ").trim(), align }); │
│ } │
│ if (cells.length) { │
│ rows.push(cells); │
│ if (isHeader) headerRows = rows.length; │
│ } │
│ }); │
│ return { rows, headerRows }; │
│ } │
│ │
│ function makeTablePre(text: string, extraClass: string): Element { │
│ return { │
│ type: "element", │
│ tagName: "pre", │
│ properties: { className: ["ascii", "table-frame", extraClass], "aria-hidden": │
│ "true" }, │
│ children: [{ type: "text", value: text }], │
│ }; │
│ } │
│ │
│ // ------------------------------------------------------------ callouts │
│ │
│ function calloutTop(label: string, frameW: number, variant: string): Element { │
│ return blockSpan("callout-rule", variant, [ │
│ { type: "text", value: `${G.TL}${G.H} ` }, │
│ { │
│ type: "element", │
│ tagName: "strong", │
│ properties: {}, │
│ children: [{ type: "text", value: label }], │
│ }, │
│ { type: "text", value: ` ${G.H.repeat(Math.max(0, frameW - 5 - len(label)))}${ │
│ G.TR}` }, │
│ ]); │
│ } │
│ │
│ function calloutBottom(frameW: number, variant: string): Element { │
│ return blockSpan("callout-rule", variant, [ │
│ { type: "text", value: `${G.BL}${G.H.repeat(frameW - 2)}${G.BR}` }, │
│ ]); │
│ } │
│ │
│ // ------------------------------------------------------------- shared │
│ │
│ function ruleSpan(frameW: number, variant: "frame-wide" | "frame-narrow", cls = "h │
│ ead-rule"): Element { │
│ return blockSpan(cls, variant, [{ type: "text", value: G.H.repeat(frameW) }]); │
│ } │
│ │
│ function captionSpan(text: string, frameW: number, variant: string): Element { │
│ const wrapped = checkGrid(wrap(text, frameW), frameW, "figure caption"); │
│ return blockSpan("fig-caption", variant, [{ type: "text", value: wrapped }]); │
│ } │
│ │
│ function fenceInfo(node: Element): { lang: string; text: string } | null { │
│ const codeEl = node.children.find( │
│ (c): c is Element => c.type === "element" && c.tagName === "code", │
│ ); │
│ if (!codeEl) return null; │
│ const codeCls = (codeEl.properties as Properties | undefined)?.className; │
│ const langClass = Array.isArray(codeCls) │
│ ? codeCls.find((c): c is string => typeof c === "string" && c.startsWith("lang │
│ uage-")) │
│ : undefined; │
│ const first = codeEl.children[0]; │
│ return { │
│ lang: langClass ? langClass.replace("language-", "") : "", │
│ text: first && first.type === "text" ? first.value : "", │
│ }; │
│ } │
│ │
│ // Fences may open with a ":: CAPTION" line; mermaid fences use │
│ // "%% :: CAPTION" so the source stays valid for Obsidian's preview. │
│ function parseFence(text: string): { body: string; captionText: string | null } { │
│ const trimmed = text.replace(/\n+$/, ""); │
│ const m = trimmed.match(/^(?:%%\s*)?::[ \t]*(.+)\n/); │
│ return m │
│ ? { body: trimmed.slice(m[0].length), captionText: m[1].trim() } │
│ : { body: trimmed, captionText: null }; │
│ } │
│ │
│ // Lenient mode is for markdown we don't own (repo readmes): a │
│ // construct that defeats the grid degrades to a plain code frame │
│ // instead of failing the build. Site content never sets it and keeps │
│ // failing loudly. │
│ export function rehypeAscii(opts: { lenient?: boolean } = {}) { │
│ const lenient = opts.lenient === true; │
│ return (tree: Root) => { │
│ let fig = 0; │
│ │
│ visitElements(tree, (node) => { │
│ if (node.tagName === "pre") { │
│ // Skip the dual-rendered pres we emit ourselves; otherwise the │
│ // visitor recurses into them and reprocesses indefinitely. │
│ if (hasClass(node, "code-block") || hasClass(node, "table-frame")) return; │
│ │
│ const info = fenceInfo(node); │
│ if (!info) return; │
│ const { lang, text } = info; │
│ │
│ // Diagram fences render as numbered figures (optional │
│ // ":: CAPTION" first line), not code frames: ```diagram is │
│ // hand-drawn ASCII art kept verbatim; ```mermaid renders │
│ // chains through the house layout and branching graphs │
│ // through mermaid-ascii. Art wider than the narrow frame │
│ // scrolls. │
│ if (lang === "diagram" || lang === "mermaid") { │
│ const { body, captionText } = parseFence(text); │
│ const caption = captionText ? `FIG. ${fig + 1} - ${captionText.toUpperCa │
│ se()}` : `FIG. ${fig + 1}`; │
│ let figChildren: ElementContent[] | null = null; │
│ try { │
│ const art = lang === "mermaid" ? mermaidToAscii(body, FRAME_W) : body; │
│ checkGrid(art, FRAME_W, "diagram"); │
│ figChildren = [ │
│ ruleSpan(FRAME_W, "frame-wide", "fig-rule"), │
│ ruleSpan(MOBILE_FRAME_W, "frame-narrow", "fig-rule"), │
│ { │
│ type: "element", │
│ tagName: "pre", │
│ properties: { className: ["ascii", "diagram-art"], "aria-hidden": │
│ "true" }, │
│ children: [{ type: "text", value: art }], │
│ }, │
│ captionSpan(caption, FRAME_W, "frame-wide"), │
│ captionSpan(caption, MOBILE_FRAME_W, "frame-narrow"), │
│ ruleSpan(FRAME_W, "frame-wide", "fig-rule"), │
│ ruleSpan(MOBILE_FRAME_W, "frame-narrow", "fig-rule"), │
│ ]; │
│ } catch (e) { │
│ // lenient: fall through and render the fence as a plain │
│ // code frame instead │
│ if (!lenient) throw e; │
│ } │
│ if (figChildren) { │
│ fig++; │
│ node.tagName = "figure"; │
│ node.properties = { className: ["figure-frame", "diagram-frame"], role │
│ : "img", "aria-label": caption }; │
│ node.children = figChildren; │
│ return; │
│ } │
│ } │
│ │
│ node.tagName = "div"; │
│ node.properties = { className: ["code-frames"] }; │
│ node.children = [ │
│ makeCodePre(text, lang, FRAME_W, "frame-wide"), │
│ makeCodePre(text, lang, MOBILE_FRAME_W, "frame-narrow"), │
│ ]; │
│ return; │
│ } │
│ │
│ if (node.tagName === "table") { │
│ if (hasClass(node, "sr-only")) return; │
│ const { rows, headerRows } = tableCells(node); │
│ if (!rows.length) return; │
│ │
│ // A table with too many columns for the frame defeats the │
│ // grid; lenient mode degrades just this table, per width, so a │
│ // table that fits the wide frame but not the narrow one stays │
│ // rich on desktop. Degrade order: transposed key/value blocks │
│ // (needs a header row), then the rows as a code frame whose │
│ // lines hard-wrap, so it can't fail. │
│ const preFor = (w: number, cls: string): Element => { │
│ try { │
│ return makeTablePre(buildAsciiTable(rows, headerRows, w), cls); │
│ } catch (e) { │
│ if (!lenient) throw e; │
│ if (headerRows) { │
│ try { │
│ return makeTablePre(buildTransposedTable(rows, headerRows, w), cls │
│ ); │
│ } catch {} │
│ } │
│ const text = rows.map((r) => r.map((c) => c.text).join(" | ")).join("\ │
│ n"); │
│ return makeCodePre(text, "table", w, cls); │
│ } │
│ }; │
│ const pres = [preFor(FRAME_W, "frame-wide"), preFor(MOBILE_FRAME_W, "frame │
│ -narrow")]; │
│ │
│ // Keep the semantic table for screen readers; the ASCII pres │
│ // are presentation only. │
│ const original: Element = { │
│ type: "element", │
│ tagName: "table", │
│ properties: { className: ["sr-only"] }, │
│ children: node.children, │
│ }; │
│ node.tagName = "div"; │
│ node.properties = { className: ["table-frames"] }; │
│ node.children = [...pres, original]; │
│ return; │
│ } │
│ │
│ if (node.tagName === "blockquote" && !hasClass(node, "callout")) { │
│ const firstP = node.children.find( │
│ (c): c is Element => c.type === "element" && c.tagName === "p", │
│ ); │
│ const firstText = firstP?.children[0]; │
│ if (!firstP || !firstText || firstText.type !== "text") return; │
│ const m = firstText.value.match(CALLOUT_RE); │
│ if (!m) return; │
│ │
│ const label = m[1]; │
│ firstText.value = firstText.value.slice(m[0].length); │
│ if (!firstText.value) firstP.children.shift(); │
│ if (!firstP.children.length) node.children = node.children.filter((c) => c │
│ !== firstP); │
│ │
│ node.properties = { className: ["callout"] }; │
│ node.children = [ │
│ calloutTop(label, FRAME_W, "frame-wide"), │
│ calloutTop(label, MOBILE_FRAME_W, "frame-narrow"), │
│ ...node.children, │
│ calloutBottom(FRAME_W, "frame-wide"), │
│ calloutBottom(MOBILE_FRAME_W, "frame-narrow"), │
│ ]; │
│ return; │
│ } │
│ │
│ if (node.tagName === "p") { │
│ const kids = node.children.filter((c) => !(c.type === "text" && !c.value.t │
│ rim())); │
│ const img = kids[0]; │
│ if (kids.length !== 1 || img.type !== "element" || img.tagName !== "img") │
│ return; │
│ │
│ const alt = String((img.properties as Properties | undefined)?.alt ?? ""). │
│ trim(); │
│ const caption = alt ? `FIG. ${fig + 1} - ${alt.toUpperCase()}` : `FIG. ${f │
│ ig + 1}`; │
│ let caps: Element[]; │
│ try { │
│ caps = [ │
│ captionSpan(caption, FRAME_W, "frame-wide"), │
│ captionSpan(caption, MOBILE_FRAME_W, "frame-narrow"), │
│ ]; │
│ } catch (e) { │
│ // lenient: an unbreakable alt defeats the caption - leave │
│ // the image as a plain paragraph │
│ if (!lenient) throw e; │
│ return; │
│ } │
│ fig++; │
│ node.tagName = "figure"; │
│ node.properties = { className: ["figure-frame"] }; │
│ node.children = [ │
│ ruleSpan(FRAME_W, "frame-wide", "fig-rule"), │
│ ruleSpan(MOBILE_FRAME_W, "frame-narrow", "fig-rule"), │
│ img, │
│ ...caps, │
│ ruleSpan(FRAME_W, "frame-wide", "fig-rule"), │
│ ruleSpan(MOBILE_FRAME_W, "frame-narrow", "fig-rule"), │
│ ]; │
│ return; │
│ } │
│ │
│ if (node.tagName === "h1") { │
│ const already = node.children.some( │
│ (c) => c.type === "element" && c.tagName === "span" && hasClass(c, "head │
│ -rule"), │
│ ); │
│ if (already) return; │
│ node.children = [ │
│ ...node.children, │
│ ruleSpan(FRAME_W, "frame-wide"), │
│ ruleSpan(MOBILE_FRAME_W, "frame-narrow"), │
│ ]; │
│ } │
│ }); │
│ }; │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ TS ─────────────────────────────────┐
│ // Markdown ? ASCII chrome, applied at build │
│ time. Fenced code blocks │
│ // and tables become framed boxes, [!NOTE]-s │
│ tyle blockquotes become │
│ // labeled callouts, lone images become numb │
│ ered figures, and each <h1> │
│ // gets a head-rule. Everything emits both t │
│ he desktop and mobile │
│ // variants in the same tree; CSS toggles vi │
│ sibility per viewport. │
│ import type { Root, Element, ElementContent, │
│ RootContent, Properties } from "hast"; │
│ import { G, checkGrid, len, wrap, type PadSi │
│ de } from "./ascii"; │
│ import { buildAsciiTable, buildTransposedTab │
│ le, type Cell } from "./ascii-table"; │
│ import { FRAME_W, MOBILE_FRAME_W } from "./c │
│ onfig"; │
│ import { mermaidToAscii } from "./mermaid"; │
│ │
│ const FN_RE = /\b([a-zA-Z_]\w*)(?=\s*\()/g; │
│ const CALLOUT_RE = /^\[!(NOTE|TIP|IMPORTANT| │
│ WARNING|CAUTION)\]\s*/; │
│ │
│ function visitElements(node: Root | RootCont │
│ ent, fn: (el: Element) => void): void { │
│ if ((node as Element).type === "element") │
│ fn(node as Element); │
│ const children = (node as { children?: Roo │
│ tContent[] }).children; │
│ if (children) for (const c of children) vi │
│ sitElements(c, fn); │
│ } │
│ │
│ function hasClass(el: Element, cls: string): │
│ boolean { │
│ const c = (el.properties as Properties | u │
│ ndefined)?.className; │
│ return Array.isArray(c) && c.includes(cls) │
│ ; │
│ } │
│ │
│ function textContent(node: RootContent): str │
│ ing { │
│ if (node.type === "text") return node.valu │
│ e; │
│ const kids = (node as { children?: RootCon │
│ tent[] }).children; │
│ return kids ? kids.map(textContent).join(" │
│ ") : ""; │
│ } │
│ │
│ function frame(value: string): Element { │
│ return { │
│ type: "element", │
│ tagName: "span", │
│ properties: { className: ["code-frame"], │
│ "aria-hidden": "true" }, │
│ children: [{ type: "text", value }], │
│ }; │
│ } │
│ │
│ function blockSpan(cls: string, variant: str │
│ ing, children: ElementContent[]): Element { │
│ return { │
│ type: "element", │
│ tagName: "span", │
│ properties: { className: [cls, variant], │
│ "aria-hidden": "true" }, │
│ children, │
│ }; │
│ } │
│ │
│ const copyBtn: Element = { │
│ type: "element", │
│ tagName: "button", │
│ properties: { type: "button", className: [ │
│ "code-copy"], "aria-label": "Copy code" }, │
│ children: [{ type: "text", value: "[ COPY │
│ ]" }], │
│ }; │
│ │
│ // ----------------------------------------- │
│ ----------------------- code │
│ │
│ /** Line class for ```diff fences; null rend │
│ ers as plain code. */ │
│ export function diffLineClass(line: string): │
│ string | null { │
│ if (line.startsWith("+++") || line.startsW │
│ ith("---") || line.startsWith("diff ") || li │
│ ne.startsWith("index ")) return "diff-meta"; │
│ if (line.startsWith("@@")) return "diff-hu │
│ nk"; │
│ if (line.startsWith("+")) return "diff-add │
│ "; │
│ if (line.startsWith("-")) return "diff-del │
│ "; │
│ return null; │
│ } │
│ │
│ function buildBoxChildren(text: string, lang │
│ : string, frameW: number): ElementContent[] │
│ { │
│ const inner = frameW - 4; │
│ const lines = text.replace(/\n+$/, "").spl │
│ it("\n").flatMap((line) => { │
│ // Classified before wrapping so continu │
│ ation chunks keep the class. │
│ const cls = lang === "diff" ? diffLineCl │
│ ass(line) : null; │
│ const cps = [...line]; │
│ if (cps.length <= inner) return [{ text: │
│ line, cls }]; │
│ const chunks: { text: string; cls: strin │
│ g | null }[] = []; │
│ for (let i = 0; i < cps.length; i += inn │
│ er) chunks.push({ text: cps.slice(i, i + inn │
│ er).join(""), cls }); │
│ return chunks; │
│ }); │
│ │
│ // Top reserves 8 chars for [ COPY ] plus │
│ one trailing ─ before the corner. │
│ const label = lang ? ` ${lang.toUpperCase( │
│ )} ` : ""; │
│ const topFill = label │
│ ? `${G.TL}${G.H}${label}${G.H.repeat(fra │
│ meW - 12 - label.length)}` │
│ : `${G.TL}${G.H.repeat(frameW - 11)}`; │
│ const bot = `${G.BL}${G.H.repeat(frameW - │
│ 2)}${G.BR}`; │
│ │
│ const children: ElementContent[] = [ │
│ frame(topFill), │
│ copyBtn, │
│ frame(G.H + G.TR), │
│ { type: "text", value: "\n" }, │
│ ]; │
│ │
│ for (const { text: line, cls } of lines) { │
│ checkGrid(line, inner, "code block"); │
│ const padLen = Math.max(0, inner - len(l │
│ ine)); │
│ children.push(frame(`${G.V} `)); │
│ if (cls) { │
│ children.push({ │
│ type: "element", │
│ tagName: "span", │
│ properties: { className: [cls] }, │
│ children: [{ type: "text", value: li │
│ ne }], │
│ }); │
│ } else { │
│ let last = 0; │
│ for (const m of line.matchAll(FN_RE)) │
│ { │
│ if (m.index! > last) children.push({ │
│ type: "text", value: line.slice(last, m.ind │
│ ex) }); │
│ children.push({ │
│ type: "element", │
│ tagName: "strong", │
│ properties: {}, │
│ children: [{ type: "text", value: │
│ m[0] }], │
│ }); │
│ last = m.index! + m[0].length; │
│ } │
│ if (last < line.length) children.push( │
│ { type: "text", value: line.slice(last) }); │
│ } │
│ children.push(frame(`${" ".repeat(padLen │
│ )} ${G.V}`)); │
│ children.push({ type: "text", value: "\n │
│ " }); │
│ } │
│ │
│ children.push(frame(bot)); │
│ return children; │
│ } │
│ │
│ function makeCodePre(text: string, lang: str │
│ ing, frameW: number, extraClass: string): El │
│ ement { │
│ return { │
│ type: "element", │
│ tagName: "pre", │
│ properties: { │
│ className: ["ascii", "code-block", ext │
│ raClass], │
│ "data-raw-code": text.replace(/\n+$/, │
│ ""), │
│ }, │
│ children: [{ │
│ type: "element", │
│ tagName: "code", │
│ properties: {}, │
│ children: buildBoxChildren(text, lang, │
│ frameW), │
│ }], │
│ }; │
│ } │
│ │
│ // ----------------------------------------- │
│ ---------------------- table │
│ │
│ function tableCells(tbl: Element): { rows: C │
│ ell[][]; headerRows: number } { │
│ const rows: Cell[][] = []; │
│ let headerRows = 0; │
│ visitElements(tbl, (el) => { │
│ if (el.tagName !== "tr") return; │
│ const cells: Cell[] = []; │
│ let isHeader = false; │
│ for (const c of el.children) { │
│ if (c.type !== "element" || (c.tagName │
│ !== "td" && c.tagName !== "th")) continue; │
│ if (c.tagName === "th") isHeader = tru │
│ e; │
│ const a = (c.properties as Properties │
│ | undefined)?.align; │
│ const align: PadSide = a === "right" ? │
│ "start" : a === "center" ? "center" : "end" │
│ ; │
│ cells.push({ text: textContent(c).repl │
│ ace(/\s+/g, " ").trim(), align }); │
│ } │
│ if (cells.length) { │
│ rows.push(cells); │
│ if (isHeader) headerRows = rows.length │
│ ; │
│ } │
│ }); │
│ return { rows, headerRows }; │
│ } │
│ │
│ function makeTablePre(text: string, extraCla │
│ ss: string): Element { │
│ return { │
│ type: "element", │
│ tagName: "pre", │
│ properties: { className: ["ascii", "tabl │
│ e-frame", extraClass], "aria-hidden": "true" │
│ }, │
│ children: [{ type: "text", value: text } │
│ ], │
│ }; │
│ } │
│ │
│ // ----------------------------------------- │
│ ------------------- callouts │
│ │
│ function calloutTop(label: string, frameW: n │
│ umber, variant: string): Element { │
│ return blockSpan("callout-rule", variant, │
│ [ │
│ { type: "text", value: `${G.TL}${G.H} ` │
│ }, │
│ { │
│ type: "element", │
│ tagName: "strong", │
│ properties: {}, │
│ children: [{ type: "text", value: labe │
│ l }], │
│ }, │
│ { type: "text", value: ` ${G.H.repeat(Ma │
│ th.max(0, frameW - 5 - len(label)))}${G.TR}` │
│ }, │
│ ]); │
│ } │
│ │
│ function calloutBottom(frameW: number, varia │
│ nt: string): Element { │
│ return blockSpan("callout-rule", variant, │
│ [ │
│ { type: "text", value: `${G.BL}${G.H.rep │
│ eat(frameW - 2)}${G.BR}` }, │
│ ]); │
│ } │
│ │
│ // ----------------------------------------- │
│ -------------------- shared │
│ │
│ function ruleSpan(frameW: number, variant: " │
│ frame-wide" | "frame-narrow", cls = "head-ru │
│ le"): Element { │
│ return blockSpan(cls, variant, [{ type: "t │
│ ext", value: G.H.repeat(frameW) }]); │
│ } │
│ │
│ function captionSpan(text: string, frameW: n │
│ umber, variant: string): Element { │
│ const wrapped = checkGrid(wrap(text, frame │
│ W), frameW, "figure caption"); │
│ return blockSpan("fig-caption", variant, [ │
│ { type: "text", value: wrapped }]); │
│ } │
│ │
│ function fenceInfo(node: Element): { lang: s │
│ tring; text: string } | null { │
│ const codeEl = node.children.find( │
│ (c): c is Element => c.type === "element │
│ " && c.tagName === "code", │
│ ); │
│ if (!codeEl) return null; │
│ const codeCls = (codeEl.properties as Prop │
│ erties | undefined)?.className; │
│ const langClass = Array.isArray(codeCls) │
│ ? codeCls.find((c): c is string => typeo │
│ f c === "string" && c.startsWith("language-" │
│ )) │
│ : undefined; │
│ const first = codeEl.children[0]; │
│ return { │
│ lang: langClass ? langClass.replace("lan │
│ guage-", "") : "", │
│ text: first && first.type === "text" ? f │
│ irst.value : "", │
│ }; │
│ } │
│ │
│ // Fences may open with a ":: CAPTION" line; │
│ mermaid fences use │
│ // "%% :: CAPTION" so the source stays valid │
│ for Obsidian's preview. │
│ function parseFence(text: string): { body: s │
│ tring; captionText: string | null } { │
│ const trimmed = text.replace(/\n+$/, ""); │
│ const m = trimmed.match(/^(?:%%\s*)?::[ \t │
│ ]*(.+)\n/); │
│ return m │
│ ? { body: trimmed.slice(m[0].length), ca │
│ ptionText: m[1].trim() } │
│ : { body: trimmed, captionText: null }; │
│ } │
│ │
│ // Lenient mode is for markdown we don't own │
│ (repo readmes): a │
│ // construct that defeats the grid degrades │
│ to a plain code frame │
│ // instead of failing the build. Site conten │
│ t never sets it and keeps │
│ // failing loudly. │
│ export function rehypeAscii(opts: { lenient? │
│ : boolean } = {}) { │
│ const lenient = opts.lenient === true; │
│ return (tree: Root) => { │
│ let fig = 0; │
│ │
│ visitElements(tree, (node) => { │
│ if (node.tagName === "pre") { │
│ // Skip the dual-rendered pres we em │
│ it ourselves; otherwise the │
│ // visitor recurses into them and re │
│ processes indefinitely. │
│ if (hasClass(node, "code-block") || │
│ hasClass(node, "table-frame")) return; │
│ │
│ const info = fenceInfo(node); │
│ if (!info) return; │
│ const { lang, text } = info; │
│ │
│ // Diagram fences render as numbered │
│ figures (optional │
│ // ":: CAPTION" first line), not cod │
│ e frames: ```diagram is │
│ // hand-drawn ASCII art kept verbati │
│ m; ```mermaid renders │
│ // chains through the house layout a │
│ nd branching graphs │
│ // through mermaid-ascii. Art wider │
│ than the narrow frame │
│ // scrolls. │
│ if (lang === "diagram" || lang === " │
│ mermaid") { │
│ const { body, captionText } = pars │
│ eFence(text); │
│ const caption = captionText ? `FIG │
│ . ${fig + 1} - ${captionText.toUpperCase()}` │
│ : `FIG. ${fig + 1}`; │
│ let figChildren: ElementContent[] │
│ | null = null; │
│ try { │
│ const art = lang === "mermaid" ? │
│ mermaidToAscii(body, FRAME_W) : body; │
│ checkGrid(art, FRAME_W, "diagram │
│ "); │
│ figChildren = [ │
│ ruleSpan(FRAME_W, "frame-wide" │
│ , "fig-rule"), │
│ ruleSpan(MOBILE_FRAME_W, "fram │
│ e-narrow", "fig-rule"), │
│ { │
│ type: "element", │
│ tagName: "pre", │
│ properties: { className: ["a │
│ scii", "diagram-art"], "aria-hidden": "true" │
│ }, │
│ children: [{ type: "text", v │
│ alue: art }], │
│ }, │
│ captionSpan(caption, FRAME_W, │
│ "frame-wide"), │
│ captionSpan(caption, MOBILE_FR │
│ AME_W, "frame-narrow"), │
│ ruleSpan(FRAME_W, "frame-wide" │
│ , "fig-rule"), │
│ ruleSpan(MOBILE_FRAME_W, "fram │
│ e-narrow", "fig-rule"), │
│ ]; │
│ } catch (e) { │
│ // lenient: fall through and ren │
│ der the fence as a plain │
│ // code frame instead │
│ if (!lenient) throw e; │
│ } │
│ if (figChildren) { │
│ fig++; │
│ node.tagName = "figure"; │
│ node.properties = { className: [ │
│ "figure-frame", "diagram-frame"], role: "img │
│ ", "aria-label": caption }; │
│ node.children = figChildren; │
│ return; │
│ } │
│ } │
│ │
│ node.tagName = "div"; │
│ node.properties = { className: ["cod │
│ e-frames"] }; │
│ node.children = [ │
│ makeCodePre(text, lang, FRAME_W, " │
│ frame-wide"), │
│ makeCodePre(text, lang, MOBILE_FRA │
│ ME_W, "frame-narrow"), │
│ ]; │
│ return; │
│ } │
│ │
│ if (node.tagName === "table") { │
│ if (hasClass(node, "sr-only")) retur │
│ n; │
│ const { rows, headerRows } = tableCe │
│ lls(node); │
│ if (!rows.length) return; │
│ │
│ // A table with too many columns for │
│ the frame defeats the │
│ // grid; lenient mode degrades just │
│ this table, per width, so a │
│ // table that fits the wide frame bu │
│ t not the narrow one stays │
│ // rich on desktop. Degrade order: t │
│ ransposed key/value blocks │
│ // (needs a header row), then the ro │
│ ws as a code frame whose │
│ // lines hard-wrap, so it can't fail │
│ . │
│ const preFor = (w: number, cls: stri │
│ ng): Element => { │
│ try { │
│ return makeTablePre(buildAsciiTa │
│ ble(rows, headerRows, w), cls); │
│ } catch (e) { │
│ if (!lenient) throw e; │
│ if (headerRows) { │
│ try { │
│ return makeTablePre(buildTra │
│ nsposedTable(rows, headerRows, w), cls); │
│ } catch {} │
│ } │
│ const text = rows.map((r) => r.m │
│ ap((c) => c.text).join(" | ")).join("\n"); │
│ return makeCodePre(text, "table" │
│ , w, cls); │
│ } │
│ }; │
│ const pres = [preFor(FRAME_W, "frame │
│ -wide"), preFor(MOBILE_FRAME_W, "frame-narro │
│ w")]; │
│ │
│ // Keep the semantic table for scree │
│ n readers; the ASCII pres │
│ // are presentation only. │
│ const original: Element = { │
│ type: "element", │
│ tagName: "table", │
│ properties: { className: ["sr-only │
│ "] }, │
│ children: node.children, │
│ }; │
│ node.tagName = "div"; │
│ node.properties = { className: ["tab │
│ le-frames"] }; │
│ node.children = [...pres, original]; │
│ return; │
│ } │
│ │
│ if (node.tagName === "blockquote" && ! │
│ hasClass(node, "callout")) { │
│ const firstP = node.children.find( │
│ (c): c is Element => c.type === "e │
│ lement" && c.tagName === "p", │
│ ); │
│ const firstText = firstP?.children[0 │
│ ]; │
│ if (!firstP || !firstText || firstTe │
│ xt.type !== "text") return; │
│ const m = firstText.value.match(CALL │
│ OUT_RE); │
│ if (!m) return; │
│ │
│ const label = m[1]; │
│ firstText.value = firstText.value.sl │
│ ice(m[0].length); │
│ if (!firstText.value) firstP.childre │
│ n.shift(); │
│ if (!firstP.children.length) node.ch │
│ ildren = node.children.filter((c) => c !== f │
│ irstP); │
│ │
│ node.properties = { className: ["cal │
│ lout"] }; │
│ node.children = [ │
│ calloutTop(label, FRAME_W, "frame- │
│ wide"), │
│ calloutTop(label, MOBILE_FRAME_W, │
│ "frame-narrow"), │
│ ...node.children, │
│ calloutBottom(FRAME_W, "frame-wide │
│ "), │
│ calloutBottom(MOBILE_FRAME_W, "fra │
│ me-narrow"), │
│ ]; │
│ return; │
│ } │
│ │
│ if (node.tagName === "p") { │
│ const kids = node.children.filter((c │
│ ) => !(c.type === "text" && !c.value.trim()) │
│ ); │
│ const img = kids[0]; │
│ if (kids.length !== 1 || img.type != │
│ = "element" || img.tagName !== "img") return │
│ ; │
│ │
│ const alt = String((img.properties a │
│ s Properties | undefined)?.alt ?? "").trim() │
│ ; │
│ const caption = alt ? `FIG. ${fig + │
│ 1} - ${alt.toUpperCase()}` : `FIG. ${fig + 1 │
│ }`; │
│ let caps: Element[]; │
│ try { │
│ caps = [ │
│ captionSpan(caption, FRAME_W, "f │
│ rame-wide"), │
│ captionSpan(caption, MOBILE_FRAM │
│ E_W, "frame-narrow"), │
│ ]; │
│ } catch (e) { │
│ // lenient: an unbreakable alt def │
│ eats the caption - leave │
│ // the image as a plain paragraph │
│ if (!lenient) throw e; │
│ return; │
│ } │
│ fig++; │
│ node.tagName = "figure"; │
│ node.properties = { className: ["fig │
│ ure-frame"] }; │
│ node.children = [ │
│ ruleSpan(FRAME_W, "frame-wide", "f │
│ ig-rule"), │
│ ruleSpan(MOBILE_FRAME_W, "frame-na │
│ rrow", "fig-rule"), │
│ img, │
│ ...caps, │
│ ruleSpan(FRAME_W, "frame-wide", "f │
│ ig-rule"), │
│ ruleSpan(MOBILE_FRAME_W, "frame-na │
│ rrow", "fig-rule"), │
│ ]; │
│ return; │
│ } │
│ │
│ if (node.tagName === "h1") { │
│ const already = node.children.some( │
│ (c) => c.type === "element" && c.t │
│ agName === "span" && hasClass(c, "head-rule" │
│ ), │
│ ); │
│ if (already) return; │
│ node.children = [ │
│ ...node.children, │
│ ruleSpan(FRAME_W, "frame-wide"), │
│ ruleSpan(MOBILE_FRAME_W, "frame-na │
│ rrow"), │
│ ]; │
│ } │
│ }); │
│ }; │
│ } │
└──────────────────────────────────────────────┘