sevgi-graphics 0.73.2 → 0.93.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -0
- data/lib/sevgi/graphics/attribute.rb +58 -6
- data/lib/sevgi/graphics/auxiliary/canvas.rb +125 -0
- data/lib/sevgi/graphics/auxiliary/content.rb +145 -0
- data/lib/sevgi/graphics/auxiliary/margin.rb +93 -0
- data/lib/sevgi/graphics/auxiliary/paper.rb +163 -0
- data/lib/sevgi/graphics/auxiliary.rb +7 -0
- data/lib/sevgi/graphics/document/base.rb +6 -0
- data/lib/sevgi/graphics/document/default.rb +1 -0
- data/lib/sevgi/graphics/document/html.rb +1 -0
- data/lib/sevgi/graphics/document/inkscape.rb +1 -0
- data/lib/sevgi/graphics/document/minimal.rb +1 -0
- data/lib/sevgi/graphics/document.rb +137 -11
- data/lib/sevgi/graphics/element.rb +68 -8
- data/lib/sevgi/graphics/mixtures/call.rb +35 -3
- data/lib/sevgi/graphics/mixtures/core.rb +107 -18
- data/lib/sevgi/graphics/mixtures/duplicate.rb +14 -0
- data/lib/sevgi/graphics/mixtures/export.rb +29 -6
- data/lib/sevgi/graphics/mixtures/hatch.rb +24 -3
- data/lib/sevgi/graphics/mixtures/identify.rb +23 -1
- data/lib/sevgi/graphics/mixtures/include.rb +31 -4
- data/lib/sevgi/graphics/mixtures/inkscape.rb +51 -5
- data/lib/sevgi/graphics/mixtures/lint.rb +12 -0
- data/lib/sevgi/graphics/mixtures/polyfills.rb +11 -0
- data/lib/sevgi/graphics/mixtures/rdf.rb +40 -1
- data/lib/sevgi/graphics/mixtures/render.rb +90 -2
- data/lib/sevgi/graphics/mixtures/save.rb +21 -3
- data/lib/sevgi/graphics/mixtures/symbols.rb +7 -0
- data/lib/sevgi/graphics/mixtures/tile.rb +41 -6
- data/lib/sevgi/graphics/mixtures/transform.rb +54 -16
- data/lib/sevgi/graphics/mixtures/underscore.rb +10 -1
- data/lib/sevgi/graphics/mixtures/validate.rb +15 -1
- data/lib/sevgi/graphics/mixtures/wrappers.rb +54 -17
- data/lib/sevgi/graphics/mixtures.rb +35 -3
- data/lib/sevgi/graphics/version.rb +2 -1
- data/lib/sevgi/graphics.rb +57 -5
- metadata +9 -8
- data/lib/sevgi/graphics/auxilary/canvas.rb +0 -72
- data/lib/sevgi/graphics/auxilary/content.rb +0 -81
- data/lib/sevgi/graphics/auxilary/margin.rb +0 -49
- data/lib/sevgi/graphics/auxilary/paper.rb +0 -95
- data/lib/sevgi/graphics/auxilary.rb +0 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2aa7779435a4a7fb66f31fc412abac9eaf39f9f310d598a02a6fe0713e9aceab
|
|
4
|
+
data.tar.gz: 5f17372a0ff45b66c9388993c7270413b359d89889633bb775bac27747ff1128
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 46fed1c304dc35516dc15e12eff33cfe5eea984ed070c675e62a8ecf659ad9f1aee3f5f1884525b98c622480c4d59ea72ab20b1d2d5b6704a30691026765bdfe
|
|
7
|
+
data.tar.gz: f35d041432cde1a165e8f753cbbe895dc2e90850a6f74cbcf89f4278ef60b022471072340959c9db21e96499ad45f09505416f7ef140b8ef620811319509a986
|
data/README.md
ADDED
|
@@ -4,20 +4,36 @@
|
|
|
4
4
|
|
|
5
5
|
module Sevgi
|
|
6
6
|
module Graphics
|
|
7
|
+
# Internal store syntax; not part of the SVG DSL command surface.
|
|
7
8
|
ATTRIBUTE_INTERNAL_PREFIX = "-"
|
|
9
|
+
|
|
10
|
+
# Attribute suffix that merges new values into an existing attribute.
|
|
8
11
|
ATTRIBUTE_UPDATE_SUFFIX = "+"
|
|
9
12
|
|
|
13
|
+
# Attribute name normalization helpers.
|
|
14
|
+
# @api private
|
|
10
15
|
module Attribute
|
|
16
|
+
# Attribute identifier helpers.
|
|
17
|
+
# @api private
|
|
11
18
|
module Ident
|
|
19
|
+
# Reports whether an attribute is internal to Sevgi rendering.
|
|
20
|
+
# @param given [String, Symbol] attribute name
|
|
21
|
+
# @return [Boolean]
|
|
12
22
|
def internal?(given)
|
|
13
23
|
(@internal ||= {})[given] ||= given.start_with?(ATTRIBUTE_INTERNAL_PREFIX)
|
|
14
24
|
end
|
|
15
25
|
|
|
26
|
+
# Returns the normalized attribute id.
|
|
27
|
+
# @param given [String, Symbol] attribute name
|
|
28
|
+
# @return [Symbol]
|
|
16
29
|
def id(given)
|
|
17
30
|
(@id ||= {})[given] ||= (updateable?(given) ? given.to_s.delete_suffix(ATTRIBUTE_UPDATE_SUFFIX) : given)
|
|
18
31
|
.to_sym
|
|
19
32
|
end
|
|
20
33
|
|
|
34
|
+
# Reports whether an attribute uses merge/update syntax.
|
|
35
|
+
# @param given [String, Symbol] attribute name
|
|
36
|
+
# @return [Boolean]
|
|
21
37
|
def updateable?(given)
|
|
22
38
|
(@updateable ||= {})[given] ||= given.end_with?(ATTRIBUTE_UPDATE_SUFFIX)
|
|
23
39
|
end
|
|
@@ -25,38 +41,59 @@ module Sevgi
|
|
|
25
41
|
|
|
26
42
|
extend Ident
|
|
27
43
|
|
|
44
|
+
# Mutable SVG attribute store with Sevgi update syntax.
|
|
28
45
|
class Store
|
|
46
|
+
# Creates an attribute store.
|
|
47
|
+
# @param attributes [Hash] initial attributes
|
|
48
|
+
# @return [void]
|
|
29
49
|
def initialize(attributes = {})
|
|
30
50
|
@store = {}
|
|
31
51
|
|
|
32
52
|
import(attributes)
|
|
33
53
|
end
|
|
34
54
|
|
|
55
|
+
# Imports attributes into the store.
|
|
56
|
+
# @param attributes [Hash] attributes to merge
|
|
57
|
+
# @return [Hash] internal store
|
|
35
58
|
def import(attributes)
|
|
36
59
|
hash = attributes
|
|
37
60
|
.compact
|
|
38
61
|
.to_a
|
|
39
62
|
.to_h do |key, value|
|
|
40
|
-
[key.to_sym, value.is_a?(::Hash) ? value.transform_keys
|
|
63
|
+
[key.to_sym, value.is_a?(::Hash) ? value.transform_keys(&:to_sym) : value]
|
|
41
64
|
end
|
|
42
65
|
|
|
43
66
|
@store.merge!(hash)
|
|
44
67
|
end
|
|
45
68
|
|
|
69
|
+
# Returns an attribute by normalized key.
|
|
70
|
+
# @param key [String, Symbol] attribute key
|
|
71
|
+
# @return [Object, nil]
|
|
46
72
|
def [](key)
|
|
47
73
|
@store[Attribute.id(key)]
|
|
48
74
|
end
|
|
49
75
|
|
|
76
|
+
# Assigns an attribute value.
|
|
77
|
+
# @param key [String, Symbol] attribute key
|
|
78
|
+
# @param value [Object, nil] attribute value; nil is ignored
|
|
79
|
+
# @return [Object, nil] assigned value or nil
|
|
80
|
+
# @raise [Sevgi::ArgumentError] when update syntax receives incompatible values
|
|
81
|
+
# @raise [Sevgi::ArgumentError] when update syntax receives an unsupported value type
|
|
50
82
|
def []=(key, value)
|
|
51
83
|
return if value.nil?
|
|
52
84
|
|
|
53
85
|
@store[id = Attribute.id(key)] = @store.key?(id) && Attribute.updateable?(key) ? update(id, value) : value
|
|
54
86
|
end
|
|
55
87
|
|
|
88
|
+
# Deletes an attribute by normalized key.
|
|
89
|
+
# @param key [String, Symbol] attribute key
|
|
90
|
+
# @return [Object, nil] deleted value
|
|
56
91
|
def delete(key)
|
|
57
92
|
@store.delete(Attribute.id(key))
|
|
58
93
|
end
|
|
59
94
|
|
|
95
|
+
# Returns public attributes ready for rendering.
|
|
96
|
+
# @return [Hash]
|
|
60
97
|
def export
|
|
61
98
|
hash = @store.reject { |id, _| Attribute.internal?(id) }
|
|
62
99
|
return hash unless hash.key?(:id)
|
|
@@ -65,10 +102,16 @@ module Sevgi
|
|
|
65
102
|
{id: hash.delete(:id), **hash}
|
|
66
103
|
end
|
|
67
104
|
|
|
105
|
+
# Reports whether an attribute exists.
|
|
106
|
+
# @param key [String, Symbol] attribute key
|
|
107
|
+
# @return [Boolean]
|
|
68
108
|
def has?(key)
|
|
69
109
|
@store.key?(Attribute.id(key))
|
|
70
110
|
end
|
|
71
111
|
|
|
112
|
+
# Copies the attribute store.
|
|
113
|
+
# @param original [Sevgi::Graphics::Attribute::Store] store to copy
|
|
114
|
+
# @return [void]
|
|
72
115
|
def initialize_copy(original)
|
|
73
116
|
@store = {}
|
|
74
117
|
original.store.each { |key, value| @store[key] = value.dup }
|
|
@@ -76,14 +119,20 @@ module Sevgi
|
|
|
76
119
|
super
|
|
77
120
|
end
|
|
78
121
|
|
|
122
|
+
# Returns public attribute names.
|
|
123
|
+
# @return [Array<Symbol>]
|
|
79
124
|
def list
|
|
80
125
|
export.keys
|
|
81
126
|
end
|
|
82
127
|
|
|
128
|
+
# Returns the internal attribute hash.
|
|
129
|
+
# @return [Hash]
|
|
83
130
|
def to_h
|
|
84
131
|
@store
|
|
85
132
|
end
|
|
86
133
|
|
|
134
|
+
# Returns rendered XML attribute lines.
|
|
135
|
+
# @return [Array<String>]
|
|
87
136
|
def to_xml_lines
|
|
88
137
|
export.map { |id, value| to_xml(id, value) }
|
|
89
138
|
end
|
|
@@ -98,7 +147,7 @@ module Sevgi
|
|
|
98
147
|
::String => proc { |old_value, new_value| [old_value, new_value].reject(&:empty?).join(" ") },
|
|
99
148
|
::Symbol => proc { |old_value, new_value| [old_value, new_value].reject(&:empty?).join(" ").to_sym },
|
|
100
149
|
::Array => proc { |old_value, new_value| [old_value, new_value] },
|
|
101
|
-
::Hash => proc { |old_value, new_value| merge(
|
|
150
|
+
::Hash => proc { |old_value, new_value| old_value.merge(new_value.transform_keys(&:to_sym)) }
|
|
102
151
|
}.freeze
|
|
103
152
|
|
|
104
153
|
def update(id, new_value)
|
|
@@ -115,18 +164,21 @@ module Sevgi
|
|
|
115
164
|
private_constant :UPDATER
|
|
116
165
|
|
|
117
166
|
def to_xml(id, value)
|
|
118
|
-
case value
|
|
167
|
+
text = case value
|
|
119
168
|
when ::Hash
|
|
120
|
-
|
|
169
|
+
value.map { |key, attr_value| "#{key}:#{attr_value}" }.join("; ")
|
|
121
170
|
when ::Array
|
|
122
|
-
|
|
171
|
+
value.join(" ")
|
|
123
172
|
else
|
|
124
|
-
|
|
173
|
+
value.to_s
|
|
125
174
|
end
|
|
175
|
+
|
|
176
|
+
"#{id}=#{text.encode(xml: :attr)}"
|
|
126
177
|
end
|
|
127
178
|
end
|
|
128
179
|
end
|
|
129
180
|
|
|
181
|
+
# Public alias for the SVG attribute store.
|
|
130
182
|
Attributes = Attribute::Store
|
|
131
183
|
end
|
|
132
184
|
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
5
|
+
module Sevgi
|
|
6
|
+
module Graphics
|
|
7
|
+
# SVG canvas size, margins, viewport, and viewBox.
|
|
8
|
+
class Canvas
|
|
9
|
+
# @overload call(arg = Undefined, **kwargs)
|
|
10
|
+
# Builds a canvas from a paper profile or explicit size.
|
|
11
|
+
# @param arg [Sevgi::Graphics::Paper, Symbol, String, Sevgi::Undefined] paper profile or paper object
|
|
12
|
+
# @param kwargs [Hash] canvas keyword arguments
|
|
13
|
+
# @return [Sevgi::Graphics::Canvas]
|
|
14
|
+
# @raise [Sevgi::ArgumentError] when the paper profile is unknown
|
|
15
|
+
def self.call(...) = from_paper(...)
|
|
16
|
+
|
|
17
|
+
# Builds a canvas from a paper profile or explicit size.
|
|
18
|
+
# @param arg [Sevgi::Graphics::Paper, Symbol, String, Sevgi::Undefined] paper profile or paper object
|
|
19
|
+
# @param kwargs [Hash] canvas keyword arguments
|
|
20
|
+
# @return [Sevgi::Graphics::Canvas]
|
|
21
|
+
# @raise [Sevgi::ArgumentError] when the paper profile is unknown
|
|
22
|
+
def self.from_paper(arg = Undefined, **kwargs)
|
|
23
|
+
case arg
|
|
24
|
+
when Undefined
|
|
25
|
+
new(**kwargs)
|
|
26
|
+
else
|
|
27
|
+
new(**paper(arg).to_h, **kwargs)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.paper(arg)
|
|
32
|
+
case arg
|
|
33
|
+
when Paper
|
|
34
|
+
arg
|
|
35
|
+
when ::Symbol, ::String
|
|
36
|
+
ArgumentError.("Unknown paper profile: #{arg}") unless Paper.exist?(arg)
|
|
37
|
+
|
|
38
|
+
Paper.public_send(arg)
|
|
39
|
+
else
|
|
40
|
+
ArgumentError.("Argument must be a Paper symbol: #{arg}")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private_class_method :paper
|
|
45
|
+
|
|
46
|
+
extend Forwardable
|
|
47
|
+
|
|
48
|
+
def_delegators :@margin, *Margin.members
|
|
49
|
+
def_delegators :@size, *Paper.members
|
|
50
|
+
|
|
51
|
+
# @!attribute [r] size
|
|
52
|
+
# @return [Sevgi::Graphics::Paper] paper size object
|
|
53
|
+
# @!attribute [r] margin
|
|
54
|
+
# @return [Sevgi::Graphics::Margin] canvas margins
|
|
55
|
+
# @!attribute [r] inner
|
|
56
|
+
# @return [Sevgi::Graphics::Paper] inner paper after margins
|
|
57
|
+
attr_reader :size, :margin, :inner
|
|
58
|
+
|
|
59
|
+
# Creates a canvas.
|
|
60
|
+
# @param width [Numeric] canvas width
|
|
61
|
+
# @param height [Numeric] canvas height
|
|
62
|
+
# @param unit [Symbol, String] SVG unit
|
|
63
|
+
# @param name [Symbol, String] paper name
|
|
64
|
+
# @param margins [Array<Numeric>] margin shorthand values
|
|
65
|
+
# @return [void]
|
|
66
|
+
# @raise [Sevgi::ArgumentError] when margins or numeric dimensions are invalid
|
|
67
|
+
def initialize(width:, height:, unit: "mm", name: :custom, margins: [])
|
|
68
|
+
@size = Paper[width, height, unit, name]
|
|
69
|
+
@margin = Margin[*margins]
|
|
70
|
+
|
|
71
|
+
compute
|
|
72
|
+
freeze
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @overload attributes(origin = Undefined)
|
|
76
|
+
# Returns SVG root viewport attributes.
|
|
77
|
+
# @param origin [Numeric, Array<Numeric>, nil, Sevgi::Undefined] viewBox origin
|
|
78
|
+
# @return [Hash]
|
|
79
|
+
# @raise [Sevgi::ArgumentError] when origin is invalid
|
|
80
|
+
def attributes(...) = {**viewport, viewBox: viewbox(...)}
|
|
81
|
+
|
|
82
|
+
# Returns SVG width and height attributes.
|
|
83
|
+
# @return [Hash]
|
|
84
|
+
def viewport = {width: "#{width}#{unit}", height: "#{height}#{unit}"}
|
|
85
|
+
|
|
86
|
+
# Returns the SVG viewBox string.
|
|
87
|
+
# @param origin [Numeric, Array<Numeric>, nil, Sevgi::Undefined] viewBox origin
|
|
88
|
+
# @return [String]
|
|
89
|
+
# @raise [Sevgi::ArgumentError] when origin is invalid
|
|
90
|
+
def viewbox(origin = Undefined) = prettify(*originate(origin), width, height).join(" ")
|
|
91
|
+
|
|
92
|
+
# Returns a canvas with selected fields replaced.
|
|
93
|
+
# @param kwargs [Hash] replacement options
|
|
94
|
+
# @return [Sevgi::Graphics::Canvas]
|
|
95
|
+
def with(**kwargs) = self.class.new(**size.to_h, margins: kwargs.fetch(:margins, margin.to_a))
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def compute
|
|
100
|
+
@inner = size.with(width: width - margin.left - margin.right, height: height - margin.top - margin.bottom)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def originate(origin)
|
|
104
|
+
case origin
|
|
105
|
+
when Undefined
|
|
106
|
+
[-margin.left, -margin.top]
|
|
107
|
+
when ::Numeric, ::NilClass
|
|
108
|
+
[origin.to_f, origin.to_f]
|
|
109
|
+
when ::Array
|
|
110
|
+
pair(origin)
|
|
111
|
+
else
|
|
112
|
+
ArgumentError.("Canvas origin must be an Array")
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def prettify(*floats)
|
|
117
|
+
floats.map { (it % 1).zero? ? it.to_i : it }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def pair(array)
|
|
121
|
+
array.size == 2 ? array.map(&:to_f) : ArgumentError.("Argument must be an Array of size 2")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sevgi
|
|
4
|
+
module Graphics
|
|
5
|
+
# Renderable text-like content inside an SVG element.
|
|
6
|
+
class Content
|
|
7
|
+
# @!attribute [r] content
|
|
8
|
+
# @return [Object] wrapped content
|
|
9
|
+
attr_reader :content
|
|
10
|
+
|
|
11
|
+
# Creates content.
|
|
12
|
+
# @param content [Object] wrapped content
|
|
13
|
+
# @return [void]
|
|
14
|
+
def initialize(content) = @content = content
|
|
15
|
+
|
|
16
|
+
# Renders content with a renderer.
|
|
17
|
+
# @abstract Subclasses implement content rendering.
|
|
18
|
+
# @param _renderer [Object] renderer receiving output
|
|
19
|
+
# @param _depth [Integer] current render depth
|
|
20
|
+
# @return [void]
|
|
21
|
+
# @raise [Sevgi::PanicError] when a subclass does not implement render
|
|
22
|
+
def render(_renderer, _depth) = PanicError.("#{self.class}#render must be implemented")
|
|
23
|
+
|
|
24
|
+
# Returns content as a string.
|
|
25
|
+
# @return [String]
|
|
26
|
+
def to_s = content.to_s
|
|
27
|
+
|
|
28
|
+
# @overload cdata(content)
|
|
29
|
+
# Builds CDATA content.
|
|
30
|
+
# @param content [Object] wrapped content
|
|
31
|
+
# @return [Sevgi::Graphics::Content::CData]
|
|
32
|
+
def self.cdata(...) = CData.new(...)
|
|
33
|
+
|
|
34
|
+
# Wraps content arguments, encoding non-content values.
|
|
35
|
+
# @param args [Array<Object>] content arguments
|
|
36
|
+
# @return [Array<Sevgi::Graphics::Content>]
|
|
37
|
+
def self.contents(*args) = args.map { it.is_a?(Content) ? it : encoded(it) }
|
|
38
|
+
|
|
39
|
+
# @overload css(content)
|
|
40
|
+
# Builds CSS content.
|
|
41
|
+
# @param content [Hash] CSS rules
|
|
42
|
+
# @return [Sevgi::Graphics::Content::CSS]
|
|
43
|
+
# @raise [Sevgi::ArgumentError] when content is not a hash
|
|
44
|
+
def self.css(...) = CSS.new(...)
|
|
45
|
+
|
|
46
|
+
# @overload encoded(content)
|
|
47
|
+
# Builds XML text-encoded content.
|
|
48
|
+
# @param content [Object] wrapped content
|
|
49
|
+
# @return [Sevgi::Graphics::Content::Encoded]
|
|
50
|
+
def self.encoded(...) = Encoded.new(...)
|
|
51
|
+
|
|
52
|
+
# Joins content lines with newlines.
|
|
53
|
+
# @param contents [Object, Array<Object>] content lines
|
|
54
|
+
# @return [String]
|
|
55
|
+
def self.text(contents) = Array(contents).join("\n")
|
|
56
|
+
|
|
57
|
+
# @overload verbatim(content)
|
|
58
|
+
# Builds verbatim content.
|
|
59
|
+
# @param content [Object] wrapped content
|
|
60
|
+
# @return [Sevgi::Graphics::Content::Verbatim]
|
|
61
|
+
def self.verbatim(...) = Verbatim.new(...)
|
|
62
|
+
|
|
63
|
+
# CDATA section content.
|
|
64
|
+
class CData < Content
|
|
65
|
+
# Renders CDATA content.
|
|
66
|
+
# @param renderer [Object] renderer receiving output
|
|
67
|
+
# @param depth [Integer] current render depth
|
|
68
|
+
# @return [void]
|
|
69
|
+
def render(renderer, depth)
|
|
70
|
+
depth += 1
|
|
71
|
+
|
|
72
|
+
renderer.append(depth, "<![CDATA[")
|
|
73
|
+
renderer.append(depth + 1, *Array(content))
|
|
74
|
+
renderer.append(depth, "]]>")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# CSS content rendered inside a CDATA section.
|
|
79
|
+
class CSS < Content
|
|
80
|
+
# Creates CSS content.
|
|
81
|
+
# @param content [Hash] CSS rules
|
|
82
|
+
# @return [void]
|
|
83
|
+
# @raise [Sevgi::ArgumentError] when content is not a hash
|
|
84
|
+
def initialize(content)
|
|
85
|
+
ArgumentError.("CSS content must be a hash: #{content}") unless content.is_a?(::Hash)
|
|
86
|
+
|
|
87
|
+
super
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# rubocop:disable Metrics/MethodLength
|
|
91
|
+
# Renders CSS content.
|
|
92
|
+
# @param renderer [Object] renderer receiving output
|
|
93
|
+
# @param depth [Integer] current render depth
|
|
94
|
+
# @return [void]
|
|
95
|
+
# @raise [Sevgi::ArgumentError] when a style value cannot be rendered
|
|
96
|
+
def render(renderer, depth)
|
|
97
|
+
depth += 1
|
|
98
|
+
|
|
99
|
+
renderer.append(depth, "<![CDATA[")
|
|
100
|
+
|
|
101
|
+
depth += 1
|
|
102
|
+
content.each do |rule, styles|
|
|
103
|
+
case styles
|
|
104
|
+
when ::Hash
|
|
105
|
+
renderer.append(depth, "#{rule} {")
|
|
106
|
+
renderer.append(depth + 1, *styles.map { |key, value| "#{key}: #{value};" })
|
|
107
|
+
renderer.append(depth, "}")
|
|
108
|
+
when ::String, ::Symbol, ::Numeric
|
|
109
|
+
renderer.append(depth, "#{rule}: #{styles};")
|
|
110
|
+
else
|
|
111
|
+
ArgumentError.("Malformed style: #{styles}")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
depth -= 1
|
|
116
|
+
|
|
117
|
+
renderer.append(depth, "]]>")
|
|
118
|
+
end
|
|
119
|
+
# rubocop:enable Metrics/MethodLength
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# XML text-encoded content.
|
|
123
|
+
class Encoded < Content
|
|
124
|
+
# Returns XML text-encoded content.
|
|
125
|
+
# @return [String]
|
|
126
|
+
def to_s = content.encode(xml: :text)
|
|
127
|
+
|
|
128
|
+
# Renders encoded text content.
|
|
129
|
+
# @param renderer [Object] renderer receiving output
|
|
130
|
+
# @param depth [Integer] current render depth
|
|
131
|
+
# @return [void]
|
|
132
|
+
def render(renderer, depth) = renderer.append(depth + 1, to_s)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Verbatim content rendered without XML text encoding.
|
|
136
|
+
class Verbatim < Content
|
|
137
|
+
# Renders verbatim content.
|
|
138
|
+
# @param renderer [Object] renderer receiving output
|
|
139
|
+
# @param depth [Integer] current render depth
|
|
140
|
+
# @return [void]
|
|
141
|
+
def render(renderer, depth) = renderer.append(depth + 1, to_s)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sevgi
|
|
4
|
+
module Graphics
|
|
5
|
+
# Four-sided margin with CSS-like shorthand normalization.
|
|
6
|
+
Margin = Data.define(:top, :right, :bottom, :left) do
|
|
7
|
+
include Comparable
|
|
8
|
+
|
|
9
|
+
# @!attribute [r] top
|
|
10
|
+
# @return [Float] top margin
|
|
11
|
+
# @!attribute [r] right
|
|
12
|
+
# @return [Float] right margin
|
|
13
|
+
# @!attribute [r] bottom
|
|
14
|
+
# @return [Float] bottom margin
|
|
15
|
+
# @!attribute [r] left
|
|
16
|
+
# @return [Float] left margin
|
|
17
|
+
|
|
18
|
+
# Creates a margin from one to four shorthand values.
|
|
19
|
+
# @param top [Numeric, nil] top value or all-sides shorthand
|
|
20
|
+
# @param right [Numeric, nil] right value or horizontal shorthand
|
|
21
|
+
# @param bottom [Numeric, nil] bottom value
|
|
22
|
+
# @param left [Numeric, nil] left value
|
|
23
|
+
# @return [void]
|
|
24
|
+
def initialize(top: nil, right: nil, bottom: nil, left: nil)
|
|
25
|
+
super(**normalize(top, right, bottom, left))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Compares margins by top, right, bottom, then left.
|
|
29
|
+
# @param other [Sevgi::Graphics::Margin] margin to compare
|
|
30
|
+
# @return [Integer, nil]
|
|
31
|
+
def <=>(other) = deconstruct <=> other.deconstruct
|
|
32
|
+
|
|
33
|
+
# Returns a margin inflated horizontally and vertically.
|
|
34
|
+
# @param h [Numeric] horizontal addition
|
|
35
|
+
# @param v [Numeric] vertical addition
|
|
36
|
+
# @return [Sevgi::Graphics::Margin]
|
|
37
|
+
def adjust(h, v) = self.class[top + v, right + h, bottom + v, left + h]
|
|
38
|
+
|
|
39
|
+
# Reports strict margin equality.
|
|
40
|
+
# @param other [Object] object to compare
|
|
41
|
+
# @return [Boolean]
|
|
42
|
+
def eql?(other) = self.class == other.class && deconstruct == other.deconstruct
|
|
43
|
+
|
|
44
|
+
# Returns a hash compatible with strict equality.
|
|
45
|
+
# @return [Integer]
|
|
46
|
+
def hash = [self.class, *deconstruct].hash
|
|
47
|
+
|
|
48
|
+
# Returns total horizontal margin.
|
|
49
|
+
# @return [Float]
|
|
50
|
+
def horizontal = left + right
|
|
51
|
+
|
|
52
|
+
# Returns total vertical margin.
|
|
53
|
+
# @return [Float]
|
|
54
|
+
def vertical = top + bottom
|
|
55
|
+
|
|
56
|
+
alias_method :==, :eql?
|
|
57
|
+
alias_method :to_a, :deconstruct
|
|
58
|
+
|
|
59
|
+
# Builds a margin from an array-like shorthand.
|
|
60
|
+
# @param array [Object] value converted with Array()
|
|
61
|
+
# @return [Sevgi::Graphics::Margin]
|
|
62
|
+
def self.margin(array)
|
|
63
|
+
self[
|
|
64
|
+
*(array = Array(array)[0...(size = Margin.members.size)]).fill(nil, array.size, size - array.size)
|
|
65
|
+
]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns a zero margin.
|
|
69
|
+
# @return [Sevgi::Graphics::Margin]
|
|
70
|
+
def self.zero = (@zero ||= self[0.0, 0.0, 0.0, 0.0])
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def normalize(top, right, bottom, left)
|
|
75
|
+
values = [top, right, bottom, left]
|
|
76
|
+
|
|
77
|
+
Margin.members.zip(values_for(values.compact.size, values).map { Float(it) }).to_h
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def values_for(size, values)
|
|
81
|
+
top, right, bottom = values
|
|
82
|
+
|
|
83
|
+
[
|
|
84
|
+
[0, 0, 0, 0],
|
|
85
|
+
[top, top, top, top],
|
|
86
|
+
[top, right, top, right],
|
|
87
|
+
[top, right, bottom, right]
|
|
88
|
+
][size] ||
|
|
89
|
+
values
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|