Class: Esvg::SVG

Inherits:
Object
  • Object
show all
Defined in:
lib/esvg/svg.rb

Constant Summary collapse

CONFIG =
{
  path: Dir.pwd,
  base_class: 'svg-icon',
  namespace: 'icon',
  optimize: false,
  npm_path: false,
  namespace_before: true,
  font_size: '1em',
  output_path: Dir.pwd,
  verbose: false,
  format: 'js',
  throttle_read: 4,
  alias: {}
}
CONFIG_RAILS =
{
  path: "app/assets/esvg"
}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ SVG

Returns a new instance of SVG.



27
28
29
30
31
32
33
34
35
# File 'lib/esvg/svg.rb', line 27

def initialize(options={})
  config(options)

  @svgo = nil
  @last_read = nil
  @svgs = {}

  read_files
end

Instance Attribute Details

#filesObject

Returns the value of attribute files.



6
7
8
# File 'lib/esvg/svg.rb', line 6

def files
  @files
end

#last_readObject

Returns the value of attribute last_read.



6
7
8
# File 'lib/esvg/svg.rb', line 6

def last_read
  @last_read
end

#svgsObject

Returns the value of attribute svgs.



6
7
8
# File 'lib/esvg/svg.rb', line 6

def svgs
  @svgs
end

Instance Method Details

#classname(name) ⇒ Object



219
220
221
222
223
224
225
226
# File 'lib/esvg/svg.rb', line 219

def classname(name)
  name = dasherize(name)
  if config[:namespace_before]
    "#{config[:namespace]}-#{name}"
  else
    "#{name}-#{config[:namespace]}"
  end
end

#config(options = {}) ⇒ Object



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/esvg/svg.rb', line 37

def config(options={})
  @config ||= begin
    paths = [options[:config_file], 'config/esvg.yml', 'esvg.yml'].compact

    config = CONFIG
    config.merge!(CONFIG_RAILS) if Esvg.rails?

    if path = paths.select{ |p| File.exist?(p)}.first
      config.merge!(symbolize_keys(YAML.load(File.read(path) || {})))
    end

    config.merge!(options)

    if config[:cli]
      config[:path] = File.expand_path(config[:path])
      config[:output_path] = File.expand_path(config[:output_path])
    end

    config[:js_path]   ||= File.join(config[:output_path], 'esvg.js')
    config[:css_path]  ||= File.join(config[:output_path], 'esvg.css')
    config[:html_path] ||= File.join(config[:output_path], 'esvg.html')
    config.delete(:output_path)
    config[:aliases] = load_aliases(config[:alias])

    config
  end
end

#cssObject



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/esvg/svg.rb', line 298

