fontisan 0.2.22 → 0.2.23
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/.rubocop.yml +6 -0
- data/.rubocop_todo.yml +93 -17
- data/CHANGELOG.md +12 -2
- data/README.adoc +6 -210
- data/fontisan.gemspec +48 -0
- data/lib/fontisan/cldr/unicode_set_parser.rb +23 -6
- data/lib/fontisan/cldr/version_resolver.rb +1 -1
- data/lib/fontisan/cli.rb +0 -170
- data/lib/fontisan/commands.rb +0 -3
- data/lib/fontisan/formatters/text_formatter.rb +0 -6
- data/lib/fontisan/formatters.rb +0 -3
- data/lib/fontisan/hints.rb +6 -3
- data/lib/fontisan/models.rb +4 -4
- data/lib/fontisan/pipeline/strategies.rb +4 -2
- data/lib/fontisan/pipeline.rb +2 -1
- data/lib/fontisan/tables/cff.rb +2 -1
- data/lib/fontisan/tables.rb +2 -1
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +0 -3
- metadata +7 -70
- data/lib/fontisan/audit/codepoint_range_coalescer.rb +0 -41
- data/lib/fontisan/audit/context.rb +0 -122
- data/lib/fontisan/audit/differ.rb +0 -124
- data/lib/fontisan/audit/extractors/aggregations.rb +0 -54
- data/lib/fontisan/audit/extractors/base.rb +0 -26
- data/lib/fontisan/audit/extractors/color_capabilities.rb +0 -141
- data/lib/fontisan/audit/extractors/coverage.rb +0 -48
- data/lib/fontisan/audit/extractors/hinting.rb +0 -197
- data/lib/fontisan/audit/extractors/identity.rb +0 -52
- data/lib/fontisan/audit/extractors/language_coverage.rb +0 -37
- data/lib/fontisan/audit/extractors/licensing.rb +0 -79
- data/lib/fontisan/audit/extractors/metrics.rb +0 -103
- data/lib/fontisan/audit/extractors/opentype_layout.rb +0 -69
- data/lib/fontisan/audit/extractors/provenance.rb +0 -29
- data/lib/fontisan/audit/extractors/style.rb +0 -32
- data/lib/fontisan/audit/extractors/variation_detail.rb +0 -99
- data/lib/fontisan/audit/extractors.rb +0 -27
- data/lib/fontisan/audit/library_aggregator.rb +0 -83
- data/lib/fontisan/audit/library_auditor.rb +0 -90
- data/lib/fontisan/audit/registry.rb +0 -60
- data/lib/fontisan/audit/style_extractor.rb +0 -80
- data/lib/fontisan/audit.rb +0 -20
- data/lib/fontisan/cli/ucd_cli.rb +0 -97
- data/lib/fontisan/commands/audit_command.rb +0 -123
- data/lib/fontisan/commands/audit_compare_command.rb +0 -66
- data/lib/fontisan/commands/audit_library_command.rb +0 -46
- data/lib/fontisan/config/ucd.yml +0 -23
- data/lib/fontisan/formatters/audit_diff_text_renderer.rb +0 -122
- data/lib/fontisan/formatters/audit_text_renderer.rb +0 -324
- data/lib/fontisan/formatters/library_summary_text_renderer.rb +0 -99
- data/lib/fontisan/models/audit/audit_axis.rb +0 -30
- data/lib/fontisan/models/audit/audit_block.rb +0 -32
- data/lib/fontisan/models/audit/audit_diff.rb +0 -77
- data/lib/fontisan/models/audit/audit_report.rb +0 -153
- data/lib/fontisan/models/audit/codepoint_range.rb +0 -40
- data/lib/fontisan/models/audit/codepoint_set_diff.rb +0 -34
- data/lib/fontisan/models/audit/color_capabilities.rb +0 -93
- data/lib/fontisan/models/audit/duplicate_group.rb +0 -23
- data/lib/fontisan/models/audit/embedding_type.rb +0 -76
- data/lib/fontisan/models/audit/field_change.rb +0 -28
- data/lib/fontisan/models/audit/fs_selection_flags.rb +0 -61
- data/lib/fontisan/models/audit/gasp_range.rb +0 -63
- data/lib/fontisan/models/audit/hinting.rb +0 -93
- data/lib/fontisan/models/audit/library_summary.rb +0 -40
- data/lib/fontisan/models/audit/licensing.rb +0 -48
- data/lib/fontisan/models/audit/metrics.rb +0 -111
- data/lib/fontisan/models/audit/named_instance.rb +0 -41
- data/lib/fontisan/models/audit/opentype_layout.rb +0 -40
- data/lib/fontisan/models/audit/script_coverage_row.rb +0 -26
- data/lib/fontisan/models/audit/script_features.rb +0 -28
- data/lib/fontisan/models/audit/variation_detail.rb +0 -44
- data/lib/fontisan/models/audit.rb +0 -33
- data/lib/fontisan/models/ucd/ucd.rb +0 -38
- data/lib/fontisan/models/ucd/ucd_char.rb +0 -67
- data/lib/fontisan/models/ucd.rb +0 -19
- data/lib/fontisan/ucd/aggregator.rb +0 -73
- data/lib/fontisan/ucd/cache_manager.rb +0 -111
- data/lib/fontisan/ucd/config.rb +0 -59
- data/lib/fontisan/ucd/download_error.rb +0 -9
- data/lib/fontisan/ucd/downloader.rb +0 -88
- data/lib/fontisan/ucd/error.rb +0 -8
- data/lib/fontisan/ucd/index.rb +0 -103
- data/lib/fontisan/ucd/index_builder.rb +0 -107
- data/lib/fontisan/ucd/range_entry.rb +0 -56
- data/lib/fontisan/ucd/unknown_version_error.rb +0 -9
- data/lib/fontisan/ucd/version_resolver.rb +0 -79
- data/lib/fontisan/ucd.rb +0 -23
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Fontisan
|
|
4
|
-
module Audit
|
|
5
|
-
# Extracts style descriptors from a loaded font's OS/2 and head tables.
|
|
6
|
-
# One extractor per font; cheap to construct.
|
|
7
|
-
#
|
|
8
|
-
# All fields return nil when the underlying table is absent or the
|
|
9
|
-
# value is unset (e.g., Type 1 fonts have no OS/2). Callers must
|
|
10
|
-
# tolerate nils.
|
|
11
|
-
#
|
|
12
|
-
# Scope: OS/2 + head only. fvar-derived fields (axes, named instances,
|
|
13
|
-
# variable presence) live on {Extractors::VariationDetail} — this is
|
|
14
|
-
# the MECE split between static style metadata and variation metadata.
|
|
15
|
-
#
|
|
16
|
-
# Duck typing: uses only `font.has_table?(tag)` and `font.table(tag)`.
|
|
17
|
-
# No class-specific branching — any object that honors the SFNT
|
|
18
|
-
# contract works (TrueTypeFont, OpenTypeFont, WoffFont, Woff2Font,
|
|
19
|
-
# and individual faces from collections).
|
|
20
|
-
class StyleExtractor
|
|
21
|
-
FS_SELECTION_ITALIC_BIT = 0
|
|
22
|
-
MAC_STYLE_BOLD_BIT = 0
|
|
23
|
-
private_constant :FS_SELECTION_ITALIC_BIT, :MAC_STYLE_BOLD_BIT
|
|
24
|
-
|
|
25
|
-
# @param font [Object] an SFNT-compatible font object
|
|
26
|
-
def initialize(font)
|
|
27
|
-
@font = font
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def weight_class
|
|
31
|
-
os2&.us_weight_class&.to_i
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def width_class
|
|
35
|
-
os2&.us_width_class&.to_i
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# OS/2.fsSelection bit 0 (ITALIC).
|
|
39
|
-
def italic
|
|
40
|
-
return nil unless os2
|
|
41
|
-
|
|
42
|
-
(os2.fs_selection.to_i & (1 << FS_SELECTION_ITALIC_BIT)).nonzero?
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# head.macStyle bit 0 (BOLD). Per OpenType convention, bold is read
|
|
46
|
-
# from head, not OS/2.
|
|
47
|
-
def bold
|
|
48
|
-
return nil unless head
|
|
49
|
-
|
|
50
|
-
(head.mac_style.to_i & (1 << MAC_STYLE_BOLD_BIT)).nonzero?
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# OS/2.panose as a space-joined 10-digit string, e.g. "2 0 5 3 0 0 0 0 0 0".
|
|
54
|
-
# Returns nil if there is no OS/2 table.
|
|
55
|
-
def panose
|
|
56
|
-
bytes = os2&.panose
|
|
57
|
-
return nil if bytes.nil?
|
|
58
|
-
|
|
59
|
-
bytes = bytes.to_a
|
|
60
|
-
return nil if bytes.empty?
|
|
61
|
-
|
|
62
|
-
bytes.join(" ")
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
private
|
|
66
|
-
|
|
67
|
-
def os2
|
|
68
|
-
return @os2 if defined?(@os2)
|
|
69
|
-
|
|
70
|
-
@os2 = @font.has_table?("OS/2") ? @font.table("OS/2") : nil
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def head
|
|
74
|
-
return @head if defined?(@head)
|
|
75
|
-
|
|
76
|
-
@head = @font.has_table?("head") ? @font.table("head") : nil
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
data/lib/fontisan/audit.rb
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Autoload hub for the Fontisan::Audit namespace.
|
|
4
|
-
#
|
|
5
|
-
# AuditCommand (under Commands::AuditCommand) builds a Context and
|
|
6
|
-
# runs every extractor in Audit::Registry, merging their outputs
|
|
7
|
-
# into a single AuditReport.
|
|
8
|
-
|
|
9
|
-
module Fontisan
|
|
10
|
-
module Audit
|
|
11
|
-
autoload :Context, "fontisan/audit/context"
|
|
12
|
-
autoload :CodepointRangeCoalescer, "fontisan/audit/codepoint_range_coalescer"
|
|
13
|
-
autoload :Differ, "fontisan/audit/differ"
|
|
14
|
-
autoload :LibraryAggregator, "fontisan/audit/library_aggregator"
|
|
15
|
-
autoload :LibraryAuditor, "fontisan/audit/library_auditor"
|
|
16
|
-
autoload :Registry, "fontisan/audit/registry"
|
|
17
|
-
autoload :Extractors, "fontisan/audit/extractors"
|
|
18
|
-
autoload :StyleExtractor, "fontisan/audit/style_extractor"
|
|
19
|
-
end
|
|
20
|
-
end
|
data/lib/fontisan/cli/ucd_cli.rb
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "thor"
|
|
4
|
-
|
|
5
|
-
module Fontisan
|
|
6
|
-
# Thor subcommand for managing the local UCD (Unicode Character
|
|
7
|
-
# Database) cache used by `fontisan audit`.
|
|
8
|
-
#
|
|
9
|
-
# fontisan ucd download [VERSION] fetch + index UCDXML
|
|
10
|
-
# fontisan ucd status show what's cached
|
|
11
|
-
# fontisan ucd path [VERSION] print local cache path
|
|
12
|
-
# fontisan ucd list list known versions
|
|
13
|
-
# fontisan ucd remove VERSION delete a cached version
|
|
14
|
-
#
|
|
15
|
-
# With no arguments, `download` resolves the configured default version
|
|
16
|
-
# (see lib/fontisan/config/ucd.yml).
|
|
17
|
-
class UcdCli < Thor
|
|
18
|
-
desc "download [VERSION]",
|
|
19
|
-
"Download and index UCDXML (default: configured default version)"
|
|
20
|
-
option :force, type: :boolean, default: false,
|
|
21
|
-
desc: "Re-download even if already cached"
|
|
22
|
-
option :latest, type: :boolean, default: false,
|
|
23
|
-
desc: "Probe unicode.org for the latest version"
|
|
24
|
-
# Download (and index) UCDXML for a version.
|
|
25
|
-
#
|
|
26
|
-
# @param version [String, nil] explicit version, or omit for default
|
|
27
|
-
def download(version = nil)
|
|
28
|
-
intent = resolve_intent(version, options[:latest])
|
|
29
|
-
actual = Ucd::VersionResolver.resolve(intent)
|
|
30
|
-
|
|
31
|
-
path = Ucd::Downloader.download(actual, force: options[:force])
|
|
32
|
-
Ucd::IndexBuilder.build(actual) unless index_present?(actual)
|
|
33
|
-
puts "UCD #{actual} ready at: #{path}"
|
|
34
|
-
rescue Ucd::Error => e
|
|
35
|
-
warn "ERROR: #{e.message}"
|
|
36
|
-
exit 1
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
desc "status", "Show cached UCD versions and default version"
|
|
40
|
-
# Print a one-screen summary of the local cache state.
|
|
41
|
-
def status
|
|
42
|
-
cached = Ucd::CacheManager.cached_versions
|
|
43
|
-
puts "Default version: #{Ucd::Config.default_version}"
|
|
44
|
-
puts "Cache root: #{Ucd::CacheManager.root}"
|
|
45
|
-
puts "Cached versions: #{cached.empty? ? '(none)' : cached.join(', ')}"
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
desc "path [VERSION]", "Print local cache directory for a version"
|
|
49
|
-
# Print the cache directory path for a version (default: default version).
|
|
50
|
-
#
|
|
51
|
-
# @param version [String, nil]
|
|
52
|
-
def path(version = nil)
|
|
53
|
-
actual = Ucd::VersionResolver.resolve(version)
|
|
54
|
-
puts Ucd::CacheManager.version_dir(actual)
|
|
55
|
-
rescue Ucd::UnknownVersionError => e
|
|
56
|
-
warn "ERROR: #{e.message}"
|
|
57
|
-
exit 1
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
desc "list", "List UCD versions known to this Fontisan release"
|
|
61
|
-
# Print the curated list of versions this Fontisan release supports.
|
|
62
|
-
def list
|
|
63
|
-
Ucd::Config.known_versions.each { |v| puts v }
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
desc "remove VERSION", "Remove a cached UCD version"
|
|
67
|
-
# Delete one cached version. No-op if absent.
|
|
68
|
-
#
|
|
69
|
-
# @param version [String]
|
|
70
|
-
def remove(version)
|
|
71
|
-
Ucd::VersionResolver.validate!(version)
|
|
72
|
-
unless Ucd::CacheManager.cached?(version)
|
|
73
|
-
warn "Version #{version} is not cached; nothing to remove."
|
|
74
|
-
return
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
Ucd::CacheManager.remove_version(version)
|
|
78
|
-
puts "Removed UCD #{version}."
|
|
79
|
-
rescue Ucd::UnknownVersionError => e
|
|
80
|
-
warn "ERROR: #{e.message}"
|
|
81
|
-
exit 1
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
private
|
|
85
|
-
|
|
86
|
-
def resolve_intent(version, latest)
|
|
87
|
-
return :latest if latest && version.nil?
|
|
88
|
-
|
|
89
|
-
version
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def index_present?(version)
|
|
93
|
-
Ucd::CacheManager.blocks_index_path(version).exist? &&
|
|
94
|
-
Ucd::CacheManager.scripts_index_path(version).exist?
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "fileutils"
|
|
4
|
-
|
|
5
|
-
module Fontisan
|
|
6
|
-
module Commands
|
|
7
|
-
# Produces a complete per-face font audit report.
|
|
8
|
-
#
|
|
9
|
-
# One AuditReport per face. For standalone fonts (TTF/OTF/WOFF/WOFF2),
|
|
10
|
-
# #run returns a single AuditReport. For collections (TTC/OTC/dfont),
|
|
11
|
-
# #run returns an Array<AuditReport> — one per face, in source order.
|
|
12
|
-
#
|
|
13
|
-
# The report is assembled by running every extractor in
|
|
14
|
-
# {Audit::Registry} against an {Audit::Context}. Each extractor
|
|
15
|
-
# owns one concern (provenance, identity, style, coverage,
|
|
16
|
-
# aggregations, …). Adding a new concern means adding one
|
|
17
|
-
# extractor class and one line in the registry — AuditCommand
|
|
18
|
-
# itself never changes.
|
|
19
|
-
class AuditCommand < BaseCommand
|
|
20
|
-
# @return [Models::Audit::AuditReport, Array<Models::Audit::AuditReport>]
|
|
21
|
-
def run
|
|
22
|
-
if FontLoader.collection?(@font_path)
|
|
23
|
-
audit_collection
|
|
24
|
-
else
|
|
25
|
-
audit_face(@font, 0, 1)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Write one file per face under `to` (a directory). Pure utility —
|
|
30
|
-
# operates on a pre-built reports array, no font_path required.
|
|
31
|
-
#
|
|
32
|
-
# @param reports [Array<Models::Audit::AuditReport>]
|
|
33
|
-
# @param to [String] output directory; created if missing
|
|
34
|
-
# @param format [Symbol] :yaml or :json
|
|
35
|
-
# @return [Array<String>] written file paths
|
|
36
|
-
def self.write_reports(reports, to:, format: :yaml)
|
|
37
|
-
FileUtils.mkdir_p(to)
|
|
38
|
-
|
|
39
|
-
reports.map do |report|
|
|
40
|
-
path = File.join(to, output_filename(report, format))
|
|
41
|
-
content = format == :json ? report.to_json : report.to_yaml
|
|
42
|
-
File.write(path, content)
|
|
43
|
-
path
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Compute the per-face filename for a report.
|
|
48
|
-
#
|
|
49
|
-
# @param report [Models::Audit::AuditReport]
|
|
50
|
-
# @param format [Symbol] :yaml or :json
|
|
51
|
-
# @return [String] filename only (no directory)
|
|
52
|
-
def self.output_filename(report, format)
|
|
53
|
-
ext = format == :json ? "json" : "yaml"
|
|
54
|
-
base = if report.num_fonts_in_source == 1
|
|
55
|
-
safe_filename(report.postscript_name || report.family_name || "font")
|
|
56
|
-
else
|
|
57
|
-
format("%<idx>02d-%<name>s",
|
|
58
|
-
idx: report.font_index,
|
|
59
|
-
name: safe_filename(report.postscript_name || "face"))
|
|
60
|
-
end
|
|
61
|
-
"#{base}.#{ext}"
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# Sanitize an arbitrary string into a filesystem-safe basename.
|
|
65
|
-
#
|
|
66
|
-
# @param name [String, nil]
|
|
67
|
-
# @return [String]
|
|
68
|
-
def self.safe_filename(name)
|
|
69
|
-
return "font" if name.nil? || name.empty?
|
|
70
|
-
|
|
71
|
-
name.gsub(/[^A-Za-z0-9._-]/, "_")
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
private
|
|
75
|
-
|
|
76
|
-
def audit_collection
|
|
77
|
-
collection = FontLoader.load_collection(@font_path)
|
|
78
|
-
num = collection.num_fonts
|
|
79
|
-
Array.new(num) do |index|
|
|
80
|
-
font = FontLoader.load(@font_path, font_index: index,
|
|
81
|
-
mode: LoadingModes::FULL)
|
|
82
|
-
audit_face(font, index, num)
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def audit_face(font, font_index, num_fonts_in_source)
|
|
87
|
-
context = Audit::Context.new(
|
|
88
|
-
font: font,
|
|
89
|
-
font_path: @font_path,
|
|
90
|
-
font_index: font_index,
|
|
91
|
-
num_fonts_in_source: num_fonts_in_source,
|
|
92
|
-
options: @options,
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
fields = {}
|
|
96
|
-
Audit::Registry.each(mode: audit_mode) do |extractor_class|
|
|
97
|
-
fields.merge!(extractor_class.new.extract(context))
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
fields[:warning] = combine_warnings(
|
|
101
|
-
context.ucd[:warning],
|
|
102
|
-
context.cldr&.dig(:warning),
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
Models::Audit::AuditReport.new(**fields)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# Audit's --brief selects a cheap extractor subset (identity, style,
|
|
109
|
-
# licensing, coverage) but still requires FULL font loading — the
|
|
110
|
-
# Coverage extractor reads `cmap`. The CLI translates the user-facing
|
|
111
|
-
# `--brief` flag into `:audit_brief` so BaseCommand's `:brief →
|
|
112
|
-
# LoadingModes::METADATA` shortcut doesn't fire.
|
|
113
|
-
def audit_mode
|
|
114
|
-
@options[:audit_brief] ? :brief : :full
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def combine_warnings(*warnings)
|
|
118
|
-
compacted = warnings.flatten.compact
|
|
119
|
-
compacted.empty? ? nil : compacted.join("; ")
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
end
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Fontisan
|
|
4
|
-
module Commands
|
|
5
|
-
# Diffs two faces or two saved audit reports.
|
|
6
|
-
#
|
|
7
|
-
# Each input is one of:
|
|
8
|
-
# - A path to a `.yaml`/`.json` file previously written by
|
|
9
|
-
# `fontisan audit -o`. Loaded as an AuditReport.
|
|
10
|
-
# - A path to a font file. Audited on-the-fly via AuditCommand.
|
|
11
|
-
#
|
|
12
|
-
# Returns an {Models::Audit::AuditDiff}. The CLI renders it as
|
|
13
|
-
# YAML/JSON (text formatter lands in TODO 25).
|
|
14
|
-
#
|
|
15
|
-
# Mixed inputs are allowed (font vs. saved report), which is useful
|
|
16
|
-
# for tracking a font's evolution against a checked-in baseline.
|
|
17
|
-
class AuditCompareCommand
|
|
18
|
-
# @param left_path [String] path to font file or saved report
|
|
19
|
-
# @param right_path [String] path to font file or saved report
|
|
20
|
-
# @param options [Hash] forwarded to AuditCommand for any input
|
|
21
|
-
# that needs to be audited fresh
|
|
22
|
-
def initialize(left_path, right_path, options = {})
|
|
23
|
-
@left_path = left_path
|
|
24
|
-
@right_path = right_path
|
|
25
|
-
@options = options
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# @return [Models::Audit::AuditDiff]
|
|
29
|
-
def run
|
|
30
|
-
left_report = load_report(@left_path)
|
|
31
|
-
right_report = load_report(@right_path)
|
|
32
|
-
Audit::Differ.new(left_report, right_report).diff
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
def load_report(path)
|
|
38
|
-
if saved_report?(path)
|
|
39
|
-
load_saved_report(path)
|
|
40
|
-
else
|
|
41
|
-
AuditCommand.new(path, audit_options).run
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def saved_report?(path)
|
|
46
|
-
ext = File.extname(path).downcase
|
|
47
|
-
[".yaml", ".yml", ".json"].include?(ext)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def load_saved_report(path)
|
|
51
|
-
case File.extname(path).downcase
|
|
52
|
-
when ".json"
|
|
53
|
-
Models::Audit::AuditReport.from_json(File.read(path))
|
|
54
|
-
else
|
|
55
|
-
Models::Audit::AuditReport.from_yaml(File.read(path))
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Forward only the audit-relevant options when auditing fresh fonts.
|
|
60
|
-
# Drops `--compare` (consumed here) and `--output` (no file output).
|
|
61
|
-
def audit_options
|
|
62
|
-
@options.except(:compare, :output)
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Fontisan
|
|
4
|
-
module Commands
|
|
5
|
-
# Audits every font in a directory (tree) and rolls the per-face
|
|
6
|
-
# reports up into a {Models::Audit::LibrarySummary}.
|
|
7
|
-
#
|
|
8
|
-
# Thin wrapper over {Audit::LibraryAuditor}: validates the root
|
|
9
|
-
# path exists, delegates to the auditor, returns the summary.
|
|
10
|
-
# The auditor itself owns file discovery and per-face auditing;
|
|
11
|
-
# this command is the CLI-facing boundary that maps user-facing
|
|
12
|
-
# options onto auditor inputs.
|
|
13
|
-
class AuditLibraryCommand
|
|
14
|
-
# @param root_path [String] directory containing fonts
|
|
15
|
-
# @param recursive [Boolean] walk into subdirectories
|
|
16
|
-
# @param options [Hash] forwarded to AuditCommand for each face
|
|
17
|
-
def initialize(root_path, recursive:, options:)
|
|
18
|
-
@root_path = root_path
|
|
19
|
-
@recursive = recursive
|
|
20
|
-
@options = options
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# @return [Models::Audit::LibrarySummary]
|
|
24
|
-
def run
|
|
25
|
-
raise Error, "library audit requires an existing directory: #{@root_path}" unless Dir.exist?(@root_path)
|
|
26
|
-
|
|
27
|
-
auditor.audit
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# @return [Array<String>] files skipped during the audit pass
|
|
31
|
-
def skipped
|
|
32
|
-
auditor.skipped
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
def auditor
|
|
38
|
-
@auditor ||= Audit::LibraryAuditor.new(
|
|
39
|
-
@root_path,
|
|
40
|
-
recursive: @recursive,
|
|
41
|
-
options: @options,
|
|
42
|
-
)
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
data/lib/fontisan/config/ucd.yml
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# UCD (Unicode Character Database) configuration.
|
|
2
|
-
#
|
|
3
|
-
# This file is the single source of truth for which UCD version Fontisan
|
|
4
|
-
# uses by default, what versions are recognized, and where to fetch them.
|
|
5
|
-
#
|
|
6
|
-
# Update `default_version` and `known_versions` when a new Unicode version
|
|
7
|
-
# is released (typically twice a year). Users may override at runtime via
|
|
8
|
-
# `fontisan ucd download --version X.Y.Z` or `fontisan audit --ucd-version X.Y.Z`.
|
|
9
|
-
|
|
10
|
-
default_version: "17.0.0"
|
|
11
|
-
|
|
12
|
-
known_versions:
|
|
13
|
-
- "15.0.0"
|
|
14
|
-
- "15.1.0"
|
|
15
|
-
- "16.0.0"
|
|
16
|
-
- "17.0.0"
|
|
17
|
-
|
|
18
|
-
# Base URL for fetching UCDXML artifacts.
|
|
19
|
-
# Full URL: <base_url>/<version>/ucdxml/ucd.all.flat.zip
|
|
20
|
-
base_url: "https://www.unicode.org/Public"
|
|
21
|
-
|
|
22
|
-
# Listing URL for `--latest` probing. HTML scraping, best-effort.
|
|
23
|
-
listing_url: "https://www.unicode.org/Public/ucdxml/"
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Fontisan
|
|
4
|
-
module Formatters
|
|
5
|
-
# Human-readable diff of two {Models::Audit::AuditReport}s.
|
|
6
|
-
#
|
|
7
|
-
# Output groups changes by kind: scalar field changes, codepoint set
|
|
8
|
-
# deltas (added/removed counts and a preview of the ranges), then
|
|
9
|
-
# structural inventory changes (scripts, features, blocks, languages).
|
|
10
|
-
# Empty sections are omitted so a no-op diff prints only the header.
|
|
11
|
-
class AuditDiffTextRenderer
|
|
12
|
-
SEPARATOR = "=" * 80
|
|
13
|
-
LIST_LIMIT = 10
|
|
14
|
-
|
|
15
|
-
# @param diff [Models::Audit::AuditDiff]
|
|
16
|
-
def initialize(diff)
|
|
17
|
-
@diff = diff
|
|
18
|
-
@lines = []
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# @return [String]
|
|
22
|
-
def render
|
|
23
|
-
render_header
|
|
24
|
-
render_field_changes
|
|
25
|
-
render_codepoint_delta
|
|
26
|
-
render_structural_changes
|
|
27
|
-
render_empty_note
|
|
28
|
-
@lines.join("\n")
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
private
|
|
32
|
-
|
|
33
|
-
def render_header
|
|
34
|
-
@lines << "AUDIT DIFF"
|
|
35
|
-
@lines << SEPARATOR
|
|
36
|
-
@lines << "left: #{@diff.left_source}"
|
|
37
|
-
@lines << "right: #{@diff.right_source}"
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def render_field_changes
|
|
41
|
-
changes = Array(@diff.field_changes)
|
|
42
|
-
return if changes.empty?
|
|
43
|
-
|
|
44
|
-
section("FIELD CHANGES (#{changes.size})")
|
|
45
|
-
changes.each do |change|
|
|
46
|
-
@lines << " #{change.field}: #{change.left.inspect} → #{change.right.inspect}"
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def render_codepoint_delta
|
|
51
|
-
delta = @diff.codepoints
|
|
52
|
-
return unless delta && (delta.added_count.to_i.positive? || delta.removed_count.to_i.positive?)
|
|
53
|
-
|
|
54
|
-
section("CODEPOINT COVERAGE")
|
|
55
|
-
@lines << " added: #{delta.added_count}"
|
|
56
|
-
@lines << " removed: #{delta.removed_count}"
|
|
57
|
-
@lines << " unchanged: #{delta.unchanged_count}"
|
|
58
|
-
preview_added(delta)
|
|
59
|
-
preview_removed(delta)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def preview_added(delta)
|
|
63
|
-
ranges = Array(delta.added)
|
|
64
|
-
return if ranges.empty?
|
|
65
|
-
|
|
66
|
-
@lines << " + #{format_ranges(ranges)}"
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def preview_removed(delta)
|
|
70
|
-
ranges = Array(delta.removed)
|
|
71
|
-
return if ranges.empty?
|
|
72
|
-
|
|
73
|
-
@lines << " - #{format_ranges(ranges)}"
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def render_structural_changes
|
|
77
|
-
render_set("SCRIPTS", @diff.added_scripts, @diff.removed_scripts)
|
|
78
|
-
render_set("FEATURES", @diff.added_features, @diff.removed_features)
|
|
79
|
-
render_set("BLOCKS", @diff.added_blocks, @diff.removed_blocks)
|
|
80
|
-
render_set("LANGUAGES", @diff.added_languages, @diff.removed_languages)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def render_set(name, added, removed)
|
|
84
|
-
added = Array(added)
|
|
85
|
-
removed = Array(removed)
|
|
86
|
-
return if added.empty? && removed.empty?
|
|
87
|
-
|
|
88
|
-
section("#{name} CHANGES")
|
|
89
|
-
@lines << " + #{truncate(added)}" unless added.empty?
|
|
90
|
-
@lines << " - #{truncate(removed)}" unless removed.empty?
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def render_empty_note
|
|
94
|
-
return unless @diff.empty?
|
|
95
|
-
|
|
96
|
-
@lines << ""
|
|
97
|
-
@lines << "(no differences)"
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
# ---- helpers --------------------------------------------------------
|
|
101
|
-
|
|
102
|
-
def section(title)
|
|
103
|
-
@lines << ""
|
|
104
|
-
@lines << title
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def truncate(list)
|
|
108
|
-
shown = list.first(LIST_LIMIT).join(", ")
|
|
109
|
-
shown += ", ..." if list.size > LIST_LIMIT
|
|
110
|
-
shown
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def format_ranges(ranges)
|
|
114
|
-
shown = ranges.first(LIST_LIMIT).map do |r|
|
|
115
|
-
"U+#{format('%04X', r.first_cp)}-U+#{format('%04X', r.last_cp)}"
|
|
116
|
-
end.join(", ")
|
|
117
|
-
shown += ", ..." if ranges.size > LIST_LIMIT
|
|
118
|
-
shown
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
end
|