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
|
@@ -7,43 +7,75 @@ module Sevgi
|
|
|
7
7
|
# Defensive copy helper for profile metadata snapshots.
|
|
8
8
|
# @api private
|
|
9
9
|
module Snapshot
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
when
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
class << self
|
|
11
|
+
# Captures recursively immutable profile metadata. Mutable non-container values are stringified once.
|
|
12
|
+
# @param value [Object] value to capture
|
|
13
|
+
# @param seen [Hash] container identities on the current traversal path
|
|
14
|
+
# @return [Object] immutable captured value
|
|
15
|
+
# @raise [Sevgi::ArgumentError] when metadata is cyclic, has colliding keys, or cannot be stringified
|
|
16
|
+
def capture(value, seen = {}.compare_by_identity)
|
|
17
|
+
case value
|
|
18
|
+
when ::Hash
|
|
19
|
+
nested(value, seen) { capture_hash(value, seen) }.freeze
|
|
20
|
+
when ::Array
|
|
21
|
+
nested(value, seen) { value.map { capture(it, seen) } }.freeze
|
|
22
|
+
else
|
|
23
|
+
capture_value(value)
|
|
24
|
+
end
|
|
21
25
|
end
|
|
22
|
-
end
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
# Returns a recursively caller-owned copy of captured metadata.
|
|
28
|
+
# @param value [Object] captured value to copy
|
|
29
|
+
# @return [Object] caller-owned copy
|
|
30
|
+
def copy(value)
|
|
31
|
+
case value
|
|
32
|
+
when ::String
|
|
33
|
+
value.dup
|
|
34
|
+
when ::Hash
|
|
35
|
+
value.to_h { |key, item| [copy(key), copy(item)] }
|
|
36
|
+
when ::Array
|
|
37
|
+
value.map { copy(it) }
|
|
38
|
+
else
|
|
39
|
+
value
|
|
40
|
+
end
|
|
35
41
|
end
|
|
36
|
-
end
|
|
37
42
|
|
|
38
|
-
|
|
43
|
+
private
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
def capture_hash(value, seen)
|
|
46
|
+
value.each_with_object({}) do |(key, item), captured|
|
|
47
|
+
key = capture(key, seen)
|
|
48
|
+
ArgumentError.("Document profile metadata keys collide after stringification") if captured.key?(key)
|
|
49
|
+
|
|
50
|
+
captured[key] = capture(item, seen)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def nested(value, seen)
|
|
55
|
+
ArgumentError.("Cyclic document profile metadata is not supported") if seen.key?(value)
|
|
56
|
+
|
|
57
|
+
seen[value] = true
|
|
58
|
+
yield
|
|
59
|
+
ensure
|
|
60
|
+
seen.delete(value)
|
|
61
|
+
end
|
|
45
62
|
|
|
46
|
-
|
|
63
|
+
def capture_value(value)
|
|
64
|
+
case value
|
|
65
|
+
when ::String
|
|
66
|
+
XML.text(value, context: "Document profile metadata").freeze
|
|
67
|
+
when ::Numeric, ::Symbol, ::NilClass, ::TrueClass, ::FalseClass
|
|
68
|
+
XML.text(value, context: "Document profile metadata")
|
|
69
|
+
value
|
|
70
|
+
else
|
|
71
|
+
stringify(value).freeze
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def stringify(value)
|
|
76
|
+
XML.text(value, context: "Document profile metadata")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
47
79
|
end
|
|
48
80
|
|
|
49
81
|
private_constant :Snapshot
|
|
@@ -51,8 +83,10 @@ module Sevgi
|
|
|
51
83
|
# Builds a root SVG element from a document profile.
|
|
52
84
|
# @param document [Symbol, String, Class] profile name or document class
|
|
53
85
|
# @param canvas [Sevgi::Graphics::Canvas, Sevgi::Graphics::Paper, Symbol, String, Sevgi::Undefined, nil] canvas input
|
|
86
|
+
# @yield evaluates the drawing DSL in the new root element
|
|
87
|
+
# @yieldreturn [Object] ignored block result
|
|
54
88
|
# @return [Sevgi::Graphics::Document::Proto] SVG root element
|
|
55
|
-
# @raise [Sevgi::ArgumentError] when the document profile
|
|
89
|
+
# @raise [Sevgi::ArgumentError] when the document profile or root XML attributes are invalid
|
|
56
90
|
def self.call(document, canvas = Undefined, **, &block)
|
|
57
91
|
klass = case document
|
|
58
92
|
when ::Class
|
|
@@ -80,15 +114,22 @@ module Sevgi
|
|
|
80
114
|
private_class_method :canvas_attributes
|
|
81
115
|
|
|
82
116
|
# Defines or returns a document profile class.
|
|
117
|
+
# Profile metadata is captured before class or thread-atomic registry mutation. Mutable non-container attribute
|
|
118
|
+
# values are stringified once during capture. Successful named definitions return the canonical class stored by
|
|
119
|
+
# the registry, including when identical definitions race.
|
|
83
120
|
# @param name [Symbol, String, Sevgi::Undefined] profile name, or Undefined for an anonymous profile
|
|
84
121
|
# @param preambles [Array<String>, nil, Sevgi::Undefined] document preamble lines
|
|
85
|
-
# @param attributes [Hash, Sevgi::Undefined] default root attributes
|
|
122
|
+
# @param attributes [Hash, nil, Sevgi::Undefined] default root attributes
|
|
86
123
|
# @param overwrite [Boolean] true to replace an existing profile
|
|
87
124
|
# @return [Class] document class
|
|
88
|
-
# @raise [Sevgi::ArgumentError] when a
|
|
125
|
+
# @raise [Sevgi::ArgumentError] when a name conflicts or metadata is invalid XML, cyclic, or cannot be stringified
|
|
89
126
|
def self.define(name = Undefined, preambles: Undefined, attributes: Undefined, overwrite: false)
|
|
90
127
|
return anonymous(attributes:, preambles:) if name == Undefined
|
|
91
128
|
|
|
129
|
+
return lookup(name) if preambles == Undefined && attributes == Undefined
|
|
130
|
+
|
|
131
|
+
name = Profile.normalize!(name)
|
|
132
|
+
|
|
92
133
|
if (current = Profile[name])
|
|
93
134
|
reject_conflict(name, current, attributes:, preambles:) unless overwrite
|
|
94
135
|
return current unless overwrite
|
|
@@ -96,6 +137,7 @@ module Sevgi
|
|
|
96
137
|
|
|
97
138
|
attributes, preambles = defaults(attributes:, preambles:)
|
|
98
139
|
Class.new(Base) { document(name, preambles:, attributes:, overwrite:) }
|
|
140
|
+
Registry[name]
|
|
99
141
|
end
|
|
100
142
|
|
|
101
143
|
def self.anonymous(attributes:, preambles:)
|
|
@@ -103,6 +145,11 @@ module Sevgi
|
|
|
103
145
|
Class.new(Base) { document(Undefined, preambles:, attributes:, register: false) }
|
|
104
146
|
end
|
|
105
147
|
|
|
148
|
+
def self.lookup(name)
|
|
149
|
+
name = Profile.normalize!(name)
|
|
150
|
+
Profile[name] || ArgumentError.("Unknown document profile: #{name}")
|
|
151
|
+
end
|
|
152
|
+
|
|
106
153
|
def self.defaults(attributes:, preambles:)
|
|
107
154
|
[attributes == Undefined ? {} : attributes, preambles == Undefined ? nil : preambles]
|
|
108
155
|
end
|
|
@@ -120,47 +167,77 @@ module Sevgi
|
|
|
120
167
|
(preambles == Undefined || Profile.new(nil, preambles:).preambles == profile.preambles)
|
|
121
168
|
end
|
|
122
169
|
|
|
123
|
-
private_class_method :anonymous, :compatible?, :defaults, :reject_conflict
|
|
170
|
+
private_class_method :anonymous, :compatible?, :defaults, :lookup, :reject_conflict
|
|
124
171
|
|
|
125
|
-
#
|
|
172
|
+
# Process-global document profile registry.
|
|
126
173
|
# @api private
|
|
127
|
-
|
|
174
|
+
module Registry
|
|
128
175
|
@available = {}
|
|
176
|
+
@mutex = ::Mutex.new
|
|
129
177
|
|
|
130
178
|
class << self
|
|
131
|
-
|
|
132
|
-
attr_reader :available
|
|
133
|
-
end
|
|
179
|
+
def [](name) = @mutex.synchronize { @available[name] }
|
|
134
180
|
|
|
135
|
-
|
|
136
|
-
# @param name [Object] profile name
|
|
137
|
-
# @return [Class, nil]
|
|
138
|
-
def self.[](name) = (name = normalize(name)) && available[name]
|
|
181
|
+
def available = @mutex.synchronize { @available.dup.freeze }
|
|
139
182
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
183
|
+
def register(name, klass, profile: nil, overwrite: false)
|
|
184
|
+
name = Profile.normalize!(name)
|
|
185
|
+
validate!(name, klass, profile)
|
|
186
|
+
|
|
187
|
+
@mutex.synchronize { store(name, klass, profile, overwrite) }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
def store(name, klass, profile, overwrite)
|
|
193
|
+
if (current = @available[name])
|
|
194
|
+
unless overwrite || current.profile == profile
|
|
195
|
+
ArgumentError.("Document profile already defined differently: #{name}")
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
return current unless overwrite
|
|
152
199
|
end
|
|
153
200
|
|
|
154
|
-
|
|
201
|
+
@available[name] = klass
|
|
155
202
|
end
|
|
156
203
|
|
|
157
|
-
|
|
204
|
+
def validate!(name, klass, profile)
|
|
205
|
+
unless klass.is_a?(::Class) && klass <= Proto
|
|
206
|
+
ArgumentError.("Document profile class must inherit Document::Proto: #{klass}")
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
return if profile.is_a?(Profile) && profile.name == name
|
|
210
|
+
|
|
211
|
+
ArgumentError.("Document profile class has invalid metadata: #{klass}")
|
|
212
|
+
end
|
|
158
213
|
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
private_constant :Registry
|
|
217
|
+
|
|
218
|
+
# Immutable, read-only document profile metadata exposed by document classes. Process-global lookup and registration
|
|
219
|
+
# are thread-atomic. Metadata containers and strings are captured recursively; other mutable attribute values are
|
|
220
|
+
# stringified once during construction.
|
|
221
|
+
# @see Sevgi::Graphics.document
|
|
222
|
+
class Profile
|
|
223
|
+
# Returns a thread-coherent immutable snapshot of registered profile classes.
|
|
224
|
+
# @return [Hash<Symbol, Class>]
|
|
225
|
+
def self.available = Registry.available
|
|
226
|
+
|
|
227
|
+
# Returns a profile class by name from the process-global registry.
|
|
228
|
+
# @param name [Object] profile name
|
|
229
|
+
# @return [Class, nil]
|
|
230
|
+
def self.[](name) = (name = normalize(name)) && Registry[name]
|
|
159
231
|
|
|
160
232
|
# Normalizes a profile name.
|
|
161
233
|
# @param name [Object] profile name
|
|
162
234
|
# @return [Symbol, nil]
|
|
163
|
-
def self.normalize(name)
|
|
235
|
+
def self.normalize(name)
|
|
236
|
+
normalized = name.to_sym if name.respond_to?(:to_sym)
|
|
237
|
+
normalized if normalized.is_a?(::Symbol)
|
|
238
|
+
rescue ::StandardError
|
|
239
|
+
nil
|
|
240
|
+
end
|
|
164
241
|
|
|
165
242
|
# Normalizes a profile name or raises.
|
|
166
243
|
# @param name [Object] profile name
|
|
@@ -173,14 +250,15 @@ module Sevgi
|
|
|
173
250
|
|
|
174
251
|
# Creates profile metadata.
|
|
175
252
|
# @param name [Object, nil] profile name
|
|
176
|
-
# @param attributes [Hash, nil] default root attributes
|
|
253
|
+
# @param attributes [Hash, nil] default root attributes; nil means an empty Hash
|
|
177
254
|
# @param preambles [Array<String>, nil] preamble lines
|
|
178
255
|
# @return [void]
|
|
179
|
-
# @raise [Sevgi::ArgumentError] when name cannot be
|
|
256
|
+
# @raise [Sevgi::ArgumentError] when name or metadata is invalid XML, cyclic, or cannot be stringified
|
|
180
257
|
def initialize(name, attributes: nil, preambles: nil)
|
|
181
258
|
@name = name.nil? ? nil : self.class.normalize!(name)
|
|
182
|
-
|
|
183
|
-
@
|
|
259
|
+
validate_attributes!(attributes)
|
|
260
|
+
@attributes = Snapshot.capture(attributes || {})
|
|
261
|
+
@preambles = capture_preambles(preambles)
|
|
184
262
|
end
|
|
185
263
|
|
|
186
264
|
# Reports strict profile equality.
|
|
@@ -199,28 +277,60 @@ module Sevgi
|
|
|
199
277
|
# Returns preamble lines.
|
|
200
278
|
# @return [Array<String>, nil] mutation-isolated preamble snapshot
|
|
201
279
|
def preambles = Snapshot.copy(@preambles)
|
|
202
|
-
end
|
|
203
280
|
|
|
204
|
-
|
|
281
|
+
private
|
|
282
|
+
|
|
283
|
+
def validate_attributes!(attributes)
|
|
284
|
+
return if attributes.nil?
|
|
285
|
+
|
|
286
|
+
ArgumentError.("Document profile attributes must be a Hash") unless attributes.is_a?(::Hash)
|
|
287
|
+
|
|
288
|
+
normalized = {}
|
|
289
|
+
attributes.each_key do |key|
|
|
290
|
+
id = normalize_attribute!(key)
|
|
291
|
+
ArgumentError.("Document profile attribute names collide after normalization: #{id}") if normalized.key?(id)
|
|
292
|
+
|
|
293
|
+
normalized[id] = true
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def normalize_attribute!(key)
|
|
298
|
+
if key.is_a?(::String) || key.is_a?(::Symbol)
|
|
299
|
+
return XML.name(key, context: "Document profile attribute name").to_sym
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
ArgumentError.("Document profile attribute names must be Strings or Symbols")
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def capture_preambles(preambles)
|
|
306
|
+
return if preambles.nil?
|
|
307
|
+
|
|
308
|
+
unless preambles.is_a?(::Array) && preambles.all?(::String)
|
|
309
|
+
ArgumentError.("Document profile preambles must be an Array of Strings")
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
Snapshot.capture(preambles)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
205
315
|
|
|
206
316
|
# Class-level DSL used while defining document classes.
|
|
207
317
|
# @api private
|
|
208
318
|
module DSL
|
|
209
|
-
# @return [Sevgi::Graphics::Document::Profile] document profile metadata
|
|
319
|
+
# @return [Sevgi::Graphics::Document::Profile] immutable document profile metadata
|
|
210
320
|
attr_reader :profile
|
|
211
321
|
|
|
212
322
|
# Sets document profile metadata on a class.
|
|
213
323
|
# @param name [Object] profile name
|
|
214
|
-
# @param attributes [Hash] default root attributes
|
|
324
|
+
# @param attributes [Hash, nil] default root attributes
|
|
215
325
|
# @param preambles [Array<String>, nil] preamble lines
|
|
216
326
|
# @param register [Boolean] true to register the profile globally
|
|
217
327
|
# @param overwrite [Boolean] true to replace an existing profile
|
|
218
|
-
# @return [Sevgi::Graphics::Document::Profile]
|
|
219
|
-
# @raise [Sevgi::ArgumentError] when registration fails
|
|
328
|
+
# @return [Sevgi::Graphics::Document::Profile] immutable document profile metadata
|
|
329
|
+
# @raise [Sevgi::ArgumentError] when registration fails or metadata is invalid XML, cyclic, or cannot be stringified
|
|
220
330
|
def document(name, attributes: {}, preambles: nil, register: true, overwrite: false)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
@profile
|
|
331
|
+
profile = Profile.new(register ? name : nil, attributes:, preambles:)
|
|
332
|
+
Registry.register(name, self, profile:, overwrite:) if register
|
|
333
|
+
@profile = profile
|
|
224
334
|
end
|
|
225
335
|
|
|
226
336
|
# Includes a graphics mixture into the document class.
|
|
@@ -255,15 +365,17 @@ module Sevgi
|
|
|
255
365
|
# @param objects [Array<Object>] optional renderer arguments
|
|
256
366
|
# @param options [Hash] render options
|
|
257
367
|
# @return [String] SVG document source
|
|
368
|
+
# @raise [Sevgi::ArgumentError] when renderer options or XML-bound values are invalid
|
|
258
369
|
def call(*, **)
|
|
259
370
|
options = DEFAULTS.merge(**)
|
|
260
371
|
|
|
261
372
|
self.PreRender(*, **options) if respond_to?(:PreRender)
|
|
262
|
-
|
|
373
|
+
render_options = options.reject { |key, _| DEFAULTS.key?(key) }
|
|
374
|
+
self.Render(*, **render_options)
|
|
263
375
|
end
|
|
264
376
|
|
|
265
377
|
# Returns inherited root attributes for this document class.
|
|
266
|
-
# @return [Hash]
|
|
378
|
+
# @return [Hash{Symbol => Object}] inherited root attributes
|
|
267
379
|
def self.attributes = self == Proto ? {} : {**superclass.attributes, **profile.attributes}
|
|
268
380
|
|
|
269
381
|
# Returns inherited preamble lines for this document class.
|
|
@@ -3,17 +3,27 @@
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Graphics
|
|
5
5
|
# SVG element node used by the graphics DSL.
|
|
6
|
+
#
|
|
7
|
+
# @!method self.valid?(name)
|
|
8
|
+
# Reports whether an SVG element name is known.
|
|
9
|
+
# @param name [Symbol] SVG element name
|
|
10
|
+
# @return [Boolean]
|
|
6
11
|
class Element
|
|
7
12
|
# Builds an element node.
|
|
8
13
|
# @param name [Symbol, String] SVG element name
|
|
9
14
|
# @param parent [Sevgi::Graphics::Element] parent element
|
|
15
|
+
# @yield evaluates the drawing DSL in the new element
|
|
16
|
+
# @yieldreturn [Object] ignored block result
|
|
10
17
|
# @return [Sevgi::Graphics::Element]
|
|
11
18
|
# @raise [Sevgi::ArgumentError] when an argument cannot be parsed as attributes or content
|
|
19
|
+
# @raise [Sevgi::ArgumentError] when the element name or an attribute is not valid XML
|
|
12
20
|
def self.element(name, *, parent:, &block) = new(name, **Dispatch.parse(name, *), parent:, &block)
|
|
13
21
|
|
|
14
22
|
# Builds an SVG root element.
|
|
15
|
-
# @
|
|
23
|
+
# @yield evaluates the drawing DSL in the root element
|
|
24
|
+
# @yieldreturn [Object] ignored block result
|
|
16
25
|
# @return [Sevgi::Graphics::Element]
|
|
26
|
+
# @raise [Sevgi::ArgumentError] when a root attribute is not valid XML
|
|
17
27
|
def self.root(*, &block) = element(:svg, *, parent: RootParent, &block)
|
|
18
28
|
|
|
19
29
|
# Reports whether an element is the root element.
|
|
@@ -52,26 +62,41 @@ module Sevgi
|
|
|
52
62
|
|
|
53
63
|
extend Ident
|
|
54
64
|
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
attr_reader :
|
|
65
|
+
# Returns the SVG element name.
|
|
66
|
+
# @return [Symbol]
|
|
67
|
+
attr_reader :name
|
|
68
|
+
|
|
69
|
+
# Returns the attribute store.
|
|
70
|
+
# @return [Sevgi::Graphics::Attributes]
|
|
71
|
+
attr_reader :attributes
|
|
72
|
+
|
|
73
|
+
# Returns child elements.
|
|
74
|
+
# @return [Array<Sevgi::Graphics::Element>]
|
|
75
|
+
attr_reader :children
|
|
76
|
+
|
|
77
|
+
# Returns element content objects.
|
|
78
|
+
# @return [Array<Sevgi::Graphics::Content>]
|
|
79
|
+
attr_reader :contents
|
|
80
|
+
|
|
81
|
+
# Returns the parent element or root sentinel.
|
|
82
|
+
# @return [Sevgi::Graphics::Element, Object] parent element or root sentinel
|
|
83
|
+
attr_reader :parent
|
|
66
84
|
|
|
67
85
|
# Creates an element.
|
|
68
86
|
# @param name [Symbol] SVG element name
|
|
69
87
|
# @param parent [Sevgi::Graphics::Element, Object] parent element or root sentinel
|
|
70
88
|
# @param attributes [Hash] SVG attributes
|
|
71
89
|
# @param contents [Array<Sevgi::Graphics::Content>] content objects
|
|
90
|
+
# @yield evaluates the drawing DSL in the new element
|
|
91
|
+
# @yieldreturn [Object] ignored block result
|
|
72
92
|
# @return [void]
|
|
93
|
+
# @raise [Sevgi::ArgumentError] when the element name or an attribute is not valid XML
|
|
73
94
|
def initialize(name, parent:, attributes: {}, contents: [], &block)
|
|
74
|
-
|
|
95
|
+
unless name.is_a?(::String) || name.is_a?(::Symbol)
|
|
96
|
+
ArgumentError.("XML element name must be a String or Symbol")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
@name = XML.name(name, context: "XML element name").to_sym
|
|
75
100
|
@attributes = Attributes.new(attributes)
|
|
76
101
|
@children = []
|
|
77
102
|
@contents = contents
|
|
@@ -84,6 +109,8 @@ module Sevgi
|
|
|
84
109
|
|
|
85
110
|
# Dispatches SVG element DSL calls and caches valid element methods.
|
|
86
111
|
# @param name [Symbol] missing method name
|
|
112
|
+
# @yield evaluates the drawing DSL in the dispatched child element
|
|
113
|
+
# @yieldreturn [Object] ignored block result
|
|
87
114
|
# @return [Sevgi::Graphics::Element]
|
|
88
115
|
# @raise [NameError] when the name is not a valid SVG element
|
|
89
116
|
# @raise [Sevgi::ArgumentError] when an argument cannot be parsed as attributes or content
|
|
@@ -95,6 +122,7 @@ module Sevgi
|
|
|
95
122
|
# @param name [Symbol] queried method name
|
|
96
123
|
# @param include_private [Boolean] standard Ruby respond_to? flag
|
|
97
124
|
# @return [Boolean]
|
|
125
|
+
# @api private
|
|
98
126
|
def respond_to_missing?(name, include_private = false)
|
|
99
127
|
Element.valid?(Element.id(name)) || super
|
|
100
128
|
end
|
|
@@ -110,7 +138,6 @@ module Sevgi
|
|
|
110
138
|
# @param tag [Symbol] SVG element name
|
|
111
139
|
# @return [Sevgi::Graphics::Element]
|
|
112
140
|
def call(element, method, tag, *, &)
|
|
113
|
-
# Low-hanging fruit optimization: define missing method to avoid dispatching cost
|
|
114
141
|
unless Element.method_defined?(method)
|
|
115
142
|
Element.class_exec do
|
|
116
143
|
define_method(method) { |*args, &block| self.class.element(tag, *args, parent: self, &block) }
|