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,324 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fontisan
4
- module Formatters
5
- # Human-readable, sectioned view of an {Models::Audit::AuditReport}.
6
- #
7
- # The text formatter is the default `--format text` output for
8
- # `fontisan audit`. Complements YAML/JSON (machine-facing) with a
9
- # terse, scannable terminal view. Every section is nil-safe so the
10
- # same renderer covers full OpenType/TrueType faces, Type 1 fonts
11
- # (no OS/2, no metrics, no layout), and partial reports.
12
- class AuditTextRenderer
13
- SEPARATOR = "=" * 80
14
- LABEL_WIDTH = 18
15
- LIST_LIMIT = 10
16
-
17
- WIDTH_NAMES = {
18
- 1 => "Ultra-condensed", 2 => "Extra-condensed", 3 => "Condensed",
19
- 4 => "Semi-condensed", 5 => "Medium", 6 => "Semi-expanded",
20
- 7 => "Expanded", 8 => "Extra-expanded", 9 => "Ultra-expanded"
21
- }.freeze
22
-
23
- # @param report [Models::Audit::AuditReport]
24
- def initialize(report)
25
- @report = report
26
- @lines = []
27
- end
28
-
29
- # @return [String]
30
- def render
31
- render_header
32
- render_identity
33
- render_style
34
- render_metrics
35
- render_coverage
36
- render_blocks
37
- render_licensing
38
- render_hinting
39
- render_color
40
- render_variation
41
- render_opentype_layout
42
- render_language_coverage
43
- render_warnings
44
- @lines.join("\n")
45
- end
46
-
47
- private
48
-
49
- def render_header
50
- @lines << (@report.postscript_name || @report.family_name || "(unknown)")
51
- @lines << SEPARATOR
52
- @lines << two_col("generated_at:", @report.generated_at,
53
- "fontisan:", @report.fontisan_version)
54
- @lines << "source_sha256: #{@report.source_sha256}"
55
- @lines << "source_file: #{@report.source_file}"
56
- @lines << two_col("source_format:", @report.source_format,
57
- "layout:", layout_descriptor)
58
- end
59
-
60
- def layout_descriptor
61
- if @report.num_fonts_in_source.nil? || @report.num_fonts_in_source <= 1
62
- "single face (1/1)"
63
- else
64
- format("collection face (%<idx>d/%<total>d)",
65
- idx: (@report.font_index || 0) + 1,
66
- total: @report.num_fonts_in_source)
67
- end
68
- end
69
-
70
- def render_identity
71
- section("IDENTITY")
72
- row("Family", @report.family_name)
73
- row("Subfamily", @report.subfamily_name)
74
- row("Full name", @report.full_name)
75
- row("PostScript", @report.postscript_name)
76
- row("Version", @report.version)
77
- row("Revision", @report.font_revision)
78
- end
79
-
80
- def render_style
81
- section("STYLE")
82
- row("Weight class", weight_descriptor)
83
- row("Width class", width_descriptor)
84
- row("Bold", yes_no(@report.bold))
85
- row("Italic", yes_no(@report.italic))
86
- row("PANOSE", @report.panose)
87
- end
88
-
89
- def render_metrics
90
- return unless @report.metrics
91
-
92
- m = @report.metrics
93
- section("METRICS")
94
- row("unitsPerEm", m.units_per_em)
95
- row("hhea", "ascent: #{m.hhea_ascent} / descent: #{m.hhea_descent} / line gap: #{m.hhea_line_gap}") if m.hhea_ascent
96
- row("OS/2 typo", "ascent: #{m.typo_ascender} / descent: #{m.typo_descender} / line gap: #{m.typo_line_gap}") if m.typo_ascender
97
- row("OS/2 win", "ascent: #{m.win_ascent} / descent: #{m.win_descent}") if m.win_ascent
98
- row("x-height", m.x_height)
99
- row("cap height", m.cap_height)
100
- row("bbox", bbox_descriptor(m)) if m.bbox_x_min || m.bbox_x_max
101
- end
102
-
103
- def render_coverage
104
- section("COVERAGE")
105
- row("Codepoints", @report.total_codepoints)
106
- row("Glyphs", @report.total_glyphs)
107
- row("cmap subtables", format("%s", Array(@report.cmap_subtables).join(", "))) unless Array(@report.cmap_subtables).empty?
108
- row("Ranges (top #{LIST_LIMIT})", codepoint_range_preview)
109
- row("Unicode scripts", truncate_list(@report.unicode_scripts))
110
- end
111
-
112
- def render_blocks
113
- blocks = Array(@report.blocks)
114
- return if blocks.empty?
115
-
116
- section("UNICODE BLOCKS (top #{LIST_LIMIT} by fill ratio)")
117
- blocks.sort_by { |b| -(b.fill_ratio || 0) }.first(LIST_LIMIT).each do |block|
118
- ratio = block.fill_ratio ? format("%<r>d%%", r: (block.fill_ratio * 100).round) : "?"
119
- @lines << format(" %<name>-40s %<covered>d/%<total>d (%<ratio>s)",
120
- name: "#{block.name}:", covered: block.covered || 0,
121
- total: block.total || 0, ratio: ratio)
122
- end
123
- end
124
-
125
- def render_licensing
126
- return unless @report.licensing
127
-
128
- l = @report.licensing
129
- section("LICENSING")
130
- row("Copyright", l.copyright)
131
- row("Trademark", l.trademark)
132
- row("Manufacturer", l.manufacturer)
133
- row("Designer", l.designer)
134
- row("License", l.license_description)
135
- row("License URL", l.license_url)
136
- row("Vendor URL", l.vendor_url)
137
- row("Designer URL", l.designer_url)
138
- row("Vendor ID", l.vendor_id)
139
- row("Embedding", l.embedding_type)
140
- end
141
-
142
- def render_hinting
143
- return unless @report.hinting
144
-
145
- h = @report.hinting
146
- section("HINTING")
147
- row("Format", h.hinting_format || (h.is_unhinted ? "unhinted" : "unknown"))
148
- row("fpgm", instruction_line(h.has_fpgm, h.fpgm_instruction_count))
149
- row("prep", instruction_line(h.has_prep, h.prep_instruction_count))
150
- row("cvt", cvt_line(h))
151
- row("gasp", gasp_line(h))
152
- row("CFF hints", h.cff_hint_count)
153
- end
154
-
155
- def render_color
156
- return unless @report.color_capabilities
157
-
158
- c = @report.color_capabilities
159
- section("COLOR")
160
- formats = Array(c.color_formats)
161
- row("Color formats", formats.empty? ? "(none)" : truncate_list(formats))
162
- row("COLR", colr_line(c)) if c.has_colr
163
- row("CPAL", "palettes: #{c.cpal_palette_count}, colors: #{c.cpal_color_count}") if c.has_cpal
164
- row("SVG documents", c.svg_document_count) if c.has_svg && c.svg_document_count
165
- row("CBDT strikes", c.cbdt_strike_count) if c.has_cbdt && c.cbdt_strike_count
166
- row("sbix strikes", c.sbix_strike_count) if c.has_sbix && c.sbix_strike_count
167
- end
168
-
169
- def render_variation
170
- v = @report.variation
171
- section("VARIABLE FONT")
172
- if v.nil? || Array(v.axes).empty?
173
- @lines << " (not variable)"
174
- return
175
- end
176
-
177
- v.axes.each do |axis|
178
- row(axis.tag, format("%<min>s .. %<max>s default %<default>s",
179
- min: axis.min_value, max: axis.max_value,
180
- default: axis.default_value))
181
- end
182
- return if Array(v.named_instances).empty?
183
-
184
- @lines << " Named instances:"
185
- v.named_instances.each do |inst|
186
- @lines << " #{inst.postscript_name || inst.subfamily_name}: #{inst.coordinates}"
187
- end
188
- end
189
-
190
- def render_opentype_layout
191
- return unless @report.opentype_layout
192
-
193
- l = @report.opentype_layout
194
- section("OPENTYPE LAYOUT")
195
- row("GSUB", yes_no(l.has_gsub))
196
- row("GPOS", yes_no(l.has_gpos))
197
- row("Scripts (#{Array(l.scripts).size})", truncate_list(l.scripts))
198
- row("Features (#{Array(l.features).size})", truncate_list(l.features))
199
- end
200
-
201
- def render_language_coverage
202
- langs = Array(@report.language_coverage)
203
- return if langs.empty?
204
-
205
- section("LANGUAGE COVERAGE (CLDR #{@report.cldr_version})")
206
- langs.first(LIST_LIMIT).each do |lang|
207
- pct = lang.coverage_ratio ? format("%<r>d%%", r: (lang.coverage_ratio * 100).round) : "?"
208
- mark = lang.fully_supported ? "*" : " "
209
- @lines << format(" %<mark>s %<lang>-8s %<covered>d/%<total>d (%<pct>s)",
210
- mark: mark, lang: "#{lang.language}:", covered: lang.covered,
211
- total: lang.total, pct: pct)
212
- end
213
- end
214
-
215
- def render_warnings
216
- section("WARNINGS")
217
- @lines << if @report.warning
218
- " #{@report.warning}"
219
- else
220
- " (none)"
221
- end
222
- end
223
-
224
- # ---- formatting helpers --------------------------------------------
225
-
226
- def section(title)
227
- @lines << ""
228
- @lines << title
229
- end
230
-
231
- def row(label, value)
232
- return if value.nil?
233
- return if value.is_a?(String) && value.empty?
234
-
235
- @lines << " #{label}:#{' ' * [LABEL_WIDTH - label.to_s.length - 1, 1].max}#{value}"
236
- end
237
-
238
- def two_col(left_label, left_value, right_label, right_value)
239
- left = "#{left_label} #{left_value}".ljust(40)
240
- "#{left}#{right_label} #{right_value}"
241
- end
242
-
243
- def yes_no(bool)
244
- bool ? "yes" : "no"
245
- end
246
-
247
- def truncate_list(items)
248
- list = Array(items)
249
- return "(none)" if list.empty?
250
-
251
- shown = list.first(LIST_LIMIT).join(", ")
252
- shown += ", ..." if list.size > LIST_LIMIT
253
- shown
254
- end
255
-
256
- def weight_descriptor
257
- return nil unless @report.weight_class
258
-
259
- name = weight_name(@report.weight_class)
260
- "#{@report.weight_class}#{" (#{name})" if name}"
261
- end
262
-
263
- def width_descriptor
264
- return nil unless @report.width_class
265
-
266
- name = WIDTH_NAMES[@report.width_class]
267
- "#{@report.width_class}#{" (#{name})" if name}"
268
- end
269
-
270
- def weight_name(value)
271
- case value
272
- when 100 then "Thin"
273
- when 200 then "Extra-light"
274
- when 300 then "Light"
275
- when 400 then "Regular"
276
- when 500 then "Medium"
277
- when 600 then "Semi-bold"
278
- when 700 then "Bold"
279
- when 800 then "Extra-bold"
280
- when 900 then "Black"
281
- end
282
- end
283
-
284
- def bbox_descriptor(metrics)
285
- "(#{metrics.bbox_x_min}, #{metrics.bbox_y_min}) → (#{metrics.bbox_x_max}, #{metrics.bbox_y_max})"
286
- end
287
-
288
- def codepoint_range_preview
289
- ranges = Array(@report.codepoint_ranges)
290
- return "(none)" if ranges.empty?
291
-
292
- shown = ranges.first(LIST_LIMIT).map do |r|
293
- "U+#{format('%04X', r.first_cp)}-U+#{format('%04X', r.last_cp)}"
294
- end.join(", ")
295
- shown += ", ..." if ranges.size > LIST_LIMIT
296
- shown
297
- end
298
-
299
- def instruction_line(has, count)
300
- return "no" unless has
301
-
302
- count ? "#{count} instructions" : "present"
303
- end
304
-
305
- def cvt_line(hinting)
306
- return "no" unless hinting.has_cvt
307
-
308
- hinting.cvt_entry_count ? "#{hinting.cvt_entry_count} entries" : "present"
309
- end
310
-
311
- def gasp_line(hinting)
312
- ranges = Array(hinting.gasp_ranges)
313
- return "no" if ranges.empty?
314
-
315
- ppems = ranges.map(&:max_ppem).compact
316
- "#{ranges.size} ranges (#{ppems.join('/')} ppem)"
317
- end
318
-
319
- def colr_line(color)
320
- "v#{color.colr_version}, #{color.colr_base_glyph_count} base glyphs, #{color.colr_layer_count} layers"
321
- end
322
- end
323
- end
324
- end
@@ -1,99 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fontisan
4
- module Formatters
5
- # Human-readable overview of a {Models::Audit::LibrarySummary}.
6
- #
7
- # Lists the per-face rollup counts, aggregate metrics, script coverage
8
- # matrix, duplicate groups, and license distribution. The full per-face
9
- # AuditReports are attached to the model; this view only shows the
10
- # cross-face summaries (use YAML/JSON output for the full per-face data).
11
- class LibrarySummaryTextRenderer
12
- SEPARATOR = "=" * 80
13
- LIST_LIMIT = 15
14
-
15
- # @param summary [Models::Audit::LibrarySummary]
16
- def initialize(summary)
17
- @summary = summary
18
- @lines = []
19
- end
20
-
21
- # @return [String]
22
- def render
23
- render_header
24
- render_aggregates
25
- render_script_coverage
26
- render_duplicates
27
- render_license_distribution
28
- @lines.join("\n")
29
- end
30
-
31
- private
32
-
33
- def render_header
34
- @lines << "LIBRARY SUMMARY"
35
- @lines << SEPARATOR
36
- @lines << "root: #{@summary.root_path}"
37
- @lines << "files: #{@summary.total_files} faces: #{@summary.total_faces}"
38
- exts = Array(@summary.scanned_extensions)
39
- @lines << "formats: #{exts.empty? ? '(none)' : exts.join(', ')}"
40
- end
41
-
42
- def render_aggregates
43
- m = @summary.aggregate_metrics || {}
44
- section("AGGREGATES")
45
- @lines << " codepoints: #{m[:total_codepoints] || 0}"
46
- @lines << " glyphs: #{m[:total_glyphs] || 0}"
47
- @lines << " total size: #{format_bytes(m[:total_size_bytes] || 0)}"
48
- end
49
-
50
- def render_script_coverage
51
- rows = Array(@summary.script_coverage)
52
- return if rows.empty?
53
-
54
- section("SCRIPT COVERAGE (top #{LIST_LIMIT})")
55
- rows.first(LIST_LIMIT).each do |row|
56
- @lines << " #{row.script}: #{row.face_count} face#{'s' unless row.face_count == 1}"
57
- end
58
- end
59
-
60
- def render_duplicates
61
- groups = Array(@summary.duplicate_groups)
62
- return if groups.empty?
63
-
64
- section("DUPLICATES (#{groups.size} group#{'s' unless groups.size == 1})")
65
- groups.each do |group|
66
- @lines << " sha #{group.source_sha256[0, 12]}:"
67
- group.files.each { |path| @lines << " #{path}" }
68
- end
69
- end
70
-
71
- def render_license_distribution
72
- dist = @summary.license_distribution || {}
73
- return if dist.empty?
74
-
75
- section("LICENSE DISTRIBUTION")
76
- dist.sort_by { |_url, count| -count }.each do |url, count|
77
- @lines << " #{count} #{url}"
78
- end
79
- end
80
-
81
- def section(title)
82
- @lines << ""
83
- @lines << title
84
- end
85
-
86
- def format_bytes(bytes)
87
- return "0 B" if bytes.nil? || bytes.zero?
88
-
89
- if bytes < 1024
90
- "#{bytes} B"
91
- elsif bytes < 1024 * 1024
92
- "#{(bytes / 1024.0).round(2)} KB"
93
- else
94
- "#{(bytes / (1024.0 * 1024)).round(2)} MB"
95
- end
96
- end
97
- end
98
- end
99
- end
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "lutaml/model"
4
-
5
- module Fontisan
6
- module Models
7
- module Audit
8
- # One fvar axis descriptor on an AuditReport.
9
- #
10
- # `min_value` / `default_value` / `max_value` are used (rather than
11
- # `min` / `default` / `max`) to avoid colliding with Ruby's built-in
12
- # `default` method on classes.
13
- class AuditAxis < Lutaml::Model::Serializable
14
- attribute :tag, :string
15
- attribute :min_value, :float
16
- attribute :default_value, :float
17
- attribute :max_value, :float
18
- attribute :name, :string
19
-
20
- key_value do
21
- map "tag", to: :tag
22
- map "min_value", to: :min_value
23
- map "default_value", to: :default_value
24
- map "max_value", to: :max_value
25
- map "name", to: :name
26
- end
27
- end
28
- end
29
- end
30
- end
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "lutaml/model"
4
-
5
- module Fontisan
6
- module Models
7
- module Audit
8
- # One Unicode block coverage row on an AuditReport.
9
- class AuditBlock < Lutaml::Model::Serializable
10
- attribute :name, :string
11
- attribute :first_cp, :integer
12
- attribute :last_cp, :integer
13
- attribute :range, :string
14
- attribute :total, :integer
15
- attribute :covered, :integer
16
- attribute :fill_ratio, :float
17
- attribute :complete, Lutaml::Model::Type::Boolean
18
-
19
- key_value do
20
- map "name", to: :name
21
- map "first_cp", to: :first_cp
22
- map "last_cp", to: :last_cp
23
- map "range", to: :range
24
- map "total", to: :total
25
- map "covered", to: :covered
26
- map "fill_ratio", to: :fill_ratio
27
- map "complete", to: :complete
28
- end
29
- end
30
- end
31
- end
32
- end
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "lutaml/model"
4
-
5
- module Fontisan
6
- module Models
7
- module Audit
8
- # Structural diff between two AuditReports.
9
- #
10
- # `left_source`/`right_source` are the original source_file paths
11
- # (or report paths) so a consumer reading the diff alone can locate
12
- # the inputs.
13
- #
14
- # `field_changes` lists scalar fields whose values changed.
15
- # `codepoints` is the cmap delta (CodepointSetDiff).
16
- # The remaining fields are array set-diffs over the report's
17
- # structural inventory: OpenType features, scripts, UCD blocks, and
18
- # CLDR languages. Each is split into `added_*` (in right, not left)
19
- # and `removed_*` (in left, not right).
20
- class AuditDiff < Lutaml::Model::Serializable
21
- attribute :left_source, :string
22
- attribute :right_source, :string
23
- attribute :field_changes, FieldChange, collection: true
24
- attribute :codepoints, CodepointSetDiff
25
- attribute :added_features, :string, collection: true
26
- attribute :removed_features, :string, collection: true
27
- attribute :added_scripts, :string, collection: true
28
- attribute :removed_scripts, :string, collection: true
29
- attribute :added_blocks, :string, collection: true
30
- attribute :removed_blocks, :string, collection: true
31
- attribute :added_languages, :string, collection: true
32
- attribute :removed_languages, :string, collection: true
33
-
34
- key_value do
35
- map "left_source", to: :left_source
36
- map "right_source", to: :right_source
37
- map "field_changes", to: :field_changes
38
- map "codepoints", to: :codepoints
39
- map "added_features", to: :added_features
40
- map "removed_features", to: :removed_features
41
- map "added_scripts", to: :added_scripts
42
- map "removed_scripts", to: :removed_scripts
43
- map "added_blocks", to: :added_blocks
44
- map "removed_blocks", to: :removed_blocks
45
- map "added_languages", to: :added_languages
46
- map "removed_languages", to: :removed_languages
47
- end
48
-
49
- # True when nothing differs. Useful for the text formatter.
50
- #
51
- # @return [Boolean]
52
- def empty?
53
- collection_empty?(field_changes) &&
54
- added_codepoints.zero? && removed_codepoints.zero? &&
55
- collection_empty?(added_features) && collection_empty?(removed_features) &&
56
- collection_empty?(added_scripts) && collection_empty?(removed_scripts) &&
57
- collection_empty?(added_blocks) && collection_empty?(removed_blocks) &&
58
- collection_empty?(added_languages) && collection_empty?(removed_languages)
59
- end
60
-
61
- def added_codepoints
62
- codepoints&.added_count || 0
63
- end
64
-
65
- def removed_codepoints
66
- codepoints&.removed_count || 0
67
- end
68
-
69
- private
70
-
71
- def collection_empty?(value)
72
- value.nil? || value.empty?
73
- end
74
- end
75
- end
76
- end
77
- end