inline_svg 0.11.0 → 1.7.2

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.
Files changed (56) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/integration_test.yml +58 -0
  3. data/.github/workflows/ruby.yml +20 -0
  4. data/.rubocop.yml +1 -0
  5. data/.rubocop_todo.yml +421 -0
  6. data/CHANGELOG.md +144 -2
  7. data/README.md +148 -34
  8. data/Rakefile +7 -0
  9. data/inline_svg.gemspec +4 -4
  10. data/lib/inline_svg.rb +41 -9
  11. data/lib/inline_svg/action_view/helpers.rb +72 -7
  12. data/lib/inline_svg/cached_asset_file.rb +71 -0
  13. data/lib/inline_svg/finds_asset_paths.rb +1 -1
  14. data/lib/inline_svg/id_generator.rb +12 -3
  15. data/lib/inline_svg/io_resource.rb +4 -3
  16. data/lib/inline_svg/railtie.rb +8 -3
  17. data/lib/inline_svg/static_asset_finder.rb +4 -2
  18. data/lib/inline_svg/transform_pipeline.rb +0 -1
  19. data/lib/inline_svg/transform_pipeline/transformations.rb +8 -1
  20. data/lib/inline_svg/transform_pipeline/transformations/aria_attributes.rb +17 -20
  21. data/lib/inline_svg/transform_pipeline/transformations/aria_hidden.rb +9 -0
  22. data/lib/inline_svg/transform_pipeline/transformations/aria_hidden_attribute.rb +9 -0
  23. data/lib/inline_svg/transform_pipeline/transformations/class_attribute.rb +5 -6
  24. data/lib/inline_svg/transform_pipeline/transformations/data_attributes.rb +10 -5
  25. data/lib/inline_svg/transform_pipeline/transformations/description.rb +7 -6
  26. data/lib/inline_svg/transform_pipeline/transformations/height.rb +3 -4
  27. data/lib/inline_svg/transform_pipeline/transformations/id_attribute.rb +3 -4
  28. data/lib/inline_svg/transform_pipeline/transformations/no_comment.rb +5 -2
  29. data/lib/inline_svg/transform_pipeline/transformations/preserve_aspect_ratio.rb +3 -4
  30. data/lib/inline_svg/transform_pipeline/transformations/size.rb +4 -5
  31. data/lib/inline_svg/transform_pipeline/transformations/style_attribute.rb +11 -0
  32. data/lib/inline_svg/transform_pipeline/transformations/title.rb +7 -6
  33. data/lib/inline_svg/transform_pipeline/transformations/transformation.rb +13 -0
  34. data/lib/inline_svg/transform_pipeline/transformations/width.rb +3 -4
  35. data/lib/inline_svg/version.rb +1 -1
  36. data/lib/inline_svg/webpack_asset_finder.rb +50 -0
  37. data/spec/cached_asset_file_spec.rb +73 -0
  38. data/spec/files/static_assets/assets0/known-document-two.svg +1 -0
  39. data/spec/files/static_assets/assets0/known-document.svg +1 -0
  40. data/spec/files/static_assets/assets0/some-document.svg +1 -0
  41. data/spec/files/static_assets/assets1/known-document.svg +1 -0
  42. data/spec/files/static_assets/assets1/other-document.svg +3 -0
  43. data/spec/files/static_assets/assets1/some-file.txt +1 -0
  44. data/spec/helpers/inline_svg_spec.rb +104 -21
  45. data/spec/id_generator_spec.rb +5 -3
  46. data/spec/inline_svg_spec.rb +48 -0
  47. data/spec/transformation_pipeline/transformations/aria_attributes_spec.rb +16 -16
  48. data/spec/transformation_pipeline/transformations/aria_hidden_attribute_spec.rb +12 -0
  49. data/spec/transformation_pipeline/transformations/data_attributes_spec.rb +18 -0
  50. data/spec/transformation_pipeline/transformations/height_spec.rb +9 -0
  51. data/spec/transformation_pipeline/transformations/style_attribute_spec.rb +26 -0
  52. data/spec/transformation_pipeline/transformations/title_spec.rb +9 -0
  53. data/spec/transformation_pipeline/transformations/transformation_spec.rb +39 -0
  54. data/spec/transformation_pipeline/transformations_spec.rb +5 -1
  55. metadata +49 -22
  56. data/circle.yml +0 -3
