postscript 0.1.0 → 0.2.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.yml +19 -0
- data/.rubocop_todo.yml +323 -0
- data/CHANGELOG.md +71 -0
- data/LICENSE +24 -0
- data/exe/postscript +7 -0
- data/lib/postscript/cli.rb +83 -0
- data/lib/postscript/color.rb +6 -4
- data/lib/postscript/format_number.rb +4 -1
- data/lib/postscript/model/operator.rb +1 -1
- data/lib/postscript/model/operators/arithmetic.rb +30 -0
- data/lib/postscript/model/operators/boolean.rb +23 -11
- data/lib/postscript/model/operators/color.rb +8 -0
- data/lib/postscript/model/operators/container.rb +24 -0
- data/lib/postscript/model/operators/control_flow.rb +16 -1
- data/lib/postscript/model/operators/dictionary.rb +8 -0
- data/lib/postscript/model/operators/font.rb +14 -0
- data/lib/postscript/model/operators/graphics_state.rb +10 -0
- data/lib/postscript/model/operators/path.rb +16 -0
- data/lib/postscript/model/operators/stack.rb +4 -0
- data/lib/postscript/model/operators/transformations.rb +11 -1
- data/lib/postscript/model/program.rb +2 -2
- data/lib/postscript/serializer.rb +32 -212
- data/lib/postscript/source/ast_builder.rb +30 -22
- data/lib/postscript/source/lexer.rb +18 -21
- data/lib/postscript/version.rb +1 -1
- data/lib/postscript.rb +30 -0
- metadata +29 -6
- data/postscript.gemspec +0 -37
|
@@ -19,7 +19,10 @@ module Postscript
|
|
|
19
19
|
attr_reader :program, :eps, :creator
|
|
20
20
|
|
|
21
21
|
def initialize(program, eps:, creator:)
|
|
22
|
-
|
|
22
|
+
unless program.is_a?(Model::Program)
|
|
23
|
+
raise ArgumentError,
|
|
24
|
+
"program must be a Model::Program"
|
|
25
|
+
end
|
|
23
26
|
|
|
24
27
|
@program = program
|
|
25
28
|
@eps = eps
|
|
@@ -30,11 +33,17 @@ module Postscript
|
|
|
30
33
|
buffer = String.new(capacity: 4096)
|
|
31
34
|
emit_header(buffer)
|
|
32
35
|
emit_body(buffer)
|
|
33
|
-
buffer << "showpage\n"
|
|
36
|
+
buffer << "showpage\n" unless body_has_showpage?
|
|
34
37
|
buffer << "%%EOF\n"
|
|
35
38
|
buffer
|
|
36
39
|
end
|
|
37
40
|
|
|
41
|
+
# Returns true if the program body already contains a +showpage+
|
|
42
|
+
# operator, so the serializer doesn't append a duplicate.
|
|
43
|
+
def body_has_showpage?
|
|
44
|
+
program.body.any? { |s| s.is_a?(Model::Operators::Device::Showpage) }
|
|
45
|
+
end
|
|
46
|
+
|
|
38
47
|
# All methods below this point are public. They are intentionally
|
|
39
48
|
# exposed (prefixed +emit_+) so dispatch can reach them via
|
|
40
49
|
# +method_defined?+ without +respond_to?+.
|
|
@@ -45,11 +54,17 @@ module Postscript
|
|
|
45
54
|
buffer << "%%Creator: #{creator}\n"
|
|
46
55
|
bbox = program.header.bounding_box
|
|
47
56
|
if bbox && !bbox.empty?
|
|
48
|
-
buffer << "%%BoundingBox: " << bbox.map { |v|
|
|
57
|
+
buffer << "%%BoundingBox: " << bbox.map { |v|
|
|
58
|
+
FormatNumber.call(v)
|
|
59
|
+
}.join(" ") << "\n"
|
|
49
60
|
end
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
hires = program.header.hires_bounding_box
|
|
62
|
+
if hires && !hires.empty?
|
|
63
|
+
buffer << "%%HiResBoundingBox: " << hires.map { |v|
|
|
64
|
+
FormatNumber.call(v)
|
|
65
|
+
}.join(" ") << "\n"
|
|
52
66
|
end
|
|
67
|
+
buffer << "%%Title: #{program.header.title}\n" if program.header.title
|
|
53
68
|
buffer << "%%LanguageLevel: #{DEFAULT_LANGUAGE_LEVEL}\n"
|
|
54
69
|
buffer << "%%EndComments\n"
|
|
55
70
|
end
|
|
@@ -73,7 +88,10 @@ module Postscript
|
|
|
73
88
|
buffer << "<" << statement.value << ">\n"
|
|
74
89
|
when Model::Literals::ArrayLiteral
|
|
75
90
|
buffer << "[ "
|
|
76
|
-
statement.elements.each
|
|
91
|
+
statement.elements.each do |e|
|
|
92
|
+
emit_inline(e, buffer)
|
|
93
|
+
buffer << " "
|
|
94
|
+
end
|
|
77
95
|
buffer << "]\n"
|
|
78
96
|
when Model::Literals::Procedure
|
|
79
97
|
buffer << "{\n"
|
|
@@ -106,220 +124,22 @@ module Postscript
|
|
|
106
124
|
end
|
|
107
125
|
end
|
|
108
126
|
|
|
109
|
-
# Dispatch
|
|
110
|
-
#
|
|
127
|
+
# Dispatch: just emit the keyword. Operands are already in the
|
|
128
|
+
# program body as separate literal statements (added by the
|
|
129
|
+
# AstBuilder). This keeps round-trip idempotent: serialized
|
|
130
|
+
# output re-parses to an identical AST.
|
|
111
131
|
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
132
|
buffer << operator.keyword << "\n"
|
|
122
133
|
end
|
|
123
134
|
|
|
124
135
|
# 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
136
|
def escape_string(text)
|
|
250
|
-
escaped = text.to_s.gsub(/[
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
137
|
+
escaped = text.to_s.gsub(/[()\\]/) { |c| "\\#{c}" }
|
|
138
|
+
.gsub("\n", '\\n')
|
|
139
|
+
.gsub("\r", '\\r')
|
|
140
|
+
.gsub("\t", '\\t')
|
|
254
141
|
"(#{escaped})"
|
|
255
142
|
end
|
|
256
143
|
|
|
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
144
|
end
|
|
325
145
|
end
|
|
@@ -33,7 +33,10 @@ module Postscript
|
|
|
33
33
|
private
|
|
34
34
|
|
|
35
35
|
def consume_until(end_index, terminators: [], depth: 0)
|
|
36
|
-
|
|
36
|
+
if depth > MAX_PROC_DEPTH
|
|
37
|
+
raise RecursionLimitError,
|
|
38
|
+
"procedure depth exceeded #{MAX_PROC_DEPTH}"
|
|
39
|
+
end
|
|
37
40
|
|
|
38
41
|
i = 0
|
|
39
42
|
collected = []
|
|
@@ -65,7 +68,8 @@ module Postscript
|
|
|
65
68
|
append_statement(node, collected)
|
|
66
69
|
1
|
|
67
70
|
when :name
|
|
68
|
-
node = Model::Literals::Name.new(token.value,
|
|
71
|
+
node = Model::Literals::Name.new(token.value,
|
|
72
|
+
literal: token.literal || false)
|
|
69
73
|
@stack.push(node)
|
|
70
74
|
append_statement(node, collected)
|
|
71
75
|
1
|
|
@@ -101,11 +105,12 @@ module Postscript
|
|
|
101
105
|
def build_number(token)
|
|
102
106
|
raw = token.value
|
|
103
107
|
value =
|
|
104
|
-
|
|
108
|
+
case raw
|
|
109
|
+
when /\A[-+]?\d+\z/
|
|
105
110
|
raw.to_i
|
|
106
|
-
|
|
111
|
+
when /\A[-+]?\d+\.\d+\z/, /\A[-+]?(\.\d+|\d+\.)\z/
|
|
107
112
|
raw.to_f
|
|
108
|
-
|
|
113
|
+
when /\A[-+]?(\d+\.?\d*|\.\d+)[eE][-+]?\d+\z/
|
|
109
114
|
raw.to_f
|
|
110
115
|
else
|
|
111
116
|
raw.to_f
|
|
@@ -129,9 +134,12 @@ module Postscript
|
|
|
129
134
|
|
|
130
135
|
def consume_array(after:, collected:)
|
|
131
136
|
start_index = find_index(after)
|
|
132
|
-
body_tokens, end_index = match_until(start_index + 1, :array_close,
|
|
137
|
+
body_tokens, end_index = match_until(start_index + 1, :array_close,
|
|
138
|
+
:array_open)
|
|
133
139
|
body_nodes = sub_build(body_tokens)
|
|
134
|
-
array = Model::Literals::ArrayLiteral.new(body_nodes.select
|
|
140
|
+
array = Model::Literals::ArrayLiteral.new(body_nodes.select do |n|
|
|
141
|
+
literal?(n)
|
|
142
|
+
end)
|
|
135
143
|
@stack.push(array)
|
|
136
144
|
append_statement(array, collected)
|
|
137
145
|
end_index - start_index + 1
|
|
@@ -139,7 +147,8 @@ module Postscript
|
|
|
139
147
|
|
|
140
148
|
def consume_dict(after:, collected:)
|
|
141
149
|
start_index = find_index(after)
|
|
142
|
-
body_tokens, end_index = match_until(start_index + 1, :dict_close,
|
|
150
|
+
body_tokens, end_index = match_until(start_index + 1, :dict_close,
|
|
151
|
+
:dict_open)
|
|
143
152
|
entries = {}
|
|
144
153
|
pending_key = nil
|
|
145
154
|
sub_nodes = sub_build(body_tokens)
|
|
@@ -221,13 +230,11 @@ module Postscript
|
|
|
221
230
|
keyword = token.value
|
|
222
231
|
|
|
223
232
|
# Resolve user-defined names to InvokeProcedure.
|
|
224
|
-
if (proc_value = lookup_user_definition(keyword))
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return 1
|
|
230
|
-
end
|
|
233
|
+
if (proc_value = lookup_user_definition(keyword)) && proc_value.is_a?(Model::Literals::Procedure)
|
|
234
|
+
inv = Model::InvokeProcedure.new(name: keyword, procedure: proc_value)
|
|
235
|
+
@body << inv
|
|
236
|
+
collected << inv
|
|
237
|
+
return 1
|
|
231
238
|
end
|
|
232
239
|
|
|
233
240
|
operator_class = Model::Operators[keyword]
|
|
@@ -262,7 +269,8 @@ module Postscript
|
|
|
262
269
|
def apply_side_effects(operator)
|
|
263
270
|
case operator
|
|
264
271
|
when Model::Operators::Dictionary::Def
|
|
265
|
-
@dict_stack.last[operator.key.to_s.
|
|
272
|
+
@dict_stack.last[operator.key.to_s.delete_prefix("/")] =
|
|
273
|
+
operator.value
|
|
266
274
|
end
|
|
267
275
|
end
|
|
268
276
|
|
|
@@ -278,14 +286,14 @@ module Postscript
|
|
|
278
286
|
case text
|
|
279
287
|
when /\ABoundingBox:\s*(-?[\d.eE+-]+)\s+(-?[\d.eE+-]+)\s+(-?[\d.eE+-]+)\s+(-?[\d.eE+-]+)\z/
|
|
280
288
|
@header = @header.with(bounding_box: [
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
289
|
+
::Regexp.last_match(1).to_f, ::Regexp.last_match(2).to_f,
|
|
290
|
+
::Regexp.last_match(3).to_f, ::Regexp.last_match(4).to_f
|
|
291
|
+
])
|
|
284
292
|
when /\AHiResBoundingBox:\s*(-?[\d.eE+-]+)\s+(-?[\d.eE+-]+)\s+(-?[\d.eE+-]+)\s+(-?[\d.eE+-]+)\z/
|
|
285
293
|
@header = @header.with(hires_bounding_box: [
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
294
|
+
::Regexp.last_match(1).to_f, ::Regexp.last_match(2).to_f,
|
|
295
|
+
::Regexp.last_match(3).to_f, ::Regexp.last_match(4).to_f
|
|
296
|
+
])
|
|
289
297
|
when /\ATitle:\s*(.+)\z/
|
|
290
298
|
@header = @header.with(title: ::Regexp.last_match(1))
|
|
291
299
|
when /\ACreator:\s*(.+)\z/
|
|
@@ -121,18 +121,15 @@ module Postscript
|
|
|
121
121
|
start_pos = @position
|
|
122
122
|
start_line = @line
|
|
123
123
|
start_col = @column
|
|
124
|
-
until eos? || @source.getbyte(@position) == 10
|
|
125
|
-
advance
|
|
126
|
-
end
|
|
124
|
+
advance until eos? || @source.getbyte(@position) == 10
|
|
127
125
|
text = @source.byteslice(start_pos, @position - start_pos)
|
|
128
|
-
@tokens << Model::Token.new(:dsc, text.chomp, line: start_line,
|
|
126
|
+
@tokens << Model::Token.new(:dsc, text.chomp, line: start_line,
|
|
127
|
+
column: start_col)
|
|
129
128
|
@state = :top
|
|
130
129
|
end
|
|
131
130
|
|
|
132
131
|
def scan_comment_body
|
|
133
|
-
until eos? || @source.getbyte(@position) == 10
|
|
134
|
-
advance
|
|
135
|
-
end
|
|
132
|
+
advance until eos? || @source.getbyte(@position) == 10
|
|
136
133
|
@state = :top
|
|
137
134
|
end
|
|
138
135
|
|
|
@@ -148,16 +145,14 @@ module Postscript
|
|
|
148
145
|
def scan_string_body
|
|
149
146
|
until eos?
|
|
150
147
|
byte = @source.getbyte(@position)
|
|
148
|
+
advance
|
|
151
149
|
case byte
|
|
152
150
|
when 92 # backslash
|
|
153
|
-
advance
|
|
154
151
|
handle_escape
|
|
155
152
|
when 40 # (
|
|
156
|
-
advance
|
|
157
153
|
@string_depth += 1
|
|
158
154
|
@string_start[:bytes] << "("
|
|
159
155
|
when 41 # )
|
|
160
|
-
advance
|
|
161
156
|
@string_depth -= 1
|
|
162
157
|
if @string_depth.zero?
|
|
163
158
|
emit_string
|
|
@@ -165,12 +160,12 @@ module Postscript
|
|
|
165
160
|
end
|
|
166
161
|
@string_start[:bytes] << ")"
|
|
167
162
|
else
|
|
168
|
-
advance
|
|
169
163
|
@string_start[:bytes] << byte.chr
|
|
170
164
|
end
|
|
171
165
|
end
|
|
172
166
|
raise LexError.new("unterminated string literal",
|
|
173
|
-
source_position: [@string_start[:line],
|
|
167
|
+
source_position: [@string_start[:line],
|
|
168
|
+
@string_start[:column]])
|
|
174
169
|
end
|
|
175
170
|
|
|
176
171
|
def handle_escape
|
|
@@ -193,17 +188,17 @@ module Postscript
|
|
|
193
188
|
else
|
|
194
189
|
char
|
|
195
190
|
end
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
191
|
+
return unless replacement.is_a?(String)
|
|
192
|
+
|
|
193
|
+
advance
|
|
194
|
+
@string_start[:bytes] << replacement
|
|
200
195
|
end
|
|
201
196
|
|
|
202
197
|
def consume_octal_escape
|
|
203
198
|
digits = +""
|
|
204
199
|
3.times do
|
|
205
200
|
byte = peek
|
|
206
|
-
break unless byte &&
|
|
201
|
+
break unless byte && byte >= 48 && byte <= 55 # 0..7
|
|
207
202
|
|
|
208
203
|
digits << byte.chr
|
|
209
204
|
advance
|
|
@@ -244,7 +239,8 @@ module Postscript
|
|
|
244
239
|
end
|
|
245
240
|
end
|
|
246
241
|
raise LexError.new("unterminated hex string literal",
|
|
247
|
-
source_position: [@hex_start[:line],
|
|
242
|
+
source_position: [@hex_start[:line],
|
|
243
|
+
@hex_start[:column]])
|
|
248
244
|
end
|
|
249
245
|
|
|
250
246
|
def emit_hexstring
|
|
@@ -266,7 +262,7 @@ module Postscript
|
|
|
266
262
|
end
|
|
267
263
|
end
|
|
268
264
|
|
|
269
|
-
def start_name_with_slash(
|
|
265
|
+
def start_name_with_slash(_prefix)
|
|
270
266
|
start_line = @line
|
|
271
267
|
start_col = @column
|
|
272
268
|
advance
|
|
@@ -278,7 +274,8 @@ module Postscript
|
|
|
278
274
|
bytes << byte.chr
|
|
279
275
|
advance
|
|
280
276
|
end
|
|
281
|
-
@tokens << Model::Token.new(:name, bytes, line: start_line,
|
|
277
|
+
@tokens << Model::Token.new(:name, bytes, line: start_line,
|
|
278
|
+
column: start_col, literal: true)
|
|
282
279
|
end
|
|
283
280
|
|
|
284
281
|
def delimiter_byte?(byte)
|
|
@@ -312,7 +309,7 @@ module Postscript
|
|
|
312
309
|
@tokens << Model::Token.new(type, bytes, line: line, column: col)
|
|
313
310
|
end
|
|
314
311
|
|
|
315
|
-
NUMBER_RE = /\A[-+]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][-+]?\d+)?\z
|
|
312
|
+
NUMBER_RE = /\A[-+]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][-+]?\d+)?\z/
|
|
316
313
|
|
|
317
314
|
def emit(type, value)
|
|
318
315
|
@tokens << Model::Token.new(type, value, line: @line, column: @column)
|
data/lib/postscript/version.rb
CHANGED
data/lib/postscript.rb
CHANGED
|
@@ -30,4 +30,34 @@ module Postscript
|
|
|
30
30
|
|
|
31
31
|
# PS source serializer
|
|
32
32
|
autoload :Serializer, "postscript/serializer"
|
|
33
|
+
|
|
34
|
+
# CLI (only loaded when explicitly required or via the +postscript+
|
|
35
|
+
# executable). Avoids pulling in +thor+ for users who only need
|
|
36
|
+
# the library API.
|
|
37
|
+
autoload :CLI, "postscript/cli"
|
|
38
|
+
|
|
39
|
+
class << self
|
|
40
|
+
# One-shot: PS source string -> +Model::Program+.
|
|
41
|
+
#
|
|
42
|
+
# Example:
|
|
43
|
+
# program = Postscript.parse(ps_source)
|
|
44
|
+
# program.body.each { |node| puts node.class }
|
|
45
|
+
def parse(source)
|
|
46
|
+
Source.parse(source)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# One-shot: +Model::Program+ -> PS source string.
|
|
50
|
+
#
|
|
51
|
+
# Example:
|
|
52
|
+
# ps = Postscript.serialize(program, eps: true)
|
|
53
|
+
def serialize(program, **opts)
|
|
54
|
+
Serializer.call(program, **opts)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# One-shot: PS source string -> array of +Model::Token+.
|
|
58
|
+
# Useful for debugging or building alternative parsers.
|
|
59
|
+
def tokenize(source)
|
|
60
|
+
Source::Lexer.tokenize(source)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
33
63
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: postscript
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
-
dependencies:
|
|
11
|
+
date: 2026-07-28 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: thor
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
12
27
|
description: |
|
|
13
28
|
Postscript is a pure-Ruby PostScript (PS) / EPS parser, typed
|
|
14
29
|
domain model, and serializer. It is the lower layer of the
|
|
@@ -16,15 +31,22 @@ description: |
|
|
|
16
31
|
that needs to read, write, or transform PostScript source.
|
|
17
32
|
email:
|
|
18
33
|
- open.source@ribose.com
|
|
19
|
-
executables:
|
|
34
|
+
executables:
|
|
35
|
+
- postscript
|
|
20
36
|
extensions: []
|
|
21
37
|
extra_rdoc_files: []
|
|
22
38
|
files:
|
|
23
39
|
- ".rspec"
|
|
40
|
+
- ".rubocop.yml"
|
|
41
|
+
- ".rubocop_todo.yml"
|
|
42
|
+
- CHANGELOG.md
|
|
24
43
|
- Gemfile
|
|
44
|
+
- LICENSE
|
|
25
45
|
- README.adoc
|
|
26
46
|
- Rakefile
|
|
47
|
+
- exe/postscript
|
|
27
48
|
- lib/postscript.rb
|
|
49
|
+
- lib/postscript/cli.rb
|
|
28
50
|
- lib/postscript/color.rb
|
|
29
51
|
- lib/postscript/errors.rb
|
|
30
52
|
- lib/postscript/format_number.rb
|
|
@@ -61,7 +83,6 @@ files:
|
|
|
61
83
|
- lib/postscript/source/lexer.rb
|
|
62
84
|
- lib/postscript/source/operand_stack.rb
|
|
63
85
|
- lib/postscript/version.rb
|
|
64
|
-
- postscript.gemspec
|
|
65
86
|
homepage: https://github.com/claricle/postscript
|
|
66
87
|
licenses:
|
|
67
88
|
- BSD-2-Clause
|
|
@@ -71,6 +92,7 @@ metadata:
|
|
|
71
92
|
changelog_uri: https://github.com/claricle/postscript/blob/main/CHANGELOG.md
|
|
72
93
|
bug_tracker_uri: https://github.com/claricle/postscript/issues
|
|
73
94
|
rubygems_mfa_required: 'true'
|
|
95
|
+
post_install_message:
|
|
74
96
|
rdoc_options: []
|
|
75
97
|
require_paths:
|
|
76
98
|
- lib
|
|
@@ -85,7 +107,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
85
107
|
- !ruby/object:Gem::Version
|
|
86
108
|
version: '0'
|
|
87
109
|
requirements: []
|
|
88
|
-
rubygems_version:
|
|
110
|
+
rubygems_version: 3.5.22
|
|
111
|
+
signing_key:
|
|
89
112
|
specification_version: 4
|
|
90
113
|
summary: Pure Ruby PostScript (PS) / EPS parser, domain model, and serializer
|
|
91
114
|
test_files: []
|
data/postscript.gemspec
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "lib/postscript/version"
|
|
4
|
-
|
|
5
|
-
Gem::Specification.new do |spec|
|
|
6
|
-
spec.name = "postscript"
|
|
7
|
-
spec.version = Postscript::VERSION
|
|
8
|
-
spec.authors = ["Ribose Inc."]
|
|
9
|
-
spec.email = ["open.source@ribose.com"]
|
|
10
|
-
|
|
11
|
-
spec.summary = "Pure Ruby PostScript (PS) / EPS parser, domain model, and serializer"
|
|
12
|
-
spec.description = <<~HEREDOC
|
|
13
|
-
Postscript is a pure-Ruby PostScript (PS) / EPS parser, typed
|
|
14
|
-
domain model, and serializer. It is the lower layer of the
|
|
15
|
-
postsvg converter and is independently reusable for any tool
|
|
16
|
-
that needs to read, write, or transform PostScript source.
|
|
17
|
-
HEREDOC
|
|
18
|
-
|
|
19
|
-
spec.homepage = "https://github.com/claricle/postscript"
|
|
20
|
-
spec.license = "BSD-2-Clause"
|
|
21
|
-
spec.required_ruby_version = ">= 3.0.0"
|
|
22
|
-
|
|
23
|
-
spec.metadata["homepage_uri"] = spec.homepage
|
|
24
|
-
spec.metadata["source_code_uri"] = "https://github.com/claricle/postscript"
|
|
25
|
-
spec.metadata["changelog_uri"] = "https://github.com/claricle/postscript/blob/main/CHANGELOG.md"
|
|
26
|
-
spec.metadata["bug_tracker_uri"] = "https://github.com/claricle/postscript/issues"
|
|
27
|
-
spec.metadata["rubygems_mfa_required"] = "true"
|
|
28
|
-
|
|
29
|
-
spec.files = Dir.chdir(__dir__) do
|
|
30
|
-
`git ls-files -z`.split("\x0").reject do |f|
|
|
31
|
-
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
spec.bindir = "exe"
|
|
35
|
-
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
36
|
-
spec.require_paths = ["lib"]
|
|
37
|
-
end
|