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
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Visitors
|
|
5
|
+
class PsVisitor
|
|
6
|
+
# Boolean / comparison operator handlers. Pop from RUNTIME stack
|
|
7
|
+
# so chained comparisons behave correctly.
|
|
8
|
+
module Boolean
|
|
9
|
+
def visit_true(_op, _ctx)
|
|
10
|
+
@stack << true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def visit_false(_op, _ctx)
|
|
14
|
+
@stack << false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def visit_eq(_op, _ctx)
|
|
18
|
+
b = @stack.pop
|
|
19
|
+
a = @stack.pop
|
|
20
|
+
@stack << (a == b)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def visit_ne(_op, _ctx)
|
|
24
|
+
b = @stack.pop
|
|
25
|
+
a = @stack.pop
|
|
26
|
+
@stack << (a != b)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def visit_gt(_op, _ctx)
|
|
30
|
+
b = pop_runtime_number
|
|
31
|
+
a = pop_runtime_number
|
|
32
|
+
@stack << (a > b)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def visit_ge(_op, _ctx)
|
|
36
|
+
b = pop_runtime_number
|
|
37
|
+
a = pop_runtime_number
|
|
38
|
+
@stack << (a >= b)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def visit_lt(_op, _ctx)
|
|
42
|
+
b = pop_runtime_number
|
|
43
|
+
a = pop_runtime_number
|
|
44
|
+
@stack << (a < b)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def visit_le(_op, _ctx)
|
|
48
|
+
b = pop_runtime_number
|
|
49
|
+
a = pop_runtime_number
|
|
50
|
+
@stack << (a <= b)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def visit_and(_op, _ctx)
|
|
54
|
+
b = @stack.pop
|
|
55
|
+
a = @stack.pop
|
|
56
|
+
result =
|
|
57
|
+
if a.is_a?(Integer) && b.is_a?(Integer)
|
|
58
|
+
a & b
|
|
59
|
+
else
|
|
60
|
+
truthy?(a) && truthy?(b)
|
|
61
|
+
end
|
|
62
|
+
@stack << result
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def visit_or(_op, _ctx)
|
|
66
|
+
b = @stack.pop
|
|
67
|
+
a = @stack.pop
|
|
68
|
+
result =
|
|
69
|
+
if a.is_a?(Integer) && b.is_a?(Integer)
|
|
70
|
+
a | b
|
|
71
|
+
else
|
|
72
|
+
truthy?(a) || truthy?(b)
|
|
73
|
+
end
|
|
74
|
+
@stack << result
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def visit_xor(_op, _ctx)
|
|
78
|
+
b = @stack.pop
|
|
79
|
+
a = @stack.pop
|
|
80
|
+
result =
|
|
81
|
+
if a.is_a?(Integer) && b.is_a?(Integer)
|
|
82
|
+
a ^ b
|
|
83
|
+
else
|
|
84
|
+
truthy?(a) != truthy?(b)
|
|
85
|
+
end
|
|
86
|
+
@stack << result
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def visit_not(_op, _ctx)
|
|
90
|
+
v = @stack.pop
|
|
91
|
+
result = v.is_a?(Integer) ? ~v : !truthy?(v)
|
|
92
|
+
@stack << result
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def visit_bitshift(_op, _ctx)
|
|
96
|
+
shift = pop_runtime_number.to_i
|
|
97
|
+
v = pop_runtime_number.to_i
|
|
98
|
+
@stack << (shift.negative? ? (v >> -shift) : (v << shift))
|
|
99
|
+
end
|
|
100
|
+
# truthy? and pop_runtime_number are provided by Common /
|
|
101
|
+
# Arithmetic respectively.
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Visitors
|
|
5
|
+
class PsVisitor
|
|
6
|
+
# Color operator handlers. Each updates the current
|
|
7
|
+
# GraphicsContext; the next paint operator reads the new colors.
|
|
8
|
+
module Color
|
|
9
|
+
def visit_setgray(op, _ctx)
|
|
10
|
+
color = Postsvg::Color.gray(op.gray.to_f)
|
|
11
|
+
@graphics.update(fill_color: color, stroke_color: color)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def visit_setrgbcolor(op, _ctx)
|
|
15
|
+
color = Postsvg::Color.rgb(op.red.to_f, op.green.to_f, op.blue.to_f)
|
|
16
|
+
@graphics.update(fill_color: color, stroke_color: color)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def visit_setcmykcolor(op, _ctx)
|
|
20
|
+
color = Postsvg::Color.cmyk(op.cyan.to_f, op.magenta.to_f, op.yellow.to_f, op.key.to_f)
|
|
21
|
+
@graphics.update(fill_color: color, stroke_color: color)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def visit_sethsbcolor(op, _ctx)
|
|
25
|
+
color = hsb_to_color(op.hue.to_f, op.saturation.to_f, op.brightness.to_f)
|
|
26
|
+
@graphics.update(fill_color: color, stroke_color: color)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# HSB to RGB. Standard algorithm.
|
|
32
|
+
def hsb_to_color(h, s, v)
|
|
33
|
+
h = h % 1.0
|
|
34
|
+
i = (h * 6).floor
|
|
35
|
+
f = (h * 6) - i
|
|
36
|
+
p = v * (1 - s)
|
|
37
|
+
q = v * (1 - (f * s))
|
|
38
|
+
t = v * (1 - ((1 - f) * s))
|
|
39
|
+
r, g, b =
|
|
40
|
+
case (i % 6)
|
|
41
|
+
when 0 then [v, t, p]
|
|
42
|
+
when 1 then [q, v, p]
|
|
43
|
+
when 2 then [p, v, t]
|
|
44
|
+
when 3 then [p, q, v]
|
|
45
|
+
when 4 then [t, p, v]
|
|
46
|
+
else [v, p, q]
|
|
47
|
+
end
|
|
48
|
+
Postsvg::Color.rgb(r, g, b)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Visitors
|
|
5
|
+
class PsVisitor
|
|
6
|
+
# Helpers shared across multiple visitor modules. Mixed into
|
|
7
|
+
# PsVisitor so every module sees the same helpers without
|
|
8
|
+
# re-defining them. DRY: previously +normalize_key+ existed
|
|
9
|
+
# in both Dictionary and Container.
|
|
10
|
+
module Common
|
|
11
|
+
# Convert a key from any literal-typed value to the canonical
|
|
12
|
+
# string form used in dictionary lookups.
|
|
13
|
+
def normalize_key(key)
|
|
14
|
+
case key
|
|
15
|
+
when Model::Literals::Name then key.value
|
|
16
|
+
when String then key.to_s.sub(/\A\//, "")
|
|
17
|
+
else key.to_s
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Coerce a runtime value to its underlying string content
|
|
22
|
+
# (handles StringLiteral / HexLiteral / plain String).
|
|
23
|
+
def string_value(node)
|
|
24
|
+
case node
|
|
25
|
+
when String then node
|
|
26
|
+
when Model::Literals::StringLiteral then node.value
|
|
27
|
+
when Model::Literals::HexLiteral then node.bytes
|
|
28
|
+
else nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Resolve a value to a number, looking up names in the dict
|
|
33
|
+
# stack when possible. Falls back to 0 for unknown values.
|
|
34
|
+
def numeric_value(value)
|
|
35
|
+
case value
|
|
36
|
+
when Numeric then value
|
|
37
|
+
when Model::Literals::Number then value.value
|
|
38
|
+
when Model::Literals::Name
|
|
39
|
+
entry = lookup_dict(value.value)
|
|
40
|
+
entry ? numeric_value(entry) : 0
|
|
41
|
+
when Model::Computed then 0
|
|
42
|
+
else 0
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def lookup_dict(name)
|
|
47
|
+
@dict_stack.reverse_each do |dict|
|
|
48
|
+
return dict[name] if dict.key?(name)
|
|
49
|
+
end
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# PS truthiness: false, nil, and 0 are falsy. Everything else
|
|
54
|
+
# (including the integer 1 and any non-zero Numeric, plus any
|
|
55
|
+
# object) is truthy.
|
|
56
|
+
def truthy?(value)
|
|
57
|
+
return false if [false, nil].include?(value)
|
|
58
|
+
|
|
59
|
+
return value != 0 if value.is_a?(Numeric)
|
|
60
|
+
|
|
61
|
+
true
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Visitors
|
|
5
|
+
class PsVisitor
|
|
6
|
+
# Operators that operate on any collection (string, array,
|
|
7
|
+
# dictionary). The visitor dispatches by +is_a?+ on the
|
|
8
|
+
# operand at runtime.
|
|
9
|
+
module Container
|
|
10
|
+
def visit_length(op, _ctx)
|
|
11
|
+
operand = op.operand
|
|
12
|
+
value =
|
|
13
|
+
case operand
|
|
14
|
+
when String then operand.length
|
|
15
|
+
when Array then operand.length
|
|
16
|
+
when Hash then operand.length
|
|
17
|
+
when Model::Literals::StringLiteral then operand.value.length
|
|
18
|
+
when Model::Literals::ArrayLiteral then operand.elements.length
|
|
19
|
+
when Model::Literals::Dictionary then operand.entries.length
|
|
20
|
+
when Model::Literals::HexLiteral then operand.value.length / 2
|
|
21
|
+
else 0
|
|
22
|
+
end
|
|
23
|
+
@stack << value
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def visit_get(op, _ctx)
|
|
27
|
+
operand = op.operand
|
|
28
|
+
key = op.key
|
|
29
|
+
value =
|
|
30
|
+
case operand
|
|
31
|
+
when Hash
|
|
32
|
+
operand[normalize_key(key)]
|
|
33
|
+
when Array
|
|
34
|
+
operand[key.to_i]
|
|
35
|
+
when String
|
|
36
|
+
operand.getbyte(key.to_i)
|
|
37
|
+
when Model::Literals::Dictionary
|
|
38
|
+
operand.entries[normalize_key(key)]
|
|
39
|
+
when Model::Literals::ArrayLiteral
|
|
40
|
+
operand.elements[key.to_i]
|
|
41
|
+
when Model::Literals::StringLiteral
|
|
42
|
+
operand.value.getbyte(key.to_i)
|
|
43
|
+
end
|
|
44
|
+
@stack << value
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def visit_put(op, _ctx)
|
|
48
|
+
# PS put mutates in place. Our value objects are immutable.
|
|
49
|
+
# Marked as a comment in the output so the gap is visible.
|
|
50
|
+
@builder.comment("put: in-place mutation not supported")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def visit_getinterval(op, _ctx)
|
|
54
|
+
case op.operand
|
|
55
|
+
when String
|
|
56
|
+
@stack << op.operand[op.start, op.count]
|
|
57
|
+
when Array
|
|
58
|
+
@stack << op.operand[op.start, op.count]
|
|
59
|
+
when Model::Literals::StringLiteral
|
|
60
|
+
@stack << op.operand.value[op.start, op.count]
|
|
61
|
+
when Model::Literals::ArrayLiteral
|
|
62
|
+
@stack << op.operand.elements[op.start, op.count]
|
|
63
|
+
else
|
|
64
|
+
@stack << ""
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def visit_putinterval(_op, _ctx)
|
|
69
|
+
@builder.comment("putinterval: in-place mutation not supported")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def visit_forall(op, _ctx)
|
|
73
|
+
return unless op.body.is_a?(Model::Literals::Procedure)
|
|
74
|
+
|
|
75
|
+
case op.collection
|
|
76
|
+
when Array
|
|
77
|
+
op.collection.each do |item|
|
|
78
|
+
@stack << item
|
|
79
|
+
op.body.body.each { |node| node.accept(self, nil) }
|
|
80
|
+
end
|
|
81
|
+
when Hash
|
|
82
|
+
op.collection.each do |key, value|
|
|
83
|
+
@stack << value
|
|
84
|
+
@stack << key
|
|
85
|
+
op.body.body.each { |node| node.accept(self, nil) }
|
|
86
|
+
end
|
|
87
|
+
when String
|
|
88
|
+
op.collection.each_byte do |byte|
|
|
89
|
+
@stack << byte
|
|
90
|
+
op.body.body.each { |node| node.accept(self, nil) }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def visit_astore(op, _ctx)
|
|
96
|
+
# astore pops N items and builds an array. The parser
|
|
97
|
+
# already gave us the array via from_operands; just push
|
|
98
|
+
# it back. The original semantics (`arr astore` arr is on
|
|
99
|
+
# bottom, n items on top) are preserved.
|
|
100
|
+
@stack << (op.array.is_a?(Array) ? op.array : [])
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def visit_search(op, _ctx)
|
|
104
|
+
haystack = string_value(op.target)
|
|
105
|
+
needle = string_value(op.pattern)
|
|
106
|
+
if haystack && needle && (idx = haystack.index(needle))
|
|
107
|
+
post = haystack[(idx + needle.length)..]
|
|
108
|
+
@stack << post
|
|
109
|
+
@stack << needle
|
|
110
|
+
@stack << haystack[0, idx]
|
|
111
|
+
@stack << true
|
|
112
|
+
else
|
|
113
|
+
@stack << haystack
|
|
114
|
+
@stack << false
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def visit_anchorsearch(op, _ctx)
|
|
119
|
+
haystack = string_value(op.target)
|
|
120
|
+
needle = string_value(op.pattern)
|
|
121
|
+
if haystack && needle && haystack.start_with?(needle)
|
|
122
|
+
@stack << haystack[needle.length..]
|
|
123
|
+
@stack << needle
|
|
124
|
+
@stack << true
|
|
125
|
+
else
|
|
126
|
+
@stack << haystack
|
|
127
|
+
@stack << false
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def visit_token(op, _ctx)
|
|
132
|
+
text = string_value(op.target)
|
|
133
|
+
return @stack << false unless text
|
|
134
|
+
|
|
135
|
+
# PS token splits on whitespace and parses the next token.
|
|
136
|
+
if (match = text.match(/\A\s*(-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)/))
|
|
137
|
+
@stack << match[1].to_f
|
|
138
|
+
@stack << text[match[0].length..]
|
|
139
|
+
@stack << true
|
|
140
|
+
else
|
|
141
|
+
@stack << false
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def visit_string(op, _ctx)
|
|
146
|
+
@stack << ("\0" * op.count.to_i)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def visit_cvs(op, _ctx)
|
|
150
|
+
text =
|
|
151
|
+
case op.value
|
|
152
|
+
when Numeric then op.value.to_s
|
|
153
|
+
when String then op.value
|
|
154
|
+
when Model::Literals::StringLiteral then op.value.value
|
|
155
|
+
when Model::Literals::Name then op.value.value
|
|
156
|
+
else op.value.to_s
|
|
157
|
+
end
|
|
158
|
+
@stack << text
|
|
159
|
+
end
|
|
160
|
+
# normalize_key and string_value are provided by PsVisitor::Common.
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Visitors
|
|
5
|
+
class PsVisitor
|
|
6
|
+
# Control flow operator handlers. Each pops operands from the
|
|
7
|
+
# RUNTIME stack (not the AST), so chained execution behaves
|
|
8
|
+
# correctly. Bodies are +Procedure+ values that +descend_into_procedure+
|
|
9
|
+
# walks in the current visitor context.
|
|
10
|
+
module ControlFlow
|
|
11
|
+
LOOP_LIMIT = 10_000
|
|
12
|
+
|
|
13
|
+
def visit_if(_op, _ctx)
|
|
14
|
+
body = @stack.pop
|
|
15
|
+
condition = @stack.pop
|
|
16
|
+
return unless truthy?(condition)
|
|
17
|
+
|
|
18
|
+
descend_into_procedure(body)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def visit_ifelse(_op, _ctx)
|
|
22
|
+
else_body = @stack.pop
|
|
23
|
+
if_body = @stack.pop
|
|
24
|
+
condition = @stack.pop
|
|
25
|
+
if truthy?(condition)
|
|
26
|
+
descend_into_procedure(if_body)
|
|
27
|
+
else
|
|
28
|
+
descend_into_procedure(else_body)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def visit_repeat(_op, _ctx)
|
|
33
|
+
body = @stack.pop
|
|
34
|
+
count = pop_runtime_number.to_i.clamp(0, LOOP_LIMIT)
|
|
35
|
+
count.times { descend_into_procedure(body) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def visit_loop(_op, _ctx)
|
|
39
|
+
body = @stack.pop
|
|
40
|
+
LOOP_LIMIT.times do
|
|
41
|
+
descend_into_procedure(body)
|
|
42
|
+
rescue Postsvg::ExitSignal
|
|
43
|
+
break
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def visit_for(_op, _ctx)
|
|
48
|
+
body = @stack.pop
|
|
49
|
+
limit = pop_runtime_number.to_f
|
|
50
|
+
increment = pop_runtime_number.to_f
|
|
51
|
+
initial = pop_runtime_number.to_f
|
|
52
|
+
iterations = 0
|
|
53
|
+
if increment.positive?
|
|
54
|
+
value = initial
|
|
55
|
+
while value <= limit
|
|
56
|
+
@stack << value
|
|
57
|
+
descend_into_procedure(body)
|
|
58
|
+
value += increment
|
|
59
|
+
iterations += 1
|
|
60
|
+
break if iterations > LOOP_LIMIT
|
|
61
|
+
end
|
|
62
|
+
elsif increment.negative?
|
|
63
|
+
value = initial
|
|
64
|
+
while value >= limit
|
|
65
|
+
@stack << value
|
|
66
|
+
descend_into_procedure(body)
|
|
67
|
+
value += increment
|
|
68
|
+
iterations += 1
|
|
69
|
+
break if iterations > LOOP_LIMIT
|
|
70
|
+
end
|
|
71
|
+
else
|
|
72
|
+
raise RenderError, "for: zero increment"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def visit_exit(_op, _ctx)
|
|
77
|
+
raise Postsvg::ExitSignal
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def visit_quit(_op, _ctx)
|
|
81
|
+
raise Postsvg::QuitSignal
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def visit_exec(_op, _ctx)
|
|
85
|
+
operand = @stack.pop
|
|
86
|
+
case operand
|
|
87
|
+
when Model::Literals::Procedure
|
|
88
|
+
descend_into_procedure(operand)
|
|
89
|
+
else
|
|
90
|
+
@stack << operand
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def visit_stopped(_op, _ctx)
|
|
95
|
+
body = @stack.pop
|
|
96
|
+
descend_into_procedure(body)
|
|
97
|
+
@stack << false
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def descend_into_procedure(procedure)
|
|
101
|
+
return unless procedure.is_a?(Model::Literals::Procedure)
|
|
102
|
+
|
|
103
|
+
procedure.body.each { |node| node.accept(self, nil) }
|
|
104
|
+
end
|
|
105
|
+
# truthy? and pop_runtime_number are provided by Common /
|
|
106
|
+
# Arithmetic respectively.
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Visitors
|
|
5
|
+
class PsVisitor
|
|
6
|
+
# Device / page operator handlers. In a single-page SVG render
|
|
7
|
+
# these are largely no-ops; they exist so programs that end
|
|
8
|
+
# with +showpage+ parse cleanly.
|
|
9
|
+
module Device
|
|
10
|
+
def visit_showpage(_op, _ctx)
|
|
11
|
+
# No-op: SVG is single-page; the renderer finalises on close.
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def visit_copypage(_op, _ctx); end
|
|
15
|
+
|
|
16
|
+
def visit_nulldevice(_op, _ctx); end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Visitors
|
|
5
|
+
class PsVisitor
|
|
6
|
+
# Dictionary operator handlers (dict-specific only). Generic
|
|
7
|
+
# collection operators (get / put / length / getinterval /
|
|
8
|
+
# putinterval) live in the +Container+ module because they
|
|
9
|
+
# dispatch on operand type at runtime.
|
|
10
|
+
module Dictionary
|
|
11
|
+
# The base PsVisitor class initializes +@dict_stack+ as a
|
|
12
|
+
# single-element array (the global dictionary). Modules assume
|
|
13
|
+
# it exists; they do not re-initialize.
|
|
14
|
+
|
|
15
|
+
def visit_dict(_op, _ctx)
|
|
16
|
+
@stack << {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def visit_begin(_op, _ctx)
|
|
20
|
+
value = @stack.pop
|
|
21
|
+
@dict_stack << (value.is_a?(Hash) ? value : {})
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def visit_end(_op, _ctx)
|
|
25
|
+
@dict_stack.pop if @dict_stack.length > 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def visit_def(op, _ctx)
|
|
29
|
+
key = normalize_key(op.key)
|
|
30
|
+
@dict_stack.last[key] = op.value
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def visit_load(op, _ctx)
|
|
34
|
+
key = normalize_key(op.key)
|
|
35
|
+
@dict_stack.reverse_each do |dict|
|
|
36
|
+
if dict.key?(key)
|
|
37
|
+
@stack << dict[key]
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
raise UndefinedOperatorError, "no value bound to /#{key}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def visit_store(op, _ctx)
|
|
45
|
+
key = normalize_key(op.key)
|
|
46
|
+
@dict_stack.reverse_each do |dict|
|
|
47
|
+
if dict.key?(key)
|
|
48
|
+
dict[key] = op.value
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
@dict_stack.last[key] = op.value
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def visit_known(op, _ctx)
|
|
56
|
+
key = normalize_key(op.key)
|
|
57
|
+
dict = op.dict.is_a?(Hash) ? op.dict : {}
|
|
58
|
+
@stack << dict.key?(key)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def visit_currentdict(_op, _ctx)
|
|
62
|
+
@stack << @dict_stack.last
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def visit_countdictstack(_op, _ctx)
|
|
66
|
+
@stack << @dict_stack.length
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def visit_dictstack(_op, _ctx)
|
|
70
|
+
@stack << @dict_stack.dup
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def visit_maxlength(op, _ctx)
|
|
74
|
+
# PS dictionaries can grow dynamically; report current size.
|
|
75
|
+
case op.operand
|
|
76
|
+
when Hash then @stack << op.operand.length
|
|
77
|
+
when Model::Literals::Dictionary then @stack << op.operand.entries.length
|
|
78
|
+
else @stack << 0
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def dict_stack
|
|
83
|
+
@dict_stack ||= [{}]
|
|
84
|
+
end
|
|
85
|
+
# normalize_key is provided by PsVisitor::Common.
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Visitors
|
|
5
|
+
class PsVisitor
|
|
6
|
+
# Font and text operator handlers. Each pops from the RUNTIME
|
|
7
|
+
# stack (not the AST) so chained font setup works:
|
|
8
|
+
#
|
|
9
|
+
# /Helvetica findfont 18 scalefont setfont
|
|
10
|
+
#
|
|
11
|
+
# pushes a FontRef after findfont, scales it after scalefont,
|
|
12
|
+
# installs it after setfont.
|
|
13
|
+
module Font
|
|
14
|
+
# Tracks the current font for later +show+. The visitor
|
|
15
|
+
# pushes FontRef value objects onto the stack.
|
|
16
|
+
FontRef = Struct.new(:name, :size, keyword_init: true) do
|
|
17
|
+
def initialize(name:, size:)
|
|
18
|
+
super
|
|
19
|
+
freeze
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def visit_findfont(op, _ctx)
|
|
24
|
+
name =
|
|
25
|
+
case op.name
|
|
26
|
+
when Model::Literals::Name then op.name.value
|
|
27
|
+
when Model::Literals::StringLiteral then op.name.value
|
|
28
|
+
else op.name.to_s
|
|
29
|
+
end
|
|
30
|
+
@stack << FontRef.new(name: name, size: nil)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def visit_scalefont(op, _ctx)
|
|
34
|
+
size = op.size.to_i || op.size.to_f
|
|
35
|
+
font = @stack.pop
|
|
36
|
+
base = font.is_a?(FontRef) ? font : FontRef.new(name: "Helvetica", size: nil)
|
|
37
|
+
@stack << FontRef.new(name: base.name, size: size.to_f)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def visit_setfont(_op, _ctx)
|
|
41
|
+
font = @stack.pop
|
|
42
|
+
ref = font.is_a?(FontRef) ? font : FontRef.new(name: "Helvetica", size: 12.0)
|
|
43
|
+
@graphics.update(font_name: ref.name || "Helvetica",
|
|
44
|
+
font_size: ref.size || 12.0)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def visit_show(_op, _ctx)
|
|
48
|
+
text = @stack.pop
|
|
49
|
+
text_str =
|
|
50
|
+
case text
|
|
51
|
+
when Model::Literals::StringLiteral then text.value
|
|
52
|
+
when String then text
|
|
53
|
+
else text.to_s
|
|
54
|
+
end
|
|
55
|
+
pos = @graphics.current.last_text_position
|
|
56
|
+
return unless pos
|
|
57
|
+
|
|
58
|
+
ctm = @graphics.current.ctm
|
|
59
|
+
screen_pos = ctm.apply_point(pos[:x], pos[:y])
|
|
60
|
+
color = @graphics.current.fill_color
|
|
61
|
+
# SVG y is inverted relative to PS — the Y-flip wrapper
|
|
62
|
+
# group already flips the whole body, so we emit a
|
|
63
|
+
# scale(1 -1) per <text> to undo the flip for text only.
|
|
64
|
+
@builder.text(
|
|
65
|
+
content: text_str,
|
|
66
|
+
x: screen_pos[:x],
|
|
67
|
+
y: -screen_pos[:y],
|
|
68
|
+
font_family: @graphics.current.font_name,
|
|
69
|
+
font_size: @graphics.current.font_size,
|
|
70
|
+
color: color || Postsvg::Color::BLACK,
|
|
71
|
+
transform: "scale(1 -1)",
|
|
72
|
+
)
|
|
73
|
+
@path.reset
|
|
74
|
+
@graphics.update(last_text_position: nil)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def visit_xyshow(_op, _ctx)
|
|
78
|
+
@builder.comment("xyshow: not yet implemented")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def visit_stringwidth(_op, _ctx)
|
|
82
|
+
# Without real font metrics, push zero advance.
|
|
83
|
+
@stack << 0
|
|
84
|
+
@stack << 0
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def visit_charpath(_op, _ctx)
|
|
88
|
+
@builder.comment("charpath: requires font metrics")
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|