By: Mayne
View and copy the raw markdown of a document node
import { useState, useEffect } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
import { Button } from "@/components/ui/button";
import { RefreshCw, Copy, Check } from "lucide-react";
export default function MarkdownHighlighter(props) {
const [markdownContent, setMarkdownContent] = useState("");
const [isRefreshing, setIsRefreshing] = useState(false);
const [copyStatus, setCopyStatus] = useState("idle"); // idle, copied
const fetchMarkdown = async () => {
setIsRefreshing(true);
try {
const nodeId = props.__context__?.currentNode?.id;
const nodeType = props.__context__?.currentNode?.type;
if (nodeType === "doc") {
const markdownContent = await eidos.currentSpace.getDocMarkdown(nodeId);
setMarkdownContent(markdownContent);
} else {
setMarkdownContent('');
}
} catch (error) {
console.error("Failed to fetch markdown content:", error);
} finally {
setIsRefreshing(false);
}
};
const copyToClipboard = () => {
navigator.clipboard.writeText(markdownContent).then(() => {
setCopyStatus("copied");
setTimeout(() => setCopyStatus("idle"), 2000);
}).catch(err => {
console.error("Failed to copy: ", err);
setCopyStatus("idle");
});
};
useEffect(() => {
fetchMarkdown();
}, [props.__context__?.currentNode]);
// Function to highlight Markdown syntax in plain text with different colors
const highlightMarkdownSyntax = (text) => {
const parts = [];
let lastIndex = 0;
const syntaxPatterns = [
{ pattern: /(#{1,6}\s+.*?(?=\n|$))/g, className: "text-blue-600 dark:text-blue-400" },
{ pattern: /(\*\*.*?\*\*)/g, className: "text-green-600 dark:text-green-400" },
{ pattern: /(\*.*?\*)/g, className: "text-purple-600 dark:text-purple-400" },
{ pattern: /(~~.*?~~)/g, className: "text-red-500 dark:text-red-400" },
{ pattern: /(`.*?`)/g, className: "text-orange-600 dark:text-orange-400" },
{ pattern: /(\[.*?\]\(.*?\))/g, className: "text-cyan-600 dark:text-cyan-400" },
{ pattern: /(-\s+.*?(?=\n|$))/g, className: "text-indigo-600 dark:text-indigo-400" },
{ pattern: /(>\s+.*?(?=\n|$))/g, className: "text-amber-600 dark:text-amber-400" },
];
let currentText = text;
let tempParts = [{ start: 0, end: text.length, content: text, className: "" }];
syntaxPatterns.forEach(({ pattern, className }) => {
const newParts = [];
tempParts.forEach((part) => {
if (part.className) {
newParts.push(part);
return;
}
let subIndex = part.start;
let match;
const subText = text.slice(part.start, part.end);
const regex = new RegExp(pattern.source, pattern.flags);
while ((match = regex.exec(subText)) !== null) {
if (subIndex < part.start + match.index) {
newParts.push({
start: subIndex,
end: part.start + match.index,
content: text.slice(subIndex, part.start + match.index),
className: "",
});
}
newParts.push({
start: part.start + match.index,
end: part.start + match.index + match[0].length,
content: match[0],
className,
});
subIndex = part.start + match.index + match[0].length;
}
if (subIndex < part.end) {
newParts.push({
start: subIndex,
end: part.end,
content: text.slice(subIndex, part.end),
className: "",
});
}
});
tempParts = newParts;
});
tempParts.forEach((part, index) => {
if (part.className) {
parts.push(
<span key={index} className={part.className}>
{part.content}
</span>
);
} else {
parts.push(part.content);
}
});
return parts;
};
// Function to split markdown content and highlight code blocks
const renderContent = () => {
const codeBlockRegex = /```(\w*)\n([\s\S]*?)\n```/g;
const parts = [];
let lastIndex = 0;
let match;
while ((match = codeBlockRegex.exec(markdownContent)) !== null) {
const language = match[1] || "text";
const code = match[2];
if (lastIndex < match.index) {
const plainText = markdownContent.slice(lastIndex, match.index);
parts.push(
<pre key={lastIndex} className="whitespace-pre-wrap font-mono text-sm text-muted-foreground">
{highlightMarkdownSyntax(plainText)}
</pre>
);
}
parts.push(
<SyntaxHighlighter
key={match.index}
style={vscDarkPlus}
language={language}
PreTag="div"
className="rounded-md overflow-hidden"
>
{code}
</SyntaxHighlighter>
);
lastIndex = match.index + match[0].length;
}
if (lastIndex < markdownContent.length) {
const remainingText = markdownContent.slice(lastIndex);
parts.push(
<pre key={lastIndex} className="whitespace-pre-wrap font-mono text-sm text-muted-foreground">
{highlightMarkdownSyntax(remainingText)}
</pre>
);
}
return parts;
};
const nodeType = props.__context__?.currentNode?.type;
const isDoc = nodeType === "doc";
return (
<div className="flex flex-col gap-2 p-4 max-w-4xl mx-auto">
<div className="flex items-center justify-between mb-4">
<h1 className="text-2xl font-semibold text-foreground">
{props.__context__?.currentNode?.name || "Untitled Document"}
</h1>
<div className="flex gap-2">
<Button
onClick={fetchMarkdown}
disabled={isRefreshing}
variant="outline"
size="sm"
className="border-muted hover:bg-muted/50"
>
<RefreshCw className="w-4 h-4 mr-1" />
{isRefreshing ? "Refreshing" : "Refresh"}
</Button>
<Button
onClick={copyToClipboard}
variant="outline"
size="sm"
className="border-muted hover:bg-muted/50 transition-all"
disabled={!isDoc}
>
{copyStatus === "copied" ? (
<>
<Check className="w-4 h-4 mr-1" />
Copied
</>
) : (
<>
<Copy className="w-4 h-4 mr-1" />
Copy
</>
)}
</Button>
</div>
</div>
<div className="p-4 rounded-md overflow-auto max-h-[90vh]">
{isDoc ? (
markdownContent ? (
renderContent()
) : (
<p className="text-muted-foreground text-center py-8">
No content available. Click "Refresh" to load.
</p>
)
) : (
<p className="text-muted-foreground text-center py-8">
Markdown content is only available for document nodes. Current node type is "{nodeType}".
</p>
)}
</div>
</div>
);
}