@dom-expressions/tagged-jsx 0.50.0-next.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/LICENSE +21 -0
- package/README.md +259 -0
- package/dist/index.d.mts +12544 -0
- package/dist/index.mjs +2 -0
- package/package.json +32 -0
- package/src/index.ts +2 -0
- package/src/parse.ts +257 -0
- package/src/tagged-jsx.ts +247 -0
- package/src/tokenize.ts +260 -0
- package/src/types.ts +69 -0
- package/tests/core.ts +27 -0
- package/tests/parse.test.ts +971 -0
- package/tests/tagged-jsx.test.ts +1119 -0
- package/tests/tokenize.test.ts +1019 -0
- package/tsconfig.json +15 -0
- package/tsdown.config.mjs +5 -0
- package/vitest.config.ts +13 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=e=>t(e)||e>=48&&e<=58||e===46||e===45,t=e=>e>=65&&e<=90||e>=97&&e<=122||e===95||e===36,n=e=>e>=9&&e<=13||e===32,r=(r,i)=>{let a=[],o=0,s=``,c=0;for(let l=0;l<r.length;l++){let u=r[l],d=u.length;for(c=0;c<d;)switch(o){case 0:{s=``;let e=u.indexOf(`<`,c);e===-1?(c<d&&a.push({type:6,value:u.slice(c)}),c=d):(e>c&&a.push({type:6,value:u.slice(c,e)}),u[e+1]===`!`&&u[e+2]===`-`&&u[e+3]===`-`?(o=3,c=e+4):(a.push({type:0}),o=1,c=e+1));break}case 1:{let r=u.charCodeAt(c);if(n(r))c++;else if(r===62)i.has(s)&&a[a.length-2]?.type!==2?o=2:(o=0,s=``),a.push({type:1}),c++;else if(r===61)a.push({type:4}),c++;else if(r===47){let e=u.charCodeAt(c+1),t=u.slice(c+2).search(/\S/),n=e===47&&a[a.length-1]?.type===0&&t!==-1&&u[c+2+t]===`>`;e===47&&!n?o=4:e===42?o=5:(a.push({type:2}),c++)}else if(r===34||r===39){let e=u[c],t=u.indexOf(e,c+1);if(t===-1)throw Error(`Unterminated string at ${l}:${c}`);a.push({type:5,value:u.slice(c+1,t),quote:e}),c=t+1}else if(t(r)){let t=c;for(;c<d&&e(u.charCodeAt(c));)c++;let n=u.slice(t,c);s===``&&(s=n),a.push({type:3,value:n})}else if(r===46&&u[c+1]===`.`&&u[c+2]===`.`)a.push({type:8}),c+=3;else throw Error(`Unexpected Character: ${u[c]} at ${l}:${c}`);break}case 2:{let e=RegExp(`<\\s*/\\s*${s}\\s*>`,`g`);e.lastIndex=c;let t=e.exec(u);if(t){let e=t.index;e>c&&a.push({type:6,value:u.slice(c,e)}),o=0,c=e,s=``}else a.push({type:6,value:u.slice(c)}),c=d;break}case 3:case 4:case 5:{let e=o===4?`
|
|
2
|
+
`:o===5?`*/`:`-->`,t=u.indexOf(e,c);t===-1?c=d:(o=o===3?0:1,c=t+e.length);break}}l<r.length-1&&(o===0||o===1||o===2)&&a.push({type:7,value:l})}return a},i=e=>{let t=e.charCodeAt(0);return t>=65&&t<=90},a=(e,t)=>{let n={type:0,children:[]},r=[n],a=0,o=e.length;for(;a<o;){let n=e[a],s=r[r.length-1];switch(n.type){case 6:{let t=n.value;if(t.trim()===``){let t=e[a-1]?.type,n=e[a+1]?.type;if(t===1||n===0){a++;continue}}s.children.push({type:3,value:t}),a++;continue}case 7:s.children.push({type:4,value:n.value}),a++;continue;case 0:{let n=e[++a];if(n.type===2){let n=e[++a],i=e[++a],o=r[r.length-1];if(r.length>1&&i.type===1&&(n?.type===3&&o.name===n.value||(n?.type===7||n.type===2)&&typeof o.name==`number`)){let e=r.pop();e?.type===1&&t.has(e.name)&&(e.children=[]),a++;continue}throw Error(`Mismatched closing tag for <${o.name}>`)}if(n.type===3||n.type===7){let t=n.value,c={type:typeof t==`number`||i(t)?2:1,name:t,props:[],children:[]};for(s.children.push(c),a++;a<o;){let t=e[a];if(t.type===1||t.type===2)break;if(t.type===8){let t=e[a+1];if(t?.type===7)c.props.push({type:3,value:t.value}),a+=2;else throw Error(`Spread operator in <${c.name}> must be followed by an expression`)}else if(t.type===3){let n=t.value;if(e[a+1]?.type===4){a+=2;let t=e[a];if(t.type===7)c.props.push({name:n,type:2,value:t.value}),a++;else if(t.type===5){let e=t.quote;c.props.push({name:n,value:t.value,quote:e,type:1}),a++}else throw Error(`Attribute value for "${n}" in <${c.name}> must be an expression or a string`)}else c.props.push({type:0,name:n,value:!0}),a++}else throw Error(`Invalid attribute in <${c.name}>`)}let l=e[a];l.type===2?a+=2:l.type===1&&(a++,r.push(c));continue}}default:throw Error(`Unexpected token: ${JSON.stringify(n)} after <${r[r.length-1].name}>`)}}if(r.length>1)throw Error(`Unclosed tag for <${r[r.length-1].name}>`);return n},o=e=>e.length===1?e[0]:e;function s(e){let t=new WeakMap,n=new Set(e.RawTextElements);n.delete(`template`);let i=document.createTreeWalker(document,129),s=t=>e.SVGElements.has(t)?document.createElementNS(`http://www.w3.org/2000/svg`,t):e.MathMLElements.has(t)?document.createElementNS(`http://www.w3.org/1998/Math/MathML`,t):document.createElement(t),c=e=>{let t=(t,...n)=>m(l(t),n,e);return t.components=e,t.jsx=t,t.define=t=>c({...e,...t}),t},l=i=>{let o=t.get(i);return o||(o=a(r(i,n),e.VoidElements),u(o,!1),t.set(i,o)),o},u=(e,t)=>{if(e.type===1){if(!t){let n=document.createElement(`template`);n.content.appendChild(f(e)),e.template=n,t=!0}e.children.forEach(e=>u(e,t))}else e.type===2||e.type===0?e.children.forEach(e=>u(e,!1)):e.type===3&&!t&&(d.innerHTML=e.value,e.value=d.content.textContent??``)},d=document.createElement(`template`),f=e=>{switch(e.type){case 3:return d.innerHTML=e.value,document.createTextNode(d.content.textContent??``);case 4:return document.createComment(`+`);case 2:return document.createComment(e.name);case 1:let t=!1,n=s(e.name);return e.props=e.props.filter(e=>e.type===1?e.name.startsWith(`prop:`)?!0:(n.setAttribute(e.name,e.value),t):e.type===0?(n.setAttribute(e.name,``),t):e.type===3?(t=!0,t):!0),(e.name===`template`?n.content:n).append(...e.children.map(f)),n}},p=(t,n,r)=>{switch(t.type){case 3:return t.value;case 4:return n[t.value];case 2:let i=typeof t.name==`string`?r[t.name]:n[t.name];if(i&&typeof i==`function`)return e.createComponent(i,h(t,n,r));throw Error(`Component "${t.name}" not found in registry`);case 1:let a=m(t,n,r),o=h(t,n,r);return e.spread(a,o,!0),a}},m=(t,n,r)=>{if(t.type!==1||!t.template)return o(t.children.map(e=>p(e,n,r)));let a=t.template.content.firstChild.cloneNode(!0);i.currentNode=a;let s=(t,i)=>{for(let a of t)if(a.type===1||a.type===4||a.type===2){let t=i.nextNode();if(a.type===4||a.type===2)e.insert(t.parentNode,p(a,n,r),t),i.currentNode=t;else{if(a.props.length){let i=h(a,n,r);e.spread(t,i,!0)}s(a.children,a.name===`template`?document.createTreeWalker(t.content,129):i)}}};return s(t.children,t.name===`template`?document.createTreeWalker(a.content,129):i),a},h=(t,n,r,i={})=>{for(let r of t.props)switch(r.type){case 0:i[r.name]=!0;break;case 1:i[r.name]=r.value;break;case 2:g(i,r.name,n[r.value]);break;case 3:let t=n[r.value];if(!t||typeof t!=`object`)throw Error(`Can only spread objects`);i=e.mergeProps(i,t);break}return t.type===2&&t.children.length&&Object.defineProperty(i,`children`,{get(){return m(t,n,r)}}),i},g=(e,t,n)=>{typeof n==`function`&&n.length===0&&t!==`ref`&&!t.startsWith(`on`)?Object.defineProperty(e,t,{get(){return n()},enumerable:!0}):e[t]=n};return c({})}export{s as createTaggedJSXRuntime};
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dom-expressions/tagged-jsx",
|
|
3
|
+
"description": "A Fine-Grained Rendering Runtime API using Tagged Template Literals",
|
|
4
|
+
"version": "0.50.0-next.15",
|
|
5
|
+
"author": "Daniel Kling",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/ryansolid/dom-expressions.git",
|
|
10
|
+
"directory": "packages/tagged-jsx"
|
|
11
|
+
},
|
|
12
|
+
"main": "dist/index.mjs",
|
|
13
|
+
"module": "dist/index.mjs",
|
|
14
|
+
"types": "dist/index.d.mts",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.mts",
|
|
19
|
+
"import": "./dist/index.mjs"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"jsdom": "^21.0.0",
|
|
24
|
+
"tsdown": "^0.20.3",
|
|
25
|
+
"vitest": "^4.0.18",
|
|
26
|
+
"@dom-expressions/runtime": "0.50.0-next.15"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsdown src/index.ts --format esm --minify",
|
|
30
|
+
"test": "vitest --run"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/index.ts
ADDED
package/src/parse.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import {
|
|
2
|
+
STRING_TOKEN,
|
|
3
|
+
CLOSE_TAG_TOKEN,
|
|
4
|
+
EQUALS_TOKEN,
|
|
5
|
+
EXPRESSION_TOKEN,
|
|
6
|
+
IDENTIFIER_TOKEN,
|
|
7
|
+
OPEN_TAG_TOKEN,
|
|
8
|
+
SLASH_TOKEN,
|
|
9
|
+
SPREAD_TOKEN,
|
|
10
|
+
TEXT_TOKEN,
|
|
11
|
+
Token
|
|
12
|
+
} from "./tokenize";
|
|
13
|
+
|
|
14
|
+
const isComponentNode = (name: string): boolean => {
|
|
15
|
+
const char = name.charCodeAt(0);
|
|
16
|
+
return (
|
|
17
|
+
char >= 65 && char <= 90 // Uppercase A-Z
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Node type constants
|
|
22
|
+
export const ROOT_NODE = 0;
|
|
23
|
+
export const ELEMENT_NODE = 1;
|
|
24
|
+
export const COMPONENT_NODE = 2;
|
|
25
|
+
export const TEXT_NODE = 3;
|
|
26
|
+
export const EXPRESSION_NODE = 4;
|
|
27
|
+
|
|
28
|
+
// Prop type constants
|
|
29
|
+
export const BOOLEAN_PROP = 0;
|
|
30
|
+
export const STATIC_PROP = 1;
|
|
31
|
+
export const EXPRESSION_PROP = 2;
|
|
32
|
+
export const SPREAD_PROP = 3;
|
|
33
|
+
|
|
34
|
+
export type NodeType =
|
|
35
|
+
| typeof ROOT_NODE
|
|
36
|
+
| typeof ELEMENT_NODE
|
|
37
|
+
| typeof COMPONENT_NODE
|
|
38
|
+
| typeof TEXT_NODE
|
|
39
|
+
| typeof EXPRESSION_NODE;
|
|
40
|
+
export type PropType =
|
|
41
|
+
| typeof BOOLEAN_PROP
|
|
42
|
+
| typeof STATIC_PROP
|
|
43
|
+
| typeof EXPRESSION_PROP
|
|
44
|
+
| typeof SPREAD_PROP;
|
|
45
|
+
|
|
46
|
+
export type ChildNode = ElementNode | TextNode | ExpressionNode | ComponentNode;
|
|
47
|
+
|
|
48
|
+
export interface RootNode {
|
|
49
|
+
type: typeof ROOT_NODE;
|
|
50
|
+
children: ChildNode[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ElementNode {
|
|
54
|
+
type: typeof ELEMENT_NODE;
|
|
55
|
+
name: string;
|
|
56
|
+
props: PropNode[];
|
|
57
|
+
children: ChildNode[];
|
|
58
|
+
template?: HTMLTemplateElement;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ComponentNode {
|
|
62
|
+
type: typeof COMPONENT_NODE;
|
|
63
|
+
name: string | number;
|
|
64
|
+
props: PropNode[];
|
|
65
|
+
children: ChildNode[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface TextNode {
|
|
69
|
+
type: typeof TEXT_NODE;
|
|
70
|
+
value: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ExpressionNode {
|
|
74
|
+
type: typeof EXPRESSION_NODE;
|
|
75
|
+
value: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface BooleanProp {
|
|
79
|
+
name: string;
|
|
80
|
+
type: typeof BOOLEAN_PROP;
|
|
81
|
+
value: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface StringProp {
|
|
85
|
+
name: string;
|
|
86
|
+
type: typeof STATIC_PROP;
|
|
87
|
+
value: string;
|
|
88
|
+
quote?: "'" | '"';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ExpressionProp {
|
|
92
|
+
name: string;
|
|
93
|
+
type: typeof EXPRESSION_PROP;
|
|
94
|
+
value: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface SpreadProp {
|
|
98
|
+
type: typeof SPREAD_PROP;
|
|
99
|
+
value: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type PropNode = BooleanProp | StringProp | ExpressionProp | SpreadProp;
|
|
103
|
+
|
|
104
|
+
export const parse = (tokens: Token[], voidElements: Set<string>): RootNode => {
|
|
105
|
+
const root: RootNode = { type: ROOT_NODE, children: [] };
|
|
106
|
+
const stack: (RootNode | ElementNode | ComponentNode)[] = [root];
|
|
107
|
+
let pos = 0;
|
|
108
|
+
const len = tokens.length;
|
|
109
|
+
|
|
110
|
+
while (pos < len) {
|
|
111
|
+
const token = tokens[pos];
|
|
112
|
+
const parent = stack[stack.length - 1];
|
|
113
|
+
|
|
114
|
+
switch (token.type) {
|
|
115
|
+
case TEXT_TOKEN: {
|
|
116
|
+
// --- TEXT ---
|
|
117
|
+
const value = token.value;
|
|
118
|
+
if (value.trim() === "") {
|
|
119
|
+
const prevType = tokens[pos - 1]?.type;
|
|
120
|
+
const nextType = tokens[pos + 1]?.type;
|
|
121
|
+
// Filter out empty text nodes between tags
|
|
122
|
+
if (prevType === CLOSE_TAG_TOKEN || nextType === OPEN_TAG_TOKEN) {
|
|
123
|
+
pos++;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
parent.children.push({ type: TEXT_NODE, value });
|
|
128
|
+
pos++;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case EXPRESSION_TOKEN: {
|
|
133
|
+
// --- EXPRESSION ---
|
|
134
|
+
parent.children.push({ type: EXPRESSION_NODE, value: token.value });
|
|
135
|
+
pos++;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
case OPEN_TAG_TOKEN: {
|
|
140
|
+
// --- TAG ---
|
|
141
|
+
const nextToken = tokens[++pos];
|
|
142
|
+
|
|
143
|
+
// Handle Closing Tag: </name>
|
|
144
|
+
if (nextToken.type === SLASH_TOKEN) {
|
|
145
|
+
const nameToken = tokens[++pos];
|
|
146
|
+
const closeToken = tokens[++pos];
|
|
147
|
+
const currentParent = stack[stack.length - 1] as ElementNode | ComponentNode;
|
|
148
|
+
if (
|
|
149
|
+
stack.length > 1 &&
|
|
150
|
+
closeToken.type === CLOSE_TAG_TOKEN &&
|
|
151
|
+
((nameToken?.type === IDENTIFIER_TOKEN && currentParent.name === nameToken.value) ||
|
|
152
|
+
((nameToken?.type === EXPRESSION_TOKEN || nameToken.type === SLASH_TOKEN) &&
|
|
153
|
+
typeof currentParent.name === "number"))
|
|
154
|
+
) {
|
|
155
|
+
const node = stack.pop();
|
|
156
|
+
if (node?.type === ELEMENT_NODE && voidElements.has(node.name)) {
|
|
157
|
+
node.children = [];
|
|
158
|
+
}
|
|
159
|
+
pos++;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
throw new Error(`Mismatched closing tag for <${currentParent.name}>`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Handle Opening Tag: <name ...>
|
|
166
|
+
if (nextToken.type === IDENTIFIER_TOKEN || nextToken.type === EXPRESSION_TOKEN) {
|
|
167
|
+
const tagName = nextToken.value;
|
|
168
|
+
const node: ElementNode | ComponentNode = {
|
|
169
|
+
type:
|
|
170
|
+
typeof tagName === "number" || isComponentNode(tagName)
|
|
171
|
+
? COMPONENT_NODE
|
|
172
|
+
: ELEMENT_NODE,
|
|
173
|
+
name: tagName,
|
|
174
|
+
props: [],
|
|
175
|
+
children: []
|
|
176
|
+
} as ElementNode | ComponentNode;
|
|
177
|
+
parent.children.push(node);
|
|
178
|
+
pos++; // Consume tag name
|
|
179
|
+
|
|
180
|
+
// --- Attribute Parsing Loop ---
|
|
181
|
+
while (pos < len) {
|
|
182
|
+
const attrToken = tokens[pos];
|
|
183
|
+
if (attrToken.type === CLOSE_TAG_TOKEN || attrToken.type === SLASH_TOKEN) {
|
|
184
|
+
break; // End of attributes
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (attrToken.type === SPREAD_TOKEN) {
|
|
188
|
+
const expr = tokens[pos + 1];
|
|
189
|
+
if (expr?.type === EXPRESSION_TOKEN) {
|
|
190
|
+
node.props.push({ type: SPREAD_PROP, value: expr.value });
|
|
191
|
+
pos += 2; // Consume '...' and expression
|
|
192
|
+
} else {
|
|
193
|
+
throw new Error(
|
|
194
|
+
`Spread operator in <${node.name}> must be followed by an expression`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
} else if (attrToken.type === IDENTIFIER_TOKEN) {
|
|
198
|
+
const name = attrToken.value;
|
|
199
|
+
const next = tokens[pos + 1];
|
|
200
|
+
|
|
201
|
+
if (next?.type === EQUALS_TOKEN) {
|
|
202
|
+
pos += 2; // Consume name and '='
|
|
203
|
+
const valToken = tokens[pos];
|
|
204
|
+
if (valToken.type === EXPRESSION_TOKEN) {
|
|
205
|
+
node.props.push({ name, type: EXPRESSION_PROP, value: valToken.value });
|
|
206
|
+
pos++;
|
|
207
|
+
} else if (valToken.type === STRING_TOKEN) {
|
|
208
|
+
const quote = valToken.quote;
|
|
209
|
+
node.props.push({
|
|
210
|
+
name,
|
|
211
|
+
value: valToken.value,
|
|
212
|
+
quote,
|
|
213
|
+
type: STATIC_PROP
|
|
214
|
+
} as StringProp);
|
|
215
|
+
pos++;
|
|
216
|
+
} else {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Attribute value for "${name}" in <${node.name}> must be an expression or a string`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
// Boolean prop
|
|
223
|
+
node.props.push({ type: BOOLEAN_PROP, name, value: true });
|
|
224
|
+
pos++;
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
throw new Error(`Invalid attribute in <${node.name}>`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// --- Tag Closing Logic ---
|
|
232
|
+
const endToken = tokens[pos];
|
|
233
|
+
if (endToken.type === SLASH_TOKEN) {
|
|
234
|
+
// Self-closing: <div/>
|
|
235
|
+
pos += 2; // Consume '/' and '>'
|
|
236
|
+
} else if (endToken.type === CLOSE_TAG_TOKEN) {
|
|
237
|
+
// Opening: <div>
|
|
238
|
+
pos++; // Consume '>'
|
|
239
|
+
stack.push(node);
|
|
240
|
+
}
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
default:
|
|
246
|
+
throw new Error(
|
|
247
|
+
`Unexpected token: ${JSON.stringify(token)} after <${(stack[stack.length - 1] as ElementNode | ComponentNode).name}>`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (stack.length > 1) {
|
|
253
|
+
throw new Error(`Unclosed tag for <${(stack[stack.length - 1] as ElementNode).name}>`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return root;
|
|
257
|
+
};
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BOOLEAN_PROP,
|
|
3
|
+
COMPONENT_NODE,
|
|
4
|
+
ChildNode,
|
|
5
|
+
ComponentNode,
|
|
6
|
+
ELEMENT_NODE,
|
|
7
|
+
EXPRESSION_NODE,
|
|
8
|
+
EXPRESSION_PROP,
|
|
9
|
+
ElementNode,
|
|
10
|
+
ROOT_NODE,
|
|
11
|
+
RootNode,
|
|
12
|
+
SPREAD_PROP,
|
|
13
|
+
STATIC_PROP,
|
|
14
|
+
TEXT_NODE,
|
|
15
|
+
parse
|
|
16
|
+
} from "./parse";
|
|
17
|
+
import { tokenize } from "./tokenize";
|
|
18
|
+
import { ComponentRegistry, TaggedJSXInstance, Runtime } from "./types";
|
|
19
|
+
import { type JSX } from "../../runtime/src/jsx";
|
|
20
|
+
|
|
21
|
+
const flat = (arr: any[]) => {
|
|
22
|
+
return arr.length === 1 ? arr[0] : arr;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function createTaggedJSXRuntime(r: Runtime) {
|
|
26
|
+
const cache = new WeakMap<TemplateStringsArray, RootNode>();
|
|
27
|
+
const rawTextElements = new Set(r.RawTextElements);
|
|
28
|
+
rawTextElements.delete("template");
|
|
29
|
+
|
|
30
|
+
// Walk over text, comment, and element nodes.
|
|
31
|
+
const walker = document.createTreeWalker(document, 129);
|
|
32
|
+
|
|
33
|
+
const createElement = (name: string) => {
|
|
34
|
+
return r.SVGElements.has(name)
|
|
35
|
+
? document.createElementNS("http://www.w3.org/2000/svg", name)
|
|
36
|
+
: r.MathMLElements.has(name)
|
|
37
|
+
? document.createElementNS("http://www.w3.org/1998/Math/MathML", name)
|
|
38
|
+
: document.createElement(name);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Internal: build a tagged JSX tag bound to a component registry.
|
|
42
|
+
const createTaggedJSX = <T extends ComponentRegistry>(components: T): TaggedJSXInstance<T> => {
|
|
43
|
+
const tag = (strings: TemplateStringsArray, ...values: any[]) => {
|
|
44
|
+
const root = getCachedRoot(strings);
|
|
45
|
+
|
|
46
|
+
return renderChildren(root, values, components);
|
|
47
|
+
};
|
|
48
|
+
tag.components = components;
|
|
49
|
+
tag.jsx = tag;
|
|
50
|
+
tag.define = <TNew extends ComponentRegistry>(newComponents: TNew) => {
|
|
51
|
+
return createTaggedJSX({ ...components, ...newComponents });
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return tag as TaggedJSXInstance<T>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const getCachedRoot = (strings: TemplateStringsArray): RootNode => {
|
|
58
|
+
let root = cache.get(strings);
|
|
59
|
+
if (!root) {
|
|
60
|
+
root = parse(tokenize(strings, rawTextElements), r.VoidElements);
|
|
61
|
+
buildTemplate(root, false);
|
|
62
|
+
cache.set(strings, root);
|
|
63
|
+
}
|
|
64
|
+
return root;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Build template elements with the same shape as the parsed tree so both can be walked in sync.
|
|
68
|
+
const buildTemplate = (node: RootNode | ChildNode, insideTemplate: boolean): void => {
|
|
69
|
+
if (node.type === ELEMENT_NODE) {
|
|
70
|
+
if (!insideTemplate) {
|
|
71
|
+
const template = document.createElement("template");
|
|
72
|
+
template.content.appendChild(buildNodes(node));
|
|
73
|
+
node.template = template;
|
|
74
|
+
insideTemplate = true;
|
|
75
|
+
}
|
|
76
|
+
node.children.forEach(child => buildTemplate(child, insideTemplate));
|
|
77
|
+
} else if (node.type === COMPONENT_NODE || node.type === ROOT_NODE) {
|
|
78
|
+
node.children.forEach(child => buildTemplate(child, false));
|
|
79
|
+
} else if (node.type === TEXT_NODE && !insideTemplate) {
|
|
80
|
+
textTemplate.innerHTML = node.value;
|
|
81
|
+
node.value = textTemplate.content.textContent ?? "";
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const textTemplate = document.createElement("template");
|
|
86
|
+
|
|
87
|
+
const buildNodes = (node: ChildNode): Node => {
|
|
88
|
+
switch (node.type) {
|
|
89
|
+
case TEXT_NODE:
|
|
90
|
+
textTemplate.innerHTML = node.value;
|
|
91
|
+
return document.createTextNode(textTemplate.content.textContent ?? "");
|
|
92
|
+
case EXPRESSION_NODE:
|
|
93
|
+
return document.createComment("+");
|
|
94
|
+
case COMPONENT_NODE:
|
|
95
|
+
return document.createComment(node.name as string);
|
|
96
|
+
case ELEMENT_NODE:
|
|
97
|
+
let hasSpread = false;
|
|
98
|
+
|
|
99
|
+
const elem = createElement(node.name);
|
|
100
|
+
// Props located after spread need to be applied after spread for possible overrides.
|
|
101
|
+
node.props = node.props.filter(prop => {
|
|
102
|
+
if (prop.type === STATIC_PROP) {
|
|
103
|
+
if (prop.name.startsWith("prop:")) return true;
|
|
104
|
+
elem.setAttribute(prop.name, prop.value);
|
|
105
|
+
return hasSpread;
|
|
106
|
+
} else if (prop.type === BOOLEAN_PROP) {
|
|
107
|
+
elem.setAttribute(prop.name, "");
|
|
108
|
+
return hasSpread;
|
|
109
|
+
} else if (prop.type === SPREAD_PROP) {
|
|
110
|
+
hasSpread = true;
|
|
111
|
+
return hasSpread;
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
});
|
|
115
|
+
const childRoot = node.name === "template" ? (elem as HTMLTemplateElement).content : elem;
|
|
116
|
+
childRoot.append(...node.children.map(buildNodes));
|
|
117
|
+
|
|
118
|
+
return elem;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const renderNode = (node: ChildNode, values: any[], components: ComponentRegistry): any => {
|
|
123
|
+
switch (node.type) {
|
|
124
|
+
case TEXT_NODE:
|
|
125
|
+
return node.value;
|
|
126
|
+
case EXPRESSION_NODE:
|
|
127
|
+
return values[node.value];
|
|
128
|
+
case COMPONENT_NODE:
|
|
129
|
+
const component = typeof node.name === "string" ? components[node.name] : values[node.name];
|
|
130
|
+
if (component && typeof component === "function") {
|
|
131
|
+
return r.createComponent(component, gatherProps(node, values, components));
|
|
132
|
+
} else {
|
|
133
|
+
throw new Error(`Component "${node.name}" not found in registry`);
|
|
134
|
+
}
|
|
135
|
+
case ELEMENT_NODE:
|
|
136
|
+
const element = renderChildren(node, values, components) as Element;
|
|
137
|
+
const props = gatherProps(node, values, components);
|
|
138
|
+
|
|
139
|
+
r.spread(element, props, true);
|
|
140
|
+
|
|
141
|
+
return element;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const renderChildren = (
|
|
146
|
+
node: RootNode | ComponentNode | ElementNode,
|
|
147
|
+
values: any[],
|
|
148
|
+
components: ComponentRegistry
|
|
149
|
+
): JSX.Element => {
|
|
150
|
+
if (node.type !== ELEMENT_NODE || !node.template) {
|
|
151
|
+
return flat(node.children.map(n => renderNode(n, values, components)));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const element = node.template.content.firstChild!.cloneNode(true) as Element;
|
|
155
|
+
walker.currentNode = element;
|
|
156
|
+
|
|
157
|
+
const walkNodes = (nodes: ChildNode[], walker: TreeWalker) => {
|
|
158
|
+
for (const node of nodes) {
|
|
159
|
+
if (
|
|
160
|
+
node.type === ELEMENT_NODE ||
|
|
161
|
+
node.type === EXPRESSION_NODE ||
|
|
162
|
+
node.type === COMPONENT_NODE
|
|
163
|
+
) {
|
|
164
|
+
const domNode = walker.nextNode()!;
|
|
165
|
+
if (node.type === EXPRESSION_NODE || node.type === COMPONENT_NODE) {
|
|
166
|
+
r.insert(domNode.parentNode!, renderNode(node, values, components), domNode);
|
|
167
|
+
walker.currentNode = domNode;
|
|
168
|
+
} else {
|
|
169
|
+
if (node.props.length) {
|
|
170
|
+
const props = gatherProps(node, values, components);
|
|
171
|
+
r.spread(domNode as Element, props, true);
|
|
172
|
+
}
|
|
173
|
+
walkNodes(
|
|
174
|
+
node.children,
|
|
175
|
+
node.name === "template"
|
|
176
|
+
? document.createTreeWalker((domNode as HTMLTemplateElement).content, 129)
|
|
177
|
+
: walker
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
walkNodes(
|
|
184
|
+
node.children,
|
|
185
|
+
node.name === "template"
|
|
186
|
+
? document.createTreeWalker((element as HTMLTemplateElement).content, 129)
|
|
187
|
+
: walker
|
|
188
|
+
);
|
|
189
|
+
return element;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const gatherProps = (
|
|
193
|
+
node: ElementNode | ComponentNode,
|
|
194
|
+
values: any[],
|
|
195
|
+
components: ComponentRegistry,
|
|
196
|
+
props: Record<string, any> = {}
|
|
197
|
+
) => {
|
|
198
|
+
for (const prop of node.props) {
|
|
199
|
+
switch (prop.type) {
|
|
200
|
+
case BOOLEAN_PROP:
|
|
201
|
+
props[prop.name] = true;
|
|
202
|
+
break;
|
|
203
|
+
case STATIC_PROP:
|
|
204
|
+
props[prop.name] = prop.value;
|
|
205
|
+
break;
|
|
206
|
+
case EXPRESSION_PROP:
|
|
207
|
+
applyGetter(props, prop.name, values[prop.value]);
|
|
208
|
+
break;
|
|
209
|
+
case SPREAD_PROP:
|
|
210
|
+
const spread = values[prop.value];
|
|
211
|
+
if (!spread || typeof spread !== "object") throw new Error("Can only spread objects");
|
|
212
|
+
props = r.mergeProps(props, spread);
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// children - childNodes overwrites any props.children
|
|
218
|
+
if (node.type === COMPONENT_NODE && node.children.length) {
|
|
219
|
+
Object.defineProperty(props, "children", {
|
|
220
|
+
get() {
|
|
221
|
+
return renderChildren(node, values, components);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return props;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const applyGetter = (props: Record<string, any>, name: string, value: any) => {
|
|
229
|
+
if (
|
|
230
|
+
typeof value === "function" &&
|
|
231
|
+
value.length === 0 &&
|
|
232
|
+
name !== "ref" &&
|
|
233
|
+
!name.startsWith("on")
|
|
234
|
+
) {
|
|
235
|
+
Object.defineProperty(props, name, {
|
|
236
|
+
get() {
|
|
237
|
+
return value();
|
|
238
|
+
},
|
|
239
|
+
enumerable: true
|
|
240
|
+
});
|
|
241
|
+
} else {
|
|
242
|
+
props[name] = value;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
return createTaggedJSX({});
|
|
247
|
+
}
|