sevgi-derender 0.95.0 → 0.98.2

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: 84e2eb700b9e867ffb80ad1223ba2bfaf32f632b19d05a07db71191a8a611cac
4
- data.tar.gz: 7cefed91f1645a9c195dd65d677f7c9cb041f6f4f4093b90aae2aa56a848342b
3
+ metadata.gz: 9d9e616e1b10140f4e9477ca8cc23f7912d56a6169b4e686a74353c816fe9706
4
+ data.tar.gz: e32b3958b5270380a74f218d87377a6fca87db2cfd3c518cd28530fcb7f97746
5
5
  SHA512:
6
- metadata.gz: fa91f3c9a83fa0b8a6edfa104131e8fcd533421bd182818713c3235c046b431926a031d2a3b52803afb4150f1757ace6b50f42977ef6065b39cbb3ea2f5bad94
7
- data.tar.gz: da2e1ce83ee737df97b9dd01b80abb2d372f2315eab3e6051b1cb563120185afd7f326b158f869760cdc71c623a1bd9e12b2dd82c05c056ced7b0e469ea78b42
6
+ metadata.gz: '058bc5a453580bc9c32fd86a1d6a64474868c18b1ffe0af32ceb40d52a60263cfa1a787e1fac9f521ef31817f8a81991ddb65a33503a8b24e3743996f2dcd468'
7
+ data.tar.gz: eb906f2cb811fbbe73abeec77b298f68ee3db1de17366996762f02f4e36c0e5d392139cf83b4bc686d7fd7a156838065313fe1e7110d918dc3b45fe2a081fc85
data/CHANGELOG.md CHANGED
@@ -5,6 +5,131 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.98.2 - 2026-07-25
9
+
10
+ ### Fixed
11
+
12
+ - Restored the `sevgi` CLI's main-object scope so bare toolkit calls remain available inside script helper classes.
13
+
14
+ ## 0.98.1 - 2026-07-20
15
+
16
+ ### Changed
17
+
18
+ - Changed the `sevgi` CLI to use the executor's isolated scope by default.
19
+
20
+ ### Removed
21
+
22
+ - Removed the redundant `-n` and `--nomain` CLI options; library consumers can still select `main: true` through
23
+ `Sevgi.execute` and `Sevgi.execute_file` when needed.
24
+
25
+ ## 0.98.0 - 2026-07-20
26
+
27
+ ### Added
28
+
29
+ - Added standard-input support to `sevgi`, `igves`, and `igsev`; `sevgi --as NAME` and
30
+ `Sevgi.execute_file(..., as: NAME)` supply the logical source basename used by implicit SVG, PDF, and PNG
31
+ destinations without changing physical load identity.
32
+
33
+ ## 0.97.0 - 2026-07-19
34
+
35
+ ### Added
36
+
37
+ - Added `sevgi-appendix`, containing the Sevgi agent skill and a RuboCop plugin for preserving readable `.sevgi` DSL
38
+ source alongside rubyfmt-formatted Ruby; the umbrella `sevgi` gem installs the matching Appendix version.
39
+ - Added `sevgi --skill` to report the validated path of the matching packaged agent skill for product-neutral setup.
40
+ - Added `igsev` to the umbrella gem for normalizing an SVG file through a complete SVG-to-Sevgi-to-SVG round trip.
41
+
42
+ ### Changed
43
+
44
+ - Made Homebrew the recommended complete CLI installation while retaining Bundler and focused component gems for Ruby
45
+ application dependencies.
46
+
47
+ ## 0.96.0 - 2026-07-18
48
+
49
+ ### Added
50
+
51
+ - Added exact, subtree-wide attribute omission to Derender content/file conversion, evaluation, and inclusion APIs;
52
+ `igves --omit` exposes the same behavior from the command line.
53
+ - Added `Sevgi.SVG` as the explicit namespaced form of the top-level SVG document entrypoint.
54
+ - Added the opt-in recursive `SVG::Modules` contract for callable drawing namespaces.
55
+ - Added callable drawing-module `base` blocks, document and paper registry introspection, Canvas structural equality,
56
+ renderer options on `RenderChildren`, axis translation helpers, and a CC BY RDF helper.
57
+ - Formalized non-rendering `-` metadata and `+` attribute updates as `Attributes::META_PREFIX` and
58
+ `Attributes::UPDATE_SUFFIX`; repeated Array updates now concatenate into a stable flat value.
59
+
60
+ ### Changed
61
+
62
+ - Breaking: replaced the `SVG = Sevgi::Graphics` alias with an independent SVG facade. Facade operations now use
63
+ capitalized names such as `SVG.Canvas` and `SVG.Document`; lowercase component helpers remain on
64
+ `Sevgi::Graphics`, and the stuttering `SVG.SVG` form was removed.
65
+ - Breaking: made low-level `Executor.execute` and `execute_file` runners private; consumers use
66
+ `Sevgi.execute` and `Sevgi.execute_file` while retaining the public result and error types.
67
+ - Breaking: removed the abstract `Document::Base` layer from the selectable document-profile registry; `Minimal` and
68
+ `Default` are now sibling concrete profiles, and custom profile hierarchies should derive from `Base`.
69
+ - Breaking: renamed Parallelogram segment and constraint arguments by geometric role: `base`, `side`, and
70
+ `constraint` replace axis-implying constructor names.
71
+ - Made degree-based sine and cosine exact at integer quarter turns, eliminating cardinal Geometry residue.
72
+ - Documented complete file-system failure families for file comparison, output, touch, and upward location helpers.
73
+ - Formalized generated Tile ids, positional CSS classes, template placement, and per-use callback signatures.
74
+ - Formalized String and Symbol ids across Derender selection APIs and documented file-read failures consistently.
75
+ - Breaking: made top-level `Decompile`, `Derender`, `Evaluate`, and `EvaluateChildren` consume inline SVG/XML;
76
+ file inputs now use the corresponding `File`-suffixed entrypoints.
77
+ - Breaking: replaced callable drawing module `call` block registration with argument-independent `base` blocks;
78
+ inherited bases run parent-first, and modules with one drawing method conventionally name it `call`.
79
+ - Breaking: made bracket and call notation the canonical Geometry constructors. Among Data value types, bracket notation
80
+ is public only for Point, Segment, LengthAngle, Margin, and Paper; use `.new` for Location and result carriers, and
81
+ `Stay` for traversal stop tokens.
82
+ - Breaking: aligned Point and Segment comparison with Ruby `Comparable`; malformed or unrelated comparison operands now
83
+ return nil from `<=>`.
84
+ - Breaking: executor entrypoints now return immutable `Executor::Result` values and expose only `execute` and
85
+ `execute_file`; inspect `result.error`, `result.stack`, and `result.value` instead of executor scopes or lifecycle state.
86
+ - Breaking: wrapper attributes and callable arguments use distinct channels, `With` rejects parentless receivers, and
87
+ unsupported direct constructors are private; use the documented factories for Content, concrete Geometry lined
88
+ elements, and Grid query values.
89
+ - Breaking: Canvas uses `.new` for explicit fields, `.from_paper(paper, **overrides)` for paper conversion, and
90
+ `.call`/`Graphics.canvas` for dispatch; the misleading keyword-only `.from_paper(width:, height:)` form was removed.
91
+ - Breaking: `Save`, `Write`, `PNG`, `PDF`, and Sundries native export normalize successful destinations to expanded
92
+ String paths and create missing parent directories; change-aware SVG writes still return nil when unchanged.
93
+ - Breaking: renamed `Derender.evaluate_file_children` to `evaluate_children_file`; decompiled nodes now own immutable
94
+ public state while parser, strategy, and construction plumbing remain private.
95
+ - Expanded runtime/YARD parity checks and exact contracts for inherited, extended, generated, and dynamic API surfaces,
96
+ forwarded options, error channels, path ownership, whitespace, namespaces, nil behavior, and return values.
97
+
98
+ ### Fixed
99
+
100
+ - Rejected directories from default file discovery while preserving custom locator matchers.
101
+ - Kept `Ancestral` context in non-rendering `-context` metadata instead of leaking it into SVG attributes.
102
+ - Preserved source Canvas units and names when deriving fitted Grid canvases.
103
+ - Prevented stale documentation assets from mixing old layouts with new HTML, and moved sidebar navigation into the
104
+ tablet menu at 1024px and below.
105
+ - Normalized numeric slots owned by path, shape, transform, tile, and Inkscape page helpers to finite SVG number spelling;
106
+ arbitrary user-supplied attributes remain untouched.
107
+ - Made named callable wrapper and symbol ids stable while omitting defaults for anonymous modules, and indexed every
108
+ rendered id value, including false and numeric values, through its serialized string.
109
+ - Preserved signed Geometry constraint directions, rejected invalid sweep/export channels through Sevgi error families,
110
+ and validated raw output paths before expansion or rendering.
111
+ - Made callable module configuration copy-owned and freeze-aware, document subclasses inherit their nearest profile, and
112
+ false executor boot receivers remain explicit rather than defaulting to an internal scope.
113
+ - Made element trees, attributes, identifiers, locator results, document profiles, Derender nodes, shell results, and
114
+ executor results retain owned immutable snapshots where their public contracts promise value semantics.
115
+ - Corrected Shell combined-output separators, nil export density errors, document render-option routing, executor source
116
+ snapshots, selected-node namespace/whitespace documentation, and Standard character-data validation.
117
+
118
+ ### Removed
119
+
120
+ - Removed public access to pluralization tables and the internal SVG save extension; pluralization rules are now deeply
121
+ immutable.
122
+ - Removed public documentation and constant access for command-line implementation modules; the `sevgi` and `igves`
123
+ executables remain unchanged.
124
+ - Removed eager loading and public documentation of private Showcase build/test support; explicit support entrypoints
125
+ now keep the harness under the Showcase namespace.
126
+ - Removed public access to document profile name normalizers; registry operations retain them as private plumbing.
127
+ - Removed the redundant `Margin.margin` constructor; use canonical bracket notation.
128
+ - Removed public executor orchestration, obsolete callable-module hooks, the old public attribute syntax constants, and
129
+ direct construction of abstract or internally wired Content, Element, and Grid query types.
130
+ - Removed accidental public access to abstract Lined factories, internal element-name/export maps, result/location
131
+ bracket constructors, and direct Stop construction.
132
+
8
133
  ## 0.95.0 - 2026-07-11
