Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Inline SVG in Middleman
# Middleman - Inline SVG Helper
# ------------------------------------------------------------------------------
#
# Installation
# ------------
# 1. Save this file at `[project_root]/helpers/image_helpers.rb`
# 2. Open the project's Gemfile and add this line: `gem "oga"`
# 3. Run `bundle install` from the command line
#
# Note: Restart your local Middleman server (if it's running) before continuing
#
# Usage
# -----
# Embed SVG files into your template files like so:
#
# ```
# <%= inline_svg "name/of/file.svg" %>
# ```
#
# The helper also allows you to pass attributes to add to the SVG tag, like so:
#
# Input:
# ```
# <%= inline_svg "name/of/file.svg", class: "foo", data: {bar: "baz"} %>
# ```
#
# Output:
# ```
# <svg <!-- existing attributes --> class="foo" data-bar="baz">
# <!-- SVG contents -->
# </svg>
# ```
#
# Acknowledgements and Contributors
# --------------------------
# This was initally adapted from the work of James Martin
# and Steven Harley, which you can read more about here:
# https://robots.thoughtbot.com/organized-workflow-for-svg
#
# Further improvements were made based on contributions by:
#
# * Cecile Veneziani (@cveneziani)
# * Allan White (@allanwhite)
#
# Thanks for improving this helper! Have questions or concerns?
# Feel free to fork the Gist or comment on it here:
# https://gist.github.com/bitmanic/0047ef8d7eaec0bf31bb
module ImageHelpers
def inline_svg(relative_image_path, optional_attributes = {})
image_path = File.join(config[:source], config[:images_dir], relative_image_path)
# If the image was found...
if File.exists?(image_path)
# Open the image
image = File.open(image_path, 'r') { |f| f.read }
# Return the image if no optional attributes were passed in
return image if optional_attributes.empty?
# Otherwise, parse the image
document = Oga.parse_xml(image)
svg = document.css('svg').first
# Then, add the attributes
# NOTE: This allows for hash-based values, but we're only going one level
# deep right now. If you know a great way to dig `N` levels deeper,
# feel free to post about it on the Gist.
optional_attributes.each do |attribute, value|
case value
when Hash
value.each do |subattribute, subvalue|
unless subvalue.class == Hash
svg.set(
"#{attribute} #{subattribute}".parameterize,
subvalue.html_safe
)
end
end
else
svg.set(attribute.to_sym, value.html_safe)
end
end
# Finally, return the image
document.to_xml
# If the file wasn't found...
else
# Embed an inline SVG image with an error message
%(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 30"
width="400px" height="30px"
>
<text font-size="16" x="8" y="20" fill="#cc0000">
Error: '#{relative_image_path}' could not be found.
</text>
<rect
x="1" y="1" width="398" height="28" fill="none"
stroke-width="1" stroke="#cc0000"
/>
</svg>
)
end
end
end
@AntonKL

This comment has been minimized.

Copy link

AntonKL commented Oct 22, 2015

I've placed my assets in a assets folder. I'm then including the file by <%= inline_svg("/assets/images/my-image.svg") %>.

I get a not found triggered from the helper. Is this setup invalid?

My folder structure

helpers
- image_helpers.rb
source
- index.html.erb
  assets
    images
    - my-image.svg
@AntonKL

This comment has been minimized.

Copy link

AntonKL commented Oct 22, 2015

Oh never mind. It's me being dumb. Didn't realized you used sprockets to locate it by filename and not url.

@hansondr

This comment has been minimized.

Copy link

hansondr commented Jun 7, 2016

modified this as follows:

# lib/image_helpers.rb
module ImageHelpers
  # usage <%= inline_svg("logo"); %> assuming logo.svg is stored at source/images/svg/logo.svg
  def inline_svg(filename, options = {})
    asset = "source/images/svg/#{filename}.svg"

    if File.exists?(asset)
      file = File.open(asset, 'r') { |f| f.read }
      doc = Nokogiri::HTML::DocumentFragment.parse(file)
      svg = doc.at_css("svg")

      if options[:class].present?
        svg["class"] = options[:class]
      end

      doc
    else
      %(
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 30"
          width="400px" height="30px"
        >
          <text font-size="16" x="8" y="20" fill="#cc0000">
            Error: '#{filename}' could not be found.
          </text>
          <rect
            x="1" y="1" width="398" height="28" fill="none"
            stroke-width="1" stroke="#cc0000"
          />
        </svg>
      )
    end
  end
