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.
- checksums.yaml +7 -0
- data/HISTORY.txt +25 -0
- data/LICENSE +23 -0
- data/README.adoc +643 -0
- data/Rakefile +189 -0
- data/example/balanced-parens/basic.rb +42 -0
- data/example/balanced-parens/basic.rb.md +86 -0
- data/example/balanced-parens/parens.rb +42 -0
- data/example/balanced-parens/ruby_transform.rb +162 -0
- data/example/big.erb +73 -0
- data/example/boolean-algebra/basic.rb +70 -0
- data/example/boolean-algebra/basic.rb.md +108 -0
- data/example/boolean-algebra/ruby_transform.rb +263 -0
- data/example/calculator/basic.rb +153 -0
- data/example/calculator/basic.rb.md +120 -0
- data/example/calculator/pattern.rb +153 -0
- data/example/calculator/ruby_transform.rb +156 -0
- data/example/calculator/ruby_transform.rb.md +32 -0
- data/example/calculator/serialized.rb +257 -0
- data/example/calculator/serialized.rb.md +32 -0
- data/example/calculator/transform.rb +153 -0
- data/example/calculator/zero_copy.rb +269 -0
- data/example/calculator/zero_copy.rb.md +36 -0
- data/example/capture/basic.rb +49 -0
- data/example/capture/basic.rb.md +106 -0
- data/example/capture/example.json +39 -0
- data/example/comments/basic.rb +35 -0
- data/example/comments/basic.rb.md +110 -0
- data/example/csv/ruby_transform.rb +148 -0
- data/example/csv/ruby_transform.rb.md +131 -0
- data/example/csv/serialized.rb +201 -0
- data/example/csv/serialized.rb.md +31 -0
- data/example/csv/zero_copy.rb +276 -0
- data/example/csv/zero_copy.rb.md +36 -0
- data/example/custom_atoms/indent_atom.rb +79 -0
- data/example/deepest-errors/basic.rb +131 -0
- data/example/deepest-errors/basic.rb.md +152 -0
- data/example/documentation/basic.rb +18 -0
- data/example/documentation/basic.rb.md +97 -0
- data/example/email/basic.rb +55 -0
- data/example/email/basic.rb.md +102 -0
- data/example/email/ruby_transform.rb +106 -0
- data/example/empty/basic.rb +13 -0
- data/example/empty/basic.rb.md +73 -0
- data/example/empty/example.json +38 -0
- data/example/erb/basic.rb +47 -0
- data/example/erb/basic.rb.md +103 -0
- data/example/erb/optimized.rb +42 -0
- data/example/error-reporting/basic.rb +132 -0
- data/example/error-reporting/basic.rb.md +122 -0
- data/example/expression-evaluator/basic.rb +284 -0
- data/example/expression-evaluator/basic.rb.md +138 -0
- data/example/ini/basic.rb +154 -0
- data/example/ini/basic.rb.md +129 -0
- data/example/ini/ruby_transform.rb +154 -0
- data/example/ip-address/basic.rb +125 -0
- data/example/ip-address/basic.rb.md +139 -0
- data/example/iso-6709/basic.rb +231 -0
- data/example/iso-6709/basic.rb.md +143 -0
- data/example/iso-8601/basic.rb +275 -0
- data/example/iso-8601/basic.rb.md +149 -0
- data/example/json/basic.rb +128 -0
- data/example/json/basic.rb.md +121 -0
- data/example/json/pattern.rb +128 -0
- data/example/json/ruby_transform.rb +200 -0
- data/example/json/ruby_transform.rb.md +32 -0
- data/example/json/serialized.rb +233 -0
- data/example/json/serialized.rb.md +31 -0
- data/example/json/transform.rb +128 -0
- data/example/json/zero_copy.rb +316 -0
- data/example/json/zero_copy.rb.md +36 -0
- data/example/local/basic.rb +34 -0
- data/example/local/basic.rb.md +91 -0
- data/example/local/example.json +38 -0
- data/example/markdown/basic.rb +287 -0
- data/example/markdown/basic.rb.md +160 -0
- data/example/markup/basic.rb +173 -0
- data/example/markup/basic.rb.md +118 -0
- data/example/mathn/basic.rb +47 -0
- data/example/mathn/basic.rb.md +96 -0
- data/example/mathn/example.json +39 -0
- data/example/minilisp/basic.rb +94 -0
- data/example/minilisp/basic.rb.md +133 -0
- data/example/modularity/basic.rb +47 -0
- data/example/modularity/basic.rb.md +152 -0
- data/example/nested-errors/basic.rb +132 -0
- data/example/nested-errors/basic.rb.md +157 -0
- data/example/output/boolean_algebra.out +4 -0
- data/example/output/calc.out +1 -0
- data/example/output/capture.out +3 -0
- data/example/output/comments.out +8 -0
- data/example/output/deepest_errors.out +54 -0
- data/example/output/documentation.err +4 -0
- data/example/output/documentation.out +1 -0
- data/example/output/email_parser.out +2 -0
- data/example/output/empty.err +1 -0
- data/example/output/erb.out +7 -0
- data/example/output/ignore.out +1 -0
- data/example/output/ignore_whitespace.out +1 -0
- data/example/output/ip_address.out +9 -0
- data/example/output/json.out +5 -0
- data/example/output/local.out +3 -0
- data/example/output/mathn.out +4 -0
- data/example/output/minilisp.out +5 -0
- data/example/output/modularity.out +0 -0
- data/example/output/nested_errors.out +54 -0
- data/example/output/optimized_erb.out +1 -0
- data/example/output/parens.out +8 -0
- data/example/output/prec_calc.out +5 -0
- data/example/output/readme.out +1 -0
- data/example/output/scopes.out +1 -0
- data/example/output/seasons.out +28 -0
- data/example/output/sentence.out +1 -0
- data/example/output/simple_xml.out +2 -0
- data/example/output/string_parser.out +3 -0
- data/example/prec-calc/basic.rb +71 -0
- data/example/prec-calc/basic.rb.md +114 -0
- data/example/readme/basic.rb +30 -0
- data/example/readme/basic.rb.md +80 -0
- data/example/scopes/basic.rb +15 -0
- data/example/scopes/basic.rb.md +73 -0
- data/example/scopes/example.json +38 -0
- data/example/seasons/basic.rb +46 -0
- data/example/seasons/basic.rb.md +117 -0
- data/example/seasons/example.json +40 -0
- data/example/sentence/basic.rb +36 -0
- data/example/sentence/basic.rb.md +81 -0
- data/example/sexp/ruby_transform.rb +180 -0
- data/example/sexp/ruby_transform.rb.md +143 -0
- data/example/simple-xml/basic.rb +54 -0
- data/example/simple-xml/basic.rb.md +125 -0
- data/example/simple.lit +3 -0
- data/example/string-literal/basic.rb +77 -0
- data/example/string-literal/basic.rb.md +128 -0
- data/example/test.lit +4 -0
- data/example/toml/basic.rb +226 -0
- data/example/toml/basic.rb.md +173 -0
- data/example/url/basic.rb +219 -0
- data/example/url/basic.rb.md +142 -0
- data/example/url/ruby_transform.rb +219 -0
- data/example/yaml/basic.rb +216 -0
- data/example/yaml/basic.rb.md +148 -0
- data/ext/parsanol_native/extconf.rb +4 -0
- data/lib/parsanol/accelerator/application.rb +62 -0
- data/lib/parsanol/accelerator/engine.rb +112 -0
- data/lib/parsanol/accelerator.rb +162 -0
- data/lib/parsanol/ast_visitor.rb +122 -0
- data/lib/parsanol/atoms/alternative.rb +97 -0
- data/lib/parsanol/atoms/base.rb +214 -0
- data/lib/parsanol/atoms/can_flatten.rb +192 -0
- data/lib/parsanol/atoms/capture.rb +41 -0
- data/lib/parsanol/atoms/context.rb +351 -0
- data/lib/parsanol/atoms/context_optimized.rb +42 -0
- data/lib/parsanol/atoms/custom.rb +110 -0
- data/lib/parsanol/atoms/cut.rb +62 -0
- data/lib/parsanol/atoms/dsl.rb +130 -0
- data/lib/parsanol/atoms/dynamic.rb +33 -0
- data/lib/parsanol/atoms/entity.rb +55 -0
- data/lib/parsanol/atoms/ignored.rb +28 -0
- data/lib/parsanol/atoms/infix.rb +121 -0
- data/lib/parsanol/atoms/lookahead.rb +64 -0
- data/lib/parsanol/atoms/named.rb +50 -0
- data/lib/parsanol/atoms/re.rb +61 -0
- data/lib/parsanol/atoms/repetition.rb +241 -0
- data/lib/parsanol/atoms/scope.rb +28 -0
- data/lib/parsanol/atoms/sequence.rb +157 -0
- data/lib/parsanol/atoms/str.rb +90 -0
- data/lib/parsanol/atoms/visitor.rb +91 -0
- data/lib/parsanol/atoms.rb +36 -0
- data/lib/parsanol/buffer.rb +130 -0
- data/lib/parsanol/builder_callbacks.rb +353 -0
- data/lib/parsanol/cause.rb +101 -0
- data/lib/parsanol/context.rb +23 -0
- data/lib/parsanol/convenience.rb +35 -0
- data/lib/parsanol/edit_tracker.rb +107 -0
- data/lib/parsanol/error_reporter/contextual.rb +122 -0
- data/lib/parsanol/error_reporter/deepest.rb +106 -0
- data/lib/parsanol/error_reporter/tree.rb +68 -0
- data/lib/parsanol/error_reporter.rb +98 -0
- data/lib/parsanol/export.rb +163 -0
- data/lib/parsanol/expression/treetop.rb +94 -0
- data/lib/parsanol/expression.rb +51 -0
- data/lib/parsanol/fast_mode.rb +145 -0
- data/lib/parsanol/first_set.rb +75 -0
- data/lib/parsanol/grammar_builder.rb +177 -0
- data/lib/parsanol/graphviz.rb +97 -0
- data/lib/parsanol/incremental_parser.rb +179 -0
- data/lib/parsanol/interval_tree.rb +215 -0
- data/lib/parsanol/lazy_result.rb +178 -0
- data/lib/parsanol/lexer.rb +146 -0
- data/lib/parsanol/native/parser.rb +630 -0
- data/lib/parsanol/native/serializer.rb +245 -0
- data/lib/parsanol/native/transformer.rb +438 -0
- data/lib/parsanol/native/types.rb +41 -0
- data/lib/parsanol/native.rb +217 -0
- data/lib/parsanol/optimizer.rb +86 -0
- data/lib/parsanol/optimizers/choice_optimizer.rb +78 -0
- data/lib/parsanol/optimizers/cut_inserter.rb +175 -0
- data/lib/parsanol/optimizers/lookahead_optimizer.rb +58 -0
- data/lib/parsanol/optimizers/quantifier_optimizer.rb +62 -0
- data/lib/parsanol/optimizers/sequence_optimizer.rb +97 -0
- data/lib/parsanol/options/ruby_transform.rb +109 -0
- data/lib/parsanol/options/serialized.rb +94 -0
- data/lib/parsanol/options/zero_copy.rb +130 -0
- data/lib/parsanol/options.rb +20 -0
- data/lib/parsanol/parallel.rb +133 -0
- data/lib/parsanol/parsanol_native.bundle +0 -0
- data/lib/parsanol/parser.rb +151 -0
- data/lib/parsanol/parslet.rb +148 -0
- data/lib/parsanol/parslet_native.bundle +0 -0
- data/lib/parsanol/pattern/binding.rb +49 -0
- data/lib/parsanol/pattern.rb +115 -0
- data/lib/parsanol/pool.rb +220 -0
- data/lib/parsanol/pools/array_pool.rb +75 -0
- data/lib/parsanol/pools/buffer_pool.rb +173 -0
- data/lib/parsanol/pools/position_pool.rb +92 -0
- data/lib/parsanol/pools/slice_pool.rb +64 -0
- data/lib/parsanol/position.rb +89 -0
- data/lib/parsanol/result.rb +44 -0
- data/lib/parsanol/result_builder.rb +208 -0
- data/lib/parsanol/result_stream.rb +262 -0
- data/lib/parsanol/rig/rspec.rb +52 -0
- data/lib/parsanol/rope.rb +78 -0
- data/lib/parsanol/scope.rb +42 -0
- data/lib/parsanol/slice.rb +172 -0
- data/lib/parsanol/source/line_cache.rb +99 -0
- data/lib/parsanol/source.rb +171 -0
- data/lib/parsanol/source_location.rb +164 -0
- data/lib/parsanol/streaming_parser.rb +124 -0
- data/lib/parsanol/string_view.rb +192 -0
- data/lib/parsanol/transform.rb +267 -0
- data/lib/parsanol/version.rb +5 -0
- data/lib/parsanol/wasm/README.md +80 -0
- data/lib/parsanol/wasm/package.json +51 -0
- data/lib/parsanol/wasm/parsanol.js +252 -0
- data/lib/parsanol/wasm/parslet.d.ts +129 -0
- data/lib/parsanol/wasm_parser.rb +239 -0
- data/lib/parsanol.rb +408 -0
- data/parsanol-ruby.gemspec +56 -0
- data/spec/acceptance/examples_spec.rb +96 -0
- data/spec/acceptance/infix_parser_spec.rb +145 -0
- data/spec/acceptance/mixing_parsers_spec.rb +74 -0
- data/spec/acceptance/regression_spec.rb +329 -0
- data/spec/acceptance/repetition_and_maybe_spec.rb +44 -0
- data/spec/acceptance/unconsumed_input_spec.rb +21 -0
- data/spec/benchmark/comparative/runner_spec.rb +105 -0
- data/spec/integration/array_pooling_spec.rb +193 -0
- data/spec/integration/buffer_allocation_spec.rb +324 -0
- data/spec/integration/position_pooling_spec.rb +184 -0
- data/spec/integration/result_builder_spec.rb +282 -0
- data/spec/integration/rope_stringview_integration_spec.rb +188 -0
- data/spec/integration/slice_pooling_spec.rb +63 -0
- data/spec/integration/string_view_integration_spec.rb +125 -0
- data/spec/lexer_spec.rb +231 -0
- data/spec/parsanol/atom_results_spec.rb +39 -0
- data/spec/parsanol/atoms/alternative_spec.rb +26 -0
- data/spec/parsanol/atoms/base_spec.rb +127 -0
- data/spec/parsanol/atoms/capture_spec.rb +21 -0
- data/spec/parsanol/atoms/combinations_spec.rb +5 -0
- data/spec/parsanol/atoms/custom_spec.rb +79 -0
- data/spec/parsanol/atoms/dsl_spec.rb +7 -0
- data/spec/parsanol/atoms/entity_spec.rb +77 -0
- data/spec/parsanol/atoms/ignored_spec.rb +15 -0
- data/spec/parsanol/atoms/infix_spec.rb +5 -0
- data/spec/parsanol/atoms/lookahead_spec.rb +22 -0
- data/spec/parsanol/atoms/named_spec.rb +4 -0
- data/spec/parsanol/atoms/re_spec.rb +14 -0
- data/spec/parsanol/atoms/repetition_spec.rb +24 -0
- data/spec/parsanol/atoms/scope_spec.rb +26 -0
- data/spec/parsanol/atoms/sequence_spec.rb +28 -0
- data/spec/parsanol/atoms/str_spec.rb +15 -0
- data/spec/parsanol/atoms/visitor_spec.rb +101 -0
- data/spec/parsanol/atoms_spec.rb +488 -0
- data/spec/parsanol/auto_optimize_spec.rb +334 -0
- data/spec/parsanol/buffer_spec.rb +219 -0
- data/spec/parsanol/builder_callbacks_spec.rb +377 -0
- data/spec/parsanol/choice_optimizer_spec.rb +231 -0
- data/spec/parsanol/convenience_spec.rb +54 -0
- data/spec/parsanol/cut_inserter_spec.rb +248 -0
- data/spec/parsanol/cut_spec.rb +66 -0
- data/spec/parsanol/edit_tracker_spec.rb +218 -0
- data/spec/parsanol/error_reporter/contextual_spec.rb +122 -0
- data/spec/parsanol/error_reporter/deepest_spec.rb +82 -0
- data/spec/parsanol/error_reporter/tree_spec.rb +7 -0
- data/spec/parsanol/export_spec.rb +67 -0
- data/spec/parsanol/expression/treetop_spec.rb +75 -0
- data/spec/parsanol/first_set_spec.rb +298 -0
- data/spec/parsanol/interval_tree_spec.rb +205 -0
- data/spec/parsanol/lazy_result_spec.rb +288 -0
- data/spec/parsanol/lookahead_optimizer_spec.rb +252 -0
- data/spec/parsanol/minilisp.citrus +29 -0
- data/spec/parsanol/minilisp.tt +29 -0
- data/spec/parsanol/optimizer_spec.rb +459 -0
- data/spec/parsanol/options/parslet_compat_spec.rb +166 -0
- data/spec/parsanol/options/ruby_transform_spec.rb +70 -0
- data/spec/parsanol/options/serialized_spec.rb +69 -0
- data/spec/parsanol/options/zero_copy_spec.rb +230 -0
- data/spec/parsanol/parser_spec.rb +36 -0
- data/spec/parsanol/parslet_spec.rb +38 -0
- data/spec/parsanol/pattern_spec.rb +272 -0
- data/spec/parsanol/pool_spec.rb +392 -0
- data/spec/parsanol/pools/array_pool_spec.rb +356 -0
- data/spec/parsanol/pools/buffer_pool_spec.rb +365 -0
- data/spec/parsanol/pools/position_pool_spec.rb +118 -0
- data/spec/parsanol/pools/slice_pool_spec.rb +262 -0
- data/spec/parsanol/position_spec.rb +14 -0
- data/spec/parsanol/result_builder_spec.rb +391 -0
- data/spec/parsanol/rig/rspec_spec.rb +54 -0
- data/spec/parsanol/rope_spec.rb +207 -0
- data/spec/parsanol/scope_spec.rb +45 -0
- data/spec/parsanol/slice_spec.rb +249 -0
- data/spec/parsanol/source/line_cache_spec.rb +74 -0
- data/spec/parsanol/source_spec.rb +207 -0
- data/spec/parsanol/string_view_spec.rb +345 -0
- data/spec/parsanol/transform/context_spec.rb +56 -0
- data/spec/parsanol/transform_spec.rb +183 -0
- data/spec/parsanol/tree_memoization_spec.rb +149 -0
- data/spec/parslet_compatibility/expressir_edge_cases_spec.rb +153 -0
- data/spec/parslet_compatibility/minimal_reproduction.rb +199 -0
- data/spec/parslet_compatibility_spec.rb +399 -0
- data/spec/parslet_imported/atom_spec.rb +93 -0
- data/spec/parslet_imported/combinator_spec.rb +161 -0
- data/spec/parslet_imported/spec_helper.rb +73 -0
- data/spec/performance/batch_parsing_benchmark.rb +129 -0
- data/spec/performance/complete_optimization_summary.rb +143 -0
- data/spec/performance/grammar_caching_analysis.rb +121 -0
- data/spec/performance/grammar_caching_benchmark.rb +80 -0
- data/spec/performance/native_benchmark_spec.rb +230 -0
- data/spec/performance/phase5_benchmark.rb +144 -0
- data/spec/performance/profiling_benchmark.rb +131 -0
- data/spec/performance/ruby_improvements_benchmark.rb +171 -0
- data/spec/performance_spec.rb +374 -0
- data/spec/spec_helper.rb +79 -0
- data/spec/support/opal.rb +8 -0
- data/spec/support/opal.rb.erb +14 -0
- metadata +485 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# INI Parser Example - RubyTransform
|
|
2
|
+
#
|
|
3
|
+
# This example demonstrates parsing INI configuration files.
|
|
4
|
+
# Shows section headers, key-value pairs, and comments.
|
|
5
|
+
#
|
|
6
|
+
# Run with: ruby -Ilib example/ini_ruby_transform.rb
|
|
7
|
+
|
|
8
|
+
$:.unshift File.dirname(__FILE__) + "/../lib"
|
|
9
|
+
|
|
10
|
+
require 'parsanol'
|
|
11
|
+
|
|
12
|
+
# Step 1: Define the INI file grammar
|
|
13
|
+
class IniParser < Parsanol::Parser
|
|
14
|
+
root :ini
|
|
15
|
+
|
|
16
|
+
rule(:ini) {
|
|
17
|
+
(section | key_value | comment).repeat
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# Section header: [section_name]
|
|
21
|
+
rule(:section) {
|
|
22
|
+
(space? >> str('[') >> section_name.as(:name) >> str(']') >> space? >> str("\n").maybe).as(:section)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
rule(:section_name) {
|
|
26
|
+
(match('[^]\n]').repeat(1))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Key-Value pair: key = value
|
|
30
|
+
rule(:key_value) {
|
|
31
|
+
(space? >> key.as(:key) >> space? >> str('=') >> space? >> value.as(:value) >> space? >> str("\n").maybe).as(:kv)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
rule(:key) {
|
|
35
|
+
match('[^\s=]').repeat(1)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
rule(:value) {
|
|
39
|
+
(match('[^\n]').repeat)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Comment: # or ; at start of line
|
|
43
|
+
rule(:comment) {
|
|
44
|
+
(space? >> (str('#') | str(';')) >> match('[^\n]').repeat >> str("\n").maybe).as(:comment)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
rule(:space?) { match('\s').repeat }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Step 2: INI data structures
|
|
51
|
+
class IniFile
|
|
52
|
+
attr_reader :sections
|
|
53
|
+
|
|
54
|
+
def initialize
|
|
55
|
+
@sections = {}
|
|
56
|
+
@current_section = nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def set_section(name)
|
|
60
|
+
@current_section = name
|
|
61
|
+
@sections[name] ||= {}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def set_key_value(key, value)
|
|
65
|
+
section = @sections[@current_section] || {}
|
|
66
|
+
section[key.to_s.strip] = value.to_s.strip
|
|
67
|
+
@sections[@current_section] = section
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def get(section, key = nil)
|
|
71
|
+
if key
|
|
72
|
+
@sections[section]&.[](key)
|
|
73
|
+
else
|
|
74
|
+
@sections[section] || {}
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def to_h
|
|
79
|
+
@sections
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Step 3: Parse and transform
|
|
84
|
+
def parse_ini(input)
|
|
85
|
+
parser = IniParser.new
|
|
86
|
+
tree = parser.parse(input)
|
|
87
|
+
|
|
88
|
+
puts "Parse tree: #{tree.inspect[0..500]}..."
|
|
89
|
+
|
|
90
|
+
ini = IniFile.new
|
|
91
|
+
|
|
92
|
+
tree.each do |item|
|
|
93
|
+
case item
|
|
94
|
+
when Hash
|
|
95
|
+
if item[:section]
|
|
96
|
+
name = item[:section][:name].to_s.strip
|
|
97
|
+
ini.set_section(name)
|
|
98
|
+
puts " Section: [#{name}]"
|
|
99
|
+
elsif item[:kv]
|
|
100
|
+
key = item[:kv][:key].to_s
|
|
101
|
+
value = item[:kv][:value].to_s
|
|
102
|
+
ini.set_key_value(key, value)
|
|
103
|
+
puts " #{key} = #{value}"
|
|
104
|
+
elsif item[:comment]
|
|
105
|
+
# Skip comments
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
ini
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Example usage
|
|
114
|
+
if __FILE__ == $0
|
|
115
|
+
puts "=" * 60
|
|
116
|
+
puts "INI Parser - RubyTransform"
|
|
117
|
+
puts "=" * 60
|
|
118
|
+
puts
|
|
119
|
+
|
|
120
|
+
ini_content = <<~INI
|
|
121
|
+
# This is a comment
|
|
122
|
+
; This is also a comment
|
|
123
|
+
|
|
124
|
+
[database]
|
|
125
|
+
host = localhost
|
|
126
|
+
port = 5432
|
|
127
|
+
|
|
128
|
+
[server]
|
|
129
|
+
host = 0.0.0.0
|
|
130
|
+
port = 8080
|
|
131
|
+
debug = true
|
|
132
|
+
|
|
133
|
+
[cache]
|
|
134
|
+
enabled = true
|
|
135
|
+
ttl = 3600
|
|
136
|
+
INI
|
|
137
|
+
|
|
138
|
+
puts "Input:"
|
|
139
|
+
puts "-" * 40
|
|
140
|
+
puts ini_content
|
|
141
|
+
puts
|
|
142
|
+
puts "Parsed:"
|
|
143
|
+
puts "-" * 40
|
|
144
|
+
|
|
145
|
+
ini = parse_ini(ini_content)
|
|
146
|
+
|
|
147
|
+
puts
|
|
148
|
+
puts "=" * 60
|
|
149
|
+
puts "Accessing parsed data:"
|
|
150
|
+
puts "=" * 60
|
|
151
|
+
puts "database.host: #{ini.get('database', 'host')}"
|
|
152
|
+
puts "database.port: #{ini.get('database', 'port')}"
|
|
153
|
+
puts "server.debug: #{ini.get('server', 'debug')}"
|
|
154
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# INI Parser - Ruby Implementation
|
|
2
|
+
|
|
3
|
+
## How to Run
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
cd parsanol-ruby/example/ini
|
|
7
|
+
ruby basic.rb
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Code Walkthrough
|
|
11
|
+
|
|
12
|
+
### Section Header Rule
|
|
13
|
+
|
|
14
|
+
Sections are enclosed in square brackets:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
rule(:section) {
|
|
18
|
+
(space? >> str('[') >> section_name.as(:name) >> str(']') >> space? >> str("\n").maybe).as(:section)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
rule(:section_name) {
|
|
22
|
+
(match('[^]\n]').repeat(1))
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Section names exclude brackets and newlines for valid syntax.
|
|
27
|
+
|
|
28
|
+
### Key-Value Pair Rule
|
|
29
|
+
|
|
30
|
+
Configuration entries follow `key = value` format:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
rule(:key_value) {
|
|
34
|
+
(space? >> key.as(:key) >> space? >> str('=') >> space? >> value.as(:value) >> space? >> str("\n").maybe).as(:kv)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
rule(:key) {
|
|
38
|
+
match('[^\s=]').repeat(1)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
rule(:value) {
|
|
42
|
+
(match('[^\n]').repeat)
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Keys cannot contain whitespace or equals; values extend to end of line.
|
|
47
|
+
|
|
48
|
+
### Comment Rule
|
|
49
|
+
|
|
50
|
+
Comments start with `#` or `;`:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
rule(:comment) {
|
|
54
|
+
(space? >> (str('#') | str(';')) >> match('[^\n]').repeat >> str("\n").maybe).as(:comment)
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Both comment styles are common in INI files.
|
|
59
|
+
|
|
60
|
+
### Top-Level Grammar
|
|
61
|
+
|
|
62
|
+
The INI file is a sequence of sections, key-value pairs, and comments:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
rule(:ini) {
|
|
66
|
+
(section | key_value | comment).repeat
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Key-value pairs before any section header belong to no section.
|
|
71
|
+
|
|
72
|
+
### IniFile Helper Class
|
|
73
|
+
|
|
74
|
+
A helper class manages parsed data:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
class IniFile
|
|
78
|
+
attr_reader :sections
|
|
79
|
+
|
|
80
|
+
def initialize
|
|
81
|
+
@sections = {}
|
|
82
|
+
@current_section = nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def set_section(name)
|
|
86
|
+
@current_section = name
|
|
87
|
+
@sections[name] ||= {}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def set_key_value(key, value)
|
|
91
|
+
section = @sections[@current_section] || {}
|
|
92
|
+
section[key.to_s.strip] = value.to_s.strip
|
|
93
|
+
@sections[@current_section] = section
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Track current section and build nested hash structure.
|
|
99
|
+
|
|
100
|
+
## Output Types
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
# Parse tree:
|
|
104
|
+
[
|
|
105
|
+
{:section=>{:name=>"database"@s}},
|
|
106
|
+
{:kv=>{:key=>"host"@s, :value=>"localhost"@s}},
|
|
107
|
+
{:kv=>{:key=>"port"@s, :value=>"5432"@s}}
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
# After processing:
|
|
111
|
+
{
|
|
112
|
+
"database" => {"host"=>"localhost", "port"=>"5432"},
|
|
113
|
+
"server" => {"host"=>"0.0.0.0", "port"=>"8080", "debug"=>"true"}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Design Decisions
|
|
118
|
+
|
|
119
|
+
### Why Track Current Section in Helper Class?
|
|
120
|
+
|
|
121
|
+
INI files are inherently sectioned; the helper maintains context as we iterate through parse results.
|
|
122
|
+
|
|
123
|
+
### Why Allow Both Comment Prefixes?
|
|
124
|
+
|
|
125
|
+
`#` is Unix-style, `;` is Windows-style. Supporting both maximizes compatibility.
|
|
126
|
+
|
|
127
|
+
### Why Not Use Transform?
|
|
128
|
+
|
|
129
|
+
Manual iteration provides more control for stateful processing (tracking current section).
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# INI Parser Example - RubyTransform
|
|
2
|
+
#
|
|
3
|
+
# This example demonstrates parsing INI configuration files.
|
|
4
|
+
# Shows section headers, key-value pairs, and comments.
|
|
5
|
+
#
|
|
6
|
+
# Run with: ruby -Ilib example/ini_ruby_transform.rb
|
|
7
|
+
|
|
8
|
+
$:.unshift File.dirname(__FILE__) + "/../lib"
|
|
9
|
+
|
|
10
|
+
require 'parsanol'
|
|
11
|
+
|
|
12
|
+
# Step 1: Define the INI file grammar
|
|
13
|
+
class IniParser < Parsanol::Parser
|
|
14
|
+
root :ini
|
|
15
|
+
|
|
16
|
+
rule(:ini) {
|
|
17
|
+
(section | key_value | comment).repeat
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# Section header: [section_name]
|
|
21
|
+
rule(:section) {
|
|
22
|
+
(space? >> str('[') >> section_name.as(:name) >> str(']') >> space? >> str("\n").maybe).as(:section)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
rule(:section_name) {
|
|
26
|
+
(match('[^]\n]').repeat(1))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Key-Value pair: key = value
|
|
30
|
+
rule(:key_value) {
|
|
31
|
+
(space? >> key.as(:key) >> space? >> str('=') >> space? >> value.as(:value) >> space? >> str("\n").maybe).as(:kv)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
rule(:key) {
|
|
35
|
+
match('[^\s=]').repeat(1)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
rule(:value) {
|
|
39
|
+
(match('[^\n]').repeat)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Comment: # or ; at start of line
|
|
43
|
+
rule(:comment) {
|
|
44
|
+
(space? >> (str('#') | str(';')) >> match('[^\n]').repeat >> str("\n").maybe).as(:comment)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
rule(:space?) { match('\s').repeat }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Step 2: INI data structures
|
|
51
|
+
class IniFile
|
|
52
|
+
attr_reader :sections
|
|
53
|
+
|
|
54
|
+
def initialize
|
|
55
|
+
@sections = {}
|
|
56
|
+
@current_section = nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def set_section(name)
|
|
60
|
+
@current_section = name
|
|
61
|
+
@sections[name] ||= {}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def set_key_value(key, value)
|
|
65
|
+
section = @sections[@current_section] || {}
|
|
66
|
+
section[key.to_s.strip] = value.to_s.strip
|
|
67
|
+
@sections[@current_section] = section
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def get(section, key = nil)
|
|
71
|
+
if key
|
|
72
|
+
@sections[section]&.[](key)
|
|
73
|
+
else
|
|
74
|
+
@sections[section] || {}
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def to_h
|
|
79
|
+
@sections
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Step 3: Parse and transform
|
|
84
|
+
def parse_ini(input)
|
|
85
|
+
parser = IniParser.new
|
|
86
|
+
tree = parser.parse(input)
|
|
87
|
+
|
|
88
|
+
puts "Parse tree: #{tree.inspect[0..500]}..."
|
|
89
|
+
|
|
90
|
+
ini = IniFile.new
|
|
91
|
+
|
|
92
|
+
tree.each do |item|
|
|
93
|
+
case item
|
|
94
|
+
when Hash
|
|
95
|
+
if item[:section]
|
|
96
|
+
name = item[:section][:name].to_s.strip
|
|
97
|
+
ini.set_section(name)
|
|
98
|
+
puts " Section: [#{name}]"
|
|
99
|
+
elsif item[:kv]
|
|
100
|
+
key = item[:kv][:key].to_s
|
|
101
|
+
value = item[:kv][:value].to_s
|
|
102
|
+
ini.set_key_value(key, value)
|
|
103
|
+
puts " #{key} = #{value}"
|
|
104
|
+
elsif item[:comment]
|
|
105
|
+
# Skip comments
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
ini
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Example usage
|
|
114
|
+
if __FILE__ == $0
|
|
115
|
+
puts "=" * 60
|
|
116
|
+
puts "INI Parser - RubyTransform"
|
|
117
|
+
puts "=" * 60
|
|
118
|
+
puts
|
|
119
|
+
|
|
120
|
+
ini_content = <<~INI
|
|
121
|
+
# This is a comment
|
|
122
|
+
; This is also a comment
|
|
123
|
+
|
|
124
|
+
[database]
|
|
125
|
+
host = localhost
|
|
126
|
+
port = 5432
|
|
127
|
+
|
|
128
|
+
[server]
|
|
129
|
+
host = 0.0.0.0
|
|
130
|
+
port = 8080
|
|
131
|
+
debug = true
|
|
132
|
+
|
|
133
|
+
[cache]
|
|
134
|
+
enabled = true
|
|
135
|
+
ttl = 3600
|
|
136
|
+
INI
|
|
137
|
+
|
|
138
|
+
puts "Input:"
|
|
139
|
+
puts "-" * 40
|
|
140
|
+
puts ini_content
|
|
141
|
+
puts
|
|
142
|
+
puts "Parsed:"
|
|
143
|
+
puts "-" * 40
|
|
144
|
+
|
|
145
|
+
ini = parse_ini(ini_content)
|
|
146
|
+
|
|
147
|
+
puts
|
|
148
|
+
puts "=" * 60
|
|
149
|
+
puts "Accessing parsed data:"
|
|
150
|
+
puts "=" * 60
|
|
151
|
+
puts "database.host: #{ini.get('database', 'host')}"
|
|
152
|
+
puts "database.port: #{ini.get('database', 'port')}"
|
|
153
|
+
puts "server.debug: #{ini.get('server', 'debug')}"
|
|
154
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# This example is heavily inspired by citrus' ip.citrus. Have a look at both
|
|
2
|
+
# of these to get some choice!
|
|
3
|
+
|
|
4
|
+
# The grammars in this file conform to the ABNF given in Appendix A of RFC 3986
|
|
5
|
+
# Uniform Resource Identifier (URI): Generic Syntax.
|
|
6
|
+
#
|
|
7
|
+
# See http://tools.ietf.org/html/rfc3986#appendix-A for more information.
|
|
8
|
+
|
|
9
|
+
$:.unshift File.dirname(__FILE__) + "/../lib"
|
|
10
|
+
|
|
11
|
+
require 'pp'
|
|
12
|
+
require 'parsanol/parslet'
|
|
13
|
+
|
|
14
|
+
module IPv4
|
|
15
|
+
include Parsanol::Parslet
|
|
16
|
+
|
|
17
|
+
# A host identified by an IPv4 literal address is represented in
|
|
18
|
+
# dotted-decimal notation (a sequence of four decimal numbers in the range 0
|
|
19
|
+
# to 255, separated by "."), as described in [RFC1123] by reference to
|
|
20
|
+
# [RFC0952]. Note that other forms of dotted notation may be interpreted on
|
|
21
|
+
# some platforms, as described in Section 7.4, but only the dotted-decimal
|
|
22
|
+
# form of four octets is allowed by this grammar.
|
|
23
|
+
rule(:ipv4) {
|
|
24
|
+
(dec_octet >> str('.') >> dec_octet >> str('.') >>
|
|
25
|
+
dec_octet >> str('.') >> dec_octet).as(:ipv4)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
rule(:dec_octet) {
|
|
29
|
+
str('25') >> match("[0-5]") |
|
|
30
|
+
str('2') >> match("[0-4]") >> digit |
|
|
31
|
+
str('1') >> digit >> digit |
|
|
32
|
+
match('[1-9]') >> digit |
|
|
33
|
+
digit
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
rule(:digit) {
|
|
37
|
+
match('[0-9]')
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Must be used in concert with IPv4
|
|
42
|
+
module IPv6
|
|
43
|
+
include Parsanol::Parslet
|
|
44
|
+
|
|
45
|
+
rule(:colon) { str(':') }
|
|
46
|
+
rule(:dcolon) { colon >> colon }
|
|
47
|
+
|
|
48
|
+
# h16 :
|
|
49
|
+
def h16r(times)
|
|
50
|
+
(h16 >> colon).repeat(times, times)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# : h16
|
|
54
|
+
def h16l(times)
|
|
55
|
+
(colon >> h16).repeat(0,times)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# A 128-bit IPv6 address is divided into eight 16-bit pieces. Each piece is
|
|
59
|
+
# represented numerically in case-insensitive hexadecimal, using one to four
|
|
60
|
+
# hexadecimal digits (leading zeroes are permitted). The eight encoded
|
|
61
|
+
# pieces are given most-significant first, separated by colon characters.
|
|
62
|
+
# Optionally, the least-significant two pieces may instead be represented in
|
|
63
|
+
# IPv4 address textual format. A sequence of one or more consecutive
|
|
64
|
+
# zero-valued 16-bit pieces within the address may be elided, omitting all
|
|
65
|
+
# their digits and leaving exactly two consecutive colons in their place to
|
|
66
|
+
# mark the elision.
|
|
67
|
+
rule(:ipv6) {
|
|
68
|
+
(
|
|
69
|
+
(
|
|
70
|
+
h16r(6) |
|
|
71
|
+
dcolon >> h16r(5) |
|
|
72
|
+
h16.maybe >> dcolon >> h16r(4) |
|
|
73
|
+
(h16 >> h16l(1)).maybe >> dcolon >> h16r(3) |
|
|
74
|
+
(h16 >> h16l(2)).maybe >> dcolon >> h16r(2) |
|
|
75
|
+
(h16 >> h16l(3)).maybe >> dcolon >> h16r(1) |
|
|
76
|
+
(h16 >> h16l(4)).maybe >> dcolon
|
|
77
|
+
) >> ls32 |
|
|
78
|
+
(h16 >> h16l(5)).maybe >> dcolon >> h16 |
|
|
79
|
+
(h16 >> h16l(6)).maybe >> dcolon
|
|
80
|
+
).as(:ipv6)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
rule(:h16) {
|
|
84
|
+
hexdigit.repeat(1,4)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
rule(:ls32) {
|
|
88
|
+
(h16 >> colon >> h16) |
|
|
89
|
+
ipv4
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
rule(:hexdigit) {
|
|
93
|
+
digit | match("[a-fA-F]")
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
class Parser
|
|
98
|
+
include IPv4
|
|
99
|
+
include IPv6
|
|
100
|
+
|
|
101
|
+
def parse(str)
|
|
102
|
+
(ipv4 | ipv6).parse(str)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
%W(
|
|
107
|
+
0.0.0.0
|
|
108
|
+
255.255.255.255
|
|
109
|
+
255.255.255
|
|
110
|
+
1:2:3:4:5:6:7:8
|
|
111
|
+
12AD:34FC:A453:1922::
|
|
112
|
+
12AD::34FC
|
|
113
|
+
12AD::
|
|
114
|
+
::
|
|
115
|
+
1:2
|
|
116
|
+
).each do |address|
|
|
117
|
+
parser = Parser.new
|
|
118
|
+
printf "%30s -> ", address
|
|
119
|
+
begin
|
|
120
|
+
result = parser.parse(address)
|
|
121
|
+
puts result.inspect
|
|
122
|
+
rescue Parsanol::ParseFailed => m
|
|
123
|
+
puts "Failed: #{m}"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# IP Address Parser - Ruby Implementation
|
|
2
|
+
|
|
3
|
+
## How to Run
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
cd parsanol-ruby/example/ip-address
|
|
7
|
+
ruby basic.rb
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Code Walkthrough
|
|
11
|
+
|
|
12
|
+
### IPv4 DecOctet Rule
|
|
13
|
+
|
|
14
|
+
Each octet must be 0-255:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
rule(:dec_octet) {
|
|
18
|
+
str('25') >> match("[0-5]") |
|
|
19
|
+
str('2') >> match("[0-4]") >> digit |
|
|
20
|
+
str('1') >> digit >> digit |
|
|
21
|
+
match('[1-9]') >> digit |
|
|
22
|
+
digit
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Ordered alternatives ensure correct matching: 250-255, 200-249, 100-199, 10-99, 0-9.
|
|
27
|
+
|
|
28
|
+
### IPv4 Address Rule
|
|
29
|
+
|
|
30
|
+
Four octets separated by dots:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
rule(:ipv4) {
|
|
34
|
+
(dec_octet >> str('.') >> dec_octet >> str('.') >>
|
|
35
|
+
dec_octet >> str('.') >> dec_octet).as(:ipv4)
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This matches the dotted-decimal notation from RFC 1123.
|
|
40
|
+
|
|
41
|
+
### IPv6 H16 Rule
|
|
42
|
+
|
|
43
|
+
Hexadecimal groups are 1-4 digits:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
rule(:h16) {
|
|
47
|
+
hexdigit.repeat(1,4)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
rule(:hexdigit) {
|
|
51
|
+
digit | match("[a-fA-F]")
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Case-insensitive hex matching follows RFC 3986.
|
|
56
|
+
|
|
57
|
+
### IPv6 Address Rule
|
|
58
|
+
|
|
59
|
+
IPv6 allows zero compression with `::`:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
rule(:ipv6) {
|
|
63
|
+
(
|
|
64
|
+
(
|
|
65
|
+
h16r(6) |
|
|
66
|
+
dcolon >> h16r(5) |
|
|
67
|
+
h16.maybe >> dcolon >> h16r(4) |
|
|
68
|
+
# ... more patterns
|
|
69
|
+
) >> ls32 |
|
|
70
|
+
(h16 >> h16l(5)).maybe >> dcolon >> h16 |
|
|
71
|
+
(h16 >> h16l(6)).maybe >> dcolon
|
|
72
|
+
).as(:ipv6)
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Multiple alternatives handle different compression positions.
|
|
77
|
+
|
|
78
|
+
### LS32 Rule
|
|
79
|
+
|
|
80
|
+
The least-significant 32 bits can be IPv4 or two h16 groups:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
rule(:ls32) {
|
|
84
|
+
(h16 >> colon >> h16) |
|
|
85
|
+
ipv4
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
IPv4-mapped IPv6 addresses are supported.
|
|
90
|
+
|
|
91
|
+
### Module Composition
|
|
92
|
+
|
|
93
|
+
Grammar modules are mixed into the parser:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
module IPv4
|
|
97
|
+
include Parsanol::Parslet
|
|
98
|
+
# IPv4 rules...
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
module IPv6
|
|
102
|
+
include Parsanol::Parslet
|
|
103
|
+
# IPv6 rules...
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
class Parser
|
|
107
|
+
include IPv4
|
|
108
|
+
include IPv6
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Modular organization keeps complex grammar manageable.
|
|
113
|
+
|
|
114
|
+
## Output Types
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
# IPv4:
|
|
118
|
+
{:ipv4=>"192.168.1.1"@s}
|
|
119
|
+
|
|
120
|
+
# IPv6:
|
|
121
|
+
{:ipv6=>"2001:db8::1"@s}
|
|
122
|
+
|
|
123
|
+
# Invalid:
|
|
124
|
+
# Raises Parsanol::ParseFailed
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Design Decisions
|
|
128
|
+
|
|
129
|
+
### Why Separate IPv4 and IPv6 Modules?
|
|
130
|
+
|
|
131
|
+
RFC 3986 defines them separately; modules allow independent testing and reuse.
|
|
132
|
+
|
|
133
|
+
### Why So Many IPv6 Alternatives?
|
|
134
|
+
|
|
135
|
+
Zero compression (`::`) can appear at any position; each alternative represents a valid compression point.
|
|
136
|
+
|
|
137
|
+
### Why Ordered Alternatives for DecOctet?
|
|
138
|
+
|
|
139
|
+
PEG parsers try alternatives in order. Largest ranges must come first to prevent premature matching.
|