const TRAVERSAL_LEAVE_ELEMENT = "__TRAVERSAL__LEAVE__ELEMENT__";
/**
* Generic DOM utilities.
*/
/*TEST|export|TEST*/ class DOMUtil {
// ------------------------------------
// Document traversal
// ------------------------------------
static traverse(node, handler) {
switch (node.nodeType) {
case Node.TEXT_NODE:
case Node.PROCESSING_INSTRUCTION_NODE:
case Node.COMMENT_NODE:
return handler(node, /*enterElement*/ undefined);
case Node.ELEMENT_NODE:
{
let result = handler(node, /*enterElement*/ true);
if (result !== null) {
return (result === TRAVERSAL_LEAVE_ELEMENT)? null : result;
}
let child = node.firstChild;
while (child !== null) {
result = DOMUtil.traverse(child, handler);
if (result !== null) {
return result;
}
child = child.nextSibling;
}
result = handler(node, /*enterElement*/ false);
if (result !== null) {
return result;
}
}
return null;
default:
return null;
}
}
static traverseBackwards(node, handler) {
switch (node.nodeType) {
case Node.TEXT_NODE:
case Node.PROCESSING_INSTRUCTION_NODE:
case Node.COMMENT_NODE:
return handler(node, /*enterElement*/ undefined);
case Node.ELEMENT_NODE:
{
let result = handler(node, /*enterElement*/ true);
if (result !== null) {
return (result === TRAVERSAL_LEAVE_ELEMENT)? null : result;
}
let child = node.lastChild;
while (child !== null) {
result = DOMUtil.traverseBackwards(child, handler);
if (result !== null) {
return result;
}
child = child.previousSibling;
}
result = handler(node, /*enterElement*/ false);
if (result !== null) {
return result;
}
}
return null;
default:
return null;
}
}
static traverseFrom(node, handler) {
let result = DOMUtil.traverse(node, handler);
if (result !== null) {
return result;
}
return DOMUtil.traverseAfter(node, handler);
}
static traverseAfter(node, handler) {
let parent = DOMUtil.parentElementWithinView(node);
if (parent === null) {
return null;
}
let sibling = node.nextSibling;
while (sibling !== null) {
let result = DOMUtil.traverse(sibling, handler);
if (result !== null) {
return result;
}
sibling = sibling.nextSibling;
}
for (;;) {
// Do not visit the siblings of root element.
let ancestor = DOMUtil.parentElementWithinView(parent);
if (ancestor === null) {
return null;
}
let from = parent.nextSibling;
if (from !== null) {
return DOMUtil.traverseFrom(from, handler);
}
parent = ancestor;
}
}
static traverseBackwardsFrom(node, handler) {
// This utility corresponds to XXE's Traversal.traverseBackwardsFromEx
// (which generates extra leave parent events
// compared to Traversal.traverseBackwardsFrom).
let result = DOMUtil.traverseBackwards(node, handler);
if (result !== null) {
return result;
}
return DOMUtil.traverseBefore(node, handler);
}
static traverseBefore(node, handler) {
// This utility corresponds to XXE's Traversal.traverseBeforeEx
// (which generates extra leave parent events
// compared to Traversal.traverseBefore).
let parent = DOMUtil.parentElementWithinView(node);
if (parent === null) {
return null;
}
let sibling = node.previousSibling;
while (sibling !== null) {
let result = DOMUtil.traverseBackwards(sibling, handler);
if (result !== null) {
return result;
}
sibling = sibling.previousSibling;
}
for (;;) {
let result = handler(parent, /*enterElement*/ false);
if (result !== null) {
return result;
}
// Do not visit the siblings of root element.
let ancestor = DOMUtil.parentElementWithinView(parent);
if (ancestor === null) {
return null;
}
let from = parent.previousSibling;
if (from !== null) {
return DOMUtil.traverseBackwardsFrom(from, handler);
}
parent = ancestor;
}
}
static parentElementWithinView(node) {
let parent = node.parentElement;
if (parent !== null && (parent instanceof DocumentView)) {
parent = null;
}
return parent;
}
// ------------------------------------
// lookupAncestorByTag
// ------------------------------------
static lookupAncestorByTag(node, ancestorName) {
let found = null;
let ancestor = node.parentElement;
while (ancestor !== null) {
if (ancestor.localName === ancestorName) {
found = ancestor;
break;
}
ancestor = ancestor.parentElement;
}
return found;
}
// ------------------------------------
// textContentLength
// ------------------------------------
static textContentLength(node) {
let count = 0;
if (node !== null) {
switch (node.nodeType) {
case Node.TEXT_NODE:
count += node.length;
break;
case Node.ELEMENT_NODE:
{
let child = node.firstChild;
while (child !== null) {
count += DOMUtil.textContentLength(child);
child = child.nextSibling;
}
}
break;
}
}
return count;
}
// ------------------------------------
// getAllTextNodes
// ------------------------------------
static getAllTextNodes(node) {
let list = [];
if (node !== null) {
DOMUtil.doGetAllTextNodes(node, list);
}
return list;
}
static doGetAllTextNodes(node, list) {
switch (node.nodeType) {
case Node.TEXT_NODE:
list.push(node)
break;
case Node.ELEMENT_NODE:
{
let child = node.firstChild;
while (child !== null) {
DOMUtil.doGetAllTextNodes(child, list);
child = child.nextSibling;
}
}
break;
}
}
// ------------------------------------
// createElementFromHTML
// ------------------------------------
static createElementFromHTML(html) {
// Unlike table, template has no content restriction.
let template = document.createElement('template');
template.innerHTML = html;
return template.content.firstElementChild;
}
// ------------------------------------
// isDisplayedNode
// ------------------------------------
static isDisplayedNode(node, root) {
let elem = node;
if (node !== null &&
node.nodeType !== Node.ELEMENT_NODE) {
elem = node.parentElement;
}
while (elem !== null && elem !== root) {
const display =
window.getComputedStyle(elem).getPropertyValue("display");
if (display === "none") {
return false;
}
elem = elem.parentElement;
}
return true;
}
// ------------------------------------
// dumpNode
// ------------------------------------
static dumpNode(node, dump="", indent=0) {
if (node !== null) {
dump = DOMUtil.indentDumpNode(dump, indent);
switch (node.nodeType) {
case Node.TEXT_NODE:
dump += '"' + node.data + '" (' + node.length + ' chars)\n';
break;
case Node.ELEMENT_NODE:
{
dump += '<' + node.localName;
const attrs = node.attributes;
for (let i = attrs.length-1; i >= 0; --i) {
dump += " " + attrs[i].name +
"='" + attrs[i].value + "'";
}
let child = node.firstChild;
if (child === null) {
dump += ' />\n';
} else {
dump += '>\n';
while (child !== null) {
dump = DOMUtil.dumpNode(child, dump, indent+2);
child = child.nextSibling;
}
dump = DOMUtil.indentDumpNode(dump, indent);
dump += '</' + node.localName + '>\n';
}
}
break;
}
}
return dump;
}
static indentDumpNode(dump, indent) {
while (indent > 0) {
dump += " ";
--indent;
}
return dump;
}
// ------------------------------------
// escapeXML/unescapeXML
// ------------------------------------
/**
* Escapes specified string (that is,
* <code>'<'</code> is replaced by "<code>&#60</code>;",
* <code>'&'</code> is replaced by "<code>&#38;</code>", etc).
*
* @param {string} text - string to be escaped.
* @param {number} [maxCode=0xFFFF] maxCode - characters with
* code > maxCode are escaped as <code>&#<i>code</i>;</code>.
* Pass 127 for US-ASCII, 255 for ISO-8859-1, otherwise pass
* <code>0xFFFF</code> (Unicode Basic Multilingual Plane).
* @return {string} escaped string.
*/
static escapeXML(text, maxCode=0xFFFF) {
let escaped = "";
const count = text.length;
for (let i = 0; i < count; ++i) {
let c = text.charAt(i);
switch (c) {
case "'":
escaped += "'";
break;
case '"':
escaped += """;
break;
case '<':
escaped += "<";
break;
case '>':
escaped += ">";
break;
case '&':
escaped += "&";
break;
default:
{
const code = c.charCodeAt(0);
if (code > maxCode) {
escaped += "&#";
escaped += String(code);
escaped += ';';
} else {
escaped += c;
}
}
}
}
return escaped;
}
/**
* Unescapes specified string. Inverse operation of <code>escapeXML</code>.
*
* @param {string} text string to be unescaped.
* @return {string} unescaped string.
*/
static unescapeXML(text) {
const parseCharRef = (charRef) => {
if (charRef.length >= 2 && charRef.charAt(0) === '#') {
let i;
if (charRef.charAt(1) === 'x') {
i = parseInt(charRef.substring(2), 16);
} else {
i = parseInt(charRef.substring(1));
}
if (isNaN(i) || i <= 0 || i > 0xFFFF) {
return '?';
} else {
return String.fromCharCode(i);
}
} if (charRef === "amp") {
return '&';
} if (charRef === "apos") {
return "'";
} if (charRef === "quot") {
return '"';
} if (charRef === "lt") {
return '<';
} if (charRef === "gt") {
return '>';
} else {
return '?';
}
}
// ---
let unescaped = "";
const count = text.length;
for (let i = 0; i < count; ++i) {
let c = text.charAt(i);
if (c === '&') {
let charRef = "";
++i;
while (i < count) {
c = text.charAt(i);
if (c === ';') {
break;
}
charRef += c;
++i;
}
c = parseCharRef(charRef);
}
unescaped += c;
}
return unescaped;
}
// -----------------------------------------------------------------------
/*TEST|
static test_escapeXML(logger) {
const texts = [
"\"Black & Decker\" (<BLACK+DECKER\u2122>) une 'bonne' marque?",
"\u00ABOh le bel \u00E9t\u00E9 que voil\u00E0!\u00BB",
"\u00C7a c\u2019est bien vrai!",
];
for (let text of texts) {
let escaped = DOMUtil.escapeXML(text, 127); // US-ASCII
let unescaped = DOMUtil.unescapeXML(escaped);
logger(`${text} --escape--> ${escaped} --unescape--> ${unescaped}`);
}
}
|TEST*/
}