react-markdown-fences
Version:
Render interactive fence dialects (buttons, charts, diagrams) inside Markdown for React applications
526 lines (520 loc) • 16.1 kB
JavaScript
'use strict';
var ReactMarkdown = require('react-markdown');
var remarkGfm = require('remark-gfm');
var React2 = require('react');
var jsxRuntime = require('react/jsx-runtime');
var zod = require('zod');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var ReactMarkdown__default = /*#__PURE__*/_interopDefault(ReactMarkdown);
var remarkGfm__default = /*#__PURE__*/_interopDefault(remarkGfm);
var React2__namespace = /*#__PURE__*/_interopNamespace(React2);
// src/ChatMarkdown.tsx
var Button = ({
label,
action,
variant = "primary",
size = "md",
payload,
onAction,
buttonDefaults
}) => {
const getVariantStyles = () => {
switch (variant) {
case "primary":
return {
backgroundColor: "#000000",
color: "#ffffff",
borderColor: "#000000"
};
case "success":
return {
backgroundColor: "#16a34a",
color: "#ffffff",
borderColor: "#16a34a"
};
case "warning":
return {
backgroundColor: "#eab308",
color: "#000000",
borderColor: "#eab308"
};
case "destructive":
return {
backgroundColor: "#dc2626",
color: "#ffffff",
borderColor: "#dc2626"
};
default:
return {
backgroundColor: "#000000",
color: "#ffffff",
borderColor: "#000000"
};
}
};
const getSizeStyles = () => {
switch (size) {
case "xs":
return {
fontSize: "12px",
padding: "4px 8px"
};
case "sm":
return {
fontSize: "14px",
padding: "6px 12px"
};
case "lg":
return {
fontSize: "16px",
padding: "12px 16px"
};
default:
return {
fontSize: "14px",
padding: "8px 12px"
};
}
};
const baseStyles = {
display: "inline-flex",
alignItems: "center",
border: "1px solid",
fontWeight: "500",
cursor: "pointer",
transition: "all 0.2s ease",
textDecoration: "none",
outline: "none"
};
const buttonStyles = {
...baseStyles,
...getVariantStyles(),
...getSizeStyles(),
// Developer defaults override everything
...buttonDefaults?.backgroundColor && { backgroundColor: buttonDefaults.backgroundColor },
...buttonDefaults?.color && { color: buttonDefaults.color },
...buttonDefaults?.borderColor && { borderColor: buttonDefaults.borderColor },
...buttonDefaults?.borderRadius && { borderRadius: buttonDefaults.borderRadius },
...buttonDefaults?.fontSize && { fontSize: buttonDefaults.fontSize },
...buttonDefaults?.padding && { padding: buttonDefaults.padding },
...buttonDefaults?.fontWeight && { fontWeight: buttonDefaults.fontWeight }
};
const defaultHoverStyles = {
opacity: 0.9,
transform: "translateY(-1px)"
};
const hoverStyles = {
...defaultHoverStyles,
...buttonDefaults?.hoverBackgroundColor && { backgroundColor: buttonDefaults.hoverBackgroundColor },
...buttonDefaults?.hoverColor && { color: buttonDefaults.hoverColor },
...buttonDefaults?.hoverTransform && { transform: buttonDefaults.hoverTransform }
};
const [isHovered, setIsHovered] = React2__namespace.useState(false);
const handleClick = () => {
onAction?.(action, payload);
};
return /* @__PURE__ */ jsxRuntime.jsx(
"button",
{
style: {
...buttonStyles,
...isHovered ? hoverStyles : {}
},
onClick: handleClick,
onMouseEnter: () => setIsHovered(true),
onMouseLeave: () => setIsHovered(false),
children: label
}
);
};
var ButtonGroup = ({ buttons, onAction, buttonDefaults }) => {
const containerStyles = {
display: "flex",
flexWrap: "wrap",
gap: "8px",
alignItems: "flex-start"
};
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: containerStyles, children: buttons.map((button, index) => /* @__PURE__ */ jsxRuntime.jsx(
Button,
{
...button,
onAction,
buttonDefaults
},
index
)) });
};
var Chart;
var SUPPORTED_CHART_TYPES = ["bar", "line"];
var ChartJSBlock = ({ spec, chartDefaults }) => {
const canvasRef = React2__namespace.useRef(null);
const chartRef = React2__namespace.useRef(null);
const [isLoading, setIsLoading] = React2__namespace.useState(true);
const [error, setError] = React2__namespace.useState(null);
if (!SUPPORTED_CHART_TYPES.includes(spec.type)) {
return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
padding: "16px",
border: "1px solid #fca5a5",
borderRadius: "8px",
backgroundColor: "#fee2e2",
color: "#dc2626",
fontSize: "14px"
}, children: [
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Chart type not supported:" }),
" '",
spec.type,
"'. Only 'bar' and 'line' charts are supported."
] });
}
React2__namespace.useEffect(() => {
let mounted = true;
const loadChart = async () => {
try {
if (!Chart) {
const mod = await import('chart.js/auto');
Chart = mod.Chart || mod.default;
}
if (!mounted || !canvasRef.current) return;
if (chartRef.current) {
chartRef.current.destroy();
}
const mergedOptions = {
...spec.options,
// Apply developer defaults to chart options
...chartDefaults?.backgroundColor && {
backgroundColor: chartDefaults.backgroundColor
},
plugins: {
...spec.options?.plugins,
legend: {
...spec.options?.plugins?.legend,
labels: {
...spec.options?.plugins?.legend?.labels,
...chartDefaults?.fontFamily && { font: { family: chartDefaults.fontFamily } },
...chartDefaults?.fontSize && { font: { size: chartDefaults.fontSize } }
}
}
},
scales: {
...spec.options?.scales,
...chartDefaults?.gridColor && {
x: {
...spec.options?.scales?.x,
grid: {
...spec.options?.scales?.x?.grid,
color: chartDefaults.gridColor
}
},
y: {
...spec.options?.scales?.y,
grid: {
...spec.options?.scales?.y?.grid,
color: chartDefaults.gridColor
}
}
}
}
};
const mergedSpec = {
...spec,
options: mergedOptions,
...chartDefaults?.borderColor && {
data: {
...spec.data,
datasets: spec.data.datasets?.map((dataset) => ({
...dataset,
borderColor: dataset.borderColor || chartDefaults.borderColor
}))
}
}
};
chartRef.current = new Chart(canvasRef.current, mergedSpec);
setIsLoading(false);
} catch (err) {
if (mounted) {
setError(err instanceof Error ? err.message : "Failed to load chart");
setIsLoading(false);
}
}
};
loadChart();
return () => {
mounted = false;
if (chartRef.current) {
chartRef.current.destroy();
}
};
}, [spec, chartDefaults]);
if (error) {
return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
padding: "16px",
border: "1px solid #e5e7eb",
borderRadius: "8px",
backgroundColor: "#f9fafb",
color: "#dc2626",
fontSize: "14px"
}, children: [
"Chart Error: ",
error
] });
}
if (isLoading) {
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
padding: "16px",
border: "1px solid #e5e7eb",
borderRadius: "8px",
backgroundColor: "#f9fafb",
color: "#6b7280",
fontSize: "14px",
textAlign: "center"
}, children: "Loading chart..." });
}
const containerStyle = {
width: chartDefaults?.width || "100%",
height: chartDefaults?.height || "320px",
position: "relative",
...chartDefaults?.backgroundColor && { backgroundColor: chartDefaults.backgroundColor }
};
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: containerStyle, children: /* @__PURE__ */ jsxRuntime.jsx(
"canvas",
{
ref: canvasRef,
style: {
width: "100%",
height: "100%",
maxWidth: "100%",
maxHeight: "100%"
}
}
) });
};
var mermaidLib;
var MermaidBlock = ({ code }) => {
const id = React2__namespace.useMemo(() => "m" + Math.random().toString(36).slice(2), []);
const hostRef = React2__namespace.useRef(null);
const [isLoading, setIsLoading] = React2__namespace.useState(true);
const [error, setError] = React2__namespace.useState(null);
React2__namespace.useEffect(() => {
let mounted = true;
const loadMermaid = async () => {
try {
if (!mermaidLib) {
const mod = await import('mermaid');
mermaidLib = mod.default || mod;
mermaidLib.initialize({
startOnLoad: false,
securityLevel: "strict",
theme: "default"
});
}
if (!mounted || !hostRef.current) return;
if (hostRef.current) {
hostRef.current.innerHTML = "";
}
const { svg } = await mermaidLib.render(id, code);
if (hostRef.current && mounted) {
hostRef.current.innerHTML = svg;
setIsLoading(false);
}
} catch (err) {
if (mounted) {
setError(err instanceof Error ? err.message : "Failed to render diagram");
setIsLoading(false);
}
}
};
loadMermaid();
return () => {
mounted = false;
};
}, [code, id]);
if (error) {
return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
padding: "16px",
border: "1px solid #e5e7eb",
borderRadius: "8px",
backgroundColor: "#f9fafb",
color: "#dc2626",
fontSize: "14px"
}, children: [
"Mermaid Error: ",
error
] });
}
if (isLoading) {
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
padding: "16px",
border: "1px solid #e5e7eb",
borderRadius: "8px",
backgroundColor: "#f9fafb",
color: "#6b7280",
fontSize: "14px",
textAlign: "center"
}, children: "Loading diagram..." });
}
return /* @__PURE__ */ jsxRuntime.jsx(
"div",
{
ref: hostRef,
style: {
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "200px"
}
}
);
};
var ButtonSpec = zod.z.object({
label: zod.z.string(),
action: zod.z.string(),
variant: zod.z.enum(["primary", "success", "warning", "destructive"]).optional(),
size: zod.z.enum(["xs", "sm", "md", "lg"]).optional(),
payload: zod.z.unknown().optional()
});
var ButtonGroupSpec = zod.z.array(ButtonSpec);
var ButtonDefaults = zod.z.object({
backgroundColor: zod.z.string().optional(),
color: zod.z.string().optional(),
borderColor: zod.z.string().optional(),
borderRadius: zod.z.string().optional(),
fontSize: zod.z.string().optional(),
padding: zod.z.string().optional(),
fontWeight: zod.z.string().optional(),
hoverBackgroundColor: zod.z.string().optional(),
hoverColor: zod.z.string().optional(),
hoverTransform: zod.z.string().optional()
}).optional();
var ChartDefaults = zod.z.object({
width: zod.z.string().optional(),
height: zod.z.string().optional(),
backgroundColor: zod.z.string().optional(),
borderColor: zod.z.string().optional(),
gridColor: zod.z.string().optional(),
fontFamily: zod.z.string().optional(),
fontSize: zod.z.number().optional()
}).optional();
var ChartJSSpec = zod.z.object({
type: zod.z.enum(["bar", "line"]),
data: zod.z.record(zod.z.string(), zod.z.any()),
options: zod.z.record(zod.z.string(), zod.z.any()).optional()
});
var safeJSON = (raw, parse) => {
try {
return parse(JSON.parse(raw));
} catch {
return null;
}
};
var defaultFences = {
button: ({ raw, onAction, buttonDefaults }) => {
const spec = safeJSON(
raw,
(v) => ButtonSpec.safeParse(v).success ? v : null
);
return spec ? /* @__PURE__ */ jsxRuntime.jsx(Button, { ...spec, onAction, buttonDefaults }) : /* @__PURE__ */ jsxRuntime.jsx("pre", { children: raw });
},
buttongroup: ({ raw, onAction, buttonDefaults }) => {
const spec = safeJSON(
raw,
(v) => ButtonGroupSpec.safeParse(v).success ? v : null
);
return spec ? /* @__PURE__ */ jsxRuntime.jsx(ButtonGroup, { buttons: spec, onAction, buttonDefaults }) : /* @__PURE__ */ jsxRuntime.jsx("pre", { children: raw });
},
chartjs: ({ raw, chartDefaults }) => {
const spec = safeJSON(
raw,
(v) => ChartJSSpec.safeParse(v).success ? v : null
);
return spec ? /* @__PURE__ */ jsxRuntime.jsx(ChartJSBlock, { spec, chartDefaults }) : /* @__PURE__ */ jsxRuntime.jsx("pre", { children: raw });
},
mermaid: ({ raw }) => /* @__PURE__ */ jsxRuntime.jsx(MermaidBlock, { code: raw })
};
var ChatMarkdown = ({
markdown,
fences = defaultFences,
onAction,
showUnknown = true,
buttonDefaults,
chartDefaults
}) => {
return /* @__PURE__ */ jsxRuntime.jsx(
ReactMarkdown__default.default,
{
remarkPlugins: [remarkGfm__default.default],
components: {
code({ inline, className, children, ...props }) {
if (inline) {
return /* @__PURE__ */ jsxRuntime.jsx(
"code",
{
className,
style: {
backgroundColor: "#f1f5f9",
padding: "2px 4px",
borderRadius: "4px",
fontSize: "0.875em",
fontFamily: "ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace"
},
...props,
children
}
);
}
const lang = (className || "").replace("language-", "").trim().toLowerCase();
const raw = String(children ?? "").trim();
const render = fences[lang];
if (render) {
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { margin: "8px 0" }, children: render({ raw, onAction, buttonDefaults, chartDefaults }) });
}
return showUnknown ? /* @__PURE__ */ jsxRuntime.jsx(
"pre",
{
className,
style: {
backgroundColor: "#f8fafc",
border: "1px solid #e2e8f0",
borderRadius: "8px",
padding: "16px",
overflow: "auto",
fontSize: "14px",
fontFamily: "ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace",
lineHeight: "1.5",
margin: "8px 0"
},
children: /* @__PURE__ */ jsxRuntime.jsx("code", { children: raw })
}
) : null;
}
},
children: markdown
}
);
};
exports.ButtonDefaults = ButtonDefaults;
exports.ButtonGroupSpec = ButtonGroupSpec;
exports.ButtonSpec = ButtonSpec;
exports.ChartDefaults = ChartDefaults;
exports.ChartJSSpec = ChartJSSpec;
exports.ChatMarkdown = ChatMarkdown;
exports.defaultFences = defaultFences;
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map