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,312 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module Ucode
|
|
6
|
+
module Audit
|
|
7
|
+
module Emitter
|
|
8
|
+
# Pure path conventions for the Mode 2 audit output tree.
|
|
9
|
+
#
|
|
10
|
+
# The only code that knows the on-disk layout of the audit output.
|
|
11
|
+
# Distinct from {Ucode::Repo::Paths} (Mode 1 canonical UCD dataset):
|
|
12
|
+
# Mode 2 output lives under `output/font_audit/<label>/` and carries
|
|
13
|
+
# a different chunk layout (planes/, blocks/, scripts/, codepoints/,
|
|
14
|
+
# glyphs/, missing/, plus collection-face subdirs).
|
|
15
|
+
#
|
|
16
|
+
# All methods are pure: no I/O, no global state. Returns Pathname
|
|
17
|
+
# instances so callers can compose further. Block names are passed
|
|
18
|
+
# through verbatim — never slugified (per `03-directory-output-spec.md`
|
|
19
|
+
# §"Block filename encoding").
|
|
20
|
+
module Paths
|
|
21
|
+
INDEX_FILENAME = "index.json"
|
|
22
|
+
HTML_FILENAME = "index.html"
|
|
23
|
+
BLOCKS_DIR = "blocks"
|
|
24
|
+
PLANES_DIR = "planes"
|
|
25
|
+
SCRIPTS_DIR = "scripts"
|
|
26
|
+
CODEPOINTS_DIR = "codepoints"
|
|
27
|
+
GLYPHS_DIR = "glyphs"
|
|
28
|
+
MISSING_DIR = "missing"
|
|
29
|
+
FONT_AUDIT_ROOT = "font_audit"
|
|
30
|
+
# Release-tree layout (TODO 27). The release tree is the
|
|
31
|
+
# fontist.org-consumable artifact assembled from one or more
|
|
32
|
+
# per-formula library audits plus the universal-set reference.
|
|
33
|
+
# Lives at `<output_root>/font_audit_release/`.
|
|
34
|
+
RELEASE_ROOT_DIR = "font_audit_release"
|
|
35
|
+
RELEASE_AUDIT_DIR = "audit"
|
|
36
|
+
RELEASE_LIBRARY_INDEX = "library.json"
|
|
37
|
+
RELEASE_MANIFEST = "manifest.json"
|
|
38
|
+
RELEASE_UNIVERSAL_SET_DIR = "universal_glyph_set"
|
|
39
|
+
RELEASE_MANIFEST_ENTRY = "manifest.json"
|
|
40
|
+
private_constant :INDEX_FILENAME, :HTML_FILENAME, :BLOCKS_DIR,
|
|
41
|
+
:PLANES_DIR, :SCRIPTS_DIR, :CODEPOINTS_DIR,
|
|
42
|
+
:GLYPHS_DIR, :MISSING_DIR, :FONT_AUDIT_ROOT,
|
|
43
|
+
:RELEASE_ROOT_DIR, :RELEASE_AUDIT_DIR,
|
|
44
|
+
:RELEASE_LIBRARY_INDEX, :RELEASE_MANIFEST,
|
|
45
|
+
:RELEASE_UNIVERSAL_SET_DIR, :RELEASE_MANIFEST_ENTRY
|
|
46
|
+
|
|
47
|
+
module_function
|
|
48
|
+
|
|
49
|
+
# Library-mode root: one level above the per-label directories.
|
|
50
|
+
# @param output_root [String, Pathname]
|
|
51
|
+
# @return [Pathname]
|
|
52
|
+
def library_root(output_root)
|
|
53
|
+
Pathname(output_root).join(FONT_AUDIT_ROOT)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Per-label directory (one face, or one TTC source).
|
|
57
|
+
# @param output_root [String, Pathname]
|
|
58
|
+
# @param label [String] safe filename (caller-sanitized)
|
|
59
|
+
# @return [Pathname]
|
|
60
|
+
def face_dir(output_root, label)
|
|
61
|
+
library_root(output_root).join(label)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# `output/font_audit/<label>/index.json` — per-face compact index.
|
|
65
|
+
# @param output_root [String, Pathname]
|
|
66
|
+
# @param label [String]
|
|
67
|
+
# @return [Pathname]
|
|
68
|
+
def face_index_path(output_root, label)
|
|
69
|
+
face_dir(output_root, label).join(INDEX_FILENAME)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# `output/font_audit/<label>/index.html` — per-face browser
|
|
73
|
+
# (added in TODO 14).
|
|
74
|
+
# @param output_root [String, Pathname]
|
|
75
|
+
# @param label [String]
|
|
76
|
+
# @return [Pathname]
|
|
77
|
+
def face_html_path(output_root, label)
|
|
78
|
+
face_dir(output_root, label).join(HTML_FILENAME)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# `output/font_audit/<label>/blocks/<NAME>.json`. Block name is
|
|
82
|
+
# verbatim — Unicode block names contain no path separators.
|
|
83
|
+
# @param output_root [String, Pathname]
|
|
84
|
+
# @param label [String]
|
|
85
|
+
# @param block_name [String]
|
|
86
|
+
# @return [Pathname]
|
|
87
|
+
def block_path(output_root, label, block_name)
|
|
88
|
+
face_dir(output_root, label).join(BLOCKS_DIR, "#{block_name}.json")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# `output/font_audit/<label>/planes/<N>.json`.
|
|
92
|
+
# @param output_root [String, Pathname]
|
|
93
|
+
# @param label [String]
|
|
94
|
+
# @param plane [Integer]
|
|
95
|
+
# @return [Pathname]
|
|
96
|
+
def plane_path(output_root, label, plane)
|
|
97
|
+
face_dir(output_root, label).join(PLANES_DIR, "#{plane}.json")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# `output/font_audit/<label>/scripts/<CODE>.json`. Script code
|
|
101
|
+
# is the ISO 15924 short form (Latn, Grek, …).
|
|
102
|
+
# @param output_root [String, Pathname]
|
|
103
|
+
# @param label [String]
|
|
104
|
+
# @param script_code [String]
|
|
105
|
+
# @return [Pathname]
|
|
106
|
+
def script_path(output_root, label, script_code)
|
|
107
|
+
face_dir(output_root, label).join(SCRIPTS_DIR, "#{script_code}.json")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# `output/font_audit/<label>/codepoints/<NAME>.json` — verbose
|
|
111
|
+
# per-block codepoint detail.
|
|
112
|
+
# @param output_root [String, Pathname]
|
|
113
|
+
# @param label [String]
|
|
114
|
+
# @param block_name [String]
|
|
115
|
+
# @return [Pathname]
|
|
116
|
+
def codepoints_path(output_root, label, block_name)
|
|
117
|
+
face_dir(output_root, label).join(CODEPOINTS_DIR, "#{block_name}.json")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# `output/font_audit/<label>/glyphs/U+XXXX.svg`.
|
|
121
|
+
# @param output_root [String, Pathname]
|
|
122
|
+
# @param label [String]
|
|
123
|
+
# @param cp_id [String] e.g. "U+0041"
|
|
124
|
+
# @return [Pathname]
|
|
125
|
+
def glyph_path(output_root, label, cp_id)
|
|
126
|
+
face_dir(output_root, label).join(GLYPHS_DIR, "#{cp_id}.svg")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# `output/font_audit/<label>/missing/` — per-block missing-glyph
|
|
130
|
+
# gallery directory (TODO 26). Each touched block with missing
|
|
131
|
+
# codepoints gets one `<BLOCK>.html` plus a paginated
|
|
132
|
+
# `<BLOCK>.json` companion for large blocks.
|
|
133
|
+
# @param output_root [String, Pathname]
|
|
134
|
+
# @param label [String]
|
|
135
|
+
# @return [Pathname]
|
|
136
|
+
def missing_dir(output_root, label)
|
|
137
|
+
face_dir(output_root, label).join(MISSING_DIR)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# `output/font_audit/<label>/missing/<BLOCK>.html`.
|
|
141
|
+
# @param output_root [String, Pathname]
|
|
142
|
+
# @param label [String]
|
|
143
|
+
# @param block_name [String]
|
|
144
|
+
# @return [Pathname]
|
|
145
|
+
def missing_glyph_page_path(output_root, label, block_name)
|
|
146
|
+
missing_dir(output_root, label).join("#{block_name}.html")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Collection-face subdirectory: `00-<face>/`, `01-<face>/`, ...
|
|
150
|
+
# The 2-digit zero-padded prefix preserves source order and
|
|
151
|
+
# disambiguates faces that share a PostScript name.
|
|
152
|
+
# @param output_root [String, Pathname]
|
|
153
|
+
# @param source_label [String]
|
|
154
|
+
# @param face_index [Integer] 0-based face index
|
|
155
|
+
# @param face_label [String] sanitized postscript_name
|
|
156
|
+
# @return [Pathname]
|
|
157
|
+
def collection_face_dir(output_root, source_label, face_index, face_label)
|
|
158
|
+
face_dir(output_root, source_label).join(format("%<idx>02d-%<label>s",
|
|
159
|
+
idx: face_index, label: face_label))
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# `output/font_audit/index.json` — library-mode top-level index.
|
|
163
|
+
# @param output_root [String, Pathname]
|
|
164
|
+
# @return [Pathname]
|
|
165
|
+
def library_index_path(output_root)
|
|
166
|
+
library_root(output_root).join(INDEX_FILENAME)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# `output/font_audit/index.html` — library browser (TODO 15).
|
|
170
|
+
# @param output_root [String, Pathname]
|
|
171
|
+
# @return [Pathname]
|
|
172
|
+
def library_html_path(output_root)
|
|
173
|
+
library_root(output_root).join(HTML_FILENAME)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# ---- Inner-path helpers ----------------------------------------
|
|
177
|
+
# These take an explicit face_dir Pathname so chunk emitters can
|
|
178
|
+
# write under either a standalone face_dir or a collection face
|
|
179
|
+
# subdir without knowing which one they're in.
|
|
180
|
+
|
|
181
|
+
# @param face_dir [String, Pathname]
|
|
182
|
+
# @return [Pathname]
|
|
183
|
+
def index_under(face_dir)
|
|
184
|
+
Pathname(face_dir).join(INDEX_FILENAME)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# @param face_dir [String, Pathname]
|
|
188
|
+
# @param block_name [String]
|
|
189
|
+
# @return [Pathname]
|
|
190
|
+
def block_under(face_dir, block_name)
|
|
191
|
+
Pathname(face_dir).join(BLOCKS_DIR, "#{block_name}.json")
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# @param face_dir [String, Pathname]
|
|
195
|
+
# @param plane [Integer]
|
|
196
|
+
# @return [Pathname]
|
|
197
|
+
def plane_under(face_dir, plane)
|
|
198
|
+
Pathname(face_dir).join(PLANES_DIR, "#{plane}.json")
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# @param face_dir [String, Pathname]
|
|
202
|
+
# @param script_code [String]
|
|
203
|
+
# @return [Pathname]
|
|
204
|
+
def script_under(face_dir, script_code)
|
|
205
|
+
Pathname(face_dir).join(SCRIPTS_DIR, "#{script_code}.json")
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# @param face_dir [String, Pathname]
|
|
209
|
+
# @param block_name [String]
|
|
210
|
+
# @return [Pathname]
|
|
211
|
+
def codepoints_under(face_dir, block_name)
|
|
212
|
+
Pathname(face_dir).join(CODEPOINTS_DIR, "#{block_name}.json")
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# @param face_dir [String, Pathname]
|
|
216
|
+
# @param cp_id [String] e.g. "U+0041"
|
|
217
|
+
# @return [Pathname]
|
|
218
|
+
def glyph_under(face_dir, cp_id)
|
|
219
|
+
Pathname(face_dir).join(GLYPHS_DIR, "#{cp_id}.svg")
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# @param face_dir [String, Pathname]
|
|
223
|
+
# @return [Pathname]
|
|
224
|
+
def missing_dir_under(face_dir)
|
|
225
|
+
Pathname(face_dir).join(MISSING_DIR)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# @param face_dir [String, Pathname]
|
|
229
|
+
# @param block_name [String]
|
|
230
|
+
# @return [Pathname]
|
|
231
|
+
def missing_glyph_page_under(face_dir, block_name)
|
|
232
|
+
missing_dir_under(face_dir).join("#{block_name}.html")
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# ---- Release-tree paths (TODO 27) -----------------------------
|
|
236
|
+
# The release tree is the fontist.org-consumable artifact. It
|
|
237
|
+
# composes per-formula audit subtrees (each laid out per the
|
|
238
|
+
# `<output_root>/font_audit/<label>/` convention) under an
|
|
239
|
+
# outer `<release_root>/audit/<slug>/` root, plus the universal
|
|
240
|
+
# glyph set, library-level index, and release manifest.
|
|
241
|
+
#
|
|
242
|
+
# The release root lives at `<output_root>/font_audit_release/`
|
|
243
|
+
# so a single tarball of `font_audit_release/` is self-contained.
|
|
244
|
+
|
|
245
|
+
# `<output_root>/font_audit_release/`.
|
|
246
|
+
# @param output_root [String, Pathname] parent of the release root
|
|
247
|
+
# @return [Pathname]
|
|
248
|
+
def release_root(output_root)
|
|
249
|
+
Pathname(output_root).join(RELEASE_ROOT_DIR)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# `<release_root>/audit/` — top-level audit subtree.
|
|
253
|
+
# @param release_root [String, Pathname]
|
|
254
|
+
# @return [Pathname]
|
|
255
|
+
def release_audit_root(release_root)
|
|
256
|
+
Pathname(release_root).join(RELEASE_AUDIT_DIR)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# `<release_root>/audit/<slug>/` — one formula's audit subtree.
|
|
260
|
+
# Per-face directories live under here. The slug is a
|
|
261
|
+
# caller-sanitized formula identifier (fontist formula slug).
|
|
262
|
+
# @param release_root [String, Pathname]
|
|
263
|
+
# @param slug [String] sanitized formula slug
|
|
264
|
+
# @return [Pathname]
|
|
265
|
+
def release_formula_dir(release_root, slug)
|
|
266
|
+
release_audit_root(release_root).join(slug)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# `<release_root>/audit/<slug>/<face_label>/` — one face.
|
|
270
|
+
# @param release_root [String, Pathname]
|
|
271
|
+
# @param slug [String] sanitized formula slug
|
|
272
|
+
# @param face_label [String] sanitized face label
|
|
273
|
+
# @return [Pathname]
|
|
274
|
+
def release_face_dir(release_root, slug, face_label)
|
|
275
|
+
release_formula_dir(release_root, slug).join(face_label)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# `<release_root>/library.json` — release-level library index
|
|
279
|
+
# aggregating every formula + face card.
|
|
280
|
+
# @param release_root [String, Pathname]
|
|
281
|
+
# @return [Pathname]
|
|
282
|
+
def release_library_index_path(release_root)
|
|
283
|
+
Pathname(release_root).join(RELEASE_LIBRARY_INDEX)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# `<release_root>/manifest.json` — release manifest (versions,
|
|
287
|
+
# sha256s, totals).
|
|
288
|
+
# @param release_root [String, Pathname]
|
|
289
|
+
# @return [Pathname]
|
|
290
|
+
def release_manifest_path(release_root)
|
|
291
|
+
Pathname(release_root).join(RELEASE_MANIFEST)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# `<release_root>/universal_glyph_set/` — the universal-set
|
|
295
|
+
# reference directory (built separately by TODO 24 and copied
|
|
296
|
+
# or symlinked into the release tree by the CI collector).
|
|
297
|
+
# @param release_root [String, Pathname]
|
|
298
|
+
# @return [Pathname]
|
|
299
|
+
def release_universal_set_root(release_root)
|
|
300
|
+
Pathname(release_root).join(RELEASE_UNIVERSAL_SET_DIR)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# `<release_root>/universal_glyph_set/manifest.json`.
|
|
304
|
+
# @param release_root [String, Pathname]
|
|
305
|
+
# @return [Pathname]
|
|
306
|
+
def release_universal_set_manifest_path(release_root)
|
|
307
|
+
release_universal_set_root(release_root).join(RELEASE_MANIFEST_ENTRY)
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
require "ucode/repo/atomic_writes"
|
|
6
|
+
require "ucode/audit/emitter/paths"
|
|
7
|
+
|
|
8
|
+
module Ucode
|
|
9
|
+
module Audit
|
|
10
|
+
module Emitter
|
|
11
|
+
# Writes `<face_dir>/planes/<N>.json` — one rollup per Unicode
|
|
12
|
+
# plane that has any coverage.
|
|
13
|
+
#
|
|
14
|
+
# The browser fetches these when the user switches to a
|
|
15
|
+
# plane-grouped view; cheaper than iterating every block.
|
|
16
|
+
class PlaneEmitter
|
|
17
|
+
include Ucode::Repo::AtomicWrites
|
|
18
|
+
|
|
19
|
+
# @param face_dir [String, Pathname]
|
|
20
|
+
# @param plane [Models::Audit::PlaneSummary]
|
|
21
|
+
# @return [Boolean] true if written, false if skipped
|
|
22
|
+
def emit(face_dir, plane)
|
|
23
|
+
write_atomic(Paths.plane_under(face_dir, plane.plane),
|
|
24
|
+
to_pretty_json(plane.to_hash))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
require "ucode/repo/atomic_writes"
|
|
6
|
+
require "ucode/audit/emitter/paths"
|
|
7
|
+
|
|
8
|
+
module Ucode
|
|
9
|
+
module Audit
|
|
10
|
+
module Emitter
|
|
11
|
+
# Writes `<face_dir>/scripts/<CODE>.json` — one rollup per ISO
|
|
12
|
+
# 15924 script code (Latn, Grek, Hani, …).
|
|
13
|
+
#
|
|
14
|
+
# The browser fetches these when the user switches to a
|
|
15
|
+
# script-grouped view; cheaper than iterating every block.
|
|
16
|
+
class ScriptEmitter
|
|
17
|
+
include Ucode::Repo::AtomicWrites
|
|
18
|
+
|
|
19
|
+
# @param face_dir [String, Pathname]
|
|
20
|
+
# @param script [Models::Audit::ScriptSummary]
|
|
21
|
+
# @return [Boolean] true if written, false if skipped
|
|
22
|
+
def emit(face_dir, script)
|
|
23
|
+
write_atomic(Paths.script_under(face_dir, script.script_code),
|
|
24
|
+
to_pretty_json(script.to_hash))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ucode
|
|
4
|
+
module Audit
|
|
5
|
+
# Mode 2 output writers: turn an in-memory {Models::Audit::AuditReport}
|
|
6
|
+
# (or {Models::Audit::LibrarySummary}) into the on-disk directory tree
|
|
7
|
+
# documented in `TODO.new/03-directory-output-spec.md`.
|
|
8
|
+
#
|
|
9
|
+
# The emitter layer is pure I/O — no audit logic, no font parsing. Every
|
|
10
|
+
# emitter writes one chunk kind and is idempotent via
|
|
11
|
+
# {Ucode::Repo::AtomicWrites} (content-hash compare, then atomic rename).
|
|
12
|
+
#
|
|
13
|
+
# Top-level orchestrator: {Emitter::FaceDirectory}. Per-chunk emitters
|
|
14
|
+
# are wired together by it; callers should never instantiate the chunk
|
|
15
|
+
# emitters directly.
|
|
16
|
+
module Emitter
|
|
17
|
+
autoload :Paths, "ucode/audit/emitter/paths"
|
|
18
|
+
autoload :IndexEmitter, "ucode/audit/emitter/index_emitter"
|
|
19
|
+
autoload :BlockEmitter, "ucode/audit/emitter/block_emitter"
|
|
20
|
+
autoload :PlaneEmitter, "ucode/audit/emitter/plane_emitter"
|
|
21
|
+
autoload :ScriptEmitter, "ucode/audit/emitter/script_emitter"
|
|
22
|
+
autoload :CodepointEmitter, "ucode/audit/emitter/codepoint_emitter"
|
|
23
|
+
autoload :GlyphEmitter, "ucode/audit/emitter/glyph_emitter"
|
|
24
|
+
autoload :CollectionEmitter, "ucode/audit/emitter/collection_emitter"
|
|
25
|
+
autoload :LibraryEmitter, "ucode/audit/emitter/library_emitter"
|
|
26
|
+
autoload :FaceDirectory, "ucode/audit/emitter/face_directory"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -18,6 +18,12 @@ module Ucode
|
|
|
18
18
|
# ucode's own SQLite-backed Database. The Database exposes
|
|
19
19
|
# `lookup_block`, `lookup_script`, `block_ranges_by_name`, and
|
|
20
20
|
# `script_ranges_by_name` — those power every aggregation here.
|
|
21
|
+
#
|
|
22
|
+
# TODO 25: the BlockAggregator now takes a {CoverageReference}
|
|
23
|
+
# rather than a raw Database. The Context supplies one —
|
|
24
|
+
# UcdOnlyReference by default, UniversalSetReference when a
|
|
25
|
+
# universal-set manifest is supplied via the CLI
|
|
26
|
+
# (`--reference-universal-set=<path>`).
|
|
21
27
|
class Aggregations < Base
|
|
22
28
|
# @param context [Ucode::Audit::Context]
|
|
23
29
|
# @return [Hash{Symbol=>Object}]
|
|
@@ -26,14 +32,15 @@ module Ucode
|
|
|
26
32
|
return empty_with_warning(baseline) unless baseline.available?
|
|
27
33
|
|
|
28
34
|
codepoints = context.codepoints
|
|
29
|
-
|
|
35
|
+
reference = context.reference
|
|
36
|
+
blocks = BlockAggregator.new(reference).call(codepoints)
|
|
30
37
|
scripts = ScriptAggregator.new(baseline.database).call(codepoints)
|
|
31
38
|
planes = PlaneAggregator.new.call(blocks)
|
|
32
39
|
discrepancies = DiscrepancyDetector.new(**os2_args(context))
|
|
33
40
|
.call
|
|
34
41
|
|
|
35
42
|
{
|
|
36
|
-
baseline: baseline
|
|
43
|
+
baseline: baseline_metadata(baseline, reference),
|
|
37
44
|
blocks: blocks,
|
|
38
45
|
scripts: scripts,
|
|
39
46
|
plane_summaries: planes,
|
|
@@ -43,6 +50,28 @@ module Ucode
|
|
|
43
50
|
|
|
44
51
|
private
|
|
45
52
|
|
|
53
|
+
# Merge reference provenance (e.g. source_config_sha256,
|
|
54
|
+
# reference_kind) into the baseline metadata so the report's
|
|
55
|
+
# `baseline` block self-describes which reference produced
|
|
56
|
+
# the per-block counts. For UcdOnlyReference this is a no-op.
|
|
57
|
+
def baseline_metadata(baseline, reference)
|
|
58
|
+
return baseline.metadata unless reference.is_a?(UniversalSetReference)
|
|
59
|
+
|
|
60
|
+
merge_universal_set_metadata(baseline.metadata, reference)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def merge_universal_set_metadata(metadata, reference)
|
|
64
|
+
extra = reference.baseline_metadata
|
|
65
|
+
metadata.class.new(
|
|
66
|
+
unicode_version: extra["unicode_version"] || metadata.unicode_version,
|
|
67
|
+
ucode_version: extra["ucode_version"] || metadata.ucode_version,
|
|
68
|
+
fontisan_version: metadata.fontisan_version,
|
|
69
|
+
source: metadata.source,
|
|
70
|
+
generated_at: metadata.generated_at,
|
|
71
|
+
reference_kind: "universal-set",
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
46
75
|
def empty_with_warning(baseline)
|
|
47
76
|
{
|
|
48
77
|
baseline: baseline.metadata,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fontisan"
|
|
4
|
+
|
|
5
|
+
module Ucode
|
|
6
|
+
module Audit
|
|
7
|
+
# Per-face orchestrator: takes a font path, runs every Extractor in
|
|
8
|
+
# the {Registry}, and assembles a single {Models::Audit::AuditReport}.
|
|
9
|
+
#
|
|
10
|
+
# For standalone fonts (TTF/OTF/WOFF/WOFF2) #call returns one
|
|
11
|
+
# AuditReport. For collections (TTC/OTC/dfont) it returns
|
|
12
|
+
# Array<AuditReport> — one per face, in source order.
|
|
13
|
+
#
|
|
14
|
+
# Extracted as its own class so {LibraryAuditor} (per-file iteration)
|
|
15
|
+
# and the future CLI AuditCommand (single face) share one orchestration
|
|
16
|
+
# path. Neither caller enumerates extractors directly — they go
|
|
17
|
+
# through this class and the {Registry}.
|
|
18
|
+
class FaceAuditor
|
|
19
|
+
# @param font_path [String, Pathname] font file to audit
|
|
20
|
+
# @param options [Hash{Symbol=>Object}] forwarded to {Context}
|
|
21
|
+
# (ucd_version, all_codepoints, with_glyphs, audit_brief, …)
|
|
22
|
+
# @param mode [Symbol] :full (default) or :brief
|
|
23
|
+
# @param font_index [Integer, nil] when set and the source is a
|
|
24
|
+
# collection (TTC/OTC/dfong), audit only that face index and
|
|
25
|
+
# return a single AuditReport. Ignored for single-face sources.
|
|
26
|
+
# @param reference [CoverageReference, nil] the baseline the
|
|
27
|
+
# audit compares against. When nil, defaults to UCD-only
|
|
28
|
+
# (TODO 25). Pass a {UniversalSetReference} to attach
|
|
29
|
+
# per-codepoint provenance to missing-codepoint rows.
|
|
30
|
+
def initialize(font_path, options: {}, mode: :full, font_index: nil,
|
|
31
|
+
reference: nil)
|
|
32
|
+
@font_path = font_path.to_s
|
|
33
|
+
@options = options
|
|
34
|
+
@mode = mode
|
|
35
|
+
@font_index = font_index
|
|
36
|
+
@reference = reference
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [Models::Audit::AuditReport, Array<Models::Audit::AuditReport>]
|
|
40
|
+
def call
|
|
41
|
+
if Fontisan::FontLoader.collection?(@font_path)
|
|
42
|
+
audit_collection
|
|
43
|
+
else
|
|
44
|
+
audit_face(load_face(0), 0, 1)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def audit_collection
|
|
51
|
+
collection = Fontisan::FontLoader.load_collection(@font_path)
|
|
52
|
+
num = collection.num_fonts
|
|
53
|
+
indices = @font_index ? [@font_index] : (0...num).to_a
|
|
54
|
+
results = indices.map do |index|
|
|
55
|
+
font = Fontisan::FontLoader.load(@font_path, font_index: index)
|
|
56
|
+
audit_face(font, index, num)
|
|
57
|
+
end
|
|
58
|
+
@font_index ? results.first : results
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def audit_face(font, font_index, num_fonts_in_source)
|
|
62
|
+
context = Context.new(
|
|
63
|
+
font: font,
|
|
64
|
+
font_path: @font_path,
|
|
65
|
+
font_index: font_index,
|
|
66
|
+
num_fonts_in_source: num_fonts_in_source,
|
|
67
|
+
options: @options,
|
|
68
|
+
reference: @reference,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
fields = {}
|
|
72
|
+
Registry.each(mode: @mode) do |extractor_class|
|
|
73
|
+
fields.merge!(extractor_class.new.extract(context))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
fields[:warning] = context.baseline.warning
|
|
77
|
+
|
|
78
|
+
Models::Audit::AuditReport.new(**fields)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def load_face(_index)
|
|
82
|
+
Fontisan::FontLoader.load(@font_path)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ucode
|
|
4
|
+
module Audit
|
|
5
|
+
module Formatters
|
|
6
|
+
# Human-readable diff of two {Models::Audit::AuditReport}s.
|
|
7
|
+
#
|
|
8
|
+
# Output groups changes by kind: scalar field changes, codepoint
|
|
9
|
+
# set deltas (added/removed counts and a preview of the ranges),
|
|
10
|
+
# then structural inventory changes (scripts, features, blocks).
|
|
11
|
+
# Empty sections are omitted so a no-op diff prints only the
|
|
12
|
+
# header and a "(no differences)" footer.
|
|
13
|
+
#
|
|
14
|
+
# ucode delta vs fontisan's AuditDiffTextRenderer: drops the
|
|
15
|
+
# LANGUAGES section (CLDR is out of scope).
|
|
16
|
+
class AuditDiffText
|
|
17
|
+
SEPARATOR = "=" * 80
|
|
18
|
+
LIST_LIMIT = 10
|
|
19
|
+
|
|
20
|
+
# @param diff [Models::Audit::AuditDiff]
|
|
21
|
+
def initialize(diff)
|
|
22
|
+
@diff = diff
|
|
23
|
+
@lines = []
|
|
24
|
+
@helper = TextFormatter.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [String]
|
|
28
|
+
def render
|
|
29
|
+
render_header
|
|
30
|
+
render_field_changes
|
|
31
|
+
render_codepoint_delta
|
|
32
|
+
render_structural_changes
|
|
33
|
+
render_empty_note
|
|
34
|
+
@lines.join("\n")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def render_header
|
|
40
|
+
@lines << Color.bold("AUDIT DIFF")
|
|
41
|
+
@lines << Color.dim(SEPARATOR)
|
|
42
|
+
@lines << " left: #{@diff.left_source}"
|
|
43
|
+
@lines << " right: #{@diff.right_source}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def render_field_changes
|
|
47
|
+
changes = Array(@diff.field_changes)
|
|
48
|
+
return if changes.empty?
|
|
49
|
+
|
|
50
|
+
section("FIELD CHANGES (#{changes.size})")
|
|
51
|
+
changes.each do |change|
|
|
52
|
+
@lines << " #{change.field}: #{change.left.inspect} → #{change.right.inspect}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def render_codepoint_delta
|
|
57
|
+
delta = @diff.codepoints
|
|
58
|
+
return unless delta && (delta.added_count.to_i.positive? || delta.removed_count.to_i.positive?)
|
|
59
|
+
|
|
60
|
+
section("CODEPOINT COVERAGE")
|
|
61
|
+
@lines << " added: #{delta.added_count}"
|
|
62
|
+
@lines << " removed: #{delta.removed_count}"
|
|
63
|
+
@lines << " unchanged: #{delta.unchanged_count}"
|
|
64
|
+
preview_added(delta)
|
|
65
|
+
preview_removed(delta)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def preview_added(delta)
|
|
69
|
+
ranges = Array(delta.added)
|
|
70
|
+
return if ranges.empty?
|
|
71
|
+
|
|
72
|
+
@lines << " + #{@helper.truncate_ranges(ranges)}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def preview_removed(delta)
|
|
76
|
+
ranges = Array(delta.removed)
|
|
77
|
+
return if ranges.empty?
|
|
78
|
+
|
|
79
|
+
@lines << " - #{@helper.truncate_ranges(ranges)}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def render_structural_changes
|
|
83
|
+
render_set("SCRIPTS", @diff.added_scripts, @diff.removed_scripts)
|
|
84
|
+
render_set("FEATURES", @diff.added_features, @diff.removed_features)
|
|
85
|
+
render_set("BLOCKS", @diff.added_blocks, @diff.removed_blocks)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def render_set(name, added, removed)
|
|
89
|
+
added = Array(added)
|
|
90
|
+
removed = Array(removed)
|
|
91
|
+
return if added.empty? && removed.empty?
|
|
92
|
+
|
|
93
|
+
section("#{name} CHANGES")
|
|
94
|
+
@lines << " + #{@helper.truncate_list(added)}" unless added.empty?
|
|
95
|
+
@lines << " - #{@helper.truncate_list(removed)}" unless removed.empty?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def render_empty_note
|
|
99
|
+
return unless @diff.empty?
|
|
100
|
+
|
|
101
|
+
@lines << ""
|
|
102
|
+
@lines << "(no differences)"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def section(title)
|
|
106
|
+
@lines << ""
|
|
107
|
+
@lines << Color.bold(title)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|