domma-cms 0.14.10 → 0.15.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/admin/js/lib/effect-defs.js +1 -0
- package/admin/js/lib/effects-builder.js +3 -0
- package/admin/js/lib/markdown-toolbar.js +17 -46
- package/admin/js/templates/effects.html +83 -0
- package/admin/js/views/page-editor.js +12 -12
- package/package.json +2 -2
- package/public/js/effects.js +1 -1
- package/server/services/markdown.js +90 -21
|
@@ -22,7 +22,7 @@ const BUILTIN_SHORTCODES = new Set([
|
|
|
22
22
|
'text', 'button', 'link', 'cta', 'grid', 'row', 'col', 'card',
|
|
23
23
|
'banner', 'slideover', 'counter', 'celebrate', 'firework', 'fireworks', 'scribe',
|
|
24
24
|
'reveal', 'breathe', 'pulse', 'shake', 'scramble', 'ripple', 'twinkle',
|
|
25
|
-
'animate', 'ambient', 'list-group',
|
|
25
|
+
'ticker-tape', 'animate', 'ambient', 'list-group',
|
|
26
26
|
]);
|
|
27
27
|
|
|
28
28
|
// Configure marked for safe output
|
|
@@ -790,7 +790,7 @@ function processPluginShortcodes(markdown) {
|
|
|
790
790
|
* Process built-in Effects shortcodes natively.
|
|
791
791
|
* Handles: [counter /], [celebrate /], [firework], [fireworks], [scribe],
|
|
792
792
|
* [reveal], [breathe], [pulse], [shake], [scramble], [ripple],
|
|
793
|
-
* [twinkle], [animate], [ambient]
|
|
793
|
+
* [twinkle], [ticker-tape], [animate], [ambient]
|
|
794
794
|
*
|
|
795
795
|
* @param {string} markdown
|
|
796
796
|
* @returns {string}
|
|
@@ -902,6 +902,20 @@ function processEffectsBlocks(markdown) {
|
|
|
902
902
|
});
|
|
903
903
|
}
|
|
904
904
|
|
|
905
|
+
// [ticker-tape /] → full-page overlay (mode=page)
|
|
906
|
+
// [ticker-tape] body [/ticker-tape] → container-scoped (mode=container)
|
|
907
|
+
apply('ticker-tape', (attrStr, body) => {
|
|
908
|
+
const attrs = parseShortcodeAttrs(attrStr);
|
|
909
|
+
const dataAttrs = Object.entries(attrs)
|
|
910
|
+
.map(([k, v]) => ` data-fx-${k}="${escapeAttr(String(v))}"`)
|
|
911
|
+
.join('');
|
|
912
|
+
if (body === null) {
|
|
913
|
+
return `<div class="dm-fx-ticker-tape" data-fx-mode="page"${dataAttrs}></div>`;
|
|
914
|
+
}
|
|
915
|
+
const innerHtml = marked.parse(processCardBlocks(processGridBlocks(body.trim())));
|
|
916
|
+
return `<div class="dm-fx-ticker-tape" data-fx-mode="container"${dataAttrs}>${innerHtml}</div>\n`;
|
|
917
|
+
});
|
|
918
|
+
|
|
905
919
|
apply('animate', (attrStr, body) => {
|
|
906
920
|
if (body === null) return '';
|
|
907
921
|
const attrs = parseShortcodeAttrs(attrStr);
|
|
@@ -975,14 +989,18 @@ function processGridBlocks(markdown) {
|
|
|
975
989
|
/\[col([^\]]*)\]([\s\S]*?)\[\/col\]/gi,
|
|
976
990
|
(_, attrStr, body) => {
|
|
977
991
|
const attrs = parseShortcodeAttrs(attrStr);
|
|
978
|
-
|
|
992
|
+
const COL_INJECTABLE = ['reveal', 'breathe', 'animate', 'ripple'];
|
|
993
|
+
const injected = extractInjectedEffects(attrs, COL_INJECTABLE);
|
|
994
|
+
const baseCls = attrs.span ? `col-span-${attrs.span}` : 'col';
|
|
995
|
+
const allClasses = [baseCls, ...injected.effectClasses].join(' ');
|
|
996
|
+
const cls = ` class="${allClasses}"`;
|
|
979
997
|
const id = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
|
|
980
998
|
// Restore the col body before passing it downstream — otherwise the
|
|
981
999
|
// scrub placeholders from this processor's store get carried into
|
|
982
1000
|
// processCardBlocks, which creates its own empty store and fails to
|
|
983
1001
|
// decode them, producing the literal string "undefined" in place of
|
|
984
1002
|
// whatever was in <pre>, ``` fences, or `inline code`.
|
|
985
|
-
return `<div${cls}${id}>${marked.parse(processCardBlocks(restore(body.trim())))}</div>`;
|
|
1003
|
+
return `<div${cls}${id}${injected.effectDataAttrs}>${marked.parse(processCardBlocks(restore(body.trim())))}</div>`;
|
|
986
1004
|
}
|
|
987
1005
|
);
|
|
988
1006
|
|
|
@@ -1102,6 +1120,47 @@ export function escapeAttr(str) {
|
|
|
1102
1120
|
.replace(/>/g, '>');
|
|
1103
1121
|
}
|
|
1104
1122
|
|
|
1123
|
+
/**
|
|
1124
|
+
* Extract injected-effect flag attributes from a parsed shortcode attrs object.
|
|
1125
|
+
*
|
|
1126
|
+
* For each effect in `allowed` whose flag attribute is present in `attrs`,
|
|
1127
|
+
* emit a `dm-fx-<effect>` class and convert all `<effect>-<attr>` keys to
|
|
1128
|
+
* `data-fx-<attr>="<escaped-value>"` fragments. Returns the consumed keys
|
|
1129
|
+
* so the host processor can skip them when emitting its own attributes.
|
|
1130
|
+
*
|
|
1131
|
+
* The flag attribute is presence-only (parseShortcodeAttrs stores bare flags
|
|
1132
|
+
* as `true`); only prefixed attributes carry a meaningful value.
|
|
1133
|
+
*
|
|
1134
|
+
* @param {Object<string,string>} attrs - parsed shortcode attribute map
|
|
1135
|
+
* @param {string[]} allowed - effect names this host accepts (e.g. ['reveal','pulse'])
|
|
1136
|
+
* @returns {{effectClasses: string[], effectDataAttrs: string, consumedKeys: Set<string>}}
|
|
1137
|
+
*/
|
|
1138
|
+
export function extractInjectedEffects(attrs, allowed) {
|
|
1139
|
+
const effectClasses = [];
|
|
1140
|
+
const dataAttrParts = [];
|
|
1141
|
+
const consumedKeys = new Set();
|
|
1142
|
+
|
|
1143
|
+
for (const effect of allowed) {
|
|
1144
|
+
if (!(effect in attrs)) continue;
|
|
1145
|
+
effectClasses.push(`dm-fx-${effect}`);
|
|
1146
|
+
consumedKeys.add(effect);
|
|
1147
|
+
|
|
1148
|
+
const prefix = `${effect}-`;
|
|
1149
|
+
for (const key of Object.keys(attrs)) {
|
|
1150
|
+
if (!key.startsWith(prefix)) continue;
|
|
1151
|
+
const param = key.slice(prefix.length);
|
|
1152
|
+
dataAttrParts.push(` data-fx-${param}="${escapeAttr(String(attrs[key]))}"`);
|
|
1153
|
+
consumedKeys.add(key);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
return {
|
|
1158
|
+
effectClasses,
|
|
1159
|
+
effectDataAttrs: dataAttrParts.join(''),
|
|
1160
|
+
consumedKeys,
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1105
1164
|
// ---------------------------------------------------------------------------
|
|
1106
1165
|
// Card shared helpers
|
|
1107
1166
|
// ---------------------------------------------------------------------------
|
|
@@ -1141,6 +1200,10 @@ function cardVariantClasses(attrs) {
|
|
|
1141
1200
|
return classes;
|
|
1142
1201
|
}
|
|
1143
1202
|
|
|
1203
|
+
// Effects that may be injected as flag-attributes onto [card] hosts.
|
|
1204
|
+
// Used by both cardRoot (LAYOUT_RENDERERS path) and renderLegacyCard (fallback).
|
|
1205
|
+
const CARD_INJECTABLE = ['reveal', 'breathe', 'pulse', 'shake', 'twinkle', 'ambient', 'animate', 'ripple'];
|
|
1206
|
+
|
|
1144
1207
|
/**
|
|
1145
1208
|
* Builds the opening `<div>` tag for a card root element.
|
|
1146
1209
|
*
|
|
@@ -1149,10 +1212,11 @@ function cardVariantClasses(attrs) {
|
|
|
1149
1212
|
* @returns {string}
|
|
1150
1213
|
*/
|
|
1151
1214
|
function cardRoot(attrs, extraClasses = []) {
|
|
1152
|
-
const
|
|
1215
|
+
const injected = extractInjectedEffects(attrs, CARD_INJECTABLE);
|
|
1216
|
+
const classes = [...cardVariantClasses(attrs), ...extraClasses, ...injected.effectClasses];
|
|
1153
1217
|
const id = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
|
|
1154
1218
|
const coll = attrs.collapsible === 'true' ? ' data-collapsible="true"' : '';
|
|
1155
|
-
return `<div class="${classes.join(' ')}"${id}${coll}>`;
|
|
1219
|
+
return `<div class="${classes.join(' ')}"${id}${coll}${injected.effectDataAttrs}>`;
|
|
1156
1220
|
}
|
|
1157
1221
|
|
|
1158
1222
|
/**
|
|
@@ -1630,12 +1694,15 @@ function renderLegacyCard(attrs, body, markedInstance, escAttr) {
|
|
|
1630
1694
|
const collapsible = attrs.collapsible === 'true';
|
|
1631
1695
|
const variant = strAttr('variant');
|
|
1632
1696
|
|
|
1697
|
+
const injected = extractInjectedEffects(attrs, CARD_INJECTABLE);
|
|
1698
|
+
|
|
1633
1699
|
// Root class list — delegate to the shared helper so variant="gradient",
|
|
1634
1700
|
// gradient="<name>", glass/accent/dark/glow, font, shadow, rounded, etc.
|
|
1635
1701
|
// all work on legacy cards (cards without a `layout` attribute).
|
|
1636
1702
|
const classes = cardVariantClasses(attrs);
|
|
1637
1703
|
// Preserve legacy support for variant="primary" (not handled by cardVariantClasses).
|
|
1638
1704
|
if (variant === 'primary') classes.push('card-primary');
|
|
1705
|
+
classes.push(...injected.effectClasses);
|
|
1639
1706
|
|
|
1640
1707
|
const id = attrs.id ? ` id="${escAttr(attrs.id)}"` : '';
|
|
1641
1708
|
const coll = collapsible ? ' data-collapsible="true"' : '';
|
|
@@ -1692,7 +1759,7 @@ function renderLegacyCard(attrs, body, markedInstance, escAttr) {
|
|
|
1692
1759
|
? `<div class="card-footer">${markedInstance.parse(footerContent)}</div>`
|
|
1693
1760
|
: footer ? `<div class="card-footer">${footer}</div>` : '';
|
|
1694
1761
|
|
|
1695
|
-
return `<div class="${classes.join(' ')}"${coll}${id}>${headerHtml}${bodyHtml}${footerHtml}</div>\n`;
|
|
1762
|
+
return `<div class="${classes.join(' ')}"${coll}${id}${injected.effectDataAttrs}>${headerHtml}${bodyHtml}${footerHtml}</div>\n`;
|
|
1696
1763
|
}
|
|
1697
1764
|
|
|
1698
1765
|
// ---------------------------------------------------------------------------
|
|
@@ -2234,6 +2301,8 @@ function processButtonBlocks(markdown) {
|
|
|
2234
2301
|
|
|
2235
2302
|
function buildButton(attrStr, inner) {
|
|
2236
2303
|
const attrs = parseShortcodeAttrs(attrStr || '');
|
|
2304
|
+
const BUTTON_INJECTABLE = ['breathe', 'pulse', 'shake', 'animate', 'ripple'];
|
|
2305
|
+
const injected = extractInjectedEffects(attrs, BUTTON_INJECTABLE);
|
|
2237
2306
|
const href = attrs.href || '#';
|
|
2238
2307
|
const label = inner !== null ? inner.trim() : (attrs.label || '');
|
|
2239
2308
|
const variant = VALID_VARIANTS.has(attrs.variant) ? attrs.variant : 'primary';
|
|
@@ -2241,11 +2310,12 @@ function processButtonBlocks(markdown) {
|
|
|
2241
2310
|
if (attrs.size === 'sm') classes.push('btn-sm');
|
|
2242
2311
|
if (attrs.size === 'lg') classes.push('btn-lg');
|
|
2243
2312
|
if (attrs.class) classes.push(escapeAttr(attrs.class));
|
|
2313
|
+
classes.push(...injected.effectClasses);
|
|
2244
2314
|
const idAttr = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
|
|
2245
2315
|
const targetAttr = attrs.target ? ` target="${escapeAttr(attrs.target)}"` : '';
|
|
2246
2316
|
const icon = attrs.icon ? `<span data-icon="${escapeAttr(attrs.icon)}"></span> ` : '';
|
|
2247
2317
|
const iconAfter = attrs['icon-after'] ? ` <span data-icon="${escapeAttr(attrs['icon-after'])}"></span>` : '';
|
|
2248
|
-
return `<a href="${escapeAttr(href)}" class="${classes.join(' ')}"${idAttr}${targetAttr}>${icon}${label}${iconAfter}</a>`;
|
|
2318
|
+
return `<a href="${escapeAttr(href)}" class="${classes.join(' ')}"${idAttr}${targetAttr}${injected.effectDataAttrs}>${icon}${label}${iconAfter}</a>`;
|
|
2249
2319
|
}
|
|
2250
2320
|
|
|
2251
2321
|
let result = scrubbed.replace(/\[button([^\]]*?)\/\]/gi, (_, attrStr) => buildButton(attrStr, null));
|
|
@@ -2351,8 +2421,7 @@ function processTableBlocks(markdown) {
|
|
|
2351
2421
|
* fullwidth - "true" breaks out of the page container to span the full viewport width (adds .hero-breakout).
|
|
2352
2422
|
* Must be the literal string "true" — the attribute is otherwise ignored.
|
|
2353
2423
|
* twinkle - Flag attribute — adds particle overlay (requires Effects plugin)
|
|
2354
|
-
* twinkle-
|
|
2355
|
-
* twinkle-colour - Particle colour (CSS value)
|
|
2424
|
+
* twinkle-* - Any prefixed attribute is forwarded as data-fx-* (see extractInjectedEffects)
|
|
2356
2425
|
* blobs - Flag attribute — adds ambient blob background
|
|
2357
2426
|
* blobs-type - Blob animation type (default: "float-blobs")
|
|
2358
2427
|
* class - Extra classes appended to .hero
|
|
@@ -2589,9 +2658,11 @@ function processHeroBlocks(markdown) {
|
|
|
2589
2658
|
const heroColor = attrs.color ? String(attrs.color) : '';
|
|
2590
2659
|
const minHeight = attrs['min-height'] ? String(attrs['min-height']) : '';
|
|
2591
2660
|
|
|
2592
|
-
const
|
|
2593
|
-
const
|
|
2594
|
-
|
|
2661
|
+
const HERO_INJECTABLE = ['reveal', 'breathe', 'pulse', 'twinkle', 'ticker-tape', 'ambient', 'animate'];
|
|
2662
|
+
const injected = extractInjectedEffects(attrs, HERO_INJECTABLE);
|
|
2663
|
+
|
|
2664
|
+
// Legacy ad-hoc 'blobs' flag — keep for backward compat. Different keyspace
|
|
2665
|
+
// (bg-ambient-* CSS class), not folded into the generic helper.
|
|
2595
2666
|
const blobs = 'blobs' in attrs;
|
|
2596
2667
|
const blobsType = attrs['blobs-type'] || 'float-blobs';
|
|
2597
2668
|
|
|
@@ -2603,7 +2674,7 @@ function processHeroBlocks(markdown) {
|
|
|
2603
2674
|
if (overlay) classes.push(`hero-overlay-${overlay}`);
|
|
2604
2675
|
if (fullwidth) classes.push('hero-breakout');
|
|
2605
2676
|
if (cls) classes.push(cls);
|
|
2606
|
-
|
|
2677
|
+
classes.push(...injected.effectClasses);
|
|
2607
2678
|
if (blobs) classes.push(`bg-ambient-${blobsType}`);
|
|
2608
2679
|
|
|
2609
2680
|
const styleParts = [];
|
|
@@ -2616,11 +2687,6 @@ function processHeroBlocks(markdown) {
|
|
|
2616
2687
|
const style = styleParts.length ? ` style="${styleParts.join(';')}"` : '';
|
|
2617
2688
|
const idAttr = id ? ` id="${escapeAttr(id)}"` : '';
|
|
2618
2689
|
|
|
2619
|
-
const twinkleAttrs = twinkle
|
|
2620
|
-
? (twinkleCount ? ` data-fx-count="${escapeAttr(twinkleCount)}"` : '') +
|
|
2621
|
-
(twinkleColour ? ` data-fx-colour="${escapeAttr(twinkleColour)}"` : '')
|
|
2622
|
-
: '';
|
|
2623
|
-
|
|
2624
2690
|
// Button/link/cta must run before marked.parse so their quoted attributes
|
|
2625
2691
|
// aren't HTML-escaped to " — that mangles parseShortcodeAttrs and
|
|
2626
2692
|
// turns `href="/x"` into a bare flag (attrs.href = true → href="true").
|
|
@@ -2634,7 +2700,7 @@ function processHeroBlocks(markdown) {
|
|
|
2634
2700
|
if (processedBody) inner += `<div class="hero-body">${marked.parse(processedBody)}</div>`;
|
|
2635
2701
|
inner += '</div>';
|
|
2636
2702
|
|
|
2637
|
-
return `<div class="${classes.join(' ')}"${idAttr}${style}${
|
|
2703
|
+
return `<div class="${classes.join(' ')}"${idAttr}${style}${injected.effectDataAttrs}>${inner}</div>\n`;
|
|
2638
2704
|
}
|
|
2639
2705
|
);
|
|
2640
2706
|
return restore(result);
|
|
@@ -2678,6 +2744,8 @@ function processBannerBlocks(markdown) {
|
|
|
2678
2744
|
/\[banner([^\]]*)\]([\s\S]*?)\[\/banner\]/gi,
|
|
2679
2745
|
(_, attrStr, body) => {
|
|
2680
2746
|
const attrs = parseShortcodeAttrs(attrStr);
|
|
2747
|
+
const BANNER_INJECTABLE = ['reveal', 'pulse', 'shake', 'ambient', 'animate'];
|
|
2748
|
+
const injected = extractInjectedEffects(attrs, BANNER_INJECTABLE);
|
|
2681
2749
|
const VALID_BANNER_TYPES = new Set(['info', 'success', 'warning', 'danger', 'neutral']);
|
|
2682
2750
|
const type = VALID_BANNER_TYPES.has(attrs.type) ? attrs.type : 'info';
|
|
2683
2751
|
const title = attrs.title || '';
|
|
@@ -2687,6 +2755,7 @@ function processBannerBlocks(markdown) {
|
|
|
2687
2755
|
|
|
2688
2756
|
const classes = [`dm-banner`, `dm-banner--${type}`];
|
|
2689
2757
|
if (extraClass) classes.push(extraClass);
|
|
2758
|
+
classes.push(...injected.effectClasses);
|
|
2690
2759
|
|
|
2691
2760
|
const iconHtml = icon
|
|
2692
2761
|
? `<span class="dm-banner__icon" data-icon="${escapeAttr(icon)}"></span>\n `
|
|
@@ -2700,7 +2769,7 @@ function processBannerBlocks(markdown) {
|
|
|
2700
2769
|
: '';
|
|
2701
2770
|
|
|
2702
2771
|
return (
|
|
2703
|
-
`<div class="${classes.join(' ')}">\n` +
|
|
2772
|
+
`<div class="${classes.join(' ')}"${injected.effectDataAttrs}>\n` +
|
|
2704
2773
|
` ${iconHtml}<div class="dm-banner__body">\n` +
|
|
2705
2774
|
` ${titleHtml}${bodyHtml}` +
|
|
2706
2775
|
` </div>${dismissHtml}\n` +
|