sevgi-derender 0.93.1 → 0.94.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 126f64b403fbdff8cea203ad63c4857bf45254f2c513c9058db6803d8137414c
4
- data.tar.gz: c531615d9b46226cf9bf35f1c828f18e678f6e148438b880337615b38da4301b
3
+ metadata.gz: 9f1b7265f14be33da5b43f634ac82059a6d438e723ecc2153a062452ea1610fc
4
+ data.tar.gz: 5c52f4aef783cd3b3c712f19883ebbcab9c28efce3802318fe1b1f55b2a7fd88
5
5
  SHA512:
6
- metadata.gz: 8aea4ef742bba26cfd22a502fb1f761b476dd4dfb2bcd0573120e121c0853e9892abedd5f70d51203869b5052a5df2017fb285d448c8029dd1ab308a23b5ec67
7
- data.tar.gz: 9447d2fc113a2eb97475ea2f2fe618e9e293573097908e7ea264fd95b10124ce8edce72ee5678e802d016fa26459e5bfea63ca6ea559b2522876f9e66ecd077f
6
+ metadata.gz: 98d494547c8854554dc9f791c4abd6ef1e99eb726face62c03b49756a0694be0267d311b103ac35a3797ad52dcc60cacf770c270569babcf5c4a6e9ee1bb0930
7
+ data.tar.gz: 3b127ab458de366d81b7efc0fb8b4ba94d6ce26cf2c9761389e4375aea02a079aaaa0c8447841416e45857674d9daee4494ffb1c28e2bca30b8944ba3addeeb3
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ # Changelog
2
+
3
+ Sevgi Derender follows the root Sevgi release notes:
4
+ https://github.com/roktas/sevgi/blob/main/CHANGELOG.md
data/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ Sevgi Derender is distributed under the GNU General Public License v3.0 or later.
2
+
3
+ SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ Full project license: https://github.com/roktas/sevgi/blob/main/LICENSE
data/README.md CHANGED
@@ -2,4 +2,43 @@
2
2
 
3
3
  Converts SVG/XML content back into Sevgi DSL source.
4
4
 
5
- See the root [README](../README.md) and the documentation site for usage.
5
+ ## Install
6
+
7
+ ```sh
8
+ gem install sevgi-derender
9
+ ```
10
+
11
+ ## Require
12
+
13
+ ```ruby
14
+ require "sevgi/derender"
15
+ ```
16
+
17
+ ## Example
18
+
19
+ ```ruby
20
+ source = "<svg><rect width=\"3\" height=\"5\"/></svg>"
21
+ Sevgi::Derender.derender(source)
22
+ ```
23
+
24
+ ## Executable
25
+
26
+ ```sh
27
+ igves drawing.svg
28
+ ```
29
+
30
+ ## Ruby compatibility
31
+
32
+ Requires Ruby 3.4.0 or newer. CI verifies Ruby 3.4 and the current development Ruby from `.ruby-version`.
33
+
34
+ ## Native prerequisites
35
+
36
+ None beyond Ruby and this gem's Ruby dependencies. Nokogiri may use platform packages depending on the target Ruby
37
+ platform.
38
+
39
+ ## Links
40
+
41
+ - Documentation: https://sevgi.roktas.dev
42
+ - API documentation: https://www.rubydoc.info/gems/sevgi-derender
43
+ - Source: https://github.com/roktas/sevgi/tree/main/derender
44
+ - Changelog: https://github.com/roktas/sevgi/blob/main/CHANGELOG.md
@@ -38,7 +38,8 @@ module Sevgi
38
38
  key = Css.to_key(key) if key.is_a?(::String)
39
39
 
40
40
  if key == "style"
41
- "{ #{Attributes.decompile(Css.to_h!(value))} }"
41
+ style = Css.to_h!(value)
42
+ style.empty? ? "{}" : "{ #{Attributes.decompile(style)} }"
42
43
  elsif value.is_a?(::String)
