svg_icon 0.2.0 → 0.4.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 +4 -4
- data/Gemfile.lock +35 -2
- data/README.md +16 -4
- data/Rakefile +9 -1
- data/docs/superpowers/plans/2026-08-18-svg-icon-fetch-cli.md +557 -0
- data/docs/superpowers/specs/2026-08-18-svg-icon-fetch-design.md +100 -0
- data/exe/svg_icon +7 -0
- data/lib/data/lucide.json +6268 -0
- data/lib/svg_icon/cli.rb +68 -0
- data/lib/svg_icon/configuration.rb +4 -1
- data/lib/svg_icon/fetcher.rb +73 -0
- data/lib/svg_icon/helper.rb +23 -12
- data/lib/svg_icon/version.rb +1 -1
- data/lib/svg_icon.rb +83 -27
- data/svg_icon.gemspec +3 -1
- metadata +39 -7
data/lib/svg_icon/cli.rb
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "fetcher"
|
|
4
|
+
|
|
5
|
+
module SvgIcon
|
|
6
|
+
class CLI
|
|
7
|
+
def self.run(argv)
|
|
8
|
+
new(argv).run
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(argv)
|
|
12
|
+
@argv = argv
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run
|
|
16
|
+
command = @argv.shift
|
|
17
|
+
case command
|
|
18
|
+
when "fetch"
|
|
19
|
+
fetch
|
|
20
|
+
when nil, "help", "--help", "-h"
|
|
21
|
+
puts usage
|
|
22
|
+
else
|
|
23
|
+
warn "Unknown command: #{command}"
|
|
24
|
+
warn usage
|
|
25
|
+
exit 1
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def fetch
|
|
32
|
+
name = @argv.shift
|
|
33
|
+
unless valid_name?(name) && @argv.empty?
|
|
34
|
+
warn "Usage: svg_icon fetch <icon_set_name>"
|
|
35
|
+
exit 1
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
destination = File.join(SvgIcon.configuration.icons_path, "#{name}.json")
|
|
39
|
+
puts "Fetching #{name} from iconify/icon-sets"
|
|
40
|
+
SvgIcon::Fetcher.new.fetch(name, destination)
|
|
41
|
+
puts "Saved to #{relative_path(destination)}"
|
|
42
|
+
rescue SvgIcon::FetchError => e
|
|
43
|
+
warn e.message
|
|
44
|
+
exit 1
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
warn "Error: #{e.message}"
|
|
47
|
+
exit 1
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def valid_name?(name)
|
|
51
|
+
!name.nil? && name =~ SvgIcon::Fetcher::NAME_PATTERN
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def relative_path(path)
|
|
55
|
+
path.sub("#{Dir.pwd}/", "")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def usage
|
|
59
|
+
<<~TEXT
|
|
60
|
+
Usage: svg_icon COMMAND
|
|
61
|
+
|
|
62
|
+
Commands:
|
|
63
|
+
fetch NAME Download icon set NAME (e.g. bi) from iconify/icon-sets into #{SvgIcon.configuration.icons_path}
|
|
64
|
+
help Show this help
|
|
65
|
+
TEXT
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module SvgIcon
|
|
4
4
|
class Configuration
|
|
5
|
-
DEFAULT_ICON = "
|
|
5
|
+
DEFAULT_ICON = "lucide"
|
|
6
6
|
|
|
7
7
|
attr_accessor :icon
|
|
8
8
|
attr_accessor :default_class
|
|
9
9
|
attr_accessor :extra_icons_path
|
|
10
|
+
attr_accessor :icons_path
|
|
10
11
|
|
|
11
12
|
def initialize
|
|
12
13
|
@icon = DEFAULT_ICON
|
|
14
|
+
@icons_path = File.join(Dir.pwd, "config", "svg_icons")
|
|
13
15
|
end
|
|
14
16
|
end
|
|
15
17
|
|
|
@@ -23,5 +25,6 @@ module SvgIcon
|
|
|
23
25
|
|
|
24
26
|
def self.configure
|
|
25
27
|
yield configuration
|
|
28
|
+
SvgIcon.clear_cache!
|
|
26
29
|
end
|
|
27
30
|
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "tempfile"
|
|
7
|
+
require "uri"
|
|
8
|
+
|
|
9
|
+
module SvgIcon
|
|
10
|
+
class FetchError < Error; end
|
|
11
|
+
|
|
12
|
+
class Fetcher
|
|
13
|
+
DEFAULT_BASE_URL = "https://raw.githubusercontent.com/iconify/icon-sets/master/json"
|
|
14
|
+
NAME_PATTERN = /\A[a-z0-9\-_]+\z/
|
|
15
|
+
|
|
16
|
+
def initialize(base_url: DEFAULT_BASE_URL, http: Net::HTTP)
|
|
17
|
+
@base_url = base_url
|
|
18
|
+
@http = http
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def fetch(name, destination)
|
|
22
|
+
validate_name!(name)
|
|
23
|
+
response = fetch_response(name)
|
|
24
|
+
raise FetchError, "Failed to fetch #{name}: HTTP #{response.code}#{not_found_hint(response.code)}" unless response.code == "200"
|
|
25
|
+
|
|
26
|
+
parse(response.body, name)
|
|
27
|
+
write(response.body, destination)
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def validate_name!(name)
|
|
34
|
+
raise FetchError, "Invalid icon set name: #{name}" unless name.is_a?(String) && name =~ NAME_PATTERN
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def fetch_response(name)
|
|
38
|
+
@http.get_response(uri_for(name))
|
|
39
|
+
rescue SocketError, SystemCallError, Timeout::Error, EOFError, IOError, OpenSSL::SSL::SSLError => e
|
|
40
|
+
raise FetchError, "Failed to fetch #{name}: #{e.message}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def uri_for(name)
|
|
44
|
+
URI.join("#{@base_url}/", "#{name}.json")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def not_found_hint(code)
|
|
48
|
+
code == "404" ? " (icon set not found)" : ""
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parse(body, name)
|
|
52
|
+
data = MultiJson.load(body)
|
|
53
|
+
return if data.is_a?(Hash) && data["icons"].is_a?(Hash)
|
|
54
|
+
|
|
55
|
+
raise FetchError, "Invalid icon set '#{name}': JSON must contain an 'icons' object"
|
|
56
|
+
rescue MultiJson::ParseError => e
|
|
57
|
+
raise FetchError, "Invalid icon set '#{name}': invalid JSON (#{e.message})"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def write(body, destination)
|
|
61
|
+
dir = File.dirname(destination)
|
|
62
|
+
FileUtils.mkdir_p(dir)
|
|
63
|
+
temp = Tempfile.new([".#{File.basename(destination)}", ".tmp"], dir)
|
|
64
|
+
begin
|
|
65
|
+
temp.write(body)
|
|
66
|
+
temp.flush
|
|
67
|
+
File.rename(temp.path, destination)
|
|
68
|
+
ensure
|
|
69
|
+
temp.close!
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/svg_icon/helper.rb
CHANGED
|
@@ -1,31 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
|
|
1
5
|
module SvgIcon
|
|
2
6
|
module Helper
|
|
3
7
|
def svg_icon(name, options = {})
|
|
4
8
|
options = options.dup
|
|
5
|
-
|
|
6
9
|
icons = SvgIcon.icons_json
|
|
10
|
+
|
|
7
11
|
height = icons["height"] || 24
|
|
8
12
|
width = icons["width"] || 24
|
|
9
13
|
|
|
10
|
-
options[:viewBox]
|
|
11
|
-
options[:version] = "1.1"
|
|
14
|
+
options[:viewBox] ||= "0 0 #{width} #{height}"
|
|
12
15
|
|
|
13
16
|
default_class = SvgIcon.configuration.default_class
|
|
14
|
-
|
|
15
17
|
options[:class] = "#{default_class} #{options[:class]}".strip if default_class.present?
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
body = icons.dig("icons", name.to_s, "body")
|
|
20
|
+
|
|
21
|
+
content = if body
|
|
22
|
+
body
|
|
23
|
+
else
|
|
24
|
+
"<!-- SVG icon file not found: '#{safe_comment_text(name.to_s)}' -->"
|
|
22
25
|
end
|
|
26
|
+
|
|
27
|
+
"<svg xmlns='http://www.w3.org/2000/svg' #{icon_html_attributes(options)}>#{content}</svg>".html_safe
|
|
23
28
|
end
|
|
24
29
|
|
|
30
|
+
private
|
|
31
|
+
|
|
25
32
|
def icon_html_attributes(options)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
options.map do |attr, value|
|
|
34
|
+
%(#{CGI.escapeHTML(attr.to_s)}="#{CGI.escapeHTML(value.to_s)}")
|
|
35
|
+
end.join(" ")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def safe_comment_text(value)
|
|
39
|
+
CGI.escapeHTML(value).gsub("-", "-")
|
|
29
40
|
end
|
|
30
41
|
end
|
|
31
42
|
end
|
data/lib/svg_icon/version.rb
CHANGED
data/lib/svg_icon.rb
CHANGED
|
@@ -5,50 +5,106 @@ require_relative "svg_icon/helper"
|
|
|
5
5
|
require_relative "svg_icon/version"
|
|
6
6
|
|
|
7
7
|
require "multi_json"
|
|
8
|
+
require "active_support/core_ext/object/blank"
|
|
9
|
+
require "active_support/core_ext/string/output_safety"
|
|
8
10
|
|
|
9
11
|
module SvgIcon
|
|
10
12
|
class Error < StandardError; end
|
|
11
13
|
|
|
14
|
+
require_relative "svg_icon/fetcher"
|
|
15
|
+
require_relative "svg_icon/cli"
|
|
16
|
+
|
|
12
17
|
extend self
|
|
13
18
|
|
|
14
|
-
def
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@extra_file_data ||= File.read(extra_data_path)
|
|
19
|
+
def icons
|
|
20
|
+
@icons ||= {}
|
|
21
|
+
@icons[file_cache_key_for(resolve_icon_path)] ||= MultiJson.load(file_data)
|
|
18
22
|
end
|
|
19
23
|
|
|
20
|
-
def
|
|
21
|
-
|
|
22
|
-
@
|
|
24
|
+
def icons_json
|
|
25
|
+
@icons_json ||= {}
|
|
26
|
+
@icons_json[cache_key] ||= merge_extra_icons(icons)
|
|
23
27
|
end
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
def clear_cache!
|
|
30
|
+
@file_data = nil
|
|
31
|
+
@icons = nil
|
|
32
|
+
@icons_json = nil
|
|
33
|
+
@extra_file_data = nil
|
|
34
|
+
@extra_icons = nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
27
38
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
def icon
|
|
40
|
+
configuration.icon
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def configuration
|
|
44
|
+
SvgIcon.configuration
|
|
45
|
+
end
|
|
35
46
|
|
|
36
47
|
def file_data
|
|
37
|
-
|
|
38
|
-
@file_data ||=
|
|
48
|
+
@file_data ||= {}
|
|
49
|
+
@file_data[file_cache_key_for(resolve_icon_path)] ||= begin
|
|
50
|
+
path = resolve_icon_path
|
|
51
|
+
raise Error, "Icon data file not found: #{icon} (looked in #{configuration.icons_path} and bundled data)" unless File.exist?(path)
|
|
52
|
+
|
|
53
|
+
File.read(path)
|
|
54
|
+
end
|
|
39
55
|
end
|
|
40
56
|
|
|
41
|
-
def
|
|
42
|
-
|
|
57
|
+
def resolve_icon_path
|
|
58
|
+
external = File.join(configuration.icons_path, "#{icon}.json")
|
|
59
|
+
return external if File.exist?(external)
|
|
60
|
+
|
|
61
|
+
File.join(__dir__, "data", "#{icon}.json")
|
|
43
62
|
end
|
|
44
63
|
|
|
45
|
-
def
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
def file_cache_key_for(path)
|
|
65
|
+
return nil unless path
|
|
66
|
+
|
|
67
|
+
"#{path}:#{File.exist?(path) ? File.mtime(path).to_i : nil}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def extra_icons
|
|
71
|
+
return {} unless configuration.extra_icons_path
|
|
72
|
+
|
|
73
|
+
@extra_icons ||= {}
|
|
74
|
+
@extra_icons[file_cache_key_for(extra_path)] ||= begin
|
|
75
|
+
data = MultiJson.load(extra_file_data)
|
|
76
|
+
raise Error, "Extra icons file must contain a JSON object: #{extra_path}" unless data.is_a?(Hash)
|
|
77
|
+
|
|
78
|
+
data
|
|
52
79
|
end
|
|
53
80
|
end
|
|
81
|
+
|
|
82
|
+
def extra_path
|
|
83
|
+
configuration.extra_icons_path
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def extra_file_data
|
|
87
|
+
@extra_file_data ||= {}
|
|
88
|
+
@extra_file_data[file_cache_key_for(extra_path)] ||= begin
|
|
89
|
+
path = extra_path
|
|
90
|
+
raise Error, "Extra icons file not found: #{path}" unless File.exist?(path)
|
|
91
|
+
|
|
92
|
+
File.read(path)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def merge_extra_icons(base_icons)
|
|
97
|
+
icons_set = base_icons["icons"]
|
|
98
|
+
raise Error, "Icon data must contain an 'icons' object: #{resolve_icon_path}" unless icons_set.is_a?(Hash)
|
|
99
|
+
|
|
100
|
+
return base_icons if extra_icons.empty?
|
|
101
|
+
|
|
102
|
+
merged = base_icons.dup
|
|
103
|
+
merged["icons"] = icons_set.merge(extra_icons)
|
|
104
|
+
merged
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def cache_key
|
|
108
|
+
"#{icon}:#{file_cache_key_for(resolve_icon_path)}:#{file_cache_key_for(extra_path)}"
|
|
109
|
+
end
|
|
54
110
|
end
|
data/svg_icon.gemspec
CHANGED
|
@@ -26,6 +26,8 @@ Gem::Specification.new do |spec|
|
|
|
26
26
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
27
27
|
spec.require_paths = ["lib"]
|
|
28
28
|
|
|
29
|
-
# Uncomment to register a new dependency of your gem
|
|
30
29
|
spec.add_dependency "multi_json"
|
|
30
|
+
spec.add_dependency "activesupport", ">= 5.0"
|
|
31
|
+
|
|
32
|
+
spec.add_development_dependency "minitest"
|
|
31
33
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: svg_icon
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- doabit
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: multi_json
|
|
@@ -24,10 +23,39 @@ dependencies:
|
|
|
24
23
|
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
25
|
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: activesupport
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '5.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: minitest
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
27
54
|
description: Svg icon render helper for rails.
|
|
28
55
|
email:
|
|
29
56
|
- doinsist@gmail.com
|
|
30
|
-
executables:
|
|
57
|
+
executables:
|
|
58
|
+
- svg_icon
|
|
31
59
|
extensions: []
|
|
32
60
|
extra_rdoc_files: []
|
|
33
61
|
files:
|
|
@@ -38,11 +66,17 @@ files:
|
|
|
38
66
|
- Rakefile
|
|
39
67
|
- bin/console
|
|
40
68
|
- bin/setup
|
|
69
|
+
- docs/superpowers/plans/2026-08-18-svg-icon-fetch-cli.md
|
|
70
|
+
- docs/superpowers/specs/2026-08-18-svg-icon-fetch-design.md
|
|
71
|
+
- exe/svg_icon
|
|
41
72
|
- lib/data/bi.json
|
|
42
73
|
- lib/data/bx.json
|
|
43
74
|
- lib/data/heroicons.json
|
|
75
|
+
- lib/data/lucide.json
|
|
44
76
|
- lib/svg_icon.rb
|
|
77
|
+
- lib/svg_icon/cli.rb
|
|
45
78
|
- lib/svg_icon/configuration.rb
|
|
79
|
+
- lib/svg_icon/fetcher.rb
|
|
46
80
|
- lib/svg_icon/helper.rb
|
|
47
81
|
- lib/svg_icon/version.rb
|
|
48
82
|
- svg_icon.gemspec
|
|
@@ -52,7 +86,6 @@ metadata:
|
|
|
52
86
|
homepage_uri: https://github.com/ruby-gems/svg_icon
|
|
53
87
|
source_code_uri: https://github.com/ruby-gems/svg_icon
|
|
54
88
|
changelog_uri: https://github.com/ruby-gems/svg_icon
|
|
55
|
-
post_install_message:
|
|
56
89
|
rdoc_options: []
|
|
57
90
|
require_paths:
|
|
58
91
|
- lib
|
|
@@ -67,8 +100,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
67
100
|
- !ruby/object:Gem::Version
|
|
68
101
|
version: '0'
|
|
69
102
|
requirements: []
|
|
70
|
-
rubygems_version: 3.
|
|
71
|
-
signing_key:
|
|
103
|
+
rubygems_version: 3.6.9
|
|
72
104
|
specification_version: 4
|
|
73
105
|
summary: Svg icon render helper for rails
|
|
74
106
|
test_files: []
|