Class: Crass::Parser

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

Overview

Parses a CSS string or list of tokens.

http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#parsing

Constant Summary collapse

BLOCK_END_TOKENS =
{
  :'{' => :'}',
  :'[' => :']',
  :'(' => :')'
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(input, options = {}) ⇒ Parser

Initializes a parser based on the given input, which may be a CSS string or an array of tokens.

See Tokenizer#initialize for options.



126
127
128
129
130
131
132
# File 'lib/crass/parser.rb', line 126

def initialize(input, options = {})
  unless input.kind_of?(Enumerable)
    input = Tokenizer.tokenize(input, options)
  end

  @tokens = TokenScanner.new(input)
end

Instance Attribute Details

#tokensObject (readonly)

TokenScanner wrapping the tokens generated from this parser's input.



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

def tokens
  @tokens
end

Class Method Details

.parse_properties(input, options = {}) ⇒ Object

Parses CSS properties (such as the contents of an HTML element's style attribute) and returns a parse tree.

See Tokenizer#initialize for options.

http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#parse-a-list-of-declarations



25
26
27
# File 'lib/crass/parser.rb', line 25

def self.parse_properties(input, options = {})
  Parser.new(input, options).parse_properties
end

.parse_rules(input, options = {}) ⇒ Object

Parses CSS rules (such as the content of a @media block) and returns a parse tree. The only difference from #parse_stylesheet is that CDO/CDC nodes (<!-- and -->) aren't ignored.

See Tokenizer#initialize for options.

http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#parse-a-list-of-rules



36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/crass/parser.rb', line 36

def self.parse_rules(input, options = {})
  parser = Parser.new(input, options)
  rules  = parser.consume_rules

  rules.map do |rule|
    if rule[:node] == :qualified_rule
      parser.create_style_rule(rule)
    else
      rule
    end
  end
end

.parse_stylesheet(input, options = {}) ⇒ Object

Parses a CSS stylesheet and returns a parse tree.

See Tokenizer#initialize for options.

http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#parse-a-stylesheet



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

def self.parse_stylesheet(input, options = {})
  parser = Parser.new(input, options)
  rules  = parser.consume_rules(:top_level => true)

  rules.map do |rule|
    if rule[:node] == :qualified_rule
      parser.create_style_rule(rule)
    else
      rule
    end
  end
end

.stringify(nodes, options = {}) ⇒ Object

Converts a node or array of nodes into a CSS string based on their original tokenized input.

Options:

  • :exclude_comments - When true, comments will be excluded.


74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/crass/parser.rb', line 74

def self.stringify(nodes, options = {})
  nodes  = [nodes] unless nodes.is_a?(Array)
  string = ''

  nodes.each do |node|
    next if node.nil?

    case node[:node]
    when :at_rule
      string << node[:tokens].first[:raw]
      string << self.stringify(node[:prelude], options)

      if node[:block]
        string << self.stringify(node[:block], options)
      end

    when :comment
      string << node[:raw] unless options[:exclude_comments]

    when :property
      string << self.stringify(node[:tokens], options)

    when :simple_block
      string << node[:start]
      string << self.stringify(node[:value], options)
      string << node[:end]

    when :style_rule
      string << self.stringify(node[:selector][:tokens], options)
      string << "{#{self.stringify(node[:children], options)}}"

    else
      if node.key?(:raw)
        string << node[:raw]
      elsif node.key?(:tokens)
        string << self.stringify(node[:tokens], options)
      end
    end
  end

  string
end

Instance Method Details

#consume_at_rule(input = @tokens) ⇒ Object



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/crass/parser.rb', line 137

def consume_at_rule(input = @tokens)
  rule = {}

  rule[:tokens] = input.collect do
    rule[:name]    = input.consume[:value]
    rule[:prelude] = []

    while token = input.consume
      case token[:node]
      when :comment
        next

      when :semicolon
        break

      when :'{'
        rule[:block] = consume_simple_block(input)
        break

      when :simple_block
        if token[:start] == '{'
          rule[:block] = token
          break
        else
          input.reconsume
          rule[:prelude] << consume_component_value(input)
        end

      else
        input.reconsume
        rule[:prelude] << consume_component_value(input)
      end
    end
  end

  create_node(:at_rule, rule)
end

#consume_component_value(input = @tokens) ⇒ Object



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/crass/parser.rb', line 178

def consume_component_value(input = @tokens)
  return nil unless token = input.consume

  case token[:node]
  when :'{', :'[', :'('
    consume_simple_block(input)

  when :function
    if token.key?(:name)
      # This is a parsed function, not a function token.
      token
    else
      consume_function(input)
    end

  else
    token
  end
end

#consume_declaration(input = @tokens) ⇒ Object

Consumes a declaration and returns it, or nil on parse error.

http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-declaration



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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/crass/parser.rb', line 201

def consume_declaration(input = @tokens)
  declaration = {}
  value       = []

  declaration[:tokens] = input.collect do
    declaration[:name] = input.consume[:value]

    token = input.consume
    token = input.consume while token && token[:node] == :whitespace

    return nil if !token || token[:node] != :colon # TODO: parse error
    value << token while token = input.consume
  end

  # Look for !important.
  pos = -1
  while token = value[pos]
    type = token[:node]

    if type == :whitespace || type == :comment || type == :semicolon
      pos -= 1
      next
    end

    if type == :ident && token[:value].downcase == 'important'
      prev_token = value[pos - 1]

      if prev_token && prev_token[:node] == :delim &&
          prev_token[:value] == '!'

        declaration[:important] = true
        value.slice!(pos - 1, 2)
      else
        break
      end
    else
      break
    end
  end

  declaration[:value] = value
  create_node(:declaration, declaration)
end

#consume_declarations(input = @tokens) ⇒ Object

Consumes a list of declarations and returns them.

NOTE: The returned list may include :comment, :semicolon, and :whitespace nodes, which is non-standard.

http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-list-of-declarations



251
252
253
254
255
256
257
258
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
# File 'lib/crass/parser.rb', line 251

def consume_declarations(input = @tokens)
  declarations = []

  while token = input.consume
    case token[:node]
    when :comment, :semicolon, :whitespace
      declarations << token

    when :at_keyword
      # TODO: this is technically a parse error when parsing a style rule,
      # but not necessarily at other times.

      # Note: The spec doesn't say we should reconsume here, but it's
      # necessary since `consume_at_rule` must consume the `:at_keyword` as
      # the rule's name or it'll end up in the prelude. The spec *does* say
      # we should reconsume when an `:at_keyword` is encountered in
      # `consume_rules`, so we either have to reconsume in both places or in
      # neither place. I've chosen to reconsume in both places.
      input.reconsume
      declarations << consume_at_rule(input)

    when :ident
      decl_tokens = [token]
      input.consume

      while input.current
        decl_tokens << input.current
        break if input.current[:node] == :semicolon
        input.consume
      end

      if decl = consume_declaration(TokenScanner.new(decl_tokens))
        declarations << decl
      end

    else
      # TODO: parse error (invalid property name, etc.)
      while token && token[:node] != :semicolon
        token = consume_component_value(input)
      end
    end
  end

  declarations
end

#consume_function(input = @tokens) ⇒ Object



300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/crass/parser.rb', line 300

def consume_function(input = @tokens)
  function = {
    :name   => input.current[:value],
    :value  => [],
    :tokens => [input.current]
  }

  function[:tokens].concat(input.collect {
    while token = input.consume
      case token[:node]
      when :')'
        break

      when :comment
        next

      else
        input.reconsume
        function[:value] << consume_component_value(input)
      end
    end
  })

  create_node(:function, function)
end

#consume_qualified_rule(input = @tokens) ⇒ Object

Consumes a qualified rule and returns it, or nil if a parse error occurs.

http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-qualified-rule



330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/crass/parser.rb', line 330

def consume_qualified_rule(input = @tokens)
  rule = {:prelude => []}

  rule[:tokens] = input.collect do
    while true
      return nil unless token = input.consume

      if token[:node] == :'{'
        rule[:block] = consume_simple_block(input)
        break
      elsif token[:node] == :simple_block
        rule[:block] = token
        break
      else
        input.reconsume
        rule[:prelude] << consume_component_value(input)
      end
    end
  end

  create_node(:qualified_rule, rule)
end

#consume_rules(flags = {}) ⇒ Object



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
# File 'lib/crass/parser.rb', line 356

def consume_rules(flags = {})
  rules = []

  while token = @tokens.consume
    case token[:node]
      when :comment, :whitespace
        rules << token

      when :cdc, :cdo
        unless flags[:top_level]
          @tokens.reconsume
          rule = consume_qualified_rule
          rules << rule if rule
        end

      when :at_keyword
        @tokens.reconsume
        rule = consume_at_rule
        rules << rule if rule

      else
        @tokens.reconsume
        rule = consume_qualified_rule
        rules << rule if rule
    end
  end

  rules
end

#consume_simple_block(input = @tokens) ⇒ Object

Consumes and returns a simple block associated with the current input token.

http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-simple-block0



390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
# File 'lib/crass/parser.rb', line 390

def consume_simple_block(input = @tokens)
  start_token = input.current[:node]
  end_token   = BLOCK_END_TOKENS[start_token]

  block = {
    :start  => start_token.to_s,
    :end    => end_token.to_s,
    :value  => [],
    :tokens => [input.current]
  }

  block[:tokens].concat(input.collect do
    while token = input.consume
      break if token[:node] == end_token

      input.reconsume
      block[:value] << consume_component_value(input)
    end
  end)

  create_node(:simple_block, block)
end

#create_node(type, properties = {}) ⇒ Object

Creates and returns a new parse node with the given properties.



414
415
416
# File 'lib/crass/parser.rb', line 414

def create_node(type, properties = {})
  {:node => type}.merge!(properties)
end

#create_selector(input) ⇒ Object

Parses the given input tokens into a selector node and returns it.

Doesn't bother splitting the selector list into individual selectors or validating them. Feel free to do that yourself! It'll be fun!



422
423
424
425
426
# File 'lib/crass/parser.rb', line 422

def create_selector(input)
  create_node(:selector,
    :value  => parse_value(input),
    :tokens => input)
end

#create_style_rule(rule) ⇒ Object



433
434
435
436
437
# File 'lib/crass/parser.rb', line 433

def create_style_rule(rule)
  create_node(:style_rule,
    :selector => create_selector(rule[:prelude]),
    :children => parse_properties(rule[:block][:value]))
end

#parse_properties(input = @tokens) ⇒ Object

Parses a list of declarations and returns an array of :property nodes (and any non-declaration nodes that were in the input). This is useful for parsing the contents of an HTML element's style attribute.

http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#parse-a-list-of-declarations



444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
# File 'lib/crass/parser.rb', line 444

def parse_properties(input = @tokens)
  input      = TokenScanner.new(input) unless input.is_a?(TokenScanner)
  properties = []

  consume_declarations(input).each do |decl|
    unless decl[:node] == :declaration
      properties << decl
      next
    end

    children = decl[:value].dup
    children.pop if children.last[:node] == :semicolon

    properties << create_node(:property,
      :name      => decl[:name],
      :value     => parse_value(decl[:value]),
      :children  => children,
      :important => decl[:important] == true,
      :tokens    => decl[:tokens])
  end

  properties
end

#parse_value(nodes) ⇒ Object

Returns the unescaped value of a selector name or property declaration.



469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
# File 'lib/crass/parser.rb', line 469

def parse_value(nodes)
  nodes  = [nodes] unless nodes.is_a?(Array)
  string = ''

  nodes.each do |node|
    case node[:node]
    when :comment, :semicolon
      next

    when :at_keyword, :ident
      string << node[:value]

    when :function
      if node[:value].is_a?(String)
        string << node[:value]
        string << '('
      else
        string << parse_value(node[:tokens])
      end

    else
      if node.key?(:raw)
        string << node[:raw]
      elsif node.key?(:tokens)
        string << parse_value(node[:tokens])
      end
    end
  end

  string.strip
end