43
44
  Css.to_value(value)
44
45
  elsif value.is_a?(::Hash)
@@ -6,17 +6,13 @@ module Sevgi
6
6
  module Derender
7
7
  # Parsed SVG/XML document wrapper used by the derender pipeline.
8
8
  class Document
9
- @cache = {}
10
-
11
- class << self
12
- # @return [Hash{String => Nokogiri::XML::Document}] parsed document cache keyed by expanded file path
13
- attr_reader :cache
14
- end
15
-
16
- # Loads and parses an SVG/XML file, using the parse cache when possible.
9
+ # Loads and parses an SVG/XML file.
10
+ #
11
+ # Each call reads the current file content and returns an isolated parsed document. Caller mutation of one loaded
12
+ # document does not affect later loads of the same path.
17
13
  # @param path [String] path to the source file, with or without `.svg` extension
18
14
  # @return [Sevgi::Derender::Document] document wrapper
19
- # @raise [Sevgi::ArgumentError] when the file cannot be found
15
+ # @raise [Sevgi::ArgumentError] when the file cannot be found or file content is malformed XML
20
16
  # @raise [Errno::EACCES] when the file cannot be read
21
17
  def self.load_file(path)
22
18
  entry = ::File.expand_path(F.qualify(path, "svg"))
@@ -24,26 +20,30 @@ module Sevgi
24
20
  ArgumentError.("File not found: #{path}") unless ::File.exist?(entry)
25
21
 
26
22
  content = ::File.read(entry)
27
- new(content) do
28
- @doc = self.class.cache[entry] ||
29
- begin
30
- self.class.cache[entry] = self.class.parse(content)
31
- end
32
- end
23
+ new(content)
33
24
  end
34
25
 
35
- # Parses SVG/XML content.
26
+ # Parses SVG/XML content in strict XML mode.
36
27
  # @param content [String] SVG/XML source content
37
28
  # @return [Nokogiri::XML::Document] parsed XML document
38
- def self.parse(content) = Nokogiri::XML(content)
29
+ # @raise [Sevgi::ArgumentError] when content is not well-formed XML
30
+ # @raise [Sevgi::ArgumentError] when content has no root element
31
+ def self.parse(content)
32
+ Nokogiri::XML(content.to_s.lstrip, &:strict).tap do |doc|
33
+ ArgumentError.("XML document has no root element") unless doc.root
34
+ end
35
+
36
+ rescue Nokogiri::XML::SyntaxError => e
37
+ raise ArgumentError, "Malformed XML: #{e.message.lines.first.strip}", cause: e
38
+ end
39
39
 
40
40
  # Extracts the XML declaration from SVG/XML content.
41
41
  # @param content [String] SVG/XML source content
42
42
  # @return [String, nil] XML declaration line, if present
43
43
  def self.declaration(content)
44
- return unless (content = content.lstrip).start_with?("<?xml ")
44
+ return unless (content = content.to_s.lstrip).start_with?("<?xml ")
45
45
 
46
- content.split("\n").first
46
+ content[/\A<\?xml\b.*?\?>/m]
47
47
  end
48
48
 
49
49
  # @!attribute [r] doc
@@ -57,6 +57,7 @@ module Sevgi
57
57
  # @yield optional initializer used by {load_file} to install cached parse state
58
58
  # @yieldreturn [void]
59
59
  # @return [void]
60
+ # @raise [Sevgi::ArgumentError] when content is malformed XML
60
61
  def initialize(content, &block)
61
62
  instance_exec(&block) if block
62
63
 
@@ -67,7 +68,7 @@ module Sevgi
67
68
  # Converts the root or selected node into a derender node.
68
69
  # @param id [String, nil] optional SVG id selecting a node inside the document
69
70
  # @return [Sevgi::Derender::Node] selected node in the derender tree
70
- # @raise [Sevgi::ArgumentError] when the id is absent
71
+ # @raise [Sevgi::ArgumentError] when the document has no root element or the id is absent
71
72
  def decompile(id = nil)
