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,219 @@
1
+ # URL Parser Example - RubyTransform: Ruby Transform
2
+ #
3
+ # This example demonstrates parsing URLs into their components.
4
+ # Shows protocol, host, port, path, query string, and fragment parsing.
5
+ #
6
+ # Run with: ruby -Ilib example/url_ruby_transform.rb
7
+
8
+ $:.unshift File.dirname(__FILE__) + "/../lib"
9
+
10
+ require 'parsanol'
11
+
12
+ # Step 1: Define the URL grammar
13
+ class UrlParser < Parsanol::Parser
14
+ root :url
15
+
16
+ rule(:url) {
17
+ protocol.as(:protocol) >>
18
+ str('://') >>
19
+ host.as(:host) >>
20
+ port.maybe.as(:port) >>
21
+ path.maybe.as(:path) >>
22
+ query.maybe.as(:query) >>
23
+ fragment.maybe.as(:fragment)
24
+ }
25
+
26
+ rule(:protocol) { (str('http') | str('https') | str('ftp') | str('ws') | str('wss')) }
27
+
28
+ rule(:host) {
29
+ (domain | ip_address).as(:address)
30
+ }
31
+
32
+ rule(:domain) {
33
+ label >> (str('.') >> label).repeat
34
+ }
35
+
36
+ rule(:label) {
37
+ match('[a-zA-Z0-9]').repeat(1)
38
+ }
39
+
40
+ rule(:ip_address) {
41
+ octet >> str('.') >> octet >> str('.') >> octet >> str('.') >> octet
42
+ }
43
+
44
+ rule(:octet) {
45
+ match('[0-9]').repeat(1, 3)
46
+ }
47
+
48
+ rule(:port) {
49
+ str(':') >> match('[0-9]').repeat(1).as(:number)
50
+ }
51
+
52
+ rule(:path) {
53
+ str('/') >> path_segment.repeat(1).as(:segments)
54
+ }
55
+
56
+ rule(:path_segment) {
57
+ (match('[^/?#]').repeat(1) >> str('/').maybe)
58
+ }
59
+
60
+ rule(:query) {
61
+ str('?') >> query_string.as(:string)
62
+ }
63
+
64
+ rule(:query_string) {
65
+ match('[^#]').repeat
66
+ }
67
+
68
+ rule(:fragment) {
69
+ str('#') >> match('.').repeat.as(:value)
70
+ }
71
+ end
72
+
73
+ # Step 2: URL class
74
+ class ParsedURL
75
+ attr_reader :protocol, :host, :port, :path, :query, :fragment
76
+
77
+ def initialize(protocol:, host:, port: nil, path: nil, query: nil, fragment: nil)
78
+ @protocol = protocol
79
+ @host = host
80
+ @port = port
81
+ @path = path
82
+ @query = query
83
+ @fragment = fragment
84
+ end
85
+
86
+ def to_s
87
+ url = "#{@protocol}://#{@host}"
88
+ url += ":#{@port}" if @port
89
+ url += @path if @path
90
+ url += "?#{@query}" if @query
91
+ url += "##{@fragment}" if @fragment
92
+ url
93
+ end
94
+
95
+ def path_segments
96
+ @path ? @path.split('/').reject(&:empty?) : []
97
+ end
98
+
99
+ def query_params
100
+ return {} unless @query
101
+ @query.split('&').each_with_object({}) do |pair, hash|
102
+ key, value = pair.split('=', 2)
103
+ hash[key] = value || ''
104
+ end
105
+ end
106
+ end
107
+
108
+ # Step 3: Transform
109
+ class UrlTransform < Parsanol::Transform
110
+ rule(
111
+ protocol: simple(:p),
112
+ host: simple(:h),
113
+ port: simple(:port),
114
+ path: simple(:path),
115
+ query: simple(:q),
116
+ fragment: simple(:f)
117
+ ) {
118
+ port_num = port&.dig(:number)&.to_i
119
+ ParsedURL.new(
120
+ protocol: p.to_s,
121
+ host: h[:address].to_s,
122
+ port: port_num,
123
+ path: path&.dig(:segments)&.to_s,
124
+ query: q&.dig(:string)&.to_s,
125
+ fragment: f.to_s
126
+ )
127
+ }
128
+
129
+ rule(
130
+ protocol: simple(:p),
131
+ host: simple(:h),
132
+ port: simple(:port),
133
+ path: simple(:path),
134
+ query: simple(:q)
135
+ ) {
136
+ port_num = port&.dig(:number)&.to_i
137
+ ParsedURL.new(
138
+ protocol: p.to_s,
139
+ host: h[:address].to_s,
140
+ port: port_num,
141
+ path: path&.dig(:segments)&.to_s,
142
+ query: q&.dig(:string)&.to_s
143
+ )
144
+ }
145
+
146
+ rule(
147
+ protocol: simple(:p),
148
+ host: simple(:h),
149
+ port: simple(:port),
150
+ path: simple(:path)
151
+ ) {
152
+ port_num = port&.dig(:number)&.to_i
153
+ ParsedURL.new(
154
+ protocol: p.to_s,
155
+ host: h[:address].to_s,
156
+ port: port_num,
157
+ path: path&.dig(:segments)&.to_s
158
+ )
159
+ }
160
+
161
+ rule(protocol: simple(:p), host: simple(:h), port: simple(:port)) {
162
+ ParsedURL.new(
163
+ protocol: p.to_s,
164
+ host: h[:address].to_s,
165
+ port: port&.dig(:number)&.to_i
166
+ )
167
+ }
168
+
169
+ rule(protocol: simple(:p), host: simple(:h)) {
170
+ ParsedURL.new(protocol: p.to_s, host: h[:address].to_s)
171
+ }
172
+ end
173
+
174
+ def parse_url(input)
175
+ parser = UrlParser.new
176
+ tree = parser.parse(input)
177
+
178
+ transform = UrlTransform.new
179
+ url = transform.apply(tree)
180
+
181
+ url
182
+ rescue Parsanol::ParseFailed => e
183
+ puts "Parse failed: #{e.message}"
184
+ nil
185
+ end
186
+
187
+ if __FILE__ == $0
188
+ puts "=" * 60
189
+ puts "URL Parser - RubyTransform"
190
+ puts "=" * 60
191
+ puts
192
+
193
+ test_urls = [
194
+ "http://example.com",
195
+ "https://example.com:8080",
196
+ "https://example.com/path/to/resource",
197
+ "https://example.com/search?q=ruby&limit=10",
198
+ "https://example.com/page#section",
199
+ "https://api.example.com:443/v1/users?id=123#results",
200
+ "http://192.168.1.1:3000/admin",
201
+ "ws://websocket.example.com/socket",
202
+ ]
203
+
204
+ test_urls.each do |url_str|
205
+ puts "-" * 40
206
+ puts "Input: #{url_str}"
207
+ url = parse_url(url_str)
208
+ if url
209
+ puts " Protocol: #{url.protocol}"
210
+ puts " Host: #{url.host}"
211
+ puts " Port: #{url.port || '(default)'}"
212
+ puts " Path: #{url.path || '/'}"
213
+ puts " Query: #{url.query || '(none)'}"
214
+ puts " Fragment: #{url.fragment || '(none)'}"
215
+ puts " Reconstructed: #{url.to_s}"
216
+ end
217
+ puts
218
+ end
219
+ end
@@ -0,0 +1,142 @@
1
+ # URL Parser - Ruby Implementation
2
+
3
+ ## How to Run
4
+
5
+ ```bash
6
+ cd parsanol-ruby/example/url
7
+ ruby basic.rb
8
+ ```
9
+
10
+ ## Code Walkthrough
11
+
12
+ ### URL Structure Rule
13
+
14
+ URLs have ordered components:
15
+
16
+ ```ruby
17
+ rule(:url) {
18
+ protocol.as(:protocol) >>
19
+ str('://') >>
20
+ host.as(:host) >>
21
+ port.maybe.as(:port) >>
22
+ path.maybe.as(:path) >>
23
+ query.maybe.as(:query) >>
24
+ fragment.maybe.as(:fragment)
25
+ }
26
+ ```
27
+
28
+ Each component is optional except protocol and host.
29
+
30
+ ### Protocol Rule
31
+
32
+ Common protocols are supported:
33
+
34
+ ```ruby
35
+ rule(:protocol) {
36
+ (str('http') | str('https') | str('ftp') | str('ws') | str('wss'))
37
+ }
38
+ ```
39
+
40
+ HTTP, FTP, and WebSocket protocols are recognized.
41
+
42
+ ### Host Rule
43
+
44
+ Hosts can be domains or IP addresses:
45
+
46
+ ```ruby
47
+ rule(:host) {
48
+ (domain | ip_address).as(:address)
49
+ }
50
+
51
+ rule(:domain) {
52
+ label >> (str('.') >> label).repeat
53
+ }
54
+
55
+ rule(:ip_address) {
56
+ octet >> str('.') >> octet >> str('.') >> octet >> str('.') >> octet
57
+ }
58
+ ```
59
+
60
+ Alternation allows either format.
61
+
62
+ ### Path and Query Rules
63
+
64
+ Path segments and query strings:
65
+
66
+ ```ruby
67
+ rule(:path) {
68
+ str('/') >> path_segment.repeat(1).as(:segments)
69
+ }
70
+
71
+ rule(:query) {
72
+ str('?') >> query_string.as(:string)
73
+ }
74
+
75
+ rule(:fragment) {
76
+ str('#') >> match('.').repeat.as(:value)
77
+ }
78
+ ```
79
+
80
+ Markers (`/`, `?`, `#`) identify each component.
81
+
82
+ ### ParsedURL Helper Class
83
+
84
+ A class provides convenient access:
85
+
86
+ ```ruby
87
+ class ParsedURL
88
+ attr_reader :protocol, :host, :port, :path, :query, :fragment
89
+
90
+ def path_segments
91
+ @path ? @path.split('/').reject(&:empty?) : []
92
+ end
93
+
94
+ def query_params
95
+ return {} unless @query
96
+ @query.split('&').each_with_object({}) do |pair, hash|
97
+ key, value = pair.split('=', 2)
98
+ hash[key] = value || ''
99
+ end
100
+ end
101
+ end
102
+ ```
103
+
104
+ Helper methods parse query strings and paths.
105
+
106
+ ### Transform Rules
107
+
108
+ Multiple transform rules handle optional components:
109
+
110
+ ```ruby
111
+ class UrlTransform < Parsanol::Transform
112
+ rule(protocol: simple(:p), host: simple(:h), port: simple(:port), ...) { ... }
113
+ rule(protocol: simple(:p), host: simple(:h), port: simple(:port)) { ... }
114
+ rule(protocol: simple(:p), host: simple(:h)) { ... }
115
+ end
116
+ ```
117
+
118
+ Each rule matches a specific combination of present components.
119
+
120
+ ## Output Types
121
+
122
+ ```ruby
123
+ # Parse tree for "https://example.com:8080/path?q=1#anchor":
124
+ {:protocol=>"https", :host=>{:address=>"example.com"}, :port=>{:number=>"8080"}, ...}
125
+
126
+ # After transform:
127
+ #<ParsedURL @protocol="https", @host="example.com", @port=8080, @path="/path", @query="q=1", @fragment="anchor">
128
+ ```
129
+
130
+ ## Design Decisions
131
+
132
+ ### Why Multiple Transform Rules?
133
+
134
+ Different URLs have different components. Multiple rules handle all valid combinations.
135
+
136
+ ### Why Helper Class Instead of Hash?
137
+
138
+ `ParsedURL` provides type-safe access and utility methods like `query_params`.
139
+
140
+ ### Why Maybe for Optional Components?
141
+
142
+ `.maybe` returns nil when absent, simplifying pattern matching in transforms.
@@ -0,0 +1,219 @@
1
+ # URL Parser Example - RubyTransform: Ruby Transform
2
+ #
3
+ # This example demonstrates parsing URLs into their components.
4
+ # Shows protocol, host, port, path, query string, and fragment parsing.
5
+ #
6
+ # Run with: ruby -Ilib example/url_ruby_transform.rb
7
+
8
+ $:.unshift File.dirname(__FILE__) + "/../lib"
9
+
10
+ require 'parsanol'
11
+
12
+ # Step 1: Define the URL grammar
13
+ class UrlParser < Parsanol::Parser
14
+ root :url
15
+
16
+ rule(:url) {
17
+ protocol.as(:protocol) >>
18
+ str('://') >>
19
+ host.as(:host) >>
20
+ port.maybe.as(:port) >>
21
+ path.maybe.as(:path) >>
22
+ query.maybe.as(:query) >>
23
+ fragment.maybe.as(:fragment)
24
+ }
25
+
26
+ rule(:protocol) { (str('http') | str('https') | str('ftp') | str('ws') | str('wss')) }
27
+
28
+ rule(:host) {
29
+ (domain | ip_address).as(:address)
30
+ }
31
+
32
+ rule(:domain) {
33
+ label >> (str('.') >> label).repeat
34
+ }
35
+
36
+ rule(:label) {
37
+ match('[a-zA-Z0-9]').repeat(1)
38
+ }
39
+
40
+ rule(:ip_address) {
41
+ octet >> str('.') >> octet >> str('.') >> octet >> str('.') >> octet
42
+ }
43
+
44
+ rule(:octet) {
45
+ match('[0-9]').repeat(1, 3)
46
+ }
47
+
48
+ rule(:port) {
49
+ str(':') >> match('[0-9]').repeat(1).as(:number)
50
+ }
51
+
52
+ rule(:path) {
53
+ str('/') >> path_segment.repeat(1).as(:segments)
54
+ }
55
+
56
+ rule(:path_segment) {
57
+ (match('[^/?#]').repeat(1) >> str('/').maybe)
58
+ }
59
+
60
+ rule(:query) {
61
+ str('?') >> query_string.as(:string)
62
+ }
63
+
64
+ rule(:query_string) {
65
+ match('[^#]').repeat
66
+ }
67
+
68
+ rule(:fragment) {
69
+ str('#') >> match('.').repeat.as(:value)
70
+ }
71
+ end
72
+
73
+ # Step 2: URL class
74
+ class ParsedURL
75
+ attr_reader :protocol, :host, :port, :path, :query, :fragment
76
+
77
+ def initialize(protocol:, host:, port: nil, path: nil, query: nil, fragment: nil)
78
+ @protocol = protocol
79
+ @host = host
80
+ @port = port
81
+ @path = path
82
+ @query = query
83
+ @fragment = fragment
84
+ end
85
+
86
+ def to_s
87
+ url = "#{@protocol}://#{@host}"
88
+ url += ":#{@port}" if @port
89
+ url += @path if @path
90
+ url += "?#{@query}" if @query
91
+ url += "##{@fragment}" if @fragment
92
+ url
93
+ end
94
+
95
+ def path_segments
96
+ @path ? @path.split('/').reject(&:empty?) : []
97
+ end
98
+
99
+ def query_params
100
+ return {} unless @query
101
+ @query.split('&').each_with_object({}) do |pair, hash|
102
+ key, value = pair.split('=', 2)
103
+ hash[key] = value || ''
104
+ end
105
+ end
106
+ end
107
+
108
+ # Step 3: Transform
109
+ class UrlTransform < Parsanol::Transform
110
+ rule(
111
+ protocol: simple(:p),
112
+ host: simple(:h),
113
+ port: simple(:port),
114
+ path: simple(:path),
115
+ query: simple(:q),
116
+ fragment: simple(:f)
117
+ ) {
118
+ port_num = port&.dig(:number)&.to_i
119
+ ParsedURL.new(
120
+ protocol: p.to_s,
121
+ host: h[:address].to_s,
122
+ port: port_num,
123
+ path: path&.dig(:segments)&.to_s,
124
+ query: q&.dig(:string)&.to_s,
125
+ fragment: f.to_s
126
+ )
127
+ }
128
+
129
+ rule(
130
+ protocol: simple(:p),
131
+ host: simple(:h),
132
+ port: simple(:port),
133
+ path: simple(:path),
134
+ query: simple(:q)
135
+ ) {
136
+ port_num = port&.dig(:number)&.to_i
137
+ ParsedURL.new(
138
+ protocol: p.to_s,
139
+ host: h[:address].to_s,
140
+ port: port_num,
141
+ path: path&.dig(:segments)&.to_s,
142
+ query: q&.dig(:string)&.to_s
143
+ )
144
+ }
145
+
146
+ rule(
147
+ protocol: simple(:p),
148
+ host: simple(:h),
149
+ port: simple(:port),
150
+ path: simple(:path)
151
+ ) {
152
+ port_num = port&.dig(:number)&.to_i
153
+ ParsedURL.new(
154
+ protocol: p.to_s,
155
+ host: h[:address].to_s,
156
+ port: port_num,
157
+ path: path&.dig(:segments)&.to_s
158
+ )
159
+ }
160
+
161
+ rule(protocol: simple(:p), host: simple(:h), port: simple(:port)) {
162
+ ParsedURL.new(
163
+ protocol: p.to_s,
164
+ host: h[:address].to_s,
165
+ port: port&.dig(:number)&.to_i
166
+ )
167
+ }
168
+
169
+ rule(protocol: simple(:p), host: simple(:h)) {
170
+ ParsedURL.new(protocol: p.to_s, host: h[:address].to_s)
171
+ }
172
+ end
173
+
174
+ def parse_url(input)
175
+ parser = UrlParser.new
176
+ tree = parser.parse(input)
177
+
178
+ transform = UrlTransform.new
179
+ url = transform.apply(tree)
180
+
181
+ url
182
+ rescue Parsanol::ParseFailed => e
183
+ puts "Parse failed: #{e.message}"
184
+ nil
185
+ end
186
+
187
+ if __FILE__ == $0
188
+ puts "=" * 60
189
+ puts "URL Parser - RubyTransform"
190
+ puts "=" * 60
191
+ puts
192
+
193
+ test_urls = [
194
+ "http://example.com",
195
+ "https://example.com:8080",
196
+ "https://example.com/path/to/resource",
197
+ "https://example.com/search?q=ruby&limit=10",
198
+ "https://example.com/page#section",
199
+ "https://api.example.com:443/v1/users?id=123#results",
200
+ "http://192.168.1.1:3000/admin",
201
+ "ws://websocket.example.com/socket",
202
+ ]
203
+
204
+ test_urls.each do |url_str|
205
+ puts "-" * 40
206
+ puts "Input: #{url_str}"
207
+ url = parse_url(url_str)
208
+ if url
209
+ puts " Protocol: #{url.protocol}"
210
+ puts " Host: #{url.host}"
211
+ puts " Port: #{url.port || '(default)'}"
212
+ puts " Path: #{url.path || '/'}"
213
+ puts " Query: #{url.query || '(none)'}"
214
+ puts " Fragment: #{url.fragment || '(none)'}"
215
+ puts " Reconstructed: #{url.to_s}"
216
+ end
217
+ puts
218
+ end
219
+ end