mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-30 00:55:00 +00:00
ae0b5f9af4
html markup handling. * Remove global 'use strict' declarations from html5 parser. * Add trailing whitespace handling in dt Overall, 55 parser tests are now passing.
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);
|
|
}
|
|
}
|