@@ -18,13 +18,13 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_development_dependency "bundler", "~> 1.5"
21
+ spec.add_development_dependency "bundler", "~> 2.0"
22
22
  spec.add_development_dependency "rake"
23
23
  spec.add_development_dependency "rspec", "~> 3.2"
24
24
  spec.add_development_dependency "rspec_junit_formatter", "0.2.2"
25
25
  spec.add_development_dependency "pry"
26
+ spec.add_development_dependency "rubocop"
26
27
 
27
- spec.add_runtime_dependency "activesupport", ">= 4.0"
28
- spec.add_runtime_dependency "nokogiri", "~> 1.6", '~> 1.6'
29
- spec.add_runtime_dependency "loofah", ">= 2.0"
28
+ spec.add_runtime_dependency "activesupport", ">= 3.0"
29
+ spec.add_runtime_dependency "nokogiri", ">= 1.6"
30
30
  end
@@ -1,8 +1,10 @@
1
1
  require "inline_svg/version"
2
2
  require "inline_svg/action_view/helpers"
3
3
  require "inline_svg/asset_file"
4
+ require "inline_svg/cached_asset_file"
4
5
  require "inline_svg/finds_asset_paths"
5
6
  require "inline_svg/static_asset_finder"
7
+ require "inline_svg/webpack_asset_finder"
6
8
  require "inline_svg/transform_pipeline"
7
9
  require "inline_svg/io_resource"
8
10
 
@@ -14,24 +16,46 @@ module InlineSvg
14
16
  class Configuration
15
17
  class Invalid < ArgumentError; end
16
18
 
17
- attr_reader :asset_finder, :custom_transformations
19
+ attr_reader :asset_file, :asset_finder, :custom_transformations, :svg_not_found_css_class
18
20
 
19
21
  def initialize
20
22
  @custom_transformations = {}
23
+ @asset_file = InlineSvg::AssetFile
24
+ @svg_not_found_css_class = nil
25
+ @raise_on_file_not_found = false
21
26
  end
22
27
 
23
- def asset_finder=(finder)
24
- if finder.respond_to?(:find_asset)
25
- @asset_finder = finder
26
- else
27
- # fallback to a naive static asset finder (sprokects >= 3.0 &&
28
- # config.assets.precompile = false
29
- # See: https://github.com/jamesmartin/inline_svg/issues/25
30
- @asset_finder = InlineSvg::StaticAssetFinder
28
+ def asset_file=(custom_asset_file)
29
+ begin
30
+ method = custom_asset_file.method(:named)
31
+ if method.arity == 1
32
+ @asset_file = custom_asset_file
33
+ else
34
+ raise InlineSvg::Configuration::Invalid.new("asset_file should implement the #named method with arity 1")
35
+ end
36
+ rescue NameError
37
+ raise InlineSvg::Configuration::Invalid.new("asset_file should implement the #named method")
31
38
  end
39
+ end
40
+
41
+ def asset_finder=(finder)
42
+ @asset_finder = if finder.respond_to?(:find_asset)
43
+ finder
44
+ else
45
+ # fallback to a naive static asset finder
46
+ # (sprokects >= 3.0 && config.assets.precompile = false
47
+ # See: https://github.com/jamesmartin/inline_svg/issues/25
48
+ InlineSvg::StaticAssetFinder
49
+ end
32
50
  asset_finder
33
51
  end
34
52
 
