fontisan 0.2.4 → 0.2.6
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_todo.yml +168 -32
- data/README.adoc +673 -1091
- data/lib/fontisan/cli.rb +94 -13
- data/lib/fontisan/collection/dfont_builder.rb +315 -0
- data/lib/fontisan/commands/convert_command.rb +118 -7
- data/lib/fontisan/commands/pack_command.rb +129 -22
- data/lib/fontisan/commands/validate_command.rb +107 -151
- data/lib/fontisan/config/conversion_matrix.yml +175 -1
- data/lib/fontisan/constants.rb +8 -0
- data/lib/fontisan/converters/collection_converter.rb +438 -0
- data/lib/fontisan/converters/woff2_encoder.rb +7 -29
- data/lib/fontisan/dfont_collection.rb +185 -0
- data/lib/fontisan/font_loader.rb +91 -6
- data/lib/fontisan/models/validation_report.rb +227 -0
- data/lib/fontisan/parsers/dfont_parser.rb +192 -0
- data/lib/fontisan/pipeline/transformation_pipeline.rb +4 -8
- data/lib/fontisan/tables/cmap.rb +82 -2
- data/lib/fontisan/tables/glyf.rb +118 -0
- data/lib/fontisan/tables/head.rb +60 -0
- data/lib/fontisan/tables/hhea.rb +74 -0
- data/lib/fontisan/tables/maxp.rb +60 -0
- data/lib/fontisan/tables/name.rb +76 -0
- data/lib/fontisan/tables/os2.rb +113 -0
- data/lib/fontisan/tables/post.rb +57 -0
- data/lib/fontisan/true_type_font.rb +8 -46
- data/lib/fontisan/validation/collection_validator.rb +265 -0
- data/lib/fontisan/validators/basic_validator.rb +85 -0
- data/lib/fontisan/validators/font_book_validator.rb +130 -0
- data/lib/fontisan/validators/opentype_validator.rb +112 -0
- data/lib/fontisan/validators/profile_loader.rb +139 -0
- data/lib/fontisan/validators/validator.rb +484 -0
- data/lib/fontisan/validators/web_font_validator.rb +102 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +78 -6
- metadata +13 -12
- data/lib/fontisan/config/validation_rules.yml +0 -149
- data/lib/fontisan/validation/checksum_validator.rb +0 -170
- data/lib/fontisan/validation/consistency_validator.rb +0 -197
- data/lib/fontisan/validation/structure_validator.rb +0 -198
- data/lib/fontisan/validation/table_validator.rb +0 -158
- data/lib/fontisan/validation/validator.rb +0 -152
- data/lib/fontisan/validation/variable_font_validator.rb +0 -218
- data/lib/fontisan/validation/woff2_header_validator.rb +0 -278
- data/lib/fontisan/validation/woff2_table_validator.rb +0 -270
- data/lib/fontisan/validation/woff2_validator.rb +0 -248
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "parsers/dfont_parser"
|
|
4
|
+
require_relative "error"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
# DfontCollection represents an Apple dfont suitcase containing multiple fonts
|
|
8
|
+
#
|
|
9
|
+
# dfont (Data Fork Font) is an Apple-specific format that stores Mac font
|
|
10
|
+
# suitcase resources in the data fork. It can contain multiple SFNT fonts
|
|
11
|
+
# (TrueType or OpenType).
|
|
12
|
+
#
|
|
13
|
+
# This class provides a collection interface similar to TrueTypeCollection
|
|
14
|
+
# and OpenTypeCollection for consistency.
|
|
15
|
+
#
|
|
16
|
+
# @example Load dfont collection
|
|
17
|
+
# collection = DfontCollection.from_file("family.dfont")
|
|
18
|
+
# puts "Collection has #{collection.num_fonts} fonts"
|
|
19
|
+
#
|
|
20
|
+
# @example Extract fonts from dfont
|
|
21
|
+
# File.open("family.dfont", "rb") do |io|
|
|
22
|
+
# fonts = collection.extract_fonts(io)
|
|
23
|
+
# fonts.each { |font| puts font.class.name }
|
|
24
|
+
# end
|
|
25
|
+
class DfontCollection
|
|
26
|
+
# Path to dfont file
|
|
27
|
+
# @return [String]
|
|
28
|
+
attr_reader :path
|
|
29
|
+
|
|
30
|
+
# Number of fonts in collection
|
|
31
|
+
# @return [Integer]
|
|
32
|
+
attr_reader :num_fonts
|
|
33
|
+
alias font_count num_fonts
|
|
34
|
+
|
|
35
|
+
# Load dfont collection from file
|
|
36
|
+
#
|
|
37
|
+
# @param path [String] Path to dfont file
|
|
38
|
+
# @return [DfontCollection] Collection object
|
|
39
|
+
# @raise [InvalidFontError] if not valid dfont
|
|
40
|
+
def self.from_file(path)
|
|
41
|
+
File.open(path, "rb") do |io|
|
|
42
|
+
unless Parsers::DfontParser.dfont?(io)
|
|
43
|
+
raise InvalidFontError, "Not a valid dfont file: #{path}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
num_fonts = Parsers::DfontParser.sfnt_count(io)
|
|
47
|
+
new(path, num_fonts)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Initialize collection
|
|
52
|
+
#
|
|
53
|
+
# @param path [String] Path to dfont file
|
|
54
|
+
# @param num_fonts [Integer] Number of fonts
|
|
55
|
+
# @api private
|
|
56
|
+
def initialize(path, num_fonts)
|
|
57
|
+
@path = path
|
|
58
|
+
@num_fonts = num_fonts
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Check if collection is valid
|
|
62
|
+
#
|
|
63
|
+
# @return [Boolean] true if valid
|
|
64
|
+
def valid?
|
|
65
|
+
File.exist?(@path) && @num_fonts.positive?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Extract all fonts from dfont
|
|
69
|
+
#
|
|
70
|
+
# @param io [IO] Open file handle
|
|
71
|
+
# @return [Array<TrueTypeFont, OpenTypeFont>] Array of fonts
|
|
72
|
+
def extract_fonts(io)
|
|
73
|
+
require "stringio"
|
|
74
|
+
|
|
75
|
+
fonts = []
|
|
76
|
+
|
|
77
|
+
@num_fonts.times do |index|
|
|
78
|
+
io.rewind
|
|
79
|
+
sfnt_data = Parsers::DfontParser.extract_sfnt(io, index: index)
|
|
80
|
+
|
|
81
|
+
# Load font from SFNT binary
|
|
82
|
+
sfnt_io = StringIO.new(sfnt_data)
|
|
83
|
+
signature = sfnt_io.read(4)
|
|
84
|
+
sfnt_io.rewind
|
|
85
|
+
|
|
86
|
+
# Create font based on signature
|
|
87
|
+
font = case signature
|
|
88
|
+
when [Constants::SFNT_VERSION_TRUETYPE].pack("N"), "true"
|
|
89
|
+
TrueTypeFont.read(sfnt_io)
|
|
90
|
+
when "OTTO"
|
|
91
|
+
OpenTypeFont.read(sfnt_io)
|
|
92
|
+
else
|
|
93
|
+
raise InvalidFontError,
|
|
94
|
+
"Invalid SFNT signature in dfont at index #{index}: #{signature.inspect}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
font.initialize_storage
|
|
98
|
+
font.loading_mode = LoadingModes::FULL
|
|
99
|
+
font.lazy_load_enabled = false
|
|
100
|
+
font.read_table_data(sfnt_io)
|
|
101
|
+
|
|
102
|
+
fonts << font
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
fonts
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# List fonts in collection (brief info)
|
|
109
|
+
#
|
|
110
|
+
# @param io [IO] Open file handle
|
|
111
|
+
# @return [Models::CollectionListInfo] Collection list info
|
|
112
|
+
def list_fonts(io)
|
|
113
|
+
require_relative "models/collection_list_info"
|
|
114
|
+
require_relative "models/collection_font_summary"
|
|
115
|
+
|
|
116
|
+
fonts = extract_fonts(io)
|
|
117
|
+
|
|
118
|
+
summaries = fonts.map.with_index do |font, index|
|
|
119
|
+
name_table = font.table("name")
|
|
120
|
+
family = name_table.english_name(Models::Tables::Name::FAMILY) || "Unknown"
|
|
121
|
+
subfamily = name_table.english_name(Models::Tables::Name::SUBFAMILY) || "Regular"
|
|
122
|
+
|
|
123
|
+
# Detect font format
|
|
124
|
+
format = if font.has_table?("CFF ") || font.has_table?("CFF2")
|
|
125
|
+
"OpenType"
|
|
126
|
+
else
|
|
127
|
+
"TrueType"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
Models::CollectionFontSummary.new(
|
|
131
|
+
index: index,
|
|
132
|
+
family_name: family,
|
|
133
|
+
subfamily_name: subfamily,
|
|
134
|
+
font_format: format,
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
Models::CollectionListInfo.new(
|
|
139
|
+
num_fonts: @num_fonts,
|
|
140
|
+
fonts: summaries,
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Get specific font from collection
|
|
145
|
+
#
|
|
146
|
+
# @param index [Integer] Font index
|
|
147
|
+
# @param io [IO] Open file handle
|
|
148
|
+
# @param mode [Symbol] Loading mode
|
|
149
|
+
# @return [TrueTypeFont, OpenTypeFont] Font object
|
|
150
|
+
# @raise [InvalidFontError] if index out of range
|
|
151
|
+
def font(index, io, mode: LoadingModes::FULL)
|
|
152
|
+
if index >= @num_fonts
|
|
153
|
+
raise InvalidFontError,
|
|
154
|
+
"Font index #{index} out of range (collection has #{@num_fonts} fonts)"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
io.rewind
|
|
158
|
+
sfnt_data = Parsers::DfontParser.extract_sfnt(io, index: index)
|
|
159
|
+
|
|
160
|
+
# Load font from SFNT binary
|
|
161
|
+
require "stringio"
|
|
162
|
+
sfnt_io = StringIO.new(sfnt_data)
|
|
163
|
+
signature = sfnt_io.read(4)
|
|
164
|
+
sfnt_io.rewind
|
|
165
|
+
|
|
166
|
+
# Create font based on signature
|
|
167
|
+
font = case signature
|
|
168
|
+
when [Constants::SFNT_VERSION_TRUETYPE].pack("N"), "true"
|
|
169
|
+
TrueTypeFont.read(sfnt_io)
|
|
170
|
+
when "OTTO"
|
|
171
|
+
OpenTypeFont.read(sfnt_io)
|
|
172
|
+
else
|
|
173
|
+
raise InvalidFontError,
|
|
174
|
+
"Invalid SFNT signature: #{signature.inspect}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
font.initialize_storage
|
|
178
|
+
font.loading_mode = mode
|
|
179
|
+
font.lazy_load_enabled = false
|
|
180
|
+
font.read_table_data(sfnt_io)
|
|
181
|
+
|
|
182
|
+
font
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
data/lib/fontisan/font_loader.rb
CHANGED
|
@@ -63,7 +63,7 @@ module Fontisan
|
|
|
63
63
|
when Constants::TTC_TAG
|
|
64
64
|
load_from_collection(io, path, font_index, mode: resolved_mode,
|
|
65
65
|
lazy: resolved_lazy)
|
|
66
|
-
when pack_uint32(Constants::SFNT_VERSION_TRUETYPE)
|
|
66
|
+
when pack_uint32(Constants::SFNT_VERSION_TRUETYPE), "true"
|
|
67
67
|
TrueTypeFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
|
|
68
68
|
when "OTTO"
|
|
69
69
|
OpenTypeFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
|
|
@@ -71,6 +71,8 @@ module Fontisan
|
|
|
71
71
|
WoffFont.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
|
|
72
72
|
when "wOF2"
|
|
73
73
|
Woff2Font.from_file(path, mode: resolved_mode, lazy: resolved_lazy)
|
|
74
|
+
when Constants::DFONT_RESOURCE_HEADER
|
|
75
|
+
extract_and_load_dfont(io, path, font_index, resolved_mode, resolved_lazy)
|
|
74
76
|
else
|
|
75
77
|
raise InvalidFontError,
|
|
76
78
|
"Unknown font format. Expected TTF, OTF, TTC, OTC, WOFF, or WOFF2 file."
|
|
@@ -92,13 +94,25 @@ module Fontisan
|
|
|
92
94
|
|
|
93
95
|
File.open(path, "rb") do |io|
|
|
94
96
|
signature = io.read(4)
|
|
95
|
-
|
|
97
|
+
io.rewind
|
|
98
|
+
|
|
99
|
+
# Check for TTC/OTC signature
|
|
100
|
+
return true if signature == Constants::TTC_TAG
|
|
101
|
+
|
|
102
|
+
# Check for multi-font dfont (suitcase) - only if it's actually a dfont
|
|
103
|
+
if signature == Constants::DFONT_RESOURCE_HEADER
|
|
104
|
+
require_relative "parsers/dfont_parser"
|
|
105
|
+
# Verify it's a valid dfont and has multiple fonts
|
|
106
|
+
return Parsers::DfontParser.dfont?(io) && Parsers::DfontParser.sfnt_count(io) > 1
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
false
|
|
96
110
|
end
|
|
97
111
|
end
|
|
98
112
|
|
|
99
113
|
# Load a collection object without extracting fonts
|
|
100
114
|
#
|
|
101
|
-
# Returns the collection object (TrueTypeCollection or
|
|
115
|
+
# Returns the collection object (TrueTypeCollection, OpenTypeCollection, or DfontCollection)
|
|
102
116
|
# without extracting individual fonts. Useful for inspecting collection
|
|
103
117
|
# metadata and structure.
|
|
104
118
|
#
|
|
@@ -112,6 +126,10 @@ module Fontisan
|
|
|
112
126
|
# - OTC typically contains OpenType fonts (CFF/CFF2 outlines)
|
|
113
127
|
# - Mixed collections are possible (both TTF and OTF in same collection)
|
|
114
128
|
#
|
|
129
|
+
# dfont (Data Fork Font) is an Apple-specific format that contains Mac
|
|
130
|
+
# font suitcase resources. It can contain multiple SFNT fonts (TrueType
|
|
131
|
+
# or OpenType).
|
|
132
|
+
#
|
|
115
133
|
# Each collection can contain multiple SFNT-format font files, with table
|
|
116
134
|
# deduplication to save space. Individual fonts within a collection are
|
|
117
135
|
# stored at different offsets within the file, each with their own table
|
|
@@ -128,13 +146,16 @@ module Fontisan
|
|
|
128
146
|
# 4. If ANY font is OpenType (CFF), returns OpenTypeCollection
|
|
129
147
|
# 5. Only returns TrueTypeCollection if ALL fonts are TrueType
|
|
130
148
|
#
|
|
149
|
+
# For dfont files, returns DfontCollection.
|
|
150
|
+
#
|
|
131
151
|
# This approach correctly handles:
|
|
132
152
|
# - Homogeneous collections (all TTF or all OTF)
|
|
133
153
|
# - Mixed collections (both TTF and OTF fonts) - uses OpenTypeCollection
|
|
134
154
|
# - Large collections with many fonts (like NotoSerifCJK.ttc with 35 fonts)
|
|
155
|
+
# - dfont suitcases (Apple-specific)
|
|
135
156
|
#
|
|
136
157
|
# @param path [String] Path to the collection file
|
|
137
|
-
# @return [TrueTypeCollection, OpenTypeCollection] The collection object
|
|
158
|
+
# @return [TrueTypeCollection, OpenTypeCollection, DfontCollection] The collection object
|
|
138
159
|
# @raise [Errno::ENOENT] if file does not exist
|
|
139
160
|
# @raise [InvalidFontError] if file is not a collection or type cannot be determined
|
|
140
161
|
#
|
|
@@ -146,10 +167,18 @@ module Fontisan
|
|
|
146
167
|
|
|
147
168
|
File.open(path, "rb") do |io|
|
|
148
169
|
signature = io.read(4)
|
|
170
|
+
io.rewind
|
|
171
|
+
|
|
172
|
+
# Check for dfont
|
|
173
|
+
if signature == Constants::DFONT_RESOURCE_HEADER || dfont_signature?(io)
|
|
174
|
+
require_relative "dfont_collection"
|
|
175
|
+
return DfontCollection.from_file(path)
|
|
176
|
+
end
|
|
149
177
|
|
|
178
|
+
# Check for TTC/OTC
|
|
150
179
|
unless signature == Constants::TTC_TAG
|
|
151
180
|
raise InvalidFontError,
|
|
152
|
-
"File is not a collection (TTC/OTC). Use FontLoader.load instead."
|
|
181
|
+
"File is not a collection (TTC/OTC/dfont). Use FontLoader.load instead."
|
|
153
182
|
end
|
|
154
183
|
|
|
155
184
|
# Read version and num_fonts
|
|
@@ -291,6 +320,50 @@ mode: LoadingModes::FULL, lazy: true)
|
|
|
291
320
|
end
|
|
292
321
|
end
|
|
293
322
|
|
|
323
|
+
# Extract and load font from dfont resource fork
|
|
324
|
+
#
|
|
325
|
+
# @param io [IO] Open file handle
|
|
326
|
+
# @param path [String] Path to dfont file
|
|
327
|
+
# @param font_index [Integer] Font index in suitcase
|
|
328
|
+
# @param mode [Symbol] Loading mode
|
|
329
|
+
# @param lazy [Boolean] Lazy loading flag
|
|
330
|
+
# @return [TrueTypeFont, OpenTypeFont] Loaded font
|
|
331
|
+
# @api private
|
|
332
|
+
def self.extract_and_load_dfont(io, path, font_index, mode, lazy)
|
|
333
|
+
require_relative "parsers/dfont_parser"
|
|
334
|
+
|
|
335
|
+
# Extract SFNT data from resource fork
|
|
336
|
+
sfnt_data = Parsers::DfontParser.extract_sfnt(io, index: font_index)
|
|
337
|
+
|
|
338
|
+
# Create StringIO with SFNT data
|
|
339
|
+
sfnt_io = StringIO.new(sfnt_data)
|
|
340
|
+
|
|
341
|
+
# Detect SFNT signature
|
|
342
|
+
signature = sfnt_io.read(4)
|
|
343
|
+
sfnt_io.rewind
|
|
344
|
+
|
|
345
|
+
# Read and setup font based on signature
|
|
346
|
+
case signature
|
|
347
|
+
when pack_uint32(Constants::SFNT_VERSION_TRUETYPE), "true"
|
|
348
|
+
font = TrueTypeFont.read(sfnt_io)
|
|
349
|
+
font.initialize_storage
|
|
350
|
+
font.loading_mode = mode
|
|
351
|
+
font.lazy_load_enabled = lazy
|
|
352
|
+
font.read_table_data(sfnt_io) unless lazy
|
|
353
|
+
font
|
|
354
|
+
when "OTTO"
|
|
355
|
+
font = OpenTypeFont.read(sfnt_io)
|
|
356
|
+
font.initialize_storage
|
|
357
|
+
font.loading_mode = mode
|
|
358
|
+
font.lazy_load_enabled = lazy
|
|
359
|
+
font.read_table_data(sfnt_io) unless lazy
|
|
360
|
+
font
|
|
361
|
+
else
|
|
362
|
+
raise InvalidFontError,
|
|
363
|
+
"Invalid SFNT data in dfont resource (signature: #{signature.inspect})"
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
294
367
|
# Pack uint32 value to big-endian bytes
|
|
295
368
|
#
|
|
296
369
|
# @param value [Integer] The uint32 value
|
|
@@ -301,6 +374,18 @@ mode: LoadingModes::FULL, lazy: true)
|
|
|
301
374
|
end
|
|
302
375
|
|
|
303
376
|
private_class_method :load_from_collection, :pack_uint32, :env_mode,
|
|
304
|
-
:env_lazy
|
|
377
|
+
:env_lazy, :extract_and_load_dfont
|
|
378
|
+
|
|
379
|
+
# Check if file has dfont signature
|
|
380
|
+
#
|
|
381
|
+
# @param io [IO] Open file handle
|
|
382
|
+
# @return [Boolean] true if dfont
|
|
383
|
+
# @api private
|
|
384
|
+
def self.dfont_signature?(io)
|
|
385
|
+
require_relative "parsers/dfont_parser"
|
|
386
|
+
Parsers::DfontParser.dfont?(io)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
private_class_method :dfont_signature?
|
|
305
390
|
end
|
|
306
391
|
end
|
|
@@ -46,6 +46,34 @@ module Fontisan
|
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
# Individual check result from DSL-based validation
|
|
50
|
+
class CheckResult < Lutaml::Model::Serializable
|
|
51
|
+
attribute :check_id, :string
|
|
52
|
+
attribute :passed, :boolean
|
|
53
|
+
attribute :severity, :string
|
|
54
|
+
attribute :messages, :string, collection: true, default: -> { [] }
|
|
55
|
+
attribute :table, :string
|
|
56
|
+
attribute :field, :string
|
|
57
|
+
|
|
58
|
+
yaml do
|
|
59
|
+
map "check_id", to: :check_id
|
|
60
|
+
map "passed", to: :passed
|
|
61
|
+
map "severity", to: :severity
|
|
62
|
+
map "messages", to: :messages
|
|
63
|
+
map "table", to: :table
|
|
64
|
+
map "field", to: :field
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
json do
|
|
68
|
+
map "check_id", to: :check_id
|
|
69
|
+
map "passed", to: :passed
|
|
70
|
+
map "severity", to: :severity
|
|
71
|
+
map "messages", to: :messages
|
|
72
|
+
map "table", to: :table
|
|
73
|
+
map "field", to: :field
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
49
77
|
# Validation summary counts
|
|
50
78
|
class Summary < Lutaml::Model::Serializable
|
|
51
79
|
attribute :errors, :integer, default: -> { 0 }
|
|
@@ -69,12 +97,22 @@ module Fontisan
|
|
|
69
97
|
attribute :valid, :boolean
|
|
70
98
|
attribute :issues, Issue, collection: true, default: -> { [] }
|
|
71
99
|
attribute :summary, Summary, default: -> { Summary.new }
|
|
100
|
+
attribute :profile, :string
|
|
101
|
+
attribute :status, :string
|
|
102
|
+
attribute :use_case, :string
|
|
103
|
+
attribute :checks_performed, :string, collection: true, default: -> { [] }
|
|
104
|
+
attribute :check_results, CheckResult, collection: true, default: -> { [] }
|
|
72
105
|
|
|
73
106
|
yaml do
|
|
74
107
|
map "font_path", to: :font_path
|
|
75
108
|
map "valid", to: :valid
|
|
76
109
|
map "summary", to: :summary
|
|
77
110
|
map "issues", to: :issues
|
|
111
|
+
map "profile", to: :profile
|
|
112
|
+
map "status", to: :status
|
|
113
|
+
map "use_case", to: :use_case
|
|
114
|
+
map "checks_performed", to: :checks_performed
|
|
115
|
+
map "check_results", to: :check_results
|
|
78
116
|
end
|
|
79
117
|
|
|
80
118
|
json do
|
|
@@ -82,6 +120,11 @@ module Fontisan
|
|
|
82
120
|
map "valid", to: :valid
|
|
83
121
|
map "summary", to: :summary
|
|
84
122
|
map "issues", to: :issues
|
|
123
|
+
map "profile", to: :profile
|
|
124
|
+
map "status", to: :status
|
|
125
|
+
map "use_case", to: :use_case
|
|
126
|
+
map "checks_performed", to: :checks_performed
|
|
127
|
+
map "check_results", to: :check_results
|
|
85
128
|
end
|
|
86
129
|
|
|
87
130
|
# Add an error to the report
|
|
@@ -198,6 +241,190 @@ module Fontisan
|
|
|
198
241
|
|
|
199
242
|
lines.join("\n")
|
|
200
243
|
end
|
|
244
|
+
|
|
245
|
+
# Check if font passed validation (alias for valid)
|
|
246
|
+
#
|
|
247
|
+
# @return [Boolean] true if font passed validation
|
|
248
|
+
def passed?
|
|
249
|
+
valid
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Check if font is valid (alias for valid attribute)
|
|
253
|
+
#
|
|
254
|
+
# @return [Boolean] true if font is valid
|
|
255
|
+
def valid?
|
|
256
|
+
valid
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Get result for a specific check by ID
|
|
260
|
+
#
|
|
261
|
+
# @param check_id [Symbol, String] The check identifier
|
|
262
|
+
# @return [CheckResult, nil] The check result or nil if not found
|
|
263
|
+
def result_of(check_id)
|
|
264
|
+
check_results.find { |cr| cr.check_id == check_id.to_s }
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Get all passed checks
|
|
268
|
+
#
|
|
269
|
+
# @return [Array<CheckResult>] Array of passed checks
|
|
270
|
+
def passed_checks
|
|
271
|
+
check_results.select(&:passed)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Get all failed checks
|
|
275
|
+
#
|
|
276
|
+
# @return [Array<CheckResult>] Array of failed checks
|
|
277
|
+
def failed_checks
|
|
278
|
+
check_results.reject(&:passed)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Severity filtering methods
|
|
282
|
+
|
|
283
|
+
# Get issues by severity level
|
|
284
|
+
#
|
|
285
|
+
# @param severity [Symbol, String] Severity level
|
|
286
|
+
# @return [Array<Issue>] Array of issues with the specified severity
|
|
287
|
+
def issues_by_severity(severity)
|
|
288
|
+
issues.select { |issue| issue.severity == severity.to_s }
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Get fatal error issues
|
|
292
|
+
#
|
|
293
|
+
# @return [Array<Issue>] Array of fatal error issues
|
|
294
|
+
def fatal_errors
|
|
295
|
+
issues_by_severity(:fatal)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Get error issues only
|
|
299
|
+
#
|
|
300
|
+
# @return [Array<Issue>] Array of error issues
|
|
301
|
+
def errors_only
|
|
302
|
+
issues_by_severity(:error)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Get warning issues only
|
|
306
|
+
#
|
|
307
|
+
# @return [Array<Issue>] Array of warning issues
|
|
308
|
+
def warnings_only
|
|
309
|
+
issues_by_severity(:warning)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Get info issues only
|
|
313
|
+
#
|
|
314
|
+
# @return [Array<Issue>] Array of info issues
|
|
315
|
+
def info_only
|
|
316
|
+
issues_by_severity(:info)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Category filtering methods
|
|
320
|
+
|
|
321
|
+
# Get issues by category
|
|
322
|
+
#
|
|
323
|
+
# @param category [String] Category name
|
|
324
|
+
# @return [Array<Issue>] Array of issues in the specified category
|
|
325
|
+
def issues_by_category(category)
|
|
326
|
+
issues.select { |issue| issue.category == category.to_s }
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Get check results for a specific table
|
|
330
|
+
#
|
|
331
|
+
# @param table_tag [String] Table tag (e.g., 'name', 'head')
|
|
332
|
+
# @return [Array<CheckResult>] Array of check results for the table
|
|
333
|
+
def table_issues(table_tag)
|
|
334
|
+
check_results.select { |cr| cr.table == table_tag.to_s }
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Get check results for a specific field in a table
|
|
338
|
+
#
|
|
339
|
+
# @param table_tag [String] Table tag
|
|
340
|
+
# @param field_name [String, Symbol] Field name
|
|
341
|
+
# @return [Array<CheckResult>] Array of check results for the field
|
|
342
|
+
def field_issues(table_tag, field_name)
|
|
343
|
+
check_results.select { |cr| cr.table == table_tag.to_s && cr.field == field_name.to_s }
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Check filtering methods
|
|
347
|
+
|
|
348
|
+
# Get checks by status
|
|
349
|
+
#
|
|
350
|
+
# @param passed [Boolean] true for passed checks, false for failed checks
|
|
351
|
+
# @return [Array<CheckResult>] Array of checks with the specified status
|
|
352
|
+
def checks_by_status(passed:)
|
|
353
|
+
check_results.select { |cr| cr.passed == passed }
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Get IDs of failed checks
|
|
357
|
+
#
|
|
358
|
+
# @return [Array<String>] Array of failed check IDs
|
|
359
|
+
def failed_check_ids
|
|
360
|
+
failed_checks.map(&:check_id)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Get IDs of passed checks
|
|
364
|
+
#
|
|
365
|
+
# @return [Array<String>] Array of passed check IDs
|
|
366
|
+
def passed_check_ids
|
|
367
|
+
passed_checks.map(&:check_id)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Statistics methods
|
|
371
|
+
|
|
372
|
+
# Calculate failure rate as percentage
|
|
373
|
+
#
|
|
374
|
+
# @return [Float] Failure rate (0.0 to 1.0)
|
|
375
|
+
def failure_rate
|
|
376
|
+
return 0.0 if check_results.empty?
|
|
377
|
+
failed_checks.length.to_f / check_results.length
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Calculate pass rate as percentage
|
|
381
|
+
#
|
|
382
|
+
# @return [Float] Pass rate (0.0 to 1.0)
|
|
383
|
+
def pass_rate
|
|
384
|
+
1.0 - failure_rate
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Get severity distribution
|
|
388
|
+
#
|
|
389
|
+
# @return [Hash] Hash with :errors, :warnings, :info counts
|
|
390
|
+
def severity_distribution
|
|
391
|
+
{
|
|
392
|
+
errors: summary.errors,
|
|
393
|
+
warnings: summary.warnings,
|
|
394
|
+
info: summary.info,
|
|
395
|
+
}
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Export format methods
|
|
399
|
+
|
|
400
|
+
# Generate full detailed text report
|
|
401
|
+
#
|
|
402
|
+
# @return [String] Detailed text report
|
|
403
|
+
def to_text_report
|
|
404
|
+
text_summary
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Generate brief summary
|
|
408
|
+
#
|
|
409
|
+
# @return [String] Brief summary string
|
|
410
|
+
def to_summary
|
|
411
|
+
"#{summary.errors} errors, #{summary.warnings} warnings, #{summary.info} info"
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# Generate tabular format for CLI
|
|
415
|
+
#
|
|
416
|
+
# @return [String] Tabular format output
|
|
417
|
+
def to_table_format
|
|
418
|
+
lines = []
|
|
419
|
+
lines << "CHECK_ID | STATUS | SEVERITY | TABLE"
|
|
420
|
+
lines << "-" * 60
|
|
421
|
+
check_results.each do |cr|
|
|
422
|
+
status = cr.passed ? "PASS" : "FAIL"
|
|
423
|
+
table = cr.table || "N/A"
|
|
424
|
+
lines << "#{cr.check_id} | #{status} | #{cr.severity} | #{table}"
|
|
425
|
+
end
|
|
426
|
+
lines.join("\n")
|
|
427
|
+
end
|
|
201
428
|
end
|
|
202
429
|
end
|
|
203
430
|
end
|