sevgi-graphics 0.93.1 → 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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +152 -0
  3. data/LICENSE +674 -0
  4. data/README.md +34 -2
  5. data/lib/sevgi/graphics/attribute.rb +146 -32
  6. data/lib/sevgi/graphics/auxiliary/canvas.rb +64 -14
  7. data/lib/sevgi/graphics/auxiliary/content.rb +156 -21
  8. data/lib/sevgi/graphics/auxiliary/margin.rb +14 -2
  9. data/lib/sevgi/graphics/auxiliary/paper.rb +91 -84
  10. data/lib/sevgi/graphics/auxiliary/scalar.rb +40 -0
  11. data/lib/sevgi/graphics/auxiliary/sizes.rb +62 -0
  12. data/lib/sevgi/graphics/auxiliary.rb +2 -0
  13. data/lib/sevgi/graphics/document.rb +234 -56
  14. data/lib/sevgi/graphics/element.rb +42 -15
  15. data/lib/sevgi/graphics/mixtures/call.rb +29 -3
  16. data/lib/sevgi/graphics/mixtures/core.rb +138 -35
  17. data/lib/sevgi/graphics/mixtures/duplicate.rb +66 -16
  18. data/lib/sevgi/graphics/mixtures/export.rb +10 -2
  19. data/lib/sevgi/graphics/mixtures/identify.rb +3 -1
  20. data/lib/sevgi/graphics/mixtures/include.rb +28 -2
  21. data/lib/sevgi/graphics/mixtures/inkscape.rb +15 -0
  22. data/lib/sevgi/graphics/mixtures/rdf.rb +20 -0
  23. data/lib/sevgi/graphics/mixtures/render.rb +103 -24
  24. data/lib/sevgi/graphics/mixtures/save.rb +9 -0
  25. data/lib/sevgi/graphics/mixtures/symbols.rb +3 -0
  26. data/lib/sevgi/graphics/mixtures/tile.rb +17 -3
  27. data/lib/sevgi/graphics/mixtures/underscore.rb +14 -3
  28. data/lib/sevgi/graphics/mixtures/validate.rb +12 -0
  29. data/lib/sevgi/graphics/mixtures/wrappers.rb +4 -1
  30. data/lib/sevgi/graphics/mixtures.rb +4 -0
  31. data/lib/sevgi/graphics/version.rb +1 -1
  32. data/lib/sevgi/graphics/xml.rb +127 -0
  33. data/lib/sevgi/graphics.rb +33 -18
  34. metadata +10 -5
@@ -5,9 +5,15 @@ require "forwardable"
5
5
  module Sevgi
6
6
  module Graphics
7
7
  module Mixtures
8
- # Internal traversal stop token.
8
+ # Traversal stop token returned by {Core#Stay}.
9
+ #
10
+ # @!attribute [r] value
11
+ # @return [Object] value returned from traversal
12
+ Stop = Data.define(:value)
13
+
14
+ # Internal traversal engine.
9
15
  # @api private
10
- Traversal = Data.define(:value) do
16
+ module Traversal
11
17
  # Runs a depth-first traversal.
12
18
  # @param element [Sevgi::Graphics::Element] starting element
13
19
  # @param depth [Integer] starting depth
@@ -21,7 +27,7 @@ module Sevgi
21
27
  end
22
28
 
23
29
  def self.stop(value)
24
- throw(:traversal, value.value) if value.is_a?(self)
30
+ throw(:traversal, value.value) if value.is_a?(Stop)
25
31
  end
26
32
 
27
33
  def self.visit(element, depth, leave, &block)
@@ -46,18 +52,17 @@ module Sevgi
46
52
  # @param index [Integer] insertion index
47
53
  # @return [Sevgi::Graphics::Element] self
48
54
  # @raise [Sevgi::ArgumentError] when the target parent has a different element class
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
49
57
  def Adopt(new_parent = nil, index: -1)
50
58
  tap do
51
- if new_parent
52
- unless instance_of?(new_parent.class)
53
- ArgumentError.("Element type does not match the new parent type: #{self.class}")
54
- end
55
- else
56
- new_parent = parent
57
- end
59
+ new_parent ||= parent
60
+ Adoption.validate(self, new_parent)
61
+
62
+ insertion = Adoption.index_for(self, new_parent, index)
58
63
 
59
64
  self.Orphan()
60
- (@parent = new_parent).children.insert(index, self)
65
+ (@parent = new_parent).children.insert(insertion, self)
61
66
  end
62
67
  end
63
68
 
@@ -65,35 +70,38 @@ module Sevgi
65
70
  # Moves this element to the beginning of a parent.
66
71
  # @param new_parent [Sevgi::Graphics::Element, nil] target parent or current parent
67
72
  # @return [Sevgi::Graphics::Element] self
73
+ # @raise [Sevgi::ArgumentError] when the target parent has a different element class
74
+ # @raise [Sevgi::ArgumentError] when the target parent is this element or one of its descendants
68
75
  def AdoptFirst(*)
69
76
  Adopt(*, index: 0)
70
77
  end
71
78
 
72
- # Appends existing elements as children.
73
- # @param elements [Array<Sevgi::Graphics::Element>] elements to append
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
74
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
75
84
  def Append(*elements)
76
- tap { elements.each { it.Adopt(self) } }
85
+ tap { Adoption.batch(elements, self, front: false) }
77
86
  end
78
87
 
79
88
  # Adds CSS classes without duplicating existing values.
80
- # @param classes [Array<String, Symbol>] class names
89
+ # @param classes [Array<String, Symbol, Array>] class tokens; strings are split on whitespace
81
90
  # @return [Sevgi::Graphics::Element] self
82
91
  def Classify(*classes)
83
92
  tap do
84
- klasses = case self[:class]
85
- when ::Array
86
- self[:class]
87
- when ::String
88
- self[:class].split
89
- when nil
90
- []
91
- else
92
- [self[:class]]
93
+ tokens = proc do |value|
94
+ case value
95
+ when nil
96
+ []
97
+ when ::Array
98
+ value.flat_map { tokens.call(it) }
99
+ else
100
+ value.to_s.split
101
+ end
93
102
  end
94
103
 
95
- classes.each { klasses << it unless klasses.include?(it) }
96
- self[:class] = klasses
104
+ self[:class] = [*tokens.call(self[:class]), *tokens.call(classes)].uniq
97
105
  end
98
106
  end
99
107
 
@@ -112,11 +120,14 @@ module Sevgi
112
120
 
113
121
  # Builds a child element with an explicit tag name.
114
122
  # @param tag [Symbol, String] SVG tag name
115
- # @param contents [Array<Object>] text or content objects
123
+ # @param contents [Array<Object>] text or content objects; non-content objects are stringified and XML-encoded
116
124
  # @param attributes [Hash] SVG attributes
125
+ # @yield evaluates the drawing DSL in the new child element
126
+ # @yieldreturn [Object] ignored block result
117
127
  # @return [Sevgi::Graphics::Element] new child element
128
+ # @raise [Sevgi::ArgumentError] when the tag, attributes, or content are not valid XML
118
129
  def Element(tag, *contents, **attributes, &block)
119
- self.class.send(:new, tag.to_sym, contents: Content.contents(*contents), attributes:, parent: self, &block)
130
+ self.class.send(:new, tag, contents: Content.contents(*contents), attributes:, parent: self, &block)
120
131
  end
121
132
 
122
133
  # Forwards this element as the first argument to another receiver.
@@ -138,16 +149,18 @@ module Sevgi
138
149
  end
139
150
 
140
151
  # Removes this element from its parent.
141
- # @return [Array<Sevgi::Graphics::Element>, nil] parent children after deletion, or nil for root elements
152
+ # @return [Sevgi::Graphics::Element, nil] the deleted element, or nil for root elements
142
153
  def Orphan
143
154
  parent.children&.delete(self) unless Root?()
144
155
  end
145
156
 
146
- # Prepends existing elements as children.
147
- # @param elements [Array<Sevgi::Graphics::Element>] elements to prepend
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
148
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
149
162
  def Prepend(*elements)
150
- tap { elements.each { it.AdoptFirst(self) } }
163
+ tap { Adoption.batch(elements, self, front: true) }
151
164
  end
152
165
 
153
166
  # Returns the root document element.
@@ -168,12 +181,16 @@ module Sevgi
168
181
  # @overload Stay(value)
169
182
  # Wraps a traversal return value as a stop token.
170
183
  # @param value [Object] value returned from traversal
171
- # @return [Sevgi::Graphics::Mixtures::Traversal]
172
- def Stay(...) = Traversal.new(...)
184
+ # @return [Sevgi::Graphics::Mixtures::Stop]
185
+ def Stay(...) = Stop.new(...)
173
186
 
174
187
  # Traverses the subtree depth-first.
175
188
  # @param depth [Integer] starting depth
176
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
177
194
  # @return [Sevgi::Graphics::Element, Object] self or the value passed through Stay
178
195
  # @raise [Sevgi::ArgumentError] when no block is given
179
196
  def Traverse(depth = 0, leave = nil, &block)
@@ -184,6 +201,10 @@ module Sevgi
184
201
 
185
202
  # Traverses ancestors from this element to the root.
186
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
187
208
  # @return [Object, nil] value passed through Stay, or nil
188
209
  # @raise [Sevgi::ArgumentError] when no block is given
189
210
  def TraverseUp(height = 0, &block)
