sevgi-graphics 0.94.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 +275 -2
- data/LICENSE +672 -3
- data/README.md +8 -8
- data/lib/sevgi/graphics/attribute.rb +271 -54
- data/lib/sevgi/graphics/auxiliary/canvas.rb +137 -58
- data/lib/sevgi/graphics/auxiliary/content.rb +200 -57
- data/lib/sevgi/graphics/auxiliary/margin.rb +33 -14
- data/lib/sevgi/graphics/auxiliary/paper.rb +118 -86
- data/lib/sevgi/graphics/auxiliary/path.rb +44 -0
- data/lib/sevgi/graphics/auxiliary/scalar.rb +69 -0
- data/lib/sevgi/graphics/auxiliary/sizes.rb +62 -0
- data/lib/sevgi/graphics/auxiliary.rb +3 -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 +370 -124
- data/lib/sevgi/graphics/element.rb +143 -33
- data/lib/sevgi/graphics/mixtures/call.rb +249 -88
- data/lib/sevgi/graphics/mixtures/core.rb +143 -33
- data/lib/sevgi/graphics/mixtures/duplicate.rb +76 -22
- data/lib/sevgi/graphics/mixtures/export.rb +58 -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 +40 -5
- data/lib/sevgi/graphics/mixtures/inkscape.rb +215 -46
- data/lib/sevgi/graphics/mixtures/rdf.rb +79 -7
- data/lib/sevgi/graphics/mixtures/render.rb +88 -19
- data/lib/sevgi/graphics/mixtures/save.rb +79 -26
- data/lib/sevgi/graphics/mixtures/symbols.rb +81 -9
- data/lib/sevgi/graphics/mixtures/tile.rb +88 -64
- data/lib/sevgi/graphics/mixtures/transform.rb +68 -28
- data/lib/sevgi/graphics/mixtures/underscore.rb +16 -7
- data/lib/sevgi/graphics/mixtures/validate.rb +14 -2
- data/lib/sevgi/graphics/mixtures/wrappers.rb +66 -24
- data/lib/sevgi/graphics/mixtures.rb +19 -13
- data/lib/sevgi/graphics/version.rb +1 -1
- data/lib/sevgi/graphics/xml.rb +127 -0
- data/lib/sevgi/graphics.rb +75 -28
- metadata +10 -6
|
@@ -3,43 +3,115 @@
|
|
|
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 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
|
|
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.
|
|
6
15
|
class Element
|
|
7
16
|
# Builds an element node.
|
|
8
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
|
|
9
20
|
# @param parent [Sevgi::Graphics::Element] parent element
|
|
21
|
+
# @yield evaluates the drawing DSL in the new element
|
|
22
|
+
# @yieldreturn [Object] ignored block result
|
|
10
23
|
# @return [Sevgi::Graphics::Element]
|
|
11
24
|
# @raise [Sevgi::ArgumentError] when an argument cannot be parsed as attributes or content
|
|
12
|
-
|
|
25
|
+
# @raise [Sevgi::ArgumentError] when the parent has a different concrete element class
|
|
26
|
+
# @raise [Sevgi::ArgumentError] when the element name or an attribute is not valid XML
|
|
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
|
|
13
31
|
|
|
14
32
|
# Builds an SVG root element.
|
|
15
|
-
# @param
|
|
33
|
+
# @param arguments [Array<Hash, String, Sevgi::Graphics::Content>] ordered content and attribute channels
|
|
34
|
+
# @yield evaluates the drawing DSL in the root element
|
|
35
|
+
# @yieldreturn [Object] ignored block result
|
|
16
36
|
# @return [Sevgi::Graphics::Element]
|
|
17
|
-
|
|
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)
|
|
18
40
|
|
|
19
41
|
# Reports whether an element is the root element.
|
|
20
42
|
# @param element [Sevgi::Graphics::Element] element to test
|
|
21
43
|
# @return [Boolean]
|
|
22
|
-
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
|
|
23
79
|
|
|
24
80
|
class << self
|
|
25
81
|
require "sevgi/standard"
|
|
26
82
|
|
|
27
|
-
# Reports whether
|
|
28
|
-
# @param name [
|
|
83
|
+
# Reports whether a candidate can dispatch as a known SVG element name.
|
|
84
|
+
# @param name [Object] candidate element name
|
|
29
85
|
# @return [Boolean]
|
|
30
|
-
def valid?(name)
|
|
86
|
+
def valid?(name)
|
|
87
|
+
Standard.element?(name)
|
|
88
|
+
rescue Sevgi::ArgumentError
|
|
89
|
+
false
|
|
90
|
+
end
|
|
91
|
+
|
|
31
92
|
rescue ::LoadError => e
|
|
32
93
|
raise unless e.path == "sevgi/standard"
|
|
33
94
|
|
|
34
|
-
# Reports whether
|
|
95
|
+
# Reports whether a candidate can dispatch as a valid XML element name.
|
|
96
|
+
# @param name [Object] candidate element name
|
|
35
97
|
# @return [Boolean]
|
|
36
|
-
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
|
|
37
106
|
end
|
|
38
107
|
|
|
39
108
|
private_class_method :new
|
|
40
109
|
|
|
41
|
-
# Sentinel
|
|
110
|
+
# Sentinel parents used by root and detached elements.
|
|
42
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
|
|
43
115
|
|
|
44
116
|
# SVG element method-name normalization.
|
|
45
117
|
# @api private
|
|
@@ -51,54 +123,85 @@ module Sevgi
|
|
|
51
123
|
end
|
|
52
124
|
|
|
53
125
|
extend Ident
|
|
126
|
+
private_class_method :id
|
|
127
|
+
private_constant :Ident
|
|
128
|
+
|
|
129
|
+
# Returns the SVG element name.
|
|
130
|
+
# @return [Symbol]
|
|
131
|
+
attr_reader :name
|
|
54
132
|
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
|
|
133
|
+
# Returns the attribute store.
|
|
134
|
+
# @return [Sevgi::Graphics::Attributes]
|
|
135
|
+
attr_reader :attributes
|
|
136
|
+
|
|
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
|
|
140
|
+
|
|
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
|
|
144
|
+
|
|
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
|
|
66
151
|
|
|
67
152
|
# Creates an element.
|
|
68
153
|
# @param name [Symbol] SVG element name
|
|
69
154
|
# @param parent [Sevgi::Graphics::Element, Object] parent element or root sentinel
|
|
70
155
|
# @param attributes [Hash] SVG attributes
|
|
71
156
|
# @param contents [Array<Sevgi::Graphics::Content>] content objects
|
|
157
|
+
# @yield evaluates the drawing DSL in the new element
|
|
158
|
+
# @yieldreturn [Object] ignored block result
|
|
72
159
|
# @return [void]
|
|
160
|
+
# @raise [Sevgi::ArgumentError] when the parent has a different concrete element class
|
|
161
|
+
# @raise [Sevgi::ArgumentError] when the element name or an attribute is not valid XML
|
|
162
|
+
# @api private
|
|
73
163
|
def initialize(name, parent:, attributes: {}, contents: [], &block)
|
|
74
|
-
|
|
164
|
+
Element.send(:validate_parent, self, parent)
|
|
165
|
+
|
|
166
|
+
unless name.is_a?(::String) || name.is_a?(::Symbol)
|
|
167
|
+
ArgumentError.("XML element name must be a String or Symbol")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
@name = XML.name(name, context: "XML element name").to_sym
|
|
75
171
|
@attributes = Attributes.new(attributes)
|
|
76
172
|
@children = []
|
|
77
|
-
@contents = contents
|
|
173
|
+
@contents = contents.dup
|
|
78
174
|
@parent = parent
|
|
79
175
|
|
|
80
|
-
|
|
176
|
+
Element.send(:attach, self, parent) unless Element.root?(self)
|
|
81
177
|
|
|
82
178
|
instance_exec(&block) if block
|
|
83
179
|
end
|
|
84
180
|
|
|
85
181
|
# Dispatches SVG element DSL calls and caches valid element methods.
|
|
86
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
|
|
185
|
+
# @yield evaluates the drawing DSL in the dispatched child element
|
|
186
|
+
# @yieldreturn [Object] ignored block result
|
|
87
187
|
# @return [Sevgi::Graphics::Element]
|
|
88
188
|
# @raise [NameError] when the name is not a valid SVG element
|
|
89
189
|
# @raise [Sevgi::ArgumentError] when an argument cannot be parsed as attributes or content
|
|
90
|
-
def method_missing(name,
|
|
91
|
-
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
|
|
92
192
|
end
|
|
93
193
|
|
|
94
194
|
# Reports whether a missing method can dispatch to an SVG element.
|
|
95
195
|
# @param name [Symbol] queried method name
|
|
96
196
|
# @param include_private [Boolean] standard Ruby respond_to? flag
|
|
97
197
|
# @return [Boolean]
|
|
198
|
+
# @api private
|
|
98
199
|
def respond_to_missing?(name, include_private = false)
|
|
99
|
-
Element.valid?(Element.id
|
|
200
|
+
Element.valid?(Element.send(:id, name)) || super
|
|
100
201
|
end
|
|
101
202
|
|
|
203
|
+
private :method_missing, :respond_to_missing?
|
|
204
|
+
|
|
102
205
|
# Element method-missing parser and cache.
|
|
103
206
|
# @api private
|
|
104
207
|
module Dispatch
|
|
@@ -110,38 +213,45 @@ module Sevgi
|
|
|
110
213
|
# @param tag [Symbol] SVG element name
|
|
111
214
|
# @return [Sevgi::Graphics::Element]
|
|
112
215
|
def call(element, method, tag, *, &)
|
|
113
|
-
# Low-hanging fruit optimization: define missing method to avoid dispatching cost
|
|
114
216
|
unless Element.method_defined?(method)
|
|
115
217
|
Element.class_exec do
|
|
116
218
|
define_method(method) { |*args, &block| self.class.element(tag, *args, parent: self, &block) }
|
|
117
219
|
end
|
|
220
|
+
|
|
221
|
+
(@methods ||= {})[method] = Element.instance_method(method)
|
|
118
222
|
end
|
|
119
223
|
|
|
120
224
|
element.public_send(method, *, &)
|
|
121
225
|
end
|
|
122
226
|
|
|
227
|
+
def cached?(method)
|
|
228
|
+
cached = @methods&.[](method)
|
|
229
|
+
|
|
230
|
+
cached && Element.method_defined?(method) && Element.instance_method(method) == cached
|
|
231
|
+
end
|
|
232
|
+
|
|
123
233
|
# Parses element DSL arguments.
|
|
124
234
|
# @param name [Symbol] SVG element name
|
|
125
|
-
# @param args [Array<Object>] positional DSL arguments
|
|
235
|
+
# @param args [Array<Object>] positional DSL arguments; Hashes are imported from left to right
|
|
126
236
|
# @return [Hash] parsed :attributes and :contents
|
|
127
237
|
# @raise [Sevgi::ArgumentError] when an argument is not a Hash, String, or Content
|
|
128
238
|
def parse(name, *args)
|
|
129
|
-
attributes, contents =
|
|
239
|
+
attributes, contents = Attributes.new, []
|
|
130
240
|
|
|
131
241
|
args.each do |arg|
|
|
132
242
|
case arg
|
|
133
243
|
when ::Hash
|
|
134
|
-
attributes
|
|
244
|
+
attributes.merge!(arg)
|
|
135
245
|
when ::String
|
|
136
246
|
contents << Content.encoded(arg)
|
|
137
247
|
when Content
|
|
138
248
|
contents << arg
|
|
139
249
|
else
|
|
140
|
-
ArgumentError.("Argument of element '#{name}' must be a Hash or
|
|
250
|
+
ArgumentError.("Argument of element '#{name}' must be a Hash, String, or Content: #{arg}")
|
|
141
251
|
end
|
|
142
252
|
end
|
|
143
253
|
|
|
144
|
-
{attributes
|
|
254
|
+
{attributes: attributes.to_h, contents:}
|
|
145
255
|
end
|
|
146
256
|
end
|
|
147
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
|