53
+ def svg_not_found_css_class=(css_class)
54
+ if css_class.present? && css_class.is_a?(String)
55
+ @svg_not_found_css_class = css_class
56
+ end
57
+ end
58
+
35
59
  def add_custom_transformation(options)
36
60
  if incompatible_transformation?(options.fetch(:transform))
37
61
  raise InlineSvg::Configuration::Invalid.new("#{options.fetch(:transform)} should implement the .create_with_value and #transform methods")
@@ -39,6 +63,14 @@ module InlineSvg
39
63
  @custom_transformations.merge!(Hash[ *[options.fetch(:attribute, :no_attribute), options] ])
40
64
  end
41
65
 
66
+ def raise_on_file_not_found=(value)
67
+ @raise_on_file_not_found = value
68
+ end
69
+
70
+ def raise_on_file_not_found?
71
+ !!@raise_on_file_not_found
72
+ end
73
+
42
74
  private
43
75
 
44
76
  def incompatible_transformation?(klass)
@@ -4,21 +4,86 @@ require 'action_view/context' if defined?(Rails)
4
4
  module InlineSvg
5
5
  module ActionView
6
6
  module Helpers
7
+ def inline_svg_tag(filename, transform_params={})
8
+ with_asset_finder(InlineSvg.configuration.asset_finder) do
9
+ render_inline_svg(filename, transform_params)
10
+ end
11
+ end
12
+
13
+ def inline_svg_pack_tag(filename, transform_params={})
14
+ with_asset_finder(InlineSvg::WebpackAssetFinder) do
15
+ render_inline_svg(filename, transform_params)
16
+ end
17
+ end
18
+
7
19
  def inline_svg(filename, transform_params={})
20
+ ActiveSupport::Deprecation.warn(
21
+ '`inline_svg` is deprecated and will be removed from inline_svg 2.0 (use `inline_svg_tag` or `inline_svg_pack_tag` instead)'
22
+ )
23
+
24
+ render_inline_svg(filename, transform_params)
25
+ end
26
+
27
+ private
28
+
29
+ def backwards_compatible_html_escape(filename)
30
+ # html_escape_once was introduced in newer versions of Rails.
31
+ if ERB::Util.respond_to?(:html_escape_once)
32
+ ERB::Util.html_escape_once(filename)
33
+ else
34
+ ERB::Util.html_escape(filename)
35
+ end
36
+ end
37
+
38
+ def render_inline_svg(filename, transform_params={})
8
39
  begin
9
- svg_file = if InlineSvg::IOResource === filename
10
- InlineSvg::IOResource.read filename
11
- else
12
- InlineSvg::AssetFile.named filename
40
+ svg_file = read_svg(filename)
41
+ rescue InlineSvg::AssetFile::FileNotFound => error
42
+ raise error if InlineSvg.configuration.raise_on_file_not_found?
43
+ return placeholder(filename) unless transform_params[:fallback].present?
44
+
45
+ if transform_params[:fallback].present?
46
+ begin
47
+ svg_file = read_svg(transform_params[:fallback])
48
+ rescue InlineSvg::AssetFile::FileNotFound
49
+ placeholder(filename)
50
+ end
13
51
  end
14
- rescue InlineSvg::AssetFile::FileNotFound
15
- return "<svg><!-- SVG file not found: '#{filename}' #{extension_hint(filename)}--></svg>".html_safe
16
52
  end
17
53
 
18
54
  InlineSvg::TransformPipeline.generate_html_from(svg_file, transform_params).html_safe
19
55
  end
20
56
 
