Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
},
];
2 changes: 1 addition & 1 deletion library.html
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ <h1>Library</h1><span class="count" id="count"></span>
</select>
</div>
<form class="lib-ask" id="ask-bar" autocomplete="off" onsubmit="return false">
<input class="lib-ask-input" id="ask-input" type="text" placeholder="Ask your library… e.g. "What's my best SSR option?"" aria-label="Ask a question about your saved repos">
<input class="lib-ask-input" id="ask-input" type="text" placeholder="Ask your library… e.g. &quot;What's my best SSR option?&quot;" aria-label="Ask a question about your saved repos">
<button class="lib-ask-btn" id="ask-btn" type="button">Ask</button>
</form>
<div class="lib-ask-answer hidden" id="ask-answer" role="region" aria-label="Ask answer" aria-live="polite"></div>
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}\""
},
Expand Down
41 changes: 41 additions & 0 deletions tools/check-html.mjs
Original file line number Diff line number Diff line change
@@ -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.`);
Loading