function TreeConstructor() { this.tokenStream = null; this.tokenStreamPos = 0; this.insertionMode = insertionMode_InitialPhase; this.document = document.implementation.createDocument('', '', null); this.openStack = []; this.activeList = []; this.headElement = null; this.formElement = null; this.fostering = 0; this.createElementsInNS = false; } TreeConstructor.prototype.construct = function (tokens) { this.tokenStream = tokens; this.run(); return this.document; } TreeConstructor.prototype.popCurrentNode = function () { this.openStack.pop(); } TreeConstructor.prototype.addToActiveList = function (e) { this.activeList.push(e); } TreeConstructor.prototype.parseError = function (err) { debug("Parse error: " + err); } TreeConstructor.prototype.currentNodeHasName = function (n) { return this.openStack[this.openStack.length-1].localName == n; } TreeConstructor.prototype.nodeBeforeCurrentHasName = function (n) { return this.openStack[this.openStack.length-2].localName == n; } TreeConstructor.prototype.popUntilOneOf = function (ns) { while (true) { var e = this.openStack.pop(); for (var i = 0; i < ns.length; ++i) if (ns[i] === e.localName) return; } } TreeConstructor.prototype.clearActiveListUpToMarker = function () { for (var i = this.activeList.length-1; i >= 0; --i) if (this.activeList[i] === null) break; this.activeList.splice(i, this.activeList.length-i); } TreeConstructor.prototype.setInsertionMode = function (mode) { this.insertionMode = mode; } TreeConstructor.prototype.reprocessAsIf = function (token, mode) { this.processToken(token, mode); } TreeConstructor.prototype.reprocessWithFosteringAsIf = function (token, mode) { ++this.fostering; this.reprocessAsIf(token, mode); --this.fostering; } TreeConstructor.prototype.hasElementInScope = function (table, names) { var n = this.openStack.length-1; while (true) { var name = this.openStack[n].localName; for (var i = 0; i < names.length; ++i) if (name == names[i]) return true; if (name == 'table') return false; if (! table && (name == 'caption' || name == 'td' || name == 'th' || name == 'button' || name == 'marquee' || name == 'object')) return false; if (name == 'html') return false; --n; } } TreeConstructor.prototype.hasElementObjectInScope = function (target) { var n = this.openStack.length-1; while (true) { if (this.openStack[n] === target) return true; var name = this.openStack[n].localName; if (name == 'table') return false; if (name == 'caption' || name == 'td' || name == 'th' || name == 'button' || name == 'marquee' || name == 'object') return false; if (name == 'html') return false; --n; } } TreeConstructor.prototype.activeListContainsA = function () { for (var i = this.activeList.length-1; i >= 0; --i) { if (this.activeList[i] === null) return false; if (this.activeList[i].localName === 'a') return true; } return false; } TreeConstructor.prototype.removeThatAElement = function () { for (var i = this.activeList.length-1; i >= 0; --i) { if (this.activeList[i] === null) return false; if (this.activeList[i].localName === 'a') { var n = this.activeList[i]; this.activeList.splice(i, 1); for (var j = this.openStack.length-1; j >= 0; --j) { if (this.openStack[j] === n) { this.openStack.splice(j, 1); break; } } break; } } } TreeConstructor.prototype.ImpliedEndTagElements = { 'dd':1, 'dt':1, 'li':1, 'p':1, 'tbody':1, 'td':1, 'tfoot':1, 'th':1, 'thead':1, 'tr':1 }; TreeConstructor.prototype.generateImpliedEndTags = function (except) { while (1) { var name = this.openStack[this.openStack.length-1].localName; for (var i = 0; i < except.length; ++i) if (name == except[i]) return; if (this.ImpliedEndTagElements[name]) this.processToken(['EndTag', name], this.insertionMode); else return; } } TreeConstructor.prototype.reconstructActiveFormattingElements = function () { if (this.activeList.length == 0) return; var n = this.activeList.length-1; var entry = this.activeList[n]; if (entry === null) return; for (var i = this.openStack.length-1; i >= 0; --i) // search backwards for maybe efficiency if (this.openStack[i] === entry) return; while (n > 0) { --n; entry = this.activeList[n]; if (entry === null) { ++n; break; } for (var i = this.openStack.length-1; i >= 0; --i) if (this.openStack[i] === entry) { ++n; break; } } while (n < this.activeList.length) { entry = this.activeList[n]; var clone = entry.cloneNode(false); this.appendToCurrentNode(clone); this.openStack.push(clone); this.activeList[n] = clone; ++n; } } TreeConstructor.prototype.adoptionAgency = function (token) { while (1) { // for (var loop = 0; loop < 16; ++loop) { // debug("@ stack = "+uneval(this.openStack.map(function(n){return n.localName}))); // debug("@ list = "+uneval(this.activeList.map(function(n){return n.localName}))); var fmt; for (var fmt_listOff = this.activeList.length-1; ; --fmt_listOff) { fmt = this.activeList[fmt_listOff]; if (! fmt) { // (reached marker (null), or fell off end of list (undefined)) this.parseError("closed formatting element that wasn't active"); return; } else if (fmt.localName === token[1]) { break; } } // debug("@ fmt = "+fmt.localName); // debug("@ fmt_listOff = "+fmt_listOff); // Find fmt in the open stack for (var fmt_stackOff = this.openStack.length-1; fmt_stackOff >= 0; --fmt_stackOff) if (this.openStack[fmt_stackOff] === fmt) break; // fmt not in stack if (fmt_stackOff < 0) { this.parseError("closed formatting element that wasn't open"); this.activeList.splice(fmt_listOff, 1); return; } // fmt in stack but not in scope if (! this.hasElementObjectInScope(fmt)) { this.parseError("closed formatting element that wasn't in scope"); return; } // debug("@ fmt_stackOff = "+fmt_stackOff); var furthest_stackOff = null, furthest = null; for (var i = fmt_stackOff+1; i < this.openStack.length; ++i) { if (this.IsScoping[this.openStack[i].localName] || this.IsSpecial[this.openStack[i].localName]) { // !(formatting || phrasing) == (scoping || special) furthest_stackOff = i; furthest = this.openStack[i]; break; } } if (! furthest) { this.openStack.splice(fmt_stackOff, this.openStack.length - fmt_stackOff); this.activeList.splice(fmt_listOff, 1); return; } // debug("@ furthest = "+furthest.localName); // debug("@ furthest_stackOff = "+furthest_stackOff); var common = this.openStack[fmt_stackOff-1]; if (furthest.parentNode) furthest.parentNode.removeChild(furthest); var bookmark = fmt_listOff; // debug("@ common = "+common.localName); var node_stackOff = furthest_stackOff, lastNode_stackOff = furthest_stackOff; while (1) { var node_listOff; // Remove inactive items while (1) { --node_stackOff; for (node_listOff = this.activeList.length-1; node_listOff >= 0; --node_listOff) if (this.activeList[node_listOff] === this.openStack[node_stackOff]) break; if (node_listOff < 0) this.openStack.splice(node_stackOff, 1); else break; } if (node_stackOff == fmt_stackOff) break; if (lastNode_stackOff == furthest_stackOff) bookmark = node_listOff; var node = this.openStack[node_stackOff]; if (node.childNodes.length) { var clone = node.cloneNode(false); this.openStack[node_stackOff] = clone; this.activeList[node_listOff] = clone; node = clone; } this.appendToElement(this.openStack[lastNode_stackOff], node); lastNode_stackOff = node_stackOff; } this.appendToElement(this.openStack[lastNode_stackOff], common); var clone = fmt.cloneNode(false); while (furthest.firstChild) clone.appendChild(furthest.firstChild); this.appendToElement(clone, furthest); this.activeList.splice(bookmark+1, 0, clone); this.activeList.splice(fmt_listOff, 1); // (bookmark >= fmt_listOff, so this should be safe) this.openStack.splice(furthest_stackOff+1, 0, clone); this.openStack.splice(fmt_stackOff, 1); // (furthest_stackOff > fmt_stackOff, so this should be safe) } } TreeConstructor.prototype.applyEndTag = function (token) { for (var i = this.openStack.length-1; i >= 0; --i) { var node = this.openStack[i]; if (node.localName === token[1]) { this.generateImpliedEndTags([]); if (this.openStack[this.openStack.length-1].localName !== token[1]) this.parseError("invalid end tag"); this.openStack.splice(i, this.openStack.length-i); return; } if (! (this.IsScoping[node.localName] || this.IsSpecial[node.localName])) { // !(formatting || phrasing) == (scoping || special) this.parseError("invalid end tag ignored"); return; } } } TreeConstructor.prototype.clearStackToContext = function (names) { for (var i = this.openStack.length-1; i >= 0; --i) { var node = this.openStack[i]; var is = false; for (var j = 0; j < names.length; ++j) if (node.localName === names[j]) is = true; if (is) break; } if (i != this.openStack.length-1) { this.parseError("found open elements when clearing to table context"); this.openStack.splice(i+1, this.openStack.length - (i+1)); } } TreeConstructor.prototype.FosteringNodes = { table:1, tbody:1, tfoot:1, thead:1, tr:1 }; TreeConstructor.prototype.appendToCurrentNode = function (e) { if (! this.fostering || ! this.FosteringNodes[this.openStack[this.openStack.length-1].localName]) { this.openStack[this.openStack.length-1].appendChild(e); } else { var fosterParent, parentedTable = null; for (var i = this.openStack.length-1; i >= 0; --i) { if (this.openStack[i].localName == 'table') { fosterParent = this.openStack[i].parentNode; if (fosterParent && fosterParent.nodeType == fosterParent.ELEMENT_NODE) parentedTable = this.openStack[i]; else fosterParent = this.openStack[i-1]; break; } } if (i < 0) fosterParent = this.openStack[0]; if (parentedTable) fosterParent.insertBefore(e, parentedTable); else fosterParent.appendChild(e); } } TreeConstructor.prototype.appendToElement = function (e, tgt) { if (tgt === this.openStack[this.openStack.length-1]) this.appendToCurrentNode(e); else tgt.appendChild(e); } TreeConstructor.prototype.insertElement = function (token) { var name = token[1]; var e; if (this.createElementsInNS) { if (! name.match(/^[a-zA-Z_][a-zA-Z0-9_.-]*$/)) { // subset of XML Name name = '_INVALID-'+name.replace(/([^a-zA-Z_])/g, function (c) { return '.'+c.charCodeAt(0).toString(16) }); } e = this.document.createElementNS('http://www.w3.org/1999/xhtml', name); } else { try { e = this.document.createElement(name); } catch (err) { name = '_INVALID-'+name.replace(/([^a-zA-Z_])/g, function (c) { return '.'+c.charCodeAt(0).toString(16) }); e = this.document.createElement(name); } } for (var i = 0; i < token[2].length; ++i) { name = token[2][i][0]; try { e.setAttribute(name, token[2][i][1]); } catch (err) { name = '_INVALID-'+name.replace(/([^a-zA-Z_])/g, function (c) { return '.'+c.charCodeAt(0).toString(16) }); e.setAttribute(name, token[2][i][1]); } } this.appendToCurrentNode(e); this.openStack.push(e); return e; } TreeConstructor.prototype.insertHTMLElement = function () { var e; if (this.createElementsInNS) e = this.document.createElementNS('http://www.w3.org/1999/xhtml', 'html'); else e = this.document.createElement('html'); this.document.appendChild(e); this.openStack.push(e); } TreeConstructor.prototype.appendCharacterToCurrentNode = function (token) { if (! this.fostering || ! this.FosteringNodes[this.openStack[this.openStack.length-1].localName]) { var c = this.openStack[this.openStack.length-1]; if (c.lastChild && c.lastChild.nodeType == c.TEXT_NODE) c.lastChild.data += token[1]; else c.appendChild(this.document.createTextNode(token[1])); } else { var fosterParent, parentedTable = null; for (var i = this.openStack.length-1; i >= 0; --i) { if (this.openStack[i].localName == 'table') { fosterParent = this.openStack[i].parentNode; if (fosterParent && fosterParent.nodeType == fosterParent.ELEMENT_NODE) parentedTable = this.openStack[i]; else fosterParent = this.openStack[i-1]; break; } } if (i < 0) fosterParent = this.openStack[0]; if (parentedTable) { if (parentedTable.previousSibling && parentedTable.previousSibling.nodeType == parentedTable.TEXT_NODE) parentedTable.previousSibling.data += token[1]; else fosterParent.insertBefore(this.document.createTextNode(token[1]), parentedTable); } else { var c = fosterParent; if (c.lastChild && c.lastChild.nodeType == c.TEXT_NODE) c.lastChild.data += token[1]; else c.appendChild(this.document.createTextNode(token[1])); } } } TreeConstructor.prototype.appendCommentToCurrentNode = function (token) { var c = this.document.createComment(token[1]); this.appendToCurrentNode(c); } TreeConstructor.prototype.appendCommentToDocument = function (token) { var c = this.document.createComment(token[1]); this.document.appendChild(c); } TreeConstructor.prototype.doDoctypeStuff = function (token) { // TODO: parse errors try { var d = this.document.implementation.createDocumentType(token[1], token[2] === null ? '' : token[2], token[3] === null ? '' : token[3]); this.document.appendChild(d); } catch (err) { var c = this.document.createComment('(pretend there\'s a doctype here)'); this.document.appendChild(c); } // TODO: quirks } TreeConstructor.prototype.resetInsertionModeAppropriately = function () { var appropriate = { // TODO: make this a constant (but cope with inclusion ordering) select: insertionMode_MainPhase_InSelect, td: insertionMode_MainPhase_InCell, th: insertionMode_MainPhase_InCell, tr: insertionMode_MainPhase_InRow, tbody: insertionMode_MainPhase_InTableBody, thead: insertionMode_MainPhase_InTableBody, tfoot: insertionMode_MainPhase_InTableBody, caption: insertionMode_MainPhase_InCaption, colgroup: insertionMode_MainPhase_InColumnGroup, table: insertionMode_MainPhase_InTable, head: insertionMode_MainPhase_InBody, body: insertionMode_MainPhase_InBody, frameset: insertionMode_MainPhase_InFrameset }; for (var i = this.openStack.length-1; i >= 0; --i) { var n = this.openStack[i].localName; // TODO: fragment stuff var m = appropriate[n]; if (m) { this.setInsertionMode(m); return; } // TODO: fragment stuff } this.setInsertionMode(insertionMode_MainPhase_InBody); }