lib/raabro.rb



module Raabro

  VERSION = '1.3.0'

  class Input

    attr_accessor :string, :offset
    attr_reader :options

    def initialize(string, offset=0, options={})

      @string = string
      @offset = offset.is_a?(Hash) ? 0 : offset
      @options = offset.is_a?(Hash) ? offset : options
    end

    def match(str_or_regex)

      if str_or_regex.is_a?(Regexp)
        m = @string[@offset..-1].match(str_or_regex)
        m && (m.offset(0).first == 0) ? m[0].length : false
      else # String or whatever responds to #to_s
        s = str_or_regex.to_s
        l = s.length
        @string[@offset, l] == s ? l : false
      end
    end

    def tring(l=-1)

      l < 0 ? @string[@offset..l] : @string[@offset, l]
    end

    def at(i)

      @string[i, 1]
    end
  end

  class Tree

    attr_accessor :name, :input
    attr_accessor :result # ((-1 error,)) 0 nomatch, 1 success
    attr_accessor :offset, :length
    attr_accessor :parter, :children

    def initialize(name, parter, input)

      @result = 0
      @name = name
      @parter = parter
      @input = input
      @offset = input.offset
      @length = 0
      @children = []
    end

    def c0; @children[0]; end
    def c1; @children[1]; end
    def c2; @children[2]; end
    def c3; @children[3]; end
    def c4; @children[4]; end
    def clast; @children.last; end

    def empty?

      @result == 1 && @length == 0
    end

    def successful_children

      @children.select { |c| c.result == 1 }
    end

    def prune!

      @children = successful_children
    end

    def string; @input.string[@offset, @length]; end
    def strinp; @input.string[@offset, @length].strip; end
    alias strim strinp
    def nonstring(l=7); @input.string[@offset, l]; end

    def lookup(name=nil)

      name = name ? name.to_s : nil

      return self if @name && name == nil
      return self if @name.to_s == name
      sublookup(name)
    end

    def sublookup(name=nil)

      @children.each { |c| if n = c.lookup(name); return n; end }

      nil
    end

    def gather(name=nil, acc=[])

      name = name ? name.to_s : nil

      if (@name && name == nil) || (@name.to_s == name)
        acc << self
      else
        subgather(name, acc)
      end

      acc
    end

    def subgather(name=nil, acc=[])

      @children.each { |c| c.gather(name, acc) }

      acc
    end

    def to_a(opts={})

      opts = Array(opts).inject({}) { |h, e| h[e] = true; h } \
        unless opts.is_a?(Hash)

      cn =
        if opts[:leaves] && (@result == 1) && @children.empty?
          string
        elsif opts[:children] != false
          @children.collect { |e| e.to_a(opts) }
        else
          @children.length
        end

      [ @name, @result, @offset, @length, @note, @parter, cn ]
    end

    def to_s(depth=0, io=StringIO.new)

      io.print "\n" if depth > 0
      io.print '  ' * depth
      io.print "#{@result} #{@name.inspect} #{@offset},#{@length}"
      io.print result == 1 && children.size == 0 ? ' ' + string.inspect : ''

      @children.each { |c| c.to_s(depth + 1, io) }

      depth == 0 ? io.string : nil
    end

    def odd_children

      cs = []; @children.each_with_index { |c, i| cs << c if i.odd? }; cs
    end

    def even_children

      cs = []; @children.each_with_index { |c, i| cs << c if i.even? }; cs
    end

    def extract_error

#Raabro.pp(self, colors: true)
      err_tree, stack = lookup_error || lookup_all_error

      line, column = line_and_column(err_tree.offset)

      err_message =
        if stack
          path = stack
           .compact.reverse.take(3).reverse
           .collect(&:inspect).join('/')
          "parsing failed .../#{path}"
        else
          "parsing failed, not all input was consumed"
        end
      visual =
        visual(line, column)

      [ line, column, err_tree.offset, err_message, visual ]
    end

    def lookup_error(stack=[])

#print "le(): "; Raabro.pp(self, colors: true)
      return nil if @result != 0
      return [ self, stack ] if @children.empty?
      @children.each { |c|
        es = c.lookup_error(stack.dup.push(self.name))
        return es if es }
      nil
    end

    # Not "lookup all errors" but "lookup all error", in other
    # words lookup the point up until which the parser stopped (not
    # consuming all the input)
    #
    def lookup_all_error

