sevgi-derender 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: 9d9e616e1b10140f4e9477ca8cc23f7912d56a6169b4e686a74353c816fe9706
4
- data.tar.gz: e32b3958b5270380a74f218d87377a6fca87db2cfd3c518cd28530fcb7f97746
3
+ metadata.gz: e02ddd4869fac0f3ab35c12f53bcdd9e23bdc936d8753a17ff56b651c8a07882
4
+ data.tar.gz: aeff138c2ced508eca978c9eccd2e547c808e2be7d4d7bac859a57700e118892
5
5
  SHA512:
6
- metadata.gz: '058bc5a453580bc9c32fd86a1d6a64474868c18b1ffe0af32ceb40d52a60263cfa1a787e1fac9f521ef31817f8a81991ddb65a33503a8b24e3743996f2dcd468'
7
- data.tar.gz: eb906f2cb811fbbe73abeec77b298f68ee3db1de17366996762f02f4e36c0e5d392139cf83b4bc686d7fd7a156838065313fe1e7110d918dc3b45fe2a081fc85
6
+ metadata.gz: dee05c8a84e8872ce946aa18f3f10071c0b07bea43170cc3a59f03ef9b434383e184478c84fe43270bf9524af3b135ee827bbe55868a64b62099af8dc34f37a1
7
+ data.tar.gz: db4b7b23ca99f153f40ff0ec6bc2cdd126c399a466b93c3d92f028669a672de39e9dc783f093f1bb68fa1b738b7809fc6693651c19e251700248e9601afdb581
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,12 +18,25 @@ require "sevgi/derender"
18
18
 
19
19
  ```ruby
20
20
  source = "<svg><rect width=\"3\" height=\"5\"/></svg>"
21
- Sevgi::Derender.derender(source)
21
+ puts Sevgi::Derender.derender(source)
22
+ # SVG do
23
+ # rect width: 3, height: 5
24
+ # end
22
25
 
23
26
  source = '<svg><rect id="mark" style="fill: red" width="3"/></svg>'
24
- Sevgi::Derender.derender(source, omit: %i[id style])
27
+ puts Sevgi::Derender.derender(source, omit: %i[id style])
28
+ # SVG do
29
+ # rect width: 3
30
+ # end
25
31
  ```
26
32
 
33
+ Use `decompile` to inspect an immutable parsed node. Use `evaluate` to add a selected node directly to an existing
34
+ Sevgi document. Generated source is ordinary Ruby. Review it and integrate it statically instead of evaluating it
35
+ dynamically.
36
+
37
+ Derender reads the XML encoding declaration or byte-order mark. Generated Ruby and rendered XML use UTF-8.
38
+ The XML declaration keeps its version and standalone flag, with any encoding field changed to UTF-8.
39
+
27
40
  ## Executable
28
41
 