9
134
 
10
135
  ### Security
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sevgi Derender
2
2
 
3
- Converts SVG/XML content back into Sevgi DSL source.
3
+ Sevgi Derender converts SVG or XML back into Sevgi DSL source.
4
4
 
5
5
  ## Install
6
6
 
@@ -19,26 +19,41 @@ require "sevgi/derender"
19
19
  ```ruby
20
20
  source = "<svg><rect width=\"3\" height=\"5\"/></svg>"
21
21
  Sevgi::Derender.derender(source)
22
+
23
+ source = '<svg><rect id="mark" style="fill: red" width="3"/></svg>'
24
+ Sevgi::Derender.derender(source, omit: %i[id style])
22
25
  ```
23
26
 
24
27
  ## Executable
25
28
 
26
29
  ```sh
27
- igves drawing.svg
30
+ igves --omit id --omit style drawing.svg
31
+ ```
32
+
33
+ Omit the file or pass `-` to read SVG from standard input:
34
+
35
+ ```sh
36
+ igves --omit id < drawing.svg
37
+ ```
38
+
39
+ `igves` prints generated Sevgi source. The umbrella `sevgi` gem also installs `igsev`, which evaluates that source and
40
+ prints normalized SVG:
41
+
42
+ ```sh
43
+ igsev --omit id --omit style < drawing.svg
28
44
  ```
29
45
 
30
46
  ## Ruby compatibility
31
47
 
32
- Requires Ruby 3.4.0 or newer. CI verifies Ruby 3.4.0 and the current development Ruby from `.ruby-version`.
48
+ Requires Ruby 3.4.0 or newer. CI verifies the current Ruby 3.4 release and the development Ruby from `.ruby-version`.
33
49
 
34
50
  ## Native prerequisites
35
51
 
36
- None beyond Ruby and this gem's Ruby dependencies. Nokogiri may use platform packages depending on the target Ruby
37
- platform.
52
+ This gem needs no native libraries of its own. Nokogiri may use platform packages depending on the target Ruby platform.
38
53
 
39
54
  ## Links
40
55
 
41
- - Documentation: https://sevgi.roktas.dev
42
- - API documentation: https://www.rubydoc.info/gems/sevgi-derender
43
- - Source: https://github.com/roktas/sevgi/tree/main/derender
44
- - Changelog: https://github.com/roktas/sevgi/blob/main/CHANGELOG.md
56
+ - Documentation: <https://sevgi.roktas.dev>
57
+ - API documentation: <https://www.rubydoc.info/gems/sevgi-derender>
58
+ - Source: <https://github.com/roktas/sevgi/tree/main/derender>
59
+ - Changelog: <https://github.com/roktas/sevgi/blob/main/CHANGELOG.md>
data/bin/igves CHANGED
@@ -3,4 +3,4 @@
3
3
 
4
4
  require "sevgi/binaries/igves"
5
5
 
6
- Sevgi::Binaries::Igves.(ARGV)
6
+ Sevgi.const_get(:Binaries, false)::Igves.(ARGV)
@@ -3,8 +3,11 @@
3
3
  require "sevgi"
4
4
 
5
5
  module Sevgi
6
+ # Command-line entrypoint implementations shipped with Sevgi.
7
+ # @api private
6
8
  module Binaries
7
9
  # Implements the `igves` executable that converts SVG files into Sevgi DSL source.
10
+ # @api private
8
11
  module Igves
9
12
  extend self
10
13
 
@@ -14,16 +17,33 @@ module Sevgi
14
17
  # Error raised for invalid command-line usage.
15
18
  Error = Class.new(::Sevgi::Error)
16
19
 
20
+ FLAGS = {
21
+ "--exception" => :vomit,
22
+ "--help" => :help,
23
+ "--version" => :version,
24
+ "-h" => :help,
25
+ "-v" => :version,
26
+ "-x" => :vomit
27
+ }.freeze
28
+ private_constant :FLAGS
29
+
17
30
  # Parsed command-line options for the `igves` executable.
18
31
  # @api private
19
- Options = Struct.new(:vomit, :help, :version) do
32
+ Options = Struct.new(:vomit, :help, :version, :omit) do
20
33
  # Parses command-line options and removes them from the argv array.
21
34
  # @param argv [Array<String>] mutable command-line argument array
22
35
  # @return [Sevgi::Binaries::Igves::Options] parsed options
23
36
  # @raise [Sevgi::Binaries::Igves::Error] when an option is not recognized
24
37
  def self.parse(argv)
25
38
  new.tap do |options|
26
- argv.first.start_with?("-") ? option(argv, options) : break until argv.empty?
39
+ until argv.empty? || argv.first == "-" || !argv.first.start_with?("-")
40
+ if argv.first == "--"
41
+ argv.shift
42
+ break
43
+ end
44
+
45
+ option(argv, options)
46
+ end
27
47
  end
28
48
  end
