/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is XML Node Positioning Service.
 *
 * The Initial Developer of the Original Code is
 * Alexander J. Vincent <ajvincent@gmail.com>.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

// Get JS modules.
Components.utils.import("resource://app/modules/XPCOMUtils.jsm");
Components.utils.import("resource://app/modules/ArrayConverter.jsm");
/* We do NOT want the ecma-debug module here.  Some of the code here - notably
   the SAXReader code - will not stop if we throw an error, so we want
   assertions unconditionally.  The ecma-debug module may decide not to trigger
   assertions based on a preference.
*/

var C_i = Components.interfaces;

const xeINodePositionService      = C_i.xeINodePositionService;
const nsIDebug                    = C_i.nsIDebug;
const xeIVerbosioUtilsService     = C_i.xeIVerbosioUtilsService;
const nsIDOMNode                  = C_i.nsIDOMNode;
const nsIDOM3Node                 = C_i.nsIDOM3Node;
const nsIDOMElement               = C_i.nsIDOMElement;
const nsIDOMNodeFilter            = C_i.nsIDOMNodeFilter;
const nsIDOMDocumentTraversal     = C_i.nsIDOMDocumentTraversal;
const nsIXMLHttpRequest           = C_i.nsIXMLHttpRequest;
const nsISAXXMLReader             = C_i.nsISAXXMLReader;
const nsIProgrammingLanguage      = C_i.nsIProgrammingLanguage;
const nsISecurityCheckedComponent = C_i.nsISecurityCheckedComponent;
const nsIClassInfo                = C_i.nsIClassInfo;
const nsIComponentRegistrar       = C_i.nsIComponentRegistrar;
const nsIFactory                  = C_i.nsIFactory;
const nsIModule                   = C_i.nsIModule;
const nsISupports                 = C_i.nsISupports;

C_i = null;

const XMLNS_NS = "http://www.w3.org/2000/xmlns/";

const nsIDebugService = Components.classes["@mozilla.org/xpcom/debug;1"]
                                  .getService(nsIDebug);
var xeVerbosioUtilsClass = "@verbosio.mozdev.org/verbosio-utils;1";
var xeVerbosioUtils = null;

// Utility function.
function ERROR_FAILURE() {
  throw Components.results.NS_ERROR_FAILURE;
}

// Utility function.
function NOT_IMPLEMENTED() {
  throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
}

// Assert function.
function assert(mustBeTrue, aMsg) {
  if (!mustBeTrue)
    throw new Error(aMsg);
  return mustBeTrue;
}

/**
 * Return a function to execute a ECMA debug contract.
 *
 * @param aContract Object with body, optional pre- and post-condition methods.
 *
 * @return JavaScript contract function.
 *
 * @note This function is borrowed and modified from
 *       resource://app/res/ecma-debug.js.  It will always run the assertions
 *       code.
 */
function getContractFunction(aContract) {
  if (typeof aContract.body != "function")
    throw new Error("Invalid contract passed into getContractFunction!");

  if (typeof aContract.precondition != "function")
    aContract.precondition = null;

  if (typeof aContract.postcondition != "function")
    aContract.postcondition = null;

  function executeContract() {
    if (aContract.precondition) {
      aContract.precondition.apply(this, arguments);
    }

    var rv = aContract.body.apply(this, arguments);

    if (aContract.postcondition) {
      var args = [];
      for (var x = 0; x < arguments.length; x++) {
        args[x] = arguments[x];
      }

      args.push(rv);
      aContract.postcondition.apply(this, args);
    }

    return rv;
  }

  function executeContractAsConstructor() {
    if (aContract.precondition) {
      aContract.precondition.apply(this, arguments);
    }

    this.__proto__ = aContract.prototype;

    aContract.body.apply(this, arguments);

    if (aContract.postcondition) {
      aContract.postcondition.apply(this, arguments);
    }
  }

  var rv;
  if (typeof aContract.isConstructor == "boolean" && aContract.isConstructor)
    rv = executeContractAsConstructor;
  else
    rv = executeContract;

  rv.isContractFunction = true;

  rv.toContractString = function toContractString() {
    return aContract.toString();
  }

  rv.toContractSource = function toContractSource() {
    return aContract.toSource();
  }

  rv.toString = function toString() {
    return aContract.body.toString();
  }

  rv.toSource = function toSource() {
    return aContract.body.toSource();
  }

  return rv;
}

