sevgi-graphics 0.95.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +221 -2
  3. data/README.md +12 -9
  4. data/lib/sevgi/graphics/attribute.rb +166 -45
  5. data/lib/sevgi/graphics/auxiliary/canvas.rb +100 -43
  6. data/lib/sevgi/graphics/auxiliary/content.rb +56 -47
  7. data/lib/sevgi/graphics/auxiliary/margin.rb +19 -12
  8. data/lib/sevgi/graphics/auxiliary/paper.rb +74 -49
  9. data/lib/sevgi/graphics/auxiliary/path.rb +44 -0
  10. data/lib/sevgi/graphics/auxiliary/scalar.rb +36 -7
  11. data/lib/sevgi/graphics/auxiliary.rb +1 -0
  12. data/lib/sevgi/graphics/document/base.rb +6 -2
  13. data/lib/sevgi/graphics/document/default.rb +1 -1
  14. data/lib/sevgi/graphics/document.rb +239 -117
  15. data/lib/sevgi/graphics/element.rb +132 -34
  16. data/lib/sevgi/graphics/mixtures/call.rb +234 -88
  17. data/lib/sevgi/graphics/mixtures/core.rb +67 -27
  18. data/lib/sevgi/graphics/mixtures/duplicate.rb +47 -25
  19. data/lib/sevgi/graphics/mixtures/export.rb +54 -12
  20. data/lib/sevgi/graphics/mixtures/hatch.rb +49 -7
  21. data/lib/sevgi/graphics/mixtures/identify.rb +30 -17
  22. data/lib/sevgi/graphics/mixtures/include.rb +26 -8
  23. data/lib/sevgi/graphics/mixtures/inkscape.rb +214 -47
  24. data/lib/sevgi/graphics/mixtures/rdf.rb +59 -7
  25. data/lib/sevgi/graphics/mixtures/render.rb +60 -120
  26. data/lib/sevgi/graphics/mixtures/save.rb +79 -35
  27. data/lib/sevgi/graphics/mixtures/symbols.rb +81 -12
  28. data/lib/sevgi/graphics/mixtures/tile.rb +85 -67
  29. data/lib/sevgi/graphics/mixtures/transform.rb +89 -30
  30. data/lib/sevgi/graphics/mixtures/underscore.rb +16 -7
  31. data/lib/sevgi/graphics/mixtures/validate.rb +2 -2
  32. data/lib/sevgi/graphics/mixtures/wrappers.rb +111 -23
  33. data/lib/sevgi/graphics/mixtures.rb +15 -13
  34. data/lib/sevgi/graphics/version.rb +1 -1
  35. data/lib/sevgi/graphics/xml.rb +4 -9
  36. data/lib/sevgi/graphics.rb +69 -18
  37. metadata +7 -6
@@ -5,21 +5,31 @@ module Sevgi
5
5
  module Mixtures
6
6
  # DSL helpers for including derendered SVG/XML fragments.
7
7
  #
8
- # @!method Include(file, id)
8
+ # @!method Include(file, id, omit: nil)
9
9
  # Includes a derendered node matching an id.
10
10
  # SVG/XML content is treated as data and is not evaluated as Ruby source.
11
+ # @example Import a fragment without editor ids and inline styles
12
+ # Sevgi::Graphics.SVG do
13
+ # Include "badge.svg", "mark", omit: %i[id style]
14
+ # end
11
15
  # @param file [String] source SVG/XML file
12
16
  # @param id [String, Symbol] source node id
17
+ # @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
18
+ # subtree after id selection
13
19
  # @return [Sevgi::Graphics::Element, nil] included element, or nil when it produces no graphics output
14
20
  # @raise [Sevgi::ArgumentError] when the file is absent or XML content is malformed, rootless, or lacks the id
21
+ # @raise [SystemCallError] when the file cannot be read
15
22
  # @raise [Sevgi::MissingComponentError] when sevgi/derender is unavailable
16
- # @!method IncludeChildren(file, id)
23
+ # @!method IncludeChildren(file, id, omit: nil)
17
24
  # Includes the children of a derendered node matching an id.
18
25
  # SVG/XML content is treated as data and is not evaluated as Ruby source.
19
26
  # @param file [String] source SVG/XML file
20
27
  # @param id [String, Symbol] source node id
21
- # @return [Array<Sevgi::Graphics::Element>] included children
28
+ # @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
29
+ # subtree after id selection
30
+ # @return [Array<Sevgi::Graphics::Element>] immutable included-child snapshot
22
31
  # @raise [Sevgi::ArgumentError] when the file is absent or XML content is malformed, rootless, or lacks the id
32
+ # @raise [SystemCallError] when the file cannot be read
23
33
  # @raise [Sevgi::MissingComponentError] when sevgi/derender is unavailable
24
34
  module Include
25
35
  require "sevgi/derender"
@@ -29,38 +39,46 @@ module Sevgi
29
39
  # SVG/XML file content is treated as data and is not evaluated as Ruby source.
30
40
  # @param file [String] source SVG/XML file
31
41
  # @param id [String, Symbol] source node id