#print "lae(): "; Raabro.pp(self, colors: true)
      @children.each { |c| return [ c, nil ] if c.result == 0 }
      @children.reverse.each { |c| es = c.lookup_all_error; return es if es }
      nil
    end

    def line_and_column(offset)

      line = 1
      column = 0

      (0..offset).each do |off|

        column += 1
        next unless @input.at(off) == "\n"

        line += 1
        column = 0
      end

      [ line, column ]
    end

    def visual(line, column)

      @input.string.split("\n")[line - 1] + "\n" +
      ' ' * (column - 1) + '^---'
    end
  end

  module ModuleMethods

    def _match(name, input, parter, regex_or_string)

      r = Raabro::Tree.new(name, parter, input)

      if l = input.match(regex_or_string)
        r.result = 1
        r.length = l
        input.offset += l
      end

      r
    end

    def str(name, input, string)

      _match(name, input, :str, string)
    end

    def rex(name, input, regex_or_string)

      _match(name, input, :rex, Regexp.new(regex_or_string))
    end

    def _quantify(parser)

      return nil if parser.is_a?(Symbol) && respond_to?(parser)
        # so that :plus and co can be overriden

      case parser
        when '?', :q, :qmark then [ 0, 1 ]
        when '*', :s, :star then [ 0, 0 ]
        when '+', :p, :plus then [ 1, 0 ]
        else nil
      end
    end

    def _narrow(parser)

      fail ArgumentError.new("lone quantifier #{parser}") if _quantify(parser)

      method(parser.to_sym)
    end

    def _parse(parser, input)

      #p [ caller.length, parser, input.tring ]
      #r = _narrow(parser).call(input)
      #p [ caller.length, parser, input.tring, r.to_a(children: false) ]
      #r
      _narrow(parser).call(input)
    end

    def seq(name, input, *parsers)

      r = ::Raabro::Tree.new(name, :seq, input)

      start = input.offset
      c = nil

      loop do

        pa = parsers.shift
        break unless pa

        if q = _quantify(parsers.first)
          parsers.shift
          c = rep(nil, input, pa, *q)
          r.children.concat(c.children)
        else
          c = _parse(pa, input)
          r.children << c
        end

        break if c.result != 1
      end

      if c && c.result == 1
        r.result = 1
        r.length = input.offset - start
      else
        input.offset = start
      end

      r
    end

    def alt(name, input, *parsers)

      greedy =
        if parsers.last == true || parsers.last == false
          parsers.pop
        else
          false
        end

      r = ::Raabro::Tree.new(name, greedy ? :altg : :alt, input)

      start = input.offset
      c = nil

      parsers.each do |pa|

        cc = _parse(pa, input)
        r.children << cc

        input.offset = start

        if greedy
          if cc.result == 1 && cc.length >= (c ? c.length : -1)
            c.result = 0 if c
            c = cc
          else
            cc.result = 0
          end
        else
          c = cc
          break if c.result == 1
        end
      end

      if c && c.result == 1
        r.result = 1
        r.length = c.length
        input.offset = start + r.length
      end

      r.prune! if input.options[:prune]

      r
    end

    def altg(name, input, *parsers)

      alt(name, input, *parsers, true)
    end

    def rep(name, input, parser, min, max=0)

      min = 0 if min == nil || min < 0
      max = nil if max.nil? || max < 1

      r = ::Raabro::Tree.new(name, :rep, input)
      start = input.offset
      count = 0

      loop do
        c = _parse(parser, input)
        r.children << c
        break if c.result != 1
        count += 1
        break if c.length < 1
        break if max && count == max
      end

      if count >= min && (max == nil || count <= max)
        r.result = 1
        r.length = input.offset - start
      else
        input.offset = start
      end

      r.prune! if input.options[:prune]

      r
    end

    def ren(name, input, parser)

      r = _parse(parser, input)
      r.name = name

      r
    end
    alias rename ren

    def nott(name, input, parser)

      start = input.offset

      r = ::Raabro::Tree.new(name, :nott, input)
      c = _parse(parser, input)
      r.children << c

      r.length = 0
      r.result = c.result == 1 ? 0 : 1

      input.offset = start

      r
    end

    def all(name, input, parser)

      start = input.offset
      length = input.string.length - input.offset

      r = ::Raabro::Tree.new(name, :all, input)
      c = _parse(parser, input)
      r.children << c

      if c.length < length
        input.offset = start
      else
        r.result = 1
        r.length = c.length
      end

      r
    end

    def eseq(name, input, startpa, eltpa, seppa=nil, endpa=nil)

      jseq = false

      if seppa.nil? && endpa.nil?
        jseq = true
        seppa = eltpa; eltpa = startpa; startpa = nil
      end

      start = input.offset
      r = ::Raabro::Tree.new(name, jseq ? :jseq : :eseq, input)
      r.result = 1
      c = nil

      if startpa
        c = _parse(startpa, input)
        r.children << c
        r.result = 0 if c.result != 1
      end

      if r.result == 1

        i = 0

        loop do

          st = i > 0 ? _parse(seppa, input) : nil
          et = st == nil || st.result == 1 ? _parse(eltpa, input) : nil

          break if st && et && st.empty? && et.result == 0
          break if st && et && st.empty? && et.empty?

          r.children << st if st
          r.children << et if et

          break if et == nil
          break if et.result != 1

          i = i + 1
        end

        r.result = 0 if jseq && i == 0
      end

      if r.result == 1 && endpa
        c = _parse(endpa, input)
        r.children << c
        r.result = 0 if c.result != 1
      end

      if r.result == 1
        r.length = input.offset - start
      else
        input.offset = start
      end

      r.prune! if input.options[:prune]

      r
    end
    alias jseq eseq

    attr_accessor :last

    def method_added(name)

      m = method(name)
      return unless m.arity == 1
      return unless m.parameters[0][1] == :i || m.parameters[0][1] == :input

      @last = name.to_sym
    end

    def parse(input, opts={})

      d = opts[:debug].to_i
      opts[:rewrite] = false if d > 0
      opts[:all] = false if d > 1
      opts[:prune] = false if d > 2

      opts[:prune] = true unless opts.has_key?(:prune)

      root = self.respond_to?(:root) ? :root : @last

      t =
        if opts[:all] == false
          _parse(root, Raabro::Input.new(input, opts))
        else
          all(nil, Raabro::Input.new(input, opts), root)
        end

      return reparse_for_error(input, opts, t) if opts[:error] && t.result != 1
      return nil if opts[:prune] != false && t.result != 1

      t = t.children.first if t.parter == :all

      return rewrite(t) if opts[:rewrite] != false

      t
    end

    def reparse_for_error(input, opts, t)

      t =
        opts[:prune] == false ?
        t :
        parse(input, opts.merge(error: false, rewrite: false, prune: false))
