glyphs 0.4.0 → 0.4.2
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 +26 -0
- data/README.md +49 -0
- data/lib/glyphs/configuration.rb +9 -1
- data/lib/glyphs/default_data.rb +59 -0
- data/lib/glyphs/icon_pruner.rb +23 -20
- 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/svg_template.rb +3 -0
- data/lib/glyphs/version.rb +1 -1
- data/lib/glyphs.rb +35 -1
- 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: 798fe439ca9ea93ff3bfb84f23de90f87abd6ffee953cd85de5543066b52a4f0
|
|
4
|
+
data.tar.gz: 58829c2ec4bf5ed6aaa24da8b40a376b7aae48525277be499eae957754a99c11
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 49acc1efae75b8d2eb0fac2079ef20c2d5fb211a519c598d61f306453c3dbc000f4ee2fd4bb75090b10a63f009e8bbbddde0e4a3277bc340b58e5eead644eec2
|
|
7
|
+
data.tar.gz: 7b3adbeb6816fbeb13eea1584a33ef1cbcb241339c7caedab14b92b44a185050ebd8240ae0ed99ab5715cc8ccaeb471968f491354e009112eee76ccbd23831a3
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
### Added
|
|
6
6
|
|
|
7
|
+
- **`config.merge_default_data` keeps a library's default data next to a
|
|
8
|
+
call's own.** Off by default. The icons gem replaces a library's configured
|
|
9
|
+
`default.data` with a call's `data:` hash, so a default marker such as
|
|
10
|
+
`data-icon` disappears from exactly the icons that carry data of their own.
|
|
11
|
+
With the setting on, the call's `data:` hash is merged over the defaults (the
|
|
12
|
+
call's keys win, its hash is not mutated; `data: nil` keeps the defaults, uncached).
|
|
13
|
+
Cached and uncached renders are byte-identical, the cache key is unchanged,
|
|
14
|
+
and a library with no default data renders as before. With it off, output is
|
|
15
|
+
unchanged. Refs #40
|
|
7
16
|
- **A non-main Ractor renders icons it has not cached.** A miss there used to
|
|
8
17
|
raise `Ractor::IsolationError` (`@configuration from Icons`), because the
|
|
9
18
|
icons gem reads an unshareable module ivar and parses with Nokogiri, which
|
|
@@ -15,6 +24,23 @@
|
|
|
15
24
|
`Glyphs::MainRactor::Unavailable` naming the icon, and `Glyphs.preload_svg`
|
|
16
25
|
still seeds a Ractor's cache. glyphs does not depend on ractor-dispatch: add
|
|
17
26
|
it outside Rails 8.2. Refs zoolutions/phlex-reactive#363
|
|
27
|
+
- **`config.raise_on_prunable` fails a render the prune would delete.** Off by
|
|
28
|
+
default. With it on, typically in the test environment, rendering such an icon
|
|
29
|
+
raises `Glyphs::PrunableIconError`, naming the library, variant and name, so a
|
|
30
|
+
test fails instead of production. The guard asks the pruner, so under one
|
|
31
|
+
configuration the two agree. The first uncached render scans the source, once
|
|
32
|
+
per process; with the setting off nothing is scanned. Enabling it may fail specs, and each such
|
|
33
|
+
failure is an icon that would have gone missing in production. Refs #35
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **`glyphs:prune_icons` no longer deletes icons forwarded through a wrapper
|
|
38
|
+
component.** A file that passes a non-literal under a key like an icon
|
|
39
|
+
(`Badge(icon:)`, `Badge(icon: row.first)`) now keeps its icon-name-shaped
|
|
40
|
+
literals for every dynamically-rendered library. They were dropped when the
|
|
41
|
+
file itself had no dynamic icon call, so the icon raised
|
|
42
|
+
`Icons::IconNotFound` in production only. Pruning can only keep a little more
|
|
43
|
+
than before. Refs #35
|
|
18
44
|
|
|
19
45
|
## [0.3.0] - 2026-10-15
|
|
20
46
|
|
data/README.md
CHANGED
|
@@ -105,6 +105,16 @@ Glyphs.configure do |config|
|
|
|
105
105
|
# on every render that goes uncached.
|
|
106
106
|
config.cache_svgs = true
|
|
107
107
|
|
|
108
|
+
# Merge a call's data: hash over the library's configured default data
|
|
109
|
+
# (`default.data` in the icons configuration) instead of replacing it, as the
|
|
110
|
+
# icons gem does. The call's keys win on a conflict; its hash is not mutated.
|
|
111
|
+
# data: nil keeps the defaults, as no data: does; any other non-hash data:
|
|
112
|
+
# renders as the icons gem renders it. Both render uncached. Cached and
|
|
113
|
+
# uncached renders are identical either way. Default: false (the icons gem's behavior).
|
|
114
|
+
# The cache holds the merged markup: call Glyphs.reset_cache! after changing
|
|
115
|
+
# this or the icons gem's default data at runtime (tests).
|
|
116
|
+
config.merge_default_data = false
|
|
117
|
+
|
|
108
118
|
# How many icons the cache holds (least recently used evicted first). One
|
|
109
119
|
# cache per Ractor, so a plain threaded server has one. Default: 1024.
|
|
110
120
|
config.svg_cache_size = 1024
|
|
@@ -137,6 +147,8 @@ Active Support; elsewhere, add `gem "ractor-dispatch"`. The markup is the main R
|
|
|
137
147
|
the calling Ractor's cache keeps it, so each icon crosses once per cache entry; cache hits never leave the
|
|
138
148
|
Ractor. The missing-icon policy runs on the main Ractor too, `on_missing_icon` included. A call with an
|
|
139
149
|
attribute that renders uncached, or any call with `cache_svgs = false`, goes to the main Ractor every time.
|
|
150
|
+
`merge_default_data` reads the default data where the icons gem renders, on the main Ractor, so it adds no
|
|
151
|
+
crossing: a hit merges the call's `data:` in the calling Ractor. With it on, a non-hash `data:` (`nil` included) renders uncached.
|
|
140
152
|
|
|
141
153
|
Without ractor-dispatch, a miss in a non-main Ractor raises `Glyphs::MainRactor::Unavailable`, naming the
|
|
142
154
|
icon. You can still render on the main Ractor and hand the markup over with `Glyphs.preload_svg`; cache hits
|
|
@@ -180,11 +192,48 @@ The scanner resolves them from source, so you rarely need `keep_icons` at all:
|
|
|
180
192
|
constant named like one (`ICON = :bell`, `STATUS_ICONS = { .. => :warning }`,
|
|
181
193
|
including trailing `.freeze`) — are kept for every dynamically-rendered
|
|
182
194
|
library, so a name declared in one file and rendered from another survives.
|
|
195
|
+
- **Forwarded** — a file that passes a non-literal under a key like an icon
|
|
196
|
+
(`Badge(icon:)`, `Badge(icon: row.first)`) hands a name on to a wrapper
|
|
197
|
+
component, so every icon-name-shaped literal in that file is kept for every
|
|
198
|
+
dynamically-rendered library. `ROWS = [[:bell, 1]]` survives when the row
|
|
199
|
+
reaches `Badge(icon:)`, and `Badge`'s `LucideIcon(@icon)` lives in another file.
|
|
183
200
|
|
|
184
201
|
Only literals are harvested, so the scanner never invents a reference; the
|
|
185
202
|
worst case is keeping a coincidentally icon-named string, which the post-prune
|
|
186
203
|
verification tolerates.
|
|
187
204
|
|
|
205
|
+
A static scan can't follow every value. A literal passed positionally to a
|
|
206
|
+
wrapper (`m.row("Alerts", :anchor)`) is still pruned, and so is a forwarded
|
|
207
|
+
keyword in a template, which is text-scanned. Nothing fails until production:
|
|
208
|
+
development and test keep every SVG, and the post-prune verification doesn't
|
|
209
|
+
assert harvested names. `raise_on_prunable` closes that gap.
|
|
210
|
+
|
|
211
|
+
### `raise_on_prunable` — fail the test that renders a prunable icon
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
# config/environments/test.rb, or an initializer guarded by Rails.env.test?
|
|
215
|
+
Glyphs.configure do |config|
|
|
216
|
+
config.raise_on_prunable = true
|
|
217
|
+
end
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
With it on, rendering an icon the prune would delete raises
|
|
221
|
+
`Glyphs::PrunableIconError`, naming the library, variant and name. Every page a
|
|
222
|
+
test renders becomes a check, and each failure is a production bug: name the icon
|
|
223
|
+
in an icon position (`icon: :anchor`, an `ICON` constant) or list it in
|
|
224
|
+
`keep_icons`.
|
|
225
|
+
|
|
226
|
+
The guard asks the pruner itself, so the two agree, including on `keep_icons`
|
|
227
|
+
globs and on the libraries a prune leaves alone. That holds for the configuration
|
|
228
|
+
the test runs with: a `keep_icons`, `fallback_icons` or `prune_source_globs` that
|
|
229
|
+
differs by `Rails.env` makes the guard answer for a different keep-set than your
|
|
230
|
+
image build prunes with. The first uncached render scans
|
|
231
|
+
your source, once per process (a parallel test worker scans for itself), and
|
|
232
|
+
cache hits never reach the check. A name that doesn't exist at all still meets
|
|
233
|
+
the missing-icon policy. The default is `false`, which scans nothing, and
|
|
234
|
+
source changed after the scan isn't seen until the process restarts, so keep this
|
|
235
|
+
in the test environment.
|
|
236
|
+
|
|
188
237
|
### `keep_icons` — the last-resort escape hatch
|
|
189
238
|
|
|
190
239
|
For names a static scan genuinely can't see — read from a database, an ENV var,
|
data/lib/glyphs/configuration.rb
CHANGED
|
@@ -18,14 +18,20 @@ module Glyphs
|
|
|
18
18
|
# cache_svgs: memoize rendered SVGs per [library, variant, name]; the
|
|
19
19
|
# caller's attributes are applied to the cached markup on every call.
|
|
20
20
|
# svg_cache_size: how many icons each Ractor's LRU cache holds (1024).
|
|
21
|
+
# merge_default_data: boolean; when true, a caller's data: hash is merged
|
|
22
|
+
# over the library/variant's configured default data (the caller's keys
|
|
23
|
+
# win) instead of replacing it, as the icons gem does. Default: false.
|
|
21
24
|
# keep_icons: icons the pruner must keep despite no static reference
|
|
22
25
|
# (dynamic / data-driven names). A flat list of names/globs, or a
|
|
23
26
|
# { library => [names/globs] } hash. fallback_icons are always kept.
|
|
24
27
|
# prune_source_globs: extra locations the pruner's scanner text-scans IN
|
|
25
28
|
# ADDITION to the built-in Ruby/template defaults — e.g. a config file that
|
|
26
29
|
# names icons. Never replaces the defaults (nil = defaults only).
|
|
30
|
+
# raise_on_prunable: boolean; when true, rendering an icon glyphs:prune_icons
|
|
31
|
+
# would delete raises Glyphs::PrunableIconError. For the test environment:
|
|
32
|
+
# the first uncached render scans the source, once per process.
|
|
27
33
|
attr_accessor :raise_on_missing, :on_missing_icon, :fallback_icons, :cache_svgs,
|
|
28
|
-
:keep_icons, :prune_source_globs
|
|
34
|
+
:keep_icons, :prune_source_globs, :raise_on_prunable, :merge_default_data
|
|
29
35
|
attr_reader :svg_cache_size
|
|
30
36
|
|
|
31
37
|
def initialize
|
|
@@ -36,6 +42,8 @@ module Glyphs
|
|
|
36
42
|
@svg_cache_size = DEFAULT_SVG_CACHE_SIZE
|
|
37
43
|
@keep_icons = []
|
|
38
44
|
@prune_source_globs = nil
|
|
45
|
+
@raise_on_prunable = false
|
|
46
|
+
@merge_default_data = false
|
|
39
47
|
end
|
|
40
48
|
|
|
41
49
|
def svg_cache_size=(size)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glyphs
|
|
4
|
+
# config.merge_default_data: merges a library's configured default data under
|
|
5
|
+
# a caller's data: hash, which the icons gem would otherwise replace it with.
|
|
6
|
+
# Reads the icons gem's configuration, so it runs on the main Ractor only
|
|
7
|
+
# (Glyphs.build_svg, which a non-main Ractor reaches through MainRactor).
|
|
8
|
+
# A default data key named like SvgTemplate's slot marker (glyphs-data-slot)
|
|
9
|
+
# would be dropped by the cached data: template, so such defaults are not
|
|
10
|
+
# merged: both paths render as the icons gem does.
|
|
11
|
+
module DefaultData
|
|
12
|
+
SLOT_NAME = SvgTemplate::SLOT_ATTRIBUTE.delete_prefix("data-").freeze
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
# The attributes with the default data merged under a data: hash, or
|
|
16
|
+
# without a data: nil (the defaults alone, as with no data:), when the
|
|
17
|
+
# library has default data; else the attributes as given. A new Hash: the
|
|
18
|
+
# caller's is never mutated.
|
|
19
|
+
def merge(attributes, library:, variant:)
|
|
20
|
+
return attributes unless attributes.key?(:data)
|
|
21
|
+
|
|
22
|
+
data = attributes[:data]
|
|
23
|
+
return attributes unless data.nil? || data.is_a?(Hash)
|
|
24
|
+
|
|
25
|
+
defaults = lookup(library:, variant:)
|
|
26
|
+
return attributes unless mergeable?(defaults)
|
|
27
|
+
|
|
28
|
+
data.nil? ? attributes.except(:data) : attributes.merge(data: by_name(defaults, data))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def mergeable?(defaults)
|
|
34
|
+
defaults.is_a?(Hash) && !defaults.empty? && defaults.each_key.none? { |key| key.to_s.tr("_", "-") == SLOT_NAME }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# One entry per attribute name, however its key is spelled (:toggle_target,
|
|
38
|
+
# "toggle-target"): at the position its name first appears, with the value
|
|
39
|
+
# it was given last, the caller's after the defaults. That is how a cache
|
|
40
|
+
# hit splices the caller's data over the template's defaults.
|
|
41
|
+
def by_name(defaults, data)
|
|
42
|
+
merged = {}
|
|
43
|
+
defaults.each { |key, value| merged[key.to_s.tr("_", "-")] = value }
|
|
44
|
+
data.each { |key, value| merged[key.to_s.tr("_", "-")] = value }
|
|
45
|
+
merged
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The icons gem's own lookup (Icons::Icon::Configurable): the variant, else
|
|
49
|
+
# the library's default_variant, else the global default_variant.
|
|
50
|
+
def lookup(library:, variant:)
|
|
51
|
+
icons = Icons.configuration
|
|
52
|
+
library = library.to_sym
|
|
53
|
+
variant ||= icons.libraries.dig(library, :default_variant) || icons.default_variant
|
|
54
|
+
variant = variant.to_s.empty? ? nil : variant.to_sym
|
|
55
|
+
(icons.libraries.dig(*[library, variant].compact) || {}).dig(:default, :data)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
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,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/svg_template.rb
CHANGED
|
@@ -18,6 +18,9 @@ module Glyphs
|
|
|
18
18
|
# A caller's data: replaces the library's default data (the icons gem merges
|
|
19
19
|
# them that way) and lands where the defaults would have, so data: calls use
|
|
20
20
|
# a template rendered with DATA_SLOT, whose marker attribute holds that place.
|
|
21
|
+
# With merge_default_data on, that template is rendered with the default data
|
|
22
|
+
# before the marker (Glyphs::DefaultData), and a caller's key that names a
|
|
23
|
+
# default replaces it in place, as rendering the merged hash would.
|
|
21
24
|
class SvgTemplate
|
|
22
25
|
DATA_SLOT = { data: { "glyphs-data-slot" => "" }.freeze }.freeze
|
|
23
26
|
SLOT_ATTRIBUTE = "data-glyphs-data-slot"
|
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
|
|
|
@@ -89,8 +92,15 @@ module Glyphs
|
|
|
89
92
|
# policy (raise / instrument / fallback) and the per-Ractor cache: one
|
|
90
93
|
# entry per [library, variant, name], the caller's attributes applied to
|
|
91
94
|
# the cached markup on every call.
|
|
95
|
+
#
|
|
96
|
+
# With merge_default_data on, the library's default data is merged under a
|
|
97
|
+
# caller's data: hash when the icons gem renders (build_svg, main Ractor
|
|
98
|
+
# only, where its configuration is readable). The data: template is then
|
|
99
|
+
# rendered with the defaults beside the slot marker, so a cache hit splices
|
|
100
|
+
# the caller's keys over them exactly as the merged hash would render. A
|
|
101
|
+
# non-hash data: renders uncached (data: nil keeps the defaults, if any).
|
|
92
102
|
def svg_for(name:, library:, variant:, attributes:)
|
|
93
|
-
return render_svg(name:, library:, variant:, attributes:)
|
|
103
|
+
return render_svg(name:, library:, variant:, attributes:) if uncached?(attributes)
|
|
94
104
|
|
|
95
105
|
data = attributes.key?(:data)
|
|
96
106
|
template = svg_cache.compute_if_absent([library, variant, name, data], configuration.svg_cache_size) do
|
|
@@ -120,6 +130,13 @@ module Glyphs
|
|
|
120
130
|
|
|
121
131
|
private
|
|
122
132
|
|
|
133
|
+
def uncached?(attributes)
|
|
134
|
+
config = configuration
|
|
135
|
+
return true unless config.cache_svgs
|
|
136
|
+
|
|
137
|
+
config.merge_default_data && attributes.key?(:data) && !attributes[:data].is_a?(Hash)
|
|
138
|
+
end
|
|
139
|
+
|
|
123
140
|
# A non-main Ractor cannot run the icons gem, so it renders on the main
|
|
124
141
|
# Ractor (see MainRactor); the missing-icon policy runs there too.
|
|
125
142
|
def render_svg(name:, library:, variant:, attributes:)
|
|
@@ -128,13 +145,30 @@ module Glyphs
|
|
|
128
145
|
resolve_svg(name:, library:, variant:, attributes:)
|
|
129
146
|
end
|
|
130
147
|
|
|
148
|
+
# Only a miss gets here, and only on the main Ractor. A name with no file is
|
|
149
|
+
# never prunable, so it passes the guard and meets the missing-icon policy.
|
|
131
150
|
def resolve_svg(name:, library:, variant:, attributes:)
|
|
151
|
+
prune_guard&.check!(name:, library:, variant:)
|
|
132
152
|
build_svg(name:, library:, variant:, attributes:)
|
|
133
153
|
rescue Icons::IconNotFound => e
|
|
134
154
|
handle_missing_icon(e, name:, library:, variant:, attributes:)
|
|
135
155
|
end
|
|
136
156
|
|
|
157
|
+
# nil while raise_on_prunable is off, so that path reads nothing. A racing
|
|
158
|
+
# thread waits for the first scan rather than repeating it. Not @lock: this
|
|
159
|
+
# runs under the SvgCache mutex, and a configure block holding @lock may render.
|
|
160
|
+
def prune_guard
|
|
161
|
+
config = configuration
|
|
162
|
+
return unless config.raise_on_prunable
|
|
163
|
+
|
|
164
|
+
@prune_guard_lock.synchronize do
|
|
165
|
+
@prune_guard = PruneGuard.new(config:) unless @prune_guard&.current?(config)
|
|
166
|
+
@prune_guard
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
137
170
|
def build_svg(name:, library:, variant:, attributes:)
|
|
171
|
+
attributes = DefaultData.merge(attributes, library:, variant:) if configuration.merge_default_data
|
|
138
172
|
Icons::Icon.new(name:, library:, variant:, arguments: attributes).svg
|
|
139
173
|
end
|
|
140
174
|
|
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.
|
|
4
|
+
version: 0.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -68,6 +68,7 @@ files:
|
|
|
68
68
|
- lib/glyphs/animated_icon.rb
|
|
69
69
|
- lib/glyphs/box_icon.rb
|
|
70
70
|
- lib/glyphs/configuration.rb
|
|
71
|
+
- lib/glyphs/default_data.rb
|
|
71
72
|
- lib/glyphs/feather_icon.rb
|
|
72
73
|
- lib/glyphs/flag_icon.rb
|
|
73
74
|
- lib/glyphs/hero_icon.rb
|
|
@@ -79,6 +80,8 @@ files:
|
|
|
79
80
|
- lib/glyphs/lucide_icon.rb
|
|
80
81
|
- lib/glyphs/main_ractor.rb
|
|
81
82
|
- lib/glyphs/phosphor_icon.rb
|
|
83
|
+
- lib/glyphs/prunable_icon_error.rb
|
|
84
|
+
- lib/glyphs/prune_guard.rb
|
|
82
85
|
- lib/glyphs/prune_report.rb
|
|
83
86
|
- lib/glyphs/prune_runner.rb
|
|
84
87
|
- lib/glyphs/radix_icon.rb
|