Skip to content

Instantly share code, notes, and snippets.

@bmeurer
Last active March 21, 2023 01:38
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bmeurer/370d2cf8dcd190318bba2b7807b92623 to your computer and use it in GitHub Desktop.
Save bmeurer/370d2cf8dcd190318bba2b7807b92623 to your computer and use it in GitHub Desktop.
// Copyright 2013-2019 Benedikt Meurer
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// <https://www.apache.org/licenses/LICENSE-2.0>
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
"use strict";
if (typeof console === 'undefined') console = {log:print};
const createDocument = (function() {
/*
const NODE_TYPES = {
ELEMENT_NODE: 1,
ATTRIBUTE_NODE: 2,
TEXT_NODE: 3,
CDATA_SECTION_NODE: 4,
ENTITY_REFERENCE_NODE: 5,
COMMENT_NODE: 6,
PROCESSING_INSTRUCTION_NODE: 7,
DOCUMENT_NODE: 9
};
*/
class Node {
constructor(nodeType, nodeName) {
this.nodeType = nodeType;
this.nodeName = nodeName;
this.nextSibling = null;
this.previousSibling = null;
this.parentNode = null;
}
appendChild(child) {
this.insertBefore(child, null);
return child;
}
insertBefore(child, ref) {
child.remove();
child.parentNode = this;
if (this.firstChild === null) {
this.firstChild = this.lastChild = child;
} else if (ref === null) {
child.previousSibling = this.lastChild;
this.lastChild.nextSibling = child;
this.lastChild = child;
} else {
const refPreviousSibling = ref.previousSibling;
if (refPreviousSibling !== null) {
child.previousSibling = refPreviousSibling;
refPreviousSibling.nextSibling = child;
} else {
this.firstChild = child;
}
ref.previousSibling = child;
child.nextSibling = ref;
}
return child;
}
removeChild(child) {
const childNextSibling = child.nextSibling;
const childPreviousSibling = child.previousSibling;
if (childNextSibling !== null) {
childNextSibling.previousSibling = childPreviousSibling;
} else {
this.lastChild = childPreviousSibling;
}
if (childPreviousSibling !== null) {
childPreviousSibling.nextSibling = childNextSibling;
} else {
this.firstChild = childNextSibling;
}
child.parentNode = null;
child.nextSibling = null;
child.previousSibling = null;
return child;
}
remove() {
const parentNode = this.parentNode;
if (parentNode !== null) {
parentNode.removeChild(this);
}
}
}
class Text extends Node {
constructor(text) {
super(3, '#text'); // TEXT_NODE
this.textContent = text;
}
}
// TODO(bmeurer): Figure out how to do the jsaction event buffering support.
let eventIndex = 0;
class Element extends Node {
constructor(nodeType, nodeName) {
super(nodeType, nodeName); // ELEMENT_NODE
this.firstChild = null;
this.lastChild = null;
this.attributes = null;
}
setAttribute(key, value) {
let attributes = this.attributes;
if (attributes === null) {
this.attributes = attributes = new Map();
}
attributes.set(key, value);
}
getAttribute(key) {
const attributes = this.attributes;
if (attributes === null) return null;
return attributes.get(key);
}
removeAttribute(key) {
const attributes = this.attributes;
if (attributes !== null) attributes.delete(key);
}
addEventListener(type, handler) {
let val = this.getAttribute('jsaction') || '';
if (val) {
val += ';';
}
let typeStr = type === 'click' ? '' : type + ':';
val += `${typeStr}${this.nodeName}.${eventIndex++}`;
this.setAttribute('tsaction', val);
// (this.__handlers[toLower(type)] || (this.__handlers[toLower(type)] = [])).push(handler);
}
removeEventListener(type, handler) {
// TODO(bmeurer)
}
toString() {
return serialize(this);
}
}
class Document extends Element {
constructor() {
super(9, '#document'); // DOCUMENT_NODE
}
get document() { return this; }
createElement(type) {
return new Element(1, type);
}
createTextNode(text) {
return new Text(text);
}
}
/** Create a minimally viable DOM Document
* @returns {Document} document
*/
return function createDocument() {
let document = new Document();
document.appendChild(
document.documentElement = document.createElement('html')
);
document.documentElement.appendChild(
document.head = document.createElement('head')
);
document.documentElement.appendChild(
document.body = document.createElement('body')
);
return document;
}
})();
const document = (function() {
const document = createDocument();
const appRoot = document.createElement('app-root');
document.body.appendChild(appRoot);
const h1 = document.createElement('h1');
h1.appendChild(document.createTextNode('Tour of Heroes'));
appRoot.appendChild(h1);
const appHeroes = document.createElement('app-heroes');
appRoot.appendChild(appHeroes);
const h2 = document.createElement('h2');
h2.appendChild(document.createTextNode('Windstorm Details'));
appHeroes.appendChild(h2);
let div = document.createElement('div');
appHeroes.appendChild(div);
let span = document.createElement('span');
span.appendChild(document.createTextNode('id: '));
div.appendChild(span);
div.appendChild(document.createTextNode('1'));
div = document.createElement('div');
appHeroes.appendChild(div);
const label = document.createElement('label');
label.appendChild(document.createTextNode('name: '));
div.appendChild(label);
const input = document.createElement('input');
input.setAttribute('placeholder', 'name');
input.setAttribute('tsaction', 'input.0');
label.appendChild(input);
return document;
})();
const TESTS = [];
(function() {
function serializeNaiveInternal(el) {
if (el.nodeType === 3) {
return escape(el.textContent);
} else {
const nodeName = el.nodeName;
let s = '<' + nodeName;
const attributes = el.attributes;
if (attributes !== null) {
for (const [key, value] of attributes) {
s += ' ' + key + '="' + escapeAttr(value) + '"';
}
}
s += '>';
for (let c = el.firstChild; c !== null; c = c.nextSibling) {
s += serializeNaiveInternal(c);
}
s += '</' + nodeName + '>';
return s;
}
}
function escape(s) {
for (var i = 0; i < s.length; ++i) {
switch (s.charCodeAt(i)) {
case 38: // &
case 60: // <
case 62: // >
case 160: // non-breaking space
return s.replace(/[&<>\u00A0]/g, function(c) {
switch(c) {
case '&': return '&amp;';
case '<': return '&lt;';
case '>': return '&gt;';
case '\u00A0': return '&nbsp;';
}
});
default:
break;
}
}
return s;
}
function escapeAttr(s) {
for (var i = 0; i < s.length; ++i) {
switch (s.charCodeAt(i)) {
case 34: // "
case 38: // &
case 160: // non-breaking space
return s.replace(/[&"\u00A0]/g, function(c) {
switch(c) {
case '&': return '&amp;';
case '"': return '&quot;';
case '\u00A0': return '&nbsp;';
}
});
default:
break;
}
}
return s;
}
TESTS.push(function serializeNaive(el) {
return '<!DOCTYPE html>' + serializeNaiveInternal(el);
});
})();
(function() {
// More clever string concatenation to focus on ConsStrings instead of
// intermediate SeqStrings
function serializeCleverInternal(el, s) {
if (el.nodeType === 3) {
return s + escape(el.textContent);
} else {
const nodeName = el.nodeName;
s = s + '<' + nodeName;
const attributes = el.attributes;
if (attributes !== null) {
for (const [key, value] of attributes) {
s = s + ' ' + key + '="' + escapeAttr(value) + '"';
}
}
s = s + '>';
for (let c = el.firstChild; c !== null; c = c.nextSibling) {
s = serializeCleverInternal(c, s);
}
s = s + '</' + nodeName + '>';
return s;
}
}
function escape(s) {
for (var i = 0; i < s.length; ++i) {
switch (s.charCodeAt(i)) {
case 38: // &
case 60: // <
case 62: // >
case 160: // non-breaking space
return s.replace(/[&<>\u00A0]/g, function(c) {
switch(c) {
case '&': return '&amp;';
case '<': return '&lt;';
case '>': return '&gt;';
case '\u00A0': return '&nbsp;';
}
});
default:
break;
}
}
return s;
}
function escapeAttr(s) {
for (var i = 0; i < s.length; ++i) {
switch (s.charCodeAt(i)) {
case 34: // "
case 38: // &
case 160: // non-breaking space
return s.replace(/[&"\u00A0]/g, function(c) {
switch(c) {
case '&': return '&amp;';
case '"': return '&quot;';
case '\u00A0': return '&nbsp;';
}
});
default:
break;
}
}
return s;
}
TESTS.push(function serializeClever(el) {
return serializeCleverInternal(el, '<!DOCTYPE html>');
});
})();
(function() {
function serializeArrayJoinInternal(el, a) {
if (el.nodeType === 3) {
a.push(escape(el.textContent));
} else {
const nodeName = el.nodeName;
a.push('<', nodeName);
const attributes = el.attributes;
if (attributes !== null) {
for (const [key, value] of attributes) {
a.push(' ', key, '="', escapeAttr(value), '"');
}
}
a.push('>');
for (let c = el.firstChild; c !== null; c = c.nextSibling) {
serializeArrayJoinInternal(c, a);
}
a.push('</', nodeName, '>');
return a;
}
}
function escape(s) {
for (var i = 0; i < s.length; ++i) {
switch (s.charCodeAt(i)) {
case 38: // &
case 60: // <
case 62: // >
case 160: // non-breaking space
return s.replace(/[&<>\u00A0]/g, function(c) {
switch(c) {
case '&': return '&amp;';
case '<': return '&lt;';
case '>': return '&gt;';
case '\u00A0': return '&nbsp;';
}
});
default:
break;
}
}
return s;
}
function escapeAttr(s) {
for (var i = 0; i < s.length; ++i) {
switch (s.charCodeAt(i)) {
case 34: // "
case 38: // &
case 160: // non-breaking space
return s.replace(/[&"\u00A0]/g, function(c) {
switch(c) {
case '&': return '&amp;';
case '"': return '&quot;';
case '\u00A0': return '&nbsp;';
}
});
default:
break;
}
}
return s;
}
TESTS.push(function serializeArrayJoin(el) {
return serializeArrayJoinInternal(el, ['<!DOCTYPE html>']).join('');
});
})();
const EXPECTED = '<!DOCTYPE html><html><head></head><body><app-root><h1>Tour of Heroes</h1><app-heroes><h2>Windstorm Details</h2><div><span>id: </span>1</div><div><label>name: <input placeholder="name" tsaction="input.0"></input></label></div></app-heroes></app-root></body></html>';
for (const serialize of TESTS) {
if (serialize(document.documentElement) !== EXPECTED) {
throw new Error(`Incorrect ${serialize.name}`);
}
}
function driver(fn, n = 1e7) {
let result;
for (let i = 0; i < n; ++i) {
result = fn(document.documentElement).charCodeAt(0);
}
return result;
}
TESTS.forEach(fn => driver(fn, 1e5));
TESTS.forEach(fn => {
const startTime = Date.now();
driver(fn);
console.log(`${fn.name}: ${Date.now() - startTime} ms.`);
});
// Copyright 2013-2019 Benedikt Meurer
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// <https://www.apache.org/licenses/LICENSE-2.0>
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
"use strict";
if (typeof console === 'undefined') console = {log:print};
const createDocument = (function() {
/*
const NODE_TYPES = {
ELEMENT_NODE: 1,
ATTRIBUTE_NODE: 2,
TEXT_NODE: 3,
CDATA_SECTION_NODE: 4,
ENTITY_REFERENCE_NODE: 5,
COMMENT_NODE: 6,
PROCESSING_INSTRUCTION_NODE: 7,
DOCUMENT_NODE: 9
};
*/
class Node {
constructor(nodeType, nodeName) {
this.nodeType = nodeType;
this.nodeName = nodeName;
this.nextSibling = null;
this.previousSibling = null;
this.parentNode = null;
}
appendChild(child) {
this.insertBefore(child, null);
return child;
}
insertBefore(child, ref) {
child.remove();
child.parentNode = this;
if (this.firstChild === null) {
this.firstChild = this.lastChild = child;
} else if (ref === null) {
child.previousSibling = this.lastChild;
this.lastChild.nextSibling = child;
this.lastChild = child;
} else {
const refPreviousSibling = ref.previousSibling;
if (refPreviousSibling !== null) {
child.previousSibling = refPreviousSibling;
refPreviousSibling.nextSibling = child;
} else {
this.firstChild = child;
}
ref.previousSibling = child;
child.nextSibling = ref;
}
return child;
}
removeChild(child) {
const childNextSibling = child.nextSibling;
const childPreviousSibling = child.previousSibling;
if (childNextSibling !== null) {
childNextSibling.previousSibling = childPreviousSibling;
} else {
this.lastChild = childPreviousSibling;
}
if (childPreviousSibling !== null) {
childPreviousSibling.nextSibling = childNextSibling;
} else {
this.firstChild = childNextSibling;
}
child.parentNode = null;
child.nextSibling = null;
child.previousSibling = null;
return child;
}
remove() {
const parentNode = this.parentNode;
if (parentNode !== null) {
parentNode.removeChild(this);
}
}
}
class Text extends Node {
constructor(text) {
super(3, '#text'); // TEXT_NODE
this.textContent = text;
}
}
// TODO(bmeurer): Figure out how to do the jsaction event buffering support.
let eventIndex = 0;
class Element extends Node {
constructor(nodeType, nodeName) {
super(nodeType, nodeName); // ELEMENT_NODE
this.firstChild = null;
this.lastChild = null;
this.attributes = null;
}
setAttribute(key, value) {
let attributes = this.attributes;
if (attributes === null) {
this.attributes = attributes = new Map();
}
attributes.set(key, value);
}
getAttribute(key) {
const attributes = this.attributes;
if (attributes === null) return null;
return attributes.get(key);
}
removeAttribute(key) {
const attributes = this.attributes;
if (attributes !== null) attributes.delete(key);
}
addEventListener(type, handler) {
let val = this.getAttribute('jsaction') || '';
if (val) {
val += ';';
}
let typeStr = type === 'click' ? '' : type + ':';
val += `${typeStr}${this.nodeName}.${eventIndex++}`;
this.setAttribute('tsaction', val);
// (this.__handlers[toLower(type)] || (this.__handlers[toLower(type)] = [])).push(handler);
}
removeEventListener(type, handler) {
// TODO(bmeurer)
}
toString() {
return serialize(this);
}
}
class Document extends Element {
constructor() {
super(9, '#document'); // DOCUMENT_NODE
}
get document() { return this; }
createElement(type) {
return new Element(1, type);
}
createTextNode(text) {
return new Text(text);
}
}
/** Create a minimally viable DOM Document
* @returns {Document} document
*/
return function createDocument() {
let document = new Document();
document.appendChild(
document.documentElement = document.createElement('html')
);
document.documentElement.appendChild(
document.head = document.createElement('head')
);
document.documentElement.appendChild(
document.body = document.createElement('body')
);
return document;
}
})();
const document = (function() {
const document = createDocument();
const appRoot = document.createElement('app-root');
document.body.appendChild(appRoot);
const h1 = document.createElement('h1');
h1.appendChild(document.createTextNode('Tour of Heroes'));
appRoot.appendChild(h1);
const appHeroes = document.createElement('app-heroes');
appRoot.appendChild(appHeroes);
const h2 = document.createElement('h2');
h2.appendChild(document.createTextNode('Windstorm Details'));
appHeroes.appendChild(h2);
let div = document.createElement('div');
appHeroes.appendChild(div);
let span = document.createElement('span');
span.appendChild(document.createTextNode('id: '));
div.appendChild(span);
div.appendChild(document.createTextNode('1'));
div = document.createElement('div');
appHeroes.appendChild(div);
const label = document.createElement('label');
label.appendChild(document.createTextNode('name: '));
div.appendChild(label);
const input = document.createElement('input');
input.setAttribute('placeholder', 'name');
input.setAttribute('tsaction', 'input.0');
label.appendChild(input);
return document;
})();
const TESTS = [];
(function() {
function serializeNaiveInternal(el) {
if (el.nodeType === 3) {
return escape(el.textContent);
} else {
const nodeName = el.nodeName;
let s = '<' + nodeName;
const attributes = el.attributes;
if (attributes !== null) {
for (const [key, value] of attributes) {
s += ' ' + key + '="' + escapeAttr(value) + '"';
}
}
s += '>';
for (let c = el.firstChild; c !== null; c = c.nextSibling) {
s += serializeNaiveInternal(c);
}
s += '</' + nodeName + '>';
return s;
}
}
function escape(s) {
for (var i = 0; i < s.length; ++i) {
switch (s.charCodeAt(i)) {
case 38: // &
case 60: // <
case 62: // >
case 160: // non-breaking space
return s.replace(/[&<>\u00A0]/g, function(c) {
switch(c) {
case '&': return '&amp;';
case '<': return '&lt;';
case '>': return '&gt;';
case '\u00A0': return '&nbsp;';
}
});
default:
break;
}
}
return s;
}
function escapeAttr(s) {
for (var i = 0; i < s.length; ++i) {
switch (s.charCodeAt(i)) {
case 34: // "
case 38: // &
case 160: // non-breaking space
return s.replace(/[&"\u00A0]/g, function(c) {
switch(c) {
case '&': return '&amp;';
case '"': return '&quot;';
case '\u00A0': return '&nbsp;';
}
});
default:
break;
}
}
return s;
}
TESTS.push(function serializeNaive(el) {
return '<!DOCTYPE html>' + serializeNaiveInternal(el);
});
})();
(function() {
// More clever string concatenation to focus on ConsStrings instead of
// intermediate SeqStrings
function serializeCleverInternal(el, s) {
if (el.nodeType === 3) {
return s + escape(el.textContent);
} else {
const nodeName = el.nodeName;
s = s + '<' + nodeName;
const attributes = el.attributes;
if (attributes !== null) {
for (const [key, value] of attributes) {
s = s + ' ' + key + '="' + escapeAttr(value) + '"';
}
}
s = s + '>';
for (let c = el.firstChild; c !== null; c = c.nextSibling) {
s = serializeCleverInternal(c, s);
}
return s + '</' + nodeName + '>';
}
}
function escape(s) {
for (var i = 0; i < s.length; ++i) {
switch (s.charCodeAt(i)) {
case 38: // &
case 60: // <
case 62: // >
case 160: // non-breaking space
return s.replace(/[&<>\u00A0]/g, function(c) {
switch(c) {
case '&': return '&amp;';
case '<': return '&lt;';
case '>': return '&gt;';
case '\u00A0': return '&nbsp;';
}
});
default:
break;
}
}
return s;
}
function escapeAttr(s) {
for (var i = 0; i < s.length; ++i) {
switch (s.charCodeAt(i)) {
case 34: // "
case 38: // &
case 160: // non-breaking space
return s.replace(/[&"\u00A0]/g, function(c) {
switch(c) {
case '&': return '&amp;';
case '"': return '&quot;';
case '\u00A0': return '&nbsp;';
}
});
default:
break;
}
}
return s;
}
TESTS.push(function serializeClever(el) {
return serializeCleverInternal(el, '<!DOCTYPE html>');
});
})();
(function() {
function serializeConcatInternal(el, s) {
if (el.nodeType === 3) {
return s.concat(escape(el.textContent));
} else {
const nodeName = el.nodeName;
s = s.concat('<', nodeName);
const attributes = el.attributes;
if (attributes !== null) {
for (const [key, value] of attributes) {
s = s.concat(' ', key, '="', escapeAttr(value), '"');
}
}
s = s.concat('>');
for (let c = el.firstChild; c !== null; c = c.nextSibling) {
s = serializeConcatInternal(c, s);
}
return s.concat('</', nodeName, '>');
}
}
function escape(s) {
for (var i = 0; i < s.length; ++i) {
switch (s.charCodeAt(i)) {
case 38: // &
case 60: // <
case 62: // >
case 160: // non-breaking space
return s.replace(/[&<>\u00A0]/g, function(c) {
switch(c) {
case '&': return '&amp;';
case '<': return '&lt;';
case '>': return '&gt;';
case '\u00A0': return '&nbsp;';
}
});
default:
break;
}
}
return s;
}
function escapeAttr(s) {
for (var i = 0; i < s.length; ++i) {
switch (s.charCodeAt(i)) {
case 34: // "
case 38: // &
case 160: // non-breaking space
return s.replace(/[&"\u00A0]/g, function(c) {
switch(c) {
case '&': return '&amp;';
case '"': return '&quot;';
case '\u00A0': return '&nbsp;';
}
});
default:
break;
}
}
return s;
}
TESTS.push(function serializeConcat(el) {
return serializeConcatInternal(el, '<!DOCTYPE html>');
});
})();
(function() {
function serializeArrayJoinInternal(el, a) {
if (el.nodeType === 3) {
a.push(escape(el.textContent));
} else {
const nodeName = el.nodeName;
a.push('<', nodeName);
const attributes = el.attributes;
if (attributes !== null) {
for (const [key, value] of attributes) {
a.push(' ', key, '="', escapeAttr(value), '"');
}
}
a.push('>');
for (let c = el.firstChild; c !== null; c = c.nextSibling) {
serializeArrayJoinInternal(c, a);
}
a.push('</', nodeName, '>');
return a;
}
}
function escape(s) {
for (var i = 0; i < s.length; ++i) {
switch (s.charCodeAt(i)) {
case 38: // &
case 60: // <
case 62: // >
case 160: // non-breaking space
return s.replace(/[&<>\u00A0]/g, function(c) {
switch(c) {
case '&': return '&amp;';
case '<': return '&lt;';
case '>': return '&gt;';
case '\u00A0': return '&nbsp;';
}
});
default:
break;
}
}
return s;
}
function escapeAttr(s) {
for (var i = 0; i < s.length; ++i) {
switch (s.charCodeAt(i)) {
case 34: // "
case 38: // &
case 160: // non-breaking space
return s.replace(/[&"\u00A0]/g, function(c) {
switch(c) {
case '&': return '&amp;';
case '"': return '&quot;';
case '\u00A0': return '&nbsp;';
}
});
default:
break;
}
}
return s;
}
TESTS.push(function serializeArrayJoin(el) {
return serializeArrayJoinInternal(el, ['<!DOCTYPE html>']).join('');
});
})();
const EXPECTED = '<!DOCTYPE html><html><head></head><body><app-root><h1>Tour of Heroes</h1><app-heroes><h2>Windstorm Details</h2><div><span>id: </span>1</div><div><label>name: <input placeholder="name" tsaction="input.0"></input></label></div></app-heroes></app-root></body></html>';
for (const serialize of TESTS) {
if (serialize(document.documentElement) !== EXPECTED) {
throw new Error(`Incorrect ${serialize.name}`);
}
}
function driver(fn, flatten, n = 1e7) {
let result;
if (flatten) {
for (let i = 0; i < n; ++i) {
result = fn(document.documentElement).charCodeAt(0);
}
} else {
for (let i = 0; i < n; ++i) {
result = fn(document.documentElement);
}
}
return result;
}
TESTS.forEach(fn => driver(fn, false, 1e5));
TESTS.forEach(fn => driver(fn, true, 1e5));
TESTS.forEach(fn => {
const startTime = Date.now();
driver(fn, false);
console.log(`${fn.name}: ${Date.now() - startTime} ms.`);
});
TESTS.forEach(fn => {
const startTime = Date.now();
driver(fn, true);
console.log(`${fn.name}Flatten: ${Date.now() - startTime} ms.`);
});
// Copyright 2013-2019 Benedikt Meurer
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// <https://www.apache.org/licenses/LICENSE-2.0>
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
"use strict";
if (typeof console === 'undefined') console = {log:print};
const createDocument = (function() {
/*
const NODE_TYPES = {
ELEMENT_NODE: 1,
ATTRIBUTE_NODE: 2,
TEXT_NODE: 3,
CDATA_SECTION_NODE: 4,
ENTITY_REFERENCE_NODE: 5,
COMMENT_NODE: 6,
PROCESSING_INSTRUCTION_NODE: 7,
DOCUMENT_NODE: 9
};
*/
class Node {
constructor(nodeType, nodeName) {
this.nodeType = nodeType;
this.nodeName = nodeName;
this.nextSibling = null;
this.previousSibling = null;
this.parentNode = null;
}
appendChild(child) {
this.insertBefore(child, null);
return child;
}
insertBefore(child, ref) {
child.remove();
child.parentNode = this;
if (this.firstChild === null) {
this.firstChild = this.lastChild = child;
} else if (ref === null) {
child.previousSibling = this.lastChild;
this.lastChild.nextSibling = child;
this.lastChild = child;
} else {
const refPreviousSibling = ref.previousSibling;
if (refPreviousSibling !== null) {
child.previousSibling = refPreviousSibling;
refPreviousSibling.nextSibling = child;
} else {
this.firstChild = child;
}
ref.previousSibling = child;
child.nextSibling = ref;
}
return child;
}
removeChild(child) {
const childNextSibling = child.nextSibling;
const childPreviousSibling = child.previousSibling;
if (childNextSibling !== null) {
childNextSibling.previousSibling = childPreviousSibling;
} else {
this.lastChild = childPreviousSibling;
}
if (childPreviousSibling !== null) {
childPreviousSibling.nextSibling = childNextSibling;
} else {
this.firstChild = childNextSibling;
}
child.parentNode = null;
child.nextSibling = null;
child.previousSibling = null;
return child;
}
remove() {
const parentNode = this.parentNode;
if (parentNode !== null) {
parentNode.removeChild(this);
}
}
}
class Text extends Node {
constructor(text) {
super(3, '#text'); // TEXT_NODE
this.textContent = text;
}
}
// TODO(bmeurer): Figure out how to do the jsaction event buffering support.
let eventIndex = 0;
class Element extends Node {
constructor(nodeType, nodeName) {
super(nodeType, nodeName); // ELEMENT_NODE
this.firstChild = null;
this.lastChild = null;
this.attributes = null;
}
setAttribute(key, value) {
let attributes = this.attributes;
if (attributes === null) {
this.attributes = attributes = new Map();
}
attributes.set(key, value);
}
getAttribute(key) {
const attributes = this.attributes;
if (attributes === null) return null;
return attributes.get(key);
}
removeAttribute(key) {
const attributes = this.attributes;
if (attributes !== null) attributes.delete(key);
}
addEventListener(type, handler) {
let val = this.getAttribute('jsaction') || '';
if (val) {
val += ';';
}
let typeStr = type === 'click' ? '' : type + ':';
val += `${typeStr}${this.nodeName}.${eventIndex++}`;
this.setAttribute('tsaction', val);
// (this.__handlers[toLower(type)] || (this.__handlers[toLower(type)] = [])).push(handler);
}
removeEventListener(type, handler) {
// TODO(bmeurer)
}
toString() {
return serialize(this);
}
}
class Document extends Element {
constructor() {
super(9, '#document'); // DOCUMENT_NODE
}
get document() { return this; }
createElement(type) {
return new Element(1, type);
}
createTextNode(text) {
return new Text(text);
}
}
/** Create a minimally viable DOM Document
* @returns {Document} document
*/
return function createDocument() {
let document = new Document();
document.appendChild(
document.documentElement = document.createElement('html')
);
document.documentElement.appendChild(
document.head = document.createElement('head')
);
document.documentElement.appendChild(
document.body = document.createElement('body')
);
return document;
}
})();
const document = (function() {
const document = createDocument();
const appRoot = document.createElement('app-root');
document.body.appendChild(appRoot);
const h1 = document.createElement('h1');
h1.appendChild(document.createTextNode('Tour of Heroes'));
appRoot.appendChild(h1);
const appHeroes = document.createElement('app-heroes');
appRoot.appendChild(appHeroes);
const h2 = document.createElement('h2');
h2.appendChild(document.createTextNode('Windstorm Details'));
appHeroes.appendChild(h2);
let div = document.createElement('div');
appHeroes.appendChild(div);
let span = document.createElement('span');
span.appendChild(document.createTextNode('id: '));
div.appendChild(span);
div.appendChild(document.createTextNode('1'));
div = document.createElement('div');
appHeroes.appendChild(div);
const label = document.createElement('label');
label.appendChild(document.createTextNode('name: '));
div.appendChild(label);
const input = document.createElement('input');
input.setAttribute('placeholder', 'name');
input.setAttribute('tsaction', 'input.0');
label.appendChild(input);
return document;
})();
const TESTS = [];
(function() {
function serializeNaiveInternal(el) {
if (el.nodeType === 3) {
return escape(el.textContent);
} else {
const nodeName = el.nodeName;
let s = '<' + nodeName;
const attributes = el.attributes;
if (attributes !== null) {
for (const [key, value] of attributes) {
s += ' ' + key + '="' + escapeAttr(value) + '"';
}
}
s += '>';
for (let c = el.firstChild; c !== null; c = c.nextSibling) {
s += serializeNaiveInternal(c);
}
s += '</' + nodeName + '>';
return s;
}
}
function escape(s) {
for (var i = 0; i < s.length; ++i) {
switch (s.charCodeAt(i)) {
case 38: // &
case 60: // <
case 62: // >
case 160: // non-breaking space
return s.replace(/[&<>\u00A0]/g, function(c) {
switch(c) {
case '&': return '&amp;';
case '<': return '&lt;';
case '>': return '&gt;';
case '\u00A0': return '&nbsp;';
}
});
default:
break;
}
}
return s;
}
function escapeAttr(s) {
for (var i = 0; i < s.length; ++i) {
switch (s.charCodeAt(i)) {
case 34: // "
case 38: // &
case 160: // non-breaking space
return s.replace(/[&"\u00A0]/g, function(c) {
switch(c) {
case '&': return '&amp;';
case '"': return '&quot;';
case '\u00A0': return '&nbsp;';
}
});
default:
break;
}
}
return s;
}
TESTS.push(function serializeNaive(el) {
return '<!DOCTYPE html>' + serializeNaiveInternal(el);
});
})();
(function() {
// More clever string concatenation to focus on ConsStrings instead of
// intermediate SeqStrings
function serializeCleverInternal(el, s) {
if (el.nodeType === 3) {
return s + escape(el.textContent);
} else {
const nodeName = el.nodeName;
s = s + '<' + nodeName;
const attributes = el.attributes;
if (attributes !== null) {
for (const [key, value] of attributes) {
s = s + ' ' + key + '="' + escapeAttr(value) + '"';
}
}
s = s + '>';
for (let c = el.firstChild; c !== null; c = c.nextSibling) {
s = serializeCleverInternal(c, s);
}
s = s + '</' + nodeName + '>';
return s;
}
}
function escape(s) {
for (var i = 0; i < s.length; ++i) {
switch (s.charCodeAt(i)) {
case 38: // &
case 60: // <
case 62: // >
case 160: // non-breaking space
return s.replace(/[&<>\u00A0]/g, function(c) {
switch(c) {
case '&': return '&amp;';
case '<': return '&lt;';
case '>': return '&gt;';
case '\u00A0': return '&nbsp;';
}
});
default:
break;
}
}
return s;
}
function escapeAttr(s) {
for (var i = 0; i < s.length; ++i) {
switch (s.charCodeAt(i)) {
case 34: // "
case 38: // &
case 160: // non-breaking space
return s.replace(/[&"\u00A0]/g, function(c) {
switch(c) {
case '&': return '&amp;';
case '"': return '&quot;';
case '\u00A0': return '&nbsp;';
}
});
default:
break;
}
}
return s;
}
TESTS.push(function serializeClever(el) {
return serializeCleverInternal(el, '<!DOCTYPE html>');
});
})();
(function() {
function serializeArrayJoinInternal(el, a) {
if (el.nodeType === 3) {
a.push(escape(el.textContent));
} else {
const nodeName = el.nodeName;
a.push('<', nodeName);
const attributes = el.attributes;
if (attributes !== null) {
for (const [key, value] of attributes) {
a.push(' ', key, '="', escapeAttr(value), '"');
}
}
a.push('>');
for (let c = el.firstChild; c !== null; c = c.nextSibling) {
serializeArrayJoinInternal(c, a);
}
a.push('</', nodeName, '>');
return a;
}
}
function escape(s) {
for (var i = 0; i < s.length; ++i) {
switch (s.charCodeAt(i)) {
case 38: // &
case 60: // <
case 62: // >
case 160: // non-breaking space
return s.replace(/[&<>\u00A0]/g, function(c) {
switch(c) {
case '&': return '&amp;';
case '<': return '&lt;';
case '>': return '&gt;';
case '\u00A0': return '&nbsp;';
}
});
default:
break;
}
}
return s;
}
function escapeAttr(s) {
for (var i = 0; i < s.length; ++i) {
switch (s.charCodeAt(i)) {
case 34: // "
case 38: // &
case 160: // non-breaking space
return s.replace(/[&"\u00A0]/g, function(c) {
switch(c) {
case '&': return '&amp;';
case '"': return '&quot;';
case '\u00A0': return '&nbsp;';
}
});
default:
break;
}
}
return s;
}
TESTS.push(function serializeArrayJoin(el) {
return serializeArrayJoinInternal(el, ['<!DOCTYPE html>']).join('');
});
})();
const EXPECTED = '<!DOCTYPE html><html><head></head><body><app-root><h1>Tour of Heroes</h1><app-heroes><h2>Windstorm Details</h2><div><span>id: </span>1</div><div><label>name: <input placeholder="name" tsaction="input.0"></input></label></div></app-heroes></app-root></body></html>';
for (const serialize of TESTS) {
if (serialize(document.documentElement) !== EXPECTED) {
throw new Error(`Incorrect ${serialize.name}`);
}
}
function driver(fn, n = 1e7) {
let result;
for (let i = 0; i < n; ++i) {
result = fn(document.documentElement);
}
return result;
}
TESTS.forEach(fn => driver(fn, 1e5));
TESTS.forEach(fn => {
const startTime = Date.now();
driver(fn);
console.log(`${fn.name}: ${Date.now() - startTime} ms.`);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment