fontist 3.0.32 → 3.0.34

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: ccd5b693c5cc7ffa551f7a24c4e1366cee390bba84c2cdd2969fda96fdb8798d
4
- data.tar.gz: 493d3d3683bf368eaa553602084c8a457ddcb911d3e73be0308423263a176a2a
3
+ metadata.gz: a1143907bf1a14a160d39b19244cdd714b337424825ad84dba942a8373ca7a60
4
+ data.tar.gz: 282f707e84083d28b1afdb8fb7c80576b35590e1050693fd1cd2d004eeeb1a0d
5
5
  SHA512:
6
- metadata.gz: 2d13031416210a2c8996623593c27c93bb963dac09b108c55a6a80833262c664c7e478a8e8eb316f69963e73b77c2295790da77e8bad07289ca24ab1865371be
7
- data.tar.gz: f2e34e0d2946338a0a2ab047ab48dbe5e5701899c2ed2731867d3389fcbc1caa0f92107ba8a8a992db2b74a78920f4b8105f8191d5636e1ec4b2a88952e80bc5
6
+ metadata.gz: 870132c48632ccf2315fbdea22a61e1b6952f79cb43aaf769c7daeea7c82f5aab15fc194e26a0694b2f82636168b5710e2a1ed900c9b23759634b589ccefe410
7
+ data.tar.gz: db7f2844943dbf51fd87954ec6e8356a96528b47ea4d74c662af94b090e1c469f9cd1fee62c075252038d0fd5e799bc4aff84f39ac88e926a4a2ebfe1abb3d23
@@ -43,9 +43,19 @@ jobs:
43
43
  with:
44
44
  path: "fontist"
45
45
 
46
- # Append rather than overwrite: metanorma's own Gemfile.devel carries
47
- # the taste-stack gems its compile specs need (metanorma/issues/630)
48
- - run: 'echo ''gem "fontist", path: "./fontist"'' >> Gemfile.devel'
46
+ # Point any existing fontist line at this checkout, appending only
47
+ # when the host Gemfile declares none (metanorma/issues/630 keeps
48
+ # the taste-stack gems it carries; since metanorma#652 its
49
+ # Gemfile.devel also pins fontist, which a blind append would
50
+ # duplicate into a version-conflict error).
51
+ - run: |
52
+ if grep -q '^gem "fontist"' Gemfile.devel Gemfile 2>/dev/null; then
53
+ sed -i.bak 's/^gem "fontist".*/gem "fontist", path: ".\/fontist"/' Gemfile.devel Gemfile 2>/dev/null || true
54
+ rm -f Gemfile.devel.bak Gemfile.bak
55
+ else
56
+ echo 'gem "fontist", path: "./fontist"' >> Gemfile.devel
57
+ fi
58
+ shell: bash
49
59
 
50
60
  - uses: ruby/setup-ruby@v1
51
61
  with:
@@ -3,8 +3,28 @@ module Fontist
3
3
  module Helpers
4
4
  module HashHelper
5
5
  class << self
6
+ # Converts a formula hash to string-keyed plain data. Values
7
+ # that are lutaml-model models join the document through their
8
+ # own to_hash serializer (the json gem used to call to_json on
9
+ # them implicitly during traversal; Yeptris::JSON does not).
6
10
  def stringify_keys(hash)
7
- Fontist::JSON.parse(Fontist::JSON.dump(hash))
11
+ deep_stringify(hash)
12
+ end
13
+
14
+ private
15
+
16
+ def deep_stringify(value)
17
+ case value
18
+ when Hash then value.to_h { |k, v| [k.to_s, deep_stringify(v)] }
19
+ when Array then value.map { |v| deep_stringify(v) }
20
+ else deep_stringify_model(value)
21
+ end
22
+ end
23
+
24
+ def deep_stringify_model(value)
25
+ return value unless value.respond_to?(:to_hash)
26
+
27
+ deep_stringify(value.to_hash)
8
28
  end
9
29
  end
10
30
  end
data/lib/fontist/repo.rb CHANGED
@@ -115,16 +115,19 @@ module Fontist
115
115
  end
116
116
  end
117
117
 
118
- def update(name)
118
+ def update(name, rebuild: true)
119
119
  path = repo_path(name)
120
120
  unless Dir.exist?(path)
121
121
  raise(Errors::RepoNotFoundError, "No such repo '#{name}'.")
122
122
  end
123
123
 
124
124
  git = Git.open(path)
125
+ before = git.rev_parse("HEAD")
125
126
  git.pull("origin", git.current_branch)
127
+ changed = git.rev_parse("HEAD") != before
126
128
 
127
- rebuild_index_if_needed
129
+ rebuild_index_if_needed if rebuild && changed
130
+ changed
128
131
  rescue StandardError => e
129
132
  # Catch all git-related errors
130
133
  if e.class.name.include?("Git") || e.message.match?(/git|pull|repository/i)
