Skip to content

Commit 6b635ae

Browse files
staredclaude
andcommitted
Add strict build-time content validation
Enforce content correctness at build time with clear error/warning rules. Invalid content now fails the build instead of showing errors in the browser. Validation rules: - ERROR (build fails): Terms used in description but not marked in equation - ERROR (build fails): Terms marked in equation but missing definitions - WARNING (build succeeds): Terms marked but not used in description - WARNING (build succeeds): Definitions exist but term not marked Implementation: - Track equationTerms and descriptionTerms separately in parser - Throw errors for validation failures - Add build-time validation script (scripts/validate-content.ts) - Run validation before TypeScript compilation in build process - Add tsx as dev dependency for running validation script Testing: - Valid content builds successfully with warnings shown - Invalid content fails build with clear error messages - Build process exits with code 1 on validation errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 9c32d17 commit 6b635ae

File tree

5 files changed

+101
-22
lines changed

5 files changed

+101
-22
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"type": "module",
66
"scripts": {
77
"dev": "vite",
8-
"build": "tsc && vite build",
8+
"validate": "tsx scripts/validate-content.ts",
9+
"build": "pnpm validate && tsc && vite build",
910
"preview": "vite preview",
1011
"lint": "eslint ."
1112
},
@@ -23,6 +24,7 @@
2324
"@typescript-eslint/eslint-plugin": "^8.46.2",
2425
"@typescript-eslint/parser": "^8.46.2",
2526
"eslint": "^9.39.0",
27+
"tsx": "^4.20.6",
2628
"typescript": "^5.9.3",
2729
"vite": "^7.1.12"
2830
},

pnpm-lock.yaml

Lines changed: 30 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/validate-content.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env node
2+
// Build-time content validation script
3+
import { readFileSync } from 'fs';
4+
import { parseContent } from '../src/parser.js';
5+
6+
console.log('Validating content.md...');
7+
8+
try {
9+
const content = readFileSync('./public/content.md', 'utf-8');
10+
const parsed = parseContent(content);
11+
console.log(`✓ Validation passed: ${parsed.termOrder.length} terms found`);
12+
process.exit(0);
13+
} catch (error) {
14+
console.error('✗ Validation failed:');
15+
if (error instanceof Error) {
16+
console.error(error.message);
17+
} else {
18+
console.error(error);
19+
}
20+
process.exit(1);
21+
}

src/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,5 +170,7 @@ document.addEventListener('DOMContentLoaded', async () => {
170170
setupHoverEffects();
171171
} catch (error) {
172172
console.error('Failed to load content:', error);
173+
// Re-throw to fail the build
174+
throw error;
173175
}
174176
});

src/parser.ts

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ export function parseContent(markdown: string): ParsedContent {
2121
let description = '';
2222
const definitions = new Map<string, string>();
2323
const termOrder: string[] = [];
24-
const seenTerms = new Set<string>();
24+
const equationTerms = new Set<string>(); // Terms marked in equation with \mark
25+
const descriptionTerms = new Set<string>(); // Terms used in description with [text]{.class}
26+
const seenTerms = new Set<string>(); // All terms seen (for color ordering)
2527

2628
let inEquation = false;
2729
let inDescription = false;
@@ -47,6 +49,7 @@ export function parseContent(markdown: string): ParsedContent {
4749
// Convert \mark[class]{latex} to \htmlClass{term-class}{latex}
4850
const converted = line.replace(/\\mark\[([^\]]+)\]\{/g, (_match, className) => {
4951
const termClass = `term-${className}`;
52+
equationTerms.add(className); // Track equation terms
5053
if (!seenTerms.has(className)) {
5154
termOrder.push(className);
5255
seenTerms.add(className);
@@ -81,6 +84,7 @@ export function parseContent(markdown: string): ParsedContent {
8184
// Convert [text]{.class} to <span class="term-class">text</span>
8285
const converted = line.replace(/\[([^\]]+)\]\{\.([^\}]+)\}/g, (_match, text, className) => {
8386
const termClass = `term-${className}`;
87+
descriptionTerms.add(className); // Track description terms
8488
if (!seenTerms.has(className)) {
8589
termOrder.push(className);
8690
seenTerms.add(className);
@@ -102,34 +106,56 @@ export function parseContent(markdown: string): ParsedContent {
102106
definitions.set(currentDefClass, currentDefContent.join('\n').trim());
103107
}
104108

105-
// Validate that all terms have definitions
106-
const missingDefinitions: string[] = [];
107-
for (const term of termOrder) {
109+
// VALIDATION: Build-time checks
110+
const errors: string[] = [];
111+
const warnings: string[] = [];
112+
113+
// ERROR: Terms used in description but not marked in equation
114+
for (const term of descriptionTerms) {
115+
if (!equationTerms.has(term)) {
116+
errors.push(
117+
`Term "${term}" is used in description [text]{.${term}} but not marked in equation with \\mark[${term}]{...}`
118+
);
119+
}
120+
}
121+
122+
// ERROR: Terms marked in equation but have no definition
123+
for (const term of equationTerms) {
108124
if (!definitions.has(term)) {
109-
missingDefinitions.push(term);
125+
errors.push(
126+
`Term "${term}" is marked in equation with \\mark[${term}]{...} but has no definition (## .${term})`
127+
);
110128
}
111129
}
112130

113-
if (missingDefinitions.length > 0) {
114-
console.warn(
115-
'Warning: The following terms are referenced but have no definitions:',
116-
missingDefinitions
117-
);
131+
// WARNING: Terms marked in equation but not used in description
132+
for (const term of equationTerms) {
133+
if (!descriptionTerms.has(term)) {
134+
warnings.push(
135+
`Term "${term}" is marked in equation but not used in description`
136+
);
137+
}
118138
}
119139

120-
// Optionally warn about unused definitions
121-
const unusedDefinitions: string[] = [];
140+
// WARNING: Definitions exist but term never marked in equation
122141
for (const defTerm of definitions.keys()) {
123-
if (!seenTerms.has(defTerm)) {
124-
unusedDefinitions.push(defTerm);
142+
if (!equationTerms.has(defTerm)) {
143+
warnings.push(
144+
`Definition "## .${defTerm}" exists but term is never marked in equation`
145+
);
125146
}
126147
}
127148

128-
if (unusedDefinitions.length > 0) {
129-
console.warn(
130-
'Warning: The following definitions are not referenced in equation or description:',
131-
unusedDefinitions
132-
);
149+
// Throw errors if any validation failed
150+
if (errors.length > 0) {
151+
const errorMessage = 'Content validation failed:\n' + errors.map(e => ` - ${e}`).join('\n');
152+
throw new Error(errorMessage);
153+
}
154+
155+
// Log warnings
156+
if (warnings.length > 0) {
157+
console.warn('Content validation warnings:');
158+
warnings.forEach(w => console.warn(` - ${w}`));
133159
}
134160

135161
return {

0 commit comments

Comments
 (0)