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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/LICENSE +5 -0
- data/README.md +37 -0
- data/lib/sevgi/graphics/attribute.rb +82 -10
- data/lib/sevgi/graphics/auxiliary/canvas.rb +153 -0
- data/lib/sevgi/graphics/auxiliary/content.rb +157 -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 +205 -13
- data/lib/sevgi/graphics/element.rb +69 -9
- data/lib/sevgi/graphics/mixtures/call.rb +62 -4
- data/lib/sevgi/graphics/mixtures/core.rb +146 -25
- data/lib/sevgi/graphics/mixtures/duplicate.rb +47 -3
- data/lib/sevgi/graphics/mixtures/export.rb +31 -6
- data/lib/sevgi/graphics/mixtures/hatch.rb +24 -3
- data/lib/sevgi/graphics/mixtures/identify.rb +26 -2
- data/lib/sevgi/graphics/mixtures/include.rb +40 -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 +136 -14
- 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 +52 -9
- data/lib/sevgi/graphics/mixtures/transform.rb +54 -16
- data/lib/sevgi/graphics/mixtures/underscore.rb +21 -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 +71 -5
- metadata +12 -9
- 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
|
@@ -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 * (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
+
# Creates an inline splice tracker.
|
|
98
|
+
# @return [void]
|
|
99
|
+
def initialize
|
|
100
|
+
@marks = []
|
|
101
|
+
@stack = []
|
|
102
|
+
end
|
|
56
103
|
|
|
57
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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|
|
|
113
|
-
nx: proc { |name, value|
|
|
114
|
-
ny: proc { |name, value|
|
|
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}"
|