sevgi-graphics 0.94.0 → 0.95.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 +150 -2
- data/LICENSE +672 -3
- data/README.md +1 -1
- data/lib/sevgi/graphics/attribute.rb +132 -38
- data/lib/sevgi/graphics/auxiliary/canvas.rb +38 -16
- data/lib/sevgi/graphics/auxiliary/content.rb +152 -29
- data/lib/sevgi/graphics/auxiliary/margin.rb +14 -2
- data/lib/sevgi/graphics/auxiliary/paper.rb +91 -84
- data/lib/sevgi/graphics/auxiliary/scalar.rb +40 -0
- data/lib/sevgi/graphics/auxiliary/sizes.rb +62 -0
- data/lib/sevgi/graphics/auxiliary.rb +2 -0
- data/lib/sevgi/graphics/document.rb +186 -74
- data/lib/sevgi/graphics/element.rb +41 -14
- data/lib/sevgi/graphics/mixtures/core.rb +89 -18
- data/lib/sevgi/graphics/mixtures/duplicate.rb +33 -13
- data/lib/sevgi/graphics/mixtures/export.rb +6 -0
- data/lib/sevgi/graphics/mixtures/include.rb +17 -0
- data/lib/sevgi/graphics/mixtures/inkscape.rb +15 -0
- data/lib/sevgi/graphics/mixtures/rdf.rb +20 -0
- data/lib/sevgi/graphics/mixtures/render.rb +56 -11
- data/lib/sevgi/graphics/mixtures/save.rb +9 -0
- data/lib/sevgi/graphics/mixtures/symbols.rb +3 -0
- data/lib/sevgi/graphics/mixtures/tile.rb +6 -0
- data/lib/sevgi/graphics/mixtures/underscore.rb +2 -2
- data/lib/sevgi/graphics/mixtures/validate.rb +12 -0
- data/lib/sevgi/graphics/mixtures/wrappers.rb +4 -1
- data/lib/sevgi/graphics/mixtures.rb +4 -0
- data/lib/sevgi/graphics/version.rb +1 -1
- data/lib/sevgi/graphics/xml.rb +127 -0
- data/lib/sevgi/graphics.rb +19 -18
- metadata +7 -4
|
@@ -53,20 +53,16 @@ module Sevgi
|
|
|
53
53
|
# @return [Sevgi::Graphics::Element] self
|
|
54
54
|
# @raise [Sevgi::ArgumentError] when the target parent has a different element class
|
|
55
55
|
# @raise [Sevgi::ArgumentError] when the target parent is this element or one of its descendants
|
|
56
|
+
# @raise [Sevgi::ArgumentError] when index is not an Integer insertion position
|
|
56
57
|
def Adopt(new_parent = nil, index: -1)
|
|
57
58
|
tap do
|
|
58
|
-
|
|
59
|
-
unless instance_of?(new_parent.class)
|
|
60
|
-
ArgumentError.("Element type does not match the new parent type: #{self.class}")
|
|
61
|
-
end
|
|
62
|
-
else
|
|
63
|
-
new_parent = parent
|
|
64
|
-
end
|
|
65
|
-
|
|
59
|
+
new_parent ||= parent
|
|
66
60
|
Adoption.validate(self, new_parent)
|
|
67
61
|
|
|
62
|
+
insertion = Adoption.index_for(self, new_parent, index)
|
|
63
|
+
|
|
68
64
|
self.Orphan()
|
|
69
|
-
(@parent = new_parent).children.insert(
|
|
65
|
+
(@parent = new_parent).children.insert(insertion, self)
|
|
70
66
|
end
|
|
71
67
|
end
|
|
72
68
|
|
|
@@ -80,11 +76,13 @@ module Sevgi
|
|
|
80
76
|
Adopt(*, index: 0)
|
|
81
77
|
end
|
|
82
78
|
|
|
83
|
-
# Appends existing elements as children.
|
|
84
|
-
#
|
|
79
|
+
# Appends distinct existing elements as children in argument order.
|
|
80
|
+
# Each element transfers from its current parent. The complete batch is validated before any element moves.
|
|
81
|
+
# @param elements [Array<Sevgi::Graphics::Element>] distinct elements to append
|
|
85
82
|
# @return [Sevgi::Graphics::Element] self
|
|
83
|
+
# @raise [Sevgi::ArgumentError] when an argument has a different element class, is repeated, or is this target or its ancestor
|
|
86
84
|
def Append(*elements)
|
|
87
|
-
tap { elements
|
|
85
|
+
tap { Adoption.batch(elements, self, front: false) }
|
|
88
86
|
end
|
|
89
87
|
|
|
90
88
|
# Adds CSS classes without duplicating existing values.
|
|
@@ -124,9 +122,12 @@ module Sevgi
|
|
|
124
122
|
# @param tag [Symbol, String] SVG tag name
|
|
125
123
|
# @param contents [Array<Object>] text or content objects; non-content objects are stringified and XML-encoded
|
|
126
124
|
# @param attributes [Hash] SVG attributes
|
|
125
|
+
# @yield evaluates the drawing DSL in the new child element
|
|
126
|
+
# @yieldreturn [Object] ignored block result
|
|
127
127
|
# @return [Sevgi::Graphics::Element] new child element
|
|
128
|
+
# @raise [Sevgi::ArgumentError] when the tag, attributes, or content are not valid XML
|
|
128
129
|
def Element(tag, *contents, **attributes, &block)
|
|
129
|
-
self.class.send(:new, tag
|
|
130
|
+
self.class.send(:new, tag, contents: Content.contents(*contents), attributes:, parent: self, &block)
|
|
130
131
|
end
|
|
131
132
|
|
|
132
133
|
# Forwards this element as the first argument to another receiver.
|
|
@@ -148,16 +149,18 @@ module Sevgi
|
|
|
148
149
|
end
|
|
149
150
|
|
|
150
151
|
# Removes this element from its parent.
|
|
151
|
-
# @return [
|
|
152
|
+
# @return [Sevgi::Graphics::Element, nil] the deleted element, or nil for root elements
|
|
152
153
|
def Orphan
|
|
153
154
|
parent.children&.delete(self) unless Root?()
|
|
154
155
|
end
|
|
155
156
|
|
|
156
|
-
# Prepends existing elements as children.
|
|
157
|
-
#
|
|
157
|
+
# Prepends distinct existing elements as children in argument order.
|
|
158
|
+
# Each element transfers from its current parent. The complete batch is validated before any element moves.
|
|
159
|
+
# @param elements [Array<Sevgi::Graphics::Element>] distinct elements to prepend
|
|
158
160
|
# @return [Sevgi::Graphics::Element] self
|
|
161
|
+
# @raise [Sevgi::ArgumentError] when an argument has a different element class, is repeated, or is this target or its ancestor
|
|
159
162
|
def Prepend(*elements)
|
|
160
|
-
tap { elements
|
|
163
|
+
tap { Adoption.batch(elements, self, front: true) }
|
|
161
164
|
end
|
|
162
165
|
|
|
163
166
|
# Returns the root document element.
|
|
@@ -184,6 +187,10 @@ module Sevgi
|
|
|
184
187
|
# Traverses the subtree depth-first.
|
|
185
188
|
# @param depth [Integer] starting depth
|
|
186
189
|
# @param leave [Proc, nil] optional leave callback
|
|
190
|
+
# @yield [element, depth] visits each element before its children
|
|
191
|
+
# @yieldparam element [Sevgi::Graphics::Element] visited element
|
|
192
|
+
# @yieldparam depth [Integer] element depth
|
|
193
|
+
# @yieldreturn [Object] ignored unless it is a {Stop} token
|
|
187
194
|
# @return [Sevgi::Graphics::Element, Object] self or the value passed through Stay
|
|
188
195
|
# @raise [Sevgi::ArgumentError] when no block is given
|
|
189
196
|
def Traverse(depth = 0, leave = nil, &block)
|
|
@@ -194,6 +201,10 @@ module Sevgi
|
|
|
194
201
|
|
|
195
202
|
# Traverses ancestors from this element to the root.
|
|
196
203
|
# @param height [Integer] starting height
|
|
204
|
+
# @yield [element, height] visits this element and each ancestor
|
|
205
|
+
# @yieldparam element [Sevgi::Graphics::Element] visited element
|
|
206
|
+
# @yieldparam height [Integer] ancestor height
|
|
207
|
+
# @yieldreturn [Object] ignored unless it is a {Stop} token
|
|
197
208
|
# @return [Object, nil] value passed through Stay, or nil
|
|
198
209
|
# @raise [Sevgi::ArgumentError] when no block is given
|
|
199
210
|
def TraverseUp(height = 0, &block)
|
|
@@ -214,6 +225,8 @@ module Sevgi
|
|
|
214
225
|
# Evaluates a block in the parent element context.
|
|
215
226
|
# @param args [Array<Object>] optional receiver override followed by block arguments
|
|
216
227
|
# @param kwargs [Hash] keyword arguments passed to the block
|
|
228
|
+
# @yield evaluates in the selected receiver's parent context
|
|
229
|
+
# @yieldreturn [Object] ignored block result
|
|
217
230
|
# @return [Sevgi::Graphics::Element] self
|
|
218
231
|
def With(*args, **kwargs, &block)
|
|
219
232
|
tap { (args.shift || self).parent.instance_exec(*args, **kwargs, &block) }
|
|
@@ -222,6 +235,8 @@ module Sevgi
|
|
|
222
235
|
# Evaluates a block in this element context.
|
|
223
236
|
# @param args [Array<Object>] optional receiver override followed by block arguments
|
|
224
237
|
# @param kwargs [Hash] keyword arguments passed to the block
|
|
238
|
+
# @yield evaluates in the selected receiver context
|
|
239
|
+
# @yieldreturn [Object] ignored block result
|
|
225
240
|
# @return [Sevgi::Graphics::Element] self
|
|
226
241
|
def Within(*args, **kwargs, &block)
|
|
227
242
|
tap { (args.shift || self).instance_exec(*args, **kwargs, &block) }
|
|
@@ -237,12 +252,32 @@ module Sevgi
|
|
|
237
252
|
# Adoption target validation that keeps tree mutation atomic.
|
|
238
253
|
# @api private
|
|
239
254
|
module Adoption
|
|
255
|
+
# Moves a validated batch to the front or back of a parent.
|
|
256
|
+
# @param elements [Array<Sevgi::Graphics::Element>] elements to move
|
|
257
|
+
# @param parent [Sevgi::Graphics::Element] target parent
|
|
258
|
+
# @param front [Boolean] whether to prepend instead of append
|
|
259
|
+
# @return [void]
|
|
260
|
+
# @raise [Sevgi::ArgumentError] when an argument is incompatible, repeated, or would create a cycle
|
|
261
|
+
def self.batch(elements, parent, front:)
|
|
262
|
+
validate_batch(elements, parent)
|
|
263
|
+
|
|
264
|
+
if front
|
|
265
|
+
elements.reverse_each { it.AdoptFirst(parent) }
|
|
266
|
+
else
|
|
267
|
+
elements.each { it.Adopt(parent) }
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
240
271
|
# Rejects target parents that would create a cycle.
|
|
241
272
|
# @param element [Sevgi::Graphics::Element] element being moved
|
|
242
273
|
# @param parent [Sevgi::Graphics::Element, Object] target parent
|
|
243
274
|
# @return [void]
|
|
244
|
-
# @raise [Sevgi::ArgumentError] when the
|
|
275
|
+
# @raise [Sevgi::ArgumentError] when the parent is incompatible or would create a cycle
|
|
245
276
|
def self.validate(element, parent)
|
|
277
|
+
unless element.instance_of?(parent.class)
|
|
278
|
+
ArgumentError.("Element type does not match the new parent type: #{element.class}")
|
|
279
|
+
end
|
|
280
|
+
|
|
246
281
|
ArgumentError.("Element cannot be adopted under itself") if parent.equal?(element)
|
|
247
282
|
|
|
248
283
|
while parent.respond_to?(:Root?)
|
|
@@ -252,6 +287,42 @@ module Sevgi
|
|
|
252
287
|
parent = parent.parent
|
|
253
288
|
end
|
|
254
289
|
end
|
|
290
|
+
|
|
291
|
+
# Normalizes an insertion index before tree mutation.
|
|
292
|
+
# @param index [Integer] requested insertion index
|
|
293
|
+
# @param size [Integer] number of available child positions
|
|
294
|
+
# @return [Integer] normalized non-negative insertion index
|
|
295
|
+
# @raise [Sevgi::ArgumentError] when the index is not an insertion position
|
|
296
|
+
def self.index(index, size)
|
|
297
|
+
ArgumentError.("Adoption index must be an Integer") unless index.is_a?(Integer)
|
|
298
|
+
|
|
299
|
+
normalized = index.negative? ? size + index + 1 : index
|
|
300
|
+
ArgumentError.("Adoption index is outside the child list") unless normalized.between?(0, size)
|
|
301
|
+
|
|
302
|
+
normalized
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Returns an insertion index after accounting for a same-parent move.
|
|
306
|
+
# @param element [Sevgi::Graphics::Element] element being moved
|
|
307
|
+
# @param parent [Sevgi::Graphics::Element] target parent
|
|
308
|
+
# @param index [Integer] requested insertion index
|
|
309
|
+
# @return [Integer] normalized insertion index
|
|
310
|
+
def self.index_for(element, parent, index)
|
|
311
|
+
same_parent = element.parent.equal?(parent) && parent.children.include?(element)
|
|
312
|
+
index(index, parent.children.size - (same_parent ? 1 : 0))
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def self.validate_batch(elements, parent)
|
|
316
|
+
seen = {}.compare_by_identity
|
|
317
|
+
elements.each do |element|
|
|
318
|
+
ArgumentError.("Element appears more than once in adoption batch") if seen.key?(element)
|
|
319
|
+
|
|
320
|
+
seen[element] = true
|
|
321
|
+
validate(element, parent)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
private_class_method :validate_batch
|
|
255
326
|
end
|
|
256
327
|
|
|
257
328
|
private_constant :Adoption
|
|
@@ -14,22 +14,15 @@ module Sevgi
|
|
|
14
14
|
# @param parent [Sevgi::Graphics::Element, nil] parent for the duplicated subtree
|
|
15
15
|
# @yield [element] optional customization hook for each copied element
|
|
16
16
|
# @yieldparam element [Sevgi::Graphics::Element] copied element
|
|
17
|
+
# @yieldreturn [Object] ignored customization result
|
|
17
18
|
# @return [Sevgi::Graphics::Element] duplicated element
|
|
18
19
|
# @raise [Sevgi::ArgumentError] when the target parent has a different element class
|
|
20
|
+
# @raise [Sevgi::ArgumentError] when copied attributes or contents contain cyclic payloads
|
|
19
21
|
def Duplicate(dx: nil, dy: nil, parent: nil, &block)
|
|
20
22
|
duplicated = Subtree.copy(self)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
element[:"#{ATTRIBUTE_INTERNAL_PREFIX}id"] = id if id
|
|
25
|
-
block&.call(element)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
duplicated.Translate(dx, dy) if dx || dy
|
|
29
|
-
|
|
30
|
-
duplicated.Adopt(parent)
|
|
31
|
-
|
|
32
|
-
duplicated
|
|
23
|
+
Subtree.prepare(duplicated, &block)
|
|
24
|
+
Subtree.translate(duplicated, dx, dy)
|
|
25
|
+
Subtree.attach(duplicated, self, parent)
|
|
33
26
|
end
|
|
34
27
|
|
|
35
28
|
# Duplicates an element subtree along the x-axis.
|
|
@@ -37,8 +30,10 @@ module Sevgi
|
|
|
37
30
|
# @param parent [Sevgi::Graphics::Element, nil] parent for the duplicated subtree
|
|
38
31
|
# @yield [element] optional customization hook for each copied element
|
|
39
32
|
# @yieldparam element [Sevgi::Graphics::Element] copied element
|
|
33
|
+
# @yieldreturn [Object] ignored customization result
|
|
40
34
|
# @return [Sevgi::Graphics::Element] duplicated element
|
|
41
35
|
# @raise [Sevgi::ArgumentError] when the target parent has a different element class
|
|
36
|
+
# @raise [Sevgi::ArgumentError] when copied attributes or contents contain cyclic payloads
|
|
42
37
|
def DuplicateX(dx, parent: nil, &block) = Duplicate(dx:, dy: 0, parent:, &block)
|
|
43
38
|
|
|
44
39
|
# Duplicates an element subtree along the y-axis.
|
|
@@ -46,8 +41,10 @@ module Sevgi
|
|
|
46
41
|
# @param parent [Sevgi::Graphics::Element, nil] parent for the duplicated subtree
|
|
47
42
|
# @yield [element] optional customization hook for each copied element
|
|
48
43
|
# @yieldparam element [Sevgi::Graphics::Element] copied element
|
|
44
|
+
# @yieldreturn [Object] ignored customization result
|
|
49
45
|
# @return [Sevgi::Graphics::Element] duplicated element
|
|
50
46
|
# @raise [Sevgi::ArgumentError] when the target parent has a different element class
|
|
47
|
+
# @raise [Sevgi::ArgumentError] when copied attributes or contents contain cyclic payloads
|
|
51
48
|
def DuplicateY(dy, parent: nil, &block) = Duplicate(dx: 0, dy:, parent:, &block)
|
|
52
49
|
|
|
53
50
|
# Recursive subtree copier that keeps duplicate implementation state out of the DSL surface.
|
|
@@ -61,10 +58,33 @@ module Sevgi
|
|
|
61
58
|
element.dup.tap do |duplicated|
|
|
62
59
|
duplicated.send(:parent=, parent)
|
|
63
60
|
duplicated.send(:attributes=, element.attributes.dup)
|
|
64
|
-
duplicated.send(:contents=, element.contents.dup)
|
|
61
|
+
duplicated.send(:contents=, element.contents.map(&:dup))
|
|
65
62
|
duplicated.send(:children=, element.children.map { |child| copy(child, duplicated) })
|
|
66
63
|
end
|
|
67
64
|
end
|
|
65
|
+
|
|
66
|
+
# Removes copied public ids and applies an optional customization hook.
|
|
67
|
+
# @api private
|
|
68
|
+
def self.prepare(element, &block)
|
|
69
|
+
element.Traverse() do |node|
|
|
70
|
+
id = node.attributes.delete(:id)
|
|
71
|
+
node[:"#{ATTRIBUTE_INTERNAL_PREFIX}id"] = id if id
|
|
72
|
+
block&.call(node)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Applies an optional translation to a copied subtree.
|
|
77
|
+
# @api private
|
|
78
|
+
def self.translate(element, dx, dy)
|
|
79
|
+
element.Translate(dx, dy) if dx || dy
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Attaches a copied subtree unless it is a detached root copy.
|
|
83
|
+
# @api private
|
|
84
|
+
def self.attach(element, source, parent)
|
|
85
|
+
element.Adopt(parent) if parent || !source.Root?()
|
|
86
|
+
element
|
|
87
|
+
end
|
|
68
88
|
end
|
|
69
89
|
|
|
70
90
|
private_constant :Subtree
|
|
@@ -8,6 +8,9 @@ module Sevgi
|
|
|
8
8
|
# Exports the document as PDF.
|
|
9
9
|
# @param path [String, nil] output path or directory
|
|
10
10
|
# @param kwargs [Hash] export options
|
|
11
|
+
# @yield [svg] transforms SVG source before rendering
|
|
12
|
+
# @yieldparam svg [String] rendered SVG source
|
|
13
|
+
# @yieldreturn [String] transformed SVG source
|
|
11
14
|
# @return [String] output path
|
|
12
15
|
# @raise [Sevgi::MissingComponentError] when sevgi/sundries is unavailable
|
|
13
16
|
# @raise [Sevgi::MissingComponentError] when native export gems are unavailable
|
|
@@ -28,6 +31,9 @@ module Sevgi
|
|
|
28
31
|
# Exports the document as PNG.
|
|
29
32
|
# @param path [String, nil] output path or directory
|
|
30
33
|
# @param kwargs [Hash] export options
|
|
34
|
+
# @yield [svg] transforms SVG source before rendering
|
|
35
|
+
# @yieldparam svg [String] rendered SVG source
|
|
36
|
+
# @yieldreturn [String] transformed SVG source
|
|
31
37
|
# @return [String] output path
|
|
32
38
|
# @raise [Sevgi::MissingComponentError] when sevgi/sundries is unavailable
|
|
33
39
|
# @raise [Sevgi::MissingComponentError] when native export gems are unavailable
|
|
@@ -4,6 +4,23 @@ module Sevgi
|
|
|
4
4
|
module Graphics
|
|
5
5
|
module Mixtures
|
|
6
6
|
# DSL helpers for including derendered SVG/XML fragments.
|
|
7
|
+
#
|
|
8
|
+
# @!method Include(file, id)
|
|
9
|
+
# Includes a derendered node matching an id.
|
|
10
|
+
# SVG/XML content is treated as data and is not evaluated as Ruby source.
|
|
11
|
+
# @param file [String] source SVG/XML file
|
|
12
|
+
# @param id [String, Symbol] source node id
|
|
13
|
+
# @return [Sevgi::Graphics::Element, nil] included element, or nil when it produces no graphics output
|
|
14
|
+
# @raise [Sevgi::ArgumentError] when the file is absent or XML content is malformed, rootless, or lacks the id
|
|
15
|
+
# @raise [Sevgi::MissingComponentError] when sevgi/derender is unavailable
|
|
16
|
+
# @!method IncludeChildren(file, id)
|
|
17
|
+
# Includes the children of a derendered node matching an id.
|
|
18
|
+
# SVG/XML content is treated as data and is not evaluated as Ruby source.
|
|
19
|
+
# @param file [String] source SVG/XML file
|
|
20
|
+
# @param id [String, Symbol] source node id
|
|
21
|
+
# @return [Array<Sevgi::Graphics::Element>] included children
|
|
22
|
+
# @raise [Sevgi::ArgumentError] when the file is absent or XML content is malformed, rootless, or lacks the id
|
|
23
|
+
# @raise [Sevgi::MissingComponentError] when sevgi/derender is unavailable
|
|
7
24
|
module Include
|
|
8
25
|
require "sevgi/derender"
|
|
9
26
|
|
|
@@ -26,6 +26,8 @@ module Sevgi
|
|
|
26
26
|
# @param mod [Module] callable drawing module
|
|
27
27
|
# @param args [Array<Object>] callable arguments
|
|
28
28
|
# @param kwargs [Hash] group attributes
|
|
29
|
+
# @yield forwards customization to the callable module
|
|
30
|
+
# @yieldreturn [Object] callable customization result
|
|
29
31
|
# @return [Sevgi::Graphics::Element] group element
|
|
30
32
|
# @raise [Sevgi::ArgumentError] when mod is not a plain module
|
|
31
33
|
def Group(mod, *args, **kwargs, &block)
|
|
@@ -37,6 +39,8 @@ module Sevgi
|
|
|
37
39
|
# @param mod [Module] callable drawing module
|
|
38
40
|
# @param args [Array<Object>] callable arguments
|
|
39
41
|
# @param kwargs [Hash] layer attributes
|
|
42
|
+
# @yield forwards customization to the callable module
|
|
43
|
+
# @yieldreturn [Object] callable customization result
|
|
40
44
|
# @return [Sevgi::Graphics::Element] layer element
|
|
41
45
|
# @raise [Sevgi::ArgumentError] when mod is not a plain module
|
|
42
46
|
def Layer(mod, *args, **kwargs, &block)
|
|
@@ -48,6 +52,8 @@ module Sevgi
|
|
|
48
52
|
# @param mod [Module] callable drawing module
|
|
49
53
|
# @param args [Array<Object>] callable arguments
|
|
50
54
|
# @param kwargs [Hash] layer attributes
|
|
55
|
+
# @yield forwards customization to the callable module
|
|
56
|
+
# @yieldreturn [Object] callable customization result
|
|
51
57
|
# @return [Sevgi::Graphics::Element] layer element
|
|
52
58
|
# @raise [Sevgi::ArgumentError] when mod is not a plain module
|
|
53
59
|
def Layer!(mod, *args, **kwargs, &block)
|
|
@@ -58,6 +64,8 @@ module Sevgi
|
|
|
58
64
|
# @overload layer(**attributes)
|
|
59
65
|
# Builds an Inkscape layer group.
|
|
60
66
|
# @param attributes [Hash] layer attributes
|
|
67
|
+
# @yield evaluates the drawing DSL in the layer
|
|
68
|
+
# @yieldreturn [Object] ignored block result
|
|
61
69
|
# @return [Sevgi::Graphics::Element] layer group
|
|
62
70
|
def layer(**, &block)
|
|
63
71
|
g("inkscape:groupmode": "layer", **, &block)
|
|
@@ -66,6 +74,8 @@ module Sevgi
|
|
|
66
74
|
# @overload layer!(**attributes)
|
|
67
75
|
# Builds an insensitive Inkscape layer group.
|
|
68
76
|
# @param attributes [Hash] layer attributes
|
|
77
|
+
# @yield evaluates the drawing DSL in the layer
|
|
78
|
+
# @yieldreturn [Object] ignored block result
|
|
69
79
|
# @return [Sevgi::Graphics::Element] layer group
|
|
70
80
|
def layer!(**, &block)
|
|
71
81
|
layer("sodipodi:insensitive": "true", **, &block)
|
|
@@ -74,6 +84,9 @@ module Sevgi
|
|
|
74
84
|
# Builds an Inkscape namedview containing page elements.
|
|
75
85
|
# @param pages [Array<Hash>] page attribute hashes
|
|
76
86
|
# @param id [String] namedview id
|
|
87
|
+
# @yield [page] customizes each generated page element
|
|
88
|
+
# @yieldparam page [Sevgi::Graphics::Element] generated page element
|
|
89
|
+
# @yieldreturn [Object] ignored customization result
|
|
77
90
|
# @return [Sevgi::Graphics::Element] namedview element
|
|
78
91
|
def Pages(*pages, id: "namedview", **, &block)
|
|
79
92
|
Element(:"sodipodi:namedview", id:, **) do
|
|
@@ -111,6 +124,8 @@ module Sevgi
|
|
|
111
124
|
# @overload symbol!(**attributes)
|
|
112
125
|
# Builds an Inkscape symbol group hidden from the symbols menu.
|
|
113
126
|
# @param attributes [Hash] symbol attributes
|
|
127
|
+
# @yield evaluates the drawing DSL in the symbol group
|
|
128
|
+
# @yieldreturn [Object] ignored block result
|
|
114
129
|
# @return [Sevgi::Graphics::Element] symbol group
|
|
115
130
|
def symbol!(**, &block)
|
|
116
131
|
if Is?(:defs)
|
|
@@ -7,6 +7,8 @@ module Sevgi
|
|
|
7
7
|
module RDF
|
|
8
8
|
# Adds RDF license metadata inside a metadata element.
|
|
9
9
|
# @param kwargs [Hash] RDF work options
|
|
10
|
+
# @yield evaluates additional RDF work metadata
|
|
11
|
+
# @yieldreturn [Object] ignored block result
|
|
10
12
|
# @return [Sevgi::Graphics::Element] metadata element
|
|
11
13
|
def License(**kwargs, &block) = metadata { RDFWork(**kwargs, &block) }
|
|
12
14
|
|
|
@@ -14,6 +16,8 @@ module Sevgi
|
|
|
14
16
|
#
|
|
15
17
|
# Adds Creative Commons BY-SA license metadata.
|
|
16
18
|
# @param kwargs [Hash] RDF work options
|
|
19
|
+
# @yield evaluates additional RDF work metadata
|
|
20
|
+
# @yieldreturn [Object] ignored block result
|
|
17
21
|
# @return [Sevgi::Graphics::Element] metadata element
|
|
18
22
|
def License_CC_BY_SA(**kwargs, &block)
|
|
19
23
|
License(**kwargs, license: "https://creativecommons.org/licenses/by-sa/4.0/", &block)
|
|
@@ -21,6 +25,8 @@ module Sevgi
|
|
|
21
25
|
|
|
22
26
|
# Adds Creative Commons BY-NC license metadata.
|
|
23
27
|
# @param kwargs [Hash] RDF work options
|
|
28
|
+
# @yield evaluates additional RDF work metadata
|
|
29
|
+
# @yieldreturn [Object] ignored block result
|
|
24
30
|
# @return [Sevgi::Graphics::Element] metadata element
|
|
25
31
|
def License_CC_BY_NC(**kwargs, &block)
|
|
26
32
|
License(**kwargs, license: "https://creativecommons.org/licenses/by-nc/4.0/", &block)
|
|
@@ -28,6 +34,8 @@ module Sevgi
|
|
|
28
34
|
|
|
29
35
|
# Adds Creative Commons BY-NC-SA license metadata.
|
|
30
36
|
# @param kwargs [Hash] RDF work options
|
|
37
|
+
# @yield evaluates additional RDF work metadata
|
|
38
|
+
# @yieldreturn [Object] ignored block result
|
|
31
39
|
# @return [Sevgi::Graphics::Element] metadata element
|
|
32
40
|
def License_CC_BY_NC_SA(**kwargs, &block)
|
|
33
41
|
License(**kwargs, license: "https://creativecommons.org/licenses/by-nc-sa/4.0/", &block)
|
|
@@ -35,6 +43,8 @@ module Sevgi
|
|
|
35
43
|
|
|
36
44
|
# Adds Creative Commons BY-ND license metadata.
|
|
37
45
|
# @param kwargs [Hash] RDF work options
|
|
46
|
+
# @yield evaluates additional RDF work metadata
|
|
47
|
+
# @yieldreturn [Object] ignored block result
|
|
38
48
|
# @return [Sevgi::Graphics::Element] metadata element
|
|
39
49
|
def License_CC_BY_ND(**kwargs, &block)
|
|
40
50
|
License(**kwargs, license: "https://creativecommons.org/licenses/by-nd/4.0/", &block)
|
|
@@ -42,6 +52,8 @@ module Sevgi
|
|
|
42
52
|
|
|
43
53
|
# Adds Creative Commons BY-NC-ND license metadata.
|
|
44
54
|
# @param kwargs [Hash] RDF work options
|
|
55
|
+
# @yield evaluates additional RDF work metadata
|
|
56
|
+
# @yieldreturn [Object] ignored block result
|
|
45
57
|
# @return [Sevgi::Graphics::Element] metadata element
|
|
46
58
|
def License_CC_BY_NC_ND(**kwargs, &block)
|
|
47
59
|
License(**kwargs, license: "https://creativecommons.org/licenses/by-nc-nd/4.0/", &block)
|
|
@@ -49,6 +61,8 @@ module Sevgi
|
|
|
49
61
|
|
|
50
62
|
# Adds Creative Commons Zero metadata.
|
|
51
63
|
# @param kwargs [Hash] RDF work options
|
|
64
|
+
# @yield evaluates additional RDF work metadata
|
|
65
|
+
# @yieldreturn [Object] ignored block result
|
|
52
66
|
# @return [Sevgi::Graphics::Element] metadata element
|
|
53
67
|
def License_CC0(**kwargs, &block)
|
|
54
68
|
License(**kwargs, license: "https://creativecommons.org/publicdomain/zero/1.0/", &block)
|
|
@@ -56,11 +70,15 @@ module Sevgi
|
|
|
56
70
|
|
|
57
71
|
# Adds Free Art License metadata.
|
|
58
72
|
# @param kwargs [Hash] RDF work options
|
|
73
|
+
# @yield evaluates additional RDF work metadata
|
|
74
|
+
# @yieldreturn [Object] ignored block result
|
|
59
75
|
# @return [Sevgi::Graphics::Element] metadata element
|
|
60
76
|
def License_LAL(**kwargs, &block) = License(**kwargs, license: "https://artlibre.org/licence/lal/en/", &block)
|
|
61
77
|
|
|
62
78
|
# Builds an RDF root element.
|
|
63
79
|
# @param _kwargs [Hash] currently unused options
|
|
80
|
+
# @yield evaluates the RDF drawing DSL
|
|
81
|
+
# @yieldreturn [Object] ignored block result
|
|
64
82
|
# @return [Sevgi::Graphics::Element] RDF element
|
|
65
83
|
# @raise [Sevgi::ArgumentError] when no block is given
|
|
66
84
|
def RDF(**_kwargs, &block)
|
|
@@ -86,6 +104,8 @@ module Sevgi
|
|
|
86
104
|
# @option kwargs [String] :date date
|
|
87
105
|
# @option kwargs [String] :language language
|
|
88
106
|
# @option kwargs [String] :license license URL
|
|
107
|
+
# @yield evaluates additional RDF work metadata
|
|
108
|
+
# @yieldreturn [Object] ignored block result
|
|
89
109
|
# @return [Sevgi::Graphics::Element] RDF element
|
|
90
110
|
def RDFWork(**kwargs, &block)
|
|
91
111
|
RDF do
|
|
@@ -10,6 +10,7 @@ module Sevgi
|
|
|
10
10
|
class Renderer
|
|
11
11
|
# Default renderer options.
|
|
12
12
|
DEFAULTS = {indent: " ", linelength: 140, style: :hybrid}.freeze
|
|
13
|
+
SVG_NAMESPACE = "http://www.w3.org/2000/svg"
|
|
13
14
|
|
|
14
15
|
# Attribute rendering strategies.
|
|
15
16
|
# @api private
|
|
@@ -77,7 +78,7 @@ module Sevgi
|
|
|
77
78
|
ELEMENTS_WITH_BLOCK_CONTENT = %i[style].freeze
|
|
78
79
|
SEPARATOR = "\n"
|
|
79
80
|
|
|
80
|
-
private_constant :ELEMENTS_WITH_INLINE_CONTENT, :ELEMENTS_WITH_BLOCK_CONTENT, :SEPARATOR
|
|
81
|
+
private_constant :ELEMENTS_WITH_INLINE_CONTENT, :ELEMENTS_WITH_BLOCK_CONTENT, :SEPARATOR, :SVG_NAMESPACE
|
|
81
82
|
|
|
82
83
|
# @return [Sevgi::Graphics::Element] root element
|
|
83
84
|
attr_reader :root
|
|
@@ -161,7 +162,7 @@ module Sevgi
|
|
|
161
162
|
# @option options [Integer] :linelength hybrid attribute line length
|
|
162
163
|
# @option options [Symbol] :style attribute style, one of :hybrid, :inline, or :block
|
|
163
164
|
# @return [void]
|
|
164
|
-
# @raise [Sevgi::ArgumentError] when
|
|
165
|
+
# @raise [Sevgi::ArgumentError] when an option is unknown, malformed, missing, or unsupported
|
|
165
166
|
def initialize(root, **)
|
|
166
167
|
@root = root
|
|
167
168
|
@options = DEFAULTS.merge(**)
|
|
@@ -176,14 +177,14 @@ module Sevgi
|
|
|
176
177
|
# @param root [Sevgi::Graphics::Element] root element
|
|
177
178
|
# @param options [Hash] renderer options
|
|
178
179
|
# @return [String] SVG source
|
|
179
|
-
# @raise [Sevgi::ArgumentError] when
|
|
180
|
+
# @raise [Sevgi::ArgumentError] when options, preambles, names, attributes, or content are invalid XML
|
|
180
181
|
def self.call(root, **) = new(root, **).call(*root.class.preambles)
|
|
181
182
|
|
|
182
183
|
# Renders a root element without document preambles.
|
|
183
184
|
# @param root [Sevgi::Graphics::Element] root element
|
|
184
185
|
# @param options [Hash] renderer options
|
|
185
186
|
# @return [String] SVG fragment source
|
|
186
|
-
# @raise [Sevgi::ArgumentError] when
|
|
187
|
+
# @raise [Sevgi::ArgumentError] when options, names, attributes, or content are invalid XML
|
|
187
188
|
def self.fragment(root, **options) = new(root, **options).call
|
|
188
189
|
|
|
189
190
|
# Appends rendered lines to the output buffer.
|
|
@@ -199,8 +200,9 @@ module Sevgi
|
|
|
199
200
|
# Renders the document.
|
|
200
201
|
# @param preambles [Array<String>] preamble lines
|
|
201
202
|
# @return [String] SVG source
|
|
203
|
+
# @raise [Sevgi::ArgumentError] when a preamble or rendered value is not valid XML text
|
|
202
204
|
def call(*preambles)
|
|
203
|
-
output.append(preambles) unless preambles.empty?
|
|
205
|
+
output.append(preambles.map { XML.text(it, context: "XML preamble") }) unless preambles.empty?
|
|
204
206
|
|
|
205
207
|
root.Traverse(
|
|
206
208
|
0,
|
|
@@ -234,6 +236,7 @@ module Sevgi
|
|
|
234
236
|
end
|
|
235
237
|
|
|
236
238
|
def build
|
|
239
|
+
validate_options!
|
|
237
240
|
ArgumentError.("Missing style") unless options[:style]
|
|
238
241
|
|
|
239
242
|
case options[:style]
|
|
@@ -254,10 +257,29 @@ module Sevgi
|
|
|
254
257
|
element.children.empty? && element.contents.empty?
|
|
255
258
|
end
|
|
256
259
|
|
|
260
|
+
def block_content?(element)
|
|
261
|
+
return false unless ELEMENTS_WITH_BLOCK_CONTENT.include?(element.name)
|
|
262
|
+
|
|
263
|
+
namespace = default_namespace(element)
|
|
264
|
+
namespace.nil? || namespace.to_s.empty? || namespace == SVG_NAMESPACE
|
|
265
|
+
end
|
|
266
|
+
|
|
257
267
|
def closed = @closed = true
|
|
258
268
|
|
|
259
269
|
def closed? = @closed.tap { unclosed }
|
|
260
270
|
|
|
271
|
+
def default_namespace(element)
|
|
272
|
+
current = element
|
|
273
|
+
|
|
274
|
+
while current.is_a?(Element)
|
|
275
|
+
return current.attributes[:xmlns] if current.attributes.has?(:xmlns)
|
|
276
|
+
|
|
277
|
+
current = current.parent
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
nil
|
|
281
|
+
end
|
|
282
|
+
|
|
261
283
|
def contents(element, depth)
|
|
262
284
|
return if element.contents.empty?
|
|
263
285
|
|
|
@@ -272,10 +294,10 @@ module Sevgi
|
|
|
272
294
|
def floating?(element) = element.Is?(:_)
|
|
273
295
|
|
|
274
296
|
def inline_content?(element)
|
|
275
|
-
return false if
|
|
297
|
+
return false if block_content?(element)
|
|
276
298
|
return false if floating?(element)
|
|
277
299
|
|
|
278
|
-
element.contents.
|
|
300
|
+
element.contents.any? || ELEMENTS_WITH_INLINE_CONTENT.include?(element.name)
|
|
279
301
|
end
|
|
280
302
|
|
|
281
303
|
def indent(depth)
|
|
@@ -284,7 +306,7 @@ module Sevgi
|
|
|
284
306
|
|
|
285
307
|
def join
|
|
286
308
|
inlines.join(output, indent: options[:indent], separator: SEPARATOR)
|
|
287
|
-
output.join(SEPARATOR)
|
|
309
|
+
XML.text(output.join(SEPARATOR), context: "SVG output")
|
|
288
310
|
end
|
|
289
311
|
|
|
290
312
|
def render_enter(element, depth)
|
|
@@ -307,6 +329,22 @@ module Sevgi
|
|
|
307
329
|
end
|
|
308
330
|
|
|
309
331
|
def unclosed = @closed = false
|
|
332
|
+
|
|
333
|
+
def validate_options!
|
|
334
|
+
unknown = options.keys - DEFAULTS.keys
|
|
335
|
+
ArgumentError.("Unknown renderer options: #{unknown.join(", ")}") unless unknown.empty?
|
|
336
|
+
|
|
337
|
+
indent = options[:indent]
|
|
338
|
+
unless indent.is_a?(::String) && /\A[\t\n\r ]*\z/.match?(indent)
|
|
339
|
+
ArgumentError.("Renderer indent must contain only XML whitespace")
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
options[:indent] = XML.text(indent, context: "Renderer indent")
|
|
343
|
+
linelength = options[:linelength]
|
|
344
|
+
return if linelength.is_a?(::Integer) && linelength >= 0
|
|
345
|
+
|
|
346
|
+
ArgumentError.("Renderer linelength must be a non-negative Integer")
|
|
347
|
+
end
|
|
310
348
|
end
|
|
311
349
|
|
|
312
350
|
private_constant :Renderer
|
|
@@ -315,10 +353,11 @@ module Sevgi
|
|
|
315
353
|
# Renders this element as SVG source.
|
|
316
354
|
# Elements with inline text content may also contain inline children such as `tspan`; the renderer keeps those
|
|
317
355
|
# descendants in the same text line. Whitespace inside content objects is preserved as given, and encoded
|
|
318
|
-
# content is XML-escaped unless a verbatim content object is used.
|
|
356
|
+
# content is XML-escaped unless a verbatim content object is used. SVG `style` elements use block content;
|
|
357
|
+
# same-named elements under a foreign default namespace retain ordinary inline text formatting.
|
|
319
358
|
# @param options [Hash] renderer options
|
|
320
359
|
# @return [String] SVG source
|
|
321
|
-
# @raise [Sevgi::ArgumentError] when
|
|
360
|
+
# @raise [Sevgi::ArgumentError] when options, preambles, names, attributes, or content are invalid XML
|
|
322
361
|
def Render(**) = Renderer.(self, **)
|
|
323
362
|
|
|
324
363
|
# Renders only this element's children.
|
|
@@ -326,7 +365,13 @@ module Sevgi
|
|
|
326
365
|
# mixed-content formatting.
|
|
327
366
|
# @param separator [String] separator between child documents
|
|
328
367
|
# @return [String] rendered child fragments
|
|
329
|
-
|
|
368
|
+
# @raise [Sevgi::ArgumentError] when separator or rendered child data is not valid XML text
|
|
369
|
+
def RenderChildren(separator = "\n\n")
|
|
370
|
+
ArgumentError.("SVG fragment separator must be a String") unless separator.is_a?(::String)
|
|
371
|
+
|
|
372
|
+
separator = XML.text(separator, context: "SVG fragment separator")
|
|
373
|
+
children.map { Renderer.fragment(it) }.join(separator)
|
|
374
|
+
end
|
|
330
375
|
end
|
|
331
376
|
end
|
|
332
377
|
end
|
|
@@ -12,6 +12,9 @@ module Sevgi
|
|
|
12
12
|
|
|
13
13
|
# Writes rendered SVG to standard output.
|
|
14
14
|
# @param kwargs [Hash] render options
|
|
15
|
+
# @yield [content] optionally transforms rendered content before output
|
|
16
|
+
# @yieldparam content [String] rendered SVG source
|
|
17
|
+
# @yieldreturn [String] transformed SVG source
|
|
15
18
|
# @return [Object] F.out return value
|
|
16
19
|
def Out(**kwargs, &filter)
|
|
17
20
|
F.out(self.(**kwargs), &filter)
|
|
@@ -21,6 +24,9 @@ module Sevgi
|
|
|
21
24
|
# @param path [String, nil] output path or directory
|
|
22
25
|
# @param default [String, nil] default output path
|
|
23
26
|
# @param backup_suffix [String, nil] suffix used for an existing-file backup
|
|
27
|
+
# @yield [content] optionally transforms rendered content before output
|
|
28
|
+
# @yieldparam content [String] rendered SVG source
|
|
29
|
+
# @yieldreturn [String] transformed SVG source
|
|
24
30
|
# @return [Object] F.out return value
|
|
25
31
|
def Save(path = nil, default: nil, backup_suffix: nil, &filter)
|
|
26
32
|
default ||= F.subext(EXT, caller_locations(1..1).first.path)
|
|
@@ -42,6 +48,9 @@ module Sevgi
|
|
|
42
48
|
# Writes rendered SVG to a path.
|
|
43
49
|
# @param path [String] output path
|
|
44
50
|
# @param kwargs [Hash] render options
|
|
51
|
+
# @yield [content] optionally transforms rendered content before output
|
|
52
|
+
# @yieldparam content [String] rendered SVG source
|
|
53
|
+
# @yieldreturn [String] transformed SVG source
|
|
45
54
|
# @return [Object] F.out return value
|
|
46
55
|
def Write(path, **kwargs, &filter)
|
|
47
56
|
F.out(self.(**kwargs), path, &filter)
|