├── .rspec ├── .gitattributes ├── spec ├── files │ ├── static_assets │ │ ├── assets1 │ │ │ ├── some-file.txt │ │ │ ├── known-document.svg │ │ │ └── other-document.svg │ │ └── assets0 │ │ │ ├── known-document.svg │ │ │ ├── known-document-two.svg │ │ │ └── some-document.svg │ └── example.svg ├── id_generator_spec.rb ├── transformation_pipeline │ ├── transformations │ │ ├── width_spec.rb │ │ ├── id_attribute_spec.rb │ │ ├── view_box_spec.rb │ │ ├── aria_hidden_attribute_spec.rb │ │ ├── preserve_aspect_ratio_spec.rb │ │ ├── height_spec.rb │ │ ├── size_spec.rb │ │ ├── class_attribute_spec.rb │ │ ├── style_attribute_spec.rb │ │ ├── description_spec.rb │ │ ├── transformation_spec.rb │ │ ├── title_spec.rb │ │ ├── data_attributes_spec.rb │ │ └── aria_attributes_spec.rb │ └── transformations_spec.rb ├── webpack_asset_finder_spec.rb ├── propshaft_asset_finder_spec.rb ├── asset_file_spec.rb ├── static_asset_finder_spec.rb ├── io_resource_spec.rb ├── cached_asset_file_spec.rb ├── finds_asset_paths_spec.rb ├── spec_helper.rb ├── inline_svg_spec.rb └── helpers │ └── inline_svg_spec.rb ├── lib ├── inline_svg │ ├── version.rb │ ├── transform_pipeline │ │ ├── transformations │ │ │ ├── width.rb │ │ │ ├── height.rb │ │ │ ├── id_attribute.rb │ │ │ ├── view_box.rb │ │ │ ├── aria_hidden.rb │ │ │ ├── aria_hidden_attribute.rb │ │ │ ├── preserve_aspect_ratio.rb │ │ │ ├── style_attribute.rb │ │ │ ├── class_attribute.rb │ │ │ ├── no_comment.rb │ │ │ ├── title.rb │ │ │ ├── description.rb │ │ │ ├── size.rb │ │ │ ├── data_attributes.rb │ │ │ ├── aria_attributes.rb │ │ │ └── transformation.rb │ │ └── transformations.rb │ ├── io_resource.rb │ ├── asset_file.rb │ ├── finds_asset_paths.rb │ ├── propshaft_asset_finder.rb │ ├── id_generator.rb │ ├── transform_pipeline.rb │ ├── railtie.rb │ ├── static_asset_finder.rb │ ├── webpack_asset_finder.rb │ ├── action_view │ │ └── helpers.rb │ └── cached_asset_file.rb └── inline_svg.rb ├── .gitignore ├── Rakefile ├── Gemfile ├── .rubocop.yml ├── .github └── workflows │ ├── rubocop.yml │ ├── ruby.yml │ └── integration_test.yml ├── inline_svg.gemspec ├── LICENSE.txt ├── .rubocop_todo.yml ├── README.md └── CHANGELOG.md /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | .rubocop_todo.yml linguist-generated 2 | -------------------------------------------------------------------------------- /spec/files/static_assets/assets1/some-file.txt: -------------------------------------------------------------------------------- 1 | Some file contents. 2 | -------------------------------------------------------------------------------- /spec/files/static_assets/assets0/known-document.svg: -------------------------------------------------------------------------------- 1 | interesting content 2 | -------------------------------------------------------------------------------- /spec/files/static_assets/assets1/known-document.svg: -------------------------------------------------------------------------------- 1 | Another known document 2 | -------------------------------------------------------------------------------- /spec/files/static_assets/assets0/known-document-two.svg: -------------------------------------------------------------------------------- 1 | other interesting content 2 | -------------------------------------------------------------------------------- /lib/inline_svg/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg 4 | VERSION = "2.0.0" 5 | end 6 | -------------------------------------------------------------------------------- /spec/files/example.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /spec/files/static_assets/assets1/other-document.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Other document 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .ruby-* 6 | .yardoc 7 | coverage 8 | doc/ 9 | Gemfile.lock 10 | InstalledFiles 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/examples.txt 15 | spec/reports 16 | test/tmp 17 | test/version_tmp 18 | tmp 19 | _yardoc 20 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | 6 | RSpec::Core::RakeTask.new(:spec) do |t| 7 | t.pattern = Dir.glob("spec/**/*_spec.rb") 8 | # t.rspec_opts = "--format documentation" 9 | end 10 | task default: :spec 11 | -------------------------------------------------------------------------------- /lib/inline_svg/transform_pipeline/transformations/width.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg::TransformPipeline::Transformations 4 | class Width < Transformation 5 | def transform(doc) 6 | with_svg(doc) do |svg| 7 | svg["width"] = value 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/inline_svg/transform_pipeline/transformations/height.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg::TransformPipeline::Transformations 4 | class Height < Transformation 5 | def transform(doc) 6 | with_svg(doc) do |svg| 7 | svg["height"] = value 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/inline_svg/transform_pipeline/transformations/id_attribute.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg::TransformPipeline::Transformations 4 | class IdAttribute < Transformation 5 | def transform(doc) 6 | with_svg(doc) do |svg| 7 | svg["id"] = value 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/inline_svg/transform_pipeline/transformations/view_box.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg::TransformPipeline::Transformations 4 | class ViewBox < Transformation 5 | def transform(doc) 6 | with_svg(doc) do |svg| 7 | svg["viewBox"] = value 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/inline_svg/transform_pipeline/transformations/aria_hidden.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg::TransformPipeline::Transformations 4 | class AriaHidden < Transformation 5 | def transform(doc) 6 | with_svg(doc) do |svg| 7 | svg["aria-hidden"] = value 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/inline_svg/transform_pipeline/transformations/aria_hidden_attribute.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg::TransformPipeline::Transformations 4 | class AriaHiddenAttribute < Transformation 5 | def transform(doc) 6 | with_svg(doc) do |svg| 7 | svg["aria-hidden"] = value 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/inline_svg/transform_pipeline/transformations/preserve_aspect_ratio.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg::TransformPipeline::Transformations 4 | class PreserveAspectRatio < Transformation 5 | def transform(doc) 6 | with_svg(doc) do |svg| 7 | svg["preserveAspectRatio"] = value 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/inline_svg/io_resource.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg 4 | module IOResource 5 | def self.===(object) 6 | object.is_a?(IO) || object.is_a?(StringIO) || object.is_a?(Tempfile) 7 | end 8 | 9 | def self.read(object) 10 | start = object.pos 11 | str = object.read 12 | object.seek start 13 | str 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/inline_svg/transform_pipeline/transformations/style_attribute.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg::TransformPipeline::Transformations 4 | class StyleAttribute < Transformation 5 | def transform(doc) 6 | with_svg(doc) do |svg| 7 | styles = svg["style"].to_s.split(";") 8 | styles << value 9 | svg["style"] = styles.join(";") 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/id_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe InlineSvg::IdGenerator do 6 | it "generates a hexencoded ID based on a salt and a random value" do 7 | randomizer = -> { "some-random-value" } 8 | 9 | expect(InlineSvg::IdGenerator.generate("some-base", "some-salt", randomness: randomizer)) 10 | .to eq("at2c17mkqnvopy36iccxspura7wnreqf") 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/inline_svg/transform_pipeline/transformations/class_attribute.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg::TransformPipeline::Transformations 4 | class ClassAttribute < Transformation 5 | def transform(doc) 6 | with_svg(doc) do |svg| 7 | classes = (svg["class"] || "").split(" ") 8 | classes << value 9 | svg["class"] = classes.join(" ") 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/inline_svg/transform_pipeline/transformations/no_comment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg::TransformPipeline 4 | module Transformations 5 | class NoComment < Transformation 6 | def transform(doc) 7 | with_svg(doc) do |svg| 8 | svg.xpath("//comment()").each do |comment| 9 | comment.remove 10 | end 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/inline_svg/asset_file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg 4 | class AssetFile 5 | class FileNotFound < IOError; end 6 | UNREADABLE_PATH = '' 7 | 8 | def self.named(filename) 9 | asset_path = FindsAssetPaths.by_filename(filename) 10 | File.read(asset_path || UNREADABLE_PATH) 11 | rescue Errno::ENOENT 12 | raise FileNotFound.new("Asset not found: #{asset_path}") 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/inline_svg/finds_asset_paths.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg 4 | class FindsAssetPaths 5 | def self.by_filename(filename) 6 | asset = configured_asset_finder.find_asset(filename) 7 | asset.try(:pathname) || asset.try(:filename) 8 | end 9 | 10 | def self.configured_asset_finder 11 | Thread.current[:inline_svg_asset_finder] || InlineSvg.configuration.asset_finder 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/inline_svg/propshaft_asset_finder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg 4 | class PropshaftAssetFinder 5 | def self.find_asset(filename) 6 | new(filename) 7 | end 8 | 9 | def initialize(filename) 10 | @filename = filename 11 | end 12 | 13 | def pathname 14 | asset_path = ::Rails.application.assets.load_path.find(@filename) 15 | asset_path.path unless asset_path.nil? 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/inline_svg/transform_pipeline/transformations/title.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg::TransformPipeline::Transformations 4 | class Title < Transformation 5 | def transform(doc) 6 | with_svg(doc) do |svg| 7 | node = Nokogiri::XML::Node.new("title", doc) 8 | node.content = value 9 | 10 | svg.search("title").each { |node| node.remove } 11 | svg.prepend_child(node) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/inline_svg/transform_pipeline/transformations/description.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg::TransformPipeline::Transformations 4 | class Description < Transformation 5 | def transform(doc) 6 | with_svg(doc) do |svg| 7 | node = Nokogiri::XML::Node.new("desc", doc) 8 | node.content = value 9 | 10 | svg.search("desc").each { |node| node.remove } 11 | svg.prepend_child(node) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/inline_svg/id_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'digest' 4 | 5 | module InlineSvg 6 | class IdGenerator 7 | class Randomness 8 | require "securerandom" 9 | def self.call 10 | SecureRandom.hex(10) 11 | end 12 | end 13 | 14 | def self.generate(base, salt, randomness: Randomness) 15 | bytes = Digest::SHA1.digest("#{base}-#{salt}-#{randomness.call}") 16 | 'a' + Digest.hexencode(bytes).to_i(16).to_s(36) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in inline_svg.gemspec 6 | gemspec 7 | 8 | gem "bundler" 9 | gem "pry" 10 | gem "rake" 11 | gem "rspec" 12 | 13 | gem "rubocop", "1.75.3", require: false 14 | gem "rubocop-packaging", "0.6.0", require: false 15 | gem "rubocop-performance", "1.25.0", require: false 16 | gem "rubocop-rake", "0.7.1", require: false 17 | gem "rubocop-rspec", "3.6.0", require: false 18 | 19 | gem "simplecov", require: false 20 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | plugins: 4 | - rubocop-packaging 5 | - rubocop-performance 6 | - rubocop-rake 7 | - rubocop-rspec 8 | 9 | AllCops: 10 | TargetRubyVersion: 3.1 11 | NewCops: enable 12 | DisplayStyleGuide: true 13 | ExtraDetails: true 14 | 15 | # https://github.com/jamesmartin/inline_svg/pull/171/files#r1798763446 16 | Style/EachWithObject: 17 | Enabled: false 18 | 19 | # https://github.com/jamesmartin/inline_svg/pull/177#issuecomment-2530239552 20 | RSpec/DescribedClass: 21 | EnforcedStyle: explicit 22 | -------------------------------------------------------------------------------- /lib/inline_svg/transform_pipeline.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg 4 | module TransformPipeline 5 | def self.generate_html_from(svg_file, transform_params) 6 | document = Nokogiri::XML::Document.parse(svg_file) 7 | Transformations.lookup(transform_params).reduce(document) do |doc, transformer| 8 | transformer.transform(doc) 9 | end.to_html.strip 10 | end 11 | end 12 | end 13 | 14 | require 'nokogiri' 15 | 16 | require_relative 'id_generator' 17 | require_relative 'transform_pipeline/transformations' 18 | -------------------------------------------------------------------------------- /lib/inline_svg/transform_pipeline/transformations/size.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg::TransformPipeline::Transformations 4 | class Size < Transformation 5 | def transform(doc) 6 | with_svg(doc) do |svg| 7 | svg["width"] = width_of(value) 8 | svg["height"] = height_of(value) 9 | end 10 | end 11 | 12 | def width_of(value) 13 | value.split("*").map(&:strip)[0] 14 | end 15 | 16 | def height_of(value) 17 | value.split("*").map(&:strip)[1] || width_of(value) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/transformation_pipeline/transformations/width_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe InlineSvg::TransformPipeline::Transformations::Width do 6 | it "adds width attribute to a SVG document" do 7 | document = Nokogiri::XML::Document.parse('Some document') 8 | transformation = InlineSvg::TransformPipeline::Transformations::Width.create_with_value("5%") 9 | 10 | expect(transformation.transform(document).to_html).to eq( 11 | "Some document\n" 12 | ) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/webpack_asset_finder_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe InlineSvg::WebpackAssetFinder do 6 | context "when the file is not found" do 7 | it "returns nil" do 8 | stub_const('Rails', double('Rails').as_null_object) 9 | stub_const('Shakapacker', double('Shakapacker').as_null_object) 10 | expect(Shakapacker.manifest).to receive(:lookup).with('some-file').and_return(nil) 11 | 12 | expect(InlineSvg::WebpackAssetFinder.find_asset('some-file').pathname).to be_nil 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/transformation_pipeline/transformations/id_attribute_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe InlineSvg::TransformPipeline::Transformations::IdAttribute do 6 | it "adds an id attribute to a SVG document" do 7 | document = Nokogiri::XML::Document.parse('Some document') 8 | transformation = InlineSvg::TransformPipeline::Transformations::IdAttribute.create_with_value("some-id") 9 | 10 | expect(transformation.transform(document).to_html).to eq( 11 | "Some document\n" 12 | ) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/transformation_pipeline/transformations/view_box_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe InlineSvg::TransformPipeline::Transformations::ViewBox do 6 | it "adds viewBox attribute to a SVG document" do 7 | document = Nokogiri::XML::Document.parse('Some document') 8 | transformation = 9 | InlineSvg::TransformPipeline::Transformations::ViewBox 10 | .create_with_value("0 0 100 100") 11 | expect(transformation.transform(document).to_html).to eq( 12 | "Some document\n" 13 | ) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/inline_svg/transform_pipeline/transformations/data_attributes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg::TransformPipeline::Transformations 4 | class DataAttributes < Transformation 5 | def transform(doc) 6 | with_svg(doc) do |svg| 7 | with_valid_hash_from(value).each_pair do |name, data| 8 | svg["data-#{dasherize(name)}"] = data 9 | end 10 | end 11 | end 12 | 13 | private 14 | 15 | def with_valid_hash_from(hash) 16 | Hash.try_convert(hash) || {} 17 | end 18 | 19 | def dasherize(string) 20 | string.to_s.tr('_', "-") 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/transformation_pipeline/transformations/aria_hidden_attribute_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe InlineSvg::TransformPipeline::Transformations::AriaHiddenAttribute do 6 | it "adds an aria-hidden='true' attribute to a SVG document" do 7 | document = Nokogiri::XML::Document.parse('Some document') 8 | transformation = InlineSvg::TransformPipeline::Transformations::AriaHiddenAttribute.create_with_value(true) 9 | 10 | expect(transformation.transform(document).to_html).to eq( 11 | "Some document\n" 12 | ) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/transformation_pipeline/transformations/preserve_aspect_ratio_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe InlineSvg::TransformPipeline::Transformations::PreserveAspectRatio do 6 | it "adds preserveAspectRatio attribute to a SVG document" do 7 | document = Nokogiri::XML::Document.parse('Some document') 8 | transformation = InlineSvg::TransformPipeline::Transformations::PreserveAspectRatio.create_with_value("xMaxYMax meet") 9 | expect(transformation.transform(document).to_html).to eq( 10 | "Some document\n" 11 | ) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /.github/workflows/rubocop.yml: -------------------------------------------------------------------------------- 1 | name: RuboCop 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | ruby-version: ['3.4'] 19 | 20 | continue-on-error: ${{ matrix.channel != 'stable' }} 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Set up Ruby 25 | uses: ruby/setup-ruby@v1 26 | with: 27 | ruby-version: ${{ matrix.ruby-version }} 28 | bundler-cache: true 29 | - name: Test with Rake 30 | run: | 31 | bundle exec rubocop --format github 32 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | ruby-version: ['3.1', '3.2', '3.3', '3.4'] 19 | channel: ['stable'] 20 | 21 | include: 22 | - ruby-version: 'head' 23 | channel: 'experimental' 24 | 25 | continue-on-error: ${{ matrix.channel != 'stable' }} 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Set up Ruby 30 | uses: ruby/setup-ruby@v1 31 | with: 32 | ruby-version: ${{ matrix.ruby-version }} 33 | bundler-cache: true 34 | - name: Test with Rake 35 | run: | 36 | bundle exec rake 37 | -------------------------------------------------------------------------------- /spec/files/static_assets/assets0/some-document.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/inline_svg/railtie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails/railtie' 4 | module InlineSvg 5 | class Railtie < ::Rails::Railtie 6 | initializer "inline_svg.action_view" do |app| 7 | ActiveSupport.on_load :action_view do 8 | require_relative "action_view/helpers" 9 | include InlineSvg::ActionView::Helpers 10 | end 11 | end 12 | 13 | config.after_initialize do |app| 14 | InlineSvg.configure do |config| 15 | # Configure the asset_finder: 16 | # Only set this when a user-configured asset finder has not been 17 | # configured already. 18 | if config.asset_finder.nil? 19 | # In default Rails apps, this will be a fully operational 20 | # Sprockets::Environment instance 21 | config.asset_finder = app.instance_variable_get(:@assets) 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/transformation_pipeline/transformations/height_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe InlineSvg::TransformPipeline::Transformations::Height do 6 | it "adds height attribute to a SVG document" do 7 | document = Nokogiri::XML::Document.parse('Some document') 8 | transformation = InlineSvg::TransformPipeline::Transformations::Height.create_with_value("5%") 9 | 10 | expect(transformation.transform(document).to_html).to eq( 11 | "Some document\n" 12 | ) 13 | end 14 | 15 | it "handles documents without SVG root elements" do 16 | document = Nokogiri::XML::Document.parse("barSome document") 17 | transformation = InlineSvg::TransformPipeline::Transformations::Height.create_with_value("5%") 18 | 19 | expect(transformation.transform(document).to_html).to eq( 20 | "bar\n" 21 | ) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/transformation_pipeline/transformations/size_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe InlineSvg::TransformPipeline::Transformations::Size do 6 | it "adds width and height attributes to a SVG document" do 7 | document = Nokogiri::XML::Document.parse('Some document') 8 | transformation = InlineSvg::TransformPipeline::Transformations::Size.create_with_value("5% * 5%") 9 | 10 | expect(transformation.transform(document).to_html).to eq( 11 | "Some document\n" 12 | ) 13 | end 14 | 15 | it "adds the same width and height value when only passed one attribute" do 16 | document = Nokogiri::XML::Document.parse('Some document') 17 | transformation = InlineSvg::TransformPipeline::Transformations::Size.create_with_value("5%") 18 | 19 | expect(transformation.transform(document).to_html).to eq( 20 | "Some document\n" 21 | ) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/inline_svg/static_asset_finder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "pathname" 4 | 5 | # Naive fallback asset finder for when sprockets >= 3.0 && 6 | # config.assets.precompile = false 7 | # Thanks to @ryanswood for the original code: 8 | # https://github.com/jamesmartin/inline_svg/commit/661bbb3bef7d1b4bd6ccd63f5f018305797b9509 9 | module InlineSvg 10 | class StaticAssetFinder 11 | def self.find_asset(filename) 12 | new(filename) 13 | end 14 | 15 | def initialize(filename) 16 | @filename = filename 17 | end 18 | 19 | def pathname 20 | if ::Rails.application.config.assets.compile 21 | asset = ::Rails.application.assets[@filename] 22 | Pathname.new(asset.filename) if asset.present? 23 | else 24 | manifest = ::Rails.application.assets_manifest 25 | asset_path = manifest.assets[@filename] 26 | unless asset_path.nil? 27 | ::Rails.root.join(manifest.directory, asset_path) 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/propshaft_asset_finder_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe InlineSvg::PropshaftAssetFinder do 6 | context "when the file is not found" do 7 | it "returns nil" do 8 | stub_const('Rails', double('Rails').as_null_object) 9 | expect(Rails.application.assets.load_path).to receive(:find).with('some-file').and_return(nil) 10 | 11 | expect(InlineSvg::PropshaftAssetFinder.find_asset('some-file').pathname).to be_nil 12 | end 13 | end 14 | 15 | context "when the file is found" do 16 | it "returns fully qualified file paths from Propshaft" do 17 | stub_const('Rails', double('Rails').as_null_object) 18 | asset = double('Asset') 19 | expect(asset).to receive(:path).and_return(Pathname.new('/full/path/to/some-file')) 20 | expect(Rails.application.assets.load_path).to receive(:find).with('some-file').and_return(asset) 21 | 22 | expect(InlineSvg::PropshaftAssetFinder.find_asset('some-file').pathname).to eq Pathname('/full/path/to/some-file') 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/transformation_pipeline/transformations/class_attribute_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe InlineSvg::TransformPipeline::Transformations::ClassAttribute do 6 | it "adds a class attribute to a SVG document" do 7 | document = Nokogiri::XML::Document.parse('Some document') 8 | transformation = InlineSvg::TransformPipeline::Transformations::ClassAttribute.create_with_value("some-class") 9 | 10 | expect(transformation.transform(document).to_html).to eq( 11 | "Some document\n" 12 | ) 13 | end 14 | 15 | it "preserves existing class attributes on a SVG document" do 16 | document = Nokogiri::XML::Document.parse('Some document') 17 | transformation = InlineSvg::TransformPipeline::Transformations::ClassAttribute.create_with_value("some-class") 18 | 19 | expect(transformation.transform(document).to_html).to eq( 20 | "Some document\n" 21 | ) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /inline_svg.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'inline_svg/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "inline_svg" 9 | spec.version = InlineSvg::VERSION 10 | spec.authors = ["James Martin"] 11 | spec.email = ["inline_svg@jmrtn.com"] 12 | spec.summary = 'Embeds an SVG document, inline.' 13 | spec.description = 'Get an SVG into your view and then style it with CSS.' 14 | spec.homepage = "https://github.com/jamesmartin/inline_svg" 15 | spec.license = "MIT" 16 | 17 | spec.files = Dir.glob('{CHANGELOG.md,LICENSE.txt,README.md,lib/**/*.rb}', File::FNM_DOTMATCH) 18 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 19 | spec.require_paths = ["lib"] 20 | 21 | spec.metadata['rubygems_mfa_required'] = 'true' 22 | spec.required_ruby_version = '>= 3.1' 23 | 24 | spec.add_dependency "activesupport", ">= 7.0" 25 | spec.add_dependency "nokogiri", ">= 1.16" 26 | end 27 | -------------------------------------------------------------------------------- /lib/inline_svg/transform_pipeline/transformations/aria_attributes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg::TransformPipeline::Transformations 4 | class AriaAttributes < Transformation 5 | def transform(doc) 6 | with_svg(doc) do |svg| 7 | # Add role 8 | svg["role"] = "img" 9 | 10 | # Build aria-labelledby string 11 | aria_elements = [] 12 | svg.search("title").each do |element| 13 | aria_elements << element["id"] = element_id_for("title", element) 14 | end 15 | 16 | svg.search("desc").each do |element| 17 | aria_elements << element["id"] = element_id_for("desc", element) 18 | end 19 | 20 | if aria_elements.any? 21 | svg["aria-labelledby"] = aria_elements.join(" ") 22 | end 23 | end 24 | end 25 | 26 | def element_id_for(base, element) 27 | if element["id"].nil? 28 | InlineSvg::IdGenerator.generate(base, element.text) 29 | else 30 | InlineSvg::IdGenerator.generate(element["id"], element.text) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/asset_file_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe InlineSvg::AssetFile do 6 | it "reads data from a file, after qualifying a full path" do 7 | example_svg_path = File.expand_path(__FILE__, 'files/example.svg') 8 | expect(InlineSvg::FindsAssetPaths).to receive(:by_filename).with('some filename').and_return example_svg_path 9 | 10 | expect(InlineSvg::AssetFile.named('some filename')).to include('This is a test') 11 | end 12 | 13 | it "complains when the file cannot be read" do 14 | allow(InlineSvg::FindsAssetPaths).to receive(:by_filename).and_return('/this/path/does/not/exist') 15 | 16 | expect do 17 | InlineSvg::AssetFile.named('some missing file') 18 | end.to raise_error InlineSvg::AssetFile::FileNotFound 19 | end 20 | 21 | it "complains when the file path was not found" do 22 | allow(InlineSvg::FindsAssetPaths).to receive(:by_filename).and_return(nil) 23 | 24 | expect do 25 | InlineSvg::AssetFile.named('some missing file') 26 | end.to raise_error InlineSvg::AssetFile::FileNotFound 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/static_asset_finder_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe InlineSvg::StaticAssetFinder do 6 | context "when the file is not found" do 7 | it "returns nil" do 8 | stub_const('Rails', double('Rails').as_null_object) 9 | expect(Rails.application.config.assets).to receive(:compile).and_return(true) 10 | 11 | expect(InlineSvg::StaticAssetFinder.find_asset('some-file').pathname).to be_nil 12 | end 13 | end 14 | 15 | context "when the file is found" do 16 | it "returns fully qualified file path from Sprockets" do 17 | stub_const('Rails', double('Rails').as_null_object) 18 | expect(Rails.application.config.assets).to receive(:compile).and_return(true) 19 | pathname = Pathname.new('/full/path/to/some-file') 20 | asset = double('Asset') 21 | expect(asset).to receive(:filename).and_return(pathname) 22 | expect(Rails.application.assets).to receive(:[]).with('some-file').and_return(asset) 23 | 24 | expect(InlineSvg::StaticAssetFinder.find_asset('some-file').pathname).to eq(pathname) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/transformation_pipeline/transformations/style_attribute_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe InlineSvg::TransformPipeline::Transformations::ClassAttribute do 6 | it "adds a style attribute to a SVG document" do 7 | document = Nokogiri::XML::Document.parse('Some document') 8 | transformation = 9 | InlineSvg::TransformPipeline::Transformations::StyleAttribute 10 | .create_with_value("padding: 10px") 11 | 12 | expect(transformation.transform(document).to_html).to eq( 13 | "Some document\n" 14 | ) 15 | end 16 | 17 | it "preserves existing style attributes on a SVG document" do 18 | xml = 'Some document' 19 | document = Nokogiri::XML::Document.parse(xml) 20 | transformation = 21 | InlineSvg::TransformPipeline::Transformations::StyleAttribute 22 | .create_with_value("padding: 10px") 23 | 24 | expect(transformation.transform(document).to_html).to eq( 25 | "Some document\n" 26 | ) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 James Martin 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/inline_svg/transform_pipeline/transformations/transformation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg::TransformPipeline::Transformations 4 | class Transformation 5 | def self.create_with_value(value) 6 | new(value) 7 | end 8 | 9 | attr_reader :value 10 | 11 | def initialize(value) 12 | @value = value 13 | end 14 | 15 | def transform(*) 16 | raise "#transform should be implemented by subclasses of Transformation" 17 | end 18 | 19 | # Parses a document and yields the contained SVG nodeset to the given block 20 | # if it exists. 21 | # 22 | # Returns a Nokogiri::XML::Document. 23 | def with_svg(doc) 24 | doc = Nokogiri::XML::Document.parse( 25 | doc.to_html(encoding: "UTF-8"), nil, "UTF-8" 26 | ) 27 | svg = doc.at_css "svg" 28 | yield svg if svg && block_given? 29 | doc 30 | end 31 | end 32 | 33 | class NullTransformation < Transformation 34 | def transform(doc) 35 | doc 36 | end 37 | end 38 | end 39 | 40 | module InlineSvg 41 | class CustomTransformation < InlineSvg::TransformPipeline::Transformations::Transformation 42 | # Inherit from this class to keep custom transformation class definitions short 43 | # E.g. 44 | # class MyTransform < InlineSvg::CustomTransformation 45 | # def transform(doc) 46 | # # Your code here... 47 | # end 48 | # end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/transformation_pipeline/transformations/description_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe InlineSvg::TransformPipeline::Transformations::Description do 6 | it "adds a desc element to the SVG document" do 7 | document = Nokogiri::XML::Document.parse('Some document') 8 | transformation = InlineSvg::TransformPipeline::Transformations::Description.create_with_value("Some Description") 9 | 10 | expect(transformation.transform(document).to_html).to eq( 11 | "Some DescriptionSome document\n" 12 | ) 13 | end 14 | 15 | it "overwrites the content of an existing description element" do 16 | document = Nokogiri::XML::Document.parse('My DescriptionSome document') 17 | transformation = InlineSvg::TransformPipeline::Transformations::Description.create_with_value("Some Description") 18 | 19 | expect(transformation.transform(document).to_html).to eq( 20 | "Some DescriptionSome document\n" 21 | ) 22 | end 23 | 24 | it "handles empty SVG documents" do 25 | document = Nokogiri::XML::Document.parse('') 26 | transformation = InlineSvg::TransformPipeline::Transformations::Description.create_with_value("Some Description") 27 | 28 | expect(transformation.transform(document).to_html).to eq( 29 | "Some Description\n" 30 | ) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/transformation_pipeline/transformations/transformation_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe InlineSvg::TransformPipeline::Transformations::Transformation do 6 | describe "#with_svg" do 7 | it "returns a Nokogiri::XML::Document representing the parsed document fragment" do 8 | document = Nokogiri::XML::Document.parse("Some document") 9 | 10 | transformation = InlineSvg::TransformPipeline::Transformations::Transformation.new(:irrelevant) 11 | expect(transformation.with_svg(document).to_html).to eq( 12 | "Some document\n" 13 | ) 14 | end 15 | 16 | it "yields to the block when the document contains an SVG element" do 17 | document = Nokogiri::XML::Document.parse("Some document") 18 | svg = document.at_css("svg") 19 | 20 | transformation = InlineSvg::TransformPipeline::Transformations::Transformation.new(:irrelevant) 21 | 22 | returned_document = nil 23 | expect do |b| 24 | returned_document = transformation.with_svg(document, &b) 25 | end.to yield_control 26 | 27 | expect(returned_document.to_s).to match(svg) 28 | end 29 | 30 | it "does not yield if the document does not contain an SVG element at the root" do 31 | document = Nokogiri::XML::Document.parse("barSome document") 32 | 33 | transformation = InlineSvg::TransformPipeline::Transformations::Transformation.new(:irrelevant) 34 | 35 | expect do |b| 36 | transformation.with_svg(document, &b) 37 | end.not_to yield_control 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/transformation_pipeline/transformations/title_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe InlineSvg::TransformPipeline::Transformations::Title do 6 | it "adds a title element as the first element in the SVG document" do 7 | document = Nokogiri::XML::Document.parse('Some document') 8 | transformation = InlineSvg::TransformPipeline::Transformations::Title.create_with_value("Some Title") 9 | 10 | expect(transformation.transform(document).to_html).to eq( 11 | "Some TitleSome document\n" 12 | ) 13 | end 14 | 15 | it "overwrites the content of an existing title element" do 16 | document = Nokogiri::XML::Document.parse('My TitleSome document') 17 | transformation = InlineSvg::TransformPipeline::Transformations::Title.create_with_value("Some Title") 18 | 19 | expect(transformation.transform(document).to_html).to eq( 20 | "Some TitleSome document\n" 21 | ) 22 | end 23 | 24 | it "handles empty SVG documents" do 25 | document = Nokogiri::XML::Document.parse('') 26 | transformation = InlineSvg::TransformPipeline::Transformations::Title.create_with_value("Some Title") 27 | 28 | expect(transformation.transform(document).to_html).to eq( 29 | "Some Title\n" 30 | ) 31 | end 32 | 33 | it "handles non-ASCII characters" do 34 | document = Nokogiri::XML::Document.parse('Some document') 35 | transformation = InlineSvg::TransformPipeline::Transformations::Title.create_with_value("åäö") 36 | 37 | expect(transformation.transform(document).to_html).to eq( 38 | "åäöSome document\n" 39 | ) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/inline_svg/webpack_asset_finder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg 4 | class WebpackAssetFinder 5 | def self.find_asset(filename) 6 | new(filename) 7 | end 8 | 9 | def initialize(filename) 10 | @filename = filename 11 | manifest_lookup = asset_helper.manifest.lookup(@filename) 12 | @asset_path = manifest_lookup.present? ? URI(manifest_lookup).path : "" 13 | end 14 | 15 | def pathname 16 | return if @asset_path.blank? 17 | 18 | if asset_helper.dev_server.running? 19 | dev_server_asset(@asset_path) 20 | elsif asset_helper.config.public_path.present? 21 | File.join(asset_helper.config.public_path, @asset_path) 22 | end 23 | end 24 | 25 | private 26 | 27 | def asset_helper 28 | @asset_helper ||= ::Shakapacker 29 | end 30 | 31 | def dev_server_asset(file_path) 32 | asset = fetch_from_dev_server(file_path) 33 | 34 | begin 35 | Tempfile.new(file_path).tap do |file| 36 | file.binmode 37 | file.write(asset) 38 | file.rewind 39 | end 40 | rescue StandardError => e 41 | Rails.logger.error "[inline_svg] Error creating tempfile for #{@filename}: #{e}" 42 | raise 43 | end 44 | end 45 | 46 | def fetch_from_dev_server(file_path) 47 | http = Net::HTTP.new(asset_helper.dev_server.host, asset_helper.dev_server.port) 48 | http.use_ssl = asset_helper.dev_server.protocol == "https" 49 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 50 | 51 | http.request(Net::HTTP::Get.new(file_path)).body 52 | rescue StandardError => e 53 | Rails.logger.error "[inline_svg] Error fetching #{@filename} from webpack-dev-server: #{e}" 54 | raise 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /.github/workflows/integration_test.yml: -------------------------------------------------------------------------------- 1 | name: Integration Tests 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | test-branch: [rails7, rails7-shakapacker] 18 | timeout-minutes: 20 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | - name: Checkout test app 23 | uses: actions/checkout@v4 24 | with: 25 | repository: jamesmartin/inline_svg_test_app 26 | ref: ${{ matrix.test-branch }} 27 | path: test_app 28 | - name: Set up Ruby 29 | uses: ruby/setup-ruby@v1 30 | with: 31 | ruby-version: 3.3.4 32 | bundler-cache: true 33 | - name: Build local gem 34 | run: | 35 | bundle exec rake build 36 | - name: Use the local gem in the test App 37 | id: uselocalgem 38 | uses: jacobtomlinson/gha-find-replace@v3 39 | with: 40 | find: "gem 'inline_svg'" 41 | replace: "gem 'inline_svg', path: '${{github.workspace}}'" 42 | - name: Check local gem in use 43 | run: | 44 | test "${{ steps.uselocalgem.outputs.modifiedFiles }}" != "0" 45 | grep "inline_svg" $GITHUB_WORKSPACE/test_app/Gemfile 46 | - name: Bundle 47 | run: | 48 | cd $GITHUB_WORKSPACE/test_app 49 | bundle install --jobs 4 --retry 3 50 | - name: Set up Node.js 20.x 51 | uses: actions/setup-node@v4 52 | with: 53 | node-version: 20 54 | if: matrix.test-branch == 'rails7-shakapacker' 55 | - name: Generate Shakapacker config 56 | run: | 57 | cd $GITHUB_WORKSPACE/test_app 58 | yarn install --check-files 59 | bundle exec rake shakapacker:compile 60 | if: matrix.test-branch == 'rails7-shakapacker' 61 | - name: Test 62 | run: | 63 | cd $GITHUB_WORKSPACE/test_app 64 | bundle exec rake test 65 | -------------------------------------------------------------------------------- /spec/transformation_pipeline/transformations/data_attributes_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe InlineSvg::TransformPipeline::Transformations::DataAttributes do 6 | it "adds a data attribute to a SVG document" do 7 | document = Nokogiri::XML::Document.parse('Some document') 8 | transformation = InlineSvg::TransformPipeline::Transformations::DataAttributes.create_with_value({ some: "value" }) 9 | 10 | expect(transformation.transform(document).to_html).to eq( 11 | "Some document\n" 12 | ) 13 | end 14 | 15 | it "dasherizes the data attribute name" do 16 | document = Nokogiri::XML::Document.parse('Some document') 17 | transformation = InlineSvg::TransformPipeline::Transformations::DataAttributes.create_with_value({ some_name: "value" }) 18 | 19 | expect(transformation.transform(document).to_html).to eq( 20 | "Some document\n" 21 | ) 22 | end 23 | 24 | it "dasherizes a data attribute name with multiple parts" do 25 | document = Nokogiri::XML::Document.parse('Some document') 26 | transformation = InlineSvg::TransformPipeline::Transformations::DataAttributes.create_with_value({ some_other_name: "value" }) 27 | 28 | expect(transformation.transform(document).to_html).to eq( 29 | "Some document\n" 30 | ) 31 | end 32 | 33 | context "when multiple data attributes are supplied" do 34 | it "adds data attributes to the SVG for each supplied value" do 35 | document = Nokogiri::XML::Document.parse('Some document') 36 | transformation = InlineSvg::TransformPipeline::Transformations::DataAttributes 37 | .create_with_value({ some: "value", other: "thing" }) 38 | 39 | expect(transformation.transform(document).to_html).to eq( 40 | "Some document\n" 41 | ) 42 | end 43 | end 44 | 45 | context "when a non-hash is supplied" do 46 | it "does not update the SVG document" do 47 | document = Nokogiri::XML::Document.parse('Some document') 48 | transformation = InlineSvg::TransformPipeline::Transformations::DataAttributes 49 | .create_with_value("some non-hash") 50 | 51 | expect(transformation.transform(document).to_html).to eq( 52 | "Some document\n" 53 | ) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/io_resource_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'tempfile' 4 | 5 | require 'spec_helper' 6 | 7 | RSpec.describe InlineSvg::IOResource do 8 | it "support api methods" do 9 | is_expected.to respond_to(:===, :read) 10 | end 11 | 12 | describe '#===' do 13 | context 'return true' do 14 | it "for IO object" do 15 | read_io, write_io = IO.pipe 16 | expect(subject === read_io).to be true 17 | expect(subject === write_io).to be true 18 | end 19 | 20 | it "for StringIO object" do 21 | expect(subject === StringIO.new).to be true 22 | end 23 | 24 | it "for File object" do 25 | expect(subject === File.new("#{Dir.tmpdir}/testfile", "w")).to be true 26 | end 27 | 28 | it "for Tempfile object" do 29 | expect(subject === Tempfile.new).to be true 30 | end 31 | end 32 | 33 | context 'return false' do 34 | it "for String object" do 35 | expect(subject === "string/filename").to be false 36 | end 37 | end 38 | end 39 | 40 | describe '#read' do 41 | tests = proc do 42 | it "closed raise error" do 43 | rio.close 44 | expect do 45 | subject.read(rio) 46 | end.to raise_error(IOError) 47 | end 48 | 49 | it "empty" do 50 | rio.read 51 | expect(subject.read(rio)).to eq '' 52 | end 53 | 54 | it "twice" do 55 | expect(subject.read(rio)).to eq answer 56 | expect(subject.read(rio)).to eq answer 57 | end 58 | 59 | it "write only raise error" do 60 | expect do 61 | subject.read wio 62 | end.to raise_error(IOError) 63 | end 64 | end 65 | 66 | context 'IO object' do 67 | let(:answer) { 'read' } 68 | let(:rio) { StringIO.new(answer, 'r') } 69 | let(:wio) { StringIO.new(+'write', 'w') } 70 | 71 | instance_exec(&tests) 72 | end 73 | 74 | context 'File object' do 75 | let(:file_path) { File.expand_path('files/example.svg', __dir__) } 76 | let(:answer) { File.read(file_path) } 77 | let(:rio) { File.new(file_path, 'r') } 78 | let(:wio) { File.new(File::NULL, 'w') } 79 | 80 | instance_exec(&tests) 81 | it 'has non empty body' do 82 | expect(answer).not_to eq '' 83 | end 84 | end 85 | 86 | context 'Tempfile object' do 87 | let(:answer) { 'read' } 88 | let(:rio) do 89 | Tempfile.new.tap do |f| 90 | f.write(answer) 91 | f.rewind 92 | end 93 | end 94 | let(:wio) { File.new(File::NULL, 'w') } # Tempfile cannot be created for write only mode 95 | 96 | instance_exec(&tests) 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/inline_svg/action_view/helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'action_view/helpers' if defined?(Rails) 4 | require 'action_view/context' if defined?(Rails) 5 | 6 | module InlineSvg 7 | module ActionView 8 | module Helpers 9 | def inline_svg_tag(filename, transform_params = {}) 10 | with_asset_finder(InlineSvg.configuration.asset_finder) do 11 | render_inline_svg(filename, transform_params) 12 | end 13 | end 14 | 15 | def inline_svg_pack_tag(filename, transform_params = {}) 16 | with_asset_finder(InlineSvg::WebpackAssetFinder) do 17 | render_inline_svg(filename, transform_params) 18 | end 19 | end 20 | 21 | def inline_svg(filename, transform_params = {}) 22 | render_inline_svg(filename, transform_params) 23 | end 24 | 25 | private 26 | 27 | def render_inline_svg(filename, transform_params = {}) 28 | begin 29 | svg_file = read_svg(filename) 30 | rescue InlineSvg::AssetFile::FileNotFound => error 31 | raise error if InlineSvg.configuration.raise_on_file_not_found? 32 | return placeholder(filename) unless transform_params[:fallback].present? 33 | 34 | if transform_params[:fallback].present? 35 | begin 36 | svg_file = read_svg(transform_params[:fallback]) 37 | rescue InlineSvg::AssetFile::FileNotFound 38 | placeholder(filename) 39 | end 40 | end 41 | end 42 | 43 | InlineSvg::TransformPipeline.generate_html_from(svg_file, transform_params).html_safe 44 | end 45 | 46 | def read_svg(filename) 47 | if InlineSvg::IOResource === filename 48 | InlineSvg::IOResource.read filename 49 | else 50 | configured_asset_file.named filename 51 | end 52 | end 53 | 54 | def placeholder(filename) 55 | css_class = InlineSvg.configuration.svg_not_found_css_class 56 | not_found_message = "'#{ERB::Util.html_escape_once(filename)}' #{extension_hint(filename)}" 57 | 58 | if css_class.nil? 59 | "".html_safe 60 | else 61 | "".html_safe 62 | end 63 | end 64 | 65 | def configured_asset_file 66 | InlineSvg.configuration.asset_file 67 | end 68 | 69 | def with_asset_finder(asset_finder) 70 | Thread.current[:inline_svg_asset_finder] = asset_finder 71 | yield 72 | ensure 73 | Thread.current[:inline_svg_asset_finder] = nil 74 | end 75 | 76 | def extension_hint(filename) 77 | filename.end_with?(".svg") ? "" : "(Try adding .svg to your filename) " 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/inline_svg/cached_asset_file.rb: -------------------------------------------------------------------------------- 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_by(&: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 | 36 | # Internal: Finds the key for a given asset name (using a Regex). In the 37 | # event of an ambiguous asset_name matching multiple assets, this method 38 | # ranks the matches by their full file path, choosing the shortest (most 39 | # exact) match over all others. 40 | # 41 | # Returns a String representing the key for the named asset or nil if there 42 | # is no match. 43 | def key_for_asset(asset_name) 44 | @sorted_asset_keys.find { |k| k.include?(asset_name) } 45 | end 46 | 47 | # Internal: Recursively descends through current_paths reading each file it 48 | # finds and adding them to the accumulator if the fullpath of the file 49 | # matches all configured filters. 50 | # 51 | # acc - Hash representing the accumulated assets keyed by full path 52 | # paths - Pathname representing the current node in the directory 53 | # structure to consider 54 | # 55 | # Returns a Hash containing the contents of each asset, keyed by fullpath 56 | # to the asset. 57 | def read_assets(acc, paths) 58 | paths.each_child do |child| 59 | if child.directory? 60 | read_assets(acc, child) 61 | elsif child.readable_real? 62 | acc[child.to_s] = File.read(child) if matches_all_filters?(child) 63 | end 64 | end 65 | acc 66 | end 67 | 68 | def matches_all_filters?(path) 69 | filters.all? { |f| f.match(path.to_s) } 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/cached_asset_file_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe InlineSvg::CachedAssetFile do 6 | let(:fixture_path) { Pathname.new(File.expand_path('files/static_assets', __dir__)) } 7 | 8 | it "loads assets under configured paths" do 9 | known_document = File.read(fixture_path.join("assets0", "known-document.svg")) 10 | 11 | asset_loader = InlineSvg::CachedAssetFile.new(paths: fixture_path.join("assets0")) 12 | 13 | expect(asset_loader.named("known-document.svg")).to eq(known_document) 14 | end 15 | 16 | it "does not include assets outside of configured paths" do 17 | asset_loader = InlineSvg::CachedAssetFile.new(paths: fixture_path.join("assets0")) 18 | 19 | expect(fixture_path.join("assets1", "other-document.svg")).to be_file 20 | expect do 21 | asset_loader.named("other-document.svg") 22 | end.to raise_error InlineSvg::AssetFile::FileNotFound 23 | end 24 | 25 | it "differentiates two files with the same name" do 26 | known_document_0 = File.read(fixture_path.join("assets0", "known-document.svg")) 27 | known_document_1 = File.read(fixture_path.join("assets1", "known-document.svg")) 28 | 29 | expect(known_document_0).not_to eq(known_document_1) 30 | 31 | asset_loader = InlineSvg::CachedAssetFile.new(paths: fixture_path) 32 | 33 | expect(known_document_0).to eq(asset_loader.named("assets0/known-document.svg")) 34 | expect(known_document_1).to eq(asset_loader.named("assets1/known-document.svg")) 35 | end 36 | 37 | it "chooses the closest exact matching file when similar files exist in the same path" do 38 | known_document = File.read(fixture_path.join("assets0", "known-document.svg")) 39 | known_document_2 = File.read(fixture_path.join("assets0", "known-document-two.svg")) 40 | 41 | expect(known_document).not_to eq(known_document_2) 42 | 43 | asset_loader = InlineSvg::CachedAssetFile.new(paths: fixture_path.join("assets0"), filters: /\.svg/) 44 | 45 | expect(asset_loader.named("known-document")).to eq(known_document) 46 | expect(asset_loader.named("known-document-two")).to eq(known_document_2) 47 | end 48 | 49 | it "filters wanted files by simple string matching" do 50 | known_document_1 = File.read(fixture_path.join("assets1", "known-document.svg")) 51 | 52 | asset_loader = InlineSvg::CachedAssetFile.new(paths: fixture_path, filters: "assets1") 53 | 54 | expect do 55 | asset_loader.named("assets0/known-document.svg") 56 | end.to raise_error InlineSvg::AssetFile::FileNotFound 57 | 58 | expect(known_document_1).to eq(asset_loader.named("assets1/known-document.svg")) 59 | end 60 | 61 | it "filters wanted files by regex matching" do 62 | known_document_1 = File.read(fixture_path.join("assets1", "known-document.svg")) 63 | 64 | asset_loader = InlineSvg::CachedAssetFile.new(paths: fixture_path, filters: ["assets1", /\.svg/]) 65 | 66 | expect do 67 | asset_loader.named("assets1/some-file.txt") 68 | end.to raise_error InlineSvg::AssetFile::FileNotFound 69 | 70 | expect(known_document_1).to eq(asset_loader.named("assets1/known-document.svg")) 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/inline_svg/transform_pipeline/transformations.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InlineSvg::TransformPipeline::Transformations 4 | # Transformations are run in priority order, lowest number first: 5 | def self.built_in_transformations 6 | { 7 | id: { transform: IdAttribute, priority: 1 }, 8 | desc: { transform: Description, priority: 2 }, 9 | title: { transform: Title, priority: 3 }, 10 | aria: { transform: AriaAttributes }, 11 | aria_hidden: { transform: AriaHiddenAttribute }, 12 | class: { transform: ClassAttribute }, 13 | style: { transform: StyleAttribute }, 14 | data: { transform: DataAttributes }, 15 | nocomment: { transform: NoComment }, 16 | preserve_aspect_ratio: { transform: PreserveAspectRatio }, 17 | size: { transform: Size }, 18 | width: { transform: Width }, 19 | height: { transform: Height }, 20 | view_box: { transform: ViewBox }, 21 | } 22 | end 23 | 24 | def self.custom_transformations 25 | magnify_priorities(InlineSvg.configuration.custom_transformations) 26 | end 27 | 28 | def self.magnify_priorities(transforms) 29 | transforms.inject({}) do |output, (name, definition)| 30 | priority = definition.fetch(:priority, built_in_transformations.size) 31 | 32 | output[name] = definition.merge({ priority: magnify(priority) }) 33 | output 34 | end 35 | end 36 | 37 | def self.magnify(priority = 0) 38 | (priority + 1) * built_in_transformations.size 39 | end 40 | 41 | def self.all_transformations 42 | in_priority_order(built_in_transformations.merge(custom_transformations)) 43 | end 44 | 45 | def self.lookup(transform_params) 46 | return [] unless transform_params.any? || custom_transformations.any? 47 | 48 | transform_params_with_defaults = params_with_defaults(transform_params) 49 | all_transformations.filter_map do |name, definition| 50 | value = transform_params_with_defaults[name] 51 | definition.fetch(:transform, no_transform).create_with_value(value) if value 52 | end 53 | end 54 | 55 | def self.in_priority_order(transforms) 56 | transforms.sort_by { |_, options| options.fetch(:priority, transforms.size) } 57 | end 58 | 59 | def self.params_with_defaults(params) 60 | without_empty_values(all_default_values.merge(params)) 61 | end 62 | 63 | def self.without_empty_values(params) 64 | params.reject { |key, value| value.nil? } 65 | end 66 | 67 | def self.all_default_values 68 | custom_transformations 69 | .values 70 | .reject { |opt| opt[:default_value].nil? } 71 | .map { |opt| [opt[:attribute], opt[:default_value]] } 72 | .inject({}) { |options, attrs| options.merge!(attrs[0] => attrs[1]) } 73 | end 74 | 75 | def self.no_transform 76 | InlineSvg::TransformPipeline::Transformations::NullTransformation 77 | end 78 | end 79 | 80 | require_relative 'transformations/transformation' 81 | require_relative 'transformations/no_comment' 82 | require_relative 'transformations/class_attribute' 83 | require_relative 'transformations/style_attribute' 84 | require_relative 'transformations/title' 85 | require_relative 'transformations/description' 86 | require_relative 'transformations/size' 87 | require_relative 'transformations/height' 88 | require_relative 'transformations/width' 89 | require_relative 'transformations/view_box' 90 | require_relative 'transformations/id_attribute' 91 | require_relative 'transformations/data_attributes' 92 | require_relative 'transformations/preserve_aspect_ratio' 93 | require_relative 'transformations/aria_attributes' 94 | require_relative 'transformations/aria_hidden_attribute' 95 | -------------------------------------------------------------------------------- /lib/inline_svg.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "inline_svg/version" 4 | require_relative "inline_svg/action_view/helpers" 5 | require_relative "inline_svg/asset_file" 6 | require_relative "inline_svg/cached_asset_file" 7 | require_relative "inline_svg/finds_asset_paths" 8 | require_relative "inline_svg/propshaft_asset_finder" 9 | require_relative "inline_svg/static_asset_finder" 10 | require_relative "inline_svg/webpack_asset_finder" 11 | require_relative "inline_svg/transform_pipeline" 12 | require_relative "inline_svg/io_resource" 13 | 14 | require_relative "inline_svg/railtie" if defined?(Rails) 15 | 16 | require 'active_support' 17 | require 'active_support/core_ext/object/blank' 18 | require 'active_support/core_ext/string' 19 | require 'nokogiri' 20 | 21 | module InlineSvg 22 | class Configuration 23 | class Invalid < ArgumentError; end 24 | 25 | attr_reader :asset_file, :asset_finder, :custom_transformations, :svg_not_found_css_class 26 | 27 | def initialize 28 | @custom_transformations = {} 29 | @asset_file = InlineSvg::AssetFile 30 | @svg_not_found_css_class = nil 31 | @raise_on_file_not_found = false 32 | end 33 | 34 | def asset_file=(custom_asset_file) 35 | method = custom_asset_file.method(:named) 36 | if method.arity == 1 37 | @asset_file = custom_asset_file 38 | else 39 | raise InlineSvg::Configuration::Invalid.new("asset_file should implement the #named method with arity 1") 40 | end 41 | rescue NameError 42 | raise InlineSvg::Configuration::Invalid.new("asset_file should implement the #named method") 43 | end 44 | 45 | def asset_finder=(finder) 46 | @asset_finder = if finder.respond_to?(:find_asset) 47 | finder 48 | elsif finder.class.name == "Propshaft::Assembly" 49 | InlineSvg::PropshaftAssetFinder 50 | else 51 | # fallback to a naive static asset finder 52 | # (sprokects >= 3.0 && config.assets.precompile = false 53 | # See: https://github.com/jamesmartin/inline_svg/issues/25 54 | InlineSvg::StaticAssetFinder 55 | end 56 | asset_finder 57 | end 58 | 59 | def svg_not_found_css_class=(css_class) 60 | if css_class.present? && css_class.is_a?(String) 61 | @svg_not_found_css_class = css_class 62 | end 63 | end 64 | 65 | def add_custom_transformation(options) 66 | if incompatible_transformation?(options.fetch(:transform)) 67 | raise InlineSvg::Configuration::Invalid.new("#{options.fetch(:transform)} should implement the .create_with_value and #transform methods") 68 | end 69 | 70 | @custom_transformations.merge!(options.fetch(:attribute, :no_attribute) => options) 71 | end 72 | 73 | def raise_on_file_not_found=(value) 74 | @raise_on_file_not_found = value 75 | end 76 | 77 | def raise_on_file_not_found? 78 | !!@raise_on_file_not_found 79 | end 80 | 81 | private 82 | 83 | def incompatible_transformation?(klass) 84 | !klass.is_a?(Class) || !klass.respond_to?(:create_with_value) || !klass.instance_methods.include?(:transform) 85 | end 86 | end 87 | 88 | @configuration = InlineSvg::Configuration.new 89 | 90 | class << self 91 | attr_reader :configuration 92 | 93 | def configure 94 | if block_given? 95 | yield configuration 96 | else 97 | raise InlineSvg::Configuration::Invalid.new('Please set configuration options with a block') 98 | end 99 | end 100 | 101 | def reset_configuration! 102 | @configuration = InlineSvg::Configuration.new 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /spec/finds_asset_paths_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe InlineSvg::FindsAssetPaths do 6 | after do 7 | InlineSvg.reset_configuration! 8 | end 9 | 10 | context "when sprockets finder returns an object which supports only the pathname method" do 11 | it "returns fully qualified file paths from Sprockets" do 12 | sprockets = double('SprocketsDouble') 13 | 14 | expect(sprockets).to receive(:find_asset).with('some-file') 15 | .and_return(double(pathname: Pathname('/full/path/to/some-file'))) 16 | 17 | InlineSvg.configure do |config| 18 | config.asset_finder = sprockets 19 | end 20 | 21 | expect(InlineSvg::FindsAssetPaths.by_filename('some-file')).to eq Pathname('/full/path/to/some-file') 22 | end 23 | end 24 | 25 | context "when sprockets finder returns an object which supports only the filename method" do 26 | it "returns fully qualified file paths from Sprockets" do 27 | sprockets = double('SprocketsDouble') 28 | 29 | expect(sprockets).to receive(:find_asset).with('some-file') 30 | .and_return(double(filename: Pathname('/full/path/to/some-file'))) 31 | 32 | InlineSvg.configure do |config| 33 | config.asset_finder = sprockets 34 | end 35 | 36 | expect(InlineSvg::FindsAssetPaths.by_filename('some-file')).to eq Pathname('/full/path/to/some-file') 37 | end 38 | end 39 | 40 | context "when asset is not found" do 41 | it "returns nil" do 42 | sprockets = double('SprocketsDouble') 43 | 44 | expect(sprockets).to receive(:find_asset).with('some-file').and_return(nil) 45 | 46 | InlineSvg.configure do |config| 47 | config.asset_finder = sprockets 48 | end 49 | 50 | expect(InlineSvg::FindsAssetPaths.by_filename('some-file')).to be_nil 51 | end 52 | end 53 | 54 | context "when propshaft finder returns an object which supports only the pathname method" do 55 | it "returns fully qualified file paths from Propshaft" do 56 | propshaft = double('PropshaftDouble') 57 | 58 | expect(propshaft).to receive(:find_asset).with('some-file') 59 | .and_return(double(pathname: Pathname('/full/path/to/some-file'))) 60 | 61 | InlineSvg.configure do |config| 62 | config.asset_finder = propshaft 63 | end 64 | 65 | expect(InlineSvg::FindsAssetPaths.by_filename('some-file')).to eq Pathname('/full/path/to/some-file') 66 | end 67 | end 68 | 69 | context "when webpack finder returns an object with a relative asset path" do 70 | it "returns the fully qualified file path" do 71 | shakapacker = double('ShakapackerDouble') 72 | 73 | expect(shakapacker).to receive(:find_asset).with('some-file') 74 | .and_return(double(filename: Pathname('/full/path/to/some-file'))) 75 | 76 | InlineSvg.configure do |config| 77 | config.asset_finder = shakapacker 78 | end 79 | 80 | expect(InlineSvg::FindsAssetPaths.by_filename('some-file')).to eq Pathname('/full/path/to/some-file') 81 | end 82 | end 83 | 84 | context "when webpack finder returns an object with an absolute http asset path" do 85 | it "returns the fully qualified file path" do 86 | shakapacker = double('ShakapackerDouble') 87 | 88 | expect(shakapacker).to receive(:find_asset).with('some-file') 89 | .and_return(double(filename: Pathname('https://test.example.org/full/path/to/some-file'))) 90 | 91 | InlineSvg.configure do |config| 92 | config.asset_finder = shakapacker 93 | end 94 | 95 | expect(InlineSvg::FindsAssetPaths.by_filename('some-file')).to eq Pathname('https://test.example.org/full/path/to/some-file') 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/transformation_pipeline/transformations/aria_attributes_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe InlineSvg::TransformPipeline::Transformations::AriaAttributes do 6 | it "adds a role attribute to the SVG document" do 7 | document = Nokogiri::XML::Document.parse("Some document") 8 | transformation = InlineSvg::TransformPipeline::Transformations::AriaAttributes.create_with_value({}) 9 | 10 | expect(transformation.transform(document).to_html).to eq( 11 | "Some document\n" 12 | ) 13 | end 14 | 15 | context "aria-labelledby attribute" do 16 | it "adds 'title' when a title element is present" do 17 | document = Nokogiri::XML::Document.parse("Some titleSome document") 18 | transformation = InlineSvg::TransformPipeline::Transformations::AriaAttributes.create_with_value(true) 19 | 20 | expect(InlineSvg::IdGenerator).to receive(:generate).with("title", "Some title") 21 | .and_return("some-id") 22 | 23 | expect(transformation.transform(document).to_html).to eq( 24 | "Some titleSome document\n" 25 | ) 26 | end 27 | 28 | it "adds 'desc' when a description element is present" do 29 | document = Nokogiri::XML::Document.parse("Some descriptionSome document") 30 | transformation = InlineSvg::TransformPipeline::Transformations::AriaAttributes.create_with_value(true) 31 | 32 | expect(InlineSvg::IdGenerator).to receive(:generate).with("desc", "Some description") 33 | .and_return("some-id") 34 | 35 | expect(transformation.transform(document).to_html).to eq( 36 | "Some descriptionSome document\n" 37 | ) 38 | end 39 | 40 | it "adds both 'desc' and 'title' when title and description elements are present" do 41 | document = Nokogiri::XML::Document.parse("Some titleSome descriptionSome document") 42 | transformation = InlineSvg::TransformPipeline::Transformations::AriaAttributes.create_with_value(true) 43 | 44 | expect(InlineSvg::IdGenerator).to receive(:generate).with("title", "Some title") 45 | .and_return("some-id") 46 | expect(InlineSvg::IdGenerator).to receive(:generate).with("desc", "Some description") 47 | .and_return("some-other-id") 48 | 49 | expect(transformation.transform(document).to_html).to eq( 50 | "Some title\nSome descriptionSome document\n" 51 | ) 52 | end 53 | 54 | it "uses existing IDs when they exist" do 55 | document = Nokogiri::XML::Document.parse("Some titleSome descriptionSome document") 56 | transformation = InlineSvg::TransformPipeline::Transformations::AriaAttributes.create_with_value(true) 57 | 58 | expect(InlineSvg::IdGenerator).to receive(:generate).with("my-title", "Some title") 59 | .and_return("some-id") 60 | expect(InlineSvg::IdGenerator).to receive(:generate).with("my-desc", "Some description") 61 | .and_return("some-other-id") 62 | 63 | expect(transformation.transform(document).to_html).to eq( 64 | "Some title\nSome descriptionSome document\n" 65 | ) 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if ENV.fetch('COVERAGE', false) 4 | require 'simplecov' 5 | 6 | SimpleCov.start do 7 | add_filter %r{^/spec/} 8 | end 9 | end 10 | 11 | require 'inline_svg' 12 | 13 | # This file was generated by the `rspec --init` command. Conventionally, all 14 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 15 | # The generated `.rspec` file contains `--require spec_helper` which will cause 16 | # this file to always be loaded, without a need to explicitly require it in any 17 | # files. 18 | # 19 | # Given that it is always loaded, you are encouraged to keep this file as 20 | # light-weight as possible. Requiring heavyweight dependencies from this file 21 | # will add to the boot time of your test suite on EVERY test run, even for an 22 | # individual file that may not need all of that loaded. Instead, consider making 23 | # a separate helper file that requires the additional dependencies and performs 24 | # the additional setup, and require it from the spec files that actually need 25 | # it. 26 | # 27 | # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 28 | RSpec.configure do |config| 29 | # rspec-expectations config goes here. You can use an alternate 30 | # assertion/expectation library such as wrong or the stdlib/minitest 31 | # assertions if you prefer. 32 | config.expect_with :rspec do |expectations| 33 | # This option will default to `true` in RSpec 4. It makes the `description` 34 | # and `failure_message` of custom matchers include text for helper methods 35 | # defined using `chain`, e.g.: 36 | # be_bigger_than(2).and_smaller_than(4).description 37 | # # => "be bigger than 2 and smaller than 4" 38 | # ...rather than: 39 | # # => "be bigger than 2" 40 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 41 | end 42 | 43 | # rspec-mocks config goes here. You can use an alternate test double 44 | # library (such as bogus or mocha) by changing the `mock_with` option here. 45 | config.mock_with :rspec do |mocks| 46 | # Prevents you from mocking or stubbing a method that does not exist on 47 | # a real object. This is generally recommended, and will default to 48 | # `true` in RSpec 4. 49 | mocks.verify_partial_doubles = true 50 | end 51 | 52 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 53 | # have no way to turn it off -- the option exists only for backwards 54 | # compatibility in RSpec 3). It causes shared context metadata to be 55 | # inherited by the metadata hash of host groups and examples, rather than 56 | # triggering implicit auto-inclusion in groups with matching metadata. 57 | config.shared_context_metadata_behavior = :apply_to_host_groups 58 | 59 | # This allows you to limit a spec run to individual examples or groups 60 | # you care about by tagging them with `:focus` metadata. When nothing 61 | # is tagged with `:focus`, all examples get run. RSpec also provides 62 | # aliases for `it`, `describe`, and `context` that include `:focus` 63 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 64 | config.filter_run_when_matching :focus 65 | 66 | # Allows RSpec to persist some state between runs in order to support 67 | # the `--only-failures` and `--next-failure` CLI options. We recommend 68 | # you configure your source control system to ignore this file. 69 | config.example_status_persistence_file_path = "spec/examples.txt" 70 | 71 | # Limits the available syntax to the non-monkey patched syntax that is 72 | # recommended. For more details, see: 73 | # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ 74 | config.disable_monkey_patching! 75 | 76 | # This setting enables warnings. It's recommended, but in some cases may 77 | # be too noisy due to issues in dependencies. 78 | config.warnings = true 79 | 80 | # Many RSpec users commonly either run the entire suite or an individual 81 | # file, and it's useful to allow more verbose output when running an 82 | # individual spec file. 83 | if config.files_to_run.one? 84 | # Use the documentation formatter for detailed output, 85 | # unless a formatter has already been configured 86 | # (e.g. via a command-line flag). 87 | config.default_formatter = "doc" 88 | end 89 | 90 | # Print the 10 slowest examples and example groups at the 91 | # end of the spec run, to help surface which specs are running 92 | # particularly slow. 93 | config.profile_examples = 10 94 | 95 | # Run specs in random order to surface order dependencies. If you find an 96 | # order dependency and want to debug it, you can fix the order by providing 97 | # the seed, which is printed after each run. 98 | # --seed 1234 99 | config.order = :random 100 | 101 | # Seed global randomization in this process using the `--seed` CLI option. 102 | # Setting this allows you to use `--seed` to deterministically reproduce 103 | # test failures related to randomization by passing the same `--seed` value 104 | # as the one that triggered the failure. 105 | Kernel.srand config.seed 106 | end 107 | -------------------------------------------------------------------------------- /spec/inline_svg_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | class MyCustomTransform 6 | def self.create_with_value(value); end 7 | def transform(doc); end 8 | end 9 | 10 | class MyInvalidCustomTransformKlass 11 | def transform(doc); end 12 | end 13 | 14 | class MyInvalidCustomTransformInstance 15 | def self.create_with_value(value); end 16 | end 17 | 18 | class MyCustomAssetFile 19 | def self.named(filename); end 20 | end 21 | 22 | RSpec.describe InlineSvg do 23 | describe "configuration" do 24 | after do 25 | InlineSvg.reset_configuration! 26 | end 27 | 28 | context "when a block is not given" do 29 | it "complains" do 30 | expect do 31 | InlineSvg.configure 32 | end.to raise_error(InlineSvg::Configuration::Invalid) 33 | end 34 | end 35 | 36 | context "asset finder" do 37 | it "allows an asset finder to be assigned" do 38 | sprockets = double('SomethingLikeSprockets', find_asset: 'some asset') 39 | InlineSvg.configure do |config| 40 | config.asset_finder = sprockets 41 | end 42 | 43 | expect(InlineSvg.configuration.asset_finder).to eq sprockets 44 | end 45 | 46 | it "falls back to StaticAssetFinder when the provided asset finder does not implement #find_asset" do 47 | InlineSvg.configure do |config| 48 | config.asset_finder = 'Not a real asset finder' 49 | end 50 | 51 | expect(InlineSvg.configuration.asset_finder).to eq InlineSvg::StaticAssetFinder 52 | end 53 | end 54 | 55 | context "configuring a custom asset file" do 56 | it "falls back to the built-in asset file implementation by default" do 57 | expect(InlineSvg.configuration.asset_file).to eq(InlineSvg::AssetFile) 58 | end 59 | 60 | it "adds a collaborator that meets the interface specification" do 61 | InlineSvg.configure do |config| 62 | config.asset_file = MyCustomAssetFile 63 | end 64 | 65 | expect(InlineSvg.configuration.asset_file).to eq MyCustomAssetFile 66 | end 67 | 68 | it "rejects a collaborator that does not conform to the interface spec" do 69 | bad_asset_file = double("bad_asset_file") 70 | 71 | expect do 72 | InlineSvg.configure do |config| 73 | config.asset_file = bad_asset_file 74 | end 75 | end.to raise_error(InlineSvg::Configuration::Invalid, /asset_file should implement the #named method/) 76 | end 77 | 78 | it "rejects a collaborator that implements the correct interface with the wrong arity" do 79 | bad_asset_file = double("bad_asset_file", named: nil) 80 | 81 | expect do 82 | InlineSvg.configure do |config| 83 | config.asset_file = bad_asset_file 84 | end 85 | end.to raise_error(InlineSvg::Configuration::Invalid, /asset_file should implement the #named method with arity 1/) 86 | end 87 | end 88 | 89 | context "configuring the default svg-not-found class" do 90 | it "sets the class name" do 91 | InlineSvg.configure do |config| 92 | config.svg_not_found_css_class = 'missing-svg' 93 | end 94 | 95 | expect(InlineSvg.configuration.svg_not_found_css_class).to eq 'missing-svg' 96 | end 97 | end 98 | 99 | context "configuring custom transformation" do 100 | it "allows a custom transformation to be added" do 101 | InlineSvg.configure do |config| 102 | config.add_custom_transformation(attribute: :my_transform, transform: MyCustomTransform) 103 | end 104 | 105 | expect(InlineSvg.configuration.custom_transformations).to eq({ my_transform: { attribute: :my_transform, transform: MyCustomTransform } }) 106 | end 107 | 108 | it "rejects transformations that do not implement .create_with_value" do 109 | expect do 110 | InlineSvg.configure do |config| 111 | config.add_custom_transformation(attribute: :irrelevant, transform: MyInvalidCustomTransformKlass) 112 | end 113 | end.to raise_error(InlineSvg::Configuration::Invalid, /#{MyInvalidCustomTransformKlass} should implement the .create_with_value and #transform methods/o) 114 | end 115 | 116 | it "rejects transformations that does not implement #transform" do 117 | expect do 118 | InlineSvg.configure do |config| 119 | config.add_custom_transformation(attribute: :irrelevant, transform: MyInvalidCustomTransformInstance) 120 | end 121 | end.to raise_error(InlineSvg::Configuration::Invalid, /#{MyInvalidCustomTransformInstance} should implement the .create_with_value and #transform methods/o) 122 | end 123 | 124 | it "rejects transformations that are not classes" do 125 | expect do 126 | InlineSvg.configure do |config| 127 | config.add_custom_transformation(attribute: :irrelevant, transform: :not_a_class) 128 | end 129 | end.to raise_error(InlineSvg::Configuration::Invalid, /not_a_class should implement the .create_with_value and #transform methods/) 130 | end 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /spec/transformation_pipeline/transformations_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | class ACustomTransform < InlineSvg::CustomTransformation 6 | def transform(doc) 7 | doc 8 | end 9 | end 10 | 11 | class ASecondCustomTransform < ACustomTransform; end 12 | 13 | RSpec.describe InlineSvg::TransformPipeline::Transformations do 14 | context "looking up transformations" do 15 | it "returns built-in transformations when parameters are supplied" do 16 | transformations = InlineSvg::TransformPipeline::Transformations.lookup( 17 | nocomment: 'irrelevant', 18 | class: 'irrelevant', 19 | style: 'irrelevant', 20 | title: 'irrelevant', 21 | desc: 'irrelevant', 22 | size: 'irrelevant', 23 | height: 'irrelevant', 24 | width: 'irrelevant', 25 | view_box: 'irrelevant', 26 | id: 'irrelevant', 27 | data: 'irrelevant', 28 | preserve_aspect_ratio: 'irrelevant', 29 | aria: 'irrelevant', 30 | aria_hidden: "true" 31 | ) 32 | 33 | expect(transformations.map(&:class)).to contain_exactly( 34 | InlineSvg::TransformPipeline::Transformations::NoComment, 35 | InlineSvg::TransformPipeline::Transformations::ClassAttribute, 36 | InlineSvg::TransformPipeline::Transformations::StyleAttribute, 37 | InlineSvg::TransformPipeline::Transformations::Title, 38 | InlineSvg::TransformPipeline::Transformations::Description, 39 | InlineSvg::TransformPipeline::Transformations::Size, 40 | InlineSvg::TransformPipeline::Transformations::Height, 41 | InlineSvg::TransformPipeline::Transformations::Width, 42 | InlineSvg::TransformPipeline::Transformations::ViewBox, 43 | InlineSvg::TransformPipeline::Transformations::IdAttribute, 44 | InlineSvg::TransformPipeline::Transformations::DataAttributes, 45 | InlineSvg::TransformPipeline::Transformations::PreserveAspectRatio, 46 | InlineSvg::TransformPipeline::Transformations::AriaAttributes, 47 | InlineSvg::TransformPipeline::Transformations::AriaHiddenAttribute 48 | ) 49 | end 50 | 51 | it "returns transformations in priority order" do 52 | built_ins = { 53 | desc: { transform: InlineSvg::TransformPipeline::Transformations::Description, priority: 1 }, 54 | size: { transform: InlineSvg::TransformPipeline::Transformations::Size }, 55 | title: { transform: InlineSvg::TransformPipeline::Transformations::Title, priority: 2 } 56 | } 57 | 58 | allow(InlineSvg::TransformPipeline::Transformations).to \ 59 | receive(:built_in_transformations).and_return(built_ins) 60 | 61 | transformations = InlineSvg::TransformPipeline::Transformations.lookup( 62 | { 63 | desc: "irrelevant", 64 | size: "irrelevant", 65 | title: "irrelevant", 66 | } 67 | ) 68 | 69 | # Use `eq` here because we care about order 70 | expect(transformations.map(&:class)).to eq([ 71 | InlineSvg::TransformPipeline::Transformations::Description, 72 | InlineSvg::TransformPipeline::Transformations::Title, 73 | InlineSvg::TransformPipeline::Transformations::Size, 74 | ]) 75 | end 76 | 77 | it "returns no transformations when asked for an unknown transform" do 78 | transformations = InlineSvg::TransformPipeline::Transformations.lookup( 79 | not_a_real_transform: 'irrelevant' 80 | ) 81 | 82 | expect(transformations.map(&:class)).to be_empty 83 | end 84 | 85 | it "does not return a transformation when a value is not supplied" do 86 | transformations = InlineSvg::TransformPipeline::Transformations.lookup( 87 | title: nil 88 | ) 89 | 90 | expect(transformations.map(&:class)).to be_empty 91 | end 92 | end 93 | 94 | context "custom transformations" do 95 | before do 96 | InlineSvg.configure do |config| 97 | config.add_custom_transformation({ transform: ACustomTransform, attribute: :my_transform, priority: 2 }) 98 | config.add_custom_transformation({ transform: ASecondCustomTransform, attribute: :my_other_transform, priority: 1 }) 99 | end 100 | end 101 | 102 | after do 103 | InlineSvg.reset_configuration! 104 | end 105 | 106 | it "returns configured custom transformations" do 107 | transformations = InlineSvg::TransformPipeline::Transformations.lookup( 108 | my_transform: :irrelevant 109 | ) 110 | 111 | expect(transformations.map(&:class)).to contain_exactly(ACustomTransform) 112 | end 113 | 114 | it "returns configured custom transformations in priority order" do 115 | transformations = InlineSvg::TransformPipeline::Transformations.lookup( 116 | my_transform: :irrelevant, 117 | my_other_transform: :irrelevant 118 | ) 119 | 120 | # Use `eq` here because we care about order: 121 | expect(transformations.map(&:class)).to eq([ASecondCustomTransform, ACustomTransform]) 122 | end 123 | 124 | it "always prioritizes built-in transforms before custom transforms" do 125 | transformations = InlineSvg::TransformPipeline::Transformations.lookup( 126 | my_transform: :irrelevant, 127 | my_other_transform: :irrelevant, 128 | desc: "irrelevant" 129 | ) 130 | 131 | # Use `eq` here because we care about order: 132 | expect(transformations.map(&:class)).to eq( 133 | [ 134 | InlineSvg::TransformPipeline::Transformations::Description, 135 | ASecondCustomTransform, 136 | ACustomTransform 137 | ] 138 | ) 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config --no-offense-counts --no-auto-gen-timestamp` 3 | # using RuboCop version 1.69.2. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # This cop supports safe autocorrection (--autocorrect). 10 | # Configuration parameters: IndentationWidth. 11 | # SupportedStyles: special_inside_parentheses, consistent, align_brackets 12 | Layout/FirstArrayElementIndentation: 13 | EnforcedStyle: consistent 14 | 15 | # This cop supports safe autocorrection (--autocorrect). 16 | # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. 17 | # SupportedHashRocketStyles: key, separator, table 18 | # SupportedColonStyles: key, separator, table 19 | # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit 20 | Layout/HashAlignment: 21 | Exclude: 22 | - 'spec/transformation_pipeline/transformations_spec.rb' 23 | 24 | # This cop supports safe autocorrection (--autocorrect). 25 | # Configuration parameters: EnforcedStyle, IndentationWidth. 26 | # SupportedStyles: aligned, indented, indented_relative_to_receiver 27 | Layout/MultilineMethodCallIndentation: 28 | Exclude: 29 | - 'spec/transformation_pipeline/transformations/data_attributes_spec.rb' 30 | - 'spec/transformation_pipeline/transformations/style_attribute_spec.rb' 31 | - 'spec/transformation_pipeline/transformations/view_box_spec.rb' 32 | 33 | Lint/ShadowingOuterLocalVariable: 34 | Exclude: 35 | - 'lib/inline_svg/transform_pipeline/transformations/description.rb' 36 | - 'lib/inline_svg/transform_pipeline/transformations/title.rb' 37 | 38 | # This cop supports safe autocorrection (--autocorrect). 39 | # Configuration parameters: AutoCorrect, IgnoreEmptyBlocks, AllowUnusedKeywordArguments. 40 | Lint/UnusedBlockArgument: 41 | Exclude: 42 | - 'lib/inline_svg/railtie.rb' 43 | - 'lib/inline_svg/transform_pipeline/transformations.rb' 44 | 45 | # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. 46 | Metrics/AbcSize: 47 | Max: 19 48 | 49 | # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. 50 | Metrics/MethodLength: 51 | Max: 16 52 | 53 | # This cop supports safe autocorrection (--autocorrect). 54 | # Configuration parameters: PreferredName. 55 | Naming/RescuedExceptionsVariableName: 56 | Exclude: 57 | - 'lib/inline_svg/action_view/helpers.rb' 58 | 59 | # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. 60 | # SupportedStyles: snake_case, normalcase, non_integer 61 | # AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64 62 | Naming/VariableNumber: 63 | Exclude: 64 | - 'spec/cached_asset_file_spec.rb' 65 | 66 | # Configuration parameters: Prefixes, AllowedPatterns. 67 | # Prefixes: when, with, without 68 | RSpec/ContextWording: 69 | Exclude: 70 | - 'spec/helpers/inline_svg_spec.rb' 71 | - 'spec/inline_svg_spec.rb' 72 | - 'spec/io_resource_spec.rb' 73 | - 'spec/transformation_pipeline/transformations/aria_attributes_spec.rb' 74 | - 'spec/transformation_pipeline/transformations_spec.rb' 75 | 76 | # This cop supports unsafe autocorrection (--autocorrect-all). 77 | # Configuration parameters: AutoCorrect. 78 | RSpec/EmptyExampleGroup: 79 | Exclude: 80 | - 'spec/io_resource_spec.rb' 81 | 82 | # Configuration parameters: CountAsOne. 83 | RSpec/ExampleLength: 84 | Max: 32 85 | 86 | # This cop supports safe autocorrection (--autocorrect). 87 | # Configuration parameters: EnforcedStyle. 88 | # SupportedStyles: single_line_only, single_statement_only, disallow, require_implicit 89 | RSpec/ImplicitSubject: 90 | Exclude: 91 | - 'spec/io_resource_spec.rb' 92 | 93 | # Configuration parameters: . 94 | # SupportedStyles: have_received, receive 95 | RSpec/MessageSpies: 96 | EnforcedStyle: receive 97 | 98 | RSpec/MultipleExpectations: 99 | Max: 4 100 | 101 | # Configuration parameters: EnforcedStyle, IgnoreSharedExamples. 102 | # SupportedStyles: always, named_only 103 | RSpec/NamedSubject: 104 | Exclude: 105 | - 'spec/io_resource_spec.rb' 106 | 107 | # Configuration parameters: AllowedGroups. 108 | RSpec/NestedGroups: 109 | Max: 4 110 | 111 | RSpec/RepeatedExampleGroupDescription: 112 | Exclude: 113 | - 'spec/helpers/inline_svg_spec.rb' 114 | 115 | # Configuration parameters: Include, CustomTransform, IgnoreMethods, IgnoreMetadata. 116 | # Include: **/*_spec.rb 117 | RSpec/SpecFilePathFormat: 118 | Enabled: false 119 | 120 | RSpec/StubbedMock: 121 | Exclude: 122 | - 'spec/asset_file_spec.rb' 123 | - 'spec/finds_asset_paths_spec.rb' 124 | - 'spec/helpers/inline_svg_spec.rb' 125 | - 'spec/propshaft_asset_finder_spec.rb' 126 | - 'spec/static_asset_finder_spec.rb' 127 | - 'spec/transformation_pipeline/transformations/aria_attributes_spec.rb' 128 | - 'spec/webpack_asset_finder_spec.rb' 129 | 130 | # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. 131 | RSpec/VerifiedDoubles: 132 | Exclude: 133 | - 'spec/finds_asset_paths_spec.rb' 134 | - 'spec/helpers/inline_svg_spec.rb' 135 | - 'spec/inline_svg_spec.rb' 136 | - 'spec/propshaft_asset_finder_spec.rb' 137 | - 'spec/static_asset_finder_spec.rb' 138 | - 'spec/webpack_asset_finder_spec.rb' 139 | 140 | # This cop supports safe autocorrection (--autocorrect). 141 | # Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, AllowBracesOnProceduralOneLiners, BracesRequiredMethods. 142 | # SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces 143 | # ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object 144 | # FunctionalMethods: let, let!, subject, watch 145 | # AllowedMethods: lambda, proc, it 146 | Style/BlockDelimiters: 147 | Exclude: 148 | - 'spec/helpers/inline_svg_spec.rb' 149 | 150 | # This cop supports safe autocorrection (--autocorrect). 151 | # Configuration parameters: AllowOnConstant, AllowOnSelfClass. 152 | Style/CaseEquality: 153 | Exclude: 154 | - 'lib/inline_svg/action_view/helpers.rb' 155 | - 'spec/io_resource_spec.rb' 156 | 157 | # This cop supports unsafe autocorrection (--autocorrect-all). 158 | # Configuration parameters: EnforcedStyle. 159 | # SupportedStyles: nested, compact 160 | Style/ClassAndModuleChildren: 161 | Enabled: false 162 | 163 | # This cop supports unsafe autocorrection (--autocorrect-all). 164 | # Configuration parameters: AllowedMethods, AllowedPatterns. 165 | # AllowedMethods: ==, equal?, eql? 166 | Style/ClassEqualityComparison: 167 | Exclude: 168 | - 'lib/inline_svg.rb' 169 | 170 | # This cop supports unsafe autocorrection (--autocorrect-all). 171 | # Configuration parameters: AllowedReceivers. 172 | Style/CollectionCompact: 173 | Exclude: 174 | - 'lib/inline_svg/transform_pipeline/transformations.rb' 175 | 176 | # Configuration parameters: AllowedConstants. 177 | Style/Documentation: 178 | Enabled: false 179 | 180 | # This cop supports safe autocorrection (--autocorrect). 181 | # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. 182 | Style/GuardClause: 183 | Exclude: 184 | - 'lib/inline_svg.rb' 185 | 186 | # This cop supports safe autocorrection (--autocorrect). 187 | Style/IfUnlessModifier: 188 | Exclude: 189 | - 'lib/inline_svg.rb' 190 | - 'lib/inline_svg/static_asset_finder.rb' 191 | - 'lib/inline_svg/transform_pipeline/transformations/aria_attributes.rb' 192 | 193 | # This cop supports unsafe autocorrection (--autocorrect-all). 194 | Style/MapIntoArray: 195 | Exclude: 196 | - 'lib/inline_svg/transform_pipeline/transformations/aria_attributes.rb' 197 | 198 | # This cop supports unsafe autocorrection (--autocorrect-all). 199 | # Configuration parameters: EnforcedStyle, AllowedCompactTypes. 200 | # SupportedStyles: compact, exploded 201 | Style/RaiseArgs: 202 | Exclude: 203 | - 'lib/inline_svg.rb' 204 | - 'lib/inline_svg/asset_file.rb' 205 | - 'lib/inline_svg/cached_asset_file.rb' 206 | 207 | # This cop supports unsafe autocorrection (--autocorrect-all). 208 | # Configuration parameters: Methods. 209 | Style/RedundantArgument: 210 | Exclude: 211 | - 'lib/inline_svg/transform_pipeline/transformations/class_attribute.rb' 212 | 213 | # This cop supports unsafe autocorrection (--autocorrect-all). 214 | # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. 215 | # AllowedMethods: present?, blank?, presence, try, try! 216 | Style/SafeNavigation: 217 | Exclude: 218 | - 'lib/inline_svg/propshaft_asset_finder.rb' 219 | 220 | # This cop supports unsafe autocorrection (--autocorrect-all). 221 | # Configuration parameters: Mode. 222 | Style/StringConcatenation: 223 | Exclude: 224 | - 'lib/inline_svg/id_generator.rb' 225 | 226 | # This cop supports safe autocorrection (--autocorrect). 227 | # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. 228 | # SupportedStyles: single_quotes, double_quotes 229 | Style/StringLiterals: 230 | Enabled: false 231 | 232 | # This cop supports unsafe autocorrection (--autocorrect-all). 233 | # Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, AllowComments. 234 | # AllowedMethods: define_method 235 | Style/SymbolProc: 236 | Exclude: 237 | - 'lib/inline_svg/transform_pipeline/transformations/description.rb' 238 | - 'lib/inline_svg/transform_pipeline/transformations/no_comment.rb' 239 | - 'lib/inline_svg/transform_pipeline/transformations/title.rb' 240 | 241 | # This cop supports safe autocorrection (--autocorrect). 242 | # Configuration parameters: EnforcedStyleForMultiline. 243 | # SupportedStylesForMultiline: comma, consistent_comma, no_comma 244 | Style/TrailingCommaInArrayLiteral: 245 | Exclude: 246 | - 'spec/transformation_pipeline/transformations_spec.rb' 247 | 248 | # This cop supports safe autocorrection (--autocorrect). 249 | # Configuration parameters: EnforcedStyleForMultiline. 250 | # SupportedStylesForMultiline: comma, consistent_comma, no_comma 251 | Style/TrailingCommaInHashLiteral: 252 | Exclude: 253 | - 'lib/inline_svg/transform_pipeline/transformations.rb' 254 | - 'spec/transformation_pipeline/transformations_spec.rb' 255 | 256 | # This cop supports safe autocorrection (--autocorrect). 257 | # Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, AllowedMethods. 258 | # AllowedMethods: to_ary, to_a, to_c, to_enum, to_h, to_hash, to_i, to_int, to_io, to_open, to_path, to_proc, to_r, to_regexp, to_str, to_s, to_sym 259 | Style/TrivialAccessors: 260 | Exclude: 261 | - 'lib/inline_svg.rb' 262 | 263 | # This cop supports safe autocorrection (--autocorrect). 264 | # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. 265 | # URISchemes: http, https 266 | Layout/LineLength: 267 | Max: 183 268 | -------------------------------------------------------------------------------- /spec/helpers/inline_svg_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | class WorkingCustomTransform < InlineSvg::CustomTransformation 6 | def transform(doc) 7 | doc = Nokogiri::XML::Document.parse(doc.to_html) 8 | svg = doc.at_css 'svg' 9 | svg['custom'] = value 10 | doc 11 | end 12 | end 13 | 14 | RSpec.describe InlineSvg::ActionView::Helpers do 15 | let(:helper) { (Class.new { include InlineSvg::ActionView::Helpers }).new } 16 | 17 | shared_examples "inline_svg helper" do |helper_method:| 18 | after do 19 | InlineSvg.reset_configuration! 20 | end 21 | 22 | context "when passed the name of an SVG that does not exist" do 23 | context "and configured to raise" do 24 | before do 25 | InlineSvg.configure do |config| 26 | config.raise_on_file_not_found = true 27 | end 28 | 29 | allow(InlineSvg::AssetFile).to receive(:named) 30 | .with('some-missing-file.svg') 31 | .and_raise(InlineSvg::AssetFile::FileNotFound.new) 32 | end 33 | 34 | it "raises an exception" do 35 | expect { 36 | helper.send(helper_method, 'some-missing-file.svg') 37 | }.to raise_error(InlineSvg::AssetFile::FileNotFound) 38 | end 39 | 40 | it "ensures thread-local variable is cleared after execution" do 41 | begin 42 | helper.send(helper_method, 'some-missing-file.svg') 43 | rescue InlineSvg::AssetFile::FileNotFound 44 | nil 45 | end 46 | 47 | expect(Thread.current[:inline_svg_asset_finder]).to be_nil 48 | end 49 | end 50 | 51 | it "returns an empty, html safe, SVG document as a placeholder" do 52 | allow(InlineSvg::AssetFile).to receive(:named) 53 | .with('some-missing-file.svg') 54 | .and_raise(InlineSvg::AssetFile::FileNotFound.new) 55 | 56 | output = helper.send(helper_method, 'some-missing-file.svg') 57 | expect(output).to eq "" 58 | expect(output).to be_html_safe 59 | end 60 | 61 | it "escapes malicious input" do 62 | malicious = "-->.svg" 63 | allow(InlineSvg::AssetFile).to receive(:named) 64 | .with(malicious) 65 | .and_raise(InlineSvg::AssetFile::FileNotFound.new) 66 | 67 | output = helper.send(helper_method, malicious) 68 | expect(output).to eq "" 69 | expect(output).to be_html_safe 70 | end 71 | 72 | it "gives a helpful hint when no .svg extension is provided in the filename" do 73 | allow(InlineSvg::AssetFile).to receive(:named) 74 | .with('missing-file-with-no-extension') 75 | .and_raise(InlineSvg::AssetFile::FileNotFound.new) 76 | 77 | output = helper.send(helper_method, 'missing-file-with-no-extension') 78 | expect(output).to eq "" 79 | end 80 | 81 | it "allows the CSS class on the empty SVG document to be changed" do 82 | InlineSvg.configure do |config| 83 | config.svg_not_found_css_class = 'missing-svg' 84 | end 85 | 86 | allow(InlineSvg::AssetFile).to receive(:named) 87 | .with('some-other-missing-file.svg') 88 | .and_raise(InlineSvg::AssetFile::FileNotFound.new) 89 | 90 | output = helper.send(helper_method, 'some-other-missing-file.svg') 91 | expect(output).to eq "" 92 | expect(output).to be_html_safe 93 | end 94 | 95 | context "and a fallback that does exist" do 96 | it "displays the fallback" do 97 | allow(InlineSvg::AssetFile).to receive(:named) 98 | .with('missing.svg') 99 | .and_raise(InlineSvg::AssetFile::FileNotFound.new) 100 | 101 | fallback_file = '' 102 | allow(InlineSvg::AssetFile).to receive(:named).with('fallback.svg').and_return(fallback_file) 103 | expect(helper.send(helper_method, 'missing.svg', fallback: 'fallback.svg')).to eq fallback_file 104 | end 105 | end 106 | end 107 | 108 | context "when passed an existing SVG file" do 109 | context "and no options" do 110 | it "returns a html safe version of the file's contents" do 111 | example_file = '' 112 | allow(InlineSvg::AssetFile).to receive(:named).with('some-file').and_return(example_file) 113 | expect(helper.send(helper_method, 'some-file')).to eq example_file 114 | end 115 | end 116 | 117 | context "and the 'title' option" do 118 | it "adds the title node to the SVG output" do 119 | input_svg = '' 120 | expected_output = 'A title' 121 | allow(InlineSvg::AssetFile).to receive(:named).with('some-file').and_return(input_svg) 122 | expect(helper.send(helper_method, 'some-file', title: 'A title')).to eq expected_output 123 | end 124 | end 125 | 126 | context "and the 'desc' option" do 127 | it "adds the description node to the SVG output" do 128 | input_svg = '' 129 | expected_output = 'A description' 130 | allow(InlineSvg::AssetFile).to receive(:named).with('some-file').and_return(input_svg) 131 | expect(helper.send(helper_method, 'some-file', desc: 'A description')).to eq expected_output 132 | end 133 | end 134 | 135 | context "and the 'nocomment' option" do 136 | it "strips comments and other unknown/unsafe nodes from the output" do 137 | input_svg = '' 138 | expected_output = '' 139 | allow(InlineSvg::AssetFile).to receive(:named).with('some-file').and_return(input_svg) 140 | expect(helper.send(helper_method, 'some-file', nocomment: true)).to eq expected_output 141 | end 142 | end 143 | 144 | context "and the 'aria_hidden' option" do 145 | it "sets 'aria-hidden=true' in the output" do 146 | input_svg = '' 147 | expected_output = '' 148 | allow(InlineSvg::AssetFile).to receive(:named).with('some-file').and_return(input_svg) 149 | expect(helper.send(helper_method, 'some-file', aria_hidden: true)).to eq expected_output 150 | end 151 | end 152 | 153 | context "and all options" do 154 | it "applies all expected transformations to the output" do 155 | input_svg = '' 156 | expected_output = 'A titleA description' 157 | allow(InlineSvg::AssetFile).to receive(:named).with('some-file').and_return(input_svg) 158 | expect(helper.send(helper_method, 'some-file', title: 'A title', desc: 'A description', nocomment: true)).to eq expected_output 159 | end 160 | end 161 | 162 | context "with custom transformations" do 163 | before do 164 | InlineSvg.configure do |config| 165 | config.add_custom_transformation({ attribute: :custom, transform: WorkingCustomTransform }) 166 | end 167 | end 168 | 169 | it "applies custm transformations to the output" do 170 | input_svg = '' 171 | expected_output = '' 172 | allow(InlineSvg::AssetFile).to receive(:named).with('some-file').and_return(input_svg) 173 | expect(helper.send(helper_method, 'some-file', custom: 'some value')).to eq expected_output 174 | end 175 | end 176 | 177 | context "with custom transformations using a default value" do 178 | before do 179 | InlineSvg.configure do |config| 180 | config.add_custom_transformation({ attribute: :custom, transform: WorkingCustomTransform, default_value: 'default value' }) 181 | end 182 | end 183 | 184 | context "without passing the attribute value" do 185 | it "applies custom transformations to the output using the default value" do 186 | input_svg = '' 187 | 188 | allow(InlineSvg::AssetFile).to receive(:named).with('some-file').and_return(input_svg) 189 | 190 | expect(helper.send(helper_method, 'some-file')).to eq "" 191 | end 192 | end 193 | 194 | context "passing the attribute value" do 195 | it "applies custom transformations to the output" do 196 | input_svg = '' 197 | 198 | allow(InlineSvg::AssetFile).to receive(:named).with('some-file').and_return(input_svg) 199 | 200 | expect(helper.send(helper_method, 'some-file', custom: 'some value')).to eq "" 201 | end 202 | end 203 | end 204 | end 205 | 206 | context 'argument polimorphizm' do 207 | let(:argument) { double('argument') } 208 | 209 | it 'accept IO' do 210 | expect(InlineSvg::IOResource).to receive(:===).with(argument).and_return(true) 211 | expect(InlineSvg::IOResource).to receive(:read).with(argument) 212 | expect(InlineSvg::AssetFile).not_to receive(:named) 213 | helper.send(helper_method, argument) 214 | end 215 | 216 | it 'accept filename' do 217 | expect(InlineSvg::IOResource).to receive(:===).with(argument).and_return(false) 218 | expect(InlineSvg::IOResource).not_to receive(:read) 219 | expect(InlineSvg::AssetFile).to receive(:named).with(argument) 220 | helper.send(helper_method, argument) 221 | end 222 | end 223 | 224 | context 'when passed IO object argument' do 225 | let(:io_object) { double('io_object') } 226 | let(:file_path) { File.expand_path('../files/example.svg', __dir__) } 227 | let(:answer) { File.read(file_path) } 228 | 229 | it 'return valid svg' do 230 | expect(InlineSvg::IOResource).to receive(:===).with(io_object).and_return(true) 231 | expect(InlineSvg::IOResource).to receive(:read).with(io_object).and_return("") 232 | output = helper.send(helper_method, io_object) 233 | expect(output).to eq "" 234 | expect(output).to be_html_safe 235 | end 236 | 237 | it 'return valid svg for file' do 238 | output = helper.send(helper_method, File.new(file_path)) 239 | expect(output).to eq "" 240 | expect(output).to be_html_safe 241 | end 242 | end 243 | 244 | context 'default output' do 245 | it "returns an SVG tag without any pre or post whitespace characters" do 246 | input_svg = '' 247 | 248 | allow(InlineSvg::AssetFile).to receive(:named).with('some-file').and_return(input_svg) 249 | 250 | expect(helper.send(helper_method, 'some-file')).to eq "" 251 | end 252 | end 253 | end 254 | 255 | describe '#inline_svg' do 256 | it_behaves_like "inline_svg helper", helper_method: :inline_svg 257 | end 258 | 259 | describe '#inline_svg_tag' do 260 | it_behaves_like "inline_svg helper", helper_method: :inline_svg_tag 261 | end 262 | 263 | describe '#inline_svg_tag' do 264 | it_behaves_like "inline_svg helper", helper_method: :inline_svg_pack_tag 265 | end 266 | end 267 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inline SVG 2 | 3 | [![Ruby](https://github.com/jamesmartin/inline_svg/actions/workflows/ruby.yml/badge.svg)](https://github.com/jamesmartin/inline_svg/actions/workflows/ruby.yml) 4 | [![Integration Tests](https://github.com/jamesmartin/inline_svg/actions/workflows/integration_test.yml/badge.svg)](https://github.com/jamesmartin/inline_svg/actions/workflows/integration_test.yml) 5 | [![RuboCop](https://github.com/jamesmartin/inline_svg/actions/workflows/rubocop.yml/badge.svg)](https://github.com/jamesmartin/inline_svg/actions/workflows/rubocop.yml) 6 | 7 | Styling a SVG document with CSS for use on the web is most reliably achieved by 8 | [adding classes to the document and 9 | embedding](http://css-tricks.com/using-svg/) it inline in the HTML. 10 | 11 | This gem adds Rails helper methods (`inline_svg_tag` and `inline_svg_pack_tag`) that read an SVG document (via Sprockets or Shakapacker, so works with the Rails Asset Pipeline), applies a CSS class attribute to the root of the document and 12 | then embeds it into a view. 13 | 14 | Inline SVG supports Rails 7.x with Propshaft, Sprockets, or Shakapacker 15 | 16 | ## Changelog 17 | 18 | This project adheres to [Semantic Versioning](http://semver.org). All notable changes are documented in the 19 | [CHANGELOG](https://github.com/jamesmartin/inline_svg/blob/master/CHANGELOG.md). 20 | 21 | ## Installation 22 | 23 | Add this line to your application's Gemfile: 24 | 25 | gem 'inline_svg' 26 | 27 | And then execute: 28 | 29 | $ bundle 30 | 31 | Or install it yourself as: 32 | 33 | $ gem install inline_svg 34 | 35 | ## Usage 36 | 37 | ```ruby 38 | # Sprockets 39 | inline_svg_tag(file_name, options={}) 40 | 41 | # Shakapacker 42 | inline_svg_pack_tag(file_name, options={}) 43 | ``` 44 | 45 | _**Note:** The remainder of this README uses `inline_svg_tag` for examples, but the exact same principles work for `inline_svg_pack_tag`._ 46 | 47 | The `file_name` can be a full path to a file, the file's basename or an `IO` 48 | object. The 49 | actual path of the file on disk is resolved using 50 | [Sprockets](://github.com/sstephenson/sprockets) (when available), a naive file finder (`/public/assets/...`) or in the case of `IO` objects the SVG data is read from the object. 51 | This means you can pre-process and fingerprint your SVG files like other Rails assets, or choose to find SVG data yourself. 52 | 53 | Here's an example of embedding an SVG document and applying a 'class' attribute: 54 | 55 | ```erb 56 | 57 | 58 | Embedded SVG Documents<title> 59 | </head> 60 | <body> 61 | <h1>Embedded SVG Documents</h1> 62 | <div> 63 | <%= inline_svg_tag "some-document.svg", class: 'some-class' %> 64 | </div> 65 | </body> 66 | </html> 67 | ``` 68 | 69 | Here's some CSS to target the SVG, resize it and turn it an attractive shade of 70 | blue: 71 | 72 | ```css 73 | .some-class { 74 | display: block; 75 | margin: 0 auto; 76 | fill: #3498db; 77 | width: 5em; 78 | height: 5em; 79 | } 80 | ``` 81 | 82 | ## Options 83 | 84 | key | description 85 | :---------------------- | :---------- 86 | `id` | set a ID attribute on the SVG 87 | `class` | set a CSS class attribute on the SVG 88 | `style` | set a CSS style attribute on the SVG 89 | `data` | add data attributes to the SVG (supply as a hash) 90 | `size` | set width and height attributes on the SVG <br/> Can also be set using `height` and/or `width` attributes, which take precedence over `size` <br/> Supplied as "{Width} * {Height}" or "{Number}", so "30px\*45px" becomes `width="30px"` and `height="45px"`, and "50%" becomes `width="50%"` and `height="50%"` 91 | `title` | add a \<title\> node inside the top level of the SVG document 92 | `desc` | add a \<desc\> node inside the top level of the SVG document 93 | `nocomment` | remove comment tags from the SVG document 94 | `preserve_aspect_ratio` | adds a `preserveAspectRatio` attribute to the SVG 95 | `view_box` | adds a `viewBox` attribute to the SVG 96 | `aria` | adds common accessibility attributes to the SVG (see [PR #34](https://github.com/jamesmartin/inline_svg/pull/34#issue-152062674) for details) 97 | `aria_hidden` | adds the `aria-hidden=true` attribute to the SVG 98 | `fallback` | set fallback SVG document 99 | 100 | Example: 101 | 102 | ```ruby 103 | inline_svg_tag( 104 | "some-document.svg", 105 | id: 'some-id', 106 | class: 'some-class', 107 | data: {some: "value"}, 108 | size: '30% * 20%', 109 | title: 'Some Title', 110 | desc: 'Some description', 111 | nocomment: true, 112 | preserve_aspect_ratio: 'xMaxYMax meet', 113 | view_box: '0 0 100 100', 114 | aria: true, 115 | aria_hidden: true, 116 | fallback: 'fallback-document.svg' 117 | ) 118 | ``` 119 | 120 | ## Accessibility 121 | 122 | Use the `aria: true` option to make `inline_svg_tag` add the following 123 | accessibility (a11y) attributes to your embedded SVG: 124 | 125 | * Adds a `role="img"` attribute to the root SVG element 126 | * Adds a `aria-labelled-by="title-id desc-id"` attribute to the root SVG 127 | element, if the document contains `<title>` or `<desc>` elements 128 | 129 | Here's an example: 130 | 131 | ```erb 132 | <%= 133 | inline_svg_tag('iconmonstr-glasses-12-icon.svg', 134 | aria: true, title: 'An SVG', 135 | desc: 'This is my SVG. There are many like it. You get the picture') 136 | %> 137 | ``` 138 | 139 | ```xml 140 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" \ 141 | role="img" aria-labelledby="bx6wix4t9pxpwxnohrhrmms3wexsw2o m439lk7mopdzmouktv2o689pl59wmd2"> 142 | <title id="bx6wix4t9pxpwxnohrhrmms3wexsw2o">An SVG 143 | This is my SVG. There are many like it. You get the picture 144 | 145 | ``` 146 | 147 | ***Note:*** The title and desc `id` attributes generated for, and referenced by, `aria-labelled-by` are one-way digests based on the value of the title and desc elements and an optional "salt" value using the SHA1 algorithm. This reduces the chance of `inline_svg_tag` embedding elements inside the SVG with `id` attributes that clash with other elements elsewhere on the page. 148 | 149 | ## Custom Transformations 150 | 151 | The transformation behavior of `inline_svg_tag` can be customized by creating custom transformation classes. 152 | 153 | For example, inherit from `InlineSvg::CustomTransformation` and implement the `#transform` method: 154 | 155 | ```ruby 156 | # Sets the `custom` attribute on the root SVG element to supplied value 157 | # Remember to return a document, as this will be passed along the transformation chain 158 | 159 | class MyCustomTransform < InlineSvg::CustomTransformation 160 | def transform(doc) 161 | with_svg(doc) do |svg| 162 | svg["custom"] = value 163 | end 164 | end 165 | end 166 | ``` 167 | 168 | Add the custom configuration in an initializer (E.g. `./config/initializers/inline_svg.rb`): 169 | 170 | ```ruby 171 | # Note that the named `attribute` will be used to pass a value to your custom transform 172 | InlineSvg.configure do |config| 173 | config.add_custom_transformation(attribute: :my_custom_attribute, transform: MyCustomTransform) 174 | end 175 | ``` 176 | 177 | The custom transformation can then be called like so: 178 | ```haml 179 | %div 180 | = inline_svg_tag "some-document.svg", my_custom_attribute: 'some value' 181 | ``` 182 | 183 | In this example, the following transformation would be applied to a SVG document: 184 | 185 | ```xml 186 | ... 187 | ``` 188 | 189 | You can also provide a default_value to the custom transformation, so even if you don't pass a value it will be triggered 190 | 191 | ```ruby 192 | # Note that the named `attribute` will be used to pass a value to your custom transform 193 | InlineSvg.configure do |config| 194 | config.add_custom_transformation(attribute: :my_custom_attribute, transform: MyCustomTransform, default_value: 'default value') 195 | end 196 | ``` 197 | 198 | The custom transformation will be triggered even if you don't pass any attribute value 199 | ```haml 200 | %div 201 | = inline_svg_tag "some-document.svg" 202 | = inline_svg_tag "some-document.svg", my_custom_attribute: 'some value' 203 | ``` 204 | 205 | In this example, the following transformation would be applied to a SVG document: 206 | 207 | ```xml 208 | ... 209 | ``` 210 | 211 | And 212 | 213 | ```xml 214 | ... 215 | ``` 216 | 217 | Passing a `priority` option with your custom transformation allows you to 218 | control the order that transformations are applied to the SVG document: 219 | 220 | ```ruby 221 | InlineSvg.configure do |config| 222 | config.add_custom_transformation(attribute: :custom_one, transform: MyCustomTransform, priority: 1) 223 | config.add_custom_transformation(attribute: :custom_two, transform: MyOtherCustomTransform, priority: 2) 224 | end 225 | ``` 226 | 227 | Transforms are applied in ascending order (lowest number first). 228 | 229 | ***Note***: Custom transformations are always applied *after* all built-in 230 | transformations, regardless of priority. 231 | 232 | ## Custom asset file loader 233 | 234 | An asset file loader returns a `String` representing a SVG document given a 235 | filename. Custom asset loaders should be a Ruby object that responds to a 236 | method called `named`, that takes one argument (a string representing the 237 | filename of the SVG document). 238 | 239 | A simple example might look like this: 240 | 241 | ```ruby 242 | class MyAssetFileLoader 243 | def self.named(filename) 244 | # ... load SVG document however you like 245 | return "some document" 246 | end 247 | end 248 | ``` 249 | 250 | Configure your custom asset file loader in an initializer like so: 251 | 252 | ```ruby 253 | InlineSvg.configure do |config| 254 | config.asset_file = MyAssetFileLoader 255 | end 256 | ``` 257 | 258 | ## Caching all assets at boot time 259 | 260 | When your deployment strategy prevents dynamic asset file loading from disk it 261 | can be helpful to cache all possible SVG assets in memory at application boot 262 | time. In this case, you can configure the `InlineSvg::CachedAssetFile` to scan 263 | any number of paths on disks and load all the assets it finds into memory. 264 | 265 | For example, in this configuration we load every `*.svg` file found beneath the 266 | configured paths into memory: 267 | 268 | ```ruby 269 | InlineSvg.configure do |config| 270 | config.asset_file = InlineSvg::CachedAssetFile.new( 271 | paths: [ 272 | "#{Rails.root}/public/path/to/assets", 273 | "#{Rails.root}/public/other/path/to/assets" 274 | ], 275 | filters: /\.svg/ 276 | ) 277 | end 278 | ``` 279 | 280 | **Note:** Paths are read recursively, so think about keeping your SVG assets 281 | restricted to as few paths as possible, and using the filter option to further 282 | restrict assets to only those likely to be used by `inline_svg_tag`. 283 | 284 | ## Missing SVG Files 285 | 286 | If the specified SVG file cannot be found a helpful, empty SVG document is 287 | embedded into the page instead. The embedded document contains a single comment 288 | displaying the filename of the SVG image the helper tried to render: 289 | 290 | ```html 291 | 292 | ``` 293 | 294 | You may apply a class to this empty SVG document by specifying the following 295 | configuration: 296 | 297 | ```rb 298 | InlineSvg.configure do |config| 299 | config.svg_not_found_css_class = 'svg-not-found' 300 | end 301 | ``` 302 | 303 | Which would instead render: 304 | 305 | ```html 306 | 307 | ``` 308 | 309 | Alternatively, `inline_svg_tag` can be configured to raise an exception when a file 310 | is not found: 311 | 312 | ```ruby 313 | InlineSvg.configure do |config| 314 | config.raise_on_file_not_found = true 315 | end 316 | ``` 317 | 318 | ## ActiveStorage 319 | 320 | ```erb 321 | <%= user.avatar.open { |file| inline_svg_tag file } %> 322 | ``` 323 | 324 | ## Contributing 325 | 326 | 1. Fork it ( [http://github.com/jamesmartin/inline_svg/fork](http://github.com/jamesmartin/inline_svg/fork) ) 327 | 2. Create your feature branch (`git checkout -b my-new-feature`) 328 | 3. Commit your changes (`git commit -am 'Add some feature'`) 329 | 4. Push to the branch (`git push origin my-new-feature`) 330 | 5. Create new Pull Request 331 | 332 | Please write tests for anything you change, add or fix. 333 | There is a [basic Rails 334 | app](http://github.com/jamesmartin/inline_svg_test_app) that demonstrates the 335 | gem's functionality in use. 336 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## [Unreleased][unreleased] 6 | 7 | - Drop Support for Webpacker, Legacy Ruby (< 3.1), and Legacy Rails (< 7.0). [#163](https://github.com/jamesmartin/inline_svg/pull/163). Thanks, [@tagliala](https://github.com/tagliala) 8 | - Reduce production gem size from 31K to 18K. [#165](https://github.com/jamesmartin/inline_svg/pull/165). Thanks, [@tagliala](https://github.com/tagliala) 9 | - Freeze string literals. [#172](https://github.com/jamesmartin/inline_svg/pull/172). Thanks, [@tagliala](https://github.com/tagliala) 10 | - Fix thread-local variable leakage in `with_asset_finder`. [#185](https://github.com/jamesmartin/inline_svg/pull/185). Thanks, [@tagliala](https://github.com/tagliala) 11 | - Remove unused `InlineSvg::IOResource.default_for` method. [#187](https://github.com/jamesmartin/inline_svg/pull/187). Thanks, [@tagliala](https://github.com/tagliala) 12 | - Add support for Tempfile. [#186](https://github.com/jamesmartin/inline_svg/pull/186). Thanks, [@javierav](https://github.com/javierav) 13 | 14 | ## [1.10.0] - 2024-09-03 15 | ### Added 16 | - Support for Shakapacker. [#158](https://github.com/jamesmartin/inline_svg/pull/158). Thanks, [@tagliala](https://github.com/tagliala) 17 | 18 | ### Fixed 19 | - Fixed documentation typos. [#157](https://github.com/jamesmartin/inline_svg/pull/157). Thanks, [@tagliala](https://github.com/tagliala) 20 | - Fixed missing ActiveSupport require. [#152](https://github.com/jamesmartin/inline_svg/pull/152). Thanks, [@xymbol](https://github.com/xymbol) 21 | - Remove wrapping whitespace from SVG tags. [#150](https://github.com/jamesmartin/inline_svg/pull/150). Thanks, [@fredboyle](https://github.com/fredboyle) 22 | 23 | ## [1.9.0] - 2023-03-29 24 | ### Added 25 | - A new option: `view_box` adds a `viewBox` attribute to the SVG. [#142](https://github.com/jamesmartin/inline_svg/pull/142). Thanks [@sunny](https://github.com/sunny) 26 | 27 | ### Fixed 28 | - Allow Propshaft assets to use fallbacks. [#140](https://github.com/jamesmartin/inline_svg/pull/140). Thanks, [@ohrite](https://github.com/ohrite) 29 | - Handling missing file when using static assets. [#141](https://github.com/jamesmartin/inline_svg/pull/141). Thanks, [@leighhalliday](https://github.com/leighhalliday) 30 | - Handle missing file when using Webpacker assets. 31 | 32 | ## [1.8.0] - 2022-01-09 33 | ### Added 34 | - Remove deprecation warning for `inline_svg`, as we intend to keep it in 2.0. [#131](https://github.com/jamesmartin/inline_svg/pull/131). Thanks [@DanielJackson-Oslo](https://github.com/DanielJackson-Oslo) 35 | - Add support for Webpacker 6 beta. [#129](https://github.com/jamesmartin/inline_svg/pull/129). Thanks [@Intrepidd](https://github.com/Intrepidd) and [@tessi](https://github.com/tessi) 36 | - Add support for Propshaft assets in Rails 7. [#134](https://github.com/jamesmartin/inline_svg/pull/134). Thanks, [@martinzamuner](https://github.com/martinzamuner) 37 | 38 | ## [1.7.2] - 2020-12-07 39 | ### Fixed 40 | - Improve performance of `CachedAssetFile`. [#118](https://github.com/jamesmartin/inline_svg/pull/118). Thanks [@stevendaniels](https://github.com/stevendaniels) 41 | - Avoid XSS by preventing malicious input of filenames. [#117](https://github.com/jamesmartin/inline_svg/pull/117). Thanks [@pbyrne](https://github.com/pbyrne). 42 | 43 | ## [1.7.1] - 2020-03-17 44 | ### Fixed 45 | - Static Asset Finder uses pathname for compatibility with Sprockets 4+. [#106](https://github.com/jamesmartin/inline_svg/pull/106). Thanks [@subdigital](https://github.com/subdigital) 46 | 47 | ## [1.7.0] - 2020-02-13 48 | ### Added 49 | - WebpackAssetFinder serves files from dev server if one is running. [#111](https://github.com/jamesmartin/inline_svg/pull/111). Thanks, [@connorshea](https://github.com/connorshea) 50 | 51 | ### Fixed 52 | - Using Webpacker and Asset Pipeline in a single App could result in SVGs not being found because the wrong `AssetFinder` was used. [#114](https://github.com/jamesmartin/inline_svg/pull/114). Thanks, [@kylefox](https://github.com/kylefox) 53 | - Prevent "EOFError error" when using webpack dev server over HTTPS [#113](https://github.com/jamesmartin/inline_svg/pull/113). Thanks, [@kylefox](https://github.com/kylefox) 54 | 55 | ## [1.6.0] - 2019-11-13 56 | ### Added 57 | - Support Webpack via the new `inline_svg_pack_tag` helper and deprecate `inline_svg` helper in preparation for v2.0. 58 | [#103](https://github.com/jamesmartin/inline_svg/pull/103) 59 | Thanks, [@kylefox](https://github.com/kylefox) 60 | 61 | ## [1.5.2] - 2019-06-20 62 | ### Fixed 63 | - Revert automatic Webpack asset finder behavior. Make Webpack "opt-in". 64 | [#98](https://github.com/jamesmartin/inline_svg/issues/98) 65 | 66 | ## [1.5.1] - 2019-06-18 67 | ### Fixed 68 | - Prevent nil asset finder when neither Sprockets or Webpacker are available 69 | [#97](https://github.com/jamesmartin/inline_svg/issues/97) 70 | 71 | ## [1.5.0] - 2019-06-17 72 | ### Added 73 | - Support for finding assets bundled by Webpacker 74 | [#96](https://github.com/jamesmartin/inline_svg/pull/96) 75 | 76 | ## [1.4.0] - 2019-04-19 77 | ### Fixed 78 | - Prevent invalid XML names being generated via IdGenerator 79 | [#87](https://github.com/jamesmartin/inline_svg/issues/87) 80 | Thanks, [@endorfin](https://github.com/endorfin) 81 | 82 | ### Added 83 | - Raise error on file not found (if configured) 84 | [#93](https://github.com/jamesmartin/inline_svg/issues/93) 85 | 86 | ## [1.3.1] - 2017-12-14 87 | ### Fixed 88 | - Allow Ruby < 2.1 to work with `CachedAssetFile` 89 | [#80](https://github.com/jamesmartin/inline_svg/pull/80) 90 | 91 | ## [1.3.0] - 2017-10-30 92 | ### Added 93 | - Aria hidden attribute 94 | [#78](https://github.com/jamesmartin/inline_svg/pull/78) 95 | and [#79](https://github.com/jamesmartin/inline_svg/pull/79) 96 | - In-line CSS style attribute 97 | [#71](https://github.com/jamesmartin/inline_svg/pull/71) 98 | 99 | ### Fixed 100 | - Make aria ID attributes unique 101 | [#77](https://github.com/jamesmartin/inline_svg/pull/77) 102 | 103 | ## [1.2.3] - 2017-08-17 104 | ### Fixed 105 | - Handle UTF-8 characters in SVG documents 106 | [#60](https://github.com/jamesmartin/inline_svg/pull/69) 107 | 108 | ## [1.2.2] - 2017-07-06 109 | ### Fixed 110 | - Handle malformed documents that don't contain a root SVG element 111 | [#60](https://github.com/jamesmartin/inline_svg/pull/65) 112 | ### Added 113 | - Add configurable CSS class to empty SVG document 114 | [#67](https://github.com/jamesmartin/inline_svg/pull/67) 115 | 116 | ## [1.2.1] - 2017-05-02 117 | ### Fixed 118 | - Select most exactly matching cached asset file when multiple files match 119 | given asset name [#64](https://github.com/jamesmartin/inline_svg/pull/64) 120 | 121 | ## [1.2.0] - 2017-04-20 122 | ### Added 123 | - Cached asset file (load assets into memory at boot time) 124 | [#62](https://github.com/jamesmartin/inline_svg/pull/62) 125 | 126 | ## [1.1.0] - 2017-04-12 127 | ### Added 128 | - Allow configurable asset file implementations 129 | [#61](https://github.com/jamesmartin/inline_svg/pull/61) 130 | 131 | ## [1.0.1] - 2017-04-10 132 | ### Fixed 133 | - Don't override custom asset finders in Railtie 134 | 135 | ## [1.0.0] - 2017-04-7 136 | ### Added 137 | - Remove dependency on `Loofah` while maintaining basic `nocomment` transform 138 | 139 | ## [0.12.1] - 2017-03-24 140 | ### Added 141 | - Relax dependency on `Nokogiri` to allow users to upgrade to v1.7x, preventing 142 | exposure to 143 | [CVE-2016-4658](https://github.com/sparklemotion/nokogiri/issues/1615): 144 | [#59](https://github.com/jamesmartin/inline_svg/issues/59) 145 | 146 | ## [0.12.0] - 2017-03-16 147 | ### Added 148 | - Relax dependency on `ActiveSupport` to allow Rails 3 applications to use the 149 | gem: [#54](https://github.com/jamesmartin/inline_svg/issues/54) 150 | 151 | ## [0.11.1] - 2016-11-22 152 | ### Fixed 153 | - Dasherize data attribute names: 154 | [#51](https://github.com/jamesmartin/inline_svg/issues/51) 155 | - Prevent ID collisions between `desc` and `title` attrs: 156 | [#52](https://github.com/jamesmartin/inline_svg/pull/52) 157 | 158 | ## [0.11.0] - 2016-07-24 159 | ### Added 160 | - Priority ordering for transformations 161 | 162 | ### Fixed 163 | - Prevent duplicate desc elements being created 164 | [#46](https://github.com/jamesmartin/inline_svg/issues/46) 165 | - Prevent class attributes being replaced 166 | [#44](https://github.com/jamesmartin/inline_svg/issues/44) 167 | 168 | ## [0.10.0] - 2016-07-24 169 | ### Added 170 | - Rails 5 support [#43](https://github.com/jamesmartin/inline_svg/pull/43) 171 | - Support for `Sprockets::Asset` 172 | [#45](https://github.com/jamesmartin/inline_svg/pull/45) 173 | 174 | ## [0.9.1] - 2016-07-18 175 | ### Fixed 176 | - Provide a hint when the .svg extension is omitted from the filename 177 | [#41](https://github.com/jamesmartin/inline_svg/issues/41) 178 | 179 | ## [0.9.0] - 2016-06-30 180 | ### Fixed 181 | - Hashed IDs for desc and title elements in aria-labeled-by attribute 182 | [#38](https://github.com/jamesmartin/inline_svg/issues/38) 183 | 184 | ## [0.8.0] - 2016-05-23 185 | ### Added 186 | - Default values for custom transformations 187 | [#36](https://github.com/jamesmartin/inline_svg/issues/36). Thanks, 188 | [@andrewaguiar](https://github.com/andrewaguiar) 189 | 190 | ## [0.7.0] - 2016-05-03 191 | ### Added 192 | - Aria attributes transform (aria-labelledby / role etc.) Addresses issue 193 | [#28](https://github.com/jamesmartin/inline_svg/issues/28) 194 | 195 | ## [0.6.4] - 2016-04-23 196 | ### Fixed 197 | - Don't duplicate the `title` element. Addresses issue 198 | [#31](https://github.com/jamesmartin/inline_svg/issues/31) 199 | - Make the `title` element the first child node of the SVG document 200 | 201 | ## [0.6.3] - 2016-04-19 202 | ### Added 203 | - Accept `IO` objects as arguments to `inline_svg`. Thanks, 204 | [@ASnow](https://github.com/ASnow). 205 | 206 | ## [0.6.2] - 2016-01-24 207 | ### Fixed 208 | - Support Sprockets >= 3.0 and config.assets.precompile = false 209 | 210 | ## [0.6.1] - 2015-08-06 211 | ### Fixed 212 | - Support Rails versions back to 4.0.4. Thanks, @walidvb. 213 | 214 | ## [0.6.0] - 2015-07-07 215 | ### Added 216 | - Apply user-supplied [custom 217 | transformations](https://github.com/jamesmartin/inline_svg/blob/master/README.md#custom-transformations) to a document. 218 | 219 | ## [0.5.3] - 2015-06-22 220 | ### Added 221 | - `preserveAspectRatio` transformation on SVG root node. Thanks, @paulozoom. 222 | 223 | ## [0.5.2] - 2015-04-03 224 | ### Fixed 225 | - Support Sprockets v2 and v3 (Sprockets::Asset no longer to_s to a filename) 226 | 227 | ## [0.5.1] - 2015-03-30 228 | ### Warning 229 | ** This version is NOT compatible with Sprockets >= 3. ** 230 | 231 | ### Fixed 232 | - Support for ActiveSupport (and hence, Rails) 4.2.x. Thanks, @jmarceli. 233 | 234 | ## [0.5.0] - 2015-03-29 235 | ### Added 236 | - A new option: `id` adds an id attribute to the SVG. 237 | - A new option: `data` adds data attributes to the SVG. 238 | 239 | ### Changed 240 | - New options: `height` and `width` override `size` and can be set independently. 241 | 242 | ## [0.4.0] - 2015-03-22 243 | ### Added 244 | - A new option: `size` adds width and height attributes to an SVG. Thanks, @2metres. 245 | 246 | ### Changed 247 | - Dramatically simplified the TransformPipeline and Transformations code. 248 | - Added tests for the pipeline and new size transformations. 249 | 250 | ### Fixed 251 | - Transformations can no longer be created with a nil value. 252 | 253 | ## [0.3.0] - 2015-03-20 254 | ### Added 255 | - Use Sprockets to find canonical asset paths (fingerprinted, post asset-pipeline). 256 | 257 | ## [0.2.0] - 2014-12-31 258 | ### Added 259 | - Optionally remove comments from SVG files. Thanks, @jmarceli. 260 | 261 | ## [0.1.0] - 2014-12-15 262 | ### Added 263 | - Optionally add a title and description to a document. Thanks, ludwig.schubert@qlearning.de. 264 | - Add integration tests for main view helper. Thanks, ludwig.schubert@qlearning.de. 265 | 266 | ## 0.0.1 - 2014-11-24 267 | ### Added 268 | - Basic Railtie and view helper to inline SVG documents to Rails views. 269 | 270 | [unreleased]: https://github.com/jamesmartin/inline_svg/compare/v1.10.0...HEAD 271 | [1.10.0]: https://github.com/jamesmartin/inline_svg/compare/v1.9.0...v1.10.0 272 | [1.9.0]: https://github.com/jamesmartin/inline_svg/compare/v1.8.0...v1.9.0 273 | [1.8.0]: https://github.com/jamesmartin/inline_svg/compare/v1.7.2...v1.8.0 274 | [1.7.2]: https://github.com/jamesmartin/inline_svg/compare/v1.7.1...v1.7.2 275 | [1.7.1]: https://github.com/jamesmartin/inline_svg/compare/v1.7.0...v1.7.1 276 | [1.7.0]: https://github.com/jamesmartin/inline_svg/compare/v1.6.0...v1.7.0 277 | [1.6.0]: https://github.com/jamesmartin/inline_svg/compare/v1.5.2...v1.6.0 278 | [1.5.2]: https://github.com/jamesmartin/inline_svg/compare/v1.5.1...v1.5.2 279 | [1.5.1]: https://github.com/jamesmartin/inline_svg/compare/v1.5.0...v1.5.1 280 | [1.5.0]: https://github.com/jamesmartin/inline_svg/compare/v1.4.0...v1.5.0 281 | [1.4.0]: https://github.com/jamesmartin/inline_svg/compare/v1.3.1...v1.4.0 282 | [1.3.1]: https://github.com/jamesmartin/inline_svg/compare/v1.3.0...v1.3.1 283 | [1.3.0]: https://github.com/jamesmartin/inline_svg/compare/v1.2.3...v1.3.0 284 | [1.2.3]: https://github.com/jamesmartin/inline_svg/compare/v1.2.2...v1.2.3 285 | [1.2.2]: https://github.com/jamesmartin/inline_svg/compare/v1.2.1...v1.2.2 286 | [1.2.1]: https://github.com/jamesmartin/inline_svg/compare/v1.2.0...v1.2.1 287 | [1.2.0]: https://github.com/jamesmartin/inline_svg/compare/v1.1.0...v1.2.0 288 | [1.1.0]: https://github.com/jamesmartin/inline_svg/compare/v1.0.1...v1.1.0 289 | [1.0.1]: https://github.com/jamesmartin/inline_svg/compare/v1.0.0...v1.0.1 290 | [1.0.0]: https://github.com/jamesmartin/inline_svg/compare/v0.12.1...v1.0.0 291 | [0.12.1]: https://github.com/jamesmartin/inline_svg/compare/v0.12.0...v0.12.1 292 | [0.12.0]: https://github.com/jamesmartin/inline_svg/compare/v0.11.1...v0.12.0 293 | [0.11.1]: https://github.com/jamesmartin/inline_svg/compare/v0.11.0...v0.11.1 294 | [0.11.0]: https://github.com/jamesmartin/inline_svg/compare/v0.10.0...v0.11.0 295 | [0.10.0]: https://github.com/jamesmartin/inline_svg/compare/v0.9.1...v0.10.0 296 | [0.9.1]: https://github.com/jamesmartin/inline_svg/compare/v0.9.0...v0.9.1 297 | [0.9.0]: https://github.com/jamesmartin/inline_svg/compare/v0.8.0...v0.9.0 298 | [0.8.0]: https://github.com/jamesmartin/inline_svg/compare/v0.7.0...v0.8.0 299 | [0.7.0]: https://github.com/jamesmartin/inline_svg/compare/v0.6.4...v0.7.0 300 | [0.6.4]: https://github.com/jamesmartin/inline_svg/compare/v0.6.3...v0.6.4 301 | [0.6.3]: https://github.com/jamesmartin/inline_svg/compare/v0.6.2...v0.6.3 302 | [0.6.2]: https://github.com/jamesmartin/inline_svg/compare/v0.6.1...v0.6.2 303 | [0.6.1]: https://github.com/jamesmartin/inline_svg/compare/v0.6.0...v0.6.1 304 | [0.6.0]: https://github.com/jamesmartin/inline_svg/compare/v0.5.3...v0.6.0 305 | [0.5.3]: https://github.com/jamesmartin/inline_svg/compare/v0.5.2...v0.5.3 306 | [0.5.2]: https://github.com/jamesmartin/inline_svg/compare/v0.5.1...v0.5.2 307 | [0.5.1]: https://github.com/jamesmartin/inline_svg/compare/v0.5.0...v0.5.1 308 | [0.5.0]: https://github.com/jamesmartin/inline_svg/compare/v0.4.0...v0.5.0 309 | [0.4.0]: https://github.com/jamesmartin/inline_svg/compare/v0.3.0...v0.4.0 310 | [0.3.0]: https://github.com/jamesmartin/inline_svg/compare/v0.2.0...v0.3.0 311 | [0.2.0]: https://github.com/jamesmartin/inline_svg/compare/v0.1.0...v0.2.0 312 | [0.1.0]: https://github.com/jamesmartin/inline_svg/compare/v0.0.1...v0.1.0 313 | --------------------------------------------------------------------------------