sevgi-derender 0.95.0 → 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: 84e2eb700b9e867ffb80ad1223ba2bfaf32f632b19d05a07db71191a8a611cac
4
- data.tar.gz: 7cefed91f1645a9c195dd65d677f7c9cb041f6f4f4093b90aae2aa56a848342b
3
+ metadata.gz: e02ddd4869fac0f3ab35c12f53bcdd9e23bdc936d8753a17ff56b651c8a07882
4
+ data.tar.gz: aeff138c2ced508eca978c9eccd2e547c808e2be7d4d7bac859a57700e118892
5
5
  SHA512:
6
- metadata.gz: fa91f3c9a83fa0b8a6edfa104131e8fcd533421bd182818713c3235c046b431926a031d2a3b52803afb4150f1757ace6b50f42977ef6065b39cbb3ea2f5bad94
7
- data.tar.gz: da2e1ce83ee737df97b9dd01b80abb2d372f2315eab3e6051b1cb563120185afd7f326b158f869760cdc71c623a1bd9e12b2dd82c05c056ced7b0e469ea78b42
6
+ metadata.gz: dee05c8a84e8872ce946aa18f3f10071c0b07bea43170cc3a59f03ef9b434383e184478c84fe43270bf9524af3b135ee827bbe55868a64b62099af8dc34f37a1
7
+ data.tar.gz: db4b7b23ca99f153f40ff0ec6bc2cdd126c399a466b93c3d92f028669a672de39e9dc783f093f1bb68fa1b738b7809fc6693651c19e251700248e9601afdb581
data/CHANGELOG.md CHANGED
@@ -3,7 +3,226 @@
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.
101
+
102
+ ## 0.98.2 - 2026-07-25
103
+
104
+ ### Fixed
105
+
106
+ - Restored the `sevgi` CLI's main-object scope so bare toolkit calls remain available inside script helper classes.
107
+
108
+ ## 0.98.1 - 2026-07-20
109
+
110
+ ### Changed
111
+
112
+ - Changed the `sevgi` CLI to use the executor's isolated scope by default.
113
+
114
+ ### Removed
115
+
116
+ - Removed the redundant `-n` and `--nomain` CLI options; library consumers can still select `main: true` through
117
+ `Sevgi.execute` and `Sevgi.execute_file` when needed.
118
+
119
+ ## 0.98.0 - 2026-07-20
120
+
121
+ ### Added
122
+
123
+ - Added standard-input support to `sevgi`, `igves`, and `igsev`; `sevgi --as NAME` and
124
+ `Sevgi.execute_file(..., as: NAME)` supply the logical source basename used by implicit SVG, PDF, and PNG
125
+ destinations without changing physical load identity.
126
+
127
+ ## 0.97.0 - 2026-07-19
128
+
129
+ ### Added
130
+
131
+ - Added `sevgi-appendix`, containing the Sevgi agent skill and a RuboCop plugin for preserving readable `.sevgi` DSL
132
+ source alongside rubyfmt-formatted Ruby; the umbrella `sevgi` gem installs the matching Appendix version.
133
+ - Added `sevgi --skill` to report the validated path of the matching packaged agent skill for product-neutral setup.
134
+ - Added `igsev` to the umbrella gem for normalizing an SVG file through a complete SVG-to-Sevgi-to-SVG round trip.
135
+
136
+ ### Changed
137
+
138
+ - Made Homebrew the recommended complete CLI installation while retaining Bundler and focused component gems for Ruby
139
+ application dependencies.
140
+
141
+ ## 0.96.0 - 2026-07-18
142
+
143
+ ### Added
144
+
145
+ - Added exact, subtree-wide attribute omission to Derender content/file conversion, evaluation, and inclusion APIs;
146
+ `igves --omit` exposes the same behavior from the command line.
147
+ - Added `Sevgi.SVG` as the explicit namespaced form of the top-level SVG document entrypoint.
148
+ - Added the opt-in recursive `SVG::Modules` contract for callable drawing namespaces.
149
+ - Added callable drawing-module `base` blocks, document and paper registry introspection, Canvas structural equality,
150
+ renderer options on `RenderChildren`, axis translation helpers, and a CC BY RDF helper.
151
+ - Formalized non-rendering `-` metadata and `+` attribute updates as `Attributes::META_PREFIX` and
152
+ `Attributes::UPDATE_SUFFIX`; repeated Array updates now concatenate into a stable flat value.
153
+
154
+ ### Changed
155
+
156
+ - Breaking: replaced the `SVG = Sevgi::Graphics` alias with an independent SVG facade. Facade operations now use
157
+ capitalized names such as `SVG.Canvas` and `SVG.Document`; lowercase component helpers remain on
158
+ `Sevgi::Graphics`, and the stuttering `SVG.SVG` form was removed.
159
+ - Breaking: made low-level `Executor.execute` and `execute_file` runners private; consumers use
160
+ `Sevgi.execute` and `Sevgi.execute_file` while retaining the public result and error types.
161
+ - Breaking: removed the abstract `Document::Base` layer from the selectable document-profile registry; `Minimal` and
162
+ `Default` are now sibling concrete profiles, and custom profile hierarchies should derive from `Base`.
163
+ - Breaking: renamed Parallelogram segment and constraint arguments by geometric role: `base`, `side`, and
164
+ `constraint` replace axis-implying constructor names.
165
+ - Made degree-based sine and cosine exact at integer quarter turns, eliminating cardinal Geometry residue.
166
+ - Documented complete file-system failure families for file comparison, output, touch, and upward location helpers.
167
+ - Formalized generated Tile ids, positional CSS classes, template placement, and per-use callback signatures.
168
+ - Formalized String and Symbol ids across Derender selection APIs and documented file-read failures consistently.
169
+ - Breaking: made top-level `Decompile`, `Derender`, `Evaluate`, and `EvaluateChildren` consume inline SVG/XML;
170
+ file inputs now use the corresponding `File`-suffixed entrypoints.
171
+ - Breaking: replaced callable drawing module `call` block registration with argument-independent `base` blocks;
172
+ inherited bases run parent-first, and modules with one drawing method conventionally name it `call`.
173
+ - Breaking: made bracket and call notation the canonical Geometry constructors. Among Data value types, bracket notation
174
+ is public only for Point, Segment, LengthAngle, Margin, and Paper; use `.new` for Location and result carriers, and
175
+ `Stay` for traversal stop tokens.
176
+ - Breaking: aligned Point and Segment comparison with Ruby `Comparable`; malformed or unrelated comparison operands now
177
+ return nil from `<=>`.
178
+ - Breaking: executor entrypoints now return immutable `Executor::Result` values and expose only `execute` and
179
+ `execute_file`; inspect `result.error`, `result.stack`, and `result.value` instead of executor scopes or lifecycle state.
180
+ - Breaking: wrapper attributes and callable arguments use distinct channels, `With` rejects parentless receivers, and
181
+ unsupported direct constructors are private; use the documented factories for Content, concrete Geometry lined
182
+ elements, and Grid query values.
183
+ - Breaking: Canvas uses `.new` for explicit fields, `.from_paper(paper, **overrides)` for paper conversion, and
184
+ `.call`/`Graphics.canvas` for dispatch; the misleading keyword-only `.from_paper(width:, height:)` form was removed.
185
+ - Breaking: `Save`, `Write`, `PNG`, `PDF`, and Sundries native export normalize successful destinations to expanded
186
+ String paths and create missing parent directories; change-aware SVG writes still return nil when unchanged.
187
+ - Breaking: renamed `Derender.evaluate_file_children` to `evaluate_children_file`; decompiled nodes now own immutable
188
+ public state while parser, strategy, and construction plumbing remain private.
189
+ - Expanded runtime/YARD parity checks and exact contracts for inherited, extended, generated, and dynamic API surfaces,
190
+ forwarded options, error channels, path ownership, whitespace, namespaces, nil behavior, and return values.
191
+
192
+ ### Fixed
193
+
194
+ - Rejected directories from default file discovery while preserving custom locator matchers.
195
+ - Kept `Ancestral` context in non-rendering `-context` metadata instead of leaking it into SVG attributes.
196
+ - Preserved source Canvas units and names when deriving fitted Grid canvases.
197
+ - Prevented stale documentation assets from mixing old layouts with new HTML, and moved sidebar navigation into the
198
+ tablet menu at 1024px and below.
199
+ - Normalized numeric slots owned by path, shape, transform, tile, and Inkscape page helpers to finite SVG number spelling;
200
+ arbitrary user-supplied attributes remain untouched.
201
+ - Made named callable wrapper and symbol ids stable while omitting defaults for anonymous modules, and indexed every
202
+ rendered id value, including false and numeric values, through its serialized string.
203
+ - Preserved signed Geometry constraint directions, rejected invalid sweep/export channels through Sevgi error families,
204
+ and validated raw output paths before expansion or rendering.
205
+ - Made callable module configuration copy-owned and freeze-aware, document subclasses inherit their nearest profile, and
206
+ false executor boot receivers remain explicit rather than defaulting to an internal scope.
207
+ - Made element trees, attributes, identifiers, locator results, document profiles, Derender nodes, shell results, and
208
+ executor results retain owned immutable snapshots where their public contracts promise value semantics.
209
+ - Corrected Shell combined-output separators, nil export density errors, document render-option routing, executor source
210
+ snapshots, selected-node namespace/whitespace documentation, and Standard character-data validation.
211
+
212
+ ### Removed
213
+
214
+ - Removed public access to pluralization tables and the internal SVG save extension; pluralization rules are now deeply
215
+ immutable.
216
+ - Removed public documentation and constant access for command-line implementation modules; the `sevgi` and `igves`
217
+ executables remain unchanged.
218
+ - Removed eager loading and public documentation of private Showcase build/test support; explicit support entrypoints
219
+ now keep the harness under the Showcase namespace.
220
+ - Removed public access to document profile name normalizers; registry operations retain them as private plumbing.
221
+ - Removed the redundant `Margin.margin` constructor; use canonical bracket notation.
222
+ - Removed public executor orchestration, obsolete callable-module hooks, the old public attribute syntax constants, and
223
+ direct construction of abstract or internally wired Content, Element, and Grid query types.
224
+ - Removed accidental public access to abstract Lined factories, internal element-name/export maps, result/location
225
+ bracket constructors, and direct Stop construction.
7
226
 
