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,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
require "ucode/audit"
|
|
7
|
+
require "ucode/audit/differ"
|
|
8
|
+
require "ucode/audit/face_auditor"
|
|
9
|
+
require "ucode/audit/formatters"
|
|
10
|
+
require "ucode/glyphs/real_fonts"
|
|
11
|
+
|
|
12
|
+
module Ucode
|
|
13
|
+
module Commands
|
|
14
|
+
module Audit
|
|
15
|
+
# `ucode audit compare LEFT RIGHT` — diff two audits.
|
|
16
|
+
#
|
|
17
|
+
# Each of LEFT and RIGHT can be:
|
|
18
|
+
# - A path to a font file (audited on-the-fly with
|
|
19
|
+
# {Audit::FaceAuditor}).
|
|
20
|
+
# - A path to a face audit directory — its `index.json` is
|
|
21
|
+
# read for the precomputed report shape.
|
|
22
|
+
# - A path to a saved `index.json` file directly.
|
|
23
|
+
#
|
|
24
|
+
# Note: reading from disk only recovers the *derived* overview
|
|
25
|
+
# shape from {Emitter::IndexEmitter}, not a full AuditReport.
|
|
26
|
+
# The compare therefore uses the field subset that the
|
|
27
|
+
# overview shape preserves (identity + coverage totals). For a
|
|
28
|
+
# full-feature diff, audit both inputs fresh from their font
|
|
29
|
+
# paths.
|
|
30
|
+
class CompareCommand
|
|
31
|
+
Result = Struct.new(:left_source, :right_source, :diff, :text,
|
|
32
|
+
:error, keyword_init: true)
|
|
33
|
+
|
|
34
|
+
# @param left [String] font path | audit dir | index.json path
|
|
35
|
+
# @param right [String] same forms as left
|
|
36
|
+
# @param unicode_version [String, nil]
|
|
37
|
+
# @param output_file [String, Pathname, nil] write text to file
|
|
38
|
+
# (default: stdout, captured as `text` in the result)
|
|
39
|
+
# @return [Result]
|
|
40
|
+
def call(left, right, unicode_version: nil, output_file: nil)
|
|
41
|
+
left_report = load_or_audit(left, unicode_version: unicode_version)
|
|
42
|
+
right_report = load_or_audit(right, unicode_version: unicode_version)
|
|
43
|
+
|
|
44
|
+
diff = Ucode::Audit::Differ.new(left_report, right_report).diff
|
|
45
|
+
text = Ucode::Audit::Formatters::AuditDiffText.new(diff).render
|
|
46
|
+
|
|
47
|
+
write_output(text, output_file) if output_file
|
|
48
|
+
|
|
49
|
+
Result.new(left_source: left, right_source: right, diff: diff,
|
|
50
|
+
text: text)
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
Result.new(left_source: left, right_source: right,
|
|
53
|
+
error: "#{e.class}: #{e.message}")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def load_or_audit(spec, unicode_version:)
|
|
59
|
+
case resolve_kind(spec)
|
|
60
|
+
when :font_file then audit_freshly(spec, unicode_version: unicode_version)
|
|
61
|
+
when :audit_dir then load_overview(Pathname.new(spec).join("index.json"))
|
|
62
|
+
when :index_json then load_overview(Pathname.new(spec))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Heuristic: a path is an index.json if it ends in `.json`;
|
|
67
|
+
# an audit directory if it doesn't end in `.json` and isn't
|
|
68
|
+
# a font file; a font file otherwise.
|
|
69
|
+
def resolve_kind(spec)
|
|
70
|
+
path = Pathname.new(spec)
|
|
71
|
+
return :index_json if path.file? && path.extname == ".json"
|
|
72
|
+
return :audit_dir if path.directory?
|
|
73
|
+
return :audit_dir if path.join("index.json").exist?
|
|
74
|
+
|
|
75
|
+
:font_file
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def audit_freshly(font_path, unicode_version:)
|
|
79
|
+
options = {}
|
|
80
|
+
options[:ucd_version] = unicode_version if unicode_version
|
|
81
|
+
Ucode::Audit::FaceAuditor.new(font_path.to_s, options: options).call
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Reconstructs a partial AuditReport from the derived overview
|
|
85
|
+
# shape. Only the fields Differ consults are populated; others
|
|
86
|
+
# are nil. This is best-effort — see class docs.
|
|
87
|
+
def load_overview(index_json_path)
|
|
88
|
+
hash = JSON.parse(index_json_path.read)
|
|
89
|
+
font = hash["font"] || {}
|
|
90
|
+
totals = hash["totals"] || {}
|
|
91
|
+
Models::Audit::AuditReport.new(
|
|
92
|
+
source_file: font["source_file"],
|
|
93
|
+
source_sha256: font["source_sha256"],
|
|
94
|
+
family_name: font["family_name"],
|
|
95
|
+
subfamily_name: font["subfamily_name"],
|
|
96
|
+
full_name: font["full_name"],
|
|
97
|
+
postscript_name: font["postscript_name"],
|
|
98
|
+
version: font["version"],
|
|
99
|
+
font_revision: font["font_revision"],
|
|
100
|
+
weight_class: font["weight_class"],
|
|
101
|
+
width_class: font["width_class"],
|
|
102
|
+
italic: font["italic"],
|
|
103
|
+
bold: font["bold"],
|
|
104
|
+
panose: font["panose"],
|
|
105
|
+
total_codepoints: totals["covered_codepoints_total"] || font["total_codepoints"],
|
|
106
|
+
total_glyphs: font["total_glyphs"],
|
|
107
|
+
codepoint_ranges: codepoint_ranges(font["codepoint_ranges"]),
|
|
108
|
+
scripts: scripts(hash["script_summaries"]),
|
|
109
|
+
blocks: blocks(hash["block_summaries"]),
|
|
110
|
+
baseline: baseline(hash["baseline"]),
|
|
111
|
+
plane_summaries: plane_summaries(hash["plane_summaries"]),
|
|
112
|
+
discrepancies: [],
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def codepoint_ranges(arr)
|
|
117
|
+
return [] unless arr
|
|
118
|
+
|
|
119
|
+
arr.map do |h|
|
|
120
|
+
Models::Audit::CodepointRange.new(
|
|
121
|
+
first_cp: h["first_cp"], last_cp: h["last_cp"],
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def scripts(arr)
|
|
127
|
+
return [] unless arr
|
|
128
|
+
|
|
129
|
+
arr.map do |h|
|
|
130
|
+
Models::Audit::ScriptSummary.new(
|
|
131
|
+
script_code: h["script_code"], script_name: h["script_name"],
|
|
132
|
+
blocks_total: h["blocks_total"], assigned_total: h["assigned_total"],
|
|
133
|
+
covered_total: h["covered_total"], coverage_percent: h["coverage_percent"],
|
|
134
|
+
status: h["status"],
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def blocks(arr)
|
|
140
|
+
return [] unless arr
|
|
141
|
+
|
|
142
|
+
arr.map do |h|
|
|
143
|
+
Models::Audit::BlockSummary.new(
|
|
144
|
+
name: h["name"], first_cp: h["first_cp"], last_cp: h["last_cp"],
|
|
145
|
+
range: h["range"], plane: h["plane"],
|
|
146
|
+
total_assigned: h["total_assigned"],
|
|
147
|
+
covered_count: h["covered_count"],
|
|
148
|
+
missing_count: h["missing_count"],
|
|
149
|
+
coverage_percent: h["coverage_percent"],
|
|
150
|
+
status: h["status"],
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def baseline(hash)
|
|
156
|
+
return nil unless hash
|
|
157
|
+
|
|
158
|
+
Models::Audit::Baseline.new(
|
|
159
|
+
unicode_version: hash["unicode_version"],
|
|
160
|
+
ucode_version: hash["ucode_version"],
|
|
161
|
+
fontisan_version: hash["fontisan_version"],
|
|
162
|
+
source: hash["source"],
|
|
163
|
+
generated_at: hash["generated_at"],
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def plane_summaries(arr)
|
|
168
|
+
return [] unless arr
|
|
169
|
+
|
|
170
|
+
arr.map do |h|
|
|
171
|
+
Models::Audit::PlaneSummary.new(
|
|
172
|
+
plane: h["plane"], blocks_total: h["blocks_total"],
|
|
173
|
+
assigned_total: h["assigned_total"],
|
|
174
|
+
covered_total: h["covered_total"],
|
|
175
|
+
coverage_percent: h["coverage_percent"],
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def write_output(text, target)
|
|
181
|
+
path = Pathname.new(target)
|
|
182
|
+
path.dirname.mkpath
|
|
183
|
+
path.write(text)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
require "ucode/glyphs/real_fonts"
|
|
7
|
+
require "ucode/audit"
|
|
8
|
+
require "ucode/audit/face_auditor"
|
|
9
|
+
require "ucode/audit/emitter/face_directory"
|
|
10
|
+
require "ucode/audit/emitter/paths"
|
|
11
|
+
|
|
12
|
+
module Ucode
|
|
13
|
+
module Commands
|
|
14
|
+
module Audit
|
|
15
|
+
# `ucode audit font PATH` — audit a single font file or
|
|
16
|
+
# fontist-resolvable name. Writes the per-face directory tree
|
|
17
|
+
# under `<output_root>/font_audit/<label>/`.
|
|
18
|
+
#
|
|
19
|
+
# Auto-detects collection sources (TTC/OTC/dfong) and falls
|
|
20
|
+
# through to collection-style emission in that case — one tree
|
|
21
|
+
# per face. For an explicit, intent-revealing form, use
|
|
22
|
+
# {CollectionCommand}.
|
|
23
|
+
#
|
|
24
|
+
# Pure: Thor never touches this. Real work is delegated to
|
|
25
|
+
# {Glyphs::RealFonts::FontLocator} (resolve spec → path),
|
|
26
|
+
# {Audit::FaceAuditor} (build report), and
|
|
27
|
+
# {Audit::Emitter::FaceDirectory} (write tree).
|
|
28
|
+
class FontCommand
|
|
29
|
+
FaceOutcome = Struct.new(:label, :postscript_name, :output_dir,
|
|
30
|
+
keyword_init: true)
|
|
31
|
+
|
|
32
|
+
Result = Struct.new(:spec, :label, :output_dir, :faces, :error,
|
|
33
|
+
keyword_init: true)
|
|
34
|
+
|
|
35
|
+
# @param spec [String] font spec — direct path, or `label=path`,
|
|
36
|
+
# or a fontist formula name.
|
|
37
|
+
# @param label [String, nil] output label override. Defaults
|
|
38
|
+
# to the report's postscript_name, or the file basename.
|
|
39
|
+
# @param unicode_version [String, nil] baseline UCD version.
|
|
40
|
+
# @param verbose [Boolean] emit per-codepoint detail chunks.
|
|
41
|
+
# @param with_glyphs [Boolean] emit per-codepoint SVG chunks
|
|
42
|
+
# (no-op until TODO 20 wires the 4-tier resolver).
|
|
43
|
+
# @param brief [Boolean] cheap-extractor-only mode.
|
|
44
|
+
# @param output_root [String, Pathname] parent directory; the
|
|
45
|
+
# audit root is `<output_root>/font_audit`.
|
|
46
|
+
# @param browse [Boolean] also write the HTML browsers.
|
|
47
|
+
# @param install [Boolean] allow fontist install on miss.
|
|
48
|
+
# @param reference [Ucode::Audit::CoverageReference, nil] the
|
|
49
|
+
# baseline to compare against (TODO 25). When nil, defaults
|
|
50
|
+
# to UCD-only inside {FaceAuditor}.
|
|
51
|
+
# @param universal_set_root [String, Pathname, nil] forwarded
|
|
52
|
+
# to {Emitter::FaceDirectory} for the face browser's
|
|
53
|
+
# universal-set section (TODO 26).
|
|
54
|
+
# @param with_missing_glyph_pages [Boolean] forward per-block
|
|
55
|
+
# standalone missing-glyph galleries (TODO 26).
|
|
56
|
+
# @return [Result]
|
|
57
|
+
def call(spec, output_root:, label: nil, unicode_version: nil, verbose: false,
|
|
58
|
+
with_glyphs: false, brief: false, browse: false,
|
|
59
|
+
install: true, reference: nil,
|
|
60
|
+
universal_set_root: nil, with_missing_glyph_pages: false)
|
|
61
|
+
located = locate(spec, install: install)
|
|
62
|
+
reports = Array(audit_faces(located.path, unicode_version: unicode_version,
|
|
63
|
+
brief: brief, reference: reference))
|
|
64
|
+
|
|
65
|
+
face_label = label || derived_face_label(reports.first, located)
|
|
66
|
+
sanitized = sanitize(face_label)
|
|
67
|
+
|
|
68
|
+
directory = Ucode::Audit::Emitter::FaceDirectory.new(
|
|
69
|
+
output_root: output_root,
|
|
70
|
+
verbose: verbose,
|
|
71
|
+
with_glyphs: with_glyphs,
|
|
72
|
+
emit_browser: browse,
|
|
73
|
+
universal_set_root: universal_set_root,
|
|
74
|
+
with_missing_glyph_pages: with_missing_glyph_pages,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
face_outcomes, top_dir =
|
|
78
|
+
if reports.one?
|
|
79
|
+
emit_standalone(directory, sanitized, reports.first)
|
|
80
|
+
else
|
|
81
|
+
emit_collection(directory, sanitized, reports, output_root)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
Result.new(spec: spec, label: face_label, output_dir: top_dir.to_s,
|
|
85
|
+
faces: face_outcomes)
|
|
86
|
+
rescue StandardError => e
|
|
87
|
+
Result.new(spec: spec, error: "#{e.class}: #{e.message}")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def locate(spec, install:)
|
|
93
|
+
Ucode::Glyphs::RealFonts::FontLocator.new.locate(spec, install: install)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def audit_faces(path, unicode_version:, brief:, reference: nil)
|
|
97
|
+
options = audit_options(unicode_version: unicode_version, brief: brief)
|
|
98
|
+
mode = brief ? :brief : :full
|
|
99
|
+
Ucode::Audit::FaceAuditor.new(path, options: options, mode: mode,
|
|
100
|
+
reference: reference).call
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def audit_options(unicode_version:, brief:)
|
|
104
|
+
opts = {}
|
|
105
|
+
opts[:ucd_version] = unicode_version if unicode_version
|
|
106
|
+
opts[:audit_brief] = true if brief
|
|
107
|
+
opts
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def derived_face_label(report, located)
|
|
111
|
+
report&.postscript_name || located.name || File.basename(located.path.to_s, ".*")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def emit_standalone(directory, label, report)
|
|
115
|
+
face_dir = directory.emit_face(label: label, report: report)
|
|
116
|
+
outcome = FaceOutcome.new(label: label,
|
|
117
|
+
postscript_name: report.postscript_name,
|
|
118
|
+
output_dir: face_dir.to_s)
|
|
119
|
+
[[outcome], face_dir]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def emit_collection(directory, source_label, reports, output_root)
|
|
123
|
+
subdirs = directory.emit_collection(source_label: source_label,
|
|
124
|
+
reports: reports)
|
|
125
|
+
top = Ucode::Audit::Emitter::Paths.face_dir(output_root, source_label)
|
|
126
|
+
outcomes = reports.zip(subdirs).map do |report, name|
|
|
127
|
+
FaceOutcome.new(label: name,
|
|
128
|
+
postscript_name: report.postscript_name,
|
|
129
|
+
output_dir: top.join(name).to_s)
|
|
130
|
+
end
|
|
131
|
+
[outcomes, top]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def sanitize(name)
|
|
135
|
+
(name || "face").to_s.gsub(/[^A-Za-z0-9._-]/, "_")
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
require "ucode/audit"
|
|
6
|
+
require "ucode/audit/library_auditor"
|
|
7
|
+
require "ucode/audit/emitter/face_directory"
|
|
8
|
+
require "ucode/audit/emitter/paths"
|
|
9
|
+
|
|
10
|
+
module Ucode
|
|
11
|
+
module Commands
|
|
12
|
+
module Audit
|
|
13
|
+
# `ucode audit library DIR` — walk a directory of fonts and
|
|
14
|
+
# produce one per-face audit plus a library-level rollup.
|
|
15
|
+
#
|
|
16
|
+
# Delegates directory walking + per-face audit to
|
|
17
|
+
# {Audit::LibraryAuditor}, then writes the per-face trees +
|
|
18
|
+
# library-level `index.json` via {Audit::Emitter::FaceDirectory}.
|
|
19
|
+
class LibraryCommand
|
|
20
|
+
SkippedFile = Struct.new(:path, :reason, keyword_init: true)
|
|
21
|
+
|
|
22
|
+
Result = Struct.new(:root, :total_files, :total_faces, :output_dir,
|
|
23
|
+
:skipped, :error, keyword_init: true)
|
|
24
|
+
|
|
25
|
+
# @param dir [String, Pathname] directory containing fonts.
|
|
26
|
+
# @param recursive [Boolean] walk subdirectories.
|
|
27
|
+
# @param unicode_version [String, nil] baseline UCD version.
|
|
28
|
+
# @param verbose [Boolean] per-codepoint detail chunks per face.
|
|
29
|
+
# @param with_glyphs [Boolean] per-codepoint SVG chunks.
|
|
30
|
+
# @param brief [Boolean] cheap-extractor-only mode.
|
|
31
|
+
# @param output_root [String, Pathname] parent of the audit root.
|
|
32
|
+
# @param browse [Boolean] also write library + face HTML browsers.
|
|
33
|
+
# @param reference [Ucode::Audit::CoverageReference, nil] baseline
|
|
34
|
+
# forwarded to every per-face audit (TODO 25).
|
|
35
|
+
# @param universal_set_root [String, Pathname, nil] forwarded to
|
|
36
|
+
# {Emitter::FaceDirectory} for the face browser's
|
|
37
|
+
# universal-set section (TODO 26).
|
|
38
|
+
# @param with_missing_glyph_pages [Boolean] emit per-block
|
|
39
|
+
# standalone missing-glyph galleries per face (TODO 26).
|
|
40
|
+
# @return [Result]
|
|
41
|
+
def call(dir, output_root:, recursive: false, unicode_version: nil, verbose: false,
|
|
42
|
+
with_glyphs: false, brief: false, browse: false, reference: nil,
|
|
43
|
+
universal_set_root: nil, with_missing_glyph_pages: false)
|
|
44
|
+
options = library_options(unicode_version: unicode_version, brief: brief)
|
|
45
|
+
auditor = Ucode::Audit::LibraryAuditor.new(dir, recursive: recursive,
|
|
46
|
+
options: options,
|
|
47
|
+
reference: reference)
|
|
48
|
+
summary = auditor.audit
|
|
49
|
+
|
|
50
|
+
directory = Ucode::Audit::Emitter::FaceDirectory.new(
|
|
51
|
+
output_root: output_root,
|
|
52
|
+
verbose: verbose,
|
|
53
|
+
with_glyphs: with_glyphs,
|
|
54
|
+
emit_browser: browse,
|
|
55
|
+
universal_set_root: universal_set_root,
|
|
56
|
+
with_missing_glyph_pages: with_missing_glyph_pages,
|
|
57
|
+
)
|
|
58
|
+
directory.emit_library(summary: summary)
|
|
59
|
+
|
|
60
|
+
Result.new(
|
|
61
|
+
root: dir.to_s,
|
|
62
|
+
total_files: summary.total_files,
|
|
63
|
+
total_faces: summary.total_faces,
|
|
64
|
+
output_dir: Ucode::Audit::Emitter::Paths.library_root(output_root).to_s,
|
|
65
|
+
skipped: auditor.skipped.map { |s| parse_skipped(s) },
|
|
66
|
+
)
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
Result.new(root: dir.to_s, error: "#{e.class}: #{e.message}")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def library_options(unicode_version:, brief:)
|
|
74
|
+
opts = {}
|
|
75
|
+
opts[:ucd_version] = unicode_version if unicode_version
|
|
76
|
+
opts[:audit_brief] = true if brief
|
|
77
|
+
opts
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def parse_skipped(entry)
|
|
81
|
+
path, _, reason = entry.rpartition(": ")
|
|
82
|
+
SkippedFile.new(path: path, reason: reason)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module Ucode
|
|
6
|
+
module Commands
|
|
7
|
+
module Audit
|
|
8
|
+
# Translates CLI flags into a {Ucode::Audit::CoverageReference}.
|
|
9
|
+
#
|
|
10
|
+
# The audit CLI exposes the universal-set reference via a
|
|
11
|
+
# `--reference-universal-set=<path>` flag (and a default lookup
|
|
12
|
+
# at `output/universal_glyph_set/manifest.json`). This builder
|
|
13
|
+
# resolves the flag value into a concrete reference instance
|
|
14
|
+
# backed by a freshly-opened {Ucode::Database}, so the command
|
|
15
|
+
# classes don't repeat the same branching.
|
|
16
|
+
#
|
|
17
|
+
# Behavior:
|
|
18
|
+
#
|
|
19
|
+
# - flag = "none" → nil (force UCD-only even if a default manifest exists)
|
|
20
|
+
# - flag = path to .json → {Ucode::Audit::UniversalSetReference}
|
|
21
|
+
# - flag = nil → look at DEFAULT_MANIFEST_PATH; use it if present,
|
|
22
|
+
# else nil (UCD-only)
|
|
23
|
+
module ReferenceBuilder
|
|
24
|
+
DEFAULT_MANIFEST_PATH = Pathname.new("output/universal_glyph_set/manifest.json")
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# @param flag [String, nil] value of the
|
|
29
|
+
# `--reference-universal-set` CLI option.
|
|
30
|
+
# @param version [String, nil] UCD version for the database
|
|
31
|
+
# that backs the reference. When nil, the default UCD
|
|
32
|
+
# version is resolved.
|
|
33
|
+
# @return [Ucode::Audit::CoverageReference, nil]
|
|
34
|
+
def build(flag:, version: nil)
|
|
35
|
+
return nil if flag == "none"
|
|
36
|
+
|
|
37
|
+
path = resolve_manifest_path(flag)
|
|
38
|
+
return nil unless path && File.exist?(path)
|
|
39
|
+
|
|
40
|
+
database = open_database(version)
|
|
41
|
+
return nil unless database
|
|
42
|
+
|
|
43
|
+
Ucode::Audit::UniversalSetReference.new(
|
|
44
|
+
manifest: path, database: database,
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def resolve_manifest_path(flag)
|
|
49
|
+
return Pathname.new(flag) if flag && flag != "none"
|
|
50
|
+
return DEFAULT_MANIFEST_PATH if DEFAULT_MANIFEST_PATH.exist?
|
|
51
|
+
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def open_database(version)
|
|
56
|
+
resolved = version || Ucode::VersionResolver.resolve(nil)
|
|
57
|
+
Ucode::Database.open(resolved)
|
|
58
|
+
rescue Ucode::UnknownVersionError, Ucode::DatabaseMissingError
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ucode
|
|
4
|
+
module Commands
|
|
5
|
+
# `ucode audit *` command classes. Pure Ruby — Thor (in
|
|
6
|
+
# `lib/ucode/cli.rb`) is responsible only for argument parsing
|
|
7
|
+
# and dispatch. Each class here is a structured-result command
|
|
8
|
+
# that delegates to the {Ucode::Audit} pipeline
|
|
9
|
+
# ({Audit::FaceAuditor}, {Audit::LibraryAuditor}, {Audit::Differ},
|
|
10
|
+
# {Audit::Emitter::FaceDirectory}, {Audit::Browser::*}).
|
|
11
|
+
module Audit
|
|
12
|
+
autoload :FontCommand, "ucode/commands/audit/font_command"
|
|
13
|
+
autoload :CollectionCommand, "ucode/commands/audit/collection_command"
|
|
14
|
+
autoload :LibraryCommand, "ucode/commands/audit/library_command"
|
|
15
|
+
autoload :CompareCommand, "ucode/commands/audit/compare_command"
|
|
16
|
+
autoload :BrowserCommand, "ucode/commands/audit/browser_command"
|
|
17
|
+
autoload :ReferenceBuilder, "ucode/commands/audit/reference_builder"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
require "ucode/repo"
|
|
6
|
+
require "ucode/version_resolver"
|
|
7
|
+
|
|
8
|
+
module Ucode
|
|
9
|
+
module Commands
|
|
10
|
+
# `ucode block-feed` — emit a compact per-block Unicode data feed
|
|
11
|
+
# from ucode's canonical output tree.
|
|
12
|
+
#
|
|
13
|
+
# Reads ucode's `output/` and produces three artifacts at the target
|
|
14
|
+
# directory:
|
|
15
|
+
#
|
|
16
|
+
# <target>/unicode-blocks.json
|
|
17
|
+
# <target>/unicode-version.json
|
|
18
|
+
# <target>/unicode/blocks/<slug>.json
|
|
19
|
+
#
|
|
20
|
+
# Each per-block file contains the codepoints in that block with
|
|
21
|
+
# their compact Unicode metadata (name, general category, script,
|
|
22
|
+
# combining class, bidi class, mirrored flag). Block slugs are
|
|
23
|
+
# derived from the block name via the standard slug algorithm.
|
|
24
|
+
class BlockFeedCommand
|
|
25
|
+
Result = Struct.new(:ucode_output_root, :block_feed_output_root,
|
|
26
|
+
:unicode_version, :blocks_written,
|
|
27
|
+
:codepoints_written, :unicode_blocks_path,
|
|
28
|
+
:unicode_version_path, keyword_init: true)
|
|
29
|
+
|
|
30
|
+
# @param ucode_output_root [String, Pathname] ucode's `output/`
|
|
31
|
+
# (must contain blocks/index.json, blocks/<ID>/index.json,
|
|
32
|
+
# index/labels.json).
|
|
33
|
+
# @param block_feed_output_root [String, Pathname] target directory.
|
|
34
|
+
# @param unicode_version [String, nil] UCD version to stamp on
|
|
35
|
+
# unicode-version.json. Defaults to the version recorded in
|
|
36
|
+
# ucode's manifest.json.
|
|
37
|
+
# @return [Result]
|
|
38
|
+
def call(ucode_output_root:, block_feed_output_root:, unicode_version: nil)
|
|
39
|
+
ucode_root = Pathname.new(ucode_output_root)
|
|
40
|
+
feed_root = Pathname.new(block_feed_output_root)
|
|
41
|
+
version = unicode_version || manifest_version(ucode_root)
|
|
42
|
+
|
|
43
|
+
emitter = Repo::BlockFeedEmitter.new(ucode_root, feed_root)
|
|
44
|
+
outcome = emitter.emit(ucd_version: version)
|
|
45
|
+
|
|
46
|
+
Result.new(
|
|
47
|
+
ucode_output_root: ucode_root.to_s,
|
|
48
|
+
block_feed_output_root: feed_root.to_s,
|
|
49
|
+
unicode_version: version,
|
|
50
|
+
blocks_written: outcome[:blocks_written],
|
|
51
|
+
codepoints_written: outcome[:codepoints_written],
|
|
52
|
+
unicode_blocks_path: outcome[:unicode_blocks_path],
|
|
53
|
+
unicode_version_path: outcome[:unicode_version_path],
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def manifest_version(ucode_root)
|
|
60
|
+
manifest = ucode_root.join("manifest.json")
|
|
61
|
+
return default_version unless manifest.exist?
|
|
62
|
+
|
|
63
|
+
JSON.parse(manifest.read)["ucd_version"] || default_version
|
|
64
|
+
rescue JSON::ParserError
|
|
65
|
+
default_version
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def default_version
|
|
69
|
+
VersionResolver.resolve(nil)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|