diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 855acc6..1dfd3ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,10 @@ jobs: run: npm test - name: Lint run: npm run lint + # Blocking: catches structural HTML errors (e.g. an unescaped " that + # terminates an attribute) that node --check + vitest + eslint can't see. + - name: HTML parse check + run: npm run check:html - name: Format check (advisory) run: npm run format:check continue-on-error: true diff --git a/eslint.config.js b/eslint.config.js index 7b0b7a7..ab1cfc8 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -28,4 +28,10 @@ export default [ files: ['tests/**/*.js'], languageOptions: { globals: { ...globals.node, ...globals.browser } }, }, + { + // CLI tooling (e.g. the HTML parse gate) reports via stdout/stderr by design. + files: ['tools/**/*.{js,mjs}'], + languageOptions: { globals: { ...globals.node } }, + rules: { 'no-console': 'off' }, + }, ]; diff --git a/library.html b/library.html index ce71db4..87deb4e 100644 --- a/library.html +++ b/library.html @@ -613,7 +613,7 @@

Library

- +
diff --git a/package.json b/package.json index 1635850..274145d 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "test:coverage": "vitest run --coverage", "lint": "eslint .", "lint:fix": "eslint . --fix", + "check:html": "node tools/check-html.mjs", "format": "prettier --write \"**/*.{js,json,css,html,md}\"", "format:check": "prettier --check \"**/*.{js,json,css,html,md}\"" }, diff --git a/tools/check-html.mjs b/tools/check-html.mjs new file mode 100644 index 0000000..3c48196 --- /dev/null +++ b/tools/check-html.mjs @@ -0,0 +1,41 @@ +#!/usr/bin/env node +// HTML parse-validity gate. +// +// Fails ONLY on a true parse error (e.g. an unescaped quote that terminates an +// attribute early), NOT on formatting drift — that stays advisory via the +// `format:check` script. This closes the blind spot that let attribute-quote +// bugs ship in options.html and library.html past node --check + vitest + eslint +// (none of which look at HTML). +// +// Uses prettier's parser via its Node API: prettier.format() throws on a parse +// error and merely reformats on style differences, so a thrown error is a real +// structural problem, not a whitespace nit. + +import prettier from 'prettier'; +import { readFileSync } from 'node:fs'; +import { execSync } from 'node:child_process'; + +const files = execSync('git ls-files "*.html"', { encoding: 'utf8' }) + .trim() + .split('\n') + .filter(Boolean); + +const failures = []; +for (const file of files) { + try { + await prettier.format(readFileSync(file, 'utf8'), { parser: 'html', filepath: file }); + } catch (err) { + failures.push({ file, message: String(err.message).split('\n')[0] }); + } +} + +if (failures.length > 0) { + console.error(`HTML parse check FAILED — ${failures.length} file(s) with structural errors:\n`); + for (const { file, message } of failures) { + console.error(` ${file}: ${message}`); + } + console.error('\nThese are real parse errors (often an unescaped " inside an attribute). Fix the markup.'); + process.exit(1); +} + +console.log(`HTML parse check passed — ${files.length} file(s) parse cleanly.`);