/*
 * Software License Agreement (BSD License)
 *
 * Copyright (c) 2007, Parakey Inc.
 * All rights reserved.
 *
 * Redistribution and use of this software in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above
 *   copyright notice, this list of conditions and the
 *   following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the
 *   following disclaimer in the documentation and/or other
 *   materials provided with the distribution.
 *
 * * Neither the name of Parakey Inc. nor the names of its
 *   contributors may be used to endorse or promote products
 *   derived from this software without specific prior
 *   written permission of Parakey Inc.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
 * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/*
 * Creator:
 *  Joe Hewitt
 * Contributors:
 *  John J. Barton (IBM Almaden)
 *  Jan Odvarko (Mozilla Corp.)
 *  Max Stepanov (Aptana Inc.)
 *  Rob Campbell (Mozilla Corp.)
 *  Hans Hillen (Paciello Group, Mozilla)
 *  Curtis Bartley (Mozilla Corp.)
 *  Mike Collins (IBM Almaden)
 *  Kevin Decker
 *  Mike Ratcliffe (Comartis AG)
 *  Hernan Rodríguez Colmeiro
 *  Austin Andrews
 *  Christoph Dorn
 *  Steven Roussey (AppCenter Inc, Network54)
 * Firefox Port Contributors:
 *  Rob Campbell
 *  Julian Viereck
 *  Mihai Sucan
 */

var EXPORTED_SYMBOLS = ["domplate", "HTMLTemplates", "domplateUtils"];

const Ci = Components.interfaces;
const Cu = Components.utils;

const invisibleTags = {
  "head": true,
  "base": true,
  "basefont": true,
  "isindex": true,
  "link": true,
  "meta": true,
  "script": true,
  "style": true,
  "title": true,
};

// End tags for void elements are forbidden
// http://wiki.whatwg.org/wiki/HTML_vs._XHTML
const selfClosingTags = {
  "meta": 1,
  "link": 1,
  "area": 1,
  "base": 1,
  "col": 1,
  "input": 1,
  "img": 1,
  "br": 1,
  "hr": 1,
  "param": 1,
  "embed": 1
};

const reNotWhitespace = /[^\s]/;
const showTextNodesWithWhitespace = false;

var DOM = {};
var domplateUtils = {};

/**
 * Utility function to allow outside caller to set a global scope within
 * domplate's DOM object. Specifically for access to DOM constants and classes.
 * @param aGlobal
 *        The global object whose scope we wish to capture.
 */
domplateUtils.setDOM = function(aGlobal)
{
  DOM = aGlobal;
  if (!aGlobal) {
    womb = null;
  }
};

/**
 * main domplate constructor function.
 */

let domplate = function()
{
  let lastSubject;
  for (let i = 0; i < arguments.length; ++i)
    lastSubject = lastSubject ? copyObject(lastSubject, arguments[i]) : arguments[i];

  for (let name in lastSubject) {
    let val = lastSubject[name];
    if (isTag(val))
      val.tag.subject = lastSubject;
  }

  return lastSubject;
};

var womb = null;

///////////////////////////////////////////////////////////////////////////
//// Base functions

function DomplateTag(tagName)
{
  this.tagName = tagName;
}

function DomplateEmbed()
{
}

function DomplateLoop()
{
}

///////////////////////////////////////////////////////////////////////////
//// Definitions

domplate.context = function(context, fn)
{
  let lastContext = domplate.lastContext;
  domplate.topContext = context;
  fn.apply(context);
  domplate.topContext = lastContext;
};

domplate.TAG = function()
{
  let embed = new DomplateEmbed();
  return embed.merge(arguments);
};

domplate.FOR = function()
{
  let loop = new DomplateLoop();
  return loop.merge(arguments);
};

