diff --git a/cjs/interface/attr.js b/cjs/interface/attr.js index 65af5bac..446798c2 100644 --- a/cjs/interface/attr.js +++ b/cjs/interface/attr.js @@ -46,7 +46,8 @@ class Attr extends Node { if (emptyAttributes.has(name) && !value) { return ignoreCase(this) ? name : `${name}=""`; } - const escapedValue = (ignoreCase(this) ? value : escape(value)).replace(QUOTE, '"'); + const rawValue = ignoreCase(this) ? value : escape(value); + const escapedValue = rawValue.includes('"') ? rawValue.replace(QUOTE, '"') : rawValue; return `${name}="${escapedValue}"`; } diff --git a/cjs/interface/element.js b/cjs/interface/element.js index 5d513d6a..b0d22435 100644 --- a/cjs/interface/element.js +++ b/cjs/interface/element.js @@ -160,35 +160,35 @@ class Element extends ParentNode { // get innerText() { - const text = []; + let text = ''; let {[NEXT]: next, [END]: end} = this; while (next !== end) { if (next.nodeType === TEXT_NODE) { - text.push(next.textContent.replace(/\s+/g, ' ')); + text += next.textContent.replace(/\s+/g, ' '); } else if( text.length && next[NEXT] != end && BLOCK_ELEMENTS.has(next.tagName) ) { - text.push('\n'); + text += '\n'; } next = next[NEXT]; } - return text.join(''); + return text; } /** * @returns {String} */ get textContent() { - const text = []; + let text = ''; let {[NEXT]: next, [END]: end} = this; while (next !== end) { const nodeType = next.nodeType; if (nodeType === TEXT_NODE || nodeType === CDATA_SECTION_NODE) - text.push(next.textContent); + text += next.textContent; next = next[NEXT]; } - return text.join(''); + return text; } set textContent(text) { @@ -435,7 +435,8 @@ class Element extends ParentNode { // toString() { - const out = []; + // the output grows by concatenation: one rope beats an array of thousands of pieces + let out = ''; const {[END]: end} = this; let next = {[NEXT]: this}; let isOpened = false; @@ -443,14 +444,14 @@ class Element extends ParentNode { next = next[NEXT]; switch (next.nodeType) { case ATTRIBUTE_NODE: { - const attr = ' ' + next; + const attr = next.toString(); switch (attr) { - case ' id': - case ' class': - case ' style': + case 'id': + case 'class': + case 'style': break; default: - out.push(attr); + out += ' ' + attr; } break; } @@ -458,39 +459,42 @@ class Element extends ParentNode { const start = next[START]; if (isOpened) { if ('ownerSVGElement' in start) - out.push(' />'); + out += ' />'; else if (isVoid(start)) - out.push(ignoreCase(start) ? '>' : ' />'); + out += ignoreCase(start) ? '>' : ' />'; else - out.push(`>`); + out += `>`; isOpened = false; } else - out.push(``); + out += ``; break; } case ELEMENT_NODE: if (isOpened) - out.push('>'); + out += '>'; if (next.toString !== this.toString) { - out.push(next.toString()); + out += next.toString(); next = next[END]; isOpened = false; } else { - out.push(`<${next.localName}`); + out += `<${next.localName}`; isOpened = true; } break; case TEXT_NODE: case COMMENT_NODE: case CDATA_SECTION_NODE: - out.push((isOpened ? '>' : '') + next); - isOpened = false; + if (isOpened) { + out += '>'; + isOpened = false; + } + out += next; break; } } while (next !== end); - return out.join(''); + return out; } toJSON() { diff --git a/cjs/interface/text.js b/cjs/interface/text.js index 241511a1..22e041aa 100644 --- a/cjs/interface/text.js +++ b/cjs/interface/text.js @@ -14,24 +14,24 @@ class Text extends CharacterData { } get wholeText() { - const text = []; let {previousSibling, nextSibling} = this; + let text = ''; while (previousSibling) { if (previousSibling.nodeType === TEXT_NODE) - text.unshift(previousSibling[VALUE]); + text = previousSibling[VALUE] + text; else break; previousSibling = previousSibling.previousSibling; } - text.push(this[VALUE]); + text += this[VALUE]; while (nextSibling) { if (nextSibling.nodeType === TEXT_NODE) - text.push(nextSibling[VALUE]); + text += nextSibling[VALUE]; else break; nextSibling = nextSibling.nextSibling; } - return text.join(''); + return text; } cloneNode() { diff --git a/cjs/mixin/parent-node.js b/cjs/mixin/parent-node.js index 47590b42..fd0b9c59 100644 --- a/cjs/mixin/parent-node.js +++ b/cjs/mixin/parent-node.js @@ -25,8 +25,6 @@ const {NodeList} = require('../interface/node-list.js'); const {moCallback} = require('../interface/mutation-observer.js'); const {connectedCallback} = require('../interface/custom-element-registry.js'); -const {nextElementSibling} = require('./non-document-type-child-node.js'); - const isNode = node => node instanceof Node; const insert = (parentNode, child, nodes) => { @@ -64,20 +62,37 @@ class ParentNode extends Node { get childNodes() { const childNodes = new NodeList; - let {firstChild} = this; - while (firstChild) { - childNodes.push(firstChild); - firstChild = nextSibling(firstChild); + // walk the list once: an element is followed by its whole subtree, so jump over it + const {[END]: end} = this; + let next = this[NEXT]; + while (next !== end) { + switch (next.nodeType) { + case ATTRIBUTE_NODE: + next = next[NEXT]; + break; + case ELEMENT_NODE: + childNodes.push(next); + next = next[END][NEXT]; + break; + default: + childNodes.push(next); + next = next[NEXT]; + } } return childNodes; } get children() { const children = new NodeList; - let {firstElementChild} = this; - while (firstElementChild) { - children.push(firstElementChild); - firstElementChild = nextElementSibling(firstElementChild); + const {[END]: end} = this; + let next = this[NEXT]; + while (next !== end) { + if (next.nodeType === ELEMENT_NODE) { + children.push(next); + next = next[END][NEXT]; + } + else + next = next[NEXT]; } return children; } diff --git a/cjs/shared/text-escaper.js b/cjs/shared/text-escaper.js index 1d4cbd7a..928d0c05 100644 --- a/cjs/shared/text-escaper.js +++ b/cjs/shared/text-escaper.js @@ -3,6 +3,8 @@ const {replace} = ''; // escape const ca = /[<>&\xA0]/g; +// same characters, without the global flag: `test` on a global regex moves lastIndex +const needsEscape = /[<>&\xA0]/; const esca = { '\xA0': ' ', @@ -20,5 +22,5 @@ const pe = m => esca[m]; * the input type is unexpected, except for boolean and numbers, * converted as string. */ -const escape = es => replace.call(es, ca, pe); +const escape = es => needsEscape.test(es) ? replace.call(es, ca, pe) : es; exports.escape = escape; diff --git a/esm/interface/attr.js b/esm/interface/attr.js index 6c617f69..5406a94b 100644 --- a/esm/interface/attr.js +++ b/esm/interface/attr.js @@ -45,7 +45,8 @@ export class Attr extends Node { if (emptyAttributes.has(name) && !value) { return ignoreCase(this) ? name : `${name}=""`; } - const escapedValue = (ignoreCase(this) ? value : escape(value)).replace(QUOTE, '"'); + const rawValue = ignoreCase(this) ? value : escape(value); + const escapedValue = rawValue.includes('"') ? rawValue.replace(QUOTE, '"') : rawValue; return `${name}="${escapedValue}"`; } diff --git a/esm/interface/element.js b/esm/interface/element.js index c98f5f7d..e643eb18 100644 --- a/esm/interface/element.js +++ b/esm/interface/element.js @@ -162,35 +162,35 @@ export class Element extends ParentNode { // get innerText() { - const text = []; + let text = ''; let {[NEXT]: next, [END]: end} = this; while (next !== end) { if (next.nodeType === TEXT_NODE) { - text.push(next.textContent.replace(/\s+/g, ' ')); + text += next.textContent.replace(/\s+/g, ' '); } else if( text.length && next[NEXT] != end && BLOCK_ELEMENTS.has(next.tagName) ) { - text.push('\n'); + text += '\n'; } next = next[NEXT]; } - return text.join(''); + return text; } /** * @returns {String} */ get textContent() { - const text = []; + let text = ''; let {[NEXT]: next, [END]: end} = this; while (next !== end) { const nodeType = next.nodeType; if (nodeType === TEXT_NODE || nodeType === CDATA_SECTION_NODE) - text.push(next.textContent); + text += next.textContent; next = next[NEXT]; } - return text.join(''); + return text; } set textContent(text) { @@ -437,7 +437,8 @@ export class Element extends ParentNode { // toString() { - const out = []; + // the output grows by concatenation: one rope beats an array of thousands of pieces + let out = ''; const {[END]: end} = this; let next = {[NEXT]: this}; let isOpened = false; @@ -445,14 +446,14 @@ export class Element extends ParentNode { next = next[NEXT]; switch (next.nodeType) { case ATTRIBUTE_NODE: { - const attr = ' ' + next; + const attr = next.toString(); switch (attr) { - case ' id': - case ' class': - case ' style': + case 'id': + case 'class': + case 'style': break; default: - out.push(attr); + out += ' ' + attr; } break; } @@ -460,39 +461,42 @@ export class Element extends ParentNode { const start = next[START]; if (isOpened) { if ('ownerSVGElement' in start) - out.push(' />'); + out += ' />'; else if (isVoid(start)) - out.push(ignoreCase(start) ? '>' : ' />'); + out += ignoreCase(start) ? '>' : ' />'; else - out.push(`>`); + out += `>`; isOpened = false; } else - out.push(``); + out += ``; break; } case ELEMENT_NODE: if (isOpened) - out.push('>'); + out += '>'; if (next.toString !== this.toString) { - out.push(next.toString()); + out += next.toString(); next = next[END]; isOpened = false; } else { - out.push(`<${next.localName}`); + out += `<${next.localName}`; isOpened = true; } break; case TEXT_NODE: case COMMENT_NODE: case CDATA_SECTION_NODE: - out.push((isOpened ? '>' : '') + next); - isOpened = false; + if (isOpened) { + out += '>'; + isOpened = false; + } + out += next; break; } } while (next !== end); - return out.join(''); + return out; } toJSON() { diff --git a/esm/interface/text.js b/esm/interface/text.js index a07a8f3a..81ca58b1 100644 --- a/esm/interface/text.js +++ b/esm/interface/text.js @@ -13,24 +13,24 @@ export class Text extends CharacterData { } get wholeText() { - const text = []; let {previousSibling, nextSibling} = this; + let text = ''; while (previousSibling) { if (previousSibling.nodeType === TEXT_NODE) - text.unshift(previousSibling[VALUE]); + text = previousSibling[VALUE] + text; else break; previousSibling = previousSibling.previousSibling; } - text.push(this[VALUE]); + text += this[VALUE]; while (nextSibling) { if (nextSibling.nodeType === TEXT_NODE) - text.push(nextSibling[VALUE]); + text += nextSibling[VALUE]; else break; nextSibling = nextSibling.nextSibling; } - return text.join(''); + return text; } cloneNode() { diff --git a/esm/mixin/parent-node.js b/esm/mixin/parent-node.js index b54c4a0e..611a7d99 100644 --- a/esm/mixin/parent-node.js +++ b/esm/mixin/parent-node.js @@ -24,8 +24,6 @@ import {NodeList} from '../interface/node-list.js'; import {moCallback} from '../interface/mutation-observer.js'; import {connectedCallback} from '../interface/custom-element-registry.js'; -import {nextElementSibling} from './non-document-type-child-node.js'; - const isNode = node => node instanceof Node; const insert = (parentNode, child, nodes) => { @@ -63,20 +61,37 @@ export class ParentNode extends Node { get childNodes() { const childNodes = new NodeList; - let {firstChild} = this; - while (firstChild) { - childNodes.push(firstChild); - firstChild = nextSibling(firstChild); + // walk the list once: an element is followed by its whole subtree, so jump over it + const {[END]: end} = this; + let next = this[NEXT]; + while (next !== end) { + switch (next.nodeType) { + case ATTRIBUTE_NODE: + next = next[NEXT]; + break; + case ELEMENT_NODE: + childNodes.push(next); + next = next[END][NEXT]; + break; + default: + childNodes.push(next); + next = next[NEXT]; + } } return childNodes; } get children() { const children = new NodeList; - let {firstElementChild} = this; - while (firstElementChild) { - children.push(firstElementChild); - firstElementChild = nextElementSibling(firstElementChild); + const {[END]: end} = this; + let next = this[NEXT]; + while (next !== end) { + if (next.nodeType === ELEMENT_NODE) { + children.push(next); + next = next[END][NEXT]; + } + else + next = next[NEXT]; } return children; } diff --git a/esm/shared/text-escaper.js b/esm/shared/text-escaper.js index 1489cd0c..4552fb82 100644 --- a/esm/shared/text-escaper.js +++ b/esm/shared/text-escaper.js @@ -2,6 +2,8 @@ const {replace} = ''; // escape const ca = /[<>&\xA0]/g; +// same characters, without the global flag: `test` on a global regex moves lastIndex +const needsEscape = /[<>&\xA0]/; const esca = { '\xA0': ' ', @@ -19,4 +21,4 @@ const pe = m => esca[m]; * the input type is unexpected, except for boolean and numbers, * converted as string. */ -export const escape = es => replace.call(es, ca, pe); +export const escape = es => needsEscape.test(es) ? replace.call(es, ca, pe) : es;