72
73
  if id
73
74
  if (found = doc.xpath("//*[@id=#{xpath_literal(id)}]") || []).empty?
@@ -79,7 +80,9 @@ module Sevgi
79
80
  doc.root
80
81
  end => element
81
82
 
82
- Node.new(element, pres)
83
+ ArgumentError.("XML document has no root element") unless element
84
+
85
+ Node.new(element, pres, namespaces: namespace_scope(element))
83
86
  end
84
87
 
85
88
  # Returns XML declaration and pre-root nodes preserved for root decompilation.
@@ -93,6 +96,18 @@ module Sevgi
93
96
 
94
97
  private
95
98
 
99
+ def namespace_scope(element)
100
+ element == doc.root ? local_namespaces(element) : element.namespaces
101
+ end
102
+
103
+ def local_namespaces(element)
104
+ return {} unless element.respond_to?(:namespace_definitions)
105
+
106
+ element.namespace_definitions.to_h do |namespace|
107
+ [namespace.prefix ? "xmlns:#{namespace.prefix}" : "xmlns", namespace.href]
108
+ end
109
+ end
110
+
96
111
  def xpath_literal(value)
97
112
  value = value.to_s
98
113
 
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevgi
4
+ module Derender
5
+ # Builds graphics element trees from derender nodes without evaluating Ruby source.
6
+ # @api private
7
+ class Evaluator
8
+ # Builds an evaluator.
9
+ # @param parent [Sevgi::Graphics::Element] target graphics parent
10
+ # @return [void]
11
+ def initialize(parent) = @parent = parent
12
+
13
+ # Appends a derender node to the target parent.
14
+ # @param node [Sevgi::Derender::Node] derender node
15
+ # @return [Sevgi::Graphics::Element, nil] included element, or nil when the node does not produce graphics output
16
+ def append(node)
17
+ case node.type
18
+ when :CSS
19
+ append_css(node)
20
+ when :Text
21
+ parent.Element(:_, node.content)
22
+ else
23
+ append_element(node)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :parent
30
+
31
+ def append_css(node)
32
+ return unless (hash = Css.to_h(node.node.content))
33
+
34
+ parent.Element(:style, Graphics::Content.css(hash), type: "text/css", **node.attributes)
35
+ end
36
+
37
+ def append_element(node)
38
+ parent.Element(node.name, *content(node), **attributes(node)).tap do |element|
39
+ node.children.each { self.class.new(element).append(it) } unless content(node).any?
40
+ end
41
+ end
42
+
43
+ def attributes(node) = node.attributes!
44
+
45
+ def content(node)
46
+ node.children.one? && node.children.first.node.text? ? [node.content] : []
47
+ end
48
+ end
49
+
50
+ private_constant :Evaluator
51
+ end
52
+ end
@@ -21,7 +21,11 @@ module Sevgi
21
21
  # Parses an inline style declaration string.
22
22
  # @param style_string [String] CSS declaration source
23
23
  # @return [Hash] parsed declarations for the synthetic universal selector
24
- def to_h!(style_string) = to_h("* { #{style_string} }")["*"]
24
+ def to_h!(style_string)
25
+ hash = to_h("* { #{style_string} }")
26
+
27
+ hash ? hash.fetch("*", {}) : {}
28
+ end
25
29
 
26
30
  # Converts a CSS key into a Ruby hash key.
27
31
  # @param arg [String] CSS key
@@ -15,10 +15,13 @@ module Sevgi
15
15
  # Builds a derender node.
16
16
  # @param node [Nokogiri::XML::Node] source XML node
17
17
  # @param pres [Array<String>] preamble XML lines carried by the root node
18
+ # @param namespaces [Hash{String => String}, nil] namespace declarations to emit on this node; selected roots use
19
+ # their full inherited namespace scope, while ordinary children use only declarations from their own element
18
20
  # @return [void]
