svgeez 1.0.3 → 3.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 +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
|