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