require 'open3'
module Esvg
class Symbol
attr_reader :name, :id, :path, :content, :optimized, :size, :group, :mtime, :defs
include Esvg::Utils
def initialize(path, parent)
@parent = parent
@path = path
@last_checked = 0
load_data
read
end
def config
@parent.config
end
def exist?
File.exist?(@path)
end
def dir
@group
end
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
def width
@size[:width]
end
def height
@size[:height]
end
# Scale width based on propotion to height
def scale_width( h )
begin
s = split_unit( h )
"#{( s[:size] / height * width ).round(2)}#{s[:unit]}"
rescue StandardError
binding.pry
end
end
# Scale height based on propotion to width
def scale_height( w )
s = split_unit( w )
"#{( s[:size] / width * height ).round(2)}#{s[:unit]}"
end
# Separate size and unit for easier math.
# Returns: { size: 10, unit: 'px' }
def split_unit( size )
m = size.to_s.match(/(\d+)\s*(\D*)/)
{ size: m[1].to_f, unit: m[2] }
end
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
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
def attr
{ id: @id, 'data-name' => @name }.merge @size
end
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}#{content}"
end
if title = options.delete(:title)
content = "
#{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{}
end
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{}
end
# Only optimize if
# - Configuration asks for it
# - SVGO is present
# - If Rails is present
def optimize?
config[:optimize] && !!Esvg.node_module('svgo') && config[:env] == 'production'
end
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
def symbol
symbolize( optimize || @content )
end
def changed?
last_modified != mtime
end
private
def load_data
if config[:cache]
config.delete(:cache).each do |name, value|
instance_variable_set("@#{name}", value)
end
end
end
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
def file_id(name)
dasherize "#{config[:prefix]}-#{name}"
end
def local_path
@local_path ||= sub_path(config[:source], @path)
end
def file_name
dasherize flatten_path.sub('.svg','')
end
def file_key
dasherize local_path.sub('.svg','')
end
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
def flatten_path
@flattened_path ||= local_path.sub(Regexp.new(config[:flatten_dir]), '')
end
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
def dimensions
if viewbox = @content.scan(//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
def post_optimize
@optimized.gsub!(/\w+=""/,'') # Remove empty attributes
end
def symbolize( str )
strip_attributes( str )
.gsub(/<\/svg/,' 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
#
def prep_defs(svg)
# 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>/m).flatten[0]
svg.sub!("#{@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
end
end