sevgi-derender 0.73.2 → 0.93.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f93d7904c06fa7f3f220a38fa776e1e6cf31e8dbe7c1dba45616dc06f045a47c
4
- data.tar.gz: 959530f54e652a97025fa83ff7a5799f45640772357059b890742faaa83768e4
3
+ metadata.gz: 126f64b403fbdff8cea203ad63c4857bf45254f2c513c9058db6803d8137414c
4
+ data.tar.gz: c531615d9b46226cf9bf35f1c828f18e678f6e148438b880337615b38da4301b
5
5
  SHA512:
6
- metadata.gz: e8d3d5908c4c9fb6d729152a59ebbf644434ca5d19ffae80c2e94599f13048e42ef9f00377a982c584a5bd6bffb018c7666de46b5b1051245e9d274505d98513
7
- data.tar.gz: b360cb383d3967c847553288ba376c994c602eff23b539abdec42bebb613c9e35c15ba2f2fe06f782a7ce41ad50aea01979fce62d3b712153e17e1fe9087ad03
6
+ metadata.gz: 8aea4ef742bba26cfd22a502fb1f761b476dd4dfb2bcd0573120e121c0853e9892abedd5f70d51203869b5052a5df2017fb285d448c8029dd1ab308a23b5ec67
7
+ data.tar.gz: 9447d2fc113a2eb97475ea2f2fe618e9e293573097908e7ea264fd95b10124ce8edce72ee5678e802d016fa26459e5bfea63ca6ea559b2522876f9e66ecd077f
data/README.md CHANGED
@@ -0,0 +1,5 @@
1
+ # Sevgi Derender
2
+
3
+ Converts SVG/XML content back into Sevgi DSL source.
4
+
5
+ See the root [README](../README.md) and the documentation site for usage.
@@ -4,14 +4,23 @@ require "sevgi"
4
4
 
5
5
  module Sevgi
6
6
  module Binaries
7
+ # Implements the `igsev` executable that derenders SVG and executes the generated Sevgi DSL.
7
8
  module Igsev
8
9
  extend self
9
10
 
11
+ # Executable name used in help output.
10
12
  PROGNAME = "igsev"
11
13
 
14
+ # Error raised for invalid command-line usage.
12
15
  Error = Class.new(::Sevgi::Error)
13
16
 
17
+ # Parsed command-line options for the `igsev` executable.
18
+ # @api private
14
19
  Options = Struct.new(:require, :vomit, :help, :version) do
20
+ # Parses command-line options and removes them from the argv array.
21
+ # @param argv [Array<String>] mutable command-line argument array
22
+ # @return [Sevgi::Binaries::Igsev::Options] parsed options
23
+ # @raise [Sevgi::Binaries::Igsev::Error] when an option is not recognized
15
24
  def self.parse(argv)
16
25
  new.tap do |options|
17
26
  argv.first.start_with?("-") ? option(argv, options) : break until argv.empty?
@@ -41,6 +50,14 @@ module Sevgi
41
50
 
42
51
  private_constant :Options
43
52
 
53
+ # Runs the `igsev` command-line interface.
54
+ # @param argv [Array<String>, String, nil] command-line arguments
55
+ # @return [nil]
56
+ # @raise [LoadError] when a required Ruby library cannot be loaded
57
+ # @raise [Sevgi::ArgumentError] when the SVG file cannot be found
58
+ # @raise [Sevgi::Executor::Error] when `--exception` or `SEVGI_VOMIT` requests raw errors
59
+ # @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
60
+ # @raise [SystemExit] when command-line usage or script execution aborts
44
61
  def call(argv)
45
62
  return puts(help) if (options = Options.parse(argv = Array(argv))).help
46
63
  return puts(::Sevgi::VERSION) if options.version
@@ -4,14 +4,23 @@ require "sevgi"
4
4
 
5
5
  module Sevgi
6
6
  module Binaries
7
+ # Implements the `igves` executable that converts SVG files into Sevgi DSL source.
7
8
  module Igves
8
9
  extend self
9
10
 
11
+ # Executable name used in help output.
10
12
  PROGNAME = "igves"
11
13
 
14
+ # Error raised for invalid command-line usage.
12
15
  Error = Class.new(::Sevgi::Error)