21
- private
57
+ def read_svg(filename)
58
+ if InlineSvg::IOResource === filename
59
+ InlineSvg::IOResource.read filename
60
+ else
61
+ configured_asset_file.named filename
62
+ end
63
+ end
64
+
65
+ def placeholder(filename)
66
+ css_class = InlineSvg.configuration.svg_not_found_css_class
67
+ not_found_message = "'#{backwards_compatible_html_escape(filename)}' #{extension_hint(filename)}"
68
+
69
+ if css_class.nil?
70
+ return "<svg><!-- SVG file not found: #{not_found_message}--></svg>".html_safe
71
+ else
72
+ return "<svg class='#{css_class}'><!-- SVG file not found: #{not_found_message}--></svg>".html_safe
73
+ end
74
+ end
75
+
76
+ def configured_asset_file
77
+ InlineSvg.configuration.asset_file
78
+ end
79
+
80
+ def with_asset_finder(asset_finder)
81
+ Thread.current[:inline_svg_asset_finder] = asset_finder
82
+ output = yield
83
+ Thread.current[:inline_svg_asset_finder] = nil
84
+
85
+ output
86
+ end
22
87
 
23
88
  def extension_hint(filename)
24
89
  filename.ends_with?(".svg") ? "" : "(Try adding .svg to your filename) "
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InlineSvg
4
+ class CachedAssetFile
5
+ attr_reader :assets, :filters, :paths
6
+
7
+ # For each of the given paths, recursively reads each asset and stores its
8
+ # contents alongside the full path to the asset.
9
+ #
10
+ # paths - One or more String representing directories on disk to search
11
+ # for asset files. Note: paths are searched recursively.
12
+ # filters - One or more Strings/Regexps to match assets against. Only
13
+ # assets matching all filters will be cached and available to load.
14
+ # Note: Specifying no filters will cache every file found in
15
+ # paths.
16
+ #
17
+ def initialize(paths: [], filters: [])
18
+ @paths = Array(paths).compact.map { |p| Pathname.new(p) }
19
+ @filters = Array(filters).map { |f| Regexp.new(f) }
20
+ @assets = @paths.reduce({}) { |assets, p| assets.merge(read_assets(assets, p)) }
21
+ @sorted_asset_keys = assets.keys.sort { |a, b| a.size <=> b.size }
22
+ end
23
+
24
+ # Public: Finds the named asset and returns the contents as a string.
25
+ #
26
+ # asset_name - A string representing the name of the asset to load
27
+ #
28
+ # Returns: A String or raises InlineSvg::AssetFile::FileNotFound error
29
+ def named(asset_name)
30
+ assets[key_for_asset(asset_name)] or
31
+ raise InlineSvg::AssetFile::FileNotFound.new("Asset not found: #{asset_name}")
32
+ end
33
+
34
+ private
35
+ # Internal: Finds the key for a given asset name (using a Regex). In the
36
+ # event of an ambiguous asset_name matching multiple assets, this method
37
+ # ranks the matches by their full file path, choosing the shortest (most
38
+ # exact) match over all others.
39
+ #
40
+ # Returns a String representing the key for the named asset or nil if there
41
+ # is no match.
42
+ def key_for_asset(asset_name)
43
+ @sorted_asset_keys.find { |k| k.include?(asset_name) }
44
+ end
45
+
46
+ # Internal: Recursively descends through current_paths reading each file it
47
+ # finds and adding them to the accumulator if the fullpath of the file
48
+ # matches all configured filters.
49
+ #
50
+ # acc - Hash representing the accumulated assets keyed by full path
51
+ # paths - Pathname representing the current node in the directory
52
+ # structure to consider
53
+ #
54
+ # Returns a Hash containing the contents of each asset, keyed by fullpath
55
+ # to the asset.
56
+ def read_assets(acc, paths)
57
+ paths.each_child do |child|
58
+ if child.directory?
59
+ read_assets(acc, child)
60
+ elsif child.readable_real?
61
+ acc[child.to_s] = File.read(child) if matches_all_filters?(child)
62
+ end
63
+ end
64
+ acc
65
+ end
66
+
67
+ def matches_all_filters?(path)
68
+ filters.all? { |f| f.match(path.to_s) }
69
+ end
70
+ end
71
+ end
@@ -6,7 +6,7 @@ module InlineSvg
6
6
  end
7
7
 
8
8
  def self.configured_asset_finder
