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.
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