19
- def initialize(node, pres = [])
21
+ def initialize(node, pres = [], namespaces: nil)
20
22
  @node = node
21
23
  @pres = pres
24
+ @namespaces = namespaces
22
25
  @type = dispatch
23
26
  end
24
27
 
@@ -39,23 +42,11 @@ module Sevgi
39
42
 
40
43
  # Returns source XML attributes keyed with namespace prefixes when present.
41
44
  # @return [Hash{String => String}] XML attributes
42
- def attributes
43
- @attributes ||= node.attribute_nodes.to_h do |attr|
44
- name, value = attr.name, attr.value
45
+ def attributes = @attributes ||= node.attribute_nodes.to_h { [attribute_key(it), it.value] }
45
46
 
46
- if attr.respond_to?(:namespace) && (namespace = attr.namespace) && (prefix = namespace.prefix)
47
- "#{prefix}:#{name}"
48
- else
49
- name
50
- end => key
51
-
52
- [key, value]
53
- end
54
- end
55
-
56
- # Returns source XML attributes.
47
+ # Returns source XML attributes and namespace declarations emitted on this node.
57
48
  # @return [Hash{String => String}] XML attributes
58
- def attributes! = attributes
49
+ def attributes! = {**attributes, **namespaces}
59
50
 
60
51
  # Returns non-ignorable child derender nodes.
61
52
  # @return [Array<Sevgi::Derender::Node>] child nodes
@@ -66,9 +57,12 @@ module Sevgi
66
57
  .reject { |child| ignorable_child?(child) }
67
58
  end
68
59
 
69
- # Returns node text content, preserving or stripping whitespace according to xml:space.
60
+ # Returns node text content.
61
+ #
62
+ # `xml:space="preserve"` keeps content verbatim. Default-space text nodes are stripped for ordinary pretty-printed
63
+ # content, but inline text boundary spaces next to element siblings are kept because they affect rendered SVG text.
70
64
  # @return [String]
71
- def content = @content ||= preserve_space? ? node.content : node.content.strip
65
+ def content = @content ||= preserve_space? ? node.content : normalized_content
72
66
 
73
67
  # Converts this node into formatted Sevgi DSL Ruby source.
74
68
  # @return [String] formatted Sevgi DSL source
@@ -82,19 +76,18 @@ module Sevgi
82
76
  # Evaluates this node under a graphics element.
83
77
  # @param element [Sevgi::Graphics::Element] target graphics element
84
78
  # @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
79
+ # @return [Sevgi::Graphics::Element, Array<Sevgi::Graphics::Element>, nil] included current element, included child
80
+ # elements when include_current is false, or nil when the node does not produce graphics output
87
81
  def evaluate(element, include_current = true)
88
- return element.instance_eval(derender) if include_current
82
+ return Evaluator.new(element).append(self) if include_current
89
83
 
90
- children.each { element.instance_eval(it.derender) }
84
+ evaluate_children(element)
91
85
  end
92
86
 
93
87
  # Evaluates only this node's children under a graphics element.
94
88
  # @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
97
- def evaluate!(element) = evaluate(element, false)
89
+ # @return [Array<Sevgi::Graphics::Element>] included child elements
90
+ def evaluate_children(element) = children.map { it.evaluate(element) }.compact
98
91
 
99
92
  # Finds the first descendant whose attribute matches a value.
100
93
  # @param arg [String] attribute value to find
@@ -103,22 +96,16 @@ module Sevgi
103
96
  def find(arg, by: "id")
104
97
  return self if attributes[by] == arg
105
98
 
106
- children&.each do |child|
107
- found = child.find(arg, by:)
108
-
109
- return found if found
110
- end
111
-
112
- nil
99
+ children.lazy.map { it.find(arg, by:) }.find(&:itself)
113
100
  end
114
101
 
115
102
  # Returns the source XML node name.
116
103
  # @return [String]
117
104
  def name = @name ||= node.name
