svgeez 1.0.3 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
- task default: :spec
14
+ RuboCop::RakeTask.new do |task|
15
+ task.fail_on_error = false
16
+ end
17
+
18
+ task default: [:rubocop, :reek, :spec]
@@ -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
@@ -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
@@ -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
- @options = options
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
- return Svgeez.logger.error(SOURCE_IS_DESTINATION_MESSAGE) if source_is_destination?
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
- Svgeez.logger.info "Generating sprite at `#{destination.file_path}` from #{source.file_paths.length} SVG#{'s' if source.file_paths.length > 1}..."
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(destination.file_path, 'w') do |f|
22
- f.write destination_file_contents
32
+ File.open(destination_file_path, 'w') do |file|
33
+ file.write destination_file_contents
23
34
  end
24
35
 
25
- Svgeez.logger.info "Successfully generated sprite at `#{destination.file_path}`."
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
- def destination
30
- @destination ||= Destination.new(@options)
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 source
34
- @source ||= Source.new(@options)
51
+ def destination_file_path
52
+ @destination_file_path ||= destination.file_path
35
53
  end
36
54
 
37
- private
55
+ def logger
56
+ @logger ||= Svgeez.logger
57
+ end
38
58
 
39
- def destination_file_contents
40
- file_contents = SvgElement.new(source, destination).build
41
- file_contents = Optimizer.new.optimize(file_contents) if @options['svgo']
59
+ def source_exists?
60
+ File.directory?(source.folder_path)
61
+ end
42
62
 
43
- file_contents.insert(4, ' style="display: none;"')
63
+ def source_files_count
64
+ source.file_paths.length
44
65
  end
45
66
 
46
67
  def source_is_destination?
@@ -1,18 +1,39 @@
1
1
  module Svgeez
2
2
  class Command
3
- def self.subclasses
4
- @subclasses ||= []
5
- end
3
+ class << self
4
+ def subclasses
5
+ @subclasses ||= []
6
+ end
6
7
 
7
- def self.inherited(base)
8
- subclasses << base
9
- super(base)
10
- end
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
- def self.add_build_options(c)
13
- c.option 'source', '-s', '--source [FOLDER]', 'Source folder (defaults to ./_svgeez)'
14
- c.option 'destination', '-d', '--destination [OUTPUT]', 'Destination file or folder (defaults to ./svgeez.svg)'
15
- c.option 'svgo', '--with-svgo', 'Optimize source SVGs with SVGO before sprite generation (non-destructive)'
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
- def self.init_with_program(p)
5
- p.command(:build) do |c|
6
- c.description 'Builds an SVG sprite from a folder of SVG icons'
7
- c.syntax 'build [options]'
4
+ class << self
5
+ def process(options)
6
+ Svgeez::Builder.new(options).build
7
+ end
8
8
 
9
- add_build_options(c)
9
+ private
10
10
 
11
- c.action do |_, options|
12
- Build.process(options)
13
- end
11
+ def command_action(options)
12
+ Build.process(options)
14
13
  end
15
- end
16
14
 
17
- def self.process(options)
18
- Svgeez::Builder.new(options).build
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
- def self.init_with_program(p)
5
- p.command(:watch) do |c|
6
- c.description 'Watches a folder of SVG icons for changes'
7
- c.syntax 'watch [options]'
4
+ class << self
5
+ def process(options)
6
+ builder = Svgeez::Builder.new(options)
7
+ folder_path = builder.source.folder_path
8
8
 
9
- add_build_options(c)
9
+ Svgeez.logger.info "Watching `#{folder_path}` for changes... Press ctrl-c to stop."
10
10
 
11
- c.action do |_, options|
12
- Build.process(options)
13
- Watch.process(options)
14
- end
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
- def self.process(options)
19
- builder = Svgeez::Builder.new(options)
17
+ private
20
18
 
21
- listener = Listen.to(builder.source.folder_path, only: /\.svg\z/) do
22
- builder.build
19
+ def command_action(options)
20
+ Build.process(options)
21
+ Watch.process(options)
23
22
  end
24
23
 
25
- Svgeez.logger.info "Watching `#{builder.source.folder_path}` for changes... Press ctrl-c to stop."
24
+ def command_description
25
+ 'Watches a folder of SVG icons for changes'
26
+ end
26
27
 
27
- listener.start
28
- sleep
29
- rescue Interrupt
30
- Svgeez.logger.info 'Quitting svgeez...'
28
+ def command_syntax
29
+ 'watch [options]'
30
+ end
31
31
  end
32
32
  end
33
33
  end
@@ -3,7 +3,7 @@ module Svgeez
3
3
  DEFAULT_DESTINATION_FILE_NAME = 'svgeez.svg'.freeze
4
4
 
5
5
  def initialize(options = {})
6
- @options = options
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
@@ -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
- return Svgeez.logger.warn(SVGO_NOT_INSTALLED) unless installed?
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
@@ -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
- @options = options
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