13
16
 
17
+ # Parsed command-line options for the `igves` executable.
18
+ # @api private
14
19
  Options = Struct.new(:vomit, :help, :version) do
20
+ # Parses command-line options and removes them from the argv array.
21
+ # @param argv [Array<String>] mutable command-line argument array
22
+ # @return [Sevgi::Binaries::Igves::Options] parsed options
23
+ # @raise [Sevgi::Binaries::Igves::Error] when an option is not recognized
15
24
  def self.parse(argv)
16
25
  new.tap do |options|
17
26
  argv.first.start_with?("-") ? option(argv, options) : break until argv.empty?
@@ -23,6 +32,8 @@ module Sevgi
23
32
 
24
33
  def option(argv, options)
25
34
  case (arg = argv.shift)
35
+ when "-x", "--exception"
36
+ options.vomit = true
26
37
  when "-h", "--help"
27
38
  options.help = true
28
39
  when "-v", "--version"
@@ -36,25 +47,49 @@ module Sevgi
36
47
 
37
48
  private_constant :Options
38
49
 
50
+ # Runs the `igves` command-line interface.
51
+ # @param argv [Array<String>, String, nil] command-line arguments
52
+ # @return [nil]
53
+ # @raise [Sevgi::ArgumentError] when the SVG file cannot be found
54
+ # @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
55
+ # @raise [StandardError] when `--exception` or `SEVGI_VOMIT` requests raw errors
56
+ # @raise [SystemExit] when command-line usage aborts
39
57
  def call(argv)
40
- return puts(help) if (options = Options.parse(argv = Array(argv))).help
41
- return puts(::Sevgi::VERSION) if options.version
42
-
43
- puts(run(argv.shift, options))
58
+ dispatch(Array(argv))
44
59
  rescue Binaries::Igves::Error => e
45
60
  abort(e.message)
46
61
  end
47
62
 
48
63
  private
49
64
 
65
+ def dispatch(argv)
66
+ options = Options.parse(argv)
67
+ return puts(help) if options.help
68
+ return puts(::Sevgi::VERSION) if options.version
69
+
70
+ print_file(argv.shift, options)
71
+ rescue Binaries::Igves::Error
72
+ raise
73
+ rescue ::StandardError => e
74
+ raise if raw_error?(options)
75
+
76
+ die(e, nil)
77
+ end
78
+
50
79
  def die(error, _file)
51
80
  warn(error.message)
52
81
  warn("")
53
- error.backtrace!.each { warn(" #{it}") }
82
+ backtrace(error).each { warn(" #{it}") }
54
83
 
55
84
  exit(1)
56
85
  end
57
86
 
87
+ def backtrace(error)
88
+ return error.backtrace! if error.respond_to?(:backtrace!)
89
+
90
+ error.backtrace || []
91
+ end
92
+
58
93
  def help
59
94
  <<~HELP
60
95
  Usage: #{PROGNAME} [options...] <SVG file>
@@ -74,6 +109,20 @@ module Sevgi
74
109
 
75
110
  Derender.derender_file(file)
76
111
  end
112
+
113
+ def print_file(file, options)
114
+ puts(run(file, options))
115
+ rescue Binaries::Igves::Error
116
+ raise
117
+ rescue ::StandardError => e
118
+ raise if raw_error?(options)
119
+
120
+ die(e, file)
121
+ end
122
+
123
+ def raw_error?(options)
124
+ options&.vomit || ENV.fetch(ENVVOMIT, nil)
125
+ end
77
126
  end
78
127
  end
79
128
  end
@@ -2,7 +2,10 @@
2
2
 
3
3
  module Sevgi
4
4
  module Derender
5
+ # Converts SVG/XML attribute hashes into Sevgi DSL keyword source.
6
+ # @api private
5
7
  module Attributes
8
+ # Attribute keys rendered before ordinary attributes.
6
9
  ATTRIBUTES_SHOULD_COME_FIRST = %w[
7
10
  id
8
11
  inkscape:label
@@ -14,11 +17,17 @@ module Sevgi
14
17
  xmlns:_
15
18
  ]
16
19
  .freeze