118
105
 
119
- # Returns source XML namespaces.
106
+ # Returns source XML namespace declarations emitted on this node.
120
107
  # @return [Hash{String => String}] namespace declarations
121
- def namespaces = (@namespaces ||= node.namespaces.to_h { |namespace, uri| [namespace, uri] })
108
+ def namespaces = (@namespaces ||= local_namespaces)
122
109
 
123
110
  # Reports whether this node is the SVG root strategy.
124
111
  # @return [Boolean]
@@ -126,14 +113,17 @@ module Sevgi
126
113
 
127
114
  private
128
115
 
116
+ def attribute_key(attribute) = [attribute.namespace&.prefix, attribute.name].compact.join(":")
117
+
129
118
  def dispatch
130
- if node.text?
119
+ case
120
+ when node.text?
131
121
  :Text
132
- elsif node.comment?
122
+ when node.comment?
133
123
  :Junk
134
- elsif node.name == "style"
124
+ when node.name == "style"
135
125
  :CSS
136
- elsif node.name == "svg"
126
+ when node.name == "svg"
137
127
  :Root
138
128
  else
139
129
  :Any
@@ -142,7 +132,8 @@ module Sevgi
142
132
  end
143
133
 
144
134
  def ignorable_child?(child)
145
- child.type == :Junk || (child.node.text? && child.node.text.strip.empty? && !child.preserve_space?)
135
+ child.type == :Junk ||
136
+ (child.node.text? && child.node.text.strip.empty? && !child.preserve_space? && !child.inline_text?)
146
137
  end
147
138
 
148
139
  protected
@@ -160,8 +151,20 @@ module Sevgi
160
151
  false
161
152
  end
162
153
 
154
+ def inline_text? = node.text? && !node.content.match?(/[\r\n]/) && node.parent&.children&.any?(&:element?)
155
+
163
156
  private
164
157
 
158
+ def normalized_content = inline_text? ? node.content : node.content.strip
159
+
160
+ def local_namespaces
161
+ return {} unless node.respond_to?(:namespace_definitions)
162
+
163
+ node.namespace_definitions.to_h do |namespace|
164
+ [namespace.prefix ? "xmlns:#{namespace.prefix}" : "xmlns", namespace.href]
165
+ end
166
+ end
167
+
165
168
  def each_node
166
169
  current = node
167
170
 
@@ -3,6 +3,6 @@
3
3
  module Sevgi
4
4
  module Derender
5
5
  # Current version of the Sevgi derender gem.
6
- VERSION = "0.93.1"
6
+ VERSION = "0.94.0"
7
7
  end
8
8
  end
@@ -5,32 +5,98 @@ require_relative "derender/internal"
5
5
  require_relative "derender/attributes"
6
6
  require_relative "derender/document"
7
7
  require_relative "derender/elements"
8
+ require_relative "derender/evaluator"
8
9
  require_relative "derender/node"
9
10
 
10
11
  require_relative "derender/version"
11
12
 
12
13
  module Sevgi
13
14
  # Converts SVG/XML content into Sevgi DSL source or evaluates it into graphics elements.
15
+ #
16
+ # Evaluation APIs treat SVG/XML as data: they build graphics element trees directly and do not execute generated Ruby
17
+ # source. Malformed, rootless, or unmatched input is rejected with {Sevgi::ArgumentError}.
14
18
  module Derender
