domma-cms 0.44.0 → 0.45.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/CLAUDE.md +53 -53
- package/README.md +27 -27
- package/admin/css/admin.css +1 -1
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/index.html +2 -2
- package/admin/js/app.js +1 -1
- package/admin/js/lib/card-builder.js +2 -2
- package/admin/js/lib/crud-tutorial.js +1 -1
- package/admin/js/lib/effect-defs.js +1 -1
- package/admin/js/lib/effects-builder.js +1 -1
- package/admin/js/lib/image-editor.js +1 -1
- package/admin/js/lib/markdown-toolbar.js +2 -2
- package/admin/js/lib/project-context.js +1 -1
- package/admin/js/lib/project-quick-create.js +1 -1
- package/admin/js/lib/scribe-composer.js +1 -1
- package/admin/js/lib/server-restart.js +1 -1
- package/admin/js/lib/shortcode-context-menu.js +2 -2
- package/admin/js/lib/simple-editor.js +1 -1
- package/admin/js/lib/themes.js +1 -1
- package/admin/js/lib/timeline-builder.js +1 -1
- package/admin/js/templates/action-editor.html +8 -8
- package/admin/js/templates/api-endpoint-editor.html +7 -7
- package/admin/js/templates/api-endpoints.html +1 -1
- package/admin/js/templates/api-reference.html +1 -1
- package/admin/js/templates/block-editor.html +6 -6
- package/admin/js/templates/collection-editor.html +1 -1
- package/admin/js/templates/component-editor.html +2 -2
- package/admin/js/templates/dashboard/cache.html +3 -3
- package/admin/js/templates/dashboard/journeys.html +3 -3
- package/admin/js/templates/dashboard/kpi-strip.html +3 -3
- package/admin/js/templates/dashboard/top-pages.html +1 -1
- package/admin/js/templates/dashboard/traffic-chart.html +1 -1
- package/admin/js/templates/docs/api-actions.html +1 -1
- package/admin/js/templates/docs/api-collections.html +4 -4
- package/admin/js/templates/docs/api-pages.html +2 -2
- package/admin/js/templates/docs/api-plugins.html +1 -1
- package/admin/js/templates/docs/api-settings.html +2 -2
- package/admin/js/templates/docs/api-users.html +1 -1
- package/admin/js/templates/docs/components-howto.html +4 -4
- package/admin/js/templates/docs/components-reference.html +21 -21
- package/admin/js/templates/docs/components-rules.html +14 -14
- package/admin/js/templates/docs/components-walkthrough.html +13 -13
- package/admin/js/templates/docs/tutorial-crud.html +49 -49
- package/admin/js/templates/docs/tutorial-forms.html +4 -4
- package/admin/js/templates/docs/tutorial-plugin.html +10 -10
- package/admin/js/templates/docs/usage-actions.html +6 -6
- package/admin/js/templates/docs/usage-cta-shortcode.html +9 -9
- package/admin/js/templates/docs/usage-dconfig.html +4 -4
- package/admin/js/templates/docs/usage-navigation.html +1 -1
- package/admin/js/templates/docs/usage-pages.html +1 -1
- package/admin/js/templates/docs/usage-shortcodes.html +24 -24
- package/admin/js/templates/docs/usage-users-roles.html +3 -3
- package/admin/js/templates/docs/usage-views.html +4 -4
- package/admin/js/templates/documentation.html +1 -1
- package/admin/js/templates/effects.html +752 -752
- package/admin/js/templates/form-editor.html +5 -5
- package/admin/js/templates/forms.html +17 -17
- package/admin/js/templates/login.html +5 -5
- package/admin/js/templates/menu-editor.html +10 -10
- package/admin/js/templates/my-profile.html +17 -17
- package/admin/js/templates/navigation.html +11 -11
- package/admin/js/templates/notifications.html +1 -1
- package/admin/js/templates/page-editor.html +4 -4
- package/admin/js/templates/pro-docs.html +14 -14
- package/admin/js/templates/role-editor.html +70 -70
- package/admin/js/templates/roles.html +10 -10
- package/admin/js/templates/settings.html +10 -10
- package/admin/js/templates/tutorials.html +3 -3
- package/admin/js/templates/view-editor.html +15 -15
- package/admin/js/views/action-editor.js +1 -1
- package/admin/js/views/actions-list.js +1 -1
- package/admin/js/views/api-endpoint-editor.js +2 -2
- package/admin/js/views/api-endpoints.js +2 -2
- package/admin/js/views/api-tokens.js +3 -3
- package/admin/js/views/blocks.js +2 -2
- package/admin/js/views/collection-editor.js +1 -1
- package/admin/js/views/collection-entries.js +1 -1
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/component-editor.js +1 -1
- package/admin/js/views/components.js +4 -4
- package/admin/js/views/dashboard/widgets/cache.js +1 -1
- package/admin/js/views/dashboard/widgets/journeys.js +1 -1
- package/admin/js/views/dashboard/widgets/kpi-strip.js +1 -1
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/form-submissions.js +1 -1
- package/admin/js/views/forms.js +1 -1
- package/admin/js/views/layouts.js +1 -1
- package/admin/js/views/login.js +2 -2
- package/admin/js/views/menu-editor.js +3 -3
- package/admin/js/views/menu-locations.js +1 -1
- package/admin/js/views/menus.js +3 -3
- package/admin/js/views/navigation.js +8 -8
- package/admin/js/views/page-editor.js +40 -40
- package/admin/js/views/pages.js +3 -3
- package/admin/js/views/plugin-code.js +2 -2
- package/admin/js/views/plugins.js +1 -1
- package/admin/js/views/project-settings.js +1 -1
- package/admin/js/views/projects.js +1 -1
- package/admin/js/views/roles.js +1 -1
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +3 -3
- package/admin/js/views/view-editor.js +1 -1
- package/admin/js/views/view-preview.js +1 -1
- package/admin/js/views/views-list.js +1 -1
- package/bin/cli.js +8 -8
- package/bin/lib/config-merge.js +44 -44
- package/bin/update.js +16 -16
- package/config/plugins.json +1 -1
- package/package.json +2 -2
- package/plugins/_template/admin/views/index.js +1 -1
- package/plugins/_template/config.js +1 -1
- package/plugins/_template/plugin.js +1 -1
- package/plugins/analytics/admin/templates/analytics.html +3 -3
- package/plugins/analytics/admin/views/analytics.js +1 -1
- package/plugins/analytics/config.js +1 -1
- package/plugins/analytics/plugin.js +4 -4
- package/plugins/analytics/plugin.json +1 -1
- package/plugins/analytics/public/inject-body.html +2 -2
- package/plugins/analytics/public/inject-head.html +1 -1
- package/plugins/blog/admin/templates/blog.html +4 -4
- package/plugins/blog/admin/views/blog.js +3 -3
- package/plugins/blog/admin/views/categories.js +2 -2
- package/plugins/blog/admin/views/comments.js +4 -4
- package/plugins/blog/admin/views/post-editor.js +1 -1
- package/plugins/blog/plugin.js +2 -2
- package/plugins/blog/plugin.json +4 -4
- package/plugins/contacts/admin/views/contacts.js +8 -8
- package/plugins/contacts/plugin.js +4 -4
- package/plugins/contacts/plugin.json +1 -1
- package/plugins/invoice/admin/templates/editor.html +6 -6
- package/plugins/invoice/admin/templates/index.html +4 -4
- package/plugins/invoice/admin/views/editor.js +3 -3
- package/plugins/invoice/admin/views/index.js +4 -4
- package/plugins/invoice/admin/views/issuers.js +1 -1
- package/plugins/invoice/admin/views/party-view.js +3 -3
- package/plugins/invoice/admin/views/receivers.js +1 -1
- package/plugins/invoice/config.js +1 -1
- package/plugins/invoice/plugin.js +1 -1
- package/plugins/invoice/plugin.json +4 -4
- package/plugins/notes/admin/views/notes.js +4 -4
- package/plugins/notes/plugin.js +1 -1
- package/plugins/notes/plugin.json +1 -1
- package/plugins/site-search/admin/templates/site-search.html +3 -3
- package/plugins/site-search/admin/views/site-search.js +1 -1
- package/plugins/site-search/config.js +1 -1
- package/plugins/site-search/plugin.js +4 -4
- package/plugins/site-search/plugin.json +1 -1
- package/plugins/surveys/admin/templates/results.html +1 -1
- package/plugins/surveys/admin/templates/survey-editor.html +4 -4
- package/plugins/surveys/admin/views/audience.js +6 -6
- package/plugins/surveys/admin/views/results.js +3 -3
- package/plugins/surveys/admin/views/survey-editor.js +1 -1
- package/plugins/surveys/admin/views/surveys.js +2 -2
- package/plugins/surveys/plugin.js +1 -1
- package/plugins/surveys/plugin.json +4 -4
- package/plugins/theme-switcher/admin/views/theme-switcher.js +1 -1
- package/plugins/theme-switcher/plugin.json +3 -3
- package/plugins/theme-switcher/public/inject-body.html +28 -28
- package/plugins/theme-switcher/public/inject-head.html +1 -1
- package/plugins/todo/plugin.js +1 -1
- package/public/css/forms.css +1 -1
- package/public/js/collection-browser.js +1 -1
- package/public/js/collection-context.js +1 -1
- package/public/js/form-logic-engine.js +1 -1
- package/public/js/forms.js +2 -1
- package/public/js/site.js +1 -1
- package/scripts/build.js +14 -14
- package/scripts/create-plugin.js +2 -2
- package/scripts/fresh.js +2 -2
- package/scripts/gen-instance-secret.js +1 -1
- package/scripts/pro.js +12 -12
- package/scripts/reset.js +2 -2
- package/scripts/setup.js +15 -15
- package/scripts/users.js +5 -5
- package/scripts/verify-assets.mjs +6 -6
- package/server/config.js +1 -1
- package/server/middleware/auth.js +253 -253
- package/server/middleware/managerAuth.js +3 -3
- package/server/routes/api/actions.js +4 -4
- package/server/routes/api/api-endpoints.js +7 -7
- package/server/routes/api/api-tokens.js +5 -5
- package/server/routes/api/auth.js +309 -309
- package/server/routes/api/blocks.js +3 -3
- package/server/routes/api/collections.js +18 -18
- package/server/routes/api/dashboard.js +5 -5
- package/server/routes/api/effects.js +2 -2
- package/server/routes/api/endpoints-public.js +5 -5
- package/server/routes/api/forms.js +47 -47
- package/server/routes/api/menu-locations.js +3 -3
- package/server/routes/api/menus.js +7 -7
- package/server/routes/api/navigation.js +42 -42
- package/server/routes/api/notifications.js +2 -2
- package/server/routes/api/pages.js +14 -2
- package/server/routes/api/plugin-marketplace.js +3 -3
- package/server/routes/api/plugins.js +6 -6
- package/server/routes/api/projects.js +9 -9
- package/server/routes/api/scaffold.js +7 -7
- package/server/routes/api/settings.js +6 -6
- package/server/routes/api/sidebar.js +1 -1
- package/server/routes/api/users.js +5 -5
- package/server/routes/api/versions.js +1 -1
- package/server/routes/api/views.js +1 -1
- package/server/routes/docs-public.js +3 -3
- package/server/routes/public.js +14 -14
- package/server/server.js +34 -16
- package/server/services/actions.js +19 -19
- package/server/services/adapters/FileAdapter.js +2 -2
- package/server/services/adapters/MongoAdapter.js +5 -5
- package/server/services/apiEndpoints.js +15 -15
- package/server/services/apiTokens.js +7 -7
- package/server/services/blocks.js +13 -13
- package/server/services/cache/drivers/MemoryDriver.js +3 -3
- package/server/services/cache/drivers/NoneDriver.js +1 -1
- package/server/services/cache/index.js +5 -5
- package/server/services/collections.js +14 -14
- package/server/services/components.js +9 -9
- package/server/services/connectionManager.js +4 -4
- package/server/services/content.js +25 -2
- package/server/services/docs.js +14 -14
- package/server/services/email.js +167 -167
- package/server/services/filterEngine.js +6 -6
- package/server/services/forms.js +18 -10
- package/server/services/health.js +1 -1
- package/server/services/hooks.js +7 -7
- package/server/services/images.js +4 -4
- package/server/services/managerClient.js +1 -1
- package/server/services/markdown.js +79 -79
- package/server/services/menuRender.js +5 -5
- package/server/services/menus-migration.js +3 -3
- package/server/services/menus.js +31 -31
- package/server/services/permissionRegistry.js +1 -1
- package/server/services/pluginFiles.js +5 -5
- package/server/services/pluginInstaller.js +17 -17
- package/server/services/pluginScaffold.js +1 -1
- package/server/services/plugins.js +17 -17
- package/server/services/presetCollections.js +2 -2
- package/server/services/projects.js +35 -26
- package/server/services/recipes/contact-list.json +1 -1
- package/server/services/recipes/onboarding.json +8 -8
- package/server/services/references.js +8 -8
- package/server/services/renderer.js +9 -9
- package/server/services/roles.js +9 -9
- package/server/services/rowAccess.js +10 -10
- package/server/services/scaffolder.js +30 -30
- package/server/services/sidebar-migration.js +10 -10
- package/server/services/sitemap.js +1 -1
- package/server/services/userProfiles.js +199 -199
- package/server/services/userRoles.js +7 -7
- package/server/services/users.js +302 -302
- package/server/services/versions.js +3 -3
- package/server/services/viewPipeline.js +6 -6
- package/server/services/views.js +11 -11
- package/server/templates/page.html +5 -5
|
@@ -52,10 +52,10 @@ function escapeHtmlText(str) {
|
|
|
52
52
|
*
|
|
53
53
|
* Two attribute styles are recognised and merged (with where_* taking precedence):
|
|
54
54
|
*
|
|
55
|
-
* 1. Legacy `where="field1=value1,field2=value2"`
|
|
55
|
+
* 1. Legacy `where="field1=value1,field2=value2"` - equality only, comma-separated.
|
|
56
56
|
* Predates the structured DSL; preserved for backward compat.
|
|
57
57
|
*
|
|
58
|
-
* 2. New `where_<field>[_<op>]="value"` attributes
|
|
58
|
+
* 2. New `where_<field>[_<op>]="value"` attributes - full operator support.
|
|
59
59
|
* Example: `where_location="London" where_salary_gte="50000"
|
|
60
60
|
* where_tags_in="remote,hybrid"`.
|
|
61
61
|
* Maps directly onto the filterEngine DSL.
|
|
@@ -130,7 +130,7 @@ async function loadBlockCss(blockName) {
|
|
|
130
130
|
*/
|
|
131
131
|
/**
|
|
132
132
|
* Format a Date (or ISO string) using moment.js-style tokens.
|
|
133
|
-
* Returns null when the input can't be parsed as a valid date
|
|
133
|
+
* Returns null when the input can't be parsed as a valid date - lets callers
|
|
134
134
|
* fall back to the raw value so non-date fields passed with a format don't
|
|
135
135
|
* silently blank out.
|
|
136
136
|
*
|
|
@@ -199,7 +199,7 @@ export function resolveExportConfig(schemaExport, attrs, defaultRole) {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
/**
|
|
202
|
-
* The role names permitted by an `access` setting
|
|
202
|
+
* The role names permitted by an `access` setting - the named role plus every
|
|
203
203
|
* more-privileged one above it.
|
|
204
204
|
*
|
|
205
205
|
* The browser cannot do this itself: it learns the visitor's role name from
|
|
@@ -223,7 +223,7 @@ function rolesAtOrAbove(access) {
|
|
|
223
223
|
/**
|
|
224
224
|
* Build the `data-ctx` attribute that powers the public-site right-click
|
|
225
225
|
* search/filter menu (public/js/collection-context.js). The payload carries
|
|
226
|
-
* ONLY the fields the display actually renders
|
|
226
|
+
* ONLY the fields the display actually renders - never the full entry data -
|
|
227
227
|
* so hidden/allowlisted-away fields can't leak through the attribute.
|
|
228
228
|
*
|
|
229
229
|
* @param {Array<{name: string, label?: string}>} fields - displayed fields
|
|
@@ -266,10 +266,10 @@ function buildBlockStyleTag(blockName, css) {
|
|
|
266
266
|
/**
|
|
267
267
|
* Interpolate a block template against a single collection entry.
|
|
268
268
|
*
|
|
269
|
-
* Triple-brace `{{{field}}}`
|
|
269
|
+
* Triple-brace `{{{field}}}` - raw HTML pass-through (no escape). Use for
|
|
270
270
|
* fields that contain pre-rendered HTML. Must be processed BEFORE double-brace
|
|
271
271
|
* so the outer `{`/`}` of a triple match aren't consumed by the double-brace
|
|
272
|
-
* regex. Double-brace `{{field}}`
|
|
272
|
+
* regex. Double-brace `{{field}}` - HTML-escaped (default, safe), with an
|
|
273
273
|
* optional `:format` suffix (e.g. `{{_createdAt:DD MMM YYYY}}`) that applies
|
|
274
274
|
* moment-style date formatting when the value parses as a Date.
|
|
275
275
|
*
|
|
@@ -312,8 +312,8 @@ function renderCollectionBlocks(entries, blockTemplate, emptyMsg, ctaOpts, cols,
|
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
// Every entry gets a neutral wrapper carrying data-entry-id (needed by the
|
|
315
|
-
// public right-click filter menu); the data-block attribute
|
|
316
|
-
// the block's CSS
|
|
315
|
+
// public right-click filter menu); the data-block attribute - which scopes
|
|
316
|
+
// the block's CSS - is only added when the block actually carries CSS.
|
|
317
317
|
const hasCss = !!(blockCss && blockCss.trim());
|
|
318
318
|
const blockAttr = hasCss && blockName ? ` data-block="${escapeAttr(blockName)}"` : '';
|
|
319
319
|
|
|
@@ -441,7 +441,7 @@ async function processStaticBlocks(content, tagSet) {
|
|
|
441
441
|
}
|
|
442
442
|
|
|
443
443
|
/**
|
|
444
|
-
* Detect a stored file reference
|
|
444
|
+
* Detect a stored file reference - the object shape produced by the form's
|
|
445
445
|
* multipart parser: { url, name, size?, mime? }.
|
|
446
446
|
*/
|
|
447
447
|
function isFileRef(v) {
|
|
@@ -480,7 +480,7 @@ function displayValue(entry, field) {
|
|
|
480
480
|
* As `displayValue` but returns an HTML fragment. References with a
|
|
481
481
|
* `linkTemplate` render as `<a>`; file references render as `<img>` (image
|
|
482
482
|
* mimes) or `<a download>` (everything else). The caller is responsible
|
|
483
|
-
* for sanitisation
|
|
483
|
+
* for sanitisation - values pass through `escapeHtmlText`/`escapeAttr`.
|
|
484
484
|
*/
|
|
485
485
|
function displayValueHtml(entry, field) {
|
|
486
486
|
if (field.type === 'reference') {
|
|
@@ -664,7 +664,7 @@ function renderCollectionTimeline(entries, opts, ctxOpts = {}) {
|
|
|
664
664
|
}
|
|
665
665
|
|
|
666
666
|
/**
|
|
667
|
-
* Render a collection as a Domma carousel
|
|
667
|
+
* Render a collection as a Domma carousel - one slide per entry.
|
|
668
668
|
* Output mirrors processCarouselBlocks so site.js's `.carousel` auto-init
|
|
669
669
|
* (Domma.elements.carousel) picks it up unchanged.
|
|
670
670
|
*
|
|
@@ -724,7 +724,7 @@ function renderCollectionCarousel(entries, opts, ctxOpts = {}) {
|
|
|
724
724
|
}
|
|
725
725
|
|
|
726
726
|
/**
|
|
727
|
-
* Render a collection as a Domma list group
|
|
727
|
+
* Render a collection as a Domma list group - one item per entry.
|
|
728
728
|
* Output mirrors processListGroupBlocks (pure CSS component, no JS init).
|
|
729
729
|
*
|
|
730
730
|
* Per-item content comes either from a block template (interpolated per
|
|
@@ -752,7 +752,7 @@ function blockNotFoundHtml(blockName) {
|
|
|
752
752
|
/**
|
|
753
753
|
* Load a block's template + CSS when a block attr is present.
|
|
754
754
|
* Returns {tpl: '', css: '', missing: false} when no block was requested,
|
|
755
|
-
* and {missing: true} when the named block can't be loaded
|
|
755
|
+
* and {missing: true} when the named block can't be loaded - callers show
|
|
756
756
|
* blockNotFoundHtml() rather than silently falling back to field rendering.
|
|
757
757
|
*
|
|
758
758
|
* @param {string} [blockName]
|
|
@@ -842,7 +842,7 @@ async function processViewBlocks(markdown, tagSet) {
|
|
|
842
842
|
}
|
|
843
843
|
tagSet?.add(`view:${slug}`);
|
|
844
844
|
|
|
845
|
-
// Interactive `[view]`
|
|
845
|
+
// Interactive `[view]` - when searchable / sortable / paginate is set,
|
|
846
846
|
// emit a Collection Browser shell against the view's executed dataset.
|
|
847
847
|
// No filter rail (views own their own filter logic) but search, sort,
|
|
848
848
|
// and pagination work as on `[collection]`. This is how cross-collection
|
|
@@ -881,7 +881,7 @@ async function processViewBlocks(markdown, tagSet) {
|
|
|
881
881
|
const entries = results.map(doc => ({id: doc.id || '', data: doc.data || doc}));
|
|
882
882
|
|
|
883
883
|
// Resolve the view's underlying collection schema to compute export
|
|
884
|
-
// permission. Views don't own a schema themselves
|
|
884
|
+
// permission. Views don't own a schema themselves - [dis]allowing
|
|
885
885
|
// export must fall back CLOSED if the source collection can't be
|
|
886
886
|
// resolved, since an open fallback would be a permission bypass.
|
|
887
887
|
const sourceSlug = viewConfig?.pipeline?.source || '';
|
|
@@ -978,7 +978,7 @@ async function processViewBlocks(markdown, tagSet) {
|
|
|
978
978
|
replacement = renderCollectionTable(`view:${slug}`, entries, fields, attrs, ctaOpts, ctxOpts);
|
|
979
979
|
}
|
|
980
980
|
} catch {
|
|
981
|
-
// View not found or pipeline error
|
|
981
|
+
// View not found or pipeline error - show empty state
|
|
982
982
|
}
|
|
983
983
|
|
|
984
984
|
if (attrs.class || attrs.id) {
|
|
@@ -1022,7 +1022,7 @@ async function processCollectionBlocks(markdown, tagSet) {
|
|
|
1022
1022
|
// scope="mine" can't share the per-role page cache (data is per-user),
|
|
1023
1023
|
// so emit a hydration placeholder. The public site JS POSTs the raw
|
|
1024
1024
|
// attrs to /api/collections/render-scope, which re-runs renderCollectionFragment
|
|
1025
|
-
// server-side with `createdBy = <current user>` injected
|
|
1025
|
+
// server-side with `createdBy = <current user>` injected - keeping rendering
|
|
1026
1026
|
// logic in ONE place (no client-side template duplication).
|
|
1027
1027
|
if (attrs.scope === 'mine') {
|
|
1028
1028
|
const encoded = Buffer.from(JSON.stringify(attrs), 'utf8').toString('base64');
|
|
@@ -1037,7 +1037,7 @@ async function processCollectionBlocks(markdown, tagSet) {
|
|
|
1037
1037
|
continue;
|
|
1038
1038
|
}
|
|
1039
1039
|
|
|
1040
|
-
// Interactive Collection Browser
|
|
1040
|
+
// Interactive Collection Browser - searchable / filterable / sortable
|
|
1041
1041
|
// turns the block into a full client-side browser with auto-derived
|
|
1042
1042
|
// filter UI, sort dropdown, search box, and pagination. We still emit
|
|
1043
1043
|
// a server-rendered fragment as the no-JS fallback (visible until the
|
|
@@ -1084,18 +1084,18 @@ function isTruthyAttr(v) {
|
|
|
1084
1084
|
* Build the hydration shell for the Collection Browser component.
|
|
1085
1085
|
*
|
|
1086
1086
|
* The shell ships three things to the client:
|
|
1087
|
-
* 1. **config**
|
|
1087
|
+
* 1. **config** - display mode, columns, page size, which fields are
|
|
1088
1088
|
* sortable/filterable, mode (client|server), labels.
|
|
1089
|
-
* 2. **schema**
|
|
1089
|
+
* 2. **schema** - the collection's field definitions, used to auto-derive
|
|
1090
1090
|
* the right filter control per field type (text → input,
|
|
1091
1091
|
* select → dropdown, number → range, date → from/to, etc.).
|
|
1092
|
-
* 3. **data**
|
|
1092
|
+
* 3. **data** - for mode=client, ALL entries (capped at `maxClientEntries`)
|
|
1093
1093
|
* so filtering is instant. For mode=server, only the first
|
|
1094
1094
|
* page is shipped and subsequent pages are fetched via the
|
|
1095
1095
|
* existing `/api/collections/:slug/public` endpoint with
|
|
1096
1096
|
* `filter[…]`, `sort`, `order`, `page`, `limit` params.
|
|
1097
1097
|
*
|
|
1098
|
-
* A static server-rendered fragment is also embedded as the no-JS fallback
|
|
1098
|
+
* A static server-rendered fragment is also embedded as the no-JS fallback -
|
|
1099
1099
|
* visible until `collection-browser.js` hydrates and replaces the shell.
|
|
1100
1100
|
*
|
|
1101
1101
|
* @param {Record<string, string>} attrs - Parsed shortcode attributes
|
|
@@ -1122,7 +1122,7 @@ async function renderCollectionBrowserShell(attrs) {
|
|
|
1122
1122
|
schema = await getCollection(slug);
|
|
1123
1123
|
if (!schema) throw new Error('not found');
|
|
1124
1124
|
|
|
1125
|
-
// Build the seed listEntries() opts
|
|
1125
|
+
// Build the seed listEntries() opts - push any author-baked where_* filter
|
|
1126
1126
|
// and the initial sort/order down to the adapter, just like the static path.
|
|
1127
1127
|
const seedFilter = buildShortcodeFilter(attrs);
|
|
1128
1128
|
const seedOpts = {
|
|
@@ -1136,7 +1136,7 @@ async function renderCollectionBrowserShell(attrs) {
|
|
|
1136
1136
|
if (mode === 'client') {
|
|
1137
1137
|
// Cap the inline payload to avoid bloating cached HTML for huge collections.
|
|
1138
1138
|
// If we hit the cap, transparently downgrade to mode=server so pagination
|
|
1139
|
-
// still works
|
|
1139
|
+
// still works - the author gets correct UX even on misconfigured pages.
|
|
1140
1140
|
seedOpts.limit = maxClientEntries + 1;
|
|
1141
1141
|
const r = await listEntries(slug, seedOpts);
|
|
1142
1142
|
if (r.total > maxClientEntries) {
|
|
@@ -1151,7 +1151,7 @@ async function renderCollectionBrowserShell(attrs) {
|
|
|
1151
1151
|
total = r.total;
|
|
1152
1152
|
}
|
|
1153
1153
|
} else {
|
|
1154
|
-
// mode=server
|
|
1154
|
+
// mode=server - ship only the first page; subsequent pages fetched live.
|
|
1155
1155
|
seedOpts.limit = pageSize;
|
|
1156
1156
|
seedOpts.page = 1;
|
|
1157
1157
|
const r = await listEntries(slug, seedOpts);
|
|
@@ -1159,7 +1159,7 @@ async function renderCollectionBrowserShell(attrs) {
|
|
|
1159
1159
|
total = r.total;
|
|
1160
1160
|
}
|
|
1161
1161
|
} catch {
|
|
1162
|
-
// Slug unknown / read error
|
|
1162
|
+
// Slug unknown / read error - emit an empty shell so the page still renders.
|
|
1163
1163
|
schema = {fields: [], title: attrs.slug};
|
|
1164
1164
|
entries = [];
|
|
1165
1165
|
total = 0;
|
|
@@ -1198,12 +1198,12 @@ async function renderCollectionBrowserShell(attrs) {
|
|
|
1198
1198
|
style: attrs['cta-style'] || 'primary',
|
|
1199
1199
|
confirm: attrs['cta-confirm'] || ''
|
|
1200
1200
|
} : null,
|
|
1201
|
-
baked: buildShortcodeFilter(attrs), // author's where_* filter
|
|
1201
|
+
baked: buildShortcodeFilter(attrs), // author's where_* filter - locked in, not user-tweakable
|
|
1202
1202
|
initial: { sort: attrs.sort || 'createdAt', order: attrs.order || 'desc' },
|
|
1203
1203
|
syncUrl: !isTruthyAttr(attrs['no-url-sync']) // default ON
|
|
1204
1204
|
};
|
|
1205
1205
|
|
|
1206
|
-
// No-JS fallback
|
|
1206
|
+
// No-JS fallback - render the same first-page state with the static renderer.
|
|
1207
1207
|
const fallbackAttrs = {...attrs, limit: String(pageSize)};
|
|
1208
1208
|
const fallback = await renderCollectionFragment(fallbackAttrs);
|
|
1209
1209
|
|
|
@@ -1224,7 +1224,7 @@ async function renderCollectionBrowserShell(attrs) {
|
|
|
1224
1224
|
|
|
1225
1225
|
/**
|
|
1226
1226
|
* Interactive `[view]` hydration shell. Saved views compose collections,
|
|
1227
|
-
* joins, and pre-filters that aren't expressible in plain `where_*` syntax
|
|
1227
|
+
* joins, and pre-filters that aren't expressible in plain `where_*` syntax -
|
|
1228
1228
|
* so the browser treats a view as a read-only data source: search, sort,
|
|
1229
1229
|
* and pagination work; the filter rail is off by default (the view's own
|
|
1230
1230
|
* filter logic is the authoritative source). Authors can still expose user
|
|
@@ -1248,7 +1248,7 @@ async function renderViewBrowserShell(attrs) {
|
|
|
1248
1248
|
viewConfig = await getView(slug).catch(() => null);
|
|
1249
1249
|
|
|
1250
1250
|
if (declaredMode === 'client') {
|
|
1251
|
-
// For views we cap aggressively
|
|
1251
|
+
// For views we cap aggressively - views can be expensive aggregations.
|
|
1252
1252
|
const r = await executeView(slug, {page: 1, limit: 500});
|
|
1253
1253
|
entries = (r.results || []).map(doc => ({id: doc.id || '', data: doc.data || doc}));
|
|
1254
1254
|
total = r.total ?? entries.length;
|
|
@@ -1258,7 +1258,7 @@ async function renderViewBrowserShell(attrs) {
|
|
|
1258
1258
|
total = r.total ?? entries.length;
|
|
1259
1259
|
}
|
|
1260
1260
|
} catch {
|
|
1261
|
-
// executeView errored (view missing or pipeline error)
|
|
1261
|
+
// executeView errored (view missing or pipeline error) - emit empty shell.
|
|
1262
1262
|
}
|
|
1263
1263
|
|
|
1264
1264
|
// Derive a schema-like fields list from the first row so the browser can
|
|
@@ -1332,12 +1332,12 @@ async function renderViewBrowserShell(attrs) {
|
|
|
1332
1332
|
* - The collection hydration endpoint (client-side render when scope="mine"
|
|
1333
1333
|
* forces per-user data that can't share the page cache)
|
|
1334
1334
|
*
|
|
1335
|
-
* `extraFilter` is merged on top of any filter derived from `where_*` attrs
|
|
1335
|
+
* `extraFilter` is merged on top of any filter derived from `where_*` attrs -
|
|
1336
1336
|
* this is how the hydration endpoint injects `createdBy = <current user>`
|
|
1337
1337
|
* for scope=mine without trusting the client to set it.
|
|
1338
1338
|
*
|
|
1339
1339
|
* Returns a complete HTML fragment including the optional outer wrapper for
|
|
1340
|
-
* `class=` / `id=` attributes. Always succeeds
|
|
1340
|
+
* `class=` / `id=` attributes. Always succeeds - render errors fall back to
|
|
1341
1341
|
* the empty-state HTML rather than throwing, so a typo in a single shortcode
|
|
1342
1342
|
* can't break a whole page render.
|
|
1343
1343
|
*
|
|
@@ -1472,7 +1472,7 @@ export async function renderCollectionFragment(attrs, { extraFilter = null } = {
|
|
|
1472
1472
|
replacement = renderCollectionTable(slug, entries, fields, attrs, ctaOpts, ctxOpts);
|
|
1473
1473
|
}
|
|
1474
1474
|
} catch {
|
|
1475
|
-
// Collection not found or read error
|
|
1475
|
+
// Collection not found or read error - show empty message
|
|
1476
1476
|
}
|
|
1477
1477
|
|
|
1478
1478
|
if (attrs.class || attrs.id) {
|
|
@@ -1491,7 +1491,7 @@ export async function renderCollectionFragment(attrs, { extraFilter = null } = {
|
|
|
1491
1491
|
*/
|
|
1492
1492
|
export function parseShortcodeAttrs(attrStr) {
|
|
1493
1493
|
const attrs = {};
|
|
1494
|
-
// Pass 1: quoted key="value" or key='value' pairs
|
|
1494
|
+
// Pass 1: quoted key="value" or key='value' pairs - proper disjunction so
|
|
1495
1495
|
// a double-quoted value can contain apostrophes (and vice versa) without
|
|
1496
1496
|
// the opposite quote terminating the match.
|
|
1497
1497
|
for (const [, key, dq, sq] of attrStr.matchAll(/([\w-]+)=(?:"([^"]*)"|'([^']*)')/g)) {
|
|
@@ -1499,7 +1499,7 @@ export function parseShortcodeAttrs(attrStr) {
|
|
|
1499
1499
|
}
|
|
1500
1500
|
// Pass 2: unquoted key=value (a value with no whitespace or quotes). Authors
|
|
1501
1501
|
// type `slug=main` constantly; without this pass it parsed as the two flags
|
|
1502
|
-
// `slug` and `main`, so the shortcode silently lost its argument
|
|
1502
|
+
// `slug` and `main`, so the shortcode silently lost its argument - and a
|
|
1503
1503
|
// [menu] with a boolean slug then resolved to nothing and vanished.
|
|
1504
1504
|
let stripped = attrStr.replace(/([\w-]+)=(?:"[^"]*"|'[^']*')/g, m => ' '.repeat(m.length));
|
|
1505
1505
|
for (const [, key, val] of stripped.matchAll(/([\w-]+)=([^\s"'\]]+)/g)) {
|
|
@@ -1527,7 +1527,7 @@ export function parseShortcodeAttrs(attrStr) {
|
|
|
1527
1527
|
* NEST: a shortcode handler may run nested processors (processCardBlocks,
|
|
1528
1528
|
* processGridBlocks, …) on an already-scrubbed body, and each of those scrubs
|
|
1529
1529
|
* again. Without a per-call id, the inner restore's regex also matched the
|
|
1530
|
-
* OUTER call's placeholders and looked them up in the wrong store
|
|
1530
|
+
* OUTER call's placeholders and looked them up in the wrong store -
|
|
1531
1531
|
* `store[idx]` → undefined, which surfaced as literal "undefined" strings
|
|
1532
1532
|
* wherever inline code appeared inside [reveal]/effects bodies. With the id,
|
|
1533
1533
|
* each restore only touches its own placeholders and the outer restore picks
|
|
@@ -1645,7 +1645,7 @@ function processEffectsBlocks(markdown) {
|
|
|
1645
1645
|
return `<div class="dm-fx-celebrate"${dataAttrs}></div>`;
|
|
1646
1646
|
});
|
|
1647
1647
|
|
|
1648
|
-
// Named celebration shortcodes
|
|
1648
|
+
// Named celebration shortcodes - sugar for [celebrate theme="..."] with override support
|
|
1649
1649
|
['christmas', 'halloween', 'st-patricks', 'valentines', 'guy-fawkes',
|
|
1650
1650
|
'st-andrews', 'st-davids', 'st-georges'].forEach(themeName => {
|
|
1651
1651
|
apply(themeName, (attrStr) => {
|
|
@@ -1704,7 +1704,7 @@ function processEffectsBlocks(markdown) {
|
|
|
1704
1704
|
else if (uAttrs.count) actions.push({undoRender: parseInt(uAttrs.count, 10)});
|
|
1705
1705
|
else actions.push({undoRender: true});
|
|
1706
1706
|
} else {
|
|
1707
|
-
// [redraw]
|
|
1707
|
+
// [redraw] - sugar for [undo all="true" /] + [render]
|
|
1708
1708
|
const rAttrs = parseShortcodeAttrs(match[5] || '');
|
|
1709
1709
|
actions.push({undoRender: 'all'});
|
|
1710
1710
|
const action = {render: match[6].trim()};
|
|
@@ -1746,7 +1746,7 @@ function processEffectsBlocks(markdown) {
|
|
|
1746
1746
|
|
|
1747
1747
|
// [butterflies /] → full-page overlay (mode=page)
|
|
1748
1748
|
// [butterflies] body [/butterflies] → container-scoped (mode=container)
|
|
1749
|
-
// [strobe /] / [strobe] body [/strobe]
|
|
1749
|
+
// [strobe /] / [strobe] body [/strobe] - same shape
|
|
1750
1750
|
for (const name of ['butterflies', 'strobe']) {
|
|
1751
1751
|
apply(name, (attrStr, body) => {
|
|
1752
1752
|
const attrs = parseShortcodeAttrs(attrStr);
|
|
@@ -1790,7 +1790,7 @@ function processEffectsBlocks(markdown) {
|
|
|
1790
1790
|
/**
|
|
1791
1791
|
* Pre-process [row], [grid], and [col] shortcodes before running through marked.
|
|
1792
1792
|
*
|
|
1793
|
-
* Uses Domma's native CSS Grid system
|
|
1793
|
+
* Uses Domma's native CSS Grid system - NOT the .col compatibility layer.
|
|
1794
1794
|
*
|
|
1795
1795
|
* Supported shortcodes:
|
|
1796
1796
|
*
|
|
@@ -1842,7 +1842,7 @@ function processGridBlocks(markdown) {
|
|
|
1842
1842
|
const allClasses = [baseCls, ...injected.effectClasses].join(' ');
|
|
1843
1843
|
const cls = ` class="${allClasses}"`;
|
|
1844
1844
|
const id = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
|
|
1845
|
-
// Restore the col body before passing it downstream
|
|
1845
|
+
// Restore the col body before passing it downstream - otherwise the
|
|
1846
1846
|
// scrub placeholders from this processor's store get carried into
|
|
1847
1847
|
// processCardBlocks, which creates its own empty store and fails to
|
|
1848
1848
|
// decode them, producing the literal string "undefined" in place of
|
|
@@ -1905,8 +1905,8 @@ function processGridBlocks(markdown) {
|
|
|
1905
1905
|
*/
|
|
1906
1906
|
/**
|
|
1907
1907
|
* Process component references on a Markdown body in two injection forms:
|
|
1908
|
-
* 1. [component name="x" attr="y" /]
|
|
1909
|
-
* 2. <dm-x attr="y"></dm-x>
|
|
1908
|
+
* 1. [component name="x" attr="y" /] - shortcode (rewritten to form 2)
|
|
1909
|
+
* 2. <dm-x attr="y"></dm-x> - raw custom-element tag
|
|
1910
1910
|
*
|
|
1911
1911
|
* Rewrites form 1 to form 2 in-place, and collects every dm-<name> tag encountered
|
|
1912
1912
|
* into a de-duplicated Set so the renderer can inject only the scripts this page uses.
|
|
@@ -2270,7 +2270,7 @@ LAYOUT_RENDERERS['avatar-profile'] = (attrs, body) => {
|
|
|
2270
2270
|
|
|
2271
2271
|
LAYOUT_RENDERERS['stat-metric'] = (attrs, _body) => {
|
|
2272
2272
|
const title = escapeAttr(attrs.title || '');
|
|
2273
|
-
const value = escapeAttr(attrs.value || '
|
|
2273
|
+
const value = escapeAttr(attrs.value || '-');
|
|
2274
2274
|
const delta = escapeAttr(attrs.delta || '');
|
|
2275
2275
|
const progress = Math.min(100, Math.max(0, parseInt(attrs.progress || '0', 10)));
|
|
2276
2276
|
const isPos = delta && !delta.startsWith('-') && !delta.startsWith('↓');
|
|
@@ -2543,7 +2543,7 @@ function renderLegacyCard(attrs, body, markedInstance, escAttr) {
|
|
|
2543
2543
|
|
|
2544
2544
|
const injected = extractInjectedEffects(attrs, CARD_INJECTABLE);
|
|
2545
2545
|
|
|
2546
|
-
// Root class list
|
|
2546
|
+
// Root class list - delegate to the shared helper so variant="gradient",
|
|
2547
2547
|
// gradient="<name>", glass/accent/dark/glow, font, shadow, rounded, etc.
|
|
2548
2548
|
// all work on legacy cards (cards without a `layout` attribute).
|
|
2549
2549
|
const classes = cardVariantClasses(attrs);
|
|
@@ -2575,7 +2575,7 @@ function renderLegacyCard(attrs, body, markedInstance, escAttr) {
|
|
|
2575
2575
|
}
|
|
2576
2576
|
);
|
|
2577
2577
|
|
|
2578
|
-
// Header
|
|
2578
|
+
// Header - sub-tag wins over attributes; attributes only used when no sub-tag
|
|
2579
2579
|
let headerHtml = '';
|
|
2580
2580
|
if (headerContent !== null) {
|
|
2581
2581
|
headerHtml = `<div class="card-header">${markedInstance.parse(headerContent)}</div>`;
|
|
@@ -2610,7 +2610,7 @@ function renderLegacyCard(attrs, body, markedInstance, escAttr) {
|
|
|
2610
2610
|
}
|
|
2611
2611
|
|
|
2612
2612
|
// ---------------------------------------------------------------------------
|
|
2613
|
-
// processCardBlocks
|
|
2613
|
+
// processCardBlocks - routing entry point
|
|
2614
2614
|
// ---------------------------------------------------------------------------
|
|
2615
2615
|
|
|
2616
2616
|
/**
|
|
@@ -2956,7 +2956,7 @@ function processTimelineBlocks(markdown) {
|
|
|
2956
2956
|
* [badge variant="danger" outline pill]Deprecated[/badge]
|
|
2957
2957
|
* [badge variant="warning" size="small"]Beta[/badge]
|
|
2958
2958
|
*
|
|
2959
|
-
* Syntax (self-closing, renders empty badge
|
|
2959
|
+
* Syntax (self-closing, renders empty badge - useful for coloured dots):
|
|
2960
2960
|
* [badge variant="primary" pill /]
|
|
2961
2961
|
*
|
|
2962
2962
|
* Supported attributes:
|
|
@@ -2992,7 +2992,7 @@ function processBadgeBlocks(markdown) {
|
|
|
2992
2992
|
// Self-closing: [badge ... /]
|
|
2993
2993
|
let result = scrubbed.replace(/\[badge([^\]]*?)\/\]/gi, (_, attrStr) => buildBadge(attrStr, ''));
|
|
2994
2994
|
|
|
2995
|
-
// Paired: [badge ...]...[/badge] (inline only
|
|
2995
|
+
// Paired: [badge ...]...[/badge] (inline only - no newlines inside)
|
|
2996
2996
|
result = result.replace(/\[badge([^\]]*)\]([^\n]*?)\[\/badge\]/gi, (_, attrStr, inner) => {
|
|
2997
2997
|
return buildBadge(attrStr, inner.trim());
|
|
2998
2998
|
});
|
|
@@ -3262,7 +3262,7 @@ function processTableBlocks(markdown) {
|
|
|
3262
3262
|
* "gradient-forest", "gradient-night"
|
|
3263
3263
|
* theme-specific (26): "gradient-{theme}-{mode}" where theme is one of
|
|
3264
3264
|
* ocean, forest, sunset, royal, lemon, silver, charcoal, christmas,
|
|
3265
|
-
* unicorn, dreamy, grayve, mint, wedding
|
|
3265
|
+
* unicorn, dreamy, grayve, mint, wedding - and mode is light or dark
|
|
3266
3266
|
* → .hero-{variant}
|
|
3267
3267
|
* image - URL for background-image + adds .hero-cover
|
|
3268
3268
|
* overlay - "light", "dark", "darker", "gradient", "gradient-reverse" → .hero-overlay-{overlay}
|
|
@@ -3270,10 +3270,10 @@ function processTableBlocks(markdown) {
|
|
|
3270
3270
|
* bg / color - Background colour (any safe CSS colour value)
|
|
3271
3271
|
* min-height - Minimum height (px, em, rem, vh, vw, %)
|
|
3272
3272
|
* fullwidth - "true" breaks out of the page container to span the full viewport width (adds .hero-breakout).
|
|
3273
|
-
* Must be the literal string "true"
|
|
3274
|
-
* twinkle - Flag attribute
|
|
3273
|
+
* Must be the literal string "true" - the attribute is otherwise ignored.
|
|
3274
|
+
* twinkle - Flag attribute - adds particle overlay (requires Effects plugin)
|
|
3275
3275
|
* twinkle-* - Any prefixed attribute is forwarded as data-fx-* (see extractInjectedEffects)
|
|
3276
|
-
* blobs - Flag attribute
|
|
3276
|
+
* blobs - Flag attribute - adds ambient blob background
|
|
3277
3277
|
* blobs-type - Blob animation type (default: "float-blobs")
|
|
3278
3278
|
* class - Extra classes appended to .hero
|
|
3279
3279
|
* id - Element id attribute
|
|
@@ -3389,7 +3389,7 @@ function processCenterBlocks(markdown) {
|
|
|
3389
3389
|
const classAttr = attrs.class ? ` class="${escapeAttr(attrs.class)}"` : '';
|
|
3390
3390
|
// Emit the wrapper with blank lines around the body so CommonMark
|
|
3391
3391
|
// parses markdown inside the div. DO NOT eagerly call marked.parse
|
|
3392
|
-
// here
|
|
3392
|
+
// here - that HTML-escapes attribute quotes on any unprocessed
|
|
3393
3393
|
// shortcodes inside (e.g. [button href="..."] becomes href="...),
|
|
3394
3394
|
// which breaks every later shortcode processor in the pipeline.
|
|
3395
3395
|
return `\n<div style="text-align:center;"${classAttr}>\n\n${body.trim()}\n\n</div>\n`;
|
|
@@ -3429,11 +3429,11 @@ function processIconBlocks(markdown) {
|
|
|
3429
3429
|
));
|
|
3430
3430
|
}
|
|
3431
3431
|
|
|
3432
|
-
// ─── [embed]
|
|
3432
|
+
// ─── [embed] - video / iframe embeds ───────────────────────────────────────
|
|
3433
3433
|
|
|
3434
3434
|
/**
|
|
3435
3435
|
* Classify an embed URL into one of: youtube, vimeo, video (self-hosted),
|
|
3436
|
-
* iframe (generic) or invalid. Pure
|
|
3436
|
+
* iframe (generic) or invalid. Pure - no allow-list logic here; the host
|
|
3437
3437
|
* allow-list is applied at render time for the generic-iframe case only.
|
|
3438
3438
|
*
|
|
3439
3439
|
* @param {string} url
|
|
@@ -3444,23 +3444,23 @@ export function classifyEmbedUrl(url) {
|
|
|
3444
3444
|
if (!u) return {type: 'invalid'};
|
|
3445
3445
|
|
|
3446
3446
|
let m;
|
|
3447
|
-
// YouTube
|
|
3447
|
+
// YouTube - watch?v=, youtu.be/, /embed/, /shorts/, /live/. Any non-empty
|
|
3448
3448
|
// id token counts so a YouTube host never falls through to the generic
|
|
3449
3449
|
// iframe path (which would show a misleading "not allow-listed" message).
|
|
3450
3450
|
if ((m = u.match(/(?:youtube(?:-nocookie)?\.com\/(?:watch\?(?:[^#]*&)?v=|embed\/|shorts\/|live\/)|youtu\.be\/)([\w-]+)/i))) {
|
|
3451
3451
|
return {type: 'youtube', id: m[1], src: `https://www.youtube-nocookie.com/embed/${m[1]}`};
|
|
3452
3452
|
}
|
|
3453
|
-
// Vimeo
|
|
3453
|
+
// Vimeo - vimeo.com/ID or player.vimeo.com/video/ID
|
|
3454
3454
|
if ((m = u.match(/vimeo\.com\/(?:video\/)?(\d+)/i))) {
|
|
3455
3455
|
return {type: 'vimeo', id: m[1], src: `https://player.vimeo.com/video/${m[1]}`};
|
|
3456
3456
|
}
|
|
3457
|
-
// Self-hosted video
|
|
3457
|
+
// Self-hosted video - by file extension (optional query/hash suffix)
|
|
3458
3458
|
if ((m = u.match(/\.(mp4|webm|ogg|ogv)(?:\?[^#]*)?(?:#.*)?$/i))) {
|
|
3459
3459
|
const ext = m[1].toLowerCase();
|
|
3460
3460
|
const mime = ext === 'mp4' ? 'video/mp4' : ext === 'webm' ? 'video/webm' : 'video/ogg';
|
|
3461
3461
|
return {type: 'video', src: u, mime};
|
|
3462
3462
|
}
|
|
3463
|
-
// Generic http(s)
|
|
3463
|
+
// Generic http(s) - host decides allow/deny at render time
|
|
3464
3464
|
if (/^https?:\/\//i.test(u)) {
|
|
3465
3465
|
try {
|
|
3466
3466
|
return {type: 'iframe', src: u, host: new URL(u).hostname};
|
|
@@ -3510,7 +3510,7 @@ export function renderEmbed(attrs, allowedHosts = []) {
|
|
|
3510
3510
|
const loop = _embedFlag(attrs.loop);
|
|
3511
3511
|
const muted = _embedFlag(attrs.muted);
|
|
3512
3512
|
|
|
3513
|
-
// Outer <figure>
|
|
3513
|
+
// Outer <figure> - width + alignment.
|
|
3514
3514
|
const figStyles = ['margin:1.5rem 0'];
|
|
3515
3515
|
if (attrs.width) {
|
|
3516
3516
|
const w = /^\d+%$/.test(attrs.width) ? attrs.width : `${parseInt(attrs.width, 10) || 0}px`;
|
|
@@ -3560,7 +3560,7 @@ export function renderEmbed(attrs, allowedHosts = []) {
|
|
|
3560
3560
|
|
|
3561
3561
|
/**
|
|
3562
3562
|
* Read the generic-iframe host allow-list from config/embeds.json. A missing
|
|
3563
|
-
* file yields no extra hosts
|
|
3563
|
+
* file yields no extra hosts - YouTube/Vimeo and self-hosted video still work.
|
|
3564
3564
|
*
|
|
3565
3565
|
* @returns {string[]}
|
|
3566
3566
|
*/
|
|
@@ -3623,7 +3623,7 @@ const FORMS_DIR = path.resolve(__dirname_md, '../../content/forms');
|
|
|
3623
3623
|
* Transform `type: 'reference'` fields on a form definition into select-style
|
|
3624
3624
|
* fields populated from the target collection. Mutates the form in place.
|
|
3625
3625
|
*
|
|
3626
|
-
* Strategy: lazy server-side expansion at render time
|
|
3626
|
+
* Strategy: lazy server-side expansion at render time - the embedded form
|
|
3627
3627
|
* doesn't know about references, it just sees a select with options. This
|
|
3628
3628
|
* keeps the client-side form engine (`F.create`) ignorant of the schema
|
|
3629
3629
|
* vocabulary and works without any extra round-trips at form render time.
|
|
@@ -3670,7 +3670,7 @@ async function expandReferenceFields(form) {
|
|
|
3670
3670
|
_ref: {collection: targetSlug, displayField, originalType: 'reference'}
|
|
3671
3671
|
};
|
|
3672
3672
|
} catch {
|
|
3673
|
-
// Target collection missing
|
|
3673
|
+
// Target collection missing - render a disabled input with an explanatory placeholder.
|
|
3674
3674
|
fields[i] = {
|
|
3675
3675
|
...f,
|
|
3676
3676
|
type: 'text',
|
|
@@ -3747,7 +3747,7 @@ function processHeroBlocks(markdown) {
|
|
|
3747
3747
|
const HERO_INJECTABLE = ['reveal', 'breathe', 'pulse', 'twinkle', 'ticker-tape', 'butterflies', 'strobe', 'ambient', 'animate'];
|
|
3748
3748
|
const injected = extractInjectedEffects(attrs, HERO_INJECTABLE);
|
|
3749
3749
|
|
|
3750
|
-
// Legacy ad-hoc 'blobs' flag
|
|
3750
|
+
// Legacy ad-hoc 'blobs' flag - keep for backward compat. Different keyspace
|
|
3751
3751
|
// (bg-ambient-* CSS class), not folded into the generic helper.
|
|
3752
3752
|
const blobs = 'blobs' in attrs;
|
|
3753
3753
|
const blobsType = attrs['blobs-type'] || 'float-blobs';
|
|
@@ -3774,7 +3774,7 @@ function processHeroBlocks(markdown) {
|
|
|
3774
3774
|
const idAttr = id ? ` id="${escapeAttr(id)}"` : '';
|
|
3775
3775
|
|
|
3776
3776
|
// Button/link/cta must run before marked.parse so their quoted attributes
|
|
3777
|
-
// aren't HTML-escaped to "
|
|
3777
|
+
// aren't HTML-escaped to " - that mangles parseShortcodeAttrs and
|
|
3778
3778
|
// turns `href="/x"` into a bare flag (attrs.href = true → href="true").
|
|
3779
3779
|
const processedBody = processCtaBlocks(processLinkBlocks(processButtonBlocks(
|
|
3780
3780
|
processTextBlocks(processBadgeBlocks(processCardBlocks(processGridBlocks(restore(body.trim())))))
|
|
@@ -3934,12 +3934,12 @@ async function processMenuBlocks(markdown, user, ctx = {}) {
|
|
|
3934
3934
|
const attrs = parseShortcodeAttrs(m[1] || '');
|
|
3935
3935
|
// A bare `slug` / `location` with no value parses as the boolean `true`
|
|
3936
3936
|
// (flag syntax). Treat that as "not supplied" rather than looking up a
|
|
3937
|
-
// menu called "true"
|
|
3937
|
+
// menu called "true" - otherwise the shortcode vanished with no clue why.
|
|
3938
3938
|
const slugAttr = typeof attrs.slug === 'string' ? attrs.slug : '';
|
|
3939
3939
|
const locationAttr = typeof attrs.location === 'string' ? attrs.location : '';
|
|
3940
3940
|
|
|
3941
3941
|
if (!slugAttr && !locationAttr) {
|
|
3942
|
-
out = out.replace(m[0], menuError('[menu] needs a slug or location
|
|
3942
|
+
out = out.replace(m[0], menuError('[menu] needs a slug or location - e.g. [menu slug="main" /]'));
|
|
3943
3943
|
continue;
|
|
3944
3944
|
}
|
|
3945
3945
|
|
|
@@ -3954,8 +3954,8 @@ async function processMenuBlocks(markdown, user, ctx = {}) {
|
|
|
3954
3954
|
out = out.replace(m[0], menuError(`Menu not found: ${slugAttr}`));
|
|
3955
3955
|
} else {
|
|
3956
3956
|
// An unmapped slot is a legitimate configuration state (no menu
|
|
3957
|
-
// assigned yet)
|
|
3958
|
-
console.warn(`[menu] location "${locationAttr}" resolves to no menu
|
|
3957
|
+
// assigned yet) - render nothing, but leave a trace in the log.
|
|
3958
|
+
console.warn(`[menu] location "${locationAttr}" resolves to no menu - rendering nothing`);
|
|
3959
3959
|
out = out.replace(m[0], '');
|
|
3960
3960
|
}
|
|
3961
3961
|
continue;
|
|
@@ -3982,7 +3982,7 @@ async function processMenuBlocks(markdown, user, ctx = {}) {
|
|
|
3982
3982
|
return out;
|
|
3983
3983
|
}
|
|
3984
3984
|
|
|
3985
|
-
/** Visible diagnostic for an unusable [menu] shortcode
|
|
3985
|
+
/** Visible diagnostic for an unusable [menu] shortcode - mirrors [form]'s. */
|
|
3986
3986
|
function menuError(message) {
|
|
3987
3987
|
return `<div class="dm-menu-missing" role="note"><em>${escapeHtmlText(message)}</em></div>`;
|
|
3988
3988
|
}
|
|
@@ -3990,7 +3990,7 @@ function menuError(message) {
|
|
|
3990
3990
|
/**
|
|
3991
3991
|
* Project a URL belongs to, for menu-binding lookups. Dynamically imported so
|
|
3992
3992
|
* the markdown service keeps no load-time dependency on the projects service.
|
|
3993
|
-
* Failures resolve to null
|
|
3993
|
+
* Failures resolve to null - a missing project only means project bindings
|
|
3994
3994
|
* don't match, never a broken page.
|
|
3995
3995
|
*
|
|
3996
3996
|
* @param {string} urlPath
|
|
@@ -4032,7 +4032,7 @@ function processDConfigBlocks(markdown) {
|
|
|
4032
4032
|
const encoded = Buffer.from(jsonStr.trim(), 'utf8').toString('base64');
|
|
4033
4033
|
return `<div class="dm-page-config" style="display:none" data-config="${encoded}"></div>\n`;
|
|
4034
4034
|
} catch {
|
|
4035
|
-
return ''; // invalid JSON
|
|
4035
|
+
return ''; // invalid JSON - drop silently
|
|
4036
4036
|
}
|
|
4037
4037
|
}
|
|
4038
4038
|
);
|
|
@@ -4091,7 +4091,7 @@ function processCtaBlocks(markdown) {
|
|
|
4091
4091
|
* @param {object} [opts]
|
|
4092
4092
|
* @param {object|null} [opts.user] - Authenticated user (`{role, additionalRoles}`)
|
|
4093
4093
|
* or null for anonymous. Used to filter menu items gated by `visibility` in
|
|
4094
|
-
* the `[menu]` shortcode. Backwards compatible
|
|
4094
|
+
* the `[menu]` shortcode. Backwards compatible - defaults to anonymous.
|
|
4095
4095
|
* @param {string} [opts.urlPath] - URL of the page being parsed. Lets
|
|
4096
4096
|
* `[menu location="…"]` honour menus bound to this page or its project.
|
|
4097
4097
|
* @param {string|null} [opts.project] - Project the page belongs to; resolved
|
|
@@ -4166,7 +4166,7 @@ export async function parseMarkdown(raw, opts = {}) {
|
|
|
4166
4166
|
// per-block custom CSS can reach the page. Admin-trust: block authors
|
|
4167
4167
|
// are admins, same security model as site-wide content/custom.css.
|
|
4168
4168
|
'style',
|
|
4169
|
-
// dm-<name> custom-element tags
|
|
4169
|
+
// dm-<name> custom-element tags - one per registered component. Refreshed
|
|
4170
4170
|
// by refreshComponentTagAllowlist() on startup and after create/delete.
|
|
4171
4171
|
// Component authors are super-admins, same trust model as plugin code.
|
|
4172
4172
|
..._dmTagAllowlist,
|
|
@@ -4195,7 +4195,7 @@ export async function parseMarkdown(raw, opts = {}) {
|
|
|
4195
4195
|
source: ['src', 'type'],
|
|
4196
4196
|
// Allow any attribute on every registered dm-* tag. Props are
|
|
4197
4197
|
// author-defined and enumerating them at sanitise time has no security
|
|
4198
|
-
// benefit
|
|
4198
|
+
// benefit - custom-element attributes cannot execute JS on their own,
|
|
4199
4199
|
// and `on*` handlers + `javascript:` URLs are still stripped globally.
|
|
4200
4200
|
..._dmTagAllowlist.reduce((acc, tag) => { acc[tag] = ['*']; return acc; }, {}),
|
|
4201
4201
|
...extensions.attributes
|
|
@@ -4204,7 +4204,7 @@ export async function parseMarkdown(raw, opts = {}) {
|
|
|
4204
4204
|
|
|
4205
4205
|
const html = applyTransforms('markdown:afterParse', sanitized);
|
|
4206
4206
|
|
|
4207
|
-
// Only emit component scripts for components that actually exist
|
|
4207
|
+
// Only emit component scripts for components that actually exist - missing
|
|
4208
4208
|
// references are already harmless (sanitiser strips the tag) but we also
|
|
4209
4209
|
// don't want a 404 on /api/components/missing.js polluting the console.
|
|
4210
4210
|
const allowed = new Set(_dmTagAllowlist.map(t => t.replace(/^dm-/, '')));
|