postscript 0.1.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 +7 -0
- data/.rspec +2 -0
- data/Gemfile +12 -0
- data/README.adoc +72 -0
- data/Rakefile +10 -0
- data/lib/postscript/color.rb +132 -0
- data/lib/postscript/errors.rb +56 -0
- data/lib/postscript/format_number.rb +22 -0
- data/lib/postscript/matrix.rb +109 -0
- data/lib/postscript/model/literals/array.rb +41 -0
- data/lib/postscript/model/literals/dictionary.rb +34 -0
- data/lib/postscript/model/literals/hex.rb +37 -0
- data/lib/postscript/model/literals/name.rb +40 -0
- data/lib/postscript/model/literals/number.rb +36 -0
- data/lib/postscript/model/literals/procedure.rb +41 -0
- data/lib/postscript/model/literals/string.rb +30 -0
- data/lib/postscript/model/literals.rb +19 -0
- data/lib/postscript/model/operator.rb +58 -0
- data/lib/postscript/model/operators/arithmetic.rb +264 -0
- data/lib/postscript/model/operators/boolean.rb +182 -0
- data/lib/postscript/model/operators/color.rb +74 -0
- data/lib/postscript/model/operators/container.rb +186 -0
- data/lib/postscript/model/operators/control_flow.rb +119 -0
- data/lib/postscript/model/operators/device.rb +21 -0
- data/lib/postscript/model/operators/dictionary.rb +118 -0
- data/lib/postscript/model/operators/font.rb +121 -0
- data/lib/postscript/model/operators/graphics_state.rb +84 -0
- data/lib/postscript/model/operators/painting.rb +29 -0
- data/lib/postscript/model/operators/path.rb +169 -0
- data/lib/postscript/model/operators/stack.rb +72 -0
- data/lib/postscript/model/operators/transformations.rb +103 -0
- data/lib/postscript/model/operators.rb +89 -0
- data/lib/postscript/model/program.rb +68 -0
- data/lib/postscript/model/token.rb +43 -0
- data/lib/postscript/model.rb +17 -0
- data/lib/postscript/serializer.rb +325 -0
- data/lib/postscript/source/ast_builder.rb +308 -0
- data/lib/postscript/source/lexer.rb +322 -0
- data/lib/postscript/source/operand_stack.rb +55 -0
- data/lib/postscript/source.rb +21 -0
- data/lib/postscript/version.rb +5 -0
- data/lib/postscript.rb +33 -0
- data/postscript.gemspec +37 -0
- metadata +91 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 657b74cfdae6702c2d5aa7bc8f6fcc4d80c4319f38806b2d1a4c0f48d9d050e9
|
|
4
|
+
data.tar.gz: f13ac7de23328c6a211cd40e915942ab4228bdb12bd0cda6f565f557547fda20
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ca8e96d0ad7ca8b75f0b41690ed71e9b0e55167f1326db5cce018ae41f49a0e104b5fd77d588d4bfe5aa0ad41ebaa7eccb166b773db5e2b1dd5312a0f90063bd
|
|
7
|
+
data.tar.gz: 43a30efc33221687d6658e4fd61dbe4b97b9851991a8a345ba14b68f7e01e11c164d9c60c5773d0ae796777e64f4e7998f4884eecd609b9b33aee921a515ad78
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/README.adoc
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
= Postscript
|
|
2
|
+
|
|
3
|
+
Pure-Ruby PostScript (PS) / EPS parser, typed domain model, and
|
|
4
|
+
serializer. The lower layer of the
|
|
5
|
+
https://github.com/claricle/postsvg[postsvg] converter. Independently
|
|
6
|
+
reusable for any tool that needs to read, write, or transform
|
|
7
|
+
PostScript source.
|
|
8
|
+
|
|
9
|
+
== Installation
|
|
10
|
+
|
|
11
|
+
[source,sh]
|
|
12
|
+
----
|
|
13
|
+
gem install postscript
|
|
14
|
+
----
|
|
15
|
+
|
|
16
|
+
== Usage
|
|
17
|
+
|
|
18
|
+
[source,ruby]
|
|
19
|
+
----
|
|
20
|
+
require "postscript"
|
|
21
|
+
|
|
22
|
+
# Parse PS source into a typed AST
|
|
23
|
+
program = Postscript::Source.parse(<<~PS)
|
|
24
|
+
%!PS-Adobe-3.0 EPSF-3.0
|
|
25
|
+
%%BoundingBox: 0 0 100 100
|
|
26
|
+
newpath
|
|
27
|
+
10 10 moveto
|
|
28
|
+
90 10 lineto
|
|
29
|
+
90 90 lineto
|
|
30
|
+
10 90 lineto
|
|
31
|
+
closepath
|
|
32
|
+
fill
|
|
33
|
+
showpage
|
|
34
|
+
PS
|
|
35
|
+
|
|
36
|
+
# Walk the AST
|
|
37
|
+
program.body.each do |node|
|
|
38
|
+
puts node.class
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Re-serialize back to PS source
|
|
42
|
+
ps_text = Postscript::Serializer.call(program)
|
|
43
|
+
puts ps_text
|
|
44
|
+
----
|
|
45
|
+
|
|
46
|
+
== Architecture
|
|
47
|
+
|
|
48
|
+
Three layers, each with a single responsibility:
|
|
49
|
+
|
|
50
|
+
[cols="1,4", options="header"]
|
|
51
|
+
|===
|
|
52
|
+
| Layer | Owns
|
|
53
|
+
| `Postscript::Source` | PS source → tokens → `Model::Program`. Lexer is comment-aware.
|
|
54
|
+
| `Postscript::Model` | Typed PS records: `Program`, `Literals::*`, `Operators::*` (one class per PS operator).
|
|
55
|
+
| `Postscript::Serializer` | `Model::Program` → PS source text.
|
|
56
|
+
|===
|
|
57
|
+
|
|
58
|
+
Adding a new PS operator = writing one subclass + one `visit_*`
|
|
59
|
+
method. No switch edits (OCP).
|
|
60
|
+
|
|
61
|
+
== Value types
|
|
62
|
+
|
|
63
|
+
This gem also hosts three general-purpose value types that
|
|
64
|
+
postsvg depends on:
|
|
65
|
+
|
|
66
|
+
* `Postscript::Matrix` — 2D affine transform.
|
|
67
|
+
* `Postscript::Color` — RGB / Gray / CMYK immutable value.
|
|
68
|
+
* `Postscript::FormatNumber` — float → PS-safe text.
|
|
69
|
+
|
|
70
|
+
== License
|
|
71
|
+
|
|
72
|
+
BSD-2-Clause. See `LICENSE`.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postscript
|
|
4
|
+
# Immutable RGB color value object. Use the constructors (+.rgb+,
|
|
5
|
+
# +.gray+, +.cmyk+, +.parse+) instead of +new+ when constructing from
|
|
6
|
+
# other color models; they normalize to integer [0, 255] channels.
|
|
7
|
+
class Color
|
|
8
|
+
include Comparable
|
|
9
|
+
|
|
10
|
+
attr_reader :red, :green, :blue
|
|
11
|
+
|
|
12
|
+
def self.clamp_byte(value)
|
|
13
|
+
Integer === value ? value.clamp(0, 255) : value.to_i.clamp(0, 255)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.scale_unit_to_byte(value)
|
|
17
|
+
(value.clamp(0.0, 1.0) * 255.0).round
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(red, green, blue)
|
|
21
|
+
@red = Color.clamp_byte(red)
|
|
22
|
+
@green = Color.clamp_byte(green)
|
|
23
|
+
@blue = Color.clamp_byte(blue)
|
|
24
|
+
freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# RGB triple in PS-native [0.0, 1.0] floats.
|
|
28
|
+
def self.rgb(r, g, b)
|
|
29
|
+
new(scale_unit_to_byte(r), scale_unit_to_byte(g), scale_unit_to_byte(b))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Gray in [0.0, 1.0].
|
|
33
|
+
def self.gray(level)
|
|
34
|
+
v = scale_unit_to_byte(level)
|
|
35
|
+
new(v, v, v)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# CMYK in [0.0, 1.0]. Conversion matches PLRM §8.2.4 simplified
|
|
39
|
+
# formula; not colorimetrically exact.
|
|
40
|
+
def self.cmyk(c, m, y, k)
|
|
41
|
+
new(
|
|
42
|
+
scale_unit_to_byte((1 - c) * (1 - k)),
|
|
43
|
+
scale_unit_to_byte((1 - m) * (1 - k)),
|
|
44
|
+
scale_unit_to_byte((1 - y) * (1 - k)),
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Parse CSS / SVG color notations: #rgb, #rrggbb, rgb(r, g, b),
|
|
49
|
+
# rgb(r g b / a), named colors (X11 subset).
|
|
50
|
+
def self.parse(text)
|
|
51
|
+
return text if text.is_a?(Color)
|
|
52
|
+
|
|
53
|
+
case text.to_s.downcase
|
|
54
|
+
when /\A#([0-9a-f]{6})\z/
|
|
55
|
+
hex = ::Regexp.last_match(1)
|
|
56
|
+
new(hex[0, 2].to_i(16), hex[2, 2].to_i(16), hex[4, 2].to_i(16))
|
|
57
|
+
when /\A#([0-9a-f]{3})\z/
|
|
58
|
+
hex = ::Regexp.last_match(1)
|
|
59
|
+
new((hex[0] * 2).to_i(16), (hex[1] * 2).to_i(16), (hex[2] * 2).to_i(16))
|
|
60
|
+
when /\argb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/
|
|
61
|
+
new(::Regexp.last_match(1).to_i, ::Regexp.last_match(2).to_i, ::Regexp.last_match(3).to_i)
|
|
62
|
+
when /\argb\(\s*(\d+)\s+(\d+)\s+(\d+)/
|
|
63
|
+
new(::Regexp.last_match(1).to_i, ::Regexp.last_match(2).to_i, ::Regexp.last_match(3).to_i)
|
|
64
|
+
when "none", "transparent"
|
|
65
|
+
nil
|
|
66
|
+
else
|
|
67
|
+
rgb = NAMED_COLORS[text.to_s]
|
|
68
|
+
return new(*rgb) if rgb
|
|
69
|
+
|
|
70
|
+
raise ArgumentError, "unrecognized color: #{text.inspect}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def gray?
|
|
75
|
+
red == green && green == blue
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def gray_level
|
|
79
|
+
(red + green + blue) / 765.0
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def to_rgb_unit
|
|
83
|
+
[red / 255.0, green / 255.0, blue / 255.0]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def to_svg
|
|
87
|
+
format("#%02x%02x%02x", red, green, blue)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def to_ps_rgb
|
|
91
|
+
to_rgb_unit.map { |c| FormatNumber.call(c) }.join(" ")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def to_ps_gray
|
|
95
|
+
raise ArgumentError, "color is not gray" unless gray?
|
|
96
|
+
|
|
97
|
+
FormatNumber.call(gray_level)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def <=>(other)
|
|
101
|
+
return nil unless other.is_a?(Color)
|
|
102
|
+
|
|
103
|
+
[red, green, blue] <=> [other.red, other.green, other.blue]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def hash
|
|
107
|
+
[red, green, blue].hash
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def eql?(other)
|
|
111
|
+
other.is_a?(Color) && other.red == red && other.green == green && other.blue == blue
|
|
112
|
+
end
|
|
113
|
+
alias == eql?
|
|
114
|
+
|
|
115
|
+
NAMED_COLORS = {
|
|
116
|
+
"black" => [0, 0, 0], "white" => [255, 255, 255],
|
|
117
|
+
"red" => [255, 0, 0], "green" => [0, 128, 0], "blue" => [0, 0, 255],
|
|
118
|
+
"yellow" => [255, 255, 0], "cyan" => [0, 255, 255],
|
|
119
|
+
"magenta" => [255, 0, 255], "gray" => [128, 128, 128],
|
|
120
|
+
"grey" => [128, 128, 128], "silver" => [192, 192, 192],
|
|
121
|
+
"maroon" => [128, 0, 0], "olive" => [128, 128, 0],
|
|
122
|
+
"navy" => [0, 0, 128], "purple" => [128, 0, 128],
|
|
123
|
+
"teal" => [0, 128, 128], "lime" => [0, 255, 0],
|
|
124
|
+
"aqua" => [0, 255, 255], "fuchsia" => [255, 0, 255],
|
|
125
|
+
"orange" => [255, 165, 0], "pink" => [255, 192, 203],
|
|
126
|
+
"brown" => [165, 42, 42],
|
|
127
|
+
}.freeze
|
|
128
|
+
|
|
129
|
+
BLACK = Color.new(0, 0, 0).freeze
|
|
130
|
+
WHITE = Color.new(255, 255, 255).freeze
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postscript
|
|
4
|
+
# Base class for every Postscript gem error. rescue this to catch
|
|
5
|
+
# anything the gem raises.
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Lexing or parsing failed. Carries +source_position+ when known.
|
|
9
|
+
class ParseError < Error
|
|
10
|
+
attr_reader :source_position
|
|
11
|
+
|
|
12
|
+
def initialize(message, source_position: nil)
|
|
13
|
+
@source_position = source_position
|
|
14
|
+
super(message)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Lexer hit a character or construct it could not consume.
|
|
19
|
+
class LexError < ParseError; end
|
|
20
|
+
|
|
21
|
+
# Tokens did not form a valid PostScript program (unbalanced braces,
|
|
22
|
+
# missing operands, malformed literal).
|
|
23
|
+
class SyntaxError < ParseError; end
|
|
24
|
+
|
|
25
|
+
# PS -> SVG / serializer execution failed.
|
|
26
|
+
class RenderError < Error
|
|
27
|
+
attr_reader :operator_name
|
|
28
|
+
|
|
29
|
+
def initialize(message, operator_name: nil)
|
|
30
|
+
@operator_name = operator_name
|
|
31
|
+
super(message)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# An operator tried to pop more values than the stack held.
|
|
36
|
+
class StackUnderflowError < RenderError; end
|
|
37
|
+
|
|
38
|
+
# An operator referenced a name that has no definition in any
|
|
39
|
+
# active dictionary.
|
|
40
|
+
class UndefinedOperatorError < RenderError; end
|
|
41
|
+
|
|
42
|
+
# A procedure invocation chain exceeded the depth limit.
|
|
43
|
+
class RecursionLimitError < RenderError; end
|
|
44
|
+
|
|
45
|
+
# Output exceeded the configured byte limit.
|
|
46
|
+
class SizeLimitError < RenderError; end
|
|
47
|
+
|
|
48
|
+
# Model -> PS source serialization failed.
|
|
49
|
+
class SerializeError < Error; end
|
|
50
|
+
|
|
51
|
+
# Internal control-flow signals used by visitors to unwind loops
|
|
52
|
+
# and stop program execution. NOT user-facing errors; caught by
|
|
53
|
+
# the orchestrator (e.g. Postscript::Renderer).
|
|
54
|
+
class ExitSignal < StandardError; end
|
|
55
|
+
class QuitSignal < StandardError; end
|
|
56
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postscript
|
|
4
|
+
# Single source of truth for formatting numbers in both directions.
|
|
5
|
+
#
|
|
6
|
+
# - Integers print without a trailing ".0".
|
|
7
|
+
# - Floats print to 4 decimals, trailing zeros stripped.
|
|
8
|
+
# - -0 normalizes to 0 (so viewBoxes don't read "0 -0 100 100").
|
|
9
|
+
# - +Inf / -Inf / NaN raise ArgumentError; callers should validate.
|
|
10
|
+
module FormatNumber
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def call(value)
|
|
14
|
+
raise ArgumentError, "non-finite number: #{value.inspect}" unless value.finite?
|
|
15
|
+
|
|
16
|
+
normalized = value.zero? ? 0.0 : value
|
|
17
|
+
return normalized.to_i.to_s if normalized == normalized.to_i
|
|
18
|
+
|
|
19
|
+
format("%.4f", normalized).sub(/\.?0+$/, "")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postscript
|
|
4
|
+
# 2D affine transformation matrix [a b c d e f].
|
|
5
|
+
#
|
|
6
|
+
# Used by both PS-side (CTM, transforms) and SVG-side (transform
|
|
7
|
+
# attribute parsing). General-purpose; lives here so both postsvg
|
|
8
|
+
# and a future pdf gem can share it.
|
|
9
|
+
class Matrix
|
|
10
|
+
attr_accessor :a, :b, :c, :d, :e, :f
|
|
11
|
+
|
|
12
|
+
def initialize(a: 1, b: 0, c: 0, d: 1, e: 0, f: 0)
|
|
13
|
+
@a = a
|
|
14
|
+
@b = b
|
|
15
|
+
@c = c
|
|
16
|
+
@d = d
|
|
17
|
+
@e = e
|
|
18
|
+
@f = f
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def multiply(matrix)
|
|
22
|
+
result = Matrix.new
|
|
23
|
+
result.a = (@a * matrix.a) + (@c * matrix.b)
|
|
24
|
+
result.b = (@b * matrix.a) + (@d * matrix.b)
|
|
25
|
+
result.c = (@a * matrix.c) + (@c * matrix.d)
|
|
26
|
+
result.d = (@b * matrix.c) + (@d * matrix.d)
|
|
27
|
+
result.e = (@a * matrix.e) + (@c * matrix.f) + @e
|
|
28
|
+
result.f = (@b * matrix.e) + (@d * matrix.f) + @f
|
|
29
|
+
result
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def translate(tx, ty)
|
|
33
|
+
multiply(Matrix.new(e: tx, f: ty))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def scale(sx, sy)
|
|
37
|
+
multiply(Matrix.new(a: sx, d: sy))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def rotate(degrees)
|
|
41
|
+
radians = degrees * Math::PI / 180.0
|
|
42
|
+
m = Matrix.new
|
|
43
|
+
m.a = Math.cos(radians)
|
|
44
|
+
m.b = Math.sin(radians)
|
|
45
|
+
m.c = -Math.sin(radians)
|
|
46
|
+
m.d = Math.cos(radians)
|
|
47
|
+
multiply(m)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def skew_x(angle)
|
|
51
|
+
radians = angle * Math::PI / 180.0
|
|
52
|
+
multiply(Matrix.new(c: Math.tan(radians)))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def skew_y(angle)
|
|
56
|
+
radians = angle * Math::PI / 180.0
|
|
57
|
+
multiply(Matrix.new(b: Math.tan(radians)))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_transform_string
|
|
61
|
+
"matrix(#{@a} #{@b} #{@c} #{@d} #{@e} #{@f})"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def apply_point(x, y)
|
|
65
|
+
{
|
|
66
|
+
x: (x * @a) + (y * @c) + @e,
|
|
67
|
+
y: (x * @b) + (y * @d) + @f,
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def decompose
|
|
72
|
+
scale_x = Math.hypot(@a, @b)
|
|
73
|
+
scale_y = ((@a * @d) - (@b * @c)) / scale_x
|
|
74
|
+
|
|
75
|
+
rotation = Math.atan2(@b, @a) * (180.0 / Math::PI)
|
|
76
|
+
|
|
77
|
+
skew_x = Math.atan2((@a * @c) + (@b * @d), scale_x * scale_x)
|
|
78
|
+
skew_y = Math.atan2((@a * @b) + (@c * @d), scale_y * scale_y)
|
|
79
|
+
|
|
80
|
+
{
|
|
81
|
+
translate: { x: @e, y: @f },
|
|
82
|
+
scale: { x: scale_x, y: scale_y },
|
|
83
|
+
rotate: rotation,
|
|
84
|
+
skew: {
|
|
85
|
+
x: skew_x * (180.0 / Math::PI),
|
|
86
|
+
y: skew_y * (180.0 / Math::PI),
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def invert
|
|
92
|
+
det = (@a * @d) - (@b * @c)
|
|
93
|
+
return Matrix.new if det.abs < 1e-10
|
|
94
|
+
|
|
95
|
+
inv = Matrix.new
|
|
96
|
+
inv.a = @d / det
|
|
97
|
+
inv.b = -@b / det
|
|
98
|
+
inv.c = -@c / det
|
|
99
|
+
inv.d = @a / det
|
|
100
|
+
inv.e = ((@c * @f) - (@d * @e)) / det
|
|
101
|
+
inv.f = ((@b * @e) - (@a * @f)) / det
|
|
102
|
+
inv
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def identity?
|
|
106
|
+
@a == 1 && @b.zero? && @c.zero? && @d == 1 && @e.zero? && @f.zero?
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postscript
|
|
4
|
+
module Model
|
|
5
|
+
module Literals
|
|
6
|
+
# Bracketed array literal: +[1 2 3]+. Elements may be any
|
|
7
|
+
# literal or operator.
|
|
8
|
+
class ArrayLiteral
|
|
9
|
+
include Enumerable
|
|
10
|
+
|
|
11
|
+
attr_reader :elements
|
|
12
|
+
|
|
13
|
+
def initialize(elements = [])
|
|
14
|
+
@elements = elements.freeze
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def each(&block)
|
|
19
|
+
@elements.each(&block)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def length = @elements.length
|
|
23
|
+
def empty? = @elements.empty?
|
|
24
|
+
def [](idx) = @elements[idx]
|
|
25
|
+
|
|
26
|
+
def accept(visitor, ctx)
|
|
27
|
+
visitor.visit_array(self, ctx)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ==(other)
|
|
31
|
+
other.is_a?(ArrayLiteral) && other.elements == @elements
|
|
32
|
+
end
|
|
33
|
+
alias eql? ==
|
|
34
|
+
|
|
35
|
+
def hash
|
|
36
|
+
@elements.hash
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postscript
|
|
4
|
+
module Model
|
|
5
|
+
module Literals
|
|
6
|
+
# Inline dictionary literal: +<< /Key (value) /Num 42 >>+.
|
|
7
|
+
class Dictionary
|
|
8
|
+
attr_reader :entries
|
|
9
|
+
|
|
10
|
+
def initialize(entries = {})
|
|
11
|
+
@entries = entries.dup.freeze
|
|
12
|
+
freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def [](key) = @entries[key]
|
|
16
|
+
def key?(key) = @entries.key?(key)
|
|
17
|
+
def each(&block) = @entries.each(&block)
|
|
18
|
+
|
|
19
|
+
def accept(visitor, ctx)
|
|
20
|
+
visitor.visit_dictionary(self, ctx)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def ==(other)
|
|
24
|
+
other.is_a?(Dictionary) && other.entries == @entries
|
|
25
|
+
end
|
|
26
|
+
alias eql? ==
|
|
27
|
+
|
|
28
|
+
def hash
|
|
29
|
+
@entries.hash
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postscript
|
|
4
|
+
module Model
|
|
5
|
+
module Literals
|
|
6
|
+
# Hex-string literal: +<DEADBEEF>+.
|
|
7
|
+
class HexLiteral
|
|
8
|
+
attr_reader :value
|
|
9
|
+
|
|
10
|
+
def initialize(value)
|
|
11
|
+
@value = value.to_s
|
|
12
|
+
freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Raw bytes encoded by the hex literal.
|
|
16
|
+
def bytes
|
|
17
|
+
hex = value.delete("^0-9a-fA-F")
|
|
18
|
+
hex << "0" if hex.length.odd?
|
|
19
|
+
hex.chars.each_slice(2).map { |pair| pair.join.to_i(16) }.pack("C*")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def accept(visitor, ctx)
|
|
23
|
+
visitor.visit_hex(self, ctx)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ==(other)
|
|
27
|
+
other.is_a?(HexLiteral) && other.value == value
|
|
28
|
+
end
|
|
29
|
+
alias eql? ==
|
|
30
|
+
|
|
31
|
+
def hash
|
|
32
|
+
value.hash
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postscript
|
|
4
|
+
module Model
|
|
5
|
+
module Literals
|
|
6
|
+
# A PostScript name. +literal:+ distinguishes +/foo+ (literal,
|
|
7
|
+
# pushed onto the stack) from +foo+ (executable, looked up and
|
|
8
|
+
# executed).
|
|
9
|
+
class Name
|
|
10
|
+
attr_reader :value, :literal
|
|
11
|
+
|
|
12
|
+
def initialize(value, literal: false)
|
|
13
|
+
@value = value.to_s
|
|
14
|
+
@literal = literal
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def literal? = literal
|
|
19
|
+
def executable? = !literal
|
|
20
|
+
|
|
21
|
+
def to_s
|
|
22
|
+
@value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def accept(visitor, ctx)
|
|
26
|
+
visitor.visit_name(self, ctx)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def ==(other)
|
|
30
|
+
other.is_a?(Name) && other.value == value && other.literal == literal
|
|
31
|
+
end
|
|
32
|
+
alias eql? ==
|
|
33
|
+
|
|
34
|
+
def hash
|
|
35
|
+
[value, literal].hash
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postscript
|
|
4
|
+
module Model
|
|
5
|
+
module Literals
|
|
6
|
+
# A PostScript numeric literal. Stored as a Ruby Numeric so
|
|
7
|
+
# arithmetic Just Works; class carries the original source
|
|
8
|
+
# representation choice (int vs float) when known.
|
|
9
|
+
class Number
|
|
10
|
+
attr_reader :value
|
|
11
|
+
|
|
12
|
+
def initialize(value)
|
|
13
|
+
@value = value.is_a?(Numeric) ? value : Float(value)
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_i = value.to_i
|
|
18
|
+
def to_f = value.to_f
|
|
19
|
+
def integer? = value.is_a?(Integer)
|
|
20
|
+
|
|
21
|
+
def accept(visitor, ctx)
|
|
22
|
+
visitor.visit_number(self, ctx)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def ==(other)
|
|
26
|
+
other.is_a?(Number) && other.value == value
|
|
27
|
+
end
|
|
28
|
+
alias eql? ==
|
|
29
|
+
|
|
30
|
+
def hash
|
|
31
|
+
value.hash
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postscript
|
|
4
|
+
module Model
|
|
5
|
+
module Literals
|
|
6
|
+
# Procedure body: +{ ... }+. Carries the inner token stream as a
|
|
7
|
+
# list of literal/operator nodes. Procedures are not executed by
|
|
8
|
+
# the parser; the renderer descends into them on InvokeProcedure.
|
|
9
|
+
class Procedure
|
|
10
|
+
include Enumerable
|
|
11
|
+
|
|
12
|
+
attr_reader :body
|
|
13
|
+
|
|
14
|
+
def initialize(body = [])
|
|
15
|
+
@body = body.freeze
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def each(&block)
|
|
20
|
+
@body.each(&block)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def length = @body.length
|
|
24
|
+
def empty? = @body.empty?
|
|
25
|
+
|
|
26
|
+
def accept(visitor, ctx)
|
|
27
|
+
visitor.visit_procedure(self, ctx)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ==(other)
|
|
31
|
+
other.is_a?(Procedure) && other.body == @body
|
|
32
|
+
end
|
|
33
|
+
alias eql? ==
|
|
34
|
+
|
|
35
|
+
def hash
|
|
36
|
+
@body.hash
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postscript
|
|
4
|
+
module Model
|
|
5
|
+
module Literals
|
|
6
|
+
# Parenthesized string literal: +(foo bar baz)+.
|
|
7
|
+
class StringLiteral
|
|
8
|
+
attr_reader :value
|
|
9
|
+
|
|
10
|
+
def initialize(value)
|
|
11
|
+
@value = value.to_s
|
|
12
|
+
freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def accept(visitor, ctx)
|
|
16
|
+
visitor.visit_string_literal(self, ctx)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ==(other)
|
|
20
|
+
other.is_a?(StringLiteral) && other.value == value
|
|
21
|
+
end
|
|
22
|
+
alias eql? ==
|
|
23
|
+
|
|
24
|
+
def hash
|
|
25
|
+
value.hash
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|