sevgi-graphics 0.98.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +96 -2
- data/README.md +4 -1
- data/lib/sevgi/graphics/attribute.rb +8 -10
- data/lib/sevgi/graphics/auxiliary/canvas.rb +4 -4
- data/lib/sevgi/graphics/auxiliary/content.rb +5 -16
- data/lib/sevgi/graphics/document/base.rb +2 -2
- data/lib/sevgi/graphics/document.rb +15 -27
- data/lib/sevgi/graphics/element.rb +23 -8
- data/lib/sevgi/graphics/mixtures/call.rb +9 -24
- data/lib/sevgi/graphics/mixtures/core.rb +10 -9
- data/lib/sevgi/graphics/mixtures/duplicate.rb +4 -16
- data/lib/sevgi/graphics/mixtures/export.rb +2 -0
- data/lib/sevgi/graphics/mixtures/hatch.rb +5 -5
- data/lib/sevgi/graphics/mixtures/identify.rb +1 -1
- data/lib/sevgi/graphics/mixtures/inkscape.rb +23 -10
- data/lib/sevgi/graphics/mixtures/rdf.rb +1 -1
- data/lib/sevgi/graphics/mixtures/render.rb +11 -95
- data/lib/sevgi/graphics/mixtures/save.rb +2 -2
- data/lib/sevgi/graphics/mixtures/symbols.rb +2 -2
- data/lib/sevgi/graphics/mixtures/tile.rb +4 -4
- data/lib/sevgi/graphics/mixtures/transform.rb +22 -3
- data/lib/sevgi/graphics/mixtures/underscore.rb +2 -2
- data/lib/sevgi/graphics/mixtures/wrappers.rb +49 -0
- data/lib/sevgi/graphics/version.rb +1 -1
- data/lib/sevgi/graphics/xml.rb +4 -9
- data/lib/sevgi/graphics.rb +22 -17
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7965ed7aa4c5960cca6a4ed927258d13a5b5be7d9e327f4bf7da5b3480b66b1f
|
|
4
|
+
data.tar.gz: 88224f9179c50ceca0ae8e4ee7a92e5d8a6a0d2634ec3912356dc5e5d2d27053
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 063d2529fc5869ef25f27ed44b927d28ae3ba6c48aca2365c0be78dbda65b895eb065b0c61ca960e66f955e0a8dbea8cc7b581ddf9ad4ec437c727e0541b9e43
|
|
7
|
+
data.tar.gz: fa1a68f7962e62bd991462e87bbbae0778321076af509ca2ede6b179a5eb1c133d707c3450d46688645ddf37050df49c63df608236fc1cfb3bcc811d20ff25a5
|
data/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,101 @@
|
|
|
3
3
|
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and follows
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## 1.0.0 - 2026-09-16
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Added Hatch and Orb Showcase examples and displayed both arc lengths in the Arc example.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Matched documentation page widths to the homepage Showcase.
|
|
15
|
+
- Aligned Derender user guidance with UTF-8 normalization and safe subtree selection rules.
|
|
16
|
+
- Clarified the packaged skill's script invocation, subtree copying, and Derender selection and encoding rules.
|
|
17
|
+
- Allowed Canvas and Paper values as the first `SVG` argument when using the default document profile.
|
|
18
|
+
- Made element `dup` and `clone` own independent subtrees while retaining IDs and Ruby copy semantics.
|
|
19
|
+
- Made Segment equality compare length and direction; ordering still compares length only.
|
|
20
|
+
- Limited Tile cell, row, and column queries to the requested geometry and accelerated XML character validation.
|
|
21
|
+
- Simplified the first-run guide and aligned library constructor examples.
|
|
22
|
+
- Updated the packaged skill for canvas origins, alignment, Paper lookup, curved geometry, and export constraints.
|
|
23
|
+
- Revised DSL catalog examples and packaged Sevgi skill guidance to use statement-style calls and show inspection results.
|
|
24
|
+
- Refined Showcase example tabs, made Ruby and XML views fill their panels, and hid executable preambles from Ruby views.
|
|
25
|
+
- Documented non-rendering element data and clarified the related DSL catalog examples.
|
|
26
|
+
- Refined Showcase heading spacing, page-title colors, and DSL catalog cards across light and dark themes.
|
|
27
|
+
- Removed an obsolete ruler example and unused site code.
|
|
28
|
+
- Kept the Squared and Copperplate guidesheet examples together at the end of the Showcase.
|
|
29
|
+
- Updated the packaged skill to avoid duplicate semantic shapes and unnecessary paint passes.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Kept horizontal scrollbars visible in Showcase example code panels.
|
|
34
|
+
- Read Derender input in its declared XML encoding and normalized generated source, XML declarations, and markup to UTF-8.
|
|
35
|
+
- Preserved open-path endpoints and derived edges independently of display precision.
|
|
36
|
+
- Kept near-axis and near-parallel line equations and intersections independent of angle and slope rounding.
|
|
37
|
+
- Corrected the Showcase social image dimensions.
|
|
38
|
+
- Added SVG namespaces to standalone drawing examples and regenerated their outputs.
|
|
39
|
+
- Made failed Sevgi Rake tasks stop dependent tasks and captured required-library failures in executor results.
|
|
40
|
+
- Removed executor signal-handler replacement so the host and shell retain SIGINT ownership.
|
|
41
|
+
- Escaped late export CSS as XML text and inserted it only at a supported final root closing tag.
|
|
42
|
+
- Preserved Derender processing instructions and rejected custom entity references and unsupported trailing document nodes before inclusion.
|
|
43
|
+
- Included both box origins in `Align`, unified Tile index validation, and isolated custom numeric attribute snapshots.
|
|
44
|
+
- Prevented avoidable overflow and underflow in point distances.
|
|
45
|
+
- Corrected browser source-panel expectations and added executable documentation, YARD, and packaged-skill checks.
|
|
46
|
+
- Rendered inline code in DSL catalog summaries and kept linked task themes visible below the sticky header.
|
|
47
|
+
- Restored Showcase tab icons with embedded Font Awesome SVGs.
|
|
48
|
+
|
|
49
|
+
## 0.100.0 - 2026-09-09
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- Added `ArcTo` and `ArcBy` for SVG-native elliptical arc paths.
|
|
54
|
+
- Added Geometry `Arc`, `Ellipse`, and `Circle` with bounds, length, predicates, affine transformations, and
|
|
55
|
+
linear/quadratic intersections. Closed ellipses and circles support sweeps and hatching.
|
|
56
|
+
- Added Geometry `Point.midpoint`, `Element#closed?`, lined `vertices`, open-path traversal, and `Rect#center`.
|
|
57
|
+
- Added Geometry aggregate bounding boxes through `Operation.box`, point collinearity through `Point.collinear?`, and
|
|
58
|
+
polygon `simple?`, `convex?`, and `concave?` predicates.
|
|
59
|
+
- Added Protractor and Arc examples and documented the drawing and calculation APIs.
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
|
|
63
|
+
- Removed redundant parent scans while duplicating element subtrees.
|
|
64
|
+
- Reused source bounds across Tile cells to reduce repeated calculations and object allocations.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- Preserved Showcase card HTML through Markdown rendering so all examples and their tabs remain visible.
|
|
69
|
+
- Preserved XML declarations, pre-root comments, CDATA sections, inline comments, and namespace-only leaves in
|
|
70
|
+
Derender output and evaluation.
|
|
71
|
+
- Limited PDF stamp recoloring to replaced text and carried graphics state across a page's content streams.
|
|
72
|
+
- Corrected decimal interval fitting and made Grid point queries honor the active numeric precision.
|
|
73
|
+
- Prevented implicit shell interpretation of single executable arguments in `F.sh` and `F.sh!`, including paths with spaces or shell metacharacters.
|
|
74
|
+
- Preserved inherited namespaces on detached Derender nodes in generated source and direct evaluation, including imports through `EvaluateChildren` and `IncludeChildren`.
|
|
75
|
+
- Prevented sweep and Hatch lines from crossing the exterior between two tangent vertices of a concave polygon.
|
|
76
|
+
- Aligned Tile cells with their declared bounds for circles, rotated shapes, and polygons whose first vertex differs from their bounding-box origin.
|
|
77
|
+
- Limited `Load` chains to 128 active sources with `Executor::LoadDepthError`, preserving cycle detection and nested load diagnostics.
|
|
78
|
+
- Rejected nil and false aggregate-box inputs with the documented geometry error.
|
|
79
|
+
- Restored precision-aware rejection of zero-length Triangle sides.
|
|
80
|
+
- Made point-set collinearity independent of input order and removed eager polygon edge-pair storage.
|
|
81
|
+
- Applied the collinearity tolerance to every three-point subset, removing coordinate-axis-dependent baseline selection.
|
|
82
|
+
|
|
83
|
+
## 0.99.0 - 2026-08-09
|
|
84
|
+
|
|
85
|
+
### Added
|
|
86
|
+
|
|
87
|
+
- Added `SVG.Module` for building anonymous callable drawing modules with their contract installed before definition.
|
|
88
|
+
- Added responsive Mermaid diagrams for the Derender round trip and SVG validation lifecycle.
|
|
89
|
+
|
|
90
|
+
### Changed
|
|
91
|
+
|
|
92
|
+
- Reorganized the documentation around getting started, usage, composition, documents, layout, output, and examples.
|
|
93
|
+
- Documented how the `igves` and `igsev` command names reflect their conversion directions.
|
|
94
|
+
- Normalized DSL catalog provider labels to lowercase and simplified polyfill-backed editor entries to `inkscape`.
|
|
95
|
+
|
|
96
|
+
### Fixed
|
|
97
|
+
|
|
98
|
+
- Allowed A5-on-A4 pdfcpu conversion to replace an existing output file.
|
|
99
|
+
- Pre-rendered Mermaid diagrams as inline SVG instead of parsing them in site visitors' browsers.
|
|
100
|
+
- Removed duplicate provider and context labels from DSL catalog entries.
|
|
7
101
|
|
|
8
102
|
## 0.98.2 - 2026-07-25
|
|
9
103
|
|
|
@@ -212,7 +306,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
212
306
|
|
|
213
307
|
### Changed
|
|
214
308
|
|
|
215
|
-
- Added the
|
|
309
|
+
- Added the checkerboard example to the documentation showcase flow.
|
|
216
310
|
- Consolidated Victor Book attribution for adapted showcase examples into a single documentation note.
|
|
217
311
|
|
|
218
312
|
### Fixed
|
data/README.md
CHANGED
|
@@ -18,9 +18,12 @@ require "sevgi/graphics"
|
|
|
18
18
|
|
|
19
19
|
```ruby
|
|
20
20
|
doc = Sevgi::Graphics.SVG(:minimal) { rect width: 3, height: 5 }
|
|
21
|
-
doc.
|
|
21
|
+
puts doc.Render
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
This focused gem exposes `Sevgi::Graphics.SVG` and lowercase component constructors. Install the umbrella `sevgi` gem
|
|
25
|
+
when you need the global `SVG` facade or the `.sevgi` script runner.
|
|
26
|
+
|
|
24
27
|
## Ruby compatibility
|
|
25
28
|
|
|
26
29
|
Requires Ruby 3.4.0 or newer. CI verifies the current Ruby 3.4 release and the development Ruby from `.ruby-version`.
|
|
@@ -60,6 +60,9 @@ module Sevgi
|
|
|
60
60
|
# Owned mutable snapshots for values entering an attribute store.
|
|
61
61
|
# @api private
|
|
62
62
|
module Snapshot
|
|
63
|
+
SCALARS = [::NilClass, ::TrueClass, ::FalseClass, ::Symbol, ::Integer, ::Float, ::Rational, ::Complex].freeze
|
|
64
|
+
private_constant :SCALARS
|
|
65
|
+
|
|
63
66
|
class << self
|
|
64
67
|
def capture(value, normalize_keys: false, seen: {}.compare_by_identity)
|
|
65
68
|
case value
|
|
@@ -90,12 +93,7 @@ module Sevgi
|
|
|
90
93
|
|
|
91
94
|
def capture_value(value)
|
|
92
95
|
text = XML.text(value, context: "XML attribute value")
|
|
93
|
-
|
|
94
|
-
when ::Numeric, ::Symbol, ::NilClass, ::TrueClass, ::FalseClass
|
|
95
|
-
value
|
|
96
|
-
else
|
|
97
|
-
text
|
|
98
|
-
end
|
|
96
|
+
SCALARS.include?(value.class) ? value : text
|
|
99
97
|
end
|
|
100
98
|
|
|
101
99
|
def nested(value, seen)
|
|
@@ -172,7 +170,7 @@ module Sevgi
|
|
|
172
170
|
# @api private
|
|
173
171
|
class Store
|
|
174
172
|
# Creates an attribute store from recursively owned snapshots. Mutable non-container leaves are stringified
|
|
175
|
-
# once
|
|
173
|
+
# once. Later caller mutation cannot change the store.
|
|
176
174
|
# @param attributes [Hash] initial attributes
|
|
177
175
|
# @return [void]
|
|
178
176
|
# @raise [Sevgi::ArgumentError] when input is not a Hash or a name/value is invalid, cyclic, colliding, or cannot
|
|
@@ -205,7 +203,7 @@ module Sevgi
|
|
|
205
203
|
|
|
206
204
|
# Assigns a recursively owned attribute snapshot. Mutable non-container leaves are stringified once.
|
|
207
205
|
# @param key [String, Symbol] attribute key
|
|
208
|
-
# @param value [Object, nil] attribute value
|
|
206
|
+
# @param value [Object, nil] attribute value. Nil is ignored
|
|
209
207
|
# @return [Object, nil] stored snapshot or nil
|
|
210
208
|
# @raise [Sevgi::ArgumentError] when update syntax receives incompatible values
|
|
211
209
|
# @raise [Sevgi::ArgumentError] when update syntax receives an unsupported value type
|
|
@@ -354,7 +352,7 @@ module Sevgi
|
|
|
354
352
|
|
|
355
353
|
# Assigns or updates a recursively owned attribute value.
|
|
356
354
|
# @param key [String, Symbol] attribute key, optionally ending in {UPDATE_SUFFIX}
|
|
357
|
-
# @param value [Object, nil] attribute value
|
|
355
|
+
# @param value [Object, nil] attribute value. Nil is ignored
|
|
358
356
|
# @return [Object, nil] recursively owned resulting value or nil when absent
|
|
359
357
|
# @raise [Sevgi::ArgumentError] when the name or value is invalid, or an existing update uses incompatible or
|
|
360
358
|
# unsupported value families
|
|
@@ -392,7 +390,7 @@ module Sevgi
|
|
|
392
390
|
def keys = @store.list.freeze
|
|
393
391
|
|
|
394
392
|
# Atomically assigns or updates recursively owned attributes.
|
|
395
|
-
# @param attributes [Hash] attributes and non-rendering metadata
|
|
393
|
+
# @param attributes [Hash] attributes and non-rendering metadata. Names can end in {UPDATE_SUFFIX}
|
|
396
394
|
# @return [Sevgi::Graphics::Attributes] self
|
|
397
395
|
# @raise [Sevgi::ArgumentError] when input is not a Hash, names collide, a name or value is invalid, or an existing
|
|
398
396
|
# update uses incompatible or unsupported value families
|
|
@@ -113,12 +113,12 @@ module Sevgi
|
|
|
113
113
|
|
|
114
114
|
# @overload attributes(origin = Undefined)
|
|
115
115
|
# Returns SVG root viewport attributes.
|
|
116
|
-
# Omission uses the negative left and top margins
|
|
116
|
+
# Omission uses the negative left and top margins. Nil uses zero for both coordinates.
|
|
117
117
|
# @example Compare margin-aware and zero-origin viewBoxes
|
|
118
118
|
# canvas = Sevgi::Graphics::Canvas.call(width: 80, height: 50, margins: [5, 10])
|
|
119
119
|
# canvas.attributes[:viewBox] # => "-10 -5 80 50"
|
|
120
120
|
# canvas.attributes(nil)[:viewBox] # => "0 0 80 50"
|
|
121
|
-
# @param origin [Numeric, Array<Numeric>, nil, Sevgi::Undefined] viewBox origin
|
|
121
|
+
# @param origin [Numeric, Array<Numeric>, nil, Sevgi::Undefined] viewBox origin. A scalar sets both coordinates
|
|
122
122
|
# @return [Hash{Symbol => String}] SVG viewport and viewBox attributes
|
|
123
123
|
# @raise [Sevgi::ArgumentError] when origin is invalid
|
|
124
124
|
def attributes(...) = {**viewport, viewBox: viewbox(...)}
|
|
@@ -142,8 +142,8 @@ module Sevgi
|
|
|
142
142
|
def viewport = {width: "#{width}#{unit}", height: "#{height}#{unit}"}
|
|
143
143
|
|
|
144
144
|
# Returns the SVG viewBox string.
|
|
145
|
-
# Omission uses the negative left and top margins
|
|
146
|
-
# @param origin [Numeric, Array<Numeric>, nil, Sevgi::Undefined] viewBox origin
|
|
145
|
+
# Omission uses the negative left and top margins. Nil uses zero for both coordinates.
|
|
146
|
+
# @param origin [Numeric, Array<Numeric>, nil, Sevgi::Undefined] viewBox origin. A scalar sets both coordinates
|
|
147
147
|
# @return [String]
|
|
148
148
|
# @raise [Sevgi::ArgumentError] when origin is invalid
|
|
149
149
|
def viewbox(origin = Undefined) = prettify(*originate(origin), width, height).join(" ")
|
|
@@ -30,7 +30,7 @@ module Sevgi
|
|
|
30
30
|
# Sevgi::Graphics.SVG(:minimal) { text Emphasis["important & safe"] }.Render
|
|
31
31
|
#
|
|
32
32
|
# # The renderer ignores Emphasis#render's return value. Custom content must escape inserted data itself.
|
|
33
|
-
# @see https://sevgi.roktas.dev/
|
|
33
|
+
# @see https://sevgi.roktas.dev/documents/#content-safety Content safety guide
|
|
34
34
|
class Content
|
|
35
35
|
private_class_method :new
|
|
36
36
|
|
|
@@ -106,7 +106,7 @@ module Sevgi
|
|
|
106
106
|
# @return [Object] wrapped content snapshot
|
|
107
107
|
def content = Snapshot.copy(@content)
|
|
108
108
|
|
|
109
|
-
# Creates immutable content from a deep payload snapshot. Strings and containers are copied recursively
|
|
109
|
+
# Creates immutable content from a deep payload snapshot. Strings and containers are copied recursively. Mutable
|
|
110
110
|
# non-container objects are stringified once during construction. The caller's objects are never retained.
|
|
111
111
|
# @param content [Object] wrapped content
|
|
112
112
|
# @return [void]
|
|
@@ -118,17 +118,6 @@ module Sevgi
|
|
|
118
118
|
XML.validate(@content)
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
-
# Copies content payload ownership for duplicated element trees.
|
|
122
|
-
# @param original [Sevgi::Graphics::Content] source content
|
|
123
|
-
# @return [void]
|
|
124
|
-
# @api private
|
|
125
|
-
def initialize_copy(original)
|
|
126
|
-
@content = Snapshot.capture(original.content)
|
|
127
|
-
super
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
private :initialize_copy
|
|
131
|
-
|
|
132
121
|
# Appends this content's serialized XML lines to rendering output.
|
|
133
122
|
# The output collaborator responds to `append(depth, *lines)`, where `depth` is an Integer or nil and every line
|
|
134
123
|
# is a String containing valid serialized XML text. The rendering engine ignores both `append` and `render` return
|
|
@@ -185,7 +174,7 @@ module Sevgi
|
|
|
185
174
|
def self.verbatim(...) = Verbatim.send(:new, ...)
|
|
186
175
|
|
|
187
176
|
# CDATA section content backed by an immutable payload snapshot. Mutable leaf objects are stringified during
|
|
188
|
-
# construction
|
|
177
|
+
# construction. Embedded terminators are split during rendering.
|
|
189
178
|
# @see Content.cdata
|
|
190
179
|
class CData < Content
|
|
191
180
|
# Renders CDATA content.
|
|
@@ -206,7 +195,7 @@ module Sevgi
|
|
|
206
195
|
def safe(value) = XML.cdata(value)
|
|
207
196
|
end
|
|
208
197
|
|
|
209
|
-
# CSS content rendered inside a CDATA section. Rules are captured recursively during construction
|
|
198
|
+
# CSS content rendered inside a CDATA section. Rules are captured recursively during construction. Mutable
|
|
210
199
|
# selectors, property names, and values are stringified once, and embedded CDATA terminators are split safely.
|
|
211
200
|
# @see Content.css
|
|
212
201
|
class CSS < Content
|
|
@@ -281,7 +270,7 @@ module Sevgi
|
|
|
281
270
|
end
|
|
282
271
|
|
|
283
272
|
# Verbatim content backed by an immutable payload snapshot. Mutable leaf objects are stringified during
|
|
284
|
-
# construction. Verbatim content bypasses XML escaping
|
|
273
|
+
# construction. Verbatim content bypasses XML escaping. Validation guarantees encoding and legal XML 1.0 code
|
|
285
274
|
# points, not well-formed markup supplied by the caller.
|
|
286
275
|
# @see Content.verbatim
|
|
287
276
|
class Verbatim < Content
|
|
@@ -4,8 +4,8 @@ module Sevgi
|
|
|
4
4
|
module Graphics
|
|
5
5
|
module Document
|
|
6
6
|
# Abstract common document layer with the profile-independent DSL mixture set. It is not registered as a
|
|
7
|
-
# selectable profile. Advanced extensions
|
|
8
|
-
#
|
|
7
|
+
# selectable profile. Advanced extensions can target this class through {Sevgi::Graphics::Mixtures.mixin}. This
|
|
8
|
+
# changes every descendant profile process-wide. Subclass it first to keep an extension scoped.
|
|
9
9
|
class Base < Proto
|
|
10
10
|
document nil, register: false
|
|
11
11
|
|
|
@@ -5,7 +5,7 @@ module Sevgi
|
|
|
5
5
|
# SVG document profile factory and process-global named-profile registry.
|
|
6
6
|
#
|
|
7
7
|
# A profile owns SVG root attributes and optional preamble lines, but not canvas size. Built-in and named profiles
|
|
8
|
-
# can be passed to {Sevgi::Graphics.SVG}
|
|
8
|
+
# can be passed to {Sevgi::Graphics.SVG}. An anonymous profile class is useful when library code needs one-off
|
|
9
9
|
# metadata without adding a global name.
|
|
10
10
|
#
|
|
11
11
|
# | Profile | Preamble | Root metadata | Additional DSL |
|
|
@@ -13,20 +13,23 @@ module Sevgi
|
|
|
13
13
|
# | `:minimal` | none | none | common document DSL |
|
|
14
14
|
# | `:default` | XML declaration | SVG namespace | common document DSL |
|
|
15
15
|
# | `:html` | none | SVG namespace | common document DSL |
|
|
16
|
-
# | `:inkscape` | XML declaration | SVG and editor namespaces
|
|
16
|
+
# | `:inkscape` | XML declaration | SVG and editor namespaces with crisp edges | `Draw`, `Hatch`, and editor/RDF helpers |
|
|
17
17
|
#
|
|
18
18
|
# The Inkscape root adds Sevgi, Inkscape, and Sodipodi namespaces plus `shape-rendering="crispEdges"`. Every
|
|
19
|
-
# selectable profile has the same validation and lint lifecycle
|
|
19
|
+
# selectable profile has the same validation and lint lifecycle. `:minimal` changes serialization metadata, not
|
|
20
20
|
# checking policy. {Base} is the public common extension layer rather than a selectable profile. {Minimal} and
|
|
21
21
|
# {Default} are sibling concrete profiles: Minimal contributes no metadata and is not the semantic base of the other
|
|
22
|
-
# profiles. Targeting Base through {Sevgi::Graphics::Mixtures.mixin} changes every descendant profile process-wide
|
|
23
|
-
#
|
|
22
|
+
# profiles. Targeting Base through {Sevgi::Graphics::Mixtures.mixin} changes every descendant profile process-wide.
|
|
23
|
+
# Subclass Base first to keep an extension scoped.
|
|
24
24
|
#
|
|
25
|
-
# @see https://sevgi.roktas.dev/
|
|
25
|
+
# @see https://sevgi.roktas.dev/documents/#profiles Document profiles guide
|
|
26
26
|
module Document
|
|
27
27
|
# Defensive copy helper for profile metadata snapshots.
|
|
28
28
|
# @api private
|
|
29
29
|
module Snapshot
|
|
30
|
+
SCALARS = [::NilClass, ::TrueClass, ::FalseClass, ::Symbol, ::Integer, ::Float, ::Rational, ::Complex].freeze
|
|
31
|
+
private_constant :SCALARS
|
|
32
|
+
|
|
30
33
|
class << self
|
|
31
34
|
# Captures recursively immutable profile metadata. Mutable non-container values are stringified once.
|
|
32
35
|
# @param value [Object] value to capture
|
|
@@ -81,19 +84,8 @@ module Sevgi
|
|
|
81
84
|
end
|
|
82
85
|
|
|
83
86
|
def capture_value(value)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
XML.text(value, context: "Document profile metadata").freeze
|
|
87
|
-
when ::Numeric, ::Symbol, ::NilClass, ::TrueClass, ::FalseClass
|
|
88
|
-
XML.text(value, context: "Document profile metadata")
|
|
89
|
-
value
|
|
90
|
-
else
|
|
91
|
-
stringify(value).freeze
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def stringify(value)
|
|
96
|
-
XML.text(value, context: "Document profile metadata")
|
|
87
|
+
text = XML.text(value, context: "Document profile metadata")
|
|
88
|
+
SCALARS.include?(value.class) ? value : text.freeze
|
|
97
89
|
end
|
|
98
90
|
end
|
|
99
91
|
end
|
|
@@ -194,7 +186,7 @@ module Sevgi
|
|
|
194
186
|
# A name without metadata performs lookup. A name plus either metadata
|
|
195
187
|
# keyword defines or compatibly reuses a named profile. Omitting the name
|
|
196
188
|
# creates an anonymous class and leaves the registry unchanged. Named
|
|
197
|
-
# profiles are process-global
|
|
189
|
+
# profiles are process-global. Use them for shared vocabulary rather than
|
|
198
190
|
# per-call configuration.
|
|
199
191
|
# Profile metadata is captured before class or thread-atomic registry mutation. Mutable non-container attribute
|
|
200
192
|
# values are stringified once, attribute names and nested Hash keys are normalized, and nil attributes are omitted
|
|
@@ -221,7 +213,7 @@ module Sevgi
|
|
|
221
213
|
overwrite!(overwrite)
|
|
222
214
|
return anonymous(attributes:, preambles:) if name == Undefined
|
|
223
215
|
|
|
224
|
-
return
|
|
216
|
+
return fetch(name) if preambles == Undefined && attributes == Undefined
|
|
225
217
|
|
|
226
218
|
name = Name.normalize!(name)
|
|
227
219
|
current = reuse(name, attributes:, preambles:, overwrite:)
|
|
@@ -240,10 +232,6 @@ module Sevgi
|
|
|
240
232
|
Class.new(Base) { document(Undefined, preambles:, attributes:, register: false) }
|
|
241
233
|
end
|
|
242
234
|
|
|
243
|
-
def lookup(name)
|
|
244
|
-
fetch(name)
|
|
245
|
-
end
|
|
246
|
-
|
|
247
235
|
def defaults(attributes:, preambles:)
|
|
248
236
|
[attributes == Undefined ? {} : attributes, preambles == Undefined ? nil : preambles]
|
|
249
237
|
end
|
|
@@ -324,7 +312,7 @@ module Sevgi
|
|
|
324
312
|
private_constant :Registry
|
|
325
313
|
|
|
326
314
|
# Immutable, read-only document profile metadata exposed by document classes. Process-global lookup and registration
|
|
327
|
-
# are thread-atomic. Metadata containers and strings are captured recursively
|
|
315
|
+
# are thread-atomic. Metadata containers and strings are captured recursively. Other mutable attribute values are
|
|
328
316
|
# stringified once during construction. Attribute names and nested Hash keys are normalized to Symbols, nil values
|
|
329
317
|
# are omitted, and update-suffix intent is retained for inheritance.
|
|
330
318
|
# Returned attribute and preamble collections are caller-owned snapshots,
|
|
@@ -336,7 +324,7 @@ module Sevgi
|
|
|
336
324
|
|
|
337
325
|
# Creates profile metadata.
|
|
338
326
|
# @param name [Object, nil] profile name
|
|
339
|
-
# @param attributes [Hash, nil] default root attributes
|
|
327
|
+
# @param attributes [Hash, nil] default root attributes. Nil means an empty Hash
|
|
340
328
|
# @param preambles [Array<String>, nil] preamble lines
|
|
341
329
|
# @return [void]
|
|
342
330
|
# @raise [Sevgi::ArgumentError] when name or metadata is invalid XML, cyclic, or cannot be stringified
|
|
@@ -6,16 +6,18 @@ module Sevgi
|
|
|
6
6
|
#
|
|
7
7
|
# @!method self.valid?(name)
|
|
8
8
|
# Reports whether a candidate can dispatch as an SVG element name.
|
|
9
|
-
# With Standard loaded, the name must be known
|
|
9
|
+
# With Standard loaded, the name must be known. Standalone Graphics accepts any valid XML name.
|
|
10
10
|
# @param name [Object] candidate element name
|
|
11
11
|
# @return [Boolean]
|
|
12
12
|
#
|
|
13
13
|
# Dynamic SVG element methods accept text, content objects, and any number of attribute Hashes in one call. Hashes
|
|
14
|
-
# are applied from left to right
|
|
14
|
+
# are applied from left to right. Later values replace earlier values unless their names use the `+` update suffix.
|
|
15
|
+
# Ordinary `dup` and `clone` create independent subtrees, preserve IDs, and detach non-root copies from their parent.
|
|
16
|
+
# Document copies remain roots. Use `Duplicate` for DSL-specific ID remapping and attachment.
|
|
15
17
|
class Element
|
|
16
18
|
# Builds an element node.
|
|
17
19
|
# @param name [Symbol, String] SVG element name
|
|
18
|
-
# @param arguments [Array<Hash, String, Sevgi::Graphics::Content>] ordered content and attribute channels
|
|
20
|
+
# @param arguments [Array<Hash, String, Sevgi::Graphics::Content>] ordered content and attribute channels. Every
|
|
19
21
|
# Hash is applied through {Sevgi::Graphics::Attributes} from left to right
|
|
20
22
|
# @param parent [Sevgi::Graphics::Element] parent element
|
|
21
23
|
# @yield evaluates the drawing DSL in the new element
|
|
@@ -178,9 +180,25 @@ module Sevgi
|
|
|
178
180
|
instance_exec(&block) if block
|
|
179
181
|
end
|
|
180
182
|
|
|
183
|
+
# Copies owned storage and reconnects child copies without attaching to the source tree.
|
|
184
|
+
# @param original [Sevgi::Graphics::Element] source node
|
|
185
|
+
# @return [void]
|
|
186
|
+
# @api private
|
|
187
|
+
def initialize_copy(original)
|
|
188
|
+
super
|
|
189
|
+
@parent = Element.root?(original) ? RootParent : DetachedParent
|
|
190
|
+
@attributes = original.attributes.dup
|
|
191
|
+
@contents = original.contents.map(&:dup)
|
|
192
|
+
@children = original.children.map do |child|
|
|
193
|
+
child.dup.tap { it.instance_variable_set(:@parent, self) }
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
private :initialize_copy
|
|
198
|
+
|
|
181
199
|
# Dispatches SVG element DSL calls and caches valid element methods.
|
|
182
200
|
# @param name [Symbol] missing method name
|
|
183
|
-
# @param arguments [Array<Hash, String, Sevgi::Graphics::Content>] ordered content and attribute channels
|
|
201
|
+
# @param arguments [Array<Hash, String, Sevgi::Graphics::Content>] ordered content and attribute channels. Later
|
|
184
202
|
# Hashes replace or update attributes assigned by earlier Hashes
|
|
185
203
|
# @yield evaluates the drawing DSL in the dispatched child element
|
|
186
204
|
# @yieldreturn [Object] ignored block result
|
|
@@ -232,7 +250,7 @@ module Sevgi
|
|
|
232
250
|
|
|
233
251
|
# Parses element DSL arguments.
|
|
234
252
|
# @param name [Symbol] SVG element name
|
|
235
|
-
# @param args [Array<Object>] positional DSL arguments
|
|
253
|
+
# @param args [Array<Object>] positional DSL arguments. Hashes are imported from left to right
|
|
236
254
|
# @return [Hash] parsed :attributes and :contents
|
|
237
255
|
# @raise [Sevgi::ArgumentError] when an argument is not a Hash, String, or Content
|
|
238
256
|
def parse(name, *args)
|
|
@@ -257,9 +275,6 @@ module Sevgi
|
|
|
257
275
|
|
|
258
276
|
private_constant :Dispatch
|
|
259
277
|
|
|
260
|
-
protected
|
|
261
|
-
|
|
262
|
-
attr_writer :attributes, :children, :contents, :parent
|
|
263
278
|
end
|
|
264
279
|
end
|
|
265
280
|
end
|
|
@@ -4,10 +4,10 @@ module Sevgi
|
|
|
4
4
|
module Graphics
|
|
5
5
|
# Callable drawing module support.
|
|
6
6
|
# Extend a plain Ruby module with this API to make its public instance methods callable drawing steps. Name the
|
|
7
|
-
# method `call` when the module has a single drawing step
|
|
7
|
+
# method `call` when the module has a single drawing step. Use descriptive method names for multiple steps.
|
|
8
8
|
# Base blocks add argument-independent shared SVG content once per invocation before the public drawing methods.
|
|
9
|
-
# Invocation does not change the configured module, so it
|
|
10
|
-
# duplicate or clone owns an independent configuration snapshot
|
|
9
|
+
# Invocation does not change the configured module, so it can be frozen after its drawing steps are defined. A
|
|
10
|
+
# duplicate or clone owns an independent configuration snapshot. Freezing a callable module prevents later base
|
|
11
11
|
# registration while leaving invocation available.
|
|
12
12
|
# @example Define and call a drawing module
|
|
13
13
|
# Widget = Module.new do
|
|
@@ -73,8 +73,9 @@ module Sevgi
|
|
|
73
73
|
|
|
74
74
|
# Registers argument-independent shared drawing steps. Every invocation runs inherited base blocks parent-first,
|
|
75
75
|
# then locally registered base blocks in registration order, before the module's public drawing methods. The block
|
|
76
|
-
# runs once
|
|
77
|
-
#
|
|
76
|
+
# runs once on the callable receiver, which delegates drawing words to the current SVG element. It receives no
|
|
77
|
+
# invocation arguments. Callable methods receive those arguments and the caller's block unchanged.
|
|
78
|
+
# @yield evaluates shared drawing steps on the callable receiver
|
|
78
79
|
# @yieldreturn [Object] ignored block result
|
|
79
80
|
# @return [nil]
|
|
80
81
|
# @raise [Sevgi::ArgumentError] when no block is given
|
|
@@ -133,7 +134,7 @@ module Sevgi
|
|
|
133
134
|
invoke(context, receiver, methods, ...)
|
|
134
135
|
end
|
|
135
136
|
|
|
136
|
-
# Returns the
|
|
137
|
+
# Returns the executable drawing methods for a callable module.
|
|
137
138
|
# @param mod [Module] module extended with {Sevgi::Graphics::Module}
|
|
138
139
|
# @return [Array<UnboundMethod>]
|
|
139
140
|
# @raise [Sevgi::ArgumentError] when mod is not a callable drawing module
|
|
@@ -173,26 +174,12 @@ module Sevgi
|
|
|
173
174
|
|
|
174
175
|
private
|
|
175
176
|
|
|
176
|
-
# Gives a
|
|
177
|
+
# Gives a copied callable module independent configuration containers.
|
|
177
178
|
# @param original [Module] source callable module
|
|
178
179
|
# @return [void]
|
|
179
180
|
# @api private
|
|
180
|
-
def
|
|
181
|
+
def initialize_copy(original)
|
|
181
182
|
super
|
|
182
|
-
copy_configuration(original)
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
# Gives a cloned callable module independent configuration containers.
|
|
186
|
-
# @param original [Module] source callable module
|
|
187
|
-
# @param freeze [Boolean] whether Ruby preserves the source frozen state
|
|
188
|
-
# @return [void]
|
|
189
|
-
# @api private
|
|
190
|
-
def initialize_clone(original, freeze: true)
|
|
191
|
-
super
|
|
192
|
-
copy_configuration(original)
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def copy_configuration(original)
|
|
196
183
|
@sevgi_bases = original.instance_variable_get(:@sevgi_bases).dup
|
|
197
184
|
@sevgi_callables = original.instance_variable_get(:@sevgi_callables).dup
|
|
198
185
|
@sevgi_configuration_owner = object_id
|
|
@@ -216,8 +203,6 @@ module Sevgi
|
|
|
216
203
|
own_configuration
|
|
217
204
|
@sevgi_callables << method
|
|
218
205
|
end
|
|
219
|
-
|
|
220
|
-
private :copy_configuration, :own_configuration
|
|
221
206
|
end
|
|
222
207
|
|
|
223
208
|
# Recursive callable drawing module support. Extend a module with this API when it and the modules defined beneath
|
|
@@ -94,7 +94,7 @@ module Sevgi
|
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
# Adds CSS classes without duplicating existing values.
|
|
97
|
-
# @param classes [Array<String, Symbol, Array>] class tokens
|
|
97
|
+
# @param classes [Array<String, Symbol, Array>] class tokens. Strings are split on whitespace
|
|
98
98
|
# @return [Sevgi::Graphics::Element] self
|
|
99
99
|
def Classify(*classes)
|
|
100
100
|
tap do
|
|
@@ -128,7 +128,7 @@ module Sevgi
|
|
|
128
128
|
|
|
129
129
|
# Builds a child element with an explicit tag name.
|
|
130
130
|
# @param tag [Symbol, String] SVG tag name
|
|
131
|
-
# @param contents [Array<Object>] text or content objects
|
|
131
|
+
# @param contents [Array<Object>] text or content objects. Non-content objects are stringified and XML-encoded
|
|
132
132
|
# @param attributes [Hash] SVG attributes
|
|
133
133
|
# @yield evaluates the drawing DSL in the new child element
|
|
134
134
|
# @yieldreturn [Object] ignored block result
|
|
@@ -176,7 +176,7 @@ module Sevgi
|
|
|
176
176
|
end
|
|
177
177
|
|
|
178
178
|
# Returns the topmost element in this tree.
|
|
179
|
-
# A detached subtree returns its detached topmost element
|
|
179
|
+
# A detached subtree returns its detached topmost element. Use {#Root?} to distinguish a document root.
|
|
180
180
|
# @return [Sevgi::Graphics::Element] document root or detached topmost element
|
|
181
181
|
def Root
|
|
182
182
|
element = self
|
|
@@ -283,7 +283,7 @@ module Sevgi
|
|
|
283
283
|
# Appends an element as a child.
|
|
284
284
|
# @param element [Sevgi::Graphics::Element] element to append
|
|
285
285
|
# @return [Sevgi::Graphics::Element] self
|
|
286
|
-
# @raise [Sevgi::ArgumentError] when the element has a different concrete class or
|
|
286
|
+
# @raise [Sevgi::ArgumentError] when the element has a different concrete class or creates a tree cycle
|
|
287
287
|
def <<(element)
|
|
288
288
|
Append(element)
|
|
289
289
|
end
|
|
@@ -296,7 +296,7 @@ module Sevgi
|
|
|
296
296
|
# @param parent [Sevgi::Graphics::Element] target parent
|
|
297
297
|
# @param front [Boolean] whether to prepend instead of append
|
|
298
298
|
# @return [void]
|
|
299
|
-
# @raise [Sevgi::ArgumentError] when an argument is incompatible, repeated, or
|
|
299
|
+
# @raise [Sevgi::ArgumentError] when an argument is incompatible, repeated, or creates a cycle
|
|
300
300
|
def self.batch(elements, parent, front:)
|
|
301
301
|
validate_batch(elements, parent)
|
|
302
302
|
|
|
@@ -307,11 +307,11 @@ module Sevgi
|
|
|
307
307
|
end
|
|
308
308
|
end
|
|
309
309
|
|
|
310
|
-
# Rejects target parents that
|
|
310
|
+
# Rejects target parents that create a cycle.
|
|
311
311
|
# @param element [Sevgi::Graphics::Element] element being moved
|
|
312
312
|
# @param parent [Sevgi::Graphics::Element, Object] target parent
|
|
313
313
|
# @return [void]
|
|
314
|
-
# @raise [Sevgi::ArgumentError] when the parent is incompatible or
|
|
314
|
+
# @raise [Sevgi::ArgumentError] when the parent is incompatible or creates a cycle
|
|
315
315
|
def self.validate(element, parent)
|
|
316
316
|
unless element.instance_of?(parent.class)
|
|
317
317
|
ArgumentError.("Element type does not match the new parent type: #{element.class}")
|
|
@@ -347,8 +347,9 @@ module Sevgi
|
|
|
347
347
|
# @param index [Integer] requested insertion index
|
|
348
348
|
# @return [Integer] normalized insertion index
|
|
349
349
|
def self.index_for(element, parent, index)
|
|
350
|
-
|
|
351
|
-
|
|
350
|
+
children = Element.send(:tree_children, parent)
|
|
351
|
+
same_parent = element.parent.equal?(parent) && children.include?(element)
|
|
352
|
+
index(index, children.size - (same_parent ? 1 : 0))
|
|
352
353
|
end
|
|
353
354
|
|
|
354
355
|
def self.validate_batch(elements, parent)
|