module HtmlConditionalComment class TokenError < StandardError def initialize(rest) super("Invalid token \"#{rest.slice(0, 25)}\"") end end ## # Converts string into array of tokens. Token is an array, first element is # symbol representing the token, second element is string value. # class Lexer LESS_THAN = /lt/i LESS_THAN_EQUAL = /lte/i GREATER_THAN = /gt/i GREATER_THAN_EQUAL = /gte/i OPEN_PAREN = /\(/ CLOSE_PAREN = /\)/ NOT = /\!/ OR = /\|/ AND = /\&/ TRUE = /true/i FALSE = /false/i IF_STATEMENT = /if/i ENDIF_STATEMENT = /endif/i #Opening statement plus positive look ahead to avoid conflicts with other #comments OPEN = // WHITE_SPACE = /\s+/ FEATURE = /[a-z]+/i VERSION_VECTOR = /\d+(\.[\d]+)?/ TOKENS = [ [:if, IF_STATEMENT], [:endif, ENDIF_STATEMENT], [:paren_open, OPEN_PAREN], [:paren_close, CLOSE_PAREN], [:operator_less_than_equal, LESS_THAN_EQUAL], [:operator_less_than, LESS_THAN], [:operator_greater_than_equal, GREATER_THAN_EQUAL], [:operator_greater_than, GREATER_THAN], [:operator_not, NOT], [:operator_or, OR], [:operator_and, AND], [:boolean_true, TRUE], [:boolean_false, FALSE], [:feature, FEATURE], [:version_vector, VERSION_VECTOR] ] def initialize(html_or_comment) @scanner = StringScanner.new(html_or_comment) end def tokenize() tokens = [] open = false #Run until nothing left in string until @scanner.eos?() #Split between if the conditional comment has been opened or not #State will help handle all the other HTML we don't care about if open @scanner.skip(WHITE_SPACE) if token = @scanner.scan(CLOSE) open = false tokens << [:close, token] else #Go through token specs and scan and stop on first one token = TOKENS.inject(nil) do |previous, spec| t = @scanner.scan(spec[1]) unless t.nil?() break [spec[0], t] end end if token tokens << token else raise TokenError.new(@scanner.rest()) end end #Closed (not opened) conditional comment else #Scan till we find an open token, if not done and use the rest if match = @scanner.scan_until(OPEN) open = true #TODO Gross way to get up till scan has succeeded match = match.slice(0..-(@scanner.matched.size() + 1)) tokens << [:html, match] unless match.empty?() tokens << [:open, @scanner.matched] else tokens << [:html, @scanner.rest()] if @scanner.rest?() break end end end @scanner.reset() tokens end end end