domma-cms 0.102.0 → 0.102.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.102.0",
3
+ "version": "0.102.1",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -176,8 +176,10 @@ export function formatDate(value, fmt) {
176
176
  export function interpolate(tpl, vars) {
177
177
  return String(tpl ?? '')
178
178
  .replace(/<!--[\s\S]*?-->/g, '')
179
- .replace(/\{\{\{(\w+)\}\}\}/g, (_, k) => String(vars[k] ?? ''))
180
- .replace(/\{\{(\w+)(?::([^{}]+))?\}\}/g, (_, k, fmt) => {
179
+ // One pass, so a value put in is never read again for placeholders: a post that
180
+ // shows "{{date}}" in its text keeps it, instead of getting today's date.
181
+ .replace(/\{\{\{(\w+)\}\}\}|\{\{(\w+)(?::([^{}]+))?\}\}/g, (_, raw, k, fmt) => {
182
+ if (raw !== undefined) return String(vars[raw] ?? '');
181
183
  const v = vars[k] ?? '';
182
184
  if (fmt) {
183
185
  const f = formatDate(v, fmt);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "blog",
3
3
  "displayName": "Blog",
4
- "version": "1.9.3",
4
+ "version": "1.9.4",
5
5
  "tier": "free",
6
6
  "description": "Public-facing blog with posts, categories and comments - layouts and listings built from editable blocks.",
7
7
  "author": "Domma CMS",
@@ -130,3 +130,9 @@ test('a card shows its date as dd/mm/yyyy; the machine value stays ISO', () => {
130
130
  assert.equal(v.date, '07/09/2026');
131
131
  assert.equal(v.publishedAt, '2026-09-07T10:00:00Z');
132
132
  });
133
+
134
+ test('interpolate: a raw value is never read again for placeholders', () => {
135
+ // A post that shows "{{date}}" in its text keeps it.
136
+ assert.equal(interpolate('<div>{{{content}}}</div><p>{{title}}</p>', {content: '<code>{{date}} {{title}}</code>', title: 'T', date: 'X'}),
137
+ '<div><code>{{date}} {{title}}</code></div><p>T</p>');
138
+ });
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "note": "Written by `make sync-free-tier` in dcms-marketplace. Do not edit these plugins here - edit them there and sync.",
3
- "source": "dcms-marketplace@45a0ace",
3
+ "source": "dcms-marketplace@f79220a",
4
4
  "plugins": {
5
5
  "blog": {
6
- "version": "1.9.3",
6
+ "version": "1.9.4",
7
7
  "files": {
8
8
  "CLAUDE.md": "926730d510cc38f6764ffcb9afbed1c3ae3a547117c6c0243b14f899271350fb",
9
9
  "admin/css/index.css": "6142a6b756e1218157842528830b1e84f592f0b84b7d5ce6449db421b6f4b039",
@@ -43,14 +43,14 @@
43
43
  "config.js": "ee67396262762f5c31269d258affd75f7392a3e364f72f7792c2f1cdafd94bba",
44
44
  "docs/guide.md": "6a7466881851797789a18e0d32c4fcb515f2259b110948c55be03923c38e0de5",
45
45
  "lib/access.js": "af47440c7233a56000641a3231d7e9ff81b12f40dfb9528aa6dd4e2dd9bca8df",
46
- "lib/layouts.js": "0a921fb427c5ffe0a31eb687a6f8f10cb5573a1ad638a88ce386b687b49a2692",
46
+ "lib/layouts.js": "dbb6ae9e514ccaf600a732702925b905131dea166d90fe759a62b6652e78e1de",
47
47
  "lib/nav-link.js": "6a8f9368bb8aee1c21e90fb335a691859cb9a9bc250c2b9167a33ca9189fa2ff",
48
48
  "lib/notices.js": "a9a2918ee6f5f7e0bbaeced0d3539584279bdbe68d15472a8615bdddb4ef905b",
49
49
  "lib/page.js": "bfb6a355a027feaf775d6d8ac871fc6d999354774e018f8e16caec551e99f40a",
50
50
  "lib/render.js": "0e97c6a950bc962c8d8ea4b2f21d06cb73fbcdc738d8942d6935d01309a29b1d",
51
51
  "lib/samples.js": "e8cdb77ce22e43937707f6854d3a5cb722437dc584e22ac7b24480bef686cf85",
52
52
  "plugin.js": "0aa46a01b5298065497f39729b88e34a9f56ac684b0a14f8d94ab6007f0221d0",
53
- "plugin.json": "f74ba12c5dfef7d0b9accd267cc0c4e333ea61a936cc8afa565db82da5b02389",
53
+ "plugin.json": "7bfba248651527bb98fa388a66234a53e9808c6a28e878504a1fa9fae216b658",
54
54
  "plugin.public.js": "67e109da613237d9ec52e7cc2f3442bb61ca5a51cb184448b9f0d993e50f7ae3",
55
55
  "public/blog.css": "fd733e6b624190da1e70dc1b6613e2376efd7f7570a28fab311d81006d03d960",
56
56
  "public/blog.js": "d01c72a9834dcc7c5241923371f2b164ac30d20ac9f1a5c5dc5b71fa3cb09f08",
@@ -66,7 +66,7 @@
66
66
  "templates/post.html": "28e9f06071115ecc4fbba007b35782732bf12ef72528525893d424e3c4f0313f",
67
67
  "tests/access.test.js": "65b760910117c3a44195543acdcff91074f39b4e5567911a2bd8676204152cb2",
68
68
  "tests/comment-notices.test.js": "34026cc9a2a4746cf87bc41df8be849fadaa43091a72ac2aa1d06c1f437bf618",
69
- "tests/layouts.test.js": "4219d0e1dab7ed11d176e8eb55086531fc4eaf375cdf3d7aea5b85073d2ace7e",
69
+ "tests/layouts.test.js": "554cff8194bd650cfb5f2a83c1f51c25f427c07f7dbfdc2ad9ad69bb38fca6a0",
70
70
  "tests/nav-link.test.js": "d36e33ad7483abca27ae6e2098ad2f75bafa18d91f3beba70372ebe6b9b88994",
71
71
  "tests/notices.test.js": "2b02aa84e780dc4c82e6c503fc8fe17c64a63bee0709627866ef97c99cb4e131",
72
72
  "tests/public.test.js": "a6fcdc4ede242aa050ffe5a52f970d6bec1f4a70ae78967eb8a7bdeec73df19d"
@@ -330,9 +330,8 @@ export function buildBlockStyleTag(blockName, css) {
330
330
  * Interpolate a block template against a single collection entry.
331
331
  *
332
332
  * Triple-brace `{{{field}}}` - raw HTML pass-through (no escape). Use for
333
- * fields that contain pre-rendered HTML. Must be processed BEFORE double-brace
334
- * so the outer `{`/`}` of a triple match aren't consumed by the double-brace
335
- * regex. Double-brace `{{field}}` - HTML-escaped (default, safe), with an
333
+ * fields that contain pre-rendered HTML. Both kinds are filled in ONE pass, so
334
+ * an inserted value is never scanned for placeholders again. Double-brace `{{field}}` - HTML-escaped (default, safe), with an
336
335
  * optional `:format` suffix (e.g. `{{_createdAt:DD MMM YYYY}}`) that applies
337
336
  * moment-style date formatting when the value parses as a Date.
338
337
  *
@@ -341,10 +340,10 @@ export function buildBlockStyleTag(blockName, css) {
341
340
  * @returns {string}
342
341
  */
343
342
  function interpolateBlockTemplate(blockTemplate, e) {
344
- let html = blockTemplate.replace(/\{\{\{(\w+)\}\}\}/g, (_, key) => {
345
- return String(e.data?.[key] ?? '');
346
- });
347
- html = html.replace(/\{\{([\w_]+)(?::([^{}]+))?\}\}/g, (_, key, fmt) => {
343
+ // One pass: a raw value put in (pre-rendered HTML) is never read again for
344
+ // placeholders - text that shows "{{title}}" keeps it.
345
+ return blockTemplate.replace(/\{\{\{(\w+)\}\}\}|\{\{([\w_]+)(?::([^{}]+))?\}\}/g, (_, raw, key, fmt) => {
346
+ if (raw !== undefined) return String(e.data?.[raw] ?? '');
348
347
  let value;
349
348
  if (key === '_id') value = e.id ?? '';
350
349
  else if (key === '_createdAt') value = e.meta?.createdAt ?? '';
@@ -356,7 +355,6 @@ function interpolateBlockTemplate(blockTemplate, e) {
356
355
  }
357
356
  return escapeHtmlText(value);
358
357
  });
359
- return html;
360
358
  }
361
359
 
362
360
  /**
@@ -408,10 +406,10 @@ function renderCollectionBlocks(entries, blockTemplate, emptyMsg, ctaOpts, cols,
408
406
  }
409
407
 
410
408
  function renderBlockFromAttrs(tmpl, attrs, blockName = '') {
411
- // Triple-brace raw pass-through first, then double-brace escaped.
412
- let rendered = tmpl.replace(/\{\{\{(\w+)\}\}\}/g, (_, key) => String(attrs[key] ?? ''));
413
- // `{{field}}` with optional `:format` suffix (e.g. `{{date:DD MMM YYYY}}`).
414
- rendered = rendered.replace(/\{\{([\w_]+)(?::([^{}]+))?\}\}/g, (_, key, fmt) => {
409
+ // Triple-brace raw pass-through and `{{field}}` (optional `:format`, e.g. `{{date:DD MMM YYYY}}`)
410
+ // in one pass, so a raw value is never read again for placeholders.
411
+ const rendered = tmpl.replace(/\{\{\{(\w+)\}\}\}|\{\{([\w_]+)(?::([^{}]+))?\}\}/g, (_, rawKey, key, fmt) => {
412
+ if (rawKey !== undefined) return String(attrs[rawKey] ?? '');
415
413
  const raw = attrs[key] ?? '';
416
414
  if (fmt) {
417
415
  const formatted = formatDate(raw, fmt);