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.
Files changed (44) hide show
  1. package/admin/js/api.js +1 -1
  2. package/admin/js/app.js +4 -4
  3. package/admin/js/lib/markdown-toolbar.js +14 -14
  4. package/admin/js/views/collection-editor.js +5 -3
  5. package/admin/js/views/collections.js +1 -1
  6. package/admin/js/views/page-editor.js +27 -27
  7. package/config/plugins.json +16 -0
  8. package/config/site.json +1 -1
  9. package/package.json +2 -2
  10. package/plugins/analytics/stats.json +1 -1
  11. package/plugins/contacts/admin/templates/contacts.html +126 -0
  12. package/plugins/contacts/admin/views/contacts.js +710 -0
  13. package/plugins/contacts/config.js +6 -0
  14. package/plugins/contacts/data/contacts.json +20 -0
  15. package/plugins/contacts/plugin.js +351 -0
  16. package/plugins/contacts/plugin.json +23 -0
  17. package/plugins/docs/admin/templates/docs.html +69 -0
  18. package/plugins/docs/admin/views/docs.js +276 -0
  19. package/plugins/docs/config.js +8 -0
  20. package/plugins/docs/data/documents/452f49b7-9c93-4a67-874d-27f882891ad2.json +11 -0
  21. package/plugins/docs/data/documents/57e003f0-68f2-47dc-9c36-ed4b10ed3deb.json +11 -0
  22. package/plugins/docs/data/folders.json +9 -0
  23. package/plugins/docs/data/templates.json +1 -0
  24. package/plugins/docs/plugin.js +375 -0
  25. package/plugins/docs/plugin.json +23 -0
  26. package/plugins/notes/admin/templates/notes.html +92 -0
  27. package/plugins/notes/admin/views/notes.js +304 -0
  28. package/plugins/notes/config.js +6 -0
  29. package/plugins/notes/data/notes.json +1 -0
  30. package/plugins/notes/plugin.js +177 -0
  31. package/plugins/notes/plugin.json +23 -0
  32. package/plugins/todo/admin/templates/todo.html +164 -0
  33. package/plugins/todo/admin/views/todo.js +328 -0
  34. package/plugins/todo/config.js +7 -0
  35. package/plugins/todo/data/todos.json +1 -0
  36. package/plugins/todo/plugin.js +155 -0
  37. package/plugins/todo/plugin.json +23 -0
  38. package/server/routes/api/auth.js +2 -0
  39. package/server/routes/api/collections.js +55 -0
  40. package/server/routes/api/forms.js +3 -0
  41. package/server/routes/api/settings.js +16 -1
  42. package/server/routes/public.js +2 -0
  43. package/server/services/markdown.js +169 -8
  44. 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
- // → countdown → timeline → spacer → center → icon → form → hero → table → badge → button → link → cta
1515
- // → grid → card → slideover → marked → sanitize → afterParse
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 withDconfig = processDConfigBlocks(withView);
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 = processBadgeBlocks(withTable);
1533
- const withButton = processButtonBlocks(withBadge);
1534
- const withLink = processLinkBlocks(withButton);
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
- await loadConfigDefaults(manifest.name);
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
- hooks: {registerShortcode, registerSanitizeRules, registerTransform, on: hooks.on.bind(hooks)}
160
+ hooks: {registerShortcode, registerSanitizeRules, registerTransform, on: hooks.on.bind(hooks)},
161
+ settings
161
162
  });
162
163
  loaded.push(manifest.name);
163
164
  } catch (err) {