20
+
21
+ # Attribute keys rendered after ordinary attributes.
17
22
  ATTRIBUTES_SHOULD_COME_LAST = %w[
18
23
  style
19
24
  ].freeze
20
25
 
26
+ # Converts an attribute hash into Sevgi DSL keyword source.
27
+ # @param hash [Hash{String, Symbol => Object}] attributes to render
28
+ # @return [String] Ruby keyword or hash source
21
29
  def decompile(hash)
30
+ hash = hash.dup
22
31
  pre, post = {}, {}
23
32
 
24
33
  ATTRIBUTES_SHOULD_COME_FIRST.each { |key| pre[key] = hash.delete(key) if hash.key?(key) }
@@ -38,7 +47,7 @@ module Sevgi
38
47
  value
39
48
  end => value
40
49
 
41
- key.match?(/\W/) ? "\"#{key}\": #{value}" : "#{key}: #{value}"
50
+ key.match?(/\W/) ? "#{Ruby.literal(key)}: #{value}" : "#{key}: #{value}"
42
51
  end
43
52
  .join(", ")
44
53
  end
@@ -4,46 +4,73 @@ require "nokogiri"
4
4
 
5
5
  module Sevgi
6
6
  module Derender
7
+ # Parsed SVG/XML document wrapper used by the derender pipeline.
7
8
  class Document
8
9
  @cache = {}
9
10
 
10
11
  class << self
12
+ # @return [Hash{String => Nokogiri::XML::Document}] parsed document cache keyed by expanded file path
11
13
  attr_reader :cache
12
14
  end
13
15
 
16
+ # Loads and parses an SVG/XML file, using the parse cache when possible.
17
+ # @param path [String] path to the source file, with or without `.svg` extension
18
+ # @return [Sevgi::Derender::Document] document wrapper
19
+ # @raise [Sevgi::ArgumentError] when the file cannot be found
20
+ # @raise [Errno::EACCES] when the file cannot be read
14
21
  def self.load_file(path)
15
22
  entry = ::File.expand_path(F.qualify(path, "svg"))
16
23
 
17
24
  ArgumentError.("File not found: #{path}") unless ::File.exist?(entry)
18
25
 
19
- new(::File.read(entry)) do
26
+ content = ::File.read(entry)
27
+ new(content) do
20
28
  @doc = self.class.cache[entry] ||
21
29
  begin
22
- self.class.cache[entry] = self.class.parse(::File.read(entry))
30
+ self.class.cache[entry] = self.class.parse(content)
23
31
  end
24
32
  end
25
33
  end
26
34
 
35
+ # Parses SVG/XML content.
36
+ # @param content [String] SVG/XML source content
37
+ # @return [Nokogiri::XML::Document] parsed XML document
27
38
  def self.parse(content) = Nokogiri::XML(content)
28
39
 
40
+ # Extracts the XML declaration from SVG/XML content.
41
+ # @param content [String] SVG/XML source content
42
+ # @return [String, nil] XML declaration line, if present
29
43
  def self.declaration(content)
30
44
  return unless (content = content.lstrip).start_with?("<?xml ")
31
45
 
32
46
  content.split("\n").first
33
47
  end
34
48
 
49
+ # @!attribute [r] doc
50
+ # @return [Nokogiri::XML::Document] parsed XML document
51
+ # @!attribute [r] decl
52
+ # @return [String, nil] XML declaration line, if present
35
53
  attr_reader :doc, :decl
36
54
 
55
+ # Builds a parsed document wrapper from SVG/XML content.
56
+ # @param content [String] SVG/XML source content
57
+ # @yield optional initializer used by {load_file} to install cached parse state
58
+ # @yieldreturn [void]
59
+ # @return [void]
37
60
  def initialize(content, &block)
38
61
  instance_exec(&block) if block
39
62
 
40
- @doc = self.class.parse(content)
63
+ @doc ||= self.class.parse(content)
41
64
  @decl = self.class.declaration(content)
42
65
  end
43
66
 
67
+ # Converts the root or selected node into a derender node.
68
+ # @param id [String, nil] optional SVG id selecting a node inside the document
69
+ # @return [Sevgi::Derender::Node] selected node in the derender tree
70
+ # @raise [Sevgi::ArgumentError] when the id is absent
44
71
  def decompile(id = nil)
