sevgi-graphics 0.95.0 → 0.98.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +125 -0
- data/README.md +8 -8
- data/lib/sevgi/graphics/attribute.rb +160 -37
- data/lib/sevgi/graphics/auxiliary/canvas.rb +100 -43
- data/lib/sevgi/graphics/auxiliary/content.rb +54 -34
- data/lib/sevgi/graphics/auxiliary/margin.rb +19 -12
- data/lib/sevgi/graphics/auxiliary/paper.rb +74 -49
- data/lib/sevgi/graphics/auxiliary/path.rb +44 -0
- data/lib/sevgi/graphics/auxiliary/scalar.rb +36 -7
- data/lib/sevgi/graphics/auxiliary.rb +1 -0
- data/lib/sevgi/graphics/document/base.rb +6 -2
- data/lib/sevgi/graphics/document/default.rb +1 -1
- data/lib/sevgi/graphics/document.rb +236 -102
- data/lib/sevgi/graphics/element.rb +114 -31
- data/lib/sevgi/graphics/mixtures/call.rb +249 -88
- data/lib/sevgi/graphics/mixtures/core.rb +59 -20
- data/lib/sevgi/graphics/mixtures/duplicate.rb +49 -15
- data/lib/sevgi/graphics/mixtures/export.rb +52 -12
- data/lib/sevgi/graphics/mixtures/hatch.rb +49 -7
- data/lib/sevgi/graphics/mixtures/identify.rb +30 -17
- data/lib/sevgi/graphics/mixtures/include.rb +26 -8
- data/lib/sevgi/graphics/mixtures/inkscape.rb +201 -47
- data/lib/sevgi/graphics/mixtures/rdf.rb +59 -7
- data/lib/sevgi/graphics/mixtures/render.rb +52 -28
- data/lib/sevgi/graphics/mixtures/save.rb +79 -35
- data/lib/sevgi/graphics/mixtures/symbols.rb +81 -12
- data/lib/sevgi/graphics/mixtures/tile.rb +85 -67
- data/lib/sevgi/graphics/mixtures/transform.rb +68 -28
- data/lib/sevgi/graphics/mixtures/underscore.rb +14 -5
- data/lib/sevgi/graphics/mixtures/validate.rb +2 -2
- data/lib/sevgi/graphics/mixtures/wrappers.rb +62 -23
- data/lib/sevgi/graphics/mixtures.rb +15 -13
- data/lib/sevgi/graphics/version.rb +1 -1
- data/lib/sevgi/graphics.rb +58 -12
- metadata +7 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 753194f4865e2c8b099f788588a684a72a116004b3e56bb40cfd626a7b14043f
|
|
4
|
+
data.tar.gz: d443e433a8c86e4d902b31a4dadb71a244905f83aca4b67d8d56c64fdd7cb6c7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 05517e6f426cc1cd7ce0077705f147db602346bfba518500551dfce4008d669935f1fe3f21fc83b4531f33a44e4eec38e5b3ff82ccceff1bf1968a30f7ce8998
|
|
7
|
+
data.tar.gz: d19ec2a6d3e5e093876ab72eaa594220241f5ff8f5c744124330ee69adff71f12a67159779b9894a6bd4589ea0b3af78ad16ed768d41b422ffc18bca77248075
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,131 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.98.2 - 2026-07-25
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Restored the `sevgi` CLI's main-object scope so bare toolkit calls remain available inside script helper classes.
|
|
13
|
+
|
|
14
|
+
## 0.98.1 - 2026-07-20
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Changed the `sevgi` CLI to use the executor's isolated scope by default.
|
|
19
|
+
|
|
20
|
+
### Removed
|
|
21
|
+
|
|
22
|
+
- Removed the redundant `-n` and `--nomain` CLI options; library consumers can still select `main: true` through
|
|
23
|
+
`Sevgi.execute` and `Sevgi.execute_file` when needed.
|
|
24
|
+
|
|
25
|
+
## 0.98.0 - 2026-07-20
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- Added standard-input support to `sevgi`, `igves`, and `igsev`; `sevgi --as NAME` and
|
|
30
|
+
`Sevgi.execute_file(..., as: NAME)` supply the logical source basename used by implicit SVG, PDF, and PNG
|
|
31
|
+
destinations without changing physical load identity.
|
|
32
|
+
|
|
33
|
+
## 0.97.0 - 2026-07-19
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- Added `sevgi-appendix`, containing the Sevgi agent skill and a RuboCop plugin for preserving readable `.sevgi` DSL
|
|
38
|
+
source alongside rubyfmt-formatted Ruby; the umbrella `sevgi` gem installs the matching Appendix version.
|
|
39
|
+
- Added `sevgi --skill` to report the validated path of the matching packaged agent skill for product-neutral setup.
|
|
40
|
+
- Added `igsev` to the umbrella gem for normalizing an SVG file through a complete SVG-to-Sevgi-to-SVG round trip.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- Made Homebrew the recommended complete CLI installation while retaining Bundler and focused component gems for Ruby
|
|
45
|
+
application dependencies.
|
|
46
|
+
|
|
47
|
+
## 0.96.0 - 2026-07-18
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- Added exact, subtree-wide attribute omission to Derender content/file conversion, evaluation, and inclusion APIs;
|
|
52
|
+
`igves --omit` exposes the same behavior from the command line.
|
|
53
|
+
- Added `Sevgi.SVG` as the explicit namespaced form of the top-level SVG document entrypoint.
|
|
54
|
+
- Added the opt-in recursive `SVG::Modules` contract for callable drawing namespaces.
|
|
55
|
+
- Added callable drawing-module `base` blocks, document and paper registry introspection, Canvas structural equality,
|
|
56
|
+
renderer options on `RenderChildren`, axis translation helpers, and a CC BY RDF helper.
|
|
57
|
+
- Formalized non-rendering `-` metadata and `+` attribute updates as `Attributes::META_PREFIX` and
|
|
58
|
+
`Attributes::UPDATE_SUFFIX`; repeated Array updates now concatenate into a stable flat value.
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- Breaking: replaced the `SVG = Sevgi::Graphics` alias with an independent SVG facade. Facade operations now use
|
|
63
|
+
capitalized names such as `SVG.Canvas` and `SVG.Document`; lowercase component helpers remain on
|
|
64
|
+
`Sevgi::Graphics`, and the stuttering `SVG.SVG` form was removed.
|
|
65
|
+
- Breaking: made low-level `Executor.execute` and `execute_file` runners private; consumers use
|
|
66
|
+
`Sevgi.execute` and `Sevgi.execute_file` while retaining the public result and error types.
|
|
67
|
+
- Breaking: removed the abstract `Document::Base` layer from the selectable document-profile registry; `Minimal` and
|
|
68
|
+
`Default` are now sibling concrete profiles, and custom profile hierarchies should derive from `Base`.
|
|
69
|
+
- Breaking: renamed Parallelogram segment and constraint arguments by geometric role: `base`, `side`, and
|
|
70
|
+
`constraint` replace axis-implying constructor names.
|
|
71
|
+
- Made degree-based sine and cosine exact at integer quarter turns, eliminating cardinal Geometry residue.
|
|
72
|
+
- Documented complete file-system failure families for file comparison, output, touch, and upward location helpers.
|
|
73
|
+
- Formalized generated Tile ids, positional CSS classes, template placement, and per-use callback signatures.
|
|
74
|
+
- Formalized String and Symbol ids across Derender selection APIs and documented file-read failures consistently.
|
|
75
|
+
- Breaking: made top-level `Decompile`, `Derender`, `Evaluate`, and `EvaluateChildren` consume inline SVG/XML;
|
|
76
|
+
file inputs now use the corresponding `File`-suffixed entrypoints.
|
|
77
|
+
- Breaking: replaced callable drawing module `call` block registration with argument-independent `base` blocks;
|
|
78
|
+
inherited bases run parent-first, and modules with one drawing method conventionally name it `call`.
|
|
79
|
+
- Breaking: made bracket and call notation the canonical Geometry constructors. Among Data value types, bracket notation
|
|
80
|
+
is public only for Point, Segment, LengthAngle, Margin, and Paper; use `.new` for Location and result carriers, and
|
|
81
|
+
`Stay` for traversal stop tokens.
|
|
82
|
+
- Breaking: aligned Point and Segment comparison with Ruby `Comparable`; malformed or unrelated comparison operands now
|
|
83
|
+
return nil from `<=>`.
|
|
84
|
+
- Breaking: executor entrypoints now return immutable `Executor::Result` values and expose only `execute` and
|
|
85
|
+
`execute_file`; inspect `result.error`, `result.stack`, and `result.value` instead of executor scopes or lifecycle state.
|
|
86
|
+
- Breaking: wrapper attributes and callable arguments use distinct channels, `With` rejects parentless receivers, and
|
|
87
|
+
unsupported direct constructors are private; use the documented factories for Content, concrete Geometry lined
|
|
88
|
+
elements, and Grid query values.
|
|
89
|
+
- Breaking: Canvas uses `.new` for explicit fields, `.from_paper(paper, **overrides)` for paper conversion, and
|
|
90
|
+
`.call`/`Graphics.canvas` for dispatch; the misleading keyword-only `.from_paper(width:, height:)` form was removed.
|
|
91
|
+
- Breaking: `Save`, `Write`, `PNG`, `PDF`, and Sundries native export normalize successful destinations to expanded
|
|
92
|
+
String paths and create missing parent directories; change-aware SVG writes still return nil when unchanged.
|
|
93
|
+
- Breaking: renamed `Derender.evaluate_file_children` to `evaluate_children_file`; decompiled nodes now own immutable
|
|
94
|
+
public state while parser, strategy, and construction plumbing remain private.
|
|
95
|
+
- Expanded runtime/YARD parity checks and exact contracts for inherited, extended, generated, and dynamic API surfaces,
|
|
96
|
+
forwarded options, error channels, path ownership, whitespace, namespaces, nil behavior, and return values.
|
|
97
|
+
|
|
98
|
+
### Fixed
|
|
99
|
+
|
|
100
|
+
- Rejected directories from default file discovery while preserving custom locator matchers.
|
|
101
|
+
- Kept `Ancestral` context in non-rendering `-context` metadata instead of leaking it into SVG attributes.
|
|
102
|
+
- Preserved source Canvas units and names when deriving fitted Grid canvases.
|
|
103
|
+
- Prevented stale documentation assets from mixing old layouts with new HTML, and moved sidebar navigation into the
|
|
104
|
+
tablet menu at 1024px and below.
|
|
105
|
+
- Normalized numeric slots owned by path, shape, transform, tile, and Inkscape page helpers to finite SVG number spelling;
|
|
106
|
+
arbitrary user-supplied attributes remain untouched.
|
|
107
|
+
- Made named callable wrapper and symbol ids stable while omitting defaults for anonymous modules, and indexed every
|
|
108
|
+
rendered id value, including false and numeric values, through its serialized string.
|
|
109
|
+
- Preserved signed Geometry constraint directions, rejected invalid sweep/export channels through Sevgi error families,
|
|
110
|
+
and validated raw output paths before expansion or rendering.
|
|
111
|
+
- Made callable module configuration copy-owned and freeze-aware, document subclasses inherit their nearest profile, and
|
|
112
|
+
false executor boot receivers remain explicit rather than defaulting to an internal scope.
|
|
113
|
+
- Made element trees, attributes, identifiers, locator results, document profiles, Derender nodes, shell results, and
|
|
114
|
+
executor results retain owned immutable snapshots where their public contracts promise value semantics.
|
|
115
|
+
- Corrected Shell combined-output separators, nil export density errors, document render-option routing, executor source
|
|
116
|
+
snapshots, selected-node namespace/whitespace documentation, and Standard character-data validation.
|
|
117
|
+
|
|
118
|
+
### Removed
|
|
119
|
+
|
|
120
|
+
- Removed public access to pluralization tables and the internal SVG save extension; pluralization rules are now deeply
|
|
121
|
+
immutable.
|
|
122
|
+
- Removed public documentation and constant access for command-line implementation modules; the `sevgi` and `igves`
|
|
123
|
+
executables remain unchanged.
|
|
124
|
+
- Removed eager loading and public documentation of private Showcase build/test support; explicit support entrypoints
|
|
125
|
+
now keep the harness under the Showcase namespace.
|
|
126
|
+
- Removed public access to document profile name normalizers; registry operations retain them as private plumbing.
|
|
127
|
+
- Removed the redundant `Margin.margin` constructor; use canonical bracket notation.
|
|
128
|
+
- Removed public executor orchestration, obsolete callable-module hooks, the old public attribute syntax constants, and
|
|
129
|
+
direct construction of abstract or internally wired Content, Element, and Grid query types.
|
|
130
|
+
- Removed accidental public access to abstract Lined factories, internal element-name/export maps, result/location
|
|
131
|
+
bracket constructors, and direct Stop construction.
|
|
132
|
+
|
|
8
133
|
## 0.95.0 - 2026-07-11
|
|
9
134
|
|
|
10
135
|
### Security
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sevgi Graphics
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Sevgi Graphics implements the SVG DSL, document profiles, and rendering.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -17,21 +17,21 @@ require "sevgi/graphics"
|
|
|
17
17
|
## Example
|
|
18
18
|
|
|
19
19
|
```ruby
|
|
20
|
-
doc = Sevgi::Graphics.SVG(:minimal) { rect
|
|
20
|
+
doc = Sevgi::Graphics.SVG(:minimal) { rect width: 3, height: 5 }
|
|
21
21
|
doc.call
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
## Ruby compatibility
|
|
25
25
|
|
|
26
|
-
Requires Ruby 3.4.0 or newer. CI verifies Ruby 3.4
|
|
26
|
+
Requires Ruby 3.4.0 or newer. CI verifies the current Ruby 3.4 release and the development Ruby from `.ruby-version`.
|
|
27
27
|
|
|
28
28
|
## Native prerequisites
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
This gem needs only Ruby and its Ruby dependencies.
|
|
31
31
|
|
|
32
32
|
## Links
|
|
33
33
|
|
|
34
|
-
- Documentation: https://sevgi.roktas.dev
|
|
35
|
-
- API documentation: https://www.rubydoc.info/gems/sevgi-graphics
|
|
36
|
-
- Source: https://github.com/roktas/sevgi/tree/main/graphics
|
|
37
|
-
- Changelog: https://github.com/roktas/sevgi/blob/main/CHANGELOG.md
|
|
34
|
+
- Documentation: <https://sevgi.roktas.dev>
|
|
35
|
+
- API documentation: <https://www.rubydoc.info/gems/sevgi-graphics>
|
|
36
|
+
- Source: <https://github.com/roktas/sevgi/tree/main/graphics>
|
|
37
|
+
- Changelog: <https://github.com/roktas/sevgi/blob/main/CHANGELOG.md>
|
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
module Sevgi
|
|
6
6
|
module Graphics
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
# Mutable facade for SVG attributes and non-rendering element metadata.
|
|
8
|
+
class Attributes
|
|
9
|
+
# Prefix marking supported non-rendering metadata names. Prefixed entries remain available through the facade but
|
|
10
|
+
# are omitted from SVG output.
|
|
11
|
+
META_PREFIX = "-"
|
|
12
|
+
|
|
13
|
+
# Suffix requesting an update of an existing value. String and Symbol values are space-joined, Arrays are
|
|
14
|
+
# concatenated, and Hashes are merged. When the attribute is absent, assignment uses normal replacement behavior.
|
|
15
|
+
UPDATE_SUFFIX = "+"
|
|
16
|
+
end
|
|
12
17
|
|
|
13
18
|
# Attribute name normalization helpers.
|
|
14
19
|
# @api private
|
|
@@ -20,7 +25,7 @@ module Sevgi
|
|
|
20
25
|
# @param given [String, Symbol] attribute name
|
|
21
26
|
# @return [Boolean]
|
|
22
27
|
def internal?(given)
|
|
23
|
-
(@internal ||= {})[given] ||= key(given).start_with?(
|
|
28
|
+
(@internal ||= {})[given] ||= key(given).start_with?(Attributes::META_PREFIX)
|
|
24
29
|
end
|
|
25
30
|
|
|
26
31
|
# Returns the normalized attribute id.
|
|
@@ -28,7 +33,7 @@ module Sevgi
|
|
|
28
33
|
# @return [Symbol]
|
|
29
34
|
def id(given)
|
|
30
35
|
(@id ||= {})[given] ||= begin
|
|
31
|
-
name = updateable?(given) ? key(given).delete_suffix(
|
|
36
|
+
name = updateable?(given) ? key(given).delete_suffix(Attributes::UPDATE_SUFFIX) : key(given)
|
|
32
37
|
XML.name(name, context: "XML attribute name") unless internal?(given)
|
|
33
38
|
name.to_sym
|
|
34
39
|
end
|
|
@@ -38,7 +43,7 @@ module Sevgi
|
|
|
38
43
|
# @param given [String, Symbol] attribute name
|
|
39
44
|
# @return [Boolean]
|
|
40
45
|
def updateable?(given)
|
|
41
|
-
(@updateable ||= {})[given] ||= key(given).end_with?(
|
|
46
|
+
(@updateable ||= {})[given] ||= key(given).end_with?(Attributes::UPDATE_SUFFIX)
|
|
42
47
|
end
|
|
43
48
|
|
|
44
49
|
private
|
|
@@ -125,6 +130,25 @@ module Sevgi
|
|
|
125
130
|
# @api private
|
|
126
131
|
def self.capture(value, normalize_keys: false) = Snapshot.capture(value, normalize_keys:)
|
|
127
132
|
|
|
133
|
+
# Returns normalized owned attributes with defaults for absent names.
|
|
134
|
+
# @param attributes [Hash] source attributes
|
|
135
|
+
# @param defaults [Hash] values inserted only when their normalized names are absent
|
|
136
|
+
# @return [Hash{Symbol => Object}] normalized owned attributes
|
|
137
|
+
# @raise [Sevgi::ArgumentError] when input contains an invalid, colliding, or unsupported attribute
|
|
138
|
+
# @api private
|
|
139
|
+
def self.defaults(attributes, **defaults)
|
|
140
|
+
attributes = Attributes.new(attributes)
|
|
141
|
+
defaults.each { |name, value| attributes[name] = value unless attributes.has?(name) }
|
|
142
|
+
attributes.to_h
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Returns normalized owned attributes.
|
|
146
|
+
# @param attributes [Hash] source attributes
|
|
147
|
+
# @return [Hash{Symbol => Object}] normalized owned attributes
|
|
148
|
+
# @raise [Sevgi::ArgumentError] when input contains an invalid, colliding, or unsupported attribute
|
|
149
|
+
# @api private
|
|
150
|
+
def self.normalize(attributes) = Attributes.new(attributes).to_h
|
|
151
|
+
|
|
128
152
|
# Returns the text form used for an XML attribute value before escaping.
|
|
129
153
|
# @param value [Object] attribute value
|
|
130
154
|
# @return [String]
|
|
@@ -144,7 +168,8 @@ module Sevgi
|
|
|
144
168
|
XML.text(text, context: "XML attribute value")
|
|
145
169
|
end
|
|
146
170
|
|
|
147
|
-
# Mutable
|
|
171
|
+
# Mutable backing store for SVG attributes.
|
|
172
|
+
# @api private
|
|
148
173
|
class Store
|
|
149
174
|
# Creates an attribute store from recursively owned snapshots. Mutable non-container leaves are stringified
|
|
150
175
|
# once; later caller mutation cannot change the store.
|
|
@@ -164,22 +189,13 @@ module Sevgi
|
|
|
164
189
|
# @raise [Sevgi::ArgumentError] when input is not a Hash or a name/value is invalid, cyclic, colliding, or cannot
|
|
165
190
|
# be converted
|
|
166
191
|
def import(attributes)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
hash = attributes.each_with_object({}) do |(key, value), captured|
|
|
170
|
-
next if value.nil?
|
|
192
|
+
updated = @store.dup
|
|
193
|
+
entries(attributes).each { |entry| assign(updated, *entry) }
|
|
171
194
|
|
|
172
|
-
|
|
173
|
-
ArgumentError.("Attribute names collide after normalization: #{id}") if captured.key?(id)
|
|
174
|
-
|
|
175
|
-
captured[id] = Attribute.capture(value, normalize_keys: value.is_a?(::Hash))
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
@store.merge!(hash)
|
|
195
|
+
@store.replace(updated)
|
|
179
196
|
end
|
|
180
197
|
|
|
181
|
-
# Returns a
|
|
182
|
-
# revalidates the resulting value.
|
|
198
|
+
# Returns a stored attribute value for the public facade.
|
|
183
199
|
# @param key [String, Symbol] attribute key
|
|
184
200
|
# @return [Object, nil]
|
|
185
201
|
# @raise [Sevgi::ArgumentError] when key is not a valid XML attribute name
|
|
@@ -199,7 +215,7 @@ module Sevgi
|
|
|
199
215
|
|
|
200
216
|
id = Attribute.id(key)
|
|
201
217
|
value = Attribute.capture(value, normalize_keys: value.is_a?(::Hash))
|
|
202
|
-
@store[id] = @store.key?(id) && Attribute.updateable?(key) ? update(id, value) : value
|
|
218
|
+
@store[id] = @store.key?(id) && Attribute.updateable?(key) ? update(@store[id], value) : value
|
|
203
219
|
end
|
|
204
220
|
|
|
205
221
|
# Deletes an attribute by normalized key.
|
|
@@ -210,13 +226,13 @@ module Sevgi
|
|
|
210
226
|
@store.delete(Attribute.id(key))
|
|
211
227
|
end
|
|
212
228
|
|
|
213
|
-
# Returns
|
|
229
|
+
# Returns rendering attributes, excluding non-rendering metadata. Nested values remain live store values.
|
|
214
230
|
# @return [Hash] shallow attribute view
|
|
215
231
|
def export
|
|
216
232
|
hash = @store.reject { |id, _| Attribute.internal?(id) }
|
|
217
233
|
return hash unless hash.key?(:id)
|
|
218
234
|
|
|
219
|
-
#
|
|
235
|
+
# Keep id first for stable, readable SVG output.
|
|
220
236
|
{id: hash.delete(:id), **hash}
|
|
221
237
|
end
|
|
222
238
|
|
|
@@ -229,9 +245,8 @@ module Sevgi
|
|
|
229
245
|
end
|
|
230
246
|
|
|
231
247
|
# Copies the attribute store with recursively independent values.
|
|
232
|
-
# @param original [Sevgi::Graphics::
|
|
248
|
+
# @param original [Sevgi::Graphics::Attributes] store to copy
|
|
233
249
|
# @return [void]
|
|
234
|
-
# @raise [Sevgi::ArgumentError] when live stored values became cyclic or invalid
|
|
235
250
|
def initialize_copy(original)
|
|
236
251
|
@store = {}
|
|
237
252
|
original.store.each { |key, value| @store[key] = Attribute.capture(value) }
|
|
@@ -239,15 +254,14 @@ module Sevgi
|
|
|
239
254
|
super
|
|
240
255
|
end
|
|
241
256
|
|
|
242
|
-
# Returns
|
|
257
|
+
# Returns rendering attribute names, excluding non-rendering metadata.
|
|
243
258
|
# @return [Array<Symbol>]
|
|
244
259
|
def list
|
|
245
260
|
export.keys
|
|
246
261
|
end
|
|
247
262
|
|
|
248
|
-
# Returns the
|
|
249
|
-
#
|
|
250
|
-
# @return [Hash] live internal store
|
|
263
|
+
# Returns the internal attribute and metadata Hash.
|
|
264
|
+
# @return [Hash] backing store
|
|
251
265
|
def to_h
|
|
252
266
|
@store
|
|
253
267
|
end
|
|
@@ -268,12 +282,32 @@ module Sevgi
|
|
|
268
282
|
UPDATER = {
|
|
269
283
|
::String => proc { |old_value, new_value| [old_value, new_value].reject(&:empty?).join(" ") },
|
|
270
284
|
::Symbol => proc { |old_value, new_value| [old_value, new_value].reject(&:empty?).join(" ").to_sym },
|
|
271
|
-
::Array => proc { |old_value, new_value|
|
|
272
|
-
::Hash => proc { |old_value, new_value| old_value.merge(new_value
|
|
285
|
+
::Array => proc { |old_value, new_value| old_value + new_value },
|
|
286
|
+
::Hash => proc { |old_value, new_value| old_value.merge(new_value) }
|
|
273
287
|
}.freeze
|
|
274
288
|
|
|
275
|
-
def
|
|
276
|
-
|
|
289
|
+
def assign(store, key, id, value)
|
|
290
|
+
store[id] = store.key?(id) && Attribute.updateable?(key) ? update(store[id], value) : value
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def entries(attributes)
|
|
294
|
+
ArgumentError.("Attributes must be imported from a Hash") unless attributes.is_a?(::Hash)
|
|
295
|
+
|
|
296
|
+
identities = {}
|
|
297
|
+
attributes.filter_map do |key, value|
|
|
298
|
+
next if value.nil?
|
|
299
|
+
|
|
300
|
+
id = Attribute.id(key)
|
|
301
|
+
ArgumentError.("Attribute names collide after normalization: #{id}") if identities.key?(id)
|
|
302
|
+
|
|
303
|
+
identities[id] = true
|
|
304
|
+
value = Attribute.capture(value, normalize_keys: value.is_a?(::Hash))
|
|
305
|
+
[key, id, value]
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def update(old_value, new_value)
|
|
310
|
+
UPDATER[new_value.class].call(*sanitized(old_value, new_value))
|
|
277
311
|
end
|
|
278
312
|
|
|
279
313
|
def sanitized(old_value, new_value)
|
|
@@ -292,7 +326,96 @@ module Sevgi
|
|
|
292
326
|
end
|
|
293
327
|
end
|
|
294
328
|
|
|
295
|
-
#
|
|
296
|
-
|
|
329
|
+
# Names beginning with {META_PREFIX} can be read, assigned, deleted, merged, and copied like ordinary
|
|
330
|
+
# attributes. They appear in {#to_h}, but public SVG attribute enumeration and rendered XML omit them. All values
|
|
331
|
+
# entering or leaving this facade are recursively owned snapshots. Appending {UPDATE_SUFFIX} to a name requests a
|
|
332
|
+
# same-family update: Strings and Symbols are space-joined, Arrays are concatenated, and Hashes are merged. An update
|
|
333
|
+
# to an absent name behaves as replacement, and nil assignments are ignored.
|
|
334
|
+
#
|
|
335
|
+
# @example Inspect and update element attributes
|
|
336
|
+
# element = Sevgi::Graphics.SVG { rect id: "copy", "-source": "original" }.children.first
|
|
337
|
+
# element.attributes[:"-source"] # => "original"
|
|
338
|
+
# element.attributes.merge!(fill: "red")
|
|
339
|
+
# element.attributes.to_h # => { id: "copy", :"-source" => "original", fill: "red" }
|
|
340
|
+
class Attributes
|
|
341
|
+
# Creates an attribute facade from recursively owned snapshots.
|
|
342
|
+
# @param attributes [Hash] initial attributes and non-rendering metadata
|
|
343
|
+
# @return [void]
|
|
344
|
+
# @raise [Sevgi::ArgumentError] when input is not a Hash or contains an invalid name or value
|
|
345
|
+
def initialize(attributes = {})
|
|
346
|
+
@store = Attribute::Store.new(attributes)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Returns an owned snapshot of an attribute value.
|
|
350
|
+
# @param key [String, Symbol] attribute key
|
|
351
|
+
# @return [Object, nil] recursively owned value or nil when absent
|
|
352
|
+
# @raise [Sevgi::ArgumentError] when key is not a valid attribute name
|
|
353
|
+
def [](key) = snapshot(@store[key])
|
|
354
|
+
|
|
355
|
+
# Assigns or updates a recursively owned attribute value.
|
|
356
|
+
# @param key [String, Symbol] attribute key, optionally ending in {UPDATE_SUFFIX}
|
|
357
|
+
# @param value [Object, nil] attribute value; nil is ignored
|
|
358
|
+
# @return [Object, nil] recursively owned resulting value or nil when absent
|
|
359
|
+
# @raise [Sevgi::ArgumentError] when the name or value is invalid, or an existing update uses incompatible or
|
|
360
|
+
# unsupported value families
|
|
361
|
+
def []=(key, value)
|
|
362
|
+
@store[key] = value
|
|
363
|
+
snapshot(@store[key])
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Deletes an attribute and returns an owned snapshot of its value.
|
|
367
|
+
# @param key [String, Symbol] attribute key
|
|
368
|
+
# @return [Object, nil] deleted value or nil when absent
|
|
369
|
+
# @raise [Sevgi::ArgumentError] when key is not a valid attribute name
|
|
370
|
+
def delete(key) = snapshot(@store.delete(key))
|
|
371
|
+
|
|
372
|
+
# Reports whether an attribute exists.
|
|
373
|
+
# @param key [String, Symbol] attribute key
|
|
374
|
+
# @return [Boolean]
|
|
375
|
+
# @raise [Sevgi::ArgumentError] when key is not a valid attribute name
|
|
376
|
+
def has?(key) = @store.has?(key)
|
|
377
|
+
|
|
378
|
+
# Copies the facade with recursively independent values.
|
|
379
|
+
# @param original [Sevgi::Graphics::Attributes] facade to copy
|
|
380
|
+
# @return [void]
|
|
381
|
+
# @raise [Sevgi::ArgumentError] when stored values became invalid
|
|
382
|
+
# @api private
|
|
383
|
+
def initialize_copy(original)
|
|
384
|
+
super
|
|
385
|
+
@store = original.store.dup
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
private :initialize_copy
|
|
389
|
+
|
|
390
|
+
# Returns rendering attribute names, excluding non-rendering metadata.
|
|
391
|
+
# @return [Array<Symbol>] frozen name snapshot
|
|
392
|
+
def keys = @store.list.freeze
|
|
393
|
+
|
|
394
|
+
# Atomically assigns or updates recursively owned attributes.
|
|
395
|
+
# @param attributes [Hash] attributes and non-rendering metadata; names may end in {UPDATE_SUFFIX}
|
|
396
|
+
# @return [Sevgi::Graphics::Attributes] self
|
|
397
|
+
# @raise [Sevgi::ArgumentError] when input is not a Hash, names collide, a name or value is invalid, or an existing
|
|
398
|
+
# update uses incompatible or unsupported value families
|
|
399
|
+
def merge!(attributes)
|
|
400
|
+
@store.import(attributes)
|
|
401
|
+
self
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Returns a recursively owned Hash snapshot including non-rendering metadata.
|
|
405
|
+
# @return [Hash{Symbol => Object}] owned attribute and metadata snapshot
|
|
406
|
+
def to_h = snapshot(@store.to_h)
|
|
407
|
+
|
|
408
|
+
private
|
|
409
|
+
|
|
410
|
+
def snapshot(value) = Attribute.capture(value)
|
|
411
|
+
|
|
412
|
+
def xml_lines = @store.to_xml_lines
|
|
413
|
+
|
|
414
|
+
protected
|
|
415
|
+
|
|
416
|
+
attr_reader :store
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
private_constant :Attribute
|
|
297
420
|
end
|
|
298
421
|
end
|