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,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
require "ucode/audit/emitter/paths"
|
|
6
|
+
require "ucode/audit/emitter/index_emitter"
|
|
7
|
+
require "ucode/audit/emitter/block_emitter"
|
|
8
|
+
require "ucode/audit/emitter/plane_emitter"
|
|
9
|
+
require "ucode/audit/emitter/script_emitter"
|
|
10
|
+
require "ucode/audit/emitter/codepoint_emitter"
|
|
11
|
+
require "ucode/audit/emitter/glyph_emitter"
|
|
12
|
+
require "ucode/audit/emitter/collection_emitter"
|
|
13
|
+
require "ucode/audit/emitter/library_emitter"
|
|
14
|
+
require "ucode/audit/browser/face_page"
|
|
15
|
+
require "ucode/audit/browser/library_page"
|
|
16
|
+
|
|
17
|
+
module Ucode
|
|
18
|
+
module Audit
|
|
19
|
+
module Emitter
|
|
20
|
+
# Top-level Mode 2 orchestrator. Walks an AuditReport (or a list
|
|
21
|
+
# of reports for a collection, or a library summary) and emits the
|
|
22
|
+
# full directory tree per `03-directory-output-spec.md`.
|
|
23
|
+
#
|
|
24
|
+
# Owns the chunk-emitter composition: callers never touch
|
|
25
|
+
# {IndexEmitter} / {BlockEmitter} / etc. directly. The chunk
|
|
26
|
+
# emitters themselves stay single-purpose (one chunk kind each)
|
|
27
|
+
# and take an explicit `face_dir` Pathname — they don't know
|
|
28
|
+
# whether the face lives at `output/font_audit/<label>/` or under
|
|
29
|
+
# a collection subdir.
|
|
30
|
+
#
|
|
31
|
+
# Three entry points:
|
|
32
|
+
#
|
|
33
|
+
# - {#emit_face} — one standalone face
|
|
34
|
+
# - {#emit_collection} — one TTC source (N sibling faces)
|
|
35
|
+
# - {#emit_library} — directory-mode (M face labels)
|
|
36
|
+
#
|
|
37
|
+
# Idempotency is delegated to each chunk emitter via
|
|
38
|
+
# {Ucode::Repo::AtomicWrites}; re-running the same audit produces
|
|
39
|
+
# zero writes on the second pass.
|
|
40
|
+
class FaceDirectory
|
|
41
|
+
# @param output_root [String, Pathname] top-level output root
|
|
42
|
+
# (e.g. "output"). The library root is `<output_root>/font_audit`.
|
|
43
|
+
# @param verbose [Boolean] emit codepoints/<NAME>.json per block
|
|
44
|
+
# @param with_glyphs [Boolean] emit glyphs/U+XXXX.svg per covered cp
|
|
45
|
+
# @param glyph_resolver [Proc(Integer) -> String, nil] SVG source
|
|
46
|
+
# for {GlyphEmitter}; defaults to a proc that returns nil
|
|
47
|
+
# (no glyphs emitted). Replaced by the canonical 4-tier
|
|
48
|
+
# resolver (TODO 20) when ready.
|
|
49
|
+
# @param database [Ucode::Database, nil] baseline UCD lookup for
|
|
50
|
+
# {CodepointEmitter} enrichment
|
|
51
|
+
# @param emit_browser [Boolean] also write the self-contained
|
|
52
|
+
# HTML browsers — `<face_dir>/index.html` per face and
|
|
53
|
+
# `<library_root>/index.html` for library mode. Default false.
|
|
54
|
+
# @param universal_set_root [String, Pathname, nil] root of a
|
|
55
|
+
# co-located universal-set build. When present and
|
|
56
|
+
# `emit_browser:` is true, the face browser advertises glyph
|
|
57
|
+
# paths in its overview JSON so missing-codepoint chips can
|
|
58
|
+
# render the universal-set glyph at runtime.
|
|
59
|
+
# @param with_missing_glyph_pages [Boolean] emit one standalone
|
|
60
|
+
# `<face_dir>/missing/<BLOCK>.html` per touched block with
|
|
61
|
+
# missing codepoints. Requires `emit_browser:` and a reachable
|
|
62
|
+
# `universal_set_root:` (silently no-ops otherwise).
|
|
63
|
+
def initialize(output_root:, verbose: false, with_glyphs: false,
|
|
64
|
+
glyph_resolver: GlyphEmitter::DEFAULT_RESOLVER,
|
|
65
|
+
database: nil, emit_browser: false,
|
|
66
|
+
universal_set_root: nil, with_missing_glyph_pages: false)
|
|
67
|
+
@output_root = output_root
|
|
68
|
+
@verbose = verbose
|
|
69
|
+
@with_glyphs = with_glyphs
|
|
70
|
+
@emit_browser = emit_browser
|
|
71
|
+
@database = database
|
|
72
|
+
@universal_set_root = universal_set_root
|
|
73
|
+
@with_missing_glyph_pages = with_missing_glyph_pages
|
|
74
|
+
@index_emitter = IndexEmitter.new
|
|
75
|
+
@block_emitter = BlockEmitter.new
|
|
76
|
+
@plane_emitter = PlaneEmitter.new
|
|
77
|
+
@script_emitter = ScriptEmitter.new
|
|
78
|
+
@codepoint_emitter = CodepointEmitter.new
|
|
79
|
+
@glyph_emitter = GlyphEmitter.new(glyph_resolver: glyph_resolver)
|
|
80
|
+
@collection_emitter = CollectionEmitter.new
|
|
81
|
+
@library_emitter = LibraryEmitter.new
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @param label [String] sanitized face label (caller-sanitized)
|
|
85
|
+
# @param report [Models::Audit::AuditReport]
|
|
86
|
+
# @return [Pathname] the per-face directory written
|
|
87
|
+
def emit_face(label:, report:)
|
|
88
|
+
emit_face_at(Paths.face_dir(@output_root, label), report)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @param source_label [String] sanitized collection label
|
|
92
|
+
# @param reports [Array<Models::Audit::AuditReport>]
|
|
93
|
+
# @return [Array<String>] per-face subdirectory names
|
|
94
|
+
def emit_collection(source_label:, reports:)
|
|
95
|
+
@collection_emitter.emit(@output_root, source_label, reports,
|
|
96
|
+
face_directory: self)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @param summary [Models::Audit::LibrarySummary]
|
|
100
|
+
# @return [Boolean] true if library index was written
|
|
101
|
+
def emit_library(summary:)
|
|
102
|
+
summary.per_face_reports.each do |report|
|
|
103
|
+
emit_face(label: face_label(report), report: report)
|
|
104
|
+
end
|
|
105
|
+
written = @library_emitter.emit(@output_root, summary)
|
|
106
|
+
emit_library_browser(summary) if @emit_browser
|
|
107
|
+
written
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Hook called by {CollectionEmitter} to write one face under a
|
|
111
|
+
# collection root. Computes the per-face subdirectory name from
|
|
112
|
+
# the face_index so the source order is preserved on disk.
|
|
113
|
+
#
|
|
114
|
+
# @api private
|
|
115
|
+
# @param source_label [String]
|
|
116
|
+
# @param face_index [Integer]
|
|
117
|
+
# @param report [Models::Audit::AuditReport]
|
|
118
|
+
# @return [String] the per-face subdirectory name (e.g. "00-Mona")
|
|
119
|
+
def emit_collection_face(source_label:, face_index:, report:)
|
|
120
|
+
face_label = format("%<idx>02d-%<label>s",
|
|
121
|
+
idx: face_index,
|
|
122
|
+
label: sanitize(report.postscript_name))
|
|
123
|
+
emit_face_at(
|
|
124
|
+
Paths.collection_face_dir(@output_root, source_label, face_index,
|
|
125
|
+
sanitize(report.postscript_name)),
|
|
126
|
+
report,
|
|
127
|
+
)
|
|
128
|
+
face_label
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Write one face's full chunk tree under an explicit face_dir.
|
|
132
|
+
#
|
|
133
|
+
# Public entry point for callers that compute their own face_dir
|
|
134
|
+
# (e.g. {Ucode::Audit::Release::Emitter} builds the release tree
|
|
135
|
+
# at `<release_root>/audit/<slug>/<face>/`). Callers that want
|
|
136
|
+
# the default library-mode layout should use {#emit_face}.
|
|
137
|
+
#
|
|
138
|
+
# @param face_dir [String, Pathname] explicit per-face directory
|
|
139
|
+
# @param report [Models::Audit::AuditReport]
|
|
140
|
+
# @return [Pathname] the face directory written
|
|
141
|
+
def emit_face_at(face_dir, report)
|
|
142
|
+
@index_emitter.emit(face_dir, report, universal_set_root: @universal_set_root)
|
|
143
|
+
report.blocks.each { |b| @block_emitter.emit(face_dir, b) }
|
|
144
|
+
report.plane_summaries.each { |p| @plane_emitter.emit(face_dir, p) }
|
|
145
|
+
report.scripts.each { |s| @script_emitter.emit(face_dir, s) }
|
|
146
|
+
emit_codepoints(face_dir, report) if @verbose
|
|
147
|
+
emit_glyphs(face_dir, report) if @with_glyphs
|
|
148
|
+
emit_browsers(face_dir, report) if @emit_browser
|
|
149
|
+
face_dir
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def emit_browsers(face_dir, report)
|
|
155
|
+
emit_face_browser(face_dir, report)
|
|
156
|
+
emit_missing_glyph_pages(face_dir, report) if @with_missing_glyph_pages
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def emit_face_browser(face_dir, report)
|
|
160
|
+
Ucode::Audit::Browser::FacePage.new(
|
|
161
|
+
report: report,
|
|
162
|
+
verbose: @verbose,
|
|
163
|
+
with_glyphs: @with_glyphs,
|
|
164
|
+
universal_set_root: @universal_set_root,
|
|
165
|
+
face_dir: face_dir,
|
|
166
|
+
).write(face_dir)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def emit_missing_glyph_pages(face_dir, report)
|
|
170
|
+
panel = Ucode::Audit::Browser::GlyphPanel.new(universal_set_root: @universal_set_root)
|
|
171
|
+
report.blocks.each do |block|
|
|
172
|
+
next if block.missing_codepoints.empty?
|
|
173
|
+
|
|
174
|
+
Ucode::Audit::Browser::MissingGlyphPage.new(
|
|
175
|
+
block_name: block.name,
|
|
176
|
+
missing_codepoints: block.missing_codepoints,
|
|
177
|
+
glyph_panel: panel,
|
|
178
|
+
).write(face_dir)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def emit_library_browser(summary)
|
|
183
|
+
Ucode::Audit::Browser::LibraryPage.new(summary: summary).write(@output_root)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def emit_codepoints(face_dir, report)
|
|
187
|
+
report.blocks.each do |block|
|
|
188
|
+
next if block.covered_codepoints.empty?
|
|
189
|
+
|
|
190
|
+
@codepoint_emitter.emit(face_dir, block,
|
|
191
|
+
database: @database,
|
|
192
|
+
with_glyph_paths: @with_glyphs)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def emit_glyphs(face_dir, report)
|
|
197
|
+
report.blocks.flat_map(&:covered_codepoints).sort.each do |cp|
|
|
198
|
+
@glyph_emitter.emit(face_dir, cp)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def face_label(report)
|
|
203
|
+
report.postscript_name || File.basename(report.source_file, ".*")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def sanitize(name)
|
|
207
|
+
(name || "face").to_s.gsub(/[^A-Za-z0-9._-]/, "_")
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
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>/glyphs/U+XXXX.svg` — the SVG outline of one
|
|
12
|
+
# audited glyph, emitted only in `--with-glyphs` mode.
|
|
13
|
+
#
|
|
14
|
+
# Glyph production is delegated to a caller-injected
|
|
15
|
+
# `glyph_resolver` proc. The proc takes a codepoint Integer and
|
|
16
|
+
# returns either an SVG string (write it) or nil (skip — no glyph
|
|
17
|
+
# available). ucode 0.2 ships with a default proc that always
|
|
18
|
+
# returns nil; the canonical 4-tier resolver (TODO 20) replaces it
|
|
19
|
+
# with the real fontist/fontisan + Last-Resort pipeline.
|
|
20
|
+
#
|
|
21
|
+
# Lazy by design: the resolver is invoked once per codepoint, and
|
|
22
|
+
# only for codepoints the caller actually iterates. No upfront
|
|
23
|
+
# font-load cost.
|
|
24
|
+
class GlyphEmitter
|
|
25
|
+
include Ucode::Repo::AtomicWrites
|
|
26
|
+
|
|
27
|
+
DEFAULT_RESOLVER = proc { |_codepoint| }
|
|
28
|
+
|
|
29
|
+
# @param glyph_resolver [Proc(Integer) -> String, nil] SVG source
|
|
30
|
+
def initialize(glyph_resolver: DEFAULT_RESOLVER)
|
|
31
|
+
@glyph_resolver = glyph_resolver
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param face_dir [String, Pathname]
|
|
35
|
+
# @param codepoint [Integer]
|
|
36
|
+
# @return [Boolean] true if written, false if skipped (no glyph
|
|
37
|
+
# available, or content-identical to existing file)
|
|
38
|
+
def emit(face_dir, codepoint)
|
|
39
|
+
svg = @glyph_resolver.call(codepoint)
|
|
40
|
+
return false if svg.nil?
|
|
41
|
+
|
|
42
|
+
path = Paths.glyph_under(face_dir, format("U+%04X", codepoint))
|
|
43
|
+
write_atomic(path, svg)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
require "ucode/repo/atomic_writes"
|
|
6
|
+
require "ucode/audit/emitter/paths"
|
|
7
|
+
require "ucode/models/audit/block_summary"
|
|
8
|
+
|
|
9
|
+
module Ucode
|
|
10
|
+
module Audit
|
|
11
|
+
module Emitter
|
|
12
|
+
# Writes `<face_dir>/index.json` — the compact face overview the
|
|
13
|
+
# browser fetches first.
|
|
14
|
+
#
|
|
15
|
+
# Compactness rules (per `03-directory-output-spec.md`):
|
|
16
|
+
#
|
|
17
|
+
# - `codepoint_details` never appears in `index.json`. The verbose
|
|
18
|
+
# per-block detail is emitted by {CodepointEmitter}.
|
|
19
|
+
# - `covered_codepoints` is dropped from each block entry. The
|
|
20
|
+
# browser fetches `codepoints/<NAME>.json` for that.
|
|
21
|
+
# - `missing_codepoints` is kept per block — it's the actionable
|
|
22
|
+
# gap list and small in practice.
|
|
23
|
+
# - Adds a derived `totals` block so a renderer doesn't have to
|
|
24
|
+
# re-aggregate to draw the headline numbers.
|
|
25
|
+
#
|
|
26
|
+
# Idempotent via {Ucode::Repo::AtomicWrites}.
|
|
27
|
+
class IndexEmitter
|
|
28
|
+
include Ucode::Repo::AtomicWrites
|
|
29
|
+
|
|
30
|
+
# @param face_dir [String, Pathname]
|
|
31
|
+
# @param report [Models::Audit::AuditReport]
|
|
32
|
+
# @param universal_set_root [String, Pathname, nil] when both
|
|
33
|
+
# this and `face_dir` are present and the root exists on
|
|
34
|
+
# disk, the index embeds a `universal_set` section with
|
|
35
|
+
# relative paths to the manifest + glyphs dir. nil otherwise.
|
|
36
|
+
# @return [Boolean] true if the file was written, false if skipped
|
|
37
|
+
def emit(face_dir, report, universal_set_root: nil)
|
|
38
|
+
payload = to_pretty_json(build_index(report, universal_set_root: universal_set_root,
|
|
39
|
+
face_dir: face_dir))
|
|
40
|
+
write_atomic(Paths.index_under(face_dir), payload)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Build the index.json shape (a Hash) for a report. Exposed so
|
|
44
|
+
# the HTML browser ({Browser::FacePage}) can reuse the exact
|
|
45
|
+
# same shape when inlining overview data into its template.
|
|
46
|
+
#
|
|
47
|
+
# @param report [Models::Audit::AuditReport]
|
|
48
|
+
# @param universal_set_root [String, Pathname, nil]
|
|
49
|
+
# @param face_dir [String, Pathname, nil] required when
|
|
50
|
+
# `universal_set_root` is supplied (relative path resolution).
|
|
51
|
+
# @return [Hash]
|
|
52
|
+
def build_index(report, universal_set_root: nil, face_dir: nil)
|
|
53
|
+
{
|
|
54
|
+
"generated_at" => report.generated_at,
|
|
55
|
+
"ucode_version" => report.ucode_version,
|
|
56
|
+
"font" => font_section(report),
|
|
57
|
+
"baseline" => report.baseline&.to_hash,
|
|
58
|
+
"totals" => build_totals(report),
|
|
59
|
+
"discrepancies" => report.discrepancies.map(&:to_hash),
|
|
60
|
+
"plane_summaries" => report.plane_summaries.map(&:to_hash),
|
|
61
|
+
"block_summaries" => block_summaries(report),
|
|
62
|
+
"script_summaries" => report.scripts.map(&:to_hash),
|
|
63
|
+
"universal_set" => universal_set_section(universal_set_root, face_dir),
|
|
64
|
+
}.compact
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def font_section(report)
|
|
70
|
+
{
|
|
71
|
+
"source_file" => report.source_file,
|
|
72
|
+
"source_sha256" => report.source_sha256,
|
|
73
|
+
"source_format" => report.source_format,
|
|
74
|
+
"font_index" => report.font_index,
|
|
75
|
+
"num_fonts_in_source" => report.num_fonts_in_source,
|
|
76
|
+
"family_name" => report.family_name,
|
|
77
|
+
"subfamily_name" => report.subfamily_name,
|
|
78
|
+
"full_name" => report.full_name,
|
|
79
|
+
"postscript_name" => report.postscript_name,
|
|
80
|
+
"version" => report.version,
|
|
81
|
+
"font_revision" => report.font_revision,
|
|
82
|
+
"weight_class" => report.weight_class,
|
|
83
|
+
"width_class" => report.width_class,
|
|
84
|
+
"italic" => report.italic,
|
|
85
|
+
"bold" => report.bold,
|
|
86
|
+
"panose" => report.panose,
|
|
87
|
+
"total_codepoints" => report.total_codepoints,
|
|
88
|
+
"total_glyphs" => report.total_glyphs,
|
|
89
|
+
"cmap_subtables" => report.cmap_subtables,
|
|
90
|
+
"codepoint_ranges" => report.codepoint_ranges.map(&:to_hash),
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def block_summaries(report)
|
|
95
|
+
report.blocks.map do |block|
|
|
96
|
+
hash = block.to_hash.except("covered_codepoints")
|
|
97
|
+
# Spec: per-block `missing_codepoints` is always embedded.
|
|
98
|
+
# lutaml-model omits empty arrays by default; re-add the key.
|
|
99
|
+
hash["missing_codepoints"] = block.missing_codepoints
|
|
100
|
+
hash
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def build_totals(report)
|
|
105
|
+
{
|
|
106
|
+
"assigned_codepoints_total" => assigned_total(report),
|
|
107
|
+
"covered_codepoints_total" => report.total_codepoints,
|
|
108
|
+
"blocks_touched" => report.blocks.size,
|
|
109
|
+
"blocks_complete" => report.blocks.count do |b|
|
|
110
|
+
b.status == Models::Audit::BlockSummary::STATUS_COMPLETE
|
|
111
|
+
end,
|
|
112
|
+
"blocks_partial" => report.blocks.count do |b|
|
|
113
|
+
b.status == Models::Audit::BlockSummary::STATUS_PARTIAL
|
|
114
|
+
end,
|
|
115
|
+
"scripts_touched" => report.scripts.size,
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def assigned_total(report)
|
|
120
|
+
report.blocks.sum(&:total_assigned)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def universal_set_section(root, face_dir)
|
|
124
|
+
return nil if root.nil? || face_dir.nil?
|
|
125
|
+
|
|
126
|
+
root_path = Pathname.new(root)
|
|
127
|
+
unless root_path.directory?
|
|
128
|
+
return {
|
|
129
|
+
"available" => false,
|
|
130
|
+
"reason" => "universal_set_root not found: #{root}",
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
{
|
|
135
|
+
"available" => true,
|
|
136
|
+
"manifest_path" => relative_path(face_dir, root_path.join("manifest.json")),
|
|
137
|
+
"glyphs_dir" => "#{relative_path(face_dir, root_path.join('glyphs'))}/",
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def relative_path(from_dir, to_path)
|
|
142
|
+
to_path.expand_path.relative_path_from(Pathname.new(from_dir).expand_path).to_s
|
|
143
|
+
rescue ArgumentError
|
|
144
|
+
to_path.to_s
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
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 `output/font_audit/index.json` — the library-mode
|
|
12
|
+
# top-level index pointing at each face's `index.json`.
|
|
13
|
+
#
|
|
14
|
+
# Aggregates the per-face {Models::Audit::LibrarySummary} into a
|
|
15
|
+
# compact card list. The browser fetches this once on load and
|
|
16
|
+
# uses the cards to render the library browser; clicking a card
|
|
17
|
+
# fetches that face's per-face directory.
|
|
18
|
+
class LibraryEmitter
|
|
19
|
+
include Ucode::Repo::AtomicWrites
|
|
20
|
+
|
|
21
|
+
# @param output_root [String, Pathname]
|
|
22
|
+
# @param summary [Models::Audit::LibrarySummary]
|
|
23
|
+
# @return [Boolean] true if written, false if skipped
|
|
24
|
+
def emit(output_root, summary)
|
|
25
|
+
path = Paths.library_index_path(output_root)
|
|
26
|
+
payload = to_pretty_json(build_index(summary))
|
|
27
|
+
write_atomic(path, payload)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Build the library index.json shape (a Hash). Exposed so the
|
|
31
|
+
# HTML browser ({Browser::LibraryPage}) can reuse the exact
|
|
32
|
+
# same shape when inlining overview data into its template.
|
|
33
|
+
#
|
|
34
|
+
# @param summary [Models::Audit::LibrarySummary]
|
|
35
|
+
# @return [Hash]
|
|
36
|
+
def build_index(summary)
|
|
37
|
+
{
|
|
38
|
+
"root_path" => summary.root_path,
|
|
39
|
+
"total_files" => summary.total_files,
|
|
40
|
+
"total_faces" => summary.total_faces,
|
|
41
|
+
"scanned_extensions" => summary.scanned_extensions,
|
|
42
|
+
"aggregate_metrics" => summary.aggregate_metrics,
|
|
43
|
+
"license_distribution" => summary.license_distribution,
|
|
44
|
+
"duplicate_groups" => summary.duplicate_groups.map(&:to_hash),
|
|
45
|
+
"script_coverage" => summary.script_coverage.map(&:to_hash),
|
|
46
|
+
"faces" => face_cards(summary),
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def face_cards(summary)
|
|
53
|
+
summary.per_face_reports.map do |report|
|
|
54
|
+
label = face_label(report)
|
|
55
|
+
covered_total, assigned_total, complete, partial = block_rollup(report)
|
|
56
|
+
{
|
|
57
|
+
"label" => label,
|
|
58
|
+
"family_name" => report.family_name,
|
|
59
|
+
"postscript_name" => report.postscript_name,
|
|
60
|
+
"weight_class" => report.weight_class,
|
|
61
|
+
"total_codepoints" => report.total_codepoints,
|
|
62
|
+
"total_glyphs" => report.total_glyphs,
|
|
63
|
+
"covered_total" => covered_total,
|
|
64
|
+
"total_assigned_total" => assigned_total,
|
|
65
|
+
"blocks_complete" => complete,
|
|
66
|
+
"blocks_partial" => partial,
|
|
67
|
+
"source_sha256" => report.source_sha256,
|
|
68
|
+
"index_path" => "#{label}/index.json",
|
|
69
|
+
"html_path" => "#{label}/index.html",
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def block_rollup(report)
|
|
75
|
+
covered = 0
|
|
76
|
+
assigned = 0
|
|
77
|
+
complete = 0
|
|
78
|
+
partial = 0
|
|
79
|
+
report.blocks.each do |b|
|
|
80
|
+
covered += b.covered_count
|
|
81
|
+
assigned += b.total_assigned
|
|
82
|
+
case b.status
|
|
83
|
+
when Models::Audit::BlockSummary::STATUS_COMPLETE then complete += 1
|
|
84
|
+
when Models::Audit::BlockSummary::STATUS_PARTIAL then partial += 1
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
[covered, assigned, complete, partial]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def face_label(report)
|
|
91
|
+
report.postscript_name || File.basename(report.source_file, ".*")
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|