domma-cms 0.9.5 → 0.9.10
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/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 +4 -4
- package/admin/js/views/plugins.js +10 -16
- package/admin/js/views/tutorials.js +1 -1
- package/config/plugins.json +4 -14
- package/config/site.json +1 -1
- package/package.json +1 -1
- package/plugins/notes/admin/views/notes.js +24 -25
- package/plugins/todo/admin/views/todo.js +32 -33
- package/public/css/site.css +1 -1
- package/server/services/markdown.js +609 -79
|
@@ -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
|
+
// ---------------------------------------------------------------------------
|
|
945
|
+
|
|
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
|
+
);
|
|
883
1382
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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>` : '';
|
|
909
1413
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1414
|
+
return `<div class="${classes.join(' ')}"${coll}${id}>${headerHtml}${bodyHtml}${footerHtml}</div>\n`;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
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
|
}
|