@@ -192,7 +213,7 @@ module Sevgi
192
213
  element = self
193
214
 
194
215
  loop do
195
- yield(element, height).tap { return it.value if it.is_a?(Traversal) }
216
+ yield(element, height).tap { return it.value if it.is_a?(Stop) }
196
217
 
197
218
  break if element.Root?()
198
219
 
@@ -204,6 +225,8 @@ module Sevgi
204
225
  # Evaluates a block in the parent element context.
205
226
  # @param args [Array<Object>] optional receiver override followed by block arguments
206
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
207
230
  # @return [Sevgi::Graphics::Element] self
208
231
  def With(*args, **kwargs, &block)
209
232
  tap { (args.shift || self).parent.instance_exec(*args, **kwargs, &block) }
@@ -212,6 +235,8 @@ module Sevgi
212
235
  # Evaluates a block in this element context.
213
236
  # @param args [Array<Object>] optional receiver override followed by block arguments
214
237
  # @param kwargs [Hash] keyword arguments passed to the block
238
+ # @yield evaluates in the selected receiver context
239
+ # @yieldreturn [Object] ignored block result
215
240
  # @return [Sevgi::Graphics::Element] self
216
241
  def Within(*args, **kwargs, &block)
217
242
  tap { (args.shift || self).instance_exec(*args, **kwargs, &block) }
@@ -223,6 +248,84 @@ module Sevgi
223
248
  def <<(element)
224
249
  Append(element)
225
250
  end
251
+
252
+ # Adoption target validation that keeps tree mutation atomic.
253
+ # @api private
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
+
271
+ # Rejects target parents that would create a cycle.
272
+ # @param element [Sevgi::Graphics::Element] element being moved
273
+ # @param parent [Sevgi::Graphics::Element, Object] target parent
274
+ # @return [void]
275
+ # @raise [Sevgi::ArgumentError] when the parent is incompatible or would create a cycle
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
+
281
+ ArgumentError.("Element cannot be adopted under itself") if parent.equal?(element)
282
+
283
+ while parent.respond_to?(:Root?)
284
+ ArgumentError.("Element cannot be adopted under its descendant") if parent.equal?(element)
285
+ break if parent.Root?()
286
+
287
+ parent = parent.parent
288
+ end
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
326
+ end
327
+
328
+ private_constant :Adoption
226
329
  end
227
330
  end
228
331
  end
@@ -3,41 +3,91 @@
3
3
  module Sevgi
4
4
  module Graphics
5
5
  module Mixtures
6
- # DSL helpers for duplicating elements and their subtrees.
6
+ # DSL helpers for duplicating independent element subtrees.
7
7
  module Duplicate
8
- # Duplicates an element subtree and optionally translates it.
8
+ # Duplicates an element subtree as an independent tree and optionally translates it.
9
+ # Copied elements receive new child arrays, attribute stores, and content arrays. Public `id` attributes are
10
+ # moved to an internal `-id` attribute before the optional block runs, allowing the block to derive replacement
11
+ # ids without rendering duplicate public ids.
9
12
  # @param dx [Numeric, nil] x translation
10
13
  # @param dy [Numeric, nil] y translation
11
14
  # @param parent [Sevgi::Graphics::Element, nil] parent for the duplicated subtree
15
+ # @yield [element] optional customization hook for each copied element
16
+ # @yieldparam element [Sevgi::Graphics::Element] copied element
17
+ # @yieldreturn [Object] ignored customization result
12
18
  # @return [Sevgi::Graphics::Element] duplicated element
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
13
21
  def Duplicate(dx: nil, dy: nil, parent: nil, &block)
14
- duplicated = dup
15
-
16
- duplicated.Traverse() do |element|
17
- element.children = element.children.map(&:dup)
18
- id = (element.attributes = element.attributes.dup).delete(:id)
19
- element[:"#{ATTRIBUTE_INTERNAL_PREFIX}id"] = id if id
20
- block&.call(element)
21
- end
22
-
23
- duplicated.Translate(dx, dy) if dx || dy
24
-
25
- duplicated.Adopt(parent)
26
-
27
- duplicated
22
+ duplicated = Subtree.copy(self)
23
+ Subtree.prepare(duplicated, &block)
24
+ Subtree.translate(duplicated, dx, dy)
25
+ Subtree.attach(duplicated, self, parent)
28
26
  end
29
27
 
30
28
  # Duplicates an element subtree along the x-axis.
31
29
  # @param dx [Numeric] x translation
32
30
  # @param parent [Sevgi::Graphics::Element, nil] parent for the duplicated subtree
31
+ # @yield [element] optional customization hook for each copied element
32
+ # @yieldparam element [Sevgi::Graphics::Element] copied element
33
+ # @yieldreturn [Object] ignored customization result
33
34
  # @return [Sevgi::Graphics::Element] duplicated element
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
34
37
  def DuplicateX(dx, parent: nil, &block) = Duplicate(dx:, dy: 0, parent:, &block)
