htm-transform 0.1.5 → 0.1.7
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.js +82 -2
- package/package.json +1 -1
- package/test.js +94 -0
package/index.js
CHANGED
|
@@ -60,20 +60,71 @@ export default function transform(code, options = {}) {
|
|
|
60
60
|
|
|
61
61
|
let hasTransformation = false;
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Recursively sets location information on all nodes in an AST subtree
|
|
65
|
+
* @param {any} node - The AST node to process
|
|
66
|
+
* @param {number} start - The start position
|
|
67
|
+
* @param {number} end - The end position
|
|
68
|
+
* @param {any} loc - The loc object with line/column info
|
|
69
|
+
*/
|
|
70
|
+
function setLocationRecursive(node, start, end, loc) {
|
|
71
|
+
if (!node || typeof node !== "object" || !node.type) return;
|
|
72
|
+
|
|
73
|
+
node.start = start;
|
|
74
|
+
node.end = end;
|
|
75
|
+
if (loc) {
|
|
76
|
+
node.loc = loc;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Recursively set location for all child nodes
|
|
80
|
+
for (const key in node) {
|
|
81
|
+
const value = node[key];
|
|
82
|
+
if (Array.isArray(value)) {
|
|
83
|
+
for (const child of value) {
|
|
84
|
+
if (child && typeof child === "object" && child.type) {
|
|
85
|
+
setLocationRecursive(child, start, end, loc);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} else if (value && typeof value === "object" && value.type) {
|
|
89
|
+
setLocationRecursive(value, start, end, loc);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
63
94
|
const traveler = makeTraveler({
|
|
95
|
+
PropertyDefinition(node, state) {
|
|
96
|
+
// Skip private fields with null values (astravel can't handle them)
|
|
97
|
+
if (node.value === null) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.super.PropertyDefinition.call(this, node, state);
|
|
101
|
+
},
|
|
64
102
|
TaggedTemplateExpression(node, state) {
|
|
65
103
|
if (node.tag.type === "Identifier" && node.tag.name === tagName) {
|
|
66
104
|
hasTransformation = true;
|
|
67
105
|
const transformed = transformTaggedTemplate(node, pragma);
|
|
68
106
|
|
|
107
|
+
// Preserve original location information
|
|
108
|
+
const originalStart = node.start;
|
|
109
|
+
const originalEnd = node.end;
|
|
110
|
+
const originalLoc = node.loc;
|
|
111
|
+
|
|
112
|
+
// Set location info recursively on transformed tree
|
|
113
|
+
setLocationRecursive(transformed, originalStart, originalEnd, originalLoc);
|
|
114
|
+
|
|
69
115
|
// Replace the node with the transformed version
|
|
70
116
|
const mutableNode = /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (node));
|
|
71
117
|
for (const key in mutableNode) {
|
|
72
118
|
delete mutableNode[key];
|
|
73
119
|
}
|
|
74
120
|
Object.assign(node, transformed);
|
|
75
|
-
|
|
76
|
-
|
|
121
|
+
|
|
122
|
+
// Manually traverse child nodes for nested html templates
|
|
123
|
+
// We use the default traversal for the new node type
|
|
124
|
+
const newNodeType = transformed.type;
|
|
125
|
+
if (this.super[newNodeType]) {
|
|
126
|
+
this.super[newNodeType].call(this, node, state);
|
|
127
|
+
}
|
|
77
128
|
return;
|
|
78
129
|
}
|
|
79
130
|
// Continue traversal for non-matching nodes
|
|
@@ -89,8 +140,28 @@ export default function transform(code, options = {}) {
|
|
|
89
140
|
}
|
|
90
141
|
|
|
91
142
|
// Attach comments to AST nodes
|
|
143
|
+
// Workaround: attachComments can't handle private fields with null values
|
|
144
|
+
// Temporarily mark them so we can restore them after
|
|
92
145
|
if (comments.length > 0) {
|
|
146
|
+
const nullFields = [];
|
|
147
|
+
const markNullFields = makeTraveler({
|
|
148
|
+
PropertyDefinition(node) {
|
|
149
|
+
if (node.value === null) {
|
|
150
|
+
nullFields.push(node);
|
|
151
|
+
// Create a placeholder literal so astravel can traverse it
|
|
152
|
+
node.value = /** @type {any} */ ({ type: 'Literal', value: null, start: node.start, end: node.end, loc: node.loc });
|
|
153
|
+
}
|
|
154
|
+
this.super.PropertyDefinition.call(this, node);
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
markNullFields.go(ast);
|
|
158
|
+
|
|
93
159
|
attachComments(ast, comments);
|
|
160
|
+
|
|
161
|
+
// Restore null values
|
|
162
|
+
for (const node of nullFields) {
|
|
163
|
+
node.value = null;
|
|
164
|
+
}
|
|
94
165
|
}
|
|
95
166
|
|
|
96
167
|
return generate(ast, { comments: true });
|
|
@@ -300,6 +371,15 @@ function tokenize(template) {
|
|
|
300
371
|
while (i < template.length) {
|
|
301
372
|
// Check for tag
|
|
302
373
|
if (template[i] === "<") {
|
|
374
|
+
// Check for empty closing tag <//>
|
|
375
|
+
if (template.slice(i, i + 4) === "<//>") {
|
|
376
|
+
/** @type {Token} */
|
|
377
|
+
const closeToken = { type: "closeTag", tag: "" };
|
|
378
|
+
tokens.push(closeToken);
|
|
379
|
+
i += 4;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
303
383
|
const tagMatch = template.slice(i).match(/^<(\/?)([a-zA-Z0-9_${}]+)([^>]*?)(\/?)>/);
|
|
304
384
|
|
|
305
385
|
if (tagMatch) {
|
package/package.json
CHANGED
package/test.js
CHANGED
|
@@ -403,3 +403,97 @@ const y = 42;`;
|
|
|
403
403
|
|
|
404
404
|
assert.strictEqual(normalize(output), normalize(expected));
|
|
405
405
|
});
|
|
406
|
+
|
|
407
|
+
test("handles nested html in logical OR expression", () => {
|
|
408
|
+
const input = `const result = html\`<div>\${name || html\`<span>Default</span>\`}</div>\`;`;
|
|
409
|
+
const output = transform(input);
|
|
410
|
+
const expected = `const result = h("div", null, name || h("span", null, "Default"));`;
|
|
411
|
+
|
|
412
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("handles empty closing tag shorthand", () => {
|
|
416
|
+
const input = `const result = html\`<\${Button}>Click<//>\`;`;
|
|
417
|
+
const output = transform(input);
|
|
418
|
+
const expected = `const result = h(Button, null, "Click");`;
|
|
419
|
+
|
|
420
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("preserves location info when html template is in array with inline comment", () => {
|
|
424
|
+
const input = `const arr = [{ foo: html\`<span class=\${"hello"}>bar</span>\` }];
|
|
425
|
+
arr.push({
|
|
426
|
+
foo: "bar", // inline comment
|
|
427
|
+
});
|
|
428
|
+
`;
|
|
429
|
+
const output = transform(input);
|
|
430
|
+
const expected = `const arr = [{
|
|
431
|
+
foo: h("span", {
|
|
432
|
+
class: "hello"
|
|
433
|
+
}, "bar")
|
|
434
|
+
}];
|
|
435
|
+
arr.push({
|
|
436
|
+
// inline comment
|
|
437
|
+
foo: "bar"
|
|
438
|
+
});`;
|
|
439
|
+
|
|
440
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test("handles private class fields with html templates", () => {
|
|
444
|
+
const input = `class Alert {
|
|
445
|
+
#timeout;
|
|
446
|
+
|
|
447
|
+
render() {
|
|
448
|
+
return html\`<div>Test</div>\`;
|
|
449
|
+
}
|
|
450
|
+
}`;
|
|
451
|
+
const output = transform(input);
|
|
452
|
+
const expected = `class Alert {
|
|
453
|
+
#timeout;
|
|
454
|
+
|
|
455
|
+
render() {
|
|
456
|
+
return h("div", null, "Test");
|
|
457
|
+
}
|
|
458
|
+
}`;
|
|
459
|
+
|
|
460
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test("transforms html template in private field initializer", () => {
|
|
464
|
+
const input = `class Foo {
|
|
465
|
+
#element = html\`<div>Test</div>\`;
|
|
466
|
+
}`;
|
|
467
|
+
const output = transform(input);
|
|
468
|
+
const expected = `class Foo {
|
|
469
|
+
#element = h("div", null, "Test");
|
|
470
|
+
}`;
|
|
471
|
+
|
|
472
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("handles multiple private fields with JSDoc comments", () => {
|
|
476
|
+
const input = `class Foo {
|
|
477
|
+
/** @type {string | undefined} */
|
|
478
|
+
#timeout;
|
|
479
|
+
/** @type {number | undefined} */
|
|
480
|
+
#count;
|
|
481
|
+
|
|
482
|
+
render() {
|
|
483
|
+
return html\`<div>Test</div>\`;
|
|
484
|
+
}
|
|
485
|
+
}`;
|
|
486
|
+
|
|
487
|
+
const output = transform(input);
|
|
488
|
+
const expected = `class Foo {
|
|
489
|
+
/** @type {string | undefined}*/
|
|
490
|
+
#timeout;
|
|
491
|
+
/** @type {number | undefined}*/
|
|
492
|
+
#count;
|
|
493
|
+
render() {
|
|
494
|
+
return h("div", null, "Test");
|
|
495
|
+
}
|
|
496
|
+
}`;
|
|
497
|
+
|
|
498
|
+
assert.strictEqual(normalize(output), normalize(expected));
|
|
499
|
+
});
|