fontist 3.0.24 → 3.0.26

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 954fe79bde0fb84cf7b2fcc96b686820a7c2f4fd6ec822abd4067b1c0d8c288e
4
- data.tar.gz: ac7eb9f594dd5a9be155e2893c4a07bad197743e107a92f16786b45f47000224
3
+ metadata.gz: 215b46de2d4feadfbd1d3644dd060cc5afdd54afd30ba1f6a50e428c5c2be4ef
4
+ data.tar.gz: 7eea12f0743488fbfbd4c5d924a2bfce06de918aaa9cf4bbc24b92fbe02d937f
5
5
  SHA512:
6
- metadata.gz: 9616ce9f46708c5acb1dc4643688f8ab0a43eeb5eafc7ee8259e9d5a110e415c738514cc00739d937b41d05d1b64868256419f3303d32159aeb41f462f260207
7
- data.tar.gz: ba15c400a1fed88380cd28f8669c7272e6d52cf0dcf4df09f42166cc0b95cf55db36dcf5456bd3704981550308e087b2074f3aaaa9419567e75733ff96099bbf
6
+ metadata.gz: 93ddd5d5863d137e0ed95f9e8829b2bb84797b7887f8d9a122f8259f920bde832f3d810b1057539504b1f7b584ba7e4d245726943cdcdc1a874c9656c52b125c
7
+ data.tar.gz: 2af73abb7b5d07aa4a5699aa309fbeec7e8053b7944f902a26a7e70804e3ff71c511dda0e0bb68e4e1e7e5f2e841e265eb08c06095de5f8390dd82e312190b79
@@ -14,7 +14,7 @@ module Fontist
14
14
 
15
15
  def formulas
16
16
  @formulas ||= if fontist_font?
17
- Indexes::FilenameIndex.from_file.load_index_formulas(File.basename(@path)).flat_map(&:name)
17
+ Indexes::FilenameIndex.lookup_index.load_index_formulas(File.basename(@path)).flat_map(&:name)
18
18
  else
19
19
  []
20
20
  end
@@ -122,15 +122,15 @@ module Fontist
122
122
  end
123
123
 
124
124
  def find(font_name)
125
- Indexes::FontIndex.from_file.load_formulas(font_name).first
125
+ Indexes::FontIndex.lookup_index.load_formulas(font_name).first
126
126
  end
127
127
 
128
128
  def find_many(font_name)
129
- Indexes::FontIndex.from_file.load_formulas(font_name)
129
+ Indexes::FontIndex.lookup_index.load_formulas(font_name)
130
130
  end
131
131
 
132
132
  def find_fonts(font_name)
133
- formulas = Indexes::FontIndex.from_file.load_formulas(font_name)
133
+ formulas = Indexes::FontIndex.lookup_index.load_formulas(font_name)
134
134
 
135
135
  formulas.map do |formula|
136
136
  formula.all_fonts.select do |f|
@@ -140,7 +140,7 @@ module Fontist
140
140
  end
141
141
 
142
142
  def find_styles(font_name, style_name)
143
- formulas = Indexes::FontIndex.from_file.load_formulas(font_name)
143
+ formulas = Indexes::FontIndex.lookup_index.load_formulas(font_name)
144
144
 
145
145
  formulas.map do |formula|
146
146
  formula.all_fonts.map do |f|
@@ -173,7 +173,7 @@ module Fontist
173
173
  end
174
174
 
175
175
  def find_by_font_file(font_file)
176
- key = Indexes::FilenameIndex.from_file
176
+ key = Indexes::FilenameIndex.lookup_index
177
177
  .load_index_formulas(File.basename(font_file))
178
178
  .flat_map(&:name)
179
179
  .first
@@ -8,6 +8,14 @@ module Fontist
8
8
  DefaultFamilyFontIndex.from_file
9
9
  end
10
10
  end
11
+
12
+ def self.lookup_index
13
+ if Fontist.preferred_family?
14
+ PreferredFamilyFontIndex.lookup_index
15
+ else
16
+ DefaultFamilyFontIndex.lookup_index
17
+ end
18
+ end
11
19
  end
12
20
  end
13
21
  end
@@ -1,5 +1,9 @@
1
+ require "json"
2
+
1
3
  module Fontist
2
4
  module Indexes
5
+ autoload :LookupIndex, "#{__dir__}/lookup_index"
6
+
3
7
  # IndexMixin provides common functionality for font index classes.
4
8
  #
5
9
  # == Performance Optimization (Tech Debt)
@@ -72,9 +76,38 @@ module Fontist
72
76
  # to force a fresh parse on next access
73
77
  Fontist::FromFileCache.clear
74
78
 
75
- # Delete the index file to force rebuild on next access
76
- # This is important for tests to ensure clean state
79
+ # Delete the index file and its lookup sidecar to force a
80
+ # rebuild on next access. This is important for tests to
81
+ # ensure clean state
77
82
  FileUtils.rm_f(path)
