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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -0
  3. data/.rubocop_todo.yml +93 -17
  4. data/CHANGELOG.md +12 -2
  5. data/README.adoc +6 -210
  6. data/fontisan.gemspec +48 -0
  7. data/lib/fontisan/cldr/unicode_set_parser.rb +23 -6
  8. data/lib/fontisan/cldr/version_resolver.rb +1 -1
  9. data/lib/fontisan/cli.rb +0 -170
  10. data/lib/fontisan/commands.rb +0 -3
  11. data/lib/fontisan/formatters/text_formatter.rb +0 -6
  12. data/lib/fontisan/formatters.rb +0 -3
  13. data/lib/fontisan/hints.rb +6 -3
  14. data/lib/fontisan/models.rb +4 -4
  15. data/lib/fontisan/pipeline/strategies.rb +4 -2
  16. data/lib/fontisan/pipeline.rb +2 -1
  17. data/lib/fontisan/tables/cff.rb +2 -1
  18. data/lib/fontisan/tables.rb +2 -1
  19. data/lib/fontisan/version.rb +1 -1
  20. data/lib/fontisan.rb +0 -3
  21. metadata +7 -70
  22. data/lib/fontisan/audit/codepoint_range_coalescer.rb +0 -41
  23. data/lib/fontisan/audit/context.rb +0 -122
  24. data/lib/fontisan/audit/differ.rb +0 -124
  25. data/lib/fontisan/audit/extractors/aggregations.rb +0 -54
  26. data/lib/fontisan/audit/extractors/base.rb +0 -26
  27. data/lib/fontisan/audit/extractors/color_capabilities.rb +0 -141
  28. data/lib/fontisan/audit/extractors/coverage.rb +0 -48
  29. data/lib/fontisan/audit/extractors/hinting.rb +0 -197
  30. data/lib/fontisan/audit/extractors/identity.rb +0 -52
  31. data/lib/fontisan/audit/extractors/language_coverage.rb +0 -37
  32. data/lib/fontisan/audit/extractors/licensing.rb +0 -79
  33. data/lib/fontisan/audit/extractors/metrics.rb +0 -103
  34. data/lib/fontisan/audit/extractors/opentype_layout.rb +0 -69
  35. data/lib/fontisan/audit/extractors/provenance.rb +0 -29
  36. data/lib/fontisan/audit/extractors/style.rb +0 -32
  37. data/lib/fontisan/audit/extractors/variation_detail.rb +0 -99
  38. data/lib/fontisan/audit/extractors.rb +0 -27
  39. data/lib/fontisan/audit/library_aggregator.rb +0 -83
  40. data/lib/fontisan/audit/library_auditor.rb +0 -90
  41. data/lib/fontisan/audit/registry.rb +0 -60
  42. data/lib/fontisan/audit/style_extractor.rb +0 -80
  43. data/lib/fontisan/audit.rb +0 -20
  44. data/lib/fontisan/cli/ucd_cli.rb +0 -97
  45. data/lib/fontisan/commands/audit_command.rb +0 -123
  46. data/lib/fontisan/commands/audit_compare_command.rb +0 -66
  47. data/lib/fontisan/commands/audit_library_command.rb +0 -46
  48. data/lib/fontisan/config/ucd.yml +0 -23
  49. data/lib/fontisan/formatters/audit_diff_text_renderer.rb +0 -122
  50. data/lib/fontisan/formatters/audit_text_renderer.rb +0 -324
  51. data/lib/fontisan/formatters/library_summary_text_renderer.rb +0 -99
  52. data/lib/fontisan/models/audit/audit_axis.rb +0 -30
  53. data/lib/fontisan/models/audit/audit_block.rb +0 -32
  54. data/lib/fontisan/models/audit/audit_diff.rb +0 -77
  55. data/lib/fontisan/models/audit/audit_report.rb +0 -153
  56. data/lib/fontisan/models/audit/codepoint_range.rb +0 -40
  57. data/lib/fontisan/models/audit/codepoint_set_diff.rb +0 -34
  58. data/lib/fontisan/models/audit/color_capabilities.rb +0 -93
  59. data/lib/fontisan/models/audit/duplicate_group.rb +0 -23
  60. data/lib/fontisan/models/audit/embedding_type.rb +0 -76
  61. data/lib/fontisan/models/audit/field_change.rb +0 -28
  62. data/lib/fontisan/models/audit/fs_selection_flags.rb +0 -61
  63. data/lib/fontisan/models/audit/gasp_range.rb +0 -63
  64. data/lib/fontisan/models/audit/hinting.rb +0 -93
  65. data/lib/fontisan/models/audit/library_summary.rb +0 -40
  66. data/lib/fontisan/models/audit/licensing.rb +0 -48
  67. data/lib/fontisan/models/audit/metrics.rb +0 -111
  68. data/lib/fontisan/models/audit/named_instance.rb +0 -41
  69. data/lib/fontisan/models/audit/opentype_layout.rb +0 -40
  70. data/lib/fontisan/models/audit/script_coverage_row.rb +0 -26
  71. data/lib/fontisan/models/audit/script_features.rb +0 -28
  72. data/lib/fontisan/models/audit/variation_detail.rb +0 -44
  73. data/lib/fontisan/models/audit.rb +0 -33
  74. data/lib/fontisan/models/ucd/ucd.rb +0 -38
  75. data/lib/fontisan/models/ucd/ucd_char.rb +0 -67
  76. data/lib/fontisan/models/ucd.rb +0 -19
  77. data/lib/fontisan/ucd/aggregator.rb +0 -73
  78. data/lib/fontisan/ucd/cache_manager.rb +0 -111
  79. data/lib/fontisan/ucd/config.rb +0 -59
  80. data/lib/fontisan/ucd/download_error.rb +0 -9
  81. data/lib/fontisan/ucd/downloader.rb +0 -88
  82. data/lib/fontisan/ucd/error.rb +0 -8
  83. data/lib/fontisan/ucd/index.rb +0 -103
  84. data/lib/fontisan/ucd/index_builder.rb +0 -107
  85. data/lib/fontisan/ucd/range_entry.rb +0 -56
  86. data/lib/fontisan/ucd/unknown_version_error.rb +0 -9
  87. data/lib/fontisan/ucd/version_resolver.rb +0 -79
  88. 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
@@ -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
@@ -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
@@ -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