@@ -148,10 +148,12 @@ module Fontist
148
148
  class SystemIndexMetadata < Lutaml::Model::Serializable
149
149
  attribute :last_scan_time, :integer, default: -> { 0 }
150
150
  attribute :directory_mtimes, :string, collection: true, default: -> { [] }
151
+ attribute :format_version, :integer, default: -> { 0 }
151
152
 
152
153
  key_value do
153
154
  map "last_scan_time", to: :last_scan_time
154
155
  map "directory_mtimes", to: :directory_mtimes
156
+ map "format_version", to: :format_version
155
157
  end
156
158
 
157
159
  def self.load(path)
@@ -173,9 +175,12 @@ module Fontist
173
175
  instances :fonts, SystemIndexFont
174
176
  attr_accessor :path, :paths_loader
175
177
 
176
- # Metadata for smart change detection
178
+ # Metadata for smart change detection. The format generation lives
179
+ # only in the companion metadata file (see restore_metadata); it is
180
+ # never serialized into the entry list itself.
177
181
  attribute :last_scan_time, :integer, default: -> { 0 }
178
182
  attribute :directory_mtimes, :string, collection: true, default: -> { [] }
183
+ attribute :format_version, :integer, default: -> { 0 }
179
184
 
180
185
  key_value do
181
186
  # A bare entry list: the scan metadata lives in a companion
@@ -188,6 +193,13 @@ module Fontist
188
193
  # Don't rebuild index more frequently than this (in seconds)
189
194
  INDEX_REBUILD_THRESHOLD = 30 * 60 # 30 minutes
190
195
 
196
+ # Index entry shape generation. v1 stored one entry per font file;
197
+ # v2 stores one entry per named instance of variable fonts (the
198
+ # FiraCode[wght].ttf class of files). Bump when the entry shape
199
+ # changes again: persisted indexes carrying an older generation
200
+ # cannot answer the current lookups and force a rebuild.
201
+ INDEX_FORMAT_VERSION = 2
202
+
191
203
  def metadata_path
192
204
  "#{@path}.metadata.yml"
193
205
  end
@@ -197,19 +209,32 @@ module Fontist
197
209
 
198
210
  SystemIndexMetadata
199
211
  .new(last_scan_time: last_scan_time,
200
- directory_mtimes: directory_mtimes)
212
+ directory_mtimes: directory_mtimes,
213
+ format_version: INDEX_FORMAT_VERSION)
201
214
  .save(metadata_path)
202
215
  end
203
216
 
204
217
  def restore_metadata
205
218
  return unless @path
206
- return unless last_scan_time.to_i.zero?
207
219
 
208
220
  meta = SystemIndexMetadata.load(metadata_path)
209
- return unless meta.last_scan_time.to_i.positive?
210
-
211
221
  self.last_scan_time = meta.last_scan_time
212
222
  self.directory_mtimes = meta.directory_mtimes
223
+ self.format_version = meta.format_version
224
+ end
225
+
226
+ # Whether the persisted index predates the current entry shape. A
227
+ # v1 index answers every lookup of a variable font's non-default
228
+ # families with "missing" (the file registered only under its
229
+ # default instance), and the directory-mtime fast paths would keep
230
+ # trusting it forever, so the generation mismatch itself is the
231
+ # staleness signal. A collection with no companion metadata file is
232
+ # fresh or in-process memory, not a persisted v1 volume: treating
233
+ # it as stale would discard every cached entry on the next rebuild.
234
+ def stale_format?
235
+ return false unless @path && File.exist?(metadata_path.to_s)
236
+
237
+ format_version.to_i != INDEX_FORMAT_VERSION
213
238
  end
214
239
 
215
240
  def set_path(path)
@@ -263,6 +288,7 @@ module Fontist
263
288
  end
264
289
 
265
290
  def find(font, style, format_spec: nil)
291
+ rebuild_if_stale_format!
266
292
  current_fonts = index
267
293
 
268
294
  return nil if current_fonts.nil? || current_fonts.empty?
@@ -285,6 +311,35 @@ module Fontist
285
311
  found_fonts.empty? ? nil : found_fonts
286
312
  end
287
313
 
314
+ # Rebuild once per process when the persisted index predates
315
+ # INDEX_FORMAT_VERSION. Runs even under read_only_mode: a stale
316
+ # generation answers every lookup of a variable font's non-default
317
+ # families with "missing", which is worse than a one-off rebuild.
318
+ # Only a VALID, non-empty persisted index heals: a fresh environment
319
+ # (no file yet) and a corrupt file (the corruption path explains the
320
+ # removal fix) take their normal routes.
321
+ def rebuild_if_stale_format!
322
+ return if @format_check_done
323
+
324
+ @format_check_done = true
325
+ return unless @path && File.exist?(@path.to_s)
326
+ return if fonts.nil? || fonts.empty?
327
+
328
+ # A corrupt index keeps its documented contract: the corruption
329
+ # error with the removal hint, not a silent heal.
330
+ check_index
331
+
332
+ restore_metadata
333
+ return unless stale_format?
334
+
335
+ Fontist.ui.warn(
336
+ "Font index format changed (v#{format_version.to_i} -> " \
337
+ "v#{INDEX_FORMAT_VERSION}); rebuilding once.",
338
+ )
339
+ build(forced: true)
340
+ mark_verified!
341
+ end
342
+
288
343
  # Drops the cached family lookup; called when fonts are removed in
