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,411 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ucode
|
|
4
|
+
module Audit
|
|
5
|
+
module Formatters
|
|
6
|
+
# Human-readable, sectioned view of an {Models::Audit::AuditReport}.
|
|
7
|
+
#
|
|
8
|
+
# The text formatter is the default output for `ucode audit`. Every
|
|
9
|
+
# section is nil-safe so the same renderer covers full OpenType /
|
|
10
|
+
# TrueType faces, Type 1 fonts (no OS/2, no metrics, no layout),
|
|
11
|
+
# and partial reports where the UCD baseline could not be resolved.
|
|
12
|
+
#
|
|
13
|
+
# ucode deltas vs fontisan's AuditTextRenderer:
|
|
14
|
+
#
|
|
15
|
+
# - Reads `report.baseline.unicode_version` instead of `ucd_version`.
|
|
16
|
+
# - Renders `report.scripts` (ScriptSummary[]) as a coverage table.
|
|
17
|
+
# - Renders `report.blocks` (BlockSummary[]) with explicit status
|
|
18
|
+
# and coverage_percent columns.
|
|
19
|
+
# - Adds `plane_summaries` and `discrepancies` sections (no
|
|
20
|
+
# fontisan equivalent).
|
|
21
|
+
# - Drops CLDR language coverage (out of scope).
|
|
22
|
+
# - Honors `ENV["NO_COLOR"]` via {Color}.
|
|
23
|
+
class AuditText
|
|
24
|
+
SEPARATOR = "=" * 80
|
|
25
|
+
LIST_LIMIT = 10
|
|
26
|
+
|
|
27
|
+
# Map OS/2 width class → human label.
|
|
28
|
+
WIDTH_NAMES = {
|
|
29
|
+
1 => "Ultra-condensed", 2 => "Extra-condensed", 3 => "Condensed",
|
|
30
|
+
4 => "Semi-condensed", 5 => "Medium (normal)", 6 => "Semi-expanded",
|
|
31
|
+
7 => "Expanded", 8 => "Extra-expanded", 9 => "Ultra-expanded"
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
# @param report [Models::Audit::AuditReport]
|
|
35
|
+
def initialize(report)
|
|
36
|
+
@report = report
|
|
37
|
+
@lines = []
|
|
38
|
+
@helper = TextFormatter.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [String]
|
|
42
|
+
def render
|
|
43
|
+
render_header
|
|
44
|
+
render_identity
|
|
45
|
+
render_style
|
|
46
|
+
render_metrics
|
|
47
|
+
render_coverage
|
|
48
|
+
render_planes
|
|
49
|
+
render_blocks
|
|
50
|
+
render_scripts
|
|
51
|
+
render_licensing
|
|
52
|
+
render_hinting
|
|
53
|
+
render_color
|
|
54
|
+
render_variation
|
|
55
|
+
render_opentype_layout
|
|
56
|
+
render_discrepancies
|
|
57
|
+
render_warnings
|
|
58
|
+
@lines.join("\n")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def render_header
|
|
64
|
+
@lines << Color.bold(@report.postscript_name || @report.family_name || "(unknown)")
|
|
65
|
+
@lines << Color.dim(SEPARATOR)
|
|
66
|
+
@lines << " generated_at: #{@report.generated_at}"
|
|
67
|
+
@lines << " ucode: #{@report.ucode_version}"
|
|
68
|
+
@lines << " source_sha256: #{@report.source_sha256}"
|
|
69
|
+
@lines << " source_file: #{@report.source_file}"
|
|
70
|
+
@lines << " source_format: #{@report.source_format || '(unknown)'}"
|
|
71
|
+
@lines << " layout: #{layout_descriptor}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def layout_descriptor
|
|
75
|
+
if @report.num_fonts_in_source.nil? || @report.num_fonts_in_source <= 1
|
|
76
|
+
"single face (1/1)"
|
|
77
|
+
else
|
|
78
|
+
format("collection face (%<idx>d/%<total>d)",
|
|
79
|
+
idx: (@report.font_index || 0) + 1,
|
|
80
|
+
total: @report.num_fonts_in_source)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def render_identity
|
|
85
|
+
section("IDENTITY")
|
|
86
|
+
@lines << @helper.row("Family", @report.family_name)
|
|
87
|
+
@lines << @helper.row("Subfamily", @report.subfamily_name)
|
|
88
|
+
@lines << @helper.row("Full name", @report.full_name)
|
|
89
|
+
@lines << @helper.row("PostScript", @report.postscript_name)
|
|
90
|
+
@lines << @helper.row("Version", @report.version)
|
|
91
|
+
@lines << @helper.row("Revision", @report.font_revision)
|
|
92
|
+
@lines.compact!
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def render_style
|
|
96
|
+
section("STYLE")
|
|
97
|
+
@lines << @helper.row("Weight class", weight_descriptor)
|
|
98
|
+
@lines << @helper.row("Width class", width_descriptor)
|
|
99
|
+
@lines << @helper.row("Bold", yes_no(@report.bold))
|
|
100
|
+
@lines << @helper.row("Italic", yes_no(@report.italic))
|
|
101
|
+
@lines << @helper.row("PANOSE", @report.panose)
|
|
102
|
+
@lines.compact!
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def render_metrics
|
|
106
|
+
return unless @report.metrics
|
|
107
|
+
|
|
108
|
+
m = @report.metrics
|
|
109
|
+
section("METRICS")
|
|
110
|
+
@lines << @helper.row("unitsPerEm", m.units_per_em)
|
|
111
|
+
if m.hhea_ascent
|
|
112
|
+
@lines << @helper.row("hhea",
|
|
113
|
+
"ascent: #{m.hhea_ascent} / descent: #{m.hhea_descent} / line gap: #{m.hhea_line_gap}")
|
|
114
|
+
end
|
|
115
|
+
if m.typo_ascender
|
|
116
|
+
@lines << @helper.row("OS/2 typo",
|
|
117
|
+
"ascent: #{m.typo_ascender} / descent: #{m.typo_descender} / line gap: #{m.typo_line_gap}")
|
|
118
|
+
end
|
|
119
|
+
if m.win_ascent
|
|
120
|
+
@lines << @helper.row("OS/2 win",
|
|
121
|
+
"ascent: #{m.win_ascent} / descent: #{m.win_descent}")
|
|
122
|
+
end
|
|
123
|
+
@lines << @helper.row("x-height", m.x_height)
|
|
124
|
+
@lines << @helper.row("cap height", m.cap_height)
|
|
125
|
+
if m.bbox_x_min || m.bbox_x_max
|
|
126
|
+
@lines << @helper.row("bbox", "(#{m.bbox_x_min}, #{m.bbox_y_min}) → (#{m.bbox_x_max}, #{m.bbox_y_max})")
|
|
127
|
+
end
|
|
128
|
+
@lines << @helper.row("metrics consistent?", yes_no(m.metrics_consistent?))
|
|
129
|
+
@lines.compact!
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def render_coverage
|
|
133
|
+
section("COVERAGE")
|
|
134
|
+
@lines << @helper.row("Codepoints", @report.total_codepoints)
|
|
135
|
+
@lines << @helper.row("Glyphs", @report.total_glyphs)
|
|
136
|
+
unless Array(@report.cmap_subtables).empty?
|
|
137
|
+
@lines << @helper.row("cmap subtables", Array(@report.cmap_subtables).join(", "))
|
|
138
|
+
end
|
|
139
|
+
@lines << @helper.row("Ranges (top #{LIST_LIMIT})",
|
|
140
|
+
@helper.truncate_ranges(@report.codepoint_ranges))
|
|
141
|
+
@lines << @helper.row("Baseline", baseline_descriptor)
|
|
142
|
+
@lines.compact!
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def baseline_descriptor
|
|
146
|
+
v = @report.baseline&.unicode_version
|
|
147
|
+
v ? "Unicode #{v} (#{@report.baseline.source})" : "(unresolved)"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def render_planes
|
|
151
|
+
planes = Array(@report.plane_summaries)
|
|
152
|
+
return if planes.empty?
|
|
153
|
+
|
|
154
|
+
section("PLANE ROLLUP")
|
|
155
|
+
planes.first(LIST_LIMIT).each do |p|
|
|
156
|
+
@lines << format(" Plane %<plane>-2d %<covered>d / %<assigned>d (%<pct>s%%)",
|
|
157
|
+
plane: p.plane, covered: p.covered_total,
|
|
158
|
+
assigned: p.assigned_total,
|
|
159
|
+
pct: format_percent(p.coverage_percent))
|
|
160
|
+
end
|
|
161
|
+
if planes.size > LIST_LIMIT
|
|
162
|
+
@lines << " … (+#{planes.size - LIST_LIMIT} more planes)"
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def render_blocks
|
|
167
|
+
blocks = Array(@report.blocks)
|
|
168
|
+
return if blocks.empty?
|
|
169
|
+
|
|
170
|
+
section(blocks_header(blocks))
|
|
171
|
+
blocks.sort_by { |b| -(b.coverage_percent || 0) }.first(LIST_LIMIT).each do |block|
|
|
172
|
+
@lines << format_block_row(block)
|
|
173
|
+
end
|
|
174
|
+
return unless blocks.size > LIST_LIMIT
|
|
175
|
+
|
|
176
|
+
@lines << " … (+#{blocks.size - LIST_LIMIT} more blocks; see report JSON for the full list)"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def blocks_header(blocks)
|
|
180
|
+
complete = blocks.count { |b| b.status == Models::Audit::BlockSummary::STATUS_COMPLETE }
|
|
181
|
+
partial = blocks.count { |b| b.status == Models::Audit::BlockSummary::STATUS_PARTIAL }
|
|
182
|
+
"UNICODE BLOCKS (#{blocks.size} touched: #{complete} complete, #{partial} partial, top #{LIST_LIMIT} by fill)"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def format_block_row(block)
|
|
186
|
+
format(" %<name>-40s %<range>s %<covered>d/%<total>d (%<pct>s%%, %<status>s)",
|
|
187
|
+
name: "#{block.name}:",
|
|
188
|
+
range: block.range,
|
|
189
|
+
covered: block.covered_count,
|
|
190
|
+
total: block.total_assigned,
|
|
191
|
+
pct: format_percent(block.coverage_percent),
|
|
192
|
+
status: block.status)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def render_scripts
|
|
196
|
+
scripts = Array(@report.scripts)
|
|
197
|
+
return if scripts.empty?
|
|
198
|
+
|
|
199
|
+
section("UNICODE SCRIPTS (#{scripts.size} touched, top #{LIST_LIMIT} by coverage)")
|
|
200
|
+
scripts.sort_by { |s| -(s.coverage_percent || 0) }.first(LIST_LIMIT).each do |script|
|
|
201
|
+
label = "#{script.script_code} (#{script.script_name}):"
|
|
202
|
+
@lines << format(" %<name>-25s %<covered>d/%<total>d (%<pct>s%%, %<status>s)",
|
|
203
|
+
name: label,
|
|
204
|
+
covered: script.covered_total,
|
|
205
|
+
total: script.assigned_total,
|
|
206
|
+
pct: format_percent(script.coverage_percent),
|
|
207
|
+
status: script.status)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def render_licensing
|
|
212
|
+
return unless @report.licensing
|
|
213
|
+
|
|
214
|
+
l = @report.licensing
|
|
215
|
+
section("LICENSING")
|
|
216
|
+
@lines << @helper.row("Copyright", l.copyright)
|
|
217
|
+
@lines << @helper.row("Trademark", l.trademark)
|
|
218
|
+
@lines << @helper.row("Manufacturer", l.manufacturer)
|
|
219
|
+
@lines << @helper.row("Designer", l.designer)
|
|
220
|
+
@lines << @helper.row("License", l.license_description)
|
|
221
|
+
@lines << @helper.row("License URL", l.license_url)
|
|
222
|
+
@lines << @helper.row("Vendor URL", l.vendor_url)
|
|
223
|
+
@lines << @helper.row("Designer URL", l.designer_url)
|
|
224
|
+
@lines << @helper.row("Vendor ID", l.vendor_id)
|
|
225
|
+
@lines << @helper.row("Embedding", l.embedding_type)
|
|
226
|
+
@lines.compact!
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def render_hinting
|
|
230
|
+
return unless @report.hinting
|
|
231
|
+
|
|
232
|
+
h = @report.hinting
|
|
233
|
+
section("HINTING")
|
|
234
|
+
@lines << @helper.row("Format", h.hinting_format || (h.is_unhinted ? "unhinted" : "unknown"))
|
|
235
|
+
@lines << @helper.row("fpgm", instruction_line(h.has_fpgm, h.fpgm_instruction_count))
|
|
236
|
+
@lines << @helper.row("prep", instruction_line(h.has_prep, h.prep_instruction_count))
|
|
237
|
+
@lines << @helper.row("cvt", cvt_line(h))
|
|
238
|
+
@lines << @helper.row("gasp", gasp_line(h))
|
|
239
|
+
@lines << @helper.row("CFF hints", h.cff_hint_count)
|
|
240
|
+
@lines.compact!
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def render_color
|
|
244
|
+
c = @report.color_capabilities
|
|
245
|
+
return unless c
|
|
246
|
+
return if Array(c.color_formats).empty?
|
|
247
|
+
|
|
248
|
+
section("COLOR")
|
|
249
|
+
@lines << @helper.row("Color formats", Array(c.color_formats).join(", "))
|
|
250
|
+
append_color_rows(c)
|
|
251
|
+
@lines.compact!
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def append_color_rows(c)
|
|
255
|
+
color_rows(c).each { |row| @lines << row }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def color_rows(c)
|
|
259
|
+
[].tap do |rows|
|
|
260
|
+
rows << colr_row(c) if c.has_colr
|
|
261
|
+
rows << cpal_row(c) if c.has_cpal
|
|
262
|
+
rows.concat(count_rows(c))
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Returns rows for color formats that surface a strike/document count.
|
|
267
|
+
# Each entry is gated by both presence flag and non-nil count.
|
|
268
|
+
def count_rows(c)
|
|
269
|
+
[].tap do |rows|
|
|
270
|
+
rows << @helper.row("SVG documents", c.svg_document_count) if c.has_svg && c.svg_document_count
|
|
271
|
+
rows << @helper.row("CBDT strikes", c.cbdt_strike_count) if c.has_cbdt && c.cbdt_strike_count
|
|
272
|
+
rows << @helper.row("sbix strikes", c.sbix_strike_count) if c.has_sbix && c.sbix_strike_count
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def colr_row(c)
|
|
277
|
+
@helper.row("COLR",
|
|
278
|
+
"v#{c.colr_version}, #{c.colr_base_glyph_count} base glyphs, #{c.colr_layer_count} layers")
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def cpal_row(c)
|
|
282
|
+
@helper.row("CPAL", "palettes: #{c.cpal_palette_count}, colors: #{c.cpal_color_count}")
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def render_variation
|
|
286
|
+
v = @report.variation
|
|
287
|
+
section("VARIABLE FONT")
|
|
288
|
+
if v.nil? || Array(v.axes).empty?
|
|
289
|
+
@lines << " (not variable)"
|
|
290
|
+
return
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
v.axes.each do |axis|
|
|
294
|
+
@lines << @helper.row(axis.tag,
|
|
295
|
+
format("%<min>s .. %<max>s default %<default>s",
|
|
296
|
+
min: axis.min_value, max: axis.max_value,
|
|
297
|
+
default: axis.default_value))
|
|
298
|
+
end
|
|
299
|
+
return if Array(v.named_instances).empty?
|
|
300
|
+
|
|
301
|
+
@lines << " Named instances:"
|
|
302
|
+
v.named_instances.first(LIST_LIMIT).each do |inst|
|
|
303
|
+
@lines << " #{inst.postscript_name || inst.subfamily_name}: #{inst.coordinates}"
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def render_opentype_layout
|
|
308
|
+
return unless @report.opentype_layout
|
|
309
|
+
|
|
310
|
+
l = @report.opentype_layout
|
|
311
|
+
section("OPENTYPE LAYOUT")
|
|
312
|
+
@lines << @helper.row("GSUB", yes_no(l.has_gsub))
|
|
313
|
+
@lines << @helper.row("GPOS", yes_no(l.has_gpos))
|
|
314
|
+
@lines << @helper.row("Scripts (#{Array(l.scripts).size})",
|
|
315
|
+
@helper.truncate_list(l.scripts))
|
|
316
|
+
@lines << @helper.row("Features (#{Array(l.features).size})",
|
|
317
|
+
@helper.truncate_list(l.features))
|
|
318
|
+
@lines.compact!
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def render_discrepancies
|
|
322
|
+
discrepancies = Array(@report.discrepancies)
|
|
323
|
+
section("DISCREPANCIES (#{discrepancies.size})")
|
|
324
|
+
if discrepancies.empty?
|
|
325
|
+
@lines << " (none)"
|
|
326
|
+
return
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
discrepancies.first(LIST_LIMIT).each do |d|
|
|
330
|
+
@lines << " [#{d.kind}] #{d.detail}"
|
|
331
|
+
end
|
|
332
|
+
if discrepancies.size > LIST_LIMIT
|
|
333
|
+
@lines << " … (+#{discrepancies.size - LIST_LIMIT} more; see report JSON for the full list)"
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def render_warnings
|
|
338
|
+
section("WARNINGS")
|
|
339
|
+
@lines << if @report.warning
|
|
340
|
+
" #{@report.warning}"
|
|
341
|
+
else
|
|
342
|
+
" (none)"
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# ---- formatting helpers --------------------------------------------
|
|
347
|
+
|
|
348
|
+
def section(title)
|
|
349
|
+
@lines << ""
|
|
350
|
+
@lines << Color.bold(title)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def yes_no(bool)
|
|
354
|
+
bool ? "yes" : "no"
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def format_percent(pct)
|
|
358
|
+
pct.nil? ? "?" : format("%<v>.2f", v: pct)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def weight_descriptor
|
|
362
|
+
return nil unless @report.weight_class
|
|
363
|
+
|
|
364
|
+
name = weight_name(@report.weight_class)
|
|
365
|
+
"#{@report.weight_class}#{" (#{name})" if name}"
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def width_descriptor
|
|
369
|
+
return nil unless @report.width_class
|
|
370
|
+
|
|
371
|
+
name = WIDTH_NAMES[@report.width_class]
|
|
372
|
+
"#{@report.width_class}#{" (#{name})" if name}"
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def weight_name(value)
|
|
376
|
+
case value
|
|
377
|
+
when 100 then "Thin"
|
|
378
|
+
when 200 then "Extra-light"
|
|
379
|
+
when 300 then "Light"
|
|
380
|
+
when 400 then "Regular"
|
|
381
|
+
when 500 then "Medium"
|
|
382
|
+
when 600 then "Semi-bold"
|
|
383
|
+
when 700 then "Bold"
|
|
384
|
+
when 800 then "Extra-bold"
|
|
385
|
+
when 900 then "Black"
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def instruction_line(has, count)
|
|
390
|
+
return "no" unless has
|
|
391
|
+
|
|
392
|
+
count ? "#{count} instructions" : "present"
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def cvt_line(hinting)
|
|
396
|
+
return "no" unless hinting.has_cvt
|
|
397
|
+
|
|
398
|
+
hinting.cvt_entry_count ? "#{hinting.cvt_entry_count} entries" : "present"
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def gasp_line(hinting)
|
|
402
|
+
ranges = Array(hinting.gasp_ranges)
|
|
403
|
+
return "no" if ranges.empty?
|
|
404
|
+
|
|
405
|
+
ppems = ranges.map(&:max_ppem).compact
|
|
406
|
+
"#{ranges.size} ranges (#{ppems.join('/')} ppem)"
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ucode
|
|
4
|
+
module Audit
|
|
5
|
+
module Formatters
|
|
6
|
+
# Minimal ANSI helper. Formatters route every color emphasis
|
|
7
|
+
# through this module so the NO_COLOR env var is honored uniformly.
|
|
8
|
+
#
|
|
9
|
+
# Follows the no-color.org convention: when ENV["NO_COLOR"] is
|
|
10
|
+
# set to any non-empty value, all color methods return the input
|
|
11
|
+
# string unchanged.
|
|
12
|
+
module Color
|
|
13
|
+
RESET = "\e[0m"
|
|
14
|
+
BOLD = "\e[1m"
|
|
15
|
+
DIM = "\e[2m"
|
|
16
|
+
RED = "\e[31m"
|
|
17
|
+
GREEN = "\e[32m"
|
|
18
|
+
CYAN = "\e[36m"
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def enabled?
|
|
23
|
+
ENV["NO_COLOR"].nil? || ENV["NO_COLOR"].empty?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def bold(text)
|
|
27
|
+
enabled? ? "#{BOLD}#{text}#{RESET}" : text
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def dim(text)
|
|
31
|
+
enabled? ? "#{DIM}#{text}#{RESET}" : text
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def cyan(text)
|
|
35
|
+
enabled? ? "#{CYAN}#{text}#{RESET}" : text
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def green(text)
|
|
39
|
+
enabled? ? "#{GREEN}#{text}#{RESET}" : text
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def red(text)
|
|
43
|
+
enabled? ? "#{RED}#{text}#{RESET}" : text
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ucode
|
|
4
|
+
module Audit
|
|
5
|
+
module Formatters
|
|
6
|
+
# Human-readable overview of a {Models::Audit::LibrarySummary}.
|
|
7
|
+
#
|
|
8
|
+
# Lists the per-face rollup counts, aggregate metrics, script
|
|
9
|
+
# coverage matrix, duplicate groups, and license distribution.
|
|
10
|
+
# The full per-face AuditReports are attached to the model; this
|
|
11
|
+
# view only shows the cross-face summaries (use YAML/JSON output
|
|
12
|
+
# for the full per-face data).
|
|
13
|
+
class LibrarySummaryText
|
|
14
|
+
SEPARATOR = "=" * 80
|
|
15
|
+
LIST_LIMIT = 15
|
|
16
|
+
|
|
17
|
+
# @param summary [Models::Audit::LibrarySummary]
|
|
18
|
+
def initialize(summary)
|
|
19
|
+
@summary = summary
|
|
20
|
+
@lines = []
|
|
21
|
+
@helper = TextFormatter.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [String]
|
|
25
|
+
def render
|
|
26
|
+
render_header
|
|
27
|
+
render_aggregates
|
|
28
|
+
render_script_coverage
|
|
29
|
+
render_duplicates
|
|
30
|
+
render_license_distribution
|
|
31
|
+
@lines.join("\n")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def render_header
|
|
37
|
+
@lines << Color.bold("LIBRARY SUMMARY")
|
|
38
|
+
@lines << Color.dim(SEPARATOR)
|
|
39
|
+
@lines << " root: #{@summary.root_path}"
|
|
40
|
+
@lines << " files: #{@summary.total_files} faces: #{@summary.total_faces}"
|
|
41
|
+
exts = Array(@summary.scanned_extensions)
|
|
42
|
+
@lines << " formats: #{exts.empty? ? '(none)' : exts.join(', ')}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def render_aggregates
|
|
46
|
+
m = @summary.aggregate_metrics || {}
|
|
47
|
+
section("AGGREGATES")
|
|
48
|
+
@lines << " codepoints: #{m[:total_codepoints] || 0}"
|
|
49
|
+
@lines << " glyphs: #{m[:total_glyphs] || 0}"
|
|
50
|
+
@lines << " total size: #{@helper.format_bytes(m[:total_size_bytes] || 0)}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def render_script_coverage
|
|
54
|
+
rows = Array(@summary.script_coverage)
|
|
55
|
+
return if rows.empty?
|
|
56
|
+
|
|
57
|
+
section("SCRIPT COVERAGE (top #{LIST_LIMIT})")
|
|
58
|
+
rows.first(LIST_LIMIT).each do |row|
|
|
59
|
+
@lines << " #{row.script}: #{row.face_count} face#{'s' unless row.face_count == 1}"
|
|
60
|
+
end
|
|
61
|
+
if rows.size > LIST_LIMIT
|
|
62
|
+
@lines << " … (+#{rows.size - LIST_LIMIT} more scripts)"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def render_duplicates
|
|
67
|
+
groups = Array(@summary.duplicate_groups)
|
|
68
|
+
return if groups.empty?
|
|
69
|
+
|
|
70
|
+
section("DUPLICATES (#{groups.size} group#{'s' unless groups.size == 1})")
|
|
71
|
+
groups.first(LIST_LIMIT).each do |group|
|
|
72
|
+
sha = group.source_sha256.to_s
|
|
73
|
+
@lines << " sha #{sha[0, 12]}:"
|
|
74
|
+
group.files.each { |path| @lines << " #{path}" }
|
|
75
|
+
end
|
|
76
|
+
if groups.size > LIST_LIMIT
|
|
77
|
+
@lines << " … (+#{groups.size - LIST_LIMIT} more duplicate groups)"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def render_license_distribution
|
|
82
|
+
dist = @summary.license_distribution || {}
|
|
83
|
+
return if dist.empty?
|
|
84
|
+
|
|
85
|
+
section("LICENSE DISTRIBUTION")
|
|
86
|
+
dist.sort_by { |_url, count| -count }.each do |url, count|
|
|
87
|
+
@lines << " #{count} #{url}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def section(title)
|
|
92
|
+
@lines << ""
|
|
93
|
+
@lines << Color.bold(title)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ucode
|
|
4
|
+
module Audit
|
|
5
|
+
module Formatters
|
|
6
|
+
# Shared utilities for the text-rendering formatters. Owns the
|
|
7
|
+
# column helpers, list truncation, byte formatting, and ANSI
|
|
8
|
+
# color hook that the audit/diff/library renderers all use.
|
|
9
|
+
#
|
|
10
|
+
# Formatters instantiate this class and delegate common formatting
|
|
11
|
+
# chores to it; the renderer classes own the section shape and
|
|
12
|
+
# model-walking logic.
|
|
13
|
+
class TextFormatter
|
|
14
|
+
LIST_LIMIT = 10
|
|
15
|
+
LABEL_WIDTH = 18
|
|
16
|
+
|
|
17
|
+
# Format a list of arbitrary items as a single-line truncated
|
|
18
|
+
# comma-separated string. Returns "(none)" for empty input.
|
|
19
|
+
#
|
|
20
|
+
# @param items [Enumerable]
|
|
21
|
+
# @return [String]
|
|
22
|
+
def truncate_list(items, limit: LIST_LIMIT)
|
|
23
|
+
list = Array(items)
|
|
24
|
+
return "(none)" if list.empty?
|
|
25
|
+
|
|
26
|
+
shown = list.first(limit).join(", ")
|
|
27
|
+
if list.size > limit
|
|
28
|
+
"#{shown}, … (+#{list.size - limit} more)"
|
|
29
|
+
else
|
|
30
|
+
shown
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Format a codepoint range list as `U+XXXX-U+XXXX, …`, truncated.
|
|
35
|
+
#
|
|
36
|
+
# @param ranges [Enumerable<Models::Audit::CodepointRange>]
|
|
37
|
+
# @return [String]
|
|
38
|
+
def truncate_ranges(ranges, limit: LIST_LIMIT)
|
|
39
|
+
list = Array(ranges)
|
|
40
|
+
return "(none)" if list.empty?
|
|
41
|
+
|
|
42
|
+
shown = list.first(limit).join(", ")
|
|
43
|
+
if list.size > limit
|
|
44
|
+
"#{shown}, … (+#{list.size - limit} more)"
|
|
45
|
+
else
|
|
46
|
+
shown
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Format an integer byte count as `B` / `KB` / `MB`.
|
|
51
|
+
#
|
|
52
|
+
# @param bytes [Integer, nil]
|
|
53
|
+
# @return [String]
|
|
54
|
+
def format_bytes(bytes)
|
|
55
|
+
return "0 B" if bytes.nil? || bytes.zero?
|
|
56
|
+
|
|
57
|
+
if bytes < 1024
|
|
58
|
+
"#{bytes} B"
|
|
59
|
+
elsif bytes < 1024 * 1024
|
|
60
|
+
format("%<v>.2f KB", v: bytes / 1024.0)
|
|
61
|
+
else
|
|
62
|
+
format("%<v>.2f MB", v: bytes / (1024.0 * 1024))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Right-pad a label to a column width, then append the value.
|
|
67
|
+
# Returns nil if value is nil or empty-string (signal to skip).
|
|
68
|
+
#
|
|
69
|
+
# @param label [String, Symbol]
|
|
70
|
+
# @param value [Object]
|
|
71
|
+
# @return [String, nil]
|
|
72
|
+
def row(label, value, width: LABEL_WIDTH)
|
|
73
|
+
return if value.nil?
|
|
74
|
+
return if value.is_a?(String) && value.empty?
|
|
75
|
+
|
|
76
|
+
label_s = label.to_s
|
|
77
|
+
padding = " " * [(width - label_s.length - 1), 1].max
|
|
78
|
+
" #{label_s}:#{padding}#{value}"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Autoload hub for the Ucode::Audit::Formatters namespace.
|
|
4
|
+
#
|
|
5
|
+
# Presentation-only: every class here takes a model instance
|
|
6
|
+
# ({Models::Audit::AuditReport}, {Models::Audit::AuditDiff}, or
|
|
7
|
+
# {Models::Audit::LibrarySummary}) and returns a human-readable string.
|
|
8
|
+
# No font parsing, no I/O.
|
|
9
|
+
#
|
|
10
|
+
# MECE with the model layer: formatters READ from models; they never
|
|
11
|
+
# mutate them or carry audit logic. Adding a new output format (e.g.
|
|
12
|
+
# Markdown) = one new file here + one autoload line.
|
|
13
|
+
module Ucode
|
|
14
|
+
module Audit
|
|
15
|
+
module Formatters
|
|
16
|
+
autoload :Color, "ucode/audit/formatters/color"
|
|
17
|
+
autoload :TextFormatter, "ucode/audit/formatters/text_formatter"
|
|
18
|
+
autoload :AuditText, "ucode/audit/formatters/audit_text"
|
|
19
|
+
autoload :AuditDiffText, "ucode/audit/formatters/audit_diff_text"
|
|
20
|
+
autoload :LibrarySummaryText, "ucode/audit/formatters/library_summary_text"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|