postsvg 0.2.2 → 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/.rubocop_todo.yml +49 -533
- data/Gemfile +1 -1
- data/lib/postsvg/cli.rb +5 -10
- data/lib/postsvg/color.rb +132 -0
- data/lib/postsvg/format_number.rb +22 -0
- data/lib/postsvg/graphics_context.rb +1 -2
- data/lib/postsvg/matrix.rb +106 -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 +1 -1
- data/lib/postsvg/parser/postscript_parser.rb +7 -7
- data/lib/postsvg/renderer.rb +3 -7
- 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/clip_path_registry.rb +1 -3
- data/lib/postsvg/svg/element.rb +1 -3
- data/lib/postsvg/svg/elements/ellipse.rb +1 -2
- data/lib/postsvg/svg/elements/svg.rb +1 -2
- data/lib/postsvg/svg/paint.rb +1 -1
- data/lib/postsvg/svg/path_data/parser.rb +6 -10
- data/lib/postsvg/svg/stroke.rb +2 -4
- data/lib/postsvg/svg/transform_list.rb +1 -1
- data/lib/postsvg/svg_builder.rb +11 -15
- data/lib/postsvg/translation/arc_converter.rb +12 -22
- data/lib/postsvg/translation/bounding_box.rb +4 -7
- data/lib/postsvg/translation/handlers/circle_handler.rb +2 -2
- data/lib/postsvg/translation/handlers/ellipse_handler.rb +3 -8
- data/lib/postsvg/translation/handlers/group_handler.rb +1 -2
- data/lib/postsvg/translation/handlers/image_handler.rb +2 -3
- data/lib/postsvg/translation/handlers/line_handler.rb +2 -6
- data/lib/postsvg/translation/handlers/open_handler.rb +2 -3
- data/lib/postsvg/translation/handlers/path_handler.rb +41 -57
- data/lib/postsvg/translation/handlers/polygon_handler.rb +2 -4
- data/lib/postsvg/translation/handlers/polyline_handler.rb +2 -4
- data/lib/postsvg/translation/handlers/rect_handler.rb +5 -10
- data/lib/postsvg/translation/handlers/shared.rb +11 -11
- data/lib/postsvg/translation/handlers/svg_handler.rb +2 -3
- data/lib/postsvg/translation/handlers/text_handler.rb +7 -8
- data/lib/postsvg/translation/handlers.rb +1 -2
- data/lib/postsvg/translation/ps_renderer.rb +1 -2
- data/lib/postsvg/version.rb +1 -1
- data/lib/postsvg/visitors/ps_visitor/arithmetic.rb +6 -6
- data/lib/postsvg/visitors/ps_visitor/color.rb +5 -8
- data/lib/postsvg/visitors/ps_visitor/common.rb +2 -1
- data/lib/postsvg/visitors/ps_visitor/container.rb +13 -13
- data/lib/postsvg/visitors/ps_visitor/control_flow.rb +3 -3
- data/lib/postsvg/visitors/ps_visitor/dictionary.rb +4 -8
- data/lib/postsvg/visitors/ps_visitor/font.rb +4 -14
- data/lib/postsvg/visitors/ps_visitor/graphics_state.rb +4 -4
- data/lib/postsvg/visitors/ps_visitor/painting.rb +1 -1
- data/lib/postsvg/visitors/ps_visitor/path.rb +4 -9
- data/lib/postsvg.rb +37 -42
- metadata +36 -16
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
# Model::Program -> PostScript source text. Walks the program's
|
|
5
|
+
# header (DSC comments) and body (Model records), emitting each as
|
|
6
|
+
# PS source. Used by the SVG -> PS direction and by tests.
|
|
7
|
+
#
|
|
8
|
+
# OCP: adding serialization for a new operator class means adding
|
|
9
|
+
# one +emit_<keyword>+ method (or falling through to the default
|
|
10
|
+
# keyword+operands emitter). No switch edits.
|
|
11
|
+
class Serializer
|
|
12
|
+
DEFAULT_LANGUAGE_LEVEL = 3
|
|
13
|
+
DEFAULT_CREATOR = "Postsvg #{::Postsvg::VERSION}".freeze
|
|
14
|
+
|
|
15
|
+
def self.call(program, eps: false, creator: DEFAULT_CREATOR, **_options)
|
|
16
|
+
new(program, eps: eps, creator: creator).call
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attr_reader :program, :eps, :creator
|
|
20
|
+
|
|
21
|
+
def initialize(program, eps:, creator:)
|
|
22
|
+
raise ArgumentError, "program must be a Model::Program" unless program.is_a?(Model::Program)
|
|
23
|
+
|
|
24
|
+
@program = program
|
|
25
|
+
@eps = eps
|
|
26
|
+
@creator = creator
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def call
|
|
30
|
+
buffer = String.new(capacity: 4096)
|
|
31
|
+
emit_header(buffer)
|
|
32
|
+
emit_body(buffer)
|
|
33
|
+
buffer << "showpage\n"
|
|
34
|
+
buffer << "%%EOF\n"
|
|
35
|
+
buffer
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# All methods below this point are public. They are intentionally
|
|
39
|
+
# exposed (prefixed +emit_+) so dispatch can reach them via
|
|
40
|
+
# +method_defined?+ without +respond_to?+.
|
|
41
|
+
def emit_header(buffer)
|
|
42
|
+
buffer << "%!PS-Adobe-#{DEFAULT_LANGUAGE_LEVEL}.0"
|
|
43
|
+
buffer << " EPSF-3.0" if eps
|
|
44
|
+
buffer << "\n"
|
|
45
|
+
buffer << "%%Creator: #{creator}\n"
|
|
46
|
+
bbox = program.header.bounding_box
|
|
47
|
+
if bbox && !bbox.empty?
|
|
48
|
+
buffer << "%%BoundingBox: " << bbox.map { |v| FormatNumber.call(v) }.join(" ") << "\n"
|
|
49
|
+
end
|
|
50
|
+
if program.header.title
|
|
51
|
+
buffer << "%%Title: #{program.header.title}\n"
|
|
52
|
+
end
|
|
53
|
+
buffer << "%%LanguageLevel: #{DEFAULT_LANGUAGE_LEVEL}\n"
|
|
54
|
+
buffer << "%%EndComments\n"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def emit_body(buffer)
|
|
58
|
+
program.body.each do |statement|
|
|
59
|
+
emit_statement(statement, buffer)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def emit_statement(statement, buffer)
|
|
64
|
+
case statement
|
|
65
|
+
when Model::Literals::Number
|
|
66
|
+
buffer << FormatNumber.call(statement.value) << "\n"
|
|
67
|
+
when Model::Literals::Name
|
|
68
|
+
prefix = statement.literal? ? "/" : ""
|
|
69
|
+
buffer << prefix << statement.value << "\n"
|
|
70
|
+
when Model::Literals::StringLiteral
|
|
71
|
+
buffer << escape_string(statement.value) << "\n"
|
|
72
|
+
when Model::Literals::HexLiteral
|
|
73
|
+
buffer << "<" << statement.value << ">\n"
|
|
74
|
+
when Model::Literals::ArrayLiteral
|
|
75
|
+
buffer << "[ "
|
|
76
|
+
statement.elements.each { |e| emit_inline(e, buffer); buffer << " " }
|
|
77
|
+
buffer << "]\n"
|
|
78
|
+
when Model::Literals::Procedure
|
|
79
|
+
buffer << "{\n"
|
|
80
|
+
statement.body.each { |s| emit_statement(s, buffer) }
|
|
81
|
+
buffer << "}\n"
|
|
82
|
+
when Model::Literals::Dictionary
|
|
83
|
+
buffer << "<<\n"
|
|
84
|
+
statement.entries.each do |k, v|
|
|
85
|
+
buffer << "/#{k} "
|
|
86
|
+
emit_inline(v, buffer)
|
|
87
|
+
buffer << "\n"
|
|
88
|
+
end
|
|
89
|
+
buffer << ">>\n"
|
|
90
|
+
when Model::Operator
|
|
91
|
+
emit_operator(statement, buffer)
|
|
92
|
+
when Model::UnknownOperator
|
|
93
|
+
buffer << "% unhandled operator: #{statement.keyword}\n"
|
|
94
|
+
when Model::InvokeProcedure
|
|
95
|
+
buffer << statement.name << "\n"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def emit_inline(statement, buffer)
|
|
100
|
+
case statement
|
|
101
|
+
when Model::Literals::Number then buffer << FormatNumber.call(statement.value)
|
|
102
|
+
when Model::Literals::Name then buffer << (statement.literal? ? "/" : "") << statement.value
|
|
103
|
+
when Model::Literals::StringLiteral then buffer << escape_string(statement.value)
|
|
104
|
+
when Model::Literals::HexLiteral then buffer << "<" << statement.value << ">"
|
|
105
|
+
else buffer << statement.class.name.to_s
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Dispatch table: emit_<keyword>. Falls back to the generic
|
|
110
|
+
# operand+keyword form when no specific method exists.
|
|
111
|
+
def emit_operator(operator, buffer)
|
|
112
|
+
method = :"emit_#{operator.visit_name}"
|
|
113
|
+
if self.class.public_method_defined?(method)
|
|
114
|
+
public_send(method, operator, buffer)
|
|
115
|
+
else
|
|
116
|
+
emit_generic_operator(operator, buffer)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def emit_generic_operator(operator, buffer)
|
|
121
|
+
buffer << operator.keyword << "\n"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Path
|
|
125
|
+
def emit_moveto(op, buf)
|
|
126
|
+
buf << "#{FormatNumber.call(op.x)} #{FormatNumber.call(op.y)} moveto\n"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def emit_rmoveto(op, buf)
|
|
130
|
+
buf << "#{FormatNumber.call(op.dx)} #{FormatNumber.call(op.dy)} rmoveto\n"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def emit_lineto(op, buf)
|
|
134
|
+
buf << "#{FormatNumber.call(op.x)} #{FormatNumber.call(op.y)} lineto\n"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def emit_rlineto(op, buf)
|
|
138
|
+
buf << "#{FormatNumber.call(op.dx)} #{FormatNumber.call(op.dy)} rlineto\n"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def emit_curveto(op, buf)
|
|
142
|
+
buf << "#{FormatNumber.call(op.x1)} #{FormatNumber.call(op.y1)} " \
|
|
143
|
+
"#{FormatNumber.call(op.x2)} #{FormatNumber.call(op.y2)} " \
|
|
144
|
+
"#{FormatNumber.call(op.x3)} #{FormatNumber.call(op.y3)} curveto\n"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def emit_rcurveto(op, buf)
|
|
148
|
+
buf << "#{FormatNumber.call(op.dx1)} #{FormatNumber.call(op.dy1)} " \
|
|
149
|
+
"#{FormatNumber.call(op.dx2)} #{FormatNumber.call(op.dy2)} " \
|
|
150
|
+
"#{FormatNumber.call(op.dx3)} #{FormatNumber.call(op.dy3)} rcurveto\n"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def emit_arc(op, buf)
|
|
154
|
+
buf << "#{FormatNumber.call(op.x)} #{FormatNumber.call(op.y)} " \
|
|
155
|
+
"#{FormatNumber.call(op.radius)} " \
|
|
156
|
+
"#{FormatNumber.call(op.angle1)} #{FormatNumber.call(op.angle2)} arc\n"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def emit_arcn(op, buf)
|
|
160
|
+
buf << "#{FormatNumber.call(op.x)} #{FormatNumber.call(op.y)} " \
|
|
161
|
+
"#{FormatNumber.call(op.radius)} " \
|
|
162
|
+
"#{FormatNumber.call(op.angle1)} #{FormatNumber.call(op.angle2)} arcn\n"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def emit_closepath(_op, buf)
|
|
166
|
+
buf << "closepath\n"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def emit_newpath(_op, buf)
|
|
170
|
+
buf << "newpath\n"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Painting
|
|
174
|
+
def emit_stroke(_op, buf) = buf << "stroke\n"
|
|
175
|
+
def emit_fill(_op, buf) = buf << "fill\n"
|
|
176
|
+
def emit_eofill(_op, buf) = buf << "eofill\n"
|
|
177
|
+
def emit_clip(_op, buf) = buf << "clip\n"
|
|
178
|
+
def emit_eoclip(_op, buf) = buf << "eoclip\n"
|
|
179
|
+
|
|
180
|
+
# Color
|
|
181
|
+
def emit_setrgbcolor(op, buf)
|
|
182
|
+
buf << "#{FormatNumber.call(op.red)} #{FormatNumber.call(op.green)} #{FormatNumber.call(op.blue)} setrgbcolor\n"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def emit_setgray(op, buf)
|
|
186
|
+
buf << "#{FormatNumber.call(op.gray)} setgray\n"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def emit_setcmykcolor(op, buf)
|
|
190
|
+
buf << "#{FormatNumber.call(op.cyan)} #{FormatNumber.call(op.magenta)} " \
|
|
191
|
+
"#{FormatNumber.call(op.yellow)} #{FormatNumber.call(op.key)} setcmykcolor\n"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def emit_sethsbcolor(op, buf)
|
|
195
|
+
buf << "#{FormatNumber.call(op.hue)} #{FormatNumber.call(op.saturation)} " \
|
|
196
|
+
"#{FormatNumber.call(op.brightness)} sethsbcolor\n"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Graphics state
|
|
200
|
+
def emit_gsave(_op, buf) = buf << "gsave\n"
|
|
201
|
+
def emit_grestore(_op, buf) = buf << "grestore\n"
|
|
202
|
+
def emit_grestoreall(_op, buf) = buf << "grestoreall\n"
|
|
203
|
+
|
|
204
|
+
def emit_setlinewidth(op, buf)
|
|
205
|
+
buf << "#{FormatNumber.call(op.width)} setlinewidth\n"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def emit_setlinecap(op, buf)
|
|
209
|
+
buf << "#{op.cap_code} setlinecap\n"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def emit_setlinejoin(op, buf)
|
|
213
|
+
buf << "#{op.join_code} setlinejoin\n"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def emit_setmiterlimit(op, buf)
|
|
217
|
+
buf << "#{FormatNumber.call(op.limit)} setmiterlimit\n"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def emit_setdash(op, buf)
|
|
221
|
+
pattern_str =
|
|
222
|
+
case op.pattern
|
|
223
|
+
when Array then "[#{op.pattern.map { |v| FormatNumber.call(v.to_f) }.join(' ')}]"
|
|
224
|
+
when Numeric then FormatNumber.call(op.pattern.to_f)
|
|
225
|
+
else "[]"
|
|
226
|
+
end
|
|
227
|
+
buf << "#{pattern_str} #{FormatNumber.call(op.offset)} setdash\n"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Transformations
|
|
231
|
+
def emit_translate(op, buf)
|
|
232
|
+
buf << "#{FormatNumber.call(op.tx)} #{FormatNumber.call(op.ty)} translate\n"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def emit_scale(op, buf)
|
|
236
|
+
buf << "#{FormatNumber.call(op.sx)} #{FormatNumber.call(op.sy)} scale\n"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def emit_rotate(op, buf)
|
|
240
|
+
buf << "#{FormatNumber.call(op.angle)} rotate\n"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def emit_concat(op, buf)
|
|
244
|
+
m = op.matrix
|
|
245
|
+
arr = m.is_a?(Array) ? m : [m.a, m.b, m.c, m.d, m.e, m.f]
|
|
246
|
+
buf << "[#{arr.map { |v| FormatNumber.call(v.to_f) }.join(' ')}] concat\n"
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def escape_string(text)
|
|
250
|
+
escaped = text.to_s.gsub(/[\(\)\\]/) { |c| "\\#{c}" }
|
|
251
|
+
.gsub("\n", "\\n")
|
|
252
|
+
.gsub("\r", "\\r")
|
|
253
|
+
.gsub("\t", "\\t")
|
|
254
|
+
"(#{escaped})"
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Font / text
|
|
258
|
+
def emit_findfont(op, buf)
|
|
259
|
+
name = font_name_value(op.name)
|
|
260
|
+
buf << "/#{name} findfont\n"
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def emit_scalefont(op, buf)
|
|
264
|
+
buf << "#{FormatNumber.call(op.size)} scalefont\n"
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def emit_setfont(_op, buf)
|
|
268
|
+
buf << "setfont\n"
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def emit_show(op, buf)
|
|
272
|
+
buf << "#{escape_string(string_value(op.text))} show\n"
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def emit_xyshow(_op, buf)
|
|
276
|
+
buf << "% xyshow: per-glyph advances not serialized\n"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def emit_stringwidth(_op, buf)
|
|
280
|
+
buf << "% stringwidth: no runtime result captured\n"
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def emit_charpath(_op, buf)
|
|
284
|
+
buf << "% charpath: requires font metrics\n"
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Container operators
|
|
288
|
+
def emit_length(_op, buf) = buf << "length\n"
|
|
289
|
+
def emit_get(_op, buf) = buf << "get\n"
|
|
290
|
+
def emit_put(_op, buf) = buf << "put\n"
|
|
291
|
+
def emit_getinterval(_op, buf) = buf << "getinterval\n"
|
|
292
|
+
def emit_putinterval(_op, buf) = buf << "putinterval\n"
|
|
293
|
+
def emit_forall(_op, buf) = buf << "forall\n"
|
|
294
|
+
def emit_astore(_op, buf) = buf << "astore\n"
|
|
295
|
+
def emit_search(_op, buf) = buf << "search\n"
|
|
296
|
+
def emit_anchorsearch(_op, buf) = buf << "anchorsearch\n"
|
|
297
|
+
def emit_token(_op, buf) = buf << "token\n"
|
|
298
|
+
def emit_string(_op, buf) = buf << "string\n"
|
|
299
|
+
def emit_cvs(_op, buf) = buf << "cvs\n"
|
|
300
|
+
|
|
301
|
+
# Dictionary operators (currentdict etc.)
|
|
302
|
+
def emit_currentdict(_op, buf) = buf << "currentdict\n"
|
|
303
|
+
def emit_countdictstack(_op, buf) = buf << "countdictstack\n"
|
|
304
|
+
def emit_dictstack(_op, buf) = buf << "dictstack\n"
|
|
305
|
+
def emit_maxlength(_op, buf) = buf << "maxlength\n"
|
|
306
|
+
|
|
307
|
+
def font_name_value(value)
|
|
308
|
+
case value
|
|
309
|
+
when Model::Literals::Name then value.value
|
|
310
|
+
when Model::Literals::StringLiteral then value.value
|
|
311
|
+
when String then value
|
|
312
|
+
else value.to_s
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def string_value(value)
|
|
317
|
+
case value
|
|
318
|
+
when Model::Literals::StringLiteral then value.value
|
|
319
|
+
when Model::Literals::HexLiteral then value.bytes
|
|
320
|
+
when String then value
|
|
321
|
+
else value.to_s
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Postsvg
|
|
4
|
+
module Source
|
|
5
|
+
# Walks a token stream from the Lexer and produces a typed
|
|
6
|
+
# Model::Program. Procedures, arrays, and dictionaries are kept as
|
|
7
|
+
# Model literal nodes; operators are constructed via the
|
|
8
|
+
# Model::Operators registry.
|
|
9
|
+
class AstBuilder
|
|
10
|
+
attr_reader :tokens, :stack, :body, :header, :dict_stack, :recursion_depth
|
|
11
|
+
|
|
12
|
+
MAX_PROC_DEPTH = 64
|
|
13
|
+
|
|
14
|
+
def initialize(tokens)
|
|
15
|
+
@tokens = tokens
|
|
16
|
+
@stack = OperandStack.new
|
|
17
|
+
@body = []
|
|
18
|
+
@dict_stack = [{}] # global dictionary
|
|
19
|
+
@header = Model::Program::Header.new
|
|
20
|
+
@recursion_depth = 0
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.build(tokens)
|
|
24
|
+
new(tokens).build
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def build
|
|
28
|
+
Model::Operators.load_all!
|
|
29
|
+
consume_until(tokens.length)
|
|
30
|
+
Model::Program.new(header: @header, body: @body)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def consume_until(end_index, terminators: [], depth: 0)
|
|
36
|
+
raise RecursionLimitError, "procedure depth exceeded #{MAX_PROC_DEPTH}" if depth > MAX_PROC_DEPTH
|
|
37
|
+
|
|
38
|
+
i = 0
|
|
39
|
+
collected = []
|
|
40
|
+
while i < end_index
|
|
41
|
+
token = tokens[i]
|
|
42
|
+
break if terminators.include?(token&.type)
|
|
43
|
+
|
|
44
|
+
i += consume_token(token, collected)
|
|
45
|
+
end
|
|
46
|
+
collected
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
50
|
+
def consume_token(token, collected)
|
|
51
|
+
case token.type
|
|
52
|
+
when :number
|
|
53
|
+
node = build_number(token)
|
|
54
|
+
@stack.push(node)
|
|
55
|
+
append_statement(node, collected)
|
|
56
|
+
1
|
|
57
|
+
when :string
|
|
58
|
+
node = Model::Literals::StringLiteral.new(token.value)
|
|
59
|
+
@stack.push(node)
|
|
60
|
+
append_statement(node, collected)
|
|
61
|
+
1
|
|
62
|
+
when :hexstring
|
|
63
|
+
node = Model::Literals::HexLiteral.new(token.value)
|
|
64
|
+
@stack.push(node)
|
|
65
|
+
append_statement(node, collected)
|
|
66
|
+
1
|
|
67
|
+
when :name
|
|
68
|
+
node = Model::Literals::Name.new(token.value, literal: token.literal || false)
|
|
69
|
+
@stack.push(node)
|
|
70
|
+
append_statement(node, collected)
|
|
71
|
+
1
|
|
72
|
+
when :proc_open
|
|
73
|
+
consume_procedure(after: token_position(token), collected: collected)
|
|
74
|
+
when :array_open
|
|
75
|
+
consume_array(after: token_position(token), collected: collected)
|
|
76
|
+
when :dict_open
|
|
77
|
+
consume_dict(after: token_position(token), collected: collected)
|
|
78
|
+
when :dsc
|
|
79
|
+
apply_dsc(token)
|
|
80
|
+
1
|
|
81
|
+
when :operator
|
|
82
|
+
consume_operator(token, collected)
|
|
83
|
+
else
|
|
84
|
+
1
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
88
|
+
|
|
89
|
+
# Push a literal onto both the parse stack and the program body.
|
|
90
|
+
# The body is what the visitor walks; the stack is what operator
|
|
91
|
+
# factories pop from.
|
|
92
|
+
def append_statement(node, collected)
|
|
93
|
+
@body << node
|
|
94
|
+
collected << node
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def token_position(token)
|
|
98
|
+
[token.line, token.column]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def build_number(token)
|
|
102
|
+
raw = token.value
|
|
103
|
+
value =
|
|
104
|
+
if raw =~ /\A[-+]?\d+\z/
|
|
105
|
+
raw.to_i
|
|
106
|
+
elsif raw =~ /\A[-+]?\d+\.\d+\z/ || raw =~ /\A[-+]?(\.\d+|\d+\.)\z/
|
|
107
|
+
raw.to_f
|
|
108
|
+
elsif raw =~ /\A[-+]?(\d+\.?\d*|\.\d+)[eE][-+]?\d+\z/
|
|
109
|
+
raw.to_f
|
|
110
|
+
else
|
|
111
|
+
raw.to_f
|
|
112
|
+
end
|
|
113
|
+
Model::Literals::Number.new(value)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Consume a procedure body. Returns the number of tokens consumed
|
|
117
|
+
# (including the closing brace). The Procedure value is pushed
|
|
118
|
+
# onto the stack AND recorded as a statement on the program body
|
|
119
|
+
# so the renderer can visit it standalone.
|
|
120
|
+
def consume_procedure(after:, collected:)
|
|
121
|
+
start_index = find_index(after)
|
|
122
|
+
body_tokens, end_index = match_braces(start_index + 1)
|
|
123
|
+
body_nodes = sub_build(body_tokens)
|
|
124
|
+
procedure = Model::Literals::Procedure.new(body_nodes)
|
|
125
|
+
@stack.push(procedure)
|
|
126
|
+
append_statement(procedure, collected)
|
|
127
|
+
end_index - start_index + 1
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def consume_array(after:, collected:)
|
|
131
|
+
start_index = find_index(after)
|
|
132
|
+
body_tokens, end_index = match_until(start_index + 1, :array_close, :array_open)
|
|
133
|
+
body_nodes = sub_build(body_tokens)
|
|
134
|
+
array = Model::Literals::ArrayLiteral.new(body_nodes.select { |n| literal?(n) })
|
|
135
|
+
@stack.push(array)
|
|
136
|
+
append_statement(array, collected)
|
|
137
|
+
end_index - start_index + 1
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def consume_dict(after:, collected:)
|
|
141
|
+
start_index = find_index(after)
|
|
142
|
+
body_tokens, end_index = match_until(start_index + 1, :dict_close, :dict_open)
|
|
143
|
+
entries = {}
|
|
144
|
+
pending_key = nil
|
|
145
|
+
sub_nodes = sub_build(body_tokens)
|
|
146
|
+
sub_nodes.each do |node|
|
|
147
|
+
if node.is_a?(Model::Literals::Name) && node.literal?
|
|
148
|
+
pending_key = node.value
|
|
149
|
+
elsif pending_key
|
|
150
|
+
entries[pending_key] = node
|
|
151
|
+
pending_key = nil
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
dict = Model::Literals::Dictionary.new(entries)
|
|
155
|
+
@stack.push(dict)
|
|
156
|
+
collected << dict
|
|
157
|
+
end_index - start_index + 1
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def literal?(node)
|
|
161
|
+
node.is_a?(Model::Literals::Number) ||
|
|
162
|
+
node.is_a?(Model::Literals::Name) ||
|
|
163
|
+
node.is_a?(Model::Literals::StringLiteral) ||
|
|
164
|
+
node.is_a?(Model::Literals::HexLiteral) ||
|
|
165
|
+
node.is_a?(Model::Literals::ArrayLiteral) ||
|
|
166
|
+
node.is_a?(Model::Literals::Procedure) ||
|
|
167
|
+
node.is_a?(Model::Literals::Dictionary)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def sub_build(sub_tokens)
|
|
171
|
+
# Build a temporary AstBuilder over a sub-token-stream and
|
|
172
|
+
# harvest its body. The temp builder shares no state with the
|
|
173
|
+
# outer one.
|
|
174
|
+
return [] if sub_tokens.empty?
|
|
175
|
+
|
|
176
|
+
program = AstBuilder.new(sub_tokens).build
|
|
177
|
+
program.body
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def find_index(line_col)
|
|
181
|
+
line, col = line_col
|
|
182
|
+
tokens.each_with_index do |tok, idx|
|
|
183
|
+
return idx if tok.line == line && tok.column == col
|
|
184
|
+
end
|
|
185
|
+
tokens.length
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def match_braces(start_index)
|
|
189
|
+
depth = 1
|
|
190
|
+
i = start_index
|
|
191
|
+
while i < tokens.length
|
|
192
|
+
tok = tokens[i]
|
|
193
|
+
if tok.type == :proc_open
|
|
194
|
+
depth += 1
|
|
195
|
+
elsif tok.type == :proc_close
|
|
196
|
+
depth -= 1
|
|
197
|
+
return [tokens[start_index...i], i] if depth.zero?
|
|
198
|
+
end
|
|
199
|
+
i += 1
|
|
200
|
+
end
|
|
201
|
+
raise SyntaxError, "unbalanced procedure braces"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def match_until(start_index, close_type, open_type)
|
|
205
|
+
depth = 1
|
|
206
|
+
i = start_index
|
|
207
|
+
while i < tokens.length
|
|
208
|
+
tok = tokens[i]
|
|
209
|
+
if tok.type == open_type
|
|
210
|
+
depth += 1
|
|
211
|
+
elsif tok.type == close_type
|
|
212
|
+
depth -= 1
|
|
213
|
+
return [tokens[start_index...i], i] if depth.zero?
|
|
214
|
+
end
|
|
215
|
+
i += 1
|
|
216
|
+
end
|
|
217
|
+
raise SyntaxError, "unbalanced #{close_type}"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def consume_operator(token, collected)
|
|
221
|
+
keyword = token.value
|
|
222
|
+
|
|
223
|
+
# Resolve user-defined names to InvokeProcedure.
|
|
224
|
+
if (proc_value = lookup_user_definition(keyword))
|
|
225
|
+
if proc_value.is_a?(Model::Literals::Procedure)
|
|
226
|
+
inv = Model::InvokeProcedure.new(name: keyword, procedure: proc_value)
|
|
227
|
+
@body << inv
|
|
228
|
+
collected << inv
|
|
229
|
+
return 1
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
operator_class = Model::Operators[keyword]
|
|
234
|
+
unless operator_class
|
|
235
|
+
unknown = Model::UnknownOperator.new(keyword: keyword)
|
|
236
|
+
@body << unknown
|
|
237
|
+
collected << unknown
|
|
238
|
+
return 1
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
operator = operator_class.from_operands(@stack)
|
|
242
|
+
return 1 if operator.nil?
|
|
243
|
+
|
|
244
|
+
# Reflect the runtime stack shape: push a +Computed+ sentinel
|
|
245
|
+
# for each value the operator would have produced at runtime.
|
|
246
|
+
# This lets chained operators (findfont ... scalefont, add +
|
|
247
|
+
# mul, etc.) parse cleanly even though their intermediate
|
|
248
|
+
# results aren't yet known.
|
|
249
|
+
operator.class.produces.to_i.times do
|
|
250
|
+
@stack.push(Model::Computed.new(operator_keyword: keyword))
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
@body << operator
|
|
254
|
+
collected << operator
|
|
255
|
+
apply_side_effects(operator)
|
|
256
|
+
1
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# When a `def` operator is constructed, capture the binding into
|
|
260
|
+
# the active dictionary so subsequent operator tokens can resolve
|
|
261
|
+
# to InvokeProcedure.
|
|
262
|
+
def apply_side_effects(operator)
|
|
263
|
+
case operator
|
|
264
|
+
when Model::Operators::Dictionary::Def
|
|
265
|
+
@dict_stack.last[operator.key.to_s.sub(/\A\//, "")] = operator.value
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def lookup_user_definition(name)
|
|
270
|
+
@dict_stack.reverse_each do |dict|
|
|
271
|
+
return dict[name] if dict.key?(name)
|
|
272
|
+
end
|
|
273
|
+
nil
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def apply_dsc(token)
|
|
277
|
+
text = token.value.strip
|
|
278
|
+
case text
|
|
279
|
+
when /\ABoundingBox:\s*(-?[\d.eE+-]+)\s+(-?[\d.eE+-]+)\s+(-?[\d.eE+-]+)\s+(-?[\d.eE+-]+)\z/
|
|
280
|
+
@header = @header.with(bounding_box: [
|
|
281
|
+
::Regexp.last_match(1).to_f, ::Regexp.last_match(2).to_f,
|
|
282
|
+
::Regexp.last_match(3).to_f, ::Regexp.last_match(4).to_f,
|
|
283
|
+
])
|
|
284
|
+
when /\AHiResBoundingBox:\s*(-?[\d.eE+-]+)\s+(-?[\d.eE+-]+)\s+(-?[\d.eE+-]+)\s+(-?[\d.eE+-]+)\z/
|
|
285
|
+
@header = @header.with(hires_bounding_box: [
|
|
286
|
+
::Regexp.last_match(1).to_f, ::Regexp.last_match(2).to_f,
|
|
287
|
+
::Regexp.last_match(3).to_f, ::Regexp.last_match(4).to_f,
|
|
288
|
+
])
|
|
289
|
+
when /\ATitle:\s*(.+)\z/
|
|
290
|
+
@header = @header.with(title: ::Regexp.last_match(1))
|
|
291
|
+
when /\ACreator:\s*(.+)\z/
|
|
292
|
+
@header = @header.with(creator: ::Regexp.last_match(1))
|
|
293
|
+
when /\ACreationDate:\s*(.+)\z/
|
|
294
|
+
@header = @header.with(creation_date: ::Regexp.last_match(1))
|
|
295
|
+
when /\APages:\s*(\d+)\s+(\d+)\z/
|
|
296
|
+
@header = @header.with(page_count: ::Regexp.last_match(1).to_i,
|
|
297
|
+
pages: ::Regexp.last_match(2).to_i)
|
|
298
|
+
when /\ALanguageLevel:\s*(\d+)\z/
|
|
299
|
+
@header = @header.with(language_level: ::Regexp.last_match(1).to_i)
|
|
300
|
+
when /\AEOF/, /\AEndComments/
|
|
301
|
+
nil
|
|
302
|
+
else
|
|
303
|
+
@header = @header.with(custom: @header.custom.merge(text => true))
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|