def css
  styles = []
  
  classes = svgs.keys.map{|k| ".#{classname(k)}"}.join(', ')
  preamble = %Q{#{classes} { 
  font-size: #{config[:font_size]};
  clip: auto;
  background-size: auto;
  background-repeat: no-repeat;
  background-position: center center;
  display: inline-block;
  overflow: hidden;
  background-size: auto 1em;
  height: 1em;
  width: 1em;
  color: inherit;
  fill: currentColor;
  vertical-align: middle;
  line-height: 1em;
}}
  styles << preamble

  svgs.each do |name, data|
    if data[:css]
      styles << css
    else
      svg_css = data[:content].gsub(/</, '%3C') # escape <
                              .gsub(/>/, '%3E') # escape >
                              .gsub(/#/, '%23') # escape #
                              .gsub(/\n/,'')    # remove newlines
      styles << data[:css] = ".#{classname(name)} { background-image: url('data:image/svg+xml;utf-8,#{svg_css}'); }"
    end
  end
  styles.join("\n")
end

#dasherize(input) ⇒ Object



228
229
230
# File 'lib/esvg/svg.rb', line 228

def dasherize(input)
  input.gsub(/[\W,_]/, '-').gsub(/-{2,}/, '-')
end

#desc(options) ⇒ Object



246
247
248
249
250
251
252
# File 'lib/esvg/svg.rb', line 246

def desc(options)
  if options[:desc]
    "<desc>#{options[:desc]}</desc>"
  else
    ''
  end
end

#dimensions(input) ⇒ Object



197
198
199
200
201
202
203
204
205
# File 'lib/esvg/svg.rb', line 197

def dimensions(input)
  dimension = input.scan(/<svg.+(viewBox=["'](.+?)["'])/).flatten
  viewbox = dimension.first
  coords = dimension.last.split(' ')

  width = coords[2].to_i - coords[0].to_i
  height = coords[3].to_i - coords[1].to_i
  %Q{#{viewbox} width="#{width}" height="#{height}"}
end

#embedObject



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/esvg/svg.rb', line 88

def embed
  return if files.empty?
  output = if config[:format] == "html"
    html
  elsif config[:format] == "js"
    js
  elsif config[:format] == "css"
    css
  end

  if Esvg.rails?
    output.html_safe
  else
    output
  end
end

#exist?(name) ⇒ Boolean Also known as: exists?

Returns:

  • (Boolean)


212
213
214
215
# File 'lib/esvg/svg.rb', line 212

def exist?(name)
  name = get_alias(name)
  !svgs[name].nil?
end

#file_key(name) ⇒ Object



448
449
450
# File 'lib/esvg/svg.rb', line 448

def file_key(name)
  dasherize(File.basename(name, ".*"))
end

#find_filesObject



232
233
234
235
# File 'lib/esvg/svg.rb', line 232

def find_files
  path = File.expand_path(File.join(config[:path], '*.svg'))
  Dir[path].uniq
end

#get_alias(name) ⇒ Object



84
85
86
# File 'lib/esvg/svg.rb', line 84

def get_alias(name)
  config[:aliases][dasherize(name).to_sym] || name
end

#htmlObject



355
356
357
358
359
360
361
362
363
364
365
366
367
368
# File 'lib/esvg/svg.rb', line 355

def html
  if @files.empty?
    ''
  else
    symbols = []
    svgs.each do |name, data|
      symbols << prep_svg(name, data[:content])
    end

    symbols = optimize(symbols.join).gsub(/class=/,'id=').gsub(/svg/,'symbol')

    %Q{<svg id="esvg-symbols" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display:none">#{symbols}</svg>}
  end
end

#jsObject



370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
# File 'lib/esvg/svg.rb', line 370

def js
  %Q{var esvg = {
  embed: function(){
if (!document.querySelector('#esvg-symbols')) {
  document.querySelector('body').insertAdjacentHTML('afterbegin', '#{html.gsub(/\n/,'').gsub("'"){"\\'"}}')
}
  },
  icon: function(name, classnames) {
var svgName = this.iconName(name)
var element = document.querySelector('#'+svgName)

if (element) {
  return '<svg class="#{config[:base_class]} '+svgName+' '+(classnames || '')+'" '+this.dimensions(element)+'><use xlink:href="#'+svgName+'"/></svg>'
} else {
  console.error('File not found: "'+name+'.svg" at #{log_path(File.join(config[:path],''))}/')
}
  },
  iconName: function(name) {
var before = #{config[:namespace_before]}
if (before) {
  return "#{config[:namespace]}-"+this.dasherize(name)
} else {
  return name+"-#{config[:namespace]}"
}
  },
  dimensions: function(el) {
return 'viewBox="'+el.getAttribute('viewBox')+'" width="'+el.getAttribute('width')+'" height="'+el.getAttribute('height')+'"'
  },
  dasherize: function(input) {
return input.replace(/[\W,_]/g, '-').replace(/-{2,}/g, '-')
  },
  load: function(){
// If DOM is already ready, embed SVGs
if (document.readyState == 'interactive') { this.embed() }

// Handle Turbolinks (or other things that fire page change events)
document.addEventListener("page:change", function(event) { this.embed() }.bind(this))

// Handle standard DOM ready events
document.addEventListener("DOMContentLoaded", function(event) { this.embed() }.bind(this))
  },
  aliases: #{config[:aliases].to_json},
  alias: function(name) {
var aliased = this.aliases[name]
if (typeof(aliased) != "undefined") {
  return aliased
} else {
  return name
}
  }
}

esvg.load()

// Work with module exports:
if(typeof(module) != 'undefined') { module.exports = esvg }
}
end

#load_aliases(aliases) ⇒ Object

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" }


74
75
76
77
78
79
80
81
82
# File 'lib/esvg/svg.rb', line 74

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

#log_path(path) ⇒ Object



269
270
271
# File 'lib/esvg/svg.rb', line 269

def log_path(path)
  File.expand_path(path).sub(File.expand_path(Dir.pwd), '').sub(/^\//,'')
end

#optimize(svg) ⇒ Object



345
346
347
348
349
350
351
352
353
# File 'lib/esvg/svg.rb', line 345

def optimize(svg)
  if config[:optimize] && svgo?
    path = write_svg(svg)
    svg = `#{@svgo} '#{path}' -o -`
    FileUtils.rm(path)
  end

  svg
end

#prep_svg(file, content) ⇒ Object



334
335
336
337
338
339
340
341
342
343
# File 'lib/esvg/svg.rb', line 334

def prep_svg(file, content)
  content = content.gsub(/<svg.+?>/, %Q{<svg class="#{classname(file)}" #{dimensions(content)}>})  # convert svg to symbols
         .gsub(/\n/, '')                 # Remove endlines
         .gsub(/\s{2,}/, ' ')            # Remove whitespace
         .gsub(/>\s+</, '><')            # Remove whitespace between tags
         .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(/\s?style=".*?";?/,'')    # Strip style property
         .gsub(/\s?fill="(#0{3,6}|black|rgba?\(0,0,0\))"/,'')      # Strip black fill
end

#process_file(file, mtime, name) ⇒ Object



147
148
149
150
151
152
153
154
# File 'lib/esvg/svg.rb', line 147

def process_file(file, mtime, name)
  content = File.read(file).gsub(/<?.+\?>/,'').gsub(/<!.+?>/,'')
  {
    content: content,
    use: use_svg(name, content),
    last_modified: mtime
  }
end

#process_filesObject

Add new svgs, update modified svgs, remove deleted svgs



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/esvg/svg.rb', line 131

def process_files
  files.each do |file, mtime|
    name = file_key(file)

    if svgs[name].nil? || svgs[name][:last_modified] != mtime
      svgs[name] = process_file(file, mtime, name)
    end
  end

  # Remove deleted svgs
  #
  (svgs.keys - files.keys.map {|file| file_key(file) }).each do |file|
    svgs.delete(file)
  end
end

#read_filesObject



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/esvg/svg.rb', line 105

def read_files
  if !@last_read.nil? && (Time.now.to_i - @last_read) < config[:throttle_read]
    return
  end

  @files = {}

  # Get a list of svg files and modification times
  #
  find_files.each do |f|
    files[f] = File.mtime(f)
  end

  @last_read = Time.now.to_i

  puts "Read #{files.size} files from #{config[:path]}" if config[:cli]

  process_files

  if files.empty? && config[:cli]
    puts "No svgs found at #{config[:path]}"
  end
end

#svg_icon(file, options = {}) ⇒ Object



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/esvg/svg.rb', line 161

def svg_icon(file, options={})
  name = dasherize(file)

  if !exist?(name)
    if fallback = options.delete(:fallback)
      svg_icon(fallback, options)
    else
      if Esvg.rails? && Rails.env.production?
        return ''
      else
        raise "no svg named '#{get_alias(file)}' exists at #{config[:path]}"
      end
    end
  else

    embed = use_icon(name)
    embed = embed.sub(/class="(.+?)"/, 'class="\1 '+options[:class]+'"') if options[:class]

    if options[:style]
      if embed.match(/style/)
        embed = embed.sub(/style="(.+?)"/, 'style="\1; '+options[:style]+'"')
      else
        embed = embed.sub(/><use/, %Q{ style="#{options[:style]}"><use})
      end
    end

    embed = embed.sub(/><\/svg/, ">#{title(options)}#{desc(options)}</svg")

    if Esvg.rails?
      embed.html_safe
    else
      embed
    end
  end
end

#svgo?Boolean

Returns:

  • (Boolean)


429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
# File 'lib/esvg/svg.rb', line 429

def svgo?
  @svgo ||= begin
    npm_path   = "#{config[:npm_path] || Dir.pwd}/node_modules"
    local_path = File.join(npm_path, "svgo/bin/svgo")

    if config[:npm_path] && !File.exist?(npm_path)
      abort "NPM Path not found: #{File.expand_path(config[:npm_path])}"
    end

    if File.exist?(local_path)
      local_path
    elsif `npm ls -g svgo`.match(/empty/).nil?
      "svgo"
    else
      false
    end
  end
end

#symbolize_keys(hash) ⇒ Object



453
454
455
456
457
# File 'lib/esvg/svg.rb', line 453

def symbolize_keys(hash)
  h = {}
  hash.each {|k,v| h[k.to_sym] = v }
  h
end

#title(options) ⇒ Object



238
239
240
241
242
243
244
# File 'lib/esvg/svg.rb', line 238

def title(options)
  if options[:title]
    "<title>#{options[:title]}</title>"
  else
    ''
  end
end

#use_icon(name) ⇒ Object



207
208
209
210
# File 'lib/esvg/svg.rb', line 207

def use_icon(name)
  name = get_alias(name)
  svgs[name][:use]
end

#use_svg(file, content) ⇒ Object



156
157
158
159
# File 'lib/esvg/svg.rb', line 156

def use_svg(file, content)
  name = classname(get_alias(file))
  %Q{<svg class="#{config[:base_class]} #{name}" #{dimensions(content)}><use xlink:href="##{name}"/></svg>}
end

#writeObject



254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/esvg/svg.rb', line 254

def write
  return if @files.empty?
  case config[:format]
  when "html"
    write_html
    puts "Written to #{log_path config[:html_path]}" if config[:cli]
  when "js"
    write_js
    puts "Written to #{log_path config[:js_path]}" if config[:cli]
  when "css"
    write_css
    puts "Written to #{log_path config[:css_path]}" if config[:cli]
  end
end

#write_cssObject



283
284
285
# File 'lib/esvg/svg.rb', line 283

def write_css
  write_file config[:css_path], css
end

#write_file(path, contents) ⇒ Object



291
292
293
294
295
296
# File 'lib/esvg/svg.rb', line 291

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

#write_htmlObject



287
288
289
# File 'lib/esvg/svg.rb', line 287

def write_html
  write_file config[:html_path], html
end

#write_jsObject



279
280
281
# File 'lib/esvg/svg.rb', line 279

def write_js
  write_file config[:js_path], js
end

#write_svg(svg) ⇒ Object



273
274
275
276
277
# File 'lib/esvg/svg.rb', line 273

def write_svg(svg)
  path = File.join(config[:path], '.esvg-cache')
  write_file path, svg
  path
end