ucode 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -0
  3. data/Gemfile.lock +2 -2
  4. data/TODO.full/00-README.md +116 -0
  5. data/TODO.full/01-panglyph-vision.md +112 -0
  6. data/TODO.full/02-panglyph-repo-bootstrap.md +184 -0
  7. data/TODO.full/03-panglyph-font-builder.md +201 -0
  8. data/TODO.full/04-panglyph-publish-pipeline.md +126 -0
  9. data/TODO.full/05-ucode-0-1-1-release.md +139 -0
  10. data/TODO.full/06-fontisan-remove-audit.md +142 -0
  11. data/TODO.full/07-fontisan-remove-ucd.md +125 -0
  12. data/TODO.full/08-archive-private-bin-build.md +143 -0
  13. data/TODO.full/09-archive-public-structure.md +164 -0
  14. data/TODO.full/10-fontist-org-woff-glyphs.md +131 -0
  15. data/TODO.full/11-fontist-org-audit-coverage.md +140 -0
  16. data/TODO.full/12-implementation-order.md +216 -0
  17. data/TODO.full/13-fontisan-font-writer-api.md +189 -0
  18. data/TODO.full/14-fontisan-table-writers.md +66 -0
  19. data/TODO.full/15-panglyph-builder-real.md +82 -0
  20. data/TODO.full/16-archive-public-sync-workflows.md +167 -0
  21. data/TODO.full/17-fontist-org-font-picker.md +73 -0
  22. data/TODO.full/18-comprehensive-spec-coverage.md +64 -0
  23. data/TODO.full/19-ucode-0-1-2-patch.md +32 -0
  24. data/TODO.full/20-fontisan-0-2-23-release.md +52 -0
  25. data/TODO.new/00-README.md +30 -0
  26. data/TODO.new/23-universal-glyph-set-source-map.md +312 -0
  27. data/TODO.new/24-universal-glyph-set-build.md +189 -0
  28. data/TODO.new/25-font-audit-against-universal-set.md +195 -0
  29. data/TODO.new/26-missing-glyph-reporter.md +189 -0
  30. data/TODO.new/27-fontist-org-consumer-integration.md +200 -0
  31. data/TODO.new/28-implementation-order-update.md +187 -0
  32. data/TODO.new/29-universal-set-curation-uc17.md +312 -0
  33. data/TODO.new/30-tier1-font-acquisition.md +241 -0
  34. data/TODO.new/31-universal-set-production-build.md +205 -0
  35. data/TODO.new/32-uc17-coverage-matrix.md +165 -0
  36. data/TODO.new/33-specialist-font-acquisition-refresh.md +138 -0
  37. data/TODO.new/34-pillar2-content-stream-correlator.md +147 -0
  38. data/TODO.new/35-universal-set-production-run.md +160 -0
  39. data/TODO.new/36-per-font-coverage-audit.md +145 -0
  40. data/TODO.new/37-coverage-highlight-reporter.md +125 -0
  41. data/TODO.new/38-fontist-org-glyph-consumer.md +141 -0
  42. data/TODO.new/39-implementation-order-update-32-38.md +258 -0
  43. data/TODO.new/40-archive-private-uses-ucode-audit.md +124 -0
  44. data/TODO.new/41-ucode-unicode-archive-bridge.md +160 -0
  45. data/config/specialist_fonts.yml +102 -0
  46. data/config/unicode17_tier1_fonts.yml +42 -0
  47. data/config/unicode17_universal_glyph_set.yml +293 -0
  48. data/lib/ucode/audit/block_aggregator.rb +57 -29
  49. data/lib/ucode/audit/browser/face_page.rb +128 -0
  50. data/lib/ucode/audit/browser/glyph_panel.rb +124 -0
  51. data/lib/ucode/audit/browser/library_page.rb +74 -0
  52. data/lib/ucode/audit/browser/missing_glyph_page.rb +87 -0
  53. data/lib/ucode/audit/browser/template.rb +47 -0
  54. data/lib/ucode/audit/browser/templates/face.css +200 -0
  55. data/lib/ucode/audit/browser/templates/face.html.erb +41 -0
  56. data/lib/ucode/audit/browser/templates/face.js +298 -0
  57. data/lib/ucode/audit/browser/templates/library.css +119 -0
  58. data/lib/ucode/audit/browser/templates/library.html.erb +42 -0
  59. data/lib/ucode/audit/browser/templates/library.js +99 -0
  60. data/lib/ucode/audit/browser/templates/missing_glyph_page.css +119 -0
  61. data/lib/ucode/audit/browser/templates/missing_glyph_page.html.erb +58 -0
  62. data/lib/ucode/audit/browser/templates/missing_glyph_page.js +2 -0
  63. data/lib/ucode/audit/browser.rb +32 -0
  64. data/lib/ucode/audit/context.rb +27 -1
  65. data/lib/ucode/audit/coverage_reference.rb +103 -0
  66. data/lib/ucode/audit/differ.rb +121 -0
  67. data/lib/ucode/audit/emitter/block_emitter.rb +52 -0
  68. data/lib/ucode/audit/emitter/codepoint_emitter.rb +87 -0
  69. data/lib/ucode/audit/emitter/collection_emitter.rb +80 -0
  70. data/lib/ucode/audit/emitter/face_directory.rb +212 -0
  71. data/lib/ucode/audit/emitter/glyph_emitter.rb +48 -0
  72. data/lib/ucode/audit/emitter/index_emitter.rb +149 -0
  73. data/lib/ucode/audit/emitter/library_emitter.rb +96 -0
  74. data/lib/ucode/audit/emitter/paths.rb +312 -0
  75. data/lib/ucode/audit/emitter/plane_emitter.rb +29 -0
  76. data/lib/ucode/audit/emitter/script_emitter.rb +29 -0
  77. data/lib/ucode/audit/emitter.rb +29 -0
  78. data/lib/ucode/audit/extractors/aggregations.rb +31 -2
  79. data/lib/ucode/audit/face_auditor.rb +86 -0
  80. data/lib/ucode/audit/formatters/audit_diff_text.rb +112 -0
  81. data/lib/ucode/audit/formatters/audit_text.rb +411 -0
  82. data/lib/ucode/audit/formatters/color.rb +48 -0
  83. data/lib/ucode/audit/formatters/library_summary_text.rb +98 -0
  84. data/lib/ucode/audit/formatters/text_formatter.rb +83 -0
  85. data/lib/ucode/audit/formatters.rb +23 -0
  86. data/lib/ucode/audit/library_aggregator.rb +86 -0
  87. data/lib/ucode/audit/library_auditor.rb +105 -0
  88. data/lib/ucode/audit/release/emitter.rb +152 -0
  89. data/lib/ucode/audit/release/face_card.rb +93 -0
  90. data/lib/ucode/audit/release/formula_audits.rb +50 -0
  91. data/lib/ucode/audit/release/library_index_builder.rb +78 -0
  92. data/lib/ucode/audit/release/manifest_builder.rb +127 -0
  93. data/lib/ucode/audit/release.rb +42 -0
  94. data/lib/ucode/audit/ucd_only_reference.rb +81 -0
  95. data/lib/ucode/audit/universal_set_reference.rb +136 -0
  96. data/lib/ucode/audit.rb +31 -0
  97. data/lib/ucode/cli.rb +339 -33
  98. data/lib/ucode/commands/audit/browser_command.rb +82 -0
  99. data/lib/ucode/commands/audit/collection_command.rb +103 -0
  100. data/lib/ucode/commands/audit/compare_command.rb +188 -0
  101. data/lib/ucode/commands/audit/font_command.rb +140 -0
  102. data/lib/ucode/commands/audit/library_command.rb +87 -0
  103. data/lib/ucode/commands/audit/reference_builder.rb +64 -0
  104. data/lib/ucode/commands/audit.rb +20 -0
  105. data/lib/ucode/commands/block_feed.rb +73 -0
  106. data/lib/ucode/commands/canonical_build.rb +138 -0
  107. data/lib/ucode/commands/fetch.rb +37 -1
  108. data/lib/ucode/commands/release.rb +115 -0
  109. data/lib/ucode/commands/universal_set.rb +211 -0
  110. data/lib/ucode/commands.rb +5 -0
  111. data/lib/ucode/coordinator/indices.rb +11 -0
  112. data/lib/ucode/coordinator.rb +138 -5
  113. data/lib/ucode/error.rb +30 -2
  114. data/lib/ucode/fetch/font_fetcher/result.rb +39 -0
  115. data/lib/ucode/fetch/font_fetcher.rb +16 -0
  116. data/lib/ucode/fetch/specialist_font_fetcher.rb +280 -0
  117. data/lib/ucode/fetch.rb +7 -3
  118. data/lib/ucode/glyphs/real_fonts/cmap_cache.rb +74 -0
  119. data/lib/ucode/glyphs/real_fonts.rb +1 -0
  120. data/lib/ucode/glyphs/resolver.rb +62 -0
  121. data/lib/ucode/glyphs/source.rb +48 -0
  122. data/lib/ucode/glyphs/source_builder.rb +61 -0
  123. data/lib/ucode/glyphs/source_config/coverage_assertion.rb +79 -0
  124. data/lib/ucode/glyphs/source_config/gap_report.rb +54 -0
  125. data/lib/ucode/glyphs/source_config.rb +104 -0
  126. data/lib/ucode/glyphs/sources/pillar1_embedded_tounicode.rb +63 -0
  127. data/lib/ucode/glyphs/sources/pillar3_last_resort.rb +51 -0
  128. data/lib/ucode/glyphs/sources/tier1_real_font.rb +104 -0
  129. data/lib/ucode/glyphs/sources.rb +20 -0
  130. data/lib/ucode/glyphs/universal_set/builder.rb +161 -0
  131. data/lib/ucode/glyphs/universal_set/coverage_report.rb +139 -0
  132. data/lib/ucode/glyphs/universal_set/idempotency.rb +86 -0
  133. data/lib/ucode/glyphs/universal_set/manifest_accumulator.rb +195 -0
  134. data/lib/ucode/glyphs/universal_set/manifest_writer.rb +61 -0
  135. data/lib/ucode/glyphs/universal_set/pre_build_check.rb +197 -0
  136. data/lib/ucode/glyphs/universal_set/validator.rb +204 -0
  137. data/lib/ucode/glyphs/universal_set.rb +45 -0
  138. data/lib/ucode/glyphs.rb +6 -0
  139. data/lib/ucode/models/audit/baseline.rb +6 -0
  140. data/lib/ucode/models/audit/block_summary.rb +7 -0
  141. data/lib/ucode/models/audit/codepoint_provenance.rb +39 -0
  142. data/lib/ucode/models/audit/release_face.rb +42 -0
  143. data/lib/ucode/models/audit/release_formula.rb +33 -0
  144. data/lib/ucode/models/audit/release_manifest.rb +43 -0
  145. data/lib/ucode/models/audit/release_universal_set.rb +37 -0
  146. data/lib/ucode/models/audit.rb +9 -0
  147. data/lib/ucode/models/block.rb +2 -0
  148. data/lib/ucode/models/build_report.rb +109 -0
  149. data/lib/ucode/models/codepoint/glyph.rb +42 -0
  150. data/lib/ucode/models/codepoint.rb +3 -0
  151. data/lib/ucode/models/glyph_source.rb +86 -0
  152. data/lib/ucode/models/glyph_source_map.rb +138 -0
  153. data/lib/ucode/models/specialist_font.rb +70 -0
  154. data/lib/ucode/models/specialist_font_manifest.rb +48 -0
  155. data/lib/ucode/models/unihan_entry.rb +81 -9
  156. data/lib/ucode/models/unihan_field.rb +21 -0
  157. data/lib/ucode/models/universal_set_entry.rb +47 -0
  158. data/lib/ucode/models/universal_set_manifest.rb +78 -0
  159. data/lib/ucode/models/validation_report.rb +99 -0
  160. data/lib/ucode/models.rb +9 -0
  161. data/lib/ucode/parsers/named_sequences.rb +5 -5
  162. data/lib/ucode/parsers/unihan.rb +50 -19
  163. data/lib/ucode/repo/aggregate_writer.rb +34 -2
  164. data/lib/ucode/repo/block_feed_emitter.rb +153 -0
  165. data/lib/ucode/repo/build_report_accumulator.rb +138 -0
  166. data/lib/ucode/repo/build_report_writer.rb +46 -0
  167. data/lib/ucode/repo/build_validator.rb +229 -0
  168. data/lib/ucode/repo/codepoint_writer.rb +50 -1
  169. data/lib/ucode/repo/paths.rb +8 -0
  170. data/lib/ucode/repo.rb +4 -0
  171. data/lib/ucode/version.rb +1 -1
  172. data/schema/block-feed.output.schema.yml +134 -0
  173. metadata +143 -2
  174. data/ucode.gemspec +0 -56