35
38
 
36
39
  # Duplicates an element subtree along the y-axis.
37
40
  # @param dy [Numeric] y translation
38
41
  # @param parent [Sevgi::Graphics::Element, nil] parent for the duplicated subtree
42
+ # @yield [element] optional customization hook for each copied element
43
+ # @yieldparam element [Sevgi::Graphics::Element] copied element
44
+ # @yieldreturn [Object] ignored customization result
39
45
  # @return [Sevgi::Graphics::Element] duplicated element
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
40
48
  def DuplicateY(dy, parent: nil, &block) = Duplicate(dx: 0, dy:, parent:, &block)
49
+
50
+ # Recursive subtree copier that keeps duplicate implementation state out of the DSL surface.
51
+ # @api private
52
+ module Subtree
53
+ # Builds an independent copy of an element subtree.
54
+ # @param element [Sevgi::Graphics::Element] source subtree root
55
+ # @param parent [Sevgi::Graphics::Element, Object] parent for the copied root
56
+ # @return [Sevgi::Graphics::Element] copied subtree root
57
+ def self.copy(element, parent = element.parent)
58
+ element.dup.tap do |duplicated|
59
+ duplicated.send(:parent=, parent)
60
+ duplicated.send(:attributes=, element.attributes.dup)
61
+ duplicated.send(:contents=, element.contents.map(&:dup))
62
+ duplicated.send(:children=, element.children.map { |child| copy(child, duplicated) })
63
+ end
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
88
+ end
89
+
90
+ private_constant :Subtree
41
91
  end
42
92
  end
43
93
  end
@@ -8,9 +8,13 @@ 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
- # @raise [Sevgi::Sundries::ExportError] when native export fails
16
+ # @raise [Sevgi::MissingComponentError] when native export gems are unavailable
17
+ # @raise [Sevgi::Sundries::Export::ExportError] when native export fails
14
18
  def PDF(path = nil, **kwargs, &block)
15
19
  begin
16
20
  require "sevgi/sundries"
@@ -27,9 +31,13 @@ module Sevgi
27
31
  # Exports the document as PNG.
28
32
  # @param path [String, nil] output path or directory
29
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
30
37
  # @return [String] output path
31
38
  # @raise [Sevgi::MissingComponentError] when sevgi/sundries is unavailable
32
- # @raise [Sevgi::Sundries::ExportError] when native export fails
39
+ # @raise [Sevgi::MissingComponentError] when native export gems are unavailable
40
+ # @raise [Sevgi::Sundries::Export::ExportError] when native export fails
33
41
  def PNG(path = nil, **kwargs, &block)
34
42
  begin
35
43
  require "sevgi/sundries"
@@ -45,7 +45,9 @@ module Sevgi
45
45
 
46
46
  def build
47
47
  element.Traverse() do |element|
48
- next unless (id = element[:id])
48
+ next unless (value = element[:id])
49
+
50
+ id = Attribute.xml_text(value)
49
51
 
50
52
  if @namespace.key?(id)
51
53
  (@collision[id] ||= [@namespace[id]]) << element
@@ -4,22 +4,48 @@ 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
 
10
27
  # Includes a derendered node matching an id.
28
+ #
29
+ # SVG/XML file content is treated as data and is not evaluated as Ruby source.
11
30
  # @param file [String] source SVG/XML file
12
31
  # @param id [String, Symbol] source node id
13
- # @return [Sevgi::Graphics::Element] included element
32
+ # @return [Sevgi::Graphics::Element, nil] included element, or nil when the selected node produces no graphics
33
+ # output
34
+ # @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
35
+ # absent
14
36
  # @raise [Sevgi::MissingComponentError] when sevgi/derender is unavailable
15
37
  def Include(file, id) = Derender.evaluate_file(file, self, id:)
16
38
 
17
39
  # Includes the children of a derendered node matching an id.
40
+ #
41
+ # SVG/XML file content is treated as data and is not evaluated as Ruby source.
18
42
  # @param file [String] source SVG/XML file
19
43
  # @param id [String, Symbol] source node id
20
44
  # @return [Array<Sevgi::Graphics::Element>] included children
45
+ # @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
46
+ # absent
21
47
  # @raise [Sevgi::MissingComponentError] when sevgi/derender is unavailable
22
- def IncludeChildren(file, id) = Derender.evaluate_file!(file, self, id:)
48
+ def IncludeChildren(file, id) = Derender.evaluate_file_children(file, self, id:)
23
49
  rescue ::LoadError => e
24
50
  raise unless e.path == "sevgi/derender"
25
51
 
@@ -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