sevgi-graphics 0.73.2 → 0.94.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/LICENSE +5 -0
  4. data/README.md +37 -0
  5. data/lib/sevgi/graphics/attribute.rb +82 -10
  6. data/lib/sevgi/graphics/auxiliary/canvas.rb +153 -0
  7. data/lib/sevgi/graphics/auxiliary/content.rb +157 -0
  8. data/lib/sevgi/graphics/auxiliary/margin.rb +93 -0
  9. data/lib/sevgi/graphics/auxiliary/paper.rb +163 -0
  10. data/lib/sevgi/graphics/auxiliary.rb +7 -0
  11. data/lib/sevgi/graphics/document/base.rb +6 -0
  12. data/lib/sevgi/graphics/document/default.rb +1 -0
  13. data/lib/sevgi/graphics/document/html.rb +1 -0
  14. data/lib/sevgi/graphics/document/inkscape.rb +1 -0
  15. data/lib/sevgi/graphics/document/minimal.rb +1 -0
  16. data/lib/sevgi/graphics/document.rb +205 -13
  17. data/lib/sevgi/graphics/element.rb +69 -9
  18. data/lib/sevgi/graphics/mixtures/call.rb +62 -4
  19. data/lib/sevgi/graphics/mixtures/core.rb +146 -25
  20. data/lib/sevgi/graphics/mixtures/duplicate.rb +47 -3
  21. data/lib/sevgi/graphics/mixtures/export.rb +31 -6
  22. data/lib/sevgi/graphics/mixtures/hatch.rb +24 -3
  23. data/lib/sevgi/graphics/mixtures/identify.rb +26 -2
  24. data/lib/sevgi/graphics/mixtures/include.rb +40 -4
  25. data/lib/sevgi/graphics/mixtures/inkscape.rb +51 -5
  26. data/lib/sevgi/graphics/mixtures/lint.rb +12 -0
  27. data/lib/sevgi/graphics/mixtures/polyfills.rb +11 -0
  28. data/lib/sevgi/graphics/mixtures/rdf.rb +40 -1
  29. data/lib/sevgi/graphics/mixtures/render.rb +136 -14
  30. data/lib/sevgi/graphics/mixtures/save.rb +21 -3
  31. data/lib/sevgi/graphics/mixtures/symbols.rb +7 -0
  32. data/lib/sevgi/graphics/mixtures/tile.rb +52 -9
  33. data/lib/sevgi/graphics/mixtures/transform.rb +54 -16
  34. data/lib/sevgi/graphics/mixtures/underscore.rb +21 -1
  35. data/lib/sevgi/graphics/mixtures/validate.rb +15 -1
  36. data/lib/sevgi/graphics/mixtures/wrappers.rb +54 -17
  37. data/lib/sevgi/graphics/mixtures.rb +35 -3
  38. data/lib/sevgi/graphics/version.rb +2 -1
  39. data/lib/sevgi/graphics.rb +71 -5
  40. metadata +12 -9
  41. data/lib/sevgi/graphics/auxilary/canvas.rb +0 -72
  42. data/lib/sevgi/graphics/auxilary/content.rb +0 -81
  43. data/lib/sevgi/graphics/auxilary/margin.rb +0 -49
  44. data/lib/sevgi/graphics/auxilary/paper.rb +0 -95
  45. data/lib/sevgi/graphics/auxilary.rb +0 -7
@@ -3,7 +3,15 @@
3
3
  module Sevgi
4
4
  module Graphics
5
5
  module Mixtures
6
+ # Inkscape-specific SVG DSL helpers.
6
7
  module Inkscape
8
+ # Adds Inkscape template metadata.
9
+ # @param name [String] template name
10
+ # @param desc [String, nil] short description
11
+ # @param author [String, nil] author
12
+ # @param date [String, nil] date
13
+ # @param keywords [Array<String>, String, nil] keywords
14
+ # @return [Sevgi::Graphics::Element] template metadata element
7
15
  def InkscapeTemplateInfo(name:, desc: nil, author: nil, date: nil, keywords: nil)
8
16
  Element(:"inkscape:_templateinfo") do
9
17
  Element(:"inkscape:_name", name)
@@ -14,29 +22,59 @@ module Sevgi
14
22
  end
15
23
  end
16
24
 
