domma-cms 0.10.0 → 0.13.0
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 +248 -159
- package/admin/css/admin.css +1 -1
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +7 -3
- package/admin/js/config/sidebar-config.js +1 -1
- package/admin/js/http-interceptor.js +1 -0
- package/admin/js/lib/safe-html.js +1 -0
- package/admin/js/templates/documentation.html +611 -2
- package/admin/js/templates/layouts.html +5 -4
- package/admin/js/templates/notifications.html +14 -0
- package/admin/js/templates/plugin-marketplace.html +16 -0
- package/admin/js/templates/plugins.html +17 -5
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/layouts.js +1 -16
- package/admin/js/views/notifications.js +1 -0
- package/admin/js/views/plugin-marketplace.js +1 -0
- package/admin/js/views/plugins.js +16 -16
- package/config/navigation.json +5 -72
- package/config/plugins.json +10 -14
- package/config/presets.json +50 -13
- package/config/site.json +11 -63
- package/package.json +2 -1
- package/plugins/_template/admin/templates/index.html +17 -0
- package/plugins/_template/admin/views/index.js +19 -0
- package/plugins/_template/config.js +8 -0
- package/plugins/_template/plugin.js +23 -0
- package/plugins/_template/plugin.json +34 -0
- package/plugins/analytics/plugin.json +41 -31
- package/plugins/blog/admin/templates/blog.html +22 -0
- package/plugins/blog/admin/templates/categories.html +7 -0
- package/plugins/blog/admin/templates/comments.html +11 -0
- package/plugins/blog/admin/templates/post-editor.html +97 -0
- package/plugins/blog/admin/templates/settings.html +11 -0
- package/plugins/blog/admin/views/blog.js +183 -0
- package/plugins/blog/admin/views/categories.js +235 -0
- package/plugins/blog/admin/views/comments.js +187 -0
- package/plugins/blog/admin/views/post-editor.js +291 -0
- package/plugins/blog/admin/views/settings.js +100 -0
- package/plugins/blog/collections/categories/schema.json +12 -0
- package/plugins/blog/collections/comments/schema.json +16 -0
- package/plugins/blog/collections/posts/schema.json +19 -0
- package/plugins/blog/config.js +8 -0
- package/plugins/blog/plugin.js +352 -0
- package/plugins/blog/plugin.json +96 -0
- package/plugins/blog/roles/blog-author.json +10 -0
- package/plugins/blog/roles/blog-editor.json +12 -0
- package/plugins/blog/templates/author.html +9 -0
- package/plugins/blog/templates/category.html +9 -0
- package/plugins/blog/templates/index.html +9 -0
- package/plugins/blog/templates/post.html +17 -0
- package/plugins/blog/templates/tag.html +9 -0
- package/plugins/contacts/collections/user-contact-groups/schema.json +1 -1
- package/plugins/contacts/collections/user-contacts/schema.json +1 -1
- package/plugins/contacts/plugin.js +4 -10
- package/plugins/contacts/plugin.json +13 -3
- package/plugins/notes/collections/user-notes/schema.json +1 -1
- package/plugins/notes/plugin.js +3 -9
- package/plugins/notes/plugin.json +13 -3
- package/plugins/site-search/plugin.json +5 -2
- package/plugins/theme-switcher/plugin.json +1 -1
- package/plugins/todo/collections/todos/schema.json +1 -1
- package/plugins/todo/plugin.js +3 -9
- package/plugins/todo/plugin.json +13 -3
- package/public/css/site.css +1 -1
- package/scripts/build.js +48 -0
- package/scripts/create-plugin.js +113 -0
- package/scripts/fresh.js +6 -7
- package/scripts/gen-instance-secret.js +46 -0
- package/scripts/reset.js +3 -3
- package/scripts/setup.js +31 -13
- package/server/middleware/auth.js +48 -0
- package/server/middleware/managerAuth.js +36 -0
- package/server/routes/api/actions.js +1 -1
- package/server/routes/api/auth.js +4 -3
- package/server/routes/api/layouts.js +173 -49
- package/server/routes/api/notifications.js +155 -0
- package/server/routes/api/plugin-marketplace.js +75 -0
- package/server/routes/api/users.js +1 -1
- package/server/routes/api/views.js +1 -1
- package/server/routes/public.js +4 -9
- package/server/server.js +32 -3
- package/server/services/actions.js +1 -1
- package/server/services/managerClient.js +182 -0
- package/server/services/markdown.js +52 -14
- package/server/services/permissionRegistry.js +245 -173
- package/server/services/pluginInstaller.js +301 -0
- package/server/services/plugins.js +117 -10
- package/server/services/presetCollections.js +66 -251
- package/server/services/renderer.js +99 -0
- package/server/services/roles.js +191 -39
- package/server/services/users.js +1 -1
- package/server/services/views.js +1 -1
- package/server/templates/page.html +2 -2
- package/plugins/docs/admin/templates/docs.html +0 -69
- package/plugins/docs/admin/views/docs.js +0 -276
- package/plugins/docs/config.js +0 -8
- package/plugins/docs/data/documents/57e003f0-68f2-47dc-9c36-ed4b10ed3deb.json +0 -11
- package/plugins/docs/data/folders.json +0 -9
- package/plugins/docs/data/templates.json +0 -1
- package/plugins/docs/data/versions/57e003f0-68f2-47dc-9c36-ed4b10ed3deb/1.json +0 -5
- package/plugins/docs/plugin.js +0 -375
- package/plugins/docs/plugin.json +0 -23
- package/plugins/form-builder/data/forms/contacts.json +0 -66
- package/plugins/form-builder/data/forms/enquiries.json +0 -103
- package/plugins/form-builder/data/forms/feedback.json +0 -131
- package/plugins/form-builder/data/forms/notes.json +0 -79
- package/plugins/form-builder/data/forms/to-do.json +0 -100
- package/plugins/form-builder/data/submissions/contacts.json +0 -1
- package/plugins/form-builder/data/submissions/enquiries.json +0 -1
- package/plugins/form-builder/data/submissions/feedback.json +0 -1
- package/plugins/form-builder/data/submissions/notes.json +0 -1
- package/plugins/form-builder/data/submissions/to-do.json +0 -1
- package/plugins/garage/admin/templates/garage.html +0 -111
- package/plugins/garage/admin/views/garage.js +0 -622
- package/plugins/garage/collections/garage-vehicles/schema.json +0 -101
- package/plugins/garage/config.js +0 -18
- package/plugins/garage/data/vehicles.json +0 -70
- package/plugins/garage/plugin.js +0 -398
- package/plugins/garage/plugin.json +0 -33
- package/scripts/seed.js +0 -1996
- package/server/services/userTypes.js +0 -227
package/server/server.js
CHANGED
|
@@ -17,7 +17,7 @@ import fs from 'fs/promises';
|
|
|
17
17
|
import {fileURLToPath} from 'url';
|
|
18
18
|
import {createRequire} from 'module';
|
|
19
19
|
import {config, getConfig} from './config.js';
|
|
20
|
-
import {registerPlugins} from './services/plugins.js';
|
|
20
|
+
import {getLoadedPlugins, getPluginSettings, registerPlugins} from './services/plugins.js';
|
|
21
21
|
import {load as loadRoles, seed as seedRoles} from './services/roles.js';
|
|
22
22
|
import {ensureAllProfiles, seed as seedUserProfiles} from './services/userProfiles.js';
|
|
23
23
|
import {seedAll as seedPresetCollections} from './services/presetCollections.js';
|
|
@@ -44,6 +44,13 @@ if (!JWT_SECRET || JWT_SECRET === 'CHANGE_ME' || JWT_SECRET.length < 32) {
|
|
|
44
44
|
process.exit(1);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// MANAGER_SECRET is optional — only needed when domma-cms-manager pushes notifications.
|
|
48
|
+
// Warn if set but insecure; silently accept if absent (manager push is disabled).
|
|
49
|
+
const MANAGER_SECRET = process.env.MANAGER_SECRET;
|
|
50
|
+
if (MANAGER_SECRET && MANAGER_SECRET.length < 32) {
|
|
51
|
+
console.warn(' WARNING: MANAGER_SECRET is set but too short (minimum 32 characters). Manager notifications disabled until fixed.');
|
|
52
|
+
}
|
|
53
|
+
|
|
47
54
|
const app = Fastify({
|
|
48
55
|
logger: {level: process.env.NODE_ENV === 'development' ? 'info' : 'warn'},
|
|
49
56
|
// When running behind a reverse proxy (e.g. domma-cms-manager), trust the
|
|
@@ -227,7 +234,8 @@ const { layoutsRoutes } = await import('./routes/api/layouts.js');
|
|
|
227
234
|
const { navigationRoutes } = await import('./routes/api/navigation.js');
|
|
228
235
|
const { mediaRoutes } = await import('./routes/api/media.js');
|
|
229
236
|
const { usersRoutes } = await import('./routes/api/users.js');
|
|
230
|
-
const { pluginsRoutes }
|
|
237
|
+
const { pluginsRoutes } = await import('./routes/api/plugins.js');
|
|
238
|
+
const { pluginMarketplaceRoutes } = await import('./routes/api/plugin-marketplace.js');
|
|
231
239
|
const { collectionsRoutes } = await import('./routes/api/collections.js');
|
|
232
240
|
const { formsRoutes } = await import('./routes/api/forms.js');
|
|
233
241
|
const { viewsRoutes } = await import('./routes/api/views.js');
|
|
@@ -242,7 +250,8 @@ await app.register(layoutsRoutes, { prefix: '/api' });
|
|
|
242
250
|
await app.register(navigationRoutes, { prefix: '/api' });
|
|
243
251
|
await app.register(mediaRoutes, { prefix: '/api' });
|
|
244
252
|
await app.register(usersRoutes, { prefix: '/api' });
|
|
245
|
-
await app.register(pluginsRoutes,
|
|
253
|
+
await app.register(pluginsRoutes, { prefix: '/api' });
|
|
254
|
+
await app.register(pluginMarketplaceRoutes, { prefix: '/api' });
|
|
246
255
|
await app.register(collectionsRoutes, { prefix: '/api' });
|
|
247
256
|
await app.register(formsRoutes, { prefix: '/api' });
|
|
248
257
|
await app.register(viewsRoutes, { prefix: '/api' });
|
|
@@ -257,6 +266,26 @@ await app.register(effectsRoutes, {prefix: '/api'});
|
|
|
257
266
|
|
|
258
267
|
await registerPlugins(app);
|
|
259
268
|
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// Public Plugin Routes (root-level, before catch-all)
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
const { authenticate: _authenticate, requireAdmin: _requireAdmin, requireVisibility: _requireVisibility } = await import('./middleware/auth.js');
|
|
274
|
+
for (const [name, plugin] of Object.entries(getLoadedPlugins())) {
|
|
275
|
+
if (!plugin.enabled || !plugin.publicEntry) continue;
|
|
276
|
+
try {
|
|
277
|
+
const { default: publicPlugin } = await import(plugin.publicEntry);
|
|
278
|
+
const settings = await getPluginSettings(name);
|
|
279
|
+
await app.register(publicPlugin, {
|
|
280
|
+
settings,
|
|
281
|
+
auth: { authenticate: _authenticate, requireAdmin: _requireAdmin, requireVisibility: _requireVisibility },
|
|
282
|
+
config: getConfig()
|
|
283
|
+
});
|
|
284
|
+
} catch (err) {
|
|
285
|
+
app.log.error(`[plugins] Failed to register public plugin ${name}: ${err.message}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
260
289
|
// ---------------------------------------------------------------------------
|
|
261
290
|
// Public Site (catch-all — must be last)
|
|
262
291
|
// ---------------------------------------------------------------------------
|
|
@@ -265,7 +265,7 @@ export async function createAction(data, userId = null) {
|
|
|
265
265
|
},
|
|
266
266
|
steps,
|
|
267
267
|
access: {
|
|
268
|
-
roles: access?.roles || ['admin'],
|
|
268
|
+
roles: access?.roles || ['admin', 'super-admin'],
|
|
269
269
|
rowLevel: access?.rowLevel || null
|
|
270
270
|
},
|
|
271
271
|
meta: { createdAt: now, updatedAt: now, createdBy: userId }
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for outbound calls from domma-cms → domma-cms-manager.
|
|
3
|
+
* Reads MANAGER_URL and INSTANCE_SECRET from env at call time.
|
|
4
|
+
* All functions return null/[] gracefully if MANAGER_URL is unset.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {createHmac} from 'node:crypto';
|
|
8
|
+
import {readFile} from 'node:fs/promises';
|
|
9
|
+
import {fileURLToPath} from 'node:url';
|
|
10
|
+
import {dirname, join} from 'node:path';
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Read the CMS version from the root package.json.
|
|
16
|
+
* @returns {Promise<string>} The version string, or '0.0.0' on error.
|
|
17
|
+
*/
|
|
18
|
+
async function getCmsVersion() {
|
|
19
|
+
try {
|
|
20
|
+
const pkgPath = join(__dirname, '../../package.json');
|
|
21
|
+
const raw = await readFile(pkgPath, 'utf-8');
|
|
22
|
+
return JSON.parse(raw).version ?? '0.0.0';
|
|
23
|
+
} catch {
|
|
24
|
+
return '0.0.0';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Register this CMS instance with domma-cms-manager.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} fingerprint - Unique instance fingerprint.
|
|
32
|
+
* @returns {Promise<{licenseToken: string, instanceId: string}|null>}
|
|
33
|
+
*/
|
|
34
|
+
export async function registerInstance(fingerprint) {
|
|
35
|
+
const MANAGER_URL = process.env.MANAGER_URL;
|
|
36
|
+
const INSTANCE_SECRET = process.env.INSTANCE_SECRET;
|
|
37
|
+
|
|
38
|
+
if (!MANAGER_URL) return null;
|
|
39
|
+
if (!INSTANCE_SECRET) {
|
|
40
|
+
console.warn('[managerClient] INSTANCE_SECRET not set — skipping registerInstance');
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const cmsVersion = await getCmsVersion();
|
|
46
|
+
const hostname = process.env.HOSTNAME ?? 'unknown';
|
|
47
|
+
|
|
48
|
+
const token = createHmac('sha256', INSTANCE_SECRET)
|
|
49
|
+
.update(fingerprint)
|
|
50
|
+
.digest('hex');
|
|
51
|
+
|
|
52
|
+
const res = await fetch(`${MANAGER_URL}/api/instances/register`, {
|
|
53
|
+
signal: AbortSignal.timeout(10_000),
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: {
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
'X-Instance-Token': token,
|
|
58
|
+
},
|
|
59
|
+
body: JSON.stringify({ fingerprint, cmsVersion, hostname }),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
console.warn(`[managerClient] registerInstance failed: ${res.status} ${res.statusText}`);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return await res.json();
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.warn('[managerClient] registerInstance error:', err.message);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Fetch the plugin catalogue from domma-cms-manager.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} licenseToken - License token obtained from registerInstance.
|
|
78
|
+
* @returns {Promise<Array<{slug: string, version: string, displayName: string, description: string, price: number, entitled: boolean}>>}
|
|
79
|
+
*/
|
|
80
|
+
export async function fetchCatalogue(licenseToken) {
|
|
81
|
+
const MANAGER_URL = process.env.MANAGER_URL;
|
|
82
|
+
|
|
83
|
+
if (!MANAGER_URL) return [];
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const res = await fetch(`${MANAGER_URL}/api/plugins/catalogue`, {
|
|
87
|
+
signal: AbortSignal.timeout(10_000),
|
|
88
|
+
headers: {
|
|
89
|
+
Authorization: `Bearer ${licenseToken}`,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
console.warn(`[managerClient] fetchCatalogue failed: ${res.status} ${res.statusText}`);
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return await res.json();
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.warn('[managerClient] fetchCatalogue error:', err.message);
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Fetch a plugin bundle (tarball + signature) from domma-cms-manager.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} slug - Plugin slug.
|
|
109
|
+
* @param {string} version - Plugin version.
|
|
110
|
+
* @param {string} licenseToken - License token obtained from registerInstance.
|
|
111
|
+
* @returns {Promise<{tarball: Buffer, signature: string, publicKeyId: string}|null>}
|
|
112
|
+
*/
|
|
113
|
+
export async function fetchPluginBundle(slug, version, licenseToken) {
|
|
114
|
+
const MANAGER_URL = process.env.MANAGER_URL;
|
|
115
|
+
|
|
116
|
+
if (!MANAGER_URL) return null;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const res = await fetch(`${MANAGER_URL}/api/plugins/install/${slug}/${version}`, {
|
|
120
|
+
signal: AbortSignal.timeout(10_000),
|
|
121
|
+
headers: {
|
|
122
|
+
Authorization: `Bearer ${licenseToken}`,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (!res.ok) {
|
|
127
|
+
console.warn(`[managerClient] fetchPluginBundle failed: ${res.status} ${res.statusText}`);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const json = await res.json();
|
|
132
|
+
|
|
133
|
+
if (typeof json.tarball !== 'string' || json.tarball.length > 100 * 1024 * 1024) {
|
|
134
|
+
console.warn('[managerClient] fetchPluginBundle: tarball too large or invalid, refusing decode');
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
tarball: Buffer.from(json.tarball, 'base64'),
|
|
140
|
+
signature: json.signature,
|
|
141
|
+
publicKeyId: json.publicKeyId,
|
|
142
|
+
};
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.warn('[managerClient] fetchPluginBundle error:', err.message);
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Send a heartbeat to domma-cms-manager with the list of installed plugins.
|
|
151
|
+
*
|
|
152
|
+
* @param {string} licenseToken - License token obtained from registerInstance.
|
|
153
|
+
* @param {string[]} installedSlugs - Array of installed plugin slugs.
|
|
154
|
+
* @returns {Promise<boolean>} True on success, false on error.
|
|
155
|
+
*/
|
|
156
|
+
export async function heartbeat(licenseToken, installedSlugs) {
|
|
157
|
+
const MANAGER_URL = process.env.MANAGER_URL;
|
|
158
|
+
|
|
159
|
+
if (!MANAGER_URL) return false;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const res = await fetch(`${MANAGER_URL}/api/instances/heartbeat`, {
|
|
163
|
+
signal: AbortSignal.timeout(10_000),
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: {
|
|
166
|
+
'Content-Type': 'application/json',
|
|
167
|
+
Authorization: `Bearer ${licenseToken}`,
|
|
168
|
+
},
|
|
169
|
+
body: JSON.stringify({ installedSlugs, versions: {} }),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (!res.ok) {
|
|
173
|
+
console.warn(`[managerClient] heartbeat failed: ${res.status} ${res.statusText}`);
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return true;
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.warn('[managerClient] heartbeat error:', err.message);
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -80,7 +80,16 @@ function renderCollectionBlocks(entries, blockTemplate, emptyMsg, ctaOpts, cols)
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
const items = entries.map(e => {
|
|
83
|
-
|
|
83
|
+
// Triple-brace `{{{field}}}` — raw HTML pass-through (no escape).
|
|
84
|
+
// Use for fields that contain pre-rendered HTML (bullet lists, code
|
|
85
|
+
// blocks, embedded elements). Must be processed BEFORE double-brace
|
|
86
|
+
// so the outer `{` and `}` of a triple match aren't consumed by the
|
|
87
|
+
// double-brace regex.
|
|
88
|
+
let html = blockTemplate.replace(/\{\{\{(\w+)\}\}\}/g, (_, key) => {
|
|
89
|
+
return String(e.data?.[key] ?? '');
|
|
90
|
+
});
|
|
91
|
+
// Double-brace `{{field}}` — HTML-escaped (default, safe).
|
|
92
|
+
html = html.replace(/\{\{([\w_]+)\}\}/g, (_, key) => {
|
|
84
93
|
if (key === '_id') return escapeHtmlText(e.id ?? '');
|
|
85
94
|
if (key === '_createdAt') return escapeHtmlText(e.meta?.createdAt ?? '');
|
|
86
95
|
if (key === '_updatedAt') return escapeHtmlText(e.meta?.updatedAt ?? '');
|
|
@@ -107,7 +116,9 @@ function renderCollectionBlocks(entries, blockTemplate, emptyMsg, ctaOpts, cols)
|
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
function renderBlockFromAttrs(tmpl, attrs) {
|
|
110
|
-
|
|
119
|
+
// Triple-brace raw pass-through first, then double-brace escaped.
|
|
120
|
+
let rendered = tmpl.replace(/\{\{\{(\w+)\}\}\}/g, (_, key) => String(attrs[key] ?? ''));
|
|
121
|
+
rendered = rendered.replace(/\{\{([\w_]+)\}\}/g, (_, key) => escapeHtmlText(attrs[key] ?? ''));
|
|
111
122
|
const extraClass = attrs.class ? ` ${escapeAttr(attrs.class)}` : '';
|
|
112
123
|
const idAttr = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
|
|
113
124
|
return `<div class="dm-static-block${extraClass}"${idAttr}>${rendered}</div>`;
|
|
@@ -442,6 +453,22 @@ async function processCollectionBlocks(markdown) {
|
|
|
442
453
|
if (!schema) throw new Error('not found');
|
|
443
454
|
|
|
444
455
|
let {entries} = await listEntries(slug);
|
|
456
|
+
|
|
457
|
+
// Row-level filter: where="field=value" (simple equality only).
|
|
458
|
+
// Comma-separate multiple predicates, all AND'd together.
|
|
459
|
+
// Example: where="tab=developers" or where="tab=developers,status=live"
|
|
460
|
+
const whereAttr = typeof attrs.where === 'string' ? attrs.where.trim() : '';
|
|
461
|
+
if (whereAttr) {
|
|
462
|
+
const predicates = whereAttr.split(',').map(p => p.trim()).filter(Boolean).map(p => {
|
|
463
|
+
const eq = p.indexOf('=');
|
|
464
|
+
if (eq === -1) return null;
|
|
465
|
+
return { key: p.slice(0, eq).trim(), val: p.slice(eq + 1).trim() };
|
|
466
|
+
}).filter(Boolean);
|
|
467
|
+
if (predicates.length) {
|
|
468
|
+
entries = entries.filter(e => predicates.every(({key, val}) => String(e.data?.[key] ?? '') === val));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
445
472
|
entries = sortEntries(entries, sort, order);
|
|
446
473
|
if (limitAttr > 0) entries = entries.slice(0, limitAttr);
|
|
447
474
|
|
|
@@ -755,7 +782,12 @@ function processGridBlocks(markdown) {
|
|
|
755
782
|
const attrs = parseShortcodeAttrs(attrStr);
|
|
756
783
|
const cls = attrs.span ? ` class="col-span-${attrs.span}"` : ' class="col"';
|
|
757
784
|
const id = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
|
|
758
|
-
|
|
785
|
+
// Restore the col body before passing it downstream — otherwise the
|
|
786
|
+
// scrub placeholders from this processor's store get carried into
|
|
787
|
+
// processCardBlocks, which creates its own empty store and fails to
|
|
788
|
+
// decode them, producing the literal string "undefined" in place of
|
|
789
|
+
// whatever was in <pre>, ``` fences, or `inline code`.
|
|
790
|
+
return `<div${cls}${id}>${marked.parse(processCardBlocks(restore(body.trim())))}</div>`;
|
|
759
791
|
}
|
|
760
792
|
);
|
|
761
793
|
|
|
@@ -1345,16 +1377,14 @@ function renderLegacyCard(attrs, body, markedInstance, escAttr) {
|
|
|
1345
1377
|
const icon = strAttr('icon');
|
|
1346
1378
|
const footer = strAttr('footer');
|
|
1347
1379
|
const collapsible = attrs.collapsible === 'true';
|
|
1348
|
-
const hover = 'hover' in attrs;
|
|
1349
1380
|
const variant = strAttr('variant');
|
|
1350
|
-
const extraClass = strAttr('class');
|
|
1351
1381
|
|
|
1352
|
-
// Root class list
|
|
1353
|
-
|
|
1382
|
+
// Root class list — delegate to the shared helper so variant="gradient",
|
|
1383
|
+
// gradient="<name>", glass/accent/dark/glow, font, shadow, rounded, etc.
|
|
1384
|
+
// all work on legacy cards (cards without a `layout` attribute).
|
|
1385
|
+
const classes = cardVariantClasses(attrs);
|
|
1386
|
+
// Preserve legacy support for variant="primary" (not handled by cardVariantClasses).
|
|
1354
1387
|
if (variant === 'primary') classes.push('card-primary');
|
|
1355
|
-
if (hover) classes.push('card-hover');
|
|
1356
|
-
if (collapsible) classes.push('card-collapsible');
|
|
1357
|
-
if (extraClass) classes.push(extraClass);
|
|
1358
1388
|
|
|
1359
1389
|
const id = attrs.id ? ` id="${escAttr(attrs.id)}"` : '';
|
|
1360
1390
|
const coll = collapsible ? ' data-collapsible="true"' : '';
|
|
@@ -1440,9 +1470,16 @@ function processCardBlocks(markdown) {
|
|
|
1440
1470
|
const attrs = parseShortcodeAttrs(attrStr);
|
|
1441
1471
|
const layout = typeof attrs.layout === 'string' ? attrs.layout.trim() : '';
|
|
1442
1472
|
const renderer = LAYOUT_RENDERERS[layout];
|
|
1473
|
+
// Restore code regions INSIDE the card body before rendering so
|
|
1474
|
+
// marked can process fenced code blocks (```lang ... ```) normally.
|
|
1475
|
+
// Without this, the body contains placeholder tokens that marked
|
|
1476
|
+
// wraps in <p>, and the outer restore() call then substitutes raw
|
|
1477
|
+
// backticks into the <p>, producing literal '```json' text and
|
|
1478
|
+
// collapsing the HTML on any inline-backtick template literal.
|
|
1479
|
+
const restoredBody = restore(body);
|
|
1443
1480
|
return renderer
|
|
1444
|
-
? renderer(attrs,
|
|
1445
|
-
: renderLegacyCard(attrs,
|
|
1481
|
+
? renderer(attrs, restoredBody, marked, escapeAttr)
|
|
1482
|
+
: renderLegacyCard(attrs, restoredBody, marked, escapeAttr);
|
|
1446
1483
|
}
|
|
1447
1484
|
));
|
|
1448
1485
|
}
|
|
@@ -1865,8 +1902,9 @@ function processTextBlocks(markdown) {
|
|
|
1865
1902
|
styles.push('font-style:italic');
|
|
1866
1903
|
}
|
|
1867
1904
|
|
|
1868
|
-
|
|
1869
|
-
|
|
1905
|
+
const colourVal = attrs.colour || attrs.color;
|
|
1906
|
+
if (colourVal) {
|
|
1907
|
+
styles.push(`color:${COLOR_TOKENS[colourVal] || colourVal}`);
|
|
1870
1908
|
}
|
|
1871
1909
|
|
|
1872
1910
|
if (attrs.font && FONT_MAP[attrs.font]) {
|