45
72
  if id
46
- if (found = doc.xpath("//*[@id='#{id}']") || []).empty?
73
+ if (found = doc.xpath("//*[@id=#{xpath_literal(id)}]") || []).empty?
47
74
  ArgumentError.("No such element with id '#{id}' in document")
48
75
  end
49
76
 
@@ -55,12 +82,29 @@ module Sevgi
55
82
  Node.new(element, pres)
56
83
  end
57
84
 
85
+ # Returns XML declaration and pre-root nodes preserved for root decompilation.
86
+ # @return [Array<String>] preamble XML lines
58
87
  def pres
59
88
  @pres ||= [].tap do |lines|
60
89
  lines.append(*doc.children.take_while { |node| node != doc.root }.map(&:to_xml))
61
90
  lines.unshift(decl) unless lines.first == decl
62
91
  end
63
92
  end
93
+
94
+ private
95
+
96
+ def xpath_literal(value)
97
+ value = value.to_s
98
+
99
+ return "'#{value}'" unless value.include?("'")
100
+ return "\"#{value}\"" unless value.include?("\"")
101
+
102
+ parts = value.split("'", -1).flat_map.with_index do |part, index|
103
+ index.zero? ? [part] : ["'", part]
104
+ end
105
+
106
+ "concat(#{parts.map { |part| part == "'" ? "\"'\"" : "'#{part}'" }.join(", ")})"
107
+ end
64
108
  end
65
109
  end
66
110
  end
@@ -3,7 +3,11 @@
3
3
  module Sevgi
4
4
  module Derender
5
5
  module Elements
6
+ # Default element strategy for ordinary SVG/XML elements.
7
+ # @api private
6
8
  module Any
9
+ # Converts this node into unformatted Sevgi DSL lines.
10
+ # @return [Array<String>] unformatted Ruby source lines
7
11
  def decompile(*)
8
12
  if children.any?
9
13
  children.one? && children.first.node.text? ? Array(leaf(has_attributes: attributes.any?)) : tree
@@ -16,11 +20,11 @@ module Sevgi
16
20
 
17
21
  def leaf(has_content: true, has_attributes: true)
18
22
  attributes = attributes!
19
- [
20
- element,
21
- *("'#{content}', " if has_content),
22
- *(Attributes.decompile(attributes) if has_attributes)
23
- ].join(" ")
23
+ args = []
24
+ args << Ruby.literal(content) if has_content
25
+ args << Attributes.decompile(attributes) if has_attributes && attributes.any?
26
+
27
+ args.empty? ? element : "#{element} #{args.join(", ")}"
24
28
  end
25
29
 
26
30
  def tree
@@ -1,10 +1,13 @@
1
- #!/usr/bin/env ruby
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module Sevgi
5
4
  module Derender
6
5
  module Elements
6
+ # Element strategy for SVG style elements.
7
+ # @api private
7
8
  module CSS
9
+ # Converts a style node into unformatted Sevgi DSL lines.
10
+ # @return [Array<String>] unformatted Ruby source lines
8
11
  def decompile(*)
9
12
  return [] unless (lines = css_lines)
10
13
 
@@ -24,7 +27,7 @@ module Sevgi
24
27
  hash
25
28
  .map do |selector, declarations|
26
29
  [
27
- "\"#{selector}\": {",
30
+ "#{Ruby.literal(selector)}: {",
28
31
  *declarations.map { |key, value| "#{Css.to_key_value(key, value)}," },
29
32
  "},"
30
33
  ]
@@ -1,10 +1,13 @@
1
- #!/usr/bin/env ruby
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module Sevgi
5
4
  module Derender
6
5
  module Elements
6
+ # Element strategy for comments and ignored XML nodes.
7
+ # @api private
7
8
  module Junk
9
+ # Drops ignored nodes from generated source.
10
+ # @return [Array<String>] empty source lines
8
11
  def decompile(*) = []
9
12
  end
10
13
  end
@@ -3,9 +3,14 @@
3
3
  module Sevgi
4
4
  module Derender
5
5
  module Elements
6
+ # Element strategy for the SVG root element.
7
+ # @api private
6
8
  module Root
7
9
  include Any
8
10
 
