parsanol 3.0.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.

Potentially problematic release.


This version of parsanol might be problematic. Click here for more details.

Files changed (336) hide show
  1. checksums.yaml +7 -0
  2. data/HISTORY.txt +25 -0
  3. data/LICENSE +23 -0
  4. data/README.adoc +643 -0
  5. data/Rakefile +189 -0
  6. data/example/balanced-parens/basic.rb +42 -0
  7. data/example/balanced-parens/basic.rb.md +86 -0
  8. data/example/balanced-parens/parens.rb +42 -0
  9. data/example/balanced-parens/ruby_transform.rb +162 -0
  10. data/example/big.erb +73 -0
  11. data/example/boolean-algebra/basic.rb +70 -0
  12. data/example/boolean-algebra/basic.rb.md +108 -0
  13. data/example/boolean-algebra/ruby_transform.rb +263 -0
  14. data/example/calculator/basic.rb +153 -0
  15. data/example/calculator/basic.rb.md +120 -0
  16. data/example/calculator/pattern.rb +153 -0
  17. data/example/calculator/ruby_transform.rb +156 -0
  18. data/example/calculator/ruby_transform.rb.md +32 -0
  19. data/example/calculator/serialized.rb +257 -0
  20. data/example/calculator/serialized.rb.md +32 -0
  21. data/example/calculator/transform.rb +153 -0
  22. data/example/calculator/zero_copy.rb +269 -0
  23. data/example/calculator/zero_copy.rb.md +36 -0
  24. data/example/capture/basic.rb +49 -0
  25. data/example/capture/basic.rb.md +106 -0
  26. data/example/capture/example.json +39 -0
  27. data/example/comments/basic.rb +35 -0
  28. data/example/comments/basic.rb.md +110 -0
  29. data/example/csv/ruby_transform.rb +148 -0
  30. data/example/csv/ruby_transform.rb.md +131 -0
  31. data/example/csv/serialized.rb +201 -0
  32. data/example/csv/serialized.rb.md +31 -0
  33. data/example/csv/zero_copy.rb +276 -0
  34. data/example/csv/zero_copy.rb.md +36 -0
  35. data/example/custom_atoms/indent_atom.rb +79 -0
  36. data/example/deepest-errors/basic.rb +131 -0
  37. data/example/deepest-errors/basic.rb.md +152 -0
  38. data/example/documentation/basic.rb +18 -0
  39. data/example/documentation/basic.rb.md +97 -0
  40. data/example/email/basic.rb +55 -0
  41. data/example/email/basic.rb.md +102 -0
  42. data/example/email/ruby_transform.rb +106 -0
  43. data/example/empty/basic.rb +13 -0
  44. data/example/empty/basic.rb.md +73 -0
  45. data/example/empty/example.json +38 -0
  46. data/example/erb/basic.rb +47 -0
  47. data/example/erb/basic.rb.md +103 -0
  48. data/example/erb/optimized.rb +42 -0
  49. data/example/error-reporting/basic.rb +132 -0
  50. data/example/error-reporting/basic.rb.md +122 -0
  51. data/example/expression-evaluator/basic.rb +284 -0
  52. data/example/expression-evaluator/basic.rb.md +138 -0
  53. data/example/ini/basic.rb +154 -0
  54. data/example/ini/basic.rb.md +129 -0
  55. data/example/ini/ruby_transform.rb +154 -0
  56. data/example/ip-address/basic.rb +125 -0
  57. data/example/ip-address/basic.rb.md +139 -0
  58. data/example/iso-6709/basic.rb +231 -0
  59. data/example/iso-6709/basic.rb.md +143 -0
  60. data/example/iso-8601/basic.rb +275 -0
  61. data/example/iso-8601/basic.rb.md +149 -0
  62. data/example/json/basic.rb +128 -0
  63. data/example/json/basic.rb.md +121 -0
  64. data/example/json/pattern.rb +128 -0
  65. data/example/json/ruby_transform.rb +200 -0
  66. data/example/json/ruby_transform.rb.md +32 -0
  67. data/example/json/serialized.rb +233 -0
  68. data/example/json/serialized.rb.md +31 -0
  69. data/example/json/transform.rb +128 -0
  70. data/example/json/zero_copy.rb +316 -0
  71. data/example/json/zero_copy.rb.md +36 -0
  72. data/example/local/basic.rb +34 -0
  73. data/example/local/basic.rb.md +91 -0
  74. data/example/local/example.json +38 -0
  75. data/example/markdown/basic.rb +287 -0
  76. data/example/markdown/basic.rb.md +160 -0
  77. data/example/markup/basic.rb +173 -0
  78. data/example/markup/basic.rb.md +118 -0
  79. data/example/mathn/basic.rb +47 -0
  80. data/example/mathn/basic.rb.md +96 -0
  81. data/example/mathn/example.json +39 -0
  82. data/example/minilisp/basic.rb +94 -0
  83. data/example/minilisp/basic.rb.md +133 -0
  84. data/example/modularity/basic.rb +47 -0
  85. data/example/modularity/basic.rb.md +152 -0
  86. data/example/nested-errors/basic.rb +132 -0
  87. data/example/nested-errors/basic.rb.md +157 -0
  88. data/example/output/boolean_algebra.out +4 -0
  89. data/example/output/calc.out +1 -0
  90. data/example/output/capture.out +3 -0
  91. data/example/output/comments.out +8 -0
  92. data/example/output/deepest_errors.out +54 -0
  93. data/example/output/documentation.err +4 -0
  94. data/example/output/documentation.out +1 -0
  95. data/example/output/email_parser.out +2 -0
  96. data/example/output/empty.err +1 -0
  97. data/example/output/erb.out +7 -0
  98. data/example/output/ignore.out +1 -0
  99. data/example/output/ignore_whitespace.out +1 -0
  100. data/example/output/ip_address.out +9 -0
  101. data/example/output/json.out +5 -0
  102. data/example/output/local.out +3 -0
  103. data/example/output/mathn.out +4 -0
  104. data/example/output/minilisp.out +5 -0
  105. data/example/output/modularity.out +0 -0
  106. data/example/output/nested_errors.out +54 -0
  107. data/example/output/optimized_erb.out +1 -0
  108. data/example/output/parens.out +8 -0
  109. data/example/output/prec_calc.out +5 -0
  110. data/example/output/readme.out +1 -0
  111. data/example/output/scopes.out +1 -0
  112. data/example/output/seasons.out +28 -0
  113. data/example/output/sentence.out +1 -0
  114. data/example/output/simple_xml.out +2 -0
  115. data/example/output/string_parser.out +3 -0
  116. data/example/prec-calc/basic.rb +71 -0
  117. data/example/prec-calc/basic.rb.md +114 -0
  118. data/example/readme/basic.rb +30 -0
  119. data/example/readme/basic.rb.md +80 -0
  120. data/example/scopes/basic.rb +15 -0
  121. data/example/scopes/basic.rb.md +73 -0
  122. data/example/scopes/example.json +38 -0
  123. data/example/seasons/basic.rb +46 -0
  124. data/example/seasons/basic.rb.md +117 -0
  125. data/example/seasons/example.json +40 -0
  126. data/example/sentence/basic.rb +36 -0
  127. data/example/sentence/basic.rb.md +81 -0
  128. data/example/sexp/ruby_transform.rb +180 -0
  129. data/example/sexp/ruby_transform.rb.md +143 -0
  130. data/example/simple-xml/basic.rb +54 -0
  131. data/example/simple-xml/basic.rb.md +125 -0
  132. data/example/simple.lit +3 -0
  133. data/example/string-literal/basic.rb +77 -0
  134. data/example/string-literal/basic.rb.md +128 -0
  135. data/example/test.lit +4 -0
  136. data/example/toml/basic.rb +226 -0
  137. data/example/toml/basic.rb.md +173 -0
  138. data/example/url/basic.rb +219 -0
  139. data/example/url/basic.rb.md +142 -0
  140. data/example/url/ruby_transform.rb +219 -0
  141. data/example/yaml/basic.rb +216 -0
  142. data/example/yaml/basic.rb.md +148 -0
  143. data/ext/parsanol_native/extconf.rb +4 -0
  144. data/lib/parsanol/accelerator/application.rb +62 -0
  145. data/lib/parsanol/accelerator/engine.rb +112 -0
  146. data/lib/parsanol/accelerator.rb +162 -0
  147. data/lib/parsanol/ast_visitor.rb +122 -0
  148. data/lib/parsanol/atoms/alternative.rb +97 -0
  149. data/lib/parsanol/atoms/base.rb +214 -0
  150. data/lib/parsanol/atoms/can_flatten.rb +192 -0
  151. data/lib/parsanol/atoms/capture.rb +41 -0
  152. data/lib/parsanol/atoms/context.rb +351 -0
  153. data/lib/parsanol/atoms/context_optimized.rb +42 -0
  154. data/lib/parsanol/atoms/custom.rb +110 -0
  155. data/lib/parsanol/atoms/cut.rb +62 -0
  156. data/lib/parsanol/atoms/dsl.rb +130 -0
  157. data/lib/parsanol/atoms/dynamic.rb +33 -0
  158. data/lib/parsanol/atoms/entity.rb +55 -0
  159. data/lib/parsanol/atoms/ignored.rb +28 -0
  160. data/lib/parsanol/atoms/infix.rb +121 -0
  161. data/lib/parsanol/atoms/lookahead.rb +64 -0
  162. data/lib/parsanol/atoms/named.rb +50 -0
  163. data/lib/parsanol/atoms/re.rb +61 -0
  164. data/lib/parsanol/atoms/repetition.rb +241 -0
  165. data/lib/parsanol/atoms/scope.rb +28 -0
  166. data/lib/parsanol/atoms/sequence.rb +157 -0
  167. data/lib/parsanol/atoms/str.rb +90 -0
  168. data/lib/parsanol/atoms/visitor.rb +91 -0
  169. data/lib/parsanol/atoms.rb +36 -0
  170. data/lib/parsanol/buffer.rb +130 -0
  171. data/lib/parsanol/builder_callbacks.rb +353 -0
  172. data/lib/parsanol/cause.rb +101 -0
  173. data/lib/parsanol/context.rb +23 -0
  174. data/lib/parsanol/convenience.rb +35 -0
  175. data/lib/parsanol/edit_tracker.rb +107 -0
  176. data/lib/parsanol/error_reporter/contextual.rb +122 -0
  177. data/lib/parsanol/error_reporter/deepest.rb +106 -0
  178. data/lib/parsanol/error_reporter/tree.rb +68 -0
  179. data/lib/parsanol/error_reporter.rb +98 -0
  180. data/lib/parsanol/export.rb +163 -0
  181. data/lib/parsanol/expression/treetop.rb +94 -0
  182. data/lib/parsanol/expression.rb +51 -0
  183. data/lib/parsanol/fast_mode.rb +145 -0
  184. data/lib/parsanol/first_set.rb +75 -0
  185. data/lib/parsanol/grammar_builder.rb +177 -0
  186. data/lib/parsanol/graphviz.rb +97 -0
  187. data/lib/parsanol/incremental_parser.rb +179 -0
  188. data/lib/parsanol/interval_tree.rb +215 -0
  189. data/lib/parsanol/lazy_result.rb +178 -0
  190. data/lib/parsanol/lexer.rb +146 -0
  191. data/lib/parsanol/native/parser.rb +630 -0
  192. data/lib/parsanol/native/serializer.rb +245 -0
  193. data/lib/parsanol/native/transformer.rb +438 -0
  194. data/lib/parsanol/native/types.rb +41 -0
  195. data/lib/parsanol/native.rb +217 -0
  196. data/lib/parsanol/optimizer.rb +86 -0
  197. data/lib/parsanol/optimizers/choice_optimizer.rb +78 -0
  198. data/lib/parsanol/optimizers/cut_inserter.rb +175 -0
  199. data/lib/parsanol/optimizers/lookahead_optimizer.rb +58 -0
  200. data/lib/parsanol/optimizers/quantifier_optimizer.rb +62 -0
  201. data/lib/parsanol/optimizers/sequence_optimizer.rb +97 -0
  202. data/lib/parsanol/options/ruby_transform.rb +109 -0
  203. data/lib/parsanol/options/serialized.rb +94 -0
  204. data/lib/parsanol/options/zero_copy.rb +130 -0
  205. data/lib/parsanol/options.rb +20 -0
  206. data/lib/parsanol/parallel.rb +133 -0
  207. data/lib/parsanol/parsanol_native.bundle +0 -0
  208. data/lib/parsanol/parser.rb +151 -0
  209. data/lib/parsanol/parslet.rb +148 -0
  210. data/lib/parsanol/parslet_native.bundle +0 -0
  211. data/lib/parsanol/pattern/binding.rb +49 -0
  212. data/lib/parsanol/pattern.rb +115 -0
  213. data/lib/parsanol/pool.rb +220 -0
  214. data/lib/parsanol/pools/array_pool.rb +75 -0
  215. data/lib/parsanol/pools/buffer_pool.rb +173 -0
  216. data/lib/parsanol/pools/position_pool.rb +92 -0
  217. data/lib/parsanol/pools/slice_pool.rb +64 -0
  218. data/lib/parsanol/position.rb +89 -0
  219. data/lib/parsanol/result.rb +44 -0
  220. data/lib/parsanol/result_builder.rb +208 -0
  221. data/lib/parsanol/result_stream.rb +262 -0
  222. data/lib/parsanol/rig/rspec.rb +52 -0
  223. data/lib/parsanol/rope.rb +78 -0
  224. data/lib/parsanol/scope.rb +42 -0
  225. data/lib/parsanol/slice.rb +172 -0
  226. data/lib/parsanol/source/line_cache.rb +99 -0
  227. data/lib/parsanol/source.rb +171 -0
  228. data/lib/parsanol/source_location.rb +164 -0
  229. data/lib/parsanol/streaming_parser.rb +124 -0
  230. data/lib/parsanol/string_view.rb +192 -0
  231. data/lib/parsanol/transform.rb +267 -0
  232. data/lib/parsanol/version.rb +5 -0
  233. data/lib/parsanol/wasm/README.md +80 -0
  234. data/lib/parsanol/wasm/package.json +51 -0
  235. data/lib/parsanol/wasm/parsanol.js +252 -0
  236. data/lib/parsanol/wasm/parslet.d.ts +129 -0
  237. data/lib/parsanol/wasm_parser.rb +239 -0
  238. data/lib/parsanol.rb +408 -0
  239. data/parsanol-ruby.gemspec +56 -0
  240. data/spec/acceptance/examples_spec.rb +96 -0
  241. data/spec/acceptance/infix_parser_spec.rb +145 -0
  242. data/spec/acceptance/mixing_parsers_spec.rb +74 -0
  243. data/spec/acceptance/regression_spec.rb +329 -0
  244. data/spec/acceptance/repetition_and_maybe_spec.rb +44 -0
  245. data/spec/acceptance/unconsumed_input_spec.rb +21 -0
  246. data/spec/benchmark/comparative/runner_spec.rb +105 -0
  247. data/spec/integration/array_pooling_spec.rb +193 -0
  248. data/spec/integration/buffer_allocation_spec.rb +324 -0
  249. data/spec/integration/position_pooling_spec.rb +184 -0
  250. data/spec/integration/result_builder_spec.rb +282 -0
  251. data/spec/integration/rope_stringview_integration_spec.rb +188 -0
  252. data/spec/integration/slice_pooling_spec.rb +63 -0
  253. data/spec/integration/string_view_integration_spec.rb +125 -0
  254. data/spec/lexer_spec.rb +231 -0
  255. data/spec/parsanol/atom_results_spec.rb +39 -0
  256. data/spec/parsanol/atoms/alternative_spec.rb +26 -0
  257. data/spec/parsanol/atoms/base_spec.rb +127 -0
  258. data/spec/parsanol/atoms/capture_spec.rb +21 -0
  259. data/spec/parsanol/atoms/combinations_spec.rb +5 -0
  260. data/spec/parsanol/atoms/custom_spec.rb +79 -0
  261. data/spec/parsanol/atoms/dsl_spec.rb +7 -0
  262. data/spec/parsanol/atoms/entity_spec.rb +77 -0
  263. data/spec/parsanol/atoms/ignored_spec.rb +15 -0
  264. data/spec/parsanol/atoms/infix_spec.rb +5 -0
  265. data/spec/parsanol/atoms/lookahead_spec.rb +22 -0
  266. data/spec/parsanol/atoms/named_spec.rb +4 -0
  267. data/spec/parsanol/atoms/re_spec.rb +14 -0
  268. data/spec/parsanol/atoms/repetition_spec.rb +24 -0
  269. data/spec/parsanol/atoms/scope_spec.rb +26 -0
  270. data/spec/parsanol/atoms/sequence_spec.rb +28 -0
  271. data/spec/parsanol/atoms/str_spec.rb +15 -0
  272. data/spec/parsanol/atoms/visitor_spec.rb +101 -0
  273. data/spec/parsanol/atoms_spec.rb +488 -0
  274. data/spec/parsanol/auto_optimize_spec.rb +334 -0
  275. data/spec/parsanol/buffer_spec.rb +219 -0
  276. data/spec/parsanol/builder_callbacks_spec.rb +377 -0
  277. data/spec/parsanol/choice_optimizer_spec.rb +231 -0
  278. data/spec/parsanol/convenience_spec.rb +54 -0
  279. data/spec/parsanol/cut_inserter_spec.rb +248 -0
  280. data/spec/parsanol/cut_spec.rb +66 -0
  281. data/spec/parsanol/edit_tracker_spec.rb +218 -0
  282. data/spec/parsanol/error_reporter/contextual_spec.rb +122 -0
  283. data/spec/parsanol/error_reporter/deepest_spec.rb +82 -0
  284. data/spec/parsanol/error_reporter/tree_spec.rb +7 -0
  285. data/spec/parsanol/export_spec.rb +67 -0
  286. data/spec/parsanol/expression/treetop_spec.rb +75 -0
  287. data/spec/parsanol/first_set_spec.rb +298 -0
  288. data/spec/parsanol/interval_tree_spec.rb +205 -0
  289. data/spec/parsanol/lazy_result_spec.rb +288 -0
  290. data/spec/parsanol/lookahead_optimizer_spec.rb +252 -0
  291. data/spec/parsanol/minilisp.citrus +29 -0
  292. data/spec/parsanol/minilisp.tt +29 -0
  293. data/spec/parsanol/optimizer_spec.rb +459 -0
  294. data/spec/parsanol/options/parslet_compat_spec.rb +166 -0
  295. data/spec/parsanol/options/ruby_transform_spec.rb +70 -0
  296. data/spec/parsanol/options/serialized_spec.rb +69 -0
  297. data/spec/parsanol/options/zero_copy_spec.rb +230 -0
  298. data/spec/parsanol/parser_spec.rb +36 -0
  299. data/spec/parsanol/parslet_spec.rb +38 -0
  300. data/spec/parsanol/pattern_spec.rb +272 -0
  301. data/spec/parsanol/pool_spec.rb +392 -0
  302. data/spec/parsanol/pools/array_pool_spec.rb +356 -0
  303. data/spec/parsanol/pools/buffer_pool_spec.rb +365 -0
  304. data/spec/parsanol/pools/position_pool_spec.rb +118 -0
  305. data/spec/parsanol/pools/slice_pool_spec.rb +262 -0
  306. data/spec/parsanol/position_spec.rb +14 -0
  307. data/spec/parsanol/result_builder_spec.rb +391 -0
  308. data/spec/parsanol/rig/rspec_spec.rb +54 -0
  309. data/spec/parsanol/rope_spec.rb +207 -0
  310. data/spec/parsanol/scope_spec.rb +45 -0
  311. data/spec/parsanol/slice_spec.rb +249 -0
  312. data/spec/parsanol/source/line_cache_spec.rb +74 -0
  313. data/spec/parsanol/source_spec.rb +207 -0
  314. data/spec/parsanol/string_view_spec.rb +345 -0
  315. data/spec/parsanol/transform/context_spec.rb +56 -0
  316. data/spec/parsanol/transform_spec.rb +183 -0
  317. data/spec/parsanol/tree_memoization_spec.rb +149 -0
  318. data/spec/parslet_compatibility/expressir_edge_cases_spec.rb +153 -0
  319. data/spec/parslet_compatibility/minimal_reproduction.rb +199 -0
  320. data/spec/parslet_compatibility_spec.rb +399 -0
  321. data/spec/parslet_imported/atom_spec.rb +93 -0
  322. data/spec/parslet_imported/combinator_spec.rb +161 -0
  323. data/spec/parslet_imported/spec_helper.rb +73 -0
  324. data/spec/performance/batch_parsing_benchmark.rb +129 -0
  325. data/spec/performance/complete_optimization_summary.rb +143 -0
  326. data/spec/performance/grammar_caching_analysis.rb +121 -0
  327. data/spec/performance/grammar_caching_benchmark.rb +80 -0
  328. data/spec/performance/native_benchmark_spec.rb +230 -0
  329. data/spec/performance/phase5_benchmark.rb +144 -0
  330. data/spec/performance/profiling_benchmark.rb +131 -0
  331. data/spec/performance/ruby_improvements_benchmark.rb +171 -0
  332. data/spec/performance_spec.rb +374 -0
  333. data/spec/spec_helper.rb +79 -0
  334. data/spec/support/opal.rb +8 -0
  335. data/spec/support/opal.rb.erb +14 -0
  336. metadata +485 -0
