/* ***** 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 Verbosio markup language bindings (XTF).
 *
 * The Initial Developer of the Original Code is
 * Alexander J. Vincent <ajvincent@gmail.com>.
 * Portions created by the Initial Developer are Copyright (C) 2007
 * 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");
Components.utils.import("resource://app/modules/ecma-debug.jsm");

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

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

// Utility function.
function ERROR_ABORT() {
  throw Components.results.NS_ERROR_ABORT;
}

// Utility function
function NOT_INITIALIZED() {
  throw Components.results.NS_ERROR_NOT_INITIALIZED;
}

// Utility function
function ALREADY_INITIALIZED() {
  throw Components.results.NS_ERROR_ALREADY_INITIALIZED;
}

var C_i = Components.interfaces;

const xeIMarkupTemplate           = C_i.xeIMarkupTemplate;
const xeIMarkupOutput             = C_i.xeIMarkupOutput;
const xeIMarkupRepeat             = C_i.xeIMarkupRepeat;
const xeIXMLLanguagePackService   = C_i.xeIXMLLanguagePackService;
const xeILanguagePackService      = C_i.xeILanguagePackService;
const xeIEntity                   = C_i.xeIEntity;
const nsIXTFElementFactory        = C_i.nsIXTFElementFactory;
const nsIXTFElement               = C_i.nsIXTFElement;
const nsIXTFPrivate               = C_i.nsIXTFPrivate;
const nsIXTFAttributeHandler      = C_i.nsIXTFAttributeHandler;
const nsIDOMNode                  = C_i.nsIDOMNode;
const nsIDOM3Node                 = C_i.nsIDOM3Node;
const nsIDOMElement               = C_i.nsIDOMElement;
const nsIDOMEventListener         = C_i.nsIDOMEventListener;
const nsIDOMNodeFilter            = C_i.nsIDOMNodeFilter;
const nsIDOMXPathNSResolver       = C_i.nsIDOMXPathNSResolver;
const nsIArray                    = C_i.nsIArray;
const nsICategoryManager          = C_i.nsICategoryManager;
const nsIProgrammingLanguage      = C_i.nsIProgrammingLanguage;
const nsISecurityCheckedComponent = C_i.nsISecurityCheckedComponent;
const nsIClassInfo                = C_i.nsIClassInfo;
const nsISupports                 = C_i.nsISupports;

/**
 * Assign user data from a source node onto an output node.
 *
 * @param aOutputNode Node to receive the user data.
 * @param aSourceNode Node to donate the user data.
 * @param aDataName   Name of the user data to copy.
 */
var assignUserData = getContractFunction({
  precondition:
  function assignUserData_pre(aOutputNode, aSourceNode, aDataName) {
    assert(aOutputNode instanceof nsIDOM3Node, "Not a DOM node?");
    assert(aSourceNode instanceof nsIDOM3Node, "Not a DOM node?");
    assert(typeof aDataName == "string", "Need a string argument!");
    dump("assignUserData(" + aOutputNode.nodeName + ", ");
    dump(aSourceNode.nodeName + ", " + aDataName + ")\n");
  },
  body: function assignUserData(aOutputNode, aSourceNode, aDataName) {
    var inUserData = aSourceNode.getUserData(aDataName);
    if (!inUserData) {
      return;
    }
    var outUserData = inUserData;

    // Clone entity arrays.
    if (inUserData instanceof nsIArray) {
      var allEntities = true;
      var entities = [];
      var len = inUserData.length;
      for (var i = 0; i < len; i++) {
        try {
          entities[i] = inUserData.queryElementAt(i, xeIEntity).clone();
        } catch (e) {
          allEntities = false;
          break;
        }
      }

      if (allEntities) {
        outUserData = ArrayConverter.nsIArray(entities);
      }
    }

    aOutputNode.setUserData(aDataName, outUserData, null);
    dump(aSourceNode.nodeName + ".getUserData('" + aDataName + "'): ");
    dump(outUserData.toSource() + "\n");
  }
});


const xtfContract = "@mozilla.org/xtf/element-factory;1?namespace=";
const nsContract  = "@verbosio.mozdev.org/language-pack-service;1?namespaceURI=";
const XML_NS      = "http://www.w3.org/XML/1998/namespace";
const XHTML_NS    = "http://www.w3.org/1999/xhtml";
const XMLNS_NS    = "http://www.w3.org/2000/xmlns/";
const MARKUP_NS   = "http://verbosio.mozdev.org/namespaces/markup/";
const XUL_NS      = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";

/**
 * Create a nsIDOMXPathNSResolver for a markup element's contents.
 *
 * @param aElement Element needing a resolver.
 */
function NSPrefixResolver(aElement) {
  if (!(aElement instanceof nsIDOMElement)) {
    throw Components.results.NS_ERROR_INVALID_ARG;
  }

  this.mNamespaceURIMap = {
    "xml": XML_NS,
    "xmlns": XMLNS_NS
  };

  var doc = aElement.ownerDocument;
  var walker = doc.createTreeWalker(
    aElement,
    nsIDOMNodeFilter.SHOW_ELEMENT,
    null,
    true);
  do {
    this.addNamespace(walker.currentNode);
    for (var i = 0; i < walker.currentNode.attributes.length; i++) {
      this.addNamespace(walker.currentNode.attributes[i]);
    }
  } while (walker.nextNode());
}
NSPrefixResolver.prototype = {
  /**
   * Add a namespace for a node if needed.
   *
   * @param aNode Element or attribute which may have a namespace and prefix.
   */
  addNamespace: function addNamespace(aNode) {
    if ((!aNode.prefix) || (aNode.prefix in this.mNamespaceURIMap)) {
      return;
    }

    this.mNamespaceURIMap[aNode.prefix] = aNode.namespaceURI;
  },

  // nsIDOMXPathNSResolver
  lookupNamespaceURI: function lookupNamespaceURI(aPrefix) {
    if (aPrefix in this.mNamespaceURIMap) {
      return this.mNamespaceURIMap[aPrefix];
    }
    return null;
  },

  // nsISupports
  QueryInterface: function QueryInterface(aIID) {
    if (aIID.equals(nsIDOMXPathNSResolver) ||
        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 NSPrefixResolver: " + prop + "\n");
        break;
      }
    }
    if (!found) {
      dump("QUERYINTERFACE NSPrefixResolver: Unknown\n");
    }

    throw Components.results.NS_ERROR_NO_INTERFACE;
    return null;
  }

}

const UNORDERED_NODE = C_i.nsIDOMXPathResult.ANY_UNORDERED_NODE_TYPE;

/**
 * Wrap a JavaScript object for passing to components code.
 */
function ObjectWrapper(object) {
  this.wrappedJSObject = object;
}

/******************************************************************************
 * Markup Language Pack Service                                               *
 *****************************************************************************/

MarkupLanguagePack = {
  // xeIXMLLanguagePackService
  namespaceURI: MARKUP_NS,

  // xeIXMLLanguagePackService
  prefix: "markup",

  _dtdURIArray: [],

  // xeIXMLLanguagePackService
  getDTDURIArray: ArrayConverter.xpcomArrayMethod("_dtdURIArray"),

  _xmlSchemaArray: [],

  // xeIXMLLanguagePackService
  getXMLSchemaArray: ArrayConverter.xpcomArrayMethod("_xmlSchemaArray"),

  // xeIXMLLanguagePackService
  attributeWrapperName: "wrapper",

  // xeIXMLLanguagePackService
  handleWhitespace: function handleWhitespace(aLocalName) {
    // XUL doesn't care about ignorable white space.
    return (xeIXMLLanguagePackService.WHITESPACE_IGNORABLE |
            xeIXMLLanguagePackService.WHITESPACE_GLOBAL);
  },

  isURINode: function isURINode(aNode) {
    // Markup elements don't have URI references.
    return false;
  },

  // xeIXMLLanguagePackService
  checkValidity: NOT_IMPLEMENTED,

  // xeILanguagePackService
  contentType: "application/x-vnd.verbosio.markup+xml",

  // xeILanguagePackService
  specificationURI: null,

  // xeILanguagePackService
  description: "Verbosio markup template language",

  // xeILanguagePackService
  id: "verbosio-markup",

  // xeILanguagePackService
  version: "0.2",

  // xeILanguagePackService
  getNewTransaction: NOT_IMPLEMENTED,

  // xeILanguagePackService
  getFirstSyntaxError: NOT_IMPLEMENTED,

  _interfaces: [xeIXMLLanguagePackService,
                xeILanguagePackService,
                nsIClassInfo],

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

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

  // nsIClassInfo
  get classDescription() {
    return this.description;
  },

  // nsIClassInfo
  classID: Components.ID("{ba1ac11a-f830-4c33-b28e-9fd16c6dcdbf}"),

  // nsIClassInfo
  contractID: nsContract + MARKUP_NS,

  // nsIClassInfo
  implementationLanguage: nsIProgrammingLanguage.JAVASCRIPT,

  // nsIClassInfo
  flags: nsIClassInfo.SINGLETON, /* service */

  // 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: MarkupLanguagePack " + prop + "\n");
        break;
      }
    }
    if (!found) {
      dump("QUERYINTERFACE: MarkupLanguagePack Unknown\n");
    }

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

/******************************************************************************
 * Markup Language XTF bindings                                               *
 ******************************************************************************/