25
+ # Renders a callable module inside a group.
26
+ # @param mod [Module] callable drawing module
27
+ # @param args [Array<Object>] callable arguments
28
+ # @param kwargs [Hash] group attributes
29
+ # @return [Sevgi::Graphics::Element] group element
30
+ # @raise [Sevgi::ArgumentError] when mod is not a plain module
17
31
  def Group(mod, *args, **kwargs, &block)
18
32
  kwargs = kwargs.merge(id: F.demodulize(mod).to_sym) unless kwargs.key?(:id)
19
33
  g(**kwargs) { Call(mod, *args, &block) }
20
34
  end
21
35
 
36
+ # Renders a callable module inside an Inkscape layer.
37
+ # @param mod [Module] callable drawing module
38
+ # @param args [Array<Object>] callable arguments
39
+ # @param kwargs [Hash] layer attributes
40
+ # @return [Sevgi::Graphics::Element] layer element
41
+ # @raise [Sevgi::ArgumentError] when mod is not a plain module
22
42
  def Layer(mod, *args, **kwargs, &block)
23
43
  kwargs = kwargs.merge(id: F.demodulize(mod).to_sym) unless kwargs.key?(:id)
24
44
  layer(**kwargs) { Call(mod, *args, &block) }
25
45
  end
26
46
 
47
+ # Renders a callable module inside an insensitive Inkscape layer.
48
+ # @param mod [Module] callable drawing module
49
+ # @param args [Array<Object>] callable arguments
50
+ # @param kwargs [Hash] layer attributes
51
+ # @return [Sevgi::Graphics::Element] layer element
52
+ # @raise [Sevgi::ArgumentError] when mod is not a plain module
27
53
  def Layer!(mod, *args, **kwargs, &block)
28
54
  kwargs = kwargs.merge(id: F.demodulize(mod).to_sym) unless kwargs.key?(:id)
29
55
  layer!(**kwargs) { Call(mod, *args, &block) }
30
56
  end
31
57
 
58
+ # @overload layer(**attributes)
59
+ # Builds an Inkscape layer group.
60
+ # @param attributes [Hash] layer attributes
61
+ # @return [Sevgi::Graphics::Element] layer group
32
62
  def layer(**, &block)
33
63
  g("inkscape:groupmode": "layer", **, &block)
34
64
  end
35
65
 
66
+ # @overload layer!(**attributes)
67
+ # Builds an insensitive Inkscape layer group.
68
+ # @param attributes [Hash] layer attributes
69
+ # @return [Sevgi::Graphics::Element] layer group
36
70
  def layer!(**, &block)
37
71
  layer("sodipodi:insensitive": "true", **, &block)
38
72
  end
39
73
 
74
+ # Builds an Inkscape namedview containing page elements.
75
+ # @param pages [Array<Hash>] page attribute hashes
76
+ # @param id [String] namedview id
77
+ # @return [Sevgi::Graphics::Element] namedview element
40
78
  def Pages(*pages, id: "namedview", **, &block)
41
79
  Element(:"sodipodi:namedview", id:, **) do
42
80
  pages.each_with_index do |page, i|
@@ -48,12 +86,20 @@ module Sevgi
48
86
  end
49
87
  end
50
88
 
89
+ # Builds a tabular set of Inkscape pages.
90
+ # @param rows [Integer] number of rows
91
+ # @param cols [Integer] number of columns
92
+ # @param width [Numeric] page width
93
+ # @param height [Numeric] page height
94
+ # @param gap [Numeric] gap between pages
95
+ # @param id [String] namedview id
96
+ # @return [Array<Array>] matrix entries as x, y, and label tuples
51
97
  def PagesTabular(rows:, cols:, width:, height:, gap:, id: "namedview", **)
52
98
  [].tap do |matrix|
53
99
  Element(:"sodipodi:namedview", id:) do
54
100
  rows.times do |row|
55
101
  cols.times do |col|
56
- matrix << (x, y, label = col * (height + gap), row * (width + gap), "#{row + 1}x#{col + 1}")
102
+ matrix << (x, y, label = col * (width + gap), row * (height + gap), "#{row + 1}x#{col + 1}")
57
103
  Element(:"inkscape:page", id: "pageview-#{label}", x:, y:, width:, height:, **)
58
104
  end
59
105
  end
@@ -62,6 +108,10 @@ module Sevgi
62
108
  end
