sevgi-graphics 0.98.2 → 1.1.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 +115 -1
- data/README.md +55 -1
- data/lib/sevgi/graphics/attribute.rb +15 -10
- data/lib/sevgi/graphics/auxiliary/canvas.rb +4 -4
- data/lib/sevgi/graphics/auxiliary/content.rb +5 -16
- data/lib/sevgi/graphics/auxiliary/paper.rb +52 -1
- data/lib/sevgi/graphics/{xml.rb → auxiliary/xml.rb} +4 -9
- data/lib/sevgi/graphics/auxiliary.rb +1 -2
- data/lib/sevgi/graphics/document/base.rb +3 -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 +19 -23
- data/lib/sevgi/graphics/mixtures/duplicate.rb +4 -16
- data/lib/sevgi/graphics/mixtures/export.rb +8 -2
- 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 +4 -4
- data/lib/sevgi/graphics/mixtures/style/declaration.rb +117 -0
- data/lib/sevgi/graphics/mixtures/style.rb +36 -0
- 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/mixtures.rb +1 -0
- data/lib/sevgi/graphics/version.rb +1 -1
- data/lib/sevgi/graphics.rb +23 -19
- metadata +21 -7
- data/lib/sevgi/graphics/auxiliary/path.rb +0 -44
- data/lib/sevgi/graphics/auxiliary/sizes.rb +0 -62
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9e205e418f5e60262a7e2cea0c4d32879942f7b238ea5fbf07bbbc01be5a9bbf
|
|
4
|
+
data.tar.gz: 889bb3fc5e72a27efba882624c3af23681a2811866d6c165f344edbb79173db7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 67bcec199b5f5a1e607a316da7dd618792e07df4b9db5bdd86104373ac1772324eb9f1b268e3ae1ce28db413bcefb68db4d531e82098ffa112c9da998de9e9dd
|
|
7
|
+
data.tar.gz: ad2787c76d899b961dc73da906dcfb57ed38f94fc2f317c8ce0c0692ebbdd181260b1e97ee02efa871350ba9239ba53341c755919a02dd1860b8d6431357c6fb
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,120 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 1.1.0 - 2026-10-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `Classes` returns an element's distinct CSS class names without changing the element; `Classify` shares its attribute normalization.
|
|
13
|
+
- Generic document-local Style declarations with `Target`, `Param`, `Group`, and `Profiles`, and optional prepared PDF export through the native Mainz CLI.
|
|
14
|
+
- Backend-independent `fallback: false` for export failures and `F.executable` for executable path lookup.
|
|
15
|
+
- `F.path` for path validation and expansion, and `F.output_path` for default output names and directory targets.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Moved Graphics XML validation into auxiliary helpers and kept built-in paper dimensions in `Paper`.
|
|
20
|
+
- Shared path validation between Graphics writers and Sundries exports while preserving output paths and error messages.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Stamp code placeholders in nested and shared PDF Forms, with cycle-safe resource traversal.
|
|
25
|
+
|
|
26
|
+
## 1.0.0 - 2026-09-16
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- Added Hatch and Orb Showcase examples and displayed both arc lengths in the Arc example.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- Matched documentation page widths to the homepage Showcase.
|
|
35
|
+
- Aligned Derender user guidance with UTF-8 normalization and safe subtree selection rules.
|
|
36
|
+
- Clarified the packaged skill's script invocation, subtree copying, and Derender selection and encoding rules.
|
|
37
|
+
- Allowed Canvas and Paper values as the first `SVG` argument when using the default document profile.
|
|
38
|
+
- Made element `dup` and `clone` own independent subtrees while retaining IDs and Ruby copy semantics.
|
|
39
|
+
- Made Segment equality compare length and direction; ordering still compares length only.
|
|
40
|
+
- Limited Tile cell, row, and column queries to the requested geometry and accelerated XML character validation.
|
|
41
|
+
- Simplified the first-run guide and aligned library constructor examples.
|
|
42
|
+
- Updated the packaged skill for canvas origins, alignment, Paper lookup, curved geometry, and export constraints.
|
|
43
|
+
- Revised DSL catalog examples and packaged Sevgi skill guidance to use statement-style calls and show inspection results.
|
|
44
|
+
- Refined Showcase example tabs, made Ruby and XML views fill their panels, and hid executable preambles from Ruby views.
|
|
45
|
+
- Documented non-rendering element data and clarified the related DSL catalog examples.
|
|
46
|
+
- Refined Showcase heading spacing, page-title colors, and DSL catalog cards across light and dark themes.
|
|
47
|
+
- Removed an obsolete ruler example and unused site code.
|
|
48
|
+
- Kept the Squared and Copperplate guidesheet examples together at the end of the Showcase.
|
|
49
|
+
- Updated the packaged skill to avoid duplicate semantic shapes and unnecessary paint passes.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- Kept horizontal scrollbars visible in Showcase example code panels.
|
|
54
|
+
- Read Derender input in its declared XML encoding and normalized generated source, XML declarations, and markup to UTF-8.
|
|
55
|
+
- Preserved open-path endpoints and derived edges independently of display precision.
|
|
56
|
+
- Kept near-axis and near-parallel line equations and intersections independent of angle and slope rounding.
|
|
57
|
+
- Corrected the Showcase social image dimensions.
|
|
58
|
+
- Added SVG namespaces to standalone drawing examples and regenerated their outputs.
|
|
59
|
+
- Made failed Sevgi Rake tasks stop dependent tasks and captured required-library failures in executor results.
|
|
60
|
+
- Removed executor signal-handler replacement so the host and shell retain SIGINT ownership.
|
|
61
|
+
- Escaped late export CSS as XML text and inserted it only at a supported final root closing tag.
|
|
62
|
+
- Preserved Derender processing instructions and rejected custom entity references and unsupported trailing document nodes before inclusion.
|
|
63
|
+
- Included both box origins in `Align`, unified Tile index validation, and isolated custom numeric attribute snapshots.
|
|
64
|
+
- Prevented avoidable overflow and underflow in point distances.
|
|
65
|
+
- Corrected browser source-panel expectations and added executable documentation, YARD, and packaged-skill checks.
|
|
66
|
+
- Rendered inline code in DSL catalog summaries and kept linked task themes visible below the sticky header.
|
|
67
|
+
- Restored Showcase tab icons with embedded Font Awesome SVGs.
|
|
68
|
+
|
|
69
|
+
## 0.100.0 - 2026-09-09
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
|
|
73
|
+
- Added `ArcTo` and `ArcBy` for SVG-native elliptical arc paths.
|
|
74
|
+
- Added Geometry `Arc`, `Ellipse`, and `Circle` with bounds, length, predicates, affine transformations, and
|
|
75
|
+
linear/quadratic intersections. Closed ellipses and circles support sweeps and hatching.
|
|
76
|
+
- Added Geometry `Point.midpoint`, `Element#closed?`, lined `vertices`, open-path traversal, and `Rect#center`.
|
|
77
|
+
- Added Geometry aggregate bounding boxes through `Operation.box`, point collinearity through `Point.collinear?`, and
|
|
78
|
+
polygon `simple?`, `convex?`, and `concave?` predicates.
|
|
79
|
+
- Added Protractor and Arc examples and documented the drawing and calculation APIs.
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
|
|
83
|
+
- Removed redundant parent scans while duplicating element subtrees.
|
|
84
|
+
- Reused source bounds across Tile cells to reduce repeated calculations and object allocations.
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
|
|
88
|
+
- Preserved Showcase card HTML through Markdown rendering so all examples and their tabs remain visible.
|
|
89
|
+
- Preserved XML declarations, pre-root comments, CDATA sections, inline comments, and namespace-only leaves in
|
|
90
|
+
Derender output and evaluation.
|
|
91
|
+
- Limited PDF stamp recoloring to replaced text and carried graphics state across a page's content streams.
|
|
92
|
+
- Corrected decimal interval fitting and made Grid point queries honor the active numeric precision.
|
|
93
|
+
- Prevented implicit shell interpretation of single executable arguments in `F.sh` and `F.sh!`, including paths with spaces or shell metacharacters.
|
|
94
|
+
- Preserved inherited namespaces on detached Derender nodes in generated source and direct evaluation, including imports through `EvaluateChildren` and `IncludeChildren`.
|
|
95
|
+
- Prevented sweep and Hatch lines from crossing the exterior between two tangent vertices of a concave polygon.
|
|
96
|
+
- Aligned Tile cells with their declared bounds for circles, rotated shapes, and polygons whose first vertex differs from their bounding-box origin.
|
|
97
|
+
- Limited `Load` chains to 128 active sources with `Executor::LoadDepthError`, preserving cycle detection and nested load diagnostics.
|
|
98
|
+
- Rejected nil and false aggregate-box inputs with the documented geometry error.
|
|
99
|
+
- Restored precision-aware rejection of zero-length Triangle sides.
|
|
100
|
+
- Made point-set collinearity independent of input order and removed eager polygon edge-pair storage.
|
|
101
|
+
- Applied the collinearity tolerance to every three-point subset, removing coordinate-axis-dependent baseline selection.
|
|
102
|
+
|
|
103
|
+
## 0.99.0 - 2026-08-09
|
|
104
|
+
|
|
105
|
+
### Added
|
|
106
|
+
|
|
107
|
+
- Added `SVG.Module` for building anonymous callable drawing modules with their contract installed before definition.
|
|
108
|
+
- Added responsive Mermaid diagrams for the Derender round trip and SVG validation lifecycle.
|
|
109
|
+
|
|
110
|
+
### Changed
|
|
111
|
+
|
|
112
|
+
- Reorganized the documentation around getting started, usage, composition, documents, layout, output, and examples.
|
|
113
|
+
- Documented how the `igves` and `igsev` command names reflect their conversion directions.
|
|
114
|
+
- Normalized DSL catalog provider labels to lowercase and simplified polyfill-backed editor entries to `inkscape`.
|
|
115
|
+
|
|
116
|
+
### Fixed
|
|
117
|
+
|
|
118
|
+
- Allowed A5-on-A4 pdfcpu conversion to replace an existing output file.
|
|
119
|
+
- Pre-rendered Mermaid diagrams as inline SVG instead of parsing them in site visitors' browsers.
|
|
120
|
+
- Removed duplicate provider and context labels from DSL catalog entries.
|
|
121
|
+
|
|
8
122
|
## 0.98.2 - 2026-07-25
|
|
9
123
|
|
|
10
124
|
### Fixed
|
|
@@ -212,7 +326,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
212
326
|
|
|
213
327
|
### Changed
|
|
214
328
|
|
|
215
|
-
- Added the
|
|
329
|
+
- Added the checkerboard example to the documentation showcase flow.
|
|
216
330
|
- Consolidated Victor Book attribution for adapted showcase examples into a single documentation note.
|
|
217
331
|
|
|
218
332
|
### Fixed
|
data/README.md
CHANGED
|
@@ -18,9 +18,63 @@ 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
|
+
|
|
27
|
+
## Editable PDF styles
|
|
28
|
+
|
|
29
|
+
`Style` attaches an immutable producer declaration to an SVG document.
|
|
30
|
+
It does not change the SVG or load a PDF backend.
|
|
31
|
+
The lowercase `style` method still creates an SVG CSS element.
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
doc = Sevgi::Graphics.SVG width: 20, height: 20 do
|
|
35
|
+
g id: "outline" do
|
|
36
|
+
rect width: 10, height: 10, fill: "none", stroke: "black"
|
|
37
|
+
end
|
|
38
|
+
Style do
|
|
39
|
+
Target "ink", selectors: ["#outline"]
|
|
40
|
+
Param "display", targets: ["ink"], property: "display",
|
|
41
|
+
schema: {type: "string", enum: %w[inline none]}
|
|
42
|
+
Param "opacity", targets: ["ink"], property: "stroke-opacity",
|
|
43
|
+
schema: {type: "number", minimum: 0, maximum: 1}
|
|
44
|
+
Group "Ink", parameters: ["opacity"]
|
|
45
|
+
Profiles "print", name: "Printer", parameters: ["opacity"], profiles: [
|
|
46
|
+
{id: "original", name: "Original", overrides: {}},
|
|
47
|
+
{id: "light", name: "Light", overrides: {opacity: 0.4}}
|
|
48
|
+
]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
doc.PDF "drawing.pdf"
|
|
53
|
+
doc.PDF "prepared.pdf", fallback: false
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`PDF` uses Mainz automatically when the document has a Style declaration.
|
|
57
|
+
If Mainz is unavailable or preparation fails, the default mode warns and exports an ordinary PDF.
|
|
58
|
+
`fallback: false` raises `Sevgi::Sundries::Mainz::Error` instead.
|
|
59
|
+
An unstyled PDF uses ordinary export without a Mainz warning.
|
|
60
|
+
PNG and SVG output retain their ordinary behavior.
|
|
61
|
+
|
|
62
|
+
`Target` selects whole SVG groups or direct `use` placements with `#id` or `#id > use.class` selectors.
|
|
63
|
+
Its optional `fill: true` allows eligible closed paths to acquire a fill.
|
|
64
|
+
`Param` binds one supported paint property to targets and supplies its value schema.
|
|
65
|
+
`Group` orders parameter references. `Profiles` records named sets of overrides.
|
|
66
|
+
Every target requires a `display` parameter.
|
|
67
|
+
`Param` accepts an optional `value:` to assign an override after Mainz captures original paint.
|
|
68
|
+
For an authored hidden target, `Target` accepts `display: "none"`.
|
|
69
|
+
Mainz owns selector resolution, supported properties, binding admission, and PDF baselines.
|
|
70
|
+
The [declaration contract](https://github.com/roktas/mainz/blob/v1.0.0/lib/doc/SPEC.md#71-svg-declarations) defines those fields.
|
|
71
|
+
|
|
72
|
+
`doc.Style` returns the declaration as deeply frozen JSON data with String keys.
|
|
73
|
+
`doc.Style declaration_hash` accepts the same fields without a builder block.
|
|
74
|
+
Applications supply their own parameter names, schemas, profiles, and publication requirements.
|
|
75
|
+
Separate documents have separate declarations. Document copies safely share an immutable declaration.
|
|
76
|
+
Invalid Ruby declarations raise `Sevgi::ArgumentError` before export.
|
|
77
|
+
|
|
24
78
|
## Ruby compatibility
|
|
25
79
|
|
|
26
80
|
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)
|
|
@@ -168,11 +166,18 @@ module Sevgi
|
|
|
168
166
|
XML.text(text, context: "XML attribute value")
|
|
169
167
|
end
|
|
170
168
|
|
|
169
|
+
# Returns distinct class names in rendered order from nested attribute values.
|
|
170
|
+
# @param values [Array<Object>] class values to combine
|
|
171
|
+
# @return [Array<String>] owned class-name snapshot
|
|
172
|
+
# @raise [Sevgi::ArgumentError] when a value is invalid, cyclic, or cannot be stringified as XML
|
|
173
|
+
# @api private
|
|
174
|
+
def self.classes(*values) = xml_text(values).split.uniq
|
|
175
|
+
|
|
171
176
|
# Mutable backing store for SVG attributes.
|
|
172
177
|
# @api private
|
|
173
178
|
class Store
|
|
174
179
|
# Creates an attribute store from recursively owned snapshots. Mutable non-container leaves are stringified
|
|
175
|
-
# once
|
|
180
|
+
# once. Later caller mutation cannot change the store.
|
|
176
181
|
# @param attributes [Hash] initial attributes
|
|
177
182
|
# @return [void]
|
|
178
183
|
# @raise [Sevgi::ArgumentError] when input is not a Hash or a name/value is invalid, cyclic, colliding, or cannot
|
|
@@ -205,7 +210,7 @@ module Sevgi
|
|
|
205
210
|
|
|
206
211
|
# Assigns a recursively owned attribute snapshot. Mutable non-container leaves are stringified once.
|
|
207
212
|
# @param key [String, Symbol] attribute key
|
|
208
|
-
# @param value [Object, nil] attribute value
|
|
213
|
+
# @param value [Object, nil] attribute value. Nil is ignored
|
|
209
214
|
# @return [Object, nil] stored snapshot or nil
|
|
210
215
|
# @raise [Sevgi::ArgumentError] when update syntax receives incompatible values
|
|
211
216
|
# @raise [Sevgi::ArgumentError] when update syntax receives an unsupported value type
|
|
@@ -354,7 +359,7 @@ module Sevgi
|
|
|
354
359
|
|
|
355
360
|
# Assigns or updates a recursively owned attribute value.
|
|
356
361
|
# @param key [String, Symbol] attribute key, optionally ending in {UPDATE_SUFFIX}
|
|
357
|
-
# @param value [Object, nil] attribute value
|
|
362
|
+
# @param value [Object, nil] attribute value. Nil is ignored
|
|
358
363
|
# @return [Object, nil] recursively owned resulting value or nil when absent
|
|
359
364
|
# @raise [Sevgi::ArgumentError] when the name or value is invalid, or an existing update uses incompatible or
|
|
360
365
|
# unsupported value families
|
|
@@ -392,7 +397,7 @@ module Sevgi
|
|
|
392
397
|
def keys = @store.list.freeze
|
|
393
398
|
|
|
394
399
|
# Atomically assigns or updates recursively owned attributes.
|
|
395
|
-
# @param attributes [Hash] attributes and non-rendering metadata
|
|
400
|
+
# @param attributes [Hash] attributes and non-rendering metadata. Names can end in {UPDATE_SUFFIX}
|
|
396
401
|
# @return [Sevgi::Graphics::Attributes] self
|
|
397
402
|
# @raise [Sevgi::ArgumentError] when input is not a Hash, names collide, a name or value is invalid, or an existing
|
|
398
403
|
# 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
|
|
@@ -180,7 +180,58 @@ module Sevgi
|
|
|
180
180
|
|
|
181
181
|
@reserved = methods.map(&:to_sym).freeze
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
{
|
|
184
|
+
a0: [841, 1189, "mm"],
|
|
185
|
+
a1: [594, 841, "mm"],
|
|
186
|
+
a2: [420, 594, "mm"],
|
|
187
|
+
a3: [297, 420, "mm"],
|
|
188
|
+
a4: [210, 297, "mm"],
|
|
189
|
+
a5: [148, 210, "mm"],
|
|
190
|
+
a6: [105, 148, "mm"],
|
|
191
|
+
a7: [74, 105, "mm"],
|
|
192
|
+
a8: [52, 74, "mm"],
|
|
193
|
+
a9: [37, 52, "mm"],
|
|
194
|
+
a10: [26, 37, "mm"],
|
|
195
|
+
|
|
196
|
+
b0: [1000, 1414, "mm"],
|
|
197
|
+
b1: [707, 1000, "mm"],
|
|
198
|
+
b2: [500, 707, "mm"],
|
|
199
|
+
b3: [353, 500, "mm"],
|
|
200
|
+
b4: [250, 353, "mm"],
|
|
201
|
+
b5: [176, 250, "mm"],
|
|
202
|
+
b6: [125, 176, "mm"],
|
|
203
|
+
b7: [88, 125, "mm"],
|
|
204
|
+
b8: [62, 88, "mm"],
|
|
205
|
+
b9: [44, 62, "mm"],
|
|
206
|
+
b10: [31, 44, "mm"],
|
|
207
|
+
|
|
208
|
+
c0: [917, 1297, "mm"],
|
|
209
|
+
c1: [648, 917, "mm"],
|
|
210
|
+
c2: [458, 648, "mm"],
|
|
211
|
+
c3: [324, 458, "mm"],
|
|
212
|
+
c4: [229, 324, "mm"],
|
|
213
|
+
c5: [162, 229, "mm"],
|
|
214
|
+
c6: [114, 162, "mm"],
|
|
215
|
+
c7: [81, 114, "mm"],
|
|
216
|
+
c8: [57, 81, "mm"],
|
|
217
|
+
c9: [40, 57, "mm"],
|
|
218
|
+
c10: [28, 40, "mm"],
|
|
219
|
+
|
|
220
|
+
business: [85, 55, "mm"],
|
|
221
|
+
large: [130, 210, "mm"],
|
|
222
|
+
passport: [88, 125, "mm"],
|
|
223
|
+
pocket: [90, 140, "mm"],
|
|
224
|
+
travelers: [110, 210, "mm"],
|
|
225
|
+
us: [216, 279, "mm"],
|
|
226
|
+
xlarge: [190, 250, "mm"],
|
|
227
|
+
|
|
228
|
+
icon16: [16, 16, "px"],
|
|
229
|
+
icon32: [32, 32, "px"],
|
|
230
|
+
icon64: [64, 64, "px"],
|
|
231
|
+
icon128: [128, 128, "px"],
|
|
232
|
+
icon256: [256, 256, "px"],
|
|
233
|
+
icon512: [512, 512, "px"]
|
|
234
|
+
}.each { |name, (width, height, unit)| define(name, width:, height:, unit:) }
|
|
184
235
|
|
|
185
236
|
# Returns the default paper profile.
|
|
186
237
|
# @return [Sevgi::Graphics::Paper]
|
|
@@ -11,8 +11,9 @@ module Sevgi
|
|
|
11
11
|
NCNAME_CHAR = "#{NCNAME_START}\\-.0-9\u{B7}\u{300}-\u{36F}\u{203F}-\u{2040}".freeze
|
|
12
12
|
NCNAME = "[#{NCNAME_START}][#{NCNAME_CHAR}]*".freeze
|
|
13
13
|
QNAME = /\A#{NCNAME}(?::#{NCNAME})?\z/u
|
|
14
|
+
ILLEGAL_CHARACTER = /[^\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/u
|
|
14
15
|
|
|
15
|
-
private_constant :NCNAME, :NCNAME_CHAR, :NCNAME_START, :QNAME
|
|
16
|
+
private_constant :NCNAME, :NCNAME_CHAR, :NCNAME_START, :QNAME, :ILLEGAL_CHARACTER
|
|
16
17
|
|
|
17
18
|
class << self
|
|
18
19
|
# Validates an XML 1.0 string representation.
|
|
@@ -104,8 +105,8 @@ module Sevgi
|
|
|
104
105
|
ArgumentError.("#{context} must be valid UTF-8") unless value.valid_encoding?
|
|
105
106
|
|
|
106
107
|
text = value.encode("UTF-8")
|
|
107
|
-
if (
|
|
108
|
-
ArgumentError.("#{context} contains illegal character U+#{format("%04X",
|
|
108
|
+
if (match = ILLEGAL_CHARACTER.match(text))
|
|
109
|
+
ArgumentError.("#{context} contains illegal character U+#{format("%04X", match[0].ord)}")
|
|
109
110
|
end
|
|
110
111
|
|
|
111
112
|
text
|
|
@@ -113,12 +114,6 @@ module Sevgi
|
|
|
113
114
|
ArgumentError.("#{context} must be valid UTF-8: #{e.message}")
|
|
114
115
|
end
|
|
115
116
|
|
|
116
|
-
def legal_codepoint?(codepoint)
|
|
117
|
-
[0x9, 0xA, 0xD].include?(codepoint) ||
|
|
118
|
-
(0x20..0xD7FF).cover?(codepoint) ||
|
|
119
|
-
(0xE000..0xFFFD).cover?(codepoint) ||
|
|
120
|
-
(0x10000..0x10FFFF).cover?(codepoint)
|
|
121
|
-
end
|
|
122
117
|
end
|
|
123
118
|
end
|
|
124
119
|
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "auxiliary/xml"
|
|
3
4
|
require_relative "auxiliary/scalar"
|
|
4
|
-
require_relative "auxiliary/path"
|
|
5
5
|
require_relative "auxiliary/margin"
|
|
6
|
-
require_relative "auxiliary/sizes"
|
|
7
6
|
require_relative "auxiliary/paper"
|
|
8
7
|
|
|
9
8
|
require_relative "auxiliary/canvas"
|
|
@@ -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
|
|
|
@@ -16,6 +16,7 @@ module Sevgi
|
|
|
16
16
|
mixture :Include
|
|
17
17
|
mixture :Lint
|
|
18
18
|
mixture :Save
|
|
19
|
+
mixture :Style
|
|
19
20
|
mixture :Tile
|
|
20
21
|
mixture :Transform
|
|
21
22
|
mixture :Underscore
|
|
@@ -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
|