templa-js 0.10.0 → 0.10.1
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/AGENTS.md +4 -2
- package/README.md +5 -3
- package/bin/templa.js +17 -7
- package/package.json +1 -1
- package/templa.js +28 -8
package/AGENTS.md
CHANGED
|
@@ -203,6 +203,8 @@ Reserved attributes — these are NOT collected as data:
|
|
|
203
203
|
|
|
204
204
|
Any `data-*` attribute on a `<template>` is also skipped from data collection — it's reserved as HTML metadata convention.
|
|
205
205
|
|
|
206
|
+
**Keys are case-insensitive.** HTML attribute names are case-insensitive in the spec, and the browser DOM lowercases them automatically. templa mirrors this by normalising both the attribute name and the `{{var}}` lookup to lowercase, so `<template ctaLabel="X">` paired with `{{ctaLabel}}` works in both runtime and build mode (both resolve via the lowercased key `ctalabel`). The convention in this codebase is **kebab-case** (`cta-label`, `og-image`, `hero-bg-color`) — it survives unchanged through every layer and reads as idiomatic HTML.
|
|
207
|
+
|
|
206
208
|
Strings handle every common case. For conditionals, the value is checked existentially (truthy unless empty), so `featured="yes"` is enough. There's no typed-value escape hatch in the core; if a project needs typed values (numbers, booleans, arrays, objects, computed values), that goes through a plugin.
|
|
207
209
|
|
|
208
210
|
A few patterns to avoid (they are silently ignored):
|
|
@@ -261,8 +263,8 @@ When to use: any partial whose presence implies its own visual rules. Co-locatin
|
|
|
261
263
|
**Existence-based only.** The value of `if`/`unless` is a single key looked up in the surrounding data. There are no expressions, no operators, no helpers, no `else`. If you need an else branch, write the inverse pair:
|
|
262
264
|
|
|
263
265
|
```html
|
|
264
|
-
<template if="
|
|
265
|
-
<template unless="
|
|
266
|
+
<template if="logged-in"><a href="/logout">Logout</a></template>
|
|
267
|
+
<template unless="logged-in"><a href="/login">Login</a></template>
|
|
266
268
|
```
|
|
267
269
|
|
|
268
270
|
Conditionals can be nested. Resolution iterates until stable.
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ It works in two modes:
|
|
|
13
13
|
```html
|
|
14
14
|
<!-- index.html -->
|
|
15
15
|
<body>
|
|
16
|
-
<template src="_partials/header.html" title="Home"
|
|
16
|
+
<template src="_partials/header.html" title="Home" logged-in="yes"></template>
|
|
17
17
|
<main>...</main>
|
|
18
18
|
<template src="_partials/footer.html"></template>
|
|
19
19
|
|
|
@@ -26,10 +26,10 @@ It works in two modes:
|
|
|
26
26
|
<!-- _partials/header.html -->
|
|
27
27
|
<header>
|
|
28
28
|
<h1>{{title}}</h1>
|
|
29
|
-
<template if="
|
|
29
|
+
<template if="logged-in">
|
|
30
30
|
<a href="/logout">Logout</a>
|
|
31
31
|
</template>
|
|
32
|
-
<template unless="
|
|
32
|
+
<template unless="logged-in">
|
|
33
33
|
<a href="/login">Login</a>
|
|
34
34
|
</template>
|
|
35
35
|
</header>
|
|
@@ -111,6 +111,8 @@ Conditionals (`<template if="key">`) are existence-based, so any non-empty strin
|
|
|
111
111
|
|
|
112
112
|
Reserved attributes — these are not collected as data: `src` (the partial path), `slot` (slot filler name), `if` / `unless` (conditional markers). Any `data-*` attribute is also skipped, by HTML metadata convention.
|
|
113
113
|
|
|
114
|
+
**Keys are case-insensitive.** HTML attribute names are case-insensitive in the spec and the browser DOM lowercases them, so templa normalises both the attribute name and the `{{var}}` lookup to lowercase. `<template ctaLabel="X">` and `{{ctaLabel}}` both resolve via `ctalabel` and behave identically in runtime and build mode. **Use kebab-case** (`cta-label`, `og-image`, `hero-bg-color`) — it survives every layer unchanged and reads as idiomatic HTML.
|
|
115
|
+
|
|
114
116
|
### Template syntax
|
|
115
117
|
|
|
116
118
|
| Syntax | Effect |
|
package/bin/templa.js
CHANGED
|
@@ -34,17 +34,27 @@ const escHtml = s => String(s)
|
|
|
34
34
|
.replace(/"/g, '"')
|
|
35
35
|
.replace(/'/g, ''');
|
|
36
36
|
|
|
37
|
+
// Data keys are case-insensitive to mirror HTML's own attribute
|
|
38
|
+
// semantics (browsers lowercase attribute names in the DOM). Both
|
|
39
|
+
// <template ctaLabel="X"> and {{ctaLabel}} normalize to "ctalabel"
|
|
40
|
+
// so kebab-case, camelCase, and PascalCase all work the same.
|
|
37
41
|
function render(html, data) {
|
|
38
42
|
return html
|
|
39
|
-
.replace(/{{{\s*([\w-]+)\s*}}}/g, (m, k) =>
|
|
40
|
-
|
|
43
|
+
.replace(/{{{\s*([\w-]+)\s*}}}/g, (m, k) => {
|
|
44
|
+
const lk = k.toLowerCase();
|
|
45
|
+
return lk in data ? data[lk] : m;
|
|
46
|
+
})
|
|
47
|
+
.replace(/{{\s*([\w-]+)\s*}}/g, (m, k) => {
|
|
48
|
+
const lk = k.toLowerCase();
|
|
49
|
+
return lk in data ? escHtml(data[lk]) : m;
|
|
50
|
+
});
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
// ─── attribute / template / slot parsing ─────────────────────────────
|
|
44
54
|
function getAttr(attrs, name) {
|
|
45
|
-
const dq = attrs.match(new RegExp(`\\b${name}\\s*=\\s*"([^"]*)"
|
|
55
|
+
const dq = attrs.match(new RegExp(`\\b${name}\\s*=\\s*"([^"]*)"`, 'i'));
|
|
46
56
|
if (dq) return dq[1];
|
|
47
|
-
const sq = attrs.match(new RegExp(`\\b${name}\\s*=\\s*'([^']*)'
|
|
57
|
+
const sq = attrs.match(new RegExp(`\\b${name}\\s*=\\s*'([^']*)'`, 'i'));
|
|
48
58
|
return sq ? sq[1] : null;
|
|
49
59
|
}
|
|
50
60
|
|
|
@@ -128,7 +138,7 @@ function collectData(attrs) {
|
|
|
128
138
|
ATTR.lastIndex = 0;
|
|
129
139
|
let m;
|
|
130
140
|
while ((m = ATTR.exec(attrs))) {
|
|
131
|
-
const name = m[1];
|
|
141
|
+
const name = m[1].toLowerCase();
|
|
132
142
|
if (RESERVED.has(name) || name.startsWith('data-')) continue;
|
|
133
143
|
data[name] = m[2] ?? m[3] ?? '';
|
|
134
144
|
}
|
|
@@ -211,9 +221,9 @@ function applyConditionals(html, data) {
|
|
|
211
221
|
const ifKey = getAttr(b.attrs, 'if');
|
|
212
222
|
const unlessKey = getAttr(b.attrs, 'unless');
|
|
213
223
|
if (ifKey !== null) {
|
|
214
|
-
html = html.slice(0, b.start) + (data[ifKey] ? b.inner : '') + html.slice(b.end);
|
|
224
|
+
html = html.slice(0, b.start) + (data[ifKey.toLowerCase()] ? b.inner : '') + html.slice(b.end);
|
|
215
225
|
} else if (unlessKey !== null) {
|
|
216
|
-
html = html.slice(0, b.start) + (!data[unlessKey] ? b.inner : '') + html.slice(b.end);
|
|
226
|
+
html = html.slice(0, b.start) + (!data[unlessKey.toLowerCase()] ? b.inner : '') + html.slice(b.end);
|
|
217
227
|
}
|
|
218
228
|
}
|
|
219
229
|
} while (html !== prev);
|
package/package.json
CHANGED
package/templa.js
CHANGED
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
*
|
|
25
25
|
* <template src="card.html" title="Tiny" body="Light"></template>
|
|
26
26
|
*
|
|
27
|
+
* - Keys are case-insensitive. HTML attribute names are case-
|
|
28
|
+
* insensitive in the spec and the browser DOM lowercases them, so
|
|
29
|
+
* templa normalises both sides: `<template ctaLabel="X">` and
|
|
30
|
+
* `{{ctaLabel}}` both resolve via the lowercased key `ctalabel`.
|
|
31
|
+
* Use kebab-case (`cta-label`) for HTML idiomaticity.
|
|
32
|
+
*
|
|
27
33
|
* Syntax:
|
|
28
34
|
* {{key}} HTML-escaped variable
|
|
29
35
|
* {{{key}}} raw variable (no escape)
|
|
@@ -87,9 +93,19 @@ const templa = (() => {
|
|
|
87
93
|
.replace(/"/g, '"')
|
|
88
94
|
.replace(/'/g, ''');
|
|
89
95
|
|
|
96
|
+
// Data keys are case-insensitive to mirror HTML's own attribute
|
|
97
|
+
// semantics (browsers lowercase attribute names in the DOM). Both
|
|
98
|
+
// <template ctaLabel="X"> and {{ctaLabel}} normalize to "ctalabel"
|
|
99
|
+
// so kebab-case, camelCase, and PascalCase all work the same.
|
|
90
100
|
const render = (html, data) => html
|
|
91
|
-
.replace(/{{{\s*([\w-]+)\s*}}}/g, (m, k) =>
|
|
92
|
-
|
|
101
|
+
.replace(/{{{\s*([\w-]+)\s*}}}/g, (m, k) => {
|
|
102
|
+
const lk = k.toLowerCase();
|
|
103
|
+
return lk in data ? data[lk] : m;
|
|
104
|
+
})
|
|
105
|
+
.replace(/{{\s*([\w-]+)\s*}}/g, (m, k) => {
|
|
106
|
+
const lk = k.toLowerCase();
|
|
107
|
+
return lk in data ? esc(data[lk]) : m;
|
|
108
|
+
});
|
|
93
109
|
|
|
94
110
|
const rebase = (html, baseUrl) => html.replace(
|
|
95
111
|
/(<template\b[^>]*\bsrc\s*=\s*["'])([^"']+)/gi,
|
|
@@ -99,9 +115,9 @@ const templa = (() => {
|
|
|
99
115
|
// Read an attribute value out of a raw attribute string. Tries double then
|
|
100
116
|
// single quoting so values containing the other quote survive intact.
|
|
101
117
|
const getAttr = (attrs, name) => {
|
|
102
|
-
const dq = attrs.match(new RegExp(`\\b${name}\\s*=\\s*"([^"]*)"
|
|
118
|
+
const dq = attrs.match(new RegExp(`\\b${name}\\s*=\\s*"([^"]*)"`, 'i'));
|
|
103
119
|
if (dq) return dq[1];
|
|
104
|
-
const sq = attrs.match(new RegExp(`\\b${name}\\s*=\\s*'([^']*)'
|
|
120
|
+
const sq = attrs.match(new RegExp(`\\b${name}\\s*=\\s*'([^']*)'`, 'i'));
|
|
105
121
|
return sq ? sq[1] : null;
|
|
106
122
|
};
|
|
107
123
|
|
|
@@ -184,9 +200,9 @@ const templa = (() => {
|
|
|
184
200
|
const ifKey = getAttr(b.attrs, 'if');
|
|
185
201
|
const unlessKey = getAttr(b.attrs, 'unless');
|
|
186
202
|
if (ifKey !== null) {
|
|
187
|
-
html = html.slice(0, b.start) + (data[ifKey] ? b.inner : '') + html.slice(b.end);
|
|
203
|
+
html = html.slice(0, b.start) + (data[ifKey.toLowerCase()] ? b.inner : '') + html.slice(b.end);
|
|
188
204
|
} else if (unlessKey !== null) {
|
|
189
|
-
html = html.slice(0, b.start) + (!data[unlessKey] ? b.inner : '') + html.slice(b.end);
|
|
205
|
+
html = html.slice(0, b.start) + (!data[unlessKey.toLowerCase()] ? b.inner : '') + html.slice(b.end);
|
|
190
206
|
}
|
|
191
207
|
}
|
|
192
208
|
} while (html !== prev);
|
|
@@ -195,12 +211,16 @@ const templa = (() => {
|
|
|
195
211
|
|
|
196
212
|
// Every attribute is a string data key, except: src/slot/if/unless are
|
|
197
213
|
// reserved, and any data-* attribute is treated as metadata (skipped).
|
|
214
|
+
// Keys are stored lowercased — HTML attribute names are case-insensitive
|
|
215
|
+
// and the browser DOM already lowercases them, so we mirror that on
|
|
216
|
+
// the build side too.
|
|
198
217
|
const RESERVED = new Set(['src', 'slot', 'if', 'unless']);
|
|
199
218
|
const collectData = el => {
|
|
200
219
|
const data = {};
|
|
201
220
|
for (const a of el.attributes) {
|
|
202
|
-
|
|
203
|
-
|
|
221
|
+
const n = a.name.toLowerCase();
|
|
222
|
+
if (RESERVED.has(n) || n.startsWith('data-')) continue;
|
|
223
|
+
data[n] = a.value;
|
|
204
224
|
}
|
|
205
225
|
return data;
|
|
206
226
|
};
|