63
109
 
64
110
  # Internal symbol which does not show up Symbols Menu
111
+ # @overload symbol!(**attributes)
112
+ # Builds an Inkscape symbol group hidden from the symbols menu.
113
+ # @param attributes [Hash] symbol attributes
114
+ # @return [Sevgi::Graphics::Element] symbol group
65
115
  def symbol!(**, &block)
66
116
  if Is?(:defs)
67
117
  g(role: "inkscape:symbol", **, &block)
@@ -70,10 +120,6 @@ module Sevgi
70
120
  end
71
121
  end
72
122
 
73
- def Symbol!(mod, *args, **kwargs, &block)
74
- kwargs = kwargs.merge(id: F.demodulize(mod).to_sym) unless kwargs.key?(:id)
75
- symbol!(**kwargs) { Call(mod, *args, &block) }
76
- end
77
123
  end
78
124
  end
79
125
  end
@@ -2,17 +2,29 @@
2
2
 
3
3
  module Sevgi
4
4
  module Graphics
5
+ # Raised when graphics lint checks fail.
5
6
  LintError = Class.new(Error)
6
7
 
7
8
  module Mixtures
9
+ # DSL helpers for pre-render structural linting.
8
10
  module Lint
11
+ # @overload Lint
12
+ # Runs graphics lint checks.
13
+ # @return [true]
14
+ # @raise [Sevgi::Graphics::LintError] when a lint check fails
9
15
  def Lint(...)
10
16
  IdentitiesAreUniq.(self, ...)
11
17
  end
12
18
 
19
+ # Lint check that rejects duplicate visible ids.
20
+ # @api private
13
21
  module IdentitiesAreUniq
14
22
  extend self
15
23
 
24
+ # Checks an element subtree for duplicate ids.
25
+ # @param element [Sevgi::Graphics::Element] root element
26
+ # @return [true]
27
+ # @raise [Sevgi::Graphics::LintError] when duplicate ids exist
16
28
  def call(element)
17
29
  return true unless (identifiers = element.Identifiers()).conflict?
18
30
 
@@ -3,9 +3,20 @@
3
3
  module Sevgi
4
4
  module Graphics
5
5
  module Mixtures
6
+ # Compatibility helpers for profiles that do not load richer mixtures.
6
7
  module Polyfills
8
+ # @overload layer(*args, **attributes)
9
+ # Builds a generic group as a layer fallback.
10
+ # @param args [Array<Object>] group content arguments
11
+ # @param attributes [Hash] group attributes
12
+ # @return [Sevgi::Graphics::Element] group element
7
13
  def layer(...) = g(...)
8
14
 
15
+ # @overload symbol!(*args, **attributes)
16
+ # Builds a regular symbol as a fallback for hidden symbols.
17
+ # @param args [Array<Object>] symbol content arguments
18
+ # @param attributes [Hash] symbol attributes
19
+ # @return [Sevgi::Graphics::Element] symbol element
9
20
  def symbol!(...) = symbol(...)
10
21
  end
11
22
  end
@@ -3,39 +3,68 @@
3
3
  module Sevgi
4
4
  module Graphics
5
5
  module Mixtures
6
+ # DSL helpers for RDF and license metadata.
6
7
  module RDF
8
+ # Adds RDF license metadata inside a metadata element.
9
+ # @param kwargs [Hash] RDF work options
10
+ # @return [Sevgi::Graphics::Element] metadata element
7
11
  def License(**kwargs, &block) = metadata { RDFWork(**kwargs, &block) }
8
12
 
9
13
  # Use SPDX license codes in underscored form: https://spdx.org/licenses/
10
14
  #
15
+ # Adds Creative Commons BY-SA license metadata.
16
+ # @param kwargs [Hash] RDF work options
17
+ # @return [Sevgi::Graphics::Element] metadata element
11
18
  def License_CC_BY_SA(**kwargs, &block)
12
19
  License(**kwargs, license: "https://creativecommons.org/licenses/by-sa/4.0/", &block)
13
20
  end
14
21
 
22
+ # Adds Creative Commons BY-NC license metadata.
23
+ # @param kwargs [Hash] RDF work options
24
+ # @return [Sevgi::Graphics::Element] metadata element
15
25
  def License_CC_BY_NC(**kwargs, &block)