8
227
  ## 0.95.0 - 2026-07-11
9
228
 
@@ -87,7 +306,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
87
306
 
88
307
  ### Changed
89
308
 
90
- - Added the checker board example to the documentation showcase flow.
309
+ - Added the checkerboard example to the documentation showcase flow.
91
310
  - Consolidated Victor Book attribution for adapted showcase examples into a single documentation note.
92
311
 
93
312
  ### Fixed
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
 
@@ -18,27 +18,55 @@ 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
25
+
26
+ source = '<svg><rect id="mark" style="fill: red" width="3"/></svg>'
27
+ puts Sevgi::Derender.derender(source, omit: %i[id style])
28
+ # SVG do
29
+ # rect width: 3
30
+ # end
22
31
  ```
23
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
+
24
40
  ## Executable
25
41
 
26
42
  ```sh
27
- igves drawing.svg
43
+ igves --omit id --omit style drawing.svg
44
+ ```
45
+
46
+ Omit the file or pass `-` to read SVG from standard input:
47
+
48
+ ```sh
49
+ igves --omit id < drawing.svg
50
+ ```
51
+
52
+ `igves` prints generated Sevgi source. The umbrella `sevgi` gem also installs `igsev`, which evaluates that source and
53
+ prints normalized SVG:
54
+
55
+ ```sh
56
+ igsev --omit id --omit style < drawing.svg
28
57
  ```
29
58
 
30
59
  ## Ruby compatibility
31
60
 
32
- Requires Ruby 3.4.0 or newer. CI verifies Ruby 3.4.0 and the current development Ruby from `.ruby-version`.
61
+ Requires Ruby 3.4.0 or newer. CI verifies the current Ruby 3.4 release and the development Ruby from `.ruby-version`.
33
62
 
34
63
  ## Native prerequisites
35
64
 
36
- None beyond Ruby and this gem's Ruby dependencies. Nokogiri may use platform packages depending on the target Ruby
37
- platform.
65
+ This gem needs no native libraries of its own. Nokogiri can use platform packages on some Ruby platforms.
38
66
 
39
67
  ## Links
40
68
 
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
69
+ - Documentation: <https://sevgi.roktas.dev>
70
+ - API documentation: <https://www.rubydoc.info/gems/sevgi-derender>
71
+ - Source: <https://github.com/roktas/sevgi/tree/main/derender>
72
+ - 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,16 +82,16 @@ 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
74
89
  raise if raw_error?(options)
75
90
 
76
- die(e, nil)
91
+ die(e)
77
92
  end
78
93
 
79
- def die(error, _file)
94
+ def die(error)
80
95
  warn(error.message)
81
96
  warn("")
82
97
  backtrace(error).each { warn(" #{it}") }
@@ -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)
@@ -117,7 +142,7 @@ module Sevgi
117
142
  rescue ::StandardError => e
118
143
  raise if raw_error?(options)
119
144
 
120
- die(e, file)
145
+ die(e)
121
146
  end
122
147
 
123
148
  def raw_error?(options)
@@ -125,4 +150,6 @@ module Sevgi
125
150
  end
126
151
  end
127
152
  end
153
+
154
+ private_constant :Binaries
128
155
  end
@@ -35,11 +35,14 @@ 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
- style = Css.to_h!(value)
42
- style.empty? ? "{}" : "{ #{Attributes.decompile(style)} }"
40
+ style = Css.declarations(value)
41
+ if style
42
+ style.empty? ? "{}" : "{ #{Attributes.decompile(style)} }"
43
+ else
44
+ Ruby.literal(value)
45
+ end
43
46
  elsif value.is_a?(::String)
44
47
  Css.to_value(value)
45
48
  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,13 +14,13 @@ 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
 
20
21
  ArgumentError.("File not found: #{path}") unless ::File.exist?(entry)
21
22
 
22
- content = ::File.read(entry)
23
+ content = ::File.binread(entry)
23
24
  new(content)
24
25
  end
25
26
 
@@ -29,7 +30,7 @@ module Sevgi
29
30
  # @raise [Sevgi::ArgumentError] when content is not well-formed XML
30
31
  # @raise [Sevgi::ArgumentError] when content has no root element
31
32
  def self.parse(content)
32
- 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|
33
34
  ArgumentError.("XML document has no root element") unless doc.root
34
35
  end
35
36
 
@@ -37,20 +38,26 @@ module Sevgi
37
38
  raise ArgumentError, "Malformed XML: #{e.message.lines.first.strip}", cause: e
38
39
  end
39
40
 
40
- # Extracts the XML declaration from SVG/XML content.
41
+ # Extracts the XML declaration and aligns its encoding with UTF-8 output.
41
42
  # @param content [String] SVG/XML source content
43
+ # @param encoding [String, nil] source encoding reported by the XML parser
42
44
  # @return [String, nil] XML declaration line, if present
43
- def self.declaration(content)
44
- return unless (content = content.to_s.lstrip).start_with?("<?xml ")
45
-
46
- 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
47
54
  end
48
55
 
49
56
  # Returns the parsed XML document.
50
57
  # @return [Nokogiri::XML::Document]
51
58
  attr_reader :doc
52
59
 
53
- # Returns the source XML declaration when present.
60
+ # Returns the XML declaration with its encoding field normalized to UTF-8, when present.
54
61
  # @return [String, nil]
55
62
  attr_reader :decl
56
63
 
@@ -61,31 +68,30 @@ module Sevgi
61
68
  # @raise [Sevgi::ArgumentError] when content has no root element
62
69
  def initialize(content)
63
70
  @doc = self.class.parse(content)
64
- @decl = self.class.declaration(content)
71
+ @decl = self.class.declaration(content, encoding: doc.encoding)
72
+ @doc.encoding = "UTF-8"
65
73
  end
66
74
 
67
75
  # Converts the root or selected node into a derender node.
68
- # @param id [String, nil] optional SVG id selecting a node inside the document
76
+ # @param id [String, Symbol, nil] optional SVG id selecting a node inside the document
77
+ # @param omit [String, Symbol, Array<String, Symbol>, nil] attribute name or names omitted from the selected subtree
69
78
  # @return [Sevgi::Derender::Node] selected node in the derender tree
70
79
  # @raise [Sevgi::ArgumentError] when the document has no root element or the id is absent
71
- def decompile(id = nil)
80
+ def decompile(id = nil, omit: nil)
72
81
  if id
73
- if (found = doc.xpath("//*[@id=#{xpath_literal(id)}]") || []).empty?
74
- ArgumentError.("No such element with id '#{id}' in document")
75
- end
76
-
77
- found.first
82
+ doc.at_xpath("//*[@id=#{xpath_literal(id)}]") || ArgumentError.("No such element with id '#{id}' in document")
78
83
  else
79
84
  doc.root
80
85
  end => element
81
86
 
82
87
  ArgumentError.("XML document has no root element") unless element
88
+ validate_selection(element, whole: !id)
83
89
 
84
- Node.new(element, pres, namespaces: namespace_scope(element))
90
+ Node.send(:new, element, pres, namespaces: namespace_scope(element), omit: omissions(omit))
85
91
  end
86
92
 
87
- # Returns XML declaration and pre-root nodes preserved for root decompilation. The result contains only String
88
- # 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.
89
95
  # @return [Array<String>] preamble XML lines
90
96
  def pres
91
97
  @pres ||= [].tap do |lines|
@@ -97,6 +103,19 @@ module Sevgi
97
103
 
98
104
  private
99
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
+
100
119
  def namespace_scope(element)
101
120
  element == doc.root ? local_namespaces(element) : element.namespaces
102
121
  end
@@ -109,6 +128,10 @@ module Sevgi
109
128
  end
110
129
  end
111
130
 
131
+ def omissions(value)
132
+ Array(value).to_h { [it.to_s.dup.freeze, true] }.freeze
133
+ end
134
+
112
135
  def xpath_literal(value)
113
136
  value = value.to_s
114
137