sevgi-graphics 0.73.2 → 0.93.1
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/README.md +5 -0
- data/lib/sevgi/graphics/attribute.rb +58 -6
- data/lib/sevgi/graphics/auxiliary/canvas.rb +125 -0
- data/lib/sevgi/graphics/auxiliary/content.rb +145 -0
- data/lib/sevgi/graphics/auxiliary/margin.rb +93 -0
- data/lib/sevgi/graphics/auxiliary/paper.rb +163 -0
- data/lib/sevgi/graphics/auxiliary.rb +7 -0
- data/lib/sevgi/graphics/document/base.rb +6 -0
- data/lib/sevgi/graphics/document/default.rb +1 -0
- data/lib/sevgi/graphics/document/html.rb +1 -0
- data/lib/sevgi/graphics/document/inkscape.rb +1 -0
- data/lib/sevgi/graphics/document/minimal.rb +1 -0
- data/lib/sevgi/graphics/document.rb +137 -11
- data/lib/sevgi/graphics/element.rb +68 -8
- data/lib/sevgi/graphics/mixtures/call.rb +35 -3
- data/lib/sevgi/graphics/mixtures/core.rb +107 -18
- data/lib/sevgi/graphics/mixtures/duplicate.rb +14 -0
- data/lib/sevgi/graphics/mixtures/export.rb +29 -6
- data/lib/sevgi/graphics/mixtures/hatch.rb +24 -3
- data/lib/sevgi/graphics/mixtures/identify.rb +23 -1
- data/lib/sevgi/graphics/mixtures/include.rb +31 -4
- data/lib/sevgi/graphics/mixtures/inkscape.rb +51 -5
- data/lib/sevgi/graphics/mixtures/lint.rb +12 -0
- data/lib/sevgi/graphics/mixtures/polyfills.rb +11 -0
- data/lib/sevgi/graphics/mixtures/rdf.rb +40 -1
- data/lib/sevgi/graphics/mixtures/render.rb +90 -2
- data/lib/sevgi/graphics/mixtures/save.rb +21 -3
- data/lib/sevgi/graphics/mixtures/symbols.rb +7 -0
- data/lib/sevgi/graphics/mixtures/tile.rb +41 -6
- data/lib/sevgi/graphics/mixtures/transform.rb +54 -16
- data/lib/sevgi/graphics/mixtures/underscore.rb +10 -1
- data/lib/sevgi/graphics/mixtures/validate.rb +15 -1
- data/lib/sevgi/graphics/mixtures/wrappers.rb +54 -17
- data/lib/sevgi/graphics/mixtures.rb +35 -3
- data/lib/sevgi/graphics/version.rb +2 -1
- data/lib/sevgi/graphics.rb +57 -5
- metadata +9 -8
- data/lib/sevgi/graphics/auxilary/canvas.rb +0 -72
- data/lib/sevgi/graphics/auxilary/content.rb +0 -81
- data/lib/sevgi/graphics/auxilary/margin.rb +0 -49
- data/lib/sevgi/graphics/auxilary/paper.rb +0 -95
- data/lib/sevgi/graphics/auxilary.rb +0 -7
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sevgi
|
|
4
|
+
module Graphics
|
|
5
|
+
# Paper size and unit profile.
|
|
6
|
+
Paper = Data.define(:width, :height, :unit, :name) do
|
|
7
|
+
include Comparable
|
|
8
|
+
|
|
9
|
+
# @!attribute [r] width
|
|
10
|
+
# @return [Float] paper width
|
|
11
|
+
# @!attribute [r] height
|
|
12
|
+
# @return [Float] paper height
|
|
13
|
+
# @!attribute [r] unit
|
|
14
|
+
# @return [Symbol] SVG unit
|
|
15
|
+
# @!attribute [r] name
|
|
16
|
+
# @return [Symbol] profile name
|
|
17
|
+
|
|
18
|
+
# Creates a paper profile.
|
|
19
|
+
# @param width [Numeric] paper width
|
|
20
|
+
# @param height [Numeric] paper height
|
|
21
|
+
# @param unit [Symbol, String] SVG unit
|
|
22
|
+
# @param name [Symbol, String] profile name
|
|
23
|
+
# @return [void]
|
|
24
|
+
# @raise [Sevgi::ArgumentError] when dimensions, unit, or profile name are invalid
|
|
25
|
+
def initialize(width:, height:, unit: "mm", name: :custom)
|
|
26
|
+
super(
|
|
27
|
+
width: self.class.send(:dimension!, :width, width),
|
|
28
|
+
height: self.class.send(:dimension!, :height, height),
|
|
29
|
+
unit: self.class.send(:symbol!, :unit, unit),
|
|
30
|
+
name: self.class.send(:symbol!, :name, name)
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Compares papers by width, height, unit, then name.
|
|
35
|
+
# @param other [Sevgi::Graphics::Paper] paper to compare
|
|
36
|
+
# @return [Integer, nil]
|
|
37
|
+
def <=>(other) = deconstruct <=> other.deconstruct
|
|
38
|
+
|
|
39
|
+
# Reports strict paper equality.
|
|
40
|
+
# @param other [Object] object to compare
|
|
41
|
+
# @return [Boolean]
|
|
42
|
+
def eql?(other) = self.class == other.class && deconstruct == other.deconstruct
|
|
43
|
+
|
|
44
|
+
# Returns a hash compatible with strict equality.
|
|
45
|
+
# @return [Integer]
|
|
46
|
+
def hash = [self.class, *deconstruct].hash
|
|
47
|
+
|
|
48
|
+
# Returns the longer side.
|
|
49
|
+
# @return [Float]
|
|
50
|
+
def longest = [width, height].max
|
|
51
|
+
|
|
52
|
+
# Returns the shorter side.
|
|
53
|
+
# @return [Float]
|
|
54
|
+
def shortest = [width, height].min
|
|
55
|
+
|
|
56
|
+
alias_method :==, :eql?
|
|
57
|
+
|
|
58
|
+
@profiles = {}
|
|
59
|
+
|
|
60
|
+
# Reports whether a named paper profile exists.
|
|
61
|
+
# @param name [Object] profile name
|
|
62
|
+
# @return [Boolean]
|
|
63
|
+
def self.exist?(name) = name.respond_to?(:to_sym) && profiles.key?(name.to_sym)
|
|
64
|
+
|
|
65
|
+
# Defines or replaces a named paper profile.
|
|
66
|
+
# @param name [Symbol, String] profile name
|
|
67
|
+
# @param spec [Hash] paper dimensions and unit
|
|
68
|
+
# @option spec [Numeric] :width paper width
|
|
69
|
+
# @option spec [Numeric] :height paper height
|
|
70
|
+
# @option spec [Symbol, String] :unit SVG unit
|
|
71
|
+
# @return [Sevgi::Graphics::Paper]
|
|
72
|
+
# @raise [Sevgi::ArgumentError] when the profile name is reserved or invalid
|
|
73
|
+
def self.define(name, **spec)
|
|
74
|
+
name = symbol!(:name, name)
|
|
75
|
+
|
|
76
|
+
ArgumentError.("Paper name is reserved: #{name}") if reserved?(name) && !exist?(name)
|
|
77
|
+
|
|
78
|
+
singleton_class.remove_method(name) if singleton_class.method_defined?(name, false)
|
|
79
|
+
singleton_class.attr_reader(name)
|
|
80
|
+
profiles[name] = instance_variable_set("@#{name}", new(name:, **spec))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.reserved?(name) = @reserved.include?(name.to_sym)
|
|
84
|
+
|
|
85
|
+
def self.profiles = @profiles
|
|
86
|
+
|
|
87
|
+
def self.dimension!(field, value)
|
|
88
|
+
Float(value)
|
|
89
|
+
rescue ::ArgumentError, ::TypeError
|
|
90
|
+
ArgumentError.("Invalid paper #{field}: #{value.inspect}")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.symbol!(field, value)
|
|
94
|
+
value.to_sym
|
|
95
|
+
rescue ::NoMethodError, ::TypeError
|
|
96
|
+
ArgumentError.("Invalid paper #{field}: #{value.inspect}")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
@reserved = methods.map(&:to_sym).freeze
|
|
100
|
+
|
|
101
|
+
private_class_method :dimension!, :profiles, :reserved?, :symbol!
|
|
102
|
+
|
|
103
|
+
{
|
|
104
|
+
a0: [841, 1189, "mm"],
|
|
105
|
+
a1: [594, 841, "mm"],
|
|
106
|
+
a2: [420, 594, "mm"],
|
|
107
|
+
a3: [297, 420, "mm"],
|
|
108
|
+
a4: [210, 297, "mm"],
|
|
109
|
+
a5: [148, 210, "mm"],
|
|
110
|
+
a6: [105, 148, "mm"],
|
|
111
|
+
a7: [74, 105, "mm"],
|
|
112
|
+
a8: [52, 74, "mm"],
|
|
113
|
+
a9: [37, 52, "mm"],
|
|
114
|
+
a10: [26, 37, "mm"],
|
|
115
|
+
|
|
116
|
+
b0: [1000, 1414, "mm"],
|
|
117
|
+
b1: [707, 1000, "mm"],
|
|
118
|
+
b2: [500, 707, "mm"],
|
|
119
|
+
b3: [353, 500, "mm"],
|
|
120
|
+
b4: [250, 353, "mm"],
|
|
121
|
+
b5: [176, 250, "mm"],
|
|
122
|
+
b6: [125, 176, "mm"],
|
|
123
|
+
b7: [88, 125, "mm"],
|
|
124
|
+
b8: [62, 88, "mm"],
|
|
125
|
+
b9: [44, 62, "mm"],
|
|
126
|
+
b10: [31, 44, "mm"],
|
|
127
|
+
|
|
128
|
+
c0: [917, 1297, "mm"],
|
|
129
|
+
c1: [648, 917, "mm"],
|
|
130
|
+
c2: [458, 648, "mm"],
|
|
131
|
+
c3: [324, 458, "mm"],
|
|
132
|
+
c4: [229, 324, "mm"],
|
|
133
|
+
c5: [162, 229, "mm"],
|
|
134
|
+
c6: [114, 162, "mm"],
|
|
135
|
+
c7: [81, 114, "mm"],
|
|
136
|
+
c8: [57, 81, "mm"],
|
|
137
|
+
c9: [40, 57, "mm"],
|
|
138
|
+
c10: [28, 40, "mm"],
|
|
139
|
+
|
|
140
|
+
business: [85, 55, "mm"],
|
|
141
|
+
large: [130, 210, "mm"],
|
|
142
|
+
passport: [88, 125, "mm"],
|
|
143
|
+
pocket: [90, 140, "mm"],
|
|
144
|
+
travelers: [110, 210, "mm"],
|
|
145
|
+
us: [216, 279, "mm"],
|
|
146
|
+
xlarge: [190, 250, "mm"],
|
|
147
|
+
|
|
148
|
+
icon16: [16, 16, "px"],
|
|
149
|
+
icon32: [32, 32, "px"],
|
|
150
|
+
icon64: [64, 64, "px"],
|
|
151
|
+
icon128: [128, 128, "px"],
|
|
152
|
+
icon256: [256, 256, "px"],
|
|
153
|
+
icon512: [512, 512, "px"]
|
|
154
|
+
}.each { |name, (width, height, unit)| define(name, width:, height:, unit:) }
|
|
155
|
+
|
|
156
|
+
class << self
|
|
157
|
+
alias_method :default, :a4
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
profiles[:default] = profiles.fetch(:a4)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Graphics
|
|
5
5
|
module Document
|
|
6
|
+
# Standard document profile with the full common DSL mixture set.
|
|
6
7
|
class Base < Proto
|
|
7
8
|
document :base
|
|
8
9
|
|
|
@@ -18,6 +19,11 @@ module Sevgi
|
|
|
18
19
|
mixture :Underscore
|
|
19
20
|
mixture :Validate
|
|
20
21
|
|
|
22
|
+
# Runs pre-render validation and lint checks.
|
|
23
|
+
# @param options [Hash] pre-render options
|
|
24
|
+
# @return [void]
|
|
25
|
+
# @raise [Sevgi::ValidationError] when validation fails
|
|
26
|
+
# @raise [Sevgi::Graphics::LintError] when linting fails
|
|
21
27
|
def PreRender(**options)
|
|
22
28
|
self.Validate() if options[:validate]
|
|
23
29
|
self.Lint() if options[:lint]
|
|
@@ -2,51 +2,166 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Graphics
|
|
5
|
+
# SVG document profile factory.
|
|
5
6
|
module Document
|
|
7
|
+
# Builds a root SVG element from a document profile.
|
|
8
|
+
# @param document [Symbol, String, Class] profile name or document class
|
|
9
|
+
# @param canvas [Sevgi::Graphics::Canvas, Sevgi::Graphics::Paper, Symbol, String, Sevgi::Undefined, nil] canvas input
|
|
10
|
+
# @return [Sevgi::Graphics::Document::Proto] SVG root element
|
|
11
|
+
# @raise [Sevgi::ArgumentError] when the document profile is unknown
|
|
6
12
|
def self.call(document, canvas = Undefined, **, &block)
|
|
13
|
+
klass = case document
|
|
14
|
+
when ::Class
|
|
15
|
+
document if document <= Proto
|
|
16
|
+
else
|
|
17
|
+
Profile[document]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
ArgumentError.("Unknown document profile: #{document}") unless klass
|
|
21
|
+
|
|
22
|
+
klass.root(**klass.attributes, **canvas_attributes(canvas), **, &block)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.canvas_attributes(canvas)
|
|
7
26
|
case canvas
|
|
8
27
|
when Undefined, ::NilClass
|
|
9
28
|
{}
|
|
10
29
|
when Canvas
|
|
11
30
|
canvas.attributes
|
|
12
31
|
else
|
|
13
|
-
Canvas.(canvas).attributes
|
|
14
|
-
end
|
|
32
|
+
Canvas.from_paper(canvas).attributes
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private_class_method :canvas_attributes
|
|
37
|
+
|
|
38
|
+
# Defines or returns a document profile class.
|
|
39
|
+
# @param name [Symbol, String, Sevgi::Undefined] profile name, or Undefined for an anonymous profile
|
|
40
|
+
# @param preambles [Array<String>, nil] document preamble lines
|
|
41
|
+
# @param attributes [Hash] default root attributes
|
|
42
|
+
# @param overwrite [Boolean] true to replace an existing profile
|
|
43
|
+
# @return [Class] document class
|
|
44
|
+
# @raise [Sevgi::ArgumentError] when a named profile conflicts with an existing profile
|
|
45
|
+
def self.define(name = Undefined, preambles: [], attributes: {}, overwrite: false)
|
|
46
|
+
return Class.new(Base) { document(name, preambles:, attributes:, register: false) } if name == Undefined
|
|
47
|
+
|
|
48
|
+
profile = Profile.new(name, attributes:, preambles:)
|
|
49
|
+
|
|
50
|
+
if (current = Profile[name])
|
|
51
|
+
unless overwrite || current.profile == profile
|
|
52
|
+
ArgumentError.("Document profile already defined differently: #{name}")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
return current unless overwrite
|
|
56
|
+
end
|
|
15
57
|
|
|
16
|
-
(
|
|
58
|
+
Class.new(Base) { document(name, preambles:, attributes:, overwrite:) }
|
|
17
59
|
end
|
|
18
60
|
|
|
61
|
+
# Immutable document profile metadata.
|
|
62
|
+
# @api private
|
|
19
63
|
class Profile
|
|
20
64
|
@available = {}
|
|
21
65
|
|
|
22
66
|
class << self
|
|
67
|
+
# @return [Hash<Symbol, Class>] registered profile classes
|
|
23
68
|
attr_reader :available
|
|
24
69
|
end
|
|
25
70
|
|
|
26
|
-
|
|
71
|
+
# Returns a profile class by name.
|
|
72
|
+
# @param name [Object] profile name
|
|
73
|
+
# @return [Class, nil]
|
|
74
|
+
def self.[](name) = (name = normalize(name)) && available[name]
|
|
75
|
+
|
|
76
|
+
# Registers a profile class.
|
|
77
|
+
# @param name [Object] profile name
|
|
78
|
+
# @param klass [Class] document class
|
|
79
|
+
# @param overwrite [Boolean] true to replace an existing profile
|
|
80
|
+
# @return [Class] registered class
|
|
81
|
+
# @raise [Sevgi::ArgumentError] when name is invalid or conflicts with an existing profile
|
|
82
|
+
def self.register(name, klass, overwrite: false)
|
|
83
|
+
name = normalize!(name)
|
|
84
|
+
|
|
85
|
+
if (current = available[name])
|
|
86
|
+
unless overwrite || current.profile == klass.profile
|
|
87
|
+
ArgumentError.("Document profile already defined differently: #{name}")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
return current unless overwrite
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
available[name] = klass
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Normalizes a profile name.
|
|
97
|
+
# @param name [Object] profile name
|
|
98
|
+
# @return [Symbol, nil]
|
|
99
|
+
def self.normalize(name) = name.respond_to?(:to_sym) ? name.to_sym : nil
|
|
100
|
+
|
|
101
|
+
# Normalizes a profile name or raises.
|
|
102
|
+
# @param name [Object] profile name
|
|
103
|
+
# @return [Symbol]
|
|
104
|
+
# @raise [Sevgi::ArgumentError] when name cannot be normalized
|
|
105
|
+
def self.normalize!(name) = normalize(name) || ArgumentError.("Invalid document profile: #{name}")
|
|
106
|
+
|
|
107
|
+
# @return [Symbol, nil] profile name
|
|
108
|
+
attr_reader :name
|
|
27
109
|
|
|
28
|
-
|
|
110
|
+
# @return [Hash] default root attributes
|
|
111
|
+
attr_reader :attributes
|
|
29
112
|
|
|
30
|
-
|
|
113
|
+
# @return [Array<String>, nil] preamble lines
|
|
114
|
+
attr_reader :preambles
|
|
31
115
|
|
|
116
|
+
# Creates profile metadata.
|
|
117
|
+
# @param name [Object, nil] profile name
|
|
118
|
+
# @param attributes [Hash, nil] default root attributes
|
|
119
|
+
# @param preambles [Array<String>, nil] preamble lines
|
|
120
|
+
# @return [void]
|
|
121
|
+
# @raise [Sevgi::ArgumentError] when name cannot be normalized
|
|
32
122
|
def initialize(name, attributes: nil, preambles: nil)
|
|
33
|
-
@name = name
|
|
123
|
+
@name = name.nil? ? nil : self.class.normalize!(name)
|
|
34
124
|
@attributes = attributes || {}
|
|
35
125
|
@preambles = preambles
|
|
36
126
|
end
|
|
127
|
+
|
|
128
|
+
# Reports strict profile equality.
|
|
129
|
+
# @param other [Object] object to compare
|
|
130
|
+
# @return [Boolean]
|
|
131
|
+
def ==(other) = self.class == other.class && deconstruct == other.deconstruct
|
|
132
|
+
|
|
133
|
+
# Returns profile components.
|
|
134
|
+
# @return [Array<(Symbol, nil), Hash, (Array<String>, nil)>]
|
|
135
|
+
def deconstruct = [name, attributes, preambles]
|
|
37
136
|
end
|
|
38
137
|
|
|
39
138
|
private_constant :Profile
|
|
40
139
|
|
|
140
|
+
# Class-level DSL used while defining document classes.
|
|
141
|
+
# @api private
|
|
41
142
|
module DSL
|
|
143
|
+
# @return [Sevgi::Graphics::Document::Profile] document profile metadata
|
|
42
144
|
attr_reader :profile
|
|
43
145
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
146
|
+
# Sets document profile metadata on a class.
|
|
147
|
+
# @param name [Object] profile name
|
|
148
|
+
# @param attributes [Hash] default root attributes
|
|
149
|
+
# @param preambles [Array<String>, nil] preamble lines
|
|
150
|
+
# @param register [Boolean] true to register the profile globally
|
|
151
|
+
# @param overwrite [Boolean] true to replace an existing profile
|
|
152
|
+
# @return [Sevgi::Graphics::Document::Profile]
|
|
153
|
+
# @raise [Sevgi::ArgumentError] when registration fails
|
|
154
|
+
def document(name, attributes: {}, preambles: nil, register: true, overwrite: false)
|
|
155
|
+
@profile = Profile.new(register ? name : nil, attributes:, preambles:)
|
|
156
|
+
Profile.register(name, self, overwrite:) if register
|
|
157
|
+
@profile
|
|
48
158
|
end
|
|
49
159
|
|
|
160
|
+
# Includes a graphics mixture into the document class.
|
|
161
|
+
# @param mixture [Symbol, String] mixture constant name
|
|
162
|
+
# @param ns [Module] namespace containing mixture modules
|
|
163
|
+
# @return [Module] included mixture module
|
|
164
|
+
# @raise [NameError] when the mixture does not exist
|
|
50
165
|
def mixture(mixture, ns: Graphics::Mixtures)
|
|
51
166
|
include(mod = ns.const_get(mixture))
|
|
52
167
|
extend(mod::ClassMethods) if defined?(mod::ClassMethods)
|
|
@@ -55,8 +170,10 @@ module Sevgi
|
|
|
55
170
|
|
|
56
171
|
private_constant :DSL
|
|
57
172
|
|
|
173
|
+
# Default render-time checks.
|
|
58
174
|
DEFAULTS = {lint: true, validate: true}.freeze
|
|
59
175
|
|
|
176
|
+
# Base document root element class.
|
|
60
177
|
class Proto < Element
|
|
61
178
|
public_class_method :new
|
|
62
179
|
|
|
@@ -67,6 +184,11 @@ module Sevgi
|
|
|
67
184
|
mixture :Render
|
|
68
185
|
mixture :Wrappers
|
|
69
186
|
|
|
187
|
+
# @overload call(*objects, **options)
|
|
188
|
+
# Renders the document.
|
|
189
|
+
# @param objects [Array<Object>] optional renderer arguments
|
|
190
|
+
# @param options [Hash] render options
|
|
191
|
+
# @return [String] SVG document source
|
|
70
192
|
def call(*, **)
|
|
71
193
|
options = DEFAULTS.merge(**)
|
|
72
194
|
|
|
@@ -74,8 +196,12 @@ module Sevgi
|
|
|
74
196
|
self.Render(*, **options)
|
|
75
197
|
end
|
|
76
198
|
|
|
199
|
+
# Returns inherited root attributes for this document class.
|
|
200
|
+
# @return [Hash]
|
|
77
201
|
def self.attributes = self == Proto ? {} : {**superclass.attributes, **profile.attributes}
|
|
78
202
|
|
|
203
|
+
# Returns inherited preamble lines for this document class.
|
|
204
|
+
# @return [Array<String>, nil]
|
|
79
205
|
def self.preambles = self == Proto ? nil : profile.preambles || superclass.preambles
|
|
80
206
|
end
|
|
81
207
|
|
|
@@ -1,36 +1,75 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "sevgi/standard"
|
|
4
|
-
|
|
5
3
|
module Sevgi
|
|
6
4
|
module Graphics
|
|
5
|
+
# SVG element node used by the graphics DSL.
|
|
7
6
|
class Element
|
|
7
|
+
# Builds an element node.
|
|
8
|
+
# @param name [Symbol, String] SVG element name
|
|
9
|
+
# @param parent [Sevgi::Graphics::Element] parent element
|
|
10
|
+
# @return [Sevgi::Graphics::Element]
|
|
11
|
+
# @raise [Sevgi::ArgumentError] when an argument cannot be parsed as attributes or content
|
|
8
12
|
def self.element(name, *, parent:, &block) = new(name, **Dispatch.parse(name, *), parent:, &block)
|
|
9
13
|
|
|
14
|
+
# Builds an SVG root element.
|
|
15
|
+
# @param block [Proc, nil] DSL block evaluated in the root element
|
|
16
|
+
# @return [Sevgi::Graphics::Element]
|
|
10
17
|
def self.root(*, &block) = element(:svg, *, parent: RootParent, &block)
|
|
11
18
|
|
|
19
|
+
# Reports whether an element is the root element.
|
|
20
|
+
# @param element [Sevgi::Graphics::Element] element to test
|
|
21
|
+
# @return [Boolean]
|
|
12
22
|
def self.root?(element) = element.parent == RootParent
|
|
13
23
|
|
|
14
24
|
class << self
|
|
15
25
|
require "sevgi/standard"
|
|
16
26
|
|
|
27
|
+
# Reports whether an SVG element name is known.
|
|
28
|
+
# @param name [Symbol] SVG element name
|
|
29
|
+
# @return [Boolean]
|
|
17
30
|
def valid?(name) = Standard.element?(name)
|
|
18
|
-
rescue ::LoadError
|
|
31
|
+
rescue ::LoadError => e
|
|
32
|
+
raise unless e.path == "sevgi/standard"
|
|
33
|
+
|
|
34
|
+
# Reports whether an SVG element name is known.
|
|
35
|
+
# @return [Boolean]
|
|
19
36
|
def valid?(...) = true
|
|
20
37
|
end
|
|
21
38
|
|
|
22
39
|
private_class_method :new
|
|
23
40
|
|
|
41
|
+
# Sentinel parent used by root SVG elements.
|
|
24
42
|
RootParent = Object.new.tap { def it.inspect = "RootParent" }.freeze
|
|
25
43
|
|
|
44
|
+
# SVG element method-name normalization.
|
|
45
|
+
# @api private
|
|
26
46
|
module Ident
|
|
47
|
+
# Normalizes a Ruby method name into an SVG element id.
|
|
48
|
+
# @param given [Symbol, String] method name
|
|
49
|
+
# @return [Symbol]
|
|
27
50
|
def id(given) = (@id ||= {})[given] ||= given.to_s.tr("_", "-").to_sym
|
|
28
51
|
end
|
|
29
52
|
|
|
30
53
|
extend Ident
|
|
31
54
|
|
|
55
|
+
# @!attribute [r] name
|
|
56
|
+
# @return [Symbol] SVG element name
|
|
57
|
+
# @!attribute [r] attributes
|
|
58
|
+
# @return [Sevgi::Graphics::Attributes] attribute store
|
|
59
|
+
# @!attribute [r] children
|
|
60
|
+
# @return [Array<Sevgi::Graphics::Element>] child elements
|
|
61
|
+
# @!attribute [r] contents
|
|
62
|
+
# @return [Array<Sevgi::Graphics::Content>] element content objects
|
|
63
|
+
# @!attribute [r] parent
|
|
64
|
+
# @return [Sevgi::Graphics::Element, Object] parent element or root sentinel
|
|
32
65
|
attr_reader :name, :attributes, :children, :contents, :parent
|
|
33
66
|
|
|
67
|
+
# Creates an element.
|
|
68
|
+
# @param name [Symbol] SVG element name
|
|
69
|
+
# @param parent [Sevgi::Graphics::Element, Object] parent element or root sentinel
|
|
70
|
+
# @param attributes [Hash] SVG attributes
|
|
71
|
+
# @param contents [Array<Sevgi::Graphics::Content>] content objects
|
|
72
|
+
# @return [void]
|
|
34
73
|
def initialize(name, parent:, attributes: {}, contents: [], &block)
|
|
35
74
|
@name = name
|
|
36
75
|
@attributes = Attributes.new(attributes)
|
|
@@ -43,28 +82,49 @@ module Sevgi
|
|
|
43
82
|
instance_exec(&block) if block
|
|
44
83
|
end
|
|
45
84
|
|
|
85
|
+
# Dispatches SVG element DSL calls and caches valid element methods.
|
|
86
|
+
# @param name [Symbol] missing method name
|
|
87
|
+
# @return [Sevgi::Graphics::Element]
|
|
88
|
+
# @raise [NameError] when the name is not a valid SVG element
|
|
89
|
+
# @raise [Sevgi::ArgumentError] when an argument cannot be parsed as attributes or content
|
|
46
90
|
def method_missing(name, *, &block)
|
|
47
|
-
Element.valid?(
|
|
91
|
+
Element.valid?(tag = Element.id(name)) ? Dispatch.(self, name, tag, *, &block) : super
|
|
48
92
|
end
|
|
49
93
|
|
|
94
|
+
# Reports whether a missing method can dispatch to an SVG element.
|
|
95
|
+
# @param name [Symbol] queried method name
|
|
96
|
+
# @param include_private [Boolean] standard Ruby respond_to? flag
|
|
97
|
+
# @return [Boolean]
|
|
50
98
|
def respond_to_missing?(name, include_private = false)
|
|
51
99
|
Element.valid?(Element.id(name)) || super
|
|
52
100
|
end
|
|
53
101
|
|
|
102
|
+
# Element method-missing parser and cache.
|
|
103
|
+
# @api private
|
|
54
104
|
module Dispatch
|
|
55
105
|
extend self
|
|
56
106
|
|
|
57
|
-
|
|
107
|
+
# Dispatches an element DSL call.
|
|
108
|
+
# @param element [Sevgi::Graphics::Element] parent element
|
|
109
|
+
# @param method [Symbol] Ruby method name
|
|
110
|
+
# @param tag [Symbol] SVG element name
|
|
111
|
+
# @return [Sevgi::Graphics::Element]
|
|
112
|
+
def call(element, method, tag, *, &)
|
|
58
113
|
# Low-hanging fruit optimization: define missing method to avoid dispatching cost
|
|
59
|
-
unless Element.method_defined?(
|
|
114
|
+
unless Element.method_defined?(method)
|
|
60
115
|
Element.class_exec do
|
|
61
|
-
define_method(
|
|
116
|
+
define_method(method) { |*args, &block| self.class.element(tag, *args, parent: self, &block) }
|
|
62
117
|
end
|
|
63
118
|
end
|
|
64
119
|
|
|
65
|
-
element.public_send(
|
|
120
|
+
element.public_send(method, *, &)
|
|
66
121
|
end
|
|
67
122
|
|
|
123
|
+
# Parses element DSL arguments.
|
|
124
|
+
# @param name [Symbol] SVG element name
|
|
125
|
+
# @param args [Array<Object>] positional DSL arguments
|
|
126
|
+
# @return [Hash] parsed :attributes and :contents
|
|
127
|
+
# @raise [Sevgi::ArgumentError] when an argument is not a Hash, String, or Content
|
|
68
128
|
def parse(name, *args)
|
|
69
129
|
attributes, contents = {}, []
|
|
70
130
|
|
|
@@ -2,19 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Graphics
|
|
5
|
+
# Callable drawing module support.
|
|
6
|
+
# @api private
|
|
5
7
|
module Module
|
|
8
|
+
# Tracks newly defined public methods as callable drawing steps.
|
|
9
|
+
# @param method [Symbol] method name Ruby reports as added
|
|
10
|
+
# @return [Array<Symbol>, nil]
|
|
6
11
|
def method_added(method)
|
|
7
12
|
super
|
|
8
13
|
|
|
9
14
|
_callables << method if public_method_defined?(method)
|
|
10
15
|
end
|
|
11
16
|
|
|
17
|
+
# Class-level DSL for callable drawing modules.
|
|
18
|
+
# @api private
|
|
12
19
|
module DSL
|
|
13
|
-
|
|
20
|
+
# Registers a before or after hook.
|
|
21
|
+
# @param after [Boolean] true to register an after hook
|
|
22
|
+
# @return [Array<Proc>] hook list
|
|
23
|
+
def call(after = false, &block) = ((after ? _afters : _befores) << block)
|
|
14
24
|
end
|
|
15
25
|
|
|
16
26
|
private_constant :DSL
|
|
17
27
|
|
|
28
|
+
# Initializes callable module state.
|
|
29
|
+
# @param base [Module] extended module
|
|
30
|
+
# @return [void]
|
|
18
31
|
def self.extended(base)
|
|
19
32
|
base.instance_exec do
|
|
20
33
|
@_callables = []
|
|
@@ -29,6 +42,13 @@ module Sevgi
|
|
|
29
42
|
end
|
|
30
43
|
end
|
|
31
44
|
|
|
45
|
+
# @overload call(mod, receiver, *args, **kwargs)
|
|
46
|
+
# Runs module hooks and callables against a receiver.
|
|
47
|
+
# @param mod [Module] callable module
|
|
48
|
+
# @param receiver [Sevgi::Graphics::Element] receiver element
|
|
49
|
+
# @param args [Array<Object>] callable arguments
|
|
50
|
+
# @param kwargs [Hash] callable keyword arguments
|
|
51
|
+
# @return [Object, nil] last callable return value
|
|
32
52
|
def self.call(mod, receiver, ...)
|
|
33
53
|
mod._befores.each { receiver.Within(receiver, &it) } if mod.respond_to?(:_befores) && mod._befores
|
|
34
54
|
# return last callable return value
|
|
@@ -37,15 +57,27 @@ module Sevgi
|
|
|
37
57
|
end
|
|
38
58
|
end
|
|
39
59
|
|
|
60
|
+
# Returns the methods that should be executed for a callable module.
|
|
61
|
+
# @param mod [Module] callable module
|
|
62
|
+
# @return [Array<UnboundMethod>]
|
|
63
|
+
# @raise [Sevgi::ArgumentError] when mod is not a plain module
|
|
40
64
|
def self.callables(mod)
|
|
41
|
-
|
|
65
|
+
ArgumentError.("Must be a module: #{mod}") unless mod.instance_of?(::Module)
|
|
42
66
|
|
|
43
67
|
(mod.respond_to?(:_callables) ? mod._callables : mod.instance_methods).map { mod.instance_method(it) }
|
|
44
68
|
end
|
|
45
69
|
end
|
|
46
70
|
|
|
47
71
|
module Mixtures
|
|
72
|
+
# DSL helpers for invoking callable drawing modules.
|
|
48
73
|
module Call
|
|
74
|
+
# @overload Call(mod, *args, **kwargs)
|
|
75
|
+
# Runs a callable drawing module in the current element context.
|
|
76
|
+
# @param mod [Module] callable module
|
|
77
|
+
# @param args [Array<Object>] callable arguments
|
|
78
|
+
# @param kwargs [Hash] callable keyword arguments
|
|
79
|
+
# @return [Object, nil] last callable return value
|
|
80
|
+
# @raise [Sevgi::ArgumentError] when mod is not a plain module
|
|
49
81
|
def Call(mod, ...)
|
|
50
82
|
Graphics::Module.call(mod, self, ...)
|
|
51
83
|
end
|
|
@@ -54,7 +86,7 @@ module Sevgi
|
|
|
54
86
|
|
|
55
87
|
# rubocop:disable Metrics/MethodLength
|
|
56
88
|
def CallWithin(mod, container, element, *args, **kwargs, &block)
|
|
57
|
-
|
|
89
|
+
ArgumentError.("Must be a module: #{mod}") unless mod.instance_of?(::Module)
|
|
58
90
|
|
|
59
91
|
kwargs = kwargs.merge(id: F.demodulize(mod).to_sym) unless kwargs.key?(:id)
|
|
60
92
|
|