11
+ # Converts the SVG root into unformatted Sevgi DSL lines.
12
+ # @param pres [Array<String>] preamble XML lines
13
+ # @return [Array<String>] unformatted Ruby source lines
9
14
  def decompile(pres = [])
10
15
  lines = super
11
16
  return lines unless pres&.any?
@@ -13,15 +18,19 @@ module Sevgi
13
18
  lines.unshift(
14
19
  [
15
20
  "SVG.document preambles: [",
16
- *pres.map { "'#{it}'," },
21
+ *pres.map { "#{Ruby.literal(it)}," },
17
22
  "]",
18
23
  ""
19
24
  ]
20
25
  )
21
26
  end
22
27
 
28
+ # Returns the root DSL word.
29
+ # @return [String]
23
30
  def element = "SVG"
24
31
 
32
+ # Returns root attributes merged with namespace declarations.
33
+ # @return [Hash{String => String}] attributes and namespaces
25
34
  def attributes! = {**attributes, **namespaces}
26
35
  end
27
36
  end
@@ -1,11 +1,14 @@
1
- #!/usr/bin/env ruby
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module Sevgi
5
4
  module Derender
6
5
  module Elements
6
+ # Element strategy for text nodes.
7
+ # @api private
7
8
  module Text
8
- def decompile(*) = ["_ #{content}"]
9
+ # Converts a text node into unformatted Sevgi DSL lines.
10
+ # @return [Array<String>] unformatted Ruby source lines
11
+ def decompile(*) = ["_ #{Ruby.literal(content)}"]
9
12
  end
10
13
  end
11
14
  end
@@ -1,5 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ module Sevgi
4
+ module Derender
5
+ # Element strategy modules mixed into derender nodes according to XML node type.
6
+ # @api private
7
+ module Elements
8
+ end
9
+ end
10
+ end
11
+
3
12
  require_relative "elements/any"
4
13
  require_relative "elements/css"
5
14
  require_relative "elements/junk"
@@ -4,37 +4,66 @@ require "sevgi/function"
4
4
 
5
5
  module Sevgi
6
6
  module Derender
7
+ # CSS parser and formatter helpers used while derendering attributes and style nodes.
8
+ # @api private
7
9
  module Css
8
10
  require "css_parser"
9
11
 
12
+ # Parses CSS rules into a selector/declaration hash.
13
+ # @param css_string [String] CSS rule source
14
+ # @return [Hash, nil] parsed CSS declarations grouped by selector
10
15
  def to_h(css_string)
11
16
  parser = ::CssParser::Parser.new
12
17
  parser.load_string!(css_string)
13
18
  parser.to_h["all"]
14
19
  end
15
20
 
21
+ # Parses an inline style declaration string.
22
+ # @param style_string [String] CSS declaration source
23
+ # @return [Hash] parsed declarations for the synthetic universal selector
16
24
  def to_h!(style_string) = to_h("* { #{style_string} }")["*"]
17
25
 
26
+ # Converts a CSS key into a Ruby hash key.
27
+ # @param arg [String] CSS key
28
+ # @return [String] Ruby hash key source
18
29
  def to_key(arg) = arg
19
30
 
20
- def to_key_value(key, value) = "\"#{to_key(key)}\": #{to_value(value)}"
31
+ # Converts a CSS key/value pair into Ruby hash source.
32
+ # @param key [String] CSS declaration key
33
+ # @param value [String] CSS declaration value
34
+ # @return [String] Ruby hash pair source
35
+ def to_key_value(key, value) = "#{Ruby.literal(to_key(key))}: #{to_value(value)}"
21
36
 
22
- def to_value(arg) = (arg.to_f.to_s == arg) || (arg.to_i.to_s == arg) ? arg : "\"#{arg}\""
37
+ # Converts a CSS value into Ruby source.
38
+ # @param arg [String] CSS value
39
+ # @return [String] Ruby literal or numeric source
40
+ def to_value(arg) = (arg.to_f.to_s == arg) || (arg.to_i.to_s == arg) ? arg : arg.inspect
23
41
 
24
42
  extend self
25
43
  end
26
44
 
27
45
  private_constant :Css
28
46
 
47
+ # Ruby source formatting helpers used by the derender pipeline.
48
+ # @api private
29
49
  module Ruby
