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 +4 -4
- data/README.md +5 -0
- data/lib/sevgi/binaries/igsev.rb +17 -0
- data/lib/sevgi/binaries/igves.rb +54 -5
- data/lib/sevgi/derender/attributes.rb +10 -1
- data/lib/sevgi/derender/document.rb +48 -4
- 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/internal.rb +32 -3
- data/lib/sevgi/derender/node.rb +89 -4
- data/lib/sevgi/derender/version.rb +2 -1
- data/lib/sevgi/derender.rb +55 -4
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 126f64b403fbdff8cea203ad63c4857bf45254f2c513c9058db6803d8137414c
|
|
4
|
+
data.tar.gz: c531615d9b46226cf9bf35f1c828f18e678f6e148438b880337615b38da4301b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8aea4ef742bba26cfd22a502fb1f761b476dd4dfb2bcd0573120e121c0853e9892abedd5f70d51203869b5052a5df2017fb285d448c8029dd1ab308a23b5ec67
|
|
7
|
+
data.tar.gz: 9447d2fc113a2eb97475ea2f2fe618e9e293573097908e7ea264fd95b10124ce8edce72ee5678e802d016fa26459e5bfea63ca6ea559b2522876f9e66ecd077f
|
data/README.md
CHANGED
data/lib/sevgi/binaries/igsev.rb
CHANGED
|
@@ -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
|
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) }
|
|
@@ -38,7 +47,7 @@ module Sevgi
|
|
|
38
47
|
value
|
|
39
48
|
end => value
|
|
40
49
|
|
|
41
|
-
key.match?(/\W/) ? "
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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"
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
data/lib/sevgi/derender/node.rb
CHANGED
|
@@ -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
|
|
44
|
-
|
|
45
|
-
|
|
63
|
+
@children ||= node
|
|
64
|
+
.children
|
|
65
|
+
.map { |child| self.class.new(child) }
|
|
66
|
+
.reject { |child| ignorable_child?(child) }
|
|
46
67
|
end
|
|
47
68
|
|
|
48
|
-
|
|
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
|
data/lib/sevgi/derender.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
39
|
+
version: 0.93.1
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: css_parser
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|