19
+ # @!method self.decompile(content, id: nil)
20
+ # Converts SVG/XML content into a derender node.
21
+ # @param content [String] SVG/XML source content
22
+ # @param id [String, nil] optional SVG id selecting a node inside the source
23
+ # @return [Sevgi::Derender::Node] selected node in the derender tree
24
+ # @raise [Sevgi::ArgumentError] when content is malformed or rootless, or when the id is absent
25
+ # @!method self.decompile_file(file, id: nil)
26
+ # Converts an SVG/XML file into a derender node.
27
+ # @param file [String] path to the source SVG/XML file
28
+ # @param id [String, nil] optional SVG id selecting a node inside the source
29
+ # @return [Sevgi::Derender::Node] selected node in the derender tree
30
+ # @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
31
+ # absent
32
+ # @!method self.derender(content, id: nil)
33
+ # Converts SVG/XML content into Sevgi DSL Ruby source.
34
+ # @param content [String] SVG/XML source content
35
+ # @param id [String, nil] optional SVG id selecting a node inside the source
36
+ # @return [String] formatted Sevgi DSL source
37
+ # @raise [Sevgi::ArgumentError] when content is malformed or rootless, or when the id is absent
38
+ # @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
39
+ # @!method self.derender_file(file, id: nil)
40
+ # Converts an SVG/XML file into Sevgi DSL Ruby source.
41
+ # @param file [String] path to the source SVG/XML file
42
+ # @param id [String, nil] optional SVG id selecting a node inside the source
43
+ # @return [String] formatted Sevgi DSL source
44
+ # @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
45
+ # absent
46
+ # @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
47
+ # @!method self.evaluate(content, element, id: nil)
48
+ # Evaluates SVG/XML content under a graphics element, including the selected node.
49
+ # @param content [String] SVG/XML source content
50
+ # @param element [Sevgi::Graphics::Element] target graphics element
51
+ # @param id [String, nil] optional SVG id selecting a node inside the source
52
+ # @return [Sevgi::Graphics::Element, nil] included selected/root graphics element, or nil when the selected node
53
+ # produces no graphics output
54
+ # @raise [Sevgi::ArgumentError] when content is malformed or rootless, or when the id is absent
55
+ # @!method self.evaluate_children(content, element, id: nil)
56
+ # Evaluates only the selected node's children under a graphics element.
57
+ # @param content [String] SVG/XML source content
58
+ # @param element [Sevgi::Graphics::Element] target graphics element
59
+ # @param id [String, nil] optional SVG id selecting a node inside the source
60
+ # @return [Array<Sevgi::Graphics::Element>] included child graphics elements
61
+ # @raise [Sevgi::ArgumentError] when content is malformed or rootless, or when the id is absent
62
+ # @!method self.evaluate_file(file, element, id: nil)
63
+ # Evaluates an SVG/XML file under a graphics element, including the selected node.
64
+ # @param file [String] path to the source SVG/XML file
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, nil] included selected/root graphics element, or nil when the selected node
68
+ # produces no graphics output
69
+ # @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
70
+ # absent
71
+ # @!method self.evaluate_file_children(file, element, id: nil)
72
+ # Evaluates only the selected node's children from an SVG/XML file under a graphics element.
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 [Array<Sevgi::Graphics::Element>] included child graphics elements
77
+ # @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
78
+ # absent
79
+
15
80
  # Converts SVG/XML content into a derender node.
16
81
  # @param content [String] SVG/XML source content
17
82
  # @param id [String, nil] optional SVG id selecting a node inside the source
18
83
  # @return [Sevgi::Derender::Node] selected node in the derender tree
19
- # @raise [Sevgi::ArgumentError] when the id is absent
84
+ # @raise [Sevgi::ArgumentError] when content is malformed or rootless, or when the id is absent
20
85
  def decompile(content, id: nil) = Document.new(content).decompile(id)
21
86
 
22
87
  # Converts an SVG/XML file into a derender node.
23
88
  # @param file [String] path to the source SVG/XML file
24
89
  # @param id [String, nil] optional SVG id selecting a node inside the source
25
90
  # @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
91
+ # @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
92
+ # absent
27
93
  def decompile_file(file, id: nil) = Document.load_file(file).decompile(id)
28
94
 
29
95
  # Converts SVG/XML content into Sevgi DSL Ruby source.
30
96
  # @param content [String] SVG/XML source content
31
97
  # @param id [String, nil] optional SVG id selecting a node inside the source
