diff --git a/CHANGELOG.md b/CHANGELOG.md index 41ba9bee0..a55642c7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -292,6 +292,15 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md ### Changed + +- The interface asks Google Fonts for only the icon glyphs it draws, instead of + the whole variable icon font, and no longer requests a second icon family that + nothing renders. A check fails when a source names a glyph the request is + missing, because an absent glyph does not draw a placeholder — the element + renders the glyph's name as text. The check reads the panel templates as well + as the scripts, and reads glyph names written as literals, so a glyph chosen + through a variable has to be added to the request by hand. Contributed by + mml-studio (#239). - Separate explicit browser build settings from standalone environment loading and local provider middleware. Preserve provider behavior and root named exports. - Rename standalone browser startup to `src/standalone/` and add a Node-only diff --git a/index.html b/index.html index 4ab989de7..2e630c4a8 100644 --- a/index.html +++ b/index.html @@ -9,8 +9,11 @@ - - + +
diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 41dfb629b..7c192c15e 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -614,5 +614,6 @@ "src/layers/vessels/ingestion.test.mjs", "src/cityPresets.test.mjs", "server/providers/cctv/range.js", - "src/data/cctvMediaRange.test.mjs" + "src/data/cctvMediaRange.test.mjs", + "src/materialSymbolsSubset.test.mjs" ] diff --git a/src/materialSymbolsSubset.test.mjs b/src/materialSymbolsSubset.test.mjs new file mode 100644 index 000000000..ec9ffe2f0 --- /dev/null +++ b/src/materialSymbolsSubset.test.mjs @@ -0,0 +1,111 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync, readdirSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const SRC_ROOT = fileURLToPath(new URL('.', import.meta.url)); +const REPO_ROOT = path.resolve(SRC_ROOT, '..'); +const INDEX_HTML = path.join(REPO_ROOT, 'index.html'); + +/** The glyph written as element text: `radar`. */ +const SPAN_TEXT = + /class="[^"]*material-symbols-outlined[^"]*"[^>]*>\s*([a-z0-9_]+)\s*} glyph -> the first file that names it. + */ +function referencedGlyphs() { + const found = new Map(); + for (const file of sourceFiles()) { + const source = readFileSync(file, 'utf8'); + const relative = path.relative(REPO_ROOT, file).split(path.sep).join('/'); + const add = (glyph) => { + if (!found.has(glyph)) found.set(glyph, relative); + }; + for (const match of source.matchAll(SPAN_TEXT)) add(match[1]); + for (const match of source.matchAll(TEXT_ASSIGNMENT)) { + const statement = match[1].replaceAll('?.', '.'); + const assigned = statement.includes('?') + ? statement.slice(statement.indexOf('?') + 1) + : statement; + for (const literal of assigned.matchAll(STRING_LITERAL)) add(literal[1]); + } + } + return found; +} + +/** The `icon_names` list index.html asks Google for. */ +function subsettedGlyphs(html = readFileSync(INDEX_HTML, 'utf8')) { + const match = + /Material\+Symbols\+Outlined[^"]*[?&]icon_names=([a-z0-9_,]+)/.exec(html); + assert.ok( + match, + 'index.html must request Material Symbols with an icon_names subset', + ); + return new Set(match[1].split(',')); +} + +test('every glyph the sources render is in the icon_names subset', () => { + const subset = subsettedGlyphs(); + const missing = [...referencedGlyphs()] + .filter(([glyph]) => !subset.has(glyph)) + .map(([glyph, file]) => `${glyph} (${file})`); + + assert.deepEqual( + missing, + [], + 'Glyphs named by the sources but absent from the index.html icon_names list. ' + + 'Add them there — an unlisted glyph renders as its own name on screen: ' + + missing.join(', '), + ); +}); + +test('the unused Material Icons Round family is not loaded', () => { + // A second icon font, 173 kB, for a family no source ever uses — and + // src/cockpitMarkup.test.mjs already asserts the markup must not use it. + const html = readFileSync(INDEX_HTML, 'utf8'); + assert.doesNotMatch( + html, + /Material\+Icons\+Round/, + 'index.html loads an icon font nothing renders', + ); +});