## -*- Ruby -*-
## XML::SimpleTreeBuilder
## 1999 by yoshidam

require 'xmlparser'
require 'xmltree'
include XML::SimpleTree

=begin
= XML::DOM::Builder

== Module XML

=end
module XML

=begin
== Class XML::DOM::Builder (XML::SimpleTreeBuilder)

=== superclass
XML::Parser

=end
  class SimpleTreeBuilder<Parser
    ## replace 'open' by WGET::open
    begin
      require 'wget'
      include WGET
    rescue
      ## ignore
    end

=begin
=== Class Methods

    --- DOM::Builder.new(level = 0, *args)

Constructor of DOM builder.

usage:
parser = XML::SimpleTreeBuilder.new(level)

  level: 0 -- ignore default events (defualt)
         1 -- catch default events and create the Comment,
              the EntityReference, the XML declaration (as PI) and
              the non-DOM-compliant DocumentType nodes.

=end

    def SimpleTreeBuilder.new(level = 0, *args)
      ret = super(*args)
      external = false
      external = true if args[0].is_a?(SimpleTreeBuilder)
      ret.__initialize__(level, external)
      ret
    end

    ## Constructor
    ##  parser = XML::SimpleTreeBuilder.new(level)
    ##    level: 0 -- ignore default events (defualt)
    ##           1 -- catch default events and create the Comment,
    ##                the EntityReference, the XML declaration (as PI) and
    ##                the non-DOM-compliant DocumentType nodes.
    def __initialize__(level = 0, external = false)
      @tree = nil
      @level = level
      @external = external
      if @level > 0
        def self.default(data); defaultHandler(data); end
      end
    end

=begin
=== Methods

    --- Builder#nameConverter(str)

User redefinable name encoding converter

=end
    ## User redefinable name encoding converter
    def nameConverter(str)
      str
    end

=begin
    --- Builder#cdataConverter(str)

User redefinable cdata encoding converter

=end
    ## User redefinable cdata encoding converter
    def cdataConverter(str)
      str
    end

=begin
=== Methods
    --- Builder#parse(xml, parse_ext = false)

parse string or stream of XML contents.

  xml:       string or stream of XML contents
  parse_ext: flag whether parse external entities or not

ex. doctree = parser.parse(xml, parse_ext)