32
98
  # @return [String] formatted Sevgi DSL source
33
- # @raise [Sevgi::ArgumentError] when the id is absent
99
+ # @raise [Sevgi::ArgumentError] when content is malformed or rootless, or when the id is absent
34
100
  # @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
35
101
  def derender(content, id: nil) = Document.new(content).decompile(id).derender
36
102
 
@@ -38,7 +104,8 @@ module Sevgi
38
104
  # @param file [String] path to the source SVG/XML file
39
105
  # @param id [String, nil] optional SVG id selecting a node inside the source
40
106
  # @return [String] formatted Sevgi DSL source
41
- # @raise [Sevgi::ArgumentError] when the file cannot be found or the id is absent
107
+ # @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
108
+ # absent
42
109
  # @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
43
110
  def derender_file(file, id: nil) = Document.load_file(file).decompile(id).derender
44
111
 
@@ -46,37 +113,39 @@ module Sevgi
46
113
  # @param content [String] SVG/XML source content
47
114
  # @param element [Sevgi::Graphics::Element] target graphics element
48
115
  # @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
116
+ # @return [Sevgi::Graphics::Element, nil] included selected/root graphics element, or nil when the selected node
117
+ # produces no graphics output
118
+ # @raise [Sevgi::ArgumentError] when content is malformed or rootless, or when the id is absent
52
119
  def evaluate(content, element, id: nil) = Document.new(content).decompile(id).evaluate(element)
53
120
 
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
121
+ # Evaluates only the selected node's children under a graphics element.
122
+ # @param content [String] SVG/XML source content
56
123
  # @param element [Sevgi::Graphics::Element] target graphics element
57
124
  # @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
61
- def evaluate_file(file, element, id: nil) = Document.load_file(file).decompile(id).evaluate(element)
125
+ # @return [Array<Sevgi::Graphics::Element>] included child graphics elements
126
+ # @raise [Sevgi::ArgumentError] when content is malformed or rootless, or when the id is absent
127
+ def evaluate_children(content, element, id: nil) = Document.new(content).decompile(id).evaluate_children(element)
62
128
 
63
- # Evaluates SVG/XML content under a graphics element, excluding the selected node itself.
64
- # @param content [String] SVG/XML source content
129
+ # Evaluates an SVG/XML file under a graphics element, including the selected node.
130
+ # @param file [String] path to the source SVG/XML file
65
131
  # @param element [Sevgi::Graphics::Element] target graphics element
66
132
  # @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
70
- def evaluate!(content, element, id: nil) = Document.new(content).decompile(id).evaluate!(element)
133
+ # @return [Sevgi::Graphics::Element, nil] included selected/root graphics element, or nil when the selected node
134
+ # produces no graphics output
135
+ # @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
136
+ # absent
137
+ def evaluate_file(file, element, id: nil) = Document.load_file(file).decompile(id).evaluate(element)
71
138
 
72
- # Evaluates an SVG/XML file under a graphics element, excluding the selected node itself.
139
+ # Evaluates only the selected node's children from an SVG/XML file under a graphics element.
73
140
  # @param file [String] path to the source SVG/XML file
74
141
  # @param element [Sevgi::Graphics::Element] target graphics element
75
142
  # @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
79
- def evaluate_file!(file, element, id: nil) = Document.load_file(file).decompile(id).evaluate!(element)
143
+ # @return [Array<Sevgi::Graphics::Element>] included child graphics elements
144
+ # @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
145
+ # absent
146
+ def evaluate_file_children(file, element, id: nil)
147
+ Document.load_file(file).decompile(id).evaluate_children(element)
148
+ end
80
149
 
81
150
  extend self
82
151
  end
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.93.1
4
+ version: 0.94.0
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.93.1
18
+ version: 0.94.0
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.93.1
25
+ version: 0.94.0
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.93.1
32
+ version: 0.94.0
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.93.1
39
+ version: 0.94.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: css_parser
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -86,9 +86,10 @@ executables:
86
86
  extensions: []
