Something went wrong. Try again.
Web frontend and supporting services for lance.blue
Something went wrong. Try again.
3.4 kB · 93 lines
JavaScript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394/** * A paragraph in the content files is an array, and the gaps between its * pieces are real. * * about.json and faq.json build a paragraph out of runs — bare strings, and * {a, href} or {b} objects that become elements. render.ts appends them with * no separator, so the space between a link and the word after it has to be * written into one of the strings. Miss it and nothing errors: the file is * still valid JSON, tsc is still happy, and the page renders "ATprotothe data * layer". Two of those shipped. * * So flatten every run the way the renderer does and read the seams. The * placeholder copy is plain strings with no markup in it, so it has no seams * and this says nothing about it. * * Run with `npm test`. */import { test } from "node:test";import assert from "node:assert/strict";
import about from "../src/content/about.json" with { type: "json" };import faq from "../src/content/faq.json" with { type: "json" };
const files = { "about.json": about, "faq.json": faq };
/** The text of one run, which is what the renderer puts in the document. */function textOf(run) { if (typeof run === "string") return run; if (run && typeof run === "object") { if (typeof run.b === "string") return run.b; if (typeof run.a === "string") return run.a; } return null;}
/** Is this array a paragraph's runs, rather than a list of blocks? */function isRunArray(value) { return ( Array.isArray(value) && value.length > 0 && value.every((run) => textOf(run) !== null) );}
/** * Every run array in a content file, wherever it sits: `lead`, a `blurb`, a * `p`, an entry of a `list`. Walking the whole tree rather than the schema * means copy added in a new shape is covered the day it lands. */function runArrays(value, path, found = []) { if (isRunArray(value)) found.push({ path, runs: value }); else if (Array.isArray(value)) { value.forEach((item, i) => runArrays(item, `${path}[${i}]`, found)); } else if (value && typeof value === "object") { for (const [key, item] of Object.entries(value)) { runArrays(item, `${path}.${key}`, found); } } return found;}
test("no run in a paragraph runs into the next one", () => { for (const [name, content] of Object.entries(files)) { for (const { path, runs } of runArrays(content, name)) { for (let i = 0; i < runs.length - 1; i++) { const left = textOf(runs[i]); const right = textOf(runs[i + 1]); // Punctuation either side of the seam is deliberate — a full stop // after a link, an opening bracket before one. Two word characters // touching is the missing space. const collides = /\w$/.test(left) && /^\w/.test(right); assert.ok( !collides, `${path}: "${left.slice(-24)}" and "${right.slice(0, 24)}" ` + `render with nothing between them`, ); } } }});
test("no paragraph has a doubled space or a space before punctuation", () => { for (const [name, content] of Object.entries(files)) { for (const { path, runs } of runArrays(content, name)) { const flat = runs.map(textOf).join(""); assert.ok(!/ {2}/.test(flat), `${path}: doubled space in "${flat}"`); assert.ok( !/\s[,.;:!?]/.test(flat), `${path}: space before punctuation in "${flat}"`, ); } }});