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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +115 -1
  3. data/README.md +55 -1
  4. data/lib/sevgi/graphics/attribute.rb +15 -10
  5. data/lib/sevgi/graphics/auxiliary/canvas.rb +4 -4
  6. data/lib/sevgi/graphics/auxiliary/content.rb +5 -16
  7. data/lib/sevgi/graphics/auxiliary/paper.rb +52 -1
  8. data/lib/sevgi/graphics/{xml.rb → auxiliary/xml.rb} +4 -9
  9. data/lib/sevgi/graphics/auxiliary.rb +1 -2
  10. data/lib/sevgi/graphics/document/base.rb +3 -2
  11. data/lib/sevgi/graphics/document.rb +15 -27
  12. data/lib/sevgi/graphics/element.rb +23 -8
  13. data/lib/sevgi/graphics/mixtures/call.rb +9 -24
  14. data/lib/sevgi/graphics/mixtures/core.rb +19 -23
  15. data/lib/sevgi/graphics/mixtures/duplicate.rb +4 -16
  16. data/lib/sevgi/graphics/mixtures/export.rb +8 -2
  17. data/lib/sevgi/graphics/mixtures/hatch.rb +5 -5
  18. data/lib/sevgi/graphics/mixtures/identify.rb +1 -1
  19. data/lib/sevgi/graphics/mixtures/inkscape.rb +23 -10
  20. data/lib/sevgi/graphics/mixtures/rdf.rb +1 -1
  21. data/lib/sevgi/graphics/mixtures/render.rb +11 -95
  22. data/lib/sevgi/graphics/mixtures/save.rb +4 -4
  23. data/lib/sevgi/graphics/mixtures/style/declaration.rb +117 -0
  24. data/lib/sevgi/graphics/mixtures/style.rb +36 -0
  25. data/lib/sevgi/graphics/mixtures/symbols.rb +2 -2
  26. data/lib/sevgi/graphics/mixtures/tile.rb +4 -4
  27. data/lib/sevgi/graphics/mixtures/transform.rb +22 -3
  28. data/lib/sevgi/graphics/mixtures/underscore.rb +2 -2
  29. data/lib/sevgi/graphics/mixtures/wrappers.rb +49 -0
  30. data/lib/sevgi/graphics/mixtures.rb +1 -0
  31. data/lib/sevgi/graphics/version.rb +1 -1
  32. data/lib/sevgi/graphics.rb +23 -19
  33. metadata +21 -7
  34. data/lib/sevgi/graphics/auxiliary/path.rb +0 -44
  35. data/lib/sevgi/graphics/auxiliary/sizes.rb +0 -62
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 753194f4865e2c8b099f788588a684a72a116004b3e56bb40cfd626a7b14043f
4
- data.tar.gz: d443e433a8c86e4d902b31a4dadb71a244905f83aca4b67d8d56c64fdd7cb6c7
3
+ metadata.gz: 9e205e418f5e60262a7e2cea0c4d32879942f7b238ea5fbf07bbbc01be5a9bbf
4
+ data.tar.gz: 889bb3fc5e72a27efba882624c3af23681a2811866d6c165f344edbb79173db7
5
5
  SHA512:
6
- metadata.gz: 05517e6f426cc1cd7ce0077705f147db602346bfba518500551dfce4008d669935f1fe3f21fc83b4531f33a44e4eec38e5b3ff82ccceff1bf1968a30f7ce8998
7
- data.tar.gz: d19ec2a6d3e5e093876ab72eaa594220241f5ff8f5c744124330ee69adff71f12a67159779b9894a6bd4589ea0b3af78ad16ed768d41b422ffc18bca77248075
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 checker board example to the documentation showcase flow.
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.call
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
- case value
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; later caller mutation cannot change the store.
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; nil is ignored
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; nil is ignored
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; names may end in {UPDATE_SUFFIX}
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; nil uses zero for both coordinates.
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; a scalar sets both coordinates
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; nil uses zero for both coordinates.
146
- # @param origin [Numeric, Array<Numeric>, nil, Sevgi::Undefined] viewBox origin; a scalar sets both coordinates
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/svg/#content-safety Content safety guide
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; mutable
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; embedded terminators are split during rendering.
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; mutable
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; validation guarantees encoding and legal XML 1.0 code
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
- PAPER_SIZES.each { |name, (width, height, unit)| define(name, width:, height:, unit:) }
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 (codepoint = text.each_codepoint.find { !legal_codepoint?(it) })
108
- ArgumentError.("#{context} contains illegal character U+#{format("%04X", codepoint)}")
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 may target this class through {Sevgi::Graphics::Mixtures.mixin}; doing
8
- # so changes every descendant profile process-wide. Subclass it first when an extension should remain scoped.
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}; an anonymous profile class is useful when library code needs one-off
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; crisp edges | `Draw`, `Hatch`, and editor/RDF helpers |
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; `:minimal` changes serialization metadata, not
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
- # subclass Base first when an extension should remain scoped.
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/svg/#document-profiles Document profiles guide
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
- case value
85
- when ::String
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; use them for shared vocabulary rather than
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 lookup(name) if preambles == Undefined && attributes == Undefined
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; other mutable attribute values are
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; nil means an empty Hash
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