pagez

The docs' rules, held to the cases

Pages and elements state each rule once, as a list item that opens with its id, and end it with the cases that hold it. This holds the docs and the tests to each other, so a rule can't be stated without a test, and a case can't test what no rule states.

A case is a folder in tests/pages/ or tests/sites/, or a test file. Code blocks are skipped, so an example can't state a rule.

The Errors table has a row for each id in src/, and only those, so a mistake can't ship without one. An error-… case fails with an id, the last thing in its error.txt, and the rule its row names cites the case. Each row is held by a case that fails with it or a test file that names it, and its rule cites one of them. The Warnings table has a row for each WarningId, held the same way by the warnings.txt of a case, which may warn with a Markz code too.

import { existsSync, readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { WARNINGS } from "@amitkaps/markz";
import { expect, test } from "vite-plus/test";

const root = new URL("../", import.meta.url).pathname;

/** The lines of a doc outside its code blocks. */
function lines(doc: string): string[] {
  let fenced = false;
  return readFileSync(`${root}docs/${doc}`, "utf8")
    .split("\n")
    .filter((line) => {
      if (/^(```|~~~)/.test(line)) fenced = !fenced;
      return !fenced;
    });
}

type Rule = { id: string; doc: string; cases: string[] };

/** Every rule a doc states, with the cases in the brackets that end it. */
function rules(doc: string): Rule[] {
  return lines(doc).flatMap((line) => {
    const rule = /^- \[`([a-z][a-z0-9-]*)`\]\{#\1\}: (.+)$/.exec(line);
    if (!rule) return [];
    const cited = /\(([^()]+)\)\.?$/.exec(rule[2]!)?.[1] ?? "";
    const cases = [...cited.matchAll(/`([^`]+)`/g)].map((match) => match[1]!);
    return [{ id: rule[1]!, doc, cases }];
  });
}

type Row = { id: string; rule: string; doc: string };

/** Each row of the table under `heading` in pages.md, with the rule it names. */
function table(heading: string): Row[] {
  const all = lines("pages.md");
  const from = all.indexOf(`## ${heading}`);
  const end = all.findIndex((line, i) => i > from && line.startsWith("## "));
  return all.slice(from, end === -1 ? undefined : end).flatMap((line) => {
    const row = /^\| \[`([a-z-]+)`\]\{#\1\} +\|.*\| +\[`([a-z0-9-]+)`\]\(([a-z.]*)#\2\) *\|$/.exec(
      line,
    );
    return row ? [{ id: row[1]!, rule: row[2]!, doc: row[3] || "pages.md" }] : [];
  });
}

const folders = (dir: string) =>
  readdirSync(`${root}tests/${dir}`, { withFileTypes: true })
    .filter((entry) => entry.isDirectory())
    .map((entry) => entry.name);

const cases = [...folders("pages"), ...folders("sites")];

/** Every file under `dir`, from the root. */
const under = (dir: string) =>
  readdirSync(`${root}${dir}`, { recursive: true, withFileTypes: true })
    .filter((entry) => entry.isFile())
    .map((entry) => join(entry.parentPath, entry.name).slice(root.length));

/** Each id the source fails with, written as a string, such as `"error-link-draft"`. */
const ids = new Set(
  under("src")
    .filter((file) => file.endsWith(".ts"))
    .flatMap((file) => [...readFileSync(`${root}${file}`, "utf8").matchAll(/"(error-[a-z-]+)"/g)])
    .map((match) => match[1]!),
);

/** The id each `error-…` case fails with, from its snapshot. */
const failsWith = new Map(
  cases
    .filter((name) => name.startsWith("error-"))
    .map((name) => {
      const dir = folders("pages").includes(name) ? "" : "sites/";
      const text = readFileSync(`${root}tests/snapshots/${dir}${name}/error.txt`, "utf8");
      return [name, /\[(error-[a-z-]+)\]\n$/.exec(text)?.[1]] as const;
    }),
);

/** Each `WarningId`, from its type. */
const warningIds = (
  /export type WarningId = ([^;]+);/.exec(
    readFileSync(`${root}src/elements/errors.ts`, "utf8"),
  )?.[1] ?? ""
)
  .split("|")
  .map((id) => id.trim().replace(/^"|"$/g, ""));

/** The ids each case warns with, from its snapshot. */
const warnsWith = cases.flatMap((name) => {
  const file = `${root}tests/snapshots/${name}/warnings.txt`;
  if (!existsSync(file)) return [];
  const text = readFileSync(file, "utf8");
  return [...text.matchAll(/ \[([a-z-]+)\]$/gm)].map((match) => [name, match[1]!] as const);
});

/** The test files, with their text, which may name an id. */
const testFiles = under("tests")
  .filter((file) => file.endsWith(".test.ts") && file !== "tests/docs.test.ts")
  .map((file) => ({ file, text: readFileSync(`${root}${file}`, "utf8") }));
const all = [...rules("pages.md"), ...rules("elements.md")];
const rows = table("Errors");
const warningRows = table("Warnings");

test("each rule and each error has one id across the docs", () => {
  const ids = [...all, ...rows, ...warningRows].map((row) => row.id);
  expect(ids.length).toBeGreaterThan(0);
  expect(ids.filter((id, i) => ids.indexOf(id) !== i)).toEqual([]);
});

test("no two cases share a name", () => {
  expect(new Set(cases).size).toBe(cases.length);
});

test("every rule cites a case or a test file that exists", () => {
  for (const rule of all) {
    expect(rule.cases, rule.id).not.toEqual([]);
    for (const name of rule.cases) {
      const found = name.startsWith("tests/") ? existsSync(`${root}${name}`) : cases.includes(name);
      expect(found, `${rule.id} cites ${name}`).toBe(true);
    }
  }
});

test("every case is cited by a rule", () => {
  const cited = new Set(all.flatMap((rule) => rule.cases));
  expect(cases.filter((name) => !cited.has(name))).toEqual([]);
});

const ruleOf = (row: Row) => all.find((rule) => rule.id === row.rule && rule.doc === row.doc);

test("the Errors table has a row for every id in the source, and only those", () => {
  expect(ids.size).toBeGreaterThan(0);
  expect(rows.map((row) => row.id).sort()).toEqual([...ids].sort());
});

test("each error case fails with an id, and its row's rule cites the case", () => {
  for (const [name, id] of failsWith) {
    const row = rows.find((row) => row.id === id);
    expect(row, `${name} fails with ${id}`).toBeDefined();
    expect(ruleOf(row!)?.cases, `${row!.rule} cites ${name}`).toContain(name);
  }
});

test("each error's rule is stated, and cites a case or a test file that holds the error", () => {
  for (const row of rows) {
    const rule = ruleOf(row);
    expect(rule, `${row.id} names ${row.doc}#${row.rule}`).toBeDefined();
    const holders = [
      ...[...failsWith].filter(([, id]) => id === row.id).map(([name]) => name),
      ...testFiles.filter(({ text }) => text.includes(`"${row.id}"`)).map(({ file }) => file),
    ];
    expect(holders, `${row.id} is held by a test`).not.toEqual([]);
    expect(
      rule!.cases.some((name) => holders.includes(name)),
      `${row.rule} cites one of ${holders.join(", ")}, for ${row.id}`,
    ).toBe(true);
  }
});

test("the Warnings table has a row for every WarningId, each held by a case its rule cites", () => {
  expect(warningRows.map((row) => row.id).sort()).toEqual(warningIds.toSorted());
  for (const row of warningRows) {
    const holders = warnsWith.filter(([, id]) => id === row.id).map(([name]) => name);
    expect(holders, `${row.id} is held by a case`).not.toEqual([]);
    expect(
      ruleOf(row)?.cases.some((name) => holders.includes(name)),
      row.id,
    ).toBe(true);
  }
  for (const [name, id] of warnsWith) {
    expect([...warningIds, ...Object.keys(WARNINGS)], `${name} warns with ${id}`).toContain(id);
  }
});