domma-cms 0.6.16 → 0.6.21
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/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/lib/markdown-toolbar.js +14 -14
- package/admin/js/views/collection-editor.js +5 -3
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/page-editor.js +27 -27
- package/config/plugins.json +16 -0
- package/config/site.json +1 -1
- package/package.json +2 -2
- package/plugins/analytics/stats.json +1 -1
- package/plugins/contacts/admin/templates/contacts.html +126 -0
- package/plugins/contacts/admin/views/contacts.js +710 -0
- package/plugins/contacts/config.js +6 -0
- package/plugins/contacts/data/contacts.json +20 -0
- package/plugins/contacts/plugin.js +351 -0
- package/plugins/contacts/plugin.json +23 -0
- package/plugins/docs/admin/templates/docs.html +69 -0
- package/plugins/docs/admin/views/docs.js +276 -0
- package/plugins/docs/config.js +8 -0
- package/plugins/docs/data/documents/452f49b7-9c93-4a67-874d-27f882891ad2.json +11 -0
- package/plugins/docs/data/documents/57e003f0-68f2-47dc-9c36-ed4b10ed3deb.json +11 -0
- package/plugins/docs/data/folders.json +9 -0
- package/plugins/docs/data/templates.json +1 -0
- package/plugins/docs/plugin.js +375 -0
- package/plugins/docs/plugin.json +23 -0
- package/plugins/notes/admin/templates/notes.html +92 -0
- package/plugins/notes/admin/views/notes.js +304 -0
- package/plugins/notes/config.js +6 -0
- package/plugins/notes/data/notes.json +1 -0
- package/plugins/notes/plugin.js +177 -0
- package/plugins/notes/plugin.json +23 -0
- package/plugins/todo/admin/templates/todo.html +164 -0
- package/plugins/todo/admin/views/todo.js +328 -0
- package/plugins/todo/config.js +7 -0
- package/plugins/todo/data/todos.json +1 -0
- package/plugins/todo/plugin.js +155 -0
- package/plugins/todo/plugin.json +23 -0
- package/server/routes/api/auth.js +2 -0
- package/server/routes/api/collections.js +55 -0
- package/server/routes/api/forms.js +3 -0
- package/server/routes/api/settings.js +16 -1
- package/server/routes/public.js +2 -0
- package/server/services/markdown.js +169 -8
- package/server/services/plugins.js +3 -2
|
@@ -96,6 +96,49 @@ function renderCollectionBlocks(entries, blockTemplate, emptyMsg, ctaOpts, cols)
|
|
|
96
96
|
return `<div class="${wrapperClass}">\n${items.join('\n')}\n</div>`;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Process [block template="name" field1="val" field2="val" /] shortcodes.
|
|
101
|
+
* Loads the named block template and substitutes {{placeholders}} with
|
|
102
|
+
* the supplied attribute values. Renders a single block instance.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} content
|
|
105
|
+
* @returns {Promise<string>}
|
|
106
|
+
*/
|
|
107
|
+
async function processStaticBlocks(content) {
|
|
108
|
+
const pattern = /\[block\s+([\s\S]+?)\/\]/g;
|
|
109
|
+
const matches = [...content.matchAll(pattern)];
|
|
110
|
+
if (!matches.length) return content;
|
|
111
|
+
|
|
112
|
+
let output = content;
|
|
113
|
+
// Process in reverse so string indices stay valid as we replace
|
|
114
|
+
for (const match of matches.toReversed()) {
|
|
115
|
+
const attrsStr = match[1];
|
|
116
|
+
const attrs = {};
|
|
117
|
+
for (const [, key, dq, sq] of attrsStr.matchAll(/([\w-]+)=(?:"([^"]*)"|'([^']*)')/g)) {
|
|
118
|
+
attrs[key] = dq ?? sq ?? '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const templateName = attrs.template;
|
|
122
|
+
if (!templateName) {
|
|
123
|
+
output = output.slice(0, match.index) + '' + output.slice(match.index + match[0].length);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let replacement = '';
|
|
128
|
+
try {
|
|
129
|
+
const tmpl = await loadBlockTemplate(templateName);
|
|
130
|
+
const rendered = tmpl.replace(/\{\{([\w_]+)\}\}/g, (_, key) => {
|
|
131
|
+
return escapeHtmlText(attrs[key] ?? '');
|
|
132
|
+
});
|
|
133
|
+
replacement = `<div class="dm-static-block">${rendered}</div>`;
|
|
134
|
+
} catch (_) {
|
|
135
|
+
// Template not found — emit nothing
|
|
136
|
+
}
|
|
137
|
+
output = output.slice(0, match.index) + replacement + output.slice(match.index + match[0].length);
|
|
138
|
+
}
|
|
139
|
+
return output;
|
|
140
|
+
}
|
|
141
|
+
|
|
99
142
|
function renderCollectionTable(slug, entries, visibleFields, attrs, ctaOpts) {
|
|
100
143
|
const columns = visibleFields.map(f => ({key: f.name, title: f.label || f.name}));
|
|
101
144
|
const rows = entries.map(e => {
|
|
@@ -1023,6 +1066,122 @@ function processBadgeBlocks(markdown) {
|
|
|
1023
1066
|
return restore(result);
|
|
1024
1067
|
}
|
|
1025
1068
|
|
|
1069
|
+
/**
|
|
1070
|
+
* Pre-process [text] shortcodes before running through marked.
|
|
1071
|
+
*
|
|
1072
|
+
* Syntax (paired):
|
|
1073
|
+
* [text size="xl" bold italic color="primary" font="Georgia"]Your text[/text]
|
|
1074
|
+
*
|
|
1075
|
+
* Outputs a <span> with all resolved styles inlined.
|
|
1076
|
+
*
|
|
1077
|
+
* @param {string} markdown
|
|
1078
|
+
* @returns {string}
|
|
1079
|
+
*/
|
|
1080
|
+
function processTextBlocks(markdown) {
|
|
1081
|
+
const {scrubbed, restore} = scrubCodeRegions(markdown);
|
|
1082
|
+
|
|
1083
|
+
const SIZE_MAP = {
|
|
1084
|
+
xs: '.75rem', sm: '.875rem', base: '1rem', lg: '1.125rem',
|
|
1085
|
+
xl: '1.25rem', '2xl': '1.5rem', '3xl': '1.875rem', '4xl': '2.25rem',
|
|
1086
|
+
};
|
|
1087
|
+
const WEIGHT_MAP = {
|
|
1088
|
+
thin: '100', light: '300', normal: '400', medium: '500',
|
|
1089
|
+
semibold: '600', bold: '700', extrabold: '800', black: '900',
|
|
1090
|
+
};
|
|
1091
|
+
const SPACING_MAP = {
|
|
1092
|
+
tight: '-0.05em', normal: '0em', wide: '0.05em', wider: '0.1em',
|
|
1093
|
+
};
|
|
1094
|
+
const COLOR_TOKENS = {
|
|
1095
|
+
primary: 'var(--dm-color-primary)', secondary: 'var(--dm-color-secondary)',
|
|
1096
|
+
muted: 'var(--dm-text-muted)', danger: 'var(--dm-color-danger)',
|
|
1097
|
+
success: 'var(--dm-color-success)', warning: 'var(--dm-color-warning)',
|
|
1098
|
+
info: 'var(--dm-color-info)',
|
|
1099
|
+
};
|
|
1100
|
+
const FONT_MAP = {
|
|
1101
|
+
'Georgia': 'Georgia,serif',
|
|
1102
|
+
'Arial': 'Arial,sans-serif',
|
|
1103
|
+
'Verdana': 'Verdana,sans-serif',
|
|
1104
|
+
'Courier New': "'Courier New',monospace",
|
|
1105
|
+
'Times New Roman': "'Times New Roman',serif",
|
|
1106
|
+
'Trebuchet MS': "'Trebuchet MS',sans-serif",
|
|
1107
|
+
};
|
|
1108
|
+
const TRANSFORM_MAP = {
|
|
1109
|
+
upper: 'uppercase', lower: 'lowercase', capitalize: 'capitalize', none: 'none',
|
|
1110
|
+
};
|
|
1111
|
+
const DECORATION_MAP = {
|
|
1112
|
+
underline: 'underline', 'line-through': 'line-through', none: 'none',
|
|
1113
|
+
};
|
|
1114
|
+
|
|
1115
|
+
function buildText(attrStr, inner) {
|
|
1116
|
+
const attrs = parseShortcodeAttrs(attrStr || '');
|
|
1117
|
+
const styles = [];
|
|
1118
|
+
|
|
1119
|
+
if (attrs.size) {
|
|
1120
|
+
styles.push(`font-size:${SIZE_MAP[attrs.size] || attrs.size}`);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
if (attrs['point-size']) {
|
|
1124
|
+
const pt = parseFloat(attrs['point-size']);
|
|
1125
|
+
if (!isNaN(pt) && pt > 0) styles.push(`font-size:${pt}pt`);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// bold flag takes precedence over weight attr
|
|
1129
|
+
const isBold = /\bbold\b/i.test(attrStr);
|
|
1130
|
+
if (isBold) {
|
|
1131
|
+
styles.push('font-weight:700');
|
|
1132
|
+
} else if (attrs.weight && WEIGHT_MAP[attrs.weight]) {
|
|
1133
|
+
styles.push(`font-weight:${WEIGHT_MAP[attrs.weight]}`);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (/\bitalic\b/i.test(attrStr)) {
|
|
1137
|
+
styles.push('font-style:italic');
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
if (attrs.color) {
|
|
1141
|
+
styles.push(`color:${COLOR_TOKENS[attrs.color] || attrs.color}`);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
if (attrs.font && FONT_MAP[attrs.font]) {
|
|
1145
|
+
styles.push(`font-family:${FONT_MAP[attrs.font]}`);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
if (attrs.transform && TRANSFORM_MAP[attrs.transform]) {
|
|
1149
|
+
styles.push(`text-transform:${TRANSFORM_MAP[attrs.transform]}`);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
if (attrs.decoration && DECORATION_MAP[attrs.decoration]) {
|
|
1153
|
+
styles.push(`text-decoration:${DECORATION_MAP[attrs.decoration]}`);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
if (attrs.spacing) {
|
|
1157
|
+
styles.push(`letter-spacing:${SPACING_MAP[attrs.spacing] || attrs.spacing}`);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
if (attrs.align) {
|
|
1161
|
+
styles.push('display:block');
|
|
1162
|
+
styles.push(`text-align:${escapeAttr(attrs.align)}`);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
if (attrs.style) {
|
|
1166
|
+
styles.push(attrs.style.trim().replace(/;+$/, ''));
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
const styleAttr = styles.length ? ` style="${styles.join(';')}"` : '';
|
|
1170
|
+
const classAttr = attrs.class ? ` class="${escapeAttr(attrs.class)}"` : '';
|
|
1171
|
+
const idAttr = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
|
|
1172
|
+
|
|
1173
|
+
return `<span${styleAttr}${classAttr}${idAttr}>${inner.trim()}</span>`;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Note: [\s\S]*? allows multi-line body content (deliberate divergence from badge's [^\n]*?)
|
|
1177
|
+
const result = scrubbed.replace(
|
|
1178
|
+
/\[text([^\]]*)\]([\s\S]*?)\[\/text\]/gi,
|
|
1179
|
+
(_, attrStr, body) => buildText(attrStr, body)
|
|
1180
|
+
);
|
|
1181
|
+
|
|
1182
|
+
return restore(result);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1026
1185
|
/**
|
|
1027
1186
|
* Pre-process [button] shortcodes before running through marked.
|
|
1028
1187
|
*
|
|
@@ -1369,7 +1528,7 @@ function processHeroBlocks(markdown) {
|
|
|
1369
1528
|
(twinkleColour ? ` data-fx-colour="${escapeAttr(twinkleColour)}"` : '')
|
|
1370
1529
|
: '';
|
|
1371
1530
|
|
|
1372
|
-
const processedBody = processBadgeBlocks(processCardBlocks(processGridBlocks(restore(body.trim()))));
|
|
1531
|
+
const processedBody = processTextBlocks(processBadgeBlocks(processCardBlocks(processGridBlocks(restore(body.trim())))));
|
|
1373
1532
|
|
|
1374
1533
|
let inner = '<div class="hero-content">';
|
|
1375
1534
|
if (title) inner += `<h1 class="hero-title hero-title-responsive">${escapeAttr(title)}</h1>`;
|
|
@@ -1510,13 +1669,14 @@ export async function parseMarkdown(raw) {
|
|
|
1510
1669
|
const extensions = getSanitizeExtensions();
|
|
1511
1670
|
|
|
1512
1671
|
// Pipeline:
|
|
1513
|
-
// beforeParse → collection → view → dconfig → plugin shortcodes → tabs → accordion → carousel
|
|
1514
|
-
|
|
1515
|
-
|
|
1672
|
+
// beforeParse → collection → view → staticBlock → dconfig → plugin shortcodes → tabs → accordion → carousel
|
|
1673
|
+
// → countdown → timeline → spacer → center → icon → form → hero → table → badge → button → link → cta
|
|
1674
|
+
// → grid → card → slideover → marked → sanitize → afterParse
|
|
1516
1675
|
const preprocessed = applyTransforms('markdown:beforeParse', content);
|
|
1517
1676
|
const withCollection = await processCollectionBlocks(preprocessed);
|
|
1518
1677
|
const withView = await processViewBlocks(withCollection);
|
|
1519
|
-
const
|
|
1678
|
+
const withStaticBlock = await processStaticBlocks(withView);
|
|
1679
|
+
const withDconfig = processDConfigBlocks(withStaticBlock);
|
|
1520
1680
|
const withPluginShortcodes = processPluginShortcodes(withDconfig);
|
|
1521
1681
|
const withTabs = processTabsBlocks(withPluginShortcodes);
|
|
1522
1682
|
const withAccordion = processAccordionBlocks(withTabs);
|
|
@@ -1529,9 +1689,10 @@ export async function parseMarkdown(raw) {
|
|
|
1529
1689
|
const withForm = await processFormBlocks(withIcon);
|
|
1530
1690
|
const withHero = processHeroBlocks(withForm);
|
|
1531
1691
|
const withTable = processTableBlocks(withHero);
|
|
1532
|
-
const withBadge
|
|
1533
|
-
|
|
1534
|
-
|
|
1692
|
+
const withBadge = processBadgeBlocks(withTable);
|
|
1693
|
+
const withText = processTextBlocks(withBadge);
|
|
1694
|
+
const withButton = processButtonBlocks(withText);
|
|
1695
|
+
const withLink = processLinkBlocks(withButton);
|
|
1535
1696
|
const withCta = processCtaBlocks(withLink);
|
|
1536
1697
|
const withGrid = processGridBlocks(withCta);
|
|
1537
1698
|
const withCard = processCardBlocks(withGrid);
|
|
@@ -152,12 +152,13 @@ export async function registerPlugins(fastify) {
|
|
|
152
152
|
const entryPath = path.join(PLUGINS_DIR, manifest.name, 'plugin.js');
|
|
153
153
|
try {
|
|
154
154
|
const { default: plugin } = await import(entryPath);
|
|
155
|
-
|
|
155
|
+
const settings = await getPluginSettings(manifest.name);
|
|
156
156
|
const prefix = `/api/plugins/${manifest.name}`;
|
|
157
157
|
await fastify.register(plugin, {
|
|
158
158
|
prefix,
|
|
159
159
|
auth: {authenticate, requireRole, requireAdmin},
|
|
160
|
-
|
|
160
|
+
hooks: {registerShortcode, registerSanitizeRules, registerTransform, on: hooks.on.bind(hooks)},
|
|
161
|
+
settings
|
|
161
162
|
});
|
|
162
163
|
loaded.push(manifest.name);
|
|
163
164
|
} catch (err) {
|