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,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ucode
|
|
4
|
+
module Audit
|
|
5
|
+
# Pure cross-face aggregation over a list of AuditReports.
|
|
6
|
+
#
|
|
7
|
+
# No I/O, no font parsing — operates only on already-built reports.
|
|
8
|
+
# The orchestrator ({LibraryAuditor}) handles file discovery and
|
|
9
|
+
# per-face auditing; this class owns the rollups that span faces.
|
|
10
|
+
#
|
|
11
|
+
# ucode deltas vs fontisan:
|
|
12
|
+
#
|
|
13
|
+
# - `build_script_coverage` reads `report.scripts` (ScriptSummary[])
|
|
14
|
+
# rather than fontisan's `unicode_scripts` (String[]). The script
|
|
15
|
+
# identifier is `script_code` (ISO 15924), and only scripts with
|
|
16
|
+
# non-zero coverage are surfaced.
|
|
17
|
+
# - `aggregate_metrics` sums `total_codepoints` and `total_glyphs`.
|
|
18
|
+
# ucode reports both as Integer (never nil) per the schema.
|
|
19
|
+
class LibraryAggregator
|
|
20
|
+
# @param reports [Array<Models::Audit::AuditReport>]
|
|
21
|
+
# @return [Hash{Symbol => Object}] keys: :aggregate_metrics,
|
|
22
|
+
# :script_coverage, :duplicate_groups, :license_distribution
|
|
23
|
+
def aggregate(reports)
|
|
24
|
+
{
|
|
25
|
+
aggregate_metrics: aggregate_metrics(reports),
|
|
26
|
+
script_coverage: build_script_coverage(reports),
|
|
27
|
+
duplicate_groups: find_duplicates(reports),
|
|
28
|
+
license_distribution: license_distribution(reports),
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def aggregate_metrics(reports)
|
|
35
|
+
{
|
|
36
|
+
total_codepoints: reports.sum { |r| r.total_codepoints.to_i },
|
|
37
|
+
total_glyphs: reports.sum { |r| r.total_glyphs.to_i },
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def build_script_coverage(reports)
|
|
42
|
+
by_script = Hash.new { |h, k| h[k] = [] }
|
|
43
|
+
reports.each do |report|
|
|
44
|
+
face = report.postscript_name || report.source_file
|
|
45
|
+
scripts_for(report).each { |script| by_script[script] << face }
|
|
46
|
+
end
|
|
47
|
+
by_script.map do |script, faces|
|
|
48
|
+
Models::Audit::ScriptCoverageRow.new(
|
|
49
|
+
script: script,
|
|
50
|
+
face_count: faces.size,
|
|
51
|
+
faces: faces.uniq.sort,
|
|
52
|
+
)
|
|
53
|
+
end.sort_by { |row| [-row.face_count, row.script] }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def find_duplicates(reports)
|
|
57
|
+
reports.group_by(&:source_sha256)
|
|
58
|
+
.select { |_sha, group| group.size > 1 }
|
|
59
|
+
.map do |sha, group|
|
|
60
|
+
Models::Audit::DuplicateGroup.new(
|
|
61
|
+
source_sha256: sha,
|
|
62
|
+
files: group.map(&:source_file).sort,
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
.sort_by(&:source_sha256)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def license_distribution(reports)
|
|
69
|
+
reports.each_with_object({}) do |report, counts|
|
|
70
|
+
url = license_url_for(report)
|
|
71
|
+
counts[url] = counts.fetch(url, 0) + 1
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Read the script identifier from the structured ScriptSummary[]
|
|
76
|
+
# rather than fontisan's flat String[] list.
|
|
77
|
+
def scripts_for(report)
|
|
78
|
+
Array(report.scripts).map(&:script_code)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def license_url_for(report)
|
|
82
|
+
report.licensing&.license_url || "(none)"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module Ucode
|
|
6
|
+
module Audit
|
|
7
|
+
# Orchestrates a library-wide audit pass.
|
|
8
|
+
#
|
|
9
|
+
# Owns the file-system side: discovers font files under a root path
|
|
10
|
+
# (recursively or not), audits each via {FaceAuditor}, and assembles
|
|
11
|
+
# a {Models::Audit::LibrarySummary} combining the per-face reports
|
|
12
|
+
# with cross-face rollups from {LibraryAggregator}.
|
|
13
|
+
#
|
|
14
|
+
# Aggregation logic lives in the pure {LibraryAggregator}; this
|
|
15
|
+
# class stays focused on discovery + per-face auditing + summary
|
|
16
|
+
# assembly. Errors auditing a single file are captured in
|
|
17
|
+
# `#skipped` so a corrupt file doesn't abort the whole pass.
|
|
18
|
+
#
|
|
19
|
+
# ucode delta vs fontisan: delegates per-face work to {FaceAuditor}
|
|
20
|
+
# instead of fontisan's Commands::AuditCommand. The discovery and
|
|
21
|
+
# rollup logic is otherwise identical.
|
|
22
|
+
class LibraryAuditor
|
|
23
|
+
FONT_EXTENSIONS = %w[.ttf .otf .ttc .otc .dfont .woff .woff2
|
|
24
|
+
.pfb .pfa .svg].freeze
|
|
25
|
+
|
|
26
|
+
# @param root_path [String, Pathname] directory containing fonts
|
|
27
|
+
# @param recursive [Boolean] walk into subdirectories
|
|
28
|
+
# @param options [Hash] forwarded to {FaceAuditor} (ucd_version,
|
|
29
|
+
# all_codepoints, audit_brief, …). Library-only keys are stripped.
|
|
30
|
+
# @param reference [CoverageReference, nil] baseline forwarded to
|
|
31
|
+
# every per-face {FaceAuditor} (TODO 25). When nil, each face
|
|
32
|
+
# defaults to UCD-only.
|
|
33
|
+
def initialize(root_path, recursive:, options:, reference: nil)
|
|
34
|
+
@root_path = Pathname.new(root_path)
|
|
35
|
+
@recursive = recursive
|
|
36
|
+
@options = options
|
|
37
|
+
@reference = reference
|
|
38
|
+
@aggregator = LibraryAggregator.new
|
|
39
|
+
@skipped = []
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [Models::Audit::LibrarySummary]
|
|
43
|
+
def audit
|
|
44
|
+
paths = discover_font_paths
|
|
45
|
+
reports = paths.flat_map { |p| audit_one(p) }
|
|
46
|
+
rolled_up = aggregates(reports)
|
|
47
|
+
|
|
48
|
+
Models::Audit::LibrarySummary.new(
|
|
49
|
+
root_path: @root_path.to_s,
|
|
50
|
+
total_files: paths.size,
|
|
51
|
+
total_faces: reports.size,
|
|
52
|
+
scanned_extensions: scanned_extensions(paths),
|
|
53
|
+
aggregate_metrics: rolled_up[:aggregate_metrics].merge(
|
|
54
|
+
total_size_bytes: paths.sum { |p| File.size(p) },
|
|
55
|
+
),
|
|
56
|
+
script_coverage: rolled_up[:script_coverage],
|
|
57
|
+
duplicate_groups: rolled_up[:duplicate_groups],
|
|
58
|
+
license_distribution: rolled_up[:license_distribution],
|
|
59
|
+
per_face_reports: reports,
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @return [Array<String>] source files that could not be audited,
|
|
64
|
+
# formatted as "path: message"
|
|
65
|
+
attr_reader :skipped
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def discover_font_paths
|
|
70
|
+
method = @recursive ? :find : :children
|
|
71
|
+
@root_path.public_send(method).select do |entry|
|
|
72
|
+
next false unless entry.file?
|
|
73
|
+
next false if entry.symlink?
|
|
74
|
+
|
|
75
|
+
FONT_EXTENSIONS.include?(entry.extname.downcase)
|
|
76
|
+
end.map(&:to_s).sort
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def audit_one(path)
|
|
80
|
+
Array(FaceAuditor.new(path, options: audit_options, mode: audit_mode,
|
|
81
|
+
reference: @reference).call)
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
@skipped << "#{path}: #{e.message}"
|
|
84
|
+
[]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Library-only options that don't apply to per-face audit.
|
|
88
|
+
def audit_options
|
|
89
|
+
@options.except(:recursive, :summary, :output)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def audit_mode
|
|
93
|
+
@options[:audit_brief] ? :brief : :full
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def scanned_extensions(paths)
|
|
97
|
+
paths.map { |p| File.extname(p).downcase }.uniq.sort
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def aggregates(reports)
|
|
101
|
+
@aggregator.aggregate(reports)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
require "ucode/repo/atomic_writes"
|
|
7
|
+
require "ucode/audit/emitter/paths"
|
|
8
|
+
require "ucode/audit/emitter/face_directory"
|
|
9
|
+
require "ucode/audit/release/formula_audits"
|
|
10
|
+
require "ucode/audit/release/face_card"
|
|
11
|
+
require "ucode/audit/release/library_index_builder"
|
|
12
|
+
require "ucode/audit/release/manifest_builder"
|
|
13
|
+
|
|
14
|
+
module Ucode
|
|
15
|
+
module Audit
|
|
16
|
+
module Release
|
|
17
|
+
# Orchestrator that assembles the fontist.org release tree from
|
|
18
|
+
# a list of per-formula {FormulaAudits} (TODO 27).
|
|
19
|
+
#
|
|
20
|
+
# Drives {Emitter::FaceDirectory} per formula to emit each face's
|
|
21
|
+
# audit subtree under `<release_root>/audit/<slug>/<face>/`, then
|
|
22
|
+
# writes the two top-level indices:
|
|
23
|
+
#
|
|
24
|
+
# - `<release_root>/library.json` — formula + face card index
|
|
25
|
+
# via {LibraryIndexBuilder}.
|
|
26
|
+
# - `<release_root>/manifest.json` — release manifest via
|
|
27
|
+
# {ManifestBuilder} (a {Models::Audit::ReleaseManifest}).
|
|
28
|
+
#
|
|
29
|
+
# The universal-set directory is NOT copied by this emitter. The
|
|
30
|
+
# CI collector is expected to pre-stage
|
|
31
|
+
# `<release_root>/universal_glyph_set/` (built separately by
|
|
32
|
+
# `ucode universal-set build`). The manifest records whether that
|
|
33
|
+
# directory is present.
|
|
34
|
+
#
|
|
35
|
+
# Idempotent: every write goes through {Repo::AtomicWrites}
|
|
36
|
+
# (byte-compare, then rename). Re-running on unchanged input
|
|
37
|
+
# produces zero file writes on the second pass.
|
|
38
|
+
class Emitter
|
|
39
|
+
include Ucode::Repo::AtomicWrites
|
|
40
|
+
|
|
41
|
+
Result = Struct.new(:release_root, :formulas_total, :faces_total,
|
|
42
|
+
:library_index_written, :manifest_written,
|
|
43
|
+
:universal_set_available, keyword_init: true)
|
|
44
|
+
|
|
45
|
+
# @param output_root [String, Pathname] parent of the release
|
|
46
|
+
# root. The release tree lives at
|
|
47
|
+
# `<output_root>/font_audit_release/`.
|
|
48
|
+
# @param universal_set_root [String, Pathname, nil] location of
|
|
49
|
+
# the universal_glyph_set directory. Defaults to
|
|
50
|
+
# `<release_root>/universal_glyph_set` (the canonical location
|
|
51
|
+
# inside the release tree).
|
|
52
|
+
# @param face_directory [Emitter::FaceDirectory] injectable for
|
|
53
|
+
# testing. Defaults to a fresh instance configured with the
|
|
54
|
+
# same `universal_set_root` and `emit_browser: true`.
|
|
55
|
+
# @param verbose [Boolean] emit per-codepoint detail chunks per
|
|
56
|
+
# face. Forwarded to {Emitter::FaceDirectory}.
|
|
57
|
+
# @param with_glyphs [Boolean] emit per-codepoint SVG chunks.
|
|
58
|
+
# Forwarded to {Emitter::FaceDirectory}.
|
|
59
|
+
# @param with_missing_glyph_pages [Boolean] emit per-block
|
|
60
|
+
# missing-glyph galleries. Forwarded to
|
|
61
|
+
# {Emitter::FaceDirectory}.
|
|
62
|
+
def initialize(output_root:, universal_set_root: nil, face_directory: nil,
|
|
63
|
+
verbose: false, with_glyphs: false, with_missing_glyph_pages: true)
|
|
64
|
+
@output_root = output_root
|
|
65
|
+
@universal_set_root = universal_set_root
|
|
66
|
+
@verbose = verbose
|
|
67
|
+
@with_glyphs = with_glyphs
|
|
68
|
+
@with_missing_glyph_pages = with_missing_glyph_pages
|
|
69
|
+
@face_directory = face_directory || build_default_face_directory
|
|
70
|
+
@library_index_builder = LibraryIndexBuilder.new
|
|
71
|
+
@manifest_builder = ManifestBuilder.new
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @param formulas [Array<FormulaAudits>]
|
|
75
|
+
# @param unicode_version [String, nil]
|
|
76
|
+
# @param generated_at [String] ISO8601 timestamp
|
|
77
|
+
# @param source_config_sha256 [String, nil]
|
|
78
|
+
# @return [Result]
|
|
79
|
+
def emit(formulas:, unicode_version:, generated_at:,
|
|
80
|
+
source_config_sha256: nil)
|
|
81
|
+
release_root = Ucode::Audit::Emitter::Paths.release_root(@output_root)
|
|
82
|
+
formulas.each { |fa| emit_formula(release_root, fa) }
|
|
83
|
+
manifest = @manifest_builder.build(
|
|
84
|
+
formulas: formulas,
|
|
85
|
+
release_root: release_root,
|
|
86
|
+
unicode_version: unicode_version,
|
|
87
|
+
ucode_version: Ucode::VERSION,
|
|
88
|
+
generated_at: generated_at,
|
|
89
|
+
source_config_sha256: source_config_sha256,
|
|
90
|
+
universal_set_root: resolved_universal_set_root(release_root),
|
|
91
|
+
)
|
|
92
|
+
library_written = write_library_index(release_root, formulas, generated_at)
|
|
93
|
+
manifest_written = write_manifest(release_root, manifest)
|
|
94
|
+
Result.new(
|
|
95
|
+
release_root: release_root.to_s,
|
|
96
|
+
formulas_total: formulas.size,
|
|
97
|
+
faces_total: formulas.sum(&:faces_total),
|
|
98
|
+
library_index_written: library_written,
|
|
99
|
+
manifest_written: manifest_written,
|
|
100
|
+
universal_set_available: manifest.universal_set.available,
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def build_default_face_directory
|
|
107
|
+
Ucode::Audit::Emitter::FaceDirectory.new(
|
|
108
|
+
output_root: @output_root,
|
|
109
|
+
verbose: @verbose,
|
|
110
|
+
with_glyphs: @with_glyphs,
|
|
111
|
+
emit_browser: true,
|
|
112
|
+
universal_set_root: @universal_set_root,
|
|
113
|
+
with_missing_glyph_pages: @with_missing_glyph_pages,
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def resolved_universal_set_root(release_root)
|
|
118
|
+
@universal_set_root || Ucode::Audit::Emitter::Paths.release_universal_set_root(release_root)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def emit_formula(release_root, formula_audits)
|
|
122
|
+
slug = formula_audits.slug
|
|
123
|
+
formula_audits.face_reports.each do |report|
|
|
124
|
+
label = face_label_for(report, slug, release_root)
|
|
125
|
+
face_dir = Ucode::Audit::Emitter::Paths.release_face_dir(release_root, slug, label)
|
|
126
|
+
@face_directory.emit_face_at(face_dir, report)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def face_label_for(report, slug, release_root)
|
|
131
|
+
FaceCard.new(report, slug, release_root).label
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def write_library_index(release_root, formulas, generated_at)
|
|
135
|
+
path = Ucode::Audit::Emitter::Paths.release_library_index_path(release_root)
|
|
136
|
+
hash = @library_index_builder.build(
|
|
137
|
+
formulas: formulas,
|
|
138
|
+
release_root: release_root,
|
|
139
|
+
generated_at: generated_at,
|
|
140
|
+
ucode_version: Ucode::VERSION,
|
|
141
|
+
)
|
|
142
|
+
write_atomic(path, to_pretty_json(hash))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def write_manifest(release_root, manifest)
|
|
146
|
+
path = Ucode::Audit::Emitter::Paths.release_manifest_path(release_root)
|
|
147
|
+
write_atomic(path, manifest.to_json(pretty: true))
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
require "ucode/audit/emitter/paths"
|
|
6
|
+
require "ucode/models/audit"
|
|
7
|
+
|
|
8
|
+
module Ucode
|
|
9
|
+
module Audit
|
|
10
|
+
module Release
|
|
11
|
+
# Value object deriving the per-face card fields shared by
|
|
12
|
+
# {LibraryIndexBuilder} (Hash for `library.json`) and
|
|
13
|
+
# {ManifestBuilder} ({Models::Audit::ReleaseFaceEntry} for
|
|
14
|
+
# `manifest.json`).
|
|
15
|
+
#
|
|
16
|
+
# Single source of truth for the rollup math (covered total,
|
|
17
|
+
# assigned total, complete/partial block counts) and the path
|
|
18
|
+
# conventions (face label sanitization, relative index/html
|
|
19
|
+
# paths under `<release_root>/audit/<slug>/<label>/`).
|
|
20
|
+
#
|
|
21
|
+
# Pure: no I/O, no mutation. Callers compose the final shape
|
|
22
|
+
# (Hash or model) from the derived fields.
|
|
23
|
+
class FaceCard
|
|
24
|
+
attr_reader :report, :slug, :release_root
|
|
25
|
+
|
|
26
|
+
# @param report [Models::Audit::AuditReport]
|
|
27
|
+
# @param slug [String] formula slug
|
|
28
|
+
# @param release_root [String, Pathname]
|
|
29
|
+
def initialize(report, slug, release_root)
|
|
30
|
+
@report = report
|
|
31
|
+
@slug = slug
|
|
32
|
+
@release_root = release_root
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [String] sanitized face label (postscript name with
|
|
36
|
+
# non-filename chars replaced by underscore)
|
|
37
|
+
def label
|
|
38
|
+
name = report.postscript_name || File.basename(report.source_file.to_s, ".*")
|
|
39
|
+
(name || "face").to_s.gsub(/[^A-Za-z0-9._-]/, "_")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [Pathname] `<release_root>/audit/<slug>/<label>`
|
|
43
|
+
def face_dir
|
|
44
|
+
Ucode::Audit::Emitter::Paths.release_face_dir(release_root, slug, label)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [Integer]
|
|
48
|
+
def covered_total
|
|
49
|
+
report.blocks.sum(&:covered_count)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [Integer]
|
|
53
|
+
def assigned_total
|
|
54
|
+
report.blocks.sum(&:total_assigned)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [Integer]
|
|
58
|
+
def blocks_complete
|
|
59
|
+
report.blocks.count do |b|
|
|
60
|
+
b.status == Models::Audit::BlockSummary::STATUS_COMPLETE
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @return [Integer]
|
|
65
|
+
def blocks_partial
|
|
66
|
+
report.blocks.count do |b|
|
|
67
|
+
b.status == Models::Audit::BlockSummary::STATUS_PARTIAL
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @return [String] relative path from release root to index.json
|
|
72
|
+
def index_path
|
|
73
|
+
relative_path(face_dir.join("index.json"))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @return [String] relative path from release root to index.html
|
|
77
|
+
def html_path
|
|
78
|
+
relative_path(face_dir.join("index.html"))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def relative_path(to)
|
|
84
|
+
Pathname.new(to).expand_path
|
|
85
|
+
.relative_path_from(Pathname.new(release_root).expand_path)
|
|
86
|
+
.to_s
|
|
87
|
+
rescue ArgumentError
|
|
88
|
+
Pathname.new(to).to_s
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ucode
|
|
4
|
+
module Audit
|
|
5
|
+
module Release
|
|
6
|
+
# Value object pairing a formula slug with the library-wide audit
|
|
7
|
+
# summary produced by running {Audit::LibraryAuditor} on that
|
|
8
|
+
# formula's font directory.
|
|
9
|
+
#
|
|
10
|
+
# Used as the input unit to {Emitter}: callers pass a list of
|
|
11
|
+
# these and the emitter walks each one's `summary.per_face_reports`
|
|
12
|
+
# to emit the per-face audit subtrees.
|
|
13
|
+
#
|
|
14
|
+
# The slug MUST be caller-sanitized (fontist formula slug form:
|
|
15
|
+
# lowercase, hyphen-separated, filesystem-safe). The emitter does
|
|
16
|
+
# not re-sanitize — it uses the slug verbatim as the directory
|
|
17
|
+
# name under `<release_root>/audit/`.
|
|
18
|
+
FormulaAudits = Struct.new(:slug, :summary, keyword_init: true) do
|
|
19
|
+
# Sanity check at construction time so a malformed slug fails
|
|
20
|
+
# fast at the call site instead of producing a broken tree.
|
|
21
|
+
def initialize(slug:, summary:)
|
|
22
|
+
raise ArgumentError, "slug must not be empty" if slug.to_s.strip.empty?
|
|
23
|
+
raise ArgumentError, "slug contains path separators: #{slug.inspect}" if slug[%r{/}]
|
|
24
|
+
raise ArgumentError, "summary is required" unless summary
|
|
25
|
+
|
|
26
|
+
slug = slug.to_s
|
|
27
|
+
raise ArgumentError, "slug is not filesystem-safe: #{slug.inspect}" unless safe_slug?(slug)
|
|
28
|
+
|
|
29
|
+
super(slug: slug, summary: summary)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [Integer] number of face reports in the summary
|
|
33
|
+
def faces_total
|
|
34
|
+
summary.total_faces
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Enumerable<Models::Audit::AuditReport>]
|
|
38
|
+
def face_reports
|
|
39
|
+
summary.per_face_reports
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def safe_slug?(slug)
|
|
45
|
+
slug.match?(/\A[A-Za-z0-9._-]+\z/)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
require "ucode/audit/emitter/paths"
|
|
6
|
+
require "ucode/audit/release/formula_audits"
|
|
7
|
+
require "ucode/audit/release/face_card"
|
|
8
|
+
|
|
9
|
+
module Ucode
|
|
10
|
+
module Audit
|
|
11
|
+
module Release
|
|
12
|
+
# Pure builder for the release-level `library.json` (TODO 27).
|
|
13
|
+
#
|
|
14
|
+
# Aggregates a list of {FormulaAudits} into a single Hash shape
|
|
15
|
+
# consumed by fontist.org's renderer. Each formula contributes a
|
|
16
|
+
# formula card with its face cards; the renderer iterates the
|
|
17
|
+
# formula list to build its font index.
|
|
18
|
+
#
|
|
19
|
+
# The shape mirrors {Emitter::LibraryEmitter#build_index} but
|
|
20
|
+
# adds the formula layer. Paths are relative to the release root
|
|
21
|
+
# so the JSON is portable across hosts.
|
|
22
|
+
#
|
|
23
|
+
# Pure: no I/O, no global state. Caller writes the result.
|
|
24
|
+
class LibraryIndexBuilder
|
|
25
|
+
# @param formulas [Array<FormulaAudits>]
|
|
26
|
+
# @param release_root [String, Pathname]
|
|
27
|
+
# @param generated_at [String] ISO8601 timestamp
|
|
28
|
+
# @param ucode_version [String]
|
|
29
|
+
# @return [Hash]
|
|
30
|
+
def build(formulas:, release_root:, generated_at:, ucode_version:)
|
|
31
|
+
@release_root = release_root
|
|
32
|
+
{
|
|
33
|
+
"generated_at" => generated_at,
|
|
34
|
+
"ucode_version" => ucode_version,
|
|
35
|
+
"formulas_total" => formulas.size,
|
|
36
|
+
"faces_total" => formulas.sum(&:faces_total),
|
|
37
|
+
"formulas" => formulas.map { |fa| formula_card(fa) },
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
attr_reader :release_root
|
|
44
|
+
|
|
45
|
+
def formula_card(formula_audits)
|
|
46
|
+
summary = formula_audits.summary
|
|
47
|
+
{
|
|
48
|
+
"slug" => formula_audits.slug,
|
|
49
|
+
"source_path" => summary.root_path,
|
|
50
|
+
"faces_total" => summary.total_faces,
|
|
51
|
+
"scanned_extensions" => summary.scanned_extensions,
|
|
52
|
+
"aggregate_metrics" => summary.aggregate_metrics,
|
|
53
|
+
"license_distribution" => summary.license_distribution,
|
|
54
|
+
"faces" => formula_audits.face_reports.map { |r| face_card(r, formula_audits.slug) },
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def face_card(report, slug)
|
|
59
|
+
card = FaceCard.new(report, slug, release_root)
|
|
60
|
+
{
|
|
61
|
+
"label" => card.label,
|
|
62
|
+
"postscript_name" => report.postscript_name,
|
|
63
|
+
"family_name" => report.family_name,
|
|
64
|
+
"weight_class" => report.weight_class,
|
|
65
|
+
"total_codepoints" => report.total_codepoints,
|
|
66
|
+
"covered_total" => card.covered_total,
|
|
67
|
+
"total_assigned_total" => card.assigned_total,
|
|
68
|
+
"blocks_complete" => card.blocks_complete,
|
|
69
|
+
"blocks_partial" => card.blocks_partial,
|
|
70
|
+
"source_sha256" => report.source_sha256,
|
|
71
|
+
"index_path" => card.index_path,
|
|
72
|
+
"html_path" => card.html_path,
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
require "ucode/audit/emitter/paths"
|
|
7
|
+
require "ucode/audit/release/formula_audits"
|
|
8
|
+
require "ucode/audit/release/face_card"
|
|
9
|
+
require "ucode/models/audit/release_manifest"
|
|
10
|
+
require "ucode/models/audit/release_formula"
|
|
11
|
+
require "ucode/models/audit/release_face"
|
|
12
|
+
require "ucode/models/audit/release_universal_set"
|
|
13
|
+
|
|
14
|
+
module Ucode
|
|
15
|
+
module Audit
|
|
16
|
+
module Release
|
|
17
|
+
# Pure builder for the release-level `manifest.json` (TODO 27).
|
|
18
|
+
#
|
|
19
|
+
# Produces a ready-to-serialize {Models::Audit::ReleaseManifest}
|
|
20
|
+
# from a list of {FormulaAudits} plus the universal-set location.
|
|
21
|
+
# Records the ucode/unicode versions, optional source-config
|
|
22
|
+
# sha256 (for Tier 1 curation provenance), aggregate formula/face
|
|
23
|
+
# counts, and the universal-set reference section.
|
|
24
|
+
#
|
|
25
|
+
# Pure: no I/O, no global state. Caller serializes the model and
|
|
26
|
+
# writes the file.
|
|
27
|
+
class ManifestBuilder
|
|
28
|
+
UNIVERSAL_SET_DIR = "universal_glyph_set"
|
|
29
|
+
UNIVERSAL_SET_MANIFEST = "manifest.json"
|
|
30
|
+
UNIVERSAL_SET_GLYPHS_DIR = "glyphs"
|
|
31
|
+
|
|
32
|
+
# @param formulas [Array<FormulaAudits>]
|
|
33
|
+
# @param release_root [String, Pathname]
|
|
34
|
+
# @param unicode_version [String, nil] baseline UCD version
|
|
35
|
+
# @param ucode_version [String]
|
|
36
|
+
# @param generated_at [String] ISO8601 timestamp
|
|
37
|
+
# @param source_config_sha256 [String, nil] sha256 of the Tier 1
|
|
38
|
+
# source-config YAML (TODO 23). nil when not applicable.
|
|
39
|
+
# @param universal_set_root [String, Pathname, nil] expected
|
|
40
|
+
# location of the universal_glyph_set directory inside the
|
|
41
|
+
# release tree (default: `<release_root>/universal_glyph_set`).
|
|
42
|
+
# @return [Models::Audit::ReleaseManifest]
|
|
43
|
+
def build(formulas:, release_root:, unicode_version:, ucode_version:,
|
|
44
|
+
generated_at:, source_config_sha256: nil, universal_set_root: nil)
|
|
45
|
+
@release_root = release_root
|
|
46
|
+
resolved_uset_root = universal_set_root ||
|
|
47
|
+
Ucode::Audit::Emitter::Paths.release_universal_set_root(release_root)
|
|
48
|
+
Models::Audit::ReleaseManifest.new(
|
|
49
|
+
ucode_version: ucode_version,
|
|
50
|
+
unicode_version: unicode_version,
|
|
51
|
+
generated_at: generated_at,
|
|
52
|
+
source_config_sha256: source_config_sha256,
|
|
53
|
+
formulas_total: formulas.size,
|
|
54
|
+
faces_total: formulas.sum(&:faces_total),
|
|
55
|
+
universal_set: build_universal_set(resolved_uset_root),
|
|
56
|
+
formulas: formulas.map { |fa| build_formula_entry(fa) },
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
attr_reader :release_root
|
|
63
|
+
|
|
64
|
+
def build_universal_set(uset_root)
|
|
65
|
+
path = Pathname.new(uset_root)
|
|
66
|
+
manifest_path = path.join(UNIVERSAL_SET_MANIFEST)
|
|
67
|
+
glyphs_dir = path.join(UNIVERSAL_SET_GLYPHS_DIR)
|
|
68
|
+
unless path.directory?
|
|
69
|
+
return unavailable("universal-set directory not found at #{path}")
|
|
70
|
+
end
|
|
71
|
+
unless manifest_path.file?
|
|
72
|
+
return unavailable("manifest.json not found at #{manifest_path}")
|
|
73
|
+
end
|
|
74
|
+
unless glyphs_dir.directory?
|
|
75
|
+
return unavailable("glyphs/ directory not found at #{glyphs_dir}")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
manifest = load_manifest(manifest_path)
|
|
79
|
+
Models::Audit::ReleaseUniversalSet.new(
|
|
80
|
+
available: true,
|
|
81
|
+
manifest_path: manifest_path.relative_path_from(path.parent).to_s,
|
|
82
|
+
glyphs_dir: glyphs_dir.relative_path_from(path.parent).to_s,
|
|
83
|
+
unicode_version: manifest["unicode_version"],
|
|
84
|
+
totals: manifest["totals"] || {},
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def unavailable(reason)
|
|
89
|
+
Models::Audit::ReleaseUniversalSet.new(available: false, reason: reason)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def load_manifest(manifest_path)
|
|
93
|
+
JSON.parse(manifest_path.read)
|
|
94
|
+
rescue JSON::ParserError => e
|
|
95
|
+
{ "parse_error" => e.message }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def build_formula_entry(formula_audits)
|
|
99
|
+
Models::Audit::ReleaseFormulaEntry.new(
|
|
100
|
+
slug: formula_audits.slug,
|
|
101
|
+
source_path: formula_audits.summary.root_path,
|
|
102
|
+
faces_total: formula_audits.faces_total,
|
|
103
|
+
faces: formula_audits.face_reports.map do |report|
|
|
104
|
+
build_face_entry(report, formula_audits.slug)
|
|
105
|
+
end,
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def build_face_entry(report, slug)
|
|
110
|
+
card = FaceCard.new(report, slug, release_root)
|
|
111
|
+
Models::Audit::ReleaseFaceEntry.new(
|
|
112
|
+
postscript_name: report.postscript_name,
|
|
113
|
+
family_name: report.family_name,
|
|
114
|
+
weight_class: report.weight_class,
|
|
115
|
+
total_codepoints: report.total_codepoints,
|
|
116
|
+
covered_codepoints: card.covered_total,
|
|
117
|
+
blocks_complete: card.blocks_complete,
|
|
118
|
+
blocks_partial: card.blocks_partial,
|
|
119
|
+
source_sha256: report.source_sha256,
|
|
120
|
+
index_path: card.index_path,
|
|
121
|
+
html_path: card.html_path,
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|