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 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="loggedIn"><a href="/logout">Logout</a></template>
265
- <template unless="loggedIn"><a href="/login">Login</a></template>
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" loggedIn="yes"></template>
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="loggedIn">
29
+ <template if="logged-in">
30
30
  <a href="/logout">Logout</a>
31
31
  </template>
32
- <template unless="loggedIn">
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, '&quot;')
35
35
  .replace(/'/g, '&#39;');
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) => k in data ? data[k] : m)
40
- .replace(/{{\s*([\w-]+)\s*}}/g, (m, k) => k in data ? escHtml(data[k]) : m);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "templa-js",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "A tiny HTML template loader using <template src>. Read as tempura.",
5
5
  "main": "templa.js",
6
6
  "scripts": {
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, '&quot;')
88
94
  .replace(/'/g, '&#39;');
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) => k in data ? data[k] : m)
92
- .replace(/{{\s*([\w-]+)\s*}}/g, (m, k) => k in data ? esc(data[k]) : m);
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
- if (RESERVED.has(a.name) || a.name.startsWith('data-')) continue;
203
- data[a.name] = a.value;
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
  };