sevgi-derender 0.73.2 → 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 +4 -4
- data/CHANGELOG.md +4 -0
- data/LICENSE +5 -0
- data/README.md +44 -0
- data/lib/sevgi/binaries/igves.rb +54 -5
- data/lib/sevgi/derender/attributes.rb +12 -2
- data/lib/sevgi/derender/document.rb +77 -18
- data/lib/sevgi/derender/elements/any.rb +9 -5
- data/lib/sevgi/derender/elements/css.rb +5 -2
- data/lib/sevgi/derender/elements/junk.rb +4 -1
- data/lib/sevgi/derender/elements/root.rb +10 -1
- data/lib/sevgi/derender/elements/text.rb +5 -2
- data/lib/sevgi/derender/elements.rb +9 -0
- data/lib/sevgi/derender/evaluator.rb +52 -0
- data/lib/sevgi/derender/internal.rb +37 -4
- data/lib/sevgi/derender/node.rb +122 -34
- data/lib/sevgi/derender/version.rb +2 -1
- data/lib/sevgi/derender.rb +126 -6
- metadata +9 -7
- data/lib/sevgi/binaries/igsev.rb +0 -97
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9f1b7265f14be33da5b43f634ac82059a6d438e723ecc2153a062452ea1610fc
|
|
4
|
+
data.tar.gz: 5c52f4aef783cd3b3c712f19883ebbcab9c28efce3802318fe1b1f55b2a7fd88
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 98d494547c8854554dc9f791c4abd6ef1e99eb726face62c03b49756a0694be0267d311b103ac35a3797ad52dcc60cacf770c270569babcf5c4a6e9ee1bb0930
|
|
7
|
+
data.tar.gz: 3b127ab458de366d81b7efc0fb8b4ba94d6ce26cf2c9761389e4375aea02a079aaaa0c8447841416e45857674d9daee4494ffb1c28e2bca30b8944ba3addeeb3
|
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
data/README.md
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Sevgi Derender
|
|
2
|
+
|
|
3
|
+
Converts SVG/XML content back into Sevgi DSL source.
|
|
4
|
+
|
|
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
|
data/lib/sevgi/binaries/igves.rb
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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) }
|
|
@@ -29,7 +38,8 @@ module Sevgi
|
|
|
29
38
|
key = Css.to_key(key) if key.is_a?(::String)
|
|
30
39
|
|
|
31
40
|
if key == "style"
|
|
32
|
-
|
|
41
|
+
style = Css.to_h!(value)
|
|
42
|
+
style.empty? ? "{}" : "{ #{Attributes.decompile(style)} }"
|
|
33
43
|
elsif value.is_a?(::String)
|
|
34
44
|
Css.to_value(value)
|
|
35
45
|
elsif value.is_a?(::Hash)
|
|
@@ -38,7 +48,7 @@ module Sevgi
|
|
|
38
48
|
value
|
|
39
49
|
end => value
|
|
40
50
|
|
|
41
|
-
key.match?(/\W/) ? "
|
|
51
|
+
key.match?(/\W/) ? "#{Ruby.literal(key)}: #{value}" : "#{key}: #{value}"
|
|
42
52
|
end
|
|
43
53
|
.join(", ")
|
|
44
54
|
end
|
|
@@ -4,46 +4,74 @@ 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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
13
|
+
# @param path [String] path to the source file, with or without `.svg` extension
|
|
14
|
+
# @return [Sevgi::Derender::Document] document wrapper
|
|
15
|
+
# @raise [Sevgi::ArgumentError] when the file cannot be found or file content is malformed XML
|
|
16
|
+
# @raise [Errno::EACCES] when the file cannot be read
|
|
14
17
|
def self.load_file(path)
|
|
15
18
|
entry = ::File.expand_path(F.qualify(path, "svg"))
|
|
16
19
|
|
|
17
20
|
ArgumentError.("File not found: #{path}") unless ::File.exist?(entry)
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
begin
|
|
22
|
-
self.class.cache[entry] = self.class.parse(::File.read(entry))
|
|
23
|
-
end
|
|
24
|
-
end
|
|
22
|
+
content = ::File.read(entry)
|
|
23
|
+
new(content)
|
|
25
24
|
end
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
# Parses SVG/XML content in strict XML mode.
|
|
27
|
+
# @param content [String] SVG/XML source content
|
|
28
|
+
# @return [Nokogiri::XML::Document] parsed XML document
|
|
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
|
|
28
35
|
|
|
36
|
+
rescue Nokogiri::XML::SyntaxError => e
|
|
37
|
+
raise ArgumentError, "Malformed XML: #{e.message.lines.first.strip}", cause: e
|
|
38
|
+
end
|
|
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
|
-
return unless (content = content.lstrip).start_with?("<?xml ")
|
|
44
|
+
return unless (content = content.to_s.lstrip).start_with?("<?xml ")
|
|
31
45
|
|
|
32
|
-
content
|
|
46
|
+
content[/\A<\?xml\b.*?\?>/m]
|
|
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]
|
|
60
|
+
# @raise [Sevgi::ArgumentError] when content is malformed XML
|
|
37
61
|
def initialize(content, &block)
|
|
38
62
|
instance_exec(&block) if block
|
|
39
63
|
|
|
40
|
-
@doc
|
|
64
|
+
@doc ||= self.class.parse(content)
|
|
41
65
|
@decl = self.class.declaration(content)
|
|
42
66
|
end
|
|
43
67
|
|
|
68
|
+
# Converts the root or selected node into a derender node.
|
|
69
|
+
# @param id [String, nil] optional SVG id selecting a node inside the document
|
|
70
|
+
# @return [Sevgi::Derender::Node] selected node in the derender tree
|
|
71
|
+
# @raise [Sevgi::ArgumentError] when the document has no root element or the id is absent
|
|
44
72
|
def decompile(id = nil)
|
|
45
73
|
if id
|
|
46
|
-
if (found = doc.xpath("//*[@id
|
|
74
|
+
if (found = doc.xpath("//*[@id=#{xpath_literal(id)}]") || []).empty?
|
|
47
75
|
ArgumentError.("No such element with id '#{id}' in document")
|
|
48
76
|
end
|
|
49
77
|
|
|
@@ -52,15 +80,46 @@ module Sevgi
|
|
|
52
80
|
doc.root
|
|
53
81
|
end => element
|
|
54
82
|
|
|
55
|
-
|
|
83
|
+
ArgumentError.("XML document has no root element") unless element
|
|
84
|
+
|
|
85
|
+
Node.new(element, pres, namespaces: namespace_scope(element))
|
|
56
86
|
end
|
|
57
87
|
|
|
88
|
+
# Returns XML declaration and pre-root nodes preserved for root decompilation.
|
|
89
|
+
# @return [Array<String>] preamble XML lines
|
|
58
90
|
def pres
|
|
59
91
|
@pres ||= [].tap do |lines|
|
|
60
92
|
lines.append(*doc.children.take_while { |node| node != doc.root }.map(&:to_xml))
|
|
61
93
|
lines.unshift(decl) unless lines.first == decl
|
|
62
94
|
end
|
|
63
95
|
end
|
|
96
|
+
|
|
97
|
+
private
|
|
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
|
+
|
|
111
|
+
def xpath_literal(value)
|
|
112
|
+
value = value.to_s
|
|
113
|
+
|
|
114
|
+
return "'#{value}'" unless value.include?("'")
|
|
115
|
+
return "\"#{value}\"" unless value.include?("\"")
|
|
116
|
+
|
|
117
|
+
parts = value.split("'", -1).flat_map.with_index do |part, index|
|
|
118
|
+
index.zero? ? [part] : ["'", part]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
"concat(#{parts.map { |part| part == "'" ? "\"'\"" : "'#{part}'" }.join(", ")})"
|
|
122
|
+
end
|
|
64
123
|
end
|
|
65
124
|
end
|
|
66
125
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
"
|
|
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 { "
|
|
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
|
-
|
|
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"
|
|
@@ -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
|
|
@@ -4,37 +4,70 @@ 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
|
|
|
16
|
-
|
|
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
|
|
24
|
+
def to_h!(style_string)
|
|
25
|
+
hash = to_h("* { #{style_string} }")
|
|
17
26
|
|
|
27
|
+
hash ? hash.fetch("*", {}) : {}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Converts a CSS key into a Ruby hash key.
|
|
31
|
+
# @param arg [String] CSS key
|
|
32
|
+
# @return [String] Ruby hash key source
|
|
18
33
|
def to_key(arg) = arg
|
|
19
34
|
|
|
20
|
-
|
|
35
|
+
# Converts a CSS key/value pair into Ruby hash source.
|
|
36
|
+
# @param key [String] CSS declaration key
|
|
37
|
+
# @param value [String] CSS declaration value
|
|
38
|
+
# @return [String] Ruby hash pair source
|
|
39
|
+
def to_key_value(key, value) = "#{Ruby.literal(to_key(key))}: #{to_value(value)}"
|
|
21
40
|
|
|
22
|
-
|
|
41
|
+
# Converts a CSS value into Ruby source.
|
|
42
|
+
# @param arg [String] CSS value
|
|
43
|
+
# @return [String] Ruby literal or numeric source
|
|
44
|
+
def to_value(arg) = (arg.to_f.to_s == arg) || (arg.to_i.to_s == arg) ? arg : arg.inspect
|
|
23
45
|
|
|
24
46
|
extend self
|
|
25
47
|
end
|
|
26
48
|
|
|
27
49
|
private_constant :Css
|
|
28
50
|
|
|
51
|
+
# Ruby source formatting helpers used by the derender pipeline.
|
|
52
|
+
# @api private
|
|
29
53
|
module Ruby
|
|
30
54
|
require "rufo"
|
|
31
55
|
|
|
56
|
+
# Formats generated Ruby source.
|
|
57
|
+
# @param unformatted_ruby [String] unformatted Ruby source
|
|
58
|
+
# @return [String] formatted Ruby source
|
|
59
|
+
# @raise [Sevgi::PanicError] when source cannot be parsed by the formatter
|
|
32
60
|
def format(unformatted_ruby)
|
|
33
61
|
Rufo::Formatter.format(unformatted_ruby)
|
|
34
62
|
rescue Rufo::SyntaxError
|
|
35
|
-
|
|
63
|
+
PanicError.(unformatted_ruby)
|
|
36
64
|
end
|
|
37
65
|
|
|
66
|
+
# Converts a value into a Ruby string literal.
|
|
67
|
+
# @param value [Object] value to stringify
|
|
68
|
+
# @return [String] Ruby string literal source
|
|
69
|
+
def literal(value) = value.to_s.inspect
|
|
70
|
+
|
|
38
71
|
extend self
|
|
39
72
|
end
|
|
40
73
|
|
data/lib/sevgi/derender/node.rb
CHANGED
|
@@ -2,17 +2,34 @@
|
|
|
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
|
|
|
8
|
-
|
|
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
|
+
# @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
|
|
20
|
+
# @return [void]
|
|
21
|
+
def initialize(node, pres = [], namespaces: nil)
|
|
9
22
|
@node = node
|
|
10
23
|
@pres = pres
|
|
24
|
+
@namespaces = namespaces
|
|
11
25
|
@type = dispatch
|
|
12
26
|
end
|
|
13
27
|
|
|
28
|
+
# Attribute namespace prefix used for Sevgi metadata.
|
|
14
29
|
META_NAMESPACE = "_:"
|
|
15
30
|
|
|
31
|
+
# Returns Sevgi metadata attributes without the metadata namespace prefix.
|
|
32
|
+
# @return [Hash{String => String}] metadata attributes
|
|
16
33
|
def _
|
|
17
34
|
@_ ||= attributes
|
|
18
35
|
.slice(
|
|
@@ -23,76 +40,147 @@ module Sevgi
|
|
|
23
40
|
|
|
24
41
|
alias meta _
|
|
25
42
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
43
|
+
# Returns source XML attributes keyed with namespace prefixes when present.
|
|
44
|
+
# @return [Hash{String => String}] XML attributes
|
|
45
|
+
def attributes = @attributes ||= node.attribute_nodes.to_h { [attribute_key(it), it.value] }
|
|
29
46
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
name
|
|
34
|
-
end => key
|
|
35
|
-
|
|
36
|
-
[key, value]
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def attributes! = attributes
|
|
47
|
+
# Returns source XML attributes and namespace declarations emitted on this node.
|
|
48
|
+
# @return [Hash{String => String}] XML attributes
|
|
49
|
+
def attributes! = {**attributes, **namespaces}
|
|
41
50
|
|
|
51
|
+
# Returns non-ignorable child derender nodes.
|
|
52
|
+
# @return [Array<Sevgi::Derender::Node>] child nodes
|
|
42
53
|
def children
|
|
43
|
-
@children ||= node
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
@children ||= node
|
|
55
|
+
.children
|
|
56
|
+
.map { |child| self.class.new(child) }
|
|
57
|
+
.reject { |child| ignorable_child?(child) }
|
|
46
58
|
end
|
|
47
59
|
|
|
48
|
-
|
|
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.
|
|
64
|
+
# @return [String]
|
|
65
|
+
def content = @content ||= preserve_space? ? node.content : normalized_content
|
|
49
66
|
|
|
67
|
+
# Converts this node into formatted Sevgi DSL Ruby source.
|
|
68
|
+
# @return [String] formatted Sevgi DSL source
|
|
69
|
+
# @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
|
|
50
70
|
def derender = Ruby.format(decompile(pres).join("\n"))
|
|
51
71
|
|
|
72
|
+
# Returns the Sevgi DSL element name for this node.
|
|
73
|
+
# @return [String]
|
|
52
74
|
def element = name
|
|
53
75
|
|
|
76
|
+
# Evaluates this node under a graphics element.
|
|
77
|
+
# @param element [Sevgi::Graphics::Element] target graphics element
|
|
78
|
+
# @param include_current [Boolean] true to evaluate this node, false to evaluate only children
|
|
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
|
|
54
81
|
def evaluate(element, include_current = true)
|
|
55
|
-
return element.
|
|
82
|
+
return Evaluator.new(element).append(self) if include_current
|
|
56
83
|
|
|
57
|
-
|
|
84
|
+
evaluate_children(element)
|
|
58
85
|
end
|
|
59
86
|
|
|
60
|
-
|
|
87
|
+
# Evaluates only this node's children under a graphics element.
|
|
88
|
+
# @param element [Sevgi::Graphics::Element] target graphics element
|
|
89
|
+
# @return [Array<Sevgi::Graphics::Element>] included child elements
|
|
90
|
+
def evaluate_children(element) = children.map { it.evaluate(element) }.compact
|
|
61
91
|
|
|
92
|
+
# Finds the first descendant whose attribute matches a value.
|
|
93
|
+
# @param arg [String] attribute value to find
|
|
94
|
+
# @param by [String] attribute name used for lookup
|
|
95
|
+
# @return [Sevgi::Derender::Node, nil] matching node, or nil
|
|
62
96
|
def find(arg, by: "id")
|
|
63
97
|
return self if attributes[by] == arg
|
|
64
98
|
|
|
65
|
-
children
|
|
66
|
-
found = child.find(arg, by:)
|
|
67
|
-
|
|
68
|
-
return found if found
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
nil
|
|
99
|
+
children.lazy.map { it.find(arg, by:) }.find(&:itself)
|
|
72
100
|
end
|
|
73
101
|
|
|
102
|
+
# Returns the source XML node name.
|
|
103
|
+
# @return [String]
|
|
74
104
|
def name = @name ||= node.name
|
|
75
105
|
|
|
76
|
-
|
|
106
|
+
# Returns source XML namespace declarations emitted on this node.
|
|
107
|
+
# @return [Hash{String => String}] namespace declarations
|
|
108
|
+
def namespaces = (@namespaces ||= local_namespaces)
|
|
77
109
|
|
|
110
|
+
# Reports whether this node is the SVG root strategy.
|
|
111
|
+
# @return [Boolean]
|
|
78
112
|
def root? = type == :Root
|
|
79
113
|
|
|
80
114
|
private
|
|
81
115
|
|
|
116
|
+
def attribute_key(attribute) = [attribute.namespace&.prefix, attribute.name].compact.join(":")
|
|
117
|
+
|
|
82
118
|
def dispatch
|
|
83
|
-
|
|
119
|
+
case
|
|
120
|
+
when node.text?
|
|
84
121
|
:Text
|
|
85
|
-
|
|
122
|
+
when node.comment?
|
|
86
123
|
:Junk
|
|
87
|
-
|
|
124
|
+
when node.name == "style"
|
|
88
125
|
:CSS
|
|
89
|
-
|
|
126
|
+
when node.name == "svg"
|
|
90
127
|
:Root
|
|
91
128
|
else
|
|
92
129
|
:Any
|
|
93
130
|
end
|
|
94
131
|
.tap { extend(Elements.const_get(it)) }
|
|
95
132
|
end
|
|
133
|
+
|
|
134
|
+
def ignorable_child?(child)
|
|
135
|
+
child.type == :Junk ||
|
|
136
|
+
(child.node.text? && child.node.text.strip.empty? && !child.preserve_space? && !child.inline_text?)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
protected
|
|
140
|
+
|
|
141
|
+
def preserve_space?
|
|
142
|
+
each_node do |current|
|
|
143
|
+
case xml_space(current)
|
|
144
|
+
when "preserve"
|
|
145
|
+
return true
|
|
146
|
+
when "default"
|
|
147
|
+
return false
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
false
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def inline_text? = node.text? && !node.content.match?(/[\r\n]/) && node.parent&.children&.any?(&:element?)
|
|
155
|
+
|
|
156
|
+
private
|
|
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
|
+
|
|
168
|
+
def each_node
|
|
169
|
+
current = node
|
|
170
|
+
|
|
171
|
+
while current
|
|
172
|
+
yield current
|
|
173
|
+
current = current.respond_to?(:parent) ? current.parent : nil
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def xml_space(current)
|
|
178
|
+
return unless current.respond_to?(:attribute_nodes)
|
|
179
|
+
|
|
180
|
+
current.attribute_nodes.find { xml_space_attribute?(it) }&.value
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def xml_space_attribute?(attribute) = attribute.name == "space" && attribute.namespace&.prefix == "xml"
|
|
96
184
|
end
|
|
97
185
|
end
|
|
98
186
|
end
|
data/lib/sevgi/derender.rb
CHANGED
|
@@ -5,27 +5,147 @@ 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
|
|
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}.
|
|
13
18
|
module Derender
|
|
14
|
-
#
|
|
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
|
+
|
|
80
|
+
# Converts SVG/XML content into a derender node.
|
|
81
|
+
# @param content [String] SVG/XML source content
|
|
82
|
+
# @param id [String, nil] optional SVG id selecting a node inside the source
|
|
83
|
+
# @return [Sevgi::Derender::Node] selected node in the derender tree
|
|
84
|
+
# @raise [Sevgi::ArgumentError] when content is malformed or rootless, or when the id is absent
|
|
15
85
|
def decompile(content, id: nil) = Document.new(content).decompile(id)
|
|
86
|
+
|
|
87
|
+
# Converts an SVG/XML file into a derender node.
|
|
88
|
+
# @param file [String] path to the source SVG/XML file
|
|
89
|
+
# @param id [String, nil] optional SVG id selecting a node inside the source
|
|
90
|
+
# @return [Sevgi::Derender::Node] selected node in the derender tree
|
|
91
|
+
# @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
|
|
92
|
+
# absent
|
|
16
93
|
def decompile_file(file, id: nil) = Document.load_file(file).decompile(id)
|
|
17
94
|
|
|
18
|
-
#
|
|
95
|
+
# Converts SVG/XML content into Sevgi DSL Ruby source.
|
|
96
|
+
# @param content [String] SVG/XML source content
|
|
97
|
+
# @param id [String, nil] optional SVG id selecting a node inside the source
|
|
98
|
+
# @return [String] formatted Sevgi DSL source
|
|
99
|
+
# @raise [Sevgi::ArgumentError] when content is malformed or rootless, or when the id is absent
|
|
100
|
+
# @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
|
|
19
101
|
def derender(content, id: nil) = Document.new(content).decompile(id).derender
|
|
102
|
+
|
|
103
|
+
# Converts an SVG/XML file into Sevgi DSL Ruby source.
|
|
104
|
+
# @param file [String] path to the source SVG/XML file
|
|
105
|
+
# @param id [String, nil] optional SVG id selecting a node inside the source
|
|
106
|
+
# @return [String] formatted Sevgi DSL source
|
|
107
|
+
# @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
|
|
108
|
+
# absent
|
|
109
|
+
# @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
|
|
20
110
|
def derender_file(file, id: nil) = Document.load_file(file).decompile(id).derender
|
|
21
111
|
|
|
22
|
-
#
|
|
112
|
+
# Evaluates SVG/XML content under a graphics element, including the selected node.
|
|
113
|
+
# @param content [String] SVG/XML source content
|
|
114
|
+
# @param element [Sevgi::Graphics::Element] target graphics element
|
|
115
|
+
# @param id [String, nil] optional SVG id selecting a node inside the source
|
|
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
|
|
23
119
|
def evaluate(content, element, id: nil) = Document.new(content).decompile(id).evaluate(element)
|
|
120
|
+
|
|
121
|
+
# Evaluates only the selected node's children under a graphics element.
|
|
122
|
+
# @param content [String] SVG/XML source content
|
|
123
|
+
# @param element [Sevgi::Graphics::Element] target graphics element
|
|
124
|
+
# @param id [String, nil] optional SVG id selecting a node inside the source
|
|
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)
|
|
128
|
+
|
|
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
|
|
131
|
+
# @param element [Sevgi::Graphics::Element] target graphics element
|
|
132
|
+
# @param id [String, nil] optional SVG id selecting a node inside the source
|
|
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
|
|
24
137
|
def evaluate_file(file, element, id: nil) = Document.load_file(file).decompile(id).evaluate(element)
|
|
25
138
|
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
|
|
139
|
+
# Evaluates only the selected node's children from an SVG/XML file under a graphics element.
|
|
140
|
+
# @param file [String] path to the source SVG/XML file
|
|
141
|
+
# @param element [Sevgi::Graphics::Element] target graphics element
|
|
142
|
+
# @param id [String, nil] optional SVG id selecting a node inside the source
|
|
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
|
|
29
149
|
|
|
30
150
|
extend self
|
|
31
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
122
|
+
version: 3.4.0
|
|
121
123
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
122
124
|
requirements:
|
|
123
125
|
- - ">="
|
data/lib/sevgi/binaries/igsev.rb
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "sevgi"
|
|
4
|
-
|
|
5
|
-
module Sevgi
|
|
6
|
-
module Binaries
|
|
7
|
-
module Igsev
|
|
8
|
-
extend self
|
|
9
|
-
|
|
10
|
-
PROGNAME = "igsev"
|
|
11
|
-
|
|
12
|
-
Error = Class.new(::Sevgi::Error)
|
|
13
|
-
|
|
14
|
-
Options = Struct.new(:require, :vomit, :help, :version) do
|
|
15
|
-
def self.parse(argv)
|
|
16
|
-
new.tap do |options|
|
|
17
|
-
argv.first.start_with?("-") ? option(argv, options) : break until argv.empty?
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
class << self
|
|
22
|
-
private
|
|
23
|
-
|
|
24
|
-
def option(argv, options)
|
|
25
|
-
case (arg = argv.shift)
|
|
26
|
-
when "-r", "--require"
|
|
27
|
-
options.require = argv.shift
|
|
28
|
-
when "-x", "--exception"
|
|
29
|
-
options.vomit = true
|
|
30
|
-
|
|
31
|
-
when "-h", "--help"
|
|
32
|
-
options.help = true
|
|
33
|
-
when "-v", "--version"
|
|
34
|
-
options.version = true
|
|
35
|
-
else
|
|
36
|
-
Error.("Not a valid option: #{arg}")
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
private_constant :Options
|
|
43
|
-
|
|
44
|
-
def call(argv)
|
|
45
|
-
return puts(help) if (options = Options.parse(argv = Array(argv))).help
|
|
46
|
-
return puts(::Sevgi::VERSION) if options.version
|
|
47
|
-
|
|
48
|
-
result = run(file = argv.shift, options)
|
|
49
|
-
|
|
50
|
-
if result.error?
|
|
51
|
-
raise result.error if options.vomit || ENV[ENVVOMIT]
|
|
52
|
-
|
|
53
|
-
die(result.error, file)
|
|
54
|
-
else
|
|
55
|
-
result.recent.Out()
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
rescue Binaries::Igsev::Error => e
|
|
59
|
-
abort(e.message)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
private
|
|
63
|
-
|
|
64
|
-
def die(error, _file)
|
|
65
|
-
warn(error.message)
|
|
66
|
-
warn("")
|
|
67
|
-
error.backtrace!.each { warn(" #{it}") }
|
|
68
|
-
|
|
69
|
-
exit(1)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def help
|
|
73
|
-
<<~HELP
|
|
74
|
-
Usage: #{PROGNAME} [options...] <SVG file>
|
|
75
|
-
|
|
76
|
-
See documentation for detailed help.
|
|
77
|
-
|
|
78
|
-
Options:
|
|
79
|
-
|
|
80
|
-
-r, --require LIB Require Ruby LIB
|
|
81
|
-
-x, --exception Raise exception instead of abort
|
|
82
|
-
|
|
83
|
-
-h, --help Show this help
|
|
84
|
-
-v, --version Display version
|
|
85
|
-
HELP
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def run(file, options)
|
|
89
|
-
Error.("No SVG file given.") unless file
|
|
90
|
-
|
|
91
|
-
sevgi = Derender.derender_file(file)
|
|
92
|
-
|
|
93
|
-
::Sevgi.execute(sevgi, require: options.require)
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|