lkml 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.md +10 -0
- data/README.md +11 -0
- data/lib/lkml/keys.rb +125 -0
- data/lib/lkml/lexer.rb +161 -0
- data/lib/lkml/parser.rb +338 -0
- data/lib/lkml/simple.rb +297 -0
- data/lib/lkml/tokens.rb +211 -0
- data/lib/lkml/tree.rb +319 -0
- data/lib/lkml/version.rb +5 -0
- data/lib/lkml/visitors.rb +91 -0
- data/lib/lkml.rb +26 -0
- metadata +54 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0d77c017023caf25f3f6acadeff2cdc098d1ddbb6c75b8ca9f0ac7968abcefef
|
4
|
+
data.tar.gz: 6613bd618a1793463394f7e4b0408eb30467fb435da58735d61b51fc00d1a22e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 668606773d646b80de95eef27aa09d060018a6b5018c4c7d2982e8ad23bc13257437272af13b5284b55d72f2c6ef2f72ff99fdf56e610aad07b87087524ef037
|
7
|
+
data.tar.gz: dc231523581f3a6125b9a94af667adeb2c6c4211143f8c614655b2da42b3efff6e23cc2b18df05dfc8c9ceacb8768a4a66b1309e3181454c65a8e07f0df71747
|
data/LICENSE.md
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2019 Josh Temple
|
4
|
+
Copyright (c) 2025 Sylvain Utard
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
7
|
+
|
8
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
9
|
+
|
10
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# Lkml
|
2
|
+
|
3
|
+
A LookML parser and serializer implemented in pure Ruby.
|
4
|
+
|
5
|
+
> This is a Ruby rewrite of the amazing [joshtemple/lkml](https://github.com/joshtemple/lkml) python library.
|
6
|
+
|
7
|
+
Why should you use `lkml`?
|
8
|
+
|
9
|
+
- Tested on **over 160K lines of LookML** from public repositories on GitHub
|
10
|
+
- Written in pure, modern Ruby with **no external dependencies**
|
11
|
+
- A **full unit test suite** with excellent coverage
|
data/lib/lkml/keys.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Defines constant sequences of LookML keys and helper methods.
|
4
|
+
|
5
|
+
# These are repeatable keys in LookML that the parser should collapse into a single
|
6
|
+
# Ruby hash key. For example, LookML can have multiple dimensions, so the parser
|
7
|
+
# will combine those dimensions into an array of hashes with a top-level key,
|
8
|
+
# `dimensions`.
|
9
|
+
|
10
|
+
module Lkml
|
11
|
+
PLURAL_KEYS = %w[
|
12
|
+
access_filter
|
13
|
+
access_grant
|
14
|
+
action
|
15
|
+
aggregate_table
|
16
|
+
allowed_value
|
17
|
+
assert
|
18
|
+
bind_filters
|
19
|
+
column
|
20
|
+
constant
|
21
|
+
datagroup
|
22
|
+
remote_dependency
|
23
|
+
derived_column
|
24
|
+
dimension
|
25
|
+
dimension_group
|
26
|
+
explore
|
27
|
+
extends
|
28
|
+
filter
|
29
|
+
filters
|
30
|
+
form_param
|
31
|
+
include
|
32
|
+
join
|
33
|
+
link
|
34
|
+
map_layer
|
35
|
+
measure
|
36
|
+
named_value_format
|
37
|
+
option
|
38
|
+
override_constant
|
39
|
+
param
|
40
|
+
parameter
|
41
|
+
query
|
42
|
+
set
|
43
|
+
sql_step
|
44
|
+
test
|
45
|
+
user_attribute_param
|
46
|
+
view
|
47
|
+
when
|
48
|
+
].freeze
|
49
|
+
|
50
|
+
# These are keys in LookML that should be recognized as expression blocks (end with ;;).
|
51
|
+
|
52
|
+
EXPR_BLOCK_KEYS = %w[
|
53
|
+
expression_custom_filter
|
54
|
+
expression
|
55
|
+
html
|
56
|
+
sql_trigger_value
|
57
|
+
sql_table_name
|
58
|
+
sql_distinct_key
|
59
|
+
sql_start
|
60
|
+
sql_always_having
|
61
|
+
sql_always_where
|
62
|
+
sql_trigger
|
63
|
+
sql_foreign_key
|
64
|
+
sql_where
|
65
|
+
sql_end
|
66
|
+
sql_create
|
67
|
+
sql_latitude
|
68
|
+
sql_longitude
|
69
|
+
sql_step
|
70
|
+
sql_on
|
71
|
+
sql
|
72
|
+
sql_preamble
|
73
|
+
].freeze
|
74
|
+
|
75
|
+
# These are keys that the serializer should quote the value of (e.g. `label: "Label"`).
|
76
|
+
# An example of an unquoted literal would be `hidden: no`.
|
77
|
+
|
78
|
+
QUOTED_LITERAL_KEYS = %w[
|
79
|
+
label
|
80
|
+
view_label
|
81
|
+
group_label
|
82
|
+
group_item_label
|
83
|
+
suggest_persist_for
|
84
|
+
default_value
|
85
|
+
direction
|
86
|
+
value_format
|
87
|
+
name
|
88
|
+
url
|
89
|
+
icon_url
|
90
|
+
form_url
|
91
|
+
default
|
92
|
+
tags
|
93
|
+
value
|
94
|
+
description
|
95
|
+
sortkeys
|
96
|
+
indexes
|
97
|
+
partition_keys
|
98
|
+
connection
|
99
|
+
include
|
100
|
+
max_cache_age
|
101
|
+
allowed_values
|
102
|
+
timezone
|
103
|
+
persist_for
|
104
|
+
cluster_keys
|
105
|
+
distribution
|
106
|
+
extents_json_url
|
107
|
+
feature_key
|
108
|
+
file
|
109
|
+
property_key
|
110
|
+
property_label_key
|
111
|
+
else
|
112
|
+
interval_trigger
|
113
|
+
].freeze
|
114
|
+
|
115
|
+
# These are keys for fields in Looker that have a "name" attribute. Since lkml uses the
|
116
|
+
# key `name` to represent the name of the field (e.g. for `dimension: dimension_name {`,
|
117
|
+
# the `name` key would hold the value `dimension_name`.)
|
118
|
+
|
119
|
+
KEYS_WITH_NAME_FIELDS = %w[
|
120
|
+
user_attribute_param
|
121
|
+
param
|
122
|
+
form_param
|
123
|
+
option
|
124
|
+
].freeze
|
125
|
+
end
|
data/lib/lkml/lexer.rb
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'keys'
|
4
|
+
require_relative 'tokens'
|
5
|
+
|
6
|
+
# Splits a LookML string into a sequence of tokens.
|
7
|
+
module Lkml
|
8
|
+
class Lexer
|
9
|
+
attr_reader :text, :index, :tokens, :line_number
|
10
|
+
|
11
|
+
CHARACTER_TO_TOKEN = {
|
12
|
+
"\0" => Tokens::StreamEndToken,
|
13
|
+
'{' => Tokens::BlockStartToken,
|
14
|
+
'}' => Tokens::BlockEndToken,
|
15
|
+
'[' => Tokens::ListStartToken,
|
16
|
+
']' => Tokens::ListEndToken,
|
17
|
+
',' => Tokens::CommaToken,
|
18
|
+
':' => Tokens::ValueToken,
|
19
|
+
';' => Tokens::ExpressionBlockEndToken
|
20
|
+
}.freeze
|
21
|
+
|
22
|
+
def initialize(text)
|
23
|
+
# Initializes the Lexer with a LookML string and sets the index.
|
24
|
+
@text = "#{text}\u0000"
|
25
|
+
@index = 0
|
26
|
+
@tokens = []
|
27
|
+
@line_number = 1
|
28
|
+
end
|
29
|
+
|
30
|
+
def peek
|
31
|
+
# Returns the character at the current index of the text being lexed.
|
32
|
+
@text[@index]
|
33
|
+
end
|
34
|
+
|
35
|
+
def peek_multiple(length)
|
36
|
+
# Returns the next n characters from the current index in the text being lexed.
|
37
|
+
@text[@index, length]
|
38
|
+
end
|
39
|
+
|
40
|
+
def advance(length = 1)
|
41
|
+
# Moves the index forward by n characters.
|
42
|
+
@index += length
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def consume
|
47
|
+
# Returns the current index character and advances the index 1 character.
|
48
|
+
advance
|
49
|
+
@text[@index - 1]
|
50
|
+
end
|
51
|
+
|
52
|
+
def scan # rubocop:disable Metrics/CyclomaticComplexity
|
53
|
+
# Tokenizes LookML into a sequence of tokens.
|
54
|
+
@tokens << Tokens::StreamStartToken.new(@line_number)
|
55
|
+
loop do
|
56
|
+
ch = peek
|
57
|
+
case ch
|
58
|
+
when "\0"
|
59
|
+
@tokens << CHARACTER_TO_TOKEN[ch].new(@line_number)
|
60
|
+
break
|
61
|
+
when "\n", "\t", ' '
|
62
|
+
@tokens << scan_whitespace
|
63
|
+
when '#'
|
64
|
+
advance
|
65
|
+
@tokens << scan_comment
|
66
|
+
when ';'
|
67
|
+
if peek_multiple(2) == ';;'
|
68
|
+
advance(2)
|
69
|
+
@tokens << CHARACTER_TO_TOKEN[ch].new(@line_number)
|
70
|
+
end
|
71
|
+
when '"'
|
72
|
+
advance
|
73
|
+
@tokens << scan_quoted_literal
|
74
|
+
when *CHARACTER_TO_TOKEN.keys
|
75
|
+
advance
|
76
|
+
@tokens << CHARACTER_TO_TOKEN[ch].new(@line_number)
|
77
|
+
else
|
78
|
+
if self.class.check_for_expression_block(peek_multiple(25))
|
79
|
+
# TODO: Handle edges here with whitespace and comments
|
80
|
+
@tokens << scan_literal
|
81
|
+
advance
|
82
|
+
@tokens << Tokens::ValueToken.new(@line_number)
|
83
|
+
@tokens << scan_expression_block
|
84
|
+
else
|
85
|
+
# TODO: This should actually check for valid literals first
|
86
|
+
# and throw an error if it doesn't match
|
87
|
+
@tokens << scan_literal
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
@tokens
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.check_for_expression_block(string)
|
95
|
+
# Returns true if the input string is an expression block.
|
96
|
+
EXPR_BLOCK_KEYS.any? { |key| string.start_with?("#{key}:") }
|
97
|
+
end
|
98
|
+
|
99
|
+
def scan_whitespace
|
100
|
+
# Returns a token from one or more whitespace characters.
|
101
|
+
chars = ''
|
102
|
+
next_char = peek
|
103
|
+
while ["\n", "\t", ' '].include?(next_char)
|
104
|
+
if next_char == "\n"
|
105
|
+
while next_char == "\n"
|
106
|
+
chars += consume
|
107
|
+
@line_number += 1
|
108
|
+
next_char = peek
|
109
|
+
end
|
110
|
+
return Tokens::LinebreakToken.new(chars, @line_number)
|
111
|
+
else
|
112
|
+
chars += consume
|
113
|
+
next_char = peek
|
114
|
+
end
|
115
|
+
end
|
116
|
+
Tokens::InlineWhitespaceToken.new(chars, @line_number)
|
117
|
+
end
|
118
|
+
|
119
|
+
def scan_comment
|
120
|
+
# Returns a token from a comment.
|
121
|
+
chars = '#'
|
122
|
+
chars += consume until ["\0", "\n"].include?(peek)
|
123
|
+
Tokens::CommentToken.new(chars, @line_number)
|
124
|
+
end
|
125
|
+
|
126
|
+
def scan_expression_block
|
127
|
+
# Returns a token from an expression block string.
|
128
|
+
chars = ''
|
129
|
+
while peek_multiple(2) != ';;'
|
130
|
+
@line_number += 1 if peek == "\n"
|
131
|
+
chars += consume
|
132
|
+
end
|
133
|
+
Tokens::ExpressionBlockToken.new(chars, @line_number)
|
134
|
+
end
|
135
|
+
|
136
|
+
def scan_literal
|
137
|
+
# Returns a token from a literal string.
|
138
|
+
chars = ''
|
139
|
+
chars += consume until ["\0", ' ', "\n", "\t", ':', '}', '{', ',', ']'].include?(peek)
|
140
|
+
Tokens::LiteralToken.new(chars, @line_number)
|
141
|
+
end
|
142
|
+
|
143
|
+
def scan_quoted_literal
|
144
|
+
# Returns a token from a quoted literal string.
|
145
|
+
chars = ''
|
146
|
+
loop do
|
147
|
+
ch = peek
|
148
|
+
break if ch == '"'
|
149
|
+
|
150
|
+
if ch == '\\'
|
151
|
+
chars += consume # Extra consume to skip the escaped character
|
152
|
+
elsif ch == "\n"
|
153
|
+
@line_number += 1
|
154
|
+
end
|
155
|
+
chars += consume
|
156
|
+
end
|
157
|
+
advance
|
158
|
+
Tokens::QuotedLiteralToken.new(chars, @line_number)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
data/lib/lkml/parser.rb
ADDED
@@ -0,0 +1,338 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Parses a sequence of tokenized LookML into a parse tree.
|
4
|
+
|
5
|
+
require_relative 'tokens'
|
6
|
+
require_relative 'tree'
|
7
|
+
|
8
|
+
module Lkml
|
9
|
+
class CommaSeparatedValues
|
10
|
+
attr_accessor :trailing_comma, :leading_comma
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@_values = []
|
14
|
+
@trailing_comma = nil
|
15
|
+
@leading_comma = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def append(value)
|
19
|
+
@_values << value
|
20
|
+
end
|
21
|
+
|
22
|
+
def values
|
23
|
+
@_values
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Parser
|
28
|
+
attr_accessor :tokens, :index, :progress, :depth, :log_debug
|
29
|
+
|
30
|
+
def initialize(stream)
|
31
|
+
stream.each { |token| raise TypeError, "Unsupported token: #{token}" unless token.is_a?(Tokens::Token) }
|
32
|
+
@tokens = stream
|
33
|
+
@index = 0
|
34
|
+
@progress = 0
|
35
|
+
@depth = -1
|
36
|
+
end
|
37
|
+
|
38
|
+
def jump_to_index(index)
|
39
|
+
@index = index
|
40
|
+
end
|
41
|
+
|
42
|
+
def peek
|
43
|
+
@tokens[@index]
|
44
|
+
end
|
45
|
+
|
46
|
+
def advance(length = 1)
|
47
|
+
@index += length
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
|
51
|
+
def consume
|
52
|
+
advance
|
53
|
+
@tokens[@index - 1]
|
54
|
+
end
|
55
|
+
|
56
|
+
def consume_token_value
|
57
|
+
token = consume
|
58
|
+
raise "Token #{token} does not have a consumable value." if token.value.nil?
|
59
|
+
|
60
|
+
token.value
|
61
|
+
end
|
62
|
+
|
63
|
+
def consume_trivia(only_newlines: false)
|
64
|
+
valid_tokens = [Tokens::CommentToken]
|
65
|
+
valid_tokens += only_newlines ? [Tokens::LinebreakToken] : [Tokens::WhitespaceToken]
|
66
|
+
|
67
|
+
trivia = ''
|
68
|
+
loop do
|
69
|
+
break unless check(*valid_tokens)
|
70
|
+
|
71
|
+
trivia += consume_token_value
|
72
|
+
end
|
73
|
+
trivia
|
74
|
+
end
|
75
|
+
|
76
|
+
def check(*token_types, skip_trivia: false)
|
77
|
+
mark = @index
|
78
|
+
consume_trivia if skip_trivia
|
79
|
+
|
80
|
+
token = begin
|
81
|
+
peek
|
82
|
+
rescue StandardError
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
result = token_types.any? { |type| token.is_a?(type) }
|
86
|
+
|
87
|
+
jump_to_index(mark) if skip_trivia
|
88
|
+
result
|
89
|
+
end
|
90
|
+
|
91
|
+
def parse
|
92
|
+
advance if check(Tokens::StreamStartToken)
|
93
|
+
prefix = consume_trivia
|
94
|
+
container = parse_container
|
95
|
+
suffix = consume_trivia
|
96
|
+
DocumentNode.new(container, prefix: prefix, suffix: suffix)
|
97
|
+
end
|
98
|
+
|
99
|
+
def parse_container
|
100
|
+
items = []
|
101
|
+
until check(Tokens::StreamEndToken, Tokens::BlockEndToken, skip_trivia: true)
|
102
|
+
block = parse_block
|
103
|
+
if block
|
104
|
+
items << block
|
105
|
+
next
|
106
|
+
end
|
107
|
+
|
108
|
+
pair = parse_pair
|
109
|
+
if pair
|
110
|
+
items << pair
|
111
|
+
next
|
112
|
+
end
|
113
|
+
|
114
|
+
list = parse_list
|
115
|
+
if list
|
116
|
+
items << list
|
117
|
+
next
|
118
|
+
end
|
119
|
+
|
120
|
+
token = @tokens[@progress]
|
121
|
+
raise SyntaxError, "Unable to find a matching expression for '#{token}' on line #{token.line_number}"
|
122
|
+
end
|
123
|
+
|
124
|
+
ContainerNode.new(items, top_level: @depth.zero?)
|
125
|
+
end
|
126
|
+
|
127
|
+
def parse_block
|
128
|
+
key = parse_key
|
129
|
+
return key if key.nil?
|
130
|
+
|
131
|
+
name = if check(Tokens::LiteralToken)
|
132
|
+
token = consume
|
133
|
+
SyntaxToken.new(token.value, token.line_number)
|
134
|
+
end
|
135
|
+
|
136
|
+
prefix = consume_trivia
|
137
|
+
return nil unless check(Tokens::BlockStartToken)
|
138
|
+
|
139
|
+
advance
|
140
|
+
suffix = consume_trivia
|
141
|
+
left_brace = LeftCurlyBrace.new(prefix: prefix, suffix: suffix)
|
142
|
+
|
143
|
+
container = parse_container
|
144
|
+
|
145
|
+
prefix = consume_trivia
|
146
|
+
return unless check(Tokens::BlockEndToken)
|
147
|
+
|
148
|
+
advance
|
149
|
+
suffix = consume_trivia(only_newlines: true)
|
150
|
+
right_brace = RightCurlyBrace.new(prefix: prefix, suffix: suffix)
|
151
|
+
|
152
|
+
BlockNode.new(
|
153
|
+
key[0],
|
154
|
+
colon: key[1],
|
155
|
+
name: name,
|
156
|
+
left_brace: left_brace,
|
157
|
+
container: container,
|
158
|
+
right_brace: right_brace
|
159
|
+
)
|
160
|
+
end
|
161
|
+
|
162
|
+
def parse_pair
|
163
|
+
key = parse_key
|
164
|
+
return nil if key.nil?
|
165
|
+
|
166
|
+
value = parse_value(parse_prefix: true, parse_suffix: true)
|
167
|
+
return nil if value.nil?
|
168
|
+
|
169
|
+
PairNode.new(key[0], value, colon: key[1])
|
170
|
+
end
|
171
|
+
|
172
|
+
def parse_key
|
173
|
+
prefix = consume_trivia
|
174
|
+
return nil unless check(Tokens::LiteralToken)
|
175
|
+
|
176
|
+
token = consume
|
177
|
+
key = SyntaxToken.new(token.value, token.line_number, prefix: prefix)
|
178
|
+
|
179
|
+
prefix = consume_trivia
|
180
|
+
|
181
|
+
colon = nil
|
182
|
+
while check(Tokens::ValueToken)
|
183
|
+
token = consume
|
184
|
+
suffix = consume_trivia
|
185
|
+
colon = Colon.new(token.line_number, prefix: prefix, suffix: suffix)
|
186
|
+
end
|
187
|
+
return nil unless colon
|
188
|
+
|
189
|
+
[key, colon]
|
190
|
+
end
|
191
|
+
|
192
|
+
def parse_value(parse_prefix: false, parse_suffix: false) # rubocop:disable Metrics/CyclomaticComplexity
|
193
|
+
prefix = parse_prefix ? consume_trivia : ''
|
194
|
+
|
195
|
+
if check(Tokens::LiteralToken)
|
196
|
+
token = consume
|
197
|
+
if token.value == '-' && consume_trivia
|
198
|
+
return nil unless check(Tokens::LiteralToken)
|
199
|
+
|
200
|
+
token = consume
|
201
|
+
token.value = "-#{token.value}"
|
202
|
+
|
203
|
+
end
|
204
|
+
suffix = parse_suffix ? consume_trivia : ''
|
205
|
+
SyntaxToken.new(token.value, token.line_number, prefix: prefix, suffix: suffix)
|
206
|
+
elsif check(Tokens::QuotedLiteralToken)
|
207
|
+
token = consume
|
208
|
+
suffix = parse_suffix ? consume_trivia : ''
|
209
|
+
QuotedSyntaxToken.new(token.value, token.line_number, prefix: prefix, suffix: suffix)
|
210
|
+
elsif check(Tokens::ExpressionBlockToken)
|
211
|
+
token = consume
|
212
|
+
match = token.value.match(/\A(\s*)(.*?)(\s*)\z/)
|
213
|
+
expr_prefix, value, expr_suffix = if match
|
214
|
+
[match[1], match[2], match[3]]
|
215
|
+
else
|
216
|
+
['', token.value, '']
|
217
|
+
end
|
218
|
+
prefix += expr_prefix
|
219
|
+
|
220
|
+
return nil unless check(Tokens::ExpressionBlockEndToken)
|
221
|
+
|
222
|
+
advance
|
223
|
+
|
224
|
+
suffix = parse_suffix ? consume_trivia : ''
|
225
|
+
ExpressionSyntaxToken.new(value, token.line_number, prefix: prefix, suffix: suffix, expr_suffix: expr_suffix)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def parse_list
|
230
|
+
key = parse_key
|
231
|
+
return key if key.nil?
|
232
|
+
|
233
|
+
prefix = consume_trivia
|
234
|
+
return nil unless check(Tokens::ListStartToken)
|
235
|
+
|
236
|
+
advance
|
237
|
+
left_bracket = LeftBracket.new(prefix: prefix)
|
238
|
+
|
239
|
+
csv = parse_csv || CommaSeparatedValues.new
|
240
|
+
|
241
|
+
return unless check(Tokens::ListEndToken, skip_trivia: true)
|
242
|
+
|
243
|
+
prefix = consume_trivia
|
244
|
+
advance
|
245
|
+
suffix = consume_trivia
|
246
|
+
right_bracket = RightBracket.new(prefix: prefix, suffix: suffix)
|
247
|
+
ListNode.new(
|
248
|
+
key[0],
|
249
|
+
items: csv.values,
|
250
|
+
left_bracket: left_bracket,
|
251
|
+
right_bracket: right_bracket,
|
252
|
+
colon: key[1],
|
253
|
+
leading_comma: csv.leading_comma,
|
254
|
+
trailing_comma: csv.trailing_comma
|
255
|
+
)
|
256
|
+
end
|
257
|
+
|
258
|
+
def parse_csv # rubocop:disable Metrics/CyclomaticComplexity
|
259
|
+
pair_mode = false
|
260
|
+
csv = CommaSeparatedValues.new
|
261
|
+
csv.leading_comma = parse_comma
|
262
|
+
|
263
|
+
pair = parse_pair
|
264
|
+
if pair
|
265
|
+
csv.append(pair)
|
266
|
+
pair_mode = true
|
267
|
+
elsif check(Tokens::LiteralToken, Tokens::QuotedLiteralToken, skip_trivia: true)
|
268
|
+
value = parse_value(parse_prefix: true, parse_suffix: true)
|
269
|
+
csv.append(value)
|
270
|
+
else
|
271
|
+
return nil
|
272
|
+
end
|
273
|
+
|
274
|
+
until check(Tokens::ListEndToken, skip_trivia: true)
|
275
|
+
return nil unless check(Tokens::CommaToken)
|
276
|
+
|
277
|
+
index = @index
|
278
|
+
advance
|
279
|
+
if check(Tokens::ListEndToken, skip_trivia: true)
|
280
|
+
jump_to_index(index)
|
281
|
+
csv.trailing_comma = parse_comma
|
282
|
+
break
|
283
|
+
end
|
284
|
+
|
285
|
+
if pair_mode
|
286
|
+
pair = parse_pair
|
287
|
+
return nil if pair.nil?
|
288
|
+
|
289
|
+
csv.append(pair)
|
290
|
+
elsif check(Tokens::LiteralToken, Tokens::QuotedLiteralToken, skip_trivia: true)
|
291
|
+
value = parse_value(parse_prefix: true, parse_suffix: true)
|
292
|
+
csv.append(value)
|
293
|
+
elsif check(Tokens::ListEndToken, skip_trivia: true)
|
294
|
+
break
|
295
|
+
else
|
296
|
+
return nil
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
csv
|
301
|
+
end
|
302
|
+
|
303
|
+
def parse_comma
|
304
|
+
prefix = consume_trivia
|
305
|
+
|
306
|
+
return unless check(Tokens::CommaToken)
|
307
|
+
|
308
|
+
advance
|
309
|
+
suffix = check(Tokens::ListEndToken, skip_trivia: true) ? '' : consume_trivia
|
310
|
+
Comma.new(prefix: prefix, suffix: suffix)
|
311
|
+
end
|
312
|
+
|
313
|
+
def self.backtrack_if_none(method_name)
|
314
|
+
original_method = instance_method(method_name)
|
315
|
+
|
316
|
+
define_method(method_name) do |*args, **kwargs|
|
317
|
+
mark = @index
|
318
|
+
@depth += 1
|
319
|
+
result = original_method.bind(self).call(*args, **kwargs)
|
320
|
+
@depth -= 1
|
321
|
+
if result.nil?
|
322
|
+
@progress = [@index, @progress].max
|
323
|
+
jump_to_index(mark)
|
324
|
+
end
|
325
|
+
result
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
backtrack_if_none :parse_container
|
330
|
+
backtrack_if_none :parse_block
|
331
|
+
backtrack_if_none :parse_pair
|
332
|
+
backtrack_if_none :parse_key
|
333
|
+
backtrack_if_none :parse_value
|
334
|
+
backtrack_if_none :parse_list
|
335
|
+
backtrack_if_none :parse_csv
|
336
|
+
backtrack_if_none :parse_comma
|
337
|
+
end
|
338
|
+
end
|