unmagic-icon 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.
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack"
4
+ require "rack/utils"
5
+
6
+ require "erb"
7
+ require "cgi"
8
+
9
+ module Unmagic
10
+ class Icon
11
+ class Web
12
+ def self.call(env)
13
+ app.call(env)
14
+ end
15
+
16
+ def self.app
17
+ @app ||= Rack::Builder.new do
18
+ public_path = File.expand_path("web/public", __dir__)
19
+ use Rack::Static, urls: [ "/public" ], root: File.dirname(public_path)
20
+ run Unmagic::Icon::Web.new
21
+ end.to_app
22
+ end
23
+
24
+ def call(env)
25
+ @request = Rack::Request.new(env)
26
+ @libraries = Unmagic::Icon::Library::Registry.all
27
+
28
+ case @request.path_info
29
+ when "/"
30
+ # Redirect to first library
31
+ first_library = @libraries.first.name.to_param
32
+ if first_library.nil?
33
+ [ 404, { "content-type" => "text/plain" }, [ "No libraries found" ] ]
34
+ else
35
+ [ 302, { "location" => url(first_library, @request.params) }, [] ]
36
+ end
37
+ else
38
+ library_name = @request.path_info.delete_prefix("/")
39
+
40
+ begin
41
+ @selected_library = Unmagic::Icon::Library::Registry.find(library_name)
42
+ rescue Unmagic::Icon::LibraryNotError
43
+ return [ 404, { "content-type" => "text/plain" }, [ "Library not found: #{@selected_library}" ] ]
44
+ end
45
+
46
+ template_path = File.expand_path("web/views/layout.html.erb", __dir__)
47
+ template = ERB.new(File.read(template_path))
48
+ html = template.result(binding)
49
+
50
+ [ 200, { "content-type" => "text/html; charset=utf-8" }, [ html ] ]
51
+ end
52
+ end
53
+
54
+ def escape(text)
55
+ CGI.escapeHTML(text.to_s)
56
+ end
57
+
58
+ def url(path_parts, params_hash = nil)
59
+ url = [ @request.env["SCRIPT_NAME"], *path_parts ].compact.join("/")
60
+ url = url.gsub(/\/\//, "/")
61
+
62
+ if params_hash
63
+ "#{url}?#{Rack::Utils.build_query(params_hash)}"
64
+ else
65
+ url
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+ require "active_support/core_ext/object/blank"
5
+ require "active_support/core_ext/string/output_safety"
6
+ require "active_support/core_ext/string/inflections"
7
+
8
+ require_relative "icon/version"
9
+ require_relative "icon/configuration"
10
+ require_relative "icon/library"
11
+ require_relative "icon/library/registry"
12
+ require_relative "icon/action_view_helpers"
13
+ require_relative "icon/engine" if defined?(Rails)
14
+
15
+ module Unmagic
16
+ class Icon
17
+ class Error < StandardError; end
18
+ class LibraryNotFoundError < Error; end
19
+ class IconNotFoundError < Error; end
20
+
21
+ class << self
22
+ def init
23
+ yield(configuration) if block_given?
24
+ @initialized = true
25
+ end
26
+
27
+ def initialized?
28
+ @initialized == true
29
+ end
30
+
31
+ def configure
32
+ yield(configuration) if block_given?
33
+ configuration
34
+ end
35
+
36
+ def configuration
37
+ @configuration ||= Unmagic::Icon::Configuration.new
38
+ end
39
+
40
+ def libraries
41
+ Unmagic::Icon::Library::Registry.all
42
+ end
43
+
44
+ def find(reference)
45
+ library_path, icon_name = parse_reference(reference)
46
+
47
+ Unmagic::Icon::Library::Registry.find(library_path).find(icon_name)
48
+ end
49
+
50
+ # Parse a "library/name" reference into [library, name], tolerating the
51
+ # emoji-style ":library/name:" decoration so both kinds share one syntax.
52
+ def parse_reference(reference)
53
+ *library_parts, name = reference.to_s.gsub(/\A:|:\z/, "").split("/")
54
+ [ library_parts.join("/"), name ]
55
+ end
56
+ end
57
+
58
+ attr_reader :name, :path
59
+
60
+ def initialize(name:, path:)
61
+ @name = name
62
+ @path = path
63
+ end
64
+
65
+ def doc
66
+ Nokogiri::XML(raw_svg_content)
67
+ end
68
+
69
+ def attributes
70
+ extracted_svg[:attributes]
71
+ end
72
+
73
+ def contents
74
+ extracted_svg[:contents].strip
75
+ end
76
+
77
+ def as_json
78
+ { name: name, svg: to_svg }
79
+ end
80
+
81
+ # Render the asset to HTML, dispatching on its kind. SVG inlines today; a
82
+ # future raster asset (png/gif) would emit an <img> here, so callers stay
83
+ # render-agnostic.
84
+ def render(options = {})
85
+ case File.extname(@path).downcase
86
+ when ".svg" then to_svg(options)
87
+ else
88
+ raise Unmagic::Icon::Error, "Don't know how to render #{@path}"
89
+ end
90
+ end
91
+
92
+ # Render the SVG with a `unmagic-icon` class (plus any caller class) and a
93
+ # `data-unmagic-icon` marker. Any other options are merged verbatim as
94
+ # attributes on the <svg> — so the caller controls accessibility
95
+ # (`aria-hidden`, `aria-label`, `role`), `id`, `data-*`, etc.
96
+ def to_svg(options = {})
97
+ return @svg_cache if options.empty? && @svg_cache
98
+
99
+ attributes = options.transform_keys(&:to_s)
100
+ css_classes = [ "unmagic-icon", attributes.delete("class") ].compact.join(" ")
101
+
102
+ svg = raw_svg_content.dup
103
+ svg.sub!(/<svg(\s+[^>]*)?>/i) do
104
+ existing = (::Regexp.last_match(1) || "").gsub(/\sclass=["'][^"']*["']/, "")
105
+ extra = attributes.map { |name, value| %(#{name}="#{CGI.escapeHTML(value.to_s)}") }
106
+ %(<svg#{existing} class="#{css_classes}" data-unmagic-icon="#{@name}"#{extra.empty? ? "" : " #{extra.join(' ')}"}>)
107
+ end
108
+
109
+ svg = svg.html_safe
110
+ @svg_cache = svg if options.empty?
111
+ svg
112
+ end
113
+
114
+ private
115
+
116
+ def raw_svg_content
117
+ @raw_svg_content ||= File.read(@path)
118
+ end
119
+
120
+ def extracted_svg
121
+ @xml ||=
122
+ begin
123
+ root = doc.at_css("//svg")
124
+ {
125
+ attributes: root.attributes.inject({}) do |hash, (key, value)|
126
+ hash[key] = value.value
127
+ hash
128
+ end,
129
+ contents: root.children.to_xml
130
+ }
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "unmagic/icon"
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: unmagic-icon
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Keith Pitt
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-06-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: railties
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '7.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '7.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: nokogiri
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 1.8.5
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 1.8.5
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.12'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.12'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '13.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '13.0'
83
+ description: Render SVG icons inline in Rails views, resolve them through library/name
84
+ references and aliases, and download popular icon sets (Heroicons, Lucide, Tabler,
85
+ Feather, and more)
86
+ email:
87
+ - keith@unreasonable-magic.com
88
+ executables: []
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - CHANGELOG.md
93
+ - LICENSE
94
+ - README.md
95
+ - lib/tasks/unmagic/icon/download.rake
96
+ - lib/tasks/unmagic/icon/install.rake
97
+ - lib/unmagic/icon.rb
98
+ - lib/unmagic/icon/action_view_helpers.rb
99
+ - lib/unmagic/icon/configuration.rb
100
+ - lib/unmagic/icon/engine.rb
101
+ - lib/unmagic/icon/library.rb
102
+ - lib/unmagic/icon/library/registry.rb
103
+ - lib/unmagic/icon/library/source.rb
104
+ - lib/unmagic/icon/library/source/devicons.rb
105
+ - lib/unmagic/icon/library/source/feather.rb
106
+ - lib/unmagic/icon/library/source/heroicons.rb
107
+ - lib/unmagic/icon/library/source/lucide.rb
108
+ - lib/unmagic/icon/library/source/material_file_icons.rb
109
+ - lib/unmagic/icon/library/source/silk.rb
110
+ - lib/unmagic/icon/library/source/simple_icons.rb
111
+ - lib/unmagic/icon/library/source/tabler.rb
112
+ - lib/unmagic/icon/version.rb
113
+ - lib/unmagic/icon/web.rb
114
+ - lib/unmagic/icon/web/public/favicon.png
115
+ - lib/unmagic/icon/web/views/layout.html.erb
116
+ - lib/unmagic_icon.rb
117
+ homepage: https://github.com/unreasonable-magic/unmagic-icon
118
+ licenses:
119
+ - MIT
120
+ metadata:
121
+ homepage_uri: https://github.com/unreasonable-magic/unmagic-icon
122
+ source_code_uri: https://github.com/unreasonable-magic/unmagic-icon
123
+ changelog_uri: https://github.com/unreasonable-magic/unmagic-icon/blob/main/CHANGELOG.md
124
+ post_install_message:
125
+ rdoc_options: []
126
+ require_paths:
127
+ - lib
128
+ required_ruby_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '3.0'
133
+ required_rubygems_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ requirements: []
139
+ rubygems_version: 3.5.22
140
+ signing_key:
141
+ specification_version: 4
142
+ summary: Inline SVG icons for Rails, with downloadable icon libraries
143
+ test_files: []