-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathtag.js
More file actions
149 lines (132 loc) · 4 KB
/
tag.js
File metadata and controls
149 lines (132 loc) · 4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import {set} from "./attr.js";
const TYPE_NODE = 1;
const TYPE_TEXT = 3;
const isFunc = (x) => typeof x === "function";
const isNode = (x) => x?.nodeType;
const isArray = Array.isArray;
const isPlainObject = (x) => typeof x === "object" && x !== null && !Array.isArray(x);
function postprocess(node) {
if (node.firstChild === node.lastChild) {
return node.firstChild;
}
return node;
}
function cloneNode(node) {
const cloned = node.cloneNode(true);
cloned.__context__ = node.__context__;
return cloned;
}
function contextOf(node) {
let p = node.parentNode;
while (p) {
if (p.__context__) return p.__context__;
p = p.parentNode;
}
return null;
}
function hydrate(node, value) {
if (!isPlainObject(value)) return;
const {data, ...attrs} = value;
const context = contextOf(node);
// Non data driven node.
if (!data && !context) {
for (const [k, v] of Object.entries(attrs)) {
const val = k.startsWith("on") ? v : isFunc(v) ? v(node) : v;
set(node, k, val);
}
return;
}
// Non data driven node in a data driven parent.
if (!data && context) {
for (const [k, v] of Object.entries(attrs)) {
const [d, i, array] = context;
if (k.startsWith("on")) {
const [l, o] = Array.isArray(v) ? v : [v];
const val = (e) => l(e, node, d, i, array);
set(node, k, [val, o]);
} else {
const val = isFunc(v) ? v(d, i, array, node) : v;
set(node, k, val);
}
}
return;
}
// Data driven node.
const nodeData = context && isFunc(data) ? data(context[0], context[1], context[2], node) : data;
const nodes = nodeData.map((d, i, array) => {
const cloned = cloneNode(node);
for (const [k, v] of Object.entries(attrs)) {
if (k.startsWith("on")) {
const [l, o] = Array.isArray(v) ? v : [v];
const val = (e) => l(e, cloned, d, i, array);
set(cloned, k, [val, o]);
} else {
const val = isFunc(v) ? v(d, i, array, node) : v;
set(cloned, k, val);
}
}
cloned.__context__ = [d, i, array, cloned];
return cloned;
});
const temp = document.createDocumentFragment();
temp.append(...nodes);
node.parentNode.insertBefore(temp, node.nextSibling);
return node;
}
function append(node, value) {
const children = [isArray(value) ? value : [value]].flat(Infinity);
const temp = document.createDocumentFragment();
const c = contextOf(node);
for (const child of children) {
const n = isFunc(child) && c ? child(c[0], c[1], c[2]) : child;
if (isNode(n)) temp.append(n);
}
node.parentNode.insertBefore(temp, node);
}
export function tag(render) {
return function ({raw: strings}) {
let string = "";
for (let j = 0, m = strings.length; j < m; j++) {
const input = strings[j];
if (j > 0) string += "::" + j;
string += input;
}
const root = render(string);
const walker = document.createTreeWalker(root); // DFS walker.
const removeNodes = [];
while (walker.nextNode()) {
const node = walker.currentNode;
switch (node.nodeType) {
case TYPE_NODE: {
const attributes = node.attributes;
for (let i = 0, n = attributes.length; i < n; i++) {
const {name} = attributes[i];
if (/^::/.test(name)) {
const value = arguments[+name.slice(2)];
node.removeAttribute(name);
const removed = hydrate(node, value);
if (removed) {
removeNodes.push(removed);
walker.nextSibling(); // Skip the children of the removed node.
}
}
}
break;
}
case TYPE_TEXT: {
const data = node.data.trim();
if (/^::/.test(data)) {
const value = arguments[+data.slice(2)];
append(node, value);
removeNodes.push(node);
}
break;
}
default:
break;
}
}
for (const node of removeNodes) node.remove();
return postprocess(root);
};
}