30
50
  require "rufo"
31
51
 
52
+ # Formats generated Ruby source.
53
+ # @param unformatted_ruby [String] unformatted Ruby source
54
+ # @return [String] formatted Ruby source
55
+ # @raise [Sevgi::PanicError] when source cannot be parsed by the formatter
32
56
  def format(unformatted_ruby)
33
57
  Rufo::Formatter.format(unformatted_ruby)
34
58
  rescue Rufo::SyntaxError
35
- raise unformatted_ruby
59
+ PanicError.(unformatted_ruby)
36
60
  end
37
61
 
62
+ # Converts a value into a Ruby string literal.
63
+ # @param value [Object] value to stringify
64
+ # @return [String] Ruby string literal source
65
+ def literal(value) = value.to_s.inspect
66
+
38
67
  extend self
39
68
  end
40
69
 
@@ -2,17 +2,31 @@
2
2
 
3
3
  module Sevgi
4
4
  module Derender
5
+ # Node in a derender tree produced from an SVG/XML node.
5
6
  class Node
7
+ # @!attribute [r] node
8
+ # @return [Nokogiri::XML::Node] source XML node
9
+ # @!attribute [r] pres
10
+ # @return [Array<String>] preamble XML lines carried by the root node
11
+ # @!attribute [r] type
12
+ # @return [Symbol] dispatch type used by derender element strategies
6
13
  attr_reader :node, :pres, :type
7
14
 
15
+ # Builds a derender node.
16
+ # @param node [Nokogiri::XML::Node] source XML node
17
+ # @param pres [Array<String>] preamble XML lines carried by the root node
18
+ # @return [void]
8
19
  def initialize(node, pres = [])
9
20
  @node = node
10
21
  @pres = pres
11
22
  @type = dispatch
12
23
  end
13
24
 
25
+ # Attribute namespace prefix used for Sevgi metadata.
14
26
  META_NAMESPACE = "_:"
15
27
 
28
+ # Returns Sevgi metadata attributes without the metadata namespace prefix.
29
+ # @return [Hash{String => String}] metadata attributes
16
30
  def _
17
31
  @_ ||= attributes