87
87
  extra_rdoc_files: []
88
88
  files:
89
+ - CHANGELOG.md
90
+ - LICENSE
89
91
  - README.md
90
92
  - bin/igves
91
- - lib/sevgi/binaries/igsev.rb
92
93
  - lib/sevgi/binaries/igves.rb
93
94
  - lib/sevgi/derender.rb
94
95
  - lib/sevgi/derender/attributes.rb
@@ -99,6 +100,7 @@ files:
99
100
  - lib/sevgi/derender/elements/junk.rb
100
101
  - lib/sevgi/derender/elements/root.rb
101
102
  - lib/sevgi/derender/elements/text.rb
103
+ - lib/sevgi/derender/evaluator.rb
102
104
  - lib/sevgi/derender/internal.rb
103
105
  - lib/sevgi/derender/node.rb
104
106
  - lib/sevgi/derender/version.rb
@@ -117,7 +119,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
117
119
  requirements:
118
120
  - - ">="
119
121
  - !ruby/object:Gem::Version
120
- version: 3.4.0.pre.preview1
122
+ version: 3.4.0
121
123
  required_rubygems_version: !ruby/object:Gem::Requirement
122
124
  requirements:
123
125
  - - ">="
@@ -1,114 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "sevgi"
4
-
5
- module Sevgi
6
- module Binaries
7
- # Implements the `igsev` executable that derenders SVG and executes the generated Sevgi DSL.
8
- module Igsev
9
- extend self
10
-
11
- # Executable name used in help output.
12
- PROGNAME = "igsev"
13
-
14
- # Error raised for invalid command-line usage.
15
- Error = Class.new(::Sevgi::Error)
16
-
17
- # Parsed command-line options for the `igsev` executable.
18
- # @api private
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
24
- def self.parse(argv)
25
- new.tap do |options|
26
- argv.first.start_with?("-") ? option(argv, options) : break until argv.empty?
27
- end
28
- end
29
-
30
- class << self
31
- private
32
-
33
- def option(argv, options)
34
- case (arg = argv.shift)
35
- when "-r", "--require"
36
- options.require = argv.shift
37
- when "-x", "--exception"
38
- options.vomit = true
39
-
40
- when "-h", "--help"
41
- options.help = true
42
- when "-v", "--version"
43
- options.version = true
44
- else
45
- Error.("Not a valid option: #{arg}")
46
- end
47
- end
48
- end
49
- end
50
-
51
- private_constant :Options
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
61
- def call(argv)
62
- return puts(help) if (options = Options.parse(argv = Array(argv))).help
63
- return puts(::Sevgi::VERSION) if options.version
64
-
65
- result = run(file = argv.shift, options)
66
-
67
- if result.error?
68
- raise result.error if options.vomit || ENV[ENVVOMIT]
69
-
70
- die(result.error, file)
71
- else
72
- result.recent.Out()
73
- end
74
-
75
- rescue Binaries::Igsev::Error => e
76
- abort(e.message)
77
- end
78
-
79
- private
80
-
81
- def die(error, _file)
82
- warn(error.message)
83
- warn("")
84
- error.backtrace!.each { warn(" #{it}") }
85
-
86
- exit(1)
87
- end
88
-
89
- def help
90
- <<~HELP
91
- Usage: #{PROGNAME} [options...] <SVG file>
92
-
93
- See documentation for detailed help.
94
-
95
- Options:
96
-
97
- -r, --require LIB Require Ruby LIB
98
- -x, --exception Raise exception instead of abort
99
-
100
- -h, --help Show this help
101
- -v, --version Display version
102
- HELP
103
- end
104
-
105
- def run(file, options)
106
- Error.("No SVG file given.") unless file
107
-
108
- sevgi = Derender.derender_file(file)
109
-
110
- ::Sevgi.execute(sevgi, require: options.require)
111
- end
112
- end
113
- end
114
- end