Class: Lkml::Parser

Inherits:
Object
  • Object
show all
Defined in:
lib/lkml/parser.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(stream) ⇒ Parser

Returns a new instance of Parser.



38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/lkml/parser.rb', line 38

def initialize(stream)
  stream.each do |token|
    raise TypeError, "Type #{token.class} for #{token} is not a valid token type." unless token.is_a?(Tokens::Token)
  end
  @tokens = stream
  @index = 0
  @progress = 0
  @depth = -1
  @logger = Logger.new($stderr)
  @logger.level = Logger::WARN
  @log_debug = false
end

Instance Attribute Details

#depthObject

Returns the value of attribute depth.



36
37
38
# File 'lib/lkml/parser.rb', line 36

def depth
  @depth
end

#indexObject

Returns the value of attribute index.



36
37
38
# File 'lib/lkml/parser.rb', line 36

def index
  @index
end

#log_debugObject

Returns the value of attribute log_debug.



36
37
38
# File 'lib/lkml/parser.rb', line 36

def log_debug
  @log_debug
end

#loggerObject (readonly)

Returns the value of attribute logger.



51
52
53
# File 'lib/lkml/parser.rb', line 51

def logger
  @logger
end

#progressObject

Returns the value of attribute progress.



36
37
38
# File 'lib/lkml/parser.rb', line 36

def progress
  @progress
end

#tokensObject

Returns the value of attribute tokens.



36
37
38
# File 'lib/lkml/parser.rb', line 36

def tokens
  @tokens
end

Instance Method Details

#advance(length = 1) ⇒ Object



61
62
63
64
# File 'lib/lkml/parser.rb', line 61

def advance(length = 1)
  @index += length
  nil
end

#check(*token_types, skip_trivia: false) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/lkml/parser.rb', line 94

def check(*token_types, skip_trivia: false)
  mark = @index
  consume_trivia if skip_trivia

  if @log_debug
    names = token_types.map(&:name).join(" or ")
    tok = peek_safe
    @logger.debug(format("%sCheck %s == %s", delimiter_repeat(1 + @depth), tok.inspect, names))
  end

  token_types.each do |token_type|
    unless token_type <= Tokens::Token || token_type == Tokens::Token
      raise TypeError, "#{token_type} is not a valid token type."
    end
  end

  token = begin
    @tokens.fetch(@index)
  rescue IndexError
    nil
  end
  result = token && token_types.any? { |t| token.is_a?(t) }
  jump_to_index(mark) if skip_trivia
  result
end

#consumeObject



66
67
68
69
# File 'lib/lkml/parser.rb', line 66

def consume
  advance
  @tokens[@index - 1]
end

#consume_token_valueObject

Raises:



71
72
73
74
75
76
# File 'lib/lkml/parser.rb', line 71

def consume_token_value
  token = consume
  raise AttributeError unless token.is_a?(Tokens::ContentToken)

  token.value
end

#consume_trivia(only_newlines: false) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/lkml/parser.rb', line 78

def consume_trivia(only_newlines: false)
  valid = if only_newlines
            [Tokens::CommentToken, Tokens::LinebreakToken]
          else
            [Tokens::CommentToken,
             Tokens::WhitespaceToken]
          end
  trivia = +""
  loop do
    break unless check(*valid)

    trivia += consume_token_value
  end
  trivia
end

#jump_to_index(idx) ⇒ Object



53
54
55
# File 'lib/lkml/parser.rb', line 53

def jump_to_index(idx)
  @index = idx
end

#parseObject



124
125
126
127
128
129
130
# File 'lib/lkml/parser.rb', line 124

def parse
  advance if check(Tokens::StreamStartToken)
  prefix = consume_trivia
  container = parse_container
  suffix = consume_trivia
  Tree::DocumentNode.new(container, prefix, suffix)
end

#parse_blockObject



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/lkml/parser.rb', line 168

def parse_block
  backtrack_if_none do
    if @log_debug
      grammar = "[block] = key literal? '{' expression '}'"
      @logger.debug(format("%sTry to parse %s", delimiter_repeat(@depth), grammar))
    end

    key = parse_key
    next unless key

    name = nil
    if check(Tokens::LiteralToken)
      token = consume
      name = Tree::SyntaxToken.new(token.value, token.line_number)
    end

    prefix = consume_trivia
    next nil unless check(Tokens::BlockStartToken)

    advance
    suffix = consume_trivia
    left_brace = Tree::LeftCurlyBrace.new("{", nil, prefix, suffix)

    container = parse_container

    prefix = consume_trivia
    next nil unless check(Tokens::BlockEndToken)

    advance
    suffix = consume_trivia(only_newlines: true)
    right_brace = Tree::RightCurlyBrace.new("}", nil, prefix, suffix)

    blk = Tree::BlockNode.new(
      type: key[0],
      colon: key[1],
      name: name,
      left_brace: left_brace,
      container: container,
      right_brace: right_brace
    )
    @logger.debug("#{delimiter_repeat(@depth)}Successfully parsed block.") if @log_debug
    blk
  end
end

#parse_commaObject



403
404
405
406
407
408
409
410
411
412
413
414
415
416
# File 'lib/lkml/parser.rb', line 403

def parse_comma
  backtrack_if_none do
    prefix = consume_trivia
    next nil unless check(Tokens::CommaToken)

    advance
    suffix = if check(Tokens::ListEndToken, skip_trivia: true)
               ""
             else
               consume_trivia
             end
    Tree::Comma.new(",", nil, prefix, suffix)
  end
end

#parse_containerObject



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/lkml/parser.rb', line 132

def parse_container
  backtrack_if_none do
    if @log_debug
      grammar = "[expression] = (block / pair / list)*"
      @logger.debug(format("%sTry to parse %s", delimiter_repeat(@depth), grammar))
    end
    items = []
    until check(Tokens::StreamEndToken, Tokens::BlockEndToken, skip_trivia: true)
      block = parse_block
      if block
        items << block
        next
      end

      pair = parse_pair
      if pair
        items << pair
        next
      end

      list = parse_list
      if list
        items << list
        next
      end

      token = @tokens[@progress]
      raise SyntaxError, "Unable to find a matching expression for '#{token.id}' " \
                         "on line #{token.line_number}"

    end

    Tree::ContainerNode.new(items: items.freeze, top_level: @depth.zero?)
  end
end

#parse_csvObject



339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/lkml/parser.rb', line 339

def parse_csv
  backtrack_if_none do
    if @log_debug
      grammar = "[csv] = \",\"? (literal / quoted_literal) (\",\" (literal / quoted_literal))* \",\"?"
      @logger.debug(format("%sTry to parse %s", delimiter_repeat(@depth), grammar))
    end

    pair_mode = false
    csv = CommaSeparatedValues.new
    csv.leading_comma = parse_comma

    pair = parse_pair
    if pair
      csv.append(pair)
      pair_mode = true
    elsif check(Tokens::LiteralToken, Tokens::QuotedLiteralToken, skip_trivia: true)
      value = parse_value(parse_prefix: true, parse_suffix: true)
      csv.append(value)
    else
      next nil
    end

    # `next` inside `until` continues the until-loop (Ruby), not the outer backtrack block — use break + flag.
    csv_abort = false
    until check(Tokens::ListEndToken, skip_trivia: true)
      unless check(Tokens::CommaToken)
        csv_abort = true
        break
      end

      comma_index = @index
      advance
      if check(Tokens::ListEndToken, skip_trivia: true)
        jump_to_index(comma_index)
        csv.trailing_comma = parse_comma
        break
      end

      if pair_mode
        pair = parse_pair
        unless pair
          csv_abort = true
          break
        end

        csv.append(pair)
      elsif check(Tokens::LiteralToken, Tokens::QuotedLiteralToken, skip_trivia: true)
        value = parse_value(parse_prefix: true, parse_suffix: true)
        csv.append(value)
      elsif check(Tokens::ListEndToken, skip_trivia: true)
        break
      else
        csv_abort = true
        break
      end
    end

    next nil if csv_abort

    @logger.debug(format("%sSuccessfully parsed comma-separated values.", delimiter_repeat(@depth))) if @log_debug
    csv
  end
end

#parse_keyObject



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/lkml/parser.rb', line 230

