require "strscan" module Take class Unit class Scanner attr_reader :tokens def initialize(input) @input = "\n" << input @scanner = StringScanner.new(input) end def scan @tokens ||= begin @tokens = [] until @scanner.eos? scan_group_body { error! } end @tokens end end def scan_group_body pre_scan scan_nested or scan_group or scan_body or scan_whitespace or yield end def scan_nested if scan_group(/group/) scan_whitespace @scanner.scan(/\{/) or error! loop do scan_group_body do if @scanner.scan(/\}/) pre_scan return emit(:group_end) else error! end end end end end def scan_group(scan = /macro|test|before|after|prefix/) if @scanner.scan(scan) group_type = @scanner[0] @scanner.scan(/ ([A-Za-z0-9_.-]+)\(/) or error! group_name = @scanner[1] @scanner.scan(/\s*\;/) emit group_type.intern, group_name, _scan_arguments(/\s*\)/, /,\s+/) end end def scan_body if @scanner.check(/\s*\{/) emit :block, _scan_block end end def scan_whitespace(newlines = true) if newlines @scanner.scan(/\s+/) else @scanner.scan(/[ \t]+/) end end private def pre_scan @pos = @scanner.pos end def line @input[0..@pos].count("\n") end def column last_newline = @input[0..@pos].rindex("\n") || 0 @pos - last_newline end def emit(*args) @tokens << args.concat([line, column]) true end def _scan_block brack = 1 body = @scanner.scan(/\s*\{/) scan_for = %r{ ( (?: " ( \\\\ | \\" | [^"] )* "? ) | (?: ' ( \\\\ | \\' | [^'] )* '? ) | (?: // .*? \n ) | (?: \# .*? \n ) | (?: /\* [\s\S]+? \*/ ) | (?: \} ) | (?: \{ ) ) }x until brack.zero? if part = @scanner.scan_until(scan_for) body << part if @scanner[1] == "}" brack -= 1 elsif @scanner[1] == "{" brack += 1 end else @scanner.scan(/(.+)/m) error! end end body.gsub(/\A\n*/, "").gsub(/\A\s*\{([\s\S]*)\}\z/, "\\1") end def _scan_arguments(ending, seperator) arguments = [] until @scanner.scan(ending) @scanner.scan(/(&?[A-Za-z0-9_-]+)/) arguments << @scanner[1] @scanner.scan(seperator) or @scanner.check(ending) or error! end arguments end def error! p @tokens raise SyntaxError, "Unexpected `#{@scanner.check(/./)}' on line #{line} (column #{column})" end end end end