Class: MiniCSS::CSS::Parser

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

Instance Method Summary collapse

Constructor Details

#initialize(tokens) ⇒ Parser

Returns a new instance of Parser.



8
9
10
11
# File 'lib/minicss/css/parser.rb', line 8

def initialize(tokens)
  @stream = TokenStream.new(tokens)
  @tokens = tokens
end

Instance Method Details

#assert_next_token(kind) ⇒ Object

Helpers



78
79
80
# File 'lib/minicss/css/parser.rb', line 78

def assert_next_token(kind)
  @stream.peek.kind == kind
end

#consume_at_rule(nested: false) ⇒ Object



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/minicss/css/parser.rb', line 105

def consume_at_rule(nested: false)
  return unless assert_next_token(:at_keyword)

  rule = AST::AtRule.new(name: @stream.consume)
  loop do
    case @stream.peek.kind
    when :semicolon, :eof
      @stream.discard
      # TODO: If rule is valid in the current context, return it; otherwise return nothing.
      return rule

    when :right_curly
      if nested
        # TODO: If rule is valid in the current context, return it. Otherwise, return nothing.
        return rule
      end

      rule.prelude << @stream.consume

    when :left_curly
      rule.child_rules.append(*consume_block)
      return rule

    else
      rule.prelude << consume_component_value
    end
  end
end

#consume_bad_declaration(nested:) ⇒ Object



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/minicss/css/parser.rb', line 273

def consume_bad_declaration(nested:)
  loop do
    case @stream.peek.kind
    when :eof, :semicolon
      @stream.discard
      return nil
    when :right_curly
      return if nested

      @stream.discard
    else
      consume_component_value
    end
  end
end

#consume_blockObject



175
176
177
178
179
180
181
182
# File 'lib/minicss/css/parser.rb', line 175

def consume_block
  return unless assert_next_token(:left_curly)

  @stream.discard
  rules = consume_block_contents
  @stream.discard
  rules
end

#consume_block_contentsObject



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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/minicss/css/parser.rb', line 184

def consume_block_contents
  rules = []
  decls = AST::DeclarationList.new

  loop do
    case @stream.peek.kind
    when :whitespace, :semicolon
      @stream.discard
    when :eof, :right_curly
      rules << decls unless decls.empty?
      return rules
    when :at_keyword
      unless decls.empty?
        rules << decls
        decls = AST::DeclarationList.new
      end
      at = consume_at_rule(nested: true)
      rules << at if at.is_a? AST::AtRule
    else
      @stream.mark
      decl = consume_declaration(nested: true)
      if decl.is_a? AST::Declaration
        decls << decl
        @stream.pop
        next
      end

      @stream.restore
      rule = consume_qualified_rule(nested: true)
      if rule.is_a? InvalidRuleError
        unless decls.empty?
          rules << decls
          decls = AST::DeclarationList.new
        end
      elsif rule.is_a? AST::QualifiedRule
        unless decls.empty?
          rules << decls
          decls = AST::DeclarationList.new
        end
        rules << rule
      end
    end
  end
end

#consume_component_valueObject



307
308
309
310
311
312
313
314
315
316
# File 'lib/minicss/css/parser.rb', line 307

def consume_component_value
  case @stream.peek_kind
  when :left_curly, :left_square_bracket, :left_parenthesis
    consume_simple_block
  when :function
    consume_function
  else
    @stream.consume
  end
end

#consume_component_value_list(stop: nil, nested: false) ⇒ Object



289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/minicss/css/parser.rb', line 289

def consume_component_value_list(stop: nil, nested: false)
  values = []
  loop do
    case @stream.peek.kind
    when :eof, stop
      return values

    when :right_curly
      return values if nested

      values << @stream.consume

    else
      values << consume_component_value
    end
  end
end

#consume_declaration(nested: false) ⇒ Object



229
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
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/minicss/css/parser.rb', line 229

def consume_declaration(nested: false)
  decl = AST::Declaration.new
  if @stream.peek.kind == :ident
    decl.name = @stream.consume
  else
    consume_bad_declaration(nested:)
    return nil
  end

  @stream.discard_whitespace

  if @stream.peek.kind == :colon
    @stream.discard
  else
    consume_bad_declaration(nested:)
    return nil
  end

  @stream.discard_whitespace

  decl.value = consume_component_value_list(nested:, stop: :semicolon)

  l1, l2 = decl.value.reject { it.kind == :whitespace }.last(2)
  if l1 && l2 && l1.kind == :delim && l1.literal == "!" && l2.kind == :ident && l2.literal.downcase == "important"
    decl.value.delete(l1)
    decl.value.delete(l2)
    decl.important = true
  end
  decl.value.pop while decl.value.last&.kind == :whitespace
  decl.original_text = decl.value.map(&:literal).join if decl.name.literal.start_with?("--")

  # Otherwise, if decl’s value contains a top-level simple block
  # with an associated token of <{-token>, and also contains any other
  # non-<whitespace-token> value, return nothing. (That is, a top-level
  # {}-block is only allowed as the entire value of a non-custom property.)
  block = decl.value.find { it.is_a?(AST::SimpleBlock) && it.associated_token == :left_curly }
  if block
    invalid = decl.value.any? { it != block && it.kind != :whitespace }
    return nil if invalid
  end

  decl