83
+ FileUtils.rm_f("#{path}.json")
84
+ end
85
+
86
+ # Read-only lookups over the sidecar JSON beside the index
87
+ # file, which skips binding every entry into model objects. The
88
+ # sidecar is (re)written whenever the index file is written,
89
+ # and regenerated here when it is missing or stale.
90
+ def lookup_index
91
+ entries_hash = sidecar_entries || rebuild_sidecar_entries
92
+ normalizer = new.method(:normalize_key)
93
+ LookupIndex.new(entries_hash, normalizer: normalizer)
94
+ end
95
+
96
+ def sidecar_entries
97
+ sidecar_path = "#{path}.json"
98
+ return unless File.exist?(sidecar_path)
99
+ return if File.exist?(path) &&
100
+ File.mtime(sidecar_path) < File.mtime(path)
101
+
102
+ JSON.parse(File.read(sidecar_path))["entries"]
103
+ rescue JSON::ParserError, TypeError
104
+ nil
105
+ end
106
+
107
+ def rebuild_sidecar_entries
108
+ entries_hash = from_file(path).entries_hash
109
+ File.write("#{path}.json", JSON.dump("entries" => entries_hash))
110
+ entries_hash
78
111
  end
79
112
  end
80
113
 
@@ -158,6 +191,19 @@ module Fontist
158
191
 
159
192
  FileUtils.mkdir_p(File.dirname(file_path))
160
193
  File.write(file_path, to_yaml)
194
+ write_lookup_sidecar(file_path)
195
+ end
196
+
197
+ def write_lookup_sidecar(file_path)
198
+ File.write("#{file_path}.json",
199
+ JSON.dump("entries" => entries_hash))
200
+ end
201
+
202
+ # key -> formula paths, in the sidecar's JSON-friendly shape
203
+ def entries_hash
204
+ Array(entries).to_h do |entry|
205
+ [normalize_key(entry.key), entry.formula_path]
206
+ end
161
207
  end
162
208
 
163
209
  private
@@ -0,0 +1,34 @@
1
+ module Fontist
2
+ module Indexes
3
+ # Read-only lookups over the JSON sidecar written beside an index
4
+ # file. Binding every index entry into model objects dominates the
5
+ # cost of loading an index; the sidecar holds the key-to-path map
6
+ # as plain data, and FormulaKeyToPath entries are materialized only
7
+ # for the requested key.
8
+ class LookupIndex
9
+ def initialize(entries_hash, normalizer:)
10
+ @entries_hash = entries_hash
11
+ @normalizer = normalizer
12
+ end
13
+
14
+ def load_index_formulas(key)
15
+ [
16
+ FormulaKeyToPath.new(
17
+ key: key,
18
+ formula_path: paths_for(@normalizer.call(key)),
19
+ ),
20
+ ]
21
+ end
22
+
23
+ def load_formulas(key)
24
+ load_index_formulas(key).flat_map(&:to_full)
25
+ end
26
+
27
+ private
28
+
29
+ def paths_for(normalized_key)
30
+ @entries_hash[normalized_key] || []
31
+ end
32
+ end
33
+ end
34
+ end
@@ -140,6 +140,33 @@ module Fontist
140
140
  end
141
141
  end
142
142
 
143
+ # Scan metadata for a system index collection, persisted in a
144
+ # companion file beside the index (an index file is a flat entry
145
+ # list and cannot carry scalars). Restoring it across processes is
146
+ # what makes the 30-minute scan threshold and the directory-mtime
147
+ # change check work at all.
148
+ class SystemIndexMetadata < Lutaml::Model::Serializable
149
+ attribute :last_scan_time, :integer, default: -> { 0 }
150
+ attribute :directory_mtimes, :string, collection: true, default: -> { [] }
151
+
152
+ key_value do
153
+ map "last_scan_time", to: :last_scan_time
154
+ map "directory_mtimes", to: :directory_mtimes
155
+ end
156
+
157
+ def self.load(path)
158
+ return new unless File.exist?(path)
159
+
160
+ from_yaml(File.read(path))
161
+ rescue Lutaml::Model::Error, Psych::SyntaxError, TypeError
162
+ new
163
+ end
164
+
165
+ def save(path)
166
+ File.write(path, to_yaml)
167
+ end
168
+ end
169
+
143
170
  class SystemIndexFontCollection < Lutaml::Model::Collection
144
171
  include Utils::Locking
145
172
 
@@ -151,14 +178,40 @@ module Fontist
151
178
  attribute :directory_mtimes, :string, collection: true, default: -> { [] }
152
179
 
153
180
  key_value do
181
+ # A bare entry list: the scan metadata lives in a companion
182
+ # metadata file (see SystemIndexMetadata), because a flat list
183
+ # cannot carry scalars and lutaml ignores scalar maps declared
184
+ # alongside map_instances
154
185
  map_instances to: :fonts
155
- map "last_scan_time", to: :last_scan_time
156
- map "directory_mtimes", to: :directory_mtimes
157
186
  end
158
187
 
159
188
  # Don't rebuild index more frequently than this (in seconds)
160
189
  INDEX_REBUILD_THRESHOLD = 30 * 60 # 30 minutes
161
190
 