29
49
 
@@ -31,16 +51,11 @@ module Sevgi
31
51
  private
32
52
 
33
53
  def option(argv, options)
34
- case (arg = argv.shift)
35
- when "-x", "--exception"
36
- options.vomit = true
37
- when "-h", "--help"
38
- options.help = true
39
- when "-v", "--version"
40
- options.version = true
41
- else
42
- Error.("Not a valid option: #{arg}")
43
- end
54
+ arg = argv.shift
55
+ return options[FLAGS[arg]] = true if FLAGS.key?(arg)
56
+ return (options.omit ||= []) << (argv.shift || Error.("No attribute given for --omit")) if arg == "--omit"
57
+
58
+ Error.("Not a valid option: #{arg}")
44
59
  end
45
60
  end
46
61
  end
@@ -53,11 +68,11 @@ module Sevgi
53
68
  # @raise [Sevgi::ArgumentError] when the SVG file cannot be found
54
69
  # @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
55
70
  # @raise [StandardError] when `--exception` or `SEVGI_VOMIT` requests raw errors
56
- # @raise [SystemExit] when command-line usage aborts
71
+ # @raise [SystemExit] when argv does not match `[options...] [--] [file|-]` or command-line usage aborts
57
72
  def call(argv)
58
73
  dispatch(Array(argv))
59
74
  rescue Binaries::Igves::Error => e
60
- abort(e.message)
75
+ abort("#{e.message}\n\n#{help}")
61
76
  end
62
77
 
63
78
  private
@@ -67,7 +82,7 @@ module Sevgi
67
82
  return puts(help) if options.help
68
83
  return puts(::Sevgi::VERSION) if options.version
69
84
 
70
- print_file(argv.shift, options)
85
+ print_file(operand(argv), options)
71
86
  rescue Binaries::Igves::Error
72
87
  raise
73
88
  rescue ::StandardError => e
@@ -92,22 +107,32 @@ module Sevgi
92
107
 
93
108
  def help
94
109
  <<~HELP
95
- Usage: #{PROGNAME} [options...] <SVG file>
110
+ Usage: #{PROGNAME} [options...] [--] [SVG file|-]
96
111
 
97
112
  See documentation for detailed help.
98
113
 
99
114
  Options:
100
115
 
116
+ --omit ATTRIBUTE Omit an attribute (repeatable)
101
117
  -x, --exception Raise exception instead of abort
118
+ -- Stop option parsing
119
+
102
120
  -h, --help Show this help
103
121
  -v, --version Display version
104
122
  HELP
105
123
  end
106
124
 
107
- def run(file, _options)
108
- Error.("No SVG file given.") unless file
125
+ def operand(argv)
126
+ file = argv.shift
127
+ Error.("Unexpected argument: #{argv.first}") unless argv.empty?
109
128
 
110
- Derender.derender_file(file)
129
+ file unless file == "-"
130
+ end
131
+
132
+ def run(file, options)
133
+ return Derender.derender_file(file, omit: options.omit) if file
134
+
135
+ Derender.derender($stdin.read, omit: options.omit)
111
136
  end
112
137
 
113
138
  def print_file(file, options)
@@ -125,4 +150,6 @@ module Sevgi
125
150
  end
126
151
  end
127
152
  end
153
+
154
+ private_constant :Binaries
128
155
  end
@@ -38,8 +38,12 @@ module Sevgi
38
38
  key = Css.to_key(key) if key.is_a?(::String)
39
39
 
40
40
  if key == "style"
41
- style = Css.to_h!(value)
42
- style.empty? ? "{}" : "{ #{Attributes.decompile(style)} }"
41
+ style = Css.declarations(value)
42
+ if style
43
+ style.empty? ? "{}" : "{ #{Attributes.decompile(style)} }"
44
+ else
45
+ Ruby.literal(value)
46
+ end
43
47
  elsif value.is_a?(::String)
44
48
  Css.to_value(value)
45
49
  elsif value.is_a?(::Hash)
@@ -5,6 +5,7 @@ require "nokogiri"
5
5
  module Sevgi
6
6
  module Derender
7
7
  # Parsed SVG/XML document wrapper used by the derender pipeline.
8
+ # @api private
8
9
  class Document
9
10
  # Loads and parses an SVG/XML file.
10
11
  #
@@ -13,7 +14,7 @@ module Sevgi
13
14
  # @param path [String] path to the source file, with or without `.svg` extension
14
15
  # @return [Sevgi::Derender::Document] document wrapper
15
16
  # @raise [Sevgi::ArgumentError] when the file cannot be found or file content is malformed XML
16
- # @raise [Errno::EACCES] when the file cannot be read
17
+ # @raise [SystemCallError] when the file cannot be read
17
18
  def self.load_file(path)
18
19
  entry = ::File.expand_path(F.qualify(path, "svg"))
19
20
 
@@ -65,10 +66,11 @@ module Sevgi
65
66
  end
66
67
 
67
68
  # Converts the root or selected node into a derender node.
68
- # @param id [String, nil] optional SVG id selecting a node inside the document
69
+ # @param id [String, Symbol, nil] optional SVG id selecting a node inside the document
70
+ # @param omit [String, Symbol, Array<String, Symbol>, nil] attribute name or names omitted from the selected subtree
69
71
  # @return [Sevgi::Derender::Node] selected node in the derender tree
70
72
  # @raise [Sevgi::ArgumentError] when the document has no root element or the id is absent
71
- def decompile(id = nil)
73
+ def decompile(id = nil, omit: nil)
72
74
  if id
73
75
  if (found = doc.xpath("//*[@id=#{xpath_literal(id)}]") || []).empty?
74
76
  ArgumentError.("No such element with id '#{id}' in document")
@@ -81,7 +83,7 @@ module Sevgi
81
83
 
82
84
  ArgumentError.("XML document has no root element") unless element
83
85
 
84
- Node.new(element, pres, namespaces: namespace_scope(element))
86
+ Node.send(:new, element, pres, namespaces: namespace_scope(element), omit: omissions(omit))
85
87
  end
86
88
 
87
89
  # Returns XML declaration and pre-root nodes preserved for root decompilation. The result contains only String
@@ -109,6 +111,10 @@ module Sevgi
109
111
  end
110
112
  end
111
113
 
114
+ def omissions(value)
115
+ Array(value).to_h { [it.to_s.dup.freeze, true] }.freeze
116
+ end
117
+
112
118
  def xpath_literal(value)
113
119
  value = value.to_s
114
120
 
@@ -10,7 +10,7 @@ module Sevgi
10
10
  # @return [Array<String>] unformatted Ruby source lines
11
11
  def decompile(*)
12
12
  if children.any?
13
- children.one? && children.first.node.text? ? Array(leaf(has_attributes: attributes.any?)) : tree
13
+ children.one? && children.first.send(:text?) ? Array(leaf(has_attributes: attributes.any?)) : tree
14
14
  else
15
15
  Array(leaf(has_content: false))
16
16
  end
@@ -19,7 +19,7 @@ module Sevgi
19
19
  private
20
20
 
21
21
  def leaf(has_content: true, has_attributes: true)
22
- attributes = attributes!
22
+ attributes = all_attributes
23
23
  args = []
24
24
  args << Ruby.literal(content) if has_content
25
25
  args << Attributes.decompile(attributes) if has_attributes && attributes.any?
@@ -40,7 +40,7 @@ module Sevgi
40
40
  def tree
41
41
  [
42
42
  "#{leaf(has_content: false)} do",
43
- *children.map(&:decompile).flatten,
43
+ *children.map { it.send(:decompile) }.flatten,
44
44
  "end"
45
45
  ]
46
46
  end
