npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@affanhamid/markdown-renderer

v2.4.0

Published

Custom markdown renderer with KaTeX support

Readme

@affanhamid/markdown-renderer

A React markdown renderer built for AI-generated content. One import. Math, code, diagrams, callouts — all handled.

The Problem

If you've used react-markdown with remark-math and rehype-katex to render LLM output, you've hit these problems:

| Problem | Details | |---------|---------| | Dollar signs break everything | remark-math uses $ for math, but $ is also currency. Their singleDollarTextMath: false disables single-dollar math entirely. This package disambiguates intelligently: $20 → currency, $x + y$ → math. | | Inconsistent math delimiters | GPT, Claude, and Gemini variously output $, $$, \(…\), and \[…\]. remark-math doesn't support \(…\) or \[…\]. This package normalizes all four formats automatically. | | Too many moving parts | The standard setup: react-markdown + remark-math + remark-gfm + rehype-katex + KaTeX CSS + syntax highlighter + custom components. This package is one import. |

Features

  • Math rendering — KaTeX with automatic delimiter normalization ($, $$, \(, \[)
  • Dollar sign disambiguation — currency vs. math, with CJK/Devanagari/fullwidth support
  • Syntax highlighting — Shiki with github-light theme and copy-to-clipboard
  • Tables — GFM-style with column alignment (left, center, right)
  • Executable code blocks — optional onRunCode callback for running Python, R, etc.
  • Inline images![alt](url) works inside paragraphs, not just as standalone blocks
  • Semantic color tags{color:important}text{/color} for highlighting (important, definition, example, note, formula)
  • Callout blocks — LaTeX-style \begin{callout}{color}...\end{callout} with any Tailwind color name
  • Mermaid diagrams — fenced mermaid code blocks render as diagrams (client-side hydration with dynamic import)
  • Auto-scaling brackets($x + y$) automatically uses \left( and \right) for proper sizing
  • Prompt appendix — exported MATH_MARKDOWN_RULES_APPENDIX string to append to your LLM system prompt, steering models toward consistent delimiter usage

Installation

npm install @affanhamid/markdown-renderer

Peer dependency: react >= 18

Usage

React component

import { MarkdownRenderer } from "@affanhamid/markdown-renderer";

function ChatMessage({ content }: { content: string }) {
  return <MarkdownRenderer markdown={content} />;
}

With executable code blocks

import { MarkdownRenderer } from "@affanhamid/markdown-renderer";

function Notebook({ content }: { content: string }) {
  const handleRunCode = async (code: string, language: string) => {
    const result = await executeOnServer(code, language);
    return {
      output: result.stdout,
      error: result.stderr,
      images: result.plots, // base64 data URIs
    };
  };

  return (
    <MarkdownRenderer
      markdown={content}
      onRunCode={handleRunCode}
      executableLanguages={["python", "r"]}
    />
  );
}

Server-side HTML (no React)

import { renderMarkdownToHtml } from "@affanhamid/markdown-renderer";

const html = renderMarkdownToHtml(markdownString);

Normalize delimiters only

If you want to preprocess markdown before passing it to your own renderer:

import { normalizeMathMarkdownDelimiters } from "@affanhamid/markdown-renderer";

// Converts \(...\) -> $...$, \[...\] -> $$...$$, inline $$...$$ -> $...$
const normalized = normalizeMathMarkdownDelimiters(rawMarkdown);

Prompt engineering helper

Append this to your LLM system prompt to reduce delimiter inconsistency at the source:

import { MATH_MARKDOWN_RULES_APPENDIX } from "@affanhamid/markdown-renderer";

const systemPrompt = `You are a helpful assistant.\n\n${MATH_MARKDOWN_RULES_APPENDIX}`;

Callout blocks

Use LaTeX-style syntax with any Tailwind color name:

\begin{callout}{amber}
**Warning:** This operation is irreversible.
\end{callout}

\begin{callout}{green}
**Tip:** Use `Ctrl+S` to save quickly.
\end{callout}

Renders as styled callout boxes with border-{color}-200, bg-{color}-50, and text-{color}-900 classes.

Mermaid diagrams

Fenced code blocks with mermaid as the language render as diagrams:

```mermaid
graph LR
  A[Input] --> B[Process]
  B --> C[Output]
```

On the client, mermaid is dynamically imported and renders after hydration. For server-side rendering (renderMarkdownToHtml), a <pre> fallback is output inside a .md-mermaid container with data-mermaid-code for later hydration.

API

<MarkdownRenderer /> (default export)

| Prop | Type | Default | Description | |------|------|---------|-------------| | markdown | string | required | Markdown content to render | | className | string | undefined | CSS class applied to the wrapper <div> | | onRunCode | (code: string, language: string) => Promise<CodeExecutionResult> | undefined | Callback for executing code blocks | | executableLanguages | string[] | ["python", "r"] | Languages that get a "Run" button |

CodeExecutionResult

interface CodeExecutionResult {
  output: string;
  error?: string;
  images?: string[]; // data URIs or URLs
}

renderMarkdownToHtml(markdown: string, options?: { executableLanguages?: string[] }): string

Renders markdown to an HTML string. Works without React (server-side, emails, PDFs).

normalizeMathMarkdownDelimiters(markdown: string): string

Normalizes \(...\) to $...$ and \[...\] to $$...$$. Converts inline $$...$$ to $...$. Leaves code fences untouched.

MATH_MARKDOWN_RULES_APPENDIX: string

A plain-text string with math formatting rules to append to LLM system prompts.

Theming with Tailwind CSS

The className prop sets a class on the outer wrapper <div>, which you can use as a scoping selector for custom themes.

<MarkdownRenderer markdown={content} className="my-theme" />

This renders:

<div class="my-theme">
  <h1>...</h1>
  <p>...</p>
  <div class="md-callout ...">...</div>
  <!-- etc. -->
</div>

Creating a theme file

Create a CSS file (e.g. markdown-theme.css) that uses descendant selectors scoped to your theme class. With Tailwind, use @apply for utility-based styling:

/* markdown-theme.css */

.my-theme h1 {
  @apply text-3xl font-bold mt-8 mb-4 text-gray-900;
}

.my-theme h2 {
  @apply text-2xl font-semibold mt-6 mb-3 text-gray-800;
}

.my-theme p {
  @apply text-base leading-7 mb-4 text-gray-700;
}

.my-theme code {
  @apply bg-gray-100 text-sm px-1.5 py-0.5 rounded font-mono;
}

.my-theme pre {
  @apply rounded-lg my-4 overflow-x-auto;
}

.my-theme .md-callout {
  @apply border-l-4 border-blue-400 bg-blue-50 px-4 py-3 my-4 rounded-r-lg;
}

.my-theme table {
  @apply w-full border-collapse my-4 text-sm;
}

.my-theme th {
  @apply bg-gray-50 font-semibold text-left px-3 py-2 border border-gray-200;
}

.my-theme td {
  @apply px-3 py-2 border border-gray-200;
}

Import the theme file in your app and pass the matching class name:

import "./markdown-theme.css";

<MarkdownRenderer markdown={content} className="my-theme" />

Semantic CSS classes

The renderer outputs these classes that you can target in your theme:

| Class | Element | |-------|---------| | md-callout | Callout/admonition blocks (> [!NOTE], etc.) | | md-code-block | Executable code block wrapper | | md-code-block-header | Header bar above executable code blocks | | md-code-output | Code execution output area | | md-code-error | Code execution error output | | md-mermaid | Mermaid diagram container | | md-run-btn | "Run" button on executable code blocks |

Works with server-side rendering

renderMarkdownToHtml() produces the same HTML structure (wrapped in <div class="prose max-w-none">), so the same theme CSS applies to both client and server-rendered output. For server rendering, scope your selectors to the prose class or add a wrapper with your theme class:

const html = renderMarkdownToHtml(content);
const themed = `<div class="my-theme">${html}</div>`;

License

MIT