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