anyicon 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +44 -2
- data/app/helpers/anyicon/rails/icon_helper.rb +1 -1
- data/lib/anyicon/collections.rb +11 -6
- data/lib/anyicon/common.rb +15 -3
- data/lib/anyicon/configuration.rb +4 -0
- data/lib/anyicon/icon.rb +61 -13
- data/lib/anyicon/version.rb +1 -1
- data/lib/generators/anyicon/install/templates/anyicon.rb +7 -0
- metadata +17 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf910787f8f2afc33957a422c85c68bb6453522921dd9d1bbe8af16abea8370a
|
|
4
|
+
data.tar.gz: 4b3a381baec2cfd44707c1ac7bb37d2ccb5b23fa607d05695c4e827582559320
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 60e1cb0856388ebf05ddd3cb4bd037445fc0e8191355079a8ee62b79f09f42e55b3be238484248af4a1578551b3fdc1e84e01a46c37e8481b84d325c9883ef72
|
|
7
|
+
data.tar.gz: '07293138a74f9cea182743a7700cd9cb8f3af4bd3e6dfd8fc2781b522ef1cc42c2f4d8fcb8a2cdd42cd583af9d48f81806271060b8f20b09b1441c97d0bb423e'
|
data/README.md
CHANGED
|
@@ -66,6 +66,33 @@ Anyicon.configure do |config|
|
|
|
66
66
|
end
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
Or add new collections while keeping the defaults:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
Anyicon.configure do |config|
|
|
73
|
+
config.add_collections(
|
|
74
|
+
my_custom_collection: { repo: 'user/repo', path: 'path/to/icons', branch: 'main' }
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### GitHub Token
|
|
80
|
+
|
|
81
|
+
Anyicon fetches icons from GitHub. Without authentication, the GitHub API limits requests to **60 per hour**. If you use `Anyicon::Collection` to download entire collections, or your app fetches many icons on first deploy, you may hit this limit.
|
|
82
|
+
|
|
83
|
+
Setting a GitHub personal access token raises the limit to **5,000 requests per hour**. The token is also used when downloading individual icons.
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
# config/initializers/anyicon.rb
|
|
87
|
+
Anyicon.configure do |config|
|
|
88
|
+
config.github_token = ENV["GITHUB_TOKEN"]
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
To create a token, go to [GitHub Settings > Developer settings > Personal access tokens > Fine-grained tokens](https://github.com/settings/tokens?type=beta) and generate a token. No special permissions/scopes are needed — public repository read access is sufficient.
|
|
93
|
+
|
|
94
|
+
> **Note:** This is optional. If you only use a few icons and they are already cached locally in `app/assets/images/icons/`, no API calls are made and no token is needed.
|
|
95
|
+
|
|
69
96
|
## Collections Available
|
|
70
97
|
|
|
71
98
|
| Collection | Github List | Example | Quantity | License |
|
|
@@ -90,13 +117,28 @@ Please, read the license before using any of these collections. This gem does no
|
|
|
90
117
|
|
|
91
118
|
Fell free to add your own collection to this list.
|
|
92
119
|
|
|
120
|
+
## Demo
|
|
121
|
+
|
|
122
|
+
To see Anyicon in action with an interactive demo page:
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
git clone https://github.com/arthurmolina/anyicon.git
|
|
126
|
+
cd anyicon
|
|
127
|
+
bundle install
|
|
128
|
+
RAILS_ENV=development bundle exec rackup -p 3000 test/dummy/config.ru
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Then open [http://localhost:3000/demo](http://localhost:3000/demo).
|
|
132
|
+
|
|
133
|
+
The demo showcases basic usage, sizing, colors, HTML/data/aria attributes, inline text components, and CSS animations.
|
|
134
|
+
|
|
93
135
|
## Development
|
|
94
136
|
|
|
95
137
|
To get started with development:
|
|
96
138
|
|
|
97
|
-
```
|
|
139
|
+
```sh
|
|
98
140
|
git clone https://github.com/arthurmolina/anyicon.git
|
|
99
|
-
cd
|
|
141
|
+
cd anyicon
|
|
100
142
|
bundle install
|
|
101
143
|
bundle exec rake test
|
|
102
144
|
```
|
|
@@ -9,7 +9,7 @@ module Anyicon
|
|
|
9
9
|
# @param props [Hash] additional properties to apply to the SVG element
|
|
10
10
|
# @return [String] the rendered SVG icon
|
|
11
11
|
def anyicon(icon = nil, **props)
|
|
12
|
-
Anyicon::Icon.render(icon
|
|
12
|
+
Anyicon::Icon.render(icon, **props)
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
end
|
data/lib/anyicon/collections.rb
CHANGED
|
@@ -31,7 +31,8 @@ module Anyicon
|
|
|
31
31
|
# @return [Array<Hash>] a list of icons with their metadata
|
|
32
32
|
def list
|
|
33
33
|
response = fetch(collection_url)
|
|
34
|
-
JSON.parse(response&.body || "
|
|
34
|
+
result = JSON.parse(response&.body || "[]")
|
|
35
|
+
result.is_a?(Array) ? result : []
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
# Downloads all icons in the collection and saves them to the local file system.
|
|
@@ -39,7 +40,7 @@ module Anyicon
|
|
|
39
40
|
# @return [void]
|
|
40
41
|
def download_all
|
|
41
42
|
if list.empty?
|
|
42
|
-
|
|
43
|
+
::Rails.logger.info "AnyIcon: No icons available for #{@collection}."
|
|
43
44
|
return
|
|
44
45
|
end
|
|
45
46
|
|
|
@@ -48,7 +49,7 @@ module Anyicon
|
|
|
48
49
|
count += 1
|
|
49
50
|
download(icon)
|
|
50
51
|
end
|
|
51
|
-
|
|
52
|
+
::Rails.logger.info "AnyIcon: #{@collection}: #{count} downloads."
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
# Retrieves the configured collections from Anyicon.
|
|
@@ -80,20 +81,24 @@ module Anyicon
|
|
|
80
81
|
# @param icon_name [String] the name of the icon
|
|
81
82
|
# @return [Pathname] the path to the icon file
|
|
82
83
|
def icon_path(icon_name)
|
|
83
|
-
|
|
84
|
+
sanitized_collection = File.basename(@collection.to_s.gsub(/[^a-zA-Z0-9_\-]/, ""))
|
|
85
|
+
sanitized_name = File.basename(icon_name.to_s)
|
|
86
|
+
::Rails.root.join("app", "assets", "images", "icons", sanitized_collection, sanitized_name)
|
|
84
87
|
end
|
|
85
88
|
|
|
86
89
|
# Constructs the URL to fetch the icon collection directory contents from the repository.
|
|
87
90
|
#
|
|
88
91
|
# @return [String, nil] the URL to fetch the collection contents, or nil if the collection is not configured
|
|
89
92
|
def collection_url
|
|
90
|
-
return nil unless collections.
|
|
93
|
+
return nil unless collections.key?(@collection)
|
|
91
94
|
|
|
92
95
|
[
|
|
93
96
|
"https://api.github.com/repos/",
|
|
94
97
|
collections[@collection][:repo],
|
|
95
98
|
"/contents/",
|
|
96
|
-
collections[@collection][:path]
|
|
99
|
+
collections[@collection][:path],
|
|
100
|
+
"?ref=",
|
|
101
|
+
collections[@collection][:branch]
|
|
97
102
|
].join("")
|
|
98
103
|
end
|
|
99
104
|
end
|
data/lib/anyicon/common.rb
CHANGED
|
@@ -12,6 +12,8 @@ module Anyicon
|
|
|
12
12
|
# response = common.fetch('https://example.com')
|
|
13
13
|
# puts response.body if response.is_a?(Net::HTTPSuccess)
|
|
14
14
|
class Common
|
|
15
|
+
ALLOWED_HOSTS = %w[github.com raw.githubusercontent.com objects.githubusercontent.com api.github.com].freeze
|
|
16
|
+
|
|
15
17
|
# Fetches the content from the given URL, following redirects if necessary.
|
|
16
18
|
#
|
|
17
19
|
# @param url [String] the URL to fetch
|
|
@@ -19,11 +21,21 @@ module Anyicon
|
|
|
19
21
|
# @return [Net::HTTPResponse] the HTTP response
|
|
20
22
|
# @raise [Net::HTTPError] if the number of redirects exceeds the limit or another HTTP error occurs
|
|
21
23
|
def fetch(url, limit = 10)
|
|
22
|
-
raise Net::HTTPError
|
|
24
|
+
raise Net::HTTPError.new("Too many HTTP redirects", nil) if limit.zero?
|
|
23
25
|
return nil if url.nil?
|
|
24
26
|
|
|
25
|
-
uri = URI.parse(URI::
|
|
26
|
-
|
|
27
|
+
uri = URI.parse(URI::RFC2396_PARSER.escape(url))
|
|
28
|
+
unless uri.is_a?(URI::HTTPS) && ALLOWED_HOSTS.include?(uri.host)
|
|
29
|
+
raise Net::HTTPError.new("Blocked request to disallowed host: #{uri.host}", nil)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
request = Net::HTTP::Get.new(uri)
|
|
33
|
+
token = Anyicon.configuration.github_token
|
|
34
|
+
request["Authorization"] = "token #{token}" if token
|
|
35
|
+
|
|
36
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
|
37
|
+
http.request(request)
|
|
38
|
+
end
|
|
27
39
|
|
|
28
40
|
case response
|
|
29
41
|
when Net::HTTPSuccess then response
|
|
@@ -42,9 +42,13 @@ module Anyicon
|
|
|
42
42
|
# @return [Hash] the configured icon collections
|
|
43
43
|
attr_accessor :collections
|
|
44
44
|
|
|
45
|
+
# @return [String, nil] optional GitHub personal access token for API requests
|
|
46
|
+
attr_accessor :github_token
|
|
47
|
+
|
|
45
48
|
# Initializes a new Configuration instance with default settings.
|
|
46
49
|
def initialize
|
|
47
50
|
@collections = DEFAULT_COLLECTIONS.dup
|
|
51
|
+
@github_token = nil
|
|
48
52
|
end
|
|
49
53
|
|
|
50
54
|
def add_collections(new_collections)
|
data/lib/anyicon/icon.rb
CHANGED
|
@@ -21,23 +21,29 @@ module Anyicon
|
|
|
21
21
|
#
|
|
22
22
|
# @param icon [String] a comma-separated string of icon names, each in the format 'collection:name'
|
|
23
23
|
# @param props [Hash] additional properties to apply to the SVG element
|
|
24
|
+
ALLOWED_ATTRIBUTES = %w[
|
|
25
|
+
class id style width height viewBox fill stroke
|
|
26
|
+
stroke-width opacity transform title aria-label aria-hidden role
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
DATA_ATTRIBUTE_PATTERN = /\Adata-[\w-]+\z/
|
|
30
|
+
|
|
24
31
|
def initialize(icon = nil, **props)
|
|
25
|
-
# binding.pry
|
|
26
32
|
super()
|
|
27
33
|
@icons = (icon || props[:icon]).to_s.split(",").map { |i| i.split(":") }
|
|
28
|
-
@
|
|
34
|
+
@icons.reject! { |i| i.length < 2 }
|
|
35
|
+
@props = props.select { |key, _| allowed_attribute?(key) }
|
|
29
36
|
end
|
|
30
37
|
|
|
31
38
|
# Renders the SVG content for the specified icons.
|
|
32
39
|
#
|
|
33
40
|
# @return [String] the HTML-safe SVG content
|
|
34
41
|
def render
|
|
35
|
-
|
|
36
|
-
@icons.each do |icon|
|
|
42
|
+
parts = @icons.map do |icon|
|
|
37
43
|
ensure_icon_exists(icon)
|
|
38
|
-
|
|
44
|
+
svg_content(icon)
|
|
39
45
|
end
|
|
40
|
-
|
|
46
|
+
ActiveSupport::SafeBuffer.new(parts.join)
|
|
41
47
|
end
|
|
42
48
|
|
|
43
49
|
private
|
|
@@ -49,6 +55,23 @@ module Anyicon
|
|
|
49
55
|
@collections ||= Anyicon.configuration.collections
|
|
50
56
|
end
|
|
51
57
|
|
|
58
|
+
# Checks if an attribute key is allowed on the SVG element.
|
|
59
|
+
#
|
|
60
|
+
# @param key [Symbol, String] the attribute key
|
|
61
|
+
# @return [Boolean]
|
|
62
|
+
def allowed_attribute?(key)
|
|
63
|
+
name = key.to_s
|
|
64
|
+
ALLOWED_ATTRIBUTES.include?(name) || name.match?(DATA_ATTRIBUTE_PATTERN)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Sanitizes an icon name component to prevent path traversal.
|
|
68
|
+
#
|
|
69
|
+
# @param name [String] the raw icon name component
|
|
70
|
+
# @return [String] the sanitized name
|
|
71
|
+
def sanitize_name(name)
|
|
72
|
+
File.basename(name.to_s.gsub(/[^a-zA-Z0-9_\-]/, ""))
|
|
73
|
+
end
|
|
74
|
+
|
|
52
75
|
# Ensures the specified icon exists by downloading it if necessary.
|
|
53
76
|
#
|
|
54
77
|
# @param icon [Array] the collection and name of the icon
|
|
@@ -77,7 +100,7 @@ module Anyicon
|
|
|
77
100
|
# @param icon [Array] the collection and name of the icon
|
|
78
101
|
# @return [Pathname] the path to the icon file
|
|
79
102
|
def icon_path(icon)
|
|
80
|
-
::Rails.root.join("app", "assets", "images", "icons", icon[0], "#{icon[1]}.svg")
|
|
103
|
+
::Rails.root.join("app", "assets", "images", "icons", sanitize_name(icon[0]), "#{sanitize_name(icon[1])}.svg")
|
|
81
104
|
end
|
|
82
105
|
|
|
83
106
|
# Constructs the URL to download the specified icon.
|
|
@@ -85,10 +108,22 @@ module Anyicon
|
|
|
85
108
|
# @param icon [Array] the collection and name of the icon
|
|
86
109
|
# @return [String, nil] the URL to download the icon, or nil if the collection is not configured
|
|
87
110
|
def icon_url(icon)
|
|
88
|
-
|
|
111
|
+
collection_key = sanitize_name(icon[0]).to_sym
|
|
112
|
+
return nil unless collections.key?(collection_key)
|
|
113
|
+
|
|
114
|
+
[ "https://github.com/", collections[collection_key][:repo], "/raw/", collections[collection_key][:branch], "/",
|
|
115
|
+
collections[collection_key][:path], "/", sanitize_name(icon[1]), ".svg" ].join("")
|
|
116
|
+
end
|
|
89
117
|
|
|
90
|
-
|
|
91
|
-
|
|
118
|
+
# Returns the cached raw SVG content for the specified icon.
|
|
119
|
+
#
|
|
120
|
+
# @param icon [Array] the collection and name of the icon
|
|
121
|
+
# @return [String, nil] the raw SVG file content, or nil if file doesn't exist
|
|
122
|
+
def cached_svg(icon)
|
|
123
|
+
path = icon_path(icon)
|
|
124
|
+
return nil unless File.file?(path)
|
|
125
|
+
|
|
126
|
+
self.class.svg_cache[path.to_s] ||= File.read(path)
|
|
92
127
|
end
|
|
93
128
|
|
|
94
129
|
# Reads and customizes the SVG content for the specified icon.
|
|
@@ -96,10 +131,10 @@ module Anyicon
|
|
|
96
131
|
# @param icon [Array] the collection and name of the icon
|
|
97
132
|
# @return [String] the customized SVG content
|
|
98
133
|
def svg_content(icon)
|
|
99
|
-
|
|
134
|
+
raw = cached_svg(icon)
|
|
135
|
+
return "" unless raw
|
|
100
136
|
|
|
101
|
-
|
|
102
|
-
doc = Nokogiri::HTML::DocumentFragment.parse(svg_content)
|
|
137
|
+
doc = Nokogiri::HTML::DocumentFragment.parse(raw)
|
|
103
138
|
svg = doc.at_css "svg"
|
|
104
139
|
|
|
105
140
|
@props.each do |key, value|
|
|
@@ -119,6 +154,19 @@ module Anyicon
|
|
|
119
154
|
def render(*args, **kwargs)
|
|
120
155
|
new(*args, **kwargs).render
|
|
121
156
|
end
|
|
157
|
+
|
|
158
|
+
# In-memory cache for raw SVG file contents, keyed by file path.
|
|
159
|
+
# Avoids repeated File.read + disk I/O for the same icon.
|
|
160
|
+
#
|
|
161
|
+
# @return [Hash] the SVG cache
|
|
162
|
+
def svg_cache
|
|
163
|
+
@svg_cache ||= {}
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Clears the in-memory SVG cache.
|
|
167
|
+
def clear_cache!
|
|
168
|
+
@svg_cache = {}
|
|
169
|
+
end
|
|
122
170
|
end
|
|
123
171
|
end
|
|
124
172
|
end
|
data/lib/anyicon/version.rb
CHANGED
|
@@ -15,4 +15,11 @@ Anyicon.configure do |config|
|
|
|
15
15
|
# }
|
|
16
16
|
# )
|
|
17
17
|
#
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
# Optional: Set a GitHub personal access token to avoid API rate limits
|
|
21
|
+
# (60 requests/hour unauthenticated vs 5,000/hour authenticated).
|
|
22
|
+
#
|
|
23
|
+
# config.github_token = ENV["GITHUB_TOKEN"]
|
|
24
|
+
#
|
|
18
25
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: anyicon
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Arthur Molina
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
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: rails
|
|
@@ -24,6 +23,20 @@ dependencies:
|
|
|
24
23
|
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
25
|
version: '5.2'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: nokogiri
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
27
40
|
- !ruby/object:Gem::Dependency
|
|
28
41
|
name: appraisal
|
|
29
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -139,7 +152,6 @@ metadata:
|
|
|
139
152
|
source_code_uri: https://github.com/arthurmolina/anyicon
|
|
140
153
|
bug_tracker_uri: https://github.com/arthurmolina/anyicon/issues
|
|
141
154
|
wiki_uri: https://github.com/arthurmolina/anyicon/wiki
|
|
142
|
-
post_install_message:
|
|
143
155
|
rdoc_options: []
|
|
144
156
|
require_paths:
|
|
145
157
|
- lib
|
|
@@ -154,8 +166,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
154
166
|
- !ruby/object:Gem::Version
|
|
155
167
|
version: '0'
|
|
156
168
|
requirements: []
|
|
157
|
-
rubygems_version: 3.
|
|
158
|
-
signing_key:
|
|
169
|
+
rubygems_version: 3.6.9
|
|
159
170
|
specification_version: 4
|
|
160
171
|
summary: Rails View Helpers for any icon collections.
|
|
161
172
|
test_files: []
|