svgeez 1.0.3 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +23 -5
- data/.reek.yml +16 -0
- data/.rspec +1 -2
- data/.rubocop +3 -0
- data/.rubocop.yml +24 -1
- data/.ruby-version +1 -1
- data/.simplecov +13 -0
- data/.travis.yml +16 -4
- data/CHANGELOG.md +37 -0
- data/CODE_OF_CONDUCT.md +10 -8
- data/CONTRIBUTING.md +22 -31
- data/Gemfile +10 -0
- data/LICENSE +6 -6
- data/README.md +59 -56
- data/Rakefile +12 -2
- data/exe/svgeez +23 -0
- data/lib/svgeez.rb +6 -3
- data/lib/svgeez/builder.rb +37 -16
- data/lib/svgeez/command.rb +32 -11
- data/lib/svgeez/commands/build.rb +14 -11
- data/lib/svgeez/commands/watch.rb +19 -19
- data/lib/svgeez/destination.rb +6 -12
- data/lib/svgeez/elements/svg_element.rb +23 -0
- data/lib/svgeez/elements/symbol_element.rb +38 -0
- data/lib/svgeez/optimizer.rb +15 -2
- data/lib/svgeez/source.rb +3 -5
- data/lib/svgeez/version.rb +1 -1
- data/svgeez.gemspec +12 -14
- metadata +22 -85
- data/.codeclimate.yml +0 -17
- data/bin/svgeez +0 -21
- data/lib/svgeez/svg_element.rb +0 -18
- data/lib/svgeez/symbol_element.rb +0 -30
data/Rakefile
CHANGED
@@ -1,8 +1,18 @@
|
|
1
1
|
require 'bundler/gem_tasks'
|
2
|
+
|
3
|
+
require 'reek/rake/task'
|
2
4
|
require 'rspec/core/rake_task'
|
3
5
|
require 'rubocop/rake_task'
|
4
6
|
|
7
|
+
Reek::Rake::Task.new do |task|
|
8
|
+
task.fail_on_error = false
|
9
|
+
task.source_files = FileList['**/*.rb'].exclude('vendor/**/*.rb')
|
10
|
+
end
|
11
|
+
|
5
12
|
RSpec::Core::RakeTask.new
|
6
|
-
RuboCop::RakeTask.new
|
7
13
|
|
8
|
-
|
14
|
+
RuboCop::RakeTask.new do |task|
|
15
|
+
task.fail_on_error = false
|
16
|
+
end
|
17
|
+
|
18
|
+
task default: [:rubocop, :reek, :spec]
|
data/exe/svgeez
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
4
|
+
|
5
|
+
require 'mercenary'
|
6
|
+
require 'svgeez'
|
7
|
+
|
8
|
+
Mercenary.program(:svgeez) do |program|
|
9
|
+
program.version Svgeez::VERSION
|
10
|
+
program.description 'Generate an SVG sprite from a folder of SVG icons.'
|
11
|
+
program.syntax 'svgeez <subcommand> [options]'
|
12
|
+
|
13
|
+
Svgeez::Command.subclasses.each do |command|
|
14
|
+
command.init_with_program(program)
|
15
|
+
end
|
16
|
+
|
17
|
+
program.action do |args, _|
|
18
|
+
if args.empty?
|
19
|
+
puts program
|
20
|
+
abort
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/svgeez.rb
CHANGED
@@ -1,21 +1,24 @@
|
|
1
1
|
require 'fileutils'
|
2
|
-
require 'listen'
|
3
2
|
require 'logger'
|
4
3
|
require 'mkmf'
|
5
4
|
require 'securerandom'
|
6
5
|
|
6
|
+
require 'listen'
|
7
|
+
require 'mercenary'
|
8
|
+
|
7
9
|
require 'svgeez/version'
|
8
10
|
|
9
11
|
require 'svgeez/command'
|
10
12
|
require 'svgeez/commands/build'
|
11
13
|
require 'svgeez/commands/watch'
|
12
14
|
|
15
|
+
require 'svgeez/elements/svg_element'
|
16
|
+
require 'svgeez/elements/symbol_element'
|
17
|
+
|
13
18
|
require 'svgeez/builder'
|
14
19
|
require 'svgeez/destination'
|
15
20
|
require 'svgeez/optimizer'
|
16
21
|
require 'svgeez/source'
|
17
|
-
require 'svgeez/svg_element'
|
18
|
-
require 'svgeez/symbol_element'
|
19
22
|
|
20
23
|
module Svgeez
|
21
24
|
def self.logger
|
data/lib/svgeez/builder.rb
CHANGED
@@ -1,46 +1,67 @@
|
|
1
1
|
module Svgeez
|
2
2
|
class Builder
|
3
3
|
SOURCE_IS_DESTINATION_MESSAGE = "Setting `source` and `destination` to the same path isn't allowed!".freeze
|
4
|
+
SOURCE_DOES_NOT_EXIST = 'Provided `source` folder does not exist.'.freeze
|
4
5
|
NO_SVGS_IN_SOURCE_MESSAGE = 'No SVGs were found in `source` folder.'.freeze
|
5
6
|
|
7
|
+
attr_reader :source, :destination, :prefix
|
8
|
+
|
6
9
|
def initialize(options = {})
|
7
|
-
@
|
10
|
+
@source = Source.new(options)
|
11
|
+
@destination = Destination.new(options)
|
12
|
+
@svgo = options.fetch('svgo', false)
|
13
|
+
@prefix = options.fetch('prefix', @destination.file_id)
|
14
|
+
|
15
|
+
raise SOURCE_IS_DESTINATION_MESSAGE if source_is_destination?
|
16
|
+
raise SOURCE_DOES_NOT_EXIST unless source_exists?
|
17
|
+
rescue RuntimeError => exception
|
18
|
+
logger.error exception.message
|
19
|
+
exit
|
8
20
|
end
|
9
21
|
|
10
22
|
# rubocop:disable Metrics/AbcSize
|
11
23
|
def build
|
12
|
-
|
13
|
-
return Svgeez.logger.warn(NO_SVGS_IN_SOURCE_MESSAGE) if source_is_empty?
|
24
|
+
raise NO_SVGS_IN_SOURCE_MESSAGE if source_is_empty?
|
14
25
|
|
15
|
-
|
26
|
+
logger.info "Generating sprite at `#{destination_file_path}` from #{source_files_count} SVG#{'s' if source_files_count > 1}..."
|
16
27
|
|
17
28
|
# Make destination folder
|
18
29
|
FileUtils.mkdir_p(destination.folder_path)
|
19
30
|
|
20
31
|
# Write the file
|
21
|
-
File.open(
|
22
|
-
|
32
|
+
File.open(destination_file_path, 'w') do |file|
|
33
|
+
file.write destination_file_contents
|
23
34
|
end
|
24
35
|
|
25
|
-
|
36
|
+
logger.info "Successfully generated sprite at `#{destination_file_path}`."
|
37
|
+
rescue RuntimeError => exception
|
38
|
+
logger.warn exception.message
|
26
39
|
end
|
27
40
|
# rubocop:enable Metrics/AbcSize
|
28
41
|
|
29
|
-
|
30
|
-
|
42
|
+
private
|
43
|
+
|
44
|
+
def destination_file_contents
|
45
|
+
file_contents = Elements::SvgElement.new(source, destination, prefix).build
|
46
|
+
file_contents = Optimizer.new.optimize(file_contents) if @svgo
|
47
|
+
|
48
|
+
file_contents.insert(4, ' style="display: none;"')
|
31
49
|
end
|
32
50
|
|
33
|
-
def
|
34
|
-
@
|
51
|
+
def destination_file_path
|
52
|
+
@destination_file_path ||= destination.file_path
|
35
53
|
end
|
36
54
|
|
37
|
-
|
55
|
+
def logger
|
56
|
+
@logger ||= Svgeez.logger
|
57
|
+
end
|
38
58
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
59
|
+
def source_exists?
|
60
|
+
File.directory?(source.folder_path)
|
61
|
+
end
|
42
62
|
|
43
|
-
|
63
|
+
def source_files_count
|
64
|
+
source.file_paths.length
|
44
65
|
end
|
45
66
|
|
46
67
|
def source_is_destination?
|
data/lib/svgeez/command.rb
CHANGED
@@ -1,18 +1,39 @@
|
|
1
1
|
module Svgeez
|
2
2
|
class Command
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
class << self
|
4
|
+
def subclasses
|
5
|
+
@subclasses ||= []
|
6
|
+
end
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
def inherited(base)
|
9
|
+
subclasses << base
|
10
|
+
super(base)
|
11
|
+
end
|
12
|
+
|
13
|
+
def init_with_program(program)
|
14
|
+
program.command(name.split('::').last.downcase.to_sym) do |command|
|
15
|
+
command.description command_description
|
16
|
+
command.syntax command_syntax
|
17
|
+
|
18
|
+
add_actions(command)
|
19
|
+
add_options(command)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def add_actions(command)
|
26
|
+
command.action do |_, options|
|
27
|
+
command_action(options)
|
28
|
+
end
|
29
|
+
end
|
11
30
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
31
|
+
def add_options(command)
|
32
|
+
command.option 'source', '-s', '--source [FOLDER]', 'Source folder (defaults to ./_svgeez)'
|
33
|
+
command.option 'destination', '-d', '--destination [OUTPUT]', 'Destination file or folder (defaults to ./svgeez.svg)'
|
34
|
+
command.option 'prefix', '-p', '--prefix [PREFIX]', 'Custom Prefix for icon id (defaults to destination filename)'
|
35
|
+
command.option 'svgo', '--with-svgo', 'Optimize source SVGs with SVGO before sprite generation (non-destructive)'
|
36
|
+
end
|
16
37
|
end
|
17
38
|
end
|
18
39
|
end
|
@@ -1,21 +1,24 @@
|
|
1
1
|
module Svgeez
|
2
2
|
module Commands
|
3
3
|
class Build < Command
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
class << self
|
5
|
+
def process(options)
|
6
|
+
Svgeez::Builder.new(options).build
|
7
|
+
end
|
8
8
|
|
9
|
-
|
9
|
+
private
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
end
|
11
|
+
def command_action(options)
|
12
|
+
Build.process(options)
|
14
13
|
end
|
15
|
-
end
|
16
14
|
|
17
|
-
|
18
|
-
|
15
|
+
def command_description
|
16
|
+
'Builds an SVG sprite from a folder of SVG icons'
|
17
|
+
end
|
18
|
+
|
19
|
+
def command_syntax
|
20
|
+
'build [options]'
|
21
|
+
end
|
19
22
|
end
|
20
23
|
end
|
21
24
|
end
|
@@ -1,33 +1,33 @@
|
|
1
1
|
module Svgeez
|
2
2
|
module Commands
|
3
3
|
class Watch < Command
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
class << self
|
5
|
+
def process(options)
|
6
|
+
builder = Svgeez::Builder.new(options)
|
7
|
+
folder_path = builder.source.folder_path
|
8
8
|
|
9
|
-
|
9
|
+
Svgeez.logger.info "Watching `#{folder_path}` for changes... Press ctrl-c to stop."
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
Listen.to(folder_path, only: /\.svg\z/) { builder.build }.start
|
12
|
+
sleep
|
13
|
+
rescue Interrupt
|
14
|
+
Svgeez.logger.info 'Quitting svgeez...'
|
15
15
|
end
|
16
|
-
end
|
17
16
|
|
18
|
-
|
19
|
-
builder = Svgeez::Builder.new(options)
|
17
|
+
private
|
20
18
|
|
21
|
-
|
22
|
-
|
19
|
+
def command_action(options)
|
20
|
+
Build.process(options)
|
21
|
+
Watch.process(options)
|
23
22
|
end
|
24
23
|
|
25
|
-
|
24
|
+
def command_description
|
25
|
+
'Watches a folder of SVG icons for changes'
|
26
|
+
end
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
Svgeez.logger.info 'Quitting svgeez...'
|
28
|
+
def command_syntax
|
29
|
+
'watch [options]'
|
30
|
+
end
|
31
31
|
end
|
32
32
|
end
|
33
33
|
end
|
data/lib/svgeez/destination.rb
CHANGED
@@ -3,7 +3,7 @@ module Svgeez
|
|
3
3
|
DEFAULT_DESTINATION_FILE_NAME = 'svgeez.svg'.freeze
|
4
4
|
|
5
5
|
def initialize(options = {})
|
6
|
-
@
|
6
|
+
@destination = File.expand_path(options.fetch('destination', "./#{DEFAULT_DESTINATION_FILE_NAME}"))
|
7
7
|
end
|
8
8
|
|
9
9
|
def file_id
|
@@ -12,8 +12,8 @@ module Svgeez
|
|
12
12
|
|
13
13
|
def file_name
|
14
14
|
@file_name ||=
|
15
|
-
if destination.end_with?('.svg')
|
16
|
-
File.split(destination)[1]
|
15
|
+
if @destination.end_with?('.svg')
|
16
|
+
File.split(@destination)[1]
|
17
17
|
else
|
18
18
|
DEFAULT_DESTINATION_FILE_NAME
|
19
19
|
end
|
@@ -25,17 +25,11 @@ module Svgeez
|
|
25
25
|
|
26
26
|
def folder_path
|
27
27
|
@folder_path ||=
|
28
|
-
if destination.end_with?('.svg')
|
29
|
-
File.split(destination)[0]
|
28
|
+
if @destination.end_with?('.svg')
|
29
|
+
File.split(@destination)[0]
|
30
30
|
else
|
31
|
-
destination
|
31
|
+
@destination
|
32
32
|
end
|
33
33
|
end
|
34
|
-
|
35
|
-
private
|
36
|
-
|
37
|
-
def destination
|
38
|
-
@destination ||= File.expand_path(@options.fetch('destination', "./#{DEFAULT_DESTINATION_FILE_NAME}"))
|
39
|
-
end
|
40
34
|
end
|
41
35
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Svgeez
|
2
|
+
module Elements
|
3
|
+
class SvgElement
|
4
|
+
def initialize(source, destination, prefix)
|
5
|
+
@source = source
|
6
|
+
@destination = destination
|
7
|
+
@prefix = prefix
|
8
|
+
end
|
9
|
+
|
10
|
+
def build
|
11
|
+
%(<svg id="#{@destination.file_id}" xmlns="http://www.w3.org/2000/svg">#{symbol_elements.join}</svg>)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def symbol_elements
|
17
|
+
@source.file_paths.map do |file_path|
|
18
|
+
SymbolElement.new(file_path, @prefix).build
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Svgeez
|
2
|
+
module Elements
|
3
|
+
class SymbolElement
|
4
|
+
def initialize(file_path, file_id)
|
5
|
+
@file_path = file_path
|
6
|
+
@file_id = file_id
|
7
|
+
end
|
8
|
+
|
9
|
+
def build
|
10
|
+
IO.read(@file_path).match(%r{^<svg\s*?(?<attributes>.*?)>(?<content>.*?)</svg>}m) do |matches|
|
11
|
+
%(<symbol #{element_attributes(matches[:attributes]).sort.join(' ')}>#{element_contents(matches[:content])}</symbol>)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def element_attributes(attributes)
|
18
|
+
attrs = attributes.scan(/(?:viewBox|xmlns:.+?)=".*?"/m)
|
19
|
+
id_prefix = @file_id
|
20
|
+
id_suffix = File.basename(@file_path, '.svg').gsub(/['"\s]/, '-')
|
21
|
+
id_attribute = [id_prefix, id_suffix].reject(&:empty?).join('-')
|
22
|
+
|
23
|
+
attrs << %(id="#{id_attribute}")
|
24
|
+
end
|
25
|
+
|
26
|
+
def element_contents(content)
|
27
|
+
content.scan(/\sid="(.+?)"/).flatten.each do |value|
|
28
|
+
uuid = SecureRandom.uuid
|
29
|
+
|
30
|
+
content.gsub!(/\s(id|xlink:href)="(#?#{value})"/m, %( \\1="\\2-#{uuid}"))
|
31
|
+
content.gsub!(/\s(clip-path|fill|filter|marker-end|marker-mid|marker-start|mask|stroke)="url\((##{value})\)"/m, %( \\1="url(\\2-#{uuid})"))
|
32
|
+
end
|
33
|
+
|
34
|
+
content
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/svgeez/optimizer.rb
CHANGED
@@ -1,11 +1,16 @@
|
|
1
1
|
module Svgeez
|
2
2
|
class Optimizer
|
3
|
+
SVGO_MINIMUM_VERSION = '1.3.0'.freeze
|
4
|
+
SVGO_MINIMUM_VERSION_MESSAGE = "svgeez relies on SVGO #{SVGO_MINIMUM_VERSION} or newer. Continuing with standard sprite generation...".freeze
|
3
5
|
SVGO_NOT_INSTALLED = 'Unable to find `svgo` in your PATH. Continuing with standard sprite generation...'.freeze
|
4
6
|
|
5
7
|
def optimize(file_contents)
|
6
|
-
|
8
|
+
raise SVGO_NOT_INSTALLED unless installed?
|
9
|
+
raise SVGO_MINIMUM_VERSION_MESSAGE unless supported?
|
7
10
|
|
8
|
-
`cat <<EOF | svgo --disable=cleanupIDs -i - -o -\n#{file_contents}\nEOF`
|
11
|
+
`cat <<EOF | svgo --disable=cleanupIDs --disable=removeHiddenElems --disable=removeViewBox -i - -o -\n#{file_contents}\nEOF`
|
12
|
+
rescue RuntimeError => exception
|
13
|
+
logger.warn exception.message
|
9
14
|
end
|
10
15
|
|
11
16
|
private
|
@@ -13,5 +18,13 @@ module Svgeez
|
|
13
18
|
def installed?
|
14
19
|
@installed ||= find_executable0('svgo')
|
15
20
|
end
|
21
|
+
|
22
|
+
def logger
|
23
|
+
@logger ||= Svgeez.logger
|
24
|
+
end
|
25
|
+
|
26
|
+
def supported?
|
27
|
+
@supported ||= Gem::Version.new(`svgo -v`.strip) >= Gem::Version.new(SVGO_MINIMUM_VERSION)
|
28
|
+
end
|
16
29
|
end
|
17
30
|
end
|
data/lib/svgeez/source.rb
CHANGED
@@ -2,18 +2,16 @@ module Svgeez
|
|
2
2
|
class Source
|
3
3
|
DEFAULT_INPUT_FOLDER_PATH = './_svgeez'.freeze
|
4
4
|
|
5
|
+
attr_reader :folder_path
|
6
|
+
|
5
7
|
def initialize(options = {})
|
6
|
-
@
|
8
|
+
@folder_path = File.expand_path(options.fetch('source', DEFAULT_INPUT_FOLDER_PATH))
|
7
9
|
end
|
8
10
|
|
9
11
|
def file_paths
|
10
12
|
Dir.glob(file_paths_pattern)
|
11
13
|
end
|
12
14
|
|
13
|
-
def folder_path
|
14
|
-
@folder_path ||= File.expand_path(@options.fetch('source', DEFAULT_INPUT_FOLDER_PATH))
|
15
|
-
end
|
16
|
-
|
17
15
|
private
|
18
16
|
|
19
17
|
def file_paths_pattern
|