glyphs 0.1.0
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 +7 -0
- data/CHANGELOG.md +15 -0
- data/LICENSE.txt +21 -0
- data/README.md +187 -0
- data/config/default.yml +25 -0
- data/lib/glyphs/animated_icon.rb +7 -0
- data/lib/glyphs/box_icon.rb +7 -0
- data/lib/glyphs/configuration.rb +24 -0
- data/lib/glyphs/feather_icon.rb +7 -0
- data/lib/glyphs/flag_icon.rb +7 -0
- data/lib/glyphs/hero_icon.rb +7 -0
- data/lib/glyphs/huge_icon.rb +7 -0
- data/lib/glyphs/icon.rb +35 -0
- data/lib/glyphs/linear_icon.rb +7 -0
- data/lib/glyphs/lucide_icon.rb +7 -0
- data/lib/glyphs/phosphor_icon.rb +7 -0
- data/lib/glyphs/radix_icon.rb +7 -0
- data/lib/glyphs/rubocop.rb +39 -0
- data/lib/glyphs/sidekick_icon.rb +7 -0
- data/lib/glyphs/tabler_icon.rb +7 -0
- data/lib/glyphs/version.rb +5 -0
- data/lib/glyphs/weather_icon.rb +7 -0
- data/lib/glyphs.rb +115 -0
- data/lib/rubocop/cop/glyphs/icon_resolution.rb +286 -0
- data/lib/rubocop/cop/glyphs/legacy_icon_helper.rb +97 -0
- data/lib/rubocop/cop/glyphs/library_call_helpers.rb +55 -0
- data/lib/rubocop/cop/glyphs/prefer_library_component.rb +46 -0
- metadata +115 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1a379bf83aad5e7a6adfc47f2f8cced12baa7ce0e970b0b675747445f8ab9f99
|
|
4
|
+
data.tar.gz: d134dcee770ac2629575ef80b020b9e61982e55bd8affd0e944ce5316ad22e4d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8d0a4346f8b888f8be02ec3403b46262dfa5593276d767cc37215e5eb03d2c2a7261d7f9709ddb2fad1d1715220dfcecfd6c5dbb928818810cf5442c62320c2e
|
|
7
|
+
data.tar.gz: 444366fd42ba9ba2a8b422a0b2ad8ec8674bc737e477fc10e91cc7f7890b9cdc52bb76eb8b5056fd87c80123a959f2fe919e25f1e95be2f09abfcb2746494961
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-07-07
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `Glyphs::Icon` base Phlex component rendering SVGs through the `icons` gem (rails_icons).
|
|
8
|
+
- Library components exposed via `Phlex::Kit`: `LucideIcon`, `PhosphorIcon`, `HeroIcon`, `TablerIcon`,
|
|
9
|
+
`FeatherIcon`, `BoxIcon`, `FlagIcon`, `HugeIcon`, `LinearIcon`, `RadixIcon`, `SidekickIcon`,
|
|
10
|
+
`WeatherIcon`, `AnimatedIcon`.
|
|
11
|
+
- `Glyphs.register_library` for custom icon libraries.
|
|
12
|
+
- Configurable missing-icon policy (`raise_on_missing_icon`, `on_missing_icon` hook, `fallback_icons`)
|
|
13
|
+
and per-process SVG render cache (`cache_svgs`).
|
|
14
|
+
- RuboCop plugin (lint_roller) with `Glyphs/LegacyIconHelper`, `Glyphs/IconResolution`, and
|
|
15
|
+
`Glyphs/PreferLibraryComponent`.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 David Alejandro
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Glyphs
|
|
2
|
+
|
|
3
|
+
Phlex icon components for every [rails_icons](https://github.com/rails-designer/rails_icons) library —
|
|
4
|
+
plus bundled RuboCop cops that validate icon names at lint time and autocorrect legacy icon helpers.
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
class ApplicationComponent < Phlex::HTML
|
|
8
|
+
include Glyphs
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
LucideIcon(:house, class: "size-4")
|
|
12
|
+
PhosphorIcon("lock", variant: :bold)
|
|
13
|
+
HeroIcon(:check, class: "size-5 text-success")
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
# Gemfile
|
|
20
|
+
gem "glyphs"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Glyphs renders the SVG files that rails_icons syncs into your app. If you haven't already:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
rails generate rails_icons:install --libraries=lucide heroicons
|
|
27
|
+
rails generate rails_icons:sync --libraries=lucide heroicons
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Outside Rails, configure the [icons](https://rubygems.org/gems/icons) gem directly:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
Icons.configure do |config|
|
|
34
|
+
config.base_path = File.expand_path(__dir__)
|
|
35
|
+
config.icons_path = "svg/icons"
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Components
|
|
40
|
+
|
|
41
|
+
`include Glyphs` (a `Phlex::Kit`) into your component base class and call icons as capitalized methods:
|
|
42
|
+
|
|
43
|
+
| Component | rails_icons library |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `LucideIcon` | lucide |
|
|
46
|
+
| `PhosphorIcon` | phosphor |
|
|
47
|
+
| `HeroIcon` | heroicons |
|
|
48
|
+
| `TablerIcon` | tabler |
|
|
49
|
+
| `FeatherIcon` | feather |
|
|
50
|
+
| `BoxIcon` | boxicons |
|
|
51
|
+
| `FlagIcon` | flags |
|
|
52
|
+
| `HugeIcon` | hugeicons |
|
|
53
|
+
| `LinearIcon` | linear |
|
|
54
|
+
| `RadixIcon` | radix |
|
|
55
|
+
| `SidekickIcon` | sidekickicons |
|
|
56
|
+
| `WeatherIcon` | weather |
|
|
57
|
+
| `AnimatedIcon` | animated (bundled spinners: `faded-spinner`, `bouncing-dots`, ...) |
|
|
58
|
+
|
|
59
|
+
Names can be symbols or strings; underscores are dasherized (`:circle_check` → `circle-check.svg`).
|
|
60
|
+
`variant:` selects the library variant (default comes from your rails_icons/icons configuration);
|
|
61
|
+
every other keyword (`class:`, `data:`, `stroke_width:`, ...) is forwarded onto the `<svg>` tag.
|
|
62
|
+
|
|
63
|
+
The generic component requires an explicit library:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
Icon(:house, library: :lucide) # flagged by Glyphs/PreferLibraryComponent, prefer LucideIcon(:house)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Custom libraries get first-class components too:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
Glyphs.register_library(:brand, component: :BrandIcon)
|
|
73
|
+
BrandIcon(:logo)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
(The SVG location for a custom library is configured through rails_icons/icons custom-library config.)
|
|
77
|
+
|
|
78
|
+
## Missing-icon policy
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
# config/initializers/glyphs.rb
|
|
82
|
+
Glyphs.configure do |config|
|
|
83
|
+
# Re-raise Icons::IconNotFound? Default: true locally (dev/test), never blind in production.
|
|
84
|
+
config.raise_on_missing_icon = -> { Rails.env.local? }
|
|
85
|
+
|
|
86
|
+
# Instrumentation hook, called before the fallback renders.
|
|
87
|
+
config.on_missing_icon = lambda do |error, name:, library:, variant:|
|
|
88
|
+
Rails.logger.error("Icon missing: #{library}/#{variant}/#{name} (#{error.message})")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Rendered instead of the missing icon (per library). nil/absent => re-raise.
|
|
92
|
+
config.fallback_icons = {
|
|
93
|
+
lucide: "circle-question-mark",
|
|
94
|
+
phosphor: "question",
|
|
95
|
+
heroicons: "question-mark-circle"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Memoize rendered SVG strings per [library, variant, name, attributes]. Default: true.
|
|
99
|
+
# Note: fallback renders are cached under the original name, so on_missing_icon
|
|
100
|
+
# fires once per process per missing icon rather than on every render.
|
|
101
|
+
config.cache_svgs = true
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## RuboCop cops
|
|
106
|
+
|
|
107
|
+
Add the plugin (RuboCop >= 1.72):
|
|
108
|
+
|
|
109
|
+
```yaml
|
|
110
|
+
# .rubocop.yml
|
|
111
|
+
plugins:
|
|
112
|
+
- glyphs
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
(Classic `require: [glyphs/rubocop]` also works, but you must enable the cops yourself.)
|
|
116
|
+
|
|
117
|
+
### Glyphs/LegacyIconHelper
|
|
118
|
+
|
|
119
|
+
Autocorrects legacy helper calls to Glyphs components:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
_lucide(:house, class: "size-4") # => LucideIcon(:house, class: "size-4")
|
|
123
|
+
_heroicon(:check) # => HeroIcon(:check)
|
|
124
|
+
icon("check", library: "lucide") # => LucideIcon("check")
|
|
125
|
+
icon("check") # => HeroIcon("check") (DefaultLibraryComponent)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```yaml
|
|
129
|
+
Glyphs/LegacyIconHelper:
|
|
130
|
+
Include:
|
|
131
|
+
- app/**/*.rb
|
|
132
|
+
DefaultLibraryComponent: HeroIcon
|
|
133
|
+
# Mappings replaces the built-in defaults (_lucide/_phosphor/_hero/_heroicon/_tabler):
|
|
134
|
+
# Mappings:
|
|
135
|
+
# _custom: CustomIcon
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Glyphs/IconResolution
|
|
139
|
+
|
|
140
|
+
Validates statically-known icon names against your synced SVG directories, honors literal
|
|
141
|
+
`variant:` keywords, suggests close matches (with autocorrect for unambiguous typos and the
|
|
142
|
+
Lucide v1 renames), and flags raw `iconify` class strings:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
LucideIcon(:alert_triangle) # => corrected to LucideIcon(:triangle_alert)
|
|
146
|
+
span(class: "iconify lucide--house size-4") # => corrected to LucideIcon(:house, class: "size-4")
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```yaml
|
|
150
|
+
Glyphs/IconResolution:
|
|
151
|
+
Include:
|
|
152
|
+
- app/**/*.rb
|
|
153
|
+
IconsPath: app/assets/svg/icons
|
|
154
|
+
# Libraries merges over the built-in defaults (component => Dir/DefaultVariant):
|
|
155
|
+
Libraries:
|
|
156
|
+
PhosphorIcon: { Dir: phosphor, DefaultVariant: regular }
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Glyphs/PreferLibraryComponent
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
Icon(:house, library: :lucide) # => LucideIcon(:house)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Migrating an app off icon helpers
|
|
166
|
+
|
|
167
|
+
1. Add `gem "glyphs"`, `include Glyphs` where the helpers used to be included.
|
|
168
|
+
2. Add the plugin and cop config above to `.rubocop.yml`.
|
|
169
|
+
3. Delete your local icon helper, then run:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
bundle exec rubocop -A --only Glyphs/LegacyIconHelper,Glyphs/PreferLibraryComponent
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
4. Wire your old missing-icon logging into `Glyphs.configure` (see above).
|
|
176
|
+
|
|
177
|
+
## Development
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
bundle install
|
|
181
|
+
bundle exec rspec
|
|
182
|
+
bundle exec rubocop
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT — see [LICENSE.txt](LICENSE.txt).
|
data/config/default.yml
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Glyphs/LegacyIconHelper:
|
|
2
|
+
Description: 'Replace legacy icon helpers (_lucide, _hero, icon(), ...) with Glyphs components.'
|
|
3
|
+
Enabled: true
|
|
4
|
+
VersionAdded: '0.1.0'
|
|
5
|
+
SafeAutoCorrect: true
|
|
6
|
+
# Mappings replaces the built-in defaults when set:
|
|
7
|
+
# Mappings:
|
|
8
|
+
# _lucide: LucideIcon
|
|
9
|
+
# _heroicon: HeroIcon
|
|
10
|
+
DefaultLibraryComponent: HeroIcon
|
|
11
|
+
|
|
12
|
+
Glyphs/IconResolution:
|
|
13
|
+
Description: 'Validate that statically-known icon names exist in the synced SVG directories.'
|
|
14
|
+
Enabled: true
|
|
15
|
+
VersionAdded: '0.1.0'
|
|
16
|
+
IconsPath: app/assets/svg/icons
|
|
17
|
+
# Libraries merges over the built-in defaults:
|
|
18
|
+
# Libraries:
|
|
19
|
+
# PhosphorIcon: { Dir: phosphor, DefaultVariant: light }
|
|
20
|
+
|
|
21
|
+
Glyphs/PreferLibraryComponent:
|
|
22
|
+
Description: 'Prefer library-specific components over generic Icon(..., library: ...).'
|
|
23
|
+
Enabled: true
|
|
24
|
+
VersionAdded: '0.1.0'
|
|
25
|
+
SafeAutoCorrect: true
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glyphs
|
|
4
|
+
class Configuration
|
|
5
|
+
DEFAULT_FALLBACK_ICONS = {
|
|
6
|
+
lucide: "circle-question-mark",
|
|
7
|
+
phosphor: "question",
|
|
8
|
+
heroicons: "question-mark-circle"
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
11
|
+
# raise_on_missing_icon: callable -> bool; when true, Icons::IconNotFound is re-raised.
|
|
12
|
+
# on_missing_icon: callable(error, name:, library:, variant:) instrumentation hook.
|
|
13
|
+
# fallback_icons: { library => icon_name } rendered instead of a missing icon.
|
|
14
|
+
# cache_svgs: memoize rendered SVG strings per [library, variant, name, attributes].
|
|
15
|
+
attr_accessor :raise_on_missing_icon, :on_missing_icon, :fallback_icons, :cache_svgs
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
@raise_on_missing_icon = -> { defined?(Rails) ? Rails.env.local? : true }
|
|
19
|
+
@on_missing_icon = ->(error, name:, library:, variant:) {}
|
|
20
|
+
@fallback_icons = DEFAULT_FALLBACK_ICONS.dup
|
|
21
|
+
@cache_svgs = true
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/glyphs/icon.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glyphs
|
|
4
|
+
# Base icon component. Prefer the library-specific subclasses (LucideIcon,
|
|
5
|
+
# HeroIcon, ...); the generic form requires an explicit library:
|
|
6
|
+
#
|
|
7
|
+
# Icon(:house, library: :lucide)
|
|
8
|
+
class Icon < Phlex::HTML
|
|
9
|
+
LIBRARY = nil
|
|
10
|
+
|
|
11
|
+
def initialize(name, variant: nil, library: self.class::LIBRARY, **attributes)
|
|
12
|
+
if library.nil?
|
|
13
|
+
raise ArgumentError,
|
|
14
|
+
"library is required — use a library component (LucideIcon, HeroIcon, ...) or pass library:"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
@name = name.to_s.tr("_", "-")
|
|
18
|
+
@library = library.to_sym
|
|
19
|
+
@variant = variant&.to_sym
|
|
20
|
+
@attributes = attributes
|
|
21
|
+
|
|
22
|
+
super()
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def view_template
|
|
26
|
+
raw safe(svg_markup)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def svg_markup
|
|
32
|
+
Glyphs.svg_for(name: @name, library: @library, variant: @variant, attributes: @attributes)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubocop"
|
|
4
|
+
require "lint_roller"
|
|
5
|
+
require "pathname"
|
|
6
|
+
require "yaml"
|
|
7
|
+
|
|
8
|
+
require_relative "version"
|
|
9
|
+
require_relative "../rubocop/cop/glyphs/library_call_helpers"
|
|
10
|
+
require_relative "../rubocop/cop/glyphs/legacy_icon_helper"
|
|
11
|
+
require_relative "../rubocop/cop/glyphs/icon_resolution"
|
|
12
|
+
require_relative "../rubocop/cop/glyphs/prefer_library_component"
|
|
13
|
+
|
|
14
|
+
module Glyphs
|
|
15
|
+
module RuboCop
|
|
16
|
+
class Plugin < LintRoller::Plugin
|
|
17
|
+
def about
|
|
18
|
+
LintRoller::About.new(
|
|
19
|
+
name: "glyphs",
|
|
20
|
+
version: Glyphs::VERSION,
|
|
21
|
+
homepage: "https://github.com/mhenrixon/glyphs",
|
|
22
|
+
description: "Icon-name validation and legacy icon-helper autocorrection for Glyphs components."
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def supported?(context)
|
|
27
|
+
context.engine == :rubocop
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def rules(_context)
|
|
31
|
+
LintRoller::Rules.new(
|
|
32
|
+
type: :path,
|
|
33
|
+
config_format: :rubocop,
|
|
34
|
+
value: Pathname.new(__dir__).join("../../config/default.yml")
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/glyphs.rb
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "phlex"
|
|
4
|
+
require "icons"
|
|
5
|
+
require "zeitwerk"
|
|
6
|
+
require_relative "glyphs/version"
|
|
7
|
+
|
|
8
|
+
loader = Zeitwerk::Loader.for_gem
|
|
9
|
+
loader.ignore("#{__dir__}/glyphs/version.rb")
|
|
10
|
+
# The RuboCop plugin lives in a foreign namespace and is only loaded inside a
|
|
11
|
+
# RuboCop process (see the bottom of this file).
|
|
12
|
+
loader.ignore("#{__dir__}/glyphs/rubocop.rb")
|
|
13
|
+
loader.ignore("#{__dir__}/rubocop")
|
|
14
|
+
loader.setup
|
|
15
|
+
|
|
16
|
+
# Ensure the icons gem is configured even outside Rails. Inside Rails the
|
|
17
|
+
# rails_icons engine calls `Icons.configure` with the app's settings; the
|
|
18
|
+
# configuration is memoized, so this never clobbers app configuration.
|
|
19
|
+
Icons.config
|
|
20
|
+
|
|
21
|
+
# Phlex icon components for every rails_icons library, exposed as a Phlex::Kit:
|
|
22
|
+
#
|
|
23
|
+
# include Glyphs
|
|
24
|
+
# LucideIcon(:house, class: "size-4")
|
|
25
|
+
module Glyphs
|
|
26
|
+
extend Phlex::Kit
|
|
27
|
+
|
|
28
|
+
@svg_cache = {}
|
|
29
|
+
@svg_cache_mutex = Mutex.new
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
def configure
|
|
33
|
+
yield(configuration) if block_given?
|
|
34
|
+
configuration
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def configuration
|
|
38
|
+
@configuration ||= Configuration.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def reset_configuration!
|
|
42
|
+
@configuration = Configuration.new
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Defines a `Glyphs::Icon` subclass for a custom icon library and exposes it
|
|
46
|
+
# as a kit method. The library's SVG location itself is configured through
|
|
47
|
+
# the icons/rails_icons custom-library configuration.
|
|
48
|
+
#
|
|
49
|
+
# Glyphs.register_library(:brand, component: :BrandIcon)
|
|
50
|
+
# # => BrandIcon(:logo, class: "size-4")
|
|
51
|
+
def register_library(library, component:)
|
|
52
|
+
library = library.to_sym
|
|
53
|
+
name = component.to_sym
|
|
54
|
+
|
|
55
|
+
if const_defined?(name, false)
|
|
56
|
+
existing = const_get(name, false)
|
|
57
|
+
return existing if existing < Icon && library == existing::LIBRARY
|
|
58
|
+
|
|
59
|
+
raise ArgumentError, "Glyphs::#{name} is already defined and does not render the #{library} library"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
klass = Class.new(Icon)
|
|
63
|
+
klass.const_set(:LIBRARY, library)
|
|
64
|
+
const_set(name, klass)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Returns the SVG markup for an icon, applying the configured missing-icon
|
|
68
|
+
# policy (raise / instrument / fallback) and an optional per-process cache.
|
|
69
|
+
def svg_for(name:, library:, variant:, attributes:)
|
|
70
|
+
return render_svg(name:, library:, variant:, attributes:) unless configuration.cache_svgs
|
|
71
|
+
|
|
72
|
+
key = [library, variant, name, attributes]
|
|
73
|
+
@svg_cache_mutex.synchronize do
|
|
74
|
+
@svg_cache[key] ||= render_svg(name:, library:, variant:, attributes:)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def reset_cache!
|
|
79
|
+
@svg_cache_mutex.synchronize { @svg_cache = {} }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def render_svg(name:, library:, variant:, attributes:)
|
|
85
|
+
build_svg(name:, library:, variant:, attributes:)
|
|
86
|
+
rescue Icons::IconNotFound => e
|
|
87
|
+
handle_missing_icon(e, name:, library:, variant:, attributes:)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def build_svg(name:, library:, variant:, attributes:)
|
|
91
|
+
Icons::Icon.new(name:, library:, variant:, arguments: attributes).svg
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def handle_missing_icon(error, name:, library:, variant:, attributes:)
|
|
95
|
+
raise error if configuration.raise_on_missing_icon.call
|
|
96
|
+
|
|
97
|
+
configuration.on_missing_icon.call(error, name:, library:, variant:)
|
|
98
|
+
|
|
99
|
+
fallback = configuration.fallback_icons[library.to_sym]
|
|
100
|
+
raise error if fallback.nil?
|
|
101
|
+
|
|
102
|
+
begin
|
|
103
|
+
build_svg(name: fallback, library:, variant:, attributes:)
|
|
104
|
+
rescue Icons::IconNotFound
|
|
105
|
+
raise error
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Make the RuboCop plugin resolvable lazily: RuboCop constantizes
|
|
112
|
+
# Glyphs::RuboCop::Plugin (from the gemspec's default_lint_roller_plugin
|
|
113
|
+
# metadata) when it loads plugins, which can happen long after this gem was
|
|
114
|
+
# required — e.g. app boots first, cop specs load RuboCop later.
|
|
115
|
+
Glyphs.autoload :RuboCop, File.expand_path("glyphs/rubocop", __dir__)
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Glyphs
|
|
6
|
+
# Validates that statically-known icon names exist in the synced SVG
|
|
7
|
+
# directories (`IconsPath`), suggests close matches for typos, and flags
|
|
8
|
+
# raw `iconify` class strings.
|
|
9
|
+
#
|
|
10
|
+
# Both Glyphs component calls (`LucideIcon(:house)`) and legacy helper
|
|
11
|
+
# calls (`_lucide(:house)`) are validated. A literal `variant:` keyword is
|
|
12
|
+
# honored; calls with dynamic names or variants are skipped.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# # bad
|
|
16
|
+
# LucideIcon(:non_existent_icon)
|
|
17
|
+
# span(class: "iconify lucide--house size-4")
|
|
18
|
+
#
|
|
19
|
+
# # good
|
|
20
|
+
# LucideIcon(:house, class: "size-4")
|
|
21
|
+
class IconResolution < Base
|
|
22
|
+
extend AutoCorrector
|
|
23
|
+
|
|
24
|
+
DEFAULT_LIBRARIES = {
|
|
25
|
+
"LucideIcon" => { "Dir" => "lucide", "DefaultVariant" => "outline" },
|
|
26
|
+
"PhosphorIcon" => { "Dir" => "phosphor", "DefaultVariant" => "regular" },
|
|
27
|
+
"HeroIcon" => { "Dir" => "heroicons", "DefaultVariant" => "outline" },
|
|
28
|
+
"TablerIcon" => { "Dir" => "tabler", "DefaultVariant" => "outline" },
|
|
29
|
+
"SidekickIcon" => { "Dir" => "sidekickicons", "DefaultVariant" => "outline" }
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
LEGACY_HELPERS = {
|
|
33
|
+
"_lucide" => "LucideIcon",
|
|
34
|
+
"_phosphor" => "PhosphorIcon",
|
|
35
|
+
"_hero" => "HeroIcon",
|
|
36
|
+
"_heroicon" => "HeroIcon",
|
|
37
|
+
"_tabler" => "TablerIcon"
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
ICONIFY_PREFIX_TO_COMPONENT = {
|
|
41
|
+
"lucide" => "LucideIcon",
|
|
42
|
+
"phosphor" => "PhosphorIcon",
|
|
43
|
+
"heroicons" => "HeroIcon"
|
|
44
|
+
}.freeze
|
|
45
|
+
|
|
46
|
+
KNOWN_LUCIDE_RENAMES = {
|
|
47
|
+
"alert-triangle" => "triangle-alert",
|
|
48
|
+
"alert-circle" => "circle-alert",
|
|
49
|
+
"x-circle" => "circle-x"
|
|
50
|
+
}.freeze
|
|
51
|
+
|
|
52
|
+
ICONIFY_PATTERN = /\biconify\s+(lucide|phosphor|heroicons)--([a-z0-9-]+)/
|
|
53
|
+
|
|
54
|
+
RAW_MSG = "Use `%{component}(:%{symbol}, class: \"%{rest}\")` instead of raw `iconify %{library}--…` class."
|
|
55
|
+
RAW_DSTR_MSG = "Use `LucideIcon(name, class: ...)` etc. instead of building a raw " \
|
|
56
|
+
"`iconify <library>--…` class string."
|
|
57
|
+
MISSING_MSG = "Icon `%{name}` not found in %{library}/%{variant}. %{suggestion}"
|
|
58
|
+
|
|
59
|
+
def on_str(node)
|
|
60
|
+
return if node.parent&.dstr_type?
|
|
61
|
+
|
|
62
|
+
check_iconify_string(node, node.value)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def on_dstr(node)
|
|
66
|
+
combined = node.children.filter_map { |child| child.str_type? ? child.value : "INTERPOLATED" }.join
|
|
67
|
+
return unless combined.include?("iconify ")
|
|
68
|
+
|
|
69
|
+
add_offense(node, message: RAW_DSTR_MSG)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def on_send(node)
|
|
73
|
+
return unless node.receiver.nil?
|
|
74
|
+
|
|
75
|
+
component = component_for(node.method_name)
|
|
76
|
+
return unless component
|
|
77
|
+
|
|
78
|
+
library = libraries[component]
|
|
79
|
+
return unless library
|
|
80
|
+
|
|
81
|
+
check_call(node, component, library)
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
source = node.location.expression.source_buffer.name
|
|
84
|
+
warn "[Glyphs/IconResolution] suppressed #{e.class}: #{e.message} at #{source}:#{node.first_line}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def component_for(method_name)
|
|
90
|
+
name = method_name.to_s
|
|
91
|
+
return name if libraries.key?(name)
|
|
92
|
+
|
|
93
|
+
LEGACY_HELPERS[name]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def libraries
|
|
97
|
+
@libraries ||= DEFAULT_LIBRARIES.merge(cop_config["Libraries"] || {})
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def check_call(node, _component, library)
|
|
101
|
+
name = literal_name(node.first_argument)
|
|
102
|
+
return unless name
|
|
103
|
+
|
|
104
|
+
variant = variant_for(node, library)
|
|
105
|
+
return if variant == :dynamic
|
|
106
|
+
|
|
107
|
+
available = available_icons(library["Dir"], variant)
|
|
108
|
+
return if available.empty?
|
|
109
|
+
return if available.include?(name)
|
|
110
|
+
|
|
111
|
+
suggestion = build_suggestion(name, available, library["Dir"])
|
|
112
|
+
add_offense(node.first_argument,
|
|
113
|
+
message: format(MISSING_MSG, name:, library: library["Dir"], variant:,
|
|
114
|
+
suggestion: suggestion[:msg])) do |corrector|
|
|
115
|
+
next unless suggestion[:autocorrect]
|
|
116
|
+
|
|
117
|
+
replacement =
|
|
118
|
+
if node.first_argument.sym_type?
|
|
119
|
+
":#{suggestion[:name].tr('-', '_')}"
|
|
120
|
+
else
|
|
121
|
+
%("#{suggestion[:name]}")
|
|
122
|
+
end
|
|
123
|
+
corrector.replace(node.first_argument, replacement)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def variant_for(node, library)
|
|
128
|
+
pair = variant_pair(node)
|
|
129
|
+
return library["DefaultVariant"] if pair.nil?
|
|
130
|
+
return :dynamic unless pair.value.str_type? || pair.value.sym_type?
|
|
131
|
+
|
|
132
|
+
pair.value.value.to_s
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def variant_pair(node)
|
|
136
|
+
hash = node.arguments.last
|
|
137
|
+
return nil unless hash&.hash_type?
|
|
138
|
+
|
|
139
|
+
hash.pairs.find { |pair| pair.key.sym_type? && pair.key.value == :variant }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def literal_name(node)
|
|
143
|
+
return unless node && (node.sym_type? || node.str_type?)
|
|
144
|
+
|
|
145
|
+
node.value.to_s.tr("_", "-")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def check_iconify_string(node, value)
|
|
149
|
+
return unless value.include?("iconify ")
|
|
150
|
+
return unless (match = ICONIFY_PATTERN.match(value))
|
|
151
|
+
|
|
152
|
+
iconify_prefix = match[1]
|
|
153
|
+
icon_name = match[2]
|
|
154
|
+
rest = remaining_classes(value, iconify_prefix, icon_name)
|
|
155
|
+
symbol = icon_name.tr("-", "_")
|
|
156
|
+
component = ICONIFY_PREFIX_TO_COMPONENT.fetch(iconify_prefix)
|
|
157
|
+
|
|
158
|
+
add_offense(node,
|
|
159
|
+
message: format(RAW_MSG, component:, library: iconify_prefix, symbol:, rest:)) do |corrector|
|
|
160
|
+
replacement = if rest.empty?
|
|
161
|
+
"#{component}(:#{symbol})"
|
|
162
|
+
else
|
|
163
|
+
"#{component}(:#{symbol}, class: \"#{rest}\")"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
span_node = enclosing_span_call(node)
|
|
167
|
+
corrector.replace(span_node, replacement) if span_node
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def enclosing_span_call(str_node)
|
|
172
|
+
pair = str_node.parent
|
|
173
|
+
return nil unless pair&.pair_type?
|
|
174
|
+
return nil unless pair.key.sym_type? && pair.key.value == :class
|
|
175
|
+
|
|
176
|
+
hash = pair.parent
|
|
177
|
+
return nil unless hash&.hash_type?
|
|
178
|
+
return nil if hash.pairs.size != 1
|
|
179
|
+
|
|
180
|
+
send_node = hash.parent
|
|
181
|
+
return nil unless send_node&.send_type?
|
|
182
|
+
return nil unless send_node.method_name == :span
|
|
183
|
+
return nil unless send_node.arguments.size == 1
|
|
184
|
+
|
|
185
|
+
send_node
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def remaining_classes(value, library, icon_name)
|
|
189
|
+
value.sub(/iconify\s+#{library}--#{Regexp.escape(icon_name)}\s*/, "").strip
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def build_suggestion(name, available, library_dir)
|
|
193
|
+
if library_dir == "lucide" && (renamed = KNOWN_LUCIDE_RENAMES[name]) && available.include?(renamed)
|
|
194
|
+
return { msg: "Did you mean `:#{renamed.tr('-', '_')}`? (Lucide v1 reordered prefix and suffix.)",
|
|
195
|
+
autocorrect: true,
|
|
196
|
+
name: renamed }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
matches = fuzzy_match(name, available)
|
|
200
|
+
if matches.size == 1
|
|
201
|
+
{ msg: "Did you mean `:#{matches.first.tr('-', '_')}`?", autocorrect: true, name: matches.first }
|
|
202
|
+
elsif matches.any?
|
|
203
|
+
list = matches.first(5).map { |match| "`:#{match.tr('-', '_')}`" }.join(", ")
|
|
204
|
+
{ msg: "Did you mean one of: #{list}?", autocorrect: false, name: nil }
|
|
205
|
+
else
|
|
206
|
+
{ msg: "No similar icons found.", autocorrect: false, name: nil }
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def fuzzy_match(name, available)
|
|
211
|
+
name_parts = name.split("-")
|
|
212
|
+
|
|
213
|
+
scored = available.filter_map do |candidate|
|
|
214
|
+
score = part_score(name_parts, candidate.split("-"))
|
|
215
|
+
distance = damerau_levenshtein(name, candidate)
|
|
216
|
+
score += [3 - distance, 0].max if distance <= 2
|
|
217
|
+
score += 2 if name.length >= 3 && candidate.include?(name)
|
|
218
|
+
score += 2 if name.length >= 3 && candidate.split("-").any? { |part| part.start_with?(name[0..2]) }
|
|
219
|
+
|
|
220
|
+
[candidate, score] if score >= 3
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
scored.sort_by { |_, score| -score }.map(&:first).take(5)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def part_score(name_parts, candidate_parts)
|
|
227
|
+
score = 0
|
|
228
|
+
score += 5 if name_parts.sort == candidate_parts.sort
|
|
229
|
+
score += (name_parts & candidate_parts).size * 2
|
|
230
|
+
score += 4 if name_parts.size == 2 && candidate_parts.size == 2 && name_parts.reverse == candidate_parts
|
|
231
|
+
score
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def damerau_levenshtein(left, right)
|
|
235
|
+
return right.length if left.empty?
|
|
236
|
+
return left.length if right.empty?
|
|
237
|
+
return 99 if (left.length - right.length).abs > 3
|
|
238
|
+
|
|
239
|
+
rows = Array.new(left.length + 1) { Array.new(right.length + 1, 0) }
|
|
240
|
+
(0..left.length).each { |i| rows[i][0] = i }
|
|
241
|
+
(0..right.length).each { |j| rows[0][j] = j }
|
|
242
|
+
|
|
243
|
+
(1..left.length).each do |i|
|
|
244
|
+
(1..right.length).each do |j|
|
|
245
|
+
cost = left[i - 1] == right[j - 1] ? 0 : 1
|
|
246
|
+
rows[i][j] = [
|
|
247
|
+
rows[i - 1][j] + 1,
|
|
248
|
+
rows[i][j - 1] + 1,
|
|
249
|
+
rows[i - 1][j - 1] + cost
|
|
250
|
+
].min
|
|
251
|
+
if i > 1 && j > 1 && left[i - 1] == right[j - 2] && left[i - 2] == right[j - 1]
|
|
252
|
+
rows[i][j] = [rows[i][j], rows[i - 2][j - 2] + 1].min
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
rows[left.length][right.length]
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def icons_base_path
|
|
261
|
+
@icons_base_path ||= File.expand_path(cop_config["IconsPath"] || "app/assets/svg/icons", Dir.pwd)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def available_icons(library_dir, variant)
|
|
265
|
+
self.class.available_icons_for(icons_base_path, library_dir, variant)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
class << self
|
|
269
|
+
def available_icons_for(base_path, library_dir, variant)
|
|
270
|
+
@available_icons_cache ||= {}
|
|
271
|
+
@available_icons_cache[[base_path, library_dir, variant]] ||= load_icons(base_path, library_dir, variant)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
private
|
|
275
|
+
|
|
276
|
+
def load_icons(base_path, library_dir, variant)
|
|
277
|
+
path = File.join(*[base_path, library_dir, variant].compact.reject { |part| part.to_s.empty? })
|
|
278
|
+
return [] unless Dir.exist?(path)
|
|
279
|
+
|
|
280
|
+
Dir.children(path).filter_map { |file| File.basename(file, ".svg") if file.end_with?(".svg") }.sort
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Glyphs
|
|
6
|
+
# Replaces legacy icon helper calls with Glyphs kit components.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# _lucide(:house, class: "size-4")
|
|
11
|
+
# icon("check", library: "lucide")
|
|
12
|
+
# icon("check")
|
|
13
|
+
#
|
|
14
|
+
# # good
|
|
15
|
+
# LucideIcon(:house, class: "size-4")
|
|
16
|
+
# LucideIcon("check")
|
|
17
|
+
# HeroIcon("check") # `DefaultLibraryComponent` when no library: is given
|
|
18
|
+
class LegacyIconHelper < Base
|
|
19
|
+
include RangeHelp
|
|
20
|
+
include LibraryCallHelpers
|
|
21
|
+
extend AutoCorrector
|
|
22
|
+
|
|
23
|
+
MSG = "Use `%{component}(...)` instead of `%{helper}(...)`."
|
|
24
|
+
MSG_DYNAMIC = "Use a Glyphs component (e.g. `LucideIcon(...)`) instead of `icon(...)` with a dynamic library."
|
|
25
|
+
MSG_UNKNOWN = "Use a Glyphs component instead of `icon(...)`; add a `LibraryComponents` mapping " \
|
|
26
|
+
"for library `%{library}`."
|
|
27
|
+
|
|
28
|
+
DEFAULT_MAPPINGS = {
|
|
29
|
+
_lucide: "LucideIcon",
|
|
30
|
+
_phosphor: "PhosphorIcon",
|
|
31
|
+
_hero: "HeroIcon",
|
|
32
|
+
_heroicon: "HeroIcon",
|
|
33
|
+
_tabler: "TablerIcon"
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
def on_send(node)
|
|
37
|
+
return unless node.receiver.nil?
|
|
38
|
+
|
|
39
|
+
if (component = mappings[node.method_name])
|
|
40
|
+
rename_offense(node, component)
|
|
41
|
+
elsif node.method_name == :icon && node.arguments.any?
|
|
42
|
+
correct_icon_call(node)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def rename_offense(node, component)
|
|
49
|
+
message = format(MSG, component:, helper: node.method_name)
|
|
50
|
+
add_offense(node.loc.selector, message:) do |corrector|
|
|
51
|
+
corrector.replace(node.loc.selector, component)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def correct_icon_call(node)
|
|
56
|
+
return if node.first_argument.hash_type? # no name argument; not an icon render call
|
|
57
|
+
|
|
58
|
+
pair = library_pair(node)
|
|
59
|
+
if pair.nil?
|
|
60
|
+
rename_offense(node, default_component)
|
|
61
|
+
elsif literal_pair?(pair)
|
|
62
|
+
correct_icon_with_library(node, pair)
|
|
63
|
+
else
|
|
64
|
+
add_offense(node.loc.selector, message: MSG_DYNAMIC)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def correct_icon_with_library(node, pair)
|
|
69
|
+
library = pair.value.value.to_s
|
|
70
|
+
component = library_components[library]
|
|
71
|
+
|
|
72
|
+
if component.nil?
|
|
73
|
+
add_offense(node.loc.selector, message: format(MSG_UNKNOWN, library:))
|
|
74
|
+
return
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
message = format(MSG, component:, helper: :icon)
|
|
78
|
+
add_offense(node.loc.selector, message:) do |corrector|
|
|
79
|
+
corrector.replace(node.loc.selector, component)
|
|
80
|
+
remove_library_pair(corrector, pair)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def mappings
|
|
85
|
+
@mappings ||= begin
|
|
86
|
+
configured = cop_config["Mappings"] || {}
|
|
87
|
+
configured.empty? ? DEFAULT_MAPPINGS : configured.to_h { |key, value| [key.to_sym, value.to_s] }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def default_component
|
|
92
|
+
cop_config["DefaultLibraryComponent"] || "HeroIcon"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Glyphs
|
|
6
|
+
# Shared logic for cops that rewrite icon calls: locating a literal
|
|
7
|
+
# `library:`/`from:` pair, mapping libraries to Glyphs components, and
|
|
8
|
+
# removing the pair without leaving dangling commas.
|
|
9
|
+
module LibraryCallHelpers
|
|
10
|
+
LIBRARY_KEYS = %i[library from].freeze
|
|
11
|
+
|
|
12
|
+
LIBRARY_TO_COMPONENT = {
|
|
13
|
+
"lucide" => "LucideIcon",
|
|
14
|
+
"phosphor" => "PhosphorIcon",
|
|
15
|
+
"heroicons" => "HeroIcon",
|
|
16
|
+
"tabler" => "TablerIcon",
|
|
17
|
+
"feather" => "FeatherIcon",
|
|
18
|
+
"boxicons" => "BoxIcon",
|
|
19
|
+
"flags" => "FlagIcon",
|
|
20
|
+
"hugeicons" => "HugeIcon",
|
|
21
|
+
"linear" => "LinearIcon",
|
|
22
|
+
"radix" => "RadixIcon",
|
|
23
|
+
"sidekickicons" => "SidekickIcon",
|
|
24
|
+
"weather" => "WeatherIcon",
|
|
25
|
+
"animated" => "AnimatedIcon"
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def library_pair(node)
|
|
31
|
+
hash = node.arguments.last
|
|
32
|
+
return nil unless hash&.hash_type?
|
|
33
|
+
|
|
34
|
+
hash.pairs.find { |pair| pair.key.sym_type? && LIBRARY_KEYS.include?(pair.key.value) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def literal_pair?(pair)
|
|
38
|
+
pair.value.str_type? || pair.value.sym_type?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def library_components
|
|
42
|
+
@library_components ||= LIBRARY_TO_COMPONENT.merge(cop_config["LibraryComponents"] || {})
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def remove_library_pair(corrector, pair)
|
|
46
|
+
hash = pair.parent
|
|
47
|
+
range = hash.pairs.one? ? hash.source_range : pair.source_range
|
|
48
|
+
corrector.remove(
|
|
49
|
+
range_with_surrounding_comma(range_with_surrounding_space(range, side: :left), :left)
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Glyphs
|
|
6
|
+
# Prefers library-specific components over the generic `Icon(...)` kit
|
|
7
|
+
# call with a `library:` argument.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# # bad
|
|
11
|
+
# Icon(:house, library: :lucide)
|
|
12
|
+
#
|
|
13
|
+
# # good
|
|
14
|
+
# LucideIcon(:house)
|
|
15
|
+
class PreferLibraryComponent < Base
|
|
16
|
+
include RangeHelp
|
|
17
|
+
include LibraryCallHelpers
|
|
18
|
+
extend AutoCorrector
|
|
19
|
+
|
|
20
|
+
MSG = "Use `%{component}(...)` instead of `Icon(..., library: ...)`."
|
|
21
|
+
MSG_DYNAMIC = "Prefer a library-specific Glyphs component over `Icon(...)` with a dynamic library."
|
|
22
|
+
|
|
23
|
+
def on_send(node)
|
|
24
|
+
return unless node.receiver.nil? && node.method_name == :Icon && node.arguments.any?
|
|
25
|
+
|
|
26
|
+
pair = library_pair(node)
|
|
27
|
+
return unless pair
|
|
28
|
+
|
|
29
|
+
unless literal_pair?(pair)
|
|
30
|
+
add_offense(node.loc.selector, message: MSG_DYNAMIC)
|
|
31
|
+
return
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
component = library_components[pair.value.value.to_s]
|
|
35
|
+
return unless component
|
|
36
|
+
|
|
37
|
+
message = format(MSG, component:)
|
|
38
|
+
add_offense(node.loc.selector, message:) do |corrector|
|
|
39
|
+
corrector.replace(node.loc.selector, component)
|
|
40
|
+
remove_library_pair(corrector, pair)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: glyphs
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Mikael Henriksson
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: phlex
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rails_icons
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.2'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.2'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: zeitwerk
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.6'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '2.6'
|
|
54
|
+
description: Glyphs renders SVG icons as Phlex components (LucideIcon, PhosphorIcon,
|
|
55
|
+
HeroIcon, ...) from rails_icons-synced icon sets, with configurable missing-icon
|
|
56
|
+
handling and bundled RuboCop cops that validate icon names and autocorrect legacy
|
|
57
|
+
icon helpers.
|
|
58
|
+
email: mikael@zoolutions.llc
|
|
59
|
+
executables: []
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- CHANGELOG.md
|
|
64
|
+
- LICENSE.txt
|
|
65
|
+
- README.md
|
|
66
|
+
- config/default.yml
|
|
67
|
+
- lib/glyphs.rb
|
|
68
|
+
- lib/glyphs/animated_icon.rb
|
|
69
|
+
- lib/glyphs/box_icon.rb
|
|
70
|
+
- lib/glyphs/configuration.rb
|
|
71
|
+
- lib/glyphs/feather_icon.rb
|
|
72
|
+
- lib/glyphs/flag_icon.rb
|
|
73
|
+
- lib/glyphs/hero_icon.rb
|
|
74
|
+
- lib/glyphs/huge_icon.rb
|
|
75
|
+
- lib/glyphs/icon.rb
|
|
76
|
+
- lib/glyphs/linear_icon.rb
|
|
77
|
+
- lib/glyphs/lucide_icon.rb
|
|
78
|
+
- lib/glyphs/phosphor_icon.rb
|
|
79
|
+
- lib/glyphs/radix_icon.rb
|
|
80
|
+
- lib/glyphs/rubocop.rb
|
|
81
|
+
- lib/glyphs/sidekick_icon.rb
|
|
82
|
+
- lib/glyphs/tabler_icon.rb
|
|
83
|
+
- lib/glyphs/version.rb
|
|
84
|
+
- lib/glyphs/weather_icon.rb
|
|
85
|
+
- lib/rubocop/cop/glyphs/icon_resolution.rb
|
|
86
|
+
- lib/rubocop/cop/glyphs/legacy_icon_helper.rb
|
|
87
|
+
- lib/rubocop/cop/glyphs/library_call_helpers.rb
|
|
88
|
+
- lib/rubocop/cop/glyphs/prefer_library_component.rb
|
|
89
|
+
homepage: https://github.com/mhenrixon/glyphs
|
|
90
|
+
licenses:
|
|
91
|
+
- MIT
|
|
92
|
+
metadata:
|
|
93
|
+
source_code_uri: https://github.com/mhenrixon/glyphs
|
|
94
|
+
changelog_uri: https://github.com/mhenrixon/glyphs/blob/main/CHANGELOG.md
|
|
95
|
+
bug_tracker_uri: https://github.com/mhenrixon/glyphs/issues
|
|
96
|
+
default_lint_roller_plugin: Glyphs::RuboCop::Plugin
|
|
97
|
+
rubygems_mfa_required: 'true'
|
|
98
|
+
rdoc_options: []
|
|
99
|
+
require_paths:
|
|
100
|
+
- lib
|
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
102
|
+
requirements:
|
|
103
|
+
- - ">="
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: '3.2'
|
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0'
|
|
111
|
+
requirements: []
|
|
112
|
+
rubygems_version: 4.0.9
|
|
113
|
+
specification_version: 4
|
|
114
|
+
summary: Phlex icon components for every rails_icons library
|
|
115
|
+
test_files: []
|