@@ -0,0 +1,298 @@
1
+ // ucode audit face browser — vanilla JS, no dependencies.
2
+ // Reads inlined overview JSON, renders header + plane band + block table.
3
+ // Lazy-fetches per-block chunk on row click.
4
+
5
+ (function () {
6
+ "use strict";
7
+
8
+ var overview = JSON.parse(
9
+ document.getElementById("audit-overview").textContent
10
+ );
11
+ var verbose = document.body.dataset.verbose === "true";
12
+ var withGlyphs = document.body.dataset.withGlyphs === "true";
13
+ var universalSet = {
14
+ available: document.body.dataset.universalSetAvailable === "true",
15
+ glyphsDir: document.body.dataset.universalSetGlyphsDir || "",
16
+ manifestPath: document.body.dataset.universalSetManifestPath || ""
17
+ };
18
+ var chunkCache = new Map();
19
+ var glyphCache = new Map();
20
+
21
+ // Detect file:// — fetches blocked in some browsers
22
+ if (location.protocol === "file:") {
23
+ document.getElementById("file-url-hint").classList.remove("hidden");
24
+ }
25
+
26
+ renderOverview();
27
+ renderBlocks();
28
+ renderDiscrepancies();
29
+
30
+ function renderOverview() {
31
+ var font = overview.font || {};
32
+ var baseline = overview.baseline || {};
33
+ var totals = overview.totals || {};
34
+
35
+ var html = '<div class="card">';
36
+ html += '<dl class="identity-grid">';
37
+ html += dt("PostScript name", font.postscript_name);
38
+ html += dt("Family", font.family_name);
39
+ html += dt("Subfamily", font.subfamily_name);
40
+ html += dt("Version", font.version);
41
+ html += dt("Source file", font.source_file);
42
+ html += dt("SHA-256", font.source_sha256);
43
+ html += dt("Baseline Unicode", baseline.unicode_version);
44
+ html += dt("Generated", overview.generated_at);
45
+ html += '</dl>';
46
+ html += '<div class="totals">';
47
+ html += cell("Codepoints covered", totals.covered_codepoints_total);
48
+ html += cell("Blocks touched", totals.blocks_touched);
49
+ html += cell("Complete", totals.blocks_complete);
50
+ html += cell("Partial", totals.blocks_partial);
51
+ html += cell("Scripts touched", totals.scripts_touched);
52
+ html += '</div>';
53
+ html += '</div>';
54
+
55
+ document.getElementById("overview").innerHTML = html;
56
+ }
57
+
58
+ function renderBlocks() {
59
+ var blocks = overview.block_summaries || [];
60
+ if (!blocks.length) {
61
+ document.getElementById("blocks").innerHTML = "";
62
+ return;
63
+ }
64
+ var html = '<h2>Blocks</h2>';
65
+ html += '<table class="block-table"><thead><tr>';
66
+ html += '<th>Block</th><th class="num">Range</th>';
67
+ html += '<th class="num">Covered</th><th class="num">Total</th>';
68
+ html += '<th class="num">%</th><th>Status</th>';
69
+ html += '</tr></thead><tbody>';
70
+ blocks.forEach(function (block, i) {
71
+ var name = escapeAttr(block.name);
72
+ html += '<tr class="block-row" data-block="' + name + '" data-index="' + i + '">';
73
+ html += '<td>' + escapeHtml(block.name) + '</td>';
74
+ html += '<td class="num">' + escapeHtml(block.range || '') + '</td>';
75
+ html += '<td class="num">' + escapeHtml(block.covered_count) + '</td>';
76
+ html += '<td class="num">' + escapeHtml(block.total_assigned) + '</td>';
77
+ html += '<td class="num">' + escapeHtml((block.coverage_percent || 0).toFixed(1)) + '%</td>';
78
+ html += '<td><span class="status status-' + escapeAttr(block.status) + '">' +
79
+ escapeHtml(block.status) + '</span></td>';
80
+ html += '</tr>';
81
+ html += '<tr class="block-detail hidden" data-detail-for="' + name + '"><td colspan="6"></td></tr>';
82
+ });
83
+ html += '</tbody></table>';
84
+ document.getElementById("blocks").innerHTML = html;
85
+
86
+ Array.prototype.forEach.call(
87
+ document.querySelectorAll(".block-row"),
88
+ function (row) {
89
+ row.addEventListener("click", onBlockRowClick);
90
+ }
91
+ );
92
+ }
93
+
94
+ function renderDiscrepancies() {
95
+ var discrepancies = overview.discrepancies || [];
96
+ if (!discrepancies.length) return;
97
+
98
+ var html = '<h2>Discrepancies</h2>';
99
+ html += '<ul class="discrepancies-list">';
100
+ discrepancies.forEach(function (d) {
101
+ html += '<li><span class="kind">' + escapeHtml(d.kind) + '</span> — ' +
102
+ escapeHtml(d.detail || '') + '</li>';
103
+ });
104
+ html += '</ul>';
105
+ var section = document.getElementById("discrepancies");
106
+ section.innerHTML = html;
107
+ section.classList.remove("hidden");
108
+ }
109
+
110
+ function onBlockRowClick(event) {
111
+ var row = event.currentTarget;
112
+ var blockName = row.dataset.block;
113
+ var detailRow = document.querySelector(
114
+ '.block-detail[data-detail-for="' + blockName + '"]'
115
+ );
116
+ var cell = detailRow.querySelector("td");
117
+
118
+ if (!detailRow.classList.contains("hidden")) {
119
+ detailRow.classList.add("hidden");
120
+ return;
121
+ }
122
+ detailRow.classList.remove("hidden");
123
+
124
+ var block = overview.block_summaries[parseInt(row.dataset.index, 10)];
125
+ cell.innerHTML = renderMissingChips(block);
126
+ if (verbose || withGlyphs) {
127
+ fetchBlockChunk(blockName).then(function (chunk) {
128
+ cell.innerHTML = renderExpandedBlock(block, chunk);
129
+ wireChipClicks(cell);
130
+ }).catch(function () {
131
+ // 404 or fetch blocked — keep the basic missing-chips view.
132
+ });
133
+ }
134
+ }
135
+
136
+ function renderMissingChips(block) {
137
+ var missing = block.missing_codepoints || [];
138
+ var html = '<p class="hint">Missing (' + escapeHtml(missing.length) + "):</p>";
139
+ if (!missing.length) return html;
140
+ html += '<div class="cp-chips">';
141
+ missing.slice(0, 200).forEach(function (cp) {
142
+ var cpInt = parseInt(cp, 10);
143
+ var cpHex = isNaN(cpInt) ? "" : cpInt.toString(16).toUpperCase().padStart(4, "0");
144
+ var glyphAttr = universalSet.available
145
+ ? ' data-show-glyph="1"'
146
+ : "";
147
+ html += '<span class="cp-chip" data-cp="' + escapeAttr(cpInt) + '"' + glyphAttr + ">U+" +
148
+ escapeHtml(cpHex) + "</span>";
149
+ });
150
+ if (missing.length > 200) {
151
+ html += '<span class="cp-chip">… +' + escapeHtml(missing.length - 200) + " more</span>";
152
+ }
153
+ html += "</div>";
154
+ return html;
155
+ }
156
+
157
+ function renderExpandedBlock(block, chunk) {
158
+ var html = renderMissingChips(block);
159
+ if (verbose && chunk && chunk.codepoints) {
160
+ html += '<p class="hint">Verbose detail available — click a chip.</p>';
161
+ }
162
+ return html;
163
+ }
164
+
165
+ function wireChipClicks(container) {
166
+ Array.prototype.forEach.call(
167
+ container.querySelectorAll(".cp-chip[data-cp]"),
168
+ function (chip) {
169
+ chip.addEventListener("click", onChipClick);
170
+ }
171
+ );
172
+ }
173
+
174
+ function onChipClick(event) {
175
+ var cp = parseInt(event.currentTarget.dataset.cp, 10);
176
+ var blockName = event.currentTarget.closest("[data-detail-for]").dataset.detailFor;
177
+ showDetail(cp, blockName);
178
+ }
179
+
180
+ function showDetail(cp, blockName) {
181
+ var panel = document.getElementById("detail");
182
+ panel.classList.remove("hidden");
183
+
184
+ fetchCodepointChunk(blockName).then(function (chunk) {
185
+ var detail = (chunk.codepoints || []).find(function (row) {
186
+ return row.codepoint === cp;
187
+ });
188
+ if (!detail) {
189
+ panel.innerHTML = '<div class="detail-panel">No detail for U+' +
190
+ escapeHtml(cp.toString(16).toUpperCase()) + "</div>";
191
+ return;
192
+ }
193
+ var html = '<div class="detail-panel"><dl class="identity-grid">';
194
+ html += dt("Codepoint", "U+" + cp.toString(16).toUpperCase().padStart(4, "0"));
195
+ html += dt("Name", detail.name || "(unknown)");
196
+ html += dt("General category", detail.general_category || "");
197
+ html += dt("Script", detail.script || "");
198
+ html += dt("Block", detail.block_name || blockName);
199
+ html += dt("Age", detail.age || "");
200
+ html += "</dl>";
201
+ if (withGlyphs && detail.glyph_svg_path) {
202
+ html += '<div class="glyph-preview" id="glyph-preview"></div>';
203
+ }
204
+ if (universalSet.available) {
205
+ html += '<div class="glyph-preview universal-set-preview" id="universal-set-glyph">';
206
+ html += '<p class="hint">Universal-set glyph (this font is missing this codepoint):</p>';
207
+ html += "</div>";
208
+ }
209
+ html += "</div>";
210
+ panel.innerHTML = html;
211
+ if (withGlyphs && detail.glyph_svg_path) {
212
+ fetchGlyph(detail.glyph_svg_path);
213
+ }
214
+ if (universalSet.available) {
215
+ fetchUniversalSetGlyph(cp);
216
+ }
217
+ }).catch(function () {
218
+ panel.innerHTML = '<div class="detail-panel">Detail fetch failed.</div>';
219
+ });
220
+ }
221
+
222
+ function fetchGlyph(path) {
223
+ fetchLocal(path).then(function (svg) {
224
+ var preview = document.getElementById("glyph-preview");
225
+ if (preview) preview.innerHTML = svg;
226
+ }).catch(function () {});
227
+ }
228
+
229
+ function fetchUniversalSetGlyph(cp) {
230
+ var hex = cp.toString(16).toUpperCase().padStart(4, "0");
231
+ var path = universalSet.glyphsDir + "U+" + hex + ".svg";
232
+ if (glyphCache.has(path)) {
233
+ renderUniversalSetGlyph(path);
234
+ return;
235
+ }
236
+ fetch(path)
237
+ .then(function (res) {
238
+ if (!res.ok) throw new Error("HTTP " + res.status);
239
+ return res.text();
240
+ })
241
+ .then(function (svg) {
242
+ glyphCache.set(path, svg);
243
+ renderUniversalSetGlyph(path);
244
+ })
245
+ .catch(function () {
246
+ var slot = document.getElementById("universal-set-glyph");
247
+ if (slot) {
248
+ slot.innerHTML = '<p class="hint">No universal-set glyph at U+' + escapeHtml(hex) + ".</p>";
249
+ }
250
+ });
251
+ }
252
+
253
+ function renderUniversalSetGlyph(path) {
254
+ var slot = document.getElementById("universal-set-glyph");
255
+ if (slot) slot.innerHTML = glyphCache.get(path);
256
+ }
257
+
258
+ function fetchBlockChunk(blockName) {
259
+ return fetchLocal("blocks/" + blockName + ".json");
260
+ }
261
+
262
+ function fetchCodepointChunk(blockName) {
263
+ return fetchLocal("codepoints/" + blockName + ".json");
264
+ }
265
+
266
+ function fetchLocal(path) {
267
+ if (chunkCache.has(path)) {
268
+ return Promise.resolve(chunkCache.get(path));
269
+ }
270
+ return fetch(path).then(function (res) {
271
+ if (!res.ok) throw new Error("HTTP " + res.status);
272
+ return res.json();
273
+ }).then(function (data) {
274
+ chunkCache.set(path, data);
275
+ return data;
276
+ });
277
+ }
278
+
279
+ // --- HTML helpers ----------------------------------------------------
280
+
281
+ function dt(label, value) {
282
+ return '<dt>' + escapeHtml(label) + '</dt><dd>' +
283
+ escapeHtml(value == null ? '' : String(value)) + '</dd>';
284
+ }
285
+ function cell(label, value) {
286
+ return '<div class="total-cell"><div class="label">' + escapeHtml(label) +
287
+ '</div><div class="value">' + escapeHtml(value == null ? '' : String(value)) +
288
+ '</div></div>';
289
+ }
290
+ function escapeHtml(s) {
291
+ return String(s).replace(/[&<>"']/g, function (c) {
292
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
293
+ });
294
+ }
295
+ function escapeAttr(s) {
296
+ return escapeHtml(s).replace(/"/g, "&quot;");
297
+ }
298
+ })();
@@ -0,0 +1,119 @@
1
+ :root {
2
+ --bg: #fafafa;
3
+ --fg: #1a1a1a;
4
+ --muted: #5a5a5a;
5
+ --card-bg: #ffffff;
6
+ --border: #d8d8d8;
7
+ --accent: #2962ff;
8
+ --coverage-complete: #1b5e20;
9
+ --coverage-strong: #66bb6a;
10
+ --coverage-weak: #fdd835;
11
+ --coverage-none: #c62828;
12
+ }
13
+
14
+ @media (prefers-color-scheme: dark) {
15
+ :root {
16
+ --bg: #1a1a1a;
17
+ --fg: #e8e8e8;
18
+ --muted: #b0b0b0;
19
+ --card-bg: #2a2a2a;
20
+ --border: #3a3a3a;
21
+ --accent: #82b1ff;
22
+ }
23
+ }
24
+
25
+ * { box-sizing: border-box; }
26
+ body {
27
+ margin: 0;
28
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
29
+ background: var(--bg);
30
+ color: var(--fg);
31
+ line-height: 1.5;
32
+ }
33
+
34
+ .page-header {
35
+ padding: 1.5rem 2rem;
36
+ border-bottom: 1px solid var(--border);
37
+ background: var(--card-bg);
38
+ }
39
+ .page-header h1 { margin: 0 0 0.25rem; font-size: 1.5rem; }
40
+ .tagline { margin: 0; color: var(--muted); font-size: 0.875rem; }
41
+
42
+ main { padding: 1.5rem 2rem; max-width: 1200px; margin: 0 auto; }
43
+
44
+ .controls {
45
+ display: flex;
46
+ gap: 0.5rem;
47
+ margin-bottom: 1.5rem;
48
+ flex-wrap: wrap;
49
+ }
50
+ .controls input[type="search"],
51
+ .controls select {
52
+ padding: 0.4rem 0.6rem;
53
+ border: 1px solid var(--border);
54
+ border-radius: 4px;
55
+ background: var(--card-bg);
56
+ color: var(--fg);
57
+ font: inherit;
58
+ }
59
+ .controls input[type="search"] { flex: 1; min-width: 240px; }
60
+
61
+ .cards {
62
+ display: grid;
63
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
64
+ gap: 1rem;
65
+ }
66
+
67
+ .font-card {
68
+ background: var(--card-bg);
69
+ border: 1px solid var(--border);
70
+ border-radius: 6px;
71
+ padding: 1rem 1.25rem;
72
+ display: flex;
73
+ flex-direction: column;
74
+ gap: 0.5rem;
75
+ text-decoration: none;
76
+ color: inherit;
77
+ transition: border-color 0.15s ease;
78
+ }
79
+ .font-card:hover { border-color: var(--accent); }
80
+
81
+ .font-name { font-size: 1.1rem; font-weight: 600; margin: 0; }
82
+ .font-meta { font-size: 0.75rem; color: var(--muted); margin: 0; }
83
+
84
+ .coverage-bar {
85
+ height: 8px;
86
+ border-radius: 4px;
87
+ background: var(--bg);
88
+ border: 1px solid var(--border);
89
+ overflow: hidden;
90
+ }
91
+ .coverage-bar > div {
92
+ height: 100%;
93
+ background: var(--coverage-strong);
94
+ }
95
+
96
+ .quick-stats {
97
+ font-size: 0.8rem;
98
+ color: var(--muted);
99
+ margin: 0;
100
+ font-variant-numeric: tabular-nums;
101
+ }
102
+
103
+ .badges { display: flex; flex-wrap: wrap; gap: 4px; }
104
+ .badge {
105
+ padding: 2px 8px;
106
+ border-radius: 10px;
107
+ font-size: 0.7rem;
108
+ font-weight: 600;
109
+ background: var(--bg);
110
+ border: 1px solid var(--border);
111
+ color: var(--muted);
112
+ }
113
+ .badge-badge-discrepancy { background: #c62828; color: #fff; border-color: #c62828; }
114
+
115
+ .empty {
116
+ text-align: center;
117
+ color: var(--muted);
118
+ padding: 4rem 1rem;
119
+ }
@@ -0,0 +1,42 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title><%= page_title %></title>
7
+ <style>
8
+ <%= _css %>
9
+ </style>
10
+ </head>
11
+ <body>
12
+ <header class="page-header">
13
+ <h1>ucode audit library</h1>
14
+ <p class="tagline" id="library-tagline"></p>
15
+ </header>
16
+
17
+ <main>
18
+ <section id="controls" class="controls">
19
+ <input type="search" id="filter-search" placeholder="Filter by family or PostScript name…"
20
+ aria-label="Search fonts">
21
+ <select id="sort-select" aria-label="Sort fonts">
22
+ <option value="name">Sort: name</option>
23
+ <option value="codepoints">Sort: codepoint count</option>
24
+ <option value="glyphs">Sort: glyph count</option>
25
+ <option value="weight">Sort: weight class</option>
26
+ </select>
27
+ <select id="status-select" aria-label="Filter by status">
28
+ <option value="">All fonts</option>
29
+ <option value="complete">Complete coverage</option>
30
+ <option value="partial">Partial only</option>
31
+ </select>
32
+ </section>
33
+
34
+ <section id="cards" aria-live="polite" aria-busy="true"></section>
35
+ </main>
36
+
37
+ <script type="application/json" id="library-overview"><%= library_json %></script>
38
+ <script>
39
+ <%= _js %>
40
+ </script>
41
+ </body>
42
+ </html>
@@ -0,0 +1,99 @@
1
+ // ucode audit library browser — vanilla JS.
2
+ // Renders a card grid with search/sort/filter.
3
+
4
+ (function () {
5
+ "use strict";
6
+
7
+ var data = JSON.parse(
8
+ document.getElementById("library-overview").textContent
9
+ );
10
+
11
+ var state = {
12
+ search: "",
13
+ sort: "name",
14
+ status: "",
15
+ };
16
+
17
+ renderTagline();
18
+ renderCards();
19
+
20
+ document.getElementById("filter-search").addEventListener("input", function (e) {
21
+ state.search = e.target.value.toLowerCase();
22
+ renderCards();
23
+ });
24
+ document.getElementById("sort-select").addEventListener("change", function (e) {
25
+ state.sort = e.target.value;
26
+ renderCards();
27
+ });
28
+ document.getElementById("status-select").addEventListener("change", function (e) {
29
+ state.status = e.target.value;
30
+ renderCards();
31
+ });
32
+
33
+ function renderTagline() {
34
+ var metrics = data.aggregate_metrics || {};
35
+ var tagline = document.getElementById("library-tagline");
36
+ tagline.textContent =
37
+ (data.total_faces || 0) + " faces across " +
38
+ (data.total_files || 0) + " source files · " +
39
+ (metrics.total_codepoints || 0) + " codepoints (with duplicates)";
40
+ }
41
+
42
+ function renderCards() {
43
+ var faces = (data.faces || []).filter(matchesFilter).sort(bySort);
44
+ var container = document.getElementById("cards");
45
+ container.setAttribute("aria-busy", "false");
46
+
47
+ if (!faces.length) {
48
+ container.innerHTML = '<p class="empty">No fonts match the current filter.</p>';
49
+ return;
50
+ }
51
+ container.innerHTML = '<div class="cards">' + faces.map(renderCard).join("") + '</div>';
52
+ }
53
+
54
+ function renderCard(face) {
55
+ var pct = (face.total_assigned_total && face.covered_total) ?
56
+ (face.covered_total / face.total_assigned_total * 100) : 0;
57
+ var badges = [];
58
+ if (face.blocks_complete) badges.push('<span class="badge">' + escapeHtml(face.blocks_complete) + ' complete</span>');
59
+ if (face.blocks_partial) badges.push('<span class="badge">' + escapeHtml(face.blocks_partial) + ' partial</span>');
60
+
61
+ var stats = escapeHtml((face.total_codepoints || 0).toLocaleString()) + " cps";
62
+ if (face.total_glyphs) stats += " · " + escapeHtml(face.total_glyphs.toLocaleString()) + " glyphs";
63
+
64
+ return '<a class="font-card" href="' + escapeAttr(face.index_path) + '">' +
65
+ '<h3 class="font-name">' + escapeHtml(face.family_name || face.label) + '</h3>' +
66
+ '<p class="font-meta">' + escapeHtml(face.postscript_name || "") + ' · wght ' +
67
+ escapeHtml(face.weight_class || 400) + '</p>' +
68
+ '<div class="coverage-bar"><div style="width:' + escapeAttr(pct.toFixed(1)) + '%"></div></div>' +
69
+ '<p class="quick-stats">' + stats + '</p>' +
70
+ '<div class="badges">' + badges.join("") + '</div>' +
71
+ '</a>';
72
+ }
73
+
74
+ function matchesFilter(face) {
75
+ if (state.status === "complete" && !face.blocks_complete) return false;
76
+ if (state.status === "partial" && !face.blocks_partial) return false;
77
+ if (!state.search) return true;
78
+ var haystack = ((face.family_name || "") + " " +
79
+ (face.postscript_name || "") + " " +
80
+ (face.label || "")).toLowerCase();
81
+ return haystack.indexOf(state.search) !== -1;
82
+ }
83
+
84
+ function bySort(a, b) {
85
+ var key = state.sort;
86
+ if (key === "name") return (a.family_name || "").localeCompare(b.family_name || "");
87
+ if (key === "codepoints") return (b.total_codepoints || 0) - (a.total_codepoints || 0);
88
+ if (key === "glyphs") return (b.total_glyphs || 0) - (a.total_glyphs || 0);
89
+ if (key === "weight") return (a.weight_class || 0) - (b.weight_class || 0);
90
+ return 0;
91
+ }
92
+
93
+ function escapeHtml(s) {
94
+ return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
95
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
96
+ });
97
+ }
98
+ function escapeAttr(s) { return escapeHtml(s); }
99
+ })();
@@ -0,0 +1,119 @@
1
+ :root {
2
+ --bg: #fafafa;
3
+ --fg: #1a1a1a;
4
+ --muted: #5a5a5a;
5
+ --card-bg: #ffffff;
6
+ --border: #d8d8d8;
7
+ --accent: #2962ff;
8
+ }
9
+ @media (prefers-color-scheme: dark) {
10
+ :root {
11
+ --bg: #1a1a1a;
12
+ --fg: #e8e8e8;
13
+ --muted: #b0b0b0;
14
+ --card-bg: #2a2a2a;
15
+ --border: #3a3a3a;
16
+ --accent: #82b1ff;
17
+ }
18
+ }
19
+
20
+ * { box-sizing: border-box; }
21
+ body {
22
+ margin: 0;
23
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
24
+ background: var(--bg);
25
+ color: var(--fg);
26
+ line-height: 1.5;
27
+ }
28
+
29
+ .page-header {
30
+ padding: 1.5rem 2rem;
31
+ border-bottom: 1px solid var(--border);
32
+ background: var(--card-bg);
33
+ }
34
+ .page-header h1 { margin: 0 0 0.25rem; font-size: 1.5rem; }
35
+ .tagline { margin: 0; color: var(--muted); font-size: 0.875rem; }
36
+
37
+ main { padding: 1.5rem 2rem; max-width: 1400px; margin: 0 auto; }
38
+
39
+ .hint {
40
+ background: #fff3cd; color: #664d03;
41
+ padding: 0.75rem 1rem; border-radius: 4px;
42
+ font-size: 0.875rem;
43
+ }
44
+ @media (prefers-color-scheme: dark) {
45
+ .hint { background: #4d3a00; color: #ffe082; }
46
+ }
47
+
48
+ .glyph-grid {
49
+ list-style: none;
50
+ margin: 0;
51
+ padding: 0;
52
+ display: grid;
53
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
54
+ gap: 0.75rem;
55
+ }
56
+
57
+ .glyph-cell {
58
+ background: var(--card-bg);
59
+ border: 1px solid var(--border);
60
+ border-radius: 6px;
61
+ padding: 0.5rem;
62
+ display: flex;
63
+ flex-direction: column;
64
+ align-items: center;
65
+ gap: 0.25rem;
66
+ }
67
+
68
+ .glyph-thumb {
69
+ width: 100%;
70
+ aspect-ratio: 1 / 1;
71
+ background: #fff;
72
+ border: 1px solid var(--border);
73
+ border-radius: 4px;
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+ overflow: hidden;
78
+ }
79
+ @media (prefers-color-scheme: dark) {
80
+ .glyph-thumb { background: #f0f0f0; }
81
+ }
82
+ .glyph-thumb svg {
83
+ max-width: 100%;
84
+ max-height: 100%;
85
+ display: block;
86
+ }
87
+ .glyph-na {
88
+ color: var(--muted);
89
+ font-size: 0.75rem;
90
+ letter-spacing: 0.05em;
91
+ }
92
+
93
+ .glyph-meta {
94
+ display: flex;
95
+ flex-direction: column;
96
+ align-items: center;
97
+ gap: 0.1rem;
98
+ }
99
+ .cp-id {
100
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
101
+ font-size: 0.75rem;
102
+ font-weight: 600;
103
+ }
104
+ .source {
105
+ font-size: 0.65rem;
106
+ color: var(--muted);
107
+ text-align: center;
108
+ word-break: break-all;
109
+ }
110
+
111
+ .overflow {
112
+ margin-top: 1rem;
113
+ padding: 0.75rem 1rem;
114
+ background: var(--bg);
115
+ border: 1px dashed var(--border);
116
+ border-radius: 4px;
117
+ color: var(--muted);
118
+ font-size: 0.85rem;
119
+ }