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.



84
85
86
87
88
89
90
# File 'lib/crass/parser.rb', line 84

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)

Array of tokens generated from this parser's input.



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

def tokens
  @tokens
end

Class Method Details

.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



24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/crass/parser.rb', line 24

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

  rules.map do |rule|
    case rule[:node]
    # TODO: handle at-rules
    when :qualified_rule then 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.


44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/crass/parser.rb', line 44

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

  nodes.each do |node|
    case node[:node]
    when :comment
      string << node[:raw] unless options[:exclude_comments]

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

    when :property
      string << options[:indent] if options[:indent]
      string << self.stringify(node[:tokens], 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



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/crass/parser.rb', line 95

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

  rule[:tokens] = input.collect do
    rule[:name]    = parse_value(input.consume)
    rule[:prelude] = []

    while token = input.consume
      case token[:node]
      when :comment then next
      when :semicolon, :eof then break

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

      # TODO: At this point, the spec says we should check for a "simple
      # block with an associated token of <<{-token>>", but isn't that
      # exactly what we just did above? And the tokenizer only ever produces
      # standalone <<{-token>>s, so how could the token stream ever contain
      # one that's already associated with a simple block? What am I
      # missing?

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

  create_node(:at_rule, rule)
end

#consume_component_value(input = @tokens) ⇒ Object



131
132
133
134
135
136
137
138
139
# File 'lib/crass/parser.rb', line 131

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

  case token[:node]
  when :'{', :'[', :'(' then consume_simple_block(input)
  when :function then consume_function(input)
  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-declaration0



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

def consume_declaration(input = @tokens)
  declaration = {}

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

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

    return nil if token[:node] != :colon # TODO: parse error

    value << token while token = input.consume
    declaration[:value] = value

    maybe_important = value.reject {|v| v[:node] == :whitespace }[-2, 2]

    if maybe_important &&
        maybe_important[0][:node] == :delim &&
        maybe_important[0][:value] == '!' &&
        maybe_important[1][:node] == :ident &&
        maybe_important[1][:value].downcase == 'important'

      declaration[:important] = true
    end
  end

  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-declarations0



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
212
213
214
215
216
217
218
219
# File 'lib/crass/parser.rb', line 180

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.

      # TODO: It seems like we should reconsume the current token here,
      # since that's what happens when consuming a list of rules.
      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



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/crass/parser.rb', line 224

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

  function[:tokens].concat(input.collect do
    while token = input.consume
      case token[:node]
      when :')', :eof then break
      when :comment then next

      else
        input.reconsume
        function[:value] << consume_component_value(input)
      end
    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-rule0



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

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 [simple block with an associated <<{-token>>??]

      # TODO: At this point, the spec says we should check for a "simple block
      # with an associated token of <<{-token>>", but isn't that exactly what
      # we just did above? And the tokenizer only ever produces standalone
      # <<{-token>>s, so how could the token stream ever contain one that's
      # already associated with a simple block? What am I missing?

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

  create_node(:qualified_rule, rule)
end

#consume_rules(flags = {}) ⇒ Object



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/crass/parser.rb', line 283

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

  while true
    return rules unless token = @tokens.consume

    case token[:node]
      when :comment, :whitespace then rules << token
      when :eof then return rules

      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
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



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

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 || token[:node] == :eof

      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.



341
342
343
# File 'lib/crass/parser.rb', line 341

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!



349
350
351
352
353
# File 'lib/crass/parser.rb', line 349

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

#create_style_rule(rule) ⇒ Object



360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/crass/parser.rb', line 360

def create_style_rule(rule)
  children = []
  tokens   = TokenScanner.new(rule[:block][:value])

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

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

  create_node(:style_rule,
    :selector => create_selector(rule[:prelude]),
    :children => children
  )
end

#parse_value(nodes) ⇒ Object

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



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

def parse_value(nodes)
  string = ''

  nodes = [nodes] unless nodes.is_a?(Array)

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

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

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

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

  string.strip
end