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,6 +5,22 @@ module Sevgi
|
|
|
5
5
|
module Mixtures
|
|
6
6
|
# Inkscape-specific SVG DSL helpers.
|
|
7
7
|
module Inkscape
|
|
8
|
+
# Normalizes callable wrapper attributes outside the DSL method surface.
|
|
9
|
+
# @api private
|
|
10
|
+
module Wrapper
|
|
11
|
+
# Returns owned wrapper attributes with a stable default id when the module is named.
|
|
12
|
+
# @param mod [Module] callable drawing module
|
|
13
|
+
# @param attributes [Hash] wrapper attributes
|
|
14
|
+
# @return [Hash{Symbol => Object}] normalized attributes
|
|
15
|
+
# @raise [Sevgi::ArgumentError] when attributes contains colliding names
|
|
16
|
+
def self.attributes(mod, attributes)
|
|
17
|
+
defaults = mod.name ? {id: F.demodulize(mod.name).to_sym} : {}
|
|
18
|
+
Attribute.defaults(attributes, **defaults)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private_constant :Wrapper
|
|
23
|
+
|
|
8
24
|
# Adds Inkscape template metadata.
|
|
9
25
|
# @param name [String] template name
|
|
10
26
|
# @param desc [String, nil] short description
|
|
@@ -23,42 +39,60 @@ module Sevgi
|
|
|
23
39
|
end
|
|
24
40
|
|
|
25
41
|
# Renders a callable module inside a group.
|
|
26
|
-
#
|
|
42
|
+
# Named modules default the group id to their final constant name. Anonymous modules omit the id unless supplied.
|
|
43
|
+
# @example Supply a stable id for an anonymous callable
|
|
44
|
+
# drawing = Module.new do
|
|
45
|
+
# extend Sevgi::Graphics::Module
|
|
46
|
+
# def call = circle r: 5
|
|
47
|
+
# end
|
|
48
|
+
# Sevgi::Graphics.SVG(:inkscape) { Group drawing, attributes: {id: "drawing"} }
|
|
49
|
+
# @param mod [Module] module extended with {Sevgi::Graphics::Module}
|
|
27
50
|
# @param args [Array<Object>] callable arguments
|
|
28
|
-
# @param
|
|
51
|
+
# @param attributes [Hash] group attributes; String and Symbol names are normalized and must not collide
|
|
52
|
+
# @param kwargs [Hash] callable keyword arguments
|
|
29
53
|
# @yield forwards customization to the callable module
|
|
30
54
|
# @yieldreturn [Object] callable customization result
|
|
31
55
|
# @return [Sevgi::Graphics::Element] group element
|
|
32
|
-
# @raise [Sevgi::ArgumentError] when mod is not a
|
|
33
|
-
def Group(mod, *args, **kwargs, &block)
|
|
34
|
-
|
|
35
|
-
|
|
56
|
+
# @raise [Sevgi::ArgumentError] when mod is not a callable drawing module or attributes is not a Hash
|
|
57
|
+
def Group(mod, *args, attributes: {}, **kwargs, &block)
|
|
58
|
+
Graphics::Module.__send__(:callables, mod)
|
|
59
|
+
ArgumentError.("Group attributes must be a Hash") unless attributes.is_a?(::Hash)
|
|
60
|
+
attributes = Wrapper.attributes(mod, attributes)
|
|
61
|
+
g(**attributes) { Call(mod, *args, **kwargs, &block) }
|
|
36
62
|
end
|
|
37
63
|
|
|
38
64
|
# Renders a callable module inside an Inkscape layer.
|
|
39
|
-
#
|
|
65
|
+
# Named modules default the layer id to their final constant name. Anonymous modules omit the id unless supplied.
|
|
66
|
+
# @param mod [Module] module extended with {Sevgi::Graphics::Module}
|
|
40
67
|
# @param args [Array<Object>] callable arguments
|
|
41
|
-
# @param
|
|
68
|
+
# @param attributes [Hash] layer attributes; String and Symbol names are normalized and must not collide
|
|
69
|
+
# @param kwargs [Hash] callable keyword arguments
|
|
42
70
|
# @yield forwards customization to the callable module
|
|
43
71
|
# @yieldreturn [Object] callable customization result
|
|
44
72
|
# @return [Sevgi::Graphics::Element] layer element
|
|
45
|
-
# @raise [Sevgi::ArgumentError] when mod is not a
|
|
46
|
-
def Layer(mod, *args, **kwargs, &block)
|
|
47
|
-
|
|
48
|
-
|
|
73
|
+
# @raise [Sevgi::ArgumentError] when mod is not a callable drawing module or attributes is not a Hash
|
|
74
|
+
def Layer(mod, *args, attributes: {}, **kwargs, &block)
|
|
75
|
+
Graphics::Module.__send__(:callables, mod)
|
|
76
|
+
ArgumentError.("Layer attributes must be a Hash") unless attributes.is_a?(::Hash)
|
|
77
|
+
attributes = Wrapper.attributes(mod, attributes)
|
|
78
|
+
layer(**attributes) { Call(mod, *args, **kwargs, &block) }
|
|
49
79
|
end
|
|
50
80
|
|
|
51
81
|
# Renders a callable module inside an insensitive Inkscape layer.
|
|
52
|
-
#
|
|
82
|
+
# Named modules default the layer id to their final constant name. Anonymous modules omit the id unless supplied.
|
|
83
|
+
# @param mod [Module] module extended with {Sevgi::Graphics::Module}
|
|
53
84
|
# @param args [Array<Object>] callable arguments
|
|
54
|
-
# @param
|
|
85
|
+
# @param attributes [Hash] layer attributes; String and Symbol names are normalized and must not collide
|
|
86
|
+
# @param kwargs [Hash] callable keyword arguments
|
|
55
87
|
# @yield forwards customization to the callable module
|
|
56
88
|
# @yieldreturn [Object] callable customization result
|
|
57
89
|
# @return [Sevgi::Graphics::Element] layer element
|
|
58
|
-
# @raise [Sevgi::ArgumentError] when mod is not a
|
|
59
|
-
def Layer!(mod, *args, **kwargs, &block)
|
|
60
|
-
|
|
61
|
-
|
|
90
|
+
# @raise [Sevgi::ArgumentError] when mod is not a callable drawing module or attributes is not a Hash
|
|
91
|
+
def Layer!(mod, *args, attributes: {}, **kwargs, &block)
|
|
92
|
+
Graphics::Module.__send__(:callables, mod)
|
|
93
|
+
ArgumentError.("Layer attributes must be a Hash") unless attributes.is_a?(::Hash)
|
|
94
|
+
attributes = Wrapper.attributes(mod, attributes)
|
|
95
|
+
layer!(**attributes) { Call(mod, *args, **kwargs, &block) }
|
|
62
96
|
end
|
|
63
97
|
|
|
64
98
|
# @overload layer(**attributes)
|
|
@@ -81,46 +115,166 @@ module Sevgi
|
|
|
81
115
|
layer("sodipodi:insensitive": "true", **, &block)
|
|
82
116
|
end
|
|
83
117
|
|
|
118
|
+
# Validates and constructs Inkscape page collections outside the DSL method surface.
|
|
119
|
+
# @api private
|
|
120
|
+
module Pagination
|
|
121
|
+
class << self
|
|
122
|
+
# Builds a namedview from normalized page attributes.
|
|
123
|
+
# @param context [Sevgi::Graphics::Element] DSL element receiving the namedview
|
|
124
|
+
# @param pages [Array<Hash>] page attributes
|
|
125
|
+
# @param namedview [Hash] namedview attributes
|
|
126
|
+
# @param page [Hash] shared page attributes
|
|
127
|
+
# @return [Sevgi::Graphics::Element] namedview element
|
|
128
|
+
# @raise [Sevgi::ArgumentError] when an attribute channel or page is invalid
|
|
129
|
+
def call(context, pages, namedview:, page:, &block)
|
|
130
|
+
namedview, page = channels(namedview, page)
|
|
131
|
+
pages = pages.each_with_index.map { |attributes, index| normalize(attributes, page, index) }
|
|
132
|
+
|
|
133
|
+
context.Element(:"sodipodi:namedview", **namedview) do
|
|
134
|
+
pages.each do |attributes|
|
|
135
|
+
element = Element(:"inkscape:page", **attributes)
|
|
136
|
+
block&.call(element)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Generates validated attributes for a rectangular page grid.
|
|
142
|
+
# @return [Array<Hash{Symbol => Object}>] page attribute hashes
|
|
143
|
+
# @raise [Sevgi::ArgumentError] when a count, dimension, or gap is invalid
|
|
144
|
+
def tabular(rows:, cols:, width:, height:, gap:)
|
|
145
|
+
width, height, gap = normalize_grid(rows:, cols:, width:, height:, gap:)
|
|
146
|
+
|
|
147
|
+
rows.times.flat_map do |row|
|
|
148
|
+
cols.times.map do |col|
|
|
149
|
+
x = col * (width + gap)
|
|
150
|
+
y = row * (height + gap)
|
|
151
|
+
label = "#{row + 1}x#{col + 1}"
|
|
152
|
+
{id: "pageview-#{label}", x:, y:, width:, height:}
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
# Validates and normalizes the namedview and page-default channels.
|
|
160
|
+
# @return [Array<Hash>] normalized namedview attributes and page defaults
|
|
161
|
+
# @raise [Sevgi::ArgumentError] when either channel is not a Hash or has invalid attributes
|
|
162
|
+
# @api private
|
|
163
|
+
def channels(namedview, page)
|
|
164
|
+
ArgumentError.("Namedview attributes must be a Hash") unless namedview.is_a?(::Hash)
|
|
165
|
+
ArgumentError.("Page attributes must be a Hash") unless page.is_a?(::Hash)
|
|
166
|
+
[Attribute.defaults(namedview, id: "namedview"), Attribute.normalize(page)]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Normalizes one explicit or generated page id.
|
|
170
|
+
# @return [Hash{String, Symbol => Object}] attributes beginning with a canonical id
|
|
171
|
+
# @raise [Sevgi::ArgumentError] when String and Symbol ids collide
|
|
172
|
+
# @api private
|
|
173
|
+
def identify(attributes, index)
|
|
174
|
+
id = attributes.key?(:id) ? attributes.delete(:id) : "page-#{index + 1}"
|
|
175
|
+
{id:}.merge(attributes)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Normalizes and validates one page.
|
|
179
|
+
# @return [Hash{String, Symbol => Object}] independent page attributes
|
|
180
|
+
# @raise [Sevgi::ArgumentError] when attributes or dimensions are invalid
|
|
181
|
+
# @api private
|
|
182
|
+
def normalize(attributes, defaults, index)
|
|
183
|
+
ArgumentError.("Page #{index + 1} must be a Hash") unless attributes.is_a?(::Hash)
|
|
184
|
+
attributes = defaults.merge(Attribute.normalize(attributes))
|
|
185
|
+
%i[x y].each { number(attributes, it, index) }
|
|
186
|
+
%i[width height].each do |field|
|
|
187
|
+
number(attributes, field, index, positive: true)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
identify(attributes, index)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Validates and normalizes page-grid arguments.
|
|
194
|
+
# @return [Array<(Integer, Float)>] normalized width, height, and gap
|
|
195
|
+
# @raise [Sevgi::ArgumentError] when a count, dimension, or gap is invalid
|
|
196
|
+
# @api private
|
|
197
|
+
def normalize_grid(rows:, cols:, width:, height:, gap:)
|
|
198
|
+
{rows:, cols:}.each do |field, count|
|
|
199
|
+
unless count.is_a?(::Integer) && count.positive?
|
|
200
|
+
ArgumentError.("Page #{field} must be a positive Integer")
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
[
|
|
205
|
+
Scalar.number(width, context: "page", field: :width, positive: true),
|
|
206
|
+
Scalar.number(height, context: "page", field: :height, positive: true),
|
|
207
|
+
Scalar.number(gap, context: "page", field: :gap, nonnegative: true)
|
|
208
|
+
]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Replaces one page field with its normalized SVG number.
|
|
212
|
+
# @return [Integer, Float] normalized number
|
|
213
|
+
# @api private
|
|
214
|
+
def number(attributes, field, index, **range)
|
|
215
|
+
key = attributes.key?(field) ? field : field.to_s
|
|
216
|
+
attributes[key] = Scalar.number(value(attributes, field, index), context: "page", field:, **range)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Returns one required page field.
|
|
220
|
+
# @return [Object] page field value
|
|
221
|
+
# @raise [Sevgi::ArgumentError] when the field is absent
|
|
222
|
+
# @api private
|
|
223
|
+
def value(attributes, field, index)
|
|
224
|
+
attributes.fetch(field) do
|
|
225
|
+
attributes.fetch(field.to_s) { ArgumentError.("Page #{index + 1} requires #{field}") }
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
private_constant :Pagination
|
|
232
|
+
|
|
84
233
|
# Builds an Inkscape namedview containing page elements.
|
|
85
|
-
# @
|
|
86
|
-
#
|
|
234
|
+
# @example Build explicit pages with separate namedview and page attributes
|
|
235
|
+
# Sevgi::Graphics.SVG(:inkscape) do
|
|
236
|
+
# Pages(
|
|
237
|
+
# {x: 0, y: 0, width: 100, height: 50, label: "front"},
|
|
238
|
+
# namedview: {id: "views"},
|
|
239
|
+
# page: {class: "print"}
|
|
240
|
+
# )
|
|
241
|
+
# end
|
|
242
|
+
# @param pages [Array<Hash>] page attribute hashes; numeric page fields are normalized to SVG numbers
|
|
243
|
+
# @param namedview [Hash] namedview attributes; String and Symbol names are normalized and must not collide
|
|
244
|
+
# @param page [Hash] page defaults; normalized page attributes override these defaults by name
|
|
87
245
|
# @yield [page] customizes each generated page element
|
|
88
246
|
# @yieldparam page [Sevgi::Graphics::Element] generated page element
|
|
89
247
|
# @yieldreturn [Object] ignored customization result
|
|
90
248
|
# @return [Sevgi::Graphics::Element] namedview element
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
pages.each_with_index do |page, i|
|
|
94
|
-
id = page[:id] || "page-#{i + 1}"
|
|
95
|
-
x, y, width, height = page.values_at(*%i[x y width height])
|
|
96
|
-
element = Element(:"inkscape:page", id:, x:, y:, width:, height:)
|
|
97
|
-
yield(element) if block
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
end
|
|
249
|
+
# @raise [Sevgi::ArgumentError] when an attribute channel, page, coordinate, or dimension is invalid
|
|
250
|
+
def Pages(*pages, namedview: {}, page: {}, &block) = Pagination.call(self, pages, namedview:, page:, &block)
|
|
101
251
|
|
|
102
252
|
# Builds a tabular set of Inkscape pages.
|
|
253
|
+
# @example Build a page grid using the same attribute channels as {#Pages}
|
|
254
|
+
# Sevgi::Graphics.SVG(:inkscape) do
|
|
255
|
+
# PagesTabular(
|
|
256
|
+
# rows: 2, cols: 3, width: 100, height: 50, gap: 5,
|
|
257
|
+
# namedview: {id: "views"}, page: {class: "print"}
|
|
258
|
+
# )
|
|
259
|
+
# end
|
|
103
260
|
# @param rows [Integer] number of rows
|
|
104
261
|
# @param cols [Integer] number of columns
|
|
105
|
-
# @param width [Numeric] page width
|
|
106
|
-
# @param height [Numeric] page height
|
|
107
|
-
# @param gap [Numeric] gap
|
|
108
|
-
# @param
|
|
109
|
-
# @
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
end
|
|
120
|
-
end
|
|
262
|
+
# @param width [Numeric] finite positive page width, normalized to an SVG number
|
|
263
|
+
# @param height [Numeric] finite positive page height, normalized to an SVG number
|
|
264
|
+
# @param gap [Numeric] finite non-negative gap, normalized to an SVG number
|
|
265
|
+
# @param namedview [Hash] namedview attributes; String and Symbol names are normalized and must not collide
|
|
266
|
+
# @param page [Hash] page defaults; normalized page attributes override these defaults by name
|
|
267
|
+
# @yield [page] customizes each generated page element
|
|
268
|
+
# @yieldparam page [Sevgi::Graphics::Element] generated page element
|
|
269
|
+
# @yieldreturn [Object] ignored customization result
|
|
270
|
+
# @return [Sevgi::Graphics::Element] namedview element
|
|
271
|
+
# @raise [Sevgi::ArgumentError] when counts, dimensions, gap, or an attribute channel is invalid
|
|
272
|
+
# @see #Pages
|
|
273
|
+
def PagesTabular(rows:, cols:, width:, height:, gap:, namedview: {}, page: {}, &block)
|
|
274
|
+
pages = Pagination.tabular(rows:, cols:, width:, height:, gap:)
|
|
275
|
+
Pages(*pages, namedview:, page:, &block)
|
|
121
276
|
end
|
|
122
277
|
|
|
123
|
-
# Internal symbol which does not show up Symbols Menu
|
|
124
278
|
# @overload symbol!(**attributes)
|
|
125
279
|
# Builds an Inkscape symbol group hidden from the symbols menu.
|
|
126
280
|
# @param attributes [Hash] symbol attributes
|
|
@@ -4,21 +4,47 @@ module Sevgi
|
|
|
4
4
|
module Graphics
|
|
5
5
|
module Mixtures
|
|
6
6
|
# DSL helpers for RDF and license metadata.
|
|
7
|
+
#
|
|
8
|
+
# @example Add Creative Commons metadata
|
|
9
|
+
# Sevgi::Graphics.SVG :inkscape do
|
|
10
|
+
# License_CC0 title: "Example", creator: "A. Creator"
|
|
11
|
+
# end
|
|
7
12
|
module RDF
|
|
13
|
+
WORK_OPTIONS = %i[title description creator publisher date language license].freeze
|
|
14
|
+
private_constant :WORK_OPTIONS
|
|
15
|
+
|
|
8
16
|
# Adds RDF license metadata inside a metadata element.
|
|
9
17
|
# @param kwargs [Hash] RDF work options
|
|
10
18
|
# @yield evaluates additional RDF work metadata
|
|
11
19
|
# @yieldreturn [Object] ignored block result
|
|
12
20
|
# @return [Sevgi::Graphics::Element] metadata element
|
|
13
|
-
|
|
21
|
+
# @raise [Sevgi::ArgumentError] when an option is unknown
|
|
22
|
+
# @see #RDFWork
|
|
23
|
+
def License(**kwargs, &block)
|
|
24
|
+
unknown = kwargs.keys - WORK_OPTIONS
|
|
25
|
+
ArgumentError.("Unknown license options: #{unknown.join(", ")}") unless unknown.empty?
|
|
26
|
+
|
|
27
|
+
metadata { RDFWork(**kwargs, &block) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Adds Creative Commons BY license metadata.
|
|
31
|
+
# @param kwargs [Hash] RDF work options
|
|
32
|
+
# @yield evaluates additional RDF work metadata
|
|
33
|
+
# @yieldreturn [Object] ignored block result
|
|
34
|
+
# @return [Sevgi::Graphics::Element] metadata element
|
|
35
|
+
# @raise [Sevgi::ArgumentError] when an option is unknown
|
|
36
|
+
# @see #RDFWork
|
|
37
|
+
def License_CC_BY(**kwargs, &block)
|
|
38
|
+
License(**kwargs, license: "https://creativecommons.org/licenses/by/4.0/", &block)
|
|
39
|
+
end
|
|
14
40
|
|
|
15
|
-
# Use SPDX license codes in underscored form: https://spdx.org/licenses/
|
|
16
|
-
#
|
|
17
41
|
# Adds Creative Commons BY-SA license metadata.
|
|
18
42
|
# @param kwargs [Hash] RDF work options
|
|
19
43
|
# @yield evaluates additional RDF work metadata
|
|
20
44
|
# @yieldreturn [Object] ignored block result
|
|
21
45
|
# @return [Sevgi::Graphics::Element] metadata element
|
|
46
|
+
# @raise [Sevgi::ArgumentError] when an option is unknown
|
|
47
|
+
# @see #RDFWork
|
|
22
48
|
def License_CC_BY_SA(**kwargs, &block)
|
|
23
49
|
License(**kwargs, license: "https://creativecommons.org/licenses/by-sa/4.0/", &block)
|
|
24
50
|
end
|
|
@@ -28,6 +54,8 @@ module Sevgi
|
|
|
28
54
|
# @yield evaluates additional RDF work metadata
|
|
29
55
|
# @yieldreturn [Object] ignored block result
|
|
30
56
|
# @return [Sevgi::Graphics::Element] metadata element
|
|
57
|
+
# @raise [Sevgi::ArgumentError] when an option is unknown
|
|
58
|
+
# @see #RDFWork
|
|
31
59
|
def License_CC_BY_NC(**kwargs, &block)
|
|
32
60
|
License(**kwargs, license: "https://creativecommons.org/licenses/by-nc/4.0/", &block)
|
|
33
61
|
end
|
|
@@ -37,6 +65,8 @@ module Sevgi
|
|
|
37
65
|
# @yield evaluates additional RDF work metadata
|
|
38
66
|
# @yieldreturn [Object] ignored block result
|
|
39
67
|
# @return [Sevgi::Graphics::Element] metadata element
|
|
68
|
+
# @raise [Sevgi::ArgumentError] when an option is unknown
|
|
69
|
+
# @see #RDFWork
|
|
40
70
|
def License_CC_BY_NC_SA(**kwargs, &block)
|
|
41
71
|
License(**kwargs, license: "https://creativecommons.org/licenses/by-nc-sa/4.0/", &block)
|
|
42
72
|
end
|
|
@@ -46,6 +76,8 @@ module Sevgi
|
|
|
46
76
|
# @yield evaluates additional RDF work metadata
|
|
47
77
|
# @yieldreturn [Object] ignored block result
|
|
48
78
|
# @return [Sevgi::Graphics::Element] metadata element
|
|
79
|
+
# @raise [Sevgi::ArgumentError] when an option is unknown
|
|
80
|
+
# @see #RDFWork
|
|
49
81
|
def License_CC_BY_ND(**kwargs, &block)
|
|
50
82
|
License(**kwargs, license: "https://creativecommons.org/licenses/by-nd/4.0/", &block)
|
|
51
83
|
end
|
|
@@ -55,6 +87,8 @@ module Sevgi
|
|
|
55
87
|
# @yield evaluates additional RDF work metadata
|
|
56
88
|
# @yieldreturn [Object] ignored block result
|
|
57
89
|
# @return [Sevgi::Graphics::Element] metadata element
|
|
90
|
+
# @raise [Sevgi::ArgumentError] when an option is unknown
|
|
91
|
+
# @see #RDFWork
|
|
58
92
|
def License_CC_BY_NC_ND(**kwargs, &block)
|
|
59
93
|
License(**kwargs, license: "https://creativecommons.org/licenses/by-nc-nd/4.0/", &block)
|
|
60
94
|
end
|
|
@@ -64,6 +98,8 @@ module Sevgi
|
|
|
64
98
|
# @yield evaluates additional RDF work metadata
|
|
65
99
|
# @yieldreturn [Object] ignored block result
|
|
66
100
|
# @return [Sevgi::Graphics::Element] metadata element
|
|
101
|
+
# @raise [Sevgi::ArgumentError] when an option is unknown
|
|
102
|
+
# @see #RDFWork
|
|
67
103
|
def License_CC0(**kwargs, &block)
|
|
68
104
|
License(**kwargs, license: "https://creativecommons.org/publicdomain/zero/1.0/", &block)
|
|
69
105
|
end
|
|
@@ -73,15 +109,19 @@ module Sevgi
|
|
|
73
109
|
# @yield evaluates additional RDF work metadata
|
|
74
110
|
# @yieldreturn [Object] ignored block result
|
|
75
111
|
# @return [Sevgi::Graphics::Element] metadata element
|
|
112
|
+
# @raise [Sevgi::ArgumentError] when an option is unknown
|
|
113
|
+
# @see #RDFWork
|
|
76
114
|
def License_LAL(**kwargs, &block) = License(**kwargs, license: "https://artlibre.org/licence/lal/en/", &block)
|
|
77
115
|
|
|
78
116
|
# Builds an RDF root element.
|
|
79
|
-
# @param
|
|
117
|
+
# @param kwargs [Hash] options; RDF currently accepts none
|
|
80
118
|
# @yield evaluates the RDF drawing DSL
|
|
81
119
|
# @yieldreturn [Object] ignored block result
|
|
82
120
|
# @return [Sevgi::Graphics::Element] RDF element
|
|
83
121
|
# @raise [Sevgi::ArgumentError] when no block is given
|
|
84
|
-
|
|
122
|
+
# @raise [Sevgi::ArgumentError] when an option is given
|
|
123
|
+
def RDF(**kwargs, &block)
|
|
124
|
+
ArgumentError.("Unknown RDF options: #{kwargs.keys.join(", ")}") unless kwargs.empty?
|
|
85
125
|
ArgumentError.("Block required") unless block
|
|
86
126
|
|
|
87
127
|
Element(
|
|
@@ -94,7 +134,6 @@ module Sevgi
|
|
|
94
134
|
end
|
|
95
135
|
end
|
|
96
136
|
|
|
97
|
-
# rubocop:disable Metrics/MethodLength
|
|
98
137
|
# Builds a Creative Commons RDF Work element.
|
|
99
138
|
# @param kwargs [Hash] RDF work options
|
|
100
139
|
# @option kwargs [String] :title work title
|
|
@@ -104,10 +143,24 @@ module Sevgi
|
|
|
104
143
|
# @option kwargs [String] :date date
|
|
105
144
|
# @option kwargs [String] :language language
|
|
106
145
|
# @option kwargs [String] :license license URL
|
|
146
|
+
#
|
|
147
|
+
# | Option | RDF element |
|
|
148
|
+
# | --- | --- |
|
|
149
|
+
# | `title` | `dc:title` |
|
|
150
|
+
# | `description` | `dc:description` |
|
|
151
|
+
# | `creator` | `dc:creator` |
|
|
152
|
+
# | `publisher` | `dc:publisher` |
|
|
153
|
+
# | `date` | `dc:date` |
|
|
154
|
+
# | `language` | `dc:language` |
|
|
155
|
+
# | `license` | `cc:license` resource |
|
|
107
156
|
# @yield evaluates additional RDF work metadata
|
|
108
157
|
# @yieldreturn [Object] ignored block result
|
|
109
158
|
# @return [Sevgi::Graphics::Element] RDF element
|
|
159
|
+
# @raise [Sevgi::ArgumentError] when an option is unknown
|
|
110
160
|
def RDFWork(**kwargs, &block)
|
|
161
|
+
unknown = kwargs.keys - WORK_OPTIONS
|
|
162
|
+
ArgumentError.("Unknown RDF work options: #{unknown.join(", ")}") unless unknown.empty?
|
|
163
|
+
|
|
111
164
|
RDF do
|
|
112
165
|
Element(:"cc:Work", "rdf:about": "") do
|
|
113
166
|
Element(:"dc:format", "image/svg+xml")
|
|
@@ -124,7 +177,6 @@ module Sevgi
|
|
|
124
177
|
end
|
|
125
178
|
end
|
|
126
179
|
end
|
|
127
|
-
# rubocop:enable Metrics/MethodLength
|
|
128
180
|
end
|
|
129
181
|
end
|
|
130
182
|
end
|
|
@@ -10,6 +10,7 @@ module Sevgi
|
|
|
10
10
|
class Renderer
|
|
11
11
|
# Default renderer options.
|
|
12
12
|
DEFAULTS = {indent: " ", linelength: 140, style: :hybrid}.freeze
|
|
13
|
+
STYLES = %i[hybrid inline block].freeze
|
|
13
14
|
SVG_NAMESPACE = "http://www.w3.org/2000/svg"
|
|
14
15
|
|
|
15
16
|
# Attribute rendering strategies.
|
|
@@ -23,7 +24,7 @@ module Sevgi
|
|
|
23
24
|
# @param depth [Integer] element depth
|
|
24
25
|
# @return [void]
|
|
25
26
|
def attributes(element, depth)
|
|
26
|
-
attributes_block(element, depth, element.attributes.
|
|
27
|
+
attributes_block(element, depth, element.attributes.send(:xml_lines))
|
|
27
28
|
end
|
|
28
29
|
end
|
|
29
30
|
|
|
@@ -35,7 +36,7 @@ module Sevgi
|
|
|
35
36
|
# @param depth [Integer] element depth
|
|
36
37
|
# @return [void]
|
|
37
38
|
def attributes(element, depth)
|
|
38
|
-
if attributes_as_block?(lines = element.attributes.
|
|
39
|
+
if attributes_as_block?(lines = element.attributes.send(:xml_lines), depth)
|
|
39
40
|
attributes_block(element, depth, lines)
|
|
40
41
|
else
|
|
41
42
|
attributes_inline(element, depth, lines)
|
|
@@ -67,7 +68,7 @@ module Sevgi
|
|
|
67
68
|
# @param depth [Integer] element depth
|
|
68
69
|
# @return [void]
|
|
69
70
|
def attributes(element, depth)
|
|
70
|
-
attributes_inline(element, depth, element.attributes.
|
|
71
|
+
attributes_inline(element, depth, element.attributes.send(:xml_lines))
|
|
71
72
|
end
|
|
72
73
|
end
|
|
73
74
|
end
|
|
@@ -78,7 +79,13 @@ module Sevgi
|
|
|
78
79
|
ELEMENTS_WITH_BLOCK_CONTENT = %i[style].freeze
|
|
79
80
|
SEPARATOR = "\n"
|
|
80
81
|
|
|
81
|
-
private_constant
|
|
82
|
+
private_constant(
|
|
83
|
+
:ELEMENTS_WITH_INLINE_CONTENT,
|
|
84
|
+
:ELEMENTS_WITH_BLOCK_CONTENT,
|
|
85
|
+
:SEPARATOR,
|
|
86
|
+
:STYLES,
|
|
87
|
+
:SVG_NAMESPACE
|
|
88
|
+
)
|
|
82
89
|
|
|
83
90
|
# @return [Sevgi::Graphics::Element] root element
|
|
84
91
|
attr_reader :root
|
|
@@ -165,7 +172,7 @@ module Sevgi
|
|
|
165
172
|
# @raise [Sevgi::ArgumentError] when an option is unknown, malformed, missing, or unsupported
|
|
166
173
|
def initialize(root, **)
|
|
167
174
|
@root = root
|
|
168
|
-
@options =
|
|
175
|
+
@options = self.class.send(:validate, **)
|
|
169
176
|
@output = []
|
|
170
177
|
@inlines = Inlines.new
|
|
171
178
|
|
|
@@ -187,6 +194,29 @@ module Sevgi
|
|
|
187
194
|
# @raise [Sevgi::ArgumentError] when options, names, attributes, or content are invalid XML
|
|
188
195
|
def self.fragment(root, **options) = new(root, **options).call
|
|
189
196
|
|
|
197
|
+
def self.validate(**options)
|
|
198
|
+
options = DEFAULTS.merge(options)
|
|
199
|
+
unknown = options.keys - DEFAULTS.keys
|
|
200
|
+
ArgumentError.("Unknown renderer options: #{unknown.join(", ")}") unless unknown.empty?
|
|
201
|
+
|
|
202
|
+
indent = options[:indent]
|
|
203
|
+
unless indent.is_a?(::String) && /\A[\t\n\r ]*\z/.match?(indent)
|
|
204
|
+
ArgumentError.("Renderer indent must contain only XML whitespace")
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
options[:indent] = XML.text(indent, context: "Renderer indent")
|
|
208
|
+
linelength = options[:linelength]
|
|
209
|
+
unless linelength.is_a?(::Integer) && linelength >= 0
|
|
210
|
+
ArgumentError.("Renderer linelength must be a non-negative Integer")
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
ArgumentError.("Unrecognized style: #{options[:style]}") unless STYLES.include?(options[:style])
|
|
214
|
+
|
|
215
|
+
options.freeze
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
private_class_method :validate
|
|
219
|
+
|
|
190
220
|
# Appends rendered lines to the output buffer.
|
|
191
221
|
# @param depth [Integer, nil] indentation depth
|
|
192
222
|
# @param lines [Array<String>] rendered lines
|
|
@@ -236,9 +266,6 @@ module Sevgi
|
|
|
236
266
|
end
|
|
237
267
|
|
|
238
268
|
def build
|
|
239
|
-
validate_options!
|
|
240
|
-
ArgumentError.("Missing style") unless options[:style]
|
|
241
|
-
|
|
242
269
|
case options[:style]
|
|
243
270
|
when :hybrid
|
|
244
271
|
extend(Attributes::Hybrid)
|
|
@@ -246,8 +273,6 @@ module Sevgi
|
|
|
246
273
|
extend(Attributes::Inline)
|
|
247
274
|
when :block
|
|
248
275
|
extend(Attributes::Block)
|
|
249
|
-
else
|
|
250
|
-
ArgumentError.("Unrecognized style: #{options[:style]}")
|
|
251
276
|
end
|
|
252
277
|
|
|
253
278
|
unclosed
|
|
@@ -330,21 +355,6 @@ module Sevgi
|
|
|
330
355
|
|
|
331
356
|
def unclosed = @closed = false
|
|
332
357
|
|
|
333
|
-
def validate_options!
|
|
334
|
-
unknown = options.keys - DEFAULTS.keys
|
|
335
|
-
ArgumentError.("Unknown renderer options: #{unknown.join(", ")}") unless unknown.empty?
|
|
336
|
-
|
|
337
|
-
indent = options[:indent]
|
|
338
|
-
unless indent.is_a?(::String) && /\A[\t\n\r ]*\z/.match?(indent)
|
|
339
|
-
ArgumentError.("Renderer indent must contain only XML whitespace")
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
options[:indent] = XML.text(indent, context: "Renderer indent")
|
|
343
|
-
linelength = options[:linelength]
|
|
344
|
-
return if linelength.is_a?(::Integer) && linelength >= 0
|
|
345
|
-
|
|
346
|
-
ArgumentError.("Renderer linelength must be a non-negative Integer")
|
|
347
|
-
end
|
|
348
358
|
end
|
|
349
359
|
|
|
350
360
|
private_constant :Renderer
|
|
@@ -355,7 +365,13 @@ module Sevgi
|
|
|
355
365
|
# descendants in the same text line. Whitespace inside content objects is preserved as given, and encoded
|
|
356
366
|
# content is XML-escaped unless a verbatim content object is used. SVG `style` elements use block content;
|
|
357
367
|
# same-named elements under a foreign default namespace retain ordinary inline text formatting.
|
|
368
|
+
# @example Keep every attribute on the element's opening line
|
|
369
|
+
# SVG(:minimal) { rect id: "card", width: 80, height: 40 }.Render(style: :inline)
|
|
358
370
|
# @param options [Hash] renderer options
|
|
371
|
+
# @option options [String] :indent (" ") XML-whitespace indentation unit
|
|
372
|
+
# @option options [Integer] :linelength (140) non-negative line length that switches hybrid attributes to block
|
|
373
|
+
# style
|
|
374
|
+
# @option options [Symbol] :style (:hybrid) attribute layout: `:hybrid`, `:inline`, or `:block`
|
|
359
375
|
# @return [String] SVG source
|
|
360
376
|
# @raise [Sevgi::ArgumentError] when options, preambles, names, attributes, or content are invalid XML
|
|
361
377
|
def Render(**) = Renderer.(self, **)
|
|
@@ -363,14 +379,22 @@ module Sevgi
|
|
|
363
379
|
# Renders only this element's children.
|
|
364
380
|
# Child render output omits document preambles and preserves each child's text whitespace and inline
|
|
365
381
|
# mixed-content formatting.
|
|
382
|
+
# @example Render child fragments with block-style attributes
|
|
383
|
+
# Sevgi::Graphics.SVG(:minimal) { rect id: "one" }.RenderChildren(style: :block, indent: "\t")
|
|
366
384
|
# @param separator [String] separator between child documents
|
|
385
|
+
# @param options [Hash] renderer options applied to every child fragment
|
|
386
|
+
# @option options [String] :indent (" ") XML-whitespace indentation unit
|
|
387
|
+
# @option options [Integer] :linelength (140) non-negative line length that switches hybrid attributes to block
|
|
388
|
+
# style
|
|
389
|
+
# @option options [Symbol] :style (:hybrid) attribute layout: `:hybrid`, `:inline`, or `:block`
|
|
367
390
|
# @return [String] rendered child fragments
|
|
368
|
-
# @raise [Sevgi::ArgumentError] when separator or rendered child data is
|
|
369
|
-
def RenderChildren(separator = "\n\n")
|
|
391
|
+
# @raise [Sevgi::ArgumentError] when separator, options, or rendered child data is invalid
|
|
392
|
+
def RenderChildren(separator = "\n\n", **options)
|
|
370
393
|
ArgumentError.("SVG fragment separator must be a String") unless separator.is_a?(::String)
|
|
371
394
|
|
|
372
395
|
separator = XML.text(separator, context: "SVG fragment separator")
|
|
373
|
-
|
|
396
|
+
Renderer.send(:validate, **options)
|
|
397
|
+
children.map { Renderer.fragment(it, **options) }.join(separator)
|
|
374
398
|
end
|
|
375
399
|
end
|
|
376
400
|
end
|