/**
 * Remove lines from the stack of a JavaScript error object.
 *
 * @param aShiftLines The number of lines to remove.
 *
 * @note This function is borrowed and modified from
 *       resource://app/res/ecma-debug.js.
 */
Error.prototype.shiftStack = function shiftStack(aShiftLines) {
  var aStackArray = this.stack.split("\n").splice(aShiftLines);
  var lastColon = aStackArray[0].lastIndexOf(":");
  this.fileName = aStackArray[0].substring(aStackArray[0].indexOf("@") + 1, lastColon);
  this.lineNumber = aStackArray[0].substr(lastColon + 1);
  this.stack = aStackArray.join("\n");
}

/**
 * Assert a value must be true, and fire a debugging assertion if it's false.
 *
 * @param mustBeTrue If false, fire the assertion.
 * @param aMsg Message to pass in to the debugging assertion.
 * @param aErrorObject Optional JavaScript or XPCOM error object.
 *
 * @return true if the assertion passed.
 * @throw JavaScript error if the assertion failed.
 */
function NS_ASSERTION(mustBeTrue, aMsg, aErrorObject) {
  /* XXX ajvincent The SAX Reader does not stop parsing when a handler throws
   * an exception.  The first indication we have of a failure is when an
   * assertion in the handler fails.  So we want to deliberately stop the
   * program for debugging here.
   *
   * We also want, in the case of a failure, to clear all source map user
   * data we've set on the various nodes.  So we set parseSuccess to false.
   */
  if (mustBeTrue) {
    return true;
  }

  var realFrame;
  var sourceLine;
  if (aErrorObject) {
    // Simulate the parts of nsIStackFrame that we need.
    realFrame = {
      filename: aErrorObject.fileName,
      lineNumber: aErrorObject.lineNumber
    }
    sourceLine = null;
  } else {
    realFrame = Components.stack.caller;
    sourceLine = realFrame.sourceLine;
  }

  if (!sourceLine && realFrame.filename) {
    try {
      var req = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
                          .createInstance(nsIXMLHttpRequest);
      req.open("GET",
               realFrame.filename,
               false);
      req.send(null);
      var source = req.responseText;
      sourceLine = source.split("\n")[realFrame.lineNumber - 1];
    }
    catch (e) {
      // Do nothing.
    }
  }

  // Remove leading whitespace on the assertion source line.
  sourceLine = sourceLine.substr(sourceLine.match(/^\s*/)[0].length);

  nsIDebugService.assertion(aMsg, sourceLine, realFrame.filename, realFrame.lineNumber);

  // Make sure SAX parsing ultimately fails.
  parseSuccess = false;
  throw new Error(aMsg);

  return false; // hide a strict warning
}

/**
 * Convert all errors not caught by a function to assertion failures.
 *
 * @param aFunction JavaScript function to wrap in try...catch block
 *
 * @return JavaScript function to replace the main function.
 */
function ErrorToAssertion(aFunction) {
  var rv = function errorWrapper() {
    try {
      aFunction.apply(this, arguments);
    }
    catch (e) {
      if (typeof e.stack != "undefined") {
        dump(e.stack + "\n");
      }
      NS_ASSERTION(false, e.message, e);
    }
  }

  return rv;
}

/**
 * Throw a NOT_IMPLEMENTED assertion, but allow parsing may succeed.
 */
function PARSE_NOT_IMPLEMENTED() {
  var cachedParseSuccess = parseSuccess;
  try {
    NS_ASSERTION(false, "NOT_IMPLEMENTED");
  }
  catch (e) {
    // let it go
  }
  parseSuccess = cachedParseSuccess;
}

/**
 * Translate XML entities into node values.
 *
 * @param aString Original string.
 *
 * @return String with no XML entities.
 */
