module HtmlConditionalComment class ParseError < StandardError def initialize(msg, example) super("#{msg} at \"#{example.slice(0, 25)}\"") end end ## # # Parse tokens into a tree of nodes # # Pseudo grammar # #template = { html | statement } #statement = "" , template , "" #expression = term [ "|" , term ] #term = factor [ "&" , factor ] #factor = subexpression | "!" , factor | "(" , expression , ")" #subexpression = [ operator ] browser | boolean #operator = "gt" | "gte" | "lt" | "lte" #boolean = "true" | "false" #browser = feature [ version_vector ] # class Parser def initialize(tokens) @symbol = nil @tokens = tokens @max_pos = tokens.size() - 1 @pos = -1 end def parse() self.next() nodes = template() #Tokens left, syntax error error() if @pos < @max_pos nodes end protected #Browser is combination of feature and optional version def browser() node = Nodes::Browser.new() node.feature = @value expect(:feature) if current(:version_vector) node.version_vector = VersionVector.new(@value) accept(:version_vector) else node.version_vector = VersionVector.new(nil) end node end #True or false def boolean() if accept(:boolean_true) Nodes::True.instance() elsif accept(:boolean_false) Nodes::False.instance() else error() end end #Comparison operators def operator() if accept(:operator_less_than) Nodes::LessThan.new() elsif accept(:operator_less_than_equal) Nodes::LessThanEqual.new() elsif accept(:operator_greater_than) Nodes::GreaterThan.new() elsif accept(:operator_greater_than_equal) Nodes::GreaterThanEqual.new() else error() end end #Either a comparison with the browser, boolean, or simply just the browser def subexpression() node = nil if current(:operator_less_than) || current(:operator_less_than_equal) || current(:operator_greater_than) || current(:operator_greater_than_equal) node = operator() node.child = browser() elsif current(:boolean_true) || current(:boolean_false) node = boolean() else #No comparison operator is assuming equals node = Nodes::Equal.new() node.child = browser() end node end #Negated self or paranthesised expression def factor() node = nil if accept(:operator_not) node = Nodes::Not.new() node.child = factor() elsif accept(:paren_open) node = expression() expect(:paren_close) else node = subexpression() end node end #And def term() node = factor() while accept(:operator_and) branch_node = Nodes::And.new() branch_node.left = node branch_node.right = factor() node = branch_node end node end #Or def expression() node = term() while accept(:operator_or) branch_node = Nodes::Or.new() branch_node.left = node branch_node.right = term() node = branch_node end node end def condition() node = Nodes::Condition.new() expect(:open) expect(:if) node.left = expression() expect(:close) unless current(:open) && peek(:endif) node.right = template() end expect(:open) expect(:endif) expect(:close) node end def html() node = Nodes::Html.new() node.content = @value expect(:html) node end def template() nodes = Nodes::Nodes.new() while current(:html) || (current(:open) && peek(:if)) nodes << if current(:html) html() elsif current(:open) && peek(:if) condition() end end nodes end protected #Accept the symbol and move on or not def accept(symbol) if(symbol == @symbol) self.next() return true else return false end end #Expect a current symbol or raise def expect(symbol) raise HtmlConditionalComment::ParseError.new("Expected #{symbol}, received #{@symbol}", @value) unless accept(symbol) end def current(symbol) @symbol == symbol end def peek(symbol) @tokens[@pos+1][0] == symbol end def next() @pos += 1 #raise HtmlConditionalComment::ParserError.new('EOF') if @pos >= @max_pos token = @tokens[@pos] || [] @symbol = token[0] @value = token[1] token end def error() raise HtmlConditionalComment::ParseError.new("Syntax error", @value) end end end