191
+ def metadata_path
192
+ "#{@path}.metadata.yml"
193
+ end
194
+
195
+ def persist_metadata
196
+ return unless @path
197
+
198
+ SystemIndexMetadata
199
+ .new(last_scan_time: last_scan_time,
200
+ directory_mtimes: directory_mtimes)
201
+ .save(metadata_path)
202
+ end
203
+
204
+ def restore_metadata
205
+ return unless @path
206
+ return unless last_scan_time.to_i.zero?
207
+
208
+ meta = SystemIndexMetadata.load(metadata_path)
209
+ return unless meta.last_scan_time.to_i.positive?
210
+
211
+ self.last_scan_time = meta.last_scan_time
212
+ self.directory_mtimes = meta.directory_mtimes
213
+ end
214
+
162
215
  def set_path(path)
163
216
  @path = path
164
217
  end
@@ -172,12 +225,17 @@ module Fontist
172
225
  return new.set_content(path, paths_loader) unless File.exist?(path)
173
226
 
174
227
  from_yaml(File.read(path)).set_content(path, paths_loader)
228
+ rescue Lutaml::Model::Error, Psych::SyntaxError, TypeError
229
+ # Corrupt or unreadable index files degrade to an empty
230
+ # collection; the next rebuild rewrites them
231
+ new.set_content(path, paths_loader)
175
232
  end
176
233
 
177
234
  def set_content(path, paths_loader)
178
235
  tap do |content|
179
236
  content.set_path(path)
180
237
  content.set_path_loader(paths_loader)
238
+ content.restore_metadata
181
239
  end
182
240
  end
183
241
 
@@ -351,7 +409,8 @@ module Fontist
351
409
 
352
410
  def build(forced: false, verbose: false, stats: nil)
353
411
  if forced
354
- return rebuild_with_lock(verbose: verbose, stats: stats)
412
+ return rebuild_with_lock(verbose: verbose, stats: stats,
413
+ honor_recent: false)
355
414
  end
356
415
 
357
416
  previous_index = load_index
@@ -372,14 +431,18 @@ module Fontist
372
431
 
373
432
  private
374
433
 
375
- def rebuild_with_lock(verbose: false, stats: nil)
434
+ def rebuild_with_lock(verbose: false, stats: nil, honor_recent: true)
376
435
  # Use file locking to prevent concurrent rebuilds across processes
377
436
  lock_path = index_lock_path
378
437
  start_time = Time.now
379
438
 
380
439
  lock(lock_path) do
381
- # Re-check if another process already rebuilt while we waited for lock
382
- if File.exist?(@path)
440
+ # Re-check if another process already rebuilt while we waited
441
+ # for lock. Forced rebuilds (add_font, explicit rebuild) carry
442
+ # write intent and must always rescan: honoring the recent
443
+ # rebuild here served the pre-write index and silently dropped
444
+ # the font that triggered the rebuild
445
+ if honor_recent && File.exist?(@path)
383
446
  existing = self.class.from_file(path: @path,
384
447
  paths_loader: @paths_loader)
385
448
 
@@ -407,6 +470,7 @@ module Fontist
407
470
  self.last_scan_time = Time.now.to_i
408
471
  self.directory_mtimes = scan_directory_mtimes.map { |k, v| "#{k}:#{v}" }
409
472
  to_file(@path)
473
+ persist_metadata
410
474
 
411
475
  # Show completion message for large collections
412
476
  elapsed = Time.now - start_time
@@ -505,6 +569,7 @@ module Fontist
505
569
 
506
570
  # Save to file
507
571
  to_file(@path)
572
+ persist_metadata
508
573
  end
509
574
 
510
575
  def detect_paths(paths, verbose: false, stats: nil)
@@ -1,3 +1,3 @@
1
1
  module Fontist
2
- VERSION = "3.0.24".freeze
2
+ VERSION = "3.0.26".freeze
3
3
  end
data/lib/fontist.rb CHANGED
@@ -25,6 +25,7 @@ module Fontist
25
25
  autoload :FontPath, "#{__dir__}/fontist/font_path"
26
26
  autoload :FontStyle, "#{__dir__}/fontist/font_style"
27
27
  autoload :Fontconfig, "#{__dir__}/fontist/fontconfig"
28
+ autoload :FormatSpec, "#{__dir__}/fontist/format_spec"
28
29
  autoload :Formula, "#{__dir__}/fontist/formula"
29
30
  autoload :FormulaCollection, "#{__dir__}/fontist/formula"
30
31
  autoload :FormulaPicker, "#{__dir__}/fontist/formula_picker"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fontist
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.24
4
+ version: 3.0.26
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -452,6 +452,7 @@ files:
452
452
  - lib/fontist/indexes/incremental_index_updater.rb
453
453
  - lib/fontist/indexes/incremental_scanner.rb
454
454
  - lib/fontist/indexes/index_mixin.rb
455
+ - lib/fontist/indexes/lookup_index.rb
455
456
  - lib/fontist/indexes/preferred_family_font_index.rb
456
457
  - lib/fontist/indexes/system_index.rb
457
458
  - lib/fontist/indexes/user_index.rb