/* * 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 "<"; case ">": return ">"; case "&": return "&"; case "'": return "'"; case '"': return """; } 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(',""'); }, 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( "<", domplate.SPAN({"class": "nodeTag"}, "$object.localName|toLowerCase"), domplate.FOR("attr", "$object|attrIterator", " $attr.localName="", domplate.SPAN({"class": "nodeValue"}, "$attr.nodeValue"), """ ), ">" ), 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"}, " ", domplate.SPAN({"class": "nodeName editable"}, "$attr.nodeName"), "="", domplate.SPAN({"class": "nodeValue editable"}, "$attr.nodeValue"), """); 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'}, "<", domplate.SPAN({"class": "nodeTag"}, "$object.nodeName|toLowerCase"), domplate.FOR("attr", "$object|attrIterator", BaseTemplates.AttrTag), domplate.SPAN({"class": "nodeBracket"}, ">") ) ), domplate.DIV({"class": "nodeChildBox", role :"group"}, domplate.FOR("child", "$object|childIterator", domplate.TAG("$child|getNodeTag", {object: "$child"}) ) ), domplate.DIV({"class": "nodeCloseLabel", role:"presentation"}, "</", domplate.SPAN({"class": "nodeTag"}, "$object.nodeName|toLowerCase"), ">" ) ), 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'}, "<", domplate.SPAN({"class": "nodeTag"}, "$object.nodeName|toLowerCase"), domplate.FOR("attr", "$object|attrIterator", BaseTemplates.AttrTag), domplate.SPAN({"class": "nodeBracket editable insertBefore"}, ">") ) ), domplate.DIV({"class": "nodeChildBox", role: "group"}), /* nodeChildBox is special signal in insideOutBox */ domplate.DIV({"class": "nodeCloseLabel", role: "presentation"}, domplate.SPAN({"class": "nodeCloseLabelBox repTarget"}, "</", domplate.SPAN({"class": "nodeTag"}, "$object.nodeName|toLowerCase"), ">" ) ) ) }); 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'}, "<", domplate.SPAN({"class": "nodeTag"}, "$object.nodeName|toLowerCase"), domplate.FOR("attr", "$object|attrIterator", BaseTemplates.AttrTag), domplate.SPAN({"class": "nodeBracket editable insertBefore"}, ">") ) ), /* nodeChildBox is special signal in insideOutBox */ domplate.DIV({"class": "nodeChildBox", role: "group"}), domplate.DIV({"class": "nodeCloseLabel", role: "presentation"}, domplate.SPAN({"class": "nodeCloseLabelBox repTarget"}, "</", domplate.SPAN({"class": "nodeTag"}, "$object.nodeName|toLowerCase"), ">" ) ) ), getDocType: function(obj) { let doctype = obj.ownerDocument.doctype; return ''; } }); 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'}, "<", domplate.SPAN({"class": "nodeTag"}, "$object.nodeName|toLowerCase"), domplate.FOR("attr", "$object|attrIterator", BaseTemplates.AttrTag), domplate.SPAN({"class": "nodeBracket editable insertBefore"}, ">"), BaseTemplates.TextTag, "</", domplate.SPAN({"class": "nodeTag"}, "$object.nodeName|toLowerCase"), ">" ) ) ) }); 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'}, "<", domplate.SPAN({"class": "nodeTag"}, "$object.nodeName|toLowerCase"), domplate.FOR("attr", "$object|attrIterator", BaseTemplates.AttrTag), domplate.SPAN({"class": "nodeBracket editable insertBefore"}, ">") ) ) ) }); 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'}, "<", domplate.SPAN({"class": "nodeTag"}, "$object.nodeName|toLowerCase"), domplate.FOR("attr", "$object|attrIterator", BaseTemplates.AttrTag), domplate.SPAN({"class": "nodeBracket editable insertBefore"}, "/>") ) ) ) }); 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'}, "<![CDATA[", domplate.SPAN({"class": "nodeText nodeCDATA editable"}, "$object.nodeValue"), "]]>") }); HTMLTemplates.CommentNode = domplate(BaseTemplates.Element, { tag: domplate.DIV({"class": "nodeBox nodeComment", _repObject: "$object", role : 'presentation'}, "<!--", domplate.SPAN({"class": "nodeComment editable"}, "$object.nodeValue"), "-->") }); HTMLTemplates.Nada = domplate(BaseTemplates.Rep, { tag: domplate.SPAN(""), className: "nada" });