lib/crass/parser.rb in crass-0.0.2 vs lib/crass/parser.rb in crass-0.1.0
- old
+ new
@@ -14,24 +14,54 @@
:'(' => :')'
}
# -- Class Methods ---------------------------------------------------------
+ # 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
+ def self.parse_properties(input, options = {})
+ Parser.new(input, options).parse_properties
+ end
+
+ # Parses a 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
+ 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
+
# 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
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
+ if rule[:node] == :qualified_rule
+ parser.create_style_rule(rule)
+ else
+ rule
end
end
end
# Converts a node or array of nodes into a CSS string based on their
@@ -44,24 +74,36 @@
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 :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)
+ 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)
@@ -72,11 +114,11 @@
string
end
# -- Instance Methods ------------------------------------------------------
- # Array of tokens generated from this parser's input.
+ # {TokenScanner} wrapping the tokens generated from this parser's input.
attr_reader :tokens
# Initializes a parser based on the given _input_, which may be a CSS string
# or an array of tokens.
#
@@ -94,11 +136,11 @@
# http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-an-at-rule
def consume_at_rule(input = @tokens)
rule = {}
rule[:tokens] = input.collect do
- rule[:name] = parse_value(input.consume)
+ rule[:name] = input.consume[:value]
rule[:prelude] = []
while token = input.consume
case token[:node]
when :comment then next
@@ -106,16 +148,18 @@
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?
+ 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
@@ -138,47 +182,61 @@
end
end
# 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
+ # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-declaration
def consume_declaration(input = @tokens)
declaration = {}
+ value = []
declaration[:tokens] = input.collect do
declaration[:name] = input.consume[:value]
- value = []
token = input.consume
- token = input.consume while token[:node] == :whitespace
+ token = input.consume while token && token[:node] == :whitespace
- return nil if token[:node] != :colon # TODO: parse error
-
+ return nil if !token || token[:node] != :colon # TODO: parse error
value << token while token = input.consume
- declaration[:value] = value
+ end
- maybe_important = value.reject {|v| v[:node] == :whitespace }[-2, 2]
+ # Look for !important.
+ pos = -1
+ while token = value[pos]
+ type = token[:node]
- if maybe_important &&
- maybe_important[0][:node] == :delim &&
- maybe_important[0][:value] == '!' &&
- maybe_important[1][:node] == :ident &&
- maybe_important[1][:value].downcase == 'important'
+ if type == :whitespace || type == :comment || type == :semicolon
+ pos -= 1
+ next
+ end
- declaration[:important] = true
+ 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
# 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
+ # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-list-of-declarations
def consume_declarations(input = @tokens)
declarations = []
while token = input.consume
case token[:node]
@@ -187,12 +245,17 @@
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.
+ # 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
@@ -245,30 +308,24 @@
end
# 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
+ # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-qualified-rule
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?
-
+ elsif token[:node] == :simple_block
+ rule[:block] = token
+ break
else
input.reconsume
rule[:prelude] << consume_component_value(input)
end
end
@@ -277,11 +334,11 @@
create_node(:qualified_rule, rule)
end
# Consumes a list of rules and returns them.
#
- # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-list-of-rules0
+ # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-list-of-rules
def consume_rules(flags = {})
rules = []
while true
return rules unless token = @tokens.consume
@@ -356,35 +413,43 @@
# it.
#
# * http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#style-rules
# * http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-list-of-declarations0
def create_style_rule(rule)
- children = []
- tokens = TokenScanner.new(rule[:block][:value])
+ create_node(:style_rule,
+ :selector => create_selector(rule[:prelude]),
+ :children => parse_properties(rule[:block][:value]))
+ end
- consume_declarations(tokens).each do |decl|
+ # 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
+ 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
- children << decl
+ properties << decl
next
end
- children << create_node(:property,
- :name => decl[:name],
- :value => parse_value(decl[:value]),
- :tokens => decl[:tokens])
+ properties << create_node(:property,
+ :name => decl[:name],
+ :value => parse_value(decl[:value]),
+ :important => decl[:important] == true,
+ :tokens => decl[:tokens])
end
- create_node(:style_rule,
- :selector => create_selector(rule[:prelude]),
- :children => children
- )
+ properties
end
# Returns the unescaped value of a selector name or property declaration.
def parse_value(nodes)
+ nodes = [nodes] unless nodes.is_a?(Array)
string = ''
-
- nodes = [nodes] unless nodes.is_a?(Array)
nodes.each do |node|
case node[:node]
when :comment, :semicolon then next