fontist 2.1.2 → 2.1.3
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/.github/workflows/rake-metanorma.yaml +21 -3
- data/.github/workflows/release.yml +2 -0
- data/.github/workflows/release.yml.orig +36 -0
- data/.gitignore +7 -6
- data/README.adoc +100 -0
- data/docs/package-lock.json +1610 -736
- data/docs/package.json +6 -6
- data/lib/fontist/font_file.rb +65 -14
- data/lib/fontist/indexes/index_mixin.rb +109 -20
- data/lib/fontist/system_index.rb +30 -1
- data/lib/fontist/version.rb +1 -1
- metadata +4 -3
- /data/{docs/google-fonts-multi-format-usage.md → google-fonts-multi-format-usage.md} +0 -0
data/docs/package.json
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
9
|
"devDependencies": {
|
|
10
|
-
"@types/node": "
|
|
11
|
-
"prettier": "^3.
|
|
12
|
-
"typescript": "
|
|
13
|
-
"vitepress": "^1.
|
|
14
|
-
"vue": "^3.
|
|
15
|
-
"vue-tsc": "^
|
|
10
|
+
"@types/node": "^25.0.0",
|
|
11
|
+
"prettier": "^3.8.1",
|
|
12
|
+
"typescript": "^5.9.3",
|
|
13
|
+
"vitepress": "^1.6.4",
|
|
14
|
+
"vue": "^3.5.29",
|
|
15
|
+
"vue-tsc": "^3.2.5"
|
|
16
16
|
}
|
|
17
17
|
}
|
data/lib/fontist/font_file.rb
CHANGED
|
@@ -42,11 +42,11 @@ module Fontist
|
|
|
42
42
|
|
|
43
43
|
# For collections, we need different handling
|
|
44
44
|
if is_collection
|
|
45
|
-
# Load
|
|
45
|
+
# Load the first font in the collection
|
|
46
46
|
font = Fontisan::FontLoader.load(path, font_index: 0,
|
|
47
47
|
mode: :metadata, lazy: true)
|
|
48
48
|
|
|
49
|
-
# Validate the font using indexability profile
|
|
49
|
+
# Validate the font using indexability profile (log issues but don't reject)
|
|
50
50
|
validator = load_indexability_validator
|
|
51
51
|
validation_report = validator.validate(font)
|
|
52
52
|
|
|
@@ -54,10 +54,9 @@ module Fontist
|
|
|
54
54
|
error_messages = validation_report.errors.map do |e|
|
|
55
55
|
"#{e.category}: #{e.message}"
|
|
56
56
|
end.join("; ")
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# rubocop:enable Layout/LineLength
|
|
57
|
+
Fontist.ui.debug(
|
|
58
|
+
"Font validation issues for #{File.basename(path)}: #{error_messages}",
|
|
59
|
+
)
|
|
61
60
|
end
|
|
62
61
|
|
|
63
62
|
else
|
|
@@ -82,11 +81,14 @@ module Fontist
|
|
|
82
81
|
validation_report = validator.validate(font)
|
|
83
82
|
|
|
84
83
|
unless validation_report.valid?
|
|
84
|
+
# Log validation issues but don't reject outright
|
|
85
|
+
# We'll check if we can still extract metadata
|
|
85
86
|
error_messages = validation_report.errors.map do |e|
|
|
86
87
|
"#{e.category}: #{e.message}"
|
|
87
88
|
end.join("; ")
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
Fontist.ui.debug(
|
|
90
|
+
"Font validation issues for #{File.basename(path)}: #{error_messages}",
|
|
91
|
+
)
|
|
90
92
|
end
|
|
91
93
|
|
|
92
94
|
font
|
|
@@ -180,17 +182,66 @@ module Fontist
|
|
|
180
182
|
return {} unless name_table
|
|
181
183
|
|
|
182
184
|
# Extract all needed name strings using Fontisan's API
|
|
185
|
+
# Fall back to non-English names if English names are not available
|
|
183
186
|
{
|
|
184
|
-
full_name: name_table.english_name(Fontisan::Tables::Name::FULL_NAME)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
full_name: name_table.english_name(Fontisan::Tables::Name::FULL_NAME) ||
|
|
188
|
+
extract_any_name(name_table, Fontisan::Tables::Name::FULL_NAME),
|
|
189
|
+
family_name: name_table.english_name(Fontisan::Tables::Name::FAMILY) ||
|
|
190
|
+
extract_any_name(name_table, Fontisan::Tables::Name::FAMILY),
|
|
191
|
+
subfamily_name: name_table.english_name(Fontisan::Tables::Name::SUBFAMILY) ||
|
|
192
|
+
extract_any_name(name_table, Fontisan::Tables::Name::SUBFAMILY),
|
|
193
|
+
preferred_family: name_table.english_name(Fontisan::Tables::Name::PREFERRED_FAMILY) ||
|
|
194
|
+
extract_any_name(name_table, Fontisan::Tables::Name::PREFERRED_FAMILY),
|
|
195
|
+
preferred_subfamily: name_table.english_name(Fontisan::Tables::Name::PREFERRED_SUBFAMILY) ||
|
|
196
|
+
extract_any_name(name_table, Fontisan::Tables::Name::PREFERRED_SUBFAMILY),
|
|
197
|
+
postscript_name: name_table.english_name(Fontisan::Tables::Name::POSTSCRIPT_NAME) ||
|
|
198
|
+
extract_any_name(name_table, Fontisan::Tables::Name::POSTSCRIPT_NAME),
|
|
190
199
|
}
|
|
191
200
|
end
|
|
192
201
|
# rubocop:enable Metrics/MethodLength
|
|
193
202
|
|
|
203
|
+
# Extract any available name for a given name ID by manually decoding the name record
|
|
204
|
+
def extract_any_name(name_table, name_id)
|
|
205
|
+
# Find any name record with the matching name_id
|
|
206
|
+
records = name_table.name_records.select { |r| r.name_id == name_id }
|
|
207
|
+
return nil if records.empty?
|
|
208
|
+
|
|
209
|
+
# Manually decode each record
|
|
210
|
+
records.each do |record|
|
|
211
|
+
decoded = decode_name_record_manually(name_table, record)
|
|
212
|
+
return decoded unless decoded.nil? || decoded.empty?
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
nil
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Manually decode a name record since the fontisan method is private
|
|
219
|
+
def decode_name_record_manually(name_table, record)
|
|
220
|
+
# Get raw string storage
|
|
221
|
+
storage_bytes = name_table.string_storage.to_s.b
|
|
222
|
+
|
|
223
|
+
# Extract this record's string
|
|
224
|
+
offset = record.string_offset
|
|
225
|
+
length = record.string_length
|
|
226
|
+
|
|
227
|
+
return nil if offset + length > storage_bytes.bytesize
|
|
228
|
+
return nil if length.zero?
|
|
229
|
+
|
|
230
|
+
string_data = storage_bytes.byteslice(offset, length)
|
|
231
|
+
|
|
232
|
+
# Decode based on platform (same logic as fontisan)
|
|
233
|
+
case record.platform_id
|
|
234
|
+
when 0, 3 # PLATFORM_UNICODE, PLATFORM_WINDOWS
|
|
235
|
+
string_data.dup.force_encoding("UTF-16BE")
|
|
236
|
+
.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
237
|
+
when 1 # PLATFORM_MACINTOSH
|
|
238
|
+
string_data.dup.force_encoding("ASCII-8BIT")
|
|
239
|
+
.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
240
|
+
else
|
|
241
|
+
string_data.dup.force_encoding("UTF-8")
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
194
245
|
def raise_font_file_error(exception)
|
|
195
246
|
raise Errors::FontFileError,
|
|
196
247
|
"Font file could not be parsed: #{exception.inspect}."
|
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
module Fontist
|
|
2
2
|
module Indexes
|
|
3
|
+
# IndexMixin provides common functionality for font index classes.
|
|
4
|
+
#
|
|
5
|
+
# == Performance Optimization (Tech Debt)
|
|
6
|
+
#
|
|
7
|
+
# This module uses a temporary Hash-based lookup cache during index building
|
|
8
|
+
# to avoid O(n²) performance when adding many entries. This is a workaround
|
|
9
|
+
# for Lutaml::Model::Collection's Array-based storage.
|
|
10
|
+
#
|
|
11
|
+
# === The Problem
|
|
12
|
+
#
|
|
13
|
+
# Lutaml::Model::Collection stores entries as an Array, which provides O(n)
|
|
14
|
+
# lookup when searching for existing keys. When building an index with
|
|
15
|
+
# thousands of entries, this creates O(n²) behavior:
|
|
16
|
+
#
|
|
17
|
+
# - 8867 font styles × average 2670 comparisons = ~23.6 million comparisons
|
|
18
|
+
# - Index building: ~26 seconds with Array lookup
|
|
19
|
+
#
|
|
20
|
+
# === The Workaround
|
|
21
|
+
#
|
|
22
|
+
# During `build` and `build_with_formulas`, we maintain a temporary
|
|
23
|
+
# `@index_build_cache` Hash that provides O(1) lookups. After building,
|
|
24
|
+
# the cache is cleared.
|
|
25
|
+
#
|
|
26
|
+
# - Index building with Hash lookup: ~0.08 seconds
|
|
27
|
+
# - Speedup: 325× faster
|
|
28
|
+
#
|
|
29
|
+
# === The Proper Fix
|
|
30
|
+
#
|
|
31
|
+
# This tech debt should be resolved by enhancing Lutaml::Model::Collection
|
|
32
|
+
# to support efficient key-based lookups. See the reproduction script at:
|
|
33
|
+
# `dev/lutaml_model_collection_lookup_benchmark.rb`
|
|
34
|
+
#
|
|
35
|
+
# Related issue: https://github.com/lutaml/lutaml-model/issues/XXX
|
|
36
|
+
#
|
|
3
37
|
module IndexMixin
|
|
4
38
|
def self.included(base)
|
|
5
39
|
base.extend(ClassMethods)
|
|
@@ -35,13 +69,17 @@ module Fontist
|
|
|
35
69
|
def reset_cache
|
|
36
70
|
# Delete the index file to force rebuild on next access
|
|
37
71
|
# This is important for tests to ensure clean state
|
|
38
|
-
|
|
72
|
+
FileUtils.rm_f(path)
|
|
39
73
|
end
|
|
40
74
|
end
|
|
41
75
|
|
|
76
|
+
# Build index by loading all formulas from disk.
|
|
77
|
+
# Uses Hash-based cache for O(1) lookups during building.
|
|
42
78
|
def build
|
|
43
|
-
|
|
44
|
-
|
|
79
|
+
with_index_build_cache do
|
|
80
|
+
Formula.all.each do |formula|
|
|
81
|
+
add_formula(formula)
|
|
82
|
+
end
|
|
45
83
|
end
|
|
46
84
|
|
|
47
85
|
to_file
|
|
@@ -49,9 +87,16 @@ module Fontist
|
|
|
49
87
|
self
|
|
50
88
|
end
|
|
51
89
|
|
|
90
|
+
# Build index from pre-loaded formulas.
|
|
91
|
+
# Uses Hash-based cache for O(1) lookups during building.
|
|
92
|
+
#
|
|
93
|
+
# This is the preferred method when formulas are already loaded,
|
|
94
|
+
# as it avoids re-loading from disk.
|
|
52
95
|
def build_with_formulas(formulas)
|
|
53
|
-
|
|
54
|
-
|
|
96
|
+
with_index_build_cache do
|
|
97
|
+
formulas.each do |formula|
|
|
98
|
+
add_formula(formula)
|
|
99
|
+
end
|
|
55
100
|
end
|
|
56
101
|
|
|
57
102
|
to_file
|
|
@@ -73,26 +118,20 @@ module Fontist
|
|
|
73
118
|
|
|
74
119
|
def index_key_for_style(_style)
|
|
75
120
|
raise NotImplementedError,
|
|
76
|
-
"index_key_for_style(style) must be implemented
|
|
121
|
+
"index_key_for_style(style) must be implemented"
|
|
77
122
|
end
|
|
78
123
|
|
|
124
|
+
# Add a font style to the index with O(1) or O(n) lookup.
|
|
125
|
+
#
|
|
126
|
+
# Uses `@index_build_cache` Hash for O(1) lookup during building,
|
|
127
|
+
# falling back to O(n) Array lookup for incremental updates.
|
|
79
128
|
def add_index_formula(style, formula_path)
|
|
80
|
-
key =
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
key = normalize_key(key)
|
|
84
|
-
formula_path = Array(formula_path)
|
|
85
|
-
paths = formula_path.map { |p| relative_formula_path(p) }
|
|
129
|
+
key = prepare_index_key(style)
|
|
130
|
+
paths = prepare_formula_paths(formula_path)
|
|
86
131
|
|
|
87
|
-
if
|
|
88
|
-
index_formula(key).formula_path.concat(paths).uniq!
|
|
89
|
-
return
|
|
90
|
-
end
|
|
132
|
+
return if merge_existing_entry?(key, paths)
|
|
91
133
|
|
|
92
|
-
|
|
93
|
-
key: key,
|
|
94
|
-
formula_path: paths,
|
|
95
|
-
)
|
|
134
|
+
create_and_add_entry(key, paths)
|
|
96
135
|
end
|
|
97
136
|
|
|
98
137
|
def load_formulas(key)
|
|
@@ -114,6 +153,56 @@ module Fontist
|
|
|
114
153
|
|
|
115
154
|
private
|
|
116
155
|
|
|
156
|
+
# Yields with a Hash-based lookup cache for O(1) key lookups.
|
|
157
|
+
#
|
|
158
|
+
# This is a performance optimization to avoid O(n²) behavior
|
|
159
|
+
# when building indexes with thousands of entries.
|
|
160
|
+
#
|
|
161
|
+
# @yield [void] Block to execute with cache enabled
|
|
162
|
+
# @return [void]
|
|
163
|
+
def with_index_build_cache
|
|
164
|
+
@index_build_cache = {}
|
|
165
|
+
yield
|
|
166
|
+
ensure
|
|
167
|
+
@index_build_cache = nil
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def prepare_index_key(style)
|
|
171
|
+
key = index_key_for_style(style)
|
|
172
|
+
raise if key.nil? || key.empty?
|
|
173
|
+
|
|
174
|
+
normalize_key(key)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def prepare_formula_paths(formula_path)
|
|
178
|
+
Array(formula_path).map { |p| relative_formula_path(p) }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Attempt to merge paths into existing entry.
|
|
182
|
+
# Returns true if merged, false if no existing entry found.
|
|
183
|
+
def merge_existing_entry?(key, paths)
|
|
184
|
+
existing = find_existing_entry(key)
|
|
185
|
+
return false unless existing
|
|
186
|
+
|
|
187
|
+
existing.formula_path.concat(paths).uniq!
|
|
188
|
+
true
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Find existing entry using cache (O(1)) or array scan (O(n))
|
|
192
|
+
def find_existing_entry(key)
|
|
193
|
+
if @index_build_cache
|
|
194
|
+
@index_build_cache[key]
|
|
195
|
+
else
|
|
196
|
+
index_formula(key)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def create_and_add_entry(key, paths)
|
|
201
|
+
entry = FormulaKeyToPath.new(key: key, formula_path: paths)
|
|
202
|
+
entries << entry
|
|
203
|
+
@index_build_cache[key] = entry if @index_build_cache
|
|
204
|
+
end
|
|
205
|
+
|
|
117
206
|
def index_formula(key)
|
|
118
207
|
Array(entries).detect { |f| normalize_key(f.key) == normalize_key(key) }
|
|
119
208
|
end
|
data/lib/fontist/system_index.rb
CHANGED
|
@@ -682,7 +682,10 @@ spinner_index = nil)
|
|
|
682
682
|
|
|
683
683
|
def parse_font(font_file, path)
|
|
684
684
|
# Skip fonts with incomplete metadata
|
|
685
|
-
|
|
685
|
+
unless font_file.full_name && font_file.family
|
|
686
|
+
warn_incomplete_metadata(path)
|
|
687
|
+
return nil
|
|
688
|
+
end
|
|
686
689
|
|
|
687
690
|
# Get file metadata for caching
|
|
688
691
|
file_size = begin
|
|
@@ -708,6 +711,32 @@ spinner_index = nil)
|
|
|
708
711
|
)
|
|
709
712
|
end
|
|
710
713
|
|
|
714
|
+
def filter_valid_fonts(fonts)
|
|
715
|
+
fonts.select do |font|
|
|
716
|
+
missing_keys = ALLOWED_KEYS.reject { |key| font.send(key) }
|
|
717
|
+
|
|
718
|
+
next true if missing_keys.empty?
|
|
719
|
+
|
|
720
|
+
warn_font_metadata_incomplete(font, missing_keys)
|
|
721
|
+
end
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
def warn_font_metadata_incomplete(font, missing_keys)
|
|
725
|
+
Fontist.ui.error(<<~MSG.chomp)
|
|
726
|
+
Skipping font with incomplete metadata: #{font.path}
|
|
727
|
+
Missing attributes: #{missing_keys.join(', ')}.
|
|
728
|
+
This font will not be indexed, but Fontist will continue to work.
|
|
729
|
+
MSG
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
def warn_incomplete_metadata(path)
|
|
733
|
+
Fontist.ui.error(<<~MSG.chomp)
|
|
734
|
+
Skipping font with incomplete metadata: #{path}
|
|
735
|
+
Missing attributes: full_name, family_name.
|
|
736
|
+
This font will not be indexed, but Fontist will continue to work.
|
|
737
|
+
MSG
|
|
738
|
+
end
|
|
739
|
+
|
|
711
740
|
def raise_font_index_corrupted(font, missing_keys)
|
|
712
741
|
raise(Errors::FontIndexCorrupted, <<~MSG.chomp)
|
|
713
742
|
Font index is corrupted.
|
data/lib/fontist/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fontist
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.1.
|
|
4
|
+
version: 2.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-03-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: down
|
|
@@ -275,6 +275,7 @@ files:
|
|
|
275
275
|
- ".github/workflows/rake-metanorma.yaml"
|
|
276
276
|
- ".github/workflows/rake.yml"
|
|
277
277
|
- ".github/workflows/release.yml"
|
|
278
|
+
- ".github/workflows/release.yml.orig"
|
|
278
279
|
- ".github/workflows/tebako-pack.yml"
|
|
279
280
|
- ".gitignore"
|
|
280
281
|
- ".gitmodules"
|
|
@@ -288,7 +289,6 @@ files:
|
|
|
288
289
|
- Rakefile
|
|
289
290
|
- docs/.gitignore
|
|
290
291
|
- docs/.vitepress/config.ts
|
|
291
|
-
- docs/google-fonts-multi-format-usage.md
|
|
292
292
|
- docs/guide/api-ruby.md
|
|
293
293
|
- docs/guide/ci.md
|
|
294
294
|
- docs/guide/fontconfig.md
|
|
@@ -305,6 +305,7 @@ files:
|
|
|
305
305
|
- fontist.gemspec
|
|
306
306
|
- formula_filename_index.yml
|
|
307
307
|
- formula_index.yml
|
|
308
|
+
- google-fonts-multi-format-usage.md
|
|
308
309
|
- lib/fontist.rb
|
|
309
310
|
- lib/fontist/cache/manager.rb
|
|
310
311
|
- lib/fontist/cache/store.rb
|
|
File without changes
|