289
344
  # place (a rebuild replaces the fonts array, which re-keys the
290
345
  # lookup by itself)
@@ -573,7 +628,12 @@ module Fontist
573
628
  end
574
629
 
575
630
  def detect_paths(paths, verbose: false, stats: nil)
576
- existing_fonts_by_path = fonts&.group_by(&:path) || {}
631
+ # A generation change invalidates every cached parse in one sweep:
632
+ # v1 entries (one per file) cannot be reused to build v2 lookups
633
+ # (one per named instance), and the per-file cache fast path would
634
+ # otherwise keep serving the stale entry for every unchanged file.
635
+ existing_fonts_by_path =
636
+ stale_format? ? {} : (fonts&.group_by(&:path) || {})
577
637
 
578
638
  # Initialize stats if not provided
579
639
  stats ||= IndexStats.new if verbose
@@ -9,23 +9,35 @@ module Fontist
9
9
  end
10
10
 
11
11
  def call
12
- update_main_repo
13
- update_private_repos
12
+ main_changed = update_main_repo
13
+ private_changed = update_private_repos
14
14
  ensure
15
- rebuild_index
15
+ # The index is a projection of the corpus: a pull that brought no
16
+ # changes leaves it valid, and a failed pull leaves the corpus
17
+ # untouched. Rebuilding costs a full pass over every formula, so
18
+ # it only runs when some repo actually moved — or when no index
19
+ # exists yet (a fresh clone never had one; lookups self-heal by
20
+ # rebuilding on demand, but an explicit update should leave one
21
+ # behind).
22
+ rebuild_index if main_changed || private_changed || index_missing?
16
23
  end
17
24
 
18
25
  private
19
26
 
27
+ def index_missing?
28
+ !File.exist?(Fontist.formula_filename_index_path)
29
+ end
30
+
20
31
  def update_main_repo
21
32
  dir = File.dirname(Fontist.formulas_repo_path)
22
33
  FileUtils.mkdir_p(dir)
23
34
 
24
35
  unless Dir.exist?(Fontist.formulas_repo_path)
25
- return Git.clone(Fontist.formulas_repo_url,
26
- Fontist.formulas_repo_path,
27
- branch: @branch,
28
- depth: 1)
36
+ Git.clone(Fontist.formulas_repo_url,
37
+ Fontist.formulas_repo_path,
38
+ branch: @branch,
39
+ depth: 1)
40
+ return true
29
41
  end
30
42
 
31
43
  git = if Dir.exist?(Fontist.formulas_repo_path.join(".git"))
@@ -36,21 +48,34 @@ module Fontist
36
48
  end
37
49
  end
38
50
 
39
- return git.pull("origin", @branch) if git.current_branch == @branch
51
+ if git.current_branch != @branch
52
+ git.config_set("remote.origin.fetch",
53
+ "+refs/heads/#{@branch}:refs/remotes/origin/#{@branch}")
54
+ git.fetch
55
+ git.checkout(@branch)
56
+ git.pull("origin", @branch)
57
+ return true
58
+ end
40
59
 
41
- git.config_set("remote.origin.fetch",
42
- "+refs/heads/#{@branch}:refs/remotes/origin/#{@branch}")
43
- git.fetch
44
- git.checkout(@branch)
60
+ # A freshly init-ed repo has no commits yet; nil before the first
61
+ # pull means "changed", since the pull then creates the corpus
62
+ before = head_sha(git)
45
63
  git.pull("origin", @branch)
64
+ head_sha(git) != before
46
65
  end
47
66
 
48
67
  def update_private_repos
49
- Repo.list.each do |name|
50
- Repo.update(name)
68
+ Repo.list.reduce(false) do |changed, name|
69
+ Repo.update(name, rebuild: false) || changed
51
70
  end
52
71
  end
53
72
 
73
+ def head_sha(git)
74
+ git.rev_parse("HEAD")
75
+ rescue Git::FailedError, Git::GitError
76
+ nil
77
+ end
78
+
54
79
  def rebuild_index
55
80
  Index.rebuild
56
81
  end
@@ -1,3 +1,3 @@
1
1
  module Fontist
2
- VERSION = "3.0.32".freeze
2
+ VERSION = "3.0.34".freeze
3
3
  end
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: 3.0.32
4
+ version: 3.0.34
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-04 00:00:00.000000000 Z
11
+ date: 2026-11-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: down