function decodeCharactersInString(aString) {
  var re = /&(lt|gt|amp|apos|quot|\#x\d{1,4});/;
  var string = aString;
  var matchArray = string.match(re);
  while (matchArray) {
    var match = matchArray[0];
    var index = string.indexOf(match);
    var replacement = "";
    switch (match) {
      case "&lt;":
        replacement = "<";
        break;

      case "&gt;":
        replacement = ">";
        break;

      case "&amp;":
        replacement = "&";
        break;

      case "&apos;":
        replacement = "'";
        break;

      case "&quot;":
        replacement = '"';
        break;
    }

    if (!replacement && (match.substr(0, 3) == "&#x")) {
      var hexadecimal = match.substr(3, -1);
      replacement = String.fromCharCode(parseInt(hexadecimal, 16));
    }

    string = string.replace(RegExp(match, "gm"), replacement);
    matchArray = string.match(re);
  }
  return string;
}

var parseSuccess = false;

/**
 * Build a source map coordinate set.
 *
 * @param aStartLineNumber   Line number where the source starts.
 * @param aStartColumnNumber Column number where the source starts.
 * @param aEndLineNumber     Line number where the source ends.
 * @param aEndColumnNumber   Column number where the source ends.
 */
var SourceMapCoordSet = getContractFunction({
  precondition: function SourceMapCoord_pre(aStartLineNumber,
                                            aStartColumnNumber,
                                            aEndLineNumber,
                                            aEndColumnNumber) {
    function isPositiveInt(aInt) {
      var msg = "Value must be a positive integer number.";
      assert(typeof aInt == "number", msg);
      assert(aInt > 0, msg);
      assert(Math.floor(aInt) == aInt, msg);
    }

    assert(arguments.length == 4, "Incorrect number of arguments");

    for (var i = 0; i < arguments.length; i++) {
      isPositiveInt(arguments[i]);
    }
  },

  body: function SourceMapCoord_body(aStartLineNumber,
                                     aStartColumnNumber,
                                     aEndLineNumber,
                                     aEndColumnNumber) {
    this.startLineNumber   = aStartLineNumber;
    this.startColumnNumber = aStartColumnNumber;
    this.endLineNumber     = aEndLineNumber;
    this.endColumnNumber   = aEndColumnNumber;
  },

  isConstructor: true
});


/* #text, #comment, #processing-instruction, #document-type
   these use a single coordinate.

 * element, #cdata-section
   these use a start coordinate and an end coordinate.

 * attribute
   this uses a tag coordinate and a value coordinate.
 */

/**
 * Clear all source mappings from the document.
 */
function clearDocumentSourceMaps(doc) {
  assert(doc, "No document?");

  var walker = doc.createTreeWalker(doc, nsIDOMNodeFilter.SHOW_ALL, null, true);
  while (walker.nextNode()) {
    var node = walker.currentNode;
    if (node instanceof nsIDOM3Node) {
      node.setUserData("sourceCodeMapping", null, null);
    }
  }
  walker = null;
}

// xeINodePositionService.updateNodePositions
function updateDocumentSourceMaps(aWrapper, aFilterTypesMatch, aNodeFilter) {
  // Most of this is SAX event handlers.  Execution starts near the end.

  function advanceWalker() {
    var nodeFound = Boolean(walker.nextNode());
    assert(nodeFound, "Tree walker beat SAX reader to the end of the document?");
  }

  function checkNodeType(aType, aNode) {
    var nodeType = aNode.nodeType;
    var msg = "Expected node type " + aType + " found type " + nodeType;
    msg += " (" + aNode.nodeName +")";
    assert(nodeType == aType, msg);
  }

  function getMinIndexOfString(aString, aOffset, aChar) {
    var index = aString.indexOf(aChar, aOffset);
    return (index == -1) ? Infinity : index;
  }

  function convertIndexToCoord(aIndex) {
    if (lineIndex > 0) {
      lineIndex--;
    }

    while (newLinesIndex[lineIndex + 1] < aIndex) {
      lineIndex++;
    }

    const errMsg = "Conversion of line index failed!"
    assert(newLinesIndex[lineIndex] < aIndex + 1, errMsg);

    return {
      lineNumber: lineIndex + 1,
      columnNumber: aIndex + 1 - newLinesIndex[lineIndex]
    };
  }

  var wrappedFilter = {
    acceptNode: function acceptNode(aNode) {
      return nsIDOMNodeFilter.FILTER_ACCEPT;
    }
  };

  if (aNodeFilter instanceof nsIDOMNodeFilter) {
    wrappedFilter.acceptNode = getContractFunction({
      body: function acceptNode_body(aNode) {
        return aNodeFilter.acceptNode(aNode);
      },

      postcondition: function acceptNode_post(aNode, rv) {
        assert(rv == nsIDOMNodeFilter.FILTER_ACCEPT ||
               rv == nsIDOMNodeFilter.FILTER_SKIP,
               "Node filter shouldn't reject nodes!");
      }
    });
  }

  function testNode(aNode) {
    var filterType = 1 << (aNode.nodeType - 1);
    if (!(filterType & aFilterTypesMatch))
      return false;

    return wrappedFilter.acceptNode(aNode) == nsIDOMNodeFilter.FILTER_ACCEPT;
  }

  var contentHandler = {
    characterIndex: 0,
    stillOnCDataNode: false,
    elementNameStack: [],
    elementStartCoordStack: [],

    // nsISAXContentHandler
    startDocument:
    ErrorToAssertion(function startDocument() {
      this.elementNameStack.length = 0;
    }),

    // nsISAXContentHandler
    endDocument:
    ErrorToAssertion(function endDocument() {
      assert(!walker.nextNode(), "SAX reader beat tree walker to the end of the doc?");
    }),

    // nsISAXContentHandler
    handleAttributes:
    ErrorToAssertion(function handleAttributes(aAttributes) {
      var element = walker.currentNode;
      for (var i = 0; i < aAttributes.length; i++) {
        var attrNamespaceURI = aAttributes.getURI(i);
        var attrLocalName = aAttributes.getLocalName(i);
        var attrNodeName = aAttributes.getQName(i);
        var value = aAttributes.getValue(i);

        assert(typeof element.getAttributeNodeNS == "function",
               "QI to nsIDOMElement!");
        var attrNode = element.getAttributeNodeNS(attrNamespaceURI,
                                                  attrLocalName);
        assert(attrNode, "No attribute with name " + attrNodeName + "?");
        assert(attrNode == element.attributes[i],
               "Attribute ordering is confused!");

        /* The attribute node will get a source coordinate and a value
         * coordinate.  The value falls within the source, so we get source
         * start, value start, value end, source end.
         */

        var index = getMinIndexOfString(source, charIndex, attrNodeName);
        var attrStart = convertIndexToCoord(index);

        var quote = source.substr(index).match(/["']/)[0];
        index = getMinIndexOfString(source, index, quote) + 1;
        var valueStart = convertIndexToCoord(index);
        var valueStartIndex = index;

        index = getMinIndexOfString(source, index, quote);
        var valueEnd = convertIndexToCoord(index);
        var valueEndIndex = index;

        var attrEnd = convertIndexToCoord(index + 1);
        var endQuoteChar = source.charAt(index + 1);
        charIndex = index + 1;

        // Sanity check:  get the source code and read it back
        var str = source.substring(valueStartIndex, valueEndIndex);
        str = decodeCharactersInString(str);
        assert(str == value, "Values don't match?");

        if (testNode(attrNode)) {
          attrNode.setUserData("sourceCodeMapping", {
            attrTag: new SourceMapCoordSet(
              attrStart.lineNumber,
              attrStart.columnNumber,
              attrEnd.lineNumber,
              attrEnd.columnNumber),
            attrValue: new SourceMapCoordSet(
              valueStart.lineNumber,
              valueStart.columnNumber,
              valueEnd.lineNumber,
              valueEnd.columnNumber)
          }, null, null);
        }
      }
    }),

    // nsISAXContentHandler
    startElement:
    ErrorToAssertion(function startElement(aNamespaceURI,
                                           aLocalName,
                                           aNodeName,
                                           aAttributes) {
      advanceWalker();
      checkNodeType(nsIDOMNode.ELEMENT_NODE, walker.currentNode);
      assert(walker.currentNode instanceof nsIDOMElement,
             "We have an element that doesn't implement nsIDOMElement?");

      var nameStack = this.elementNameStack;
      nameStack[nameStack.length] = walker.currentNode;

      var match = "<" + aNodeName;

      // Go back one space because of &lt;.
      var index = getMinIndexOfString(source, charIndex - nameStack.length, match);
      tagOpen = convertIndexToCoord(index);

      this.handleAttributes(aAttributes);

      index = getMinIndexOfString(source, charIndex, ">");
      charIndex = index;
      var tagClose = convertIndexToCoord(index);

      charIndex = getMinIndexOfString(source, charIndex, ">") + 1;

      var coordStack = this.elementStartCoordStack;
      coordStack[coordStack.length] = new SourceMapCoordSet(
        tagOpen.lineNumber,
        tagOpen.columnNumber,
        tagClose.lineNumber,
        tagClose.columnNumber);
    }),

    // nsISAXContentHandler
    endElement:
    ErrorToAssertion(function endElement(aNamespaceURI,
                                         aLocalName,
                                         aNodeName) {
      var currentNode = this.elementNameStack.pop();
      checkNodeType(nsIDOMNode.ELEMENT_NODE, currentNode);
      assert(currentNode.namespaceURI == aNamespaceURI, "Check current node!");
      assert(currentNode.localName == aLocalName, "Check current node!");

      var startTagMap = this.elementStartCoordStack.pop();
      var endTagMap = null;

      var nodeAccepted = testNode(currentNode);

      if (source.substr(charIndex - 2, 2) == "/>") {
        // Self-closing element.
      } else {
        var re = RegExp("</" + aNodeName + "\\s*>");
        var leftover = source.substr(charIndex);
        var match = leftover.match(re);
        assert(match, "No match?");
        match = match[0];
        var index = getMinIndexOfString(source, charIndex, match);

        var tagOpen  = convertIndexToCoord(index);
        var tagClose = convertIndexToCoord(index + match.length - 1);
        charIndex = index + match.length;

        if (nodeAccepted) {
          endTagMap = new SourceMapCoordSet(
              tagOpen.lineNumber,
              tagOpen.columnNumber,
              tagClose.lineNumber,
              tagClose.columnNumber)
        }
      } // end if self-closing element.

      if (nodeAccepted) {
        currentNode.setUserData("sourceCodeMapping", {
          openTag:  startTagMap,
          closeTag: endTagMap
        }, null, null);
      }
    }),

    cdataStartIndex: 0,
    inCDataSection: false,

    // nsISAXContentHandler
    characters:
    ErrorToAssertion(function characters(aData) {
      if (this.elementNameStack.length == 0) {
        // This node won't show up in the document tree.
        return;
      }

      if (this.inCDataSection) {
        // lexicalHandler will take care of this.
        return;
      }

      if (!this.stillOnCDataNode) {
        advanceWalker();
      }
      checkNodeType(nsIDOMNode.TEXT_NODE, walker.currentNode);
      var nodeAccepted = testNode(walker.currentNode);

      this.cdataStartIndex = charIndex;

      /* Calls to this often end up with aData less than the current node's
       * length.  Which is odd, but manageable.  In a case like that, we want
       * a substring of the nodeValue, starting with the last known index and
       * with a length equal to aData.length.
       */

      var value = walker.currentNode.nodeValue;
      assert(value.substr(this.characterIndex, aData.length) == aData,
             "Text fragments don't match!");

      this.characterIndex += aData.length;

      assert(this.characterIndex <= value.length, "characterIndex is too big!");
      if (this.characterIndex >= value.length) {
        var cdataEndIndex = this.cdataStartIndex + this.characterIndex;

        var cdataStartCoord = convertIndexToCoord(this.cdataStartIndex);
        var cdataEndCoord = convertIndexToCoord(cdataEndIndex);

        var checkSource = source.substring(this.cdataStartIndex, cdataEndIndex);
        checkSource = decodeCharactersInString(checkSource)
        assert(checkSource == walker.currentNode.nodeValue, "Check values!");

        if (nodeAccepted) {
          var textMap = new SourceMapCoordSet(
            cdataStartCoord.lineNumber,
            cdataStartCoord.columnNumber,
            cdataEndCoord.lineNumber,
            cdataEndCoord.columnNumber);

          walker.currentNode.setUserData("sourceCodeMapping", {
            singleTagSet: textMap
          }, null, null);
        }

        this.characterIndex = 0;
        this.stillOnCDataNode = false;

        charIndex = cdataEndIndex;
      } else {
        this.stillOnCDataNode = true;
      }
    }), // end characters

    // nsISAXContentHandler
    processingInstruction:
    ErrorToAssertion(function processingInstruction(aTarget, aData) {
      advanceWalker();
      checkNodeType(nsIDOMNode.PROCESSING_INSTRUCTION_NODE, walker.currentNode);
      var nodeAccepted = testNode(walker.currentNode);

      var endIndex = charIndex + 2 + aTarget.length;
      assert(source.substring(charIndex, endIndex) == "<?" + aTarget,
             "Why aren't we at the start of a PI?");
      var startCoord = convertIndexToCoord(charIndex);

      charIndex = getMinIndexOfString(source, charIndex, "?>") + 2;

      if (!nodeAccepted) {
        return;
      }

      var endCoord = convertIndexToCoord(charIndex);

      walker.currentNode.setUserData("sourceCodeMapping", {
        singleTagSet: new SourceMapCoordSet(
          startCoord.lineNumber,
          startCoord.columnNumber,
          endCoord.lineNumber,
          endCoord.columnNumber)
      }, null, null);
    }),

    // nsISAXContentHandler
    ignorableWhitespace:
    ErrorToAssertion(function ignorableWhitespace(aWhitespace) {
      PARSE_NOT_IMPLEMENTED();
    }),

    // nsISAXContentHandler
    startPrefixMapping:
    ErrorToAssertion(function startPrefixMapping(aPrefix, aURI) {
      // Do nothing
    }),

    // nsISAXContentHandler
    endPrefixMapping:
    ErrorToAssertion(function endPrefixMapping(aPrefix) {
      // Do nothing
    })
  };

  var lexicalHandler = {
    // nsISAXLexicalHandler
    comment:
    ErrorToAssertion(function comment(aContents) {
      advanceWalker();
      checkNodeType(nsIDOMNode.COMMENT_NODE, walker.currentNode);
      var nodeAccepted = testNode(walker.currentNode);

      assert(source.substring(charIndex, charIndex + 4) == "<!--",
             "Why aren't we at the start of a comment?");
      var startCoord = convertIndexToCoord(charIndex);

      charIndex = getMinIndexOfString(source, charIndex, "-->") + 3;

      if (!nodeAccepted) {
        return;
      }

      var endCoord = convertIndexToCoord(charIndex);

      walker.currentNode.setUserData("sourceCodeMapping", {
        singleTagSet: new SourceMapCoordSet(
          startCoord.lineNumber,
          startCoord.columnNumber,
          endCoord.lineNumber,
          endCoord.columnNumber)
      }, null, null);
    }),

    // nsISAXLexicalHandler
    startDTD:
    ErrorToAssertion(function startDTD(aName, aPublicId, aSystemId) {
      advanceWalker();
      checkNodeType(nsIDOMNode.DOCUMENT_TYPE_NODE, walker.currentNode);
      var nodeAccepted = testNode(walker.currentNode);

      /* XXX ajvincent At present, nsSAXXMLReader does not do anything with the
       * internal subset of a DOCTYPE tag, including entities, comments,
       * <!ELEMENT > and <!ATTLIST > tags, etc.  Until it does, we handle our
       * DOCTYPE tag as one big blob, doing nothing about its contents.
       */
      var re = RegExp("<!DOCTYPE\\s+" + aName + "\\b");
      var leftover = source.substr(charIndex);
      var match = leftover.match(re)[0];
      assert(match, "No match?");
      var index = getMinIndexOfString(source, charIndex, match);

      if (nodeAccepted) {
        var firstDTDCoord = convertIndexToCoord(index);
      }

      if (!xeVerbosioUtils) {
        xeVerbosioUtils = Components.classes[xeVerbosioUtilsClass]
                                    .getService(xeIVerbosioUtils);
      }

      var dtdString = xeVerbosioUtils.getDTDFragment(source, index);
      charIndex = index + dtdString.length + 1;

      var srcCheck = source.substring(index, charIndex);
      assert(srcCheck.substr(0, match.length) == match,
             "Failed to match first characters!");
      assert(srcCheck.charAt(srcCheck.length - 1) == ">",
             "Failed to end at > character!");

      if (nodeAccepted) {
        var secondDTDCoord = convertIndexToCoord(firstGT);
      }
      charIndex = firstGT + 1;

      if (nodeAccepted) {
        walker.currentNode.setUserData("sourceCodeMapping", {
          singleTagSet: new SourceMapCoordSet(
            firstDTDCoord.lineNumber,
            firstDTDCoord.columnNumber,
            secondDTDCoord.lineNumber,
            secondDTDCoord.columnNumber)
        }, null, null);
      }
    }), // end startDTD

    // nsISAXLexicalHandler
    endDTD:
    ErrorToAssertion(function endDTD() {
      checkNodeType(nsIDOMNode.DOCUMENT_TYPE_NODE, walker.currentNode);
    }),

    // nsISAXLexicalHandler
    startCDATA:
    ErrorToAssertion(function startCDATA() {
      advanceWalker();
      checkNodeType(nsIDOMNode.CDATA_SECTION_NODE, walker.currentNode);

      contentHandler.inCDataSection = true;

      assert(source.substring(charIndex, charIndex + 9) == "<![CDATA[",
             "Why aren't we at the start of a CDATA section?");
      this.cdataStartIndex = charIndex;
    }),

    // nsISAXLexicalHandler
    endCDATA:
    ErrorToAssertion(function endCDATA() {
      checkNodeType(nsIDOMNode.CDATA_SECTION_NODE, walker.currentNode);
      var nodeAccepted = testNode(walker.currentNode);

      charIndex = getMinIndexOfString(source, charIndex, "]]>") + 3;

      var startCoord = convertIndexToCoord(this.cdataStartIndex);
      this.cdataStartIndex = 0;
      var endCoord = convertIndexToCoord(charIndex);

      if (nodeAccepted) {
        walker.currentNode.setUserData("sourceCodeMapping", {
          singleTagSet: new SourceMapCoordSet(
            startCoord.lineNumber,
            startCoord.columnNumber,
            endCoord.lineNumber,
            endCoord.columnNumber)
        }, null, null);
      }

      contentHandler.inCDataSection = false;
    }),

    // nsISAXLexicalHandler
    startEntity:
    ErrorToAssertion(function startEntity(aName) {
      PARSE_NOT_IMPLEMENTED();
    }),

    // nsISAXLexicalHandler
    endEntity:
    ErrorToAssertion(function endEntity(aName) {
      PARSE_NOT_IMPLEMENTED();
    })
  };

  var dtdHandler = {
    // nsISAXDTDHandler
    notationDecl:
    ErrorToAssertion(function notationDecl(aName, aPublicId, aSystemId) {
      PARSE_NOT_IMPLEMENTED();
    }),

    // nsISAXDTDHandler
    unparsedEntityDecl:
    ErrorToAssertion(function unparsedEntityDecl(aName,
                                                 aPublicId,
                                                 aSystemId,
                                                 aNotationName) {
      PARSE_NOT_IMPLEMENTED();
    })
  };

  var errorHandler = {
    // nsISAXErrorHandler
    error:
    ErrorToAssertion(function error(aLocator, aError) {
      PARSE_NOT_IMPLEMENTED();
    }),

    // nsISAXErrorHandler
    fatalError:
    ErrorToAssertion(function fatalError(aLocator, aError) {
      throw new Error(aError, aWrapper.publicURI, aLocator.lineNumber);
    }),

    // nsISAXErrorHandler
    ignorableWarning:
    ErrorToAssertion(function ignorableWarning(aLocator, aError) {
      PARSE_NOT_IMPLEMENTED();
    })
  };

  // Execution really begins here.
  var filterTypeMsg = "You have to accept something... ";
  filterTypeMsg += "try nsIDOMNodeFilter.SHOW_ALL.";
  assert(aFilterTypesMatch > 0, filterTypeMsg);

  var doc = aWrapper.wrappedDocument;
  var source = aWrapper.wrappedSource;

  /* Determine where the new line characters are.  This lets us convert
   * an index in the source to a line number and column number.
   */
  var sourceByLines = source.split("\n");
  var newLinesIndex = [0];
  for (var i = 0; i < sourceByLines.length; i++) {
    newLinesIndex[i + 1] = newLinesIndex[i] + sourceByLines[i].length + 1;
  }

  var charIndex = 0;
  var lineIndex = 0;
  var lastLineIndex = 0;

  parseSuccess = true;

  assert(doc instanceof nsIDOMDocumentTraversal,
         "Can't create tree walker!");
  if (!parseSuccess) {
    return parseSuccess;
  }

  clearDocumentSourceMaps(doc);
  if (!parseSuccess) {
    return parseSuccess;
  }

  var walker = doc.createTreeWalker(doc, nsIDOMNodeFilter.SHOW_ALL, null, true);
  const saxReader = Components.classes["@mozilla.org/saxparser/xmlreader;1"]
                              .createInstance(nsISAXXMLReader);

  namespacePrefixesFlag = false;
  try {
    saxReader.setFeature("http://xml.org/sax/features/namespace-prefixes", true);
    namespacePrefixesFlag = true;
  }
  catch (e) {
    dump(e + "\n");
  }

  // namespace flag, "http://xml.org/sax/features/namespace", defaults to true.

  saxReader.contentHandler = contentHandler;
  saxReader.lexicalHandler = lexicalHandler;
  saxReader.dtdHandler     = dtdHandler;
  saxReader.errorHandler   = errorHandler;

  saxReader.parseFromString(source, "application/xml");

  // Just in case it leaks.
  saxReader.contentHandler = null;
  saxReader.lexicalHandler = null;
  saxReader.dtdHandler     = null;
  saxReader.errorHandler   = null;

  if (parseSuccess) {
    // Postcondition checks:  make sure we set all the needed mappings.
    walker = doc.createTreeWalker(doc,
                                  aFilterTypesMatch,
                                  wrappedFilter,
                                  true);
    while (walker.nextNode()) {
      var currentNode = walker.currentNode;
      var assertMsg = "Didn't set source code mapping on ";
      assertMsg += currentNode.nodeName + " (type ";
      assertMsg += currentNode.nodeType + ")\n";
      try {
        // We really do want to assert here; we want to be sure we skipped nothing.
        NS_ASSERTION(currentNode.getUserData("sourceCodeMapping"), assertMsg);
      } catch (e) {
        // do nothing, parseSuccess is already false.
      }
    }

    // We're not done yet... check attributes.
    walker = doc.createTreeWalker(doc, nsIDOMNodeFilter.SHOW_ELEMENT, null, true);
    while (walker.nextNode()) {
      var currentNode = walker.currentNode;
      assert(currentNode instanceof nsIDOMElement, "Not an element?");
      for (var i = 0; i < currentNode.attributes.length; i++) {
        var attrNode = currentNode.attributes[i];

        if (!namespacePrefixesFlag && attrNode.namespaceURI == XMLNS_NS) {
          continue; // xml namespace attributes aren't supported.
        }

        if (testNode(attrNode)) {
          var assertMsg = "Didn't set source code mapping on ";
          assertMsg += attrNode.nodeName + " (type ";
          assertMsg += attrNode.nodeType + ")\n";
          try {
          // We really do want to assert here; we want to be sure we skipped nothing.
            NS_ASSERTION(attrNode.getUserData("sourceCodeMapping"), assertMsg);
          } catch (e) {
            // do nothing, parseSuccess is already false.
          }
        }
      }
    }
  } // end postcondition checks

  if (!parseSuccess) {
    clearDocumentSourceMaps(doc);
  }

  walker = null; // Clear leaks.
  return parseSuccess;
}

const NodePositionService = {
  // xeINodePositionService
  updateNodePositions: updateDocumentSourceMaps,

  _interfaces: [xeINodePositionService,
                nsIClassInfo],

  // nsIClassInfo
  getInterfaces: ArrayConverter.xpcomArrayMethod("_interfaces"),

  // nsIClassInfo
  getHelperForLanguage: function getHelperForLanguage(aLanguage) {
    return null;
  },

  // nsIClassInfo
  classDescription: "Node position service, JavaScript",

  // nsIClassInfo
  classID: Components.ID("{727542da-0138-4ddd-8714-5e95a88e2ff3}"),

  // nsIClassInfo
  contractID: "@verbosio.mozdev.org/node-position-service;1",

  // nsIClassInfo
  implementationLanguage: nsIProgrammingLanguage.JAVASCRIPT,

  // nsIClassInfo
  flags: nsIClassInfo.SINGLETON,

  // nsISupports
  QueryInterface: function QueryInterface(aIID) {
    for (var j = 0; j < this._interfaces.length; j++) {
      if (aIID.equals(this._interfaces[j])) {
        return this;
      }
    }

    if (aIID.equals(nsISupports))
      return this;

    // Discover what interfaces we need
    var i = Components.interfaces;
    var found = false;
    for (var prop in i) {
      if (aIID.equals(i[prop])) {
        found = true;
        dump("QUERYINTERFACE nodePositionService: " + prop + "\n");
        break;
      }
    }
    if (!found) {
      dump("QUERYINTERFACE nodePositionService: Unknown\n");
    }

    throw Components.results.NS_ERROR_NO_INTERFACE;
    return null;
  }
};

var NSGetModule = XPCOMUtils.generateNSGetModule([
  wrapServiceComponent(NodePositionService)
]);

dump("Node position service loaded!\n");
