// Builds firmware/follow-me-wizard.h: the setup wizard as one gzipped page // for the sensor to serve at /followme. // // node wizard/tools/embed.mjs // // Inlines every url(fonts/) in wizard/wizard.html as a data: URI, // drops tags to Google Fonts (unreachable offline), marks the page as // served by the device (), gzips it at level // 9 and writes it as a C++ byte array. Exits non-zero if a referenced font // file is missing or a Google Fonts reference remains. import { readFileSync, writeFileSync, existsSync } from 'node:fs'; import { dirname, join, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; import { gzipSync, constants } from 'node:zlib'; const WIZARD_DIR = join(dirname(fileURLToPath(import.meta.url)), '..'); const ROOT = join(WIZARD_DIR, '..'); const SRC = join(WIZARD_DIR, 'wizard.html'); const OUT = join(ROOT, 'firmware', 'follow-me-wizard.h'); const MIME = { woff2: 'font/woff2', woff: 'font/woff', ttf: 'font/ttf', otf: 'font/otf' }; function fail(msg) { console.error(`embed: ${msg}`); process.exit(1); } let html = readFileSync(SRC, 'utf8'); // Google Fonts tags (stylesheet and preconnect). html = html.replace(/[ \t]*]*fonts\.(?:googleapis|gstatic)\.com[^>]*>[ \t]*\r?\n?/gi, ''); if (/fonts\.(?:googleapis|gstatic)\.com/i.test(html)) fail('wizard.html still references Google Fonts outside a tag'); // url(fonts/), quoted or not. const fonts = new Set(); html = html.replace(/url\(\s*(['"]?)(?:\.\/)?fonts\/([^'")\s]+)\1\s*\)/g, (_, _q, file) => { const path = join(WIZARD_DIR, 'fonts', file); if (!existsSync(path)) fail(`font file missing: ${relative(ROOT, path)}`); const ext = file.split('.').pop().toLowerCase(); if (!MIME[ext]) fail(`unknown font type: ${file}`); fonts.add(file); return `url(data:${MIME[ext]};base64,${readFileSync(path).toString('base64')})`; }); const META = ''; const charset = //i; if (!charset.test(html)) fail('wizard.html has no '); html = html.replace(charset, (m) => `${m}\n${META}`); const raw = Buffer.from(html, 'utf8'); const gz = gzipSync(raw, { level: constants.Z_BEST_COMPRESSION }); const lines = []; for (let i = 0; i < gz.length; i += 16) lines.push(' ' + [...gz.subarray(i, i + 16)].map((b) => '0x' + b.toString(16).padStart(2, '0')).join(', ') + ','); const header = `#pragma once // Generated by wizard/tools/embed.mjs; do not edit. Regenerate after changing // wizard/wizard.html or wizard/fonts: node wizard/tools/embed.mjs // // The setup wizard page with its fonts inlined, gzipped (${raw.length} bytes raw). #include #include namespace follow_me { static const uint8_t WIZARD_HTML_GZ[] = { ${lines.join('\n')} }; static const size_t WIZARD_HTML_GZ_LEN = ${gz.length}; } // namespace follow_me `; writeFileSync(OUT, header); console.log(`fonts inlined: ${fonts.size ? [...fonts].join(', ') : 'none'}`); console.log(`raw ${raw.length} bytes, gzipped ${gz.length} bytes -> ${relative(ROOT, OUT)}`);