42
+ # @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
43
+ # subtree after id selection
32
44
  # @return [Sevgi::Graphics::Element, nil] included element, or nil when the selected node produces no graphics
33
45
  # output
34
46
  # @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
35
47
  # absent
48
+ # @raise [SystemCallError] when the file cannot be read
36
49
  # @raise [Sevgi::MissingComponentError] when sevgi/derender is unavailable
37
- def Include(file, id) = Derender.evaluate_file(file, self, id:)
50
+ def Include(file, id, omit: nil) = Derender.evaluate_file(file, self, id:, omit:)
38
51
 
39
52
  # Includes the children of a derendered node matching an id.
40
53
  #
41
54
  # SVG/XML file content is treated as data and is not evaluated as Ruby source.
42
55
  # @param file [String] source SVG/XML file
43
56
  # @param id [String, Symbol] source node id
44
- # @return [Array<Sevgi::Graphics::Element>] included children
57
+ # @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
58
+ # subtree after id selection
59
+ # @return [Array<Sevgi::Graphics::Element>] immutable included-child snapshot
45
60
  # @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
46
61
  # absent
62
+ # @raise [SystemCallError] when the file cannot be read
47
63
  # @raise [Sevgi::MissingComponentError] when sevgi/derender is unavailable
48
- def IncludeChildren(file, id) = Derender.evaluate_file_children(file, self, id:)
64
+ def IncludeChildren(file, id, omit: nil) = Derender.evaluate_children_file(file, self, id:, omit:)
49
65
  rescue ::LoadError => e
50
66
  raise unless e.path == "sevgi/derender"
51
67
 
52
- # @overload IncludeChildren(file, id)
68
+ # @overload IncludeChildren(file, id, omit: nil)
53
69
  # Raises because sevgi/derender is unavailable.
54
70
  # @param file [String] source SVG/XML file
55
71
  # @param id [String, Symbol] source node id
72
+ # @param omit [String, Symbol, Array<String, Symbol>, nil] ignored because the component is unavailable
56
73
  # @return [void]
57
74
  # @raise [Sevgi::MissingComponentError] always
58
75
  def IncludeChildren(...) = MissingComponentError.("sevgi/derender")
59
76
 
60
- # @overload Include(file, id)
77
+ # @overload Include(file, id, omit: nil)
61
78
  # Raises because sevgi/derender is unavailable.
62
79
  # @param file [String] source SVG/XML file
63
80
  # @param id [String, Symbol] source node id
81
+ # @param omit [String, Symbol, Array<String, Symbol>, nil] ignored because the component is unavailable
64
82
  # @return [void]
65
83
  # @raise [Sevgi::MissingComponentError] always
66
84
  def Include(...) = MissingComponentError.("sevgi/derender")
@@ -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
- # @param mod [Module] callable drawing module
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 kwargs [Hash] group attributes
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 plain module
33
- def Group(mod, *args, **kwargs, &block)
34
- kwargs = kwargs.merge(id: F.demodulize(mod).to_sym) unless kwargs.key?(:id)
35
- g(**kwargs) { Call(mod, *args, &block) }
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
- # @param mod [Module] callable drawing module
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 kwargs [Hash] layer attributes
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 plain module
46
- def Layer(mod, *args, **kwargs, &block)
47
- kwargs = kwargs.merge(id: F.demodulize(mod).to_sym) unless kwargs.key?(:id)
48
- layer(**kwargs) { Call(mod, *args, &block) }
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
- # @param mod [Module] callable drawing module
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 kwargs [Hash] layer attributes
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 plain module
59
- def Layer!(mod, *args, **kwargs, &block)
60
- kwargs = kwargs.merge(id: F.demodulize(mod).to_sym) unless kwargs.key?(:id)
61
- layer!(**kwargs) { Call(mod, *args, &block) }
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,179 @@ 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"), page_attributes(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(page_attributes(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
+ # Coerces numeric page fields before attribute snapshots stringify mutable numeric subclasses.
194
+ def page_attributes(attributes)
195
+ Attribute.normalize(
196
+ attributes.to_h do |key, value|
197
+ if %w[x y width height].include?(key.to_s) && value.is_a?(::Numeric)
198
+ value = Scalar.number(value, context: "page", field: key)
199
+ end
200
+
201
+ [key, value]
202
+ end
203
+ )
204
+ end
205
+
206
+ # Validates and normalizes page-grid arguments.
207
+ # @return [Array<(Integer, Float)>] normalized width, height, and gap
208
+ # @raise [Sevgi::ArgumentError] when a count, dimension, or gap is invalid
209
+ # @api private
210
+ def normalize_grid(rows:, cols:, width:, height:, gap:)
211
+ {rows:, cols:}.each do |field, count|
212
+ unless count.is_a?(::Integer) && count.positive?
213
+ ArgumentError.("Page #{field} must be a positive Integer")
214
+ end
215
+ end
216
+
217
+ [
218
+ Scalar.number(width, context: "page", field: :width, positive: true),
219
+ Scalar.number(height, context: "page", field: :height, positive: true),
220
+ Scalar.number(gap, context: "page", field: :gap, nonnegative: true)
221
+ ]
222
+ end
223
+
224
+ # Replaces one page field with its normalized SVG number.
225
+ # @return [Integer, Float] normalized number
226
+ # @api private
227
+ def number(attributes, field, index, **range)
228
+ key = attributes.key?(field) ? field : field.to_s
229
+ attributes[key] = Scalar.number(value(attributes, field, index), context: "page", field:, **range)
230
+ end
231
+
232
+ # Returns one required page field.
233
+ # @return [Object] page field value
234
+ # @raise [Sevgi::ArgumentError] when the field is absent
235
+ # @api private
236
+ def value(attributes, field, index)
237
+ attributes.fetch(field) do
238
+ attributes.fetch(field.to_s) { ArgumentError.("Page #{index + 1} requires #{field}") }
239
+ end
240
+ end
241
+ end
242
+ end
243
+
244
+ private_constant :Pagination
245
+
84
246
  # Builds an Inkscape namedview containing page elements.