16
26
  License(**kwargs, license: "https://creativecommons.org/licenses/by-nc/4.0/", &block)
17
27
  end
18
28
 
29
+ # Adds Creative Commons BY-NC-SA license metadata.
30
+ # @param kwargs [Hash] RDF work options
31
+ # @return [Sevgi::Graphics::Element] metadata element
19
32
  def License_CC_BY_NC_SA(**kwargs, &block)
20
33
  License(**kwargs, license: "https://creativecommons.org/licenses/by-nc-sa/4.0/", &block)
21
34
  end
22
35
 
36
+ # Adds Creative Commons BY-ND license metadata.
37
+ # @param kwargs [Hash] RDF work options
38
+ # @return [Sevgi::Graphics::Element] metadata element
23
39
  def License_CC_BY_ND(**kwargs, &block)
24
40
  License(**kwargs, license: "https://creativecommons.org/licenses/by-nd/4.0/", &block)
25
41
  end
26
42
 
43
+ # Adds Creative Commons BY-NC-ND license metadata.
44
+ # @param kwargs [Hash] RDF work options
45
+ # @return [Sevgi::Graphics::Element] metadata element
27
46
  def License_CC_BY_NC_ND(**kwargs, &block)
28
47
  License(**kwargs, license: "https://creativecommons.org/licenses/by-nc-nd/4.0/", &block)
29
48
  end
30
49
 
50
+ # Adds Creative Commons Zero metadata.
51
+ # @param kwargs [Hash] RDF work options
52
+ # @return [Sevgi::Graphics::Element] metadata element
31
53
  def License_CC0(**kwargs, &block)
32
54
  License(**kwargs, license: "https://creativecommons.org/publicdomain/zero/1.0/", &block)
33
55
  end
34
56
 
57
+ # Adds Free Art License metadata.
58
+ # @param kwargs [Hash] RDF work options
59
+ # @return [Sevgi::Graphics::Element] metadata element
35
60
  def License_LAL(**kwargs, &block) = License(**kwargs, license: "https://artlibre.org/licence/lal/en/", &block)
36
61
 
62
+ # Builds an RDF root element.
63
+ # @param _kwargs [Hash] currently unused options
64
+ # @return [Sevgi::Graphics::Element] RDF element
65
+ # @raise [Sevgi::ArgumentError] when no block is given
37
66
  def RDF(**_kwargs, &block)
38
- raise ArgumentError, "Block required" unless block
67
+ ArgumentError.("Block required") unless block
39
68
 