@@ -0,0 +1,149 @@
1
+ # ISO 8601 Date/Time Parser - Ruby Implementation
2
+
3
+ ## How to Run
4
+
5
+ ```bash
6
+ cd parsanol-ruby/example/iso-8601
7
+ ruby basic.rb
8
+ ```
9
+
10
+ ## Code Walkthrough
11
+
12
+ ### Calendar Date Rule
13
+
14
+ Standard YYYY-MM-DD format with optional separators:
15
+
16
+ ```ruby
17
+ rule(:calendar_date) {
18
+ year >> date_separator >> month >> date_separator >> day
19
+ }
20
+
21
+ rule(:date_separator) { str('-').maybe }
22
+ ```
23
+
24
+ The `.maybe` on the separator allows both `2024-01-15` and `20240115`.
25
+
26
+ ### Week Date Rule
27
+
28
+ Year, week number, and weekday:
29
+
30
+ ```ruby
31
+ rule(:week_date) {
32
+ year >> str('-W') >>
33
+ match('[0-9]').repeat(2, 2).as(:week) >>
34
+ str('-') >>
35
+ match('[1-7]').as(:weekday)
36
+ }
37
+ ```
38
+
39
+ Weekday is 1-7 (Monday to Sunday), per ISO 8601.
40
+
41
+ ### Ordinal Date Rule
42
+
43
+ Year and day-of-year:
44
+
45
+ ```ruby
46
+ rule(:ordinal_date) {
47
+ year >> str('-') >>
48
+ match('[0-9]').repeat(3, 3).as(:ordinal_day)
49
+ }
50
+ ```
51
+
52
+ Three-digit day number (001-366).
53
+
54
+ ### Time Rule
55
+
56
+ Hours, minutes, seconds with optional fraction:
57
+
58
+ ```ruby
59
+ rule(:time_basic) {
60
+ hour >> time_separator >> minute >> time_separator >> second >> fraction.maybe
61
+ }
62
+
63
+ rule(:fraction) { str('.') >> match('[0-9]').repeat(1).as(:fraction) }
64
+ ```
65
+
66
+ Fraction supports arbitrary precision subseconds.
67
+
68
+ ### Timezone Rules
69
+
70
+ UTC designator or offset:
71
+
72
+ ```ruby
73
+ rule(:utc_designator) { str('Z').as(:utc) }
74
+
75
+ rule(:tz_offset) {
76
+ tz_sign >> tz_hour >> tz_minute
77
+ }
78
+
79
+ rule(:timezone) { utc_designator | tz_offset | str('') }
80
+ ```
81
+
82
+ Empty string allows local time without timezone.
83
+
84
+ ### DateTime Combination
85
+
86
+ Date and time joined by `T`:
87
+
88
+ ```ruby
89
+ rule(:datetime) {
90
+ (calendar_date | week_date | ordinal_date) >>
91
+ (str('T') | str(' ')) >>
92
+ time
93
+ }
94
+ ```
95
+
96
+ Space separator is allowed by some implementations.
97
+
98
+ ### Duration Rule
99
+
100
+ P[nY][nM][nD][T[nH][nM][nS]] format:
101
+
102
+ ```ruby
103
+ rule(:duration) {
104
+ str('P') >>
105
+ (
106
+ (match('[0-9]').repeat(1).as(:years) >> str('Y')).maybe >>
107
+ (match('[0-9]').repeat(1).as(:months) >> str('M')).maybe >>
108
+ (match('[0-9]').repeat(1).as(:days) >> str('D')).maybe >>
109
+ (str('T') >> (...)).maybe
110
+ )
111
+ }
112
+ ```
113
+
114
+ Every component is optional; at least one is required.
115
+
116
+ ## Output Types
117
+
118
+ ```ruby
119
+ # Calendar date
120
+ IsoDate.new("2024", "01", "15", nil, nil, nil)
121
+ # to_s => "2024-01-15"
122
+
123
+ # Time with timezone
124
+ IsoTime.new("10", "30", "00", nil, "Z", nil, nil)
125
+ # to_s => "10:30:00Z"
126
+
127
+ # Duration
128
+ IsoDuration.new("1", "2", "3", "4", "5", "6")
129
+ # to_s => "P1Y2M3DT4H5M6S"
130
+ # to_seconds => 36993906
131
+ ```
132
+
133
+ ## Design Decisions
134
+
135
+ ### Why Separate Date/Time/Duration Classes?
136
+
137
+ Each ISO 8601 type has distinct fields and semantics. Separate classes provide type safety and appropriate methods.
138
+
139
+ ### Why Maybe on Separators?
140
+
141
+ ISO 8601 allows both basic (no separators) and extended (with separators) formats. The grammar handles both.
142
+
143
+ ### Why Empty String in Timezone Alternative?
144
+
145
+ Local time without timezone is valid ISO 8601. The empty string matches when neither Z nor offset is present.
146
+
147
+ ### Why to_seconds for Duration?
148
+
149
+ Duration calculations often need total seconds. The method approximates using average month/year lengths.
@@ -0,0 +1,128 @@
1
+ $:.unshift File.dirname(__FILE__) + "/../lib"
2
+
3
+ #
4
+ # MIT License - (c) 2011 John Mettraux
5
+ #
6
+
7
+ require 'rubygems'
8
+ require 'parsanol/parslet' # gem install parslet
9
+
10
+
11
+ module MyJson
12
+
13
+ class Parser < Parsanol::Parser
14
+
15
+ rule(:spaces) { match('\s').repeat(1) }
16
+ rule(:spaces?) { spaces.maybe }
17
+
18
+ rule(:comma) { spaces? >> str(',') >> spaces? }
19
+ rule(:digit) { match('[0-9]') }
20
+
21
+ rule(:number) {
22
+ (
23
+ str('-').maybe >> (
24
+ str('0') | (match('[1-9]') >> digit.repeat)
25
+ ) >> (
26
+ str('.') >> digit.repeat(1)
27
+ ).maybe >> (
28
+ match('[eE]') >> (str('+') | str('-')).maybe >> digit.repeat(1)
29
+ ).maybe
30
+ ).as(:number)
31
+ }
32
+
33
+ rule(:string) {
34
+ str('"') >> (
35
+ str('\\') >> any | str('"').absent? >> any
36
+ ).repeat.as(:string) >> str('"')
37
+ }
38
+
39
+ rule(:array) {
40
+ str('[') >> spaces? >>
41
+ (value >> (comma >> value).repeat).maybe.as(:array) >>
42
+ spaces? >> str(']')
43
+ }
44
+
45
+ rule(:object) {
46
+ str('{') >> spaces? >>
47
+ (entry >> (comma >> entry).repeat).maybe.as(:object) >>
48
+ spaces? >> str('}')
49
+ }
50
+
51
+ rule(:value) {
52
+ string | number |
53
+ object | array |
54
+ str('true').as(:true) | str('false').as(:false) |
55
+ str('null').as(:null)
56
+ }
57
+
58
+ rule(:entry) {
59
+ (
60
+ string.as(:key) >> spaces? >>
61
+ str(':') >> spaces? >>
62
+ value.as(:val)
63
+ ).as(:entry)
64
+ }
65
+
66
+ rule(:attribute) { (entry | value).as(:attribute) }
67
+
68
+ rule(:top) { spaces? >> value >> spaces? }
69
+
70
+ root(:top)
71
+ end
72
+
73
+ class Transformer < Parsanol::Transform
74
+
75
+ class Entry < Struct.new(:key, :val); end
76
+
77
+ rule(:array => subtree(:ar)) {
78
+ ar.is_a?(Array) ? ar : [ ar ]
79
+ }
80
+ rule(:object => subtree(:ob)) {
81
+ (ob.is_a?(Array) ? ob : [ ob ]).inject({}) { |h, e| h[e.key] = e.val; h }
82
+ }
83
+
84
+ rule(:entry => { :key => simple(:ke), :val => simple(:va) }) {
85
+ Entry.new(ke, va)
86
+ }
87
+
88
+ rule(:string => simple(:st)) {
89
+ st.to_s
90
+ }
91
+ rule(:number => simple(:nb)) {
92
+ nb.match(/[eE\.]/) ? Float(nb) : Integer(nb)
93
+ }
94
+
95
+ rule(:null => simple(:nu)) { nil }
96
+ rule(:true => simple(:tr)) { true }
97
+ rule(:false => simple(:fa)) { false }
98
+ end
99
+
100
+ def self.parse(s)
101
+
102
+ parser = Parser.new
103
+ transformer = Transformer.new
104
+
105
+ tree = parser.parse(s)
106
+ puts; p tree; puts
107
+ out = transformer.apply(tree)
108
+
109
+ out
110
+ end
111
+ end
112
+
113
+
114
+ s = %{
115
+ [ 1, 2, 3, null,
116
+ "asdfasdf asdfds", { "a": -1.2 }, { "b": true, "c": false },
117
+ 0.1e24, true, false, [ 1 ] ]
118
+ }
119
+
120
+ out = MyJson.parse(s)
121
+
122
+ p out; puts
123
+
124
+ out == [
125
+ 1, 2, 3, nil,
126
+ "asdfasdf asdfds", { "a" => -1.2 }, { "b" => true, "c" => false },
127
+ 0.1e24, true, false, [ 1 ]
128
+ ] || raise("MyJson is a failure")
@@ -0,0 +1,121 @@
1
+ # JSON Parser - Ruby Implementation
2
+
3
+ ## How to Run
4
+
5
+ ```bash
6
+ cd parsanol-ruby/example/json
7
+ ruby basic.rb
8
+ ```
9
+
10
+ ## Code Walkthrough
11
+
12
+ ### Number Rule Definition
13
+
14
+ Numbers in JSON can be integers, floats, or scientific notation:
15
+
16
+ ```ruby
17
+ rule(:number) {
18
+ (
19
+ str('-').maybe >> (
20
+ str('0') | (match('[1-9]') >> digit.repeat)
21
+ ) >> (
22
+ str('.') >> digit.repeat(1)
23
+ ).maybe >> (
24
+ match('[eE]') >> (str('+') | str('-')).maybe >> digit.repeat(1)
25
+ ).maybe
26
+ ).as(:number)
27
+ }
28
+ ```
29
+
30
+ The rule handles negative numbers, decimal points, and exponent notation like `1e24`.
31
+
32
+ ### String Rule Definition
33
+
34
+ Strings support escape sequences with backslash:
35
+
36
+ ```ruby
37
+ rule(:string) {
38
+ str('"') >> (
39
+ str('\\') >> any | str('"').absent? >> any
40
+ ).repeat.as(:string) >> str('"')
41
+ }
42
+ ```
43
+
44
+ The pattern `str('\\') >> any` matches any escaped character; `str('"').absent? >> any` matches non-quote characters.
45
+
46
+ ### Array and Object Rules
47
+
48
+ Arrays and objects use recursive definitions:
49
+
50
+ ```ruby
51
+ rule(:array) {
52
+ str('[') >> spaces? >>
53
+ (value >> (comma >> value).repeat).maybe.as(:array) >>
54
+ spaces? >> str(']')
55
+ }
56
+
57
+ rule(:object) {
58
+ str('{') >> spaces? >>
59
+ (entry >> (comma >> entry).repeat).maybe.as(:object) >>
60
+ spaces? >> str('}')
61
+ }
62
+ ```
63
+
64
+ Both support nested structures through the recursive `value` rule.
65
+
66
+ ### Value Rule
67
+
68
+ The value rule combines all JSON types:
69
+
70
+ ```ruby
71
+ rule(:value) {
72
+ string | number |
73
+ object | array |
74
+ str('true').as(:true) | str('false').as(:false) |
75
+ str('null').as(:null)
76
+ }
77
+ ```
78
+
79
+ Order matters: try specific types before generic ones.
80
+
81
+ ### Transform Rules
82
+
83
+ The transformer converts parse tree to Ruby objects:
84
+
85
+ ```ruby
86
+ rule(:string => simple(:st)) {
87
+ st.to_s
88
+ }
89
+ rule(:number => simple(:nb)) {
90
+ nb.match(/[eE\.]/) ? Float(nb) : Integer(nb)
91
+ }
92
+ rule(:null => simple(:nu)) { nil }
93
+ rule(:true => simple(:tr)) { true }
94
+ rule(:false => simple(:fa)) { false }
95
+ ```
96
+
97
+ Pattern matching on tree labels produces typed Ruby values.
98
+
99
+ ## Output Types
100
+
101
+ ```ruby
102
+ # JSON values become native Ruby types:
103
+ String # "hello" -> "hello"
104
+ Integer # 42 -> 42
105
+ Float # 3.14 -> 3.14
106
+ TrueClass # true -> true
107
+ FalseClass # false -> false
108
+ NilClass # null -> nil
109
+ Array # [1,2,3] -> [1,2,3]
110
+ Hash # {"a":1} -> {"a"=>1}
111
+ ```
112
+
113
+ ## Design Decisions
114
+
115
+ ### Why Separate Parser and Transformer?
116
+
117
+ Parslet's two-stage model separates syntax (parsing) from semantics (transformation). This makes grammars reusable and transformations customizable.
118
+
119
+ ### Why Pattern Matching in Transform?
120
+
121
+ Pattern matching on tree structure is more maintainable than manual tree traversal. Each rule handles one case, making bugs easier to isolate.
@@ -0,0 +1,128 @@
1
+ $:.unshift File.dirname(__FILE__) + "/../lib"
2
+
3
+ #
4
+ # MIT License - (c) 2011 John Mettraux
5
+ #
6
+
7
+ require 'rubygems'
8
+ require 'parsanol/parslet' # gem install parslet
9
+
10
+
11
+ module MyJson
12
+
13
+ class Parser < Parsanol::Parser
14
+
15
+ rule(:spaces) { match('\s').repeat(1) }
16
+ rule(:spaces?) { spaces.maybe }
17
+
18
+ rule(:comma) { spaces? >> str(',') >> spaces? }
19
+ rule(:digit) { match('[0-9]') }
20
+
21
+ rule(:number) {
22
+ (
23
+ str('-').maybe >> (
24
+ str('0') | (match('[1-9]') >> digit.repeat)
25
+ ) >> (
26
+ str('.') >> digit.repeat(1)
27
+ ).maybe >> (
28
+ match('[eE]') >> (str('+') | str('-')).maybe >> digit.repeat(1)
29
+ ).maybe
30
+ ).as(:number)
31
+ }
32
+
33
+ rule(:string) {
34
+ str('"') >> (
35
+ str('\\') >> any | str('"').absent? >> any
36
+ ).repeat.as(:string) >> str('"')
37
+ }
38
+
39
+ rule(:array) {
40
+ str('[') >> spaces? >>
41
+ (value >> (comma >> value).repeat).maybe.as(:array) >>
42
+ spaces? >> str(']')
43
+ }
44
+
45
+ rule(:object) {
46
+ str('{') >> spaces? >>
47
+ (entry >> (comma >> entry).repeat).maybe.as(:object) >>
48
+ spaces? >> str('}')
49
+ }
50
+
51
+ rule(:value) {
52
+ string | number |
53
+ object | array |
54
+ str('true').as(:true) | str('false').as(:false) |
55
+ str('null').as(:null)
56
+ }
57
+
58
+ rule(:entry) {
59
+ (
60
+ string.as(:key) >> spaces? >>
61
+ str(':') >> spaces? >>
62
+ value.as(:val)
63
+ ).as(:entry)
64
+ }
65
+
66
+ rule(:attribute) { (entry | value).as(:attribute) }
67
+
68
+ rule(:top) { spaces? >> value >> spaces? }
69
+
70
+ root(:top)
71
+ end
72
+
73
+ class Transformer < Parsanol::Transform
74
+
75
+ class Entry < Struct.new(:key, :val); end
76
+
77
+ rule(:array => subtree(:ar)) {
78
+ ar.is_a?(Array) ? ar : [ ar ]
79
+ }
80
+ rule(:object => subtree(:ob)) {
81
+ (ob.is_a?(Array) ? ob : [ ob ]).inject({}) { |h, e| h[e.key] = e.val; h }
82
+ }
83
+
84
+ rule(:entry => { :key => simple(:ke), :val => simple(:va) }) {
85
+ Entry.new(ke, va)
86
+ }
87
+
88
+ rule(:string => simple(:st)) {
89
+ st.to_s
90
+ }
91
+ rule(:number => simple(:nb)) {
92
+ nb.match(/[eE\.]/) ? Float(nb) : Integer(nb)
93
+ }
94
+
95
+ rule(:null => simple(:nu)) { nil }
96
+ rule(:true => simple(:tr)) { true }
97
+ rule(:false => simple(:fa)) { false }
98
+ end
99
+
100
+ def self.parse(s)
101
+
102
+ parser = Parser.new
103
+ transformer = Transformer.new
104
+
105
+ tree = parser.parse(s)
106
+ puts; p tree; puts
107
+ out = transformer.apply(tree)
108
+
109
+ out
110
+ end
111
+ end
112
+
113
+
114
+ s = %{
115
+ [ 1, 2, 3, null,
116
+ "asdfasdf asdfds", { "a": -1.2 }, { "b": true, "c": false },
117
+ 0.1e24, true, false, [ 1 ] ]
118
+ }
119
+
120
+ out = MyJson.parse(s)
121
+
122
+ p out; puts
123
+
124
+ out == [
125
+ 1, 2, 3, nil,
126
+ "asdfasdf asdfds", { "a" => -1.2 }, { "b" => true, "c" => false },
127
+ 0.1e24, true, false, [ 1 ]
128
+ ] || raise("MyJson is a failure")