class Esvg::Symbol

Attributes

content[R]
defs[R]
group[R]
id[R]
mtime[R]
name[R]
optimized[R]
path[R]
size[R]

Public Class Methods

new(path, parent) click to toggle source
# File lib/esvg/symbol.rb, line 9
def initialize(path, parent)
  @parent  = parent
  @path    = path
  @last_checked = 0
  load_data
  read
end

Public Instance Methods

attr() click to toggle source
# File lib/esvg/symbol.rb, line 110
def attr
  { id: @id, 'data-name' => @name }.merge @size
end
changed?() click to toggle source
# File lib/esvg/symbol.rb, line 209
def changed?
  last_modified != mtime
end
config() click to toggle source
# File lib/esvg/symbol.rb, line 17
def config
  @parent.config
end
data() click to toggle source
# File lib/esvg/symbol.rb, line 95
def data
  {
    path: @path,
    name: @name,
    group: @group,
    mtime: @mtime,
    size: @size,
    content: @content,
    defs: @defs,
    optimized: @optimized,
    optimized_at: @optimized_at,
    svgo_optimized: optimize? && @svgo_optimized
  }
end
dir() click to toggle source
# File lib/esvg/symbol.rb, line 25
def dir
  @group
end
exist?() click to toggle source
# File lib/esvg/symbol.rb, line 21
def exist?
  File.exist?(@path)
end
height() click to toggle source
# File lib/esvg/symbol.rb, line 56
def height
  @size[:height]