end

#consume_functionObject



339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/minicss/css/parser.rb', line 339

def consume_function
  return unless assert_next_token(:function)

  func = AST::Function.new(name: @stream.consume)

  loop do
    case @stream.peek.kind
    when :eof, :right_parenthesis
      @stream.discard
      return func
    else
      func.value << consume_component_value
    end
  end
end

#consume_qualified_rule(stop: nil, nested: false) ⇒ Object



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
167
168
169
170
171
172
173
# File 'lib/minicss/css/parser.rb', line 134

def consume_qualified_rule(stop: nil, nested: false)
  rule = AST::QualifiedRule.new

  loop do
    case @stream.peek.kind
    when :eof, stop
      return nil

    when :right_curly
      return nil if nested

      rule.prelude << @stream.consume

    when :left_curly
      non_ws = rule.prelude.reject { it.kind == :whitespace }
      first, second = non_ws.first(2)

      if first&.kind == :ident && first.literal.start_with?("--") && second&.kind == :colon
        nested ? consume_bad_declaration : consume_block
        return nil
      end

      child_rules = consume_block

      rule.decls = child_rules.shift if child_rules&.first.is_a?(AST::DeclarationList)

      # Replace declaration lists with nested declaration rules
      rule.child_rules = child_rules.map do |item|
        item.is_a?(AST::DeclarationList) ? AST::NestedDeclarationRules.new(child: item) : item
      end

      # TODO: If rule is valid in the current context, return it; otherwise
      # return an invalid rule error.
      return rule

    else
      rule.prelude << consume_component_value
    end
  end
end

#consume_simple_blockObject



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/minicss/css/parser.rb', line 318

def consume_simple_block
  return if !assert_next_token(:left_curly) && !assert_next_token(:left_square_bracket) && !assert_next_token(:left_parenthesis)

  block = AST::SimpleBlock.new(associated_token: @stream.consume.kind)
  ending_token = {
    left_curly: :right_curly,
    left_parenthesis: :right_parenthesis,
    left_square_bracket: :right_square_bracket
  }[block.associated_token]

  loop do
    case @stream.peek.kind
    when :eof, ending_token
      @stream.discard
      return block
    else
      block.value << consume_component_value
    end
  end
end

#consume_stylesheet_contentsObject

Parsers



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/minicss/css/parser.rb', line 84

def consume_stylesheet_contents
  rules = []
  loop do
    case @stream.peek.kind
    when :whitespace, :cdo, :cdc
      @stream.discard

    when :eof
      return rules

    when :at_keyword
      rule = consume_at_rule
      rules << rule if rule

    else
      rule = consume_qualified_rule
      rules << rule if rule
    end
  end
end

#parse_block_contentsObject



23
24
25
# File 'lib/minicss/css/parser.rb', line 23

def parse_block_contents
  consume_block_contents
end

#parse_component_valueObject



54
55
56
57
58
59
60
61
62
63
# File 'lib/minicss/css/parser.rb', line 54

def parse_component_value
  @stream.discard_whitespace
  return SyntaxError.new("empty") if @stream.empty?

  value = consume_component_value
  @stream.discard_whitespace
  return SyntaxError.new("extra-input") unless @stream.empty?

  value
end

#parse_component_value_comma_listObject



67
68
69
70
71
72
73
74
# File 'lib/minicss/css/parser.rb', line 67

def parse_component_value_comma_list
  groups = []
  until @stream.empty?
    groups << consume_component_value_list(stop: :comma)
    @stream.discard
  end
  groups
end

#parse_component_value_listObject



65
# File 'lib/minicss/css/parser.rb', line 65

def parse_component_value_list = consume_component_value_list

#parse_declarationObject



44
45
46
47
48
49
50
51
52
# File 'lib/minicss/css/parser.rb', line 44

def parse_declaration
  @stream.discard_whitespace
  return SyntaxError.new("empty") if @stream.empty?

  decl = consume_declaration
  return SyntaxError.new("invalid") unless decl

  decl
end

#parse_ruleObject



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/minicss/css/parser.rb', line 27

def parse_rule
  @stream.discard_whitespace
  return SyntaxError.new("empty") if @stream.empty?

  result = if @stream.peek.kind == :at_keyword
    consume_at_rule
  else
    consume_qualified_rule
  end
  return SyntaxError.new("invalid") unless result

  @stream.discard_whitespace
  return SyntaxError.new("extra-input") unless @stream.empty?

  result
end

#parse_stylesheetObject

Entrypoints



15
16
17
# File 'lib/minicss/css/parser.rb', line 15

def parse_stylesheet
  AST::Stylesheet.new(rules: consume_stylesheet_contents)
end

#parse_stylesheet_contentsObject



19
20
21
# File 'lib/minicss/css/parser.rb', line 19

def parse_stylesheet_contents
  consume_stylesheet_contents
end