85
- # @param pages [Array<Hash>] page attribute hashes
86
- # @param id [String] namedview id
247
+ # @example Build explicit pages with separate namedview and page attributes
248
+ # Sevgi::Graphics.SVG(:inkscape) do
249
+ # Pages(
250
+ # {x: 0, y: 0, width: 100, height: 50, label: "front"},
251
+ # namedview: {id: "views"},
252
+ # page: {class: "print"}
253
+ # )
254
+ # end
255
+ # @param pages [Array<Hash>] page attribute hashes. Numeric page fields are normalized to SVG numbers
256
+ # @param namedview [Hash] namedview attributes. String and Symbol names are normalized and must not collide
257
+ # @param page [Hash] page defaults. Normalized page attributes override these defaults by name
87
258
  # @yield [page] customizes each generated page element
88
259
  # @yieldparam page [Sevgi::Graphics::Element] generated page element
89
260
  # @yieldreturn [Object] ignored customization result
90
261
  # @return [Sevgi::Graphics::Element] namedview element
91
- def Pages(*pages, id: "namedview", **, &block)
92
- Element(:"sodipodi:namedview", id:, **) do
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
262
+ # @raise [Sevgi::ArgumentError] when an attribute channel, page, coordinate, or dimension is invalid
263
+ def Pages(*pages, namedview: {}, page: {}, &block) = Pagination.call(self, pages, namedview:, page:, &block)
101
264
 
102
265
  # Builds a tabular set of Inkscape pages.
266
+ # @example Build a page grid using the same attribute channels as {#Pages}
267
+ # Sevgi::Graphics.SVG(:inkscape) do
268
+ # PagesTabular(
269
+ # rows: 2, cols: 3, width: 100, height: 50, gap: 5,
270
+ # namedview: {id: "views"}, page: {class: "print"}
271
+ # )
272
+ # end
103
273
  # @param rows [Integer] number of rows
104
274
  # @param cols [Integer] number of columns
105
- # @param width [Numeric] page width
106
- # @param height [Numeric] page height
107
- # @param gap [Numeric] gap between pages
108
- # @param id [String] namedview id
109
- # @return [Array<Array>] matrix entries as x, y, and label tuples
110
- def PagesTabular(rows:, cols:, width:, height:, gap:, id: "namedview", **)
111
- [].tap do |matrix|
112
- Element(:"sodipodi:namedview", id:) do
113
- rows.times do |row|
114
- cols.times do |col|
115
- matrix << (x, y, label = col * (width + gap), row * (height + gap), "#{row + 1}x#{col + 1}")
116
- Element(:"inkscape:page", id: "pageview-#{label}", x:, y:, width:, height:, **)
117
- end
118
- end
119
- end
120
- end
275
+ # @param width [Numeric] finite positive page width, normalized to an SVG number
276
+ # @param height [Numeric] finite positive page height, normalized to an SVG number
277
+ # @param gap [Numeric] finite non-negative gap, normalized to an SVG number
278
+ # @param namedview [Hash] namedview attributes. String and Symbol names are normalized and must not collide
279
+ # @param page [Hash] page defaults. Normalized page attributes override these defaults by name
280
+ # @yield [page] customizes each generated page element
281
+ # @yieldparam page [Sevgi::Graphics::Element] generated page element
282
+ # @yieldreturn [Object] ignored customization result
283
+ # @return [Sevgi::Graphics::Element] namedview element
284
+ # @raise [Sevgi::ArgumentError] when counts, dimensions, gap, or an attribute channel is invalid
285
+ # @see #Pages
286
+ def PagesTabular(rows:, cols:, width:, height:, gap:, namedview: {}, page: {}, &block)
287
+ pages = Pagination.tabular(rows:, cols:, width:, height:, gap:)
288
+ Pages(*pages, namedview:, page:, &block)
121
289
  end
122
290
 
123
- # Internal symbol which does not show up Symbols Menu
124
291
  # @overload symbol!(**attributes)
125
292
  # Builds an Inkscape symbol group hidden from the symbols menu.
126
293
  # @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
- def License(**kwargs, &block) = metadata { RDFWork(**kwargs, &block) }
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 _kwargs [Hash] currently unused options
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
- def RDF(**_kwargs, &block)
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