"use strict"; var HTML5 = require('../html5'); var assert = require('assert'); //if(!Array.prototype.last) { // Array.prototype.last = function() { return this[this.length - 1] }; //} HTML5.TreeBuilder = function TreeBuilder(document) { this.open_elements = []; this.document = document; this.activeFormattingElements = []; } var b = HTML5.TreeBuilder; b.prototype.reset = function() { } b.prototype.copyAttributeToElement = function(element, attribute) { if(attribute.nodeType && attribute.nodeType == attribute.ATTRIBUTE_NODE) { element.setAttributeNode(attribute.cloneNode()); } else { try { element.setAttribute(attribute.nodeName, attribute.nodeValue) } catch(e) { console.log("Can't set attribute '" + attribute.nodeName + "' to value '" + attribute.nodeValue + "': (" + e + ')'); } if(attribute.namespace) { var at = element.getAttributeNode(attribute.nodeName); at.namespace = attribute.namespace; } } } b.prototype.createElement = function (name, attributes, namespace) { try { var el = this.document.createElement(name); } catch(e) { console.log("Can't create element '"+ name + "' (" + e + ")") } el.namespace = namespace; if(attributes) { if(attributes.item) { for(var i = 0; i < attributes.length; i++) { HTML5.debug('treebuilder.copyAttributes', attributes.item(i)); this.copyAttributeToElement(el, attributes.item(i)); } } else { for(var i = 0; i < attributes.length; i++) { HTML5.debug('treebuilder.copyAttributes', attributes[i]); this.copyAttributeToElement(el, attributes[i]); } } } return el; } b.prototype.insert_element = function(name, attributes, namespace) { HTML5.debug('treebuilder.insert_element', name) if(this.insert_from_table) { return this.insert_element_from_table(name, attributes, namespace) } else { return this.insert_element_normal(name, attributes, namespace) } } b.prototype.insert_foreign_element = function(name, attributes, namespace) { return this.insert_element(name, attributes, namespace); } b.prototype.insert_element_normal = function(name, attributes, namespace) { var element = this.createElement(name, attributes, namespace); this.open_elements[this.open_elements.length - 1].appendChild(element); this.open_elements.push(element); return element; } b.prototype.insert_element_from_table = function(name, attributes, namespace) { var element = this.createElement(name, attributes, namespace) if(HTML5.TABLE_INSERT_MODE_ELEMENTS.indexOf(this.open_elements[this.open_elements.length - 1].tagName.toLowerCase()) != -1) { // We should be in the InTable mode. This means we want to do // special magic element rearranging var t = this.getTableMisnestedNodePosition() if(!t.insertBefore) { t.parent.appendChild(element) } else { t.parent.insertBefore(element, t.insertBefore) } this.open_elements.push(element) } else { return this.insert_element_normal(name, attributes, namespace); } return element; } b.prototype.insert_comment = function(data, parent) { try { var c = this.document.createComment(data); if(!parent) parent = this.open_elements[this.open_elements.length - 1]; parent.appendChild(c); } catch(e) { console.log("Can't create comment ("+ data + ")") } } b.prototype.insert_doctype = function (name, publicId, systemId) { try { var doctype = this.document.implementation.createDocumentType(name, publicId, systemId); this.document.appendChild(doctype); } catch(e) { console.log("Can't create doctype ("+ name + " / " + publicId + " / " + systemId + ")") } } b.prototype.insert_text = function(data, parent) { if(!parent) parent = this.open_elements[this.open_elements.length - 1]; if(!this.insert_from_table || HTML5.TABLE_INSERT_MODE_ELEMENTS.indexOf(this.open_elements[this.open_elements.length - 1].tagName.toLowerCase()) == -1) { if(parent.lastChild && parent.lastChild.nodeType == parent.TEXT_NODE) { parent.lastChild.appendData(data); } else { try { var tn = this.document.createTextNode(data); parent.appendChild(tn); } catch(e) { console.log("Can't create tex node (" + data + ")"); } } } else { // We should be in the inTable phase. This means we want to do special // magic element rearranging. var t = this.getTableMisnestedNodePosition(); insertText(t.parent, data, t.insertBefore) } } b.prototype.remove_open_elements_until = function(nameOrCb) { HTML5.debug('treebuilder.remove_open_elements_until', nameOrCb) var finished = false; while(!finished) { var element = this.pop_element(); finished = (typeof nameOrCb == 'function' ? nameOrCb(element) : element.tagName.toLowerCase() == nameOrCb); } return element; } b.prototype.pop_element = function() { var el = this.open_elements.pop() HTML5.debug('treebuilder.pop_element', el.name) return el } function insertText(node, data, before) { var t = node.ownerDocument.createTextNode(data) if(before) { if(before.previousSibling && before.previousSibling.nodeType == before.previousSibling.TEXT_NODE) { before.previousSibling.nodeValue += data; } else { node.insertBefore(t, before) } } else { node.appendChild(t) } } b.prototype.getTableMisnestedNodePosition = function() { // The foster parent element is the one which comes before the most // recently opened table element // XXX - this is really inelegant var lastTable, fosterParent, insertBefore for(var i = this.open_elements.length - 1; i >= 0; i--) { var element = this.open_elements[i] if(element.tagName.toLowerCase() == 'table') { lastTable = element break } } if(lastTable) { // XXX - we should check that the parent really is a node here if(lastTable.parentNode) { fosterParent = lastTable.parentNode insertBefore = lastTable } else { fosterParent = this.open_elements[this.open_elements.indexOf(lastTable) - 1] } } else { fosterParent = this.open_elements[0] } return {parent: fosterParent, insertBefore: insertBefore} } b.prototype.elementInScope = function(name, tableVariant) { if(this.open_elements.length == 0) return false for(var i = this.open_elements.length - 1; i >= 0; i--) { if (this.open_elements[i].tagName == undefined) return false else if(this.open_elements[i].tagName.toLowerCase() == name) return true else if(this.open_elements[i].tagName.toLowerCase() == 'table') return false else if(!tableVariant && HTML5.SCOPING_ELEMENTS.indexOf(this.open_elements[i].tagName.toLowerCase()) != -1) return false else if(this.open_elements[i].tagName.toLowerCase() == 'html') return false; } return false; } b.prototype.generateImpliedEndTags = function(exclude) { if(exclude) exclude = exclude.toLowerCase() if(this.open_elements.length == 0) { HTML5.debug('treebuilder.generateImpliedEndTags', 'no open elements') return } var name = this.open_elements[this.open_elements.length - 1].tagName.toLowerCase(); if(['dd', 'dt', 'li', 'p', 'td', 'th', 'tr'].indexOf(name) != -1 && name != exclude) { var p = this.pop_element(); this.generateImpliedEndTags(exclude); } } b.prototype.reconstructActiveFormattingElements = function() { // Within this algorithm the order of steps decribed in the specification // is not quite the same as the order of steps in the code. It should still // do the same though. // Step 1: stop if there's nothing to do if(this.activeFormattingElements.length == 0) return; // Step 2 and 3: start with the last element var i = this.activeFormattingElements.length - 1; var entry = this.activeFormattingElements[i]; if(entry == HTML5.Marker || this.open_elements.indexOf(entry) != -1) return; while(entry != HTML5.Marker && this.open_elements.indexOf(entry) == -1) { i -= 1; entry = this.activeFormattingElements[i]; if(!entry) break; } while(true) { i += 1; var clone = this.activeFormattingElements[i].cloneNode(); var element = this.insert_element(clone.tagName, clone.attributes); this.activeFormattingElements[i] = element; if(element == this.activeFormattingElements[this.activeFormattingElements.length - 1]) break; } } b.prototype.elementInActiveFormattingElements = function(name) { var els = this.activeFormattingElements; for(var i = els.length - 1; i >= 0; i--) { if(els[i] == HTML5.Marker) break; if(els[i].tagName.toLowerCase() == name) return els[i]; } return false; } b.prototype.reparentChildren = function(o, n) { while(o.childNodes.length > 0) { var el = o.removeChild(o.childNodes[0]); n.appendChild(el); } } b.prototype.clearActiveFormattingElements = function() { while(!(this.activeFormattingElements.length == 0 || this.activeFormattingElements.pop() == HTML5.Marker)); } b.prototype.getFragment = function() { // assert.ok(this.parser.inner_html) var fragment = this.document.createDocumentFragment() this.reparentChildren(this.root_pointer, fragment) return fragment } b.prototype.create_structure_elements = function(container) { this.html_pointer = this.document.getElementsByTagName('html')[0] if(!this.html_pointer) { this.html_pointer = this.createElement('html'); this.document.appendChild(this.html_pointer); } if(container == 'html') return; if(!this.head_pointer) { this.head_pointer = this.document.getElementsByTagName('head')[0] if(!this.head_pointer) { this.head_pointer = this.createElement('head'); this.html_pointer.appendChild(this.head_pointer); } } if(container == 'head') return; this.body_pointer = this.document.getElementsByTagName('body')[0] if(!this.body_pointer) { this.body_pointer = this.createElement('body'); this.html_pointer.appendChild(this.body_pointer); } }