streamdown

📁 omerakben/omer-akben 📅 1 day ago
0
总安装量
1
周安装量
安装命令
npx skills add https://github.com/omerakben/omer-akben --skill streamdown

Agent 安装分布

amp 1
cline 1
opencode 1
cursor 1
continue 1
kimi-cli 1

Skill 文档

Streamdown v2.x

Drop-in replacement for react-markdown optimized for AI-powered streaming. Handles incomplete Markdown gracefully during token-by-token generation.

Installation

pnpm add streamdown @streamdown/code @streamdown/mermaid @streamdown/math
# Optional: @streamdown/cjk for CJK language support

Update globals.css for Tailwind:

@source "../node_modules/streamdown/dist/*.js";

Basic Usage

import { Streamdown } from "streamdown";
import { code } from "@streamdown/code";
import { mermaid } from "@streamdown/mermaid";
import { math } from "@streamdown/math";

<Streamdown
  plugins={{ code, mermaid, math }}
  isAnimating={isStreaming}
>
  {markdownContent}
</Streamdown>

Props Reference

Prop Type Default Description
children string Markdown content to render
isAnimating boolean false Enable streaming mode (incomplete markdown handling)
plugins PluginConfig {} Code, mermaid, math, cjk plugins
components Components {} Custom React components for elements
controls ControlsConfig true Show/hide interactive controls
mermaid MermaidOptions Mermaid diagram configuration
caret "block" | "circle" Streaming cursor style
className string Container CSS class
shikiTheme [BundledTheme, BundledTheme] ["github-light", "github-dark"] Shiki themes [light, dark]
linkSafety LinkSafetyConfig External link confirmation modal
remend RemendOptions Incomplete markdown parsing options
parseIncompleteMarkdown boolean true Use remend for streaming

Plugins

Code Plugin (@streamdown/code)

Shiki-powered syntax highlighting with copy/download buttons.

import { code, createCodePlugin } from "@streamdown/code";

// Default
plugins={{ code }}

// Custom themes
const customCode = createCodePlugin({
  themes: ["one-dark-pro", "one-light"]
});
plugins={{ code: customCode }}

Math Plugin (@streamdown/math)

KaTeX rendering for LaTeX equations.

import { math, createMathPlugin } from "@streamdown/math";
import "katex/dist/katex.min.css"; // Required!

// Default
plugins={{ math }}

// Enable single $ for inline math
const customMath = createMathPlugin({
  singleDollarTextMath: true,
  errorColor: "#ff0000"
});
plugins={{ math: customMath }}

Syntax:

  • Inline: $E = mc^2$ or \\(E = mc^2\\)
  • Block: $$\sum_{i=1}^n x_i$$ or \\[...\\]

Mermaid Plugin (@streamdown/mermaid)

Interactive diagram rendering.

import { mermaid, createMermaidPlugin } from "@streamdown/mermaid";

plugins={{ mermaid }}

// With config prop
<Streamdown
  plugins={{ mermaid }}
  mermaid={{
    config: {
      theme: "base",
      themeVariables: {
        primaryColor: "#73000a",
        primaryTextColor: "#ffffff",
      }
    },
    errorComponent: MyErrorComponent
  }}
>

CJK Plugin (@streamdown/cjk)

WARNING: This project disabled CJK due to English text corruption. Only use if CJK language support is required.

import { cjk } from "@streamdown/cjk";
plugins={{ cjk }}

Custom Components

Override default HTML element rendering:

const components: Components = {
  code({ className, children, ...props }) {
    const language = className?.match(/language-(\w+)/)?.[1];
    if (language) {
      return <SyntaxHighlighter language={language}>{children}</SyntaxHighlighter>;
    }
    return <code className="inline-code" {...props}>{children}</code>;
  },

  pre({ children }) {
    return <>{children}</>; // Let code handle wrapping
  },

  a({ href, children, ...props }) {
    const isExternal = href?.startsWith("http");
    return (
      <a
        href={href}
        target={isExternal ? "_blank" : undefined}
        rel={isExternal ? "noopener noreferrer" : undefined}
        {...props}
      >
        {children}
      </a>
    );
  },

  table({ children }) {
    return (
      <div className="overflow-x-auto">
        <table className="min-w-full">{children}</table>
      </div>
    );
  }
};

<Streamdown components={components}>...</Streamdown>

Controls Configuration

// Enable all (default)
controls={true}

// Disable all
controls={false}