function MarkupElement() {}
MarkupElement.prototype =
{
  // nsIXTFElement
  onCreated: function onCreated(aWrapper) {
    aWrapper.notificationMask = 0;
    /* XXX ajvincent Before setting the .inner property, make sure the document
     * is trusted!!  (Check aWrapper.elementNode.ownerDocument.documentURI?)
     */
    this._elementWrapper = aWrapper;
  },

  // nsIXTFElement
  onDestroyed: function onDestroyed() {
    this._elementWrapper = null;
  },

  // nsIXTFElement
  isAttributeHandler: false,

  _scriptingInterfaces: [],

  // nsIXTFElement
  getScriptingInterfaces:
  ArrayConverter.xpcomArrayMethod("_scriptingInterfaces"),

  // nsIXTFElement
  willChangeDocument: NOT_IMPLEMENTED,
  documentChanged: NOT_IMPLEMENTED,
  willChangeParent: NOT_IMPLEMENTED,
  parentChanged: NOT_IMPLEMENTED,
  willInsertChild: NOT_IMPLEMENTED,
  childInserted: NOT_IMPLEMENTED,
  willAppendChild: NOT_IMPLEMENTED,
  childAppended: NOT_IMPLEMENTED,
  willRemoveChild: NOT_IMPLEMENTED,
  childRemoved: NOT_IMPLEMENTED,
  /* XXX ajvincent Stupid bloody API doesn't support namespaced attributes!!!
     <markup:foo bar:baz="wop" foo:baz="bleh"/> will cause problems.
     Alternative: use nsIXTFElement.handleDefault()
  */
  willSetAttribute: NOT_IMPLEMENTED,
  attributeSet: NOT_IMPLEMENTED,
  willRemoveAttribute: NOT_IMPLEMENTED,
  attributeRemoved: NOT_IMPLEMENTED,

  beginAddingChildren: NOT_IMPLEMENTED,
  doneAddingChildren: NOT_IMPLEMENTED,
  handleDefault: NOT_IMPLEMENTED,

  cloneState: function cloneState(aElement) {
    // do nothing
  },

  get accesskeyNode() {
    return null;
  },
  performAccesskey: NOT_IMPLEMENTED,

  // nsISupports
  QueryInterface: function QueryInterface(aIID) {
    if (aIID.equals(nsIXTFPrivate) ||
        aIID.equals(nsIXTFElement) ||
        aIID.equals(nsISupports))
      return this;

    // Maybe we've scripted support for it.
    var sInterfaces = this.getScriptingInterfaces({});
    for (i = 0; i < sInterfaces.length; i++) {
      if (aIID.equals(sInterfaces[i])) {
        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: MarkupElement " + prop + "\n");
        break;
      }
    }
    if (!found) {
      dump("QUERYINTERFACE: MarkupElement Unknown\n");
    }

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

MarkupConstructors = {
}

function addMarkupConstructor(aName, aFunction, aProperties) {
  MarkupConstructors[aName] = aFunction;
  aFunction.prototype = new MarkupElement();
  for (var prop in aProperties) {
    aFunction.prototype[prop] = aProperties[prop];
  }
}

// whitespaceMap:  Determines when whitespace matters for <markup:template/>.

const WHITESPACE_GLOBAL_REMOVE    = {};
const WHITESPACE_GLOBAL_IGNORABLE = {};
const WHITESPACE_GLOBAL_PRESERVE  = {};
const WHITESPACE_MAP_NEW          = {};
const WHITESPACE_NO_PACK_FOUND    = {};

const whitespaceMap = {
  _data: {},

  _packs: {
    MARKUP_NS: MarkupLanguagePack.QueryInterface(xeIXMLLanguagePackService)
  },

  /**
   * Check how to handle whitespace children of a node.
   *
   * @param aNode The node to look up whitespace handling on.
   *
   * @return xeIXMLLanguagePackService.WHITESPACE_REMOVE or
   *         xeIXMLLanguagePackService.WHITESPACE_IGNORABLE or
   *         xeIXMLLanguagePackService.WHITESPACE_PRESERVE.
   */
  checkNode: getContractFunction({
    precondition: function checkNode_pre(aNode) {
      dump("Entering whitespaceMap.checkNode\n");
      if (!(aNode.namespaceURI in this._data)) {
        return;
      }
      var map = this._data[aNode.namespaceURI];
      assert(map != WHITESPACE_MAP_NEW, "Bad state!");
    },

    body: function checkNode(aNode) {
      if (!(aNode.namespaceURI in this._data)) {
        this._data[aNode.namespaceURI] = WHITESPACE_MAP_NEW;
      }
      var map = this._data[aNode.namespaceURI];
      // Did we cache a value we support already?
      switch (map) {
        case WHITESPACE_GLOBAL_REMOVE:
          return xeIXMLLanguagePackService.WHITESPACE_REMOVE;
        case WHITESPACE_GLOBAL_IGNORABLE:
          return xeIXMLLanguagePackService.WHITESPACE_IGNORABLE;
        case WHITESPACE_GLOBAL_PRESERVE:
          return xeIXMLLanguagePackService.WHITESPACE_PRESERVE;
        case WHITESPACE_MAP_NEW:
          // We're just starting out, and we don't know anything.
          break;

        default: // Ordinary {} object; we may have cached the value.
          if (aNode.localName in map) {
            return map[aNode.localName];
          }
      }

      // Get the language pack.
      var pack;
      if (!(aNode.namespaceURI in this._packs)) {
        var classID = nsContract + aNode.namespaceURI;
        pack = Components.classes[classID]
                         .getService(xeIXMLLanguagePackService);
        this._packs[aNode.namespaceURI] = pack ?
                                          pack :
                                          WHITESPACE_NO_PACK_FOUND;
      }
      pack = this._packs[aNode.namespaceURI];

      // Find out how the language pack handles this element.
      if (pack == WHITESPACE_NO_PACK_FOUND) {
        // We must preserve all whitespace.
        this._data[aNode.namespaceURI] = WHITESPACE_GLOBAL_PRESERVE;
        return xeIXMLLanguagePackService.WHITESPACE_PRESERVE;
      }
      var flags = pack.handleWhitespace(aNode.localName);
      var flagPreserve  = flags & xeIXMLLanguagePackService.WHITESPACE_PRESERVE;
      var flagIgnorable = flags & xeIXMLLanguagePackService.WHITESPACE_IGNORABLE;
      var flagRemove    = flags & xeIXMLLanguagePackService.WHITESPACE_REMOVE;
      var flagGlobal    = flags & xeIXMLLanguagePackService.WHITESPACE_GLOBAL;

      // "Must set exactly one of the following: preserve, ignore, remove"
      var flagTotal = 0;
      if (flagPreserve) {
        flagTotal++;
      }
      if (flagIgnorable) {
        flagTotal++;
      }
      if (flagRemove) {
        flagTotal++;
      }
      // We don't count the global flag among the total check.

      if (getDebugPref()) { // Debug checks

        dump(aNode.nodeName + "\n");
        dump("flagPreserve: " + flagPreserve + "\n");
        dump("flagIgnorable:" + flagIgnorable + "\n");
        dump("flagRemove: " + flagRemove + "\n");
        dump("flagGlobal: " + flagGlobal + "\n");

        assert((map == WHITESPACE_MAP_NEW) || !flagGlobal,
               "Global whitespace handling for an existing language pack?");
        assert(flagTotal == 1,
               "Must set exactly one of the following: preserve, ignore, remove");
      }

      if (flagGlobal) {
        // We should store one of the global whitespace object flags.
        switch (true) {
          case Boolean(flagPreserve):
            this._data[aNode.namespaceURI] = WHITESPACE_GLOBAL_PRESERVE;
            return flagPreserve;
            break;
          case Boolean(flagIgnorable):
            this._data[aNode.namespaceURI] = WHITESPACE_GLOBAL_IGNORABLE;
            return flagIgnorable;
            break;
          case Boolean(flagRemove):
            this._data[aNode.namespaceURI] = WHITESPACE_GLOBAL_REMOVE;
            return flagRemove;
            break;
        }
      } else if (map == WHITESPACE_MAP_NEW) {
        map = {};
        this._data[aNode.namespaceURI] = map;
      }

      assert(!(aNode.localName in map), "Shouldn't have " + aNode.localName);
      map[aNode.localName] = 0;
      switch (true) {
        case Boolean(flagPreserve):
          map[aNode.localName] = flagPreserve;
          break;
        case Boolean(flagIgnorable):
          map[aNode.localName] = flagIgnorable;
          break;
        case Boolean(flagRemove):
          map[aNode.localName] = flagRemove;
          break;
      }

      assert(map[aNode.localName] > 0, "What happened?");

      return map[aNode.localName];
    },

    postcondition: function checkNode_postcondition(aNode, rv) {
      assert((rv == xeIXMLLanguagePackService.WHITESPACE_REMOVE) ||
             (rv == xeIXMLLanguagePackService.WHITESPACE_IGNORABLE) ||
             (rv == xeIXMLLanguagePackService.WHITESPACE_PRESERVE),
             "Bad return from whitespaceMap.checkNode():" + rv);
      dump("Exiting whitespaceMap.checkNode successfully\n");
    }
  })
}

/* <markup:template> */
addMarkupConstructor("template",
  function MarkupTemplate() {
    // nsIXTFPrivate
    this.inner = null;

    this._elementWrapper = null;
    this.originalNode = null;
  },
  {
    // nsIXTFElement
    onCreated: function onCreated(aWrapper) {
      aWrapper.notificationMask = 0;
      /* XXX ajvincent Before setting the .inner property, make sure the document
       * is trusted!!  (Check aWrapper.elementNode.ownerDocument.documentURI?)
       */
      this._elementWrapper = aWrapper;
      aWrapper.elementNode.addEventListener("input", this, true);
    },

    // xeIMarkupTemplate
    getOutputContainer: function getOutputContainer() {
      var element = this._elementWrapper.elementNode;
      var rv = element.getElementsByTagNameNS(MARKUP_NS, "output")[0];
      if (!(rv instanceof xeIMarkupOutput)) {
        return null;
      }
      return rv;
    },

    /**
     * Determine if a node is a repeating template node for another.
     *
     * @param baseContainer  Base template node (contains match nodes).
     * @param checkContainer Parent node for node to check.
     * @param checkOffset    Index of node to check among sibling nodes.
     * @param endCheckOffset Index of check node to stop searching.
     *
     * @retval null if the node is not a base template of the check node.
     * @retval {count: number the number of matches on the base node} .
     */
    isRepeatBaseNode: function isRepeatBaseNode(baseContainer,
                                                checkContainer,
                                                checkOffset,
                                                endCheckOffset) {
      /* matchCount increments for each match of all the repeat node's
       * children to the check container, starting with the checkOffset
       * node.  subMatchCount is the number of matches for individual
       * children of the repeat node against a group of check container
       * child nodes.
       */

      var interval = baseContainer.childNodes.length;
      var matchCount = 0;
      var subMatchCount = 0;
      var checkNode = checkContainer.childNodes[checkOffset];
      var endCheckNode = null;
      if (endCheckOffset < checkContainer.childNodes.length) {
        endCheckNode = checkContainer.childNodes[endCheckOffset];
      }

      dump("Check node: " + checkNode + "\n");
      dump("End check node: " + endCheckNode + "\n");

      var isOk;
      while (checkNode != endCheckNode) {
        subMatchCount++;
        var index = checkOffset + (matchCount * interval) + subMatchCount - 1;
        isOk = this.isBaseNode(baseContainer,
                               subMatchCount - 1,
                               checkContainer,
                               index);
        dump("isOk: " + isOk.toSource() + "\n");
        if (!isOk) {
          break;
        }

        checkNode = checkNode.nextSibling;
        if (subMatchCount != interval) {
          continue;
        }

        matchCount++;
        subMatchCount = 0;

        if (matchCount == baseContainer.maxCount) {
          break;
        }
      }

      dump("matchCount: " + matchCount + "\n");

      if (matchCount < baseContainer.minCount) {
        dump("markup:repeat, found " + matchCount + " matches, ");
        dump("required " + baseContainer.minCount + ".\n");

        return null;
      }

      return {count: matchCount * interval};
    },

    /**
     * Determine if a node is a base template node for another.
     *
     * @param baseContainer  Parent node for base template node.
     * @param baseOffset     Index of base template node among sibling nodes.
     * @param checkContainer Parent node for node to check.
     * @param checkOffset    Index of node to check among sibling nodes.
     *
     * @retval null if the node is not a base template of the check node.
     * @retval {count: number the number of matches for the check node}.
     */
    isBaseNode: function isBaseNode(baseContainer,
                                    baseOffset,
                                    checkContainer,
                                    checkOffset) {
      const defaultAdvance = {count: 1};
      var wsFlag;

      var baseNode = null;
      if (baseOffset < baseContainer.childNodes.length) {
        baseNode = baseContainer.childNodes[baseOffset];
      }

      wsFlag = -1;
      while ((baseNode.nodeType == nsIDOMNode.TEXT_NODE) &&
             !(/\S/.test(baseNode.nodeValue))) {
        if (wsFlag == -1) {
          wsFlag = whitespaceMap.checkNode(baseContainer);
        }
        if (wsFlag == xeIXMLLanguagePackService.WHITESPACE_PRESERVE) {
          break;
        }
        baseNode = baseNode.nextSibling;
      }

      var checkNode = null;
      if (checkOffset < checkContainer.childNodes.length) {
        checkNode = checkContainer.childNodes[checkOffset];
      }

      wsFlag = -1;
      while ((checkNode.nodeType == nsIDOMNode.TEXT_NODE) &&
             !(/\S/.test(checkNode.nodeValue))) {
        if (wsFlag == -1) {
          wsFlag = whitespaceMap.checkNode(checkContainer);
        }
        if (wsFlag == xeIXMLLanguagePackService.WHITESPACE_PRESERVE) {
          break;
        }
        checkNode = checkNode.nextSibling;
      }


      if ((baseNode) &&
          (baseNode.nodeType == nsIDOMNode.ELEMENT_NODE) &&
          (baseNode.namespaceURI == MARKUP_NS)) {
        if (baseNode.localName == "repeat") {
          /* A repeat isn't a base node for anything.  But its contents can
           * be base nodes for several nodes.  So we do a little magic
           * specific to <markup:repeat/> elements.
           */
          var interval = baseNode.childNodes.length;
          dump("Interval: " + interval + "\n");
          var endCheckOffset = interval * baseNode.maxCount + checkOffset;

          if (endCheckOffset > checkContainer.childNodes.length) {
            endCheckOffset = checkContainer.childNodes.length;
          }

          dump("baseNode: " + baseNode.nodeName + "\n");
          dump("checkContainer: " + checkContainer.nodeName + "\n");
          dump("checkOffset: " + checkOffset + "\n");
          dump("endCheckOffset: " + endCheckOffset + "\n");

          var response = this.isRepeatBaseNode(baseNode,
                                               checkContainer,
                                               checkOffset,
                                               endCheckOffset);
          dump("isRepeatBaseNode returned " + response.toSource() + "\n");
          return response;
        } // end baseNode == <markup:repeat/>

        if (baseNode.localName == "children") {
          return {count: baseNode.getMatchCount(checkNode)};
        } // end baseNode == <markup:children/>
      }

      if (!baseNode && !checkNode) {
        // When we've reached here, we know everything is cool.
        return defaultAdvance;
      }

      if (!baseNode || !checkNode) {
        /* We were expecting a node and didn't get one, or we got a node
         * we didn't expect.
         */
        return null;
      }

      dump("Entering node checks for " + baseNode.nodeName);
      dump(" == " + checkNode.nodeName + "\n");

      if (baseNode.nodeType != checkNode.nodeType) {
        dump("Node types didn't match.\n");
        return null;
      }

      if (baseNode.namespaceURI != checkNode.namespaceURI) {
        dump("Namespace URI's didn't match.\n");
        return null;
      }

      if (baseNode.localName != checkNode.localName) {
        dump("Local names didn't match.\n");
        return null;
      }

      var i;
      var j;
      switch (baseNode.nodeType) {
        case nsIDOMNode.ELEMENT_NODE:
          for (i = 0; i < baseNode.attributes.length; i++) {
            var attr = baseNode.attributes[i];
            if (!checkNode.hasAttributeNS(attr.namespaceURI, attr.localName)) {
              dump("Didn't find attribute with namespace " + attr.namespaceURI);
              dump(" and local name " + attr.localName + "\n");
              return null;
            }
          }
          break;

        case nsIDOMNode.DOCUMENT_FRAGMENT_NODE:
          break;

        case nsIDOMNode.TEXT_NODE:
        case nsIDOMNode.CDATA_SECTION_NODE:
        case nsIDOMNode.PROCESSING_INSTRUCTION_NODE:
        case nsIDOMNode.COMMENT_NODE:
        case nsIDOMNode.DOCUMENT_TYPE_NODE:
          dump("Base node: " + baseNode.nodeName + ", ");
          dump("returning defaultAdvance: " + defaultAdvance + "\n");
          return defaultAdvance;

        default:
          throw new Error("Unexpected base node type: " + baseNode.nodeType);
      }

      i = 0;
      j = 0;
      while (i < baseNode.childNodes.length) {
        var advance = this.isBaseNode(baseNode, i, checkNode, j);
        if (!advance) {
          dump("Failed to advance for " + baseNode.nodeName);
          dump(" not matching " + checkNode.nodeName + "\n");
          return null;
        }

        i++;
        if (advance.count < 1) {
          throw new Error("Why don't we have a positive advance count?");
        }
        j += advance.count;
      }

      dump("base node: " + baseNode.nodeName + "\n");
      dump("checkNode: " + checkNode.nodeName + "\n");

      if (j != checkNode.childNodes.length) {
        dump("Child count didn't match: " + j);
        dump(" != " + checkNode.childNodes.length + "\n");
        return null;
      }

      dump("Success with " + baseNode.nodeName + "\n");
      return defaultAdvance;
    },

    /**
     * Determine if the bound element is a base template for a node.
     *
     * @param aNode Node to check against the bound element.
     *
     * @retval True if the bound element is a matching base template.
     */
    isBaseTemplate: function isBaseTemplate(aNode) {
      /* XXX This is just enough code to get by without re-implementing
       * the isBaseNode method.
       */
      var checkContainer = {
        childNodes: [aNode]
      }
      var checkOffset = 0;
      return Boolean(this.isBaseNode(this.getOutputContainer(),
                                     checkOffset,
                                     checkContainer,
                                     checkOffset));
    },

    /**
     * Return a DOM fragment representing the edited content of the template.
     *
     * @param attachOriginals True when including nodes outside template.
     *
     * @return nsIDOMDocumentFragment containing template results.
     */
    exportEditedFragment: function exportEditedFragment(attachOriginals) {
      var refNode = this.getOutputContainer();
      var wrapper = refNode.cloneNode(true);

/*
      if (attachOriginals) {
        // What is hbox, response?
        var sourceKids = hbox.getElementsByTagNameNS(MARKUP_NS, "children");
        var targetKids = response.getElementsByTagNameNS(MARKUP_NS, "children");

        for (i = 0; i < sourceKids.length; i++) {
          // We don't support <markup:children/> yet.
          NOT_IMPLEMENTED();
          targetKids[i].originalNodeList = sourceKids[i].originalNodeList;
        }
      }
*/

      var sourceRepeats = refNode.getElementsByTagNameNS(MARKUP_NS, "repeat");
      var targetRepeats = wrapper.getElementsByTagNameNS(MARKUP_NS, "repeat");
      if (sourceRepeats.length > 0) {
        NOT_IMPLEMENTED(); // we don't support <markup:repeat/> yet.
      }

      var i;
      for (i = 0; i < sourceRepeats.length; i++) {
        sourceRepeats[i].exportEditedNode(targetRepeats[i], attachOriginals);
      }

/*
      removeWhitespaceByNamespace(editedWrapper, XUL_NS);
      removeWhitespaceByNamespace(editedWrapper, MARKUP_NS);
*/

      var fragment = refNode.ownerDocument.createDocumentFragment();
      while (wrapper.firstChild) {
        fragment.appendChild(wrapper.firstChild);
      }

      return fragment;
    },

    // xeIMarkupLanguage
    rateSupport: function rateSupport(aNode) {
      if (!this.isBaseTemplate(aNode)) {
        return 0;
      }

      // nsIXTFPrivate.inner.wrappedJSObject.rateSupport()?

      return 1;
    },

    /**
     * Get the output container's node
     *
     * @param aSourceNode XUL control node (or event target) to import from.
     */
    matchUINodeToSourceNode:
    function matchUINodeToSourceNode(aSourceNode) {
      // Get the baseline XUL widget we depend on.
      var baseNode = aSourceNode;
      if ((baseNode.namespaceURI == XHTML_NS) &&
          ((baseNode.localName == "input") || (baseNode.localName == "textarea"))) {
        try {
          baseNode = baseNode.ownerDocument.getBindingParent(baseNode);
        }
        finally {
          if (!baseNode) {
            baseNode = aSourceNode;
          }
        }
      }

      /* Record the xpath we are emulating.  Split off the last step,
       * which has an attribute or text node that probably doesn't exist
       * yet...
       */
      var xpath = baseNode.getAttributeNS(MARKUP_NS, "xpath");
      assert(xpath, "No xpath here?");
      var targetNode = null;
      var fieldType = null;
      var re = null;
      var modifyNode = null;
      var matchIndex = -1;

      // Match everything.
      if (!fieldType) {
        re = /\/\*$/;
        matchIndex = xpath.search(re);
        if (matchIndex != -1) {
          fieldType = "*";
        }
      }

      // Attributes.
      if (!fieldType) {
        re = /\/\@\w+(\:\w+)*$/;
        matchIndex = xpath.search(re);
        if (matchIndex != -1) {
          fieldType = "attr";
          var attrName = xpath.substr(matchIndex + 2);
          xpath = xpath.substr(0, matchIndex);
        }
      }

      // Text nodes.
      if (!fieldType) {
        re = /\/text\((\n*)\)$/;
        matchIndex = xpath.search(re);
        if (matchIndex != -1) {
          fieldType = "text";
          var textMatch = xpath.substr(matchIndex + 1);
          xpath = xpath.substr(0, matchIndex);
        }
      }

      // XXX ajvincent Comments?  Processing instruction target or data?
      assert(fieldType,
             "markup:xpath must refer to an attribute, a text node or CDATA section!");

      // Find the repeat elements from the source node to this - and note the row indexes.
      var node = aSourceNode;
      var rowNode = null;
      var anonymousRepeats = [];
      while (node) {
        if ((node.namespaceURI == XUL_NS) &&
            (node.localName == "row") &&
            (typeof node.parentIndex == "number")) {
          rowNode = node;
        }

        if ((node.namespaceURI == MARKUP_NS) &&
            (node.localName == "repeat") &&
            (rowNode)) {
          anonymousRepeats.unshift({
            index: rowNode.parentIndex,
            localid: node.getAttribute("localid")
          });
          rowNode = null;
        }

        if (node == this._elementWrapper.elementNode) {
          break;
        }

        node = node.parentNode;
      }

      // Iterate to find the context node (through anon content) for our xpath.
      var xpathContextNode = this.getOutputContainer();

      var filter = {
        localid: "",
        acceptNode: function acceptNode(aNode) {
          return (aNode.getAttribute("localid") == this.localid) ?
                   nsIDOMNodeFilter.FILTER_ACCEPT :
                   nsIDOMNodeFilter.FILTER_SKIP;
        }
      }

      for (var i = 0; i < anonymousRepeats.length; i++) {
        var walker = document.createTreeWalker(
          xpathContextNode,
          nsIDOMNodeFilter.SHOW_ELEMENT,
          filter,
          true
        );
        filter.localid = anonymousRepeats[i].localid;
        var repeatNode = walker.nextNode();
        walker = null; // leak clean-up :)

        var index = anonymousRepeats[i].index;
        // xul:row/xul:hbox
        xpathContextNode = repeatNode.rows.childNodes[index].firstChild;
      }

      var doc = xpathContextNode.ownerDocument;
      /* XXX ajvincent The xpath context node by now has been cloned and
         removed from the original markup template document it came from.
         Gecko's native namespace resolver checks the ancestors of the
         context node, but now those ancestors are hardly guaranteed to have
         namespace prefixes.

         The NSPrefixResolver() constructor walks over the context node's
         elements and attributes, and builds a namespace map based on them.
      */
      var resolver = new NSPrefixResolver(xpathContextNode);
      var xpathExpression = doc.createExpression(xpath, resolver);
      var xpathResult = xpathExpression.evaluate(
        xpathContextNode,
        UNORDERED_NODE,
        null
      );
      var targetNode = xpathResult.singleNodeValue;

      var response = {
        targetNode: targetNode,
        fieldType: fieldType,
        attrName: attrName,
        textMatch: textMatch,
        resolver: resolver
      };

      assert(targetNode, "We should have a target node here...");
      return response;
    },

    /**
     * Get the output container's node
     *
     * @param aSourceNode XUL control node (or event target) to import from.
     */
    updateFromImportedTemplate:
    function updateFromImportedTemplate(aSourceNode) {
      // assert(aSourceNode has a XBL binding, a value property);
      var wrapperObj = this.matchUINodeToSourceNode(aSourceNode);
      var targetNode = wrapperObj.targetNode;
      var fieldType  = wrapperObj.fieldType;
      var attrName   = wrapperObj.attrName;
      var textMatch  = wrapperObj.textMatch;
      var resolver   = wrapperObj.resolver;

      switch (fieldType) {
        case "attr":
          var nsURI = null;
          if (attrName.indexOf(":") != -1) {
            var nsPrefix = attrName.substr(0, attrName.indexOf(":"));
            nsURI = resolver.lookupNamespaceURI(nsPrefix);
          }

          var attrNode = targetNode.getAttributeNodeNS(nsURI, attrName);
          if (!attrNode) {
            break;
          }

          switch (aSourceNode.namespaceURI) {
            case XUL_NS:
              aSourceNode.value = attrNode.nodeValue;
              break;
            case MARKUP_NS:
              if (aSourceNode.localName == "entitybox") {
                aSourceNode.setUserData("value", attrNode.nodeValue, null);
                break;
              }
              // fall through
            default:
              dump(aSourceNode.nodeName + "\n");
              throw new Error("Unexpected node type for setting value!");
          }

          assignUserData(aSourceNode, attrNode, "entityMatches");
          break;

        case "*":
          dump("this.originalNode hasn't been set\n");
          ERROR_FAILURE();
          if ((this.originalNode instanceof nsIDOMNode) &&
              (this.originalNode.childNodes.length > 1) ||
              ((this.originalNode.nodeType != nsIDOMNode.TEXT_NODE) &&
               (this.originalNode.nodeType != nsIDOMNode.CDATA_SECTION_NODE)))
          {
            throw new Error("This input is tied to a <markup:children/> " +
                            "element, and can't accept any direct input!");
          }
          // Otherwise, we fall through cleanly into the text node case.
          textMatch = "text()";

        case "text":
          var xpathExpression = document.createExpression(textMatch, resolver);

          if (!targetNode) {
            throw new Error("Expected a target node!");
          }

          var xpathResult = xpathExpression.evaluate(
            targetNode,
            UNORDERED_NODE,
            null
          );
          var textNode = xpathResult.singleNodeValue;
          var newValue = textNode.nodeValue;

          aSourceNode.value = newValue;
          break;

        default:
          throw new Error("Not implemented for node type " +
                          aSourceNode.nodeType + ".");
      }
    },

    /**
     * Update the template based on user input for an attribute or text node.
     *
     * @param aSourceNode The control which a user has just changed.
     */
    updateFromUISection: function updateFromUISection(aSourceNode) {
      var wrapperObj = this.matchUINodeToSourceNode(aSourceNode);
      var targetNode = wrapperObj.targetNode;
      var fieldType  = wrapperObj.fieldType;
      var attrName   = wrapperObj.attrName;
      var textMatch  = wrapperObj.textMatch;
      var resolver   = wrapperObj.resolver;

      /* If we've gotten here without barfing, then we have the node we're
         supposed to base our modification on.
      */

      var newValue = aSourceNode.value;

      switch (fieldType) {
        case "attr":
          var nsURI = null;
          if (attrName.indexOf(":") != -1) {
            var nsPrefix = attrName.substr(0, attrName.indexOf(":"));
            nsURI = resolver.lookupNamespaceURI(nsPrefix);
          }

          targetNode.setAttributeNS(nsURI, attrName, newValue);
          break;

        case "*":
          dump("this.originalNode hasn't been set\n");
          ERROR_FAILURE();
          if ((this.originalNode instanceof nsIDOMNode) &&
              (this.originalNode.childNodes.length > 1) ||
              ((this.originalNode.nodeType != nsIDOMNode.TEXT_NODE) &&
               (this.originalNode.nodeType != nsIDOMNode.CDATA_SECTION_NODE)))
          {
            var msg = "This input is tied to a <markup:children/> element, ";
            msg += "and can't accept any direct input!"
            throw new Error(msg);
          }
          // Otherwise, we fall through cleanly into the text node case.
          textMatch = "text()";

        case "text":
          var expr = document.createExpression(textMatch, resolver);
          xpathResult = expr.evaluate(targetNode,
                                      UNORDERED_NODE,
                                      null);
          var textNode = xpathResult.singleNodeValue;
          if (textNode) {
            textNode.value = newValue;
          } else {
            textNode = document.createTextNode(newValue);
            targetNode.appendChild(textNode);
          }

          break;

        default:
          msg = "Not implemented for node type " + aSourceNode.nodeType + ".";
          throw new Error(msg);
      }
    },

    // xeIMarkupTemplate
    getOutputNode: function getOutputNode(aNode) {
      var wrapper = this.matchUINodeToSourceNode(aNode);
      return wrapper.targetNode;
    },

    // xeIMarkupTemplate
    processXPathInputs: function processXPathInputs() {
      const filter = {
        acceptedType: 0,

        isMarkupRepeatElement: function isMarkupRepeatElement(aNode) {
          return ((aNode.namespaceURI == MARKUP_NS) &&
                  (aNode.localName == "repeat"));
        },

        // nsIDOMNodeFilter
        acceptNode: function acceptNode(aNode) {
          this.acceptedType = 0;

          if ((aNode.namespaceURI == MARKUP_NS) &&
              (aNode.localName == "output")) {
              return nsIDOMNodeFilter.FILTER_REJECT;
          }

          if (this.isMarkupRepeatElement(aNode.parentNode)) {
            return nsIDOMNodeFilter.FILTER_REJECT;
          }

          if (this.isMarkupRepeatElement(aNode)) {
            this.acceptedType = 2;
            return nsIDOMNodeFilter.FILTER_ACCEPT;
          }

          if (aNode.hasAttributeNS(MARKUP_NS, "xpath")) {
            this.acceptedType = 1;
            return nsIDOMNodeFilter.FILTER_ACCEPT;
          }

          return nsIDOMNodeFilter.FILTER_SKIP;
        }
      } // end filter definition

      var doc = this._elementWrapper.elementNode.ownerDocument;
      var walker = doc.createTreeWalker(this._elementWrapper.elementNode,
                                        nsIDOMNodeFilter.SHOW_ELEMENT,
                                        filter,
                                        true);
      while (walker.nextNode()) {
        switch (filter.acceptedType) {
          case 1:
            this.updateFromImportedTemplate(walker.currentNode);
            dump("Imported value: " + walker.currentNode.value + "\n");
            break;

          case 2:
            var targetLocalId = walker.currentNode.getAttribute("localid");
            if (!targetLocalId) {
              throw new Error("Local id needed on ui-section markup:repeat!");
            }

            var repeats = this.getOutputContainer()
                              .getElementsByTagNameNS(MARKUP_NS, "repeat");
            var sourceRepeat = null;
            for (i = 0; (i < repeats.length) && !sourceRepeat; i++) {
              if (repeats[i].getAttribute("localid") == targetLocalId)
                sourceRepeat = repeats[i];
            }

            if (!sourceRepeat) {
              throw new Error("source repeat node not found!");
            }

            var rowCount = sourceRepeat.rowCount;
            assert(walker.currentNode instanceof xeIMarkupRepeat,
                   "Expected <markup:repeat/> element here!");
            walker.currentNode.clearRows();
            for (i = 0; i < rowCount; i++) {
              walker.currentNode.addRow(null);
              var row = walker.currentNode.lastRow.previousSibling;
              // XXX ajvincent This really should use a recursive tree walker.
              var xpaths = row.getElementsByAttributeNS(MARKUP_NS, "xpath", "*");
              for (var j = 0; j < xpaths.length; j++) {
                this.updateFromImportedTemplate(xpaths[j]);
              }
            }

            break;

          default:
            dump(typeof filter.acceptedType + " " + filter.acceptedType + "\n");
            throw new Error("Unexpected type found for filter (" + filter.acceptedType + ")");
        } // end switch (filter.acceptedType)
      }

      // Clean up leakables.
      walker = null;
      filter = null;
    },

    // nsIXTFElement
    _scriptingInterfaces: [xeIMarkupTemplate,
                           nsIDOMEventListener],

    // nsIDOMEventListener
    handleEvent: function handleEvent(aEvent) {
      this.updateFromUISection(aEvent.target);
    }
  }
); /* </markup:template> */

/* <markup:output> */
addMarkupConstructor("output",
  function MarkupOutput() {
    // nsIXTFPrivate
    this.inner = null;

    this._elementWrapper = null;
    this._originalNode = null;
  },
  {
    /**
     * Assign attributes (or node value) to output node from source node.
     *
     * @param aOutputNode The <markup:output/> descendant.
     * @param aSourceNode The equivalent real markup node.
     */
    assignAttributes: function assignAttributes(aOutputNode, aSourceNode) {
      dump("assignAttributes(" + aSourceNode + ", " + aOutputNode + "\n");
      if (!aSourceNode) {
        return;
      }

      assert(aSourceNode.nodeType == aOutputNode.nodeType,
             "Different node types!");
      if (aSourceNode.nodeType == nsIDOMNode.ELEMENT_NODE) {
        assert(aSourceNode.namespaceURI == aOutputNode.namespaceURI,
               "Different namespace URI's!");
        assert(aSourceNode.localName == aOutputNode.localName,
               "Different local names!");
        for (var i = 0; i < aSourceNode.attributes.length; i++) {
          var attr = aSourceNode.attributes[i];
          var cloneAttr = attr.cloneNode(true);
          aOutputNode.ownerDocument.adoptNode(cloneAttr);
          aOutputNode.setAttributeNodeNS(cloneAttr);

          assignUserData(cloneAttr, attr, "entityMatches");
        }
        return;
      }

      aOutputNode.nodeValue = aSourceNode.nodeValue;
    },

    // xeIMarkupOutput
    importSourceNode: function importSourceNode(aRealRootNode) {
      this._originalNode = aRealRootNode;

      /* This requires we walk the trees of the source fragment and
       * "this" at the same time, so we use a custom tree walker
       * routine.
       */
      var templateStack = [];
      var templateIterator = this._elementWrapper.elementNode.firstChild;
      while (templateIterator.nodeType != nsIDOMNode.ELEMENT_NODE) {
        templateIterator = templateIterator.nextSibling;
      }
      var realStack = [];
      var realIterator = aRealRootNode;

      this.assignAttributes(templateIterator, realIterator);

      do { // Start walking the trees.
        var skipFirstChild = false;

        if (templateIterator && realIterator &&
            (templateIterator.nodeType == nsIDOMNode.ELEMENT_NODE) &&
            (templateIterator.namespaceURI == MARKUP_NS)) {
          NOT_IMPLEMENTED(); // We haven't gotten here yet.
          if (templateIterator.localName == "repeat") {
            /* We're going to call on isRepeatBaseNode to determine how
             * many times to repeat the contents.
             */
            var checkOffset = 0;
            var checkNode = realIterator.previousSibling;
            while (checkNode) {
              checkOffset++;
              checkNode = checkNode.previousSibling;
            }

            var interval = templateIterator.childNodes.length;
            var endCheckOffset = interval * templateIterator.maxCount
                                 + checkOffset;

            var familyCount = realIterator.parentNode.childNodes.length;
            endCheckOffset = Math.min(endCheckOffset, familyCount);

            var response = MarkupTemplate.prototype.isRepeatBaseNode(
                             templateIterator,
                             realIterator.parentNode,
                             checkOffset,
                             endCheckOffset);

            /* Okay, we know how many nodes to throw in the template.
               Let's go to work.
             */
            assert(templateIterator instanceof xeIMarkupRepeat,
                   "<markup:repeat/> not implementing xeIMarkupRepeat?");
            templateIterator.clearRows();
            var index = checkOffset;
            for (var i = 0; i < response.count; i++) {
              var startNode = realIterator.parentNode.childNodes[index];
              templateIterator.addRow(startNode);
              index += templateIterator.childNodes.length;
            }

            /* Move our real iterator forward almost all the way. We'll
               move it to parallel our templateIterator by skipping the
               first child check.
             */
            realIterator = realIterator.parentNode.childNodes[index - 1];

            // By using <markup:repeat/>, the child nodes are off limits.
            skipFirstChild = true;

          } else if (templateIterator.localName == "children") {
            /* XXX ajvincent We cheat a bit here.  If this were a normal
               tree walker, we'd have to accept the node and do
               something with our precious time.
             */

            // Get the number of nodes that markup:children matches.
            assert(templateIterator instanceof xeIMarkupChildren,
                   "<markup:children/> not implementing xeIMarkupChildren?");
            var count = templateIterator.getMatchCount(realIterator);
            var origNodeList = [];

            // Add the same number of real nodes to the children's list.
            while (count > 0) {
              origNodeList[origNodeList.length] = realIterator;
              realIterator = realIterator.nextSibling;
              count -= 1;
            }
            templateIterator.originalNodeList = origNodeList;
          }
        } // end templateIterator in MARKUP namespace

        // Move into the first child of the template.
        if (!skipFirstChild && templateIterator.firstChild) {
          if (!realIterator.firstChild) {
            dump("Expected first child of realIterator\n");
            dump("templateIterator: " + templateIterator.nodeName + "\n");
            dump("realIterator: " + realIterator.nodeName + "\n");
            return false;
          }

          // Move the real node tree walker as well.
          realStack.unshift(realIterator);
          realIterator = realIterator.firstChild;

          templateStack.unshift(templateIterator);
          templateIterator = templateIterator.firstChild;

          // In a normal nodefilter, this would be what runs on accept.
          if (templateIterator && realIterator &&
              (templateIterator.nodeType == nsIDOMNode.ELEMENT_NODE) &&
              (templateIterator.namespaceURI == MARKUP_NS) &&
              (templateIterator.localName == "children")) {
            // <markup:children/> elements don't adopt attributes
          } else {
            this.assignAttributes(templateIterator, realIterator);
          }

          continue;
        }

        do {
          if (templateIterator.nextSibling &&
              (templateIterator != this)) {
            /* If the template has a next sibling, and the real iterator
               does not, abort.
             */
            if (!realIterator) {
              dump("expected next sibling of realIterator\n");
              return false;
            }
            realIterator = realIterator.nextSibling;
            templateIterator = templateIterator.nextSibling;

            // In a normal nodefilter, this would be what runs on accept.
            if (templateIterator && realIterator &&
                (templateIterator.nodeType == nsIDOMNode.ELEMENT_NODE) &&
                (templateIterator.namespaceURI == MARKUP_NS) &&
                (templateIterator.localName == "children")) {
              // <markup:children/> elements don't adopt attributes
            } else {
              this.assignAttributes(templateIterator, realIterator);
            }

            break;
          }

          /* If the template iterator doesn't have a nextSibling, but
             the real iterator does, abort.
           */
          if (!templateIterator.nextSibling &&
              (templateIterator != this) &&
              realIterator &&
              realIterator.nextSibling) {
            dump("expected next sibling of templateIterator\n");
            return false;
          }

          // Go up one level, so we can get to our uncle.
          if (templateStack.length > 0) {
            realIterator = realStack.shift();
            templateIterator = templateStack.shift();
            continue;
          }

          return true;
        } while (true);
      } while (templateStack.length > 0);

      // We've successfully walked the two trees and imported the node.
      return true;
    },

    // nsIXTFElement
    _scriptingInterfaces: [xeIMarkupOutput]
  });
/* </markup:output> */

const ElementFactory =
{
  // nsIXTFElementFactory
  createElement: function createElement(aLocalName) {
    switch (aLocalName) {
      case "template":
      case "output":
        var elem = new MarkupConstructors[aLocalName]();
        return elem.QueryInterface(nsIXTFElement);
    }
    return null;
  },

  _interfaces: [nsIXTFElementFactory,
                nsIClassInfo],

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

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

  // nsIClassInfo
  classDescription: "Verbosio markup language factory",

  // nsIClassInfo
  classID: Components.ID("{ff354d76-4b89-4474-b86e-a986b5f4714a}"),

  // nsIClassInfo
  contractID: xtfContract + MARKUP_NS,

  // nsIClassInfo
  implementationLanguage: nsIProgrammingLanguage.JAVASCRIPT,

  // nsIClassInfo
  flags: nsIClassInfo.SINGLETON, /* service */

  // 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: ElementFactory " + prop + "\n");
        break;
      }
    }
    if (!found) {
      dump("QUERYINTERFACE: ElementFactory Unknown\n");
    }

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

var NSGetModule = XPCOMUtils.generateNSGetModule([
  wrapServiceComponent(MarkupLanguagePack),
  wrapServiceComponent(ElementFactory)
], null, null);
