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.
Files changed (3) hide show
  1. package/index.js +82 -2
  2. package/package.json +1 -1
  3. 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
- // Continue traversing into the transformed node to handle nested templates
76
- this.go(node, state);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "htm-transform",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Transform htm tagged templates into h function calls using acorn",
5
5
  "type": "module",
6
6
  "main": "index.js",
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
+ });