domma-cms 1.8.5 → 1.8.6
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/CHANGELOG.md +4 -0
- package/CLAUDE.md +7 -0
- package/package.json +1 -1
- package/server/services/markdown.js +60 -23
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.8.6] - 2026-10-16
|
|
4
|
+
|
|
5
|
+
- **Security: what is stored in a collection no longer runs as a shortcode.** A `[collection]` or `[view]` used to pass its entries on to the later shortcode steps, so an entry - a public form submission, say - holding `[form]`, `[button]`, `[embed]`, `[slideover]` or another `[collection]` was carried out on the page. Entry values now show exactly as typed, in every display type and in `{{field}}` / `{{{field}}}` block placeholders, while the page's own shortcodes and those written into a block template work as before. Filter, search and export still see the stored values. A value containing `$&` or `` $` `` is also shown as written instead of pulling in other parts of the page.
|
|
6
|
+
|
|
3
7
|
## [1.8.5] - 2026-10-16
|
|
4
8
|
|
|
5
9
|
- **Import from WordPress brings custom post types across.** Content a theme or plugin added to WordPress - events, team members, portfolio pieces - now has its own Custom types section in the preview, with how many there are, a few titles, and what each will become. By default a type becomes a collection (wp-event, wp-team-member…) holding each item's title, address, text, excerpt, date, status and featured image, its categories, and every custom field it had, Advanced Custom Fields included - dates, numbers, links and pictures recognised as such. A list page at the type's old address (/events) shows the published ones, and the old single-item addresses and `?p=` links lead there; drafts come across but are never shown or readable from the site. Or choose Pages - each item a page at its old address, its categories as tags - or Skip. The type's address is read from the export's own links, never guessed. WordPress's and plugins' own housekeeping (revisions, reusable blocks, templates, form definitions, orders…) is listed once with why it is not imported. Run it again and entries you edited here are left alone unless you choose Overwrite, and a collection only ever gains fields - your own fields and labels stay.
|
package/CLAUDE.md
CHANGED
|
@@ -80,6 +80,13 @@ Containers that render their own body (tabs, accordion, carousel, timeline, hero
|
|
|
80
80
|
wrappers) run the inline tags plus `[grid]`/`[card]` over it first - keep that set in step with
|
|
81
81
|
`processInlineShortcodes()` when adding an inline tag. The full order is in `docs/markdown-shortcodes.md`.
|
|
82
82
|
|
|
83
|
+
**Entry data never runs as a shortcode.** `[collection]` / `[view]` output carries entry values into stages 3-10,
|
|
84
|
+
so every value they draw goes through `inertShortcodes()` (the character after each `[` becomes an entity, so
|
|
85
|
+
`[form` is `[form` and nothing matches, while Markdown links in values still work): displays with no authored
|
|
86
|
+
markup encode their whole output, block templates encode each `{{field}}` / `{{{field}}}` value and leave the
|
|
87
|
+
template's own shortcodes live. A new display or placeholder must do the same. The base64 `data-ctx` / table
|
|
88
|
+
payloads stay raw (no brackets in base64), so filter, search and export see the stored value.
|
|
89
|
+
|
|
83
90
|
Safe nesting: `[card]` inside `[slideover]`, `[grid]` inside `[slideover]`, `[col]` inside `[grid]`, `[card]` inside
|
|
84
91
|
`[box]` and `[box]` inside `[card]`.
|
|
85
92
|
Do NOT nest `[slideover]` inside another `[slideover]`.
|
package/package.json
CHANGED
|
@@ -57,6 +57,37 @@ marked.setOptions({
|
|
|
57
57
|
// Collection shortcode helpers
|
|
58
58
|
// ---------------------------------------------------------------------------
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Make text from a collection entry inert to the shortcode stages that run
|
|
62
|
+
* after `[collection]` / `[view]`.
|
|
63
|
+
*
|
|
64
|
+
* Entry values are DATA - public form submissions among them - and the
|
|
65
|
+
* rendered display is spliced into the page before tabs, form, embed, badge,
|
|
66
|
+
* button, card, slideover and the rest run, so a stored `[form id="x" /]`
|
|
67
|
+
* would otherwise be executed on the public page. Applied to the HTML that
|
|
68
|
+
* carries entry values (escaped text, attribute values, a rendered Markdown
|
|
69
|
+
* body, a `{{{raw}}}` placeholder), never to authored markup: a block
|
|
70
|
+
* template's own shortcodes still run.
|
|
71
|
+
*
|
|
72
|
+
* Every shortcode pattern starts `[name`, `[/name` or `[ name`, so the
|
|
73
|
+
* character after each `[` is written as a numeric entity (`[form` becomes
|
|
74
|
+
* `[form`) and a trailing `[` as `[`. Nothing can match, yet the
|
|
75
|
+
* brackets themselves stay, so a Markdown link in a value that a block
|
|
76
|
+
* template leaves to marked (`[text](url)`) is still a link. Entities read
|
|
77
|
+
* back as the same characters in text and attribute values, and the final
|
|
78
|
+
* sanitiser decodes them, so nothing visible changes; the base64 `data-ctx`
|
|
79
|
+
* / table payloads carry no brackets and are left as is, so filter, search
|
|
80
|
+
* and export see the stored values.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} html - already-escaped HTML or text
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
export function inertShortcodes(html) {
|
|
86
|
+
return String(html ?? '')
|
|
87
|
+
.replace(/\[([\w\s\/-])/g, (_, c) => `[&#${c.codePointAt(0)};`)
|
|
88
|
+
.replace(/\[$/, '[');
|
|
89
|
+
}
|
|
90
|
+
|
|
60
91
|
function escapeHtmlText(str) {
|
|
61
92
|
return String(str ?? '')
|
|
62
93
|
.replace(/&/g, '&')
|
|
@@ -349,7 +380,9 @@ function interpolateBlockTemplate(blockTemplate, e) {
|
|
|
349
380
|
// One pass: a raw value put in (pre-rendered HTML) is never read again for
|
|
350
381
|
// placeholders - text that shows "{{title}}" keeps it.
|
|
351
382
|
return blockTemplate.replace(/\{\{\{(\w+)\}\}\}|\{\{([\w_]+)(?::([^{}]+))?\}\}/g, (_, raw, key, fmt) => {
|
|
352
|
-
|
|
383
|
+
// Entry values never carry a shortcode into the page (inertShortcodes);
|
|
384
|
+
// the template's own markup is untouched.
|
|
385
|
+
if (raw !== undefined) return inertShortcodes(String(e.data?.[raw] ?? ''));
|
|
353
386
|
let value;
|
|
354
387
|
if (key === '_id') value = e.id ?? '';
|
|
355
388
|
else if (key === '_createdAt') value = e.meta?.createdAt ?? '';
|
|
@@ -357,9 +390,9 @@ function interpolateBlockTemplate(blockTemplate, e) {
|
|
|
357
390
|
else value = e.data?.[key] ?? '';
|
|
358
391
|
if (fmt) {
|
|
359
392
|
const formatted = formatDate(value, fmt);
|
|
360
|
-
if (formatted !== null) return escapeHtmlText(formatted);
|
|
393
|
+
if (formatted !== null) return inertShortcodes(escapeHtmlText(formatted));
|
|
361
394
|
}
|
|
362
|
-
return escapeHtmlText(value);
|
|
395
|
+
return inertShortcodes(escapeHtmlText(value));
|
|
363
396
|
});
|
|
364
397
|
}
|
|
365
398
|
|
|
@@ -401,14 +434,14 @@ function renderCollectionBlocks(entries, blockTemplate, emptyMsg, ctaOpts, cols,
|
|
|
401
434
|
|
|
402
435
|
const items = entries.map(e => {
|
|
403
436
|
const entryCls = entryClassSuffix(ctxOpts).trim();
|
|
404
|
-
const wrapOpen = `<div${entryCls ? ` class="${entryCls}"` : ''}${blockAttr} data-entry-id="${escapeAttr(String(e.id ?? ''))}">`;
|
|
437
|
+
const wrapOpen = `<div${entryCls ? ` class="${entryCls}"` : ''}${blockAttr} data-entry-id="${inertShortcodes(escapeAttr(String(e.id ?? '')))}">`;
|
|
405
438
|
const wrapClose = `</div>`;
|
|
406
439
|
let html = interpolateBlockTemplate(blockTemplate, e);
|
|
407
440
|
|
|
408
441
|
if (ctaOpts) {
|
|
409
442
|
const ctaStyle = escapeAttr(ctaOpts.style || 'primary');
|
|
410
443
|
let btnCls = `btn btn-${ctaStyle} dm-cta-trigger`;
|
|
411
|
-
let btnData = `data-action="${escapeAttr(ctaOpts.action)}" data-entry="${escapeAttr(e.id || '')}"`;
|
|
444
|
+
let btnData = `data-action="${escapeAttr(ctaOpts.action)}" data-entry="${inertShortcodes(escapeAttr(e.id || ''))}"`;
|
|
412
445
|
if (ctaOpts.confirm) btnData += ` data-confirm="${escapeAttr(ctaOpts.confirm)}"`;
|
|
413
446
|
if (ctaOpts.scope) btnData += ` data-scope="${escapeAttr(ctaOpts.scope)}"`;
|
|
414
447
|
const iconHtml = ctaOpts.icon ? `<span data-icon="${escapeAttr(ctaOpts.icon)}"></span> ` : '';
|
|
@@ -636,15 +669,16 @@ function renderCollectionCards(entries, visibleFields, titleField, columns, empt
|
|
|
636
669
|
if (ctaOpts) {
|
|
637
670
|
const ctaStyle = escapeAttr(ctaOpts.style || 'primary');
|
|
638
671
|
let btnCls = `btn btn-${ctaStyle} dm-cta-trigger`;
|
|
639
|
-
let btnData = `data-action="${escapeAttr(ctaOpts.action)}" data-entry="${escapeAttr(e.id || '')}"`;
|
|
672
|
+
let btnData = `data-action="${escapeAttr(ctaOpts.action)}" data-entry="${inertShortcodes(escapeAttr(e.id || ''))}"`;
|
|
640
673
|
if (ctaOpts.confirm) btnData += ` data-confirm="${escapeAttr(ctaOpts.confirm)}"`;
|
|
641
674
|
if (ctaOpts.scope) btnData += ` data-scope="${escapeAttr(ctaOpts.scope)}"`;
|
|
642
675
|
const iconHtml = ctaOpts.icon ? `<span data-icon="${escapeAttr(ctaOpts.icon)}"></span> ` : '';
|
|
643
676
|
footer = `<div class="card-footer"><button class="${btnCls}" ${btnData}>${iconHtml}${escapeHtmlText(ctaOpts.label || 'Run')}</button></div>`;
|
|
644
677
|
}
|
|
645
|
-
return `<div class="card${entryClassSuffix(ctxOpts)}" data-entry-id="${escapeAttr(String(e.id ?? ''))}">${title ? `<div class="card-header">${title}</div>` : ''}<div class="card-body">${body || ' '}</div>${footer}</div>`;
|
|
678
|
+
return `<div class="card${entryClassSuffix(ctxOpts)}" data-entry-id="${inertShortcodes(escapeAttr(String(e.id ?? '')))}">${title ? `<div class="card-header">${title}</div>` : ''}<div class="card-body">${body || ' '}</div>${footer}</div>`;
|
|
646
679
|
}).join('\n');
|
|
647
|
-
|
|
680
|
+
// No authored markup in this display - everything bracketed in it is entry data.
|
|
681
|
+
return inertShortcodes(`<div class="dm-collection-display grid ${responsiveGridCols(cols).join(' ')} gap-4"${buildCtxAttr(visibleFields, entries, ctxOpts)}>\n${cards}\n</div>`);
|
|
648
682
|
}
|
|
649
683
|
|
|
650
684
|
function renderCollectionList(entries, visibleFields, titleField, emptyMsg, ctaOpts, ctxOpts = {}) {
|
|
@@ -664,15 +698,16 @@ function renderCollectionList(entries, visibleFields, titleField, emptyMsg, ctaO
|
|
|
664
698
|
if (ctaOpts) {
|
|
665
699
|
const ctaStyle = escapeAttr(ctaOpts.style || 'primary');
|
|
666
700
|
let btnCls = `btn btn-${ctaStyle} dm-cta-trigger`;
|
|
667
|
-
let btnData = `data-action="${escapeAttr(ctaOpts.action)}" data-entry="${escapeAttr(e.id || '')}"`;
|
|
701
|
+
let btnData = `data-action="${escapeAttr(ctaOpts.action)}" data-entry="${inertShortcodes(escapeAttr(e.id || ''))}"`;
|
|
668
702
|
if (ctaOpts.confirm) btnData += ` data-confirm="${escapeAttr(ctaOpts.confirm)}"`;
|
|
669
703
|
if (ctaOpts.scope) btnData += ` data-scope="${escapeAttr(ctaOpts.scope)}"`;
|
|
670
704
|
const iconHtml = ctaOpts.icon ? `<span data-icon="${escapeAttr(ctaOpts.icon)}"></span> ` : '';
|
|
671
705
|
ctaHtml = `<button class="${btnCls}" ${btnData}>${iconHtml}${escapeHtmlText(ctaOpts.label || 'Run')}</button>`;
|
|
672
706
|
}
|
|
673
|
-
return `<div class="dm-collection-list-item${entryClassSuffix(ctxOpts)}" data-entry-id="${escapeAttr(String(e.id ?? ''))}">${title ? `<strong>${title}</strong>` : ''}${rest}${ctaHtml}</div>`;
|
|
707
|
+
return `<div class="dm-collection-list-item${entryClassSuffix(ctxOpts)}" data-entry-id="${inertShortcodes(escapeAttr(String(e.id ?? '')))}">${title ? `<strong>${title}</strong>` : ''}${rest}${ctaHtml}</div>`;
|
|
674
708
|
}).join('\n');
|
|
675
|
-
|
|
709
|
+
// No authored markup in this display - everything bracketed in it is entry data.
|
|
710
|
+
return inertShortcodes(`<div class="dm-collection-display dm-collection-list"${buildCtxAttr(visibleFields, entries, ctxOpts)}>\n${items}\n</div>`);
|
|
676
711
|
}
|
|
677
712
|
|
|
678
713
|
/**
|
|
@@ -695,7 +730,7 @@ function renderCollectionAccordion(entries, titleField, bodyField, multiple, emp
|
|
|
695
730
|
const bodyVal = String(e.data?.[bodyField] ?? '');
|
|
696
731
|
const bodyHtml = marked.parse(bodyVal);
|
|
697
732
|
return (
|
|
698
|
-
`<div class="accordion-item${entryClassSuffix(ctxOpts)}" data-entry-id="${escapeAttr(String(e.id ?? ''))}">\n` +
|
|
733
|
+
`<div class="accordion-item${entryClassSuffix(ctxOpts)}" data-entry-id="${inertShortcodes(escapeAttr(String(e.id ?? '')))}">\n` +
|
|
699
734
|
` <h3 class="accordion-header"><button class="accordion-button" type="button">${title}` +
|
|
700
735
|
`<span class="accordion-icon" data-icon="chevron-down"></span></button></h3>\n` +
|
|
701
736
|
` <div class="accordion-body"><div class="accordion-content">${bodyHtml}</div></div>\n` +
|
|
@@ -704,7 +739,8 @@ function renderCollectionAccordion(entries, titleField, bodyField, multiple, emp
|
|
|
704
739
|
}).join('\n');
|
|
705
740
|
|
|
706
741
|
const ctx = buildCtxAttr([{name: titleField}, {name: bodyField}], entries, ctxOpts);
|
|
707
|
-
|
|
742
|
+
// No authored markup in this display - everything bracketed in it is entry data.
|
|
743
|
+
return inertShortcodes(`<div class="dm-collection-display accordion"${multiAttr}${ctx}>\n${items}\n</div>`);
|
|
708
744
|
}
|
|
709
745
|
|
|
710
746
|
/**
|
|
@@ -740,7 +776,7 @@ function renderCollectionTimeline(entries, opts, ctxOpts = {}) {
|
|
|
740
776
|
const bodyHtml = bodyField
|
|
741
777
|
? marked.parse(String(e.data?.[bodyField] ?? ''))
|
|
742
778
|
: '';
|
|
743
|
-
return `<div class="dm-progression-item${entryClassSuffix(ctxOpts)}" data-entry-id="${escapeAttr(String(e.id ?? ''))}"${date}${status}${icon}><div class="dm-progression-item-title">${title}</div><div class="dm-progression-item-body">${bodyHtml}</div></div>`;
|
|
779
|
+
return `<div class="dm-progression-item${entryClassSuffix(ctxOpts)}" data-entry-id="${inertShortcodes(escapeAttr(String(e.id ?? '')))}"${date}${status}${icon}><div class="dm-progression-item-title">${title}</div><div class="dm-progression-item-body">${bodyHtml}</div></div>`;
|
|
744
780
|
}).join('\n');
|
|
745
781
|
|
|
746
782
|
const ctx = buildCtxAttr(
|
|
@@ -748,7 +784,8 @@ function renderCollectionTimeline(entries, opts, ctxOpts = {}) {
|
|
|
748
784
|
entries,
|
|
749
785
|
ctxOpts
|
|
750
786
|
);
|
|
751
|
-
|
|
787
|
+
// No authored markup in this display - everything bracketed in it is entry data.
|
|
788
|
+
return inertShortcodes(`<div class="dm-collection-display dm-progression" data-layout="${layout}" data-theme="${theme}" data-mode="${mode}"${ctx}>\n${items}\n</div>`);
|
|
752
789
|
}
|
|
753
790
|
|
|
754
791
|
/**
|
|
@@ -799,9 +836,9 @@ function renderCollectionCarousel(entries, opts, ctxOpts = {}) {
|
|
|
799
836
|
const bodyHtml = bodyField && e.data?.[bodyField] != null
|
|
800
837
|
? `<div class="carousel-slide-description">${marked.parse(String(e.data[bodyField]))}</div>`
|
|
801
838
|
: '';
|
|
802
|
-
inner = img + (title || bodyHtml ? `<div class="carousel-slide-content">${title}${bodyHtml}</div>` : '');
|
|
839
|
+
inner = inertShortcodes(img + (title || bodyHtml ? `<div class="carousel-slide-content">${title}${bodyHtml}</div>` : ''));
|
|
803
840
|
}
|
|
804
|
-
return `<div class="carousel-slide${entryClassSuffix(ctxOpts)}" data-entry-id="${escapeAttr(String(e.id ?? ''))}">${inner}</div>`;
|
|
841
|
+
return `<div class="carousel-slide${entryClassSuffix(ctxOpts)}" data-entry-id="${inertShortcodes(escapeAttr(String(e.id ?? '')))}">${inner}</div>`;
|
|
805
842
|
}).join('\n');
|
|
806
843
|
|
|
807
844
|
const ctxFields = blockTemplate
|
|
@@ -892,9 +929,9 @@ function renderCollectionListgroup(entries, opts, ctxOpts = {}) {
|
|
|
892
929
|
const body = bodyField && e.data?.[bodyField] != null
|
|
893
930
|
? `${title ? ' ' : ''}${marked.parseInline(String(e.data[bodyField]))}`
|
|
894
931
|
: '';
|
|
895
|
-
inner = (title + body) || escapeHtmlText(String(e.id ?? ''));
|
|
932
|
+
inner = inertShortcodes((title + body) || escapeHtmlText(String(e.id ?? '')));
|
|
896
933
|
}
|
|
897
|
-
return `<div class="${itemClassStr}${entryClassSuffix(ctxOpts)}" data-entry-id="${escapeAttr(String(e.id ?? ''))}">${inner}</div>`;
|
|
934
|
+
return `<div class="${itemClassStr}${entryClassSuffix(ctxOpts)}" data-entry-id="${inertShortcodes(escapeAttr(String(e.id ?? '')))}">${inner}</div>`;
|
|
898
935
|
}).join('\n');
|
|
899
936
|
|
|
900
937
|
const ctxFields = blockTemplate
|
|
@@ -954,7 +991,7 @@ async function processViewBlocks(markdown, tagSet, user = null, flags = {}) {
|
|
|
954
991
|
// browsing happens without the shortcode itself having to learn joins.
|
|
955
992
|
if (isInteractiveCollection(attrs)) {
|
|
956
993
|
const shell = await renderViewBrowserShell(attrs, {viewConfig, allowed, user});
|
|
957
|
-
result = result.replace(fullMatch, shell);
|
|
994
|
+
result = result.replace(fullMatch, () => shell);
|
|
958
995
|
continue;
|
|
959
996
|
}
|
|
960
997
|
|
|
@@ -1097,7 +1134,7 @@ async function processViewBlocks(markdown, tagSet, user = null, flags = {}) {
|
|
|
1097
1134
|
replacement = `<div class="dm-view-wrapper${cls}"${id}>${replacement}</div>`;
|
|
1098
1135
|
}
|
|
1099
1136
|
|
|
1100
|
-
result = result.replace(fullMatch, replacement);
|
|
1137
|
+
result = result.replace(fullMatch, () => replacement);
|
|
1101
1138
|
}
|
|
1102
1139
|
|
|
1103
1140
|
return restore(result);
|
|
@@ -1154,12 +1191,12 @@ async function processCollectionBlocks(markdown, tagSet) {
|
|
|
1154
1191
|
// browser hydrates and replaces it).
|
|
1155
1192
|
if (isInteractiveCollection(attrs)) {
|
|
1156
1193
|
const shell = await renderCollectionBrowserShell(attrs);
|
|
1157
|
-
result = result.replace(fullMatch, shell);
|
|
1194
|
+
result = result.replace(fullMatch, () => shell);
|
|
1158
1195
|
continue;
|
|
1159
1196
|
}
|
|
1160
1197
|
|
|
1161
1198
|
const replacement = await renderCollectionFragment(attrs);
|
|
1162
|
-
result = result.replace(fullMatch, replacement);
|
|
1199
|
+
result = result.replace(fullMatch, () => replacement);
|
|
1163
1200
|
}
|
|
1164
1201
|
|
|
1165
1202
|
return restore(result);
|