end
optimize() click to toggle source
# File lib/esvg/symbol.rb, line 179
def optimize
  read if changed?

  # Only optimize again if the file has changed
  if @optimized && @optimized_at && @optimized_at > @mtime
    return @optimized
  end

  # Only optimize if SVGO is installed
  if optimize?
    puts "Optimizing #{name}.svg" if config[:print]

    response = Open3.capture3(%Q{#{Esvg.node_module('svgo')} --disable=removeUselessDefs -s '#{@content}' -o -})
    if !response[0].empty? && response[2].success?
      @optimized = response[0]
      @svgo_optimized = true
    end

    post_optimize
    @optimized_at = Time.now.to_i

    @optimized
  end

end
optimize?() click to toggle source

Only optimize if

  • Configuration asks for it

  • SVGO is present

  • If Rails is present

# File lib/esvg/symbol.rb, line 175
def optimize?
  config[:optimize] && !!Esvg.node_module('svgo') && config[:env] == 'production'
end
read() click to toggle source
# File lib/esvg/symbol.rb, line 29
def read
  return if !exist?

  # Ensure that cache optimization matches current optimization settings
  # If config has changed name, reset optimized build (name gets baked in)
  if changed? || @svgo_optimized != optimize? || name != file_name
    @optimized = nil
    @optimized_at = nil
  end

  @group = dir_key
  @name  = file_name
  @id    = file_id file_key

  if changed?
    @content = prep_defs pre_optimize File.read(@path)
    @mtime   = last_modified
    @size    = dimensions
  end

  self
end
scale( a ) click to toggle source
# File lib/esvg/symbol.rb, line 79
def scale( a )
  # Width was set, determine scaled height
  if a[:width]
    a[:height] ||= scale_height( a[:width] )
  # Height was set, determine scaled width
  elsif a[:height]
    a[:width] ||= scale_width( a[:height] )
  # Nothing was set, default to dimensions
  else
    a[:width]  = width
    a[:height] = height
  end

  a
end
scale_height( w ) click to toggle source

Scale height based on propotion to width

# File lib/esvg/symbol.rb, line 67
def scale_height( w )
  s = split_unit( w )
  "#{( s[:size] / width * height ).round(2)}#{s[:unit]}"
end
scale_width( h ) click to toggle source

Scale width based on propotion to height

# File lib/esvg/symbol.rb, line 61
def scale_width( h )
  s = split_unit( h )
  "#{( s[:size] / height * width ).round(2)}#{s[:unit]}"
end
split_unit( size ) click to toggle source

Separate size and unit for easier math. Returns: { size: 10, unit: 'px' }

# File lib/esvg/symbol.rb, line 74
def split_unit( size )
  m = size.to_s.match(/(\d+)\s*(\D*)/)
  { size: m[1].to_f, unit: m[2] }
end
symbol() click to toggle source
# File lib/esvg/symbol.rb, line 205
def symbol
  symbolize( optimize || @content )
end
use(options={}) click to toggle source
# File lib/esvg/symbol.rb, line 114
def use(options={})
  
  # If preset key is set, merge presets from configuration
  if options[:preset] && preset = config[:presets][ options.delete(:preset).to_sym ]
    options = options.merge( preset )
  end

  # If size key is set, merge size class from configuration
  if options[:size] && size_class = config[:sizes][ options.delete(:size).to_sym ]
    options = options.merge( size_class )
  end

  options.delete(:fallback)
  content = options.delete(:content) || ''

  if desc   = options.delete(:desc)
    content = "<desc>#{desc}</desc>#{content}"
  end
  if title  = options.delete(:title)
    content = "<title>#{title}</title>#{content}"
  end

  use_attr = options.delete(:use) || {}

  svg_attr = {
    class: [config[:class], config[:prefix]+"-"+@name, options.delete(:class)].compact.join(' '),
    viewBox: @size[:viewBox],
    role: 'img'
  }.merge(options)

  if svg_attr[:scale]
    # User doesn't want dimensions to be set
    svg_attr.delete(:scale)
  else
    # Scale dimensions based on attributes
    svg_attr = scale( svg_attr )
  end

  %Q{<svg #{attributes(svg_attr)}>#{use_tag(use_attr)}#{content}</svg>}
end
use_tag(options={}) click to toggle source
# File lib/esvg/symbol.rb, line 155
def use_tag(options={})
  options["xlink:href"] = "##{@id}"

  if options[:scale] && config[:scale]
    # User doesn't want dimensions to be set
    options.delete(:scale)
  else
    # Scale dimensions based on attributes
    options = scale( options )
  end

  options.delete(:scale)

  %Q{<use #{attributes(options)}></use>}
end
width() click to toggle source
# File lib/esvg/symbol.rb, line 52
def width
  @size[:width]
end

Private Instance Methods

dimensions() click to toggle source
# File lib/esvg/symbol.rb, line 275
def dimensions
  if viewbox = @content.scan(/<svg.+(viewBox=["'](.+?)["'])/).flatten.last
    coords  = viewbox.split(' ')

    {
      viewBox: viewbox,
      width: (coords[2].to_i - coords[0].to_i).abs,
      height: (coords[3].to_i - coords[1].to_i).abs
    }
  else
    # In case a viewbox hasn't been set, derive one.
    if height = @content.scan(/<svg.+(height=["'](.+?)["'])/).flatten.last &&
       width = @content.scan(/<svg.+(width=["'](.+?)["'])/).flatten.last
      {
        viewBox: "0 0 #{width} #{height}",
        width: width.to_i,
        height: height.to_i
      }
    else
      {}
    end
  end
end
dir_key() click to toggle source
# File lib/esvg/symbol.rb, line 248
def dir_key
  dir = File.dirname(flatten_path)

  # Flattened paths which should be treated as assets will use '_' as their dir key
  # - flatten: _foo - _foo/icon.svg will have a dirkey of _
  # - filename: _icons - treats all root or flattened files as assets
  if dir == '.' && ( local_path.start_with?('_') || config[:filename].start_with?('_') )
    '_'
  else
    dir
  end
end
file_id(name) click to toggle source
# File lib/esvg/symbol.rb, line 232
def file_id(name)
  dasherize "#{config[:prefix]}-#{name}"
end
file_key() click to toggle source
# File lib/esvg/symbol.rb, line 244
def file_key
  dasherize local_path.sub('.svg','')
end
file_name() click to toggle source
# File lib/esvg/symbol.rb, line 240
def file_name
  dasherize flatten_path.sub('.svg','')
end
flatten_path() click to toggle source
# File lib/esvg/symbol.rb, line 261
def flatten_path
  @flattened_path ||= local_path.sub(Regexp.new(config[:flatten_dir]), '')
end
last_modified() click to toggle source
# File lib/esvg/symbol.rb, line 223
def last_modified
  if Time.now.to_i - @last_checked < config[:throttle_read]
    @last_modified
  else
    @last_checked = Time.now.to_i
    @last_modified = File.mtime(@path).to_i
  end
end
load_data() click to toggle source
# File lib/esvg/symbol.rb, line 215
def load_data
  if config[:cache]
    config.delete(:cache).each do |name, value|
      instance_variable_set("@#{name}", value)
    end
  end
end
local_path() click to toggle source
# File lib/esvg/symbol.rb, line 236
def local_path
  @local_path ||= sub_path(config[:source], @path)
end
name_key(key) click to toggle source
# File lib/esvg/symbol.rb, line 265
def name_key(key)
  if key == '_'  # Root level asset file
    "_#{config[:filename]}".sub(/_+/, '_')
  elsif key == '.'      # Root level build file
    config[:filename]
  else
    "#{key}"
  end
end
post_optimize() click to toggle source
# File lib/esvg/symbol.rb, line 316
def post_optimize
  @optimized.gsub!(/\w+=""/,'') # Remove empty attributes
end
pre_optimize(svg) click to toggle source
# File lib/esvg/symbol.rb, line 299
def pre_optimize(svg)

  # Generate a regex of attributes to be removed
  att = Regexp.new %w(xmlns xmlns:xlink xml:space version).map { |m| "#{m}=\".+?\"" }.join('|')

  svg.strip
    .gsub(att, '')                                       # Remove unwanted attributes
    .sub(/.+?<svg/,'<svg')                               # Get rid of doctypes and comments
    .gsub(/<!--(.+?)-->/m, '')                           # Remove XML comments
    .gsub(/style="([^"]*?)fill:(.+?);/m, 'fill="\2" style="\1')                   # Make fill a property instead of a style
    .gsub(/style="([^"]*?)fill-opacity:(.+?);/m, 'fill-opacity="\2" style="\1')   # Move fill-opacity a property instead of a style
    .gsub(/\n/m, ' ')                                    # Remove endlines
    .gsub(/\s{2,}/, ' ')                                 # Remove whitespace
    .gsub(/>\s+</, '><')                                 # Remove whitespace between tags
    .gsub(/\s?fill="(#0{3,6}|black|none|rgba?\(0,0,0\))"/,'') # Strip black fill
end
prep_defs(svg) click to toggle source

Scans <def> blocks for IDs If urls(id) are used, ensure these IDs are unique to this file Only replace IDs if urls exist to avoid replacing defs used in other svg files

# File lib/esvg/symbol.rb, line 340
def prep_defs(svg)

  # <defs> should be moved to the beginning of the SVG file for braod browser support. Ahem, Firefox ಠ_ಠ
  # When symbols are reassembled, @defs will be added back

  if @defs = svg.scan(/<defs>(.+)<\/defs>/m).flatten[0]
    svg.sub!("<defs>#{@defs}</defs>", '')
    @defs.gsub!(/(\n|\s{2,})/,'')

    @defs.scan(/id="(.+?)"/).flatten.uniq.each_with_index do |id, index|

      # If there are urls matching def ids
      if svg.match(/url\(##{id}\)/)

        new_id = "def-#{@id}-#{index}"                 # Generate a unique id
        @defs.gsub!(/id="#{id}"/, %Q{id="#{new_id}"})  # Replace the def ids
        svg.gsub!(/url\(##{id}\)/, "url(##{new_id})")  # Replace url references to these old def ids
      end
    end
  end

  svg
end
strip_attributes( str ) click to toggle source
# File lib/esvg/symbol.rb, line 327
def strip_attributes( str )
  attr.keys.each do |key|
    str.sub!(/ #{key}=".+?"/,'')
  end

  str
end
symbolize( str ) click to toggle source
# File lib/esvg/symbol.rb, line 320
def symbolize( str )
  strip_attributes( str )
    .gsub(/<\/svg/,'</symbol')      # Replace svgs with symbols
    .gsub(/\w+=""/,'')              # Remove empty attributes
    .sub(/<svg/, "<symbol #{attributes(attr)}")
end