DomplateTag.prototype =
{
  /**
   * Initializer for DOM templates. Called to create new Functions objects
   * like TR, TD, OBJLINK, etc. See defineTag
   * @param args keyword argments for the template, the {} brace stuff after
   *        the tag name, eg TR({...}, TD(...
   * @param oldTag a nested tag, eg the TD tag in TR({...}, TD(...
   */
  merge: function(args, oldTag)
  {
    if (oldTag)
      this.tagName = oldTag.tagName;

    this.context = oldTag ? oldTag.context : null;  // normally null on construction
    this.subject = oldTag ? oldTag.subject : null;
    this.attrs = oldTag ? copyObject(oldTag.attrs) : {};
    this.classes = oldTag ? copyObject(oldTag.classes) : {};
    this.props = oldTag ? copyObject(oldTag.props) : null;
    this.listeners = oldTag ? copyArray(oldTag.listeners) : null;
    this.children = oldTag ? copyArray(oldTag.children) : [];
    this.vars = oldTag ? copyArray(oldTag.vars) : [];

    let attrs = args.length ? args[0] : null;
    let hasAttrs = typeof(attrs) == "object" && !isTag(attrs);

    // Do not clear children, they can be copied from the oldTag.
    //this.children = [];

    if (domplate.topContext)
      this.context = domplate.topContext;

    if (args.length)
      parseChildren(args, hasAttrs ? 1 : 0, this.vars, this.children);

    if (hasAttrs)
      this.parseAttrs(attrs);

    return creator(this, DomplateTag);
  },

  /**
   * Parse node attributes.
   * @param args
   *        Object of arguments to process.
   */
  parseAttrs: function(args)
  {
    for (let name in args) {
      let val = parseValue(args[name]);
      readPartNames(val, this.vars);

      if (name.indexOf("on") == 0) {
        let eventName = name.substr(2);
        if (!this.listeners)
          this.listeners = [];
        this.listeners.push(eventName, val);
      } else if (name[0] == "_") {
        let propName = name.substr(1);
        if (!this.props)
          this.props = {};
        this.props[propName] = val;
      } else if (name[0] == "$") {
        let className = name.substr(1);
        if (!this.classes)
          this.classes = {};
        this.classes[className] = val;
      } else {
        if (name == "class" && this.attrs.hasOwnProperty(name))
          this.attrs[name] += " " + val;
        else
          this.attrs[name] = val;
      }
    }
  },

  compile: function()
  {
    if (this.renderMarkup)
      return;

    this.compileMarkup();
    this.compileDOM();
  },

  compileMarkup: function()
  {
    this.markupArgs = [];
    let topBlock = [], topOuts = [], blocks = [],
      info = {args: this.markupArgs, argIndex: 0};

    this.generateMarkup(topBlock, topOuts, blocks, info);
    this.addCode(topBlock, topOuts, blocks);

    let fnBlock = ['(function (__code__, __context__, __in__, __out__'];
    for (let i = 0; i < info.argIndex; ++i)
      fnBlock.push(', s', i);
    fnBlock.push(') {\n');

    if (this.subject)
      fnBlock.push('with (this) {\n');
    if (this.context)
      fnBlock.push('with (__context__) {\n');
    fnBlock.push('with (__in__) {\n');

    fnBlock.push.apply(fnBlock, blocks);

    if (this.subject)
      fnBlock.push('}\n');
    if (this.context)
      fnBlock.push('}\n');

    fnBlock.push('}})\n');

    function __link__(tag, code, outputs, args)
    {
      tag.tag.compile();

      let tagOutputs = [];
      let markupArgs = [code, tag.tag.context, args, tagOutputs];
      markupArgs.push.apply(markupArgs, tag.tag.markupArgs);
      tag.tag.renderMarkup.apply(tag.tag.subject, markupArgs);

      outputs.push(tag);
      outputs.push(tagOutputs);
    }

    function __escape__(value)
    {
      function replaceChars(ch)
      {
        switch (ch) {
          case "<":
            return "&lt;";
          case ">":
            return "&gt;";
          case "&":
            return "&amp;";
          case "'":
            return "&#39;";
          case '"':
            return "&quot;";
        }
        return "?";
      };
      return String(value).replace(/[<>&"']/g, replaceChars);
    }

    function __loop__(iter, outputs, fn)
    {
      let iterOuts = [];
      outputs.push(iterOuts);

      if (iter instanceof Array)
        iter = new ArrayIterator(iter);

      try {
        while (1) {
          let value = iter.next();
          let itemOuts = [0, 0];
          iterOuts.push(itemOuts);
          fn.apply(this, [value, itemOuts]);
        }
      } catch (exc) {
        if (exc != StopIteration)
          throw exc;
      }
    }

    let js = fnBlock.join("");
    this.renderMarkup = eval(js);
  },

  getVarNames: function(args)
  {
    if (this.vars)
      args.push.apply(args, this.vars);

    for (let i = 0; i < this.children.length; ++i) {
      let child = this.children[i];
      if (isTag(child))
        child.tag.getVarNames(args);
      else if (child instanceof Parts) {
        for (let i = 0; i < child.parts.length; ++i) {
          if (child.parts[i] instanceof Variable) {
            let name = child.parts[i].name;
            let names = name.split(".");
            args.push(names[0]);
          }
        }
      }
    }
  },

  generateMarkup: function(topBlock, topOuts, blocks, info)
  {
    topBlock.push(',"<', this.tagName, '"');

    for (let name in this.attrs) {
      if (name != "class") {
        let val = this.attrs[name];
        topBlock.push(', " ', name, '=\\""');
        addParts(val, ',', topBlock, info, true);
        topBlock.push(', "\\""');
      }
    }

    if (this.listeners) {
      for (let i = 0; i < this.listeners.length; i += 2)
        readPartNames(this.listeners[i+1], topOuts);
    }

    if (this.props) {
      for (let name in this.props)
        readPartNames(this.props[name], topOuts);
    }

    if (this.attrs.hasOwnProperty("class") || this.classes) {
      topBlock.push(', " class=\\""');
      if (this.attrs.hasOwnProperty("class"))
        addParts(this.attrs["class"], ',', topBlock, info, true);
      topBlock.push(', " "');
      for (let name in this.classes) {
        topBlock.push(', (');
        addParts(this.classes[name], '', topBlock, info);
        topBlock.push(' ? "', name, '" + " " : "")');
      }
      topBlock.push(', "\\""');
    }
    topBlock.push(',">"');

    this.generateChildMarkup(topBlock, topOuts, blocks, info);
    topBlock.push(',"</', this.tagName, '>"');
  },

  generateChildMarkup: function(topBlock, topOuts, blocks, info)
  {
    for (let i = 0; i < this.children.length; ++i) {
      let child = this.children[i];
      if (isTag(child))
        child.tag.generateMarkup(topBlock, topOuts, blocks, info);
      else
        addParts(child, ',', topBlock, info, true);
    }
  },

  addCode: function(topBlock, topOuts, blocks)
  {
    if (topBlock.length)
      blocks.push('__code__.push(""', topBlock.join(""), ');\n');
    if (topOuts.length)
      blocks.push('__out__.push(', topOuts.join(","), ');\n');
    topBlock.splice(0, topBlock.length);
    topOuts.splice(0, topOuts.length);
  },

  addLocals: function(blocks)
  {
    let varNames = [];
    this.getVarNames(varNames);

    let map = {};
    for (let i = 0; i < varNames.length; ++i) {
      let name = varNames[i];
      if ( map.hasOwnProperty(name) )
        continue;

      map[name] = 1;
      let names = name.split(".");
      blocks.push('var ', names[0] + ' = ' + '__in__.' + names[0] + ';\n');
    }
  },

  compileDOM: function()
  {
    let path = [];
    let blocks = [];
    this.domArgs = [];
    path.embedIndex = 0;
    path.loopIndex = 0;
    path.staticIndex = 0;
    path.renderIndex = 0;
    let nodeCount = this.generateDOM(path, blocks, this.domArgs);

    let fnBlock = ['(function (root, context, o'];

    for (let i = 0; i < path.staticIndex; ++i)
      fnBlock.push(', ', 's'+i);

    for (let i = 0; i < path.renderIndex; ++i)
      fnBlock.push(', ', 'd'+i);

    fnBlock.push(') {\n');
    for (let i = 0; i < path.loopIndex; ++i)
      fnBlock.push('var l', i, ' = 0;\n');
    for (let i = 0; i < path.embedIndex; ++i)
      fnBlock.push('var e', i, ' = 0;\n');

    if (this.subject)
      fnBlock.push('with (this) {\n');
    if (this.context)
      fnBlock.push('with (context) {\n');

    fnBlock.push(blocks.join(""));

    if (this.subject)
      fnBlock.push('}\n');
    if (this.context)
      fnBlock.push('}\n');

    fnBlock.push('return ', nodeCount, ';\n');
    fnBlock.push('})\n');

    function __bind__(object, fn)
    {
      return function(event) { return fn.apply(object, [event]); }
    }

    function __link__(node, tag, args)
    {
      if (!tag || !tag.tag)
        return;

      tag.tag.compile();

      let domArgs = [node, tag.tag.context, 0];
      domArgs.push.apply(domArgs, tag.tag.domArgs);
      domArgs.push.apply(domArgs, args);

      return tag.tag.renderDOM.apply(tag.tag.subject, domArgs);
    }

    function __loop__(iter, fn)
    {
      let nodeCount = 0;
      for (let i = 0; i < iter.length; ++i) {
        iter[i][0] = i;
        iter[i][1] = nodeCount;
        nodeCount += fn.apply(this, iter[i]);
      }
      return nodeCount;
    }

    function __path__(parent, offset)
    {
      let root = parent;

      for (let i = 2; i < arguments.length; ++i) {
        let index = arguments[i];
        if (i == 3)
          index += offset;

        if (index == -1)
          parent = parent.parentNode;
        else
          parent = parent.childNodes[index];
      }

      return parent;
    }
    let js = fnBlock.join("");
    // Exceptions on this line are often in the eval
    this.renderDOM = eval(js);
  },

  generateDOM: function(path, blocks, args)
  {
    if (this.listeners || this.props)
      this.generateNodePath(path, blocks);

    if (this.listeners) {
      for (let i = 0; i < this.listeners.length; i += 2) {
        let val = this.listeners[i+1];
        let arg = generateArg(val, path, args);
        blocks.push('node.addEventListener("', this.listeners[i],
          '", __bind__(this, ', arg, '), false);\n');
      }
    }

    if (this.props) {
      for (let name in this.props) {
        let val = this.props[name];
        let arg = generateArg(val, path, args);
        blocks.push('node.', name, ' = ', arg, ';\n');
      }
    }

    this.generateChildDOM(path, blocks, args);
    return 1;
  },

  generateNodePath: function(path, blocks)
  {
    blocks.push("var node = __path__(root, o");
    for (let i = 0; i < path.length; ++i)
      blocks.push(",", path[i]);
    blocks.push(");\n");
  },

  generateChildDOM: function(path, blocks, args)
  {
    path.push(0);
    for (let i = 0; i < this.children.length; ++i) {
      let child = this.children[i];
      if (isTag(child))
        path[path.length - 1] += '+' + child.tag.generateDOM(path, blocks, args);
      else
        path[path.length - 1] += '+1';
    }
    path.pop();
  },

  /*
   * We are just hiding from javascript.options.strict. For some reasons it's
   * ok if we return undefined here.
   * @return null or undefined or possibly a context.
   */
  getContext: function()
  {
    return this.context;
  }
};

// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

DomplateEmbed.prototype = copyObject(DomplateTag.prototype,
{
  merge: function(args, oldTag)
  {
    this.value = oldTag ? oldTag.value : parseValue(args[0]);
    this.attrs = oldTag ? oldTag.attrs : {};
    this.vars = oldTag ? copyArray(oldTag.vars) : [];

    let attrs = args[1];
    for (let name in attrs) {
      let val = parseValue(attrs[name]);
      this.attrs[name] = val;
      readPartNames(val, this.vars);
    }

    return creator(this, DomplateEmbed);
  },

  getVarNames: function(names)
  {
    if (this.value instanceof Parts)
      names.push(this.value.parts[0].name);

    if (this.vars)
      names.push.apply(names, this.vars);
  },

  generateMarkup: function(topBlock, topOuts, blocks, info)
  {
    this.addCode(topBlock, topOuts, blocks);

    blocks.push('__link__(');
    addParts(this.value, '', blocks, info);
    blocks.push(', __code__, __out__, {\n');

    let lastName = null;
    for (let name in this.attrs) {
      if (lastName)
        blocks.push(',');
      lastName = name;

      let val = this.attrs[name];
      blocks.push('"', name, '":');
      addParts(val, '', blocks, info);
    }

    blocks.push('});\n');
  },

  generateDOM: function(path, blocks, args)
  {
    let embedName = 'e' + path.embedIndex++;

    this.generateNodePath(path, blocks);

    let valueName = 'd' + path.renderIndex++;
    let argsName = 'd' + path.renderIndex++;
    blocks.push(embedName + ' = __link__(node, ', valueName, ', ',
      argsName, ');\n');

    return embedName;
  }
});

// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

DomplateLoop.prototype = copyObject(DomplateTag.prototype,
{
  merge: function(args, oldTag)
  {
    this.isLoop = true;
    this.varName = oldTag ? oldTag.varName : args[0];
    this.iter = oldTag ? oldTag.iter : parseValue(args[1]);
    this.vars = [];

    this.children = oldTag ? copyArray(oldTag.children) : [];

    let offset = Math.min(args.length, 2);
    parseChildren(args, offset, this.vars, this.children);

    return creator(this, DomplateLoop);
  },

  getVarNames: function(names)
  {
    if (this.iter instanceof Parts)
      names.push(this.iter.parts[0].name);

    DomplateTag.prototype.getVarNames.apply(this, [names]);
  },

  generateMarkup: function(topBlock, topOuts, blocks, info)
  {
    this.addCode(topBlock, topOuts, blocks);

    let iterName;
    if (this.iter instanceof Parts) {
      let part = this.iter.parts[0];
      iterName = part.name;

      if (part.format) {
        for (let i = 0; i < part.format.length; ++i)
          iterName = part.format[i] + "(" + iterName + ")";
      }
    } else
      iterName = this.iter;

    blocks.push('__loop__.apply(this, [', iterName, ', __out__, function(', this.varName, ', __out__) {\n');
    this.generateChildMarkup(topBlock, topOuts, blocks, info);
    this.addCode(topBlock, topOuts, blocks);
    blocks.push('}]);\n');
  },

  generateDOM: function(path, blocks, args)
  {
    let iterName = 'd' + path.renderIndex++;
    let counterName = 'i' + path.loopIndex;
    let loopName = 'l' + path.loopIndex++;

    if (!path.length)
      path.push(-1, 0);

    let preIndex = path.renderIndex;
    path.renderIndex = 0;

    let nodeCount = 0;

    let subBlocks = [];
    let basePath = path[path.length-1];
    for (let i = 0; i < this.children.length; ++i) {
      path[path.length - 1] = basePath + '+' + loopName + '+' + nodeCount;

      let child = this.children[i];
      if (isTag(child))
        nodeCount += '+' + child.tag.generateDOM(path, subBlocks, args);
      else
        nodeCount += '+1';
    }

    path[path.length - 1] = basePath + '+' + loopName;

    blocks.push(loopName,' = __loop__.apply(this, [',
      iterName, ', function(', counterName, ',', loopName);
    for (let i = 0; i < path.renderIndex; ++i)
      blocks.push(',d' + i);
    blocks.push(') {\n');
    blocks.push(subBlocks.join(""));
    blocks.push('return ', nodeCount, ';\n');
    blocks.push('}]);\n');

    path.renderIndex = preIndex;

    return loopName;
  }
});

///////////////////////////////////////////////////////////////////////////

function Variable(name, format)
{
  this.name = name;
  this.format = format;
}

function Parts(parts)
{
  this.parts = parts;
}

///////////////////////////////////////////////////////////////////////////

function parseParts(str)
{
  let re = /\$([_A-Za-z][_A-Za-z0-9.|]*)/g;
  let index = 0;
  let parts = [];

  let m;
  while (m = re.exec(str)) {
    let pre = str.substr(index, (re.lastIndex-m[0].length)-index);
    if (pre)
      parts.push(pre);

    let expr = m[1].split("|");
    parts.push(new Variable(expr[0], expr.slice(1)));
    index = re.lastIndex;
  }

  if (!index)
    return str;

  let post = str.substr(index);
  if (post)
    parts.push(post);

  return new Parts(parts);
}

function parseValue(val)
{
  return typeof(val) == 'string' ? parseParts(val) : val;
}

function parseChildren(args, offset, vars, children)
{
  for (let i = offset; i < args.length; ++i) {
    let val = parseValue(args[i]);
    children.push(val);
    readPartNames(val, vars);
  }
}

function readPartNames(val, vars)
{
  if (val instanceof Parts) {
    for (let i = 0; i < val.parts.length; ++i) {
      let part = val.parts[i];
      if (part instanceof Variable)
        vars.push(part.name);
    }
  }
}

function generateArg(val, path, args)
{
  if (val instanceof Parts) {
    let vals = [];
    for (let i = 0; i < val.parts.length; ++i) {
      let part = val.parts[i];
      if (part instanceof Variable) {
        let varName = 'd' + path.renderIndex++;
        if (part.format) {
          for (let j = 0; j < part.format.length; ++j)
            varName = part.format[j] + '(' + varName + ')';
        }

        vals.push(varName);
      }
      else
        vals.push('"' + part.replace(/"/g, '\\"') + '"');
    }

    return vals.join('+');
  } else {
    args.push(val);
    return 's' + path.staticIndex++;
  }
}

function addParts(val, delim, block, info, escapeIt)
{
  let vals = [];
  if (val instanceof Parts) {
    for (let i = 0; i < val.parts.length; ++i) {
      let part = val.parts[i];
      if (part instanceof Variable) {
        let partName = part.name;
        if (part.format) {
          for (let j = 0; j < part.format.length; ++j)
            partName = part.format[j] + "(" + partName + ")";
        }

        if (escapeIt)
          vals.push("__escape__(" + partName + ")");
        else
          vals.push(partName);
      }
      else
        vals.push('"' + part + '"');
    }
  } else if (isTag(val)) {
    info.args.push(val);
    vals.push('s' + info.argIndex++);
  } else
    vals.push('"' + val + '"');

  let parts = vals.join(delim);
  if (parts)
    block.push(delim, parts);
}

function isTag(obj)
{
  return (typeof(obj) == "function" || obj instanceof Function) && !!obj.tag;
}

///////////////////////////////////////////////////////////////////////////
//// creator

function creator(tag, cons)
{
  let fn = new Function(
    "var tag = arguments.callee.tag;" +
    "var cons = arguments.callee.cons;" +
    "var newTag = new cons();" +
    "return newTag.merge(arguments, tag);");

  fn.tag = tag;
  fn.cons = cons;
  extend(fn, Renderer);

  return fn;
}

///////////////////////////////////////////////////////////////////////////
//// Utility functions

function arrayInsert(array, index, other)
{
  for (let i = 0; i < other.length; ++i)
    array.splice(i+index, 0, other[i]);

  return array;
}

function cloneArray(array, fn)
{
  let newArray = [];

  if (fn)
    for (let i = 0; i < array.length; ++i)
      newArray.push(fn(array[i]));
  else
    for (let i = 0; i < array.length; ++i)
      newArray.push(array[i]);

  return newArray;
}

// fn, thisObject, args => thisObject.fn(args, arguments);
function bind()
{
  let args = cloneArray(arguments), fn = args.shift(), object = args.shift();
  return function bind()
  {
    return fn.apply(object, arrayInsert(cloneArray(args), 0, arguments));
  }
}

function copyArray(oldArray)
{
  let array = [];
  if (oldArray)
    for (let i = 0; i < oldArray.length; ++i)
      array.push(oldArray[i]);
  return array;
}

function copyObject(l, r)
{
  let m = {};
  extend(m, l);
  extend(m, r);
  return m;
}

function escapeNewLines(value)
{
  return value.replace(/\r/gm, "\\r").replace(/\n/gm, "\\n");
}

function extend(l, r)
{
  for (let n in r)
    l[n] = r[n];
}

function cropString(text, limit, alterText)
{
  if (!alterText)
    alterText = "..."; //…

  text = text + "";

  if (!limit)
    limit = 88; // todo
  var halfLimit = (limit / 2);
  halfLimit -= 2; // adjustment for alterText's increase in size

  if (text.length > limit)
    return text.substr(0, halfLimit) + alterText +
      text.substr(text.length - halfLimit);
  else
    return text;
}

function cropMultipleLines(text, limit)
{
  return escapeNewLines(this.cropString(text, limit));
}

function isVisible(elt)
{
  if (elt.localName) {
    return elt.offsetWidth > 0 || elt.offsetHeight > 0 ||
      elt.localName.toLowerCase() in invisibleTags;
  } else {
    return elt.offsetWidth > 0 || elt.offsetHeight > 0;
  }
    // || isElementSVG(elt) || isElementMathML(elt);
}

// Local Helpers

function isElementXHTML(node)
{
  return node.nodeName == node.nodeName.toLowerCase();
}

function isContainerElement(element)
{
  let tag = element.localName.toLowerCase();
  switch (tag) {
    case "script":
    case "style":
    case "iframe":
    case "frame":
    case "tabbrowser":
    case "browser":
      return true;
    case "link":
      return element.getAttribute("rel") == "stylesheet";
    case "embed":
      return element.getSVGDocument();
  }
  return false;
}

domplateUtils.isWhitespace = function isWhitespace(text)
{
  return !reNotWhitespace.exec(text);
};

domplateUtils.isWhitespaceText = function isWhitespaceText(node)
{
  if (node instanceof DOM.HTMLAppletElement)
    return false;
  return node.nodeType == DOM.Node.TEXT_NODE && this.isWhitespace(node.nodeValue);
}

function isSelfClosing(element)
{
  //if (isElementSVG(element) || isElementMathML(element))
  //    return true;
  var tag = element.localName.toLowerCase();
  return (selfClosingTags.hasOwnProperty(tag));
};

function isEmptyElement(element)
{
  if (showTextNodesWithWhitespace) {
    return !element.firstChild && isSelfClosing(element);
  } else {
    for (let child = element.firstChild; child; child = child.nextSibling) {
      if (!domplateUtils.isWhitespaceText(child))
        return false;
    }
  }
  return isSelfClosing(element);
}

function getEmptyElementTag(node)
{
  let isXhtml= isElementXHTML(node);
  if (isXhtml)
    return HTMLTemplates.XEmptyElement.tag;
  else
    return HTMLTemplates.EmptyElement.tag;
}

/**
 * Determines if the given node has any children which are elements.
 *
 * @param {Element} element Element to test.
 * @return true if immediate children of type Element exist, false otherwise
 */
function hasNoElementChildren(element)
{
  if (element.childElementCount != 0)
    return false;

  return true;
}

domplateUtils.getNodeTag = function getNodeTag(node, expandAll)
{
  if (node instanceof DOM.Element) {
    if (node instanceof DOM.HTMLHtmlElement && node.ownerDocument
        && node.ownerDocument.doctype)
      return HTMLTemplates.HTMLHtmlElement.tag;
    else if (node instanceof DOM.HTMLAppletElement)
      return getEmptyElementTag(node);
    else if (isContainerElement(node))
      return HTMLTemplates.Element.tag;
    else if (isEmptyElement(node))
      return getEmptyElementTag(node);
    else if (hasNoElementChildren(node))
      return HTMLTemplates.TextElement.tag;
    else
      return HTMLTemplates.Element.tag;
  }
  else if (node instanceof DOM.Text)
    return HTMLTemplates.TextNode.tag;
  else if (node instanceof DOM.CDATASection)
    return HTMLTemplates.CDATANode.tag;
  else if (node instanceof DOM.Comment)
    return HTMLTemplates.CommentNode.tag;
  else if (node instanceof DOM.SourceText)
    return HTMLTemplates.SourceText.tag;
  else
    return HTMLTemplates.Nada.tag;
}

function getNodeBoxTag(nodeBox)
{
  let re = /([^\s]+)NodeBox/;
  let m = re.exec(nodeBox.className);
  if (!m)
    return null;

  let nodeBoxType = m[1];
  if (nodeBoxType == "container")
    return HTMLTemplates.Element.tag;
  else if (nodeBoxType == "text")
    return HTMLTemplates.TextElement.tag;
  else if (nodeBoxType == "empty")
    return HTMLTemplates.EmptyElement.tag;
}

///////////////////////////////////////////////////////////////////////////
//// ArrayIterator

function ArrayIterator(array)
{
  let index = -1;

  this.next = function()
  {
    if (++index >= array.length)
      throw StopIteration;

    return array[index];
  };
}

function StopIteration() {}

domplate.$break = function()
{
  throw StopIteration;
};

///////////////////////////////////////////////////////////////////////////
//// Renderer

var Renderer =
{
  renderHTML: function(args, outputs, self)
  {
    let code = [];
    let markupArgs = [code, this.tag.getContext(), args, outputs];
    markupArgs.push.apply(markupArgs, this.tag.markupArgs);
    this.tag.renderMarkup.apply(self ? self : this.tag.subject, markupArgs);
    return code.join("");
  },

  insertRows: function(args, before, self)
  {
    if (!args)
      args = {};

    this.tag.compile();

    let outputs = [];
    let html = this.renderHTML(args, outputs, self);

    let doc = before.ownerDocument;
    let table = doc.createElement("table");
    table.innerHTML = html;

    let tbody = table.firstChild;
    let parent = before.localName.toLowerCase() == "tr" ? before.parentNode : before;
    let after = before.localName.toLowerCase() == "tr" ? before.nextSibling : null;

    let firstRow = tbody.firstChild, lastRow;
    while (tbody.firstChild) {
      lastRow = tbody.firstChild;
      if (after)
        parent.insertBefore(lastRow, after);
      else
        parent.appendChild(lastRow);
    }

    // To save the next poor soul:
    // In order to properly apply properties and event handlers on elements
    // constructed by a FOR tag, the tag needs to be able to iterate up and
    // down the tree, meaning if FOR is the root element as is the case with
    // many insertRows calls, it will need to iterator over portions of the
    // new parent.
    //
    // To achieve this end, __path__ defines the -1 operator which allows
    // parent traversal. When combined with the offset that we calculate
    // below we are able to iterate over the elements.
    //
    // This fails when applied to a non-loop element as non-loop elements
    // Do not generate to proper path to bounce up and down the tree.
    let offset = 0;
    if (this.tag.isLoop) {
      let node = firstRow.parentNode.firstChild;
      for (; node && node != firstRow; node = node.nextSibling)
        ++offset;
    }

    // strict warning: this.tag.context undefined
    let domArgs = [firstRow, this.tag.getContext(), offset];
    domArgs.push.apply(domArgs, this.tag.domArgs);
    domArgs.push.apply(domArgs, outputs);

    this.tag.renderDOM.apply(self ? self : this.tag.subject, domArgs);
    return [firstRow, lastRow];
  },

  insertBefore: function(args, before, self)
  {
    return this.insertNode(args, before.ownerDocument,
      function(frag) {
        before.parentNode.insertBefore(frag, before);
      }, self);
  },

  insertAfter: function(args, after, self)
  {
    return this.insertNode(args, after.ownerDocument,
      function(frag) {
        after.parentNode.insertBefore(frag, after.nextSibling);
      }, self);
  },

  insertNode: function(args, doc, inserter, self)
  {
    if (!args)
      args = {};

    this.tag.compile();

    let outputs = [];
    let html = this.renderHTML(args, outputs, self);

    let range = doc.createRange();
    range.selectNode(doc.body);
    let frag = range.createContextualFragment(html);

    let root = frag.firstChild;
    root = inserter(frag) || root;

    let domArgs = [root, this.tag.context, 0];
    domArgs.push.apply(domArgs, this.tag.domArgs);
    domArgs.push.apply(domArgs, outputs);

    this.tag.renderDOM.apply(self ? self : this.tag.subject, domArgs);

    return root;
  },

  replace: function(args, parent, self)
  {
    if (!args)
      args = {};

    this.tag.compile();

    let outputs = [];
    let html = this.renderHTML(args, outputs, self);

    let root;
    if (parent.nodeType == DOM.Node.ELEMENT_NODE) {
      parent.innerHTML = html;
      root = parent.firstChild;
    } else {
      if (!parent || parent.nodeType != DOM.Node.DOCUMENT_NODE)
        return;

      if (!womb || womb.ownerDocument != parent)
        womb = parent.createElement("div");

      womb.innerHTML = html;

      root = womb.firstChild;
    }

    let domArgs = [root, this.tag.context, 0];
    domArgs.push.apply(domArgs, this.tag.domArgs);
    domArgs.push.apply(domArgs, outputs);
    this.tag.renderDOM.apply(self ? self : this.tag.subject, domArgs);

    return root;
  },

  append: function(args, parent, self)
  {
    if (!args)
      args = {};

    this.tag.compile();

    let outputs = [];
    let html = this.renderHTML(args, outputs, self);

    if (!womb || womb.ownerDocument != parent.ownerDocument)
      womb = parent.ownerDocument.createElement("div");
    womb.innerHTML = html;

    let root = womb.firstChild;
    while (womb.firstChild)
      parent.appendChild(womb.firstChild);

    let domArgs = [root, this.tag.context, 0];
    domArgs.push.apply(domArgs, this.tag.domArgs);
    domArgs.push.apply(domArgs, outputs);

    this.tag.renderDOM.apply(self ? self : this.tag.subject, domArgs);

    return root;
  }
};

///////////////////////////////////////////////////////////////////////////
//// defineTags macro

/**
 * Create default tags for a list of tag names.
 * @param Arguments
 *        list of string arguments
 */

function defineTags()
{
  for (let i = 0; i < arguments.length; ++i) {
    let tagName = arguments[i];
    let fn = new Function("var newTag = new DomplateTag('" + tagName +
      "'); return newTag.merge(arguments);");

    let fnName = tagName.toUpperCase();
    domplate[fnName] = fn;
  }
}

defineTags(
  "a", "button", "br", "canvas", "col", "colgroup", "div", "fieldset", "form",
  "h1", "h2", "h3", "hr", "img", "input", "label", "legend", "li", "ol",
  "optgroup", "option", "p", "pre", "select", "b", "span", "strong", "table",
  "tbody", "td", "textarea", "tfoot", "th", "thead", "tr", "tt", "ul", "iframe",
  "code"
);

///////////////////////////////////////////////////////////////////////////
//// HTMLTemplates

let HTMLTemplates = {
  showTextNodesWithWhitespace: false
};

let BaseTemplates = {
  showTextNodesWithWhitespace: false
};

///////////////////////////////////////////////////////////////////////////
//// HTMLTemplates.Reps

BaseTemplates.OBJECTLINK = domplate.A({
  "class": "objectLink objectLink-$className a11yFocus",
  _repObject: "$object"
});

BaseTemplates.Rep = domplate(
{
  className: "",
  inspectable: true,

  supportsObject: function(object, type)
  {
    return false;
  },

  inspectObject: function(object, context)
  {
    // Firebug.chrome.select(object);  // todo
  },

  browseObject: function(object, context)
  {
  },

  persistObject: function(object, context)
  {
  },

  getRealObject: function(object, context)
  {
    return object;
  },

  /**
   * Return a sensible string title for the given object, removing any wrapper
   * information from it.
   * @param aObject
   *        The object to get the title of.
   * @returns string
   */

  getTitle: function(aObject)
  {
    // e.g., [object XPCWrappedNative [object foo]]
    let label = safeToString(aObject);

    const re =/\[object ([^\]]*)/;
    let objectMatch = re.exec(label);
    let secondObjectMatch = null;
    if (objectMatch) {
      // e.g., XPCWrappedNative [object foo
      secondObjectMatch = re.exec(objectMatch[1]);
    }

    if (secondObjectMatch)
      return secondObjectMatch[1];  // eg foo
    else
      return objectMatch ? objectMatch[1] : label;
  },

  getTooltip: function(object)
  {
    return null;
  },

  /**
   * Called by chrome.onContextMenu to build the context menu when the
   * underlying object has this rep.
   * See also Panel for a similar function also called by onContextMenu.
   * Extensions may monkey patch and chain off this call.
   * @param object: the 'realObject', a model value, eg a DOM property
   * @param target: the HTML element clicked on.
   * @param context: the context, probably FirebugContext
   * @returns an array of menu items.
   */
  getContextMenuItems: function(object, target, context)
  {
    return [];
  },

  /////////////////////////////////////////////////////////////////////////
  // Convenience for domplates

  STR: function(name)
  {
    return name; // todo getproperty?
  },

  cropString: function(text)
  {
    return cropString(text);
  },

  cropMultipleLines: function(text, limit)
  {
    return cropMultipleLines(text, limit);
  },

  toLowerCase: function(text)
  {
    return text ? text.toLowerCase() : text;
  },

  plural: function(n)
  {
    return n == 1 ? "" : "s";
  }
});

BaseTemplates.Element = domplate(BaseTemplates.Rep,
{
  tag:
    BaseTemplates.OBJECTLINK(
      "&lt;",
      domplate.SPAN({"class": "nodeTag"},
        "$object.localName|toLowerCase"),
      domplate.FOR("attr", "$object|attrIterator",
        "&nbsp;$attr.localName=&quot;",
        domplate.SPAN({"class": "nodeValue"},
          "$attr.nodeValue"),
        "&quot;"
      ),
      "&gt;"
    ),

  shortTag:
    BaseTemplates.OBJECTLINK(
      domplate.SPAN({"class": "$object|getVisible"},
        domplate.SPAN({"class": "selectorTag"},
          "$object|getSelectorTag"),
        domplate.SPAN({"class": "selectorId"},
          "$object|getSelectorId"),
        domplate.SPAN({"class": "selectorClass"},
          "$object|getSelectorClass"),
        domplate.SPAN({"class": "selectorValue"},
          "$object|getValue")
      )
    ),

  getVisible: function(elt)
  {
    return isVisible(elt) ? "" : "selectorHidden";
  },

  getSelectorTag: function(elt)
  {
    return elt.localName.toLowerCase();
  },

  getSelectorId: function(elt)
  {
    return elt.id ? ("#" + elt.id) : "";
  },

  getSelectorClass: function(elt)
  {
    return elt.getAttribute("class")
      ? ("." + elt.getAttribute("class").split(" ")[0])
      : "";
  },

  getValue: function(elt)
  { // todo getFileName
    let value;
/*
    if (elt instanceof HTMLImageElement)
      value = getFileName(elt.getAttribute("src"));
    else if (elt instanceof HTMLAnchorElement)
      value = getFileName(elt.getAttribute("href"));
    else if (elt instanceof HTMLInputElement)
      value = elt.getAttribute("value");
    else if (elt instanceof HTMLFormElement)
      value = getFileName(elt.getAttribute("action"));
    else if (elt instanceof HTMLScriptElement)
      value = getFileName(elt.getAttribute("src"));

    return value ? " " + cropMultipleLines(value, 20) : ""; */
    // trying a simplified version from above commented section
    // todo
    if (elt instanceof DOM.HTMLImageElement)
      value = elt.getAttribute("src");
    else if (elt instanceof DOM.HTMLAnchorElement)
      value = elt.getAttribute("href");
    else if (elt instanceof DOM.HTMLInputElement)
      value = elt.getAttribute("value");
    else if (elt instanceof DOM.HTMLFormElement)
      value = elt.getAttribute("action");
    else if (elt instanceof DOM.HTMLScriptElement)
      value = elt.getAttribute("src");

    return value ? " " + cropMultipleLines(value, 20) : "";
  },

  attrIterator: function(elt)
  {
    let attrs = [];
    let idAttr, classAttr;
    if (elt.attributes) {
      for (let i = 0; i < elt.attributes.length; ++i) {
        var attr = elt.attributes[i];
        if (attr.localName.indexOf("-moz-math") != -1)
          continue;
        else if (attr.localName == "id")
          idAttr = attr;
        else if (attr.localName == "class")
          classAttr = attr;
        else
          attrs.push(attr);
      }
    }
    if (classAttr)
      attrs.unshift(classAttr);
    if (idAttr)
      attrs.unshift(idAttr);
    return attrs;
  },

  shortAttrIterator: function(elt)
  {
    let attrs = [];
    if (elt.attributes) {
      for (let i = 0; i < elt.attributes.length; ++i) {
        let attr = elt.attributes[i];
          if (attr.localName == "id" || attr.localName == "class")
            attrs.push(attr);
      }
    }

    return attrs;
  },

  getHidden: function(elt)
  {
    return isVisible(elt) ? "" : "nodeHidden";
  },

/* getXPath: function(elt)
  {
    return getElementTreeXPath(elt); // todo
  }, */

  getNodeTextGroups: function(element)
  {
    let text =  element.textContent;
    return [{str: text, 'class': '', extra: ''}];
  },

  className: "element",

  supportsObject: function(object, type)
  {
    return object instanceof DOM.Element;
  },

  browseObject: function(elt, context)
  {
    let tag = elt.localName.toLowerCase();
    return true;
  },
});


///////////////////////////////////////////////////////////////////////////
//// HTMLTemplates.tags

BaseTemplates.AttrTag =
  domplate.SPAN({"class": "nodeAttr editGroup"},
    "&nbsp;",
    domplate.SPAN({"class": "nodeName editable"}, "$attr.nodeName"),
    "=&quot;",
    domplate.SPAN({"class": "nodeValue editable", "data-attributeName": "$attr.nodeName"}, "$attr.nodeValue"),
    "&quot;");

BaseTemplates.TextTag =
  domplate.SPAN({"class": "nodeText editable"},
    domplate.FOR("chr", "$object|getNodeTextGroups",
      domplate.SPAN({"class": "$chr.class $chr.extra"},
        "$chr.str")));

///////////////////////////////////////////////////////////////////////////
//// HTMLTemplates



HTMLTemplates.CompleteElement = domplate(BaseTemplates.Element,
{
  tag:
    domplate.DIV({"class":
        "nodeBox open $object|getHidden repIgnore",
        _repObject: "$object", role : 'presentation'},
      domplate.DIV({"class": "nodeLabel", role: "presentation"},
        domplate.SPAN({"class": "nodeLabelBox repTarget repTarget",
          role : 'treeitem', 'aria-expanded' : 'false'},
          "&lt;",
          domplate.SPAN({"class": "nodeTag"},
            "$object.nodeName|toLowerCase"),
          domplate.FOR("attr", "$object|attrIterator", BaseTemplates.AttrTag),
          domplate.SPAN({"class": "nodeBracket"}, "&gt;")
        )
      ),
      domplate.DIV({"class": "nodeChildBox", role :"group"},
        domplate.FOR("child", "$object|childIterator",
          domplate.TAG("$child|getNodeTag", {object: "$child"})
        )
      ),
      domplate.DIV({"class": "nodeCloseLabel", role:"presentation"},
        "&lt;/",
        domplate.SPAN({"class": "nodeTag"},
          "$object.nodeName|toLowerCase"),
        "&gt;"
      )
    ),

  getNodeTag: function(node)
  {
    return domplateUtils.getNodeTag(node, true);
  },

  childIterator: function(node)
  {
    if (node.contentDocument)
      return [node.contentDocument.documentElement];

    if (this.showTextNodesWithWhitespace)
      return cloneArray(node.childNodes);
    else {
      let nodes = [];
      for (let child = node.firstChild; child; child = child.nextSibling) {
        if (child.nodeType != DOM.Node.TEXT_NODE || !domplateUtils.isWhitespaceText(child))
          nodes.push(child);
      }
      return nodes;
    }
  }
});

HTMLTemplates.SoloElement = domplate(HTMLTemplates.CompleteElement,
{
  tag:
    domplate.DIV({"class": "soloElement",
      onmousedown: "$onMouseDown"},
      HTMLTemplates.CompleteElement.tag),

  onMouseDown: function(event)
  {
    for (let child = event.target; child; child = child.parentNode) {
      if (child.repObject) { // todo
          // let panel = Firebug.getElementPanel(child);
          // Firebug.chrome.select(child.repObject);
          break;
      }
    }
  }
});

HTMLTemplates.Element = domplate(BaseTemplates.Element,
{
  tag:
    domplate.DIV({"class": "nodeBox containerNodeBox $object|getHidden repIgnore",
      _repObject: "$object", role: "presentation"},
      domplate.DIV({"class": "nodeLabel", role: "presentation"},
        domplate.IMG({"class": "twisty", role: "presentation"}),
        domplate.SPAN({"class": "nodeLabelBox repTarget",
          role: 'treeitem', 'aria-expanded': 'false'},
          "&lt;",
          domplate.SPAN({"class": "nodeTag"},
            "$object.nodeName|toLowerCase"),
          domplate.FOR("attr", "$object|attrIterator", BaseTemplates.AttrTag),
          domplate.SPAN({"class": "nodeBracket editable insertBefore"},
            "&gt;")
        )
      ),
      domplate.DIV({"class": "nodeChildBox", role: "group"}), /* nodeChildBox is special signal in insideOutBox */
      domplate.DIV({"class": "nodeCloseLabel", role: "presentation"},
        domplate.SPAN({"class": "nodeCloseLabelBox repTarget"},
          "&lt;/",
          domplate.SPAN({"class": "nodeTag"}, "$object.nodeName|toLowerCase"),
          "&gt;"
        )
      )
    )
});

HTMLTemplates.HTMLHtmlElement = domplate(BaseTemplates.Element,
{
  tag:
    domplate.DIV({"class":
        "nodeBox htmlNodeBox containerNodeBox $object|getHidden repIgnore",
        _repObject: "$object", role: "presentation"},
      domplate.DIV({"class": "docType"},
        "$object|getDocType"),
      domplate.DIV({"class": "nodeLabel", role: "presentation"},
        domplate.IMG({"class": "twisty", role: "presentation"}),
        domplate.SPAN({"class": "nodeLabelBox repTarget",
            role: 'treeitem', 'aria-expanded' : 'false'},
          "&lt;",
          domplate.SPAN({"class": "nodeTag"},
            "$object.nodeName|toLowerCase"),
          domplate.FOR("attr", "$object|attrIterator", BaseTemplates.AttrTag),
          domplate.SPAN({"class":
            "nodeBracket editable insertBefore"}, "&gt;")
        )
      ), /* nodeChildBox is special signal in insideOutBox */
      domplate.DIV({"class": "nodeChildBox", role: "group"}),
      domplate.DIV({"class": "nodeCloseLabel", role:  "presentation"},
        domplate.SPAN({"class": "nodeCloseLabelBox repTarget"},
          "&lt;/",
          domplate.SPAN({"class": "nodeTag"},
            "$object.nodeName|toLowerCase"),
          "&gt;"
        )
      )
    ),

  getDocType: function(obj)
  {
    let doctype = obj.ownerDocument.doctype;
    return '<!DOCTYPE ' + doctype.name + (doctype.publicId ? ' PUBLIC "' +
      doctype.publicId + '"': '') + (doctype.systemId ? ' "' +
      doctype.systemId + '"' : '') + '>';
  }
});

HTMLTemplates.TextElement = domplate(BaseTemplates.Element,
{
  tag:
    domplate.DIV({"class":
        "nodeBox textNodeBox $object|getHidden repIgnore",
        _repObject: "$object", role: 'presentation'},
      domplate.DIV({"class": "nodeLabel", role: "presentation"},
        domplate.SPAN({"class": "nodeLabelBox repTarget",
            role: 'treeitem'},
          "&lt;",
          domplate.SPAN({"class": "nodeTag"},
            "$object.nodeName|toLowerCase"),
          domplate.FOR("attr", "$object|attrIterator", BaseTemplates.AttrTag),
          domplate.SPAN({"class":
            "nodeBracket editable insertBefore"}, "&gt;"),
          BaseTemplates.TextTag,
          "&lt;/",
          domplate.SPAN({"class": "nodeTag"},
            "$object.nodeName|toLowerCase"),
          "&gt;"
        )
      )
    )
});

HTMLTemplates.EmptyElement = domplate(BaseTemplates.Element,
{
  tag:
    domplate.DIV({"class":
        "nodeBox emptyNodeBox $object|getHidden repIgnore",
        _repObject: "$object", role: 'presentation'},
      domplate.DIV({"class": "nodeLabel", role: "presentation"},
        domplate.SPAN({"class": "nodeLabelBox repTarget",
            role: 'treeitem'},
          "&lt;",
          domplate.SPAN({"class": "nodeTag"},
            "$object.nodeName|toLowerCase"),
          domplate.FOR("attr", "$object|attrIterator", BaseTemplates.AttrTag),
          domplate.SPAN({"class":
            "nodeBracket editable insertBefore"}, "&gt;")
        )
      )
    )
});

HTMLTemplates.XEmptyElement = domplate(BaseTemplates.Element,
{
  tag:
    domplate.DIV({"class":
        "nodeBox emptyNodeBox $object|getHidden repIgnore",
        _repObject: "$object", role: 'presentation'},
      domplate.DIV({"class": "nodeLabel", role: "presentation"},
        domplate.SPAN({"class": "nodeLabelBox repTarget",
            role : 'treeitem'},
          "&lt;",
          domplate.SPAN({"class": "nodeTag"},
            "$object.nodeName|toLowerCase"),
          domplate.FOR("attr", "$object|attrIterator", BaseTemplates.AttrTag),
          domplate.SPAN({"class":
            "nodeBracket editable insertBefore"}, "/&gt;")
        )
      )
    )
});

HTMLTemplates.AttrNode = domplate(BaseTemplates.Element,
{
  tag: BaseTemplates.AttrTag
});

HTMLTemplates.TextNode = domplate(BaseTemplates.Element,
{
  tag:
    domplate.DIV({"class": "nodeBox", _repObject: "$object",
      role: 'presentation'}, BaseTemplates.TextTag)
});

HTMLTemplates.CDATANode = domplate(BaseTemplates.Element,
{
  tag:
    domplate.DIV({"class": "nodeBox", _repObject: "$object",
      role: 'presentation'},
        "&lt;![CDATA[",
        domplate.SPAN({"class": "nodeText nodeCDATA editable"},
          "$object.nodeValue"),
        "]]&gt;")
});

HTMLTemplates.CommentNode = domplate(BaseTemplates.Element,
{
  tag:
    domplate.DIV({"class": "nodeBox nodeComment",
        _repObject: "$object", role : 'presentation'},
      "&lt;!--",
      domplate.SPAN({"class": "nodeComment editable"},
        "$object.nodeValue"),
      "--&gt;")
});

HTMLTemplates.Nada = domplate(BaseTemplates.Rep,
{
  tag: domplate.SPAN(""),
  className: "nada"
});

