summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/SimpleMarkdown.tsx
blob: 4f09644534bd00631e14461f9781e6fbf3fbd54a (plain) (tree)












































































































































































                                                                                                                              
import { useMemo } from "react";

interface SimpleMarkdownProps {
  content: string;
  className?: string;
}

/**
 * A simplistic markdown renderer that handles:
 * - Newlines (paragraphs)
 * - Bold (**text**)
 * - Inline code (`code`)
 * - Code blocks (```code```)
 * - Headers (# ## ###)
 * - Lists (- item)
 */
export function SimpleMarkdown({ content, className = "" }: SimpleMarkdownProps) {
  const rendered = useMemo(() => {
    if (!content) return null;

    // Split by code blocks first to handle them separately
    const parts = content.split(/(```[\s\S]*?```)/g);

    return parts.map((part, partIndex) => {
      // Handle code blocks
      if (part.startsWith("```") && part.endsWith("```")) {
        const code = part.slice(3, -3).replace(/^\w+\n/, ""); // Remove language hint
        return (
          <pre
            key={partIndex}
            className="bg-[#0a1525] border border-[rgba(117,170,252,0.2)] p-2 my-1 overflow-x-auto text-[#9bc3ff] text-[10px]"
          >
            <code>{code.trim()}</code>
          </pre>
        );
      }

      // Split by newlines and process each line
      const lines = part.split("\n");

      return lines.map((line, lineIndex) => {
        const key = `${partIndex}-${lineIndex}`;

        // Skip empty lines but add spacing
        if (!line.trim()) {
          return <div key={key} className="h-1" />;
        }

        // Headers
        if (line.startsWith("### ")) {
          return (
            <div key={key} className="font-bold text-white/90 mt-2 mb-1">
              {processInline(line.slice(4))}
            </div>
          );
        }
        if (line.startsWith("## ")) {
          return (
            <div key={key} className="font-bold text-white mt-2 mb-1">
              {processInline(line.slice(3))}
            </div>
          );
        }
        if (line.startsWith("# ")) {
          return (
            <div key={key} className="font-bold text-white text-sm mt-2 mb-1">
              {processInline(line.slice(2))}
            </div>
          );
        }

        // List items
        if (line.match(/^[-*]\s/)) {
          return (
            <div key={key} className="flex gap-1">
              <span className="text-[#555]">-</span>
              <span>{processInline(line.slice(2))}</span>
            </div>
          );
        }

        // Numbered list items
        if (line.match(/^\d+\.\s/)) {
          const match = line.match(/^(\d+)\.\s(.*)$/);
          if (match) {
            return (
              <div key={key} className="flex gap-1">
                <span className="text-[#555]">{match[1]}.</span>
                <span>{processInline(match[2])}</span>
              </div>
            );
          }
        }

        // Regular paragraph
        return <div key={key}>{processInline(line)}</div>;
      });
    });
  }, [content]);

  return <div className={`space-y-0.5 ${className}`}>{rendered}</div>;
}

/**
 * Process inline markdown: bold, inline code
 */
function processInline(text: string): React.ReactNode {
  if (!text) return null;

  // Split by inline code and bold patterns
  const parts: React.ReactNode[] = [];
  let remaining = text;
  let keyIndex = 0;

  while (remaining.length > 0) {
    // Check for inline code first
    const codeMatch = remaining.match(/^(.*?)`([^`]+)`(.*)$/);
    if (codeMatch) {
      if (codeMatch[1]) {
        parts.push(...processInlineBold(codeMatch[1], keyIndex++));
      }
      parts.push(
        <code
          key={`code-${keyIndex++}`}
          className="bg-[#0a1525] px-1 py-0.5 text-[#9bc3ff] text-[10px]"
        >
          {codeMatch[2]}
        </code>
      );
      remaining = codeMatch[3];
      continue;
    }

    // No more inline code, process bold in remaining text
    parts.push(...processInlineBold(remaining, keyIndex));
    break;
  }

  return parts.length === 1 ? parts[0] : parts;
}

/**
 * Process bold text (**text**)
 */
function processInlineBold(text: string, startKey: number): React.ReactNode[] {
  const parts: React.ReactNode[] = [];
  let remaining = text;
  let keyIndex = startKey;

  while (remaining.length > 0) {
    const boldMatch = remaining.match(/^(.*?)\*\*([^*]+)\*\*(.*)$/);
    if (boldMatch) {
      if (boldMatch[1]) {
        parts.push(<span key={`text-${keyIndex++}`}>{boldMatch[1]}</span>);
      }
      parts.push(
        <strong key={`bold-${keyIndex++}`} className="text-white/90">
          {boldMatch[2]}
        </strong>
      );
      remaining = boldMatch[3];
      continue;
    }

    // No more bold patterns
    if (remaining) {
      parts.push(<span key={`text-${keyIndex++}`}>{remaining}</span>);
    }
    break;
  }

  return parts;
}