react-markdown-fences
Version:
Render interactive fence dialects (buttons, charts, diagrams) inside Markdown for React applications
494 lines (492 loc) • 14.6 kB
JavaScript
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import * as React2 from 'react';
import { jsx, jsxs } from 'react/jsx-runtime';
import { z } from 'zod';
// 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.useState(false);
const handleClick = () => {
onAction?.(action, payload);
};
return /* @__PURE__ */ 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__ */ jsx("div", { style: containerStyles, children: buttons.map((button, index) => /* @__PURE__ */ jsx(
Button,
{
...button,
onAction,
buttonDefaults
},
index
)) });
};
var Chart;
var SUPPORTED_CHART_TYPES = ["bar", "line"];
var ChartJSBlock = ({ spec, chartDefaults }) => {
const canvasRef = React2.useRef(null);
const chartRef = React2.useRef(null);
const [isLoading, setIsLoading] = React2.useState(true);
const [error, setError] = React2.useState(null);
if (!SUPPORTED_CHART_TYPES.includes(spec.type)) {
return /* @__PURE__ */ jsxs("div", { style: {
padding: "16px",
border: "1px solid #fca5a5",
borderRadius: "8px",
backgroundColor: "#fee2e2",
color: "#dc2626",
fontSize: "14px"
}, children: [
/* @__PURE__ */ jsx("strong", { children: "Chart type not supported:" }),
" '",
spec.type,
"'. Only 'bar' and 'line' charts are supported."
] });
}
React2.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__ */ 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__ */ 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__ */ jsx("div", { style: containerStyle, children: /* @__PURE__ */ jsx(
"canvas",
{
ref: canvasRef,
style: {
width: "100%",
height: "100%",
maxWidth: "100%",
maxHeight: "100%"
}
}
) });
};
var mermaidLib;
var MermaidBlock = ({ code }) => {
const id = React2.useMemo(() => "m" + Math.random().toString(36).slice(2), []);
const hostRef = React2.useRef(null);
const [isLoading, setIsLoading] = React2.useState(true);
const [error, setError] = React2.useState(null);
React2.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__ */ 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__ */ jsx("div", { style: {
padding: "16px",
border: "1px solid #e5e7eb",
borderRadius: "8px",
backgroundColor: "#f9fafb",
color: "#6b7280",
fontSize: "14px",
textAlign: "center"
}, children: "Loading diagram..." });
}
return /* @__PURE__ */ jsx(
"div",
{
ref: hostRef,
style: {
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "200px"
}
}
);
};
var ButtonSpec = z.object({
label: z.string(),
action: z.string(),
variant: z.enum(["primary", "success", "warning", "destructive"]).optional(),
size: z.enum(["xs", "sm", "md", "lg"]).optional(),
payload: z.unknown().optional()
});
var ButtonGroupSpec = z.array(ButtonSpec);
var ButtonDefaults = z.object({
backgroundColor: z.string().optional(),
color: z.string().optional(),
borderColor: z.string().optional(),
borderRadius: z.string().optional(),
fontSize: z.string().optional(),
padding: z.string().optional(),
fontWeight: z.string().optional(),
hoverBackgroundColor: z.string().optional(),
hoverColor: z.string().optional(),
hoverTransform: z.string().optional()
}).optional();
var ChartDefaults = z.object({
width: z.string().optional(),
height: z.string().optional(),
backgroundColor: z.string().optional(),
borderColor: z.string().optional(),
gridColor: z.string().optional(),
fontFamily: z.string().optional(),
fontSize: z.number().optional()
}).optional();
var ChartJSSpec = z.object({
type: z.enum(["bar", "line"]),
data: z.record(z.string(), z.any()),
options: z.record(z.string(), 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__ */ jsx(Button, { ...spec, onAction, buttonDefaults }) : /* @__PURE__ */ jsx("pre", { children: raw });
},
buttongroup: ({ raw, onAction, buttonDefaults }) => {
const spec = safeJSON(
raw,
(v) => ButtonGroupSpec.safeParse(v).success ? v : null
);
return spec ? /* @__PURE__ */ jsx(ButtonGroup, { buttons: spec, onAction, buttonDefaults }) : /* @__PURE__ */ jsx("pre", { children: raw });
},
chartjs: ({ raw, chartDefaults }) => {
const spec = safeJSON(
raw,
(v) => ChartJSSpec.safeParse(v).success ? v : null
);
return spec ? /* @__PURE__ */ jsx(ChartJSBlock, { spec, chartDefaults }) : /* @__PURE__ */ jsx("pre", { children: raw });
},
mermaid: ({ raw }) => /* @__PURE__ */ jsx(MermaidBlock, { code: raw })
};
var ChatMarkdown = ({
markdown,
fences = defaultFences,
onAction,
showUnknown = true,
buttonDefaults,
chartDefaults
}) => {
return /* @__PURE__ */ jsx(
ReactMarkdown,
{
remarkPlugins: [remarkGfm],
components: {
code({ inline, className, children, ...props }) {
if (inline) {
return /* @__PURE__ */ 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__ */ jsx("div", { style: { margin: "8px 0" }, children: render({ raw, onAction, buttonDefaults, chartDefaults }) });
}
return showUnknown ? /* @__PURE__ */ 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__ */ jsx("code", { children: raw })
}
) : null;
}
},
children: markdown
}
);
};
export { ButtonDefaults, ButtonGroupSpec, ButtonSpec, ChartDefaults, ChartJSSpec, ChatMarkdown, defaultFences };
//# sourceMappingURL=index.mjs.map
//# sourceMappingURL=index.mjs.map