18
32
  .slice(
@@ -23,6 +37,8 @@ module Sevgi
23
37
 
24
38
  alias meta _
25
39
 
40
+ # Returns source XML attributes keyed with namespace prefixes when present.
41
+ # @return [Hash{String => String}] XML attributes
26
42
  def attributes
27
43
  @attributes ||= node.attribute_nodes.to_h do |attr|
28
44
  name, value = attr.name, attr.value
@@ -37,28 +53,53 @@ module Sevgi
37
53
  end
38
54
  end
39
55
 
56
+ # Returns source XML attributes.
57
+ # @return [Hash{String => String}] XML attributes
40
58
  def attributes! = attributes
41
59
 
60
+ # Returns non-ignorable child derender nodes.
61
+ # @return [Array<Sevgi::Derender::Node>] child nodes
42
62
  def children
43
- @children ||= node.children.map { |child| self.class.new(child) }.reject do |child|
44
- (child.node.text? and child.node.text.strip.empty?) or child.type == :Junk
45
- end
63
+ @children ||= node
64
+ .children
65
+ .map { |child| self.class.new(child) }
66
+ .reject { |child| ignorable_child?(child) }
46
67
  end
47
68
 
48
- def content = @content ||= node.content.strip
69
+ # Returns node text content, preserving or stripping whitespace according to xml:space.
70
+ # @return [String]
71
+ def content = @content ||= preserve_space? ? node.content : node.content.strip
49
72
 
73
+ # Converts this node into formatted Sevgi DSL Ruby source.
74
+ # @return [String] formatted Sevgi DSL source
75
+ # @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
50
76
  def derender = Ruby.format(decompile(pres).join("\n"))
51
77
 
78
+ # Returns the Sevgi DSL element name for this node.
79
+ # @return [String]
52
80
  def element = name
53
81
 
82
+ # Evaluates this node under a graphics element.
83
+ # @param element [Sevgi::Graphics::Element] target graphics element
84
+ # @param include_current [Boolean] true to evaluate this node, false to evaluate only children
85
+ # @return [Sevgi::Graphics::Element] target graphics element
86
+ # @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
54
87
  def evaluate(element, include_current = true)
55
88
  return element.instance_eval(derender) if include_current
56
89
 
57
90
  children.each { element.instance_eval(it.derender) }
58
91
  end
59
92
 
93
+ # Evaluates only this node's children under a graphics element.
94
+ # @param element [Sevgi::Graphics::Element] target graphics element
95
+ # @return [Sevgi::Graphics::Element] target graphics element
96
+ # @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
60
97
  def evaluate!(element) = evaluate(element, false)
61
98
 
99
+ # Finds the first descendant whose attribute matches a value.
100
+ # @param arg [String] attribute value to find
101
+ # @param by [String] attribute name used for lookup
102
+ # @return [Sevgi::Derender::Node, nil] matching node, or nil
62
103
  def find(arg, by: "id")
63
104
  return self if attributes[by] == arg
64
105
 
@@ -71,10 +112,16 @@ module Sevgi
71
112
  nil
72
113
  end
73
114
 
115
+ # Returns the source XML node name.
116
+ # @return [String]
74
117
  def name = @name ||= node.name
75
118
 
119
+ # Returns source XML namespaces.
120
+ # @return [Hash{String => String}] namespace declarations
76
121
  def namespaces = (@namespaces ||= node.namespaces.to_h { |namespace, uri| [namespace, uri] })
77
122
 
123
+ # Reports whether this node is the SVG root strategy.
124
+ # @return [Boolean]
78
125
  def root? = type == :Root
79
126
 
80
127
  private
@@ -93,6 +140,44 @@ module Sevgi
93
140
  end
94
141
  .tap { extend(Elements.const_get(it)) }
95
142
  end
143
+
144
+ def ignorable_child?(child)
145
+ child.type == :Junk || (child.node.text? && child.node.text.strip.empty? && !child.preserve_space?)
146
+ end
147
+
148
+ protected
149
+
150
+ def preserve_space?
151
+ each_node do |current|
152
+ case xml_space(current)
153
+ when "preserve"
154
+ return true
155
+ when "default"
156
+ return false
157
+ end
158
+ end
159
+
160
+ false
161
+ end
162
+
163
+ private
164
+
165
+ def each_node
166
+ current = node
167
+
168
+ while current
169
+ yield current
170
+ current = current.respond_to?(:parent) ? current.parent : nil
171
+ end
172
+ end
173
+
174
+ def xml_space(current)
175
+ return unless current.respond_to?(:attribute_nodes)
176
+
177
+ current.attribute_nodes.find { xml_space_attribute?(it) }&.value
178
+ end
179
+
180
+ def xml_space_attribute?(attribute) = attribute.name == "space" && attribute.namespace&.prefix == "xml"
96
181
  end
97
182
  end
98
183
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Sevgi
4
4
  module Derender
5
- VERSION = "0.73.2"
5
+ # Current version of the Sevgi derender gem.
6
+ VERSION = "0.93.1"
6
7
  end
7
8
  end
@@ -10,21 +10,72 @@ require_relative "derender/node"
10
10
  require_relative "derender/version"
11
11
 
12
12
  module Sevgi
13
+ # Converts SVG/XML content into Sevgi DSL source or evaluates it into graphics elements.
13
14
  module Derender
14
- # Takes SVG (XML content) and returns a Derender::Node object.
15
+ # Converts SVG/XML content into a derender node.
16
+ # @param content [String] SVG/XML source content
17
+ # @param id [String, nil] optional SVG id selecting a node inside the source
18
+ # @return [Sevgi::Derender::Node] selected node in the derender tree
19
+ # @raise [Sevgi::ArgumentError] when the id is absent
15
20
  def decompile(content, id: nil) = Document.new(content).decompile(id)
21
+
22
+ # Converts an SVG/XML file into a derender node.
23
+ # @param file [String] path to the source SVG/XML file
24
+ # @param id [String, nil] optional SVG id selecting a node inside the source
25
+ # @return [Sevgi::Derender::Node] selected node in the derender tree
26
+ # @raise [Sevgi::ArgumentError] when the file cannot be found or the id is absent
16
27
  def decompile_file(file, id: nil) = Document.load_file(file).decompile(id)
17
28
 
18
- # Takes SVG (XML content) and returns Sevgi DSL string (formatted Ruby content).
29
+ # Converts SVG/XML content into Sevgi DSL Ruby source.
30
+ # @param content [String] SVG/XML source content
31
+ # @param id [String, nil] optional SVG id selecting a node inside the source
32
+ # @return [String] formatted Sevgi DSL source
33
+ # @raise [Sevgi::ArgumentError] when the id is absent
34
+ # @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
19
35
  def derender(content, id: nil) = Document.new(content).decompile(id).derender
36
+
37
+ # Converts an SVG/XML file into Sevgi DSL Ruby source.
38
+ # @param file [String] path to the source SVG/XML file
39
+ # @param id [String, nil] optional SVG id selecting a node inside the source
40
+ # @return [String] formatted Sevgi DSL source
41
+ # @raise [Sevgi::ArgumentError] when the file cannot be found or the id is absent
42
+ # @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
20
43
  def derender_file(file, id: nil) = Document.load_file(file).decompile(id).derender
21
44
 
22
- # Takes SVG (XML content), evaluates it under the given Graphics element and returns the element.
45
+ # Evaluates SVG/XML content under a graphics element, including the selected node.
46
+ # @param content [String] SVG/XML source content
47
+ # @param element [Sevgi::Graphics::Element] target graphics element
48
+ # @param id [String, nil] optional SVG id selecting a node inside the source
49
+ # @return [Sevgi::Graphics::Element] target graphics element
50
+ # @raise [Sevgi::ArgumentError] when the id is absent
51
+ # @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
23
52
  def evaluate(content, element, id: nil) = Document.new(content).decompile(id).evaluate(element)
53
+
54
+ # Evaluates an SVG/XML file under a graphics element, including the selected node.
55
+ # @param file [String] path to the source SVG/XML file
56
+ # @param element [Sevgi::Graphics::Element] target graphics element
57
+ # @param id [String, nil] optional SVG id selecting a node inside the source
58
+ # @return [Sevgi::Graphics::Element] target graphics element
59
+ # @raise [Sevgi::ArgumentError] when the file cannot be found or the id is absent
60
+ # @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
24
61
  def evaluate_file(file, element, id: nil) = Document.load_file(file).decompile(id).evaluate(element)
25
62
 
26
- # Takes SVG (XML content), evaluates the inner node under the given Graphics element and returns the element.
63
+ # Evaluates SVG/XML content under a graphics element, excluding the selected node itself.
64
+ # @param content [String] SVG/XML source content
65
+ # @param element [Sevgi::Graphics::Element] target graphics element
66
+ # @param id [String, nil] optional SVG id selecting a node inside the source
67
+ # @return [Sevgi::Graphics::Element] target graphics element
68
+ # @raise [Sevgi::ArgumentError] when the id is absent
69
+ # @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
27
70
  def evaluate!(content, element, id: nil) = Document.new(content).decompile(id).evaluate!(element)
71
+
72
+ # Evaluates an SVG/XML file under a graphics element, excluding the selected node itself.
73
+ # @param file [String] path to the source SVG/XML file
74
+ # @param element [Sevgi::Graphics::Element] target graphics element
75
+ # @param id [String, nil] optional SVG id selecting a node inside the source
76
+ # @return [Sevgi::Graphics::Element] target graphics element
77
+ # @raise [Sevgi::ArgumentError] when the file cannot be found or the id is absent
78
+ # @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
28
79
  def evaluate_file!(file, element, id: nil) = Document.load_file(file).decompile(id).evaluate!(element)
29
80
 
30
81
  extend self
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sevgi-derender
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.73.2
4
+ version: 0.93.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Recai Oktaş
@@ -15,28 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.73.2
18
+ version: 0.93.1
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.73.2
25
+ version: 0.93.1
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: sevgi-graphics
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - '='
31
31
  - !ruby/object:Gem::Version
32
- version: 0.73.2
32
+ version: 0.93.1
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - '='
38
38
  - !ruby/object:Gem::Version
39
- version: 0.73.2
39
+ version: 0.93.1
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: css_parser
42
42
  requirement: !ruby/object:Gem::Requirement