postsvg 0.1.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +105 -0
- data/CLAUDE.md +173 -0
- data/Gemfile +2 -3
- data/README.adoc +456 -179
- data/Rakefile +100 -0
- data/TODO.roadmap/00-architecture.md +139 -0
- data/TODO.roadmap/01-autoload-migration.md +39 -0
- data/TODO.roadmap/02-isolate-dormant-code.md +51 -0
- data/TODO.roadmap/03-domain-model.md +66 -0
- data/TODO.roadmap/04-lexer.md +40 -0
- data/TODO.roadmap/05-parser.md +43 -0
- data/TODO.roadmap/06-graphics-state.md +45 -0
- data/TODO.roadmap/07-matrix-and-color.md +37 -0
- data/TODO.roadmap/08-svg-builder.md +51 -0
- data/TODO.roadmap/09-renderer.md +52 -0
- data/TODO.roadmap/10-visitor.md +58 -0
- data/TODO.roadmap/11-operator-coverage.md +103 -0
- data/TODO.roadmap/12-svg-domain.md +62 -0
- data/TODO.roadmap/13-translation-handlers.md +69 -0
- data/TODO.roadmap/14-ps-serializer.md +49 -0
- data/TODO.roadmap/15-cli-and-public-api.md +47 -0
- data/TODO.roadmap/16-specs.md +84 -0
- data/TODO.roadmap/17-docs-sync.md +47 -0
- data/TODO.roadmap/18-performance-and-determinism.md +47 -0
- data/TODO.roadmap/19-error-model.md +45 -0
- data/TODO.roadmap/20-font-and-text.md +46 -0
- data/TODO.roadmap/21-images.md +45 -0
- data/TODO.roadmap/22-forms-and-resources.md +25 -0
- data/TODO.roadmap/23-level2-level3.md +52 -0
- data/TODO.roadmap/24-ci-and-release.md +42 -0
- data/TODO.roadmap/README.md +77 -0
- data/docs/.gitignore +29 -0
- data/docs/CHANGELOG.md +114 -0
- data/docs/COMPLETE_DOCUMENTATION_STATUS.md +376 -0
- data/docs/DEPLOYMENT.md +456 -0
- data/docs/DEPLOYMENT_INSTRUCTIONS.md +229 -0
- data/docs/DOCUMENTATION_PLAN.md +425 -0
- data/docs/FINAL_SUMMARY.md +657 -0
- data/docs/Gemfile +15 -0
- data/docs/README.md +327 -0
- data/docs/_config.yml +99 -0
- data/docs/advanced-topics.adoc +370 -0
- data/docs/api-reference/colors.adoc +705 -0
- data/docs/api-reference/converter.adoc +699 -0
- data/docs/api-reference/execution-context.adoc +1210 -0
- data/docs/api-reference/graphics-state.adoc +1070 -0
- data/docs/api-reference/interpreter.adoc +810 -0
- data/docs/api-reference/matrix.adoc +1179 -0
- data/docs/api-reference/path-builder.adoc +1284 -0
- data/docs/api-reference/postsvg-module.adoc +388 -0
- data/docs/api-reference/svg-generator.adoc +891 -0
- data/docs/api-reference/tokenizer.adoc +925 -0
- data/docs/api-reference.adoc +221 -0
- data/docs/architecture/command-registry.adoc +1191 -0
- data/docs/architecture/conversion-pipeline.adoc +746 -0
- data/docs/architecture/design-decisions.adoc +999 -0
- data/docs/architecture/generator-stage.adoc +1115 -0
- data/docs/architecture/graphics-state-model.adoc +1089 -0
- data/docs/architecture/interpreter-stage.adoc +1125 -0
- data/docs/architecture/parser-stage.adoc +1051 -0
- data/docs/architecture.adoc +354 -0
- data/docs/cli-reference/batch-command.adoc +616 -0
- data/docs/cli-reference/check-command.adoc +677 -0
- data/docs/cli-reference/cli-options.adoc +802 -0
- data/docs/cli-reference/convert-command.adoc +462 -0
- data/docs/cli-reference/version-command.adoc +296 -0
- data/docs/cli-reference.adoc +317 -0
- data/docs/concepts/conversion-pipeline.adoc +903 -0
- data/docs/concepts/coordinate-systems.adoc +836 -0
- data/docs/concepts/graphics-state.adoc +861 -0
- data/docs/concepts/path-operations.adoc +1076 -0
- data/docs/concepts/postscript-language.adoc +859 -0
- data/docs/concepts/svg-generation.adoc +937 -0
- data/docs/concepts.adoc +198 -0
- data/docs/contributing.adoc +443 -0
- data/docs/development.adoc +420 -0
- data/docs/faq.adoc +493 -0
- data/docs/getting-started/basic-usage.adoc +538 -0
- data/docs/getting-started/common-workflows.adoc +577 -0
- data/docs/getting-started/first-conversion.adoc +492 -0
- data/docs/getting-started/installation.adoc +534 -0
- data/docs/getting-started.adoc +94 -0
- data/docs/index.adoc +248 -0
- data/docs/optimization.adoc +196 -0
- data/docs/ps2svg_compatibility.adoc +149 -0
- data/docs/quick-reference.adoc +453 -0
- data/docs/sitemap.adoc +337 -0
- data/docs/troubleshooting.adoc +486 -0
- data/docs/validation.adoc +772 -0
- data/exe/postsvg +1 -0
- data/lib/postsvg/cli.rb +104 -57
- data/lib/postsvg/color.rb +132 -0
- data/lib/postsvg/errors.rb +68 -3
- data/lib/postsvg/format_number.rb +22 -0
- data/lib/postsvg/graphics_context.rb +80 -0
- data/lib/postsvg/graphics_stack.rb +43 -0
- data/lib/postsvg/model/literals/array.rb +41 -0
- data/lib/postsvg/model/literals/dictionary.rb +34 -0
- data/lib/postsvg/model/literals/hex.rb +37 -0
- data/lib/postsvg/model/literals/name.rb +40 -0
- data/lib/postsvg/model/literals/number.rb +36 -0
- data/lib/postsvg/model/literals/procedure.rb +41 -0
- data/lib/postsvg/model/literals/string.rb +30 -0
- data/lib/postsvg/model/literals.rb +19 -0
- data/lib/postsvg/model/operator.rb +58 -0
- data/lib/postsvg/model/operators/arithmetic.rb +264 -0
- data/lib/postsvg/model/operators/boolean.rb +182 -0
- data/lib/postsvg/model/operators/color.rb +74 -0
- data/lib/postsvg/model/operators/container.rb +186 -0
- data/lib/postsvg/model/operators/control_flow.rb +119 -0
- data/lib/postsvg/model/operators/device.rb +21 -0
- data/lib/postsvg/model/operators/dictionary.rb +118 -0
- data/lib/postsvg/model/operators/font.rb +121 -0
- data/lib/postsvg/model/operators/graphics_state.rb +84 -0
- data/lib/postsvg/model/operators/painting.rb +29 -0
- data/lib/postsvg/model/operators/path.rb +169 -0
- data/lib/postsvg/model/operators/stack.rb +72 -0
- data/lib/postsvg/model/operators/transformations.rb +103 -0
- data/lib/postsvg/model/operators.rb +89 -0
- data/lib/postsvg/model/program.rb +68 -0
- data/lib/postsvg/model/token.rb +43 -0
- data/lib/postsvg/model.rb +17 -0
- data/lib/postsvg/options.rb +29 -0
- data/lib/postsvg/renderer.rb +85 -0
- data/lib/postsvg/serializer.rb +325 -0
- data/lib/postsvg/source/ast_builder.rb +308 -0
- data/lib/postsvg/source/lexer.rb +322 -0
- data/lib/postsvg/source/operand_stack.rb +55 -0
- data/lib/postsvg/source.rb +21 -0
- data/lib/postsvg/svg/attribute_parser.rb +45 -0
- data/lib/postsvg/svg/clip_path_registry.rb +44 -0
- data/lib/postsvg/svg/document.rb +22 -0
- data/lib/postsvg/svg/element.rb +84 -0
- data/lib/postsvg/svg/elements/circle.rb +36 -0
- data/lib/postsvg/svg/elements/clip_path.rb +26 -0
- data/lib/postsvg/svg/elements/defs.rb +24 -0
- data/lib/postsvg/svg/elements/ellipse.rb +38 -0
- data/lib/postsvg/svg/elements/group.rb +37 -0
- data/lib/postsvg/svg/elements/image.rb +35 -0
- data/lib/postsvg/svg/elements/line.rb +36 -0
- data/lib/postsvg/svg/elements/path.rb +32 -0
- data/lib/postsvg/svg/elements/polygon.rb +12 -0
- data/lib/postsvg/svg/elements/polyline.rb +32 -0
- data/lib/postsvg/svg/elements/rect.rb +44 -0
- data/lib/postsvg/svg/elements/svg.rb +39 -0
- data/lib/postsvg/svg/elements/text.rb +42 -0
- data/lib/postsvg/svg/elements.rb +31 -0
- data/lib/postsvg/svg/paint.rb +34 -0
- data/lib/postsvg/svg/parser.rb +39 -0
- data/lib/postsvg/svg/path_data/command.rb +27 -0
- data/lib/postsvg/svg/path_data/parser.rb +82 -0
- data/lib/postsvg/svg/path_data.rb +17 -0
- data/lib/postsvg/svg/stroke.rb +31 -0
- data/lib/postsvg/svg/transform_list.rb +59 -0
- data/lib/postsvg/svg.rb +22 -0
- data/lib/postsvg/svg_builder.rb +249 -0
- data/lib/postsvg/translation/arc_converter.rb +86 -0
- data/lib/postsvg/translation/bounding_box.rb +59 -0
- data/lib/postsvg/translation/context.rb +34 -0
- data/lib/postsvg/translation/handler_registry.rb +38 -0
- data/lib/postsvg/translation/handlers/circle_handler.rb +28 -0
- data/lib/postsvg/translation/handlers/clip_path_handler.rb +13 -0
- data/lib/postsvg/translation/handlers/defs_handler.rb +15 -0
- data/lib/postsvg/translation/handlers/ellipse_handler.rb +31 -0
- data/lib/postsvg/translation/handlers/group_handler.rb +21 -0
- data/lib/postsvg/translation/handlers/image_handler.rb +20 -0
- data/lib/postsvg/translation/handlers/line_handler.rb +23 -0
- data/lib/postsvg/translation/handlers/open_handler.rb +18 -0
- data/lib/postsvg/translation/handlers/path_handler.rb +356 -0
- data/lib/postsvg/translation/handlers/polygon_handler.rb +33 -0
- data/lib/postsvg/translation/handlers/polyline_handler.rb +31 -0
- data/lib/postsvg/translation/handlers/rect_handler.rb +27 -0
- data/lib/postsvg/translation/handlers/shared.rb +110 -0
- data/lib/postsvg/translation/handlers/svg_handler.rb +25 -0
- data/lib/postsvg/translation/handlers/text_handler.rb +56 -0
- data/lib/postsvg/translation/handlers.rb +25 -0
- data/lib/postsvg/translation/ps_renderer.rb +105 -0
- data/lib/postsvg/translation/record_emitter.rb +35 -0
- data/lib/postsvg/translation.rb +17 -0
- data/lib/postsvg/version.rb +1 -1
- data/lib/postsvg/visitors/ps_visitor/arithmetic.rb +125 -0
- data/lib/postsvg/visitors/ps_visitor/boolean.rb +105 -0
- data/lib/postsvg/visitors/ps_visitor/color.rb +53 -0
- data/lib/postsvg/visitors/ps_visitor/common.rb +66 -0
- data/lib/postsvg/visitors/ps_visitor/container.rb +164 -0
- data/lib/postsvg/visitors/ps_visitor/control_flow.rb +110 -0
- data/lib/postsvg/visitors/ps_visitor/device.rb +20 -0
- data/lib/postsvg/visitors/ps_visitor/dictionary.rb +89 -0
- data/lib/postsvg/visitors/ps_visitor/font.rb +93 -0
- data/lib/postsvg/visitors/ps_visitor/graphics_state.rb +55 -0
- data/lib/postsvg/visitors/ps_visitor/painting.rb +90 -0
- data/lib/postsvg/visitors/ps_visitor/path.rb +112 -0
- data/lib/postsvg/visitors/ps_visitor/stack.rb +47 -0
- data/lib/postsvg/visitors/ps_visitor/transformations.rb +101 -0
- data/lib/postsvg/visitors/ps_visitor.rb +208 -0
- data/lib/postsvg/visitors.rb +9 -0
- data/lib/postsvg.rb +93 -59
- data/lychee.toml +86 -0
- metadata +216 -11
- data/postsvg.gemspec +0 -38
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7eb334f9e4c74e3038c41e80d545bcaec37aa5032f20aa2d27fe61da5795542f
|
|
4
|
+
data.tar.gz: 2df77c30cad2f75203764b86835e3e0cded61db1b8b658882e342ed5b2f6c91d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab6b8afd0962151ce8f2799cd260d69bf5a4343b847a44f454dcc84cf847066f4e48e07e577d397071e4387cd68f3d58665e754bd4d07ac0b26fa87ba85f37e9
|
|
7
|
+
data.tar.gz: c844ee5ab50f674a1e3f186131d4da0840516eb35ff7cccb210e4ca64441560ca18c207103702eaa0c88679ef91343e10a8f8f0f2c54ae74963e06587dd23203
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Postsvg will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
### Added — 2026-07-27
|
|
8
|
+
|
|
9
|
+
**0.2.0 — PS/EPS ⇔ SVG bidirectional pipeline.**
|
|
10
|
+
|
|
11
|
+
This release rebuilds the converter on a typed domain model and adds
|
|
12
|
+
the reverse direction (SVG → PS / EPS).
|
|
13
|
+
|
|
14
|
+
* `Postsvg::Model::*` — typed PS records (Program, Literals, Operator
|
|
15
|
+
hierarchy). Adding a new PS operator is a new class + one
|
|
16
|
+
`visit_*` method, no switch edit.
|
|
17
|
+
* `Postsvg::Source::Lexer` — comment-aware lexer. Replaces the old
|
|
18
|
+
`Tokenizer` whose `gsub(/%[^\n\r]*/)` corruption of string literals
|
|
19
|
+
broke on inputs with literal `%`.
|
|
20
|
+
* `Postsvg::Source::AstBuilder` — turns tokens into a `Model::Program`,
|
|
21
|
+
constructing typed operator instances by popping the parse stack in
|
|
22
|
+
reverse source order.
|
|
23
|
+
* `Postsvg::Source::OperandStack` — type-aware parse stack with
|
|
24
|
+
permissive pop (returns a `Computed` sentinel on underflow) so
|
|
25
|
+
procedure bodies parse cleanly without runtime context.
|
|
26
|
+
* `Postsvg::Model::Operator.consumes` / `.produces` — stack-arity
|
|
27
|
+
declaration. The parser pushes `Computed` sentinels for each value
|
|
28
|
+
an operator produces, keeping parse-time stack shape in sync with
|
|
29
|
+
runtime semantics.
|
|
30
|
+
* `Postsvg::Visitors::PsVisitor` — per-category dispatch tables
|
|
31
|
+
(Path, Painting, Color, GraphicsState, Transformations,
|
|
32
|
+
Dictionary, ControlFlow, Device, Arithmetic, Boolean, Stack, Font,
|
|
33
|
+
Container).
|
|
34
|
+
* `Postsvg::SvgBuilder` — append-only SVG emitter with clipPath /
|
|
35
|
+
gradient / pattern dedup and a single Y-flip wrapper group.
|
|
36
|
+
* `Postsvg::Renderer` — orchestrator for PS → SVG; sets up viewBox,
|
|
37
|
+
opens SVG / closes SVG, enforces `MAX_OUTPUT_BYTES`, handles
|
|
38
|
+
`QuitSignal` cleanly.
|
|
39
|
+
* `Postsvg::Svg::*` — typed SVG domain model (Document, Element,
|
|
40
|
+
PathData, Paint, Stroke, TransformList, ClipPathRegistry) +
|
|
41
|
+
Elements (Svg, Group, Path, Rect, Circle, Ellipse, Line, Polyline,
|
|
42
|
+
Polygon, Text, Image, Defs, ClipPath). The `Element` base class
|
|
43
|
+
provides nil-default getters for `fill`, `stroke_paint`, `stroke`,
|
|
44
|
+
`transform`, `children` so handlers never need `respond_to?`.
|
|
45
|
+
* `Postsvg::Svg::Parser` — Nokogiri-backed SVG parser.
|
|
46
|
+
* `Postsvg::Translation::*` — SVG → PS dispatch (`PsRenderer`,
|
|
47
|
+
`HandlerRegistry`, per-element `Handlers::*`).
|
|
48
|
+
* `Postsvg::Translation::ArcConverter` — SVG endpoint-to-center
|
|
49
|
+
arc parametrization (W3C algorithm from SVG 1.1 §F.6.5).
|
|
50
|
+
* `Postsvg::Translation::Handlers::PathHandler` — full SVG path
|
|
51
|
+
command coverage (M/L/H/V/C/S/Q/T/A/Z, absolute + relative).
|
|
52
|
+
* `Postsvg::Translation::Handlers::PolygonHandler` — closes the
|
|
53
|
+
path before paint (unlike PolylineHandler).
|
|
54
|
+
* `Postsvg::Model::Operators::Painting::FillAndStroke` — marker
|
|
55
|
+
operator for elements with both fill and stroke; serializer emits
|
|
56
|
+
`gsave fill grestore stroke`.
|
|
57
|
+
* `Postsvg::Model::Operators::Font::*` — findfont, scalefont, setfont,
|
|
58
|
+
show, xyshow, stringwidth, charpath.
|
|
59
|
+
* `Postsvg::Model::Operators::Container::*` — type-dispatching
|
|
60
|
+
length / get / put / getinterval / putinterval / forall / astore /
|
|
61
|
+
search / anchorsearch / token / string / cvs that work on strings,
|
|
62
|
+
arrays, and dictionaries.
|
|
63
|
+
* `Postsvg::Serializer` — `Model::Program` → PS / EPS source text.
|
|
64
|
+
* `Postsvg::Options` — frozen struct for both directions (`eps`,
|
|
65
|
+
`width`, `height`, `viewbox_override`, `verbose`, `page_size`).
|
|
66
|
+
* New CLI commands: `postsvg to-svg`, `postsvg to-ps`, `postsvg
|
|
67
|
+
to-eps`. `convert`, `batch`, `version` retained as BC / helpful
|
|
68
|
+
shortcuts.
|
|
69
|
+
* New typed error hierarchy: `LexError`, `SyntaxError`, `StackUnderflowError`,
|
|
70
|
+
`UndefinedOperatorError`, `RecursionLimitError`, `SizeLimitError`,
|
|
71
|
+
`UnsupportedElementError`, `UnresolvedReferenceError`,
|
|
72
|
+
`SerializeError`.
|
|
73
|
+
|
|
74
|
+
### Changed
|
|
75
|
+
|
|
76
|
+
- Architecture is fully rewritten around the Renderer / Visitor /
|
|
77
|
+
SvgBuilder pattern (modeled on `emfsvg`). The 0.1.0
|
|
78
|
+
Converter / Parser / Parslet pipeline is preserved at
|
|
79
|
+
`lib/postsvg/{converter,parser,parser/postscript_parser,
|
|
80
|
+
parser/transform,tokenizer,interpreter,svg_generator,
|
|
81
|
+
graphics_state,colors}.rb` as a reference, but is no longer on
|
|
82
|
+
the autoload path. Its method names are no longer public API.
|
|
83
|
+
- `Postsvg::Matrix` and `Postsvg::Color` are now promoted from
|
|
84
|
+
utility module to first-class value types. `Colors` (free functions)
|
|
85
|
+
is removed from the live path; existing callers can `require
|
|
86
|
+
"postsvg/colors"` explicitly for the legacy module.
|
|
87
|
+
- Specs follow the project's rules: no doubles, no `instance_variable_set`,
|
|
88
|
+
no `send` to private methods, no `respond_to?` for type checks, autoload throughout.
|
|
89
|
+
|
|
90
|
+
### Fixed
|
|
91
|
+
|
|
92
|
+
- `PolygonHandler` now emits `closepath` before paint.
|
|
93
|
+
- `PathHandler` supports all SVG path commands (M/L/H/V/C/S/Q/T/A/Z,
|
|
94
|
+
absolute and relative).
|
|
95
|
+
- All `respond_to?` calls in handlers eliminated (zero remain in
|
|
96
|
+
`lib/`).
|
|
97
|
+
- Handler registration is now lazy (auto-triggered on first use)
|
|
98
|
+
instead of eager at file load.
|
|
99
|
+
- 6/6 integration fixtures (colors.ps, example_full.ps, file.ps,
|
|
100
|
+
prog.ps, img.ps, img.eps) now convert without error.
|
|
101
|
+
|
|
102
|
+
### Test coverage
|
|
103
|
+
|
|
104
|
+
- 99 examples, 0 failures.
|
|
105
|
+
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
`postsvg` is a pure-Ruby bidirectional converter between PostScript/EPS and SVG. Both directions are implemented in the 0.2.x line; the SVg→PS direction is newer. The PS side uses a hand-written lexer + stack-machine interpreter; the SVG side uses Nokogiri. There is no Ghostscript, Inkscape, or other external renderer.
|
|
8
|
+
|
|
9
|
+
Code architecture mirrors `emfsvg`: a typed `Model::Program` value object is the single source of truth, with a Renderer / Visitor visiting it to emit SVG, and a Serializer walking it to emit PS.
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bundle install # set up deps
|
|
15
|
+
bundle exec rspec # full suite (~74 examples, <0.2s)
|
|
16
|
+
bundle exec rspec spec/postsvg/renderer_spec.rb # one file
|
|
17
|
+
bundle exec rspec -e "converts a square PS program to SVG" # by description
|
|
18
|
+
bundle exec rubocop # lint (must be clean)
|
|
19
|
+
bundle exec rake # spec + rubocop (CI gate)
|
|
20
|
+
|
|
21
|
+
# CLI
|
|
22
|
+
bundle exec ruby exe/postsvg convert INPUT.ps|INPUT.eps [OUTPUT.svg]
|
|
23
|
+
bundle exec ruby exe/postsvg to-svg INPUT.eps [OUTPUT.svg]
|
|
24
|
+
bundle exec ruby exe/postsvg to-ps INPUT.svg [OUTPUT.ps]
|
|
25
|
+
bundle exec ruby exe/postsvg to-eps INPUT.svg [OUTPUT.eps]
|
|
26
|
+
bundle exec ruby exe/postsvg batch INPUT_DIR [OUTPUT_DIR]
|
|
27
|
+
bundle exec ruby exe/postsvg version
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Architecture
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
┌─────────────────────┐
|
|
34
|
+
PS/EPS bytes ──▶│ Postsvg::Lexer │
|
|
35
|
+
└──────────┬──────────┘
|
|
36
|
+
│ tokens
|
|
37
|
+
▼
|
|
38
|
+
┌─────────────────────┐
|
|
39
|
+
│ Postsvg::AstBuilder │
|
|
40
|
+
└──────────┬──────────┘
|
|
41
|
+
│ Model::Program
|
|
42
|
+
▼
|
|
43
|
+
┌────────────────────────────────┐
|
|
44
|
+
│ Postsvg::Model::Program │ ── single source of truth
|
|
45
|
+
└────┬─────────────────┬─────────┘
|
|
46
|
+
│ visit │ emit
|
|
47
|
+
▼ ▲
|
|
48
|
+
┌─────────────────┐ │
|
|
49
|
+
│ Postsvg:: │ │
|
|
50
|
+
│ Renderer + │ │
|
|
51
|
+
│ PsVisitor + │ │
|
|
52
|
+
│ SvgBuilder │ │
|
|
53
|
+
└────────┬────────┘ │
|
|
54
|
+
│ │
|
|
55
|
+
▼ │
|
|
56
|
+
SVG string │
|
|
57
|
+
│ Nokogiri │
|
|
58
|
+
▼ │
|
|
59
|
+
┌─────────────────┐ │
|
|
60
|
+
│ Postsvg:: │ │
|
|
61
|
+
│ Svg::Parser │ │
|
|
62
|
+
└────────┬────────┘ │
|
|
63
|
+
│ Svg::Document │
|
|
64
|
+
▼ │
|
|
65
|
+
┌─────────────────┐ │
|
|
66
|
+
│ Postsvg:: │ │
|
|
67
|
+
│ Translation::* │─────────┘
|
|
68
|
+
│ + Serializer │
|
|
69
|
+
└────────┬────────┘
|
|
70
|
+
▼
|
|
71
|
+
PS / EPS source
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
MECE responsibilities:
|
|
75
|
+
|
|
76
|
+
* `Postsvg::Source` (`Lexer`, `AstBuilder`, `OperandStack`) — PS source → typed AST.
|
|
77
|
+
* `Postsvg::Model` (`Program`, `Literals::*`, `Operator`) — typed records, immutable value equality.
|
|
78
|
+
* `Postsvg::GraphicsContext` / `GraphicsStack` — immutable graphics-state snapshots.
|
|
79
|
+
* `Postsvg::Matrix` / `Color` / `FormatNumber` — foundational value types.
|
|
80
|
+
* `Postsvg::SvgBuilder` — append-only SVG emitter (dedups clipPaths / gradients).
|
|
81
|
+
* `Postsvg::Renderer` + `Postsvg::Visitors::*` — PS → SVG orchestration / dispatch.
|
|
82
|
+
* `Postsvg::Svg` (`Parser`, `Element`, `Elements::*`) — SVG domain model.
|
|
83
|
+
* `Postsvg::Translation` (`PsRenderer`, `HandlerRegistry`, `Handlers::*`) — SVG → PS dispatch.
|
|
84
|
+
* `Postsvg::Serializer` — Model records → PS / EPS source text.
|
|
85
|
+
* `Postsvg::CLI` — Thor command-line wrapper.
|
|
86
|
+
|
|
87
|
+
## Dispatch model
|
|
88
|
+
|
|
89
|
+
Both directions use an OCP-friendly dispatch via `Model::Operator#accept(visitor, ctx)` for the forward direction and `Translation::HandlerRegistry#handler_for(element)` for the reverse direction.
|
|
90
|
+
|
|
91
|
+
* **Forward direction**: each `Model::Operator` subclass calls `Operator#register_as(keyword)`, which registers the class in `Model::Operators.@registry` and `define_method(:visit_name)` to route to a `visitor.visit_<name>` method. Adding a new PS operator means writing one class + one `visit_*` method; no switch edits.
|
|
92
|
+
* **Reverse direction**: each `Svg::Elements::*` subclass calls `Element.register(tag_name, self)` on load. The `HandlerRegistry` looks up handlers by exact class, then walks the superclass chain so subclassing handlers inherits behaviour. Adding a new SVG element means writing one handler class + one `Element.register` call; no switch edits.
|
|
93
|
+
|
|
94
|
+
The per-category visitor modules (`lib/postsvg/visitors/ps_visitor/*.rb`) are split by PLRM chapter (Stack, Arithmetic, Boolean, Path, Painting, Color, GraphicsState, Transformations, Dictionary, ControlFlow, Device, Font, Container, Common). Adding a category = new file + `include` line.
|
|
95
|
+
|
|
96
|
+
## Runtime stack vs AST operands
|
|
97
|
+
|
|
98
|
+
The visitor maintains its OWN operand stack (`@stack`), separate from the parser's `OperandStack`. Most operators pop from the RUNTIME stack — not the AST — because chained ops like `1 2 add 3 mul` produce `Computed` sentinels at parse time that have no real value. Operators whose AST operands can be `Computed` (arithmetic, boolean, control flow, font, container) MUST pop from runtime:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
def visit_add(_op, _ctx)
|
|
102
|
+
b = pop_runtime_number
|
|
103
|
+
a = pop_runtime_number
|
|
104
|
+
@stack << a + b
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Operators whose AST operands are always literal-typed at parse time (moveto, setrgbcolor) can use AST operands directly.
|
|
109
|
+
|
|
110
|
+
The shared helpers (`numeric_value`, `truthy?`, `lookup_dict`, `normalize_key`, `string_value`) live in `PsVisitor::Common` (`lib/postsvg/visitors/ps_visitor/common.rb`) — DRY: previously `normalize_key` was duplicated in Dictionary and Container.
|
|
111
|
+
|
|
112
|
+
## Source order semantics
|
|
113
|
+
|
|
114
|
+
`Postsvg::Source::AstBuilder` writes every consumed token — literals and operators — into `Program.body` in source order. This is what the visitor walks. It is NOT just the operators. So `program.body.first` may be a `Number`, not the first operator. This is by design: the visitor must see the literals to populate the operand stack correctly.
|
|
115
|
+
|
|
116
|
+
## Y-flip and viewbox
|
|
117
|
+
|
|
118
|
+
PostScript origin is bottom-left; SVG origin is top-left. `Renderer#call` opens the SVG with the viewBox from the DSC header and wraps all shapes in `<g transform="translate(0 H) scale(1 -1)">` so PS-native coordinates produce visually identical output. `SvgBuilder#open_y_flip_group` is the entry point.
|
|
119
|
+
|
|
120
|
+
When no `%%BoundingBox` is present the renderer falls back to the A4 page size (595×842 pt) from `Options::DEFAULT_PAGE_SIZE`.
|
|
121
|
+
|
|
122
|
+
## Dispatch gotcha: `private_class_method` and explicit receivers
|
|
123
|
+
|
|
124
|
+
`Postsvg::Color` has utility class methods (`clamp_byte`, `scale_unit_to_byte`) that look private but are called from inside `initialize` with explicit `Color.clamp_byte(...)`. Marking them private via `private_class_method` causes `NoMethodError: private method 'clamp_byte' called` at load time. **Keep these public.** The project rule forbids `send`, but it's silent on `private_class_method`; we follow the spirit (no privacy barriers) and keep them public.
|
|
125
|
+
|
|
126
|
+
## Determinism invariants
|
|
127
|
+
|
|
128
|
+
- Two runs of the same input produce byte-equal output from both directions.
|
|
129
|
+
- `SvgBuilder` IDs (`clip1`, `grad1`, `pattern1`) start at 1 on every `SvgBuilder.new`.
|
|
130
|
+
- `FormatNumber` is the single source of truth for number formatting.
|
|
131
|
+
- No `Time.now`, `SecureRandom`, or `Object#object_id` in IDs.
|
|
132
|
+
|
|
133
|
+
## Style and code-quality rules
|
|
134
|
+
|
|
135
|
+
These come from the user's private global `~/.claude/CLAUDE.md` (Code Quality Standards) and are non-negotiable.
|
|
136
|
+
|
|
137
|
+
- **No `require_relative` in `lib/`** (and no `require` of internal paths either). Use Ruby `autoload` declared in the immediate parent namespace's file (`lib/postsvg.rb` autoloads top-level constants; `lib/postsvg/visitors/ps_visitor.rb` autoloads `PsVisitor::Stack` etc.). The exe (`exe/postsvg`) is allowed to require internal paths.
|
|
138
|
+
- **No doubles in specs.** Use real instances or `Struct.new`.
|
|
139
|
+
- **No `send` to private methods, no `instance_variable_set/get`, no `respond_to?` for type checks.** For dispatcher "does class define this method?" checks use `self.class.public_method_defined?(method_name)`, not `respond_to?`.
|
|
140
|
+
- **No AI attribution** anywhere (commits, PRs, code comments, changelog).
|
|
141
|
+
- **Never delete source files.** Legacy Parslet-based files live at their original paths. The current pipeline does not autoload them; they are kept for reference (see `TODO.roadmap/02-isolate-dormant-code.md`).
|
|
142
|
+
- **Never commit to `main`, never push tags, never push to `main`.** All changes go through PRs.
|
|
143
|
+
- **Library packages have no side effects.** The gem never writes to its own installed location.
|
|
144
|
+
|
|
145
|
+
## Public API
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
# Forward direction (PS / EPS → SVG)
|
|
149
|
+
Postsvg.to_svg(ps_or_eps_string, **opts)
|
|
150
|
+
Postsvg.to_svg_file(input_path, output_path=nil, **opts)
|
|
151
|
+
Postsvg.convert(...) # alias for to_svg (BC)
|
|
152
|
+
Postsvg.convert_file(...) # alias (BC)
|
|
153
|
+
|
|
154
|
+
# Reverse direction (SVG → PS / EPS)
|
|
155
|
+
Postsvg.to_ps(svg_string, eps: false, **opts)
|
|
156
|
+
Postsvg.to_eps(svg_string, **opts)
|
|
157
|
+
Postsvg.to_ps_file(input_path, output_path=nil, eps: false, **opts)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Shared `Postsvg::Options` (frozen struct): `eps`, `width`, `height`, `viewbox_override`, `verbose`, `page_size`.
|
|
161
|
+
|
|
162
|
+
Errors: `Postsvg::ParseError`, `LexError`, `SyntaxError`, `RenderError`, `StackUnderflowError`, `UndefinedOperatorError`, `RecursionLimitError`, `SizeLimitError`, `TranslationError`, `UnsupportedElementError`, `UnresolvedReferenceError`, `SerializeError`. All inherit from `Postsvg::Error`.
|
|
163
|
+
|
|
164
|
+
## Dependencies
|
|
165
|
+
|
|
166
|
+
- `parslet`, `thor`, `nokogiri`, `lutaml/canon` (dev only, for XML matcher in integration specs) — see `Gemfile`.
|
|
167
|
+
|
|
168
|
+
## Limitations
|
|
169
|
+
|
|
170
|
+
See `README.adoc` and `TODO.roadmap/`. Highlights:
|
|
171
|
+
|
|
172
|
+
- Real text/font rendering, image round-trip, Level 3 shading, forms are P2 / P3.
|
|
173
|
+
- 5 integration specs are currently `pending` (awaiting new-pipeline parity with the legacy SvgBuilder output for `colors.ps`, `example_full.ps`, `file.ps`, `prog.ps`, and `img.ps` / `img.eps`).
|
data/Gemfile
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
source "https://rubygems.org"
|
|
4
4
|
|
|
5
|
-
# Specify your gem's dependencies in xml-c14n.gemspec
|
|
6
5
|
gemspec
|
|
7
6
|
|
|
8
|
-
gem "canon", github: "lutaml/canon",
|
|
9
|
-
|
|
7
|
+
gem "canon", github: "lutaml/canon", branch: "main"
|
|
8
|
+
gem "nokogiri"
|
|
10
9
|
gem "rake"
|
|
11
10
|
gem "rspec"
|
|
12
11
|
gem "rubocop"
|