9
- InlineSvg.configuration.asset_finder
9
+ Thread.current[:inline_svg_asset_finder] || InlineSvg.configuration.asset_finder
10
10
  end
11
11
  end
12
12
  end
@@ -1,8 +1,17 @@
1
+ require 'digest'
2
+
1
3
  module InlineSvg
2
4
  class IdGenerator
3
- def self.generate(base, salt)
4
- bytes = Digest::SHA1.digest("#{base}-#{salt}")
5
- Digest.hexencode(bytes).to_i(16).to_s(36)
5
+ class Randomness
6
+ require "securerandom"
7
+ def self.call
8
+ SecureRandom.hex(10)
9
+ end
10
+ end
11
+
12
+ def self.generate(base, salt, randomness: Randomness)
13
+ bytes = Digest::SHA1.digest("#{base}-#{salt}-#{randomness.call}")
14
+ 'a' + Digest.hexencode(bytes).to_i(16).to_s(36)
6
15
  end
7
16
  end
8
17
  end
@@ -1,16 +1,17 @@
1
1
  module InlineSvg
2
2
  module IOResource
3
- def self.=== object
3
+ def self.===(object)
4
4
  object.is_a?(IO) || object.is_a?(StringIO)
5
5
  end
6
6
 
7
- def self.default_for object
7
+ def self.default_for(object)
8
8
  case object
9
9
  when StringIO then ''
10
10
  when IO then 1
11
11
  end
12
12
  end
13
- def self.read object
13
+
14
+ def self.read(object)
14
15
  start = object.pos
15
16
  str = object.read
16
17
  object.seek start
@@ -10,9 +10,14 @@ module InlineSvg
10
10
 
11
11
  config.after_initialize do |app|
12
12
  InlineSvg.configure do |config|
13
- # In default Rails apps, this will be a fully operational
14
- # Sprockets::Environment instance
15
- config.asset_finder = app.instance_variable_get(:@assets)
13
+ # Configure the asset_finder:
14
+ # Only set this when a user-configured asset finder has not been
15
+ # configured already.
16
+ if config.asset_finder.nil?
17
+ # In default Rails apps, this will be a fully operational
18
+ # Sprockets::Environment instance
19
+ config.asset_finder = app.instance_variable_get(:@assets)
20
+ end
16
21
  end
17
22
  end
18
23
  end
@@ -1,7 +1,9 @@
1
+ require "pathname"
2
+
1
3
  # Naive fallback asset finder for when sprockets >= 3.0 &&
2
4
  # config.assets.precompile = false
3
5
  # Thanks to @ryanswood for the original code:
4
- # https://github.com/AbleHealth/inline_svg/commit/661bbb3bef7d1b4bd6ccd63f5f018305797b9509
6
+ # https://github.com/jamesmartin/inline_svg/commit/661bbb3bef7d1b4bd6ccd63f5f018305797b9509
5
7
  module InlineSvg
6
8
  class StaticAssetFinder
7
9
  def self.find_asset(filename)
@@ -14,7 +16,7 @@ module InlineSvg
14
16
 
15
17
  def pathname
16
18
  if ::Rails.application.config.assets.compile
17
- ::Rails.application.assets[@filename].pathname
19
+ Pathname.new(::Rails.application.assets[@filename].filename)
18
20
  else
19
21
  manifest = ::Rails.application.assets_manifest
20
22
  asset_path = manifest.assets[@filename]
@@ -10,6 +10,5 @@ module InlineSvg
10
10
  end
11
11
 
12
12
  require 'nokogiri'
13
- require 'loofah'
14
13
  require 'inline_svg/id_generator'
15
14
  require 'inline_svg/transform_pipeline/transformations'
@@ -6,7 +6,9 @@ module InlineSvg::TransformPipeline::Transformations
6
6
  desc: { transform: Description, priority: 2 },
7
7
  title: { transform: Title, priority: 3 },