end

Then add the following to your config.rb

...

require "lib/image_helpers"
helpers ImageHelpers
@cveneziani

This comment has been minimized.

Copy link

cveneziani commented Jan 5, 2017

For the ones that don't like to add the huge Nokogiri as dependency, I propose an alternative with Oga.

By just replacing Nokogiri implementation:

doc = Nokogiri::HTML::DocumentFragment.parse(file)
svg = doc.at_css("svg")

if options[:class].present?
  svg["class"] = options[:class]
end

doc

With Oga implementation:

css_class = options[:class]
return file if css_class.nil?

document        = Oga.parse_xml(file)
svg             = document.css('svg').first
class_attribute = svg.attribute(:class)

if class_attribute
  class_attribute.value = css_class
else
  class_attribute = Oga::XML::Attribute.new(name: :class, value: css_class)
  svg.add_attribute(class_attribute)
end

document.to_xml

BTW I also avoid parsing the XML if no class option is provided. I directly return the content of the file =)

@cveneziani

This comment has been minimized.

Copy link

cveneziani commented Jan 5, 2017

Refactored "a bit" :)

css_class = options[:class]
return file if css_class.nil?

document = Oga.parse_xml(file)
svg      = document.css('svg').first

svg.set(:class, css_class)

document.to_xml
@hellokatili

This comment has been minimized.

Copy link

hellokatili commented May 24, 2017

Is there a way to modify this helper so I can use it with HAML-SVGs? (I have to set the xlink:href of an image with the image_path helper.)

Example SVG:

%svg{viewbox: '0 0 200 200', xmlns: 'http//www.w3.org/2000/svg', 'xmlns:xlink' => 'http//www.w3.org/1999/xlink'}
  %image#image{height: '20', width: '20', 'xlink:href' => image_path('image.jpg')}
@allanwhite

This comment has been minimized.

Copy link

allanwhite commented Dec 6, 2017

I found this gist and approach really very helpful, and am using it now in our middleman-driven site. I made a few tweaks someone might find useful:

# /helpers/image_helpers.rb 
module ImageHelpers
  # usage <%= inline_svg("logo"); %> assuming logo.svg is stored at source/assets/icons/logo.svg
  def inline_svg(filename, options = {})
    asset = "source/assets/icons/#{filename}.svg"

    if File.exists?(asset)
      file = File.open(asset, 'r') { |f| f.read }
      # we pass svg-targeting css classes through here. The class targets fill, stroke, poly, circle, etc. 
      css_class = options[:class]
      aspect_ratio = options[:preserveAspectRatio]
      # added some aspect-ratio settings; default covers most of our svg use cases. We want all our SVG icon artwork to scale to fit. This could be an argument if needed.
      radio_default = "xMidYMid meet"
      return file if css_class.nil?
      document = Oga.parse_xml(file)
      svg      = document.css('svg').first
      svg.set(:class, css_class)
      svg.set(:preserveAspectRatio, radio_default)
      document.to_xml

    else
      # added a little more helpful info here so it's easier to track down a problem.
      puts "inline_svg '#{asset}' at #{current_page.url} could not be found!"
      %(
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 60" width="400px" height="60px">
          <text font-size="12" x="8" y="20" fill="#cc0000">
            Error: '#{asset}' could not be found.
          </text>
          <rect x="1" y="1" width="398" height="38" fill="none" stroke-width="1" stroke="#cc0000" />
        </svg>
      )
    end
  end
end
@bitmanic

This comment has been minimized.

Copy link
Owner Author

bitmanic commented Apr 10, 2018

I've updated this Gist to perform better and support more attributes. This work relies on the improvements made by @cveneziani and @allanwhite. Thanks so much for your contributions!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.