inline_svg 0.11.0 → 1.7.2

Sign up to get free protection for your applications and to get access to all the features.
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