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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +72 -0
- data/Gemfile.lock +2 -2
- data/TODO.full/00-README.md +116 -0
- data/TODO.full/01-panglyph-vision.md +112 -0
- data/TODO.full/02-panglyph-repo-bootstrap.md +184 -0
- data/TODO.full/03-panglyph-font-builder.md +201 -0
- data/TODO.full/04-panglyph-publish-pipeline.md +126 -0
- data/TODO.full/05-ucode-0-1-1-release.md +139 -0
- data/TODO.full/06-fontisan-remove-audit.md +142 -0
- data/TODO.full/07-fontisan-remove-ucd.md +125 -0
- data/TODO.full/08-archive-private-bin-build.md +143 -0
- data/TODO.full/09-archive-public-structure.md +164 -0
- data/TODO.full/10-fontist-org-woff-glyphs.md +131 -0
- data/TODO.full/11-fontist-org-audit-coverage.md +140 -0
- data/TODO.full/12-implementation-order.md +216 -0
- data/TODO.full/13-fontisan-font-writer-api.md +189 -0
- data/TODO.full/14-fontisan-table-writers.md +66 -0
- data/TODO.full/15-panglyph-builder-real.md +82 -0
- data/TODO.full/16-archive-public-sync-workflows.md +167 -0
- data/TODO.full/17-fontist-org-font-picker.md +73 -0
- data/TODO.full/18-comprehensive-spec-coverage.md +64 -0
- data/TODO.full/19-ucode-0-1-2-patch.md +32 -0
- data/TODO.full/20-fontisan-0-2-23-release.md +52 -0
- data/TODO.new/00-README.md +30 -0
- data/TODO.new/23-universal-glyph-set-source-map.md +312 -0
- data/TODO.new/24-universal-glyph-set-build.md +189 -0
- data/TODO.new/25-font-audit-against-universal-set.md +195 -0
- data/TODO.new/26-missing-glyph-reporter.md +189 -0
- data/TODO.new/27-fontist-org-consumer-integration.md +200 -0
- data/TODO.new/28-implementation-order-update.md +187 -0
- data/TODO.new/29-universal-set-curation-uc17.md +312 -0
- data/TODO.new/30-tier1-font-acquisition.md +241 -0
- data/TODO.new/31-universal-set-production-build.md +205 -0
- data/TODO.new/32-uc17-coverage-matrix.md +165 -0
- data/TODO.new/33-specialist-font-acquisition-refresh.md +138 -0
- data/TODO.new/34-pillar2-content-stream-correlator.md +147 -0
- data/TODO.new/35-universal-set-production-run.md +160 -0
- data/TODO.new/36-per-font-coverage-audit.md +145 -0
- data/TODO.new/37-coverage-highlight-reporter.md +125 -0
- data/TODO.new/38-fontist-org-glyph-consumer.md +141 -0
- data/TODO.new/39-implementation-order-update-32-38.md +258 -0
- data/TODO.new/40-archive-private-uses-ucode-audit.md +124 -0
- data/TODO.new/41-ucode-unicode-archive-bridge.md +160 -0
- data/config/specialist_fonts.yml +102 -0
- data/config/unicode17_tier1_fonts.yml +42 -0
- data/config/unicode17_universal_glyph_set.yml +293 -0
- data/lib/ucode/audit/block_aggregator.rb +57 -29
- data/lib/ucode/audit/browser/face_page.rb +128 -0
- data/lib/ucode/audit/browser/glyph_panel.rb +124 -0
- data/lib/ucode/audit/browser/library_page.rb +74 -0
- data/lib/ucode/audit/browser/missing_glyph_page.rb +87 -0
- data/lib/ucode/audit/browser/template.rb +47 -0
- data/lib/ucode/audit/browser/templates/face.css +200 -0
- data/lib/ucode/audit/browser/templates/face.html.erb +41 -0
- data/lib/ucode/audit/browser/templates/face.js +298 -0
- data/lib/ucode/audit/browser/templates/library.css +119 -0
- data/lib/ucode/audit/browser/templates/library.html.erb +42 -0
- data/lib/ucode/audit/browser/templates/library.js +99 -0
- data/lib/ucode/audit/browser/templates/missing_glyph_page.css +119 -0
- data/lib/ucode/audit/browser/templates/missing_glyph_page.html.erb +58 -0
- data/lib/ucode/audit/browser/templates/missing_glyph_page.js +2 -0
- data/lib/ucode/audit/browser.rb +32 -0
- data/lib/ucode/audit/context.rb +27 -1
- data/lib/ucode/audit/coverage_reference.rb +103 -0
- data/lib/ucode/audit/differ.rb +121 -0
- data/lib/ucode/audit/emitter/block_emitter.rb +52 -0
- data/lib/ucode/audit/emitter/codepoint_emitter.rb +87 -0
- data/lib/ucode/audit/emitter/collection_emitter.rb +80 -0
- data/lib/ucode/audit/emitter/face_directory.rb +212 -0
- data/lib/ucode/audit/emitter/glyph_emitter.rb +48 -0
- data/lib/ucode/audit/emitter/index_emitter.rb +149 -0
- data/lib/ucode/audit/emitter/library_emitter.rb +96 -0
- data/lib/ucode/audit/emitter/paths.rb +312 -0
- data/lib/ucode/audit/emitter/plane_emitter.rb +29 -0
- data/lib/ucode/audit/emitter/script_emitter.rb +29 -0
- data/lib/ucode/audit/emitter.rb +29 -0
- data/lib/ucode/audit/extractors/aggregations.rb +31 -2
- data/lib/ucode/audit/face_auditor.rb +86 -0
- data/lib/ucode/audit/formatters/audit_diff_text.rb +112 -0
- data/lib/ucode/audit/formatters/audit_text.rb +411 -0
- data/lib/ucode/audit/formatters/color.rb +48 -0
- data/lib/ucode/audit/formatters/library_summary_text.rb +98 -0
- data/lib/ucode/audit/formatters/text_formatter.rb +83 -0
- data/lib/ucode/audit/formatters.rb +23 -0
- data/lib/ucode/audit/library_aggregator.rb +86 -0
- data/lib/ucode/audit/library_auditor.rb +105 -0
- data/lib/ucode/audit/release/emitter.rb +152 -0
- data/lib/ucode/audit/release/face_card.rb +93 -0
- data/lib/ucode/audit/release/formula_audits.rb +50 -0
- data/lib/ucode/audit/release/library_index_builder.rb +78 -0
- data/lib/ucode/audit/release/manifest_builder.rb +127 -0
- data/lib/ucode/audit/release.rb +42 -0
- data/lib/ucode/audit/ucd_only_reference.rb +81 -0
- data/lib/ucode/audit/universal_set_reference.rb +136 -0
- data/lib/ucode/audit.rb +31 -0
- data/lib/ucode/cli.rb +339 -33
- data/lib/ucode/commands/audit/browser_command.rb +82 -0
- data/lib/ucode/commands/audit/collection_command.rb +103 -0
- data/lib/ucode/commands/audit/compare_command.rb +188 -0
- data/lib/ucode/commands/audit/font_command.rb +140 -0
- data/lib/ucode/commands/audit/library_command.rb +87 -0
- data/lib/ucode/commands/audit/reference_builder.rb +64 -0
- data/lib/ucode/commands/audit.rb +20 -0
- data/lib/ucode/commands/block_feed.rb +73 -0
- data/lib/ucode/commands/canonical_build.rb +138 -0
- data/lib/ucode/commands/fetch.rb +37 -1
- data/lib/ucode/commands/release.rb +115 -0
- data/lib/ucode/commands/universal_set.rb +211 -0
- data/lib/ucode/commands.rb +5 -0
- data/lib/ucode/coordinator/indices.rb +11 -0
- data/lib/ucode/coordinator.rb +138 -5
- data/lib/ucode/error.rb +30 -2
- data/lib/ucode/fetch/font_fetcher/result.rb +39 -0
- data/lib/ucode/fetch/font_fetcher.rb +16 -0
- data/lib/ucode/fetch/specialist_font_fetcher.rb +280 -0
- data/lib/ucode/fetch.rb +7 -3
- data/lib/ucode/glyphs/real_fonts/cmap_cache.rb +74 -0
- data/lib/ucode/glyphs/real_fonts.rb +1 -0
- data/lib/ucode/glyphs/resolver.rb +62 -0
- data/lib/ucode/glyphs/source.rb +48 -0
- data/lib/ucode/glyphs/source_builder.rb +61 -0
- data/lib/ucode/glyphs/source_config/coverage_assertion.rb +79 -0
- data/lib/ucode/glyphs/source_config/gap_report.rb +54 -0
- data/lib/ucode/glyphs/source_config.rb +104 -0
- data/lib/ucode/glyphs/sources/pillar1_embedded_tounicode.rb +63 -0
- data/lib/ucode/glyphs/sources/pillar3_last_resort.rb +51 -0
- data/lib/ucode/glyphs/sources/tier1_real_font.rb +104 -0
- data/lib/ucode/glyphs/sources.rb +20 -0
- data/lib/ucode/glyphs/universal_set/builder.rb +161 -0
- data/lib/ucode/glyphs/universal_set/coverage_report.rb +139 -0
- data/lib/ucode/glyphs/universal_set/idempotency.rb +86 -0
- data/lib/ucode/glyphs/universal_set/manifest_accumulator.rb +195 -0
- data/lib/ucode/glyphs/universal_set/manifest_writer.rb +61 -0
- data/lib/ucode/glyphs/universal_set/pre_build_check.rb +197 -0
- data/lib/ucode/glyphs/universal_set/validator.rb +204 -0
- data/lib/ucode/glyphs/universal_set.rb +45 -0
- data/lib/ucode/glyphs.rb +6 -0
- data/lib/ucode/models/audit/baseline.rb +6 -0
- data/lib/ucode/models/audit/block_summary.rb +7 -0
- data/lib/ucode/models/audit/codepoint_provenance.rb +39 -0
- data/lib/ucode/models/audit/release_face.rb +42 -0
- data/lib/ucode/models/audit/release_formula.rb +33 -0
- data/lib/ucode/models/audit/release_manifest.rb +43 -0
- data/lib/ucode/models/audit/release_universal_set.rb +37 -0
- data/lib/ucode/models/audit.rb +9 -0
- data/lib/ucode/models/block.rb +2 -0
- data/lib/ucode/models/build_report.rb +109 -0
- data/lib/ucode/models/codepoint/glyph.rb +42 -0
- data/lib/ucode/models/codepoint.rb +3 -0
- data/lib/ucode/models/glyph_source.rb +86 -0
- data/lib/ucode/models/glyph_source_map.rb +138 -0
- data/lib/ucode/models/specialist_font.rb +70 -0
- data/lib/ucode/models/specialist_font_manifest.rb +48 -0
- data/lib/ucode/models/unihan_entry.rb +81 -9
- data/lib/ucode/models/unihan_field.rb +21 -0
- data/lib/ucode/models/universal_set_entry.rb +47 -0
- data/lib/ucode/models/universal_set_manifest.rb +78 -0
- data/lib/ucode/models/validation_report.rb +99 -0
- data/lib/ucode/models.rb +9 -0
- data/lib/ucode/parsers/named_sequences.rb +5 -5
- data/lib/ucode/parsers/unihan.rb +50 -19
- data/lib/ucode/repo/aggregate_writer.rb +34 -2
- data/lib/ucode/repo/block_feed_emitter.rb +153 -0
- data/lib/ucode/repo/build_report_accumulator.rb +138 -0
- data/lib/ucode/repo/build_report_writer.rb +46 -0
- data/lib/ucode/repo/build_validator.rb +229 -0
- data/lib/ucode/repo/codepoint_writer.rb +50 -1
- data/lib/ucode/repo/paths.rb +8 -0
- data/lib/ucode/repo.rb +4 -0
- data/lib/ucode/version.rb +1 -1
- data/schema/block-feed.output.schema.yml +134 -0
- metadata +143 -2
- 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 { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
function escapeAttr(s) {
|
|
296
|
+
return escapeHtml(s).replace(/"/g, """);
|
|
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 { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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
|
+
}
|