htm-transform 0.1.3 → 0.1.4
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/index.d.ts +75 -0
- package/index.js +266 -82
- package/package.json +7 -4
- package/test.js +62 -0
- package/tsconfig.json +15 -0
- package/jsconfig.json +0 -12
package/index.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} ElementNode
|
|
3
|
+
* @property {'element'} type
|
|
4
|
+
* @property {boolean | {type: 'expression', expr: import('acorn').Node}} tag
|
|
5
|
+
* @property {Object<string, string | boolean | {type: 'expression', expr: import('acorn').Node}>} props
|
|
6
|
+
* @property {Array<ElementNode | TextNode | ExpressionNode>} children
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {object} TextNode
|
|
10
|
+
* @property {'text'} type
|
|
11
|
+
* @property {string} value
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object} ExpressionNode
|
|
15
|
+
* @property {'expression'} type
|
|
16
|
+
* @property {string} value
|
|
17
|
+
* @property {import('acorn').Node} [expr]
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} Token
|
|
21
|
+
* @property {'openTag' | 'closeTag' | 'text' | 'expression'} type
|
|
22
|
+
* @property {string} [tag]
|
|
23
|
+
* @property {Record<string, string | boolean>} [props]
|
|
24
|
+
* @property {boolean} [selfClosing]
|
|
25
|
+
* @property {string} [value]
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Transforms htm tagged templates into h function calls
|
|
29
|
+
* @param {string} code - JavaScript code containing htm tagged templates
|
|
30
|
+
* @param {object} [options] - Transform options
|
|
31
|
+
* @param {string} [options.pragma] - The h function name (default: 'h')
|
|
32
|
+
* @param {string} [options.tag] - The tag name to look for (default: 'html')
|
|
33
|
+
* @param {object} [options.import] - Import configuration
|
|
34
|
+
* @param {string} options.import.from - Module to import from (e.g., 'preact', 'react')
|
|
35
|
+
* @param {string} options.import.name - Export name to import (e.g., 'h', 'createElement')
|
|
36
|
+
* @returns {string} - Transformed code
|
|
37
|
+
*/
|
|
38
|
+
export default function transform(code: string, options?: {
|
|
39
|
+
pragma?: string;
|
|
40
|
+
tag?: string;
|
|
41
|
+
import?: {
|
|
42
|
+
from: string;
|
|
43
|
+
name: string;
|
|
44
|
+
};
|
|
45
|
+
}): string;
|
|
46
|
+
export type ElementNode = {
|
|
47
|
+
type: "element";
|
|
48
|
+
tag: boolean | {
|
|
49
|
+
type: "expression";
|
|
50
|
+
expr: import("acorn").Node;
|
|
51
|
+
};
|
|
52
|
+
props: {
|
|
53
|
+
[x: string]: string | boolean | {
|
|
54
|
+
type: "expression";
|
|
55
|
+
expr: import("acorn").Node;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
children: Array<ElementNode | TextNode | ExpressionNode>;
|
|
59
|
+
};
|
|
60
|
+
export type TextNode = {
|
|
61
|
+
type: "text";
|
|
62
|
+
value: string;
|
|
63
|
+
};
|
|
64
|
+
export type ExpressionNode = {
|
|
65
|
+
type: "expression";
|
|
66
|
+
value: string;
|
|
67
|
+
expr?: import("acorn").Node;
|
|
68
|
+
};
|
|
69
|
+
export type Token = {
|
|
70
|
+
type: "openTag" | "closeTag" | "text" | "expression";
|
|
71
|
+
tag?: string;
|
|
72
|
+
props?: Record<string, string | boolean>;
|
|
73
|
+
selfClosing?: boolean;
|
|
74
|
+
value?: string;
|
|
75
|
+
};
|
package/index.js
CHANGED
|
@@ -11,23 +11,23 @@ import { simple } from "acorn-walk";
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* @typedef {
|
|
14
|
+
* @typedef {object} TextNode
|
|
15
15
|
* @property {'text'} type
|
|
16
16
|
* @property {string} value
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* @typedef {
|
|
20
|
+
* @typedef {object} ExpressionNode
|
|
21
21
|
* @property {'expression'} type
|
|
22
22
|
* @property {string} value
|
|
23
23
|
* @property {import('acorn').Node} [expr]
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* @typedef {
|
|
27
|
+
* @typedef {object} Token
|
|
28
28
|
* @property {'openTag' | 'closeTag' | 'text' | 'expression'} type
|
|
29
29
|
* @property {string} [tag]
|
|
30
|
-
* @property {
|
|
30
|
+
* @property {Record<string, string | boolean>} [props]
|
|
31
31
|
* @property {boolean} [selfClosing]
|
|
32
32
|
* @property {string} [value]
|
|
33
33
|
*/
|
|
@@ -35,10 +35,10 @@ import { simple } from "acorn-walk";
|
|
|
35
35
|
/**
|
|
36
36
|
* Transforms htm tagged templates into h function calls
|
|
37
37
|
* @param {string} code - JavaScript code containing htm tagged templates
|
|
38
|
-
* @param {
|
|
39
|
-
* @param {string} options.pragma - The h function name (default: 'h')
|
|
40
|
-
* @param {string} options.tag - The tag name to look for (default: 'html')
|
|
41
|
-
* @param {
|
|
38
|
+
* @param {object} [options] - Transform options
|
|
39
|
+
* @param {string} [options.pragma] - The h function name (default: 'h')
|
|
40
|
+
* @param {string} [options.tag] - The tag name to look for (default: 'html')
|
|
41
|
+
* @param {object} [options.import] - Import configuration
|
|
42
42
|
* @param {string} options.import.from - Module to import from (e.g., 'preact', 'react')
|
|
43
43
|
* @param {string} options.import.name - Export name to import (e.g., 'h', 'createElement')
|
|
44
44
|
* @returns {string} - Transformed code
|
|
@@ -46,22 +46,28 @@ import { simple } from "acorn-walk";
|
|
|
46
46
|
export default function transform(code, options = {}) {
|
|
47
47
|
const { pragma = "h", tag: tagName = "html", import: importConfig } = options;
|
|
48
48
|
|
|
49
|
-
const ast = acorn.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
const ast = /** @type {import('acorn').Program} */ (
|
|
50
|
+
acorn.parse(code, {
|
|
51
|
+
ecmaVersion: "latest",
|
|
52
|
+
sourceType: "module",
|
|
53
|
+
})
|
|
54
|
+
);
|
|
53
55
|
|
|
54
56
|
let hasTransformation = false;
|
|
55
57
|
|
|
56
58
|
simple(ast, {
|
|
59
|
+
/**
|
|
60
|
+
* @param {import('acorn').TaggedTemplateExpression} node
|
|
61
|
+
*/
|
|
57
62
|
TaggedTemplateExpression(node) {
|
|
58
63
|
if (node.tag.type === "Identifier" && node.tag.name === tagName) {
|
|
59
64
|
hasTransformation = true;
|
|
60
65
|
const transformed = transformTaggedTemplate(node, pragma);
|
|
61
66
|
|
|
62
67
|
// Replace the node with the transformed version
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
const mutableNode = /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (node));
|
|
69
|
+
for (const key in mutableNode) {
|
|
70
|
+
delete mutableNode[key];
|
|
65
71
|
}
|
|
66
72
|
Object.assign(node, transformed);
|
|
67
73
|
}
|
|
@@ -78,43 +84,73 @@ export default function transform(code, options = {}) {
|
|
|
78
84
|
|
|
79
85
|
/**
|
|
80
86
|
* Adds an import declaration at the top of the AST
|
|
81
|
-
* @param {import('acorn').
|
|
87
|
+
* @param {import('acorn').Program} ast - The AST to modify
|
|
82
88
|
* @param {string} moduleName - The module to import from
|
|
83
89
|
* @param {string} exportName - The export name to import
|
|
84
90
|
* @returns {void}
|
|
85
91
|
*/
|
|
86
92
|
function addImportDeclaration(ast, moduleName, exportName) {
|
|
87
93
|
const hasImport = ast.body.some(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
)
|
|
94
|
+
/**
|
|
95
|
+
* @param {import('acorn').Statement | import('acorn').ModuleDeclaration} node
|
|
96
|
+
* @returns {boolean}
|
|
97
|
+
*/
|
|
98
|
+
(node) => {
|
|
99
|
+
if (node.type !== "ImportDeclaration") return false;
|
|
100
|
+
const importNode = /** @type {import('acorn').ImportDeclaration} */ (node);
|
|
101
|
+
return (
|
|
102
|
+
importNode.source.value === moduleName &&
|
|
103
|
+
importNode.specifiers.some(
|
|
104
|
+
/**
|
|
105
|
+
* @param {import('acorn').ImportSpecifier | import('acorn').ImportDefaultSpecifier | import('acorn').ImportNamespaceSpecifier} spec
|
|
106
|
+
* @returns {boolean}
|
|
107
|
+
*/
|
|
108
|
+
(spec) => {
|
|
109
|
+
if (spec.type !== "ImportSpecifier") return false;
|
|
110
|
+
const importSpec = /** @type {import('acorn').ImportSpecifier} */ (spec);
|
|
111
|
+
return (
|
|
112
|
+
importSpec.imported.type === "Identifier" && importSpec.imported.name === exportName
|
|
113
|
+
);
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
},
|
|
94
118
|
);
|
|
95
119
|
|
|
96
120
|
if (hasImport) return;
|
|
97
121
|
|
|
98
122
|
// Create import declaration: import { exportName } from 'moduleName';
|
|
123
|
+
/** @type {import('acorn').ImportDeclaration} */
|
|
99
124
|
const importDeclaration = {
|
|
100
125
|
type: "ImportDeclaration",
|
|
126
|
+
start: 0,
|
|
127
|
+
end: 0,
|
|
101
128
|
specifiers: [
|
|
102
129
|
{
|
|
103
130
|
type: "ImportSpecifier",
|
|
131
|
+
start: 0,
|
|
132
|
+
end: 0,
|
|
104
133
|
imported: {
|
|
105
134
|
type: "Identifier",
|
|
135
|
+
start: 0,
|
|
136
|
+
end: 0,
|
|
106
137
|
name: exportName,
|
|
107
138
|
},
|
|
108
139
|
local: {
|
|
109
140
|
type: "Identifier",
|
|
141
|
+
start: 0,
|
|
142
|
+
end: 0,
|
|
110
143
|
name: exportName,
|
|
111
144
|
},
|
|
112
145
|
},
|
|
113
146
|
],
|
|
114
147
|
source: {
|
|
115
148
|
type: "Literal",
|
|
149
|
+
start: 0,
|
|
150
|
+
end: 0,
|
|
116
151
|
value: moduleName,
|
|
117
152
|
},
|
|
153
|
+
attributes: [],
|
|
118
154
|
};
|
|
119
155
|
|
|
120
156
|
// Add to the beginning of the body
|
|
@@ -123,9 +159,9 @@ function addImportDeclaration(ast, moduleName, exportName) {
|
|
|
123
159
|
|
|
124
160
|
/**
|
|
125
161
|
* Transforms a single tagged template expression
|
|
126
|
-
* @param {import('acorn').
|
|
162
|
+
* @param {import('acorn').TaggedTemplateExpression} node - The tagged template expression node
|
|
127
163
|
* @param {string} pragma - The pragma function name
|
|
128
|
-
* @returns {import('acorn').
|
|
164
|
+
* @returns {import('acorn').Literal | import('acorn').CallExpression | import('acorn').ArrayExpression | import('acorn').Expression} The transformed AST node
|
|
129
165
|
*/
|
|
130
166
|
function transformTaggedTemplate(node, pragma) {
|
|
131
167
|
const quasi = node.quasi;
|
|
@@ -149,15 +185,21 @@ function transformTaggedTemplate(node, pragma) {
|
|
|
149
185
|
|
|
150
186
|
// Convert to h() calls
|
|
151
187
|
if (elements.length === 0) {
|
|
152
|
-
|
|
188
|
+
/** @type {import('acorn').Literal} */
|
|
189
|
+
const literal = { type: "Literal", start: 0, end: 0, value: null };
|
|
190
|
+
return literal;
|
|
153
191
|
} else if (elements.length === 1) {
|
|
154
192
|
return elementsToAST(elements[0], pragma);
|
|
155
193
|
} else {
|
|
156
194
|
// Multiple root elements - return array
|
|
157
|
-
|
|
195
|
+
/** @type {import('acorn').ArrayExpression} */
|
|
196
|
+
const arrayExpr = {
|
|
158
197
|
type: "ArrayExpression",
|
|
198
|
+
start: 0,
|
|
199
|
+
end: 0,
|
|
159
200
|
elements: elements.map((el) => elementsToAST(el, pragma)),
|
|
160
201
|
};
|
|
202
|
+
return arrayExpr;
|
|
161
203
|
}
|
|
162
204
|
}
|
|
163
205
|
|
|
@@ -185,15 +227,16 @@ function parseTemplate(template, placeholders) {
|
|
|
185
227
|
|
|
186
228
|
if (token.type === "openTag") {
|
|
187
229
|
i++;
|
|
230
|
+
/** @type {ElementNode} */
|
|
188
231
|
const element = {
|
|
189
232
|
type: "element",
|
|
190
|
-
tag: token.tag,
|
|
191
|
-
props: token.props,
|
|
233
|
+
tag: token.tag || "",
|
|
234
|
+
props: token.props || {},
|
|
192
235
|
children: [],
|
|
193
236
|
};
|
|
194
237
|
|
|
195
238
|
if (!token.selfClosing) {
|
|
196
|
-
element.children = parse(token.tag);
|
|
239
|
+
element.children = parse(token.tag || "");
|
|
197
240
|
}
|
|
198
241
|
|
|
199
242
|
children.push(element);
|
|
@@ -204,13 +247,17 @@ function parseTemplate(template, placeholders) {
|
|
|
204
247
|
}
|
|
205
248
|
i++;
|
|
206
249
|
} else if (token.type === "text") {
|
|
207
|
-
const text = token.value.trim();
|
|
250
|
+
const text = (token.value || "").trim();
|
|
208
251
|
if (text) {
|
|
209
|
-
|
|
252
|
+
/** @type {TextNode} */
|
|
253
|
+
const textNode = { type: "text", value: text };
|
|
254
|
+
children.push(textNode);
|
|
210
255
|
}
|
|
211
256
|
i++;
|
|
212
257
|
} else if (token.type === "expression") {
|
|
213
|
-
|
|
258
|
+
/** @type {ExpressionNode} */
|
|
259
|
+
const exprNode = { type: "expression", value: token.value || "" };
|
|
260
|
+
children.push(exprNode);
|
|
214
261
|
i++;
|
|
215
262
|
} else {
|
|
216
263
|
i++;
|
|
@@ -232,6 +279,7 @@ function parseTemplate(template, placeholders) {
|
|
|
232
279
|
* @returns {Array<Token>} Array of tokens
|
|
233
280
|
*/
|
|
234
281
|
function tokenize(template) {
|
|
282
|
+
/** @type {Token[]} */
|
|
235
283
|
const tokens = [];
|
|
236
284
|
let i = 0;
|
|
237
285
|
|
|
@@ -244,21 +292,27 @@ function tokenize(template) {
|
|
|
244
292
|
const [full, isClosing, tagName, attrsStr, selfClosing] = tagMatch;
|
|
245
293
|
|
|
246
294
|
if (isClosing) {
|
|
247
|
-
|
|
295
|
+
/** @type {Token} */
|
|
296
|
+
const closeToken = { type: "closeTag", tag: tagName };
|
|
297
|
+
tokens.push(closeToken);
|
|
248
298
|
} else {
|
|
249
299
|
const props = parseAttributes(attrsStr);
|
|
250
|
-
|
|
300
|
+
/** @type {Token} */
|
|
301
|
+
const openToken = {
|
|
251
302
|
type: "openTag",
|
|
252
303
|
tag: tagName,
|
|
253
304
|
props,
|
|
254
305
|
selfClosing: selfClosing === "/",
|
|
255
|
-
}
|
|
306
|
+
};
|
|
307
|
+
tokens.push(openToken);
|
|
256
308
|
}
|
|
257
309
|
|
|
258
310
|
i += full.length;
|
|
259
311
|
} else {
|
|
260
312
|
// Not a valid tag, treat as text
|
|
261
|
-
|
|
313
|
+
/** @type {Token} */
|
|
314
|
+
const textToken = { type: "text", value: "<" };
|
|
315
|
+
tokens.push(textToken);
|
|
262
316
|
i++;
|
|
263
317
|
}
|
|
264
318
|
} else {
|
|
@@ -272,9 +326,13 @@ function tokenize(template) {
|
|
|
272
326
|
// Check if this is an expression placeholder
|
|
273
327
|
const exprMatch = text.match(/^(__EXPR_\d+__)/);
|
|
274
328
|
if (exprMatch && text === exprMatch[0]) {
|
|
275
|
-
|
|
329
|
+
/** @type {Token} */
|
|
330
|
+
const exprToken = { type: "expression", value: exprMatch[0] };
|
|
331
|
+
tokens.push(exprToken);
|
|
276
332
|
} else if (text.trim()) {
|
|
277
|
-
|
|
333
|
+
/** @type {Token} */
|
|
334
|
+
const textToken = { type: "text", value: text };
|
|
335
|
+
tokens.push(textToken);
|
|
278
336
|
}
|
|
279
337
|
}
|
|
280
338
|
}
|
|
@@ -288,6 +346,7 @@ function tokenize(template) {
|
|
|
288
346
|
* @returns {Object<string, string | boolean>} Parsed attributes object
|
|
289
347
|
*/
|
|
290
348
|
function parseAttributes(attrsStr) {
|
|
349
|
+
/** @type {Record<string, string | boolean>} */
|
|
291
350
|
const props = {};
|
|
292
351
|
const attrRegex = /([a-zA-Z0-9_:-]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
293
352
|
let match;
|
|
@@ -319,45 +378,69 @@ function resolvePlaceholders(nodes, placeholders) {
|
|
|
319
378
|
if (node.type === "element") {
|
|
320
379
|
// Resolve tag name if it's a placeholder (component)
|
|
321
380
|
if (typeof node.tag === "string" && node.tag.startsWith("__EXPR_")) {
|
|
322
|
-
|
|
381
|
+
const expr = placeholderMap.get(node.tag);
|
|
382
|
+
if (expr) {
|
|
383
|
+
node.tag = { type: "expression", expr };
|
|
384
|
+
}
|
|
323
385
|
}
|
|
324
386
|
|
|
325
387
|
// Resolve props, preserving order
|
|
388
|
+
/** @type {Record<string, string | boolean | {type: 'expression', expr: import('acorn').Node}>} */
|
|
326
389
|
const newProps = {};
|
|
327
390
|
for (const [key, value] of Object.entries(node.props)) {
|
|
328
391
|
if (key.startsWith("__EXPR_")) {
|
|
329
392
|
// Handle spread props (key is the placeholder)
|
|
330
|
-
|
|
393
|
+
const expr = placeholderMap.get(key);
|
|
394
|
+
if (expr) {
|
|
395
|
+
newProps["...spread"] = { type: "expression", expr };
|
|
396
|
+
}
|
|
331
397
|
} else if (typeof value === "string" && placeholderMap.has(value)) {
|
|
332
398
|
// Single placeholder value
|
|
333
|
-
|
|
399
|
+
const expr = placeholderMap.get(value);
|
|
400
|
+
if (expr) {
|
|
401
|
+
newProps[key] = { type: "expression", expr };
|
|
402
|
+
}
|
|
334
403
|
} else if (typeof value === "string" && value.includes("__EXPR_")) {
|
|
335
404
|
// Template string with placeholders - create TemplateLiteral
|
|
336
405
|
const parts = value.split(/(__EXPR_\d+__)/);
|
|
406
|
+
/** @type {Array<import('acorn').TemplateElement>} */
|
|
337
407
|
const quasis = [];
|
|
408
|
+
/** @type {Array<import('acorn').Expression>} */
|
|
338
409
|
const expressions = [];
|
|
339
410
|
|
|
340
411
|
for (let i = 0; i < parts.length; i++) {
|
|
341
412
|
if (i % 2 === 0) {
|
|
342
413
|
// Static string part
|
|
343
|
-
|
|
414
|
+
/** @type {import('acorn').TemplateElement} */
|
|
415
|
+
const templateElement = {
|
|
344
416
|
type: "TemplateElement",
|
|
417
|
+
start: 0,
|
|
418
|
+
end: 0,
|
|
345
419
|
value: { raw: parts[i], cooked: parts[i] },
|
|
346
420
|
tail: i === parts.length - 1,
|
|
347
|
-
}
|
|
421
|
+
};
|
|
422
|
+
quasis.push(templateElement);
|
|
348
423
|
} else {
|
|
349
424
|
// Expression placeholder
|
|
350
|
-
|
|
425
|
+
const expr = placeholderMap.get(parts[i]);
|
|
426
|
+
if (expr) {
|
|
427
|
+
expressions.push(/** @type {import('acorn').Expression} */ (expr));
|
|
428
|
+
}
|
|
351
429
|
}
|
|
352
430
|
}
|
|
353
431
|
|
|
432
|
+
/** @type {import('acorn').TemplateLiteral} */
|
|
433
|
+
const templateLiteral = {
|
|
434
|
+
type: "TemplateLiteral",
|
|
435
|
+
start: 0,
|
|
436
|
+
end: 0,
|
|
437
|
+
quasis,
|
|
438
|
+
expressions,
|
|
439
|
+
};
|
|
440
|
+
|
|
354
441
|
newProps[key] = {
|
|
355
442
|
type: "expression",
|
|
356
|
-
expr:
|
|
357
|
-
type: "TemplateLiteral",
|
|
358
|
-
quasis,
|
|
359
|
-
expressions,
|
|
360
|
-
},
|
|
443
|
+
expr: templateLiteral,
|
|
361
444
|
};
|
|
362
445
|
} else {
|
|
363
446
|
newProps[key] = value;
|
|
@@ -366,7 +449,9 @@ function resolvePlaceholders(nodes, placeholders) {
|
|
|
366
449
|
node.props = newProps;
|
|
367
450
|
|
|
368
451
|
// Resolve children
|
|
369
|
-
node.children =
|
|
452
|
+
node.children = /** @type {Array<ElementNode | TextNode | ExpressionNode>} */ (
|
|
453
|
+
node.children.map(resolve).flat()
|
|
454
|
+
);
|
|
370
455
|
} else if (node.type === "expression") {
|
|
371
456
|
const expr = placeholderMap.get(node.value);
|
|
372
457
|
if (expr) {
|
|
@@ -376,21 +461,30 @@ function resolvePlaceholders(nodes, placeholders) {
|
|
|
376
461
|
// Check if text contains placeholders
|
|
377
462
|
const parts = node.value.split(/(__EXPR_\d+__)/);
|
|
378
463
|
if (parts.length > 1) {
|
|
379
|
-
|
|
464
|
+
/** @type {Array<ElementNode | TextNode | ExpressionNode>} */
|
|
465
|
+
const result = parts
|
|
380
466
|
.filter((p) => p)
|
|
381
467
|
.map((p) => {
|
|
382
468
|
if (p.startsWith("__EXPR_")) {
|
|
383
|
-
|
|
469
|
+
const expr = placeholderMap.get(p);
|
|
470
|
+
if (expr) {
|
|
471
|
+
/** @type {ExpressionNode} */
|
|
472
|
+
const exprNode = { type: "expression", value: p, expr };
|
|
473
|
+
return exprNode;
|
|
474
|
+
}
|
|
384
475
|
}
|
|
385
|
-
|
|
476
|
+
/** @type {TextNode} */
|
|
477
|
+
const textNode = { type: "text", value: p };
|
|
478
|
+
return textNode;
|
|
386
479
|
});
|
|
480
|
+
return result;
|
|
387
481
|
}
|
|
388
482
|
}
|
|
389
483
|
|
|
390
484
|
return node;
|
|
391
485
|
}
|
|
392
486
|
|
|
393
|
-
return nodes.map(resolve).flat();
|
|
487
|
+
return /** @type {Array<ElementNode | TextNode | ExpressionNode>} */ (nodes.map(resolve).flat());
|
|
394
488
|
}
|
|
395
489
|
|
|
396
490
|
/**
|
|
@@ -407,26 +501,36 @@ function needsQuoting(key) {
|
|
|
407
501
|
* Converts element nodes to AST nodes representing h() calls
|
|
408
502
|
* @param {ElementNode | TextNode | ExpressionNode} node - The node to convert
|
|
409
503
|
* @param {string} pragma - The pragma function name
|
|
410
|
-
* @returns {import('acorn').
|
|
504
|
+
* @returns {import('acorn').Literal | import('acorn').CallExpression | import('acorn').Expression} The AST node
|
|
411
505
|
*/
|
|
412
506
|
function elementsToAST(node, pragma) {
|
|
413
507
|
if (node.type === "text") {
|
|
414
|
-
|
|
508
|
+
/** @type {import('acorn').Literal} */
|
|
509
|
+
const literal = { type: "Literal", start: 0, end: 0, value: node.value };
|
|
510
|
+
return literal;
|
|
415
511
|
}
|
|
416
512
|
|
|
417
513
|
if (node.type === "expression") {
|
|
418
|
-
return node.expr;
|
|
514
|
+
return /** @type {import('acorn').Expression} */ (node.expr);
|
|
419
515
|
}
|
|
420
516
|
|
|
421
517
|
if (node.type === "element") {
|
|
422
518
|
// Build h(tag, props, ...children)
|
|
519
|
+
/** @type {Array<import('acorn').Expression | import('acorn').SpreadElement>} */
|
|
423
520
|
const args = [];
|
|
424
521
|
|
|
425
522
|
// Tag argument
|
|
426
|
-
if (node.tag.type === "expression") {
|
|
427
|
-
args.push(node.tag.expr);
|
|
523
|
+
if (typeof node.tag === "object" && node.tag.type === "expression") {
|
|
524
|
+
args.push(/** @type {import('acorn').Expression} */ (node.tag.expr));
|
|
428
525
|
} else {
|
|
429
|
-
|
|
526
|
+
/** @type {import('acorn').Literal} */
|
|
527
|
+
const tagLiteral = {
|
|
528
|
+
type: "Literal",
|
|
529
|
+
start: 0,
|
|
530
|
+
end: 0,
|
|
531
|
+
value: /** @type {string} */ (node.tag),
|
|
532
|
+
};
|
|
533
|
+
args.push(tagLiteral);
|
|
430
534
|
}
|
|
431
535
|
|
|
432
536
|
// Props argument
|
|
@@ -434,54 +538,122 @@ function elementsToAST(node, pragma) {
|
|
|
434
538
|
if (propsEntries.length > 0) {
|
|
435
539
|
const spreadProp = node.props["...spread"];
|
|
436
540
|
|
|
437
|
-
if (
|
|
541
|
+
if (
|
|
542
|
+
spreadProp &&
|
|
543
|
+
typeof spreadProp === "object" &&
|
|
544
|
+
spreadProp.type === "expression" &&
|
|
545
|
+
propsEntries.length === 1
|
|
546
|
+
) {
|
|
438
547
|
// Just spread props
|
|
439
|
-
args.push(spreadProp.expr);
|
|
440
|
-
} else if (spreadProp) {
|
|
548
|
+
args.push(/** @type {import('acorn').Expression} */ (spreadProp.expr));
|
|
549
|
+
} else if (spreadProp && typeof spreadProp === "object" && spreadProp.type === "expression") {
|
|
441
550
|
// Merge spread with other props, preserving order
|
|
551
|
+
/** @type {Array<import('acorn').Property | import('acorn').SpreadElement>} */
|
|
442
552
|
const properties = propsEntries.map(([key, value]) => {
|
|
443
|
-
if (key === "...spread") {
|
|
444
|
-
|
|
553
|
+
if (key === "...spread" && typeof value === "object" && value.type === "expression") {
|
|
554
|
+
/** @type {import('acorn').SpreadElement} */
|
|
555
|
+
const spread = {
|
|
445
556
|
type: "SpreadElement",
|
|
446
|
-
|
|
557
|
+
start: 0,
|
|
558
|
+
end: 0,
|
|
559
|
+
argument: /** @type {import('acorn').Expression} */ (value.expr),
|
|
447
560
|
};
|
|
561
|
+
return spread;
|
|
448
562
|
}
|
|
449
|
-
|
|
563
|
+
/** @type {import('acorn').Property} */
|
|
564
|
+
const prop = {
|
|
450
565
|
type: "Property",
|
|
566
|
+
start: 0,
|
|
567
|
+
end: 0,
|
|
451
568
|
key: needsQuoting(key)
|
|
452
|
-
?
|
|
453
|
-
|
|
454
|
-
|
|
569
|
+
? /** @type {import('acorn').Literal} */ ({
|
|
570
|
+
type: "Literal",
|
|
571
|
+
start: 0,
|
|
572
|
+
end: 0,
|
|
573
|
+
value: key,
|
|
574
|
+
})
|
|
575
|
+
: /** @type {import('acorn').Identifier} */ ({
|
|
576
|
+
type: "Identifier",
|
|
577
|
+
start: 0,
|
|
578
|
+
end: 0,
|
|
579
|
+
name: key,
|
|
580
|
+
}),
|
|
581
|
+
value:
|
|
582
|
+
typeof value === "object" && value.type === "expression"
|
|
583
|
+
? /** @type {import('acorn').Expression} */ (value.expr)
|
|
584
|
+
: /** @type {import('acorn').Literal} */ ({
|
|
585
|
+
type: "Literal",
|
|
586
|
+
start: 0,
|
|
587
|
+
end: 0,
|
|
588
|
+
value: /** @type {string | boolean} */ (value),
|
|
589
|
+
}),
|
|
455
590
|
kind: "init",
|
|
456
591
|
method: false,
|
|
457
592
|
shorthand: false,
|
|
458
593
|
computed: false,
|
|
459
594
|
};
|
|
595
|
+
return prop;
|
|
460
596
|
});
|
|
461
597
|
|
|
462
|
-
|
|
598
|
+
/** @type {import('acorn').ObjectExpression} */
|
|
599
|
+
const objExpr = {
|
|
463
600
|
type: "ObjectExpression",
|
|
601
|
+
start: 0,
|
|
602
|
+
end: 0,
|
|
464
603
|
properties,
|
|
465
|
-
}
|
|
604
|
+
};
|
|
605
|
+
args.push(objExpr);
|
|
466
606
|
} else {
|
|
467
607
|
// Normal props
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
608
|
+
/** @type {Array<import('acorn').Property | import('acorn').SpreadElement>} */
|
|
609
|
+
const properties = propsEntries.map(([key, value]) => {
|
|
610
|
+
/** @type {import('acorn').Property} */
|
|
611
|
+
const prop = {
|
|
471
612
|
type: "Property",
|
|
613
|
+
start: 0,
|
|
614
|
+
end: 0,
|
|
472
615
|
key: needsQuoting(key)
|
|
473
|
-
?
|
|
474
|
-
|
|
475
|
-
|
|
616
|
+
? /** @type {import('acorn').Literal} */ ({
|
|
617
|
+
type: "Literal",
|
|
618
|
+
start: 0,
|
|
619
|
+
end: 0,
|
|
620
|
+
value: key,
|
|
621
|
+
})
|
|
622
|
+
: /** @type {import('acorn').Identifier} */ ({
|
|
623
|
+
type: "Identifier",
|
|
624
|
+
start: 0,
|
|
625
|
+
end: 0,
|
|
626
|
+
name: key,
|
|
627
|
+
}),
|
|
628
|
+
value:
|
|
629
|
+
typeof value === "object" && value.type === "expression"
|
|
630
|
+
? /** @type {import('acorn').Expression} */ (value.expr)
|
|
631
|
+
: /** @type {import('acorn').Literal} */ ({
|
|
632
|
+
type: "Literal",
|
|
633
|
+
start: 0,
|
|
634
|
+
end: 0,
|
|
635
|
+
value: /** @type {string | boolean} */ (value),
|
|
636
|
+
}),
|
|
476
637
|
kind: "init",
|
|
477
638
|
method: false,
|
|
478
639
|
shorthand: false,
|
|
479
640
|
computed: false,
|
|
480
|
-
}
|
|
641
|
+
};
|
|
642
|
+
return prop;
|
|
481
643
|
});
|
|
644
|
+
/** @type {import('acorn').ObjectExpression} */
|
|
645
|
+
const objExpr = {
|
|
646
|
+
type: "ObjectExpression",
|
|
647
|
+
start: 0,
|
|
648
|
+
end: 0,
|
|
649
|
+
properties,
|
|
650
|
+
};
|
|
651
|
+
args.push(objExpr);
|
|
482
652
|
}
|
|
483
653
|
} else {
|
|
484
|
-
|
|
654
|
+
/** @type {import('acorn').Literal} */
|
|
655
|
+
const nullLiteral = { type: "Literal", start: 0, end: 0, value: null };
|
|
656
|
+
args.push(nullLiteral);
|
|
485
657
|
}
|
|
486
658
|
|
|
487
659
|
// Children arguments
|
|
@@ -489,12 +661,24 @@ function elementsToAST(node, pragma) {
|
|
|
489
661
|
args.push(elementsToAST(child, pragma));
|
|
490
662
|
}
|
|
491
663
|
|
|
492
|
-
|
|
664
|
+
/** @type {import('acorn').CallExpression} */
|
|
665
|
+
const callExpr = {
|
|
493
666
|
type: "CallExpression",
|
|
494
|
-
|
|
667
|
+
start: 0,
|
|
668
|
+
end: 0,
|
|
669
|
+
callee: /** @type {import('acorn').Identifier} */ ({
|
|
670
|
+
type: "Identifier",
|
|
671
|
+
start: 0,
|
|
672
|
+
end: 0,
|
|
673
|
+
name: pragma,
|
|
674
|
+
}),
|
|
495
675
|
arguments: args,
|
|
676
|
+
optional: false,
|
|
496
677
|
};
|
|
678
|
+
return callExpr;
|
|
497
679
|
}
|
|
498
680
|
|
|
499
|
-
|
|
681
|
+
/** @type {import('acorn').Literal} */
|
|
682
|
+
const nullLiteral = { type: "Literal", start: 0, end: 0, value: null };
|
|
683
|
+
return nullLiteral;
|
|
500
684
|
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "htm-transform",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Transform htm tagged templates into h function calls using acorn",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
|
-
"types": "index.
|
|
7
|
+
"types": "index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"types": "./index.
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
11
|
"default": "./index.js"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
15
|
"test": "node --test test.js",
|
|
16
|
-
"
|
|
16
|
+
"dts": "tsc"
|
|
17
17
|
},
|
|
18
18
|
"keywords": [
|
|
19
19
|
"htm",
|
|
@@ -28,5 +28,8 @@
|
|
|
28
28
|
"acorn": "^8.11.3",
|
|
29
29
|
"acorn-walk": "^8.3.2",
|
|
30
30
|
"astring": "^1.8.6"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"typescript": "^5.9.3"
|
|
31
34
|
}
|
|
32
35
|
}
|
package/test.js
CHANGED
|
@@ -390,4 +390,66 @@ const result = myH("div", null, "Test");`;
|
|
|
390
390
|
|
|
391
391
|
assert.strictEqual(normalize(output), normalize(expected));
|
|
392
392
|
});
|
|
393
|
+
|
|
394
|
+
test("todo", () => {
|
|
395
|
+
const input = `import html from "~/util/preact/html.js";
|
|
396
|
+
/** @import { JSX } from "preact" */
|
|
397
|
+
|
|
398
|
+
import button from "~/style/button.js";
|
|
399
|
+
import Icon from "./Icon.js";
|
|
400
|
+
|
|
401
|
+
import { adopt } from "~/util/css.js";
|
|
402
|
+
import sheet from "./Button.module.css" with { type: "css" };
|
|
403
|
+
const css = adopt(sheet);
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* @typedef {object} Props
|
|
407
|
+
* @property {"primary" | "secondary" | "tertiary"} [level = "secondary"]
|
|
408
|
+
* @property {"neutral" | "positive" | "negative"} [kind = "neutral"]
|
|
409
|
+
* @property {"small" | "base" | "large"} [size = "base"]
|
|
410
|
+
* @property {boolean} [loading]
|
|
411
|
+
* @property {string} [popovertarget]
|
|
412
|
+
* @property {(el: HTMLButtonElement) => void} [innerRef]
|
|
413
|
+
*/
|
|
414
|
+
|
|
415
|
+
/** @param {Props & JSX.ButtonHTMLAttributes<HTMLButtonElement>} props */
|
|
416
|
+
export default function Button(props) {
|
|
417
|
+
const {
|
|
418
|
+
level,
|
|
419
|
+
kind = "neutral",
|
|
420
|
+
size = "base",
|
|
421
|
+
loading,
|
|
422
|
+
disabled,
|
|
423
|
+
type = "button",
|
|
424
|
+
children,
|
|
425
|
+
innerRef: ref,
|
|
426
|
+
...rest
|
|
427
|
+
} = props;
|
|
428
|
+
|
|
429
|
+
return html\`
|
|
430
|
+
<button
|
|
431
|
+
class="\${css.wrapper} \${button.button}"
|
|
432
|
+
data-level=\${level}
|
|
433
|
+
data-kind=\${kind}
|
|
434
|
+
data-size=\${size}
|
|
435
|
+
type=\${type}
|
|
436
|
+
disabled=\${disabled || loading}
|
|
437
|
+
ref=\${ref}
|
|
438
|
+
...\${rest}
|
|
439
|
+
>
|
|
440
|
+
<span class="\${css.layer} \${css.contents}">\${children}</span>
|
|
441
|
+
\${loading
|
|
442
|
+
? html\`
|
|
443
|
+
<span class="\${css.layer} \${css.loader}">
|
|
444
|
+
<\${Icon} class="\${css.loader}" name="loader" />
|
|
445
|
+
</span>
|
|
446
|
+
\`
|
|
447
|
+
: null}
|
|
448
|
+
</button>
|
|
449
|
+
\`;
|
|
450
|
+
}
|
|
451
|
+
`;
|
|
452
|
+
|
|
453
|
+
const output = transform(input);
|
|
454
|
+
});
|
|
393
455
|
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "esnext",
|
|
4
|
+
"moduleResolution": "bundler",
|
|
5
|
+
"target": "es2022",
|
|
6
|
+
"lib": ["es2022"],
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"emitDeclarationOnly": true,
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"checkJs": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
},
|
|
13
|
+
"include": ["index.js"],
|
|
14
|
+
"exclude": ["node_modules", "test.js"],
|
|
15
|
+
}
|
package/jsconfig.json
DELETED