require 'forwardable' require 'salt/scanner/main' require 'salt/scanner/token' module Salt # Scans a given input. # # @see http://ruby-doc.org/stdlib-2.1.2/libdoc/strscan/rdoc/StringScanner.html class Scanner extend Forwardable include Main # An array of the tokens that the scanner scanned. # # @return [Array] attr_reader :tokens # Scans a file. It returns the tokens resulting from scanning. # # @param source [String] the source to scan. This should be # compatible with StringScanner. # @param name [String] the name of the source file. This is # primarily used in backtrace information. # @return [Array] # @see #tokens def self.scan(source, name = '(file)') new(source, name).scan_file end # Initialize the scanner with the input. # # @param input [String] The source to scan. # @param source [String] the source file. This is primarily # used in backtrace information. def initialize(input, source = '(file)') @source = source @scanner = StringScanner.new(input) @tokens = [] end # Scans the file in parts. # # @raise [SyntaxError] if the source is malformed in some way. # @return [Array] the tokens that # were scanned in this file. # @see #scan_first_part # @see #scan_second_part # @see #scan_third_part # @see #tokens def scan_file @line = 1 scan tokens rescue SyntaxError => e start = [@scanner.pos - 8, 0].max stop = [@scanner.pos + 8, @scanner.string.length].min snip = @scanner.string[start..stop].strip.inspect char = @scanner.string[@scanner.pos] char = if char char.inspect else 'EOF' end new_line = "#{@source}:#{@line}: unexpected #{char} " \ "(near #{snip})" raise e, e.message, [new_line, *e.backtrace] end # Scans for whitespace. If the next character is whitespace, it # will consume all whitespace until the next non-whitespace # character. # # @return [Boolean] if any whitespace was matched. def scan_whitespace @line += @scanner[1].count("\n") if @scanner.scan(/(\s+)/) end private attr_reader :line def column @scanner.pos - (@scanner.string.rindex(/\n|\r/, @scanner.pos) || 0) end # Raises an error. # # @raise [SyntaxError] always. # @return [void] def error! raise SyntaxError, 'invalid syntax' end # Matches using the scanner. If it matches, it returns a truthy # value. If it fails to match, it returns nil. # # @param scan [String, Symbol, Regexp] The match. If it is a # string or a symbol, it is turned into a regular expression # through interpolation. # @return [Boolean, nil] def on(scan) case scan when String, Symbol @scanner.scan(/#{Regexp.escape(scan.to_s)}/) when Regexp @scanner.scan(scan) else raise ArgumentError, "Unexpected #{scan.class}, expected " \ 'String, Symbol, or Regexp' end end # Generates a token with the given name and values. # # @param name [Symbol] the name of the token. # @param values [Object] the values to be part of the token. # @return [Token] def token(name, *values) Token.new(name, values, line, column) end # Emits a token, by first generating the token, and then # appending it to the token array. # # @see #token # @param (see #token) # @return [Array] The token array. def emit(name, *values) tokens << token(name, *values) end # The group from the last match. It responds to #[] only. # # @return [#[]] def group @scanner end def_delegators :@scanner, :eos? end end