@@ -9,20 +9,27 @@ module Sevgi
9
9
  # Converts a style node into unformatted Sevgi DSL lines.
10
10
  # @return [Array<String>] unformatted Ruby source lines
11
11
  def decompile(*)
12
- return [] unless (lines = css_lines)
12
+ return raw_style unless (lines = css_lines)
13
13
 
14
14
  [
15
15
  "css({",
16
16
  *lines,
17
- "})",
17
+ "}, #{css_attributes})",
18
18
  ""
19
19
  ]
20
20
  end
21
21
 
22
22
  private
23
23
 
24
+ def css_attributes
25
+ attributes = all_attributes
26
+ source = Attributes.decompile(attributes)
27
+
28
+ attributes.key?("type") ? source : [source, "type: nil"].reject(&:empty?).join(", ")
29
+ end
30
+
24
31
  def css_lines
25
- return unless (hash = Css.to_h(node.content))
32
+ return unless (hash = Css.rules(node.content))
26
33
 
27
34
  hash
28
35
  .map do |selector, declarations|
@@ -34,6 +41,13 @@ module Sevgi
34
41
  end
35
42
  .flatten
36
43
  end
44
+
45
+ def raw_style
46
+ arguments = ["Sevgi::Graphics::Content.cdata(#{Ruby.literal(node.content)})"]
47
+ arguments << Attributes.decompile(all_attributes) if all_attributes.any?
48
+
49
+ ["style #{arguments.join(", ")}", ""]
50
+ end
37
51
  end
38
52
  end
39
53
  end
@@ -17,7 +17,7 @@ module Sevgi
17
17
 
