mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-16 02:51:50 +00:00
b750ce38b8
Builds a DOM tree (jsdom) from the tokens and then serializes that using document.innerHTML. This is all very experimental, so don't be surprised by rough edges.
302 lines
9.7 KiB
JavaScript
302 lines
9.7 KiB
JavaScript
"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);
|
|
}
|
|
}
|