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,1051 @@
|
|
|
1
|
+
|
|
2
|
+
= Parser Stage
|
|
3
|
+
:page-nav_order: 2
|
|
4
|
+
|
|
5
|
+
== Purpose
|
|
6
|
+
|
|
7
|
+
This document describes the Parser Stage of Postsvg's conversion pipeline, which transforms raw PostScript source code into structured tokens through lexical analysis. Understanding the parser helps developers comprehend token classification, string handling, number parsing, and how PostScript syntax is decomposed for interpretation.
|
|
8
|
+
|
|
9
|
+
== References
|
|
10
|
+
|
|
11
|
+
* link:../architecture.adoc[Architecture Overview]
|
|
12
|
+
* link:conversion-pipeline.adoc[Conversion Pipeline]
|
|
13
|
+
* link:interpreter-stage.adoc[Interpreter Stage]
|
|
14
|
+
* link:../api-reference/tokenizer.adoc[Tokenizer API Reference]
|
|
15
|
+
|
|
16
|
+
== Concepts
|
|
17
|
+
|
|
18
|
+
**Lexical Analysis**:: Breaking source code into smallest meaningful units (tokens).
|
|
19
|
+
|
|
20
|
+
**Token Classification**:: Determining token type (number, operator, string, etc.) during scanning.
|
|
21
|
+
|
|
22
|
+
**Single-Pass Scanning**:: Processing input characters once, left-to-right, with no backtracking.
|
|
23
|
+
|
|
24
|
+
**Comment Removal**:: Eliminating PostScript comments before tokenization.
|
|
25
|
+
|
|
26
|
+
**Escape Sequence Handling**:: Processing backslash escapes in strings and hex strings.
|
|
27
|
+
|
|
28
|
+
== Tokenizer Architecture
|
|
29
|
+
|
|
30
|
+
=== Class Structure
|
|
31
|
+
|
|
32
|
+
**Location:** [`lib/postsvg/tokenizer.rb`](../../lib/postsvg/tokenizer.rb:8)
|
|
33
|
+
|
|
34
|
+
**Responsibilities:**
|
|
35
|
+
|
|
36
|
+
* Character-by-character scanning
|
|
37
|
+
* Token type classification
|
|
38
|
+
* String and number parsing
|
|
39
|
+
* Whitespace skipping
|
|
40
|
+
* Comment removal
|
|
41
|
+
|
|
42
|
+
**Design Pattern:** Static utility class with class methods (no instance state).
|
|
43
|
+
|
|
44
|
+
=== Token Structure
|
|
45
|
+
|
|
46
|
+
[source,ruby]
|
|
47
|
+
----
|
|
48
|
+
# lib/postsvg/tokenizer.rb:5
|
|
49
|
+
Token = Struct.new(:type, :value, keyword_init: true)
|
|
50
|
+
|
|
51
|
+
# Usage:
|
|
52
|
+
Token.new(type: "number", value: "42")
|
|
53
|
+
Token.new(type: "operator", value: "moveto")
|
|
54
|
+
Token.new(type: "string", value: "Hello World")
|
|
55
|
+
----
|
|
56
|
+
|
|
57
|
+
**Token Fields:**
|
|
58
|
+
|
|
59
|
+
* `type` - String identifier for token classification
|
|
60
|
+
* `value` - String representation of the token's value
|
|
61
|
+
|
|
62
|
+
== Token Types
|
|
63
|
+
|
|
64
|
+
=== Complete Token Type Taxonomy
|
|
65
|
+
|
|
66
|
+
[cols="1,2,2"]
|
|
67
|
+
|===
|
|
68
|
+
|Token Type |Example Input |Token Value
|
|
69
|
+
|
|
70
|
+
|`number`
|
|
71
|
+
|`42`, `3.14`, `1.5e-3`
|
|
72
|
+
|`"42"`, `"3.14"`, `"1.5e-3"`
|
|
73
|
+
|
|
74
|
+
|`operator`
|
|
75
|
+
|`moveto`, `add`, `gsave`
|
|
76
|
+
|`"moveto"`, `"add"`, `"gsave"`
|
|
77
|
+
|
|
78
|
+
|`name`
|
|
79
|
+
|`/Helvetica`, `/Pattern1`
|
|
80
|
+
|`"Helvetica"`, `"Pattern1"` (without `/`)
|
|
81
|
+
|
|
82
|
+
|`string`
|
|
83
|
+
|`(Hello)`, `(nested (parens))`
|
|
84
|
+
|`"Hello"`, `"nested (parens)"`
|
|
85
|
+
|
|
86
|
+
|`hexstring`
|
|
87
|
+
|`<48656C6C6F>`, `<DEADBEEF>`
|
|
88
|
+
|Decoded binary string
|
|
89
|
+
|
|
90
|
+
|`brace`
|
|
91
|
+
|`{`, `}`
|
|
92
|
+
|`"{"`, `"}"`
|
|
93
|
+
|
|
94
|
+
|`bracket`
|
|
95
|
+
|`[`, `]`
|
|
96
|
+
|`"["`, `"]"`
|
|
97
|
+
|
|
98
|
+
|`dict`
|
|
99
|
+
|`<<`, `>>`
|
|
100
|
+
|`"<<"`, `">>"`
|
|
101
|
+
|===
|
|
102
|
+
|
|
103
|
+
=== Type Classification Rules
|
|
104
|
+
|
|
105
|
+
**Numbers:** Match regex `-?(?:\d+\.\d+|\d+\.|\.\d+|\d+)(?:[eE][+-]?\d+)?`
|
|
106
|
+
|
|
107
|
+
**Names:** Start with `/`, followed by identifier characters
|
|
108
|
+
|
|
109
|
+
**Operators:** Identifier characters without leading `/`
|
|
110
|
+
|
|
111
|
+
**Strings:** Enclosed in parentheses `(...)`, supports nesting
|
|
112
|
+
|
|
113
|
+
**Hex Strings:** Enclosed in angle brackets `<...>` (not `<<`)
|
|
114
|
+
|
|
115
|
+
**Delimiters:** Single or double character delimiters
|
|
116
|
+
|
|
117
|
+
== Tokenization Algorithm
|
|
118
|
+
|
|
119
|
+
=== Main Tokenization Loop
|
|
120
|
+
|
|
121
|
+
[source,ruby]
|
|
122
|
+
----
|
|
123
|
+
# lib/postsvg/tokenizer.rb:9
|
|
124
|
+
def self.tokenize(ps_code)
|
|
125
|
+
# Step 1: Remove comments (% to end of line)
|
|
126
|
+
ps = ps_code.gsub(/%[^\n\r]*/, " ")
|
|
127
|
+
|
|
128
|
+
tokens = []
|
|
129
|
+
index = 0
|
|
130
|
+
|
|
131
|
+
# Step 2: Scan character by character
|
|
132
|
+
while index < ps.length
|
|
133
|
+
# Skip whitespace
|
|
134
|
+
index += 1 while index < ps.length && ps[index].match?(/\s/)
|
|
135
|
+
break if index >= ps.length
|
|
136
|
+
|
|
137
|
+
# Try to match a token
|
|
138
|
+
token, new_index = match_token(ps, index)
|
|
139
|
+
if token
|
|
140
|
+
tokens << token
|
|
141
|
+
index = new_index
|
|
142
|
+
else
|
|
143
|
+
# Skip invalid character
|
|
144
|
+
index += 1
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
tokens
|
|
149
|
+
end
|
|
150
|
+
----
|
|
151
|
+
|
|
152
|
+
**Algorithm Steps:**
|
|
153
|
+
|
|
154
|
+
1. **Preprocess:** Remove all comments
|
|
155
|
+
2. **Skip Whitespace:** Advance past spaces, tabs, newlines
|
|
156
|
+
3. **Match Token:** Attempt to identify token type
|
|
157
|
+
4. **Advance Index:** Move past consumed characters
|
|
158
|
+
5. **Repeat:** Continue until end of input
|
|
159
|
+
|
|
160
|
+
=== Comment Removal
|
|
161
|
+
|
|
162
|
+
**Implementation:**
|
|
163
|
+
|
|
164
|
+
[source,ruby]
|
|
165
|
+
----
|
|
166
|
+
# lib/postsvg/tokenizer.rb:11
|
|
167
|
+
ps = ps_code.gsub(/%[^\n\r]*/, " ")
|
|
168
|
+
----
|
|
169
|
+
|
|
170
|
+
**Behavior:**
|
|
171
|
+
|
|
172
|
+
* Removes `%` character and everything until newline
|
|
173
|
+
* Replaces with space to maintain character positions
|
|
174
|
+
* Handles both Unix (`\n`) and Windows (`\r\n`) line endings
|
|
175
|
+
|
|
176
|
+
**Example:**
|
|
177
|
+
|
|
178
|
+
[source,postscript]
|
|
179
|
+
----
|
|
180
|
+
% This is a comment
|
|
181
|
+
100 50 moveto % Move to position
|
|
182
|
+
stroke % Draw the line
|
|
183
|
+
----
|
|
184
|
+
|
|
185
|
+
**After Comment Removal:**
|
|
186
|
+
|
|
187
|
+
[source,postscript]
|
|
188
|
+
----
|
|
189
|
+
|
|
190
|
+
100 50 moveto
|
|
191
|
+
stroke
|
|
192
|
+
----
|
|
193
|
+
|
|
194
|
+
=== Token Matching Strategy
|
|
195
|
+
|
|
196
|
+
The tokenizer tries patterns in priority order:
|
|
197
|
+
|
|
198
|
+
.Token Matching Priority
|
|
199
|
+
[source]
|
|
200
|
+
----
|
|
201
|
+
1. Strings: (...)
|
|
202
|
+
2. Numbers: 123, 3.14, 1.5e-3
|
|
203
|
+
3. Braces: { }
|
|
204
|
+
4. Brackets: [ ]
|
|
205
|
+
5. Dicts: << >>
|
|
206
|
+
6. Hex Strings: <...>
|
|
207
|
+
7. Names/Ops: /name or operator
|
|
208
|
+
----
|
|
209
|
+
|
|
210
|
+
**Why Priority Matters:**
|
|
211
|
+
|
|
212
|
+
* `<<` must be checked before `<` (dict vs hex string)
|
|
213
|
+
* Numbers must be checked before operators (avoid parsing digits as operator names)
|
|
214
|
+
* Strings must be checked early (contain arbitrary characters)
|
|
215
|
+
|
|
216
|
+
== Number Parsing
|
|
217
|
+
|
|
218
|
+
=== Number Recognition
|
|
219
|
+
|
|
220
|
+
**Regex Pattern:**
|
|
221
|
+
|
|
222
|
+
[source,ruby]
|
|
223
|
+
----
|
|
224
|
+
# lib/postsvg/tokenizer.rb:40
|
|
225
|
+
-?(?:\d+\.\d+|\d+\.|\.\d+|\d+)(?:[eE][+-]?\d+)?
|
|
226
|
+
----
|
|
227
|
+
|
|
228
|
+
**Pattern Breakdown:**
|
|
229
|
+
|
|
230
|
+
[source]
|
|
231
|
+
----
|
|
232
|
+
-? # Optional minus sign
|
|
233
|
+
(?: # Non-capturing group for number formats:
|
|
234
|
+
\d+\.\d+ # 123.456 (decimal with digits on both sides)
|
|
235
|
+
| \d+\. # 123. (decimal with trailing dot)
|
|
236
|
+
| \.\d+ # .456 (decimal with leading dot)
|
|
237
|
+
| \d+ # 123 (integer)
|
|
238
|
+
)
|
|
239
|
+
(?:[eE][+-]?\d+)? # Optional scientific notation: e+10, E-5
|
|
240
|
+
----
|
|
241
|
+
|
|
242
|
+
=== Number Examples
|
|
243
|
+
|
|
244
|
+
[cols="1,2,1"]
|
|
245
|
+
|===
|
|
246
|
+
|Input |Token Value |Converted To
|
|
247
|
+
|
|
248
|
+
|`42`
|
|
249
|
+
|`"42"`
|
|
250
|
+
|`42` (Integer)
|
|
251
|
+
|
|
252
|
+
|`-17`
|
|
253
|
+
|`"-17"`
|
|
254
|
+
|`-17` (Integer)
|
|
255
|
+
|
|
256
|
+
|`3.14`
|
|
257
|
+
|`"3.14"`
|
|
258
|
+
|`3.14` (Float)
|
|
259
|
+
|
|
260
|
+
|`123.`
|
|
261
|
+
|`"123."`
|
|
262
|
+
|`123.0` (Float)
|
|
263
|
+
|
|
264
|
+
|`.5`
|
|
265
|
+
|`".5"`
|
|
266
|
+
|`0.5` (Float)
|
|
267
|
+
|
|
268
|
+
|`1.5e-3`
|
|
269
|
+
|`"1.5e-3"`
|
|
270
|
+
|`0.0015` (Float)
|
|
271
|
+
|
|
272
|
+
|`2E10`
|
|
273
|
+
|`"2E10"`
|
|
274
|
+
|`20000000000.0` (Float)
|
|
275
|
+
|===
|
|
276
|
+
|
|
277
|
+
=== Integer vs Float Detection
|
|
278
|
+
|
|
279
|
+
The Interpreter (not Tokenizer) determines integer vs float:
|
|
280
|
+
|
|
281
|
+
[source,ruby]
|
|
282
|
+
----
|
|
283
|
+
# In Interpreter
|
|
284
|
+
num_str = token.value
|
|
285
|
+
num = if num_str.include?(".") || num_str.match?(/[eE]/)
|
|
286
|
+
num_str.to_f # Float
|
|
287
|
+
else
|
|
288
|
+
num_str.to_i # Integer
|
|
289
|
+
end
|
|
290
|
+
----
|
|
291
|
+
|
|
292
|
+
**Rules:**
|
|
293
|
+
|
|
294
|
+
* Contains `.` → Float
|
|
295
|
+
* Contains `e` or `E` → Float
|
|
296
|
+
* Otherwise → Integer
|
|
297
|
+
|
|
298
|
+
== String Parsing
|
|
299
|
+
|
|
300
|
+
=== Regular Strings
|
|
301
|
+
|
|
302
|
+
**Syntax:** Enclosed in parentheses `(...)`
|
|
303
|
+
|
|
304
|
+
**Implementation:**
|
|
305
|
+
|
|
306
|
+
[source,ruby]
|
|
307
|
+
----
|
|
308
|
+
# lib/postsvg/tokenizer.rb:79
|
|
309
|
+
def self.match_string(ps, index)
|
|
310
|
+
result = +""
|
|
311
|
+
i = index + 1 # Skip opening (
|
|
312
|
+
depth = 1 # Track nesting level
|
|
313
|
+
|
|
314
|
+
while i < ps.length && depth > 0
|
|
315
|
+
case ps[i]
|
|
316
|
+
when "\\"
|
|
317
|
+
# Handle escape sequence
|
|
318
|
+
i += 1
|
|
319
|
+
result << process_escape(ps[i])
|
|
320
|
+
when "("
|
|
321
|
+
depth += 1
|
|
322
|
+
result << ps[i]
|
|
323
|
+
when ")"
|
|
324
|
+
depth -= 1
|
|
325
|
+
return [Token.new(type: "string", value: result), i + 1] if depth == 0
|
|
326
|
+
result << ps[i]
|
|
327
|
+
else
|
|
328
|
+
result << ps[i]
|
|
329
|
+
end
|
|
330
|
+
i += 1
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
[Token.new(type: "string", value: result), i]
|
|
334
|
+
end
|
|
335
|
+
----
|
|
336
|
+
|
|
337
|
+
**Features:**
|
|
338
|
+
|
|
339
|
+
* **Nesting Support:** Tracks parenthesis depth for nested strings
|
|
340
|
+
* **Escape Sequences:** Processes backslash escapes
|
|
341
|
+
* **Boundary Tracking:** Returns position after closing parenthesis
|
|
342
|
+
|
|
343
|
+
=== String Nesting
|
|
344
|
+
|
|
345
|
+
**Example:**
|
|
346
|
+
|
|
347
|
+
[source,postscript]
|
|
348
|
+
----
|
|
349
|
+
(This is a (nested (string)))
|
|
350
|
+
----
|
|
351
|
+
|
|
352
|
+
**Depth Tracking:**
|
|
353
|
+
|
|
354
|
+
[source]
|
|
355
|
+
----
|
|
356
|
+
Character: ( T h i s i s a ( n e s t e d ( s t r i n g ) ) )
|
|
357
|
+
Depth: 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 2 1 0
|
|
358
|
+
^ ^
|
|
359
|
+
Start End
|
|
360
|
+
----
|
|
361
|
+
|
|
362
|
+
**Result:** `"This is a (nested (string))"`
|
|
363
|
+
|
|
364
|
+
=== Escape Sequences
|
|
365
|
+
|
|
366
|
+
**Standard Escapes:**
|
|
367
|
+
|
|
368
|
+
[cols="1,2,1"]
|
|
369
|
+
|===
|
|
370
|
+
|Escape |Meaning |Result
|
|
371
|
+
|
|
372
|
+
|`\n`
|
|
373
|
+
|Newline
|
|
374
|
+
|U+000A
|
|
375
|
+
|
|
376
|
+
|`\r`
|
|
377
|
+
|Carriage return
|
|
378
|
+
|U+000D
|
|
379
|
+
|
|
380
|
+
|`\t`
|
|
381
|
+
|Tab
|
|
382
|
+
|U+0009
|
|
383
|
+
|
|
384
|
+
|`\b`
|
|
385
|
+
|Backspace
|
|
386
|
+
|U+0008
|
|
387
|
+
|
|
388
|
+
|`\f`
|
|
389
|
+
|Form feed
|
|
390
|
+
|U+000C
|
|
391
|
+
|
|
392
|
+
|`\\`
|
|
393
|
+
|Backslash
|
|
394
|
+
|`\`
|
|
395
|
+
|
|
396
|
+
|`\(`
|
|
397
|
+
|Left parenthesis
|
|
398
|
+
|`(`
|
|
399
|
+
|
|
400
|
+
|`\)`
|
|
401
|
+
|Right parenthesis
|
|
402
|
+
|`)`
|
|
403
|
+
|===
|
|
404
|
+
|
|
405
|
+
**Octal Escapes:**
|
|
406
|
+
|
|
407
|
+
[source,ruby]
|
|
408
|
+
----
|
|
409
|
+
# lib/postsvg/tokenizer.rb:103
|
|
410
|
+
# \ddd where d = 0-7 (octal digit)
|
|
411
|
+
if ps[i].between?("0", "7")
|
|
412
|
+
octal = ps[i]
|
|
413
|
+
# Read up to 3 octal digits
|
|
414
|
+
i += 1
|
|
415
|
+
if i < ps.length && ps[i].between?("0", "7")
|
|
416
|
+
octal += ps[i]
|
|
417
|
+
i += 1
|
|
418
|
+
if i < ps.length && ps[i].between?("0", "7")
|
|
419
|
+
octal += ps[i]
|
|
420
|
+
i += 1
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
code = octal.to_i(8)
|
|
424
|
+
code = 255 if code > 255 # Clamp to valid range
|
|
425
|
+
result << code.chr
|
|
426
|
+
end
|
|
427
|
+
----
|
|
428
|
+
|
|
429
|
+
**Octal Examples:**
|
|
430
|
+
|
|
431
|
+
[source]
|
|
432
|
+
----
|
|
433
|
+
\101 → 'A' (65 in decimal)
|
|
434
|
+
\102 → 'B' (66 in decimal)
|
|
435
|
+
\040 → ' ' (32 in decimal, space)
|
|
436
|
+
\012 → '\n' (10 in decimal, newline)
|
|
437
|
+
\377 → 'ÿ' (255 in decimal, max byte)
|
|
438
|
+
----
|
|
439
|
+
|
|
440
|
+
=== String Examples
|
|
441
|
+
|
|
442
|
+
[cols="1,2"]
|
|
443
|
+
|===
|
|
444
|
+
|Input |Decoded Value
|
|
445
|
+
|
|
446
|
+
|`(Hello)`
|
|
447
|
+
|`"Hello"`
|
|
448
|
+
|
|
449
|
+
|`(Hello\nWorld)`
|
|
450
|
+
|`"Hello\nWorld"` (with newline)
|
|
451
|
+
|
|
452
|
+
|`(Tab\there)`
|
|
453
|
+
|`"Tab\there"` (with tab)
|
|
454
|
+
|
|
455
|
+
|`(Quote \(this\))`
|
|
456
|
+
|`"Quote (this)"`
|
|
457
|
+
|
|
458
|
+
|`(Backslash \\)`
|
|
459
|
+
|`"Backslash \"`
|
|
460
|
+
|
|
461
|
+
|`(Nested (string))`
|
|
462
|
+
|`"Nested (string)"`
|
|
463
|
+
|
|
464
|
+
|`(Octal \101\102\103)`
|
|
465
|
+
|`"Octal ABC"`
|
|
466
|
+
|===
|
|
467
|
+
|
|
468
|
+
== Hex String Parsing
|
|
469
|
+
|
|
470
|
+
=== Hex String Syntax
|
|
471
|
+
|
|
472
|
+
**Format:** `<hexdigits>` (angle brackets, not `<<`)
|
|
473
|
+
|
|
474
|
+
**Implementation:**
|
|
475
|
+
|
|
476
|
+
[source,ruby]
|
|
477
|
+
----
|
|
478
|
+
# lib/postsvg/tokenizer.rb:142
|
|
479
|
+
def self.match_hex_string(ps, index)
|
|
480
|
+
i = index + 1
|
|
481
|
+
hex_content = +""
|
|
482
|
+
|
|
483
|
+
# Collect hex digits (skip whitespace)
|
|
484
|
+
while i < ps.length && ps[i] != ">"
|
|
485
|
+
hex_content << ps[i] unless ps[i].match?(/\s/)
|
|
486
|
+
i += 1
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
# Convert hex pairs to bytes
|
|
490
|
+
str = +""
|
|
491
|
+
(0...hex_
|
|
492
|
+
content.length).step(2) do |j|
|
|
493
|
+
byte = hex_content[j, 2]
|
|
494
|
+
str << byte.to_i(16).chr
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
[Token.new(type: "hexstring", value: str), i + 1]
|
|
498
|
+
end
|
|
499
|
+
----
|
|
500
|
+
|
|
501
|
+
**Features:**
|
|
502
|
+
|
|
503
|
+
* **Whitespace Skipping:** Ignores spaces, tabs, newlines in hex data
|
|
504
|
+
* **Pair Processing:** Converts each hex pair to a byte
|
|
505
|
+
* **Binary Output:** Returns binary string (not hex text)
|
|
506
|
+
|
|
507
|
+
=== Hex String Examples
|
|
508
|
+
|
|
509
|
+
[cols="1,1,2"]
|
|
510
|
+
|===
|
|
511
|
+
|Input |Hex Content |Decoded Value
|
|
512
|
+
|
|
513
|
+
|`<48656C6C6F>`
|
|
514
|
+
|`48656C6C6F`
|
|
515
|
+
|`"Hello"` (bytes: 0x48='H', 0x65='e', 0x6C='l', 0x6C='l', 0x6F='o')
|
|
516
|
+
|
|
517
|
+
|`<48 65 6C 6C 6F>`
|
|
518
|
+
|`48656C6C6F`
|
|
519
|
+
|`"Hello"` (whitespace ignored)
|
|
520
|
+
|
|
521
|
+
|`<DEADBEEF>`
|
|
522
|
+
|`DEADBEEF`
|
|
523
|
+
|Binary: 0xDE, 0xAD, 0xBE, 0xEF
|
|
524
|
+
|
|
525
|
+
|`<>`
|
|
526
|
+
|Empty
|
|
527
|
+
|`""` (empty string)
|
|
528
|
+
|
|
529
|
+
|`<FF00>`
|
|
530
|
+
|`FF00`
|
|
531
|
+
|Binary: 0xFF, 0x00
|
|
532
|
+
|===
|
|
533
|
+
|
|
534
|
+
=== Odd-Length Hex Strings
|
|
535
|
+
|
|
536
|
+
PostScript specification allows odd-length hex strings (implicit trailing 0):
|
|
537
|
+
|
|
538
|
+
[source,postscript]
|
|
539
|
+
----
|
|
540
|
+
<ABC> → Equivalent to <ABC0>
|
|
541
|
+
<1> → Equivalent to <10>
|
|
542
|
+
----
|
|
543
|
+
|
|
544
|
+
**Current Implementation:** Handles odd lengths by processing pairs:
|
|
545
|
+
|
|
546
|
+
[source]
|
|
547
|
+
----
|
|
548
|
+
<ABC>:
|
|
549
|
+
Pair 1: "AB" → 0xAB
|
|
550
|
+
Pair 2: "C" → "C" (single char, .to_i(16) = 12 = 0x0C)
|
|
551
|
+
----
|
|
552
|
+
|
|
553
|
+
== Name and Operator Parsing
|
|
554
|
+
|
|
555
|
+
=== Identifier Pattern
|
|
556
|
+
|
|
557
|
+
**Regex:**
|
|
558
|
+
|
|
559
|
+
[source,ruby]
|
|
560
|
+
----
|
|
561
|
+
# lib/postsvg/tokenizer.rb:65
|
|
562
|
+
%r{\A/?[A-Za-z_\-.?*][A-Za-z0-9_\-.?*]*}
|
|
563
|
+
----
|
|
564
|
+
|
|
565
|
+
**Pattern Breakdown:**
|
|
566
|
+
|
|
567
|
+
[source]
|
|
568
|
+
----
|
|
569
|
+
\A # Start of string
|
|
570
|
+
/? # Optional leading slash
|
|
571
|
+
[A-Za-z_\-.?*] # First character: letter, underscore, or special
|
|
572
|
+
[A-Za-z0-9_\-.?*]* # Subsequent: letter, digit, or special
|
|
573
|
+
----
|
|
574
|
+
|
|
575
|
+
**Allowed Characters:**
|
|
576
|
+
|
|
577
|
+
* Letters: `A-Z`, `a-z`
|
|
578
|
+
* Digits: `0-9` (not as first character after `/`)
|
|
579
|
+
* Special: `_`, `-`, `.`, `?`, `*`
|
|
580
|
+
|
|
581
|
+
=== Name vs Operator Classification
|
|
582
|
+
|
|
583
|
+
[source,ruby]
|
|
584
|
+
----
|
|
585
|
+
# lib/postsvg/tokenizer.rb:66
|
|
586
|
+
value = match[0]
|
|
587
|
+
if value.start_with?("/")
|
|
588
|
+
# Name: remove leading slash
|
|
589
|
+
return [Token.new(type: "name", value: value[1..]), index + value.length]
|
|
590
|
+
else
|
|
591
|
+
# Operator
|
|
592
|
+
return [Token.new(type: "operator", value: value), index + value.length]
|
|
593
|
+
end
|
|
594
|
+
----
|
|
595
|
+
|
|
596
|
+
**Rule:** Leading `/` indicates a name (literal), otherwise it's an operator.
|
|
597
|
+
|
|
598
|
+
=== Name Examples
|
|
599
|
+
|
|
600
|
+
[cols="1,1,2"]
|
|
601
|
+
|===
|
|
602
|
+
|Input |Token Type |Token Value
|
|
603
|
+
|
|
604
|
+
|`/Helvetica`
|
|
605
|
+
|`name`
|
|
606
|
+
|`"Helvetica"`
|
|
607
|
+
|
|
608
|
+
|`/Pattern1`
|
|
609
|
+
|`name`
|
|
610
|
+
|`"Pattern1"`
|
|
611
|
+
|
|
612
|
+
|`/my-font`
|
|
613
|
+
|`name`
|
|
614
|
+
|`"my-font"`
|
|
615
|
+
|
|
616
|
+
|`/font.size`
|
|
617
|
+
|`name`
|
|
618
|
+
|`"font.size"`
|
|
619
|
+
|
|
620
|
+
|`/Color?`
|
|
621
|
+
|`name`
|
|
622
|
+
|`"Color?"`
|
|
623
|
+
|===
|
|
624
|
+
|
|
625
|
+
=== Operator Examples
|
|
626
|
+
|
|
627
|
+
[cols="1,1"]
|
|
628
|
+
|===
|
|
629
|
+
|Input |Token Value
|
|
630
|
+
|
|
631
|
+
|`moveto`
|
|
632
|
+
|`"moveto"`
|
|
633
|
+
|
|
634
|
+
|`setrgbcolor`
|
|
635
|
+
|`"setrgbcolor"`
|
|
636
|
+
|
|
637
|
+
|`add`
|
|
638
|
+
|`"add"`
|
|
639
|
+
|
|
640
|
+
|`gsave`
|
|
641
|
+
|`"gsave"`
|
|
642
|
+
|
|
643
|
+
|`showpage`
|
|
644
|
+
|`"showpage"`
|
|
645
|
+
|===
|
|
646
|
+
|
|
647
|
+
== Delimiter Parsing
|
|
648
|
+
|
|
649
|
+
=== Brace Delimiters
|
|
650
|
+
|
|
651
|
+
**Tokens:** `{` and `}`
|
|
652
|
+
|
|
653
|
+
**Purpose:** Mark procedure boundaries
|
|
654
|
+
|
|
655
|
+
[source,ruby]
|
|
656
|
+
----
|
|
657
|
+
# lib/postsvg/tokenizer.rb:46
|
|
658
|
+
return [Token.new(type: "brace", value: ps[index]), index + 1]
|
|
659
|
+
if ["{", "}"].include?(ps[index])
|
|
660
|
+
----
|
|
661
|
+
|
|
662
|
+
**Example:**
|
|
663
|
+
|
|
664
|
+
[source,postscript]
|
|
665
|
+
----
|
|
666
|
+
{ 1 add } def
|
|
667
|
+
----
|
|
668
|
+
|
|
669
|
+
**Tokens:**
|
|
670
|
+
|
|
671
|
+
[source]
|
|
672
|
+
----
|
|
673
|
+
Token(type: "brace", value: "{")
|
|
674
|
+
Token(type: "number", value: "1")
|
|
675
|
+
Token(type: "operator", value: "add")
|
|
676
|
+
Token(type: "brace", value: "}")
|
|
677
|
+
Token(type: "operator", value: "def")
|
|
678
|
+
----
|
|
679
|
+
|
|
680
|
+
=== Bracket Delimiters
|
|
681
|
+
|
|
682
|
+
**Tokens:** `[` and `]`
|
|
683
|
+
|
|
684
|
+
**Purpose:** Mark array boundaries
|
|
685
|
+
|
|
686
|
+
[source,ruby]
|
|
687
|
+
----
|
|
688
|
+
# lib/postsvg/tokenizer.rb:50
|
|
689
|
+
return [Token.new(type: "bracket", value: ps[index]), index + 1]
|
|
690
|
+
if ["[", "]"].include?(ps[index])
|
|
691
|
+
----
|
|
692
|
+
|
|
693
|
+
**Example:**
|
|
694
|
+
|
|
695
|
+
[source,postscript]
|
|
696
|
+
----
|
|
697
|
+
[1 2 3] def
|
|
698
|
+
----
|
|
699
|
+
|
|
700
|
+
**Tokens:**
|
|
701
|
+
|
|
702
|
+
[source]
|
|
703
|
+
----
|
|
704
|
+
Token(type: "bracket", value: "[")
|
|
705
|
+
Token(type: "number", value: "1")
|
|
706
|
+
Token(type: "number", value: "2")
|
|
707
|
+
Token(type: "number", value: "3")
|
|
708
|
+
Token(type: "bracket", value: "]")
|
|
709
|
+
Token(type: "operator", value: "def")
|
|
710
|
+
----
|
|
711
|
+
|
|
712
|
+
=== Dictionary Delimiters
|
|
713
|
+
|
|
714
|
+
**Tokens:** `<<` and `>>`
|
|
715
|
+
|
|
716
|
+
**Purpose:** Mark dictionary boundaries
|
|
717
|
+
|
|
718
|
+
[source,ruby]
|
|
719
|
+
----
|
|
720
|
+
# lib/postsvg/tokenizer.rb:54
|
|
721
|
+
return [Token.new(type: "dict", value: ps[index, 2]), index + 2]
|
|
722
|
+
if ["<<", ">>"].include?(ps[index, 2])
|
|
723
|
+
----
|
|
724
|
+
|
|
725
|
+
**Priority:** Must check before `<` (hex string) to avoid misidentification
|
|
726
|
+
|
|
727
|
+
**Example:**
|
|
728
|
+
|
|
729
|
+
[source,postscript]
|
|
730
|
+
----
|
|
731
|
+
<< /Type /Pattern >>
|
|
732
|
+
----
|
|
733
|
+
|
|
734
|
+
**Tokens:**
|
|
735
|
+
|
|
736
|
+
[source]
|
|
737
|
+
----
|
|
738
|
+
Token(type: "dict", value: "<<")
|
|
739
|
+
Token(type: "name", value: "Type")
|
|
740
|
+
Token(type: "name", value: "Pattern")
|
|
741
|
+
Token(type: "dict", value: ">>")
|
|
742
|
+
----
|
|
743
|
+
|
|
744
|
+
== Whitespace Handling
|
|
745
|
+
|
|
746
|
+
=== Whitespace Characters
|
|
747
|
+
|
|
748
|
+
**Recognized as whitespace:**
|
|
749
|
+
|
|
750
|
+
* Space (0x20)
|
|
751
|
+
* Tab (0x09)
|
|
752
|
+
* Newline (0x0A)
|
|
753
|
+
* Carriage return (0x0D)
|
|
754
|
+
* Form feed (0x0C)
|
|
755
|
+
|
|
756
|
+
**Implementation:**
|
|
757
|
+
|
|
758
|
+
[source,ruby]
|
|
759
|
+
----
|
|
760
|
+
# lib/postsvg/tokenizer.rb:18
|
|
761
|
+
index += 1 while index < ps.length && ps[index].match?(/\s/)
|
|
762
|
+
----
|
|
763
|
+
|
|
764
|
+
**Regex:** `/\s/` matches all Unicode whitespace
|
|
765
|
+
|
|
766
|
+
=== Whitespace Significance
|
|
767
|
+
|
|
768
|
+
**Token Separation:** Whitespace separates tokens but is not itself a token.
|
|
769
|
+
|
|
770
|
+
**Example:**
|
|
771
|
+
|
|
772
|
+
[source,postscript]
|
|
773
|
+
----
|
|
774
|
+
100 50 moveto
|
|
775
|
+
----
|
|
776
|
+
|
|
777
|
+
**Without whitespace (invalid):**
|
|
778
|
+
|
|
779
|
+
[source,postscript]
|
|
780
|
+
----
|
|
781
|
+
10050moveto → Would tokenize as number "10050" + operator "moveto"
|
|
782
|
+
----
|
|
783
|
+
|
|
784
|
+
**In Strings:** Whitespace inside strings is preserved:
|
|
785
|
+
|
|
786
|
+
[source,postscript]
|
|
787
|
+
----
|
|
788
|
+
(Hello World) → "Hello World" (3 spaces preserved)
|
|
789
|
+
----
|
|
790
|
+
|
|
791
|
+
== Complete Tokenization Example
|
|
792
|
+
|
|
793
|
+
=== Input PostScript
|
|
794
|
+
|
|
795
|
+
[source,postscript]
|
|
796
|
+
----
|
|
797
|
+
%%BoundingBox: 0 0 200 100
|
|
798
|
+
% Draw a rectangle
|
|
799
|
+
/Helvetica findfont 12 scalefont setfont
|
|
800
|
+
newpath
|
|
801
|
+
50 50 moveto
|
|
802
|
+
150 50 lineto
|
|
803
|
+
150 75 lineto
|
|
804
|
+
50 75 lineto
|
|
805
|
+
closepath
|
|
806
|
+
0.5 setgray
|
|
807
|
+
fill
|
|
808
|
+
(Hello) show
|
|
809
|
+
----
|
|
810
|
+
|
|
811
|
+
=== Output Token Stream
|
|
812
|
+
|
|
813
|
+
[source,ruby]
|
|
814
|
+
----
|
|
815
|
+
[
|
|
816
|
+
Token(type: "name", value: "Helvetica"),
|
|
817
|
+
Token(type: "operator", value: "findfont"),
|
|
818
|
+
Token(type: "number", value: "12"),
|
|
819
|
+
Token(type: "operator", value: "scalefont"),
|
|
820
|
+
Token(type: "operator", value: "setfont"),
|
|
821
|
+
Token(type: "operator", value: "newpath"),
|
|
822
|
+
Token(type: "number", value: "50"),
|
|
823
|
+
Token(type: "number", value: "50"),
|
|
824
|
+
Token(type: "operator", value: "moveto"),
|
|
825
|
+
Token(type: "number", value: "150"),
|
|
826
|
+
Token(type: "number", value: "50"),
|
|
827
|
+
Token(type: "operator", value: "lineto"),
|
|
828
|
+
Token(type: "number", value: "150"),
|
|
829
|
+
Token(type: "number", value: "75"),
|
|
830
|
+
Token(type: "operator", value: "lineto"),
|
|
831
|
+
Token(type: "number", value: "50"),
|
|
832
|
+
Token(type: "number", value: "75"),
|
|
833
|
+
Token(type: "operator", value: "lineto"),
|
|
834
|
+
Token(type: "operator", value: "closepath"),
|
|
835
|
+
Token(type: "number", value: "0.5"),
|
|
836
|
+
Token(type: "operator", value: "setgray"),
|
|
837
|
+
Token(type: "operator", value: "fill"),
|
|
838
|
+
Token(type: "string", value: "Hello"),
|
|
839
|
+
Token(type: "operator", value: "show")
|
|
840
|
+
]
|
|
841
|
+
----
|
|
842
|
+
|
|
843
|
+
**Note:** Comments and `%%BoundingBox` line are removed during preprocessing.
|
|
844
|
+
|
|
845
|
+
== Error Handling
|
|
846
|
+
|
|
847
|
+
=== Invalid Characters
|
|
848
|
+
|
|
849
|
+
**Strategy:** Skip invalid characters, continue parsing
|
|
850
|
+
|
|
851
|
+
[source,ruby]
|
|
852
|
+
----
|
|
853
|
+
# lib/postsvg/tokenizer.rb:27
|
|
854
|
+
if token
|
|
855
|
+
tokens << token
|
|
856
|
+
index = new_index
|
|
857
|
+
else
|
|
858
|
+
# Skip invalid character
|
|
859
|
+
index += 1
|
|
860
|
+
end
|
|
861
|
+
----
|
|
862
|
+
|
|
863
|
+
**Behavior:** Silently ignores characters that don't match any token pattern.
|
|
864
|
+
|
|
865
|
+
=== Unclosed Strings
|
|
866
|
+
|
|
867
|
+
**String without closing `)`:**
|
|
868
|
+
|
|
869
|
+
[source,postscript]
|
|
870
|
+
----
|
|
871
|
+
(This string is unclosed
|
|
872
|
+
----
|
|
873
|
+
|
|
874
|
+
**Behavior:** Tokenizer continues to end of input, returns partial string.
|
|
875
|
+
|
|
876
|
+
**Result:** `Token(type: "string", value: "This string is unclosed\n")`
|
|
877
|
+
|
|
878
|
+
=== Invalid Escape Sequences
|
|
879
|
+
|
|
880
|
+
**Unrecognized escape:**
|
|
881
|
+
|
|
882
|
+
[source,postscript]
|
|
883
|
+
----
|
|
884
|
+
(Invalid \z escape)
|
|
885
|
+
----
|
|
886
|
+
|
|
887
|
+
**Behavior:** Uses the literal character following backslash.
|
|
888
|
+
|
|
889
|
+
**Result:** `"Invalid z escape"` (backslash consumed, 'z' literal)
|
|
890
|
+
|
|
891
|
+
== Performance Characteristics
|
|
892
|
+
|
|
893
|
+
=== Time Complexity
|
|
894
|
+
|
|
895
|
+
**Single-Pass Algorithm:** O(n) where n = input length
|
|
896
|
+
|
|
897
|
+
**Character Processing:**
|
|
898
|
+
|
|
899
|
+
* Each character examined once
|
|
900
|
+
* Constant-time token type checks
|
|
901
|
+
* No backtracking or lookahead beyond current token
|
|
902
|
+
|
|
903
|
+
**Regex Matching:**
|
|
904
|
+
|
|
905
|
+
* Number regex: O(k) where k = digits in number
|
|
906
|
+
* Identifier regex: O(m) where m = identifier length
|
|
907
|
+
* Both are bounded and small relative to input
|
|
908
|
+
|
|
909
|
+
**Overall:** O(n) linear time complexity
|
|
910
|
+
|
|
911
|
+
=== Space Complexity
|
|
912
|
+
|
|
913
|
+
**Token Array:** O(t) where t = number of tokens
|
|
914
|
+
|
|
915
|
+
**Token Overhead:** Each token requires:
|
|
916
|
+
|
|
917
|
+
* Type string (interned)
|
|
918
|
+
* Value string
|
|
919
|
+
* Struct overhead
|
|
920
|
+
|
|
921
|
+
**Typical Ratio:** tokens ≈ n/5 (average 5 chars per token)
|
|
922
|
+
|
|
923
|
+
**Temporary Storage:** O(1) for current token being built
|
|
924
|
+
|
|
925
|
+
**Overall:** O(n) space complexity
|
|
926
|
+
|
|
927
|
+
=== Optimization Opportunities
|
|
928
|
+
|
|
929
|
+
**String Building:**
|
|
930
|
+
|
|
931
|
+
[source,ruby]
|
|
932
|
+
----
|
|
933
|
+
result = +"" # Mutable string buffer
|
|
934
|
+
result << char # Efficient append
|
|
935
|
+
----
|
|
936
|
+
|
|
937
|
+
**Regex Pre-compilation:** Patterns could be compiled as constants (currently inline).
|
|
938
|
+
|
|
939
|
+
**Interned Strings:** Token types could use symbols instead of strings.
|
|
940
|
+
|
|
941
|
+
== Testing the Parser
|
|
942
|
+
|
|
943
|
+
=== Unit Tests
|
|
944
|
+
|
|
945
|
+
**Number Parsing:**
|
|
946
|
+
|
|
947
|
+
[source,ruby]
|
|
948
|
+
----
|
|
949
|
+
describe "number tokenization" do
|
|
950
|
+
it "parses integers" do
|
|
951
|
+
tokens = Tokenizer.tokenize("42")
|
|
952
|
+
expect(tokens.length).to eq(1)
|
|
953
|
+
expect(tokens[0].type).to eq("number")
|
|
954
|
+
expect(tokens[0].value).to eq("42")
|
|
955
|
+
end
|
|
956
|
+
|
|
957
|
+
it "parses floats" do
|
|
958
|
+
tokens = Tokenizer.tokenize("3.14")
|
|
959
|
+
expect(tokens[0].value).to eq("3.14")
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
it "parses scientific notation" do
|
|
963
|
+
tokens = Tokenizer.tokenize("1.5e-3")
|
|
964
|
+
expect(tokens[0].value).to eq("1.5e-3")
|
|
965
|
+
end
|
|
966
|
+
end
|
|
967
|
+
----
|
|
968
|
+
|
|
969
|
+
**String Parsing:**
|
|
970
|
+
|
|
971
|
+
[source,ruby]
|
|
972
|
+
----
|
|
973
|
+
describe "string tokenization" do
|
|
974
|
+
it "parses simple strings" do
|
|
975
|
+
tokens = Tokenizer.tokenize("(Hello)")
|
|
976
|
+
expect(tokens[0].type).to eq("string")
|
|
977
|
+
expect(tokens[0].value).to eq("Hello")
|
|
978
|
+
end
|
|
979
|
+
|
|
980
|
+
it "handles nested parentheses" do
|
|
981
|
+
tokens = Tokenizer.tokenize("(nested (parens))")
|
|
982
|
+
expect(tokens[0].value).to eq("nested (parens)")
|
|
983
|
+
end
|
|
984
|
+
|
|
985
|
+
it "processes escape sequences" do
|
|
986
|
+
tokens = Tokenizer.tokenize("(line1\\nline2)")
|
|
987
|
+
expect(tokens[0].value).to eq("line1\nline2")
|
|
988
|
+
end
|
|
989
|
+
end
|
|
990
|
+
----
|
|
991
|
+
|
|
992
|
+
**Operator Parsing:**
|
|
993
|
+
|
|
994
|
+
[source,ruby]
|
|
995
|
+
----
|
|
996
|
+
describe "operator tokenization" do
|
|
997
|
+
it "parses operator names" do
|
|
998
|
+
tokens = Tokenizer.tokenize("moveto lineto stroke")
|
|
999
|
+
expect(tokens.map(&:value)).to eq(["moveto", "lineto", "stroke"])
|
|
1000
|
+
end
|
|
1001
|
+
|
|
1002
|
+
it "distinguishes names from operators" do
|
|
1003
|
+
tokens = Tokenizer.tokenize("/Helvetica Helvetica")
|
|
1004
|
+
expect(tokens[0].type).to eq("name")
|
|
1005
|
+
expect(tokens[0].value).to eq("Helvetica")
|
|
1006
|
+
expect(tokens[1].type).to eq("operator")
|
|
1007
|
+
expect(tokens[1].value).to eq("Helvetica")
|
|
1008
|
+
end
|
|
1009
|
+
end
|
|
1010
|
+
----
|
|
1011
|
+
|
|
1012
|
+
=== Integration Tests
|
|
1013
|
+
|
|
1014
|
+
**Complete Program:**
|
|
1015
|
+
|
|
1016
|
+
[source,ruby]
|
|
1017
|
+
----
|
|
1018
|
+
describe "full tokenization" do
|
|
1019
|
+
it "tokenizes a complete PostScript program" do
|
|
1020
|
+
ps = <<~PS
|
|
1021
|
+
%%BoundingBox: 0 0 100 100
|
|
1022
|
+
/Helvetica findfont 12 scalefont setfont
|
|
1023
|
+
100 50 moveto
|
|
1024
|
+
(Hello) show
|
|
1025
|
+
PS
|
|
1026
|
+
|
|
1027
|
+
tokens = Tokenizer.tokenize(ps)
|
|
1028
|
+
|
|
1029
|
+
# Verify token sequence
|
|
1030
|
+
expect(tokens[0]).to have_attributes(type: "name", value: "Helvetica")
|
|
1031
|
+
expect(tokens[1]).to have_attributes(type: "operator", value: "findfont")
|
|
1032
|
+
# ... etc
|
|
1033
|
+
end
|
|
1034
|
+
end
|
|
1035
|
+
----
|
|
1036
|
+
|
|
1037
|
+
== Next Steps
|
|
1038
|
+
|
|
1039
|
+
* Review link:interpreter-stage.adoc[Interpreter Stage] to see how tokens are executed
|
|
1040
|
+
* Explore link:conversion-pipeline.adoc[Conversion Pipeline] for overall architecture
|
|
1041
|
+
* See link:../api-reference/tokenizer.adoc[Tokenizer API Reference] for usage details
|
|
1042
|
+
* Check link:../development.adoc[Development Guide] for contributing
|
|
1043
|
+
|
|
1044
|
+
== Bibliography
|
|
1045
|
+
|
|
1046
|
+
* link:conversion-pipeline.adoc[Conversion Pipeline Documentation]
|
|
1047
|
+
* link:interpreter-stage.adoc[Interpreter Stage Documentation]
|
|
1048
|
+
* link:../api-reference/tokenizer.adoc[Tokenizer API Reference]
|
|
1049
|
+
* PostScript Language Reference Manual, 3rd Edition (Adobe Systems)
|
|
1050
|
+
* Compilers: Principles, Techniques, and Tools (Dragon Book)
|
|
1051
|
+
* Lexical Analysis in Programming Language Implementation
|