glyphs 0.3.0 → 0.4.1
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/CHANGELOG.md +51 -1
- data/README.md +48 -5
- data/lib/glyphs/configuration.rb +5 -1
- data/lib/glyphs/icon_pruner.rb +23 -20
- data/lib/glyphs/main_ractor.rb +46 -0
- data/lib/glyphs/prunable_icon_error.rb +8 -0
- data/lib/glyphs/prune_guard.rb +45 -0
- data/lib/glyphs/prune_runner.rb +6 -0
- data/lib/glyphs/source_scanner.rb +36 -3
- data/lib/glyphs/version.rb +1 -1
- data/lib/glyphs.rb +27 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e0ed91cb2af5c3414aa3db654c772ec9c3ad506f4f7d9998223b76144897d33
|
|
4
|
+
data.tar.gz: 37a81b162e603e83060c7adfd395cef55f1d20c4896d9bb4d4b0ec96e3e83908
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 82eb60a70f7fd553e38130cf57025834ca8daf9c02c54c315df9900c07346138eb1230d48783204509ab897791203434db38cf62ed1035337bf3ded8aee7b2ed
|
|
7
|
+
data.tar.gz: 188c29bbe5a8a8f6dbae785e39af4a6b49ee4c660a302bf8fd032525012c47ce7b6559e45c342f5b6544f06994162ab443b4e4fc8e51eb81b2c0bbc6ceb38152
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **A non-main Ractor renders icons it has not cached.** A miss there used to
|
|
8
|
+
raise `Ractor::IsolationError` (`@configuration from Icons`), because the
|
|
9
|
+
icons gem reads an unshareable module ivar and parses with Nokogiri, which
|
|
10
|
+
only the main Ractor can run. `Glyphs::MainRactor` now renders the miss on
|
|
11
|
+
the main Ractor through [ractor-dispatch](https://github.com/jhawthorn/ractor-dispatch)
|
|
12
|
+
(Ruby 4.0+, a dependency of Rails 8.2's Active Support) and that Ractor's
|
|
13
|
+
cache keeps the markup, byte for byte the main Ractor's. The missing-icon
|
|
14
|
+
policy runs on the main Ractor too. Without ractor-dispatch the miss raises
|
|
15
|
+
`Glyphs::MainRactor::Unavailable` naming the icon, and `Glyphs.preload_svg`
|
|
16
|
+
still seeds a Ractor's cache. glyphs does not depend on ractor-dispatch: add
|
|
17
|
+
it outside Rails 8.2. Refs zoolutions/phlex-reactive#363
|
|
18
|
+
- **`config.raise_on_prunable` fails a render the prune would delete.** Off by
|
|
19
|
+
default. With it on, typically in the test environment, rendering such an icon
|
|
20
|
+
raises `Glyphs::PrunableIconError`, naming the library, variant and name, so a
|
|
21
|
+
test fails instead of production. The guard asks the pruner, so under one
|
|
22
|
+
configuration the two agree. The first uncached render scans the source, once
|
|
23
|
+
per process; with the setting off nothing is scanned. Enabling it may fail specs, and each such
|
|
24
|
+
failure is an icon that would have gone missing in production. Refs #35
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **`glyphs:prune_icons` no longer deletes icons forwarded through a wrapper
|
|
29
|
+
component.** A file that passes a non-literal under a key like an icon
|
|
30
|
+
(`Badge(icon:)`, `Badge(icon: row.first)`) now keeps its icon-name-shaped
|
|
31
|
+
literals for every dynamically-rendered library. They were dropped when the
|
|
32
|
+
file itself had no dynamic icon call, so the icon raised
|
|
33
|
+
`Icons::IconNotFound` in production only. Pruning can only keep a little more
|
|
34
|
+
than before. Refs #35
|
|
35
|
+
|
|
36
|
+
## [0.3.0] - 2026-10-15
|
|
4
37
|
|
|
5
38
|
### Changed
|
|
6
39
|
|
|
@@ -136,6 +169,23 @@
|
|
|
136
169
|
rather than the primary mechanism. Only literals are harvested, so the scanner
|
|
137
170
|
never invents a reference.
|
|
138
171
|
|
|
172
|
+
## [0.2.1] - 2026-07-11
|
|
173
|
+
|
|
174
|
+
### Added
|
|
175
|
+
|
|
176
|
+
- **`glyphs:prune_icons` prunes unused icons for smaller Docker images.**
|
|
177
|
+
`Glyphs::SourceScanner` finds the icons an app references and the task deletes
|
|
178
|
+
every other synced SVG in the Docker build, keeping `config.keep_icons` (new,
|
|
179
|
+
for dynamic or data-driven names) and every `fallback_icons` entry, then
|
|
180
|
+
verifies each kept icon still resolves. Dry-run by default; it deletes only
|
|
181
|
+
with `PRUNE=1 GLYPHS_PRUNE_ICONS=1`. `config.prune_source_globs` adds paths to
|
|
182
|
+
scan. Refs #2
|
|
183
|
+
|
|
184
|
+
### Changed
|
|
185
|
+
|
|
186
|
+
- **Breaking:** `required_ruby_version` is now `>= 3.4` (CI runs 3.4 and 4.0).
|
|
187
|
+
Refs #2
|
|
188
|
+
|
|
139
189
|
## [0.2.0] - 2026-07-07
|
|
140
190
|
|
|
141
191
|
### Changed
|
data/README.md
CHANGED
|
@@ -130,11 +130,17 @@ readable on the main Ractor only, which is all an app without Ractors needs; ano
|
|
|
130
130
|
object with a `#call` method that `Ractor.shareable?` accepts (frozen, with nothing unshareable inside) or
|
|
131
131
|
`Ractor.shareable_proc` (Ruby 4.0+).
|
|
132
132
|
|
|
133
|
-
The icons gem cannot render inside a non-main Ractor
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
133
|
+
The icons gem cannot render inside a non-main Ractor (its configuration is unshareable module state, and
|
|
134
|
+
Nokogiri is not marked Ractor-safe), so a miss in one renders on the main Ractor instead, through
|
|
135
|
+
[ractor-dispatch](https://github.com/jhawthorn/ractor-dispatch) (Ruby 4.0+). Rails 8.2 apps have it through
|
|
136
|
+
Active Support; elsewhere, add `gem "ractor-dispatch"`. The markup is the main Ractor's, byte for byte, and
|
|
137
|
+
the calling Ractor's cache keeps it, so each icon crosses once per cache entry; cache hits never leave the
|
|
138
|
+
Ractor. The missing-icon policy runs on the main Ractor too, `on_missing_icon` included. A call with an
|
|
139
|
+
attribute that renders uncached, or any call with `cache_svgs = false`, goes to the main Ractor every time.
|
|
140
|
+
|
|
141
|
+
Without ractor-dispatch, a miss in a non-main Ractor raises `Glyphs::MainRactor::Unavailable`, naming the
|
|
142
|
+
icon. You can still render on the main Ractor and hand the markup over with `Glyphs.preload_svg`; cache hits
|
|
143
|
+
then work in that Ractor for calls without `data:` whose attributes the cache can apply:
|
|
138
144
|
|
|
139
145
|
```ruby
|
|
140
146
|
svg = Glyphs.svg_for(name: "house", library: :lucide, variant: nil, attributes: {}) # main Ractor
|
|
@@ -174,11 +180,48 @@ The scanner resolves them from source, so you rarely need `keep_icons` at all:
|
|
|
174
180
|
constant named like one (`ICON = :bell`, `STATUS_ICONS = { .. => :warning }`,
|
|
175
181
|
including trailing `.freeze`) — are kept for every dynamically-rendered
|
|
176
182
|
library, so a name declared in one file and rendered from another survives.
|
|
183
|
+
- **Forwarded** — a file that passes a non-literal under a key like an icon
|
|
184
|
+
(`Badge(icon:)`, `Badge(icon: row.first)`) hands a name on to a wrapper
|
|
185
|
+
component, so every icon-name-shaped literal in that file is kept for every
|
|
186
|
+
dynamically-rendered library. `ROWS = [[:bell, 1]]` survives when the row
|
|
187
|
+
reaches `Badge(icon:)`, and `Badge`'s `LucideIcon(@icon)` lives in another file.
|
|
177
188
|
|
|
178
189
|
Only literals are harvested, so the scanner never invents a reference; the
|
|
179
190
|
worst case is keeping a coincidentally icon-named string, which the post-prune
|
|
180
191
|
verification tolerates.
|
|
181
192
|
|
|
193
|
+
A static scan can't follow every value. A literal passed positionally to a
|
|
194
|
+
wrapper (`m.row("Alerts", :anchor)`) is still pruned, and so is a forwarded
|
|
195
|
+
keyword in a template, which is text-scanned. Nothing fails until production:
|
|
196
|
+
development and test keep every SVG, and the post-prune verification doesn't
|
|
197
|
+
assert harvested names. `raise_on_prunable` closes that gap.
|
|
198
|
+
|
|
199
|
+
### `raise_on_prunable` — fail the test that renders a prunable icon
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
# config/environments/test.rb, or an initializer guarded by Rails.env.test?
|
|
203
|
+
Glyphs.configure do |config|
|
|
204
|
+
config.raise_on_prunable = true
|
|
205
|
+
end
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
With it on, rendering an icon the prune would delete raises
|
|
209
|
+
`Glyphs::PrunableIconError`, naming the library, variant and name. Every page a
|
|
210
|
+
test renders becomes a check, and each failure is a production bug: name the icon
|
|
211
|
+
in an icon position (`icon: :anchor`, an `ICON` constant) or list it in
|
|
212
|
+
`keep_icons`.
|
|
213
|
+
|
|
214
|
+
The guard asks the pruner itself, so the two agree, including on `keep_icons`
|
|
215
|
+
globs and on the libraries a prune leaves alone. That holds for the configuration
|
|
216
|
+
the test runs with: a `keep_icons`, `fallback_icons` or `prune_source_globs` that
|
|
217
|
+
differs by `Rails.env` makes the guard answer for a different keep-set than your
|
|
218
|
+
image build prunes with. The first uncached render scans
|
|
219
|
+
your source, once per process (a parallel test worker scans for itself), and
|
|
220
|
+
cache hits never reach the check. A name that doesn't exist at all still meets
|
|
221
|
+
the missing-icon policy. The default is `false`, which scans nothing, and
|
|
222
|
+
source changed after the scan isn't seen until the process restarts, so keep this
|
|
223
|
+
in the test environment.
|
|
224
|
+
|
|
182
225
|
### `keep_icons` — the last-resort escape hatch
|
|
183
226
|
|
|
184
227
|
For names a static scan genuinely can't see — read from a database, an ENV var,
|
data/lib/glyphs/configuration.rb
CHANGED
|
@@ -24,8 +24,11 @@ module Glyphs
|
|
|
24
24
|
# prune_source_globs: extra locations the pruner's scanner text-scans IN
|
|
25
25
|
# ADDITION to the built-in Ruby/template defaults — e.g. a config file that
|
|
26
26
|
# names icons. Never replaces the defaults (nil = defaults only).
|
|
27
|
+
# raise_on_prunable: boolean; when true, rendering an icon glyphs:prune_icons
|
|
28
|
+
# would delete raises Glyphs::PrunableIconError. For the test environment:
|
|
29
|
+
# the first uncached render scans the source, once per process.
|
|
27
30
|
attr_accessor :raise_on_missing, :on_missing_icon, :fallback_icons, :cache_svgs,
|
|
28
|
-
:keep_icons, :prune_source_globs
|
|
31
|
+
:keep_icons, :prune_source_globs, :raise_on_prunable
|
|
29
32
|
attr_reader :svg_cache_size
|
|
30
33
|
|
|
31
34
|
def initialize
|
|
@@ -36,6 +39,7 @@ module Glyphs
|
|
|
36
39
|
@svg_cache_size = DEFAULT_SVG_CACHE_SIZE
|
|
37
40
|
@keep_icons = []
|
|
38
41
|
@prune_source_globs = nil
|
|
42
|
+
@raise_on_prunable = false
|
|
39
43
|
end
|
|
40
44
|
|
|
41
45
|
def svg_cache_size=(size)
|
data/lib/glyphs/icon_pruner.rb
CHANGED
|
@@ -38,14 +38,23 @@ module Glyphs
|
|
|
38
38
|
next
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
stat = prune_group(library, variant, files, keep, deleted_names)
|
|
43
|
-
stats << stat
|
|
41
|
+
stats << prune_group(library, variant, files, unkept_files(library, variant, files), deleted_names)
|
|
44
42
|
end
|
|
45
43
|
|
|
46
44
|
PruneReport.new(stats:, deleted_names:, dry_run: @dry_run)
|
|
47
45
|
end
|
|
48
46
|
|
|
47
|
+
# The Set of IconReferences #call would delete: same files, same keep-sets.
|
|
48
|
+
# Deletes nothing and warns about nothing.
|
|
49
|
+
def prunable
|
|
50
|
+
groups = grouped_files.select { |(library, _), _| library_used?(library) }
|
|
51
|
+
groups.each_with_object(Set.new) do |((library, variant), files), references|
|
|
52
|
+
unkept_files(library, variant, files).each do |path|
|
|
53
|
+
references << IconReference.new(library:, variant:, name: File.basename(path, ".svg"))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
49
58
|
private
|
|
50
59
|
|
|
51
60
|
# { [library, variant] => [absolute svg paths] }, excluding the animated lib.
|
|
@@ -69,25 +78,19 @@ module Glyphs
|
|
|
69
78
|
end
|
|
70
79
|
end
|
|
71
80
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
files.each do |path|
|
|
78
|
-
name = File.basename(path, ".svg")
|
|
79
|
-
if keep_file?(name, keep)
|
|
80
|
-
kept += 1
|
|
81
|
-
next
|
|
82
|
-
end
|
|
81
|
+
# The one decision of what goes, shared by #call and #prunable.
|
|
82
|
+
def unkept_files(library, variant, files)
|
|
83
|
+
keep = keep_names_for(library, variant)
|
|
84
|
+
files.reject { |path| keep_file?(File.basename(path, ".svg"), keep) }
|
|
85
|
+
end
|
|
83
86
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
end
|
|
87
|
+
def prune_group(library, variant, files, unkept, deleted_names)
|
|
88
|
+
bytes_freed = unkept.sum { |path| File.size(path) }
|
|
89
|
+
deleted_names.concat(unkept.map { |path| File.basename(path, ".svg") })
|
|
90
|
+
unkept.each { |path| File.delete(path) } unless @dry_run
|
|
89
91
|
|
|
90
|
-
PruneReport::LibraryStat.new(library:, variant:, kept
|
|
92
|
+
PruneReport::LibraryStat.new(library:, variant:, kept: files.size - unkept.size, deleted: unkept.size,
|
|
93
|
+
bytes_freed:)
|
|
91
94
|
end
|
|
92
95
|
|
|
93
96
|
def keep_file?(name, keep)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glyphs
|
|
4
|
+
# Renders an icon on the main Ractor for a non-main one. The icons gem reads
|
|
5
|
+
# Icons.configuration (an unshareable module ivar) and parses with Nokogiri
|
|
6
|
+
# (a C extension not marked Ractor-safe), so only the main Ractor can run it.
|
|
7
|
+
# ractor-dispatch (Ruby 4.0+, a dependency of Rails 8.2's Active Support)
|
|
8
|
+
# runs the render there and sends the markup back, where the calling Ractor's
|
|
9
|
+
# own cache keeps it: each icon crosses once per Ractor, not once per render.
|
|
10
|
+
module MainRactor
|
|
11
|
+
# Raised in a non-main Ractor that cannot reach the main one.
|
|
12
|
+
class Unavailable < StandardError; end
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
# Whether this Ractor runs the icons gem itself: the main Ractor, or any
|
|
16
|
+
# code on an engine without Ractors.
|
|
17
|
+
def current?
|
|
18
|
+
defined?(::Ractor) ? ::Ractor.main? : true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Glyphs' render (missing-icon policy included) of these arguments, run
|
|
22
|
+
# on the main Ractor; an Icons::IconNotFound it raises is raised here.
|
|
23
|
+
def render(**arguments)
|
|
24
|
+
dispatcher = main_dispatcher(arguments)
|
|
25
|
+
shared = ::Ractor.make_shareable(arguments, copy: true) # a shareable block captures only shareable values
|
|
26
|
+
dispatcher.run { Glyphs.__send__(:render_svg, **shared) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def main_dispatcher(arguments)
|
|
32
|
+
icon = "#{arguments[:library]}/#{arguments[:name]}"
|
|
33
|
+
unless defined?(::Ractor::Port)
|
|
34
|
+
raise Unavailable, "#{icon} isn't cached in this Ractor, and rendering it on the main Ractor " \
|
|
35
|
+
"needs ractor-dispatch, which needs Ruby 4.0+. Seed the cache with Glyphs.preload_svg."
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
require "ractor/dispatch"
|
|
39
|
+
::Ractor::Dispatch.main
|
|
40
|
+
rescue LoadError
|
|
41
|
+
raise Unavailable, "#{icon} isn't cached in this Ractor, and rendering it on the main Ractor " \
|
|
42
|
+
"needs the ractor-dispatch gem (Rails 8.2 apps have it): add gem \"ractor-dispatch\"."
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glyphs
|
|
4
|
+
# Raised on render, with Configuration#raise_on_prunable on, for an icon that
|
|
5
|
+
# exists now and that glyphs:prune_icons would delete from the image. Not an
|
|
6
|
+
# Icons::IconNotFound: the missing-icon policy must not turn it into a fallback.
|
|
7
|
+
class PrunableIconError < StandardError; end
|
|
8
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glyphs
|
|
4
|
+
# Backs Configuration#raise_on_prunable: a render of an icon the prune would
|
|
5
|
+
# delete fails in the test suite instead of in production, where development's
|
|
6
|
+
# full SVG tree no longer hides it. Dynamic keeps are advisory, so
|
|
7
|
+
# PruneRunner#verify! cannot catch this; a render names a real icon, so here
|
|
8
|
+
# it is safe to fail.
|
|
9
|
+
#
|
|
10
|
+
# It asks the pruner, so under one configuration the two agree, and it asks
|
|
11
|
+
# once: the scan runs in the constructor and #check! only reads the result. It
|
|
12
|
+
# deletes nothing. Glyphs keeps one per process, replacing it when #current?
|
|
13
|
+
# says the settings behind that answer changed.
|
|
14
|
+
class PruneGuard
|
|
15
|
+
def initialize(config: Glyphs.configuration, runner: PruneRunner.new(config:))
|
|
16
|
+
@keep_settings = keep_settings(config)
|
|
17
|
+
@prunable = runner.prunable
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def current?(config)
|
|
21
|
+
@keep_settings == keep_settings(config)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def check!(name:, library:, variant:)
|
|
25
|
+
library = library.to_sym
|
|
26
|
+
variant = variant.nil? ? IconReference.default_variant_for(library) : IconReference.normalize_variant(variant)
|
|
27
|
+
reference = IconReference.new(library:, variant:, name: name.to_s)
|
|
28
|
+
raise PrunableIconError, message(reference) if @prunable.include?(reference)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# A hash of the values, so a list changed in place reads as a change.
|
|
34
|
+
def keep_settings(config)
|
|
35
|
+
[config.keep_icons, config.fallback_icons, config.prune_source_globs].hash
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def message(reference)
|
|
39
|
+
icon = [reference.library, reference.variant, reference.name].compact.join("/")
|
|
40
|
+
"glyphs:prune_icons would delete #{icon}, so this render would fail in production. " \
|
|
41
|
+
"Name it in an icon position (`icon: :#{reference.name.tr('-', '_')}`, an `ICON` constant) " \
|
|
42
|
+
"or list it in keep_icons."
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/glyphs/prune_runner.rb
CHANGED
|
@@ -28,6 +28,12 @@ module Glyphs
|
|
|
28
28
|
report
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# The IconReferences a prune would delete, deleting nothing: the pruner's own
|
|
32
|
+
# answer over the same scan and keep-sets #call uses.
|
|
33
|
+
def prunable
|
|
34
|
+
pruner.prunable
|
|
35
|
+
end
|
|
36
|
+
|
|
31
37
|
# Asserts every kept icon resolves to a file on disk. Raises
|
|
32
38
|
# VerificationError listing the first few misses.
|
|
33
39
|
def verify!
|
|
@@ -156,7 +156,7 @@ module Glyphs
|
|
|
156
156
|
# name the AST can't read (`LucideIcon(tile[:icon])`) — across every scanned
|
|
157
157
|
# Ruby file, then turns that evidence into IconReferences.
|
|
158
158
|
#
|
|
159
|
-
#
|
|
159
|
+
# Three sources of candidate names, all harvested only from literals so we
|
|
160
160
|
# never invent a reference:
|
|
161
161
|
#
|
|
162
162
|
# 1. file-scoped — a file that dynamically renders library L contributes
|
|
@@ -166,6 +166,10 @@ module Glyphs
|
|
|
166
166
|
# (a hash pair keyed `/icon/i`, or a constant named `/ICON/`) are
|
|
167
167
|
# candidates for EVERY dynamically-rendered library, since a bare
|
|
168
168
|
# `ICON = :bell` in one file is often rendered from another.
|
|
169
|
+
# 3. forwarded — a file passing a non-literal under a key like an icon
|
|
170
|
+
# (`Badge(icon:)`, `Badge(icon: row.first)`) hands a name on to a wrapper
|
|
171
|
+
# that renders it elsewhere, so ALL its literals are candidates for
|
|
172
|
+
# every dynamically-rendered library, as declarations are.
|
|
169
173
|
#
|
|
170
174
|
# Names resolve at the library's default variant: a dynamic call names no
|
|
171
175
|
# variant in practice, and the pruner keeps per (library, variant).
|
|
@@ -179,6 +183,7 @@ module Glyphs
|
|
|
179
183
|
# Folds one file's visitor results into the running accumulator.
|
|
180
184
|
def absorb(visitor)
|
|
181
185
|
@declaration_literals.merge(visitor.declaration_literals)
|
|
186
|
+
@declaration_literals.merge(visitor.file_literals) if visitor.forwards_icon
|
|
182
187
|
return if visitor.dynamic_libraries.empty?
|
|
183
188
|
|
|
184
189
|
@dynamic_libraries.merge(visitor.dynamic_libraries)
|
|
@@ -205,18 +210,28 @@ module Glyphs
|
|
|
205
210
|
# Excludes CSS classes (spaces/slashes), paths, and interpolated strings.
|
|
206
211
|
ICON_NAME_LITERAL = /\A[a-z][a-z0-9_-]*\z/
|
|
207
212
|
|
|
213
|
+
# Values of an icon-keyed pair that forward nothing: a declaration harvest
|
|
214
|
+
# reads the names, and `icon: nil` or `icon_size: 16` names no icon at all.
|
|
215
|
+
LITERAL_VALUES = [
|
|
216
|
+
Prism::SymbolNode, Prism::StringNode, Prism::NilNode, Prism::TrueNode, Prism::FalseNode,
|
|
217
|
+
Prism::IntegerNode, Prism::FloatNode
|
|
218
|
+
].freeze
|
|
219
|
+
|
|
208
220
|
# Libraries this file renders with a dynamic (non-literal) first argument.
|
|
209
221
|
attr_reader :dynamic_libraries
|
|
210
222
|
# Icon-name-shaped literals anywhere in this file (file-scoped candidates).
|
|
211
223
|
attr_reader :file_literals
|
|
212
224
|
# Literals in icon-declaration positions (`icon:` pairs, `ICON` constants).
|
|
213
225
|
attr_reader :declaration_literals
|
|
226
|
+
# Whether this file passes a non-literal under a key like an icon.
|
|
227
|
+
attr_reader :forwards_icon
|
|
214
228
|
|
|
215
229
|
def initialize(references)
|
|
216
230
|
@references = references
|
|
217
231
|
@dynamic_libraries = Set.new
|
|
218
232
|
@file_literals = Set.new
|
|
219
233
|
@declaration_literals = Set.new
|
|
234
|
+
@forwards_icon = false
|
|
220
235
|
super()
|
|
221
236
|
end
|
|
222
237
|
|
|
@@ -247,10 +262,14 @@ module Glyphs
|
|
|
247
262
|
end
|
|
248
263
|
|
|
249
264
|
# `icon: :activity` / `menu_icon: "gear"` — a hash pair keyed like an icon
|
|
250
|
-
# declares an icon name, wherever it lives.
|
|
265
|
+
# declares an icon name, wherever it lives. A non-literal value there
|
|
266
|
+
# (`icon:`, `icon: row.first`) forwards a name instead.
|
|
251
267
|
def visit_assoc_node(node)
|
|
252
268
|
key = node.key
|
|
253
|
-
|
|
269
|
+
if key.is_a?(Prism::SymbolNode) && key.unescaped.match?(/icon/i)
|
|
270
|
+
collect_declaration(node.value)
|
|
271
|
+
@forwards_icon ||= forwarded_value?(node.value)
|
|
272
|
+
end
|
|
254
273
|
super
|
|
255
274
|
end
|
|
256
275
|
|
|
@@ -258,6 +277,20 @@ module Glyphs
|
|
|
258
277
|
|
|
259
278
|
attr_reader :references
|
|
260
279
|
|
|
280
|
+
# A container forwards when anything inside it does: `icon: [:a, pick]`.
|
|
281
|
+
def forwarded_value?(value_node)
|
|
282
|
+
value_node = unwrap_declaration_wrappers(value_node)
|
|
283
|
+
case value_node
|
|
284
|
+
when Prism::ArrayNode then value_node.elements.any? { |element| forwarded_value?(element) }
|
|
285
|
+
when Prism::HashNode then value_node.elements.any? { |element| forwarded_pair?(element) }
|
|
286
|
+
else LITERAL_VALUES.none? { |literal| value_node.is_a?(literal) }
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def forwarded_pair?(element)
|
|
291
|
+
!element.is_a?(Prism::AssocNode) || forwarded_value?(element.value)
|
|
292
|
+
end
|
|
293
|
+
|
|
261
294
|
def record(node)
|
|
262
295
|
method = node.name.to_s
|
|
263
296
|
if GENERIC_CALLS.include?(node.name)
|
data/lib/glyphs/version.rb
CHANGED
data/lib/glyphs.rb
CHANGED
|
@@ -38,6 +38,8 @@ module Glyphs
|
|
|
38
38
|
# readers never take it, so svg_for runs in any Ractor once finalized.
|
|
39
39
|
@lock = Monitor.new
|
|
40
40
|
@configuration = Configuration.new
|
|
41
|
+
@prune_guard = nil
|
|
42
|
+
@prune_guard_lock = Mutex.new
|
|
41
43
|
|
|
42
44
|
class << self
|
|
43
45
|
# The current configuration: a plain read, shareable after finalize!.
|
|
@@ -49,6 +51,7 @@ module Glyphs
|
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
def reset_configuration!
|
|
54
|
+
@prune_guard_lock.synchronize { @prune_guard = nil }
|
|
52
55
|
@lock.synchronize { @configuration = Configuration.new }
|
|
53
56
|
end
|
|
54
57
|
|
|
@@ -120,12 +123,36 @@ module Glyphs
|
|
|
120
123
|
|
|
121
124
|
private
|
|
122
125
|
|
|
126
|
+
# A non-main Ractor cannot run the icons gem, so it renders on the main
|
|
127
|
+
# Ractor (see MainRactor); the missing-icon policy runs there too.
|
|
123
128
|
def render_svg(name:, library:, variant:, attributes:)
|
|
129
|
+
return MainRactor.render(name:, library:, variant:, attributes:) unless MainRactor.current?
|
|
130
|
+
|
|
131
|
+
resolve_svg(name:, library:, variant:, attributes:)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Only a miss gets here, and only on the main Ractor. A name with no file is
|
|
135
|
+
# never prunable, so it passes the guard and meets the missing-icon policy.
|
|
136
|
+
def resolve_svg(name:, library:, variant:, attributes:)
|
|
137
|
+
prune_guard&.check!(name:, library:, variant:)
|
|
124
138
|
build_svg(name:, library:, variant:, attributes:)
|
|
125
139
|
rescue Icons::IconNotFound => e
|
|
126
140
|
handle_missing_icon(e, name:, library:, variant:, attributes:)
|
|
127
141
|
end
|
|
128
142
|
|
|
143
|
+
# nil while raise_on_prunable is off, so that path reads nothing. A racing
|
|
144
|
+
# thread waits for the first scan rather than repeating it. Not @lock: this
|
|
145
|
+
# runs under the SvgCache mutex, and a configure block holding @lock may render.
|
|
146
|
+
def prune_guard
|
|
147
|
+
config = configuration
|
|
148
|
+
return unless config.raise_on_prunable
|
|
149
|
+
|
|
150
|
+
@prune_guard_lock.synchronize do
|
|
151
|
+
@prune_guard = PruneGuard.new(config:) unless @prune_guard&.current?(config)
|
|
152
|
+
@prune_guard
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
129
156
|
def build_svg(name:, library:, variant:, attributes:)
|
|
130
157
|
Icons::Icon.new(name:, library:, variant:, arguments: attributes).svg
|
|
131
158
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: glyphs
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -77,7 +77,10 @@ files:
|
|
|
77
77
|
- lib/glyphs/icon_reference.rb
|
|
78
78
|
- lib/glyphs/linear_icon.rb
|
|
79
79
|
- lib/glyphs/lucide_icon.rb
|
|
80
|
+
- lib/glyphs/main_ractor.rb
|
|
80
81
|
- lib/glyphs/phosphor_icon.rb
|
|
82
|
+
- lib/glyphs/prunable_icon_error.rb
|
|
83
|
+
- lib/glyphs/prune_guard.rb
|
|
81
84
|
- lib/glyphs/prune_report.rb
|
|
82
85
|
- lib/glyphs/prune_runner.rb
|
|
83
86
|
- lib/glyphs/radix_icon.rb
|