require 'yaml' require 'json' require 'zlib' require 'digest' require 'esvg/symbol' module Esvg class Svgs include Esvg::Utils attr_reader :symbols CONFIG = { filename: 'svgs', class: 'svg-symbol', prefix: 'svg', cache_file: '.symbols', core: true, optimize: false, gzip: false, scale: false, fingerprint: true, throttle_read: 4, flatten: [], alias: {} } CONFIG_RAILS = { source: "app/assets/svgs", assets: "app/assets/javascripts", build: "public/javascripts", temp: "tmp" } def initialize(options={}) config(options) @symbols = [] @svgs = [] @last_read = 0 read_cache load_files end def config(options={}) @config ||= begin paths = [options[:config_file], 'config/esvg.yml', 'esvg.yml'].compact config = CONFIG.dup if Esvg.rails? || options[:rails] config.merge!(CONFIG_RAILS) end if path = paths.select{ |p| File.exist?(p)}.first config.merge!(symbolize_keys(YAML.load(File.read(path) || {}))) end config.merge!(options) p config config[:filename] = File.basename(config[:filename], '.*') config[:pwd] = File.expand_path Dir.pwd config[:source] = File.expand_path config[:source] || config[:pwd] config[:build] = File.expand_path config[:build] || config[:pwd] config[:assets] = File.expand_path config[:assets] || config[:pwd] config[:temp] = config[:pwd] if config[:temp].nil? config[:temp] = File.expand_path File.join(config[:temp], '.esvg-cache') config[:aliases] = load_aliases(config[:alias]) config[:flatten] = [config[:flatten]].flatten.map { |dir| File.join(dir, '/') }.join('|') config end end def load_files return if (Time.now.to_i - @last_read) < config[:throttle_read] files = Dir[File.join(config[:source], '**/*.svg')].uniq.sort if files.empty? && config[:print] puts "No svgs found at #{config[:source]}" return end # Remove deleted files @symbols.reject(&:read).each { |s| @symbols.delete(s) } files.each do |path| unless @symbols.find { |s| s.path == path } @symbols << Symbol.new(path, config) end end @svgs.clear sort(@symbols.group_by(&:group)).each do |name, symbols| @svgs << Svg.new(name, symbols, config) end @last_read = Time.now.to_i puts "Read #{@symbols.size} files from #{config[:source]}" if config[:print] end def build paths = [] if config[:core] path = File.join config[:assets], "_esvg.js" write_file path, js_core paths << path end @svgs.each do |file| write_file file.path, js(file.embed) puts "Writing #{file.path}" if config[:print] paths << file.path if !file.asset && config[:gzip] && gz = compress(file.path) puts "Writing #{gz}" if config[:print] paths << gz end end write_cache paths end def write_cache puts "Writing cache" if config[:print] write_tmp config[:cache_file], @symbols.map(&:data).to_yaml end def read_cache (YAML.load(read_tmp config[:cache_file]) || []).each do |c| config[:cache] = c @symbols << Symbol.new(c[:path], config) end end # Embed only build scripts def embed_script(names=nil) embeds = buildable_svgs(names).map(&:embed) write_cache if cache_stale? if !embeds.empty? "" end end def cache_stale? path = File.join(config[:temp], config[:cache_file]) # No cache file exists or cache file is older than a new symbol !File.exist?(path) || File.mtime(path).to_i < @symbols.map(&:mtime).sort.last end def build_paths(names=nil) buildable_svgs(names).map{ |f| File.basename(f.path) } end def find_symbol(name, fallback=nil) # Ensure that file changes are picked up in development load_files unless Esvg.rails? && Rails.env.production? name = get_alias dasherize(name) if svg = @symbols.find { |s| s.name == name } svg elsif fallback find_symbol(fallback) end end def find_svgs(names=nil) return @svgs if names.nil? || names.empty? @svgs.select { |svg| svg.named?(names) } end def buildable_svgs(names=nil) find_svgs(names).reject(&:asset) end private def js(embed) %Q{(function(){ function embed() { #{embed} } // If DOM is already ready, embed SVGs if (document.readyState == 'interactive') { embed() } // Handle Turbolinks page change events if ( window.Turbolinks ) { document.addEventListener("turbolinks:load", function(event) { embed() }) } // Handle standard DOM ready events document.addEventListener("DOMContentLoaded", function(event) { embed() }) })()} end def js_core %Q{(function(){ var names function attr( source, name ){ if (typeof source == 'object') return name+'="'+source.getAttribute(name)+'" ' else return name+'="'+source+'" ' } function dasherize( input ) { return input.replace(/[\\W,_]/g, '-').replace(/-{2,}/g, '-') } function svgName( name ) { return "#{config[:prefix]}-"+dasherize( name ) } function use( name, options ) { options = options || {} var id = dasherize( name ) var symbol = svgs()[id] if ( symbol ) { var parent = symbol.parentElement var prefix = parent.dataset.prefix var base = parent.dataset.symbolClass var svg = document.createRange().createContextualFragment( '' ).firstChild; svg.setAttribute( 'class', base + ' ' + prefix + '-' + id + ' ' + ( options.class || '' ).trim() ) svg.setAttribute( 'viewBox', symbol.getAttribute( 'viewBox' ) ) if ( !( options.width || options.height || options.scale ) ) { svg.setAttribute('width', symbol.getAttribute('width')) svg.setAttribute('height', symbol.getAttribute('height')) } else { if ( options.width ) svg.setAttribute( 'width', options.width ) if ( options.height ) svg.setAttribute( 'height', options.height ) } return svg } else { console.error('Cannot find "'+name+'" svg symbol. Ensure that svg scripts are loaded') } } function svgs(){ if ( !names ) { names = {} for( var symbol of document.querySelectorAll( 'svg[id^=esvg] symbol' ) ) { names[symbol.dataset.name] = symbol } } return names } var esvg = { svgs: svgs, use: use } // Handle Turbolinks page change events if ( window.Turbolinks ) { document.addEventListener( "turbolinks:load", function( event ) { names = null; esvg.svgs() }) } if( typeof( module ) != 'undefined' ) { module.exports = esvg } else window.esvg = esvg })()} end def get_alias(name) config[:aliases][dasherize(name).to_sym] || name end # Load aliases from configuration. # returns a hash of aliasees mapped to a name. # Converts configuration YAML: # alias: # foo: bar # baz: zip, zop # To output: # { :bar => "foo", :zip => "baz", :zop => "baz" } # def load_aliases(aliases) a = {} aliases.each do |name,alternates| alternates.split(',').each do |val| a[dasherize(val.strip).to_sym] = dasherize(name.to_s) end end a end def write_tmp(name, content) path = File.join(config[:temp], name) FileUtils.mkdir_p(File.dirname(path)) write_file path, content path end def read_tmp(name) path = File.join(config[:temp], name) if File.exist? path File.read path else '' end end def write_file(path, contents) FileUtils.mkdir_p(File.expand_path(File.dirname(path))) File.open(path, 'w') do |io| io.write(contents) end end end end