40
69
  Element(
41
70
  :"rdf:RDF",
@@ -48,6 +77,16 @@ module Sevgi
48
77
  end
49
78
 
50
79
  # rubocop:disable Metrics/MethodLength
80
+ # Builds a Creative Commons RDF Work element.
81
+ # @param kwargs [Hash] RDF work options
82
+ # @option kwargs [String] :title work title
83
+ # @option kwargs [String] :description work description
84
+ # @option kwargs [String] :creator creator
85
+ # @option kwargs [String] :publisher publisher
86
+ # @option kwargs [String] :date date
87
+ # @option kwargs [String] :language language
88
+ # @option kwargs [String] :license license URL
89
+ # @return [Sevgi::Graphics::Element] RDF element
51
90
  def RDFWork(**kwargs, &block)
52
91
  RDF do
53
92
  Element(:"cc:Work", "rdf:about": "") do
@@ -3,18 +3,36 @@
3
3
  module Sevgi
4
4
  module Graphics
5
5
  module Mixtures
6
+ # DSL helpers for rendering SVG documents and children.
6
7
  module Render
8
+ # SVG source renderer.
9
+ # @api private
7
10
  class Renderer
11
+ # Default renderer options.
8
12
  DEFAULTS = {indent: " ", linelength: 140, style: :hybrid}.freeze
9
13
 
14
+ # Attribute rendering strategies.
15
+ # @api private
10
16
  module Attributes
17
+ # Block-style attribute renderer.
18
+ # @api private
11
19
  module Block
20
+ # Renders attributes in block form.
21
+ # @param element [Sevgi::Graphics::Element] rendered element
22
+ # @param depth [Integer] element depth
23
+ # @return [void]
12
24
  def attributes(element, depth)
13
25
  attributes_block(element, depth, element.attributes.to_xml_lines)
14
26
  end
15
27
  end
16
28
 
29
+ # Hybrid attribute renderer.
30
+ # @api private
17
31
  module Hybrid
32
+ # Renders attributes inline or in block form according to line length.
33
+ # @param element [Sevgi::Graphics::Element] rendered element
34
+ # @param depth [Integer] element depth
35
+ # @return [void]
18
36
  def attributes(element, depth)
19
37
  if attributes_as_block?(lines = element.attributes.to_xml_lines, depth)
20
38
  attributes_block(element, depth, lines)
@@ -23,16 +41,30 @@ module Sevgi
23
41
  end
24
42
  end
25
43
 
44
+ # Reports whether attributes should be rendered in block form.
45
+ # @param lines [Array<String>] rendered attribute lines
46
+ # @param depth [Integer] element depth
47
+ # @return [Boolean]
26
48
  def attributes_as_block?(lines, depth)
27
49
  linelength(lines, depth) > options[:linelength]
28
50
  end
29
51
 
52
+ # Returns the effective inline line length.
53
+ # @param lines [Array<String>] rendered attribute lines
54
+ # @param depth [Integer] element depth
55
+ # @return [Integer]
30
56
  def linelength(lines, depth)
31
57
  indent(depth).length + lines.sum(&:length)
32
58
  end
33
59
  end
34
60
 
61
+ # Inline attribute renderer.
62
+ # @api private
35
63
  module Inline
64
+ # Renders attributes inline.
65
+ # @param element [Sevgi::Graphics::Element] rendered element
66
+ # @param depth [Integer] element depth
67
+ # @return [void]
36
68
  def attributes(element, depth)
37
69
  attributes_inline(element, depth, element.attributes.to_xml_lines)
38
70
  end
@@ -47,34 +79,89 @@ module Sevgi
47
79
 
48
80
  private_constant :ELEMENTS_WITH_INLINE_CONTENT, :ELEMENTS_WITH_BLOCK_CONTENT, :SEPARATOR
49
81
 
50
- attr_reader :root, :options, :output
82
+ # @return [Sevgi::Graphics::Element] root element
83
+ attr_reader :root
51
84
 
85
+ # @return [Hash] renderer options
86
+ attr_reader :options
87
+
88
+ # @return [Array<Array<String>>] buffered output lines
89
+ attr_reader :output
90
+
91
+ # Inline-content output splice helper.
92
+ # @api private
52
93
  class Inlines
94
+ # Inline mark with start, stop, and depth.
53
95
  Mark = Struct.new(:start, :stop, :depth)
54
96
 
55
- def initialize = @marks = []
97
+ # Creates an inline splice tracker.
98
+ # @return [void]
99
+ def initialize
100
+ @marks = []
101
+ @stack = []
102
+ end
56
103
 
57
- def start(index, depth) = @marks << Mark.new(start: index, depth:)
104
+ # Starts an inline splice range.
105
+ # @param index [Integer] output index
106
+ # @param depth [Integer] element depth
107
+ # @return [Sevgi::Graphics::Mixtures::Render::Renderer::Inlines::Mark] opened mark
108
+ def start(index, depth)
109
+ Mark.new(start: index, depth:).tap do |mark|
110
+ @marks << mark
111
+ @stack << mark
112
+ end
113
+ end
58
114
 
59
- def stop(index) = @marks.last.stop = index
115
+ # Ends the innermost inline splice range.
116
+ # @param index [Integer] output index
117
+ # @return [Integer]
118
+ # @raise [Sevgi::PanicError] when no inline range is open
119
+ def stop(index)
120
+ mark = @stack.pop
121
+ PanicError.("Inline content range was not opened") unless mark
60
122
 
123
+ mark.stop = index
124
+ end
125
+
126
+ # Joins marked output ranges into inline content.
127
+ # @param output [Array<Array<String>, nil>] renderer output buffer
128
+ # @param indent [String] indentation unit
129
+ # @param separator [String] line separator
130
+ # @return [Array<Array<String>, nil>, nil]
131
+ # @raise [Sevgi::PanicError] when an inline range was not closed
61
132
  def join(output, indent:, separator:)
62
133
  return if @marks.empty?
63
134
 
64
- @marks.each do |mark|
65
- depth = mark.depth
66
-
67
- ((mark.start + 1)..mark.stop).each do |i|
68
- output[i].map! { |line| line.delete_prefix(indent * (depth + 1)) }
69
- output[mark.start][-1] += output[i].join(separator)
70
- output[i] = nil
71
- end
72
- end
135
+ @marks.reverse_each { |mark| join_mark(output, mark, indent:, separator:) }
73
136
 
74
137
  output.compact!
75
138
  end
139
+
140
+ private
141
+
142
+ def join_mark(output, mark, indent:, separator:)
143
+ PanicError.("Inline content range was not closed") unless mark.stop
144
+
145
+ ((mark.start + 1)..mark.stop).each do |index|
146
+ lines = output[index]
147
+ next unless lines
148
+
149
+ lines.map! { |line| line.delete_prefix(indent * (mark.depth + 1)) }
150
+ output[mark.start][-1] += lines.join(separator)
151
+ output[index] = nil
152
+ end
153
+ end
76
154
  end
77
155
 
156
+ # @overload initialize(root, **options)
157
+ # Creates a renderer.
158
+ # @param root [Sevgi::Graphics::Element] root element
159
+ # @param options [Hash] renderer options
160
+ # @option options [String] :indent indentation unit
161
+ # @option options [Integer] :linelength hybrid attribute line length
162
+ # @option options [Symbol] :style attribute style, one of :hybrid, :inline, or :block
163
+ # @return [void]
164
+ # @raise [Sevgi::ArgumentError] when style is missing or unsupported
78
165
  def initialize(root, **)
79
166
  @root = root
80
167
  @options = DEFAULTS.merge(**)
@@ -84,14 +171,34 @@ module Sevgi
84
171
  build
85
172
  end
86
173
 
174
+ # @overload call(root, **options)
175
+ # Renders a root element.
176
+ # @param root [Sevgi::Graphics::Element] root element
177
+ # @param options [Hash] renderer options
178
+ # @return [String] SVG source
179
+ # @raise [Sevgi::ArgumentError] when style is missing or unsupported
87
180
  def self.call(root, **) = new(root, **).call(*root.class.preambles)
88
181
 
182
+ # Renders a root element without document preambles.
183
+ # @param root [Sevgi::Graphics::Element] root element
184
+ # @param options [Hash] renderer options
185
+ # @return [String] SVG fragment source
186
+ # @raise [Sevgi::ArgumentError] when style is missing or unsupported
187
+ def self.fragment(root, **options) = new(root, **options).call
188
+
189
+ # Appends rendered lines to the output buffer.
190
+ # @param depth [Integer, nil] indentation depth
191
+ # @param lines [Array<String>] rendered lines
192
+ # @return [Array<Array<String>>, nil]
89
193
  def append(depth, *lines)
90
194
  return if lines.empty?
91
195
 
92
196
  output.append(lines.map { "#{indent(depth) if depth}#{it}" })
93
197
  end
94
198
 
199
+ # Renders the document.
200
+ # @param preambles [Array<String>] preamble lines
201
+ # @return [String] SVG source
95
202
  def call(*preambles)
96
203
  output.append(preambles) unless preambles.empty?
97
204
 
@@ -108,6 +215,8 @@ module Sevgi
108
215
  attr_reader :inlines
109
216
 
110
217
  def attributes_block(element, depth, lines)
218
+ return attributes_inline(element, depth, lines) if lines.empty?
219
+
111
220
  append(depth, "<#{element.name}")
112
221
  append(depth + 1, *lines)
113
222
  append(depth, ">") unless childless?(element)
@@ -202,9 +311,22 @@ module Sevgi
202
311
 
203
312
  private_constant :Renderer
204
313
 
314
+ # @overload Render(**options)
315
+ # Renders this element as SVG source.
316
+ # Elements with inline text content may also contain inline children such as `tspan`; the renderer keeps those
317
+ # descendants in the same text line. Whitespace inside content objects is preserved as given, and encoded
318
+ # content is XML-escaped unless a verbatim content object is used.
319
+ # @param options [Hash] renderer options
320
+ # @return [String] SVG source
321
+ # @raise [Sevgi::ArgumentError] when style is missing or unsupported
205
322
  def Render(**) = Renderer.(self, **)
206
323
 
207
- def Render!(separator = SEPARATOR * 2) = children.map(&:Render!).join(separator)
324
+ # Renders only this element's children.
325
+ # Child render output omits document preambles and preserves each child's text whitespace and inline
326
+ # mixed-content formatting.
327
+ # @param separator [String] separator between child documents
328
+ # @return [String] rendered child fragments
329
+ def RenderChildren(separator = "\n\n") = children.map { Renderer.fragment(it) }.join(separator)
208
330
  end
209
331
  end
210
332
  end
@@ -5,13 +5,23 @@ require "fileutils"
5
5
  module Sevgi
6
6
  module Graphics
7
7
  module Mixtures
8
+ # DSL helpers for writing rendered SVG output.
8
9
  module Save
10
+ # Default SVG extension.
9
11
  EXT = ".svg"
10
12
 
11
- def Out(*, **, &filter)
12
- F.out(self.(**), *, &filter)
13
+ # Writes rendered SVG to standard output.
14
+ # @param kwargs [Hash] render options
15
+ # @return [Object] F.out return value
16
+ def Out(**kwargs, &filter)
17
+ F.out(self.(**kwargs), &filter)
13
18
  end
14
19
 
20
+ # Saves rendered SVG to a path derived from the caller by default.
21
+ # @param path [String, nil] output path or directory
22
+ # @param default [String, nil] default output path
23
+ # @param backup_suffix [String, nil] suffix used for an existing-file backup
24
+ # @return [Object] F.out return value
15
25
  def Save(path = nil, default: nil, backup_suffix: nil, &filter)
16
26
  default ||= F.subext(EXT, caller_locations(1..1).first.path)
17
27
 
@@ -26,7 +36,15 @@ module Sevgi
26
36
  ::FileUtils.cp(path, "#{path}#{backup_suffix}")
27
37
  end
28
38
 
29
- Out(path, &filter)
39
+ Write(path, &filter)
40
+ end
41
+
42
+ # Writes rendered SVG to a path.
43
+ # @param path [String] output path
44
+ # @param kwargs [Hash] render options
45
+ # @return [Object] F.out return value
46
+ def Write(path, **kwargs, &filter)
47
+ F.out(self.(**kwargs), path, &filter)
30
48
  end
31
49
  end
32
50
  end
@@ -3,7 +3,14 @@
3
3
  module Sevgi
4
4
  module Graphics
5
5
  module Mixtures
6
+ # DSL helpers for expanding callable modules into SVG symbols.
6
7
  module Symbols
8
+ # Renders module callables as symbols under defs.
9
+ # @param mod [Module] callable drawing module
10
+ # @param args [Array<Object>] callable arguments
11
+ # @param kwargs [Hash] defs attributes
12
+ # @return [Sevgi::Graphics::Element] defs element
13
+ # @raise [Sevgi::ArgumentError] when mod is not a plain module
7
14
  def Symbols(mod, *args, **kwargs, &block)
8
15
  CallWithin(mod, :defs, :symbol, *args, **kwargs) do |name, element|
9
16
  element[:id] = block ? block.call(name) : name.to_s.split("_").join("-")
@@ -4,9 +4,22 @@ module Sevgi
4
4
  module Graphics
5
5
  module Mixtures
6
6
  # rubocop:disable Metrics/MethodLength
7
+ # DSL helpers for repeated SVG use elements.
7
8
  module Tile
9
+ # Prefix used for generated tile CSS classes.
8
10
  PREFIX = "tile"
9
11
 
12
+ # Builds a two-dimensional tile grid.
13
+ # @param id [String] referenced template id
14
+ # @param nx [Integer] number of columns
15
+ # @param dx [Numeric] horizontal spacing
16
+ # @param ox [Numeric] horizontal offset
17
+ # @param ny [Integer] number of rows
18
+ # @param dy [Numeric] vertical spacing
19
+ # @param oy [Numeric] vertical offset
20
+ # @param proc [Proc, nil] optional coordinate/customization proc
21
+ # @return [Sevgi::Graphics::Element] self
22
+ # @raise [Sevgi::ArgumentError] when a required tile argument is missing or invalid
10
23
  def Tile(
11
24
  id = Undefined,
12
25
  nx: Undefined,
@@ -48,8 +61,14 @@ module Sevgi
48
61
  end
49
62
  end
50
63
 
51
- def Tile!(id, klass = PREFIX, ...) = g(id:, class: klass) { Tile(id, ...) }
52
-
64
+ # Builds a one-dimensional horizontal tile row.
65
+ # @param id [String] referenced template id
66
+ # @param n [Integer] number of instances
67
+ # @param d [Numeric] horizontal spacing
68
+ # @param o [Numeric] horizontal offset
69
+ # @param proc [Proc, nil] optional coordinate/customization proc
70
+ # @return [Sevgi::Graphics::Element] self
71
+ # @raise [Sevgi::ArgumentError] when a required tile argument is missing or invalid
53
72
  def TileX(id = Undefined, n: Undefined, d: Undefined, o: 0, proc: nil, &block)
54
73
  Helper.assert(id:, n:, d:, o:, proc:)
55
74
 
@@ -75,8 +94,14 @@ module Sevgi
75
94
  end
76
95
  end
77
96
 
78
- def TileX!(id, klass = PREFIX, ...) = g(id:, class: klass) { TileX(id, ...) }
79
-
97
+ # Builds a one-dimensional vertical tile column.
98
+ # @param id [String] referenced template id
99
+ # @param n [Integer] number of instances
100
+ # @param d [Numeric] vertical spacing
101
+ # @param o [Numeric] vertical offset
102
+ # @param proc [Proc, nil] optional coordinate/customization proc
103
+ # @return [Sevgi::Graphics::Element] self
104
+ # @raise [Sevgi::ArgumentError] when a required tile argument is missing or invalid
80
105
  def TileY(id = Undefined, n: Undefined, d: Undefined, o: 0, proc: nil, &block)
81
106
  Helper.assert(id:, n:, d:, o:, proc:)
82
107
 
@@ -102,16 +127,17 @@ module Sevgi
102
127
  end
103
128
  end
104
129
 
105
- def TileY!(id, klass = PREFIX, ...) = g(id:, class: klass) { TileY(id, ...) }
106
-
130
+ # Tile argument validation and class helpers.
131
+ # @api private
107
132
  module Helper
108
133
  extend self
109
134
 
135
+ # Argument validators for tile helpers.
110
136
  ASSERTION = {
111
137
  id: proc { |name, value| "Argument '#{name}' must be a string" unless value.is_a?(::String) },
112
- n: proc { |name, value| "Argument '#{name}' must be a positive integer" unless value.positive? },
113
- nx: proc { |name, value| "Argument '#{name}' must be a positive integer" unless value.positive? },
114
- ny: proc { |name, value| "Argument '#{name}' must be a positive integer" unless value.positive? },
138
+ n: proc { |name, value| positive_integer_issue(name, value) },
139
+ nx: proc { |name, value| positive_integer_issue(name, value) },
140
+ ny: proc { |name, value| positive_integer_issue(name, value) },
115
141
  d: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
116
142
  dx: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
117
143
  dy: proc { |name, value| "Argument '#{name}' must be a number" unless value.is_a?(::Numeric) },
@@ -121,6 +147,18 @@ module Sevgi
121
147
  proc: proc { |name, value| "Argument '#{name}' must be a proc" unless value.nil? || value.is_a?(::Proc) }
122
148
  }.freeze
123
149
 
150
+ # Returns a validation issue unless value is a positive integer.
151
+ # @param name [Symbol] argument name
152
+ # @param value [Object] argument value
153
+ # @return [String, nil] validation issue
154
+ def positive_integer_issue(name, value)
155
+ "Argument '#{name}' must be a positive integer" unless value.is_a?(::Integer) && value.positive?
156
+ end
157
+
158
+ # Validates tile arguments.
159
+ # @param kwargs [Hash] tile arguments
160
+ # @return [nil]
161
+ # @raise [Sevgi::ArgumentError] when an argument is missing or invalid
124
162
  def assert(**kwargs)
125
163
  kwargs.each do |name, value|
126
164
  issue = "Argument '#{name}' required" if value == Undefined
@@ -135,6 +173,11 @@ module Sevgi
135
173
  # rubocop:enable Metrics/MethodLength
136
174
  end
137
175
 
176
+ # Returns positional tile CSS classes.
177
+ # @param as [String] class axis label
178
+ # @param index [Integer] zero-based index
179
+ # @param upper [Integer] item count
180
+ # @return [Array<String>]
138
181
  def classify(as:, index:, upper:)
139
182
  [].tap do |classes|
140
183
  classes << "#{PREFIX}-#{as}-#{index + 1}"