kumi-parser 0.0.2 → 0.0.3
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.
- checksums.yaml +4 -4
- data/.rspec +3 -0
- data/LICENSE +21 -0
- data/README.md +76 -0
- data/Rakefile +10 -0
- data/examples/debug_text_parser.rb +41 -0
- data/examples/debug_transform_rule.rb +26 -0
- data/examples/text_parser_comprehensive_test.rb +333 -0
- data/examples/text_parser_test_with_comments.rb +146 -0
- data/kumi-parser.gemspec +45 -0
- data/lib/kumi/parser/analyzer_diagnostic_converter.rb +84 -0
- data/lib/kumi/parser/error_extractor.rb +89 -0
- data/lib/kumi/parser/syntax_validator.rb +43 -0
- data/lib/kumi/parser/text_parser/api.rb +60 -0
- data/lib/kumi/parser/text_parser/editor_diagnostic.rb +102 -0
- data/lib/kumi/parser/text_parser/grammar.rb +214 -0
- data/lib/kumi/parser/text_parser/parser.rb +168 -0
- data/lib/kumi/parser/text_parser/transform.rb +170 -0
- data/lib/kumi/parser/text_parser.rb +53 -0
- data/lib/kumi/parser/version.rb +7 -0
- data/lib/kumi/parser.rb +8 -0
- data/lib/kumi-parser.rb +18 -0
- data/test_basic.rb +44 -0
- metadata +24 -2
@@ -0,0 +1,168 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'grammar'
|
4
|
+
require_relative 'transform'
|
5
|
+
require 'kumi/core/error_reporting'
|
6
|
+
|
7
|
+
module Kumi
|
8
|
+
module Parser
|
9
|
+
module TextParser
|
10
|
+
# Parslet-based parser with proper arithmetic operator precedence
|
11
|
+
class Parser
|
12
|
+
include Kumi::Core::ErrorReporting
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@grammar = Grammar.new
|
16
|
+
@transform = Transform.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse(text_dsl, source_file: '<parslet_parser>')
|
20
|
+
# Parse with Parslet grammar
|
21
|
+
parse_tree = @grammar.parse(text_dsl)
|
22
|
+
|
23
|
+
# Transform to AST
|
24
|
+
ast = @transform.apply(parse_tree)
|
25
|
+
|
26
|
+
# Post-process to create final Root node if needed
|
27
|
+
post_process(ast)
|
28
|
+
rescue Parslet::ParseFailed => e
|
29
|
+
raise_syntax_error(
|
30
|
+
"Parse error: #{e.parse_failure_cause.ascii_tree}",
|
31
|
+
location: create_location(source_file, 1, 1)
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def post_process(ast)
|
38
|
+
# If it's already a Root node, return it
|
39
|
+
return ast if ast.is_a?(Syntax::Root)
|
40
|
+
|
41
|
+
# If it's a hash with input and declarations, convert it
|
42
|
+
if ast.is_a?(Hash) && ast[:input] && ast[:declarations]
|
43
|
+
input_decls = ast[:input][:declarations] || []
|
44
|
+
input_decls = [input_decls] unless input_decls.is_a?(Array)
|
45
|
+
|
46
|
+
# Process input declarations
|
47
|
+
processed_input_decls = input_decls.map do |input_decl|
|
48
|
+
process_input_declaration(input_decl)
|
49
|
+
end
|
50
|
+
|
51
|
+
other_decls = ast[:declarations] || []
|
52
|
+
other_decls = [other_decls] unless other_decls.is_a?(Array)
|
53
|
+
|
54
|
+
# Convert remaining hash values to proper nodes
|
55
|
+
values = []
|
56
|
+
traits = []
|
57
|
+
|
58
|
+
other_decls.each do |decl|
|
59
|
+
if decl.is_a?(Hash) && decl[:name] && decl[:expr]
|
60
|
+
expr = process_expression(decl[:expr])
|
61
|
+
values << Syntax::ValueDeclaration.new(decl[:name], expr, loc: create_location('<parslet>', 1, 1))
|
62
|
+
elsif decl.is_a?(Syntax::ValueDeclaration)
|
63
|
+
# Need to process the expression if it's still a hash
|
64
|
+
if decl.expression.is_a?(Hash)
|
65
|
+
processed_expr = process_expression(decl.expression)
|
66
|
+
values << Syntax::ValueDeclaration.new(decl.name, processed_expr, loc: decl.loc)
|
67
|
+
else
|
68
|
+
values << decl
|
69
|
+
end
|
70
|
+
elsif decl.is_a?(Syntax::TraitDeclaration)
|
71
|
+
traits << decl
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
Syntax::Root.new(processed_input_decls, values, traits, loc: create_location('<parslet>', 1, 1))
|
76
|
+
else
|
77
|
+
ast
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def process_expression(expr)
|
82
|
+
# If it's already a proper AST node, return it
|
83
|
+
return expr unless expr.is_a?(Hash)
|
84
|
+
|
85
|
+
# Handle nested expression structures
|
86
|
+
if expr[:left] && expr[:ops]
|
87
|
+
left_expr = process_expression(expr[:left])
|
88
|
+
ops = expr[:ops] || []
|
89
|
+
ops = [ops] unless ops.is_a?(Array)
|
90
|
+
|
91
|
+
result = ops.inject(left_expr) do |left, op|
|
92
|
+
if op.is_a?(Hash) && op[:op] && op[:right]
|
93
|
+
op_name = op[:op].keys.first
|
94
|
+
right_expr = process_expression(op[:right])
|
95
|
+
Syntax::CallExpression.new(op_name, [left, right_expr], loc: create_location('<parslet>', 1, 1))
|
96
|
+
else
|
97
|
+
left
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Handle comparison
|
102
|
+
if expr[:comp] && expr[:comp][:op] && expr[:comp][:right]
|
103
|
+
comp = expr[:comp]
|
104
|
+
op_name = comp[:op].keys.first
|
105
|
+
right_expr = process_expression(comp[:right])
|
106
|
+
Syntax::CallExpression.new(op_name, [result, right_expr], loc: create_location('<parslet>', 1, 1))
|
107
|
+
else
|
108
|
+
result
|
109
|
+
end
|
110
|
+
else
|
111
|
+
expr
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def process_input_declaration(input_decl)
|
116
|
+
return input_decl if input_decl.is_a?(Syntax::InputDeclaration)
|
117
|
+
|
118
|
+
if input_decl.is_a?(Hash)
|
119
|
+
if input_decl[:nested_fields]
|
120
|
+
# Array input declaration
|
121
|
+
nested_fields = input_decl[:nested_fields] || []
|
122
|
+
nested_fields = [nested_fields] unless nested_fields.is_a?(Array)
|
123
|
+
|
124
|
+
processed_fields = nested_fields.map do |field|
|
125
|
+
if field.is_a?(Hash) && field[:type] && field[:name]
|
126
|
+
Syntax::InputDeclaration.new(
|
127
|
+
field[:name],
|
128
|
+
field[:domain],
|
129
|
+
field[:type].to_sym,
|
130
|
+
[],
|
131
|
+
loc: create_location('<parslet>', 1, 1)
|
132
|
+
)
|
133
|
+
else
|
134
|
+
field
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
Syntax::InputDeclaration.new(
|
139
|
+
input_decl[:name],
|
140
|
+
nil,
|
141
|
+
:array,
|
142
|
+
processed_fields,
|
143
|
+
loc: create_location('<parslet>', 1, 1)
|
144
|
+
)
|
145
|
+
elsif input_decl[:type] && input_decl[:name]
|
146
|
+
# Simple input declaration
|
147
|
+
Syntax::InputDeclaration.new(
|
148
|
+
input_decl[:name],
|
149
|
+
input_decl[:domain],
|
150
|
+
input_decl[:type].to_sym,
|
151
|
+
[],
|
152
|
+
loc: create_location('<parslet>', 1, 1)
|
153
|
+
)
|
154
|
+
else
|
155
|
+
input_decl
|
156
|
+
end
|
157
|
+
else
|
158
|
+
input_decl
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def create_location(file, line, column)
|
163
|
+
Syntax::Location.new(file: file, line: line, column: column)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'parslet'
|
4
|
+
require 'kumi/syntax/node'
|
5
|
+
require 'kumi/syntax/root'
|
6
|
+
require 'kumi/syntax/input_declaration'
|
7
|
+
require 'kumi/syntax/value_declaration'
|
8
|
+
require 'kumi/syntax/trait_declaration'
|
9
|
+
require 'kumi/syntax/call_expression'
|
10
|
+
require 'kumi/syntax/input_reference'
|
11
|
+
require 'kumi/syntax/input_element_reference'
|
12
|
+
require 'kumi/syntax/declaration_reference'
|
13
|
+
require 'kumi/syntax/literal'
|
14
|
+
|
15
|
+
module Kumi
|
16
|
+
module Parser
|
17
|
+
module TextParser
|
18
|
+
class Transform < Parslet::Transform
|
19
|
+
LOC = Kumi::Syntax::Location.new(file: '<parslet_parser>', line: 1, column: 1)
|
20
|
+
|
21
|
+
# Literals
|
22
|
+
rule(integer: simple(:x)) { Kumi::Syntax::Literal.new(x.to_i, loc: LOC) }
|
23
|
+
rule(float: simple(:x)) { Kumi::Syntax::Literal.new(x.to_f, loc: LOC) }
|
24
|
+
rule(string: simple(:x)) { Kumi::Syntax::Literal.new(x.to_s, loc: LOC) }
|
25
|
+
rule(true: simple(:_)) { Kumi::Syntax::Literal.new(true, loc: LOC) }
|
26
|
+
rule(false: simple(:_)) { Kumi::Syntax::Literal.new(false, loc: LOC) }
|
27
|
+
|
28
|
+
# Symbols
|
29
|
+
rule(symbol: simple(:name)) { name.to_sym }
|
30
|
+
|
31
|
+
# Input and declaration references
|
32
|
+
rule(input_ref: simple(:path)) do
|
33
|
+
# Handle multi-level paths like "items.price"
|
34
|
+
path_parts = path.to_s.split('.')
|
35
|
+
if path_parts.length == 1
|
36
|
+
Kumi::Syntax::InputReference.new(path_parts[0].to_sym, loc: LOC)
|
37
|
+
else
|
38
|
+
Kumi::Syntax::InputElementReference.new(path_parts.map(&:to_sym), loc: LOC)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
rule(decl_ref: simple(:name)) { Kumi::Syntax::DeclarationReference.new(name.to_sym, loc: LOC) }
|
43
|
+
|
44
|
+
# Function calls
|
45
|
+
rule(fn_name: simple(:name), args: sequence(:args)) do
|
46
|
+
Kumi::Syntax::CallExpression.new(name, args, loc: LOC)
|
47
|
+
end
|
48
|
+
|
49
|
+
rule(fn_name: simple(:name), args: []) do
|
50
|
+
Kumi::Syntax::CallExpression.new(name, [], loc: LOC)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Arithmetic expressions with left-associativity
|
54
|
+
rule(left: simple(:l), ops: sequence(:operations)) do
|
55
|
+
operations.inject(l) do |left_expr, op|
|
56
|
+
op_name = op[:op].keys.first
|
57
|
+
Kumi::Syntax::CallExpression.new(op_name, [left_expr, op[:right]], loc: LOC)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
rule(left: simple(:l), ops: []) { l }
|
62
|
+
|
63
|
+
# Comparison expressions
|
64
|
+
rule(left: simple(:l), comp: simple(:comparison)) do
|
65
|
+
if comparison && comparison[:op] && comparison[:right]
|
66
|
+
op_name = comparison[:op].keys.first
|
67
|
+
Kumi::Syntax::CallExpression.new(op_name, [l, comparison[:right]], loc: LOC)
|
68
|
+
else
|
69
|
+
l
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
rule(left: simple(:l), comp: nil) { l }
|
74
|
+
|
75
|
+
# Simple input declarations
|
76
|
+
rule(type: simple(:type), name: simple(:name)) do
|
77
|
+
Kumi::Syntax::InputDeclaration.new(name, nil, type.to_sym, [], loc: LOC)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Nested array declarations
|
81
|
+
rule(name: simple(:name), nested_fields: sequence(:fields)) do
|
82
|
+
# Transform nested field hashes to InputDeclaration objects
|
83
|
+
transformed_fields = fields.map do |field|
|
84
|
+
if field.is_a?(Hash) && field[:type] && field[:name]
|
85
|
+
Kumi::Syntax::InputDeclaration.new(
|
86
|
+
field[:name],
|
87
|
+
field[:domain],
|
88
|
+
field[:type].to_sym,
|
89
|
+
[],
|
90
|
+
loc: LOC
|
91
|
+
)
|
92
|
+
else
|
93
|
+
field
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Create an array input declaration with nested fields
|
98
|
+
Kumi::Syntax::InputDeclaration.new(
|
99
|
+
name,
|
100
|
+
nil,
|
101
|
+
:array,
|
102
|
+
transformed_fields,
|
103
|
+
loc: LOC
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
rule(name: simple(:name), nested_fields: simple(:field)) do
|
108
|
+
# Single nested field case
|
109
|
+
transformed_field = if field.is_a?(Hash) && field[:type] && field[:name]
|
110
|
+
Kumi::Syntax::InputDeclaration.new(
|
111
|
+
field[:name],
|
112
|
+
field[:domain],
|
113
|
+
field[:type].to_sym,
|
114
|
+
[],
|
115
|
+
loc: LOC
|
116
|
+
)
|
117
|
+
else
|
118
|
+
field
|
119
|
+
end
|
120
|
+
|
121
|
+
Kumi::Syntax::InputDeclaration.new(
|
122
|
+
name,
|
123
|
+
nil,
|
124
|
+
:array,
|
125
|
+
[transformed_field],
|
126
|
+
loc: LOC
|
127
|
+
)
|
128
|
+
end
|
129
|
+
|
130
|
+
rule(type: simple(:type), name: simple(:name), expr: simple(:expr)) do
|
131
|
+
# Differentiate between value and trait declarations based on type
|
132
|
+
if type.to_s == 'value'
|
133
|
+
Kumi::Syntax::ValueDeclaration.new(name, expr, loc: LOC)
|
134
|
+
elsif type.to_s == 'trait'
|
135
|
+
Kumi::Syntax::TraitDeclaration.new(name, expr, loc: LOC)
|
136
|
+
else
|
137
|
+
# Fallback - shouldn't happen
|
138
|
+
Kumi::Syntax::ValueDeclaration.new(name, expr, loc: LOC)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Handle the intermediate case before the expression gets fully transformed
|
143
|
+
rule(type: simple(:type), name: { symbol: simple(:name) }, expr: simple(:expr)) do
|
144
|
+
# Differentiate between value and trait declarations based on type
|
145
|
+
if type.to_s == 'value'
|
146
|
+
Kumi::Syntax::ValueDeclaration.new(name.to_sym, expr, loc: LOC)
|
147
|
+
elsif type.to_s == 'trait'
|
148
|
+
Kumi::Syntax::TraitDeclaration.new(name.to_sym, expr, loc: LOC)
|
149
|
+
else
|
150
|
+
# Fallback - shouldn't happen
|
151
|
+
Kumi::Syntax::ValueDeclaration.new(name.to_sym, expr, loc: LOC)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Schema structure - convert the hash to Root node
|
156
|
+
rule(input: { declarations: sequence(:input_decls) }, declarations: sequence(:other_decls)) do
|
157
|
+
values = other_decls.select { |d| d.is_a?(Kumi::Syntax::ValueDeclaration) }
|
158
|
+
traits = other_decls.select { |d| d.is_a?(Kumi::Syntax::TraitDeclaration) }
|
159
|
+
Kumi::Syntax::Root.new(input_decls, values, traits, loc: LOC)
|
160
|
+
end
|
161
|
+
|
162
|
+
rule(input: { declarations: simple(:input_decl) }, declarations: sequence(:other_decls)) do
|
163
|
+
values = other_decls.select { |d| d.is_a?(Kumi::Syntax::ValueDeclaration) }
|
164
|
+
traits = other_decls.select { |d| d.is_a?(Kumi::Syntax::TraitDeclaration) }
|
165
|
+
Kumi::Syntax::Root.new([input_decl], values, traits, loc: LOC)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'syntax_validator'
|
4
|
+
|
5
|
+
module Kumi
|
6
|
+
module Parser
|
7
|
+
module TextParser
|
8
|
+
# TextParser module - all classes are autoloaded by Zeitwerk
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# Check if text is syntactically valid
|
12
|
+
def valid?(text, source_file: '<input>')
|
13
|
+
validator.valid?(text, source_file: source_file)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Validate text and return diagnostic collection
|
17
|
+
def validate(text, source_file: '<input>')
|
18
|
+
validator.validate(text, source_file: source_file)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Get Monaco Editor format diagnostics
|
22
|
+
def diagnostics_for_monaco(text, source_file: '<input>')
|
23
|
+
validate(text, source_file: source_file).to_monaco
|
24
|
+
end
|
25
|
+
|
26
|
+
# Get CodeMirror format diagnostics
|
27
|
+
def diagnostics_for_codemirror(text, source_file: '<input>')
|
28
|
+
validate(text, source_file: source_file).to_codemirror
|
29
|
+
end
|
30
|
+
|
31
|
+
# Get JSON format diagnostics
|
32
|
+
def diagnostics_as_json(text, source_file: '<input>')
|
33
|
+
validate(text, source_file: source_file).to_json
|
34
|
+
end
|
35
|
+
|
36
|
+
# Parse text (compatibility method)
|
37
|
+
def parse(text, source_file: '<input>')
|
38
|
+
parser.parse(text, source_file: source_file)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def validator
|
44
|
+
@validator ||= SyntaxValidator.new
|
45
|
+
end
|
46
|
+
|
47
|
+
def parser
|
48
|
+
@parser ||= TextParser::Parser.new
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/kumi/parser.rb
ADDED
data/lib/kumi-parser.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'kumi'
|
4
|
+
require 'zeitwerk'
|
5
|
+
require 'parslet'
|
6
|
+
|
7
|
+
loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
|
8
|
+
loader.ignore("#{__dir__}/kumi-parser.rb")
|
9
|
+
loader.ignore("#{__dir__}/kumi/parser/version.rb")
|
10
|
+
loader.setup
|
11
|
+
|
12
|
+
require_relative 'kumi/parser/version'
|
13
|
+
|
14
|
+
module Kumi
|
15
|
+
module Parser
|
16
|
+
# Parser extension for Kumi DSL
|
17
|
+
end
|
18
|
+
end
|
data/test_basic.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Basic functionality test for kumi-parser
|
3
|
+
|
4
|
+
require_relative 'lib/kumi/parser'
|
5
|
+
|
6
|
+
schema_text = <<~SCHEMA
|
7
|
+
schema do
|
8
|
+
input do
|
9
|
+
integer :age
|
10
|
+
end
|
11
|
+
#{' '}
|
12
|
+
trait :adult, input.age >= 18
|
13
|
+
value :bonus, 100
|
14
|
+
end
|
15
|
+
SCHEMA
|
16
|
+
|
17
|
+
puts 'Testing kumi-parser...'
|
18
|
+
puts '=' * 40
|
19
|
+
|
20
|
+
begin
|
21
|
+
# Test validation
|
22
|
+
puts '1. Testing validation...'
|
23
|
+
diagnostics = Kumi::Parser::TextParser.validate(schema_text)
|
24
|
+
puts " ✅ Validation: #{diagnostics.empty? ? 'PASSED' : 'FAILED'}"
|
25
|
+
|
26
|
+
unless diagnostics.empty?
|
27
|
+
diagnostics.to_a.each do |d|
|
28
|
+
puts " Error: Line #{d.line}, Column #{d.column}: #{d.message}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Test parsing if validation passed
|
33
|
+
if diagnostics.empty?
|
34
|
+
puts '2. Testing parsing...'
|
35
|
+
ast = Kumi::Parser::TextParser.parse(schema_text)
|
36
|
+
puts " ✅ Parsing: #{ast ? 'PASSED' : 'FAILED'}"
|
37
|
+
puts " AST type: #{ast.class}"
|
38
|
+
end
|
39
|
+
|
40
|
+
puts "\n🎉 Basic functionality test completed!"
|
41
|
+
rescue StandardError => e
|
42
|
+
puts "❌ Error: #{e.message}"
|
43
|
+
puts e.backtrace.first(5)
|
44
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kumi-parser
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kumi Team
|
@@ -128,7 +128,29 @@ email:
|
|
128
128
|
executables: []
|
129
129
|
extensions: []
|
130
130
|
extra_rdoc_files: []
|
131
|
-
files:
|
131
|
+
files:
|
132
|
+
- ".rspec"
|
133
|
+
- LICENSE
|
134
|
+
- README.md
|
135
|
+
- Rakefile
|
136
|
+
- examples/debug_text_parser.rb
|
137
|
+
- examples/debug_transform_rule.rb
|
138
|
+
- examples/text_parser_comprehensive_test.rb
|
139
|
+
- examples/text_parser_test_with_comments.rb
|
140
|
+
- kumi-parser.gemspec
|
141
|
+
- lib/kumi-parser.rb
|
142
|
+
- lib/kumi/parser.rb
|
143
|
+
- lib/kumi/parser/analyzer_diagnostic_converter.rb
|
144
|
+
- lib/kumi/parser/error_extractor.rb
|
145
|
+
- lib/kumi/parser/syntax_validator.rb
|
146
|
+
- lib/kumi/parser/text_parser.rb
|
147
|
+
- lib/kumi/parser/text_parser/api.rb
|
148
|
+
- lib/kumi/parser/text_parser/editor_diagnostic.rb
|
149
|
+
- lib/kumi/parser/text_parser/grammar.rb
|
150
|
+
- lib/kumi/parser/text_parser/parser.rb
|
151
|
+
- lib/kumi/parser/text_parser/transform.rb
|
152
|
+
- lib/kumi/parser/version.rb
|
153
|
+
- test_basic.rb
|
132
154
|
homepage: https://github.com/amuta/kumi-parser
|
133
155
|
licenses:
|
134
156
|
- MIT
|