Eidos

markdown utils

By: Mayne

Install Latest (v0.0.1)

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>
  );
}

Information

Author
Mayne
Type
m_block
Latest Version
0.0.1
Last Updated
05/26/2025
Published
05/26/2025

Version History

  • v0.0.1 05/26/2025