#Raabro.pp(t, colours: true)

      t.extract_error
    end

    def rewrite_(tree)

      t = tree.lookup(nil)

      t ? rewrite(t) : nil
    end

    def rewrite(tree)

      return !! methods.find { |m| m.to_s.match(/^rewrite_/) } if tree == 0
        # return true when "rewrite_xxx" methods seem to have been provided

      send("rewrite_#{tree.name}", tree)
    end

    def make_includable

      def self.included(target)

        target.instance_eval do
          extend ::Raabro::ModuleMethods
          extend self
        end
      end
    end
  end
  extend ModuleMethods

  make_includable

    # Black       0;30     Dark Gray     1;30
    # Blue        0;34     Light Blue    1;34
    # Green       0;32     Light Green   1;32
    # Cyan        0;36     Light Cyan    1;36
    # Red         0;31     Light Red     1;31
    # Purple      0;35     Light Purple  1;35
    # Brown       0;33     Yellow        1;33
    # Light Gray  0;37     White         1;37

  def self.pp(tree, depth=0, opts={})

    fail ArgumentError.new(
      'tree is not an instance of Raabro::Tree'
    ) unless tree.is_a?(Raabro::Tree)

    depth, opts = 0, depth if depth.is_a?(Hash)

    _rs, _dg, _gn, _yl, _bl, _lg =
      (opts[:colors] || opts[:colours] || $stdout.tty?) ?
      [ "", "", "", "", "", "" ] :
      [ '', '', '', '', '', '' ]

    lc = tree.result == 1 ? _gn : _dg
    nc = tree.result == 1 ? _bl : _lg
    nc = lc if tree.name == nil
    sc = tree.result == 1 ? _yl : _dg

    str =
      if tree.children.size == 0
        " #{sc}#{tree.string.length == 0 ?
        "#{_dg} >#{tree.nonstring(14).inspect[1..-2]}<" :
        tree.string.inspect}"
      else
        ''
      end

    print "#{_dg}t---\n" if depth == 0

    #print "#{'  ' * depth}"
    depth.times do |i|
      pipe = i % 3 == 0 ? ': ' : '| '
      print i.even? ? "#{_dg}#{pipe} " : '  '
    end

    print "#{lc}#{tree.result}"
    print " #{nc}#{tree.name.inspect} #{lc}#{tree.offset},#{tree.length}"
    print str
    print "#{_rs}\n"

    tree.children.each { |c| self.pp(c, depth + 1, opts) }

    if depth == 0
      print _dg
      print "input ln: #{tree.input.string.length}, tree ln: #{tree.length} "
      print "---t\n"
      print _rs
    end
  end
end