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,925 @@
1
+ = Tokenizer Class
2
+ :page-nav_order: 11
3
+ :page-parent: API Reference
4
+
5
+ == Purpose
6
+
7
+ The [`Tokenizer`](../../lib/postsvg/tokenizer.rb:8) class provides lexical analysis for PostScript code, breaking source code into discrete tokens that can be interpreted. It handles all PostScript token types including numbers, strings, operators, procedures, arrays, and dictionaries.
8
+
9
+ == References
10
+
11
+ * link:../index.adoc[Documentation Home]
12
+ * link:../api-reference.adoc[API Reference Overview]
13
+ * link:interpreter.adoc[Interpreter Class]
14
+ * link:converter.adoc[Converter Class]
15
+ * link:../architecture.adoc[Architecture Overview]
16
+
17
+ == Concepts
18
+
19
+ **Tokenization**:: The process of breaking source code into meaningful units (tokens) for interpretation.
20
+
21
+ **Lexical Analysis**:: The first phase of parsing that converts character sequences into tokens.
22
+
23
+ **Token**:: A categorized unit of PostScript code with a type and value (e.g., number, string, operator).
24
+
25
+ **PostScript Syntax**:: The grammar rules defining valid PostScript code structure, including literals, operators, and composite objects.
26
+
27
+ **Escape Sequences**:: Special character sequences in strings starting with backslash (`\`) that represent non-printable or special characters.
28
+
29
+ **Hexadecimal Strings**:: String literals encoded in hexadecimal format, enclosed in angle brackets `<...>`.
30
+
31
+ == Class Overview
32
+
33
+ The [`Tokenizer`](../../lib/postsvg/tokenizer.rb:8) class is defined in [`lib/postsvg/tokenizer.rb`](../../lib/postsvg/tokenizer.rb:1).
34
+
35
+ **Responsibilities:**
36
+
37
+ * Parse PostScript source code into tokens
38
+ * Remove PostScript comments
39
+ * Handle string escape sequences
40
+ * Parse numeric literals (integers, floats, scientific notation)
41
+ * Recognize operators and names
42
+ * Parse composite structures (procedures, arrays, dictionaries)
43
+ * Handle hexadecimal string encoding
44
+
45
+ **Token Structure:**
46
+
47
+ Tokens are represented using the [`Token`](../../lib/postsvg/tokenizer.rb:5) struct:
48
+
49
+ [source,ruby]
50
+ ----
51
+ Token = Struct.new(:type, :value, keyword_init: true)
52
+ ----
53
+
54
+ **Token Types:**
55
+
56
+ * `"number"` - Numeric literals
57
+ * `"string"` - String literals in parentheses
58
+ * `"hexstring"` - Hexadecimal encoded strings
59
+ * `"operator"` - PostScript operators
60
+ * `"name"` - Name literals (start with `/`)
61
+ * `"brace"` - Procedure delimiters `{` `}`
62
+ * `"bracket"` - Array delimiters `[` `]`
63
+ * `"dict"` - Dictionary delimiters `<<` `>>`
64
+
65
+ == Class Methods
66
+
67
+ === tokenize
68
+
69
+ Tokenize PostScript source code into an array of tokens.
70
+
71
+ **Syntax:**
72
+
73
+ [source,ruby]
74
+ ----
75
+ tokens = Postsvg::Tokenizer.tokenize(ps_code) <1>
76
+ ----
77
+ <1> Parse PostScript code and return token array
78
+
79
+ **Where:**
80
+
81
+ `ps_code`:: String containing PostScript source code
82
+
83
+ **Returns:**
84
+
85
+ Array of [`Token`](../../lib/postsvg/tokenizer.rb:5) objects, each with:
86
+ * `type` - String indicating token type
87
+ * `value` - Token value (String or as-is)
88
+
89
+ **Processing Steps:**
90
+
91
+ 1. Remove PostScript comments (text from `%` to end of line)
92
+ 2. Skip whitespace characters
93
+ 3. Match and classify tokens sequentially
94
+ 4. Handle escape sequences in strings
95
+ 5. Convert hexadecimal strings to binary
96
+
97
+ **Source:**
98
+
99
+ [`lib/postsvg/tokenizer.rb:9-33`](../../lib/postsvg/tokenizer.rb:9)
100
+
101
+ .Basic tokenization
102
+ [example]
103
+ ====
104
+ [source,ruby]
105
+ ----
106
+ require 'postsvg'
107
+
108
+ ps_code = "10 20 add"
109
+ tokens = Postsvg::Tokenizer.tokenize(ps_code)
110
+
111
+ tokens.each do |token|
112
+ puts "#{token.type}: #{token.value}"
113
+ end
114
+
115
+ # Output:
116
+ # number: 10
117
+ # number: 20
118
+ # operator: add
119
+ ----
120
+ ====
121
+
122
+ .Tokenize complex PostScript
123
+ [example]
124
+ ====
125
+ [source,ruby]
126
+ ----
127
+ ps_code = <<~PS
128
+ /name 100 def
129
+ { 10 20 moveto } exec
130
+ [1 2 3] length
131
+ PS
132
+
133
+ tokens = Postsvg::Tokenizer.tokenize(ps_code)
134
+
135
+ puts "Total tokens: #{tokens.length}"
136
+ tokens.each do |token|
137
+ puts " #{token.type.ljust(10)} #{token.value}"
138
+ end
139
+
140
+ # Output:
141
+ # Total tokens: 15
142
+ # name name
143
+ # number 100
144
+ # operator def
145
+ # brace {
146
+ # number 10
147
+ # number 20
148
+ # operator moveto
149
+ # brace }
150
+ # operator exec
151
+ # bracket [
152
+ # number 1
153
+ # number 2
154
+ # number 3
155
+ # bracket ]
156
+ # operator length
157
+ ----
158
+ ====
159
+
160
+ .Handle comments
161
+ [example]
162
+ ====
163
+ [source,ruby]
164
+ ----
165
+ ps_code = <<~PS
166
+ % This is a comment
167
+ 10 20 add % inline comment
168
+ % Another comment
169
+ 30 sub
170
+ PS
171
+
172
+ tokens = Postsvg::Tokenizer.tokenize(ps_code)
173
+
174
+ # Comments are removed during tokenization
175
+ puts tokens.map(&:value).join(" ")
176
+ # → "10 20 add 30 sub"
177
+ ----
178
+ ====
179
+
180
+ .Tokenize strings
181
+ [example]
182
+ ====
183
+ [source,ruby]
184
+ ----
185
+ ps_code = '(Hello World) show'
186
+ tokens = Postsvg::Tokenizer.tokenize(ps_code)
187
+
188
+ puts tokens[0].type # → "string"
189
+ puts tokens[0].value # → "Hello World"
190
+ puts tokens[1].type # → "operator"
191
+ puts tokens[1].value # → "show"
192
+ ----
193
+ ====
194
+
195
+ === match_token
196
+
197
+ Match a single token at the specified position (private class method).
198
+
199
+ **Syntax:**
200
+
201
+ [source,ruby]
202
+ ----
203
+ token, new_index = Postsvg::Tokenizer.match_token(ps, index) <1>
204
+ ----
205
+ <1> Internal method for matching tokens
206
+
207
+ **Where:**
208
+
209
+ `ps`:: PostScript source string
210
+
211
+ `index`:: Current parsing position (Integer)
212
+
213
+ **Returns:**
214
+
215
+ Array containing:
216
+ * `token` - [`Token`](../../lib/postsvg/tokenizer.rb:5) object or `nil` if no match
217
+ * `new_index` - Updated position after token
218
+
219
+ **Matching Order:**
220
+
221
+ 1. Strings `(...)`
222
+ 2. Numbers (integers, floats, scientific notation)
223
+ 3. Braces `{` `}`
224
+ 4. Brackets `[` `]`
225
+ 5. Dict markers `<<` `>>`
226
+ 6. Hex strings `<...>`
227
+ 7. Names `/name` and operators `name`
228
+
229
+ **Source:**
230
+
231
+ [`lib/postsvg/tokenizer.rb:35-77`](../../lib/postsvg/tokenizer.rb:35)
232
+
233
+ === match_string
234
+
235
+ Parse a PostScript string literal (private class method).
236
+
237
+ **Syntax:**
238
+
239
+ [source,ruby]
240
+ ----
241
+ token, new_index = Postsvg::Tokenizer.match_string(ps, index) <1>
242
+ ----
243
+ <1> Internal method for parsing strings
244
+
245
+ **Where:**
246
+
247
+ `ps`:: PostScript source string
248
+
249
+ `index`:: Position of opening `(`
250
+
251
+ **Returns:**
252
+
253
+ Array containing:
254
+ * `token` - Token with type `"string"` and parsed value
255
+ * `new_index` - Position after closing `)`
256
+
257
+ **Features:**
258
+
259
+ * Handles nested parentheses
260
+ * Processes escape sequences
261
+ * Supports multi-line strings
262
+ * Tracks string depth for proper closure
263
+
264
+ **Escape Sequences:**
265
+
266
+ * `\n` → newline
267
+ * `\r` → carriage return
268
+ * `\t` → tab
269
+ * `\b` → backspace
270
+ * `\f` → form feed
271
+ * `\(` → left parenthesis
272
+ * `\)` → right parenthesis
273
+ * `\\` → backslash
274
+ * `\ddd` → octal character code (up to 3 digits)
275
+
276
+ **Source:**
277
+
278
+ [`lib/postsvg/tokenizer.rb:79-140`](../../lib/postsvg/tokenizer.rb:79)
279
+
280
+ .String with escape sequences
281
+ [example]
282
+ ====
283
+ [source,ruby]
284
+ ----
285
+ ps_code = '(Line1\nLine2\tTabbed)'
286
+ tokens = Postsvg::Tokenizer.tokenize(ps_code)
287
+
288
+ puts tokens[0].value
289
+ # Output:
290
+ # Line1
291
+ # Line2 Tabbed
292
+ ----
293
+ ====
294
+
295
+ .Nested parentheses
296
+ [example]
297
+ ====
298
+ [source,ruby]
299
+ ----
300
+ ps_code = '(outer (nested) string)'
301
+ tokens = Postsvg::Tokenizer.tokenize(ps_code)
302
+
303
+ puts tokens[0].value # → "outer (nested) string"
304
+ ----
305
+ ====
306
+
307
+ .Octal escape sequences
308
+ [example]
309
+ ====
310
+ [source,ruby]
311
+ ----
312
+ # \101 is octal for 'A'
313
+ ps_code = '(\101\102\103)'
314
+ tokens = Postsvg::Tokenizer.tokenize(ps_code)
315
+
316
+ puts tokens[0].value # → "ABC"
317
+ ----
318
+ ====
319
+
320
+ === match_hex_string
321
+
322
+ Parse a hexadecimal string literal (private class method).
323
+
324
+ **Syntax:**
325
+
326
+ [source,ruby]
327
+ ----
328
+ token, new_index = Postsvg::Tokenizer.match_hex_string(ps, index) <1>
329
+ ----
330
+ <1> Internal method for parsing hex strings
331
+
332
+ **Where:**
333
+
334
+ `ps`:: PostScript source string
335
+
336
+ `index`:: Position of opening `<`
337
+
338
+ **Returns:**
339
+
340
+ Array containing:
341
+ * `token` - Token with type `"hexstring"` and decoded value
342
+ * `new_index` - Position after closing `>`
343
+
344
+ **Processing:**
345
+
346
+ 1. Extract hexadecimal characters between `<` and `>`
347
+ 2. Ignore whitespace in hex data
348
+ 3. Convert pairs of hex digits to bytes
349
+ 4. Return decoded binary string
350
+
351
+ **Source:**
352
+
353
+ [`lib/postsvg/tokenizer.rb:142-159`](../../lib/postsvg/tokenizer.rb:142)
354
+
355
+ .Hexadecimal string
356
+ [example]
357
+ ====
358
+ [source,ruby]
359
+ ----
360
+ # <48656C6C6F> is "Hello" in hex
361
+ ps_code = '<48656C6C6F>'
362
+ tokens = Postsvg::Tokenizer.tokenize(ps_code)
363
+
364
+ puts tokens[0].type # → "hexstring"
365
+ puts tokens[0].value # → "Hello"
366
+ ----
367
+ ====
368
+
369
+ .Hex string with whitespace
370
+ [example]
371
+ ====
372
+ [source,ruby]
373
+ ----
374
+ # Whitespace is ignored in hex strings
375
+ ps_code = '<48 65 6C 6C 6F>'
376
+ tokens = Postsvg::Tokenizer.tokenize(ps_code)
377
+
378
+ puts tokens[0].value # → "Hello"
379
+ ----
380
+ ====
381
+
382
+ == Token Types
383
+
384
+ === Number Tokens
385
+
386
+ **Formats:**
387
+
388
+ * Integers: `10`, `-5`, `0`
389
+ * Floats: `10.5`, `.5`, `10.`
390
+ * Scientific notation: `1e3`, `3.14e-2`, `2E+5`
391
+
392
+ **Detection:**
393
+
394
+ [source,ruby]
395
+ ----
396
+ /\A-?(?:\d+\.\d+|\d+\.|\.\d+|\d+)(?:[eE][+-]?\d+)?/
397
+ ----
398
+
399
+ .Number examples
400
+ [example]
401
+ ====
402
+ [source,ruby]
403
+ ----
404
+ ps_code = "10 -5 3.14 .5 1e3 2.5e-2"
405
+ tokens = Postsvg::Tokenizer.tokenize(ps_code)
406
+
407
+ tokens.each do |token|
408
+ puts token.value
409
+ end
410
+
411
+ # Output:
412
+ # 10
413
+ # -5
414
+ # 3.14
415
+ # .5
416
+ # 1e3
417
+ # 2.5e-2
418
+ ----
419
+ ====
420
+
421
+ === String Tokens
422
+
423
+ **Format:** `(string content)`
424
+
425
+ **Features:**
426
+
427
+ * Nested parentheses support
428
+ * Escape sequence processing
429
+ * Multi-line strings
430
+ * Binary data support
431
+
432
+ .String token examples
433
+ [example]
434
+ ====
435
+ [source,ruby]
436
+ ----
437
+ examples = [
438
+ '(simple)',
439
+ '(with (nested) parens)',
440
+ '(line1\nline2)',
441
+ '(tab\there)',
442
+ '(\101\102\103)' # Octal
443
+ ]
444
+
445
+ examples.each do |ps|
446
+ token = Postsvg::Tokenizer.tokenize(ps)[0]
447
+ puts "Input: #{ps}"
448
+ puts "Value: #{token.value.inspect}\n\n"
449
+ end
450
+ ----
451
+ ====
452
+
453
+ === Operator Tokens
454
+
455
+ **Format:** Alphanumeric names not starting with `/`
456
+
457
+ **Pattern:** `[A-Za-z_\-.?*][A-Za-z0-9_\-.?*]*`
458
+
459
+ **Examples:** `add`, `moveto`, `gsave`, `setrgbcolor`
460
+
461
+ .Operator examples
462
+ [example]
463
+ ====
464
+ [source,ruby]
465
+ ----
466
+ ps_code = "moveto lineto stroke gsave grestore"
467
+ tokens = Postsvg::Tokenizer.tokenize(ps_code)
468
+
469
+ tokens.each do |token|
470
+ puts "#{token.type}: #{token.value}"
471
+ end
472
+
473
+ # All recognized as operators
474
+ ----
475
+ ====
476
+
477
+ === Name Tokens
478
+
479
+ **Format:** Names starting with `/`
480
+
481
+ **Value:** Name without the leading `/`
482
+
483
+ **Examples:** `/FontName`, `/MyVariable`, `/123`
484
+
485
+ .Name token examples
486
+ [example]
487
+ ====
488
+ [source,ruby]
489
+ ----
490
+ ps_code = "/name1 /name2 /123"
491
+ tokens = Postsvg::Tokenizer.tokenize(ps_code)
492
+
493
+ tokens.each do |token|
494
+ puts "#{token.type}: #{token.value}"
495
+ end
496
+
497
+ # Output:
498
+ # name: name1
499
+ # name: name2
500
+ # name: 123
501
+ ----
502
+ ====
503
+
504
+ === Brace Tokens
505
+
506
+ **Format:** `{` and `}`
507
+
508
+ **Usage:** Delimit procedure bodies
509
+
510
+ .Procedure tokenization
511
+ [example]
512
+ ====
513
+ [source,ruby]
514
+ ----
515
+ ps_code = "{ 10 20 moveto }"
516
+ tokens = Postsvg::Tokenizer.tokenize(ps_code)
517
+
518
+ tokens.each do |token|
519
+ puts "#{token.type}: #{token.value}"
520
+ end
521
+
522
+ # Output:
523
+ # brace: {
524
+ # number: 10
525
+ # number: 20
526
+ # operator: moveto
527
+ # brace: }
528
+ ----
529
+ ====
530
+
531
+ === Bracket Tokens
532
+
533
+ **Format:** `[` and `]`
534
+
535
+ **Usage:** Delimit array literals
536
+
537
+ .Array tokenization
538
+ [example]
539
+ ====
540
+ [source,ruby]
541
+ ----
542
+ ps_code = "[1 2 3 4 5]"
543
+ tokens = Postsvg::Tokenizer.tokenize(ps_code)
544
+
545
+ tokens.each do |token|
546
+ puts "#{token.type}: #{token.value}"
547
+ end
548
+
549
+ # Output:
550
+ # bracket: [
551
+ # number: 1
552
+ # number: 2
553
+ # number: 3
554
+ # number: 4
555
+ # number: 5
556
+ # bracket: ]
557
+ ----
558
+ ====
559
+
560
+ === Dict Tokens
561
+
562
+ **Format:** `<<` and `>>`
563
+
564
+ **Usage:** Delimit dictionary literals
565
+
566
+ .Dictionary tokenization
567
+ [example]
568
+ ====
569
+ [source,ruby]
570
+ ----
571
+ ps_code = "<< /Type /Pattern /Width 100 >>"
572
+ tokens = Postsvg::Tokenizer.tokenize(ps_code)
573
+
574
+ tokens.each do |token|
575
+ puts "#{token.type}: #{token.value}"
576
+ end
577
+
578
+ # Output:
579
+ # dict: <<
580
+ # name: Type
581
+ # name: Pattern
582
+ # name: Width
583
+ # number: 100
584
+ # dict: >>
585
+ ----
586
+ ====
587
+
588
+ == Usage Patterns
589
+
590
+ === Pattern 1: Pre-processing for Interpretation
591
+
592
+ [source,ruby]
593
+ ----
594
+ require 'postsvg'
595
+
596
+ def process_postscript(ps_code)
597
+ # Tokenize
598
+ tokens = Postsvg::Tokenizer.tokenize(ps_code)
599
+
600
+ # Analyze tokens
601
+ stats = {
602
+ total: tokens.length,
603
+ numbers: tokens.count { |t| t.type == "number" },
604
+ strings: tokens.count { |t| t.type == "string" },
605
+ operators: tokens.count { |t| t.type == "operator" },
606
+ names: tokens.count { |t| t.type == "name" }
607
+ }
608
+
609
+ puts "Token Statistics:"
610
+ stats.each { |key, value| puts " #{key}: #{value}" }
611
+
612
+ # Pass to interpreter
613
+ interpreter = Postsvg::Interpreter.new
614
+ interpreter.interpret(tokens, bbox)
615
+ end
616
+ ----
617
+
618
+ === Pattern 2: Token Stream Analysis
619
+
620
+ [source,ruby]
621
+ ----
622
+ require 'postsvg'
623
+
624
+ class TokenAnalyzer
625
+ def initialize(ps_code)
626
+ @tokens = Postsvg::Tokenizer.tokenize(ps_code)
627
+ end
628
+
629
+ def find_operator_usage(operator_name)
630
+ indices = []
631
+ @tokens.each_with_index do |token, i|
632
+ indices << i if token.type == "operator" && token.value == operator_name
633
+ end
634
+ indices
635
+ end
636
+
637
+ def extract_procedures
638
+ procedures = []
639
+ depth = 0
640
+ current_proc = []
641
+
642
+ @tokens.each do |token|
643
+ if token.type == "brace"
644
+ if token.value == "{"
645
+ depth += 1
646
+ current_proc = [] if depth == 1
647
+ elsif token.value == "}"
648
+ depth -= 1
649
+ if depth == 0 && !current_proc.empty?
650
+ procedures << current_proc.dup
651
+ current_proc = []
652
+ end
653
+ end
654
+ elsif depth > 0
655
+ current_proc << token
656
+ end
657
+ end
658
+
659
+ procedures
660
+ end
661
+
662
+ def list_defined_names
663
+ names = []
664
+ @tokens.each_with_index do |token, i|
665
+ if token.type == "name" &&
666
+ i + 2 < @tokens.length &&
667
+ @tokens[i + 2].type == "operator" &&
668
+ @tokens[i + 2].value == "def"
669
+ names << token.value
670
+ end
671
+ end
672
+ names
673
+ end
674
+ end
675
+
676
+ # Usage
677
+ ps_code = <<~PS
678
+ /myvar 100 def
679
+ /myproc { 10 20 moveto } def
680
+ myvar myproc
681
+ PS
682
+
683
+ analyzer = TokenAnalyzer.new(ps_code)
684
+ puts "Defined names: #{analyzer.list_defined_names.join(', ')}"
685
+ puts "Procedures found: #{analyzer.extract_procedures.length}"
686
+ ----
687
+
688
+ === Pattern 3: Token Filtering
689
+
690
+ [source,ruby]
691
+ ----
692
+ require 'postsvg'
693
+
694
+ def filter_tokens(ps_code, &block)
695
+ tokens = Postsvg::Tokenizer.tokenize(ps_code)
696
+ tokens.select(&block)
697
+ end
698
+
699
+ # Find all numbers
700
+ numbers = filter_tokens(ps_code) { |t| t.type == "number" }
701
+ puts "Numbers: #{numbers.map(&:value).join(', ')}"
702
+
703
+ # Find all operators
704
+ operators = filter_tokens(ps_code) { |t| t.type == "operator" }
705
+ puts "Operators: #{operators.map(&:value).join(', ')}"
706
+
707
+ # Find all strings
708
+ strings = filter_tokens(ps_code) { |t| t.type == "string" }
709
+ puts "Strings: #{strings.length} found"
710
+ ----
711
+
712
+ === Pattern 4: Token Validation
713
+
714
+ [source,ruby]
715
+ ----
716
+ require 'postsvg'
717
+
718
+ class TokenValidator
719
+ def initialize(ps_code)
720
+ @tokens = Postsvg::Tokenizer.tokenize(ps_code)
721
+ @errors = []
722
+ end
723
+
724
+ def validate
725
+ check_balanced_braces
726
+ check_balanced_brackets
727
+ check_balanced_dicts
728
+
729
+ {
730
+ valid: @errors.empty?,
731
+ errors: @errors
732
+ }
733
+ end
734
+
735
+ private
736
+
737
+ def check_balanced_braces
738
+ depth = 0
739
+ @tokens.each do |token|
740
+ next unless token.type == "brace"
741
+ depth += (token.value == "{" ? 1 : -1)
742
+ @errors << "Unmatched '}'" if depth < 0
743
+ end
744
+ @errors << "Unclosed '{'" if depth > 0
745
+ end
746
+
747
+ def check_balanced_brackets
748
+ depth = 0
749
+ @tokens.each do |token|
750
+ next unless token.type == "bracket"
751
+ depth += (token.value == "[" ? 1 : -1)
752
+ @errors << "Unmatched ']'" if depth < 0
753
+ end
754
+ @errors << "Unclosed '['" if depth > 0
755
+ end
756
+
757
+ def check_balanced_dicts
758
+ depth = 0
759
+ @tokens.each do |token|
760
+ next unless token.type == "dict"
761
+ depth += (token.value == "<<" ? 1 : -1)
762
+ @errors << "Unmatched '>>'" if depth < 0
763
+ end
764
+ @errors << "Unclosed '<<'" if depth > 0
765
+ end
766
+ end
767
+
768
+ # Usage
769
+ validator = TokenValidator.new(ps_code)
770
+ result = validator.validate
771
+
772
+ if result[:valid]
773
+ puts "✓ Valid PostScript syntax"
774
+ else
775
+ puts "✗ Validation errors:"
776
+ result[:errors].each { |e| puts " - #{e}" }
777
+ end
778
+ ----
779
+
780
+ == Thread Safety
781
+
782
+ The `Tokenizer` class is **completely thread-safe** because:
783
+
784
+ 1. All methods are stateless class methods
785
+ 2. No shared mutable state
786
+ 3. Each tokenization creates independent token array
787
+ 4. Pure functions (same input → same output)
788
+
789
+ .Thread-safe usage
790
+ [example]
791
+ ====
792
+ [source,ruby]
793
+ ----
794
+ # Safe: Multiple threads can tokenize concurrently
795
+ ps_files = Dir.glob('*.ps')
796
+
797
+ threads = ps_files.map do |file|
798
+ Thread.new do
799
+ ps_code = File.read(file)
800
+ tokens = Postsvg::Tokenizer.tokenize(ps_code)
801
+ { file: file, token_count: tokens.length }
802
+ end
803
+ end
804
+
805
+ results = threads.map(&:value)
806
+ results.each do |r|
807
+ puts "#{r[:file]}: #{r[:token_count]} tokens"
808
+ end
809
+ ----
810
+ ====
811
+
812
+ == Performance Considerations
813
+
814
+ **Time Complexity:**
815
+
816
+ * Overall: O(n) where n = source code length
817
+ * Each character processed at most once
818
+ * Regex matching at each position
819
+
820
+ **Space Complexity:**
821
+
822
+ * O(t) where t = number of tokens
823
+ * Each token stores type and value
824
+ * String tokens duplicate content
825
+
826
+ **Performance Characteristics:**
827
+
828
+ * Linear time complexity
829
+ * Efficient single-pass parsing
830
+ * No backtracking
831
+ * Minimal memory overhead
832
+
833
+ .Performance measurement
834
+ [example]
835
+ ====
836
+ [source,ruby]
837
+ ----
838
+ require 'postsvg'
839
+ require 'benchmark'
840
+
841
+ # Generate large PostScript file
842
+ ps_code = (1..10_000).map do |i|
843
+ "#{i} #{i * 2} moveto #{i + 10} #{i * 2 + 10} lineto"
844
+ end.join("\n")
845
+
846
+ puts "Source size: #{ps_code.bytesize / 1024} KB"
847
+
848
+ time = Benchmark.measure do
849
+ @tokens = Postsvg::Tokenizer.tokenize(ps_code)
850
+ end
851
+
852
+ puts "Tokenized in #{'%.3f' % time.real}s"
853
+ puts "Tokens: #{@tokens.length}"
854
+ puts "Rate: #{(ps_code.bytesize / time.real / 1024).to_i} KB/sec"
855
+ puts "Throughput: #{(@tokens.length / time.real).to_i} tokens/sec"
856
+ ----
857
+ ====
858
+
859
+ **Optimization Tips:**
860
+
861
+ 1. **Batch processing**: Tokenize once, interpret many times if needed
862
+ 2. **Cache tokens**: Store tokenized representation for repeated use
863
+ 3. **Pre-filter**: Remove comments before tokenization if not needed
864
+ 4. **Stream processing**: For huge files, consider streaming tokenization
865
+
866
+ == Error Handling
867
+
868
+ The tokenizer is designed to be tolerant:
869
+
870
+ **Behavior:**
871
+
872
+ * Invalid characters are skipped
873
+ * Malformed tokens result in nil return
874
+ * Unclosed strings/hexstrings proceed to end
875
+ * No exceptions thrown during tokenization
876
+
877
+ **Validation:**
878
+
879
+ Consider post-tokenization validation for critical applications (see Pattern 4 above).
880
+
881
+ .Handle potentially invalid input
882
+ [example]
883
+ ====
884
+ [source,ruby]
885
+ ----
886
+ def safe_tokenize(ps_code)
887
+ begin
888
+ tokens = Postsvg::Tokenizer.tokenize(ps_code)
889
+
890
+ # Check for reasonable token count
891
+ if tokens.length > 1_000_000
892
+ { success: false, error: "Token limit exceeded" }
893
+ else
894
+ { success: true, tokens: tokens, count: tokens.length }
895
+ end
896
+ rescue => e
897
+ { success: false, error: e.message }
898
+ end
899
+ end
900
+
901
+ result = safe_tokenize(ps_code)
902
+
903
+ if result[:success]
904
+ puts "Tokenized #{result[:count]} tokens"
905
+ else
906
+ puts "Error: #{result[:error]}"
907
+ end
908
+ ----
909
+ ====
910
+
911
+ == Next Steps
912
+
913
+ * Learn about link:interpreter.adoc[Interpreter] which processes tokenized output
914
+ * Review link:converter.adoc[Converter] for the complete conversion pipeline
915
+ * See link:../architecture.adoc[Architecture] for system design
916
+ * Check link:../getting-started/basic-usage.adoc[Basic Usage] for examples
917
+
918
+ == Bibliography
919
+
920
+ * link:interpreter.adoc[Interpreter Documentation]
921
+ * link:converter.adoc[Converter Documentation]
922
+ * link:../architecture.adoc[Architecture Overview]
923
+ * link:../getting-started/basic-usage.adoc[Basic Usage Guide]
924
+ * link:https://www.adobe.com/jp/print/postscript/pdfs/PLRM.pdf[PostScript Language Reference Manual - Syntax]
925
+ * link:https://en.wikipedia.org/wiki/Lexical_analysis[Wikipedia: Lexical Analysis]