….
// Loose textalso supported. const codeChild = el.querySelector("code"); const inner = codeChild ?? el; const lang = parseLangFromClass(inner.getAttribute("class") ?? ""); return { t: "code", lang, src: decodeEntities(inner.innerHTML) }; }; const parseImg = (el: HTMLElement): SxBlock | null => { const src = el.getAttribute("src") ?? ""; if (!src) return null; const block: { t: "img"; src: string; alt?: string; w?: number; h?: number } = { t: "img", src }; const alt = el.getAttribute("alt"); if (alt) block.alt = alt; const w = numAttr(el, "width"); if (w !== undefined) block.w = w; const h = numAttr(el, "height"); if (h !== undefined) block.h = h; return block as SxBlock; }; const parseFigure = (el: HTMLElement): SxBlock => { const img = el.querySelector("img"); const caption = el.querySelector("figcaption"); if (img) { const src = img.getAttribute("src") ?? ""; if (src) { const block: { t: "img"; src: string; alt?: string; caption?: string; w?: number; h?: number } = { t: "img", src }; const alt = img.getAttribute("alt"); if (alt) block.alt = alt; if (caption) block.caption = caption.text; const w = numAttr(img, "width"); if (w !== undefined) block.w = w; const h = numAttr(img, "height"); if (h !== undefined) block.h = h; return block as SxBlock; } } return { t: "html", src: el.outerHTML }; }; // ─── inline parsing ────────────────────────────────────────────────────── const parseInline = (nodes: Node[] | undefined, marks: SxMark[]): SxInline[] => { if (!nodes) return []; const out: SxInline[] = []; for (const node of nodes) { if (node.nodeType === NodeType.TEXT_NODE) { const v = decodeEntities(node.text ?? ""); if (v.length > 0) { out.push({ t: "text", v, ...(marks.length ? { m: dedupeMarks(marks) } : {}) }); } continue; } if (node.nodeType !== NodeType.ELEMENT_NODE) continue; const el = node as HTMLElement; const tag = el.tagName?.toLowerCase(); if (!tag) continue; if (tag === "br") { out.push({ t: "text", v: "\n", ...(marks.length ? { m: dedupeMarks(marks) } : {}) }); continue; } if (tag === "a") { const href = el.getAttribute("href") ?? ""; out.push({ t: "a", href, c: parseInline(el.childNodes, marks) }); continue; } const mark = MARK_FOR_TAG[tag]; if (mark) { out.push(...parseInline(el.childNodes, [...marks, mark])); continue; } // , , etc. — strip wrapper, keep contents. out.push(...parseInline(el.childNodes, marks)); } return out; }; const dedupeMarks = (marks: SxMark[]): SxMark[] => { const seen = new Set
contains [[sx:foo]] tokens mixed with text, split it into
// (paragraph)(shortcode)(paragraph) blocks so the document is queryable
// per-shortcode rather than per-paragraph-with-substring.
const splitShortcodesFromParagraph = (inlines: SxInline[]): SxBlock[] => {
const out: SxBlock[] = [];
let buf: SxInline[] = [];
const flush = (): void => {
if (buf.length > 0 && buf.some((i) => !(i.t === "text" && i.v.trim() === ""))) {
out.push({ t: "p", c: buf });
}
buf = [];
};
for (const i of inlines) {
if (i.t !== "text" || !SHORTCODE_RE.test(i.v)) {
buf.push(i);
continue;
}
SHORTCODE_RE.lastIndex = 0;
const blocks = textWithShortcodesToBlocks(i.v, i.m ?? []);
for (const b of blocks) {
if (b.t === "shortcode") {
flush();
out.push(b);
} else if (b.t === "p") {
for (const inner of b.c) buf.push(inner);
}
}
}
flush();
return out;
};
const textWithShortcodesToBlocks = (text: string, marks: SxMark[]): SxBlock[] => {
const out: SxBlock[] = [];
let last = 0;
SHORTCODE_RE.lastIndex = 0;
for (const m of text.matchAll(SHORTCODE_RE)) {
const idx = m.index ?? 0;
if (idx > last) {
const before = text.slice(last, idx);
if (before.trim() !== "") {
out.push({ t: "p", c: [{ t: "text", v: before, ...(marks.length ? { m: marks } : {}) }] });
}
}
const name = m[1]!;
const args: Record