29
42
  ```sh
@@ -49,7 +62,7 @@ Requires Ruby 3.4.0 or newer. CI verifies the current Ruby 3.4 release and the d
49
62
 
50
63
  ## Native prerequisites
51
64
 
52
- This gem needs no native libraries of its own. Nokogiri may use platform packages depending on the target Ruby platform.
65
+ This gem needs no native libraries of its own. Nokogiri can use platform packages on some Ruby platforms.
53
66
 
54
67
  ## Links
55
68
 
@@ -88,10 +88,10 @@ module Sevgi
88
88
  rescue ::StandardError => e
89
89
  raise if raw_error?(options)
90
90
 
91
- die(e, nil)
91
+ die(e)
92
92
  end
93
93
 
94
- def die(error, _file)
94
+ def die(error)
95
95
  warn(error.message)
96
96
  warn("")
97
97
  backtrace(error).each { warn(" #{it}") }
@@ -142,7 +142,7 @@ module Sevgi
142
142
  rescue ::StandardError => e
143
143
  raise if raw_error?(options)
144
144
 
145
- die(e, file)
145
+ die(e)
146
146
  end
147
147
 
148
148
  def raw_error?(options)
@@ -35,7 +35,6 @@ module Sevgi
35
35
 
36
36
  {**pre, **hash, **post}
37
37
  .map do |key, value|
38
- key = Css.to_key(key) if key.is_a?(::String)
39
38
 
40
39
  if key == "style"
41
40
  style = Css.declarations(value)
@@ -20,7 +20,7 @@ module Sevgi
20
20
 
21
21
  ArgumentError.("File not found: #{path}") unless ::File.exist?(entry)
22
22
 
23
- content = ::File.read(entry)
23
+ content = ::File.binread(entry)
24
24
  new(content)
25
25
  end
26
26
 
@@ -30,7 +30,7 @@ module Sevgi
30
30
  # @raise [Sevgi::ArgumentError] when content is not well-formed XML
31
31
  # @raise [Sevgi::ArgumentError] when content has no root element
32
32
  def self.parse(content)
33
- Nokogiri::XML(content.to_s.lstrip, &:strict).tap do |doc|
33
+ Nokogiri::XML(content.to_s.b.sub(/\A[ \t\r\n]+/, ""), &:strict).tap do |doc|
34
34
  ArgumentError.("XML document has no root element") unless doc.root
35
35
  end
36
36
 
@@ -38,20 +38,26 @@ module Sevgi
38
38
  raise ArgumentError, "Malformed XML: #{e.message.lines.first.strip}", cause: e
39
39
  end
40
40
 
41
- # Extracts the XML declaration from SVG/XML content.
41
+ # Extracts the XML declaration and aligns its encoding with UTF-8 output.
42
42
  # @param content [String] SVG/XML source content
43
+ # @param encoding [String, nil] source encoding reported by the XML parser
43
44
  # @return [String, nil] XML declaration line, if present
44
- def self.declaration(content)
45
- return unless (content = content.to_s.lstrip).start_with?("<?xml ")
46
-
47
- content[/\A<\?xml\b.*?\?>/m]
45
+ def self.declaration(content, encoding: nil)
46
+ content = content.to_s.b
47
+ encoding ||= content.start_with?("\xFF\xFE".b, "\xFE\xFF".b) ? "UTF-16" : "UTF-8"
48
+ content = content.encode("UTF-8", encoding).delete_prefix("\uFEFF").lstrip
49
+ declaration = content[/\A<\?xml[ \t\r\n].*?\?>/m]
50
+
51
+ declaration&.sub(/encoding\s*=\s*(["']).*?\1/, "encoding=\"UTF-8\"")
52
+ rescue EncodingError, ::ArgumentError => e
53
+ raise ArgumentError, "Invalid XML encoding: #{e.message}", cause: e
48
54
  end
49
55
 
50
56
  # Returns the parsed XML document.
51
57
  # @return [Nokogiri::XML::Document]
52
58
  attr_reader :doc
53
59
 
54
- # Returns the source XML declaration when present.
60
+ # Returns the XML declaration with its encoding field normalized to UTF-8, when present.
55
61
  # @return [String, nil]
56
62
  attr_reader :decl
57
63
 
@@ -62,7 +68,8 @@ module Sevgi
62
68
  # @raise [Sevgi::ArgumentError] when content has no root element
63
69
  def initialize(content)
64
70
  @doc = self.class.parse(content)
65
- @decl = self.class.declaration(content)
71
+ @decl = self.class.declaration(content, encoding: doc.encoding)
72
+ @doc.encoding = "UTF-8"
66
73
  end
67
74
 
68
75
  # Converts the root or selected node into a derender node.
@@ -72,22 +79,19 @@ module Sevgi
72
79
  # @raise [Sevgi::ArgumentError] when the document has no root element or the id is absent
73
80
  def decompile(id = nil, omit: nil)
74
81
  if id
75
- if (found = doc.xpath("//*[@id=#{xpath_literal(id)}]") || []).empty?
76
- ArgumentError.("No such element with id '#{id}' in document")
77
- end
78
-
79
- found.first
82
+ doc.at_xpath("//*[@id=#{xpath_literal(id)}]") || ArgumentError.("No such element with id '#{id}' in document")
80
83
  else
81
84
  doc.root
82
85
  end => element
83
86
 
84
87
  ArgumentError.("XML document has no root element") unless element
88
+ validate_selection(element, whole: !id)
85
89
 
86
90
  Node.send(:new, element, pres, namespaces: namespace_scope(element), omit: omissions(omit))
87
91
  end
88
92
 
89
- # Returns XML declaration and pre-root nodes preserved for root decompilation. The result contains only String
90
- # lines and omits the declaration when the source did not provide one.
93
+ # Returns the XML declaration and pre-root nodes as UTF-8 for root decompilation.
94
+ # The result omits the declaration when the source did not provide one.
91
95
  # @return [Array<String>] preamble XML lines
92
96
  def pres
93
97
  @pres ||= [].tap do |lines|
@@ -99,6 +103,19 @@ module Sevgi
99
103
 
100
104
  private
101
105
 
106
+ def validate_selection(element, whole:)
107
+ if whole && doc.root.next_sibling
108
+ ArgumentError.("XML nodes after the root element are not supported")
109
+ end
110
+
111
+ element.traverse do |node|
112
+ references = [node, *node.attribute_nodes.flat_map { it.children.to_a }]
113
+ if references.any? { it.type == Nokogiri::XML::Node::ENTITY_REF_NODE }
114
+ ArgumentError.("Custom XML entity references are not supported")
115
+ end
116
+ end
117
+ end
118
+
102
119
  def namespace_scope(element)
103
120
  element == doc.root ? local_namespaces(element) : element.namespaces
104
121
  end
@@ -8,21 +8,19 @@ module Sevgi
8
8
  module Any
9
9
  # Converts this node into unformatted Sevgi DSL lines.
10
10
  # @return [Array<String>] unformatted Ruby source lines
11
- def decompile(*)
11
+ def decompile(*, namespaces: self.namespaces())
12
12
  if children.any?
13
- children.one? && children.first.send(:text?) ? Array(leaf(has_attributes: attributes.any?)) : tree
13
+ text_leaf? ? Array(leaf(Ruby.literal(content), namespaces:)) : tree(namespaces)
14
14
  else
15
- Array(leaf(has_content: false))
15
+ Array(leaf(namespaces:))
16
16
  end
17
17
  end
18
18
 
19
19
  private
20
20
 
21
- def leaf(has_content: true, has_attributes: true)
22
- attributes = all_attributes
23
- args = []
24
- args << Ruby.literal(content) if has_content
25
- args << Attributes.decompile(attributes) if has_attributes && attributes.any?
21
+ def leaf(*args, namespaces:)
22
+ attributes = all_attributes(namespaces)
23
+ args << Attributes.decompile(attributes) if attributes.any?
26
24
 
27
25
  return explicit_leaf(args) unless bare?
28
26
 
@@ -37,9 +35,11 @@ module Sevgi
37
35
  args.empty? ? "#{call})" : "#{call}, #{args.join(", ")})"
38
36
  end
39
37
 
40
- def tree
38
+ def tree(namespaces)
39
+ opening = inline_content? ? leaf(Ruby.literal(""), namespaces:) : leaf(namespaces:)
40
+
41
41
  [
42
- "#{leaf(has_content: false)} do",
42
+ "#{opening} do",
43
43
  *children.map { it.send(:decompile) }.flatten,
44
44
  "end"
45
45
  ]
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevgi
4
+ module Derender
5
+ module Elements
6
+ # Element strategy for XML comments.
7
+ # @api private
8
+ module Comment
9
+ # Converts a comment into a floating DSL node.
10
+ # @return [Array<String>] unformatted Ruby source lines
11
+ def decompile(*)
12
+ markup = "<!--#{content}-->"
13
+ ["_ Sevgi::Graphics::Content.verbatim(#{Ruby.literal(markup)})"]
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -8,21 +8,21 @@ module Sevgi
8
8
  module CSS
9
9
  # Converts a style node into unformatted Sevgi DSL lines.
10
10
  # @return [Array<String>] unformatted Ruby source lines
11
- def decompile(*)
12
- return raw_style unless (lines = css_lines)
11
+ def decompile(*, namespaces: self.namespaces())
12
+ return raw_style(namespaces) unless (lines = css_lines)
13
13
 
14
14
  [
15
15
  "css({",
16
16
  *lines,
17
- "}, #{css_attributes})",
17
+ "}, #{css_attributes(namespaces)})",
18
18
  ""
19
19
  ]
20
20
  end
21
21
 
22
22
  private
23
23
 
24
- def css_attributes
25
- attributes = all_attributes
24
+ def css_attributes(namespaces)
25
+ attributes = all_attributes(namespaces)
26
26
  source = Attributes.decompile(attributes)
27
27
 
28
28
  attributes.key?("type") ? source : [source, "type: nil"].reject(&:empty?).join(", ")
@@ -42,9 +42,10 @@ module Sevgi
42
42
  .flatten
43
43
  end
44
44
 
45
- def raw_style
45
+ def raw_style(namespaces)
46
46
  arguments = ["Sevgi::Graphics::Content.cdata(#{Ruby.literal(node.content)})"]
47
- arguments << Attributes.decompile(all_attributes) if all_attributes.any?
47
+ attributes = all_attributes(namespaces)
48
+ arguments << Attributes.decompile(attributes) if attributes.any?
48
49
 
49
50
  ["style #{arguments.join(", ")}", ""]
50
51
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevgi
4
+ module Derender
5
+ module Elements
6
+ # Preserves XML processing instructions as inert markup.
7
+ # @api private
8
+ module Instruction
9
+ # Converts a processing instruction into a floating DSL node.
10
+ # @return [Array<String>] unformatted Ruby source lines
11
+ def decompile(*)
12
+ ["_ Sevgi::Graphics::Content.verbatim(#{Ruby.literal(content)})"]
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -11,20 +11,27 @@ module Sevgi
11
11
  # Converts the SVG root into unformatted Sevgi DSL lines.
12
12
  # @param pres [Array<String>] preamble XML lines
13
13
  # @return [Array<String>] unformatted Ruby source lines
14
- def decompile(pres = [])
14
+ def decompile(pres = [], **)
15
15
  lines = super
16
16
  return lines unless pres&.any?
17
17
 
18
- lines.unshift(
19
- [
20
- "SVG.Document preambles: [",
21
- *pres.map { "#{Ruby.literal(it)}," },
22
- "]",
23
- ""
24
- ]
25
- )
18
+ lines[0] = document_call(lines.first)
19
+
20
+ lines.unshift(preamble_lines(pres))
21
+ end
22
+
23
+ def document_call(line)
24
+ return line.sub("SVG", "SVG document") if ["SVG", "SVG do"].include?(line)
25
+
26
+ line.sub(/\ASVG /, "SVG document, ")
26
27
  end
27
28
 
29
+ def preamble_lines(pres)
30
+ ["document = SVG.Document preambles: [", *pres.map { "#{Ruby.literal(it)}," }, "]", ""]
31
+ end
32
+
33
+ private :document_call, :preamble_lines
34
+
28
35
  # Returns the root DSL word.
29
36
  # @return [String]
30
37
  def element = "SVG"
@@ -10,6 +10,17 @@ module Sevgi
10
10
  # @return [Array<String>] unformatted Ruby source lines
11
11
  def decompile(*) = ["_ #{Ruby.literal(content)}"]
12
12
  end
13
+
14
+ # Element strategy for CDATA sections.
15
+ # @api private
16
+ module CData
17
+ # Converts CDATA into a floating content node.
18
+ # @return [Array<String>] unformatted Ruby source lines
19
+ def decompile(*)
20
+ markup = "<![CDATA[#{Graphics.const_get(:XML).cdata(content)}]]>"
21
+ ["_ Sevgi::Graphics::Content.verbatim(#{Ruby.literal(markup)})"]
22
+ end
23
+ end
13
24
  end
14
25
  end
15
26
  end
@@ -10,7 +10,8 @@ module Sevgi
10
10
  end
11
11
 
12
12
  require_relative "elements/any"
13
+ require_relative "elements/comment"
14
+ require_relative "elements/instruction"
13
15
  require_relative "elements/css"
14
- require_relative "elements/junk"
15
16
  require_relative "elements/root"
16
17
  require_relative "elements/text"
@@ -12,42 +12,48 @@ module Sevgi
12
12
 
13
13
  # Appends a derender node to the target parent.
14
14
  # @param node [Sevgi::Derender::Node] derender node
15
+ # @param namespaces [Hash{String => String}] namespace declarations for the appended node
15
16
  # @return [Sevgi::Graphics::Element, nil] included element, or nil when the node does not produce graphics output
16
- def append(node)
17
- case node.send(:type)
18
- when :CSS
19
- append_css(node)
20
- when :Text
21
- build(:_, node.content)
22
- else
23
- append_element(node)
24
- end
17
+ def append(node, namespaces: node.namespaces)
18
+ type = node.send(:type)
19
+ return append_css(node, namespaces) if type == :CSS
20
+ return build(:_, node.content) if type == :Text
21
+ return append_cdata(node) if type == :CData
22
+ return build(:_, Graphics::Content.verbatim("<!--#{node.content}-->")) if type == :Comment
23
+ return build(:_, Graphics::Content.verbatim(node.content)) if type == :Instruction
24
+
25
+ append_element(node, namespaces)
25
26
  end
26
27
 
27
28
  private
28
29
 
29
30
  attr_reader :parent
30
31
 
31
- def append_css(node)
32
+ def append_cdata(node)
33
+ body = Graphics.const_get(:XML).cdata(node.content)
34
+ build(:_, Graphics::Content.verbatim("<![CDATA[#{body}]]>"))
35
+ end
36
+
37
+ def append_css(node, namespaces)
32
38
  content = if (hash = Css.rules(node.content))
33
39
  Graphics::Content.css(hash)
34
40
  else
35
41
  Graphics::Content.cdata(node.content)
36
42
  end
37
43
 
38
- build(:style, content, **node.send(:all_attributes))
44
+ build(:style, content, **node.send(:all_attributes, namespaces))
39
45
  end
40
46
 
41
- def append_element(node)
47
+ def append_element(node, namespaces)
42
48
  contents = contents(node)
43
49
 
44
- build(node.name, *contents, **attributes(node)).tap do |element|
45
- node.children.each { self.class.new(element).append(it) } if contents.empty?
50
+ build(node.name, *contents, **attributes(node, namespaces)).tap do |element|
51
+ node.children.each { self.class.new(element).append(it) } unless node.send(:text_leaf?)
46
52
  end
47
53
  end
48
54
 
49
- def attributes(node)
50
- attributes = node.send(:all_attributes)
55
+ def attributes(node, namespaces)
56
+ attributes = node.send(:all_attributes, namespaces)
51
57
  return attributes unless (style = attributes["style"])
52
58
  return attributes unless (declarations = Css.declarations(style))
53
59
 
@@ -55,7 +61,9 @@ module Sevgi
55
61
  end
56
62
 
57
63
  def contents(node)
58
- node.children.one? && node.children.first.send(:text?) ? [node.content] : []
64
+ return [node.content] if node.send(:text_leaf?)
65
+
66
+ node.send(:inline_content?) ? [""] : []
59
67
  end
60
68
 
61
69
  def build(name, *contents, **attributes)
@@ -51,16 +51,11 @@ module Sevgi
51
51
  nil
52
52
  end
53
53
 
54
- # Converts a CSS key into a Ruby hash key.
55
- # @param arg [String] CSS key
56
- # @return [String] Ruby hash key source
57
- def to_key(arg) = arg
58
-
59
54
  # Converts a CSS key/value pair into Ruby hash source.
60
55
  # @param key [String] CSS declaration key
61
56
  # @param value [String] CSS declaration value
62
57
  # @return [String] Ruby hash pair source
63
- def to_key_value(key, value) = "#{Ruby.literal(to_key(key))}: #{to_value(value)}"
58
+ def to_key_value(key, value) = "#{Ruby.literal(key)}: #{to_value(value)}"
64
59
 
65
60
  # Converts a CSS value into Ruby source.
66
61
  # @param arg [String] CSS value
@@ -25,11 +25,20 @@ module Sevgi
25
25
  # Captures owned Node state during construction.
26
26
  # @api private
27
27
  module Capture
28
+ NODE_TYPES = {
29
+ Nokogiri::XML::Node::TEXT_NODE => :Text,
30
+ Nokogiri::XML::Node::CDATA_SECTION_NODE => :CData,
31
+ Nokogiri::XML::Node::COMMENT_NODE => :Comment,
32
+ Nokogiri::XML::Node::PI_NODE => :Instruction
33
+ }.freeze
34
+ private_constant :NODE_TYPES
35
+
28
36
  private
29
37
 
30
38
  def capture_context(pres, namespaces)
31
39
  @pres = pres.map { it.to_s.dup.freeze }.freeze
32
40
  @namespaces = snapshot(namespaces || local_namespaces)
41
+ @scope = snapshot(node.namespaces)
33
42
  @type = dispatch
34
43
  singleton_class.send(:private, :decompile)
35
44
  end
@@ -53,10 +62,17 @@ module Sevgi
53
62
  end
54
63
 
55
64
  def capture_identity
56
- @content = (preserve_space? ? node.content : normalized_content).dup.freeze
65
+ @content = captured_content.dup.freeze
66
+
57
67
  @name = [node.namespace&.prefix, node.name].compact.join(":").freeze
58
68
  end
59
69
 
70
+ def captured_content
71
+ return node.to_xml if node.processing_instruction?
72
+
73
+ literal_content? || preserve_space? ? node.content : normalized_content
74
+ end
75
+
60
76
  def metadata(key, value)
61
77
  [key.delete_prefix(META_NAMESPACE), value] if key.start_with?(META_NAMESPACE)
62
78
  end
@@ -74,7 +90,7 @@ module Sevgi
74
90
 
75
91
  # Immutable conversion result for one SVG/XML node.
76
92
  #
77
- # Attributes, namespaces, content, and descendants are owned snapshots; parser objects and dispatch strategies
93
+ # Attributes, namespaces, content, and descendants are owned snapshots. Parser objects and dispatch strategies
78
94
  # remain internal to Derender. Attributes omitted during decompilation are absent throughout the captured subtree.
79
95
  class Node
80
96
  include Capture
@@ -90,7 +106,8 @@ module Sevgi
90
106
  attr_reader :children
91
107
 
92
108
  # Returns immutable normalized text content. `xml:space="preserve"` and single-line mixed text retain their exact
93
- # text. Other text trims surrounding whitespace; multiline mixed text removes only its outer indentation lines.
109
+ # text. Other text trims surrounding whitespace. Multiline mixed text removes only its outer indentation lines.
110
+ # For a processing instruction, content is its complete XML markup, including the target and data.
94
111
  # @return [String] frozen owned text snapshot
95
112
  attr_reader :content
96
113
 
@@ -98,8 +115,9 @@ module Sevgi
98
115
  # @return [String] owned element name
99
116
  attr_reader :name
100
117
 
101
- # Returns immutable namespace declarations emitted for this node. A conversion root owns its local declarations;
102
- # a separately selected node owns all declarations in scope; descendant snapshots own their local declarations.
118
+ # Returns immutable namespace declarations emitted for this node. A conversion root owns its local declarations.
119
+ # A separately selected node owns all declarations in scope. Descendant snapshots own their local declarations.
120
+ # Independent source conversion and evaluation also emit the inherited declarations captured for that node.
103
121
  # @return [Hash{String => String}] frozen owned namespace snapshot
104
122
  attr_reader :namespaces
105
123
 
@@ -128,20 +146,22 @@ module Sevgi
128
146
 
129
147
  alias meta _
130
148
 
131
- # Converts this node into formatted Sevgi DSL Ruby source.
149
+ # Converts this node into formatted Sevgi DSL Ruby source. The returned String is ordinary Ruby source. Review and
150
+ # integrate it statically rather than using Ruby's raw dynamic evaluation methods.
132
151
  # @return [String] formatted Sevgi DSL source
133
152
  # @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
134
153
  # @note Foreign namespace elements use the explicit `Element` DSL path, and nested `svg` nodes remain elements.
135
154
  # @note Unsafe bare Ruby names are emitted through the explicit `Element` DSL word.
136
- def derender = Ruby.format(decompile(@pres).join("\n"))
155
+ def derender = Ruby.format(decompile(@pres, namespaces: @scope).join("\n"))
137
156
 
138
157
  # Evaluates this node under a graphics element.
139
158
  # @param element [Sevgi::Graphics::Element] target graphics element
140
159
  # @return [Sevgi::Graphics::Element, nil] included current element, or nil when it produces no graphics output
141
160
  # @note Namespace declarations, qualified attributes, significant text, and nested `svg` nodes are preserved.
142
- def evaluate(element) = Evaluator.new(element).append(self)
161
+ def evaluate(element) = Evaluator.new(element).append(self, namespaces: @scope)
143
162
 
144
163
  # Evaluates only this node's children under a graphics element.
164
+ # Each imported element retains its namespace scope without changing the target parent's declarations.
145
165
  # @param element [Sevgi::Graphics::Element] target graphics element
146
166
  # @return [Array<Sevgi::Graphics::Element>] immutable included-child snapshot
147
167
  def evaluate_children(element) = children.filter_map { it.evaluate(element) }.freeze
@@ -165,36 +185,34 @@ module Sevgi
165
185
 
166
186
  attr_reader :node, :type
167
187
 
168
- def all_attributes = {**attributes, **namespaces}.freeze
188
+ def all_attributes(namespaces = self.namespaces()) = {**attributes, **namespaces}.freeze
169
189
 
170
190
  def attribute_key(attribute) = [attribute.namespace&.prefix, attribute.name].compact.join(":")
171
191
 
172
192
  def dispatch
173
- case
174
- when node.text?
175
- :Text
176
- when node.comment?
177
- :Junk
178
- when Namespace.svg?(node, "style")
193
+ type = NODE_TYPES[node.type]
194
+ type ||= if Namespace.svg?(node, "style") && node.children.all? { it.text? || it.cdata? }
179
195
  :CSS
180
- when @top && Namespace.svg?(node, "svg")
196
+ elsif @top && Namespace.svg?(node, "svg")
181
197
  :Root
182
198
  else
183
199
  :Any
184
200
  end
185
- .tap { extend(Elements.const_get(it)) }
201
+
202
+ type.tap { extend(Elements.const_get(it)) }
186
203
  end
187
204
 
188
205
  def element = name
189
206
 
190
207
  def ignorable_child?(child)
191
- child.send(:type) == :Junk ||
192
- (child.send(:text?) &&
193
- child.content.strip.empty? &&
194
- !child.send(:preserve_space?) &&
195
- !child.send(:inline_text?))
208
+ child.send(:text?) &&
209
+ child.content.strip.empty? &&
210
+ !child.send(:preserve_space?) &&
211
+ !child.send(:inline_text?)
196
212
  end
197
213
 
214
+ def literal_content? = node.cdata? || node.comment?
215
+
198
216
  def preserve_space?
199
217
  each_node do |current|
200
218
  case xml_space(current)
@@ -210,6 +228,12 @@ module Sevgi
210
228
 
211
229
  def inline_text? = mixed_text? && !node.content.match?(/[\r\n]/)
212
230
 
231
+ def inline_content?
232
+ return false unless children.any? { it.send(:text?) || it.send(:type) == :CData }
233
+
234
+ preserve_space? || node.children.none? { it.text? && it.content.match?(/[\r\n]/) }
235
+ end
236
+
213
237
  def normalized_content
214
238
  return node.content if mixed_text? && inline_text?
215
239
  return node.content.strip unless mixed_text?
@@ -217,7 +241,10 @@ module Sevgi
217
241
  node.content.sub(/\A[ \t]*\r?\n[ \t]*/, "").sub(/[ \t]*\r?\n[ \t]*\z/, "")
218
242
  end
219
243
 
220
- def mixed_text? = node.text? && node.parent&.children&.any?(&:element?)
244
+ def mixed_text?
245
+ node.text? &&
246
+ node.parent&.children&.any? { it.element? || it.cdata? || it.comment? || it.processing_instruction? }
247
+ end
221
248
 
222
249
  def local_namespaces
223
250
  return {} unless node.respond_to?(:namespace_definitions)
@@ -238,6 +265,8 @@ module Sevgi
238
265
 
239
266
  def text? = node.text?
240
267
 
268
+ def text_leaf? = children.one? && children.first.send(:text?)
269
+
241
270
  def xml_space(current)
242
271
  return unless current.respond_to?(:attribute_nodes)
243
272
 
@@ -3,6 +3,6 @@
3
3
  module Sevgi
4
4
  module Derender
5
5
  # Current version of the Sevgi derender gem.
6
- VERSION = "0.98.2"
6
+ VERSION = "1.0.0"
7
7
  end
8
8
  end
@@ -13,8 +13,8 @@ require_relative "derender/version"
13
13
  module Sevgi
14
14
  # Brings editor-authored SVG/XML into programmatic Sevgi workflows.
15
15
  #
16
- # Vector geometry such as a Bezier-heavy logo or hand-adjusted illustration may be better authored in a visual editor
17
- # than reconstructed as Ruby. Derender preserves that SVG/XML tree as inspectable data, formatted Sevgi DSL source,
16
+ # Some vector geometry is easier to create in a visual editor than to reconstruct as Ruby. Examples include a
17
+ # Bezier-heavy logo and a hand-adjusted illustration. Derender preserves that SVG/XML tree as inspectable data,
18
18
  # or graphics elements under an existing document. This lets editor-authored geometry participate in programmatic
19
19
  # composition, styling, layout, and output. Use it when SVG/XML is a real input artifact, not as an intermediate
20
20
  # authoring format for ordinary Sevgi drawing code.
@@ -24,16 +24,23 @@ module Sevgi
24
24
  # tree without invoking same-named Ruby methods.
25
25
  #
26
26
  # Evaluation APIs treat SVG/XML as data: they build graphics element trees directly and do not execute generated Ruby
27
- # source. Malformed, rootless, or unmatched input is rejected with {Sevgi::ArgumentError}.
27
+ # source. The String returned by source-generation APIs is ordinary Ruby source. Review and integrate it statically
28
+ # rather than passing it to Ruby's raw dynamic evaluation methods. Malformed, rootless, or unmatched input is rejected
29
+ # with {Sevgi::ArgumentError}.
30
+ # Processing instructions retain their target, data, and order as inert XML markup. Custom entity references in the
31
+ # selected subtree are rejected before inclusion changes a target. Predefined and numeric references remain valid.
32
+ # Whole-document conversion rejects nodes after the root. Explicit id selection ignores unrelated document siblings.
33
+ # Input uses the XML encoding declaration or byte-order mark. Generated source and rendered XML use UTF-8.
34
+ # The XML declaration retains its version and standalone flag, with any encoding field changed to UTF-8.
28
35
  #
29
36
  # Namespace dispatch treats qualified and foreign elements as ordinary XML nodes. Their element identity, namespace
30
37
  # declarations, qualified attributes, significant text, and nested `svg` elements survive source generation and direct
31
38
  # evaluation. CSS specialization applies only to unqualified `style` elements in no namespace or the default SVG
32
- # namespace; the document-root strategy additionally requires an unqualified `svg` at the root of the conversion.
39
+ # namespace. The document-root strategy also requires an unqualified `svg` at the root of the conversion.
33
40
  # Simple CSS rules use the readable `css({...})` DSL form. At-rules, duplicate declarations, and other CSS that cannot
34
41
  # be represented losslessly as a Hash remain owned raw style content.
35
42
  #
36
- # Attribute omission uses exact, case-sensitive names across the selected subtree. ID selection happens first;
43
+ # Attribute omission uses exact, case-sensitive names across the selected subtree. ID selection happens first.
37
44
  # namespace declarations remain intact, and omitting the `style` attribute does not omit `style` elements.
38
45
  #
39
46
  # @example Inspect, select, and convert an immutable result
@@ -82,7 +89,8 @@ module Sevgi
82
89
  # @see Sevgi.DecompileFile
83
90
  def self.decompile_file(file, id: nil, omit: nil) = Document.load_file(file).decompile(id, omit:)
84
91
 
85
- # Converts SVG/XML content into Sevgi DSL Ruby source.
92
+ # Converts SVG/XML content into Sevgi DSL Ruby source. The returned String is ordinary Ruby source. Review and
93
+ # integrate it statically rather than using Ruby's raw dynamic evaluation methods.
86
94
  # @param content [String] SVG/XML source content
87
95
  # @param id [String, Symbol, nil] optional SVG id selecting a node inside the source
88
96
  # @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
@@ -100,7 +108,8 @@ module Sevgi
100
108
  # @see Sevgi.Derender
101
109
  def self.derender(content, id: nil, omit: nil) = Document.new(content).decompile(id, omit:).derender
102
110
 
103
- # Converts an SVG/XML file into Sevgi DSL Ruby source.
111
+ # Converts an SVG/XML file into Sevgi DSL Ruby source. The returned String is ordinary Ruby source. Review and
112
+ # integrate it statically rather than using Ruby's raw dynamic evaluation methods.
104
113
  # @param file [String] path to the source SVG/XML file
105
114
  # @param id [String, Symbol, nil] optional SVG id selecting a node inside the source
106
115
  # @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sevgi-derender
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.98.2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Recai Oktaş
@@ -15,28 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.98.2
18
+ version: 1.0.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.98.2
25
+ version: 1.0.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: sevgi-graphics
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - '='
31
31
  - !ruby/object:Gem::Version
32
- version: 0.98.2
32
+ version: 1.0.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - '='
38
38
  - !ruby/object:Gem::Version
39
- version: 0.98.2
39
+ version: 1.0.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: css_parser
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -96,8 +96,9 @@ files:
96
96
  - lib/sevgi/derender/document.rb
97
97
  - lib/sevgi/derender/elements.rb
98
98
  - lib/sevgi/derender/elements/any.rb
99
+ - lib/sevgi/derender/elements/comment.rb
99
100
  - lib/sevgi/derender/elements/css.rb
100
- - lib/sevgi/derender/elements/junk.rb
101
+ - lib/sevgi/derender/elements/instruction.rb
101
102
  - lib/sevgi/derender/elements/root.rb
102
103
  - lib/sevgi/derender/elements/text.rb
103
104
  - lib/sevgi/derender/evaluator.rb
@@ -126,7 +127,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
126
127
  - !ruby/object:Gem::Version
127
128
  version: '0'
128
129
  requirements: []
129
- rubygems_version: 4.0.16
130
+ rubygems_version: 4.0.20
130
131
  specification_version: 4
131
132
  summary: SVG-to-Sevgi source conversion.
132
133
  test_files: []
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sevgi
4
- module Derender
5
- module Elements
6
- # Element strategy for comments and ignored XML nodes.
7
- # @api private
8
- module Junk
9
- # Drops ignored nodes from generated source.
10
- # @return [Array<String>] empty source lines
11
- def decompile(*) = []
12
- end
13
- end
14
- end
15
- end