8
8
  aria: { transform: AriaAttributes },
9
+ aria_hidden: { transform: AriaHiddenAttribute },
9
10
  class: { transform: ClassAttribute },
11
+ style: { transform: StyleAttribute },
10
12
  data: { transform: DataAttributes },
11
13
  height: { transform: Height },
12
14
  nocomment: { transform: NoComment },
@@ -38,8 +40,11 @@ module InlineSvg::TransformPipeline::Transformations
38
40
  end
39
41
 
40
42
  def self.lookup(transform_params)
43
+ return [] unless transform_params.any? || custom_transformations.any?
44
+
45
+ transform_params_with_defaults = params_with_defaults(transform_params)
41
46
  all_transformations.map { |name, definition|
42
- value = params_with_defaults(transform_params)[name]
47
+ value = transform_params_with_defaults[name]
43
48
  definition.fetch(:transform, no_transform).create_with_value(value) if value
44
49
  }.compact
45
50
  end
@@ -72,6 +77,7 @@ end
72
77
  require 'inline_svg/transform_pipeline/transformations/transformation'
73
78
  require 'inline_svg/transform_pipeline/transformations/no_comment'
74
79
  require 'inline_svg/transform_pipeline/transformations/class_attribute'
80
+ require 'inline_svg/transform_pipeline/transformations/style_attribute'
75
81
  require 'inline_svg/transform_pipeline/transformations/title'
76
82
  require 'inline_svg/transform_pipeline/transformations/description'
77
83
  require 'inline_svg/transform_pipeline/transformations/size'
@@ -81,3 +87,4 @@ require 'inline_svg/transform_pipeline/transformations/id_attribute'
81
87
  require 'inline_svg/transform_pipeline/transformations/data_attributes'
82
88
  require 'inline_svg/transform_pipeline/transformations/preserve_aspect_ratio'
83
89
  require 'inline_svg/transform_pipeline/transformations/aria_attributes'
90
+ require "inline_svg/transform_pipeline/transformations/aria_hidden_attribute"
@@ -1,34 +1,31 @@
1
1
  module InlineSvg::TransformPipeline::Transformations
2
2
  class AriaAttributes < Transformation
3
3
  def transform(doc)
4
- doc = Nokogiri::XML::Document.parse(doc.to_html)
5
- svg = doc.at_css("svg")
4
+ with_svg(doc) do |svg|
5
+ # Add role
6
+ svg["role"] = "img"
6
7
 
7
- # Add role
8
- svg["role"] = "img"
8
+ # Build aria-labelledby string
9
+ aria_elements = []
10
+ svg.search("title").each do |element|
11
+ aria_elements << element["id"] = element_id_for("title", element)
12
+ end
9
13
 
10
- # Build aria-labelledby string
11
- aria_elements = []
12
- doc.search("svg title").each do |element|
13
- aria_elements << element['id'] = element_id_for("title", element)
14
- end
15
-
16
- doc.search("svg desc").each do |element|
17
- aria_elements << element['id'] = element_id_for("desc", element)
18
- end
14
+ svg.search("desc").each do |element|
15
+ aria_elements << element["id"] = element_id_for("desc", element)
16
+ end
19
17
 
20
- if aria_elements.any?
21
- svg["aria-labelledby"] = aria_elements.join(" ")
18
+ if aria_elements.any?
19
+ svg["aria-labelledby"] = aria_elements.join(" ")
20
+ end
22
21
  end
23
-
24
- doc
25
22
  end
26
23
 
27
24
  def element_id_for(base, element)
28
- if element['id'].nil?
29
- InlineSvg::IdGenerator.generate(base, value)
25
+ if element["id"].nil?
26
+ InlineSvg::IdGenerator.generate(base, element.text)
30
27
  else
31
- InlineSvg::IdGenerator.generate(element['id'], value)
28
+ InlineSvg::IdGenerator.generate(element["id"], element.text)
32
29
  end
33
30
  end
34
31
  end