esvg 4.1.6 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/exe/esvg +1 -1
- data/lib/esvg/helpers.rb +3 -27
- data/lib/esvg/svg-old.rb +717 -0
- data/lib/esvg/svg.rb +48 -696
- data/lib/esvg/svgs.rb +320 -0
- data/lib/esvg/symbol.rb +249 -0
- data/lib/esvg/utils.rb +52 -0
- data/lib/esvg/version.rb +1 -1
- data/lib/esvg.rb +62 -3
- metadata +6 -3
- data/lib/esvg/symbols.rb +0 -10
data/lib/esvg/svg-old.rb
ADDED
@@ -0,0 +1,717 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Esvg
|
5
|
+
class SVG
|
6
|
+
attr_accessor :svgs, :last_read, :svg_symbols
|
7
|
+
|
8
|
+
include Esvg::Utils
|
9
|
+
|
10
|
+
CONFIG = {
|
11
|
+
filename: 'svgs',
|
12
|
+
class: 'svg-symbol',
|
13
|
+
namespace: 'svg',
|
14
|
+
core: true,
|
15
|
+
namespace_before: true,
|
16
|
+
optimize: false,
|
17
|
+
gzip: false,
|
18
|
+
fingerprint: true,
|
19
|
+
throttle_read: 4,
|
20
|
+
flatten: [],
|
21
|
+
alias: {}
|
22
|
+
}
|
23
|
+
|
24
|
+
CONFIG_RAILS = {
|
25
|
+
source: "app/assets/svgs",
|
26
|
+
assets: "app/assets/javascripts",
|
27
|
+
build: "public/javascripts",
|
28
|
+
temp: "tmp"
|
29
|
+
}
|
30
|
+
|
31
|
+
def initialize(options={})
|
32
|
+
config(options)
|
33
|
+
|
34
|
+
@modules = {}
|
35
|
+
@last_read = nil
|
36
|
+
|
37
|
+
read_cache
|
38
|
+
read_files
|
39
|
+
end
|
40
|
+
|
41
|
+
def config(options={})
|
42
|
+
@config ||= begin
|
43
|
+
paths = [options[:config_file], 'config/esvg.yml', 'esvg.yml'].compact
|
44
|
+
|
45
|
+
config = CONFIG.dup
|
46
|
+
|
47
|
+
if Esvg.rails? || options[:rails]
|
48
|
+
config.merge!(CONFIG_RAILS)
|
49
|
+
end
|
50
|
+
|
51
|
+
if path = paths.select{ |p| File.exist?(p)}.first
|
52
|
+
config.merge!(symbolize_keys(YAML.load(File.read(path) || {})))
|
53
|
+
end
|
54
|
+
|
55
|
+
config.merge!(options)
|
56
|
+
|
57
|
+
config[:filename] = File.basename(config[:filename], '.*')
|
58
|
+
|
59
|
+
config[:pwd] = File.expand_path Dir.pwd
|
60
|
+
config[:source] = File.expand_path config[:source] || config[:pwd]
|
61
|
+
config[:build] = File.expand_path config[:build] || config[:pwd]
|
62
|
+
config[:assets] = File.expand_path config[:assets] || config[:pwd]
|
63
|
+
|
64
|
+
config[:temp] = config[:pwd] if config[:temp].nil?
|
65
|
+
config[:temp] = File.expand_path File.join(config[:temp], '.esvg-cache')
|
66
|
+
|
67
|
+
config[:aliases] = load_aliases(config[:alias])
|
68
|
+
config[:flatten] = [config[:flatten]].flatten.map { |dir| File.join(dir, '/') }.join('|')
|
69
|
+
|
70
|
+
config
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def read_files
|
75
|
+
if !@last_read.nil? && (Time.now.to_i - @last_read) < config[:throttle_read]
|
76
|
+
return
|
77
|
+
end
|
78
|
+
|
79
|
+
# Get a list of svg files and modification times
|
80
|
+
#
|
81
|
+
find_files
|
82
|
+
|
83
|
+
@last_read = Time.now.to_i
|
84
|
+
|
85
|
+
puts "Read #{svgs.size} files from #{config[:source]}" if config[:print]
|
86
|
+
|
87
|
+
if svgs.empty? && config[:print]
|
88
|
+
puts "No svgs found at #{config[:source]}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def find_files
|
93
|
+
files = Dir[File.join(config[:source], '**/*.svg')].uniq.sort
|
94
|
+
@svg_symbols = {}
|
95
|
+
|
96
|
+
# Remove deleted files from svg cache
|
97
|
+
(svgs.keys - file_keys(files)).each { |f| svgs.delete(f) }
|
98
|
+
|
99
|
+
dirs = {}
|
100
|
+
|
101
|
+
files.each do |path|
|
102
|
+
mtime = File.mtime(path).to_i
|
103
|
+
key = file_key path
|
104
|
+
dkey = dir_key path
|
105
|
+
|
106
|
+
# Use cache if possible
|
107
|
+
if svgs[key].nil? || svgs[key][:last_modified] != mtime
|
108
|
+
svgs[key] = process_file(path, mtime)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Name may have changed due to flatten config
|
112
|
+
svgs[key][:name] = file_name(path)
|
113
|
+
svgs[key][:attr][:name] = id(svgs[key][:name])
|
114
|
+
|
115
|
+
dirs[dkey] ||= {}
|
116
|
+
(dirs[dkey][:files] ||= []) << key
|
117
|
+
|
118
|
+
if dirs[dkey][:last_modified].nil? || dirs[dkey][:last_modified] < mtime
|
119
|
+
dirs[dkey][:last_modified] = mtime
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
dirs = sort(dirs)
|
124
|
+
|
125
|
+
dirs.each do |dir, data|
|
126
|
+
|
127
|
+
# overwrite cache if
|
128
|
+
if svg_symbols[dir].nil? || # No cache for this dir yet
|
129
|
+
svg_symbols[dir][:last_modified] != data[:last_modified] || # New or updated file
|
130
|
+
svg_symbols[dir][:files] != data[:files] # Changed files
|
131
|
+
|
132
|
+
attributes = data[:files].map { |f| svgs[f][:attr] }
|
133
|
+
mtimes = data[:files].map { |f| svgs[f][:last_modified] }.join
|
134
|
+
|
135
|
+
svg_symbols[dir] = data.merge({
|
136
|
+
name: dir,
|
137
|
+
asset: File.basename(dir).start_with?('_'),
|
138
|
+
version: config[:version] || Digest::MD5.hexdigest(mtimes)
|
139
|
+
})
|
140
|
+
|
141
|
+
svg_symbols[dir][:path] = write_path(dir)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
@svg_symbols = sort(@svg_symbols)
|
146
|
+
@svgs = sort(@svgs)
|
147
|
+
end
|
148
|
+
|
149
|
+
def read_cache
|
150
|
+
@svgs = YAML.load(read_tmp '.svgs') || {}
|
151
|
+
end
|
152
|
+
|
153
|
+
def write_cache
|
154
|
+
return if production?
|
155
|
+
|
156
|
+
write_tmp '.svgs', sort(@svgs).to_yaml
|
157
|
+
end
|
158
|
+
|
159
|
+
def sort(hash)
|
160
|
+
sorted = {}
|
161
|
+
hash.sort.each do |h|
|
162
|
+
sorted[h.first] = h.last
|
163
|
+
end
|
164
|
+
sorted
|
165
|
+
end
|
166
|
+
|
167
|
+
def embed_script(key=nil)
|
168
|
+
if script = js(key)
|
169
|
+
"<script>#{script}</script>"
|
170
|
+
else
|
171
|
+
''
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def build_paths(keys=nil)
|
176
|
+
build_files(keys).map { |s| File.basename(s[:path]) }
|
177
|
+
end
|
178
|
+
|
179
|
+
def build_files(keys=nil)
|
180
|
+
valid_keys(keys).reject do |k|
|
181
|
+
svg_symbols[k][:asset]
|
182
|
+
end.map { |k| svg_symbols[k] }
|
183
|
+
end
|
184
|
+
|
185
|
+
def asset_files(keys=nil)
|
186
|
+
valid_keys(keys).select do |k|
|
187
|
+
svg_symbols[k][:asset]
|
188
|
+
end.map { |k| svg_symbols[k] }
|
189
|
+
end
|
190
|
+
|
191
|
+
def process_file(path, mtime)
|
192
|
+
content = File.read(path)
|
193
|
+
id = id(file_key(path))
|
194
|
+
size_attr = dimensions(content)
|
195
|
+
|
196
|
+
{
|
197
|
+
path: path,
|
198
|
+
use: %Q{<use xlink:href="##{id}"/>},
|
199
|
+
last_modified: mtime,
|
200
|
+
attr: { id: id }.merge(size_attr),
|
201
|
+
content: content
|
202
|
+
}
|
203
|
+
end
|
204
|
+
|
205
|
+
def use(file, options={})
|
206
|
+
if svg = find_svg(file, options[:fallback])
|
207
|
+
|
208
|
+
if options[:color]
|
209
|
+
options[:style] ||= ''
|
210
|
+
options[:style] += "color:#{options[:color]};#{options[:style]}"
|
211
|
+
end
|
212
|
+
|
213
|
+
attr = {
|
214
|
+
fill: options[:fill],
|
215
|
+
style: options[:style],
|
216
|
+
viewBox: svg[:attr][:viewBox],
|
217
|
+
class: [config[:class], id(svg[:name]), options[:class]].compact.join(' ')
|
218
|
+
}
|
219
|
+
|
220
|
+
# If user doesn't pass a size or set scale: true
|
221
|
+
if !(options[:width] || options[:height] || options[:scale])
|
222
|
+
|
223
|
+
# default to svg dimensions
|
224
|
+
attr[:width] = svg[:attr][:width]
|
225
|
+
attr[:height] = svg[:attr][:height]
|
226
|
+
else
|
227
|
+
|
228
|
+
# Add sizes (nil options will be stripped)
|
229
|
+
attr[:width] = options[:width]
|
230
|
+
attr[:height] = options[:height]
|
231
|
+
end
|
232
|
+
|
233
|
+
use = %Q{<svg #{attributes(attr)}>#{svg[:use]}#{title(options)}#{desc(options)}#{options[:content]||''}</svg>}
|
234
|
+
|
235
|
+
if Esvg.rails?
|
236
|
+
use.html_safe
|
237
|
+
else
|
238
|
+
use
|
239
|
+
end
|
240
|
+
else
|
241
|
+
if production?
|
242
|
+
return ''
|
243
|
+
else
|
244
|
+
raise "no svg named '#{get_alias(file)}' exists at #{config[:source]}"
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
alias :svg_icon :use
|
250
|
+
|
251
|
+
def dimensions(input)
|
252
|
+
viewbox = input.scan(/<svg.+(viewBox=["'](.+?)["'])/).flatten.last
|
253
|
+
if viewbox
|
254
|
+
coords = viewbox.split(' ')
|
255
|
+
|
256
|
+
{
|
257
|
+
viewBox: viewbox,
|
258
|
+
width: coords[2].to_i - coords[0].to_i,
|
259
|
+
height: coords[3].to_i - coords[1].to_i
|
260
|
+
}
|
261
|
+
else
|
262
|
+
{}
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def attributes(hash)
|
267
|
+
att = []
|
268
|
+
hash.each do |key, value|
|
269
|
+
att << %Q{#{key}="#{value}"} unless value.nil?
|
270
|
+
end
|
271
|
+
att.join(' ')
|
272
|
+
end
|
273
|
+
|
274
|
+
def exist?(name, fallback=nil)
|
275
|
+
!find_svg(name, fallback).nil?
|
276
|
+
end
|
277
|
+
|
278
|
+
def find_svg(name, fallback=nil)
|
279
|
+
name = get_alias dasherize(name)
|
280
|
+
|
281
|
+
if svg = svgs.values.find { |v| v[:name] == name }
|
282
|
+
svg
|
283
|
+
elsif fallback
|
284
|
+
find_svg(fallback)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
alias_method :exists?, :exist?
|
289
|
+
|
290
|
+
def id(name)
|
291
|
+
name = name_key(name)
|
292
|
+
if config[:namespace_before]
|
293
|
+
dasherize "#{config[:namespace]}-#{name}"
|
294
|
+
else
|
295
|
+
dasherize "#{name}-#{config[:namespace]}"
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
def title(options)
|
300
|
+
if options[:title]
|
301
|
+
"<title>#{options[:title]}</title>"
|
302
|
+
else
|
303
|
+
''
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def desc(options)
|
308
|
+
if options[:desc]
|
309
|
+
"<desc>#{options[:desc]}</desc>"
|
310
|
+
else
|
311
|
+
''
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
def version(key)
|
316
|
+
svg_symbols[key][:version]
|
317
|
+
end
|
318
|
+
|
319
|
+
def build
|
320
|
+
paths = write_files svg_symbols.values
|
321
|
+
|
322
|
+
if config[:core]
|
323
|
+
path = File.join config[:assets], "_esvg.js"
|
324
|
+
write_file(path, js_core)
|
325
|
+
paths << path
|
326
|
+
end
|
327
|
+
|
328
|
+
paths
|
329
|
+
end
|
330
|
+
|
331
|
+
def write_files(files)
|
332
|
+
paths = []
|
333
|
+
|
334
|
+
files.each do |file|
|
335
|
+
content = js(file[:name])
|
336
|
+
|
337
|
+
write_file(file[:path], content)
|
338
|
+
puts "Writing #{file[:path]}" if config[:print]
|
339
|
+
paths << file[:path]
|
340
|
+
|
341
|
+
if !file[:asset] && gz = compress(file[:path])
|
342
|
+
puts "Writing #{gz}" if config[:print]
|
343
|
+
paths << gz
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
write_cache
|
348
|
+
|
349
|
+
paths
|
350
|
+
end
|
351
|
+
|
352
|
+
def symbols(keys)
|
353
|
+
symbols = valid_keys(keys).map { |key|
|
354
|
+
# Build on demand
|
355
|
+
build_symbols(svg_symbols[key][:files])
|
356
|
+
}.join.gsub(/\n/,'')
|
357
|
+
|
358
|
+
%Q{<svg id="esvg-#{key_id(keys)}" version="1.1" style="height:0;position:absolute">#{symbols}</svg>}
|
359
|
+
end
|
360
|
+
|
361
|
+
def build_symbols(files)
|
362
|
+
files.map { |file|
|
363
|
+
if svgs[file][:optimized_at].nil? || svgs[file][:optimized_at] < svgs[file][:last_modified]
|
364
|
+
svgs[file][:optimized_content] = optimize(svgs[file])
|
365
|
+
end
|
366
|
+
svgs[file][:optimized_content]
|
367
|
+
}.join.gsub(/\n/,'')
|
368
|
+
end
|
369
|
+
|
370
|
+
def js(key)
|
371
|
+
keys = valid_keys(key)
|
372
|
+
return if keys.empty?
|
373
|
+
|
374
|
+
script key_id(keys), symbols(keys).gsub('/n','').gsub("'"){"\\'"}
|
375
|
+
end
|
376
|
+
|
377
|
+
def script(id, symbols)
|
378
|
+
%Q{(function(){
|
379
|
+
|
380
|
+
function embed() {
|
381
|
+
if (!document.querySelector('#esvg-#{id}')) {
|
382
|
+
document.querySelector('body').insertAdjacentHTML('afterbegin', '#{symbols}')
|
383
|
+
}
|
384
|
+
}
|
385
|
+
|
386
|
+
// If DOM is already ready, embed SVGs
|
387
|
+
if (document.readyState == 'interactive') { embed() }
|
388
|
+
|
389
|
+
// Handle Turbolinks page change events
|
390
|
+
if ( window.Turbolinks ) {
|
391
|
+
document.addEventListener("turbolinks:load", function(event) { embed() })
|
392
|
+
}
|
393
|
+
|
394
|
+
// Handle standard DOM ready events
|
395
|
+
document.addEventListener("DOMContentLoaded", function(event) { embed() })
|
396
|
+
})()}
|
397
|
+
end
|
398
|
+
|
399
|
+
def js_core
|
400
|
+
%Q{(function(){
|
401
|
+
var names
|
402
|
+
|
403
|
+
function attr( source, name ){
|
404
|
+
if (typeof source == 'object')
|
405
|
+
return name+'="'+source.getAttribute(name)+'" '
|
406
|
+
else
|
407
|
+
return name+'="'+source+'" ' }
|
408
|
+
|
409
|
+
function dasherize( input ) {
|
410
|
+
return input.replace(/[\\W,_]/g, '-').replace(/-{2,}/g, '-')
|
411
|
+
}
|
412
|
+
|
413
|
+
function svgName( name ) {
|
414
|
+
#{if config[:namespace_before]
|
415
|
+
%Q{return "#{config[:namespace]}-"+dasherize( name )}
|
416
|
+
else
|
417
|
+
%Q{return dasherize( name )+"-#{config[:namespace]}"}
|
418
|
+
end}
|
419
|
+
}
|
420
|
+
|
421
|
+
function use( name, options ) {
|
422
|
+
options = options || {}
|
423
|
+
var id = dasherize( svgName( name ) )
|
424
|
+
var symbol = svgs()[id]
|
425
|
+
|
426
|
+
if ( symbol ) {
|
427
|
+
var svg = document.createRange().createContextualFragment( '<svg><use xlink:href="#'+id+'"/></svg>' ).firstChild;
|
428
|
+
svg.setAttribute( 'class', '#{config[:class]} '+id+' '+( options.classname || '' ).trim() )
|
429
|
+
svg.setAttribute( 'viewBox', symbol.getAttribute( 'viewBox' ) )
|
430
|
+
|
431
|
+
if ( !( options.width || options.height || options.scale ) ) {
|
432
|
+
|
433
|
+
svg.setAttribute('width', symbol.getAttribute('width'))
|
434
|
+
svg.setAttribute('height', symbol.getAttribute('height'))
|
435
|
+
|
436
|
+
} else {
|
437
|
+
|
438
|
+
if ( options.width ) svg.setAttribute( 'width', options.width )
|
439
|
+
if ( options.height ) svg.setAttribute( 'height', options.height )
|
440
|
+
}
|
441
|
+
|
442
|
+
return svg
|
443
|
+
} else {
|
444
|
+
console.error('Cannot find "'+name+'" svg symbol. Ensure that svg scripts are loaded')
|
445
|
+
}
|
446
|
+
}
|
447
|
+
|
448
|
+
function svgs(){
|
449
|
+
if ( !names ) {
|
450
|
+
names = {}
|
451
|
+
for( var symbol of document.querySelectorAll( 'svg[id^=esvg] symbol' ) ) {
|
452
|
+
names[symbol.getAttribute('name')] = symbol
|
453
|
+
}
|
454
|
+
}
|
455
|
+
return names
|
456
|
+
}
|
457
|
+
|
458
|
+
var esvg = {
|
459
|
+
svgs: svgs,
|
460
|
+
use: use
|
461
|
+
}
|
462
|
+
|
463
|
+
// Handle Turbolinks page change events
|
464
|
+
if ( window.Turbolinks ) {
|
465
|
+
document.addEventListener( "turbolinks:load", function( event ) { names = null; esvg.svgs() })
|
466
|
+
}
|
467
|
+
|
468
|
+
if( typeof( module ) != 'undefined' ) { module.exports = esvg }
|
469
|
+
else window.esvg = esvg
|
470
|
+
|
471
|
+
})()}
|
472
|
+
end
|
473
|
+
|
474
|
+
private
|
475
|
+
|
476
|
+
def file_key(path)
|
477
|
+
dasherize sub_path(config[:source], path).sub('.svg','')
|
478
|
+
end
|
479
|
+
|
480
|
+
def dir_key(path)
|
481
|
+
dir = File.dirname(flatten_path(path))
|
482
|
+
|
483
|
+
# Flattened paths which should be treated as assets will use '_' as their dir key
|
484
|
+
# - flatten: _foo - _foo/icon.svg will have a dirkey of _
|
485
|
+
# - filename: _icons - treats all root or flattened files as assets
|
486
|
+
if dir == '.' && ( sub_path(config[:source], path).start_with?('_') || config[:filename].start_with?('_') )
|
487
|
+
'_'
|
488
|
+
else
|
489
|
+
dir
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
def file_name(path)
|
494
|
+
dasherize flatten_path(path).sub('.svg','')
|
495
|
+
end
|
496
|
+
|
497
|
+
def flatten_path(path)
|
498
|
+
sub_path(config[:source], path).sub(Regexp.new(config[:flatten]), '')
|
499
|
+
end
|
500
|
+
|
501
|
+
def file_keys(paths)
|
502
|
+
paths.flatten.map { |p| file_key(p) }
|
503
|
+
end
|
504
|
+
|
505
|
+
def name_key(key)
|
506
|
+
if key == '_' # Root level asset file
|
507
|
+
"_#{config[:filename]}".sub(/_+/, '_')
|
508
|
+
elsif key == '.' # Root level build file
|
509
|
+
config[:filename]
|
510
|
+
else
|
511
|
+
"#{key}"
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
def write_path(key)
|
516
|
+
name = name_key(key)
|
517
|
+
|
518
|
+
if name.start_with?('_') # Is it an asset?
|
519
|
+
File.join config[:assets], "#{name}.js"
|
520
|
+
else # or a build file?
|
521
|
+
|
522
|
+
# User doesn't want a fingerprinted build file and hasn't set a version
|
523
|
+
if !config[:fingerprint] && !config[:version]
|
524
|
+
File.join config[:build], "#{name}.js"
|
525
|
+
else
|
526
|
+
File.join config[:build], "#{name}-#{version(key)}.js"
|
527
|
+
end
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
def svgo?
|
532
|
+
!!(config[:optimize] && svgo_cmd)
|
533
|
+
end
|
534
|
+
|
535
|
+
def optimize(svg)
|
536
|
+
svg[:optimized_content] = pre_optimize svg[:content]
|
537
|
+
svg[:optimized_content] = sub_def_ids svg
|
538
|
+
|
539
|
+
if svgo?
|
540
|
+
response = Open3.capture3(%Q{#{} --disable=removeUselessDefs -s '#{svg[:content]}' -o -})
|
541
|
+
svg[:optimized_content] = response[0] if response[2].success?
|
542
|
+
end
|
543
|
+
|
544
|
+
svg[:optimized_at] = Time.now.to_i
|
545
|
+
svg[:optimized_content] = post_optimize svg
|
546
|
+
end
|
547
|
+
|
548
|
+
def pre_optimize(svg)
|
549
|
+
reg = %w(xmlns xmlns:xlink xml:space version).map { |m| "#{m}=\".+?\"" }.join('|')
|
550
|
+
svg.gsub(Regexp.new(reg), '') # Remove unwanted attributes
|
551
|
+
.sub(/.+?<svg/,'<svg') # Get rid of doctypes and comments
|
552
|
+
.gsub(/style="([^"]*?)fill:(.+?);/m, 'fill="\2" style="\1') # Make fill a property instead of a style
|
553
|
+
.gsub(/style="([^"]*?)fill-opacity:(.+?);/m, 'fill-opacity="\2" style="\1') # Move fill-opacity a property instead of a style
|
554
|
+
.gsub(/\n/, '') # Remove endlines
|
555
|
+
.gsub(/\s{2,}/, ' ') # Remove whitespace
|
556
|
+
.gsub(/>\s+</, '><') # Remove whitespace between tags
|
557
|
+
.gsub(/\s?fill="(#0{3,6}|black|rgba?\(0,0,0\))"/,'') # Strip black fill
|
558
|
+
end
|
559
|
+
|
560
|
+
def post_optimize(svg)
|
561
|
+
svg[:optimized_content] = set_attributes(svg)
|
562
|
+
.gsub(/<\/svg/,'</symbol') # Replace svgs with symbols
|
563
|
+
.gsub(/class="def-/,'id="def-') # Replace <def> classes with ids (classes are generated in sub_def_ids)
|
564
|
+
.gsub(/\w+=""/,'') # Remove empty attributes
|
565
|
+
end
|
566
|
+
|
567
|
+
def set_attributes(svg)
|
568
|
+
svg[:attr].keys.each { |key| svg[:optimized_content].sub!(/ #{key}=".+?"/,'') }
|
569
|
+
svg[:optimized_content].sub(/<svg/, "<symbol #{attributes(svg[:attr])}")
|
570
|
+
end
|
571
|
+
|
572
|
+
def svgo_cmd
|
573
|
+
find_node_module('svgo')
|
574
|
+
end
|
575
|
+
|
576
|
+
# Scans <def> blocks for IDs
|
577
|
+
# If urls(#id) are used, ensure these IDs are unique to this file
|
578
|
+
# Only replace IDs if urls exist to avoid replacing defs
|
579
|
+
# used in other svg files
|
580
|
+
#
|
581
|
+
def sub_def_ids(svg)
|
582
|
+
content = svg[:optimized_content]
|
583
|
+
name = svg[:attr][:id]
|
584
|
+
|
585
|
+
return content unless !!content.match(/<defs>/)
|
586
|
+
|
587
|
+
content.scan(/<defs>.+<\/defs>/m).flatten.each do |defs|
|
588
|
+
defs.scan(/id="(.+?)"/).flatten.uniq.each_with_index do |id, index|
|
589
|
+
|
590
|
+
if content.match(/url\(##{id}\)/)
|
591
|
+
new_id = "def-#{name}-#{index}"
|
592
|
+
|
593
|
+
content = content.gsub(/id="#{id}"/, %Q{class="#{new_id}"})
|
594
|
+
.gsub(/url\(##{id}\)/, "url(##{new_id})" )
|
595
|
+
else
|
596
|
+
content = content.gsub(/id="#{id}"/, %Q{class="#{id}"})
|
597
|
+
end
|
598
|
+
end
|
599
|
+
end
|
600
|
+
|
601
|
+
content
|
602
|
+
end
|
603
|
+
|
604
|
+
def compress(file)
|
605
|
+
return if !config[:gzip]
|
606
|
+
|
607
|
+
mtime = File.mtime(file)
|
608
|
+
gz_file = "#{file}.gz"
|
609
|
+
|
610
|
+
return if (File.exist?(gz_file) && File.mtime(gz_file) >= mtime)
|
611
|
+
|
612
|
+
File.open(gz_file, "wb") do |dest|
|
613
|
+
gz = ::Zlib::GzipWriter.new(dest, Zlib::BEST_COMPRESSION)
|
614
|
+
gz.mtime = mtime.to_i
|
615
|
+
IO.copy_stream(open(file), gz)
|
616
|
+
gz.close
|
617
|
+
end
|
618
|
+
|
619
|
+
File.utime(mtime, mtime, gz_file)
|
620
|
+
|
621
|
+
gz_file
|
622
|
+
end
|
623
|
+
|
624
|
+
def write_tmp(name, content)
|
625
|
+
path = File.join(config[:temp], name)
|
626
|
+
FileUtils.mkdir_p(File.dirname(path))
|
627
|
+
write_file path, content
|
628
|
+
path
|
629
|
+
end
|
630
|
+
|
631
|
+
def read_tmp(name)
|
632
|
+
path = File.join(config[:temp], name)
|
633
|
+
if File.exist? path
|
634
|
+
File.read path
|
635
|
+
else
|
636
|
+
''
|
637
|
+
end
|
638
|
+
end
|
639
|
+
|
640
|
+
def log_path(path)
|
641
|
+
File.expand_path(path).sub(config[:pwd], '').sub(/^\//,'')
|
642
|
+
end
|
643
|
+
|
644
|
+
def write_file(path, contents)
|
645
|
+
FileUtils.mkdir_p(File.expand_path(File.dirname(path)))
|
646
|
+
File.open(path, 'w') do |io|
|
647
|
+
io.write(contents)
|
648
|
+
end
|
649
|
+
end
|
650
|
+
|
651
|
+
def key_id(keys)
|
652
|
+
keys.map do |key|
|
653
|
+
(key == '.') ? 'symbols' : id(key)
|
654
|
+
end.join('-')
|
655
|
+
end
|
656
|
+
|
657
|
+
# Determine if an NPM module is installed by checking paths with `npm bin`
|
658
|
+
# Returns path to binary if installed
|
659
|
+
def find_node_module(cmd)
|
660
|
+
require 'open3'
|
661
|
+
|
662
|
+
return @modules[cmd] unless @modules[cmd].nil?
|
663
|
+
|
664
|
+
@modules[cmd] = begin
|
665
|
+
local = "$(npm bin)/#{cmd}"
|
666
|
+
global = "$(npm -g bin)/#{cmd}"
|
667
|
+
|
668
|
+
if Open3.capture3(local)[2].success?
|
669
|
+
local
|
670
|
+
elsif Open3.capture3(global)[2].success?
|
671
|
+
global
|
672
|
+
else
|
673
|
+
false
|
674
|
+
end
|
675
|
+
end
|
676
|
+
end
|
677
|
+
|
678
|
+
# Return non-empty key names for groups of svgs
|
679
|
+
def valid_keys(keys)
|
680
|
+
if keys.nil? || keys.empty?
|
681
|
+
svg_symbols.keys
|
682
|
+
else
|
683
|
+
keys = [keys].flatten.map { |k| dasherize k }
|
684
|
+
svg_symbols.keys.select { |k| keys.include? dasherize(k) }
|
685
|
+
end
|
686
|
+
end
|
687
|
+
|
688
|
+
# Load aliases from configuration.
|
689
|
+
# returns a hash of aliasees mapped to a name.
|
690
|
+
# Converts configuration YAML:
|
691
|
+
# alias:
|
692
|
+
# foo: bar
|
693
|
+
# baz: zip, zop
|
694
|
+
# To output:
|
695
|
+
# { :bar => "foo", :zip => "baz", :zop => "baz" }
|
696
|
+
#
|
697
|
+
def load_aliases(aliases)
|
698
|
+
a = {}
|
699
|
+
aliases.each do |name,alternates|
|
700
|
+
alternates.split(',').each do |val|
|
701
|
+
a[dasherize(val.strip).to_sym] = dasherize(name.to_s)
|
702
|
+
end
|
703
|
+
end
|
704
|
+
a
|
705
|
+
end
|
706
|
+
|
707
|
+
def get_alias(name)
|
708
|
+
config[:aliases][dasherize(name).to_sym] || name
|
709
|
+
end
|
710
|
+
|
711
|
+
def production?
|
712
|
+
config[:produciton] || if Esvg.rails?
|
713
|
+
Rails.env.production?
|
714
|
+
end
|
715
|
+
end
|
716
|
+
end
|
717
|
+
end
|