18
18
  lines.unshift(
19
19
  [
20
- "SVG.document preambles: [",
20
+ "SVG.Document preambles: [",
21
21
  *pres.map { "#{Ruby.literal(it)}," },
22
22
  "]",
23
23
  ""
@@ -29,9 +29,6 @@ module Sevgi
29
29
  # @return [String]
30
30
  def element = "SVG"
31
31
 
32
- # Returns root attributes merged with namespace declarations.
33
- # @return [Hash{String => String}] attributes and namespaces
34
- def attributes! = {**attributes, **namespaces}
35
32
  end
36
33
  end
37
34
  end
@@ -14,7 +14,7 @@ module Sevgi
14
14
  # @param node [Sevgi::Derender::Node] derender node
15
15
  # @return [Sevgi::Graphics::Element, nil] included element, or nil when the node does not produce graphics output
16
16
  def append(node)
17
- case node.type
17
+ case node.send(:type)
18
18
  when :CSS
19
19
  append_css(node)
20
20
  when :Text
@@ -29,21 +29,33 @@ module Sevgi
29
29
  attr_reader :parent
30
30
 
31
31
  def append_css(node)
32
- return unless (hash = Css.to_h(node.node.content))
32
+ content = if (hash = Css.rules(node.content))
33
+ Graphics::Content.css(hash)
34
+ else
35
+ Graphics::Content.cdata(node.content)
36
+ end
33
37
 
34
- build(:style, Graphics::Content.css(hash), type: "text/css", **node.attributes)
38
+ build(:style, content, **node.send(:all_attributes))
35
39
  end
36
40
 
37
41
  def append_element(node)
38
42
  contents = contents(node)
39
43
 
40
- build(node.name, *contents, **node.attributes!).tap do |element|
44
+ build(node.name, *contents, **attributes(node)).tap do |element|
41
45
  node.children.each { self.class.new(element).append(it) } if contents.empty?
42
46
  end
43
47
  end
44
48
 
49
+ def attributes(node)
50
+ attributes = node.send(:all_attributes)
51
+ return attributes unless (style = attributes["style"])
52
+ return attributes unless (declarations = Css.declarations(style))
53
+
54
+ {**attributes, "style" => declarations}
55
+ end
56
+
45
57
  def contents(node)
46
- node.children.one? && node.children.first.node.text? ? [node.content] : []
58
+ node.children.one? && node.children.first.send(:text?) ? [node.content] : []
47
59
  end
48
60
 
49
61
  def build(name, *contents, **attributes)
@@ -27,6 +27,30 @@ module Sevgi
27
27
  hash ? hash.fetch("*", {}) : {}
28
28
  end
29
29
 
30
+ # Returns CSS rules only when Hash conversion preserves the source declarations.
31
+ # @param css_string [String] CSS rule source
32
+ # @return [Hash, nil] losslessly representable rules, or nil
33
+ def rules(css_string)
34
+ parsed = to_h(css_string)
35
+ sourced = source_rules(css_string)
36
+
37
+ parsed if parsed == sourced
38
+ rescue ::StandardError
39
+ nil
40
+ end
41
+
42
+ # Returns inline declarations only when Hash conversion preserves the source declarations.
43
+ # @param style_string [String] inline CSS declaration source
44
+ # @return [Hash, nil] losslessly representable declarations, or nil
45
+ def declarations(style_string)
46
+ parsed = to_h!(style_string)
47
+ sourced = source_declarations(style_string)
48
+
49
+ parsed if parsed == sourced
50
+ rescue ::StandardError
51
+ nil
52
+ end
53
+
30
54
  # Converts a CSS key into a Ruby hash key.
31
55
  # @param arg [String] CSS key
32
56
  # @return [String] Ruby hash key source
@@ -43,6 +67,47 @@ module Sevgi
43
67
  # @return [String] Ruby literal or numeric source
44
68
  def to_value(arg) = (arg.to_f.to_s == arg) || (arg.to_i.to_s == arg) ? arg : arg.inspect
45
69
 
70
+ private
71
+
72
+ def source_declarations(source)
73
+ declarations = source.split(";", -1).map(&:strip).reject(&:empty?)
74
+ pairs = declarations.map { source_declaration(it) }
75
+ return if pairs.any?(&:nil?)
76
+ return unless pairs.map(&:first).uniq.size == pairs.size
77
+
78
+ pairs.to_h
79
+ end
80
+
81
+ def source_declaration(source)
82
+ key, value = source.split(":", 2).map(&:strip)
83
+
84
+ [key, value] if key && value && !key.empty? && !value.empty?
85
+ end
86
+
87
+ def source_rules(source)
88
+ source = source.dup
89
+ rules = {}
90
+
91
+ until source.strip.empty?
92
+ selector, declarations, source = source_rule(source)
93
+ return unless selector && !rules.key?(selector)
94
+
95
+ rules[selector] = declarations
96
+ end
97
+
98
+ rules
99
+ end
100
+
101
+ def source_rule(source)
102
+ match = /\A\s*([^{}]+?)\s*\{([^{}]*)\}\s*/m.match(source)
103
+ return [nil, nil, source] unless match
104
+
105
+ selector = match[1].strip
106
+ declarations = source_declarations(match[2])
107
+ selector = nil if selector.empty? || selector.start_with?("@") || declarations.nil?
108
+ [selector, declarations, match.post_match]
109
+ end
110
+
46
111
  extend self
47
112
  end
48
113
 
@@ -133,11 +198,17 @@ module Sevgi
133
198
  private
134
199
 
135
200
  def receiver_collision?(name)
136
- document = Graphics::Document.const_get(:Base, false)
201
+ documents = [Graphics::Document::Proto]
202
+ documents.concat(Graphics::Document.keys.map { Graphics::Document.fetch(it) })
137
203
 
138
- document.ancestors.any? do |ancestor|
139
- ancestor.method_defined?(name, false) || ancestor.private_method_defined?(name, false)
140
- end
204
+ documents.uniq.any? { effective_collision?(it, name) }
205
+ end
206
+
207
+ def effective_collision?(document, name)
208
+ return false unless document.method_defined?(name) || document.private_method_defined?(name)
209
+
210
+ method = document.instance_method(name)
211
+ method.owner != Graphics::Element || !Graphics::Element.send(:element_method?, name)
141
212
  end
142
213
 
143
214
  extend self
@@ -19,121 +19,153 @@ module Sevgi
19
19
 
20
20
  private_constant :Namespace
21
21
 
22
- # Node in a derender tree produced from an SVG/XML node.
22
+ META_NAMESPACE = "_:"
23
+ private_constant :META_NAMESPACE
24
+
25
+ # Captures owned Node state during construction.
26
+ # @api private
27
+ module Capture
28
+ private
29
+
30
+ def capture_context(pres, namespaces)
31
+ @pres = pres.map { it.to_s.dup.freeze }.freeze
32
+ @namespaces = snapshot(namespaces || local_namespaces)
33
+ @type = dispatch
34
+ singleton_class.send(:private, :decompile)
35
+ end
36
+
37
+ def capture_children
38
+ @children = node
39
+ .children
40
+ .map { self.class.send(:new, it, omit: @omit, top: false) }
41
+ .reject { ignorable_child?(it) }
42
+ .freeze
43
+ end
44
+
45
+ def capture_values
46
+ @attributes = snapshot(
47
+ node.attribute_nodes.filter_map do |attribute|
48
+ key = attribute_key(attribute)
49
+ [key, attribute.value] unless @omit.key?(key)
50
+ end
51
+ )
52
+ @meta = snapshot(attributes.filter_map { |key, value| metadata(key, value) })
53
+ end
54
+
55
+ def capture_identity
56
+ @content = (preserve_space? ? node.content : normalized_content).dup.freeze
57
+ @name = [node.namespace&.prefix, node.name].compact.join(":").freeze
58
+ end
59
+
60
+ def metadata(key, value)
61
+ [key.delete_prefix(META_NAMESPACE), value] if key.start_with?(META_NAMESPACE)
62
+ end
63
+
64
+ def snapshot(pairs)
65
+ pairs
66
+ .each_with_object({}) do |(key, value), result|
67
+ result[key.to_s.dup.freeze] = value.to_s.dup.freeze
68
+ end
69
+ .freeze
70
+ end
71
+ end
72
+
73
+ private_constant :Capture
74
+
75
+ # Immutable conversion result for one SVG/XML node.
76
+ #
77
+ # Attributes, namespaces, content, and descendants are owned snapshots; parser objects and dispatch strategies
78
+ # remain internal to Derender. Attributes omitted during decompilation are absent throughout the captured subtree.
23
79
  class Node
24
- # Returns the source XML node.
25
- # @return [Nokogiri::XML::Node]
26
- attr_reader :node
80
+ include Capture
81
+
82
+ private_class_method :new
27
83
 
28
- # Returns preamble XML lines carried by the root node.
29
- # @return [Array<String>]
30
- attr_reader :pres
84
+ # Returns immutable XML attributes.
85
+ # @return [Hash{String => String}] owned attribute snapshot
86
+ attr_reader :attributes
31
87
 
32
- # Returns the dispatch type used by derender element strategies.
33
- # @return [Symbol]
34
- attr_reader :type
88
+ # Returns immutable child nodes.
89
+ # @return [Array<Sevgi::Derender::Node>] owned child snapshot
90
+ attr_reader :children
35
91
 
36
- # Builds a derender node.
92
+ # 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.
94
+ # @return [String] frozen owned text snapshot
95
+ attr_reader :content
96
+
97
+ # Returns the immutable qualified element name.
98
+ # @return [String] owned element name
99
+ attr_reader :name
100
+
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.
103
+ # @return [Hash{String => String}] frozen owned namespace snapshot
104
+ attr_reader :namespaces
105
+
106
+ # Builds an owned derender result.
37
107
  # @param node [Nokogiri::XML::Node] source XML node
38
108
  # @param pres [Array<String>] preamble XML lines carried by the root node
39
- # @param namespaces [Hash{String => String}, nil] namespace declarations to emit on this node; selected roots use
40
- # their full inherited namespace scope, while ordinary children use only declarations from their own element
109
+ # @param namespaces [Hash{String => String}, nil] namespace declarations to emit on this node
110
+ # @param omit [Hash{String => Boolean}, nil] normalized attribute omission set shared by the captured subtree
41
111
  # @param top [Boolean] true when this node is the root of the current conversion
42
112
  # @return [void]
43
- def initialize(node, pres = [], namespaces: nil, top: true)
113
+ # @api private
114
+ def initialize(node, pres = [], namespaces: nil, omit: nil, top: true)
44
115
  @node = node
45
- @pres = pres
46
- @namespaces = namespaces
116
+ @omit = omit || {}.freeze
47
117
  @top = top
48
- @type = dispatch
118
+ capture_context(pres, namespaces)
119
+ capture_values
120
+ capture_children
121
+ capture_identity
122
+ freeze
49
123
  end
50
124
 
51
- # Attribute namespace prefix used for Sevgi metadata.
52
- META_NAMESPACE = "_:"
53
-
54
125
  # Returns Sevgi metadata attributes without the metadata namespace prefix.
55
- # @return [Hash{String => String}] metadata attributes
56
- def _
57
- @_ ||= attributes
58
- .slice(
59
- *attributes.keys.select { |key| key.start_with?(META_NAMESPACE) }
60
- )
61
- .transform_keys! { |key| key.delete_prefix(META_NAMESPACE) }
62
- end
126
+ # @return [Hash{String => String}] immutable metadata snapshot
127
+ def _ = @meta
63
128
 
64
129
  alias meta _
65
130
 
66
- # Returns source XML attributes keyed with namespace prefixes when present.
67
- # @return [Hash{String => String}] XML attributes
68
- def attributes = @attributes ||= node.attribute_nodes.to_h { [attribute_key(it), it.value] }
69
-
70
- # Returns source XML attributes and namespace declarations emitted on this node.
71
- # @return [Hash{String => String}] XML attributes
72
- def attributes! = {**attributes, **namespaces}
73
-
74
- # Returns non-ignorable child derender nodes.
75
- # @return [Array<Sevgi::Derender::Node>] child nodes
76
- def children = @children ||= node.children.map { self.class.new(it, top: false) }.reject { ignorable_child?(it) }
77
-
78
- # Returns node text content.
79
- #
80
- # `xml:space="preserve"` keeps content verbatim. Default-space text nodes are stripped for ordinary pretty-printed
81
- # content; line-break indentation around mixed-content children is removed while inline boundary spaces and tabs are
82
- # kept because they affect rendered SVG text.
83
- # @return [String]
84
- def content = @content ||= preserve_space? ? node.content : normalized_content
85
-
86
131
  # Converts this node into formatted Sevgi DSL Ruby source.
87
132
  # @return [String] formatted Sevgi DSL source
88
133
  # @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
89
134
  # @note Foreign namespace elements use the explicit `Element` DSL path, and nested `svg` nodes remain elements.
90
135
  # @note Unsafe bare Ruby names are emitted through the explicit `Element` DSL word.
91
- def derender = Ruby.format(decompile(pres).join("\n"))
92
-
93
- # Returns the Sevgi DSL element name for this node.
94
- # @return [String]
95
- def element = name
136
+ def derender = Ruby.format(decompile(@pres).join("\n"))
96
137
 
97
138
  # Evaluates this node under a graphics element.
98
139
  # @param element [Sevgi::Graphics::Element] target graphics element
99
- # @param include_current [Boolean] true to evaluate this node, false to evaluate only children
100
- # @return [Sevgi::Graphics::Element, Array<Sevgi::Graphics::Element>, nil] included current element, included child
101
- # elements when include_current is false, or nil when the node does not produce graphics output
140
+ # @return [Sevgi::Graphics::Element, nil] included current element, or nil when it produces no graphics output
102
141
  # @note Namespace declarations, qualified attributes, significant text, and nested `svg` nodes are preserved.
103
- def evaluate(element, include_current = true)
104
- return Evaluator.new(element).append(self) if include_current
105
-
106
- evaluate_children(element)
107
- end
142
+ def evaluate(element) = Evaluator.new(element).append(self)
108
143
 
109
144
  # Evaluates only this node's children under a graphics element.
110
145
  # @param element [Sevgi::Graphics::Element] target graphics element
111
- # @return [Array<Sevgi::Graphics::Element>] included child elements
112
- def evaluate_children(element) = children.map { it.evaluate(element) }.compact
146
+ # @return [Array<Sevgi::Graphics::Element>] immutable included-child snapshot
147
+ def evaluate_children(element) = children.filter_map { it.evaluate(element) }.freeze
113
148
 
114
- # Finds the first descendant whose attribute matches a value.
149
+ # Finds this node or the first descendant whose attribute matches a value.
115
150
  # @param arg [String] attribute value to find
116
151
  # @param by [String] attribute name used for lookup
117
- # @return [Sevgi::Derender::Node, nil] matching node, or nil
152
+ # @return [Sevgi::Derender::Node, nil] matching immutable node, or nil
153
+ # @note Attributes omitted during decompilation are unavailable for later searches.
118
154
  def find(arg, by: "id")
119
155
  return self if attributes[by] == arg
120
156
 
121
157
  children.lazy.map { it.find(arg, by:) }.find(&:itself)
122
158
  end
123
159
 
124
- # Returns the source XML qualified name, including its namespace prefix when present.
125
- # @return [String]
126
- def name = @name ||= [node.namespace&.prefix, node.name].compact.join(":")
160
+ # Reports whether this node represents an SVG document root.
161
+ # @return [Boolean] true for the conversion root strategy
162
+ def root? = @type == :Root
127
163
 
128
- # Returns source XML namespace declarations emitted on this node.
129
- # @return [Hash{String => String}] namespace declarations
130
- def namespaces = (@namespaces ||= local_namespaces)
164
+ private
131
165
 
132
- # Reports whether this node is the SVG root strategy.
133
- # @return [Boolean]
134
- def root? = type == :Root
166
+ attr_reader :node, :type
135
167
 
136
- private
168
+ def all_attributes = {**attributes, **namespaces}.freeze
137
169
 
138
170
  def attribute_key(attribute) = [attribute.namespace&.prefix, attribute.name].compact.join(":")
139
171
 
@@ -153,13 +185,16 @@ module Sevgi
153
185
  .tap { extend(Elements.const_get(it)) }
154
186
  end
155
187
 
188
+ def element = name
189
+
156
190
  def ignorable_child?(child)
157
- child.type == :Junk ||
158
- (child.node.text? && child.node.text.strip.empty? && !child.preserve_space? && !child.inline_text?)
191
+ child.send(:type) == :Junk ||
192
+ (child.send(:text?) &&
193
+ child.content.strip.empty? &&
194
+ !child.send(:preserve_space?) &&
195
+ !child.send(:inline_text?))
159
196
  end
160
197
 
161
- protected
162
-
163
198
  def preserve_space?
164
199
  each_node do |current|
165
200
  case xml_space(current)
@@ -175,8 +210,6 @@ module Sevgi
175
210
 
176
211
  def inline_text? = mixed_text? && !node.content.match?(/[\r\n]/)
177
212
 
178
- private
179
-
180
213
  def normalized_content
181
214
  return node.content if mixed_text? && inline_text?
182
215
  return node.content.strip unless mixed_text?
@@ -203,6 +236,8 @@ module Sevgi
203
236
  end
204
237
  end
205
238
 
239
+ def text? = node.text?
240
+
206
241
  def xml_space(current)
207
242
  return unless current.respond_to?(:attribute_nodes)
208
243
 
@@ -3,6 +3,6 @@
3
3
  module Sevgi
4
4
  module Derender
5
5
  # Current version of the Sevgi derender gem.
6
- VERSION = "0.95.0"
6
+ VERSION = "0.98.2"
7
7
  end
8
8
  end
@@ -11,7 +11,13 @@ require_relative "derender/node"
11
11
  require_relative "derender/version"
12
12
 
13
13
  module Sevgi
14
- # Converts SVG/XML content into Sevgi DSL source or evaluates it into graphics elements.
14
+ # Brings editor-authored SVG/XML into programmatic Sevgi workflows.
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,
18
+ # or graphics elements under an existing document. This lets editor-authored geometry participate in programmatic
19
+ # composition, styling, layout, and output. Use it when SVG/XML is a real input artifact, not as an intermediate
20
+ # authoring format for ordinary Sevgi drawing code.
15
21
  #
16
22
  # Generated source uses bare DSL calls only for recognized element names that cannot dispatch to an existing Ruby or
17
23
  # Sevgi method. Other XML names use the explicit `Element` DSL word so executing generated source preserves the XML
@@ -24,154 +30,166 @@ module Sevgi
24
30
  # declarations, qualified attributes, significant text, and nested `svg` elements survive source generation and direct
25
31
  # evaluation. CSS specialization applies only to unqualified `style` elements in no namespace or the default SVG
26
32
  # namespace; the document-root strategy additionally requires an unqualified `svg` at the root of the conversion.
33
+ # Simple CSS rules use the readable `css({...})` DSL form. At-rules, duplicate declarations, and other CSS that cannot
34
+ # be represented losslessly as a Hash remain owned raw style content.
35
+ #
36
+ # Attribute omission uses exact, case-sensitive names across the selected subtree. ID selection happens first;
37
+ # namespace declarations remain intact, and omitting the `style` attribute does not omit `style` elements.
38
+ #
39
+ # @example Inspect, select, and convert an immutable result
40
+ # result = Sevgi::Derender.decompile('<svg><rect id="mark" width="10"/></svg>')
41
+ # mark = result.find("mark")
42
+ # mark.attributes #=> {"id"=>"mark", "width"=>"10"}
43
+ # mark.derender #=> "rect id: \"mark\", width: 10\n"
44
+ # @example Preserve an at-rule as raw style content
45
+ # Sevgi::Derender.derender("<style>@media print { rect { fill: black; } }</style>")
46
+ # #=> "style Sevgi::Graphics::Content.cdata(\"@media print { rect { fill: black; } }\")\n"
47
+ # @example Select a node while omitting editor-only attributes
48
+ # source = '<svg><g id="mark" style="fill: red"><rect/></g></svg>'
49
+ # Sevgi::Derender.derender(source, id: "mark", omit: %i[id style])
50
+ # #=> "g do\n rect\nend\n"
51
+ # @see https://sevgi.roktas.dev/derender/ Derender guide
27
52
  module Derender
28
- # @!method self.decompile(content, id: nil)
29
- # Converts SVG/XML content into a derender node.
30
- # @param content [String] SVG/XML source content
31
- # @param id [String, nil] optional SVG id selecting a node inside the source
32
- # @return [Sevgi::Derender::Node] selected node in the derender tree
33
- # @raise [Sevgi::ArgumentError] when content is malformed or rootless, or when the id is absent
34
- # @!method self.decompile_file(file, id: nil)
35
- # Converts an SVG/XML file into a derender node.
36
- # @param file [String] path to the source SVG/XML file
37
- # @param id [String, nil] optional SVG id selecting a node inside the source
38
- # @return [Sevgi::Derender::Node] selected node in the derender tree
39
- # @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
40
- # absent
41
- # @!method self.derender(content, id: nil)
42
- # Converts SVG/XML content into Sevgi DSL Ruby source.
43
- # @param content [String] SVG/XML source content
44
- # @param id [String, nil] optional SVG id selecting a node inside the source
45
- # @return [String] formatted Sevgi DSL source
46
- # @raise [Sevgi::ArgumentError] when content is malformed or rootless, or when the id is absent
47
- # @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
48
- # @note Namespace-aware dispatch preserves ordinary foreign/qualified nodes and nested SVG elements.
49
- # @note Unsafe bare Ruby names are emitted through the explicit `Element` DSL word.
50
- # @!method self.derender_file(file, id: nil)
51
- # Converts an SVG/XML file into Sevgi DSL Ruby source.
52
- # @param file [String] path to the source SVG/XML file
53
- # @param id [String, nil] optional SVG id selecting a node inside the source
54
- # @return [String] formatted Sevgi DSL source
55
- # @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
56
- # absent
57
- # @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
58
- # @note Namespace-aware dispatch preserves ordinary foreign/qualified nodes and nested SVG elements.
59
- # @note Unsafe bare Ruby names are emitted through the explicit `Element` DSL word.
60
- # @!method self.evaluate(content, element, id: nil)
61
- # Evaluates SVG/XML content under a graphics element, including the selected node.
62
- # @param content [String] SVG/XML source content
63
- # @param element [Sevgi::Graphics::Element] target graphics element
64
- # @param id [String, nil] optional SVG id selecting a node inside the source
65
- # @return [Sevgi::Graphics::Element, nil] included selected/root graphics element, or nil when the selected node
66
- # produces no graphics output
67
- # @raise [Sevgi::ArgumentError] when content is malformed or rootless, or when the id is absent
68
- # @note Namespace-aware dispatch preserves ordinary foreign/qualified nodes and nested SVG elements.
69
- # @!method self.evaluate_children(content, element, id: nil)
70
- # Evaluates only the selected node's children under a graphics element.
71
- # @param content [String] SVG/XML source content
72
- # @param element [Sevgi::Graphics::Element] target graphics element
73
- # @param id [String, nil] optional SVG id selecting a node inside the source
74
- # @return [Array<Sevgi::Graphics::Element>] included child graphics elements
75
- # @raise [Sevgi::ArgumentError] when content is malformed or rootless, or when the id is absent
76
- # @note Namespace-aware dispatch preserves ordinary foreign/qualified nodes and nested SVG elements.
77
- # @!method self.evaluate_file(file, element, id: nil)
78
- # Evaluates an SVG/XML file under a graphics element, including the selected node.
79
- # @param file [String] path to the source SVG/XML file
80
- # @param element [Sevgi::Graphics::Element] target graphics element
81
- # @param id [String, nil] optional SVG id selecting a node inside the source
82
- # @return [Sevgi::Graphics::Element, nil] included selected/root graphics element, or nil when the selected node
83
- # produces no graphics output
84
- # @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
85
- # absent
86
- # @note Namespace-aware dispatch preserves ordinary foreign/qualified nodes and nested SVG elements.
87
- # @!method self.evaluate_file_children(file, element, id: nil)
88
- # Evaluates only the selected node's children from an SVG/XML file under a graphics element.
89
- # @param file [String] path to the source SVG/XML file
90
- # @param element [Sevgi::Graphics::Element] target graphics element
91
- # @param id [String, nil] optional SVG id selecting a node inside the source
92
- # @return [Array<Sevgi::Graphics::Element>] included child graphics elements
93
- # @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
94
- # absent
95
- # @note Namespace-aware dispatch preserves ordinary foreign/qualified nodes and nested SVG elements.
53
+ private_constant :Attributes, :Document, :Elements
96
54
 
97
- # Converts SVG/XML content into a derender node.
55
+ # Converts SVG/XML content into an immutable derender result.
98
56
  # @param content [String] SVG/XML source content
99
- # @param id [String, nil] optional SVG id selecting a node inside the source
100
- # @return [Sevgi::Derender::Node] selected node in the derender tree
57
+ # @param id [String, Symbol, nil] optional SVG id selecting a node inside the source
58
+ # @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
59
+ # subtree after id selection
60
+ # @return [Sevgi::Derender::Node] owned immutable selected node
101
61
  # @raise [Sevgi::ArgumentError] when content is malformed or rootless, or when the id is absent
102
- def decompile(content, id: nil) = Document.new(content).decompile(id)
62
+ # @example Inspect a selected immutable node
63
+ # root = Sevgi::Derender.decompile('<svg><g id="mark"><rect width="4"/></g></svg>')
64
+ # mark = root.find("mark")
65
+ # mark.name #=> "g"
66
+ # mark.children.first.name #=> "rect"
67
+ # mark.attributes["id"] #=> "mark"
68
+ # @see Sevgi::Derender.decompile_file
69
+ # @see Sevgi.Decompile
70
+ def self.decompile(content, id: nil, omit: nil) = Document.new(content).decompile(id, omit:)
103
71
 
104
- # Converts an SVG/XML file into a derender node.
72
+ # Converts an SVG/XML file into an immutable derender result.
105
73
  # @param file [String] path to the source SVG/XML file
106
- # @param id [String, nil] optional SVG id selecting a node inside the source
107
- # @return [Sevgi::Derender::Node] selected node in the derender tree
74
+ # @param id [String, Symbol, nil] optional SVG id selecting a node inside the source
75
+ # @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
76
+ # subtree after id selection
77
+ # @return [Sevgi::Derender::Node] owned immutable selected node
108
78
  # @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
109
79
  # absent
110
- def decompile_file(file, id: nil) = Document.load_file(file).decompile(id)
80
+ # @raise [SystemCallError] when the file cannot be read
81
+ # @see Sevgi::Derender.decompile
82
+ # @see Sevgi.DecompileFile
83
+ def self.decompile_file(file, id: nil, omit: nil) = Document.load_file(file).decompile(id, omit:)
111
84
 
112
85
  # Converts SVG/XML content into Sevgi DSL Ruby source.
113
86
  # @param content [String] SVG/XML source content
114
- # @param id [String, nil] optional SVG id selecting a node inside the source
87
+ # @param id [String, Symbol, nil] optional SVG id selecting a node inside the source
88
+ # @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
89
+ # subtree after id selection
115
90
  # @return [String] formatted Sevgi DSL source
116
91
  # @raise [Sevgi::ArgumentError] when content is malformed or rootless, or when the id is absent
117
92
  # @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
118
93
  # @note Namespace-aware dispatch preserves ordinary foreign/qualified nodes and nested SVG elements.
119
94
  # @note Unsafe bare Ruby names are emitted through the explicit `Element` DSL word.
120
- def derender(content, id: nil) = Document.new(content).decompile(id).derender
95
+ # @example Select first, then omit attributes from generated source
96
+ # xml = '<svg><g id="mark" style="fill: red"><rect id="part"/></g></svg>'
97
+ # Sevgi::Derender.derender(xml, id: :mark, omit: [:id, "style"])
98
+ # #=> "g do\n rect\nend\n"
99
+ # @see Sevgi::Derender.derender_file
100
+ # @see Sevgi.Derender
101
+ def self.derender(content, id: nil, omit: nil) = Document.new(content).decompile(id, omit:).derender
121
102
 
122
103
  # Converts an SVG/XML file into Sevgi DSL Ruby source.
123
104
  # @param file [String] path to the source SVG/XML file
124
- # @param id [String, nil] optional SVG id selecting a node inside the source
105
+ # @param id [String, Symbol, nil] optional SVG id selecting a node inside the source
106
+ # @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
107
+ # subtree after id selection
125
108
  # @return [String] formatted Sevgi DSL source
126
109
  # @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
127
110
  # absent
128
111
  # @raise [Sevgi::PanicError] when generated Ruby source cannot be formatted
112
+ # @raise [SystemCallError] when the file cannot be read
129
113
  # @note Namespace-aware dispatch preserves ordinary foreign/qualified nodes and nested SVG elements.
130
114
  # @note Unsafe bare Ruby names are emitted through the explicit `Element` DSL word.
131
- def derender_file(file, id: nil) = Document.load_file(file).decompile(id).derender
115
+ # @see Sevgi::Derender.derender
116
+ # @see Sevgi.DerenderFile
117
+ def self.derender_file(file, id: nil, omit: nil) = Document.load_file(file).decompile(id, omit:).derender
132
118
 
133
119
  # Evaluates SVG/XML content under a graphics element, including the selected node.
134
120
  # @param content [String] SVG/XML source content
135
121
  # @param element [Sevgi::Graphics::Element] target graphics element
136
- # @param id [String, nil] optional SVG id selecting a node inside the source
122
+ # @param id [String, Symbol, nil] optional SVG id selecting a node inside the source
123
+ # @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
124
+ # subtree after id selection
137
125
  # @return [Sevgi::Graphics::Element, nil] included selected/root graphics element, or nil when the selected node
138
126
  # produces no graphics output
139
127
  # @raise [Sevgi::ArgumentError] when content is malformed or rootless, or when the id is absent
140
128
  # @note Namespace-aware dispatch preserves ordinary foreign/qualified nodes and nested SVG elements.
141
- def evaluate(content, element, id: nil) = Document.new(content).decompile(id).evaluate(element)
129
+ # @example Include the selected node under an existing element
130
+ # drawing = Sevgi::Graphics.SVG(:minimal)
131
+ # included = Sevgi::Derender.evaluate('<circle id="mark" r="4"/>', drawing)
132
+ # included.name #=> :circle
133
+ # included[:id] #=> "mark"
134
+ # @see Sevgi::Derender.evaluate_file
135
+ # @see Sevgi.Evaluate
136
+ def self.evaluate(content, element, id: nil, omit: nil)
137
+ Document.new(content).decompile(id, omit:).evaluate(element)
138
+ end
142
139
 
143
140
  # Evaluates only the selected node's children under a graphics element.
144
141
  # @param content [String] SVG/XML source content
145
142
  # @param element [Sevgi::Graphics::Element] target graphics element
146
- # @param id [String, nil] optional SVG id selecting a node inside the source
147
- # @return [Array<Sevgi::Graphics::Element>] included child graphics elements
143
+ # @param id [String, Symbol, nil] optional SVG id selecting a node inside the source
144
+ # @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
145
+ # subtree after id selection
146
+ # @return [Array<Sevgi::Graphics::Element>] immutable included-child snapshot
148
147
  # @raise [Sevgi::ArgumentError] when content is malformed or rootless, or when the id is absent
149
148
  # @note Namespace-aware dispatch preserves ordinary foreign/qualified nodes and nested SVG elements.
150
- def evaluate_children(content, element, id: nil) = Document.new(content).decompile(id).evaluate_children(element)
149
+ # @example Include only children and retain an immutable result snapshot
150
+ # drawing = Sevgi::Graphics.SVG(:minimal)
151
+ # children = Sevgi::Derender.evaluate_children('<g><rect/><circle/></g>', drawing)
152
+ # children.map(&:name) #=> [:rect, :circle]
153
+ # children.frozen? #=> true
154
+ # @see Sevgi::Derender.evaluate_children_file
155
+ # @see Sevgi.EvaluateChildren
156
+ def self.evaluate_children(content, element, id: nil, omit: nil)
157
+ Document.new(content).decompile(id, omit:).evaluate_children(element)
158
+ end
151
159
 
152
160
  # Evaluates an SVG/XML file under a graphics element, including the selected node.
153
161
  # @param file [String] path to the source SVG/XML file
154
162
  # @param element [Sevgi::Graphics::Element] target graphics element
155
- # @param id [String, nil] optional SVG id selecting a node inside the source
163
+ # @param id [String, Symbol, nil] optional SVG id selecting a node inside the source
164
+ # @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
165
+ # subtree after id selection
156
166
  # @return [Sevgi::Graphics::Element, nil] included selected/root graphics element, or nil when the selected node
157
167
  # produces no graphics output
158
168
  # @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
159
169
  # absent
170
+ # @raise [SystemCallError] when the file cannot be read
160
171
  # @note Namespace-aware dispatch preserves ordinary foreign/qualified nodes and nested SVG elements.
161
- def evaluate_file(file, element, id: nil) = Document.load_file(file).decompile(id).evaluate(element)
172
+ # @see Sevgi::Derender.evaluate
173
+ # @see Sevgi.EvaluateFile
174
+ def self.evaluate_file(file, element, id: nil, omit: nil)
175
+ Document.load_file(file).decompile(id, omit:).evaluate(element)
176
+ end
162
177
 
163
178
  # Evaluates only the selected node's children from an SVG/XML file under a graphics element.
164
179
  # @param file [String] path to the source SVG/XML file
165
180
  # @param element [Sevgi::Graphics::Element] target graphics element
166
- # @param id [String, nil] optional SVG id selecting a node inside the source
167
- # @return [Array<Sevgi::Graphics::Element>] included child graphics elements
181
+ # @param id [String, Symbol, nil] optional SVG id selecting a node inside the source
182
+ # @param omit [String, Symbol, Array<String, Symbol>, nil] exact attribute name or names omitted from the selected
183
+ # subtree after id selection
184
+ # @return [Array<Sevgi::Graphics::Element>] immutable included-child snapshot
168
185
  # @raise [Sevgi::ArgumentError] when the file cannot be found, file content is malformed or rootless, or the id is
169
186
  # absent
187
+ # @raise [SystemCallError] when the file cannot be read
170
188
  # @note Namespace-aware dispatch preserves ordinary foreign/qualified nodes and nested SVG elements.
171
- def evaluate_file_children(file, element, id: nil)
172
- Document.load_file(file).decompile(id).evaluate_children(element)
189
+ # @see Sevgi::Derender.evaluate_children
190
+ # @see Sevgi.EvaluateChildrenFile
191
+ def self.evaluate_children_file(file, element, id: nil, omit: nil)
192
+ Document.load_file(file).decompile(id, omit:).evaluate_children(element)
173
193
  end
174
-
175
- extend self
176
194
  end
177
195
  end
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.95.0
4
+ version: 0.98.2
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.95.0
18
+ version: 0.98.2
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.95.0
25
+ version: 0.98.2
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.95.0
32
+ version: 0.98.2
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.95.0
39
+ version: 0.98.2
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: css_parser
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -79,7 +79,7 @@ dependencies:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
81
  version: '0.18'
82
- description: Takes an SVG input and converts it to a Sevgi script.
82
+ description: Converts SVG or XML trees into Sevgi DSL source.
83
83
  email: roktas@gmail.com
84
84
  executables:
85
85
  - igves
@@ -126,7 +126,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
126
126
  - !ruby/object:Gem::Version
127
127
  version: '0'
128
128
  requirements: []
129
- rubygems_version: 4.0.11
129
+ rubygems_version: 4.0.16
130
130
  specification_version: 4
131
- summary: Convert SVG to Sevgi DSL.
131
+ summary: SVG-to-Sevgi source conversion.
132
132
  test_files: []