Gateprep / scripts /build-katex-inline.mjs
banu4prasad's picture
feat: improve result download
392f1be
Raw History Blame Contribute Delete
6.21 kB
#!/usr/bin/env node
/**
* scripts/build-katex-inline.mjs
*
* One-time generation script to produce 100% self-contained KaTeX & branded font assets
* for server-side HTML result report rendering.
*
* It:
* 1. Reads KaTeX version from frontend/package.json.
* 2. Inlines all KaTeX woff2 font files into backend/app/static/katex/katex-inline.css as base64 data URIs.
* 3. Strips unused relative .woff and .ttf fallback references so no broken relative paths remain.
* 4. Copies katex.min.js and auto-render.min.js verbatim to backend/app/static/katex/.
* 5. Inlines all branded fonts (Syne, DM Sans, JetBrains Mono) into backend/app/static/fonts/fonts-inline.css.
*
* Note on Version-Drift:
* If the katex dependency in frontend/package.json is updated, this script must be re-run
* to maintain version parity. Automated tests verify this header in backend/tests/test_result_html.py.
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootDir = path.resolve(__dirname, '..');
const frontendPkgPath = path.join(rootDir, 'frontend', 'package.json');
const katexDistDir = path.join(rootDir, 'frontend', 'node_modules', 'katex', 'dist');
const katexFontsDir = path.join(katexDistDir, 'fonts');
const katexCssPath = path.join(katexDistDir, 'katex.min.css');
const katexJsPath = path.join(katexDistDir, 'katex.min.js');
const katexAutoRenderJsPath = path.join(katexDistDir, 'contrib', 'auto-render.min.js');
const backendStaticKatexDir = path.join(rootDir, 'backend', 'app', 'static', 'katex');
const backendStaticFontsDir = path.join(rootDir, 'backend', 'app', 'static', 'fonts');
const fontsCssPath = path.join(rootDir, 'frontend', 'src', 'fonts.css');
const publicFontsDir = path.join(rootDir, 'frontend', 'public', 'fonts');
// 1. Verify source files exist
if (!fs.existsSync(frontendPkgPath)) {
console.error(`Error: frontend/package.json not found at ${frontendPkgPath}`);
process.exit(1);
}
if (!fs.existsSync(katexCssPath)) {
console.error(`Error: katex.min.css not found at ${katexCssPath}`);
process.exit(1);
}
if (!fs.existsSync(fontsCssPath)) {
console.error(`Error: fonts.css not found at ${fontsCssPath}`);
process.exit(1);
}
const frontendPkg = JSON.parse(fs.readFileSync(frontendPkgPath, 'utf8'));
const katexVersionRaw = frontendPkg.dependencies?.katex || '0.17.0';
const katexVersion = katexVersionRaw.replace(/^[\^~]/, '');
console.log(`Building airtight inline assets for KaTeX v${katexVersion}...`);
// Ensure destination directories exist
fs.mkdirSync(backendStaticKatexDir, { recursive: true });
fs.mkdirSync(backendStaticFontsDir, { recursive: true });
// 2. Build katex-inline.css with base64 embedded woff2 fonts and stripped dead fallbacks
let katexCss = fs.readFileSync(katexCssPath, 'utf8');
// First: Strip dead relative .woff and .ttf fallback sources (e.g. ",url(fonts/...woff) format("woff")")
katexCss = katexCss.replace(/,url\((['"]?)(?:fonts\/)?[^'")]+?\.(?:woff|ttf)(['"]?)\)\s*format\((['"]?)(?:woff|truetype)(['"]?)\)/g, '');
// Second: Replace remaining url(fonts/...woff2) with base64 data URIs
katexCss = katexCss.replace(/url\((['"]?)(?:fonts\/)?([^'")]+?\.woff2)(['"]?)\)/g, (match, q1, fontFileName) => {
const cleanName = path.basename(fontFileName);
const fontFilePath = path.join(katexFontsDir, cleanName);
if (fs.existsSync(fontFilePath)) {
const fontBase64 = fs.readFileSync(fontFilePath).toString('base64');
return `url("data:font/woff2;base64,${fontBase64}")`;
}
console.warn(`Warning: KaTeX font file not found: ${fontFilePath}`);
return match;
});
// Verification check: ensure no relative url(...) remains in KaTeX CSS
const remainingRelativeUrls = katexCss.match(/url\((?!['"]?data:)[^)]+\)/g);
if (remainingRelativeUrls) {
console.warn('Warning: Remaining relative URLs in KaTeX CSS:', remainingRelativeUrls);
} else {
console.log('βœ“ Verified: 0 relative URLs remain in katex-inline.css (100% self-contained data URIs).');
}
// Add KaTeX version header for drift detection
const katexInlineCssContent = `/* KaTeX v${katexVersion} (inlined fonts) */\n` + katexCss;
const outputKatexCssPath = path.join(backendStaticKatexDir, 'katex-inline.css');
fs.writeFileSync(outputKatexCssPath, katexInlineCssContent, 'utf8');
console.log(`βœ“ Generated ${outputKatexCssPath} (${(katexInlineCssContent.length / 1024).toFixed(1)} KB)`);
// 3. Copy katex.min.js and auto-render.min.js
const outputKatexJsPath = path.join(backendStaticKatexDir, 'katex.min.js');
fs.copyFileSync(katexJsPath, outputKatexJsPath);
console.log(`βœ“ Copied katex.min.js to ${outputKatexJsPath}`);
const outputAutoRenderJsPath = path.join(backendStaticKatexDir, 'auto-render.min.js');
fs.copyFileSync(katexAutoRenderJsPath, outputAutoRenderJsPath);
console.log(`βœ“ Copied auto-render.min.js to ${outputAutoRenderJsPath}`);
// 4. Build fonts-inline.css with base64 embedded woff2 fonts for Syne & DM Sans
let fontsCss = fs.readFileSync(fontsCssPath, 'utf8');
fontsCss = fontsCss.replace(/url\((['"]?)(?:\/fonts\/|fonts\/)?([^'")]+?\.woff2)(['"]?)\)/g, (match, q1, fontFileName) => {
const cleanName = path.basename(fontFileName);
const fontFilePath = path.join(publicFontsDir, cleanName);
if (fs.existsSync(fontFilePath)) {
const fontBase64 = fs.readFileSync(fontFilePath).toString('base64');
return `url("data:font/woff2;base64,${fontBase64}")`;
}
console.warn(`Warning: Brand font file not found: ${fontFilePath}`);
return match;
});
const remainingBrandRelativeUrls = fontsCss.match(/url\((?!['"]?data:)[^)]+\)/g);
if (remainingBrandRelativeUrls) {
console.warn('Warning: Remaining relative URLs in fonts CSS:', remainingBrandRelativeUrls);
} else {
console.log('βœ“ Verified: 0 relative URLs remain in fonts-inline.css (100% self-contained data URIs).');
}
const outputFontsCssPath = path.join(backendStaticFontsDir, 'fonts-inline.css');
fs.writeFileSync(outputFontsCssPath, fontsCss, 'utf8');
console.log(`βœ“ Generated ${outputFontsCssPath} (${(fontsCss.length / 1024).toFixed(1)} KB)`);
console.log('βœ“ All inline assets built successfully.');