// Selective
controls={{
  table: true,      // Table copy/download
  code: true,       // Code copy/download
  mermaid: {
    download: true,
    copy: true,
    fullscreen: true,
    panZoom: true
  }
}}

Link Safety

Confirmation modal for external links:

<Streamdown
  linkSafety={{
    enabled: true,
    onLinkCheck: async (url) => {
      // Return true to auto-allow, false to show modal
      return url.startsWith("https://trusted-domain.com");
    },
    renderModal: ({ url, isOpen, onClose, onConfirm }) => (
      <Dialog open={isOpen} onOpenChange={onClose}>
        <DialogContent>
          <p>Open external link: {url}?</p>
          <Button onClick={onConfirm}>Continue</Button>
        </DialogContent>
      </Dialog>
    )
  }}
>

Streaming Best Practices

Error Boundary Pattern

Wrap Streamdown in an error boundary for graceful fallback:

class StreamdownErrorBoundary extends Component {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      return <pre className="whitespace-pre-wrap">{this.props.fallback}</pre>;
    }
    return this.props.children;
  }
}

<StreamdownErrorBoundary fallback={rawContent}>
  <Streamdown isAnimating={isStreaming}>{content}</Streamdown>
</StreamdownErrorBoundary>

Post-Stream Cleanup

Clean incomplete markers when streaming ends:

const processedContent = useMemo(() => {
  if (isStreaming || !content?.trim()) return content;

  let cleaned = content;
  // Remove trailing incomplete bold/italic
  cleaned = cleaned.replace(/\s*\*{1,2}\s*$/, "");
  // Balance orphaned **
  const boldMatches = cleaned.match(/\*\*/g);
  if (boldMatches && boldMatches.length % 2 !== 0) {
    cleaned = cleaned.replace(/\*\*(\s*)$/, "$1");
  }
  return cleaned;
}, [content, isStreaming]);

Lazy Loading

Reduce initial bundle size:

const StreamdownContent = lazy(() =>
  import("./streamdown-content").then((mod) => ({
    default: mod.StreamdownContent,
  }))
);

<Suspense fallback={<PlainTextFallback content={content} />}>
  <StreamdownContent content={content} isStreaming={isStreaming} />
</Suspense>

Common Issues

CJK Plugin Corrupts English Text

Symptom: Random Chinese characters like “落” appear in English text. Solution: Remove @streamdown/cjk plugin unless CJK support is required.

Mermaid Diagrams Crash

Symptom: Invalid mermaid syntax causes render error. Solution: Use error boundary with fallback + custom errorComponent prop.

Math Not Rendering

Symptom: Raw LaTeX syntax displayed. Solution: Ensure katex/dist/katex.min.css is imported.

Code Blocks Unstyled

Symptom: Code blocks have no syntax highlighting. Solution: Add @source directive in globals.css and use @streamdown/code plugin.

Streaming Markers Persist

Symptom: Trailing ** or * visible after streaming ends. Solution: Implement post-stream content cleanup (see pattern above).

Malformed Nested Bold

Symptom: Raw ** appears before keywords in bullet points, e.g., **The P-O-L-C **Framework**:. Cause: AI model tries to emphasize a keyword within already-bold text, creating invalid nested markdown. Solution: Add cleanup regex in post-stream processing:

// Fix malformed nested bold: "**prefix **keyword**:" → "**prefix keyword**:"
cleaned = cleaned.replace(/\*\*([^*]+)\s\*\*(\w+)\*\*(:?)/g, "**$1 $2**$3");

TypeScript Types

import type {
  StreamdownProps,
  Components,
  PluginConfig,
  ControlsConfig,
  MermaidOptions,
  LinkSafetyConfig,
  CodeHighlighterPlugin,
  DiagramPlugin,
  MathPlugin,
  CjkPlugin,
} from "streamdown";

import type { BundledLanguage, BundledTheme } from "shiki";

Integration with AI SDK

import { useChat } from "@ai-sdk/react";

export function Chat() {
  const { messages, status } = useChat();
  const isStreaming = status === "streaming";

  return (
    <div>
      {messages.map((message) => (
        <div key={message.id}>
          {message.parts.map((part, i) =>
            part.type === "text" ? (
              <Streamdown
                key={i}
                plugins={{ code, mermaid, math }}
                isAnimating={isStreaming && i === message.parts.length - 1}
              >
                {part.text}
              </Streamdown>
            ) : null
          )}
        </div>
      ))}
    </div>
  );
}

For project-specific implementation patterns, see components/ui/streamdown-content.tsx.