More tests, more bugs, more fixes
This commit is contained in:
parent
a8fd5aa2fe
commit
626194fc99
|
@ -58,10 +58,31 @@ function followPath(topNodes: ChildNode[], path: number[]): Node {
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlaceholderAction = (variableParts: HtmlFragment[], node: Node) => void;
|
type PlaceholderState = { [key: string]: any };
|
||||||
|
|
||||||
function nodeInserter(n: number): PlaceholderAction {
|
interface PlaceholderAction {
|
||||||
return (vs, node) => {
|
reset(state: PlaceholderState, node: Node): void;
|
||||||
|
act(state: PlaceholderState, variableParts: HtmlFragment[], node: Node): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NodeInserter implements PlaceholderAction {
|
||||||
|
constructor(public variablePartIndex: number) {}
|
||||||
|
|
||||||
|
reset(state: PlaceholderState, node: Node) {
|
||||||
|
if ('nodeCount' in state) {
|
||||||
|
const previouslyInsertedNodes = state.nodeCount as number;
|
||||||
|
node.parentNode?.insertBefore(makePlaceholder(), node);
|
||||||
|
for (let n = 0; n < previouslyInsertedNodes; n++) {
|
||||||
|
const nextNode = node.nextSibling;
|
||||||
|
node.parentNode?.removeChild(node);
|
||||||
|
if (nextNode === null) break;
|
||||||
|
node = nextNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.nodeCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
act(state: PlaceholderState, vs: HtmlFragment[], node: Node): void {
|
||||||
function walk(f: HtmlFragment) {
|
function walk(f: HtmlFragment) {
|
||||||
if (Array.isArray(f)) {
|
if (Array.isArray(f)) {
|
||||||
f.forEach(walk);
|
f.forEach(walk);
|
||||||
|
@ -88,35 +109,80 @@ function nodeInserter(n: number): PlaceholderAction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
node.parentNode?.insertBefore(newNode, node);
|
node.parentNode?.insertBefore(newNode, node);
|
||||||
|
state.nodeCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
walk(vs[n]);
|
walk(vs[this.variablePartIndex]);
|
||||||
node.parentNode?.removeChild(node);
|
node.parentNode?.removeChild(node);
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function attributesInserter(n: number): PlaceholderAction {
|
class AttributesInserter implements PlaceholderAction {
|
||||||
return (vs, node) => {
|
constructor(public variablePartIndex: number) {}
|
||||||
|
|
||||||
|
reset(state: PlaceholderState, node: Node): void {
|
||||||
|
const attrNames: string[] = (state.attrNames ?? []);
|
||||||
|
attrNames.forEach(n => (node as Element).removeAttribute(n));
|
||||||
|
state.attrNames = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
act(state: PlaceholderState, vs: HtmlFragment[], node: Node): void {
|
||||||
const e = document.createElement('template');
|
const e = document.createElement('template');
|
||||||
|
const n = this.variablePartIndex;
|
||||||
e.innerHTML = `<x-dummy ${renderFragment(vs[n], false).join('')}></x-dummy>`;
|
e.innerHTML = `<x-dummy ${renderFragment(vs[n], false).join('')}></x-dummy>`;
|
||||||
Array.from(e.content.firstElementChild!.attributes).forEach(a =>
|
Array.from(e.content.firstElementChild!.attributes).forEach(a => {
|
||||||
(node as Element).setAttribute(a.name, a.value));
|
state.attrNames.push(a.name);
|
||||||
};
|
(node as Element).setAttribute(a.name, a.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function attributeValueInserter(
|
class AttributeValueInserter implements PlaceholderAction {
|
||||||
attrName: string,
|
constructor(
|
||||||
constantParts: string[],
|
public attrName: string,
|
||||||
placeholders: number[],
|
public constantParts: string[],
|
||||||
): PlaceholderAction {
|
public placeholders: number[],
|
||||||
return (vs, node) => {
|
) {}
|
||||||
const pieces = [constantParts[0]];
|
|
||||||
placeholders.forEach((n, i) => {
|
reset(_state: PlaceholderState, _node: Node): void {}
|
||||||
|
|
||||||
|
act(_state: PlaceholderState, vs: HtmlFragment[], node: Node): void {
|
||||||
|
const pieces = [this.constantParts[0]];
|
||||||
|
this.placeholders.forEach((n, i) => {
|
||||||
pieces.push(...renderFragment(vs[n], false));
|
pieces.push(...renderFragment(vs[n], false));
|
||||||
pieces.push(constantParts[i + 1]);
|
pieces.push(this.constantParts[i + 1]);
|
||||||
});
|
});
|
||||||
(node as Element).setAttribute(attrName, pieces.join(''));
|
(node as Element).setAttribute(this.attrName, pieces.join(''));
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CompoundAction implements PlaceholderAction {
|
||||||
|
constructor(public actions: PlaceholderAction[]) {}
|
||||||
|
|
||||||
|
reset(states: PlaceholderState, n: Node) {
|
||||||
|
this.actions.forEach((a, i) => {
|
||||||
|
const state = states[i] ??= {};
|
||||||
|
a.reset(state, n);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
act(states: PlaceholderState, vs: HtmlFragment[], n: Node) {
|
||||||
|
this.actions.forEach((a, i) => {
|
||||||
|
const state = states[i] ??= {};
|
||||||
|
a.act(state, vs, n);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HtmlFragmentInstance {
|
||||||
|
state: { [path: string]: PlaceholderState } = {};
|
||||||
|
constructor(
|
||||||
|
public container: ChildNode[],
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makePlaceholder(): Element {
|
||||||
|
return document.createElement('x-placeholder');
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HtmlFragmentBuilder {
|
export class HtmlFragmentBuilder {
|
||||||
|
@ -138,8 +204,7 @@ export class HtmlFragmentBuilder {
|
||||||
splitByPlaceholders(n.textContent ?? '');
|
splitByPlaceholders(n.textContent ?? '');
|
||||||
constantParts.forEach((c, i) => {
|
constantParts.forEach((c, i) => {
|
||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
const placeholder = document.createElement('x-placeholder');
|
n.parentNode?.insertBefore(makePlaceholder(), n);
|
||||||
n.parentNode?.insertBefore(placeholder, n);
|
|
||||||
}
|
}
|
||||||
n.parentNode?.insertBefore(document.createTextNode(c), n);
|
n.parentNode?.insertBefore(document.createTextNode(c), n);
|
||||||
});
|
});
|
||||||
|
@ -147,10 +212,10 @@ export class HtmlFragmentBuilder {
|
||||||
n.parentNode?.removeChild(n);
|
n.parentNode?.removeChild(n);
|
||||||
placeholders.forEach((n, i) => {
|
placeholders.forEach((n, i) => {
|
||||||
const currentPath = path.slice();
|
const currentPath = path.slice();
|
||||||
currentPath[currentPath.length - 1] = i * 2 + 1;
|
currentPath[currentPath.length - 1] += i * 2 + 1;
|
||||||
this.placeholderActions.push({
|
this.placeholderActions.push({
|
||||||
path: currentPath,
|
path: currentPath,
|
||||||
action: nodeInserter(n),
|
action: new NodeInserter(n),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
path[path.length - 1] += constantParts.length + placeholders.length;
|
path[path.length - 1] += constantParts.length + placeholders.length;
|
||||||
|
@ -167,12 +232,12 @@ export class HtmlFragmentBuilder {
|
||||||
e.removeAttributeNode(attr);
|
e.removeAttributeNode(attr);
|
||||||
i--;
|
i--;
|
||||||
const n = parseInt(nameIsPlaceholder[1], 10);
|
const n = parseInt(nameIsPlaceholder[1], 10);
|
||||||
actions.push(attributesInserter(n));
|
actions.push(new AttributesInserter(n));
|
||||||
} else {
|
} else {
|
||||||
const { constantParts, placeholders } =
|
const { constantParts, placeholders } =
|
||||||
splitByPlaceholders(attr.value);
|
splitByPlaceholders(attr.value);
|
||||||
if (constantParts.length !== 1) {
|
if (constantParts.length !== 1) {
|
||||||
actions.push(attributeValueInserter(
|
actions.push(new AttributeValueInserter(
|
||||||
attrName,
|
attrName,
|
||||||
constantParts,
|
constantParts,
|
||||||
placeholders));
|
placeholders));
|
||||||
|
@ -182,7 +247,7 @@ export class HtmlFragmentBuilder {
|
||||||
if (actions.length) {
|
if (actions.length) {
|
||||||
this.placeholderActions.push({
|
this.placeholderActions.push({
|
||||||
path: path.slice(),
|
path: path.slice(),
|
||||||
action: (vs, n) => actions.forEach(a => a(vs, n)),
|
action: actions.length === 1 ? actions[0] : new CompoundAction(actions),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -217,14 +282,19 @@ export class HtmlFragmentBuilder {
|
||||||
walk(this.template.content);
|
walk(this.template.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
clone(): ChildNode[] {
|
clone(): HtmlFragmentInstance {
|
||||||
return Array.from(
|
return new HtmlFragmentInstance(Array.from(
|
||||||
(this.template.cloneNode(true) as HTMLTemplateElement).content.childNodes);
|
(this.template.cloneNode(true) as HTMLTemplateElement).content.childNodes));
|
||||||
}
|
}
|
||||||
|
|
||||||
update(template: ChildNode[], variableParts: Array<HtmlFragment>) {
|
update(template: HtmlFragmentInstance, variableParts: Array<HtmlFragment>) {
|
||||||
this.placeholderActions.forEach(({ path, action }) => {
|
this.placeholderActions.forEach(({ path, action }) => {
|
||||||
action(variableParts, followPath(template, path));
|
const state = template.state[path.map(n => '' + n).join('.')] ??= {};
|
||||||
|
action.reset(state, followPath(template.container, path));
|
||||||
|
});
|
||||||
|
this.placeholderActions.forEach(({ path, action }) => {
|
||||||
|
const state = template.state[path.map(n => '' + n).join('.')] ??= {};
|
||||||
|
action.act(state, variableParts, followPath(template.container, path));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -238,17 +308,17 @@ export type HtmlTemplater =
|
||||||
=> ChildNode[];
|
=> ChildNode[];
|
||||||
|
|
||||||
export function template(): HtmlTemplater {
|
export function template(): HtmlTemplater {
|
||||||
let container: ChildNode[] | null = null;
|
let instance: HtmlFragmentInstance | null = null;
|
||||||
return (constantParts, ... variableParts) => {
|
return (constantParts, ... variableParts) => {
|
||||||
let b = templateCache.get(constantParts);
|
let b = templateCache.get(constantParts);
|
||||||
if (b === void 0) {
|
if (b === void 0) {
|
||||||
b = new HtmlFragmentBuilder(constantParts);
|
b = new HtmlFragmentBuilder(constantParts);
|
||||||
templateCache.set(constantParts, b);
|
templateCache.set(constantParts, b);
|
||||||
}
|
}
|
||||||
if (container === null) {
|
if (instance === null) {
|
||||||
container = b.clone();
|
instance = b.clone();
|
||||||
}
|
}
|
||||||
b.update(container, variableParts);
|
b.update(instance, variableParts);
|
||||||
return container;
|
return instance.container;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,11 @@ describe('basic templating', () => {
|
||||||
compareHTML(template()`<x>${y}</x>`, '<x>abc</x>');
|
compareHTML(template()`<x>${y}</x>`, '<x>abc</x>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should substitute multiple strings', () => {
|
||||||
|
const y = 'abc';
|
||||||
|
compareHTML(template()`<x>${y} ${y} ${y}</x>`, '<x>abc abc abc</x>');
|
||||||
|
});
|
||||||
|
|
||||||
it('should substitute a node', () => {
|
it('should substitute a node', () => {
|
||||||
const y = template()`<z>q</z>`;
|
const y = template()`<z>q</z>`;
|
||||||
compareHTML(template()`<x>${y}</x>`, '<x><z>q</z></x>');
|
compareHTML(template()`<x>${y}</x>`, '<x><z>q</z></x>');
|
||||||
|
@ -41,4 +46,57 @@ describe('basic templating', () => {
|
||||||
compareHTML(template()`<q><x t="${v()}">${ps.map(p => p())}</x></q>`,
|
compareHTML(template()`<q><x t="${v()}">${ps.map(p => p())}</x></q>`,
|
||||||
'<q><x t="aaa">123234</x></q>');
|
'<q><x t="aaa">123234</x></q>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should substitute into attributes, with children', () => {
|
||||||
|
const v = () => 'aaa';
|
||||||
|
const ps = [() => '123', () => '234'];
|
||||||
|
const f = 'z';
|
||||||
|
compareHTML(template()`<q><x t="${v()}">${f}</x>${ps.map(p => p())}</q>`,
|
||||||
|
'<q><x t="aaa">z</x>123234</q>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should substitute into attributes, with children and whitespace', () => {
|
||||||
|
const v = () => 'aaa';
|
||||||
|
const ps = [() => '123', () => '234'];
|
||||||
|
const f = 'z';
|
||||||
|
compareHTML(template()`<q><x ttt="${v()}">${f}</x>
|
||||||
|
${ps.map(p => p())}
|
||||||
|
</q>`,
|
||||||
|
`<q><x ttt="aaa">z</x>
|
||||||
|
123234
|
||||||
|
</q>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('example from paradise.js', () => {
|
||||||
|
const pieces = ['C', 'D'];
|
||||||
|
compareHTML(template()`<p class="event">
|
||||||
|
<span class="timestamp" title="${'A'}">${'B'}</span>
|
||||||
|
${pieces.map(p => p)}
|
||||||
|
</p>`,
|
||||||
|
`<p class="event">
|
||||||
|
<span class="timestamp" title="A">B</span>
|
||||||
|
CD
|
||||||
|
</p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be callable multiple times', () => {
|
||||||
|
const v = () => 'aaa';
|
||||||
|
const ps = [() => '123', () => '234', () => '345'];
|
||||||
|
const expected = '<q><x t="aaa">123234345</x></q>';
|
||||||
|
const t = template();
|
||||||
|
compareHTML(t`<q><x t="${v()}">${ps.map(p => p())}</x></q>`, expected);
|
||||||
|
compareHTML(t`<q><x t="${v()}">${ps.map(p => p())}</x></q>`, expected);
|
||||||
|
compareHTML(t`<q><x t="${v()}">${ps.map(p => p())}</x></q>`, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be callable multiple times with extra items', () => {
|
||||||
|
const v = () => 'aaa';
|
||||||
|
const t = template();
|
||||||
|
const ps = [() => '1'];
|
||||||
|
compareHTML(t`<q><x t="${v()}">${ps.map(p => p())}</x></q>`, '<q><x t="aaa">1</x></q>');
|
||||||
|
ps.push(() => '2');
|
||||||
|
compareHTML(t`<q><x t="${v()}">${ps.map(p => p())}</x></q>`, '<q><x t="aaa">12</x></q>');
|
||||||
|
ps.push(() => '3');
|
||||||
|
compareHTML(t`<q><x t="${v()}">${ps.map(p => p())}</x></q>`, '<q><x t="aaa">123</x></q>');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue