domma-cms 0.9.6 → 0.10.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/app.js +2 -2
- package/admin/js/lib/card-builder.js +7 -0
- package/admin/js/lib/markdown-toolbar.js +5 -5
- package/admin/js/lib/shortcode-modal.js +1 -0
- package/admin/js/templates/api-reference.html +943 -1107
- package/admin/js/templates/documentation.html +828 -655
- package/admin/js/templates/tutorials.html +202 -177
- package/admin/js/views/api-reference.js +1 -1
- package/admin/js/views/documentation.js +1 -1
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/page-editor.js +37 -33
- package/admin/js/views/tutorials.js +1 -1
- package/config/plugins.json +1 -1
- package/config/site.json +1 -1
- package/package.json +1 -1
- package/plugins/notes/admin/views/notes.js +24 -26
- package/plugins/todo/admin/views/todo.js +32 -34
- package/public/css/site.css +1 -1
- package/public/js/site.js +1 -1
- package/server/services/markdown.js +685 -88
|
@@ -20,7 +20,7 @@ const BUILTIN_SHORTCODES = new Set([
|
|
|
20
20
|
'accordion', 'item', 'carousel', 'slide', 'countdown', 'timeline',
|
|
21
21
|
'event', 'spacer', 'center', 'icon', 'form', 'hero', 'table', 'badge',
|
|
22
22
|
'text', 'button', 'link', 'cta', 'grid', 'row', 'col', 'card',
|
|
23
|
-
'slideover', 'counter', 'celebrate', 'firework', 'fireworks', 'scribe',
|
|
23
|
+
'banner', 'slideover', 'counter', 'celebrate', 'firework', 'fireworks', 'scribe',
|
|
24
24
|
'reveal', 'breathe', 'pulse', 'shake', 'scramble', 'ripple', 'twinkle',
|
|
25
25
|
'animate', 'ambient', 'list-group',
|
|
26
26
|
]);
|
|
@@ -819,100 +819,630 @@ export function escapeAttr(str) {
|
|
|
819
819
|
.replace(/>/g, '>');
|
|
820
820
|
}
|
|
821
821
|
|
|
822
|
+
// ---------------------------------------------------------------------------
|
|
823
|
+
// Card shared helpers
|
|
824
|
+
// ---------------------------------------------------------------------------
|
|
825
|
+
|
|
822
826
|
/**
|
|
823
|
-
*
|
|
827
|
+
* Returns the full class array for a card root element, incorporating variant,
|
|
828
|
+
* gradient, hover, collapsible, and custom class attributes.
|
|
824
829
|
*
|
|
825
|
-
*
|
|
826
|
-
*
|
|
827
|
-
|
|
828
|
-
|
|
830
|
+
* @param {object} attrs
|
|
831
|
+
* @returns {string[]}
|
|
832
|
+
*/
|
|
833
|
+
function cardVariantClasses(attrs) {
|
|
834
|
+
const variant = typeof attrs.variant === 'string' ? attrs.variant.trim() : '';
|
|
835
|
+
const gradient = typeof attrs.gradient === 'string' ? attrs.gradient.trim() : 'indigo';
|
|
836
|
+
const classes = ['card', 'mb-4'];
|
|
837
|
+
const valid = ['clean', 'gradient', 'glass', 'accent', 'dark', 'glow'];
|
|
838
|
+
if (valid.includes(variant)) {
|
|
839
|
+
classes.push(`dm-card-${variant}`);
|
|
840
|
+
if (variant === 'gradient') classes.push(`card-gradient-${gradient}`);
|
|
841
|
+
}
|
|
842
|
+
if ('hover' in attrs) classes.push('card-hover');
|
|
843
|
+
if (attrs.collapsible === 'true') classes.push('card-collapsible');
|
|
844
|
+
if (typeof attrs.class === 'string') classes.push(attrs.class.trim());
|
|
845
|
+
const font = typeof attrs.font === 'string' ? attrs.font.trim() : '';
|
|
846
|
+
if (['serif', 'mono'].includes(font)) classes.push(`card-font-${font}`);
|
|
847
|
+
const fontSize = typeof attrs['font-size'] === 'string' ? attrs['font-size'].trim() : '';
|
|
848
|
+
if (['sm', 'lg', 'xl'].includes(fontSize)) classes.push(`card-text-${fontSize}`);
|
|
849
|
+
if ('borderless' in attrs) classes.push('card-borderless');
|
|
850
|
+
const shadow = typeof attrs.shadow === 'string' ? attrs.shadow.trim() : '';
|
|
851
|
+
if (['none', 'md', 'lg'].includes(shadow)) classes.push(`card-shadow-${shadow}`);
|
|
852
|
+
const rounded = typeof attrs.rounded === 'string' ? attrs.rounded.trim() : '';
|
|
853
|
+
if (['none', 'sm', 'lg', 'full'].includes(rounded)) classes.push(`card-rounded-${rounded}`);
|
|
854
|
+
const textAlign = typeof attrs['text-align'] === 'string' ? attrs['text-align'].trim() : '';
|
|
855
|
+
if (['center', 'right'].includes(textAlign)) classes.push(`card-align-${textAlign}`);
|
|
856
|
+
const padding = typeof attrs.padding === 'string' ? attrs.padding.trim() : '';
|
|
857
|
+
if (['compact', 'spacious'].includes(padding)) classes.push(`card-pad-${padding}`);
|
|
858
|
+
return classes;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Builds the opening `<div>` tag for a card root element.
|
|
829
863
|
*
|
|
830
|
-
*
|
|
831
|
-
*
|
|
832
|
-
*
|
|
864
|
+
* @param {object} attrs
|
|
865
|
+
* @param {string[]} [extraClasses=[]]
|
|
866
|
+
* @returns {string}
|
|
867
|
+
*/
|
|
868
|
+
function cardRoot(attrs, extraClasses = []) {
|
|
869
|
+
const classes = [...cardVariantClasses(attrs), ...extraClasses];
|
|
870
|
+
const id = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
|
|
871
|
+
const coll = attrs.collapsible === 'true' ? ' data-collapsible="true"' : '';
|
|
872
|
+
return `<div class="${classes.join(' ')}"${id}${coll}>`;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Returns a card-footer div from the `footer` attribute, or an empty string.
|
|
833
877
|
*
|
|
834
|
-
* @param {
|
|
878
|
+
* @param {object} attrs
|
|
835
879
|
* @returns {string}
|
|
836
880
|
*/
|
|
837
|
-
function
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
/\[card([^\]]*)\]([\s\S]*?)\[\/card\]/gi,
|
|
841
|
-
(_, attrStr, body) => {
|
|
842
|
-
const attrs = parseShortcodeAttrs(attrStr);
|
|
843
|
-
const strAttr = (key) => typeof attrs[key] === 'string' ? attrs[key].trim() : '';
|
|
844
|
-
const title = strAttr('title');
|
|
845
|
-
const subtitle = strAttr('subtitle');
|
|
846
|
-
const icon = strAttr('icon');
|
|
847
|
-
const footer = strAttr('footer');
|
|
848
|
-
const collapsible = attrs.collapsible === 'true';
|
|
849
|
-
const hover = 'hover' in attrs;
|
|
850
|
-
const variant = strAttr('variant');
|
|
851
|
-
const extraClass = strAttr('class');
|
|
852
|
-
|
|
853
|
-
// Root class list
|
|
854
|
-
const classes = ['card', 'mb-4'];
|
|
855
|
-
if (variant === 'primary') classes.push('card-primary');
|
|
856
|
-
if (hover) classes.push('card-hover');
|
|
857
|
-
if (collapsible) classes.push('card-collapsible');
|
|
858
|
-
if (extraClass) classes.push(extraClass);
|
|
881
|
+
function cardFooterAttr(attrs) {
|
|
882
|
+
return attrs.footer ? `<div class="card-footer">${escapeAttr(attrs.footer)}</div>` : '';
|
|
883
|
+
}
|
|
859
884
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
);
|
|
885
|
+
/**
|
|
886
|
+
* Returns an inline `style` attribute with a background-image from the `image` attribute.
|
|
887
|
+
*
|
|
888
|
+
* @param {object} attrs
|
|
889
|
+
* @returns {string}
|
|
890
|
+
*/
|
|
891
|
+
function imgStyle(attrs) {
|
|
892
|
+
const img = typeof attrs.image === 'string' ? attrs.image.trim() : '';
|
|
893
|
+
return img ? ` style="background-image:url('${escapeAttr(img)}')"` : '';
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Parses all occurrences of `[tagName attrs]body[/tagName]` from a body string.
|
|
898
|
+
*
|
|
899
|
+
* @param {string} body
|
|
900
|
+
* @param {string} tagName
|
|
901
|
+
* @returns {{ attrs: object, content: string }[]}
|
|
902
|
+
*/
|
|
903
|
+
function parseSubTagList(body, tagName) {
|
|
904
|
+
const re = new RegExp(`\\[${tagName}([^\\]]*)\\]([\\s\\S]*?)\\[\\/${tagName}\\]`, 'gi');
|
|
905
|
+
const items = [];
|
|
906
|
+
body.replace(re, (_, attrStr, inner) => {
|
|
907
|
+
items.push({attrs: parseShortcodeAttrs(attrStr), content: inner.trim()});
|
|
908
|
+
});
|
|
909
|
+
return items;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Returns the inner content of the first `[tagName]body[/tagName]` found, or null.
|
|
914
|
+
*
|
|
915
|
+
* @param {string} body
|
|
916
|
+
* @param {string} tagName
|
|
917
|
+
* @returns {string|null}
|
|
918
|
+
*/
|
|
919
|
+
function parseSubTag(body, tagName) {
|
|
920
|
+
const re = new RegExp(`\\[${tagName}([^\\]]*)\\]([\\s\\S]*?)\\[\\/${tagName}\\]`, 'i');
|
|
921
|
+
const m = body.match(re);
|
|
922
|
+
return m ? m[2].trim() : null;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Returns a cycling inline style string for tag/badge colours.
|
|
927
|
+
*
|
|
928
|
+
* @param {number} i
|
|
929
|
+
* @returns {string}
|
|
930
|
+
*/
|
|
931
|
+
function tagColorStyle(i) {
|
|
932
|
+
const p = [
|
|
933
|
+
'background:#ede9fe;color:#7c3aed',
|
|
934
|
+
'background:#dbeafe;color:#1d4ed8',
|
|
935
|
+
'background:#d1fae5;color:#065f46',
|
|
936
|
+
'background:#fef3c7;color:#92400e',
|
|
937
|
+
'background:#fce7f3;color:#9d174d',
|
|
938
|
+
];
|
|
939
|
+
return p[i % p.length];
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// ---------------------------------------------------------------------------
|
|
943
|
+
// Card layout renderers registry (populated by layout-specific tasks)
|
|
944
|
+
// ---------------------------------------------------------------------------
|
|
883
945
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
946
|
+
const LAYOUT_RENDERERS = {};
|
|
947
|
+
|
|
948
|
+
LAYOUT_RENDERERS['basic'] = (attrs, body) => {
|
|
949
|
+
const bodyHtml = marked.parse(body.trim());
|
|
950
|
+
return `${cardRoot(attrs)}<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
LAYOUT_RENDERERS['header-body'] = (attrs, body) => {
|
|
954
|
+
const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
|
|
955
|
+
const hdr = title ? `<div class="card-header"><div class="card-title">${escapeAttr(title)}</div></div>` : '';
|
|
956
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
957
|
+
return `${cardRoot(attrs)}${hdr}<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
LAYOUT_RENDERERS['header-body-footer'] = (attrs, body) => {
|
|
961
|
+
const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
|
|
962
|
+
const footer = typeof attrs.footer === 'string' ? attrs.footer.trim() : '';
|
|
963
|
+
const hdr = title ? `<div class="card-header"><div class="card-title">${escapeAttr(title)}</div></div>` : '';
|
|
964
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
965
|
+
const ftr = footer ? `<div class="card-footer">${escapeAttr(footer)}</div>` : '';
|
|
966
|
+
return `${cardRoot(attrs)}${hdr}<div class="card-body">${bodyHtml}</div>${ftr}</div>\n`;
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
LAYOUT_RENDERERS['no-header-footer'] = (attrs, body) => {
|
|
970
|
+
const footer = typeof attrs.footer === 'string' ? attrs.footer.trim() : '';
|
|
971
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
972
|
+
const ftr = footer ? `<div class="card-footer">${escapeAttr(footer)}</div>` : '';
|
|
973
|
+
return `${cardRoot(attrs)}<div class="card-body">${bodyHtml}</div>${ftr}</div>\n`;
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
LAYOUT_RENDERERS['icon-top'] = (attrs, body) => {
|
|
977
|
+
const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
|
|
978
|
+
const subtitle = typeof attrs.subtitle === 'string' ? attrs.subtitle.trim() : '';
|
|
979
|
+
const icon = typeof attrs.icon === 'string' ? attrs.icon.trim() : '';
|
|
980
|
+
const iconHtml = icon ? `<span data-icon="${escapeAttr(icon)}" class="card-icon"></span>` : '';
|
|
981
|
+
const subHtml = subtitle ? `<div class="card-subtitle">${escapeAttr(subtitle)}</div>` : '';
|
|
982
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
983
|
+
return `${cardRoot(attrs)}
|
|
984
|
+
<div class="card-header card-header-icon-stacked">
|
|
985
|
+
<div class="card-avatar-wrap">${iconHtml}<div class="card-title">${escapeAttr(title)}</div>${subHtml}</div>
|
|
986
|
+
</div>
|
|
987
|
+
<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
988
|
+
};
|
|
989
|
+
|
|
990
|
+
LAYOUT_RENDERERS['icon-inline'] = (attrs, body) => {
|
|
991
|
+
const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
|
|
992
|
+
const subtitle = typeof attrs.subtitle === 'string' ? attrs.subtitle.trim() : '';
|
|
993
|
+
const icon = typeof attrs.icon === 'string' ? attrs.icon.trim() : '';
|
|
994
|
+
const iconHtml = icon ? `<span data-icon="${escapeAttr(icon)}"></span>` : '';
|
|
995
|
+
const subHtml = subtitle ? `<div class="card-subtitle">${escapeAttr(subtitle)}</div>` : '';
|
|
996
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
997
|
+
return `${cardRoot(attrs)}
|
|
998
|
+
<div class="card-header card-header-icon-inline">
|
|
999
|
+
${iconHtml}<div class="card-header-content"><div class="card-title">${escapeAttr(title)}</div>${subHtml}</div>
|
|
1000
|
+
</div>
|
|
1001
|
+
<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
LAYOUT_RENDERERS['image-top'] = (attrs, body) => {
|
|
1005
|
+
const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
|
|
1006
|
+
const hdr = title ? `<div class="card-header"><div class="card-title">${escapeAttr(title)}</div></div>` : '';
|
|
1007
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
1008
|
+
return `${cardRoot(attrs)}<div class="card-img-top"${imgStyle(attrs)}></div>${hdr}<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
LAYOUT_RENDERERS['image-overlay'] = (attrs, body) => {
|
|
1012
|
+
const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
|
|
1013
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
1014
|
+
return `${cardRoot(attrs)}
|
|
1015
|
+
<div class="card-img-overlay"${imgStyle(attrs)}>
|
|
1016
|
+
<div class="card-overlay-text"><div class="card-title">${escapeAttr(title)}</div></div>
|
|
1017
|
+
</div>
|
|
1018
|
+
<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
LAYOUT_RENDERERS['thumb-left'] = (attrs, body) => {
|
|
1022
|
+
const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
|
|
1023
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
1024
|
+
return `${cardRoot(attrs, ['card-layout-thumb-left'])}<div class="card-img-left"${imgStyle(attrs)}></div>
|
|
1025
|
+
<div class="card-body">${title ? `<div class="card-title">${escapeAttr(title)}</div>` : ''}${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
LAYOUT_RENDERERS['thumb-right'] = (attrs, body) => {
|
|
1029
|
+
const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
|
|
1030
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
1031
|
+
return `${cardRoot(attrs, ['card-layout-thumb-right'])}
|
|
1032
|
+
<div class="card-body">${title ? `<div class="card-title">${escapeAttr(title)}</div>` : ''}${bodyHtml}</div>
|
|
1033
|
+
<div class="card-img-right"${imgStyle(attrs)}></div></div>\n`;
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
LAYOUT_RENDERERS['wide-left-image'] = (attrs, body) => {
|
|
1037
|
+
const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
|
|
1038
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
1039
|
+
return `${cardRoot(attrs, ['card-layout-horizontal'])}<div class="card-img-wide"${imgStyle(attrs)}></div>
|
|
1040
|
+
<div class="card-body">${title ? `<div class="card-title">${escapeAttr(title)}</div>` : ''}${bodyHtml}${cardFooterAttr(attrs)}</div></div>\n`;
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
LAYOUT_RENDERERS['full-bg'] = (attrs, body) => {
|
|
1044
|
+
const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
|
|
1045
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
1046
|
+
return `${cardRoot(attrs, ['dm-card-full-bg'])}<div class="card-img-top"${imgStyle(attrs)}></div>
|
|
1047
|
+
<div class="card-body">${title ? `<div class="card-title">${escapeAttr(title)}</div>` : ''}${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
1048
|
+
};
|
|
1049
|
+
|
|
1050
|
+
LAYOUT_RENDERERS['split-half'] = (attrs, body) => {
|
|
1051
|
+
const title = typeof attrs.title === 'string' ? attrs.title.trim() : '';
|
|
1052
|
+
const gradient = typeof attrs.gradient === 'string' ? attrs.gradient.trim() : 'indigo';
|
|
1053
|
+
const bodyHtml = marked.parse(processGridBlocks(body.trim()));
|
|
1054
|
+
return `${cardRoot(attrs, ['card-layout-split'])}<div class="card-split-left card-gradient-${escapeAttr(gradient)}"${imgStyle(attrs)}></div>
|
|
1055
|
+
<div class="card-split-right"><div class="card-body">${title ? `<div class="card-title">${escapeAttr(title)}</div>` : ''}${bodyHtml}</div></div></div>\n`;
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
// ---------------------------------------------------------------------------
|
|
1059
|
+
// Tier 1 content-specific layout renderers
|
|
1060
|
+
// ---------------------------------------------------------------------------
|
|
1061
|
+
|
|
1062
|
+
LAYOUT_RENDERERS['avatar-profile'] = (attrs, body) => {
|
|
1063
|
+
const title = escapeAttr(attrs.title || '');
|
|
1064
|
+
const subtitle = escapeAttr(attrs.subtitle || '');
|
|
1065
|
+
const icon = escapeAttr(attrs.icon || '');
|
|
1066
|
+
const tags = typeof attrs.tags === 'string' ? attrs.tags.split(',').map(t => t.trim()).filter(Boolean) : [];
|
|
1067
|
+
const tagHtml = tags.length ? `<div class="card-tag-pills">${tags.map(t => `<span class="card-pill">${escapeAttr(t)}</span>`).join('')}</div>` : '';
|
|
1068
|
+
const iconHtml = icon ? `<span data-icon="${icon}"></span>` : '';
|
|
1069
|
+
const bodyHtml = body.trim() ? `<div class="card-body">${marked.parse(body.trim())}</div>` : '';
|
|
1070
|
+
return `${cardRoot(attrs)}<div class="card-avatar-wrap">
|
|
1071
|
+
<div class="card-avatar">${iconHtml}</div>
|
|
1072
|
+
${title ? `<div class="card-title">${title}</div>` : ''}
|
|
1073
|
+
${subtitle ? `<div class="card-subtitle">${subtitle}</div>` : ''}${tagHtml}
|
|
1074
|
+
</div>${bodyHtml}${cardFooterAttr(attrs)}</div>\n`;
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
LAYOUT_RENDERERS['stat-metric'] = (attrs, _body) => {
|
|
1078
|
+
const title = escapeAttr(attrs.title || '');
|
|
1079
|
+
const value = escapeAttr(attrs.value || '—');
|
|
1080
|
+
const delta = escapeAttr(attrs.delta || '');
|
|
1081
|
+
const progress = Math.min(100, Math.max(0, parseInt(attrs.progress || '0', 10)));
|
|
1082
|
+
const isPos = delta && !delta.startsWith('-') && !delta.startsWith('↓');
|
|
1083
|
+
const deltaHtml = delta ? `<div class="card-stat-delta ${isPos ? 'positive' : 'negative'}">${delta}</div>` : '';
|
|
1084
|
+
const barHtml = progress > 0 ? `<div class="card-stat-bar"><div class="card-stat-fill" style="width:${progress}%"></div></div>` : '';
|
|
1085
|
+
return `${cardRoot(attrs)}<div class="card-body">
|
|
1086
|
+
${title ? `<div class="card-subtitle">${title}</div>` : ''}
|
|
1087
|
+
<div class="card-stat-value">${value}</div>${deltaHtml}${barHtml}
|
|
1088
|
+
</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
LAYOUT_RENDERERS['quote-testimonial'] = (attrs, body) => {
|
|
1092
|
+
const author = escapeAttr(attrs.title || '');
|
|
1093
|
+
const role = escapeAttr(attrs.subtitle || '');
|
|
1094
|
+
const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
|
|
1095
|
+
return `${cardRoot(attrs)}<span class="card-quote-mark">\u201c</span>
|
|
1096
|
+
<div class="card-quote-text">${bodyHtml}</div>
|
|
1097
|
+
<div class="card-footer card-quote-attr">
|
|
1098
|
+
<div class="card-quote-avatar"></div>
|
|
1099
|
+
<div>${author ? `<div style="font-weight:700;font-size:.85rem">${author}</div>` : ''}${role ? `<div class="card-subtitle">${role}</div>` : ''}</div>
|
|
1100
|
+
</div></div>\n`;
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
LAYOUT_RENDERERS['callout'] = (attrs, body) => {
|
|
1104
|
+
const type = typeof attrs['callout-type'] === 'string' ? attrs['callout-type'].trim() : 'info';
|
|
1105
|
+
const defaults = {info: '💡', warn: '⚠️', success: '✅', error: '❌'};
|
|
1106
|
+
const icon = escapeAttr(attrs.icon || defaults[type] || '💡');
|
|
1107
|
+
const title = escapeAttr(attrs.title || '');
|
|
1108
|
+
const bodyHtml = marked.parse(body.trim());
|
|
1109
|
+
return `${cardRoot(attrs, ['card-callout', type])}<div class="card-callout-inner card-body">
|
|
1110
|
+
<div class="card-callout-icon"><span data-icon="${icon}"></span></div>
|
|
1111
|
+
<div>${title ? `<div class="card-title">${title}</div>` : ''}${bodyHtml}</div>
|
|
1112
|
+
</div></div>\n`;
|
|
1113
|
+
};
|
|
1114
|
+
|
|
1115
|
+
LAYOUT_RENDERERS['video-media'] = (attrs, body) => {
|
|
1116
|
+
const title = escapeAttr(attrs.title || '');
|
|
1117
|
+
const duration = escapeAttr(attrs.duration || '');
|
|
1118
|
+
const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
|
|
1119
|
+
const dur = duration ? `<div class="card-video-duration">${duration}</div>` : '';
|
|
1120
|
+
return `${cardRoot(attrs)}<div class="card-video-thumb"${imgStyle(attrs)}>
|
|
1121
|
+
<div class="card-play-btn">\u25B6</div>${dur}
|
|
1122
|
+
</div>${title ? `<div class="card-header"><div class="card-title">${title}</div></div>` : ''}
|
|
1123
|
+
<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
1124
|
+
};
|
|
1125
|
+
|
|
1126
|
+
LAYOUT_RENDERERS['location-map'] = (attrs, body) => {
|
|
1127
|
+
const address = escapeAttr(attrs.address || '');
|
|
1128
|
+
const title = escapeAttr(attrs.title || '');
|
|
1129
|
+
const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
|
|
1130
|
+
return `${cardRoot(attrs)}<div class="card-map-placeholder">
|
|
1131
|
+
<div class="card-map-grid"></div><div class="card-map-pin">📍</div>
|
|
1132
|
+
</div>${title || address ? `<div class="card-body">${title ? `<div class="card-title">${title}</div>` : ''}${address ? `<div class="card-subtitle">${address}</div>` : ''}${bodyHtml}</div>` : ''}</div>\n`;
|
|
1133
|
+
};
|
|
1134
|
+
|
|
1135
|
+
LAYOUT_RENDERERS['step-numbered'] = (attrs, body) => {
|
|
1136
|
+
const step = escapeAttr(String(attrs.step || '1'));
|
|
1137
|
+
const title = escapeAttr(attrs.title || '');
|
|
1138
|
+
const gradient = escapeAttr(attrs.gradient || 'indigo');
|
|
1139
|
+
const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
|
|
1140
|
+
return `${cardRoot(attrs)}<div class="card-step-bg card-gradient-${gradient}">
|
|
1141
|
+
<div class="card-step-badge">${step}</div>
|
|
1142
|
+
<div class="card-step-ghost">${step}</div>
|
|
1143
|
+
</div>${title ? `<div class="card-header"><div class="card-title">${title}</div></div>` : ''}
|
|
1144
|
+
<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
LAYOUT_RENDERERS['corner-badge'] = (attrs, body) => {
|
|
1148
|
+
const badge = escapeAttr(attrs.badge || 'New');
|
|
1149
|
+
const title = escapeAttr(attrs.title || '');
|
|
1150
|
+
const icon = escapeAttr(attrs.icon || '');
|
|
1151
|
+
const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
|
|
1152
|
+
return `${cardRoot(attrs, ['card-corner-badge-wrap'])}<span class="card-corner-badge">${badge}</span>
|
|
1153
|
+
${icon ? `<div style="display:flex;justify-content:center;padding:18px 16px 4px"><span data-icon="${icon}" style="font-size:2rem"></span></div>` : ''}
|
|
1154
|
+
<div class="card-body" style="text-align:center">${title ? `<div class="card-title">${title}</div>` : ''}${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
LAYOUT_RENDERERS['badge-band'] = (attrs, body) => {
|
|
1158
|
+
const badge = escapeAttr(attrs.badge || 'New');
|
|
1159
|
+
const icon = escapeAttr(attrs.icon || '');
|
|
1160
|
+
const gradient = escapeAttr(attrs.gradient || 'indigo');
|
|
1161
|
+
const title = escapeAttr(attrs.title || '');
|
|
1162
|
+
const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
|
|
1163
|
+
return `${cardRoot(attrs)}<div class="card-badge-band card-gradient-${gradient}">
|
|
1164
|
+
<span class="card-badge-band-label">${badge}</span>
|
|
1165
|
+
${icon ? `<span class="card-badge-band-icon" data-icon="${icon}"></span>` : ''}
|
|
1166
|
+
</div>${title ? `<div class="card-header"><div class="card-title">${title}</div></div>` : ''}
|
|
1167
|
+
<div class="card-body">${bodyHtml}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
LAYOUT_RENDERERS['glass-gradient-border'] = (attrs, body) => {
|
|
1171
|
+
const title = escapeAttr(attrs.title || '');
|
|
1172
|
+
const bodyHtml = marked.parse(body.trim());
|
|
1173
|
+
return `<div class="card-glass-outer mb-4">
|
|
1174
|
+
<div class="card-glass-inner">
|
|
1175
|
+
${title ? `<div class="card-title">${title}</div>` : ''}<div>${bodyHtml}</div>
|
|
1176
|
+
</div></div>\n`;
|
|
1177
|
+
};
|
|
1178
|
+
|
|
1179
|
+
LAYOUT_RENDERERS['rating-review'] = (attrs, body) => {
|
|
1180
|
+
const rating = Math.min(5, Math.max(0, parseInt(attrs.rating || '5', 10)));
|
|
1181
|
+
const stars = '\u2605'.repeat(rating) + '\u2606'.repeat(5 - rating);
|
|
1182
|
+
const author = escapeAttr(attrs.title || '');
|
|
1183
|
+
const role = escapeAttr(attrs.subtitle || '');
|
|
1184
|
+
const verified = 'verified' in attrs;
|
|
1185
|
+
const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
|
|
1186
|
+
return `${cardRoot(attrs)}<span class="card-stars">${stars}</span>
|
|
1187
|
+
<div class="card-quote-text">${bodyHtml}</div>
|
|
1188
|
+
<div class="card-footer card-quote-attr">
|
|
1189
|
+
<div class="card-quote-avatar"></div>
|
|
1190
|
+
<div>${author ? `<div style="font-weight:700;font-size:.82rem">${author}${verified ? '<span class="card-verified">Verified</span>' : ''}</div>` : ''}
|
|
1191
|
+
${role ? `<div class="card-subtitle">${role}</div>` : ''}</div>
|
|
1192
|
+
</div></div>\n`;
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
LAYOUT_RENDERERS['tag-cloud'] = (attrs, body) => {
|
|
1196
|
+
const title = escapeAttr(attrs.title || '');
|
|
1197
|
+
const tagAttr = typeof attrs.tags === 'string' ? attrs.tags.split(',').map(t => t.trim()).filter(Boolean) : [];
|
|
1198
|
+
const tagHtml = tagAttr.map((t, i) => `<span class="card-tag" style="${tagColorStyle(i)}">${escapeAttr(t)}</span>`).join('');
|
|
1199
|
+
const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
|
|
1200
|
+
return `${cardRoot(attrs)}${title ? `<div class="card-header"><div class="card-title">${title}</div></div>` : ''}
|
|
1201
|
+
<div class="card-tag-cloud">${tagHtml}${bodyHtml}</div></div>\n`;
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
LAYOUT_RENDERERS['timeline-entry'] = (attrs, body) => {
|
|
1205
|
+
const date = escapeAttr(attrs.subtitle || '');
|
|
1206
|
+
const title = escapeAttr(attrs.title || '');
|
|
1207
|
+
const tag = escapeAttr(attrs.badge || '');
|
|
1208
|
+
const bodyHtml = body.trim() ? marked.parse(body.trim()) : '';
|
|
1209
|
+
return `${cardRoot(attrs)}<div class="card-timeline-row">
|
|
1210
|
+
<div class="card-timeline-side"><div class="card-timeline-dot"></div><div class="card-timeline-line"></div></div>
|
|
1211
|
+
<div class="card-timeline-body">
|
|
1212
|
+
${date ? `<div class="card-timeline-date">${date}</div>` : ''}
|
|
1213
|
+
${title ? `<div class="card-title">${title}</div>` : ''}
|
|
1214
|
+
${bodyHtml}
|
|
1215
|
+
${tag ? `<span class="card-timeline-tag">${tag}</span>` : ''}
|
|
1216
|
+
</div>
|
|
1217
|
+
</div></div>\n`;
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
LAYOUT_RENDERERS['code-snippet'] = (attrs, body) => {
|
|
1221
|
+
const lang = escapeAttr(attrs.lang || 'Code');
|
|
1222
|
+
const code = body.trim()
|
|
1223
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1224
|
+
return `${cardRoot(attrs)}<div class="card-code-header">
|
|
1225
|
+
<span class="card-code-lang">${lang}</span>
|
|
1226
|
+
</div><div class="card-code-body"><pre><code>${code}</code></pre></div></div>\n`;
|
|
1227
|
+
};
|
|
1228
|
+
|
|
1229
|
+
LAYOUT_RENDERERS['before-after'] = (attrs, body) => {
|
|
1230
|
+
const title = escapeAttr(attrs.title || '');
|
|
1231
|
+
const beforeRaw = parseSubTag(body, 'before') || '';
|
|
1232
|
+
const afterRaw = parseSubTag(body, 'after') || '';
|
|
1233
|
+
const beforeItems = beforeRaw.split(/[·\n]/).map(s => s.trim()).filter(Boolean);
|
|
1234
|
+
const afterItems = afterRaw.split(/[·\n]/).map(s => s.trim()).filter(Boolean);
|
|
1235
|
+
const colBefore = beforeItems.map(i => `<div class="card-compare-item"><span class="card-compare-x">\u2717</span>${escapeAttr(i)}</div>`).join('');
|
|
1236
|
+
const colAfter = afterItems.map(i => `<div class="card-compare-item"><span class="card-compare-check">\u2713</span>${escapeAttr(i)}</div>`).join('');
|
|
1237
|
+
return `${cardRoot(attrs)}${title ? `<div class="card-header"><div class="card-title">${title}</div></div>` : ''}
|
|
1238
|
+
<div class="card-compare-grid">
|
|
1239
|
+
<div class="card-compare-col"><span class="card-compare-label before">Before</span>${colBefore}</div>
|
|
1240
|
+
<div class="card-compare-col"><span class="card-compare-label after">After</span>${colAfter}</div>
|
|
1241
|
+
</div></div>\n`;
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
LAYOUT_RENDERERS['pricing'] = (attrs, body) => {
|
|
1245
|
+
const title = escapeAttr(attrs.title || '');
|
|
1246
|
+
const price = escapeAttr(attrs.price || '');
|
|
1247
|
+
const period = escapeAttr(attrs.period || '');
|
|
1248
|
+
const gradient = escapeAttr(attrs.gradient || 'indigo');
|
|
1249
|
+
const cta = escapeAttr(attrs.footer || 'Get started');
|
|
1250
|
+
const features = parseSubTagList(body, 'feature');
|
|
1251
|
+
const featHtml = features.map(f => `<div class="card-pricing-feature"><span class="card-pricing-check">\u2713</span>${escapeAttr(f.content)}</div>`).join('');
|
|
1252
|
+
return `${cardRoot(attrs)}<div class="card-header card-gradient-${gradient}">
|
|
1253
|
+
<div class="card-subtitle">${title}</div>
|
|
1254
|
+
<div class="card-stat-value" style="color:#fff">${price}</div>
|
|
1255
|
+
${period ? `<div style="opacity:.75;font-size:.8rem">${period}</div>` : ''}
|
|
1256
|
+
</div><div class="card-pricing-features">${featHtml}</div>
|
|
1257
|
+
<a class="card-pricing-cta" href="#">${cta}</a></div>\n`;
|
|
1258
|
+
};
|
|
1259
|
+
|
|
1260
|
+
LAYOUT_RENDERERS['feature-comparison'] = (attrs, body) => {
|
|
1261
|
+
const title = escapeAttr(attrs.title || '');
|
|
1262
|
+
const plan = escapeAttr(attrs.subtitle || '');
|
|
1263
|
+
const gradient = escapeAttr(attrs.gradient || 'indigo');
|
|
1264
|
+
const features = parseSubTagList(body, 'feature');
|
|
1265
|
+
const rows = features.map(f => {
|
|
1266
|
+
const included = !('excluded' in f.attrs);
|
|
1267
|
+
return `<div class="card-fc-row"><span>${escapeAttr(f.content)}</span><span class="${included ? 'card-fc-yes' : 'card-fc-no'}">${included ? '\u2713' : '\u2717'}</span></div>`;
|
|
1268
|
+
}).join('');
|
|
1269
|
+
return `${cardRoot(attrs)}<div class="card-header card-gradient-${gradient}">
|
|
1270
|
+
${plan ? `<div class="card-subtitle" style="opacity:.8;font-size:.75rem">${plan}</div>` : ''}
|
|
1271
|
+
<div class="card-title">${title}</div>
|
|
1272
|
+
</div>${rows}</div>\n`;
|
|
1273
|
+
};
|
|
1274
|
+
|
|
1275
|
+
LAYOUT_RENDERERS['activity-feed'] = (attrs, body) => {
|
|
1276
|
+
const title = escapeAttr(attrs.title || 'Activity');
|
|
1277
|
+
// [activity] is self-closing: [activity user="..." action="..." time="..."]
|
|
1278
|
+
const actRe = /\[activity([^\]]*)\]/gi;
|
|
1279
|
+
const activities = [];
|
|
1280
|
+
body.replace(actRe, (_, attrStr) => activities.push(parseShortcodeAttrs(attrStr)));
|
|
1281
|
+
const items = activities.map(a => {
|
|
1282
|
+
const userRaw = a.user || '';
|
|
1283
|
+
const user = escapeAttr(userRaw);
|
|
1284
|
+
const action = escapeAttr(a.action || '');
|
|
1285
|
+
const time = escapeAttr(a.time || '');
|
|
1286
|
+
const initial = userRaw.charAt(0).toUpperCase();
|
|
1287
|
+
return `<div class="card-activity-item">
|
|
1288
|
+
<div class="card-activity-avatar">${initial}</div>
|
|
1289
|
+
<div><div style="font-size:.82rem"><strong>${user}</strong> ${action}</div>${time ? `<div style="font-size:.7rem;color:var(--dm-text-muted,#9ca3af);margin-top:2px">${time}</div>` : ''}</div>
|
|
1290
|
+
</div>`;
|
|
1291
|
+
}).join('');
|
|
1292
|
+
return `${cardRoot(attrs)}<div class="card-header"><div class="card-title">${title}</div></div>
|
|
1293
|
+
<div>${items}</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
1294
|
+
};
|
|
1295
|
+
|
|
1296
|
+
LAYOUT_RENDERERS['progress-goal'] = (attrs, body) => {
|
|
1297
|
+
const title = escapeAttr(attrs.title || '');
|
|
1298
|
+
const subtitle = escapeAttr(attrs.subtitle || '');
|
|
1299
|
+
const progress = Math.min(100, Math.max(0, parseInt(attrs.progress || '0', 10)));
|
|
1300
|
+
const milestones = parseSubTagList(body, 'milestone');
|
|
1301
|
+
const msHtml = milestones.map(m => {
|
|
1302
|
+
const done = 'done' in m.attrs;
|
|
1303
|
+
return `<div class="card-milestone"><div class="card-milestone-dot ${done ? 'done' : 'pending'}"></div>${escapeAttr(m.content)}</div>`;
|
|
1304
|
+
}).join('');
|
|
1305
|
+
return `${cardRoot(attrs)}<div class="card-body">
|
|
1306
|
+
${title ? `<div class="card-title">${title}</div>` : ''}
|
|
1307
|
+
<div style="display:flex;justify-content:space-between;font-size:.82rem;margin-top:8px">
|
|
1308
|
+
<span>${subtitle}</span><strong style="color:var(--dm-primary,#6366f1)">${progress}%</strong>
|
|
1309
|
+
</div>
|
|
1310
|
+
<div class="card-progress-bar"><div class="card-progress-fill" style="width:${progress}%"></div></div>
|
|
1311
|
+
${msHtml}
|
|
1312
|
+
</div>${cardFooterAttr(attrs)}</div>\n`;
|
|
1313
|
+
};
|
|
1314
|
+
|
|
1315
|
+
LAYOUT_RENDERERS['file-document'] = (attrs, _body) => {
|
|
1316
|
+
const filename = escapeAttr(attrs.filename || 'document');
|
|
1317
|
+
const filesize = escapeAttr(attrs.filesize || '');
|
|
1318
|
+
const filetype = escapeAttr((attrs.filetype || '').toUpperCase());
|
|
1319
|
+
const title = escapeAttr(attrs.title || filename);
|
|
1320
|
+
return `${cardRoot(attrs)}<div class="card-file-row">
|
|
1321
|
+
<div class="card-file-icon">\uD83D\uDCC4<div class="card-file-ext">${filetype}</div></div>
|
|
1322
|
+
<div><div class="card-title" style="font-size:.9rem">${title}</div>
|
|
1323
|
+
${filesize ? `<div class="card-subtitle">${filesize}</div>` : ''}
|
|
1324
|
+
<a class="card-file-dl" href="#">\u2193 Download</a></div>
|
|
1325
|
+
</div></div>\n`;
|
|
1326
|
+
};
|
|
1327
|
+
|
|
1328
|
+
// ---------------------------------------------------------------------------
|
|
1329
|
+
// Legacy card renderer (used when no `layout` attribute is present)
|
|
1330
|
+
// ---------------------------------------------------------------------------
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Renders a card using the original/legacy logic. Receives pre-parsed attrs.
|
|
1334
|
+
*
|
|
1335
|
+
* @param {object} attrs
|
|
1336
|
+
* @param {string} body
|
|
1337
|
+
* @param {object} markedInstance
|
|
1338
|
+
* @param {Function} escAttr
|
|
1339
|
+
* @returns {string}
|
|
1340
|
+
*/
|
|
1341
|
+
function renderLegacyCard(attrs, body, markedInstance, escAttr) {
|
|
1342
|
+
const strAttr = (key) => typeof attrs[key] === 'string' ? attrs[key].trim() : '';
|
|
1343
|
+
const title = strAttr('title');
|
|
1344
|
+
const subtitle = strAttr('subtitle');
|
|
1345
|
+
const icon = strAttr('icon');
|
|
1346
|
+
const footer = strAttr('footer');
|
|
1347
|
+
const collapsible = attrs.collapsible === 'true';
|
|
1348
|
+
const hover = 'hover' in attrs;
|
|
1349
|
+
const variant = strAttr('variant');
|
|
1350
|
+
const extraClass = strAttr('class');
|
|
1351
|
+
|
|
1352
|
+
// Root class list
|
|
1353
|
+
const classes = ['card', 'mb-4'];
|
|
1354
|
+
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
|
+
|
|
1359
|
+
const id = attrs.id ? ` id="${escAttr(attrs.id)}"` : '';
|
|
1360
|
+
const coll = collapsible ? ' data-collapsible="true"' : '';
|
|
1361
|
+
|
|
1362
|
+
const iconLayout = (attrs['icon-layout'] || 'inline').trim(); // 'inline' | 'stacked'
|
|
1363
|
+
|
|
1364
|
+
// Extract [header]...[/header] and [footer]...[/footer] sub-tags (Pattern B)
|
|
1365
|
+
let headerContent = null;
|
|
1366
|
+
let footerContent = null;
|
|
1367
|
+
let remaining = body;
|
|
1368
|
+
remaining = remaining.replace(
|
|
1369
|
+
/\[header\]([\s\S]*?)\[\/header\]/i,
|
|
1370
|
+
(_, inner) => {
|
|
1371
|
+
headerContent = inner.trim();
|
|
1372
|
+
return '';
|
|
1373
|
+
}
|
|
1374
|
+
);
|
|
1375
|
+
remaining = remaining.replace(
|
|
1376
|
+
/\[footer\]([\s\S]*?)\[\/footer\]/i,
|
|
1377
|
+
(_, inner) => {
|
|
1378
|
+
footerContent = inner.trim();
|
|
1379
|
+
return '';
|
|
1380
|
+
}
|
|
1381
|
+
);
|
|
1382
|
+
|
|
1383
|
+
// Header — sub-tag wins over attributes; attributes only used when no sub-tag
|
|
1384
|
+
let headerHtml = '';
|
|
1385
|
+
if (headerContent !== null) {
|
|
1386
|
+
headerHtml = `<div class="card-header">${markedInstance.parse(headerContent)}</div>`;
|
|
1387
|
+
} else if (title || icon) {
|
|
889
1388
|
let inner = '';
|
|
890
1389
|
if (icon && iconLayout === 'stacked') {
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1390
|
+
// Stacked: icon centred above title, all centred
|
|
1391
|
+
const subtitleHtml = subtitle ? `<div class="card-subtitle">${subtitle}</div>` : '';
|
|
1392
|
+
inner = `<span data-icon="${escAttr(icon)}"></span>` +
|
|
1393
|
+
`<div class="card-title">${title}</div>${subtitleHtml}`;
|
|
1394
|
+
headerHtml = `<div class="card-header card-header-icon-stacked">${inner}</div>`;
|
|
896
1395
|
} else if (icon && title) {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1396
|
+
// Inline: icon left, title to its right in a flex row
|
|
1397
|
+
const subtitleHtml = subtitle ? `<div class="card-subtitle">${subtitle}</div>` : '';
|
|
1398
|
+
inner = `<span data-icon="${escAttr(icon)}"></span>` +
|
|
1399
|
+
`<div class="card-header-content"><div class="card-title">${title}</div>${subtitleHtml}</div>`;
|
|
1400
|
+
headerHtml = `<div class="card-header card-header-icon-inline">${inner}</div>`;
|
|
902
1401
|
} else {
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1402
|
+
// Title only (no icon)
|
|
1403
|
+
inner = `<div class="card-title">${title}</div>`;
|
|
1404
|
+
if (subtitle) inner += `<div class="card-subtitle">${subtitle}</div>`;
|
|
1405
|
+
headerHtml = `<div class="card-header">${inner}</div>`;
|
|
907
1406
|
}
|
|
908
|
-
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
const bodyHtml = `<div class="card-body">${markedInstance.parse(remaining.trim())}</div>`;
|
|
1410
|
+
const footerHtml = footerContent !== null
|
|
1411
|
+
? `<div class="card-footer">${markedInstance.parse(footerContent)}</div>`
|
|
1412
|
+
: footer ? `<div class="card-footer">${footer}</div>` : '';
|
|
1413
|
+
|
|
1414
|
+
return `<div class="${classes.join(' ')}"${coll}${id}>${headerHtml}${bodyHtml}${footerHtml}</div>\n`;
|
|
1415
|
+
}
|
|
909
1416
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
: footer ? `<div class="card-footer">${footer}</div>` : '';
|
|
1417
|
+
// ---------------------------------------------------------------------------
|
|
1418
|
+
// processCardBlocks — routing entry point
|
|
1419
|
+
// ---------------------------------------------------------------------------
|
|
914
1420
|
|
|
915
|
-
|
|
1421
|
+
/**
|
|
1422
|
+
* Pre-process [card] shortcodes before running through marked.
|
|
1423
|
+
*
|
|
1424
|
+
* Syntax:
|
|
1425
|
+
* [card title="Optional Title" collapsible="true"]
|
|
1426
|
+
* Body content (supports Markdown).
|
|
1427
|
+
* [/card]
|
|
1428
|
+
*
|
|
1429
|
+
* Cards with a `layout` attribute are dispatched to LAYOUT_RENDERERS[layout].
|
|
1430
|
+
* Cards without (or with an unknown) layout fall back to renderLegacyCard.
|
|
1431
|
+
*
|
|
1432
|
+
* @param {string} markdown
|
|
1433
|
+
* @returns {string}
|
|
1434
|
+
*/
|
|
1435
|
+
function processCardBlocks(markdown) {
|
|
1436
|
+
const {scrubbed, restore} = scrubCodeRegions(markdown);
|
|
1437
|
+
return restore(scrubbed.replace(
|
|
1438
|
+
/\[card([^\]]*)\]([\s\S]*?)\[\/card\]/gi,
|
|
1439
|
+
(_, attrStr, body) => {
|
|
1440
|
+
const attrs = parseShortcodeAttrs(attrStr);
|
|
1441
|
+
const layout = typeof attrs.layout === 'string' ? attrs.layout.trim() : '';
|
|
1442
|
+
const renderer = LAYOUT_RENDERERS[layout];
|
|
1443
|
+
return renderer
|
|
1444
|
+
? renderer(attrs, body, marked, escapeAttr)
|
|
1445
|
+
: renderLegacyCard(attrs, body, marked, escapeAttr);
|
|
916
1446
|
}
|
|
917
1447
|
));
|
|
918
1448
|
}
|
|
@@ -943,7 +1473,10 @@ function processTabsBlocks(markdown) {
|
|
|
943
1473
|
const prefix = `dm-tab-${counter}`;
|
|
944
1474
|
const attrs = parseShortcodeAttrs(attrStr);
|
|
945
1475
|
const pillsClass = attrs.style === 'pills' ? ' tabs-pills' : '';
|
|
1476
|
+
const borderedClass = attrs.style === 'bordered' ? ' tabs--bordered' : '';
|
|
1477
|
+
const fadeClass = 'fade' in attrs ? ' tabs--fade' : '';
|
|
946
1478
|
const centeredClass = attrs.align === 'center' ? ' tabs-centered' : '';
|
|
1479
|
+
const rightClass = attrs.align === 'right' ? ' tabs--right' : '';
|
|
947
1480
|
const idAttr = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
|
|
948
1481
|
|
|
949
1482
|
// Parse inner [tab title="..."] items
|
|
@@ -959,20 +1492,22 @@ function processTabsBlocks(markdown) {
|
|
|
959
1492
|
// that would confuse subsequent scrubCodeRegions calls.
|
|
960
1493
|
const restoredBody = restore(tabBody.trim());
|
|
961
1494
|
const bodyHtml = marked.parse(processCardBlocks(processGridBlocks(restoredBody)));
|
|
962
|
-
|
|
1495
|
+
const icon = tabAttrs.icon ? tabAttrs.icon.trim() : '';
|
|
1496
|
+
items.push({title, icon, paneId, bodyHtml, first: tabIdx === 1});
|
|
963
1497
|
});
|
|
964
1498
|
|
|
965
1499
|
if (!items.length) return '';
|
|
966
1500
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1501
|
+
const navItems = items.map(t => {
|
|
1502
|
+
const iconHtml = t.icon ? `<span data-icon="${escapeAttr(t.icon)}"></span>` : '';
|
|
1503
|
+
return `<button class="tab-item${t.first ? ' active' : ''}">${iconHtml}${escapeAttr(t.title)}</button>`;
|
|
1504
|
+
}).join('\n ');
|
|
970
1505
|
const panes = items.map(t =>
|
|
971
1506
|
`<div class="tab-panel${t.first ? ' active' : ''}">${t.bodyHtml}</div>`
|
|
972
1507
|
).join('\n ');
|
|
973
1508
|
|
|
974
1509
|
return (
|
|
975
|
-
`<div class="tabs${pillsClass}${centeredClass}"${idAttr}>\n` +
|
|
1510
|
+
`<div class="tabs${pillsClass}${borderedClass}${fadeClass}${centeredClass}${rightClass}"${idAttr}>\n` +
|
|
976
1511
|
` <div class="tab-list">\n ${navItems}\n </div>\n` +
|
|
977
1512
|
` <div class="tab-content">\n ${panes}\n </div>\n` +
|
|
978
1513
|
`</div>\n`
|
|
@@ -1749,6 +2284,8 @@ function processHeroBlocks(markdown) {
|
|
|
1749
2284
|
const cls = attrs.class || '';
|
|
1750
2285
|
const id = attrs.id || '';
|
|
1751
2286
|
const bg = attrs.bg || '';
|
|
2287
|
+
const heroColor = attrs.color ? String(attrs.color) : '';
|
|
2288
|
+
const minHeight = attrs['min-height'] ? String(attrs['min-height']) : '';
|
|
1752
2289
|
|
|
1753
2290
|
const twinkle = 'twinkle' in attrs;
|
|
1754
2291
|
const twinkleCount = attrs['twinkle-count'] || '';
|
|
@@ -1769,7 +2306,11 @@ function processHeroBlocks(markdown) {
|
|
|
1769
2306
|
|
|
1770
2307
|
const styleParts = [];
|
|
1771
2308
|
if (image) styleParts.push(`background-image:url('${escapeAttr(image)}')`);
|
|
1772
|
-
|
|
2309
|
+
const resolvedBg = heroColor || bg;
|
|
2310
|
+
const safeBg = resolvedBg && /^[a-zA-Z0-9#(),.\s%/-]+$/.test(resolvedBg) ? resolvedBg : '';
|
|
2311
|
+
if (safeBg) styleParts.push(`background-color:${safeBg}`);
|
|
2312
|
+
const safeMinHeight = minHeight && /^[0-9]+(?:px|em|rem|vh|vw|%)$/.test(minHeight) ? minHeight : '';
|
|
2313
|
+
if (safeMinHeight) styleParts.push(`min-height:${safeMinHeight}`);
|
|
1773
2314
|
const style = styleParts.length ? ` style="${styleParts.join(';')}"` : '';
|
|
1774
2315
|
const idAttr = id ? ` id="${escapeAttr(id)}"` : '';
|
|
1775
2316
|
|
|
@@ -1807,6 +2348,61 @@ function processHeroBlocks(markdown) {
|
|
|
1807
2348
|
* @param {string} markdown
|
|
1808
2349
|
* @returns {string}
|
|
1809
2350
|
*/
|
|
2351
|
+
/**
|
|
2352
|
+
* Pre-process [banner] shortcodes before running through marked.
|
|
2353
|
+
*
|
|
2354
|
+
* Syntax:
|
|
2355
|
+
* [banner type="info" title="Note" icon="info" dismissible]
|
|
2356
|
+
* Body content (Markdown parsed).
|
|
2357
|
+
* [/banner]
|
|
2358
|
+
*
|
|
2359
|
+
* Supported attributes:
|
|
2360
|
+
* type - info (default) / success / warning / danger / neutral
|
|
2361
|
+
* title - optional heading text
|
|
2362
|
+
* icon - Domma icon name (renders <span data-icon="...">)
|
|
2363
|
+
* dismissible - bare flag; adds dismiss button
|
|
2364
|
+
* class - extra CSS class on the wrapper
|
|
2365
|
+
*
|
|
2366
|
+
* @param {string} markdown
|
|
2367
|
+
* @returns {string}
|
|
2368
|
+
*/
|
|
2369
|
+
function processBannerBlocks(markdown) {
|
|
2370
|
+
return markdown.replace(
|
|
2371
|
+
/\[banner([^\]]*)\]([\s\S]*?)\[\/banner\]/gi,
|
|
2372
|
+
(_, attrStr, body) => {
|
|
2373
|
+
const attrs = parseShortcodeAttrs(attrStr);
|
|
2374
|
+
const VALID_BANNER_TYPES = new Set(['info', 'success', 'warning', 'danger', 'neutral']);
|
|
2375
|
+
const type = VALID_BANNER_TYPES.has(attrs.type) ? attrs.type : 'info';
|
|
2376
|
+
const title = attrs.title || '';
|
|
2377
|
+
const icon = attrs.icon || '';
|
|
2378
|
+
const dismissible = 'dismissible' in attrs;
|
|
2379
|
+
const extraClass = attrs.class || '';
|
|
2380
|
+
|
|
2381
|
+
const classes = [`dm-banner`, `dm-banner--${type}`];
|
|
2382
|
+
if (extraClass) classes.push(extraClass);
|
|
2383
|
+
|
|
2384
|
+
const iconHtml = icon
|
|
2385
|
+
? `<span class="dm-banner__icon" data-icon="${escapeAttr(icon)}"></span>\n `
|
|
2386
|
+
: '';
|
|
2387
|
+
const titleHtml = title
|
|
2388
|
+
? `<strong class="dm-banner__title">${escapeAttr(title)}</strong>\n `
|
|
2389
|
+
: '';
|
|
2390
|
+
const bodyHtml = marked.parse(body.trim());
|
|
2391
|
+
const dismissHtml = dismissible
|
|
2392
|
+
? `\n <button class="dm-banner__dismiss" aria-label="Dismiss">×</button>`
|
|
2393
|
+
: '';
|
|
2394
|
+
|
|
2395
|
+
return (
|
|
2396
|
+
`<div class="${classes.join(' ')}">\n` +
|
|
2397
|
+
` ${iconHtml}<div class="dm-banner__body">\n` +
|
|
2398
|
+
` ${titleHtml}${bodyHtml}` +
|
|
2399
|
+
` </div>${dismissHtml}\n` +
|
|
2400
|
+
`</div>\n`
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2405
|
+
|
|
1810
2406
|
function processSlideoverBlocks(markdown) {
|
|
1811
2407
|
const {scrubbed, restore} = scrubCodeRegions(markdown);
|
|
1812
2408
|
let counter = 0;
|
|
@@ -1948,7 +2544,8 @@ export async function parseMarkdown(raw) {
|
|
|
1948
2544
|
const withCta = processCtaBlocks(withLink);
|
|
1949
2545
|
const withGrid = processGridBlocks(withCta);
|
|
1950
2546
|
const withCard = processCardBlocks(withGrid);
|
|
1951
|
-
|
|
2547
|
+
const withBanner = processBannerBlocks(withCard);
|
|
2548
|
+
const withSlideover = processSlideoverBlocks(withBanner);
|
|
1952
2549
|
const rendered = marked.parse(withSlideover);
|
|
1953
2550
|
|
|
1954
2551
|
const sanitized = sanitizeHtml(rendered, {
|
|
@@ -1971,7 +2568,7 @@ export async function parseMarkdown(raw) {
|
|
|
1971
2568
|
select: ['name', 'required', 'disabled', 'multiple'],
|
|
1972
2569
|
option: ['value', 'selected', 'disabled'],
|
|
1973
2570
|
optgroup: ['label', 'disabled'],
|
|
1974
|
-
|
|
2571
|
+
button: ['type', 'disabled', 'aria-label', 'data-action', 'data-entry', 'data-confirm'],
|
|
1975
2572
|
label: ['for'],
|
|
1976
2573
|
fieldset: ['disabled'],
|
|
1977
2574
|
...extensions.attributes
|