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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 753194f4865e2c8b099f788588a684a72a116004b3e56bb40cfd626a7b14043f
4
- data.tar.gz: d443e433a8c86e4d902b31a4dadb71a244905f83aca4b67d8d56c64fdd7cb6c7
3
+ metadata.gz: 7965ed7aa4c5960cca6a4ed927258d13a5b5be7d9e327f4bf7da5b3480b66b1f
4
+ data.tar.gz: 88224f9179c50ceca0ae8e4ee7a92e5d8a6a0d2634ec3912356dc5e5d2d27053
5
5
  SHA512:
6
- metadata.gz: 05517e6f426cc1cd7ce0077705f147db602346bfba518500551dfce4008d669935f1fe3f21fc83b4531f33a44e4eec38e5b3ff82ccceff1bf1968a30f7ce8998
7
- data.tar.gz: d19ec2a6d3e5e093876ab72eaa594220241f5ff8f5c744124330ee69adff71f12a67159779b9894a6bd4589ea0b3af78ad16ed768d41b422ffc18bca77248075
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
- ## Unreleased
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 checker board example to the documentation showcase flow.
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.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
+
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
- 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)
@@ -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; later caller mutation cannot change the store.
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; nil is ignored
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; nil is ignored
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; names may end in {UPDATE_SUFFIX}
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; 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
@@ -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
 
@@ -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
@@ -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; standalone Graphics accepts any valid XML name.
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; later values replace earlier values unless their names use the `+` update suffix.
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; every
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; later
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; Hashes are imported from left to right
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; use descriptive method names when it has multiple steps.
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 may 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
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 in the current element context and does not receive the invocation arguments.
77
- # @yield evaluates the drawing DSL in the current element context
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 methods that should be executed for a callable module.
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 duplicated callable module independent configuration containers.
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 initialize_dup(original)
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; strings are split on whitespace
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; non-content objects are stringified and XML-encoded
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; use {#Root?} to distinguish a document root.
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 would create a tree cycle
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 would create a cycle
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 would create a cycle.
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 would create a cycle
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
- same_parent = element.parent.equal?(parent) && parent.children.include?(element)
351
- index(index, parent.children.size - (same_parent ? 1 : 0))
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)