=end
    ## Parse
    ##   doctree = parser.parse(xml, parse_ext)
    ##     xml:       string or stream of XML contents
    ##     parse_ext: flag whether parse external entities or not
    def parse(xml, parse_ext = false)
      if @external
        @tree = DocumentFragment.new
      else
        @tree = Document.new
      end
      @parse_ext = parse_ext
      @current = @tree
      @inDocDecl = 0
      @decl = ""
      @inDecl = 0
      @idRest = 0
      @extID = nil
      @cdata_f = 0
      @cdata_buf = ''
      super(xml)
      @tree
    end

    def startElement(name, data)
      attr = {}
      data.each do |key, value|
        attr[nameConverter(key)] = cdataConverter(value)
      end
      elem = Element.new(nameConverter(name), attr)
      @current.appendChild(elem)
      @current = elem
    end

    def endElement(name)
      @current = @current.parentNode
    end

    def character(data)
      ## old expat only
      if data == ''
        if @level > 0
          @cdata_f = 1
          defaultCurrent
        end
        return
      end

      if @cdata_f == 2
        @cdata_buf += data
      else
        cdata = Text.new(cdataConverter(data))
        @current.appendChild(cdata)
      end
     end

    def processingInstruction(name, data)
      pi = ProcessingInstruction.new(nameConverter(name),
                                     cdataConverter(data))
      ## PI data should not be converted
      @current.appendChild(pi)
    end

    def externalEntityRef(context, base, systemId, publicId)
      tree = nil
      if @parse_ext
        extp = SimpleTreeBuilder.new(@level, self, context)
        extp.setBase(base) if base
        file = systemId
        if systemId !~ /^\/|^\.|^http:|^ftp:/ && !base.nil?
          file = base + systemId
        end
        begin
          tree = extp.parse(open(file).read, @parse_ext)
        rescue XML::ParserError
          raise XML::ParserError.new("#{systemId}(#{extp.line}): #{$!}")
        rescue Errno::ENOENT
          raise Errno::ENOENT.new("#{$!}")
        end
        extp.done
      end
      if @level > 0
        entref = EntityReference.new(nameConverter(context))
        @current.appendChild(entref)
        entref.appendChild(tree) if tree
      else
        @current.appendChild(tree) if tree
      end
    end

    ## new expat only
    def startCdata
      return if @level < 1
      @cdata_f = 2
      @cdata_buf = ''
    end

    ## new expat only
    def endCdata
      return if @level < 1
      cdata = CDATASection.new(cdataConverter(@cdata_buf))
      @current.appendChild(cdata)
      @cdata_buf = ''
      @cdata_f = 0
    end

    ## new expat only
    def comment(data)
      comment = Comment.new(cdataConverter(data))
      ## Comment should not be converted
      @current.appendChild(comment)
    end

    def defaultHandler(data)
      ## old expat only
      if @cdata_f == 1
        if data == '<![CDATA['
          @cdata_f = 2
          @cdata_buf = ''
        elsif data == ']]>'
          cdata = CDATASection.new(cdataConverter(@cdata_buf))
          @current.appendChild(cdata)
          @cdata_buf = ''
          @cdata_f = 0
        else
          print "Unexpected default event\n"
          @cdata_f = 0
        end
        return
      end

      ## Large comment may crash regexp of Ruby
      ##          if data =~ /^<!--([\s\S]*)-->$/
      ##            comment = Comment.new(cdataConverter($1))
      if data =~ /^<!--/ && data =~ /-->$/ && data.length >= 7 
        ## old expat only
        comment = Comment.new(cdataConverter(data[4..-4]))
        ## Comment should not be converted
        @current.appendChild(comment)
      elsif data =~ /^\&(.+);$/
        eref = EntityReference.new(nameConverter($1))
        @current.appendChild(eref)
      elsif data =~ /^<\?xml\s*([\s\S]*)\?>$/
        pi = ProcessingInstruction.new("xml",
                                       cdataConverter($1))
        ## PI data should not be converted
        @current.appendChild(pi)
      elsif @inDocDecl == 0 && data =~ /^<\!DOCTYPE$/
        @inDocDecl = 1
        @inDecl = 0
        @idRest = 0
        @extID = nil
      elsif @inDocDecl == 1
        if data == "["
          @inDocDecl = 2
        elsif data == ">"
          if !@extID.nil?
            @current.nodeValue = @extID
          end
          @inDocDecl = 0
          @current = @current.parentNode
        elsif data == "SYSTEM"
          @idRest = 1
          @extID = data
        elsif data == "PUBLIC"
          @idRest = 2
          @extID = data
        elsif data !~ /^\s+$/
          if @idRest > 0
            ## SysID or PubID
            @extID <<= " " + data
            @idRest -= 1
          else
            ## Root Element Type
            docType = data
            doctype = DocumentType.new(nameConverter(docType))
            @current.appendChild(doctype)
            @current = doctype
          end
        end
      elsif @inDocDecl == 2
        if @inDecl == 0
          if data == "]"
            @inDocDecl = 1
          elsif data =~ /^<\!/
            @decl = data
            @inDecl = 1
          elsif data =~ /^%(.+);$/
            ## PERef
            cdata = Text.new(nameConverter(data))
            @current.appendChild(cdata)
          else
            ## WHITESPCAE
          end
        else ## inDecl == 1
          if data == ">"
            @decl <<= data
            @inDecl = 0
            ## Markup Decl
            cdata = Text.new(cdataConverter(@decl))
            ## Markup decl should not be converted
            @current.appendChild(cdata)
          elsif data =~ /^\s+$/
            ## WHITESPACE
            @decl << " "
          else
            @decl << data
          end
        end
      else
        ## maybe WHITESPACE
        cdata = Text.new(cdataConverter(data))
        @current.appendChild(cdata)
      end
    end

  end

  module DOM
    Builder = SimpleTreeBuilder
  end
end
