// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT // Draws the syntax diagram at the top of a sequence page: each piece of the // sequence as a tile with its name on top and its bytes underneath. The // page carries the sequence as text in `data-sequence`, so it reads fine // with no script at all. const C0 = [ "NUL", "SOH", "STX", "ETX", "EOT", "ENQ", "ACK", "BEL", "BS", "HT", "LF", "VT", "FF", "CR", "SO", "SI", "DLE", "DC1", "DC2", "DC3", "DC4", "NAK", "SYN", "ETB", "CAN", "EM", "SUB", "ESC", "FS", "GS", "RS", "US", ]; const INTRODUCERS = { CSI: { bytes: [0x1b, 0x5b], title: "Control Sequence Introducer" }, OSC: { bytes: [0x1b, 0x5d], title: "Operating System Command" }, DCS: { bytes: [0x1b, 0x50], title: "Device Control String" }, ST: { bytes: [0x1b, 0x5c], title: "String Terminator" }, }; // Only the names that mean the same thing wherever they appear. Pc is a // column to CUP, a color to OSC 4 and a selection to OSC 52, and Pl a line // to CUP, a level to DECSCL and a lock to DECUDK, so those are left to the // page. const PARAMETERS = { n: "a number", s: "a number selecting a behavior", t: "text", m: "a list of numbers", }; // DEC's edges, as a rectangle (DECCARA's Pt ; Pl ; Pb ; Pr) or as margins // (DECSTBM's Pt ; Pb, DECSLRM's Pl ; Pr), where Pt is the top line rather // than text. const EDGES = { Pt: "the top line", Pl: "the left column", Pb: "the bottom line", Pr: "the right column", }; // `parameters` names words to draw as parameters whatever they look like: // a single lowercase letter is otherwise the byte itself. export function tokenize(text, parameters = []) { const edges = /\bPt ; (Pl ; )?Pb\b|\bPl ; Pr\b/.test(text); return text.trim().split(/\s+/).map((word) => { if (parameters.includes(word)) return { kind: "param", label: word, bytes: [], title: `${word}: as the page describes` }; if (word in INTRODUCERS) return { kind: "intro", label: word, ...INTRODUCERS[word] }; const c0 = C0.indexOf(word); if (c0 >= 0) return { kind: "control", label: word, bytes: [c0], title: `C0 control ${word}` }; if (word === "DEL") return { kind: "control", label: word, bytes: [0x7f], title: "Delete" }; if (word === "SP") return { kind: "control", label: word, bytes: [0x20], title: "a space, here an intermediate byte" }; if (word === "..." || word === "…") return { kind: "more", label: "…", bytes: [], title: "repeated" }; // DEC writes a reply's hexadecimal digits as D…D, as in DECCKSR. if (/^D(…|\.\.\.)D$/.test(word)) return { kind: "param", label: "D…D", bytes: [], title: "hexadecimal digits" }; // A choice of final byte, as in a mode's `h/l`: one tile showing both. // Only between single characters, so that DECUDK's `Ky1/St1` stays as // it is. const choice = /^(.)\/(.)$/.exec(word); if (choice) { const [, a, b] = choice; const title = word === "h/l" ? "h sets the mode, l resets it" : `either ${a} or ${b}`; return { kind: "literal", label: word, bytes: [], alternatives: [a, b].map((ch) => [ch.charCodeAt(0)]), title }; } // A word of two or more lowercase letters names what goes there: // ReGIS's `commands`, an OSC's `text` or `payload`, `key=value`, sixel's // `s…s`. A single letter, such as the `p` that ends ReGIS's // introducer, is the byte itself. if (/^[a-z][a-z…=]*[a-z…]$/.test(word)) return { kind: "param", label: word, bytes: [], title: `${word}: as the page describes` }; const param = /^P([a-z0-9]+)$/.exec(word); if (param) { if (edges && word in EDGES) return { kind: "param", label: word, bytes: [], title: `${word}: ${EDGES[word]}` }; return { kind: "param", label: word, bytes: [], title: `${word}: ${PARAMETERS[param[1]] ?? "as the page describes"}` }; } // DEC's names for the parts of a string, such as DECDLD's Dscs and // Sxbp1 or DECUDK's Ky1/St1: a capital, then lowercase letters and digits. if (/^[A-Z][a-z][a-z0-9]*(\/[A-Z][a-z][a-z0-9]*)?$/.test(word)) { return { kind: "param", label: word, bytes: [], title: `${word}: as the page describes` }; } return { kind: "literal", label: word, bytes: Array.from(new TextEncoder().encode(word)), title: `the characters ${word}` }; }); } function hex(bytes) { return bytes.map((b) => b.toString(16).toUpperCase().padStart(2, "0")).join(" "); } function render(figure) { const tokens = tokenize(figure.dataset.sequence, (figure.dataset.parameters ?? "").split(/\s+/).filter(Boolean)); const list = document.createElement("ol"); list.className = "wire"; for (const t of tokens) { const li = document.createElement("li"); li.className = `tile ${t.kind}`; li.title = t.title; const label = document.createElement("span"); label.className = "label"; label.textContent = t.label; const bytes = document.createElement("span"); bytes.className = "hex"; bytes.textContent = t.alternatives ? t.alternatives.map(hex).join(" / ") : t.kind === "param" ? "· · ·" : t.kind === "more" ? "" : hex(t.bytes); li.append(label, bytes); list.append(li); } figure.replaceChildren(list); figure.setAttribute("aria-label", `Syntax: ${figure.dataset.sequence}`); } if (typeof document !== "undefined") { for (const figure of document.querySelectorAll("figure.syntax[data-sequence]")) render(figure); }