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.
Files changed (201) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +105 -0
  3. data/CLAUDE.md +173 -0
  4. data/Gemfile +2 -3
  5. data/README.adoc +456 -179
  6. data/Rakefile +100 -0
  7. data/TODO.roadmap/00-architecture.md +139 -0
  8. data/TODO.roadmap/01-autoload-migration.md +39 -0
  9. data/TODO.roadmap/02-isolate-dormant-code.md +51 -0
  10. data/TODO.roadmap/03-domain-model.md +66 -0
  11. data/TODO.roadmap/04-lexer.md +40 -0
  12. data/TODO.roadmap/05-parser.md +43 -0
  13. data/TODO.roadmap/06-graphics-state.md +45 -0
  14. data/TODO.roadmap/07-matrix-and-color.md +37 -0
  15. data/TODO.roadmap/08-svg-builder.md +51 -0
  16. data/TODO.roadmap/09-renderer.md +52 -0
  17. data/TODO.roadmap/10-visitor.md +58 -0
  18. data/TODO.roadmap/11-operator-coverage.md +103 -0
  19. data/TODO.roadmap/12-svg-domain.md +62 -0
  20. data/TODO.roadmap/13-translation-handlers.md +69 -0
  21. data/TODO.roadmap/14-ps-serializer.md +49 -0
  22. data/TODO.roadmap/15-cli-and-public-api.md +47 -0
  23. data/TODO.roadmap/16-specs.md +84 -0
  24. data/TODO.roadmap/17-docs-sync.md +47 -0
  25. data/TODO.roadmap/18-performance-and-determinism.md +47 -0
  26. data/TODO.roadmap/19-error-model.md +45 -0
  27. data/TODO.roadmap/20-font-and-text.md +46 -0
  28. data/TODO.roadmap/21-images.md +45 -0
  29. data/TODO.roadmap/22-forms-and-resources.md +25 -0
  30. data/TODO.roadmap/23-level2-level3.md +52 -0
  31. data/TODO.roadmap/24-ci-and-release.md +42 -0
  32. data/TODO.roadmap/README.md +77 -0
  33. data/docs/.gitignore +29 -0
  34. data/docs/CHANGELOG.md +114 -0
  35. data/docs/COMPLETE_DOCUMENTATION_STATUS.md +376 -0
  36. data/docs/DEPLOYMENT.md +456 -0
  37. data/docs/DEPLOYMENT_INSTRUCTIONS.md +229 -0
  38. data/docs/DOCUMENTATION_PLAN.md +425 -0
  39. data/docs/FINAL_SUMMARY.md +657 -0
  40. data/docs/Gemfile +15 -0
  41. data/docs/README.md +327 -0
  42. data/docs/_config.yml +99 -0
  43. data/docs/advanced-topics.adoc +370 -0
  44. data/docs/api-reference/colors.adoc +705 -0
  45. data/docs/api-reference/converter.adoc +699 -0
  46. data/docs/api-reference/execution-context.adoc +1210 -0
  47. data/docs/api-reference/graphics-state.adoc +1070 -0
  48. data/docs/api-reference/interpreter.adoc +810 -0
  49. data/docs/api-reference/matrix.adoc +1179 -0
  50. data/docs/api-reference/path-builder.adoc +1284 -0
  51. data/docs/api-reference/postsvg-module.adoc +388 -0
  52. data/docs/api-reference/svg-generator.adoc +891 -0
  53. data/docs/api-reference/tokenizer.adoc +925 -0
  54. data/docs/api-reference.adoc +221 -0
  55. data/docs/architecture/command-registry.adoc +1191 -0
  56. data/docs/architecture/conversion-pipeline.adoc +746 -0
  57. data/docs/architecture/design-decisions.adoc +999 -0
  58. data/docs/architecture/generator-stage.adoc +1115 -0
  59. data/docs/architecture/graphics-state-model.adoc +1089 -0
  60. data/docs/architecture/interpreter-stage.adoc +1125 -0
  61. data/docs/architecture/parser-stage.adoc +1051 -0
  62. data/docs/architecture.adoc +354 -0
  63. data/docs/cli-reference/batch-command.adoc +616 -0
  64. data/docs/cli-reference/check-command.adoc +677 -0
  65. data/docs/cli-reference/cli-options.adoc +802 -0
  66. data/docs/cli-reference/convert-command.adoc +462 -0
  67. data/docs/cli-reference/version-command.adoc +296 -0
  68. data/docs/cli-reference.adoc +317 -0
  69. data/docs/concepts/conversion-pipeline.adoc +903 -0
  70. data/docs/concepts/coordinate-systems.adoc +836 -0
  71. data/docs/concepts/graphics-state.adoc +861 -0
  72. data/docs/concepts/path-operations.adoc +1076 -0
  73. data/docs/concepts/postscript-language.adoc +859 -0
  74. data/docs/concepts/svg-generation.adoc +937 -0
  75. data/docs/concepts.adoc +198 -0
  76. data/docs/contributing.adoc +443 -0
  77. data/docs/development.adoc +420 -0
  78. data/docs/faq.adoc +493 -0
  79. data/docs/getting-started/basic-usage.adoc +538 -0
  80. data/docs/getting-started/common-workflows.adoc +577 -0
  81. data/docs/getting-started/first-conversion.adoc +492 -0
  82. data/docs/getting-started/installation.adoc +534 -0
  83. data/docs/getting-started.adoc +94 -0
  84. data/docs/index.adoc +248 -0
  85. data/docs/optimization.adoc +196 -0
  86. data/docs/ps2svg_compatibility.adoc +149 -0
  87. data/docs/quick-reference.adoc +453 -0
  88. data/docs/sitemap.adoc +337 -0
  89. data/docs/troubleshooting.adoc +486 -0
  90. data/docs/validation.adoc +772 -0
  91. data/exe/postsvg +1 -0
  92. data/lib/postsvg/cli.rb +104 -57
  93. data/lib/postsvg/color.rb +132 -0
  94. data/lib/postsvg/errors.rb +68 -3
  95. data/lib/postsvg/format_number.rb +22 -0
  96. data/lib/postsvg/graphics_context.rb +80 -0
  97. data/lib/postsvg/graphics_stack.rb +43 -0
  98. data/lib/postsvg/model/literals/array.rb +41 -0
  99. data/lib/postsvg/model/literals/dictionary.rb +34 -0
  100. data/lib/postsvg/model/literals/hex.rb +37 -0
  101. data/lib/postsvg/model/literals/name.rb +40 -0
  102. data/lib/postsvg/model/literals/number.rb +36 -0
  103. data/lib/postsvg/model/literals/procedure.rb +41 -0
  104. data/lib/postsvg/model/literals/string.rb +30 -0
  105. data/lib/postsvg/model/literals.rb +19 -0
  106. data/lib/postsvg/model/operator.rb +58 -0
  107. data/lib/postsvg/model/operators/arithmetic.rb +264 -0
  108. data/lib/postsvg/model/operators/boolean.rb +182 -0
  109. data/lib/postsvg/model/operators/color.rb +74 -0
  110. data/lib/postsvg/model/operators/container.rb +186 -0
  111. data/lib/postsvg/model/operators/control_flow.rb +119 -0
  112. data/lib/postsvg/model/operators/device.rb +21 -0
  113. data/lib/postsvg/model/operators/dictionary.rb +118 -0
  114. data/lib/postsvg/model/operators/font.rb +121 -0
  115. data/lib/postsvg/model/operators/graphics_state.rb +84 -0
  116. data/lib/postsvg/model/operators/painting.rb +29 -0
  117. data/lib/postsvg/model/operators/path.rb +169 -0
  118. data/lib/postsvg/model/operators/stack.rb +72 -0
  119. data/lib/postsvg/model/operators/transformations.rb +103 -0
  120. data/lib/postsvg/model/operators.rb +89 -0
  121. data/lib/postsvg/model/program.rb +68 -0
  122. data/lib/postsvg/model/token.rb +43 -0
  123. data/lib/postsvg/model.rb +17 -0
  124. data/lib/postsvg/options.rb +29 -0
  125. data/lib/postsvg/renderer.rb +85 -0
  126. data/lib/postsvg/serializer.rb +325 -0
  127. data/lib/postsvg/source/ast_builder.rb +308 -0
  128. data/lib/postsvg/source/lexer.rb +322 -0
  129. data/lib/postsvg/source/operand_stack.rb +55 -0
  130. data/lib/postsvg/source.rb +21 -0
  131. data/lib/postsvg/svg/attribute_parser.rb +45 -0
  132. data/lib/postsvg/svg/clip_path_registry.rb +44 -0
  133. data/lib/postsvg/svg/document.rb +22 -0
  134. data/lib/postsvg/svg/element.rb +84 -0
  135. data/lib/postsvg/svg/elements/circle.rb +36 -0
  136. data/lib/postsvg/svg/elements/clip_path.rb +26 -0
  137. data/lib/postsvg/svg/elements/defs.rb +24 -0
  138. data/lib/postsvg/svg/elements/ellipse.rb +38 -0
  139. data/lib/postsvg/svg/elements/group.rb +37 -0
  140. data/lib/postsvg/svg/elements/image.rb +35 -0
  141. data/lib/postsvg/svg/elements/line.rb +36 -0
  142. data/lib/postsvg/svg/elements/path.rb +32 -0
  143. data/lib/postsvg/svg/elements/polygon.rb +12 -0
  144. data/lib/postsvg/svg/elements/polyline.rb +32 -0
  145. data/lib/postsvg/svg/elements/rect.rb +44 -0
  146. data/lib/postsvg/svg/elements/svg.rb +39 -0
  147. data/lib/postsvg/svg/elements/text.rb +42 -0
  148. data/lib/postsvg/svg/elements.rb +31 -0
  149. data/lib/postsvg/svg/paint.rb +34 -0
  150. data/lib/postsvg/svg/parser.rb +39 -0
  151. data/lib/postsvg/svg/path_data/command.rb +27 -0
  152. data/lib/postsvg/svg/path_data/parser.rb +82 -0
  153. data/lib/postsvg/svg/path_data.rb +17 -0
  154. data/lib/postsvg/svg/stroke.rb +31 -0
  155. data/lib/postsvg/svg/transform_list.rb +59 -0
  156. data/lib/postsvg/svg.rb +22 -0
  157. data/lib/postsvg/svg_builder.rb +249 -0
  158. data/lib/postsvg/translation/arc_converter.rb +86 -0
  159. data/lib/postsvg/translation/bounding_box.rb +59 -0
  160. data/lib/postsvg/translation/context.rb +34 -0
  161. data/lib/postsvg/translation/handler_registry.rb +38 -0
  162. data/lib/postsvg/translation/handlers/circle_handler.rb +28 -0
  163. data/lib/postsvg/translation/handlers/clip_path_handler.rb +13 -0
  164. data/lib/postsvg/translation/handlers/defs_handler.rb +15 -0
  165. data/lib/postsvg/translation/handlers/ellipse_handler.rb +31 -0
  166. data/lib/postsvg/translation/handlers/group_handler.rb +21 -0
  167. data/lib/postsvg/translation/handlers/image_handler.rb +20 -0
  168. data/lib/postsvg/translation/handlers/line_handler.rb +23 -0
  169. data/lib/postsvg/translation/handlers/open_handler.rb +18 -0
  170. data/lib/postsvg/translation/handlers/path_handler.rb +356 -0
  171. data/lib/postsvg/translation/handlers/polygon_handler.rb +33 -0
  172. data/lib/postsvg/translation/handlers/polyline_handler.rb +31 -0
  173. data/lib/postsvg/translation/handlers/rect_handler.rb +27 -0
  174. data/lib/postsvg/translation/handlers/shared.rb +110 -0
  175. data/lib/postsvg/translation/handlers/svg_handler.rb +25 -0
  176. data/lib/postsvg/translation/handlers/text_handler.rb +56 -0
  177. data/lib/postsvg/translation/handlers.rb +25 -0
  178. data/lib/postsvg/translation/ps_renderer.rb +105 -0
  179. data/lib/postsvg/translation/record_emitter.rb +35 -0
  180. data/lib/postsvg/translation.rb +17 -0
  181. data/lib/postsvg/version.rb +1 -1
  182. data/lib/postsvg/visitors/ps_visitor/arithmetic.rb +125 -0
  183. data/lib/postsvg/visitors/ps_visitor/boolean.rb +105 -0
  184. data/lib/postsvg/visitors/ps_visitor/color.rb +53 -0
  185. data/lib/postsvg/visitors/ps_visitor/common.rb +66 -0
  186. data/lib/postsvg/visitors/ps_visitor/container.rb +164 -0
  187. data/lib/postsvg/visitors/ps_visitor/control_flow.rb +110 -0
  188. data/lib/postsvg/visitors/ps_visitor/device.rb +20 -0
  189. data/lib/postsvg/visitors/ps_visitor/dictionary.rb +89 -0
  190. data/lib/postsvg/visitors/ps_visitor/font.rb +93 -0
  191. data/lib/postsvg/visitors/ps_visitor/graphics_state.rb +55 -0
  192. data/lib/postsvg/visitors/ps_visitor/painting.rb +90 -0
  193. data/lib/postsvg/visitors/ps_visitor/path.rb +112 -0
  194. data/lib/postsvg/visitors/ps_visitor/stack.rb +47 -0
  195. data/lib/postsvg/visitors/ps_visitor/transformations.rb +101 -0
  196. data/lib/postsvg/visitors/ps_visitor.rb +208 -0
  197. data/lib/postsvg/visitors.rb +9 -0
  198. data/lib/postsvg.rb +93 -59
  199. data/lychee.toml +86 -0
  200. metadata +216 -11
  201. 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