htl-to-js 0.1.0
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/LICENSE +21 -0
- package/README.md +489 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +65 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +19 -0
- package/dist/loader.js +32 -0
- package/dist/loader.js.map +1 -0
- package/dist/transpiler/directives.d.ts +42 -0
- package/dist/transpiler/directives.js +207 -0
- package/dist/transpiler/directives.js.map +1 -0
- package/dist/transpiler/expr.d.ts +34 -0
- package/dist/transpiler/expr.js +134 -0
- package/dist/transpiler/expr.js.map +1 -0
- package/dist/transpiler/index.d.ts +15 -0
- package/dist/transpiler/index.js +292 -0
- package/dist/transpiler/index.js.map +1 -0
- package/dist/transpiler/walker.d.ts +18 -0
- package/dist/transpiler/walker.js +252 -0
- package/dist/transpiler/walker.js.map +1 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 htl-to-js contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
# htl-to-js
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/htl-to-js)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
|
|
6
|
+
Webpack loader and CLI that transpiles AEM HTL (Sightly) templates into JavaScript functions returning template literals.
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
import { createAccordion } from '../../jcr_root/apps/mysite/components/accordion/accordion.html';
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install --save-dev htl-to-js
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Requires Node.js >= 18.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Storybook setup (webpack5)
|
|
25
|
+
|
|
26
|
+
Add the loader rule in `.storybook/main.js`. Since Storybook config files are often ESM, use `createRequire` to resolve the loader path:
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
import { createRequire } from 'module';
|
|
30
|
+
const require = createRequire(import.meta.url);
|
|
31
|
+
|
|
32
|
+
const config = {
|
|
33
|
+
// ...
|
|
34
|
+
async webpackFinal(config) {
|
|
35
|
+
config.module.rules.push({
|
|
36
|
+
test: /\.html$/,
|
|
37
|
+
include: /jcr_root[\\/]apps/, // only AEM component HTML
|
|
38
|
+
use: require.resolve('htl-to-js/loader'),
|
|
39
|
+
});
|
|
40
|
+
return config;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default config;
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If your Storybook config uses CommonJS:
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
module.exports = {
|
|
51
|
+
async webpackFinal(config) {
|
|
52
|
+
config.module.rules.push({
|
|
53
|
+
test: /\.html$/,
|
|
54
|
+
include: /jcr_root[\\/]apps/,
|
|
55
|
+
use: require.resolve('htl-to-js/loader'),
|
|
56
|
+
});
|
|
57
|
+
return config;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## HTL directive support
|
|
65
|
+
|
|
66
|
+
| Directive | Behavior |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `data-sly-use.name="..."` | Becomes a function parameter |
|
|
69
|
+
| `data-sly-test="${cond}"` | Conditional rendering via ternary |
|
|
70
|
+
| `data-sly-test.varName="${cond}"` | Conditional + assigns result to variable |
|
|
71
|
+
| `data-sly-repeat.item="${list}"` | Loop: repeats the **whole element** per item |
|
|
72
|
+
| `data-sly-list.item="${list}"` | Loop: outer tag rendered once, **inner content** repeated |
|
|
73
|
+
| `data-sly-element="${expr}"` | Dynamic tag name (falls back to original tag) |
|
|
74
|
+
| `data-sly-unwrap` / `data-sly-unwrap="${cond}"` | Strips wrapper tag (always or conditionally) |
|
|
75
|
+
| `data-sly-set.varName="${expr}"` | Local variable declaration |
|
|
76
|
+
| `data-sly-text="${expr}"` | Replaces element inner content with expression |
|
|
77
|
+
| `data-sly-attribute.name="${expr}"` | Dynamic named attribute (null omits, true → valueless) |
|
|
78
|
+
| `data-sly-attribute="${obj}"` | Object spread as multiple attributes |
|
|
79
|
+
| `data-sly-template.name="${ @ params }"` | Named export function |
|
|
80
|
+
| `data-sly-call="${tmpl @ p=v}"` | Invokes a template function |
|
|
81
|
+
| `data-sly-resource="${expr}"` | Slot via `_includes` map |
|
|
82
|
+
| `data-sly-include="./file.html"` | Delegates to `_includes` map |
|
|
83
|
+
| `<sly>` | Transparent wrapper — only children are rendered |
|
|
84
|
+
|
|
85
|
+
Both `data-sly-repeat` and `data-sly-list` support bare forms (without `.varName`) that default to `item` as the iteration variable. They also provide a `${itemList}` status object with `index`, `count`, `first`, `last`, `odd`, and `even` properties.
|
|
86
|
+
|
|
87
|
+
### Expression conversions
|
|
88
|
+
|
|
89
|
+
| HTL | Generated JS |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `${expr @ context='html'}` | `${expr}` (context options stripped) |
|
|
92
|
+
| `${'string' @ i18n}` | `${_i18n?.['string'] ?? 'string'}` (dictionary lookup) |
|
|
93
|
+
| `${list.size}` | `${list.length}` |
|
|
94
|
+
| `${obj.jcr:title}` | `${obj?.['jcr:title']}` |
|
|
95
|
+
| `${tags @ join=', '}` | `${(tags).join(', ')}` |
|
|
96
|
+
| `${'pattern {0}/{1}' @ format=[a, b]}` | `${a + '/' + b}` |
|
|
97
|
+
| `${key in obj}` | `${(obj && key in obj)}` (null-safe) |
|
|
98
|
+
|
|
99
|
+
### HTML escaping
|
|
100
|
+
|
|
101
|
+
Attribute values are automatically escaped via the `_htlAttr` helper:
|
|
102
|
+
- `&` → `&`, `<` → `<`, `>` → `>`, `"` → `"`
|
|
103
|
+
- Object values are serialized with `JSON.stringify`
|
|
104
|
+
- `null`/`undefined` produce an empty string
|
|
105
|
+
|
|
106
|
+
Dynamic named attributes (`data-sly-attribute.name`) use `_htlDynAttr`:
|
|
107
|
+
- `null`/`false` → attribute omitted entirely
|
|
108
|
+
- `true` → valueless boolean attribute (e.g. `disabled`)
|
|
109
|
+
- Other values → `name="escaped-value"`
|
|
110
|
+
|
|
111
|
+
### AEM implicit objects
|
|
112
|
+
|
|
113
|
+
The following AEM implicit objects are automatically detected and added as optional parameters with safe defaults:
|
|
114
|
+
|
|
115
|
+
| Object | Default |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `wcmmode` | `{ edit: false, disabled: true, preview: false }` |
|
|
118
|
+
| `properties` | `{}` |
|
|
119
|
+
| `pageProperties` | `{}` |
|
|
120
|
+
| `inheritedPageProperties` | `{}` |
|
|
121
|
+
| `component` | `{}` |
|
|
122
|
+
| `currentDesign` | `{}` |
|
|
123
|
+
| `currentStyle` | `{}` |
|
|
124
|
+
| `currentPage` | `{}` |
|
|
125
|
+
| `resource` | `{}` |
|
|
126
|
+
| `model` | `{}` |
|
|
127
|
+
| `_includes` | `{}` |
|
|
128
|
+
| `_i18n` | `{}` |
|
|
129
|
+
| `request` | `{ requestPathInfo: { selectorString: '', suffix: '', resourcePath: '' }, contextPath: '' }` |
|
|
130
|
+
|
|
131
|
+
Variables declared via `data-sly-use.X` are always included as parameters. Any other free variables referenced in directive expressions are also detected and added as parameters with `{}` defaults.
|
|
132
|
+
|
|
133
|
+
### Automatic attribute stripping
|
|
134
|
+
|
|
135
|
+
The following AEM author-mode and analytics attributes are stripped by default:
|
|
136
|
+
|
|
137
|
+
- `data-cmp-data-layer` — analytics data layer JSON
|
|
138
|
+
- `data-placeholder-text` — author mode placeholder
|
|
139
|
+
- `data-panelcontainer` — author mode panel container
|
|
140
|
+
- `data-component-name` — AEM component tracking
|
|
141
|
+
- `data-region-id` — analytics region tracking
|
|
142
|
+
- `data-emptytext` — author mode empty text
|
|
143
|
+
|
|
144
|
+
> **Note:** `data-cmp-hook-*` attributes are **not** stripped by default because the AEM Core Components site JS uses them at runtime.
|
|
145
|
+
|
|
146
|
+
### Other features
|
|
147
|
+
|
|
148
|
+
- **Void elements** (`<br>`, `<img>`, `<input>`, etc.) are rendered as self-closing tags
|
|
149
|
+
- **HTL block comments** (`<!--/* ... */-->`) are stripped from output
|
|
150
|
+
- **Regular HTML comments** (`<!-- ... -->`) are preserved
|
|
151
|
+
- **Self-closing `<sly/>`** is expanded automatically
|
|
152
|
+
- **camelCase variable names** are preserved through parse5's lowercasing
|
|
153
|
+
- **Reserved words** (`class`, `for`) are escaped to `_class`, `_for` in generated JS
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Generated output
|
|
158
|
+
|
|
159
|
+
Given `accordion.html`:
|
|
160
|
+
|
|
161
|
+
```html
|
|
162
|
+
<div data-sly-use.accordion="com.example.Accordion"
|
|
163
|
+
class="cmp-accordion ${properties.theme}"
|
|
164
|
+
id="${accordion.id}">
|
|
165
|
+
<div data-sly-repeat.item="${accordion.items}"
|
|
166
|
+
data-sly-test="${accordion.items.size > 0}">
|
|
167
|
+
<span>${item.title}</span>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The loader generates:
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
// AUTO-GENERATED from accordion.html — DO NOT EDIT
|
|
176
|
+
|
|
177
|
+
const _htlAttr = (v) => v == null ? '' : (typeof v === 'object' ? JSON.stringify(v).replace(/"/g, '"') : String(v).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'));
|
|
178
|
+
const _htlDynAttr = (name, val) => { ... };
|
|
179
|
+
const _htlSpreadAttrs = (obj) => { ... };
|
|
180
|
+
|
|
181
|
+
const createAccordion = ({ accordion = {}, properties = {} } = {}) => {
|
|
182
|
+
return /* html */`<div class="cmp-accordion ${_htlAttr(properties?.theme)}" id="${_htlAttr(accordion?.id)}">${(accordion?.items?.length > 0) ? `${((accordion?.items) || []).map((item, _i, _arr) => { if (item == null) return ''; const itemList = { index: _i, count: _i + 1, first: _i === 0, last: _i === _arr.length - 1, odd: (_i + 1) % 2 !== 0, even: (_i + 1) % 2 === 0 }; return `<div><span>${(item?.title) ?? ''}</span></div>`; }).join('')}` : ''}</div>`;
|
|
183
|
+
};
|
|
184
|
+
module.exports = { createAccordion };
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Named templates (`data-sly-template`)
|
|
188
|
+
|
|
189
|
+
Files that define named templates generate one export per template:
|
|
190
|
+
|
|
191
|
+
```html
|
|
192
|
+
<!-- template/default.html -->
|
|
193
|
+
<template data-sly-template.default="${ @ model }">
|
|
194
|
+
<a class="template" href="${model.url}">
|
|
195
|
+
<h3>${model.title}</h3>
|
|
196
|
+
</a>
|
|
197
|
+
</template>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Generates:
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
const createDefault = ({ model = {} } = {}) => {
|
|
204
|
+
return /* html */`<a class="template" href="${_htlAttr(model?.url)}">
|
|
205
|
+
<h3>${(model?.title) ?? ''}</h3>
|
|
206
|
+
</a>`;
|
|
207
|
+
};
|
|
208
|
+
module.exports = { createDefault };
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## `data-sly-resource` (slots)
|
|
214
|
+
|
|
215
|
+
`data-sly-resource` loads a child JCR node at runtime in AEM — there is no equivalent in Storybook. The loader converts it to an `_includes` slot:
|
|
216
|
+
|
|
217
|
+
```html
|
|
218
|
+
<!-- HTL -->
|
|
219
|
+
<sly data-sly-resource="${'header'}"></sly>
|
|
220
|
+
|
|
221
|
+
<!-- With @path fallback -->
|
|
222
|
+
<sly data-sly-resource="${@ path=model.path}"></sly>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
```js
|
|
226
|
+
// Generated
|
|
227
|
+
${_includes?.['header']?.() ?? ''}
|
|
228
|
+
${_includes?.[model?.path]?.() ?? ''}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Pass content via `_includes` in your story args:
|
|
232
|
+
|
|
233
|
+
```js
|
|
234
|
+
export const Default = {
|
|
235
|
+
args: {
|
|
236
|
+
_includes: {
|
|
237
|
+
header: () => '<nav>Navigation</nav>',
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## `data-sly-call`
|
|
246
|
+
|
|
247
|
+
Calls a named template passing parameters. The binding declared via `data-sly-use` becomes a function parameter — pass the imported template module as its value.
|
|
248
|
+
|
|
249
|
+
```html
|
|
250
|
+
<!-- HTL -->
|
|
251
|
+
<sly data-sly-use.template="default.html"
|
|
252
|
+
data-sly-call="${template.default @ model=item}"></sly>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Generated:
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
${require('./default.html').createDefault?.({ model: item, _includes }) ?? ''}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
When the host element is not `<sly>`, the call output is wrapped in that element:
|
|
262
|
+
|
|
263
|
+
```html
|
|
264
|
+
<div class="wrapper" data-sly-call="${myFn @ text='Hi'}"></div>
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
```js
|
|
268
|
+
<div class="wrapper">${myFn?.({ text: 'Hi', _includes }) ?? ''}</div>
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
In the story, pass the imported template function:
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
import { createDefault } from '../default.html';
|
|
275
|
+
|
|
276
|
+
export const Default = {
|
|
277
|
+
args: {
|
|
278
|
+
template: { default: createDefault },
|
|
279
|
+
item: { title: 'Card Title', url: '/path' },
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## `data-sly-include`
|
|
287
|
+
|
|
288
|
+
Includes another HTL file at runtime. The loader generates a slot in the `_includes` map.
|
|
289
|
+
|
|
290
|
+
```html
|
|
291
|
+
<!-- Literal path -->
|
|
292
|
+
<sly data-sly-include="./header.html"></sly>
|
|
293
|
+
|
|
294
|
+
<!-- Dynamic path -->
|
|
295
|
+
<sly data-sly-include="${model.templatePath}"></sly>
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Generated:
|
|
299
|
+
|
|
300
|
+
```js
|
|
301
|
+
// Literal path
|
|
302
|
+
${_includes['./header.html']?.() ?? ''}
|
|
303
|
+
|
|
304
|
+
// Dynamic path
|
|
305
|
+
${_includes[model?.templatePath]?.() ?? ''}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
In the story:
|
|
309
|
+
|
|
310
|
+
```js
|
|
311
|
+
import { createHeader } from '../header.html';
|
|
312
|
+
|
|
313
|
+
export const Default = {
|
|
314
|
+
args: {
|
|
315
|
+
_includes: {
|
|
316
|
+
'./header.html': createHeader,
|
|
317
|
+
'./footer.html': () => '<footer>Footer content</footer>',
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## i18n (internationalization)
|
|
326
|
+
|
|
327
|
+
HTL expressions with `@ i18n` are converted into runtime dictionary lookups. Pass a JSON dictionary via the `_i18n` parameter to translate strings:
|
|
328
|
+
|
|
329
|
+
```html
|
|
330
|
+
<!-- HTL -->
|
|
331
|
+
<span>${'Read more' @ i18n}</span>
|
|
332
|
+
<a title="${'Go home' @ i18n}" href="/">...</a>
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Generated:
|
|
336
|
+
|
|
337
|
+
```js
|
|
338
|
+
<span>${_i18n?.['Read more'] ?? 'Read more'}</span>
|
|
339
|
+
<a title="${_htlAttr(_i18n?.['Go home'] ?? 'Go home')}" href="/">...</a>
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
In the story, pass the dictionary as `_i18n`:
|
|
343
|
+
|
|
344
|
+
```js
|
|
345
|
+
import dict from './i18n/es.json';
|
|
346
|
+
|
|
347
|
+
export const Spanish = {
|
|
348
|
+
args: {
|
|
349
|
+
_i18n: dict,
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Example `i18n/es.json`:
|
|
355
|
+
|
|
356
|
+
```json
|
|
357
|
+
{
|
|
358
|
+
"Read more": "Leer más",
|
|
359
|
+
"Go home": "Ir al inicio",
|
|
360
|
+
"Title": "Título"
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
When no dictionary is passed (or when a key is missing), the original string is used as fallback.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## `data-sly-repeat` vs `data-sly-list`
|
|
369
|
+
|
|
370
|
+
Both iterate over a list, but they differ in what gets repeated:
|
|
371
|
+
|
|
372
|
+
| | `data-sly-repeat` | `data-sly-list` |
|
|
373
|
+
|---|---|---|
|
|
374
|
+
| **Repeats** | The entire host element | Only inner content |
|
|
375
|
+
| **Outer tag** | Rendered once per item | Rendered once total |
|
|
376
|
+
|
|
377
|
+
```html
|
|
378
|
+
<!-- repeat: <li> repeated per item -->
|
|
379
|
+
<li data-sly-repeat.item="${items}">${item}</li>
|
|
380
|
+
|
|
381
|
+
<!-- list: <ul> once, <li> repeated per item -->
|
|
382
|
+
<ul data-sly-list.item="${items}"><li>${item}</li></ul>
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Both support:
|
|
386
|
+
- Null items are automatically skipped
|
|
387
|
+
- A `${itemList}` status object with `index`, `count`, `first`, `last`, `odd`, `even`
|
|
388
|
+
- Combined `data-sly-test.var` + `data-sly-repeat` on the same element (test var is hoisted before the loop in a scoped IIFE)
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## Options
|
|
393
|
+
|
|
394
|
+
Both the `transpile()` function and the webpack loader accept the following options:
|
|
395
|
+
|
|
396
|
+
### `omitAttrs`
|
|
397
|
+
|
|
398
|
+
Array of regular expressions matching attribute names to exclude from output.
|
|
399
|
+
|
|
400
|
+
**Override in webpack loader:**
|
|
401
|
+
|
|
402
|
+
```js
|
|
403
|
+
config.module.rules.push({
|
|
404
|
+
test: /\.html$/,
|
|
405
|
+
include: /jcr_root[\\/]apps/,
|
|
406
|
+
use: {
|
|
407
|
+
loader: require.resolve('htl-to-js/loader'),
|
|
408
|
+
options: {
|
|
409
|
+
omitAttrs: [
|
|
410
|
+
/^data-cmp-data-layer$/,
|
|
411
|
+
/^data-my-custom-attr/,
|
|
412
|
+
]
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Pass `omitAttrs: []` to disable filtering entirely.
|
|
419
|
+
|
|
420
|
+
### `modelTransforms`
|
|
421
|
+
|
|
422
|
+
Object mapping use-class patterns to property injections. Enables build-time property merging based on the `data-sly-use` class name.
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## Programmatic API
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
import { transpile } from 'htl-to-js';
|
|
430
|
+
import fs from 'fs';
|
|
431
|
+
|
|
432
|
+
const source = fs.readFileSync('accordion.html', 'utf8');
|
|
433
|
+
const jsModule = transpile(source, {
|
|
434
|
+
filename: 'accordion.html',
|
|
435
|
+
omitAttrs: [],
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
console.log(jsModule);
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## CLI
|
|
444
|
+
|
|
445
|
+
Generate `.template.js` files alongside their `.html` source:
|
|
446
|
+
|
|
447
|
+
```bash
|
|
448
|
+
npx htl-gen "src/**/*.html"
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Watch mode:
|
|
452
|
+
|
|
453
|
+
```bash
|
|
454
|
+
npx htl-gen --watch "components/**/*.html"
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Output files are placed next to the source:
|
|
458
|
+
```
|
|
459
|
+
accordion.html → accordion.template.js
|
|
460
|
+
card/default.html → card/default.template.js
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## Known limitations
|
|
466
|
+
|
|
467
|
+
- **`data-sly-include` with args** — passing parameters to included files (`@ wcmmode=wcmmode`) is not supported; only the path is used.
|
|
468
|
+
- **`data-sly-call` across files** — the called template must be imported and passed explicitly via args; cross-file resolution at build time is not supported unless the file is declared via `data-sly-use`.
|
|
469
|
+
- **Java expressions** in `data-sly-use` — the class path is ignored; the binding name becomes a function parameter.
|
|
470
|
+
- **`data-sly-use` with `@` defaults** — the default values are extracted as destructuring defaults, but complex expressions are not supported.
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## Development
|
|
475
|
+
|
|
476
|
+
Written in TypeScript. Source in `src/`, compiled to `dist/`.
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
npm run build # compile TypeScript
|
|
480
|
+
npm test # run 111 Jest tests
|
|
481
|
+
npm run test:watch # watch mode
|
|
482
|
+
npm run test:coverage # with coverage
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## License
|
|
488
|
+
|
|
489
|
+
[MIT](./LICENSE)
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const index_1 = require("./transpiler/index");
|
|
8
|
+
const glob_1 = require("glob");
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
if (!args.length || args.includes('--help') || args.includes('-h')) {
|
|
13
|
+
console.log(`
|
|
14
|
+
htl-gen — transpile HTL templates to JS template string functions
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
htl-gen <glob> Transpile matching files once
|
|
18
|
+
htl-gen --watch <glob> Watch and re-transpile on changes
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
htl-gen "components/**/*.html"
|
|
22
|
+
htl-gen accordion.html
|
|
23
|
+
htl-gen --watch "src/**/*.html"
|
|
24
|
+
`);
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
const watchMode = args.includes('--watch') || args.includes('-w');
|
|
28
|
+
const patternArg = args.find(a => !a.startsWith('-'));
|
|
29
|
+
if (!patternArg) {
|
|
30
|
+
console.error('Error: no glob pattern provided.');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const pattern = patternArg;
|
|
34
|
+
function processFile(file) {
|
|
35
|
+
try {
|
|
36
|
+
const source = node_fs_1.default.readFileSync(file, 'utf8');
|
|
37
|
+
const output = (0, index_1.transpile)(source, { filename: file });
|
|
38
|
+
const outFile = file.replace(/\.html$/, '.template.js');
|
|
39
|
+
node_fs_1.default.writeFileSync(outFile, output, 'utf8');
|
|
40
|
+
console.log(`✓ ${node_path_1.default.relative(process.cwd(), file)} → ${node_path_1.default.basename(outFile)}`);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
console.error(`✗ ${node_path_1.default.relative(process.cwd(), file)}: ${err.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function main() {
|
|
47
|
+
const files = await (0, glob_1.glob)(pattern, { absolute: true });
|
|
48
|
+
if (!files.length) {
|
|
49
|
+
console.warn(`No files matched: ${pattern}`);
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
for (const file of files)
|
|
53
|
+
processFile(file);
|
|
54
|
+
if (watchMode) {
|
|
55
|
+
console.log(`\nWatching ${files.length} file(s) for changes…\n`);
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
node_fs_1.default.watch(file, () => {
|
|
58
|
+
console.log(`↻ ${node_path_1.default.relative(process.cwd(), file)} changed`);
|
|
59
|
+
processFile(file);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
main(); // NOSONAR
|
|
65
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";;;;;;AAEA,8CAA+C;AAC/C,+BAA4B;AAC5B,sDAAyB;AACzB,0DAA6B;AAE7B,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IACnE,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;CAWb,CAAC,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AAClE,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;AAEtD,IAAI,CAAC,UAAU,EAAE,CAAC;IAChB,OAAO,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;IAClD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,OAAO,GAAW,UAAU,CAAC;AAEnC,SAAS,WAAW,CAAC,IAAY;IAC/B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,iBAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC7C,MAAM,MAAM,GAAG,IAAA,iBAAS,EAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;QACxD,iBAAE,CAAC,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;QAC1C,OAAO,CAAC,GAAG,CAAC,MAAM,mBAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,MAAM,mBAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACtF,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,OAAO,CAAC,KAAK,CAAC,MAAM,mBAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;IAC5E,CAAC;AACH,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,KAAK,GAAG,MAAM,IAAA,WAAI,EAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IAEtD,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QAClB,OAAO,CAAC,IAAI,CAAC,qBAAqB,OAAO,EAAE,CAAC,CAAC;QAC7C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,WAAW,CAAC,IAAI,CAAC,CAAC;IAE5C,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,CAAC,GAAG,CAAC,cAAc,KAAK,CAAC,MAAM,yBAAyB,CAAC,CAAC;QACjE,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,iBAAE,CAAC,KAAK,CAAC,IAAI,EAAE,GAAG,EAAE;gBAClB,OAAO,CAAC,GAAG,CAAC,MAAM,mBAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;gBAChE,WAAW,CAAC,IAAI,CAAC,CAAC;YACpB,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,CAAC,UAAU"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { transpile } from './transpiler/index';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.transpile = void 0;
|
|
4
|
+
var index_1 = require("./transpiler/index");
|
|
5
|
+
Object.defineProperty(exports, "transpile", { enumerable: true, get: function () { return index_1.transpile; } });
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,4CAA+C;AAAtC,kGAAA,SAAS,OAAA"}
|
package/dist/loader.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webpack loader for HTL templates.
|
|
3
|
+
*
|
|
4
|
+
* Configuration (webpack.config.js or .storybook/main.js):
|
|
5
|
+
*
|
|
6
|
+
* module: {
|
|
7
|
+
* rules: [{
|
|
8
|
+
* test: /\.html$/,
|
|
9
|
+
* include: /jcr_root[\\/]apps/,
|
|
10
|
+
* use: 'htl-to-js/loader',
|
|
11
|
+
* }]
|
|
12
|
+
* }
|
|
13
|
+
*
|
|
14
|
+
* Then in your Storybook stories:
|
|
15
|
+
*
|
|
16
|
+
* import { createAccordion } from '../path/to/accordion.html';
|
|
17
|
+
*/
|
|
18
|
+
declare function htlLoader(this: any, source: string): string;
|
|
19
|
+
export = htlLoader;
|
package/dist/loader.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const index_1 = require("./transpiler/index");
|
|
3
|
+
/**
|
|
4
|
+
* Webpack loader for HTL templates.
|
|
5
|
+
*
|
|
6
|
+
* Configuration (webpack.config.js or .storybook/main.js):
|
|
7
|
+
*
|
|
8
|
+
* module: {
|
|
9
|
+
* rules: [{
|
|
10
|
+
* test: /\.html$/,
|
|
11
|
+
* include: /jcr_root[\\/]apps/,
|
|
12
|
+
* use: 'htl-to-js/loader',
|
|
13
|
+
* }]
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* Then in your Storybook stories:
|
|
17
|
+
*
|
|
18
|
+
* import { createAccordion } from '../path/to/accordion.html';
|
|
19
|
+
*/
|
|
20
|
+
function htlLoader(source) {
|
|
21
|
+
this.cacheable(true);
|
|
22
|
+
const options = this.getOptions ? this.getOptions() : {};
|
|
23
|
+
try {
|
|
24
|
+
return (0, index_1.transpile)(source, { filename: this.resourcePath, ...options });
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
this.emitError(new Error(`[htl-to-js] ${this.resourcePath}: ${err.message}`));
|
|
28
|
+
return 'module.exports = {};';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
module.exports = htlLoader;
|
|
32
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":";AAAA,8CAA+C;AAE/C;;;;;;;;;;;;;;;;GAgBG;AACH,SAAS,SAAS,CAAY,MAAc;IAC1C,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IAErB,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAEzD,IAAI,CAAC;QACH,OAAO,IAAA,iBAAS,EAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;IACxE,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,IAAI,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,eAAe,IAAI,CAAC,YAAY,KAAK,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;QAC9E,OAAO,sBAAsB,CAAC;IAChC,CAAC;AACH,CAAC;AAED,iBAAS,SAAS,CAAC"}
|