def parse_key
  backtrack_if_none do
    if @log_debug
      grammar = "[key] = literal ':'"
      @logger.debug(format("%sTry to parse %s", delimiter_repeat(@depth), grammar))
    end

    prefix = consume_trivia
    next nil unless check(Tokens::LiteralToken)

    token = consume
    key = Tree::SyntaxToken.new(token.value, token.line_number, prefix)

    prefix = consume_trivia

    colon = nil
    while check(Tokens::ValueToken)
      token = consume
      suffix = consume_trivia
      colon = Tree::Colon.new(":", token.line_number, prefix, suffix)
    end
    next unless colon

    @logger.debug(format("%sSuccessfully parsed key.", delimiter_repeat(@depth))) if @log_debug

    [key, colon]
  end
end

#parse_listObject



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/lkml/parser.rb', line 301

def parse_list
  backtrack_if_none do
    if @log_debug
      grammar = "[list] = key '[' csv? ']'"
      @logger.debug(format("%sTry to parse %s", delimiter_repeat(@depth), grammar))
    end

    key = parse_key
    next unless key

    prefix = consume_trivia
    next nil unless check(Tokens::ListStartToken)

    advance
    left_bracket = Tree::LeftBracket.new("[", nil, prefix, "")

    csv = parse_csv
    csv ||= CommaSeparatedValues.new

    next nil unless check(Tokens::ListEndToken, skip_trivia: true)

    prefix = consume_trivia
    advance
    suffix = consume_trivia
    right_bracket = Tree::RightBracket.new("]", nil, prefix, suffix)

    Tree::ListNode.new(
      type: key[0],
      colon: key[1],
      left_bracket: left_bracket,
      items: csv.frozen_tuple,
      right_bracket: right_bracket,
      leading_comma: csv.leading_comma,
      trailing_comma: csv.trailing_comma
    )
  end
end

#parse_pairObject



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/lkml/parser.rb', line 213

def parse_pair
  backtrack_if_none do
    if @log_debug
      grammar = "[pair] = key value"
      @logger.debug(format("%sTry to parse %s", delimiter_repeat(@depth), grammar))
    end

    key = parse_key
    next unless key

    value = parse_value(parse_prefix: true, parse_suffix: true)
    next unless value

    Tree::PairNode.new(type: key[0], value: value, colon: key[1])
  end
end

#parse_value(parse_prefix: false, parse_suffix: false) ⇒ Object



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/lkml/parser.rb', line 259

def parse_value(parse_prefix: false, parse_suffix: false)
  backtrack_if_none do
    if @log_debug
      grammar = "[value] = literal / quoted_literal / expression_block"
      @logger.debug(format("%sTry to parse %s", delimiter_repeat(@depth), grammar))
    end

    prefix = parse_prefix ? consume_trivia : ""

    if check(Tokens::LiteralToken)
      token = consume
      if token.value == "-" && !consume_trivia.empty?
        next nil unless check(Tokens::LiteralToken)

        token = consume
        token.value = "-#{token.value}"

      end
      suffix = parse_suffix ? consume_trivia : ""
      @logger.debug(format("%sSuccessfully parsed value.", delimiter_repeat(@depth))) if @log_debug
      Tree::SyntaxToken.new(token.value, token.line_number, prefix, suffix)
    elsif check(Tokens::QuotedLiteralToken)
      token = consume
      suffix = parse_suffix ? consume_trivia : ""
      @logger.debug(format("%sSuccessfully parsed value.", delimiter_repeat(@depth))) if @log_debug
      Tree::QuotedSyntaxToken.new(token.value, token.line_number, prefix, suffix)
    elsif check(Tokens::ExpressionBlockToken)
      token = consume
      expr_prefix, value, expr_suffix = Utils.strip(token.value)
      prefix += expr_prefix

      next nil unless check(Tokens::ExpressionBlockEndToken)

      advance

      suffix = parse_suffix ? consume_trivia : ""
      @logger.debug(format("%sSuccessfully parsed value.", delimiter_repeat(@depth))) if @log_debug
      Tree::ExpressionSyntaxToken.new(value, token.line_number, prefix, suffix, expr_suffix)
    end
  end
end

#peekObject



57
58
59
# File 'lib/lkml/parser.rb', line 57

def peek
  @tokens[@index]
end

#peek_safeObject



120
121
122
# File 'lib/lkml/parser.rb', line 120

def peek_safe
  @tokens.fetch(@index)
end