sevgi-graphics 0.95.0 → 1.0.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 +221 -2
- data/README.md +12 -9
- data/lib/sevgi/graphics/attribute.rb +166 -45
- data/lib/sevgi/graphics/auxiliary/canvas.rb +100 -43
- data/lib/sevgi/graphics/auxiliary/content.rb +56 -47
- data/lib/sevgi/graphics/auxiliary/margin.rb +19 -12
- data/lib/sevgi/graphics/auxiliary/paper.rb +74 -49
- data/lib/sevgi/graphics/auxiliary/path.rb +44 -0
- data/lib/sevgi/graphics/auxiliary/scalar.rb +36 -7
- data/lib/sevgi/graphics/auxiliary.rb +1 -0
- data/lib/sevgi/graphics/document/base.rb +6 -2
- data/lib/sevgi/graphics/document/default.rb +1 -1
- data/lib/sevgi/graphics/document.rb +239 -117
- data/lib/sevgi/graphics/element.rb +132 -34
- data/lib/sevgi/graphics/mixtures/call.rb +234 -88
- data/lib/sevgi/graphics/mixtures/core.rb +67 -27
- data/lib/sevgi/graphics/mixtures/duplicate.rb +47 -25
- data/lib/sevgi/graphics/mixtures/export.rb +54 -12
- data/lib/sevgi/graphics/mixtures/hatch.rb +49 -7
- data/lib/sevgi/graphics/mixtures/identify.rb +30 -17
- data/lib/sevgi/graphics/mixtures/include.rb +26 -8
- data/lib/sevgi/graphics/mixtures/inkscape.rb +214 -47
- data/lib/sevgi/graphics/mixtures/rdf.rb +59 -7
- data/lib/sevgi/graphics/mixtures/render.rb +60 -120
- data/lib/sevgi/graphics/mixtures/save.rb +79 -35
- data/lib/sevgi/graphics/mixtures/symbols.rb +81 -12
- data/lib/sevgi/graphics/mixtures/tile.rb +85 -67
- data/lib/sevgi/graphics/mixtures/transform.rb +89 -30
- data/lib/sevgi/graphics/mixtures/underscore.rb +16 -7
- data/lib/sevgi/graphics/mixtures/validate.rb +2 -2
- data/lib/sevgi/graphics/mixtures/wrappers.rb +111 -23
- data/lib/sevgi/graphics/mixtures.rb +15 -13
- data/lib/sevgi/graphics/version.rb +1 -1
- data/lib/sevgi/graphics/xml.rb +4 -9
- data/lib/sevgi/graphics.rb +69 -18
- metadata +7 -6
|
@@ -5,51 +5,115 @@ module Sevgi
|
|
|
5
5
|
# SVG element node used by the graphics DSL.
|
|
6
6
|
#
|
|
7
7
|
# @!method self.valid?(name)
|
|
8
|
-
# Reports whether an SVG element name
|
|
9
|
-
#
|
|
8
|
+
# Reports whether a candidate can dispatch as an SVG element name.
|
|
9
|
+
# With Standard loaded, the name must be known. Standalone Graphics accepts any valid XML name.
|
|
10
|
+
# @param name [Object] candidate element name
|
|
10
11
|
# @return [Boolean]
|
|
12
|
+
#
|
|
13
|
+
# Dynamic SVG element methods accept text, content objects, and any number of attribute Hashes in one call. Hashes
|
|
14
|
+
# are applied from left to right. Later values replace earlier values unless their names use the `+` update suffix.
|
|
15
|
+
# Ordinary `dup` and `clone` create independent subtrees, preserve IDs, and detach non-root copies from their parent.
|
|
16
|
+
# Document copies remain roots. Use `Duplicate` for DSL-specific ID remapping and attachment.
|
|
11
17
|
class Element
|
|
12
18
|
# Builds an element node.
|
|
13
19
|
# @param name [Symbol, String] SVG element name
|
|
20
|
+
# @param arguments [Array<Hash, String, Sevgi::Graphics::Content>] ordered content and attribute channels. Every
|
|
21
|
+
# Hash is applied through {Sevgi::Graphics::Attributes} from left to right
|
|
14
22
|
# @param parent [Sevgi::Graphics::Element] parent element
|
|
15
23
|
# @yield evaluates the drawing DSL in the new element
|
|
16
24
|
# @yieldreturn [Object] ignored block result
|
|
17
25
|
# @return [Sevgi::Graphics::Element]
|
|
18
26
|
# @raise [Sevgi::ArgumentError] when an argument cannot be parsed as attributes or content
|
|
27
|
+
# @raise [Sevgi::ArgumentError] when the parent has a different concrete element class
|
|
19
28
|
# @raise [Sevgi::ArgumentError] when the element name or an attribute is not valid XML
|
|
20
|
-
def self.element(name,
|
|
29
|
+
def self.element(name, *arguments, parent:, &block)
|
|
30
|
+
validate_parent_class(self, parent)
|
|
31
|
+
new(name, **Dispatch.parse(name, *arguments), parent:, &block)
|
|
32
|
+
end
|
|
21
33
|
|
|
22
34
|
# Builds an SVG root element.
|
|
35
|
+
# @param arguments [Array<Hash, String, Sevgi::Graphics::Content>] ordered content and attribute channels
|
|
23
36
|
# @yield evaluates the drawing DSL in the root element
|
|
24
37
|
# @yieldreturn [Object] ignored block result
|
|
25
38
|
# @return [Sevgi::Graphics::Element]
|
|
26
|
-
# @raise [Sevgi::ArgumentError] when
|
|
27
|
-
|
|
39
|
+
# @raise [Sevgi::ArgumentError] when an argument is not a Hash, String, or Content
|
|
40
|
+
# @raise [Sevgi::ArgumentError] when a root attribute or content value is not valid XML
|
|
41
|
+
def self.root(*arguments, &block) = element(:svg, *arguments, parent: RootParent, &block)
|
|
28
42
|
|
|
29
43
|
# Reports whether an element is the root element.
|
|
30
44
|
# @param element [Sevgi::Graphics::Element] element to test
|
|
31
45
|
# @return [Boolean]
|
|
32
|
-
def self.root?(element) = element.
|
|
46
|
+
def self.root?(element) = Element.send(:tree_parent, element).equal?(RootParent)
|
|
47
|
+
|
|
48
|
+
def self.element_method?(name) = Dispatch.cached?(name)
|
|
49
|
+
|
|
50
|
+
private_class_method :element_method?
|
|
51
|
+
|
|
52
|
+
class << self
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def attach(element, parent, index: nil)
|
|
56
|
+
children = tree_children(parent)
|
|
57
|
+
index ? children.insert(index, element) : children << element
|
|
58
|
+
element.instance_variable_set(:@parent, parent)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def detach(element)
|
|
62
|
+
parent = tree_parent(element)
|
|
63
|
+
tree_children(parent).delete(element) if parent.is_a?(element.class)
|
|
64
|
+
element.instance_variable_set(:@parent, DetachedParent)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def tree_children(element) = element.instance_variable_get(:@children)
|
|
68
|
+
|
|
69
|
+
def tree_parent(element) = element.instance_variable_get(:@parent)
|
|
70
|
+
|
|
71
|
+
def validate_parent(element, parent)
|
|
72
|
+
validate_parent_class(element.class, parent)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def validate_parent_class(element_class, parent)
|
|
76
|
+
return if parent.equal?(RootParent) || parent.instance_of?(element_class)
|
|
77
|
+
|
|
78
|
+
ArgumentError.("Element type does not match the parent type: #{element_class}")
|
|
79
|
+
end
|
|
80
|
+
end
|
|
33
81
|
|
|
34
82
|
class << self
|
|
35
83
|
require "sevgi/standard"
|
|
36
84
|
|
|
37
|
-
# Reports whether
|
|
38
|
-
# @param name [
|
|
85
|
+
# Reports whether a candidate can dispatch as a known SVG element name.
|
|
86
|
+
# @param name [Object] candidate element name
|
|
39
87
|
# @return [Boolean]
|
|
40
|
-
def valid?(name)
|
|
88
|
+
def valid?(name)
|
|
89
|
+
Standard.element?(name)
|
|
90
|
+
rescue Sevgi::ArgumentError
|
|
91
|
+
false
|
|
92
|
+
end
|
|
93
|
+
|
|
41
94
|
rescue ::LoadError => e
|
|
42
95
|
raise unless e.path == "sevgi/standard"
|
|
43
96
|
|
|
44
|
-
# Reports whether
|
|
97
|
+
# Reports whether a candidate can dispatch as a valid XML element name.
|
|
98
|
+
# @param name [Object] candidate element name
|
|
45
99
|
# @return [Boolean]
|
|
46
|
-
def valid?(
|
|
100
|
+
def valid?(name)
|
|
101
|
+
return false unless name.is_a?(::String) || name.is_a?(::Symbol)
|
|
102
|
+
|
|
103
|
+
XML.name(name, context: "SVG element name")
|
|
104
|
+
true
|
|
105
|
+
rescue Sevgi::ArgumentError
|
|
106
|
+
false
|
|
107
|
+
end
|
|
47
108
|
end
|
|
48
109
|
|
|
49
110
|
private_class_method :new
|
|
50
111
|
|
|
51
|
-
# Sentinel
|
|
112
|
+
# Sentinel parents used by root and detached elements.
|
|
52
113
|
RootParent = Object.new.tap { def it.inspect = "RootParent" }.freeze
|
|
114
|
+
DetachedParent = Object.new.tap { def it.inspect = "DetachedParent" }.freeze
|
|
115
|
+
|
|
116
|
+
private_constant :DetachedParent, :RootParent
|
|
53
117
|
|
|
54
118
|
# SVG element method-name normalization.
|
|
55
119
|
# @api private
|
|
@@ -61,6 +125,8 @@ module Sevgi
|
|
|
61
125
|
end
|
|
62
126
|
|
|
63
127
|
extend Ident
|
|
128
|
+
private_class_method :id
|
|
129
|
+
private_constant :Ident
|
|
64
130
|
|
|
65
131
|
# Returns the SVG element name.
|
|
66
132
|
# @return [Symbol]
|
|
@@ -70,17 +136,20 @@ module Sevgi
|
|
|
70
136
|
# @return [Sevgi::Graphics::Attributes]
|
|
71
137
|
attr_reader :attributes
|
|
72
138
|
|
|
73
|
-
# Returns child elements.
|
|
74
|
-
# @return [Array<Sevgi::Graphics::Element>]
|
|
75
|
-
|
|
139
|
+
# Returns a read-only snapshot of child elements in rendering order.
|
|
140
|
+
# @return [Array<Sevgi::Graphics::Element>] frozen child snapshot
|
|
141
|
+
def children = @children.dup.freeze
|
|
76
142
|
|
|
77
|
-
# Returns element content objects.
|
|
78
|
-
# @return [Array<Sevgi::Graphics::Content>]
|
|
79
|
-
|
|
143
|
+
# Returns a read-only snapshot of element content objects in rendering order.
|
|
144
|
+
# @return [Array<Sevgi::Graphics::Content>] frozen content snapshot
|
|
145
|
+
def contents = @contents.dup.freeze
|
|
80
146
|
|
|
81
|
-
# Returns the parent element
|
|
82
|
-
# @return [Sevgi::Graphics::Element,
|
|
83
|
-
|
|
147
|
+
# Returns the parent element.
|
|
148
|
+
# @return [Sevgi::Graphics::Element, nil] parent element, or nil for a root or detached element
|
|
149
|
+
# @note Use `Root?` to distinguish a document root from a detached subtree root.
|
|
150
|
+
def parent
|
|
151
|
+
@parent if @parent.is_a?(self.class)
|
|
152
|
+
end
|
|
84
153
|
|
|
85
154
|
# Creates an element.
|
|
86
155
|
# @param name [Symbol] SVG element name
|
|
@@ -90,8 +159,12 @@ module Sevgi
|
|
|
90
159
|
# @yield evaluates the drawing DSL in the new element
|
|
91
160
|
# @yieldreturn [Object] ignored block result
|
|
92
161
|
# @return [void]
|
|
162
|
+
# @raise [Sevgi::ArgumentError] when the parent has a different concrete element class
|
|
93
163
|
# @raise [Sevgi::ArgumentError] when the element name or an attribute is not valid XML
|
|
164
|
+
# @api private
|
|
94
165
|
def initialize(name, parent:, attributes: {}, contents: [], &block)
|
|
166
|
+
Element.send(:validate_parent, self, parent)
|
|
167
|
+
|
|
95
168
|
unless name.is_a?(::String) || name.is_a?(::Symbol)
|
|
96
169
|
ArgumentError.("XML element name must be a String or Symbol")
|
|
97
170
|
end
|
|
@@ -99,23 +172,41 @@ module Sevgi
|
|
|
99
172
|
@name = XML.name(name, context: "XML element name").to_sym
|
|
100
173
|
@attributes = Attributes.new(attributes)
|
|
101
174
|
@children = []
|
|
102
|
-
@contents = contents
|
|
175
|
+
@contents = contents.dup
|
|
103
176
|
@parent = parent
|
|
104
177
|
|
|
105
|
-
|
|
178
|
+
Element.send(:attach, self, parent) unless Element.root?(self)
|
|
106
179
|
|
|
107
180
|
instance_exec(&block) if block
|
|
108
181
|
end
|
|
109
182
|
|
|
183
|
+
# Copies owned storage and reconnects child copies without attaching to the source tree.
|
|
184
|
+
# @param original [Sevgi::Graphics::Element] source node
|
|
185
|
+
# @return [void]
|
|
186
|
+
# @api private
|
|
187
|
+
def initialize_copy(original)
|
|
188
|
+
super
|
|
189
|
+
@parent = Element.root?(original) ? RootParent : DetachedParent
|
|
190
|
+
@attributes = original.attributes.dup
|
|
191
|
+
@contents = original.contents.map(&:dup)
|
|
192
|
+
@children = original.children.map do |child|
|
|
193
|
+
child.dup.tap { it.instance_variable_set(:@parent, self) }
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
private :initialize_copy
|
|
198
|
+
|
|
110
199
|
# Dispatches SVG element DSL calls and caches valid element methods.
|
|
111
200
|
# @param name [Symbol] missing method name
|
|
201
|
+
# @param arguments [Array<Hash, String, Sevgi::Graphics::Content>] ordered content and attribute channels. Later
|
|
202
|
+
# Hashes replace or update attributes assigned by earlier Hashes
|
|
112
203
|
# @yield evaluates the drawing DSL in the dispatched child element
|
|
113
204
|
# @yieldreturn [Object] ignored block result
|
|
114
205
|
# @return [Sevgi::Graphics::Element]
|
|
115
206
|
# @raise [NameError] when the name is not a valid SVG element
|
|
116
207
|
# @raise [Sevgi::ArgumentError] when an argument cannot be parsed as attributes or content
|
|
117
|
-
def method_missing(name,
|
|
118
|
-
Element.valid?(tag = Element.id
|
|
208
|
+
def method_missing(name, *arguments, &block)
|
|
209
|
+
Element.valid?(tag = Element.send(:id, name)) ? Dispatch.(self, name, tag, *arguments, &block) : super
|
|
119
210
|
end
|
|
120
211
|
|
|
121
212
|
# Reports whether a missing method can dispatch to an SVG element.
|
|
@@ -124,9 +215,11 @@ module Sevgi
|
|
|
124
215
|
# @return [Boolean]
|
|
125
216
|
# @api private
|
|
126
217
|
def respond_to_missing?(name, include_private = false)
|
|
127
|
-
Element.valid?(Element.id
|
|
218
|
+
Element.valid?(Element.send(:id, name)) || super
|
|
128
219
|
end
|
|
129
220
|
|
|
221
|
+
private :method_missing, :respond_to_missing?
|
|
222
|
+
|
|
130
223
|
# Element method-missing parser and cache.
|
|
131
224
|
# @api private
|
|
132
225
|
module Dispatch
|
|
@@ -142,41 +235,46 @@ module Sevgi
|
|
|
142
235
|
Element.class_exec do
|
|
143
236
|
define_method(method) { |*args, &block| self.class.element(tag, *args, parent: self, &block) }
|
|
144
237
|
end
|
|
238
|
+
|
|
239
|
+
(@methods ||= {})[method] = Element.instance_method(method)
|
|
145
240
|
end
|
|
146
241
|
|
|
147
242
|
element.public_send(method, *, &)
|
|
148
243
|
end
|
|
149
244
|
|
|
245
|
+
def cached?(method)
|
|
246
|
+
cached = @methods&.[](method)
|
|
247
|
+
|
|
248
|
+
cached && Element.method_defined?(method) && Element.instance_method(method) == cached
|
|
249
|
+
end
|
|
250
|
+
|
|
150
251
|
# Parses element DSL arguments.
|
|
151
252
|
# @param name [Symbol] SVG element name
|
|
152
|
-
# @param args [Array<Object>] positional DSL arguments
|
|
253
|
+
# @param args [Array<Object>] positional DSL arguments. Hashes are imported from left to right
|
|
153
254
|
# @return [Hash] parsed :attributes and :contents
|
|
154
255
|
# @raise [Sevgi::ArgumentError] when an argument is not a Hash, String, or Content
|
|
155
256
|
def parse(name, *args)
|
|
156
|
-
attributes, contents =
|
|
257
|
+
attributes, contents = Attributes.new, []
|
|
157
258
|
|
|
158
259
|
args.each do |arg|
|
|
159
260
|
case arg
|
|
160
261
|
when ::Hash
|
|
161
|
-
attributes
|
|
262
|
+
attributes.merge!(arg)
|
|
162
263
|
when ::String
|
|
163
264
|
contents << Content.encoded(arg)
|
|
164
265
|
when Content
|
|
165
266
|
contents << arg
|
|
166
267
|
else
|
|
167
|
-
ArgumentError.("Argument of element '#{name}' must be a Hash or
|
|
268
|
+
ArgumentError.("Argument of element '#{name}' must be a Hash, String, or Content: #{arg}")
|
|
168
269
|
end
|
|
169
270
|
end
|
|
170
271
|
|
|
171
|
-
{attributes
|
|
272
|
+
{attributes: attributes.to_h, contents:}
|
|
172
273
|
end
|
|
173
274
|
end
|
|
174
275
|
|
|
175
276
|
private_constant :Dispatch
|
|
176
277
|
|
|
177
|
-
protected
|
|
178
|
-
|
|
179
|
-
attr_writer :attributes, :children, :contents, :parent
|
|
180
278
|
end
|
|
181
279
|
end
|
|
182
280
|
end
|
|
@@ -3,95 +3,266 @@
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Graphics
|
|
5
5
|
# Callable drawing module support.
|
|
6
|
-
# Extend a plain Ruby module with this API to make its public instance methods callable drawing steps.
|
|
6
|
+
# Extend a plain Ruby module with this API to make its public instance methods callable drawing steps. Name the
|
|
7
|
+
# method `call` when the module has a single drawing step. Use descriptive method names for multiple steps.
|
|
8
|
+
# Base blocks add argument-independent shared SVG content once per invocation before the public drawing methods.
|
|
9
|
+
# Invocation does not change the configured module, so it can be frozen after its drawing steps are defined. A
|
|
10
|
+
# duplicate or clone owns an independent configuration snapshot. Freezing a callable module prevents later base
|
|
11
|
+
# registration while leaving invocation available.
|
|
7
12
|
# @example Define and call a drawing module
|
|
8
13
|
# Widget = Module.new do
|
|
9
14
|
# extend Sevgi::Graphics::Module
|
|
10
15
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
16
|
+
# base { css ".widget" => { fill: "red" } }
|
|
17
|
+
#
|
|
18
|
+
# def call(id)
|
|
19
|
+
# draw id
|
|
13
20
|
# end
|
|
21
|
+
#
|
|
22
|
+
# private
|
|
23
|
+
#
|
|
24
|
+
# def draw(id) = rect id:, class: "widget"
|
|
14
25
|
# end
|
|
15
26
|
#
|
|
16
|
-
# SVG { Call
|
|
27
|
+
# Sevgi::Graphics.SVG { Call Widget, "box" }
|
|
17
28
|
module Module
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# @
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
# Ephemeral callable receiver that preserves a module's normal method lookup while forwarding drawing operations
|
|
30
|
+
# to the current element.
|
|
31
|
+
# @api private
|
|
32
|
+
class Context < ::BasicObject
|
|
33
|
+
# Creates a delegated callable receiver.
|
|
34
|
+
# @param callable [Module] callable module
|
|
35
|
+
# @param receiver [Sevgi::Graphics::Element] current drawing element
|
|
36
|
+
# @return [void]
|
|
37
|
+
def initialize(callable, receiver)
|
|
38
|
+
@callable = callable
|
|
39
|
+
@receiver = receiver
|
|
40
|
+
end
|
|
24
41
|
|
|
25
|
-
|
|
42
|
+
# Reports methods available through either callable composition or the drawing element.
|
|
43
|
+
# @param name [Symbol, String] method name
|
|
44
|
+
# @param include_private [Boolean] whether private methods count
|
|
45
|
+
# @return [Boolean]
|
|
46
|
+
def respond_to?(name, include_private = false) = respond_to_missing?(name, include_private)
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Reports methods available through callable composition or receiver delegation.
|
|
51
|
+
# @param name [Symbol, String] method name
|
|
52
|
+
# @param include_private [Boolean] whether private methods count
|
|
53
|
+
# @return [Boolean]
|
|
54
|
+
def respond_to_missing?(name, include_private = false)
|
|
55
|
+
@callable.public_method_defined?(name) ||
|
|
56
|
+
(include_private &&
|
|
57
|
+
(@callable.protected_method_defined?(name) || @callable.private_method_defined?(name))) ||
|
|
58
|
+
@receiver.respond_to?(name, include_private)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Forwards operations outside the callable module's lookup chain to the drawing element.
|
|
62
|
+
# @param name [Symbol] missing method name
|
|
63
|
+
# @param arguments [Array<Object>] positional arguments
|
|
64
|
+
# @param keywords [Hash] keyword arguments
|
|
65
|
+
# @param block [Proc, nil] forwarded block
|
|
66
|
+
# @return [Object] delegated result
|
|
67
|
+
def method_missing(name, *arguments, **keywords, &block)
|
|
68
|
+
@receiver.__send__(name, *arguments, **keywords, &block)
|
|
69
|
+
end
|
|
26
70
|
end
|
|
27
71
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
72
|
+
private_constant :Context
|
|
73
|
+
|
|
74
|
+
# Registers argument-independent shared drawing steps. Every invocation runs inherited base blocks parent-first,
|
|
75
|
+
# then locally registered base blocks in registration order, before the module's public drawing methods. The block
|
|
76
|
+
# runs once on the callable receiver, which delegates drawing words to the current SVG element. It receives no
|
|
77
|
+
# invocation arguments. Callable methods receive those arguments and the caller's block unchanged.
|
|
78
|
+
# @yield evaluates shared drawing steps on the callable receiver
|
|
79
|
+
# @yieldreturn [Object] ignored block result
|
|
80
|
+
# @return [nil]
|
|
81
|
+
# @raise [Sevgi::ArgumentError] when no block is given
|
|
82
|
+
# @raise [FrozenError] when the callable module is frozen
|
|
83
|
+
def base(&block)
|
|
84
|
+
raise ::FrozenError, "can't modify frozen callable module" if frozen?
|
|
85
|
+
|
|
86
|
+
ArgumentError.("Block required") unless block
|
|
87
|
+
|
|
88
|
+
own_configuration
|
|
89
|
+
@sevgi_bases << block
|
|
90
|
+
nil
|
|
35
91
|
end
|
|
36
92
|
|
|
37
|
-
|
|
93
|
+
class << self
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
# Initializes callable module state.
|
|
97
|
+
# @param base [Module] extended module
|
|
98
|
+
# @return [void]
|
|
99
|
+
def extended(base)
|
|
100
|
+
base.instance_variable_set(:@sevgi_bases, [])
|
|
101
|
+
base.instance_variable_set(:@sevgi_callables, base.public_instance_methods(false))
|
|
102
|
+
base.instance_variable_set(:@sevgi_configuration_owner, base.object_id)
|
|
103
|
+
end
|
|
38
104
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
105
|
+
# Returns an owned snapshot of inherited and local base blocks in execution order.
|
|
106
|
+
# @param mod [Module] module extended with {Sevgi::Graphics::Module}
|
|
107
|
+
# @return [Array<Proc>] parent-first base blocks followed by local base blocks
|
|
108
|
+
# @raise [Sevgi::ArgumentError] when mod is not a callable drawing module
|
|
109
|
+
def bases(mod)
|
|
110
|
+
validate(mod)
|
|
111
|
+
|
|
112
|
+
mod
|
|
113
|
+
.ancestors
|
|
114
|
+
.reverse_each
|
|
115
|
+
.filter_map do |ancestor|
|
|
116
|
+
ancestor.instance_variable_get(:@sevgi_bases) if ancestor.instance_variable_defined?(:@sevgi_bases)
|
|
117
|
+
end
|
|
118
|
+
.flatten
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @overload call(mod, receiver, *args, **kwargs)
|
|
122
|
+
# Runs module bases and callables against a receiver.
|
|
123
|
+
# @param mod [Module] module extended with {Sevgi::Graphics::Module}
|
|
124
|
+
# @param receiver [Sevgi::Graphics::Element] receiver element
|
|
125
|
+
# @param args [Array<Object>] callable arguments
|
|
126
|
+
# @param kwargs [Hash] callable keyword arguments
|
|
127
|
+
# @return [Object, nil] last callable return value
|
|
128
|
+
# @raise [Sevgi::ArgumentError] when mod is not a callable drawing module
|
|
129
|
+
def call(mod, receiver, ...)
|
|
130
|
+
methods = callables(mod)
|
|
131
|
+
context = context(mod, receiver)
|
|
132
|
+
bases(mod).each { context.instance_exec(&it) }
|
|
133
|
+
|
|
134
|
+
invoke(context, receiver, methods, ...)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Returns the executable drawing methods for a callable module.
|
|
138
|
+
# @param mod [Module] module extended with {Sevgi::Graphics::Module}
|
|
139
|
+
# @return [Array<UnboundMethod>]
|
|
140
|
+
# @raise [Sevgi::ArgumentError] when mod is not a callable drawing module
|
|
141
|
+
def callables(mod)
|
|
142
|
+
validate(mod)
|
|
143
|
+
|
|
144
|
+
callable_names(mod).uniq.filter_map do |name|
|
|
145
|
+
mod.instance_method(name) if mod.public_method_defined?(name)
|
|
50
146
|
end
|
|
147
|
+
end
|
|
51
148
|
|
|
52
|
-
|
|
149
|
+
def callable_names(mod)
|
|
150
|
+
mod.ancestors.reverse_each.flat_map do |ancestor|
|
|
151
|
+
if ancestor.instance_variable_defined?(:@sevgi_callables)
|
|
152
|
+
ancestor.instance_variable_get(:@sevgi_callables) + ancestor.public_instance_methods(false)
|
|
153
|
+
else
|
|
154
|
+
ancestor.public_instance_methods(false)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def context(mod, receiver)
|
|
160
|
+
::Class.new(Context) { include(mod) }.new(mod, receiver)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def invoke(context, receiver, methods, ...)
|
|
164
|
+
result = methods.map { context.__send__(it.name, ...) }.last
|
|
165
|
+
result.equal?(context) ? receiver : result
|
|
53
166
|
end
|
|
54
|
-
end
|
|
55
167
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# @param args [Array<Object>] callable arguments
|
|
61
|
-
# @param kwargs [Hash] callable keyword arguments
|
|
62
|
-
# @return [Object, nil] last callable return value
|
|
63
|
-
# @raise [Sevgi::ArgumentError] when mod is not a plain module
|
|
64
|
-
def self.call(mod, receiver, ...)
|
|
65
|
-
mod._befores.each { receiver.Within(receiver, &it) } if mod.respond_to?(:_befores) && mod._befores
|
|
66
|
-
# return last callable return value
|
|
67
|
-
callables(mod).map { it.bind(receiver).call(...) }.last.tap do
|
|
68
|
-
mod._afters.each { receiver.Within(receiver, &it) } if mod.respond_to?(:_afters) && mod._afters
|
|
168
|
+
def validate(mod)
|
|
169
|
+
return mod if mod.instance_of?(::Module) && mod.is_a?(Graphics::Module)
|
|
170
|
+
|
|
171
|
+
ArgumentError.("Callable drawing module must extend Sevgi::Graphics::Module: #{mod}")
|
|
69
172
|
end
|
|
70
173
|
end
|
|
71
174
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
#
|
|
75
|
-
# @
|
|
76
|
-
|
|
77
|
-
|
|
175
|
+
private
|
|
176
|
+
|
|
177
|
+
# Gives a copied callable module independent configuration containers.
|
|
178
|
+
# @param original [Module] source callable module
|
|
179
|
+
# @return [void]
|
|
180
|
+
# @api private
|
|
181
|
+
def initialize_copy(original)
|
|
182
|
+
super
|
|
183
|
+
@sevgi_bases = original.instance_variable_get(:@sevgi_bases).dup
|
|
184
|
+
@sevgi_callables = original.instance_variable_get(:@sevgi_callables).dup
|
|
185
|
+
@sevgi_configuration_owner = object_id
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def own_configuration
|
|
189
|
+
return if @sevgi_configuration_owner == object_id
|
|
190
|
+
|
|
191
|
+
@sevgi_bases = @sevgi_bases.dup
|
|
192
|
+
@sevgi_callables = @sevgi_callables.dup
|
|
193
|
+
@sevgi_configuration_owner = object_id
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Tracks newly defined methods as callable drawing candidates.
|
|
197
|
+
# Invocation runs unique methods that are still public, preserving tracked definition order.
|
|
198
|
+
# @param method [Symbol] method name Ruby reports as added
|
|
199
|
+
# @return [Array<Symbol>, nil]
|
|
200
|
+
def method_added(method)
|
|
201
|
+
super
|
|
202
|
+
|
|
203
|
+
own_configuration
|
|
204
|
+
@sevgi_callables << method
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Recursive callable drawing module support. Extend a module with this API when it and the modules defined beneath
|
|
209
|
+
# it form a callable drawing namespace. Existing and subsequently defined descendants receive the same recursive
|
|
210
|
+
# contract. Classes, autoloads, and modules aliased from another namespace are left unchanged.
|
|
211
|
+
# @example Define a callable drawing namespace
|
|
212
|
+
# module Icons
|
|
213
|
+
# extend Sevgi::Graphics::Modules
|
|
214
|
+
#
|
|
215
|
+
# module Alert
|
|
216
|
+
# base { css ".alert" => { fill: "red" } }
|
|
217
|
+
#
|
|
218
|
+
# def call(id)
|
|
219
|
+
# circle id:, class: "alert", r: 5
|
|
220
|
+
# end
|
|
221
|
+
# end
|
|
222
|
+
# end
|
|
223
|
+
#
|
|
224
|
+
# Sevgi::Graphics.SVG { Call Icons::Alert, "warning" }
|
|
225
|
+
# @see Sevgi::Graphics::Module
|
|
226
|
+
module Modules
|
|
227
|
+
# Propagates the recursive contract when a module constant is added.
|
|
228
|
+
# @api private
|
|
229
|
+
module Propagation
|
|
230
|
+
private
|
|
78
231
|
|
|
79
|
-
|
|
80
|
-
|
|
232
|
+
def const_added(name)
|
|
233
|
+
super
|
|
234
|
+
Graphics::Modules.__send__(:adopt, self, name)
|
|
81
235
|
end
|
|
82
236
|
end
|
|
83
237
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
238
|
+
private_constant :Propagation
|
|
239
|
+
|
|
240
|
+
class << self
|
|
241
|
+
private
|
|
242
|
+
|
|
243
|
+
# Applies the local callable contract and adopts owned descendants.
|
|
244
|
+
# @param base [Module] extended module
|
|
245
|
+
# @return [void]
|
|
246
|
+
def extended(base)
|
|
247
|
+
base.extend(Graphics::Module)
|
|
248
|
+
base.singleton_class.prepend(Propagation) unless base.singleton_class < Propagation
|
|
249
|
+
base.constants(false).each { adopt(base, it) }
|
|
87
250
|
end
|
|
88
251
|
|
|
89
|
-
|
|
252
|
+
# Applies this contract to an owned module constant without resolving autoloads or following aliases.
|
|
253
|
+
# @param owner [Module] constant owner
|
|
254
|
+
# @param name [Symbol] constant name
|
|
255
|
+
# @return [Module, nil] adopted module, or nil when the constant is outside the contract
|
|
256
|
+
def adopt(owner, name)
|
|
257
|
+
return if owner.autoload?(name, false)
|
|
90
258
|
|
|
91
|
-
|
|
92
|
-
|
|
259
|
+
child = owner.const_get(name, false)
|
|
260
|
+
return unless child.instance_of?(::Module)
|
|
261
|
+
return unless child.name == "#{owner}::#{name}"
|
|
93
262
|
|
|
94
|
-
|
|
263
|
+
child.extend(self) unless child.is_a?(self)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
95
266
|
end
|
|
96
267
|
|
|
97
268
|
module Mixtures
|
|
@@ -99,40 +270,15 @@ module Sevgi
|
|
|
99
270
|
module Call
|
|
100
271
|
# @overload Call(mod, *args, **kwargs)
|
|
101
272
|
# Runs a callable drawing module in the current element context.
|
|
102
|
-
# @param mod [Module]
|
|
273
|
+
# @param mod [Module] module extended with {Sevgi::Graphics::Module}
|
|
103
274
|
# @param args [Array<Object>] callable arguments
|
|
104
275
|
# @param kwargs [Hash] callable keyword arguments
|
|
105
276
|
# @return [Object, nil] last callable return value
|
|
106
|
-
# @raise [Sevgi::ArgumentError] when mod is not a
|
|
277
|
+
# @raise [Sevgi::ArgumentError] when mod is not a callable drawing module
|
|
107
278
|
def Call(mod, ...)
|
|
108
|
-
Graphics::Module.call
|
|
279
|
+
Graphics::Module.__send__(:call, mod, self, ...)
|
|
109
280
|
end
|
|
110
281
|
|
|
111
|
-
private
|
|
112
|
-
|
|
113
|
-
# rubocop:disable Metrics/MethodLength
|
|
114
|
-
def CallWithin(mod, container, element, *args, **kwargs, &block)
|
|
115
|
-
ArgumentError.("Must be a module: #{mod}") unless mod.instance_of?(::Module)
|
|
116
|
-
|
|
117
|
-
kwargs = kwargs.merge(id: F.demodulize(mod).to_sym) unless kwargs.key?(:id)
|
|
118
|
-
|
|
119
|
-
mod._befores.each { Within(self, &it) } if mod.respond_to?(:_befores) && mod._befores
|
|
120
|
-
|
|
121
|
-
public_send(container, **kwargs) do
|
|
122
|
-
Graphics::Module.callables(mod).each do |method|
|
|
123
|
-
public_send(element) do
|
|
124
|
-
Within(self, method.name, self, &block)
|
|
125
|
-
|
|
126
|
-
method.bind(self).call(*args)
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
mod._afters.each { Within(self, &it) } if mod.respond_to?(:_afters) && mod._afters
|
|
132
|
-
|
|
133
|
-
self
|
|
134
|
-
end
|
|
135
|
-
# rubocop:enable Metrics/MethodLength
|
|
136
282
|
end
|
|
137
283
|
end
|
|
138
284
|
end
|