domma-cms 0.46.4 → 0.47.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.46.4",
3
+ "version": "0.47.1",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -0,0 +1 @@
1
+ export const SIZE_MAP={xs:".75rem",sm:".875rem",base:"1rem",lg:"1.125rem",xl:"1.25rem","2xl":"1.5rem","3xl":"1.875rem","4xl":"2.25rem"},WEIGHT_MAP={thin:"100",light:"300",normal:"400",medium:"500",semibold:"600",bold:"700",extrabold:"800",black:"900"},SPACING_MAP={tight:"-0.05em",normal:"0em",wide:"0.05em",wider:"0.1em"},COLOUR_TOKENS={primary:"var(--dm-color-primary)",secondary:"var(--dm-color-secondary)",muted:"var(--dm-text-muted)",danger:"var(--dm-color-danger)",success:"var(--dm-color-success)",warning:"var(--dm-color-warning)",info:"var(--dm-color-info)"},FONT_MAP={Georgia:"Georgia,serif",Arial:"Arial,sans-serif",Verdana:"Verdana,sans-serif","Courier New":"'Courier New',monospace","Times New Roman":"'Times New Roman',serif","Trebuchet MS":"'Trebuchet MS',sans-serif"},TRANSFORM_MAP={upper:"uppercase",lower:"lowercase",capitalize:"capitalize",none:"none"},DECORATION_MAP={underline:"underline","line-through":"line-through",none:"none"};function l(e){return e!=null&&e!==!1&&e!=="false"&&e!=="0"}function i(e){return String(e).replace(/["<>]/g,"").trim()}export function buildTextStyle(e={}){const o=[];if(e.size&&o.push(`font-size:${SIZE_MAP[e.size]||i(e.size)}`),e["point-size"]){const r=parseFloat(e["point-size"]);!isNaN(r)&&r>0&&o.push(`font-size:${r}pt`)}l(e.bold)?o.push("font-weight:700"):e.weight&&WEIGHT_MAP[e.weight]&&o.push(`font-weight:${WEIGHT_MAP[e.weight]}`),l(e.italic)&&o.push("font-style:italic");const n=e.colour||e.color;return n&&o.push(`color:${COLOUR_TOKENS[n]||i(n)}`),e.font&&FONT_MAP[e.font]&&o.push(`font-family:${FONT_MAP[e.font]}`),e.transform&&TRANSFORM_MAP[e.transform]&&o.push(`text-transform:${TRANSFORM_MAP[e.transform]}`),e.decoration&&DECORATION_MAP[e.decoration]&&o.push(`text-decoration:${DECORATION_MAP[e.decoration]}`),e.spacing&&o.push(`letter-spacing:${SPACING_MAP[e.spacing]||i(e.spacing)}`),e.align&&(o.push("display:block"),o.push(`text-align:${i(e.align)}`)),e.style&&o.push(i(e.style).replace(/;+$/,"")),o.join(";")}
@@ -15,6 +15,7 @@ import {getMenu, resolveLocation, resolveMenuDecorations} from './menus.js';
15
15
  import {buildMenuNav} from './menuRender.js';
16
16
  import {checkVisibility} from '../middleware/auth.js';
17
17
  import {getRoleHierarchy} from './roles.js';
18
+ import {buildTextStyle} from '../../public/js/text-style.mjs';
18
19
 
19
20
  const __dirname_md = path.dirname(fileURLToPath(import.meta.url));
20
21
  const BLOCKS_DIR = path.resolve(__dirname_md, '../../content/blocks');
@@ -1667,7 +1668,7 @@ function processEffectsBlocks(markdown) {
1667
1668
  if (body === null) return `<div class="${classes.join(' ')}"></div>`;
1668
1669
  // Restore code placeholders before the nested parse (tabs-handler
1669
1670
  // pattern) so code spans/fences render as <code>, not raw backticks.
1670
- const innerHtml = marked.parse(processCardBlocks(processGridBlocks(restore(body.trim()))));
1671
+ const innerHtml = marked.parse(processCardBlocks(processGridBlocks(processInlineShortcodes(restore(body.trim())))));
1671
1672
  return `<div class="${classes.join(' ')}">${innerHtml}</div>\n`;
1672
1673
  });
1673
1674
 
@@ -1684,7 +1685,7 @@ function processEffectsBlocks(markdown) {
1684
1685
  if (!hasActions) {
1685
1686
  // Restore code placeholders before the nested parse (tabs-handler
1686
1687
  // pattern) so code spans/fences render as <code>, not raw backticks.
1687
- const innerHtml = marked.parse(processCardBlocks(processGridBlocks(restore(body.trim()))));
1688
+ const innerHtml = marked.parse(processCardBlocks(processGridBlocks(processInlineShortcodes(restore(body.trim())))));
1688
1689
  return `<div class="dm-fx-scribe"${dataAttrs}>${innerHtml}</div>\n`;
1689
1690
  }
1690
1691
  const actions = [];
@@ -1723,7 +1724,7 @@ function processEffectsBlocks(markdown) {
1723
1724
  const dataAttrs = Object.entries(attrs).map(([k, v]) => ` data-fx-${k}="${v}"`).join('');
1724
1725
  // Restore code placeholders before the nested parse (tabs-handler
1725
1726
  // pattern) so code spans/fences render as <code>, not raw backticks.
1726
- const innerHtml = marked.parse(processCardBlocks(processGridBlocks(restore(body.trim()))));
1727
+ const innerHtml = marked.parse(processCardBlocks(processGridBlocks(processInlineShortcodes(restore(body.trim())))));
1727
1728
  return `<div class="dm-fx-${name}"${dataAttrs}>${innerHtml}</div>\n`;
1728
1729
  });
1729
1730
  }
@@ -1740,7 +1741,7 @@ function processEffectsBlocks(markdown) {
1740
1741
  }
1741
1742
  // Restore code placeholders before the nested parse (tabs-handler
1742
1743
  // pattern) so code spans/fences render as <code>, not raw backticks.
1743
- const innerHtml = marked.parse(processCardBlocks(processGridBlocks(restore(body.trim()))));
1744
+ const innerHtml = marked.parse(processCardBlocks(processGridBlocks(processInlineShortcodes(restore(body.trim())))));
1744
1745
  return `<div class="dm-fx-ticker-tape" data-fx-mode="container"${dataAttrs}>${innerHtml}</div>\n`;
1745
1746
  });
1746
1747
 
@@ -1758,7 +1759,7 @@ function processEffectsBlocks(markdown) {
1758
1759
  }
1759
1760
  // Restore code placeholders before the nested parse (tabs-handler
1760
1761
  // pattern) so code spans/fences render as <code>, not raw backticks.
1761
- const innerHtml = marked.parse(processCardBlocks(processGridBlocks(restore(body.trim()))));
1762
+ const innerHtml = marked.parse(processCardBlocks(processGridBlocks(processInlineShortcodes(restore(body.trim())))));
1762
1763
  return `<div class="dm-fx-${name}" data-fx-mode="container"${dataAttrs}>${innerHtml}</div>\n`;
1763
1764
  });
1764
1765
  }
@@ -1771,7 +1772,7 @@ function processEffectsBlocks(markdown) {
1771
1772
  if (attrs.duration) classes.push(`animate-duration-${attrs.duration}`);
1772
1773
  if (attrs.delay) classes.push(`animate-delay-${attrs.delay}`);
1773
1774
  if (attrs.repeat) classes.push(`animate-${attrs.repeat}`);
1774
- return `<div class="${classes.join(' ')}">${marked.parse(processCardBlocks(processGridBlocks(restore(body.trim()))))}</div>\n`;
1775
+ return `<div class="${classes.join(' ')}">${marked.parse(processCardBlocks(processGridBlocks(processInlineShortcodes(restore(body.trim())))))}</div>\n`;
1775
1776
  });
1776
1777
 
1777
1778
  apply('ambient', (attrStr, body) => {
@@ -1781,7 +1782,7 @@ function processEffectsBlocks(markdown) {
1781
1782
  if (attrs.type) classes.push(`bg-ambient-${attrs.type}`);
1782
1783
  if (attrs.speed) classes.push(`bg-ambient-${attrs.speed}`);
1783
1784
  if (attrs.intensity) classes.push(`bg-ambient-${attrs.intensity}`);
1784
- return `<div class="${classes.join(' ')}">${marked.parse(processCardBlocks(processGridBlocks(restore(body.trim()))))}</div>\n`;
1785
+ return `<div class="${classes.join(' ')}">${marked.parse(processCardBlocks(processGridBlocks(processInlineShortcodes(restore(body.trim())))))}</div>\n`;
1785
1786
  });
1786
1787
 
1787
1788
  return restore(result);
@@ -1847,7 +1848,7 @@ function processGridBlocks(markdown) {
1847
1848
  // processCardBlocks, which creates its own empty store and fails to
1848
1849
  // decode them, producing the literal string "undefined" in place of
1849
1850
  // whatever was in <pre>, ``` fences, or `inline code`.
1850
- return `<div${cls}${id}${injected.effectDataAttrs}>${marked.parse(processCardBlocks(restore(body.trim())))}</div>`;
1851
+ return `<div${cls}${id}${injected.effectDataAttrs}>${marked.parse(processCardBlocks(processInlineShortcodes(restore(body.trim()))))}</div>`;
1851
1852
  }
1852
1853
  );
1853
1854
 
@@ -2147,7 +2148,7 @@ LAYOUT_RENDERERS['basic'] = (attrs, body) => {
2147
2148
  LAYOUT_RENDERERS['header-body'] = (attrs, body) => {
2148
2149
  const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
2149
2150
  const hdr = title ? `<div class="card-header"><div class="card-title">${escapeAttr(title)}</div></div>` : '';
2150
- const bodyHtml = marked.parse(processGridBlocks(body.trim()));
2151
+ const bodyHtml = marked.parse(processGridBlocks(processInlineShortcodes(body.trim())));
2151
2152
  return `${cardRoot(attrs)}${hdr}<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
2152
2153
  };
2153
2154
 
@@ -2155,14 +2156,14 @@ LAYOUT_RENDERERS['header-body-footer'] = (attrs, body) => {
2155
2156
  const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
2156
2157
  const footer = typeof attrs.footer === 'string' ? attrs.footer.trim() : '';
2157
2158
  const hdr = title ? `<div class="card-header"><div class="card-title">${escapeAttr(title)}</div></div>` : '';
2158
- const bodyHtml = marked.parse(processGridBlocks(body.trim()));
2159
+ const bodyHtml = marked.parse(processGridBlocks(processInlineShortcodes(body.trim())));
2159
2160
  const ftr = footer ? `<div class="card-footer">${escapeAttr(footer)}</div>` : '';
2160
2161
  return `${cardRoot(attrs)}${hdr}<div class="card-body">${bodyHtml}</div>${ftr}</div>\n`;
2161
2162
  };
2162
2163
 
2163
2164
  LAYOUT_RENDERERS['no-header-footer'] = (attrs, body) => {
2164
2165
  const footer = typeof attrs.footer === 'string' ? attrs.footer.trim() : '';
2165
- const bodyHtml = marked.parse(processGridBlocks(body.trim()));
2166
+ const bodyHtml = marked.parse(processGridBlocks(processInlineShortcodes(body.trim())));
2166
2167
  const ftr = footer ? `<div class="card-footer">${escapeAttr(footer)}</div>` : '';
2167
2168
  return `${cardRoot(attrs)}<div class="card-body">${bodyHtml}</div>${ftr}</div>\n`;
2168
2169
  };
@@ -2173,7 +2174,7 @@ LAYOUT_RENDERERS['icon-top'] = (attrs, body) => {
2173
2174
  const icon = typeof attrs.icon === 'string' ? attrs.icon.trim() : '';
2174
2175
  const iconHtml = icon ? `<span data-icon="${escapeAttr(icon)}" class="card-icon"></span>` : '';
2175
2176
  const subHtml = subtitle ? `<div class="card-subtitle">${escapeAttr(subtitle)}</div>` : '';
2176
- const bodyHtml = marked.parse(processGridBlocks(body.trim()));
2177
+ const bodyHtml = marked.parse(processGridBlocks(processInlineShortcodes(body.trim())));
2177
2178
  return `${cardRoot(attrs)}
2178
2179
  <div class="card-header card-header-icon-stacked">
2179
2180
  <div class="card-avatar-wrap">${iconHtml}<div class="card-title">${escapeAttr(title)}</div>${subHtml}</div>
@@ -2187,7 +2188,7 @@ LAYOUT_RENDERERS['icon-inline'] = (attrs, body) => {
2187
2188
  const icon = typeof attrs.icon === 'string' ? attrs.icon.trim() : '';
2188
2189
  const iconHtml = icon ? `<span data-icon="${escapeAttr(icon)}"></span>` : '';
2189
2190
  const subHtml = subtitle ? `<div class="card-subtitle">${escapeAttr(subtitle)}</div>` : '';
2190
- const bodyHtml = marked.parse(processGridBlocks(body.trim()));
2191
+ const bodyHtml = marked.parse(processGridBlocks(processInlineShortcodes(body.trim())));
2191
2192
  return `${cardRoot(attrs)}
2192
2193
  <div class="card-header card-header-icon-inline">
2193
2194
  ${iconHtml}<div class="card-header-content"><div class="card-title">${escapeAttr(title)}</div>${subHtml}</div>
@@ -2198,13 +2199,13 @@ LAYOUT_RENDERERS['icon-inline'] = (attrs, body) => {
2198
2199
  LAYOUT_RENDERERS['image-top'] = (attrs, body) => {
2199
2200
  const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
2200
2201
  const hdr = title ? `<div class="card-header"><div class="card-title">${escapeAttr(title)}</div></div>` : '';
2201
- const bodyHtml = marked.parse(processGridBlocks(body.trim()));
2202
+ const bodyHtml = marked.parse(processGridBlocks(processInlineShortcodes(body.trim())));
2202
2203
  return `${cardRoot(attrs)}<div class="card-img-top"${imgStyle(attrs)}></div>${hdr}<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
2203
2204
  };
2204
2205
 
2205
2206
  LAYOUT_RENDERERS['image-overlay'] = (attrs, body) => {
2206
2207
  const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
2207
- const bodyHtml = marked.parse(processGridBlocks(body.trim()));
2208
+ const bodyHtml = marked.parse(processGridBlocks(processInlineShortcodes(body.trim())));
2208
2209
  return `${cardRoot(attrs)}
2209
2210
  <div class="card-img-overlay"${imgStyle(attrs)}>
2210
2211
  <div class="card-overlay-text"><div class="card-title">${escapeAttr(title)}</div></div>
@@ -2214,14 +2215,14 @@ LAYOUT_RENDERERS['image-overlay'] = (attrs, body) => {
2214
2215
 
2215
2216
  LAYOUT_RENDERERS['thumb-left'] = (attrs, body) => {
2216
2217
  const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
2217
- const bodyHtml = marked.parse(processGridBlocks(body.trim()));
2218
+ const bodyHtml = marked.parse(processGridBlocks(processInlineShortcodes(body.trim())));
2218
2219
  return `${cardRoot(attrs, ['card-layout-thumb-left'])}<div class="card-img-left"${imgStyle(attrs)}></div>
2219
2220
  <div class="card-body">${title ? `<div class="card-title">${escapeAttr(title)}</div>` : ''}${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
2220
2221
  };
2221
2222
 
2222
2223
  LAYOUT_RENDERERS['thumb-right'] = (attrs, body) => {
2223
2224
  const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
2224
- const bodyHtml = marked.parse(processGridBlocks(body.trim()));
2225
+ const bodyHtml = marked.parse(processGridBlocks(processInlineShortcodes(body.trim())));
2225
2226
  return `${cardRoot(attrs, ['card-layout-thumb-right'])}
2226
2227
  <div class="card-body">${title ? `<div class="card-title">${escapeAttr(title)}</div>` : ''}${bodyHtml}</div>
2227
2228
  <div class="card-img-right"${imgStyle(attrs)}></div></div>\n`;
@@ -2229,14 +2230,14 @@ LAYOUT_RENDERERS['thumb-right'] = (attrs, body) => {
2229
2230
 
2230
2231
  LAYOUT_RENDERERS['wide-left-image'] = (attrs, body) => {
2231
2232
  const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
2232
- const bodyHtml = marked.parse(processGridBlocks(body.trim()));
2233
+ const bodyHtml = marked.parse(processGridBlocks(processInlineShortcodes(body.trim())));
2233
2234
  return `${cardRoot(attrs, ['card-layout-horizontal'])}<div class="card-img-wide"${imgStyle(attrs)}></div>
2234
2235
  <div class="card-body">${title ? `<div class="card-title">${escapeAttr(title)}</div>` : ''}${bodyHtml}${cardFooterAttr(attrs)}</div></div>\n`;
2235
2236
  };
2236
2237
 
2237
2238
  LAYOUT_RENDERERS['full-bg'] = (attrs, body) => {
2238
2239
  const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
2239
- const bodyHtml = marked.parse(processGridBlocks(body.trim()));
2240
+ const bodyHtml = marked.parse(processGridBlocks(processInlineShortcodes(body.trim())));
2240
2241
  return `${cardRoot(attrs, ['dm-card-full-bg'])}<div class="card-img-top"${imgStyle(attrs)}></div>
2241
2242
  <div class="card-body">${title ? `<div class="card-title">${escapeAttr(title)}</div>` : ''}${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
2242
2243
  };
@@ -2244,7 +2245,7 @@ LAYOUT_RENDERERS['full-bg'] = (attrs, body) => {
2244
2245
  LAYOUT_RENDERERS['split-half'] = (attrs, body) => {
2245
2246
  const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
2246
2247
  const gradient = typeof attrs.gradient === 'string' ? attrs.gradient.trim() : 'indigo';
2247
- const bodyHtml = marked.parse(processGridBlocks(body.trim()));
2248
+ const bodyHtml = marked.parse(processGridBlocks(processInlineShortcodes(body.trim())));
2248
2249
  return `${cardRoot(attrs, ['card-layout-split'])}<div class="card-split-left card-gradient-${escapeAttr(gradient)}"${imgStyle(attrs)}></div>
2249
2250
  <div class="card-split-right"><div class="card-body">${title ? `<div class="card-title">${escapeAttr(title)}</div>` : ''}${bodyHtml}</div></div></div>\n`;
2250
2251
  };
@@ -2627,6 +2628,29 @@ function renderLegacyCard(attrs, body, markedInstance, escAttr) {
2627
2628
  * @param {string} markdown
2628
2629
  * @returns {string}
2629
2630
  */
2631
+ /**
2632
+ * Resolve the inline shortcodes in a nested body BEFORE it reaches
2633
+ * marked.parse.
2634
+ *
2635
+ * marked HTML-escapes the quotes in any `[tag attr="x"]` still sitting in the
2636
+ * text, so a tag processed after it sees `attr=&quot;x&quot;` and loses every
2637
+ * attribute - `[text size="lg"]` inside a `[tab]` or a `[card]` rendered
2638
+ * unstyled, and `[button href="/x"]` came out with `href="true"`. The
2639
+ * top-level pipeline runs these five before the container tags for exactly
2640
+ * that reason (see parseMarkdown); every container that renders a nested body
2641
+ * has to match the order rather than invent its own.
2642
+ *
2643
+ * Idempotent: a body whose inline tags are already HTML has none left to match.
2644
+ *
2645
+ * @param {string} markdown
2646
+ * @returns {string}
2647
+ */
2648
+ function processInlineShortcodes(markdown) {
2649
+ return processCtaBlocks(processLinkBlocks(processButtonBlocks(
2650
+ processTextBlocks(processBadgeBlocks(markdown))
2651
+ )));
2652
+ }
2653
+
2630
2654
  function processCardBlocks(markdown) {
2631
2655
  const {scrubbed, restore} = scrubCodeRegions(markdown);
2632
2656
  return restore(scrubbed.replace(
@@ -2693,7 +2717,7 @@ function processTabsBlocks(markdown) {
2693
2717
  // so fenced code blocks render as <pre><code> rather than surviving as raw text
2694
2718
  // that would confuse subsequent scrubCodeRegions calls.
2695
2719
  const restoredBody = restore(tabBody.trim());
2696
- const bodyHtml = marked.parse(processCardBlocks(processGridBlocks(restoredBody)));
2720
+ const bodyHtml = marked.parse(processCardBlocks(processGridBlocks(processInlineShortcodes(restoredBody))));
2697
2721
  const icon = tabAttrs.icon ? tabAttrs.icon.trim() : '';
2698
2722
  items.push({title, icon, paneId, bodyHtml, first: tabIdx === 1});
2699
2723
  });
@@ -2748,7 +2772,7 @@ function processAccordionBlocks(markdown) {
2748
2772
  body.replace(/\[item([^\]]*)\]([\s\S]*?)\[\/item\]/gi, (__, itemAttr, itemBody) => {
2749
2773
  const itemAttrs = parseShortcodeAttrs(itemAttr);
2750
2774
  const title = itemAttrs.title || 'Item';
2751
- const bodyHtml = marked.parse(processCardBlocks(processGridBlocks(restore(itemBody.trim()))));
2775
+ const bodyHtml = marked.parse(processCardBlocks(processGridBlocks(processInlineShortcodes(restore(itemBody.trim())))));
2752
2776
  items += (
2753
2777
  `<div class="accordion-item">\n` +
2754
2778
  ` <h3 class="accordion-header"><button class="accordion-button" type="button">${escapeAttr(title)}` +
@@ -2812,7 +2836,7 @@ function processCarouselBlocks(markdown) {
2812
2836
  if (slideAttrs.image) {
2813
2837
  inner += `<img src="${escapeAttr(slideAttrs.image)}" alt="${escapeAttr(slideAttrs.title || '')}">\n`;
2814
2838
  }
2815
- const contentHtml = marked.parse(processCardBlocks(processGridBlocks(restore(slideBody.trim()))));
2839
+ const contentHtml = marked.parse(processCardBlocks(processGridBlocks(processInlineShortcodes(restore(slideBody.trim())))));
2816
2840
  if (slideAttrs.title || slideBody.trim()) {
2817
2841
  inner += `<div class="carousel-slide-content">`;
2818
2842
  if (slideAttrs.title) inner += `<h2 class="carousel-slide-title">${escapeAttr(slideAttrs.title)}</h2>`;
@@ -2914,7 +2938,7 @@ function processTimelineBlocks(markdown) {
2914
2938
  const date = ia.date ? ` data-date="${escapeAttr(ia.date)}"` : '';
2915
2939
  const status = ia.status ? ` data-status="${escapeAttr(ia.status)}"` : '';
2916
2940
  const icon = ia.icon ? ` data-icon="${escapeAttr(ia.icon)}"` : '';
2917
- const descHtml = marked.parse(processCardBlocks(processGridBlocks(restore(itemBody.trim()))));
2941
+ const descHtml = marked.parse(processCardBlocks(processGridBlocks(processInlineShortcodes(restore(itemBody.trim())))));
2918
2942
  return `<div class="dm-progression-item"${date}${status}${icon}><div class="dm-progression-item-title">${title}</div><div class="dm-progression-item-body">${descHtml}</div></div>`;
2919
2943
  }
2920
2944
  );
@@ -3014,94 +3038,12 @@ function processBadgeBlocks(markdown) {
3014
3038
  function processTextBlocks(markdown) {
3015
3039
  const {scrubbed, restore} = scrubCodeRegions(markdown);
3016
3040
 
3017
- const SIZE_MAP = {
3018
- xs: '.75rem', sm: '.875rem', base: '1rem', lg: '1.125rem',
3019
- xl: '1.25rem', '2xl': '1.5rem', '3xl': '1.875rem', '4xl': '2.25rem',
3020
- };
3021
- const WEIGHT_MAP = {
3022
- thin: '100', light: '300', normal: '400', medium: '500',
3023
- semibold: '600', bold: '700', extrabold: '800', black: '900',
3024
- };
3025
- const SPACING_MAP = {
3026
- tight: '-0.05em', normal: '0em', wide: '0.05em', wider: '0.1em',
3027
- };
3028
- const COLOR_TOKENS = {
3029
- primary: 'var(--dm-color-primary)', secondary: 'var(--dm-color-secondary)',
3030
- muted: 'var(--dm-text-muted)', danger: 'var(--dm-color-danger)',
3031
- success: 'var(--dm-color-success)', warning: 'var(--dm-color-warning)',
3032
- info: 'var(--dm-color-info)',
3033
- };
3034
- const FONT_MAP = {
3035
- 'Georgia': 'Georgia,serif',
3036
- 'Arial': 'Arial,sans-serif',
3037
- 'Verdana': 'Verdana,sans-serif',
3038
- 'Courier New': "'Courier New',monospace",
3039
- 'Times New Roman': "'Times New Roman',serif",
3040
- 'Trebuchet MS': "'Trebuchet MS',sans-serif",
3041
- };
3042
- const TRANSFORM_MAP = {
3043
- upper: 'uppercase', lower: 'lowercase', capitalize: 'capitalize', none: 'none',
3044
- };
3045
- const DECORATION_MAP = {
3046
- underline: 'underline', 'line-through': 'line-through', none: 'none',
3047
- };
3048
-
3049
3041
  function buildText(attrStr, inner) {
3050
3042
  const attrs = parseShortcodeAttrs(attrStr || '');
3051
- const styles = [];
3052
-
3053
- if (attrs.size) {
3054
- styles.push(`font-size:${SIZE_MAP[attrs.size] || attrs.size}`);
3055
- }
3056
-
3057
- if (attrs['point-size']) {
3058
- const pt = parseFloat(attrs['point-size']);
3059
- if (!isNaN(pt) && pt > 0) styles.push(`font-size:${pt}pt`);
3060
- }
3061
-
3062
- // bold flag takes precedence over weight attr
3063
- const isBold = /\bbold\b/i.test(attrStr);
3064
- if (isBold) {
3065
- styles.push('font-weight:700');
3066
- } else if (attrs.weight && WEIGHT_MAP[attrs.weight]) {
3067
- styles.push(`font-weight:${WEIGHT_MAP[attrs.weight]}`);
3068
- }
3069
-
3070
- if (/\bitalic\b/i.test(attrStr)) {
3071
- styles.push('font-style:italic');
3072
- }
3073
-
3074
- const colourVal = attrs.colour || attrs.color;
3075
- if (colourVal) {
3076
- styles.push(`color:${COLOR_TOKENS[colourVal] || colourVal}`);
3077
- }
3078
-
3079
- if (attrs.font && FONT_MAP[attrs.font]) {
3080
- styles.push(`font-family:${FONT_MAP[attrs.font]}`);
3081
- }
3082
-
3083
- if (attrs.transform && TRANSFORM_MAP[attrs.transform]) {
3084
- styles.push(`text-transform:${TRANSFORM_MAP[attrs.transform]}`);
3085
- }
3086
-
3087
- if (attrs.decoration && DECORATION_MAP[attrs.decoration]) {
3088
- styles.push(`text-decoration:${DECORATION_MAP[attrs.decoration]}`);
3089
- }
3090
-
3091
- if (attrs.spacing) {
3092
- styles.push(`letter-spacing:${SPACING_MAP[attrs.spacing] || attrs.spacing}`);
3093
- }
3094
-
3095
- if (attrs.align) {
3096
- styles.push('display:block');
3097
- styles.push(`text-align:${escapeAttr(attrs.align)}`);
3098
- }
3099
-
3100
- if (attrs.style) {
3101
- styles.push(attrs.style.trim().replace(/;+$/, ''));
3102
- }
3103
-
3104
- const styleAttr = styles.length ? ` style="${styles.join(';')}"` : '';
3043
+ // The attribute → CSS mapping is shared with the admin editor's live
3044
+ // preview (public/js/text-style.mjs), so the two cannot disagree.
3045
+ const style = buildTextStyle(attrs);
3046
+ const styleAttr = style ? ` style="${style}"` : '';
3105
3047
  const classAttr = attrs.class ? ` class="${escapeAttr(attrs.class)}"` : '';
3106
3048
  const idAttr = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
3107
3049
 
@@ -3338,7 +3280,7 @@ function processListGroupBlocks(markdown) {
3338
3280
  const itemClassStr = itemClasses.join(' ');
3339
3281
  let items = '';
3340
3282
  body.replace(/\[item\]([\s\S]*?)\[\/item\]/gi, (__, content) => {
3341
- const html = marked.parseInline(restore(content.trim()));
3283
+ const html = marked.parseInline(processInlineShortcodes(restore(content.trim())));
3342
3284
  items += `<div class="${itemClassStr}">${html}</div>\n`;
3343
3285
  });
3344
3286
 
@@ -3773,12 +3715,13 @@ function processHeroBlocks(markdown) {
3773
3715
  const style = styleParts.length ? ` style="${styleParts.join(';')}"` : '';
3774
3716
  const idAttr = id ? ` id="${escapeAttr(id)}"` : '';
3775
3717
 
3776
- // Button/link/cta must run before marked.parse so their quoted attributes
3777
- // aren't HTML-escaped to &quot; - that mangles parseShortcodeAttrs and
3778
- // turns `href="/x"` into a bare flag (attrs.href = true → href="true").
3779
- const processedBody = processCtaBlocks(processLinkBlocks(processButtonBlocks(
3780
- processTextBlocks(processBadgeBlocks(processCardBlocks(processGridBlocks(restore(body.trim())))))
3781
- )));
3718
+ // The inline tags must run before marked.parse (see
3719
+ // processInlineShortcodes) AND before the container tags, because
3720
+ // processCardBlocks hands the card body straight to marked - which is
3721
+ // what used to escape the attributes of anything left inside a card.
3722
+ const processedBody = processCardBlocks(processGridBlocks(
3723
+ processInlineShortcodes(restore(body.trim()))
3724
+ ));
3782
3725
 
3783
3726
  let inner = '<div class="hero-content">';
3784
3727
  if (title) inner += `<h1 class="hero-title hero-title-responsive">${escapeAttr(title)}</h1>`;
@@ -3878,7 +3821,7 @@ function processSlideoverBlocks(markdown) {
3878
3821
  const trigger = escapeAttr(attrs.trigger || 'Open');
3879
3822
  const size = escapeAttr(attrs.size || 'md');
3880
3823
  const position = escapeAttr(attrs.position || 'right');
3881
- const bodyHtml = marked.parse(processCardBlocks(processGridBlocks(restore(body.trim()))));
3824
+ const bodyHtml = marked.parse(processCardBlocks(processGridBlocks(processInlineShortcodes(restore(body.trim())))));
3882
3825
  return (
3883
3826
  `<button class="btn btn-link dm-so-trigger" data-so-target="${id}">${trigger}</button>\n` +
3884
3827
  `<div class="dm-so-content" id="${id}" style="display:none" ` +