icons 0.8.1 → 0.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 258de5224f1792f19bc6024149d3b83796c215fb2dafcdb4ad832817396fa497
4
- data.tar.gz: 735659b38f49f975bfd758c7260e7ea5ac56971a9cd3942293b8f0ae2c240f3d
3
+ metadata.gz: 6a560ee22eaef4722cc54a4810c2471fa9ee24aa015e609e80ba0c876a207b23
4
+ data.tar.gz: f03071f72a369fc8eff35b9fca520ba6108576d361537b97406204b2bec58b42
5
5
  SHA512:
6
- metadata.gz: 62fa8a1399597b1e99e524f16db444af2680a42e23b5b86d938ed9855fa0859e134f2810aed56d51e398e79f366c71a9106bd12de70e0f62a26ccdbe449e1e32
7
- data.tar.gz: 233185f2705e719ec0230091aec8b2390a70e7ea74bdbe09376942388fa6db1dbf5ab6f5757afbf5fab89f5f83b0c7ce90c2a72ea732a9dfc52ac5e96db1f6cc
6
+ metadata.gz: 1d71a79a568b7a19e1eadb13f846fb571357aa3542026c80683ad623bbeeb6af1c779c8f0634bcdebe445e26d10b8639b5ea8571a15fb1c682b3089f4c3362d3
7
+ data.tar.gz: 1ffb7d8ef4233e67b9011028efe012459d3bd2562a1e08a753152523ce850a684165aedf48699433c85cdca2a0481d379d4b9215900dcc5ae85d3fe24b6d69c0
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- icons (0.8.1)
4
+ icons (0.10.0)
5
5
  nokogiri (~> 1.16, >= 1.16.4)
6
6
 
7
7
  GEM
@@ -119,7 +119,7 @@ CHECKSUMS
119
119
  date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
120
120
  debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6
121
121
  erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5
122
- icons (0.8.1)
122
+ icons (0.10.0)
123
123
  io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
124
124
  irb (1.16.0) sha256=2abe56c9ac947cdcb2f150572904ba798c1e93c890c256f8429981a7675b0806
125
125
  json (2.18.0) sha256=b10506aee4183f5cf49e0efc48073d7b75843ce3782c68dbeb763351c08fd505
data/README.md CHANGED
@@ -5,7 +5,7 @@ Add any icon library to a Ruby app. Icons has first-party support for a [dozen o
5
5
 
6
6
  ## Installation
7
7
 
8
- Add the core gem to your Gemfile:
8
+ Add the core gem to the Gemfile:
9
9
  ```ruby
10
10
  gem "icons"
11
11
  ```
@@ -34,11 +34,81 @@ Icons::Sync.new("lucide").now
34
34
  # Render an icon
35
35
  icon = Icons::Icon.new(name: "check", library: "lucide", variant: "outline", arguments: { class: "text-gray-500" })
36
36
  svg = icon.svg
37
+
38
+ # Generate SVG sprite for performance
39
+ sprite = Icons::Sprite.new(icons: ["check", "search"], library: "lucide", variant: "outline")
40
+ svg = sprite.svg
41
+ # => <svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
42
+ # <symbol id="lucide_outline_check" viewBox="0 0 24 24">...</symbol>
43
+ # <symbol id="lucide_outline_search" viewBox="0 0 24 24">...</symbol>
44
+ # </svg>
45
+
46
+ # Or configure globally
47
+ Icons.configure do |config|
48
+ config.sprite = { lucide: { outline: ["check", "search"] } }
49
+ end
50
+
51
+ sprite = Icons::Sprite.new
52
+ sprite.svg
37
53
  ```
38
54
 
39
55
  The resulting SVG will include the proper attributes and the SVG content from the library’s asset path.
40
56
 
41
57
 
58
+ ## Custom libraries
59
+
60
+ Add non first-party icon libraries. The Icons gem looks for SVGs at `<icons_path>/<library_name>/<name>.svg`, so any name works without configuration.
61
+
62
+
63
+ ### Zero config
64
+
65
+ Place SVGs at the expected path:
66
+ ```bash
67
+ app/assets/svg/icons/my_icons/check.svg
68
+ ```
69
+
70
+ Usage:
71
+ ```ruby
72
+ Icons::Icon.new(name: "check", library: "my_icons", arguments: {}).svg
73
+ ```
74
+
75
+
76
+ ### Registered custom libraries
77
+
78
+ Use `config.custom_library` to declare a custom library with defaults:
79
+ ```ruby
80
+ Icons.configure do |config|
81
+ config.custom_library :my_icons
82
+ end
83
+ ```
84
+
85
+ Then set library specific defaults:
86
+ ```ruby
87
+ Icons.configure do |config|
88
+ config.custom_library :my_icons
89
+
90
+ config.libraries.my_icons.default.css = "size-6"
91
+ config.libraries.my_icons.default.stroke_width = 1.5
92
+ end
93
+ ```
94
+
95
+ ### Custom libraries with a git source
96
+
97
+ Pass a `source:` option to make a custom library syncable from a Git repository:
98
+ ```ruby
99
+ Icons.configure do |config|
100
+ config.custom_library :my_icons, source: {
101
+ url: "https://github.com/user/icons.git",
102
+ variants: { default: "." }
103
+ }
104
+ end
105
+
106
+ Icons::Sync.new(:my_icons).now
107
+ ```
108
+
109
+ After syncing, `Icons::Icon.new(name: "check", library: "my_icons", arguments: {}).svg` , works just like any first party library.
110
+
111
+
42
112
  ## First-party libraries
43
113
 
44
114
  - [Boxicons](https://railsdesigner.com/open-source/rails-icons/boxicons/) (1600+ icons)
data/icons.gemspec CHANGED
@@ -18,5 +18,7 @@ Gem::Specification.new do |spec|
18
18
 
19
19
  spec.files = Dir["{bin,config,lib}/**/*", "Rakefile", "README.md", "icons.gemspec", "Gemfile", "Gemfile.lock"]
20
20
 
21
+ spec.required_ruby_version = ">= 3.1.0"
22
+
21
23
  spec.add_runtime_dependency "nokogiri", "~> 1.16", ">= 1.16.4"
22
24
  end
@@ -23,6 +23,7 @@ module Icons
23
23
  # Override Boxicons defaults
24
24
  # config.libraries.boxicons.default_variant = "" # Set a default variant for Boxicons
25
25
  # config.libraries.boxicons.exclude_variants = [] # Exclude specific variants
26
+ # config.libraries.boxicons.variants = [] # Only sync these variants
26
27
 
27
28
  # config.libraries.boxicons.solid.css = "size-6"
28
29
  # config.libraries.boxicons.solid.data = {}
@@ -50,7 +51,11 @@ module Icons
50
51
  {
51
52
  filenames: {
52
53
  delete_prefix: ["bxl-", "bx-", "bxs-"]
53
- }
54
+ },
55
+
56
+ svg: [
57
+ {element: "path", action: :set_attribute, attribute: "fill", value: "currentColor"}
58
+ ]
54
59
  }
55
60
  end
56
61
 
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Icons
4
+ class Configuration
5
+ class CustomLibrary
6
+ attr_reader :source
7
+
8
+ def initialize(source: nil)
9
+ @source = source
10
+ end
11
+
12
+ def config
13
+ Options.new.tap do |options|
14
+ options.default_variant = nil
15
+ options.exclude_variants = []
16
+ end
17
+ end
18
+
19
+ def initializer_config
20
+ ""
21
+ end
22
+ end
23
+ end
24
+ end
@@ -21,6 +21,7 @@ module Icons
21
21
  # Override Feather defaults
22
22
  # config.libraries.feather.default_variant = "" # Feather has no variants, this is provided for backwards compatibility
23
23
  # config.libraries.feather.exclude_variants = [] # Feather has no variants, this is provided for backwards compatibility
24
+ # config.libraries.feather.variants = [] # Feather has no variants, provided for consistency
24
25
 
25
26
  # config.libraries.feather.default.css = "size-6"
26
27
  # config.libraries.feather.default.stroke_width = "2"
@@ -22,6 +22,7 @@ module Icons
22
22
  # Override Flags defaults
23
23
  # config.libraries.flags.default_variant = "" # Set a default variant for Flags
24
24
  # config.libraries.flags.exclude_variants = [:square, :landscape]
25
+ # config.libraries.flags.variants = [] # Only sync these variants
25
26
 
26
27
  # config.libraries.flags.square.default.css = "size-6"
27
28
  # config.libraries.flags.square.default.data = {}
@@ -24,6 +24,7 @@ module Icons
24
24
  # Override Heroicon defaults
25
25
  # config.libraries.heroicons.default_variant = "" # Set a default variant for Heroicons
26
26
  # config.libraries.heroicons.exclude_variants = [:mini, :micro] # Exclude specific variants
27
+ # config.libraries.heroicons.variants = [] # Only sync these variants
27
28
 
28
29
  # config.libraries.heroicons.outline.default.css = "size-6"
29
30
  # config.libraries.heroicons.outline.default.stroke_width = "1.5"
@@ -21,6 +21,7 @@ module Icons
21
21
  # Override Hugeicons defaults
22
22
  # config.libraries.hugeicons.default_variant = "" # Hugeicons has no variants, this is provided for backwards compatibility
23
23
  # config.libraries.hugeicons.exclude_variants = [] # Hugeicons has no variants, this is provided for backwards compatibility
24
+ # config.libraries.hugeicons.variants = [] # Hugeicons has no variants, provided for consistency
24
25
 
25
26
  # config.libraries.hugeicons.default.css = "size-6"
26
27
  # config.libraries.hugeicons.default.stroke_width = "1.5"
@@ -21,6 +21,7 @@ module Icons
21
21
  # Override Linear defaults
22
22
  # config.libraries.linear.default_variant = "" # Set a default variant for Linear
23
23
  # config.libraries.linear.exclude_variants = []
24
+ # config.libraries.linear.variants = [] # Linear has no variants, provided for consistency
24
25
 
25
26
  # config.libraries.linear.default.css = "size-6"
26
27
  # config.libraries.linear.default.stroke_width = "2"
@@ -21,6 +21,7 @@ module Icons
21
21
  # Override Lucide defaults
22
22
  # config.libraries.lucide.default_variant = "" # Set a default variant for Lucide
23
23
  # config.libraries.lucide.exclude_variants = [] # Exclude specific variants
24
+ # config.libraries.lucide.variants = [] # Only sync these variants
24
25
 
25
26
  # config.libraries.lucide.outline.default.css = "size-6"
26
27
  # config.libraries.lucide.outline.default.stroke_width = "1.5"
@@ -26,6 +26,7 @@ module Icons
26
26
  # Override Phosphor defaults
27
27
  # config.libraries.phosphor.default_variant = "" # Set a default variant for Phosphor
28
28
  # config.libraries.phosphor.exclude_variants = [:duotone, :thin] # Exclude specific variants
29
+ # config.libraries.phosphor.variants = [] # Only sync these variants
29
30
 
30
31
  # config.libraries.phosphor.bold.default.css = "size-6"
31
32
  # config.libraries.phosphor.bold.default.data = {}
@@ -21,6 +21,8 @@ module Icons
21
21
  # Override Radix defaults
22
22
  # config.libraries.radix.default_variant = "" # Radix has no variants, this is provided for backwards compatibility
23
23
  # config.libraries.radix.exclude_variants = [] # Radix has no variants, this is provided for backwards compatibility
24
+ # config.libraries.radix.variants = [] # Radix has no variants, provided for consistency
25
+
24
26
  # config.libraries.radix.default.css = "size-6"
25
27
  # config.libraries.radix.default.stroke_width = "2"
26
28
  # config.libraries.radix.default.data = {}
@@ -24,6 +24,7 @@ module Icons
24
24
  # Override Sidekickicons defaults
25
25
  # config.libraries.sidekickicons.default_variant = "" # Set a default variant for Sidekickicons
26
26
  # config.libraries.sidekickicons.exclude_variants = [:mini, :micro] # Exclude specific variants
27
+ # config.libraries.sidekickicons.variants = [] # Only sync these variants
27
28
 
28
29
  # config.libraries.sidekickicons.outline.default.css = "size-6"
29
30
  # config.libraries.sidekickicons.outline.default.stroke_width = "1.5"
@@ -22,6 +22,7 @@ module Icons
22
22
  # Override Tabler defaults
23
23
  # config.libraries.tabler.default_variant = "" # Set a default variant for Tabler
24
24
  # config.libraries.tabler.exclude_variants = [] # Exclude specific variants
25
+ # config.libraries.tabler.variants = [] # Only sync these variants
25
26
 
26
27
  # config.libraries.tabler.regular.default.css = "size-6"
27
28
  # config.libraries.tabler.solid.default.css = "size-6"
@@ -21,6 +21,7 @@ module Icons
21
21
  # Override Weather defaults
22
22
  # config.libraries.weather.default_variant = "" # Set a default variant for Wweather
23
23
  # config.libraries.weather.exclude_variants = []
24
+ # config.libraries.weather.variants = [] # Only sync these variants
24
25
 
25
26
  # config.libraries.weather.default.css = "size-6"
26
27
  # config.libraries.weather.default.data = {}
@@ -5,7 +5,10 @@ require "icons/configuration/options"
5
5
 
6
6
  module Icons
7
7
  class Configuration
8
- attr_accessor :default_library, :icons_path, :default_variant
8
+ # @return [String, nil]
9
+ attr_accessor :default_library, :icons_path, :default_variant, :sprite, :default_sprite_location, :validate_sprite_icons
10
+
11
+ # @return [Options]
9
12
  attr_reader :libraries
10
13
 
11
14
  def initialize
@@ -15,32 +18,70 @@ module Icons
15
18
  set_libraries_config
16
19
  end
17
20
 
21
+ # @deprecated Use {#icons_path} instead
22
+ # @return [String]
23
+ #
18
24
  def destination_path
19
25
  warn "[DEPRECATION] `destination_path` is deprecated. Use `icons_path` instead."
20
26
 
21
27
  @icons_path
22
28
  end
23
29
 
30
+ # @deprecated Use {#icons_path=} instead
31
+ #
24
32
  def destination_path=(value)
25
33
  warn "[DEPRECATION] `destination_path=` is deprecated. Use `icons_path=` instead."
26
34
 
27
35
  @icons_path = value
28
36
  end
29
37
 
38
+ # @return [Pathname]
39
+ #
30
40
  def base_path
31
41
  @base_path ||= Pathname.new(Dir.pwd)
32
42
  end
33
43
 
44
+ # @param value [Pathname, String]
45
+ #
34
46
  def base_path=(value)
35
47
  @base_path = value.is_a?(Pathname) ? value : Pathname.new(value)
36
48
  end
37
49
 
50
+ # Register a custom library at configuration time.
51
+ #
52
+ # @param name [Symbol] The library name used in `icon "name", library: :your_name`
53
+ # @param source [Hash, nil] Optional git source hash with `:url` and `:variants`.
54
+ # When provided, the library becomes syncable via `Icons::Sync.new(:your_name).now`.
55
+ #
56
+ # @example Add a local custom library
57
+ # Icons.configure do |config|
58
+ # config.custom_library :my_icons
59
+ # end
60
+ #
61
+ # @example Add a custom library with a git source
62
+ # Icons.configure do |config|
63
+ # config.custom_library :my_icons, source: { url: "https://github.com/user/icons.git", variants: { default: "." } }
64
+ # end
65
+ #
66
+ def custom_library(name, source: nil)
67
+ name = name.to_sym
68
+
69
+ library_config = CustomLibrary.new(source: source)
70
+
71
+ Icons.register_library(name, library_config)
72
+
73
+ @libraries[name] = library_config.config
74
+ end
75
+
38
76
  private
39
77
 
40
78
  def set_default_config
41
79
  @default_library = nil
42
80
  @default_variant = nil
43
81
  @icons_path = "app/assets/svg/icons"
82
+ @sprite = {}
83
+ @default_sprite_location = nil
84
+ @validate_sprite_icons = false
44
85
  end
45
86
 
46
87
  def set_libraries_config
data/lib/icons/errors.rb CHANGED
@@ -4,7 +4,7 @@ module Icons
4
4
  class IconNotFound < StandardError
5
5
  def initialize(icon_name = nil)
6
6
  if icon_name
7
- super("The icon `#{icon_name}` is not available. Please check the icon name and try again.")
7
+ super("The icon `#{icon_name}` is not available. Check the icon name and try again.")
8
8
  else
9
9
  super("Icon not found")
10
10
  end
@@ -12,12 +12,12 @@ module Icons
12
12
  end
13
13
 
14
14
  class LibraryNotFound < StandardError
15
- def initialize(library_name)
16
- if library_name.empty?
17
- libraries = Icons.libraries.keys.join(", ")
18
- super("No libraries were specified. Please choose from: #{libraries}")
15
+ def initialize(library_name = nil)
16
+ if library_name
17
+ super("The library `#{library_name}` is not available. Check the library name and try again.")
19
18
  else
20
- super("The library `#{library_name}` is not available. Please check the library name and try again.")
19
+ libraries = Icons.libraries.keys.join(", ")
20
+ super("No libraries were specified. Choose from: #{libraries}")
21
21
  end
22
22
  end
23
23
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Icons
4
+ class Icon
5
+ module Configurable
6
+ private
7
+
8
+ def set_variant
9
+ value = @config.libraries.dig(@library, :default_variant) || @config.default_variant
10
+
11
+ value.to_s.empty? ? nil : value
12
+ end
13
+
14
+ def error_message
15
+ attributes = [
16
+ @library,
17
+ @variant,
18
+ @name
19
+ ].compact
20
+
21
+ "Icon not found: `#{attributes.join(" / ")}`"
22
+ end
23
+
24
+ def attach_attributes(to:)
25
+ Icons::Icon::Attributes
26
+ .new(default_attributes: default_attributes, arguments: @arguments)
27
+ .attach(to: to)
28
+ end
29
+
30
+ def default_attributes
31
+ {
32
+ "stroke-width": default(:stroke_width),
33
+ data: default(:data),
34
+ class: default(:css)
35
+ }.compact
36
+ end
37
+
38
+ def default(key)
39
+ library_attributes.dig(:default, key)
40
+ end
41
+
42
+ def library_attributes
43
+ keys = [@library, @variant].compact
44
+
45
+ @config.libraries.dig(*keys) || {}
46
+ end
47
+ end
48
+ end
49
+ end
data/lib/icons/icon.rb CHANGED
@@ -4,8 +4,16 @@ require "nokogiri"
4
4
 
5
5
  require "icons/icon/file_path"
6
6
  require "icons/icon/attributes"
7
+ require "icons/icon/configurable"
7
8
 
8
9
  class Icons::Icon
10
+ include Icons::Icon::Configurable
11
+
12
+ # @param name [String] The icon name
13
+ # @param library [String, Symbol] The icon library
14
+ # @param variant [String, Symbol, nil] The icon variant (optional)
15
+ # @param arguments [Hash] Additional attributes including class, data, stroke_width, etc.
16
+ #
9
17
  def initialize(name:, library:, arguments:, variant: nil)
10
18
  @config = Icons.configuration
11
19
 
@@ -15,6 +23,12 @@ class Icons::Icon
15
23
  @arguments = arguments
16
24
  end
17
25
 
26
+ # Returns the rendered SVG markup for the icon
27
+ #
28
+ # @return [String] The SVG markup as an HTML string
29
+ #
30
+ # @raise [Icons::IconNotFound] If the icon file does not exist
31
+ #
18
32
  def svg
19
33
  Nokogiri::HTML::DocumentFragment.parse(File.read(file_path))
20
34
  .at_css("svg")
@@ -26,48 +40,7 @@ class Icons::Icon
26
40
 
27
41
  private
28
42
 
29
- def set_variant
30
- value = @config.libraries.dig(@library, :default_variant) ||
31
- @config.default_variant
32
-
33
- value.to_s.empty? ? nil : value
34
- end
35
-
36
- def error_message
37
- attributes = [
38
- @library,
39
- @variant,
40
- @name
41
- ].compact
42
-
43
- "Icon not found: `#{attributes.join(" / ")}`"
44
- end
45
-
46
43
  def file_path
47
44
  Icons::Icon::FilePath.new(name: @name, library: @library, variant: @variant).call
48
45
  end
49
-
50
- def attach_attributes(to:)
51
- Icons::Icon::Attributes
52
- .new(default_attributes: default_attributes, arguments: @arguments)
53
- .attach(to: to)
54
- end
55
-
56
- def default_attributes
57
- {
58
- "stroke-width": default(:stroke_width),
59
- data: default(:data),
60
- class: default(:css)
61
- }
62
- end
63
-
64
- def default(key)
65
- library_attributes.dig(:default, key)
66
- end
67
-
68
- def library_attributes
69
- keys = [@library, @variant].compact
70
-
71
- @config.libraries.dig(*keys) || {}
72
- end
73
46
  end
@@ -15,20 +15,40 @@ require "icons/configuration/weather"
15
15
  module Icons
16
16
  extend self
17
17
 
18
+ BUILT_IN_LIBRARIES = {
19
+ boxicons: Icons::Configuration::Boxicons,
20
+ feather: Icons::Configuration::Feather,
21
+ flags: Icons::Configuration::Flags,
22
+ heroicons: Icons::Configuration::Heroicons,
23
+ hugeicons: Icons::Configuration::Hugeicons,
24
+ linear: Icons::Configuration::Linear,
25
+ lucide: Icons::Configuration::Lucide,
26
+ phosphor: Icons::Configuration::Phosphor,
27
+ radix: Icons::Configuration::Radix,
28
+ sidekickicons: Icons::Configuration::Sidekickicons,
29
+ tabler: Icons::Configuration::Tabler,
30
+ weather: Icons::Configuration::Weather
31
+ }.freeze
32
+
33
+ # @return [Hash{Symbol => Class}] A map of library names to their configuration classes
34
+ #
18
35
  def libraries
19
- {
20
- boxicons: Icons::Configuration::Boxicons,
21
- feather: Icons::Configuration::Feather,
22
- flags: Icons::Configuration::Flags,
23
- heroicons: Icons::Configuration::Heroicons,
24
- hugeicons: Icons::Configuration::Hugeicons,
25
- linear: Icons::Configuration::Linear,
26
- lucide: Icons::Configuration::Lucide,
27
- phosphor: Icons::Configuration::Phosphor,
28
- radix: Icons::Configuration::Radix,
29
- sidekickicons: Icons::Configuration::Sidekickicons,
30
- tabler: Icons::Configuration::Tabler,
31
- weather: Icons::Configuration::Weather
32
- }
36
+ BUILT_IN_LIBRARIES.merge(@registered_libraries || {})
37
+ end
38
+
39
+ # Register a custom library for use with the icons gem.
40
+ #
41
+ # @param name [Symbol] The library name
42
+ # @param library_module [Module, Object] A configuration module or object
43
+ # that responds to `.config` and optionally `.source` and `.custom_path`
44
+ #
45
+ def register_library(name, library_module)
46
+ @registered_libraries ||= {}
47
+
48
+ @registered_libraries[name.to_sym] = library_module
49
+ end
50
+
51
+ def reset_registered_libraries
52
+ @registered_libraries = {}
33
53
  end
34
54
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Icons
4
+ class Sprite
5
+ Reference = Data.define(:name, :library, :variant) do
6
+ def id = [library, variant, name].join("_")
7
+
8
+ def file_path
9
+ Icons::Icon::FilePath.new(
10
+ name: name.to_s,
11
+ library: library.to_s,
12
+ variant: variant.to_s
13
+ ).call
14
+ end
15
+
16
+ def exists?
17
+ File.exist?(file_path)
18
+ rescue Icons::IconNotFound
19
+ false
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Icons
4
+ class Sprite
5
+ # @param config [Configuration] The configuration object (defaults to Icons.configuration)
6
+ # @param icons [Array<String>, nil] Optional list of icon names to include (defaults to configured icons)
7
+ # @param library [String, Symbol, nil] The icon library to use when icons are provided
8
+ # @param variant [String, Symbol, nil] The icon variant to use when icons are provided
9
+ #
10
+ def initialize(config: Icons.configuration, icons: nil, library: nil, variant: nil)
11
+ @config = config
12
+ @icons = icons
13
+ @library = library
14
+ @variant = variant
15
+ end
16
+
17
+ # Returns the combined SVG sprite markup containing all icon symbols
18
+ #
19
+ # @return [String] The SVG markup with `<symbol>` elements wrapped in a hidden `<svg>`
20
+ #
21
+ def svg
22
+ symbols = references.filter_map { |reference| symbol_from(reference) }
23
+
24
+ <<~SVG
25
+ <svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
26
+ #{symbols.join("\n ")}
27
+ </svg>
28
+ SVG
29
+ end
30
+
31
+ private
32
+
33
+ def references
34
+ @icons ? override_references : configured_references
35
+ end
36
+
37
+ def override_references
38
+ library = @library || @config.default_library
39
+ variant = @variant || @config.default_variant
40
+
41
+ @icons.map { |name| Sprite::Reference.new(name: name, library: library, variant: variant) }
42
+ end
43
+
44
+ def configured_references
45
+ sprite_config = @config.sprite || {}
46
+
47
+ sprite_config.flat_map do |library, variants|
48
+ variants.flat_map do |variant, names|
49
+ names.map { |name| Sprite::Reference.new(name: name, library: library, variant: variant) }
50
+ end
51
+ end
52
+ end
53
+
54
+ def symbol_from(reference)
55
+ return unless reference.exists?
56
+
57
+ svg_element = Nokogiri::XML(File.read(reference.file_path)).at_css("svg")
58
+ view_box = svg_element["viewBox"] || "0 0 24 24"
59
+ content = svg_element.children.map(&:to_s).join
60
+
61
+ %(<symbol id="#{reference.id}" viewBox="#{view_box}">#{content}</symbol>)
62
+ rescue Icons::IconNotFound
63
+ warn "Icon not found: #{reference.name} from #{reference.library}/#{reference.variant}"
64
+
65
+ nil
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ require "icons/icon/attributes"
6
+ require "icons/icon/configurable"
7
+
8
+ class Icons::SpriteIcon
9
+ include Icons::Icon::Configurable
10
+
11
+ # @param name [String] The icon name
12
+ # @param library [String, Symbol] The icon library
13
+ # @param variant [String, Symbol, nil] The icon variant (optional)
14
+ # @param arguments [Hash] Additional attributes including class, data, stroke_width, etc.
15
+ # @param sprite_location [String, nil] Override URL for the sprite file (optional)
16
+ # @param config [Configuration] The configuration object (defaults to Icons.configuration)
17
+ #
18
+ def initialize(name:, library:, arguments:, variant: nil, sprite_location: nil, config: Icons.configuration)
19
+ @config = config
20
+
21
+ @name = name
22
+ @library = library.to_sym
23
+ @variant = (variant || set_variant)&.to_sym
24
+ @arguments = arguments
25
+ @sprite_location = sprite_location || @config.default_sprite_location
26
+ end
27
+
28
+ # Returns the SVG markup referencing the icon from a sprite sheet
29
+ #
30
+ # @return [String] The SVG markup using a `<use>` tag referencing the sprite symbol
31
+ #
32
+ # @raise [Icons::IconNotFound] If validate_sprite_icons is enabled and the icon does not exist
33
+ #
34
+ def svg
35
+ if @config.validate_sprite_icons
36
+ raise Icons::IconNotFound, error_message unless reference.exists?
37
+ end
38
+
39
+ sprite_svg
40
+ end
41
+
42
+ private
43
+
44
+ def reference
45
+ @reference ||= Icons::Sprite::Reference.new(name: @name, library: @library, variant: @variant)
46
+ end
47
+
48
+ def sprite_svg
49
+ sprite_href = @sprite_location.nil? ? "##{reference.id}" : "#{@sprite_location}##{reference.id}"
50
+
51
+ svg_content = <<~SVG
52
+ <svg><use href="#{sprite_href}"></use></svg>
53
+ SVG
54
+
55
+ Nokogiri::HTML::DocumentFragment.parse(svg_content)
56
+ .at_css("svg")
57
+ .tap { |svg| attach_attributes(to: svg) }
58
+ .to_html
59
+ end
60
+ end
@@ -6,17 +6,19 @@ require "icons/sync/transformations"
6
6
  module Icons
7
7
  class Sync
8
8
  class ProcessVariants
9
- def initialize(temp_directory, name, library)
10
- @temp_directory, @name, @library = temp_directory, name, library
9
+ def initialize(temp_directory, name, library, variants: nil)
10
+ @temp_directory = temp_directory
11
+ @name = name
12
+ @library = library
13
+ @variants = variants
11
14
  end
12
15
 
13
16
  def process
14
17
  original_variants = Dir.children(@temp_directory)
15
18
  excluded_variants = Icons.configuration.libraries.dig(@name.to_sym)&.exclude_variants || []
19
+ effective_variants = (@variants || @library[:variants].keys) - excluded_variants
16
20
 
17
- @library[:variants].each do |variant_name, variant_source_path|
18
- next if excluded_variants.include?(variant_name)
19
-
21
+ @library[:variants].slice(*effective_variants).each do |variant_name, variant_source_path|
20
22
  source = File.join(@temp_directory, variant_source_path)
21
23
  destination = File.join(@temp_directory, variant_name.to_s)
22
24
 
@@ -25,12 +27,11 @@ module Icons
25
27
  raise "[Icons] Failed to find the icons directory: '#{source}'" unless Dir.exist?(source)
26
28
 
27
29
  move_icons(source, destination)
28
-
29
30
  apply_transformations_to(destination)
30
31
  end
31
32
 
32
33
  remove_files_and_folders_for(original_variants)
33
- remove_previously_downloaded(excluded_variants)
34
+ remove_previously_downloaded(@library[:variants].keys - effective_variants)
34
35
 
35
36
  puts "[Icons] Icon variants processed successfully"
36
37
  end
@@ -47,10 +48,13 @@ module Icons
47
48
 
48
49
  def apply_transformations_to(destination)
49
50
  Dir.each_child(destination) do |filename|
50
- File.rename(
51
- File.join(destination, filename),
52
- File.join(destination, Sync::Transformations.transform(filename, transformations.fetch(:filenames, {})))
53
- )
51
+ original_file_path = File.join(destination, filename)
52
+ transformed_filename = Sync::Transformations.transform(filename, transformations)
53
+ transformed_file_path = File.join(destination, transformed_filename)
54
+
55
+ File.rename(original_file_path, transformed_file_path)
56
+
57
+ transform_svg(transformed_file_path)
54
58
  end
55
59
  end
56
60
 
@@ -75,6 +79,14 @@ module Icons
75
79
  {}
76
80
  end
77
81
  end
82
+
83
+ def transform_svg(file_path)
84
+ return if File.extname(file_path) != ".svg"
85
+
86
+ svg_transformations = transformations.fetch(:svg, [])
87
+
88
+ Sync::Transformations.transform_svg(file_path, svg_transformations)
89
+ end
78
90
  end
79
91
  end
80
92
  end
@@ -1,21 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "nokogiri"
4
+
3
5
  module Icons
4
6
  class Sync
5
7
  class Transformations
6
8
  def self.transform(filename, rules = {})
7
9
  basename = File.basename(filename, File.extname(filename))
8
10
 
9
- transformed = rules.reduce(basename) do |fn, (type, value)|
10
- TRANSFORMERS.fetch(type).call(fn, value)
11
+ transformed = rules.fetch(:filenames, {}).reduce(basename) do |fn, (type, value)|
12
+ FILENAME_TRANSFORMERS.fetch(type).call(fn, value)
11
13
  end
12
14
 
13
15
  [transformed, File.extname(filename)].join
14
16
  end
15
17
 
18
+ def self.transform_svg(file_path, rules = [])
19
+ return unless rules.any?
20
+
21
+ svg_document = Nokogiri::HTML::DocumentFragment.parse(File.read(file_path))
22
+
23
+ rules.each do |rule|
24
+ SVG_TRANSFORMERS.fetch(rule[:action]).call(
25
+ svg_document,
26
+ rule[:element],
27
+ rule[:attribute],
28
+ rule[:value]
29
+ )
30
+ end
31
+
32
+ File.write(file_path, svg_document.to_html)
33
+ end
34
+
16
35
  private
17
36
 
18
- TRANSFORMERS = {
37
+ FILENAME_TRANSFORMERS = {
19
38
  delete_prefix: ->(filename, prefixes) {
20
39
  Array(prefixes).reduce(filename) { |fn, prefix| fn.delete_prefix(prefix) }
21
40
  },
@@ -24,6 +43,14 @@ module Icons
24
43
  Array(suffixes).reduce(filename) { |fn, suffix| fn.delete_suffix(suffix) }
25
44
  }
26
45
  }
46
+
47
+ SVG_TRANSFORMERS = {
48
+ set_attribute: ->(document, element_selector, attribute_name, attribute_value) {
49
+ document.css(element_selector).each do |element|
50
+ element[attribute_name] = attribute_value
51
+ end
52
+ }
53
+ }
27
54
  end
28
55
  end
29
56
  end
data/lib/icons/sync.rb CHANGED
@@ -5,12 +5,13 @@ require "icons/sync/process_variants"
5
5
 
6
6
  module Icons
7
7
  class Sync
8
- def initialize(name)
9
- raise "[Icons] Not a valid library" if Icons.libraries.keys.exclude?(name.to_sym)
8
+ def initialize(name, variants: nil)
9
+ raise "[Icons] Not a valid library" unless Icons.libraries.key?(name.to_sym)
10
10
 
11
11
  @name = name
12
12
  @library = Icons.libraries.fetch(name.to_sym).source
13
- @temp_directory = File.join(temp_directory_root, name)
13
+ @variants = variants
14
+ @temp_directory = File.join(temp_directory_root, name.to_s)
14
15
  end
15
16
 
16
17
  def now
@@ -36,13 +37,30 @@ module Icons
36
37
  end
37
38
 
38
39
  def clone_repository
39
- raise "[Icons] Failed to clone repository" unless system("git clone '#{@library[:url]}' '#{@temp_directory}'")
40
+ unless clone_repository_sparse
41
+ puts "[Icons] Sparse clone failed, falling back to full clone"
42
+ FileUtils.rm_rf(@temp_directory)
43
+
44
+ raise "[Icons] Failed to clone repository" unless system("git clone '#{@library[:url]}' '#{@temp_directory}'")
45
+ end
40
46
 
41
47
  puts "[Icons] '#{@name}' repository cloned"
42
48
  end
43
49
 
50
+ def clone_repository_sparse
51
+ system("git clone --depth 1 --filter=blob:none --sparse '#{@library[:url]}' '#{@temp_directory}'") &&
52
+ system("git -C '#{@temp_directory}' sparse-checkout set #{sparse_checkout_paths}")
53
+ end
54
+
55
+ def sparse_checkout_paths
56
+ paths = @library[:variants]
57
+ paths = paths.slice(*@variants) if @variants
58
+
59
+ paths.values.map { |path| "'#{path}'" }.join(" ")
60
+ end
61
+
44
62
  def process_variants
45
- Sync::ProcessVariants.new(@temp_directory, @name, @library).process
63
+ Sync::ProcessVariants.new(@temp_directory, @name, @library, variants: @variants).process
46
64
  end
47
65
 
48
66
  def remove_non_svg_files
data/lib/icons/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Icons
2
- VERSION = "0.8.1"
2
+ VERSION = "0.10.0"
3
3
  end
data/lib/icons.rb CHANGED
@@ -4,18 +4,27 @@ require "icons/version"
4
4
  require "icons/errors"
5
5
  require "icons/libraries"
6
6
  require "icons/configuration"
7
+ require "icons/configuration/custom_library"
7
8
  require "icons/icon"
9
+ require "icons/sprite/reference"
10
+ require "icons/sprite"
8
11
  require "icons/sync"
9
12
 
10
13
  module Icons
11
14
  class << self
15
+ # @return [Configuration]
12
16
  attr_accessor :configuration
13
17
 
18
+ # @yield [config] Yields the configuration object for customization
19
+ # @yieldparam config [Configuration]
20
+ #
14
21
  def configure
15
22
  self.configuration ||= Configuration.new
16
23
  yield(configuration) if block_given?
17
24
  end
18
25
 
26
+ # @return [Configuration]
27
+ #
19
28
  def config
20
29
  configuration || configure {}
21
30
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: icons
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rails Designer
@@ -52,6 +52,7 @@ files:
52
52
  - lib/icons/configuration.rb
53
53
  - lib/icons/configuration/animated.rb
54
54
  - lib/icons/configuration/boxicons.rb
55
+ - lib/icons/configuration/custom_library.rb
55
56
  - lib/icons/configuration/feather.rb
56
57
  - lib/icons/configuration/flags.rb
57
58
  - lib/icons/configuration/heroicons.rb
@@ -67,8 +68,12 @@ files:
67
68
  - lib/icons/errors.rb
68
69
  - lib/icons/icon.rb
69
70
  - lib/icons/icon/attributes.rb
71
+ - lib/icons/icon/configurable.rb
70
72
  - lib/icons/icon/file_path.rb
71
73
  - lib/icons/libraries.rb
74
+ - lib/icons/sprite.rb
75
+ - lib/icons/sprite/reference.rb
76
+ - lib/icons/sprite_icon.rb
72
77
  - lib/icons/sync.rb
73
78
  - lib/icons/sync/process_variants.rb
74
79
  - lib/icons/sync/transformations.rb
@@ -86,14 +91,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
86
91
  requirements:
87
92
  - - ">="
88
93
  - !ruby/object:Gem::Version
89
- version: '0'
94
+ version: 3.1.0
90
95
  required_rubygems_version: !ruby/object:Gem::Requirement
91
96
  requirements:
92
97
  - - ">="
93
98
  - !ruby/object:Gem::Version
94
99
  version: '0'
95
100
  requirements: []
96
- rubygems_version: 4.0.8
101
+ rubygems_version: 4.0.22
97
102
  specification_version: 4
98
103
  summary: Add any icon library to a Ruby app
99
104
  test_files: []