npm.io
0.3.0 • Published 5d ago

@libretimes/markdown

Licence
MIT
Version
0.3.0
Deps
23
Size
61 kB
Vulns
0
Weekly
140

@libretimes/markdown

Markdown rendering pipeline for academic publishing: KaTeX, syntax highlighting (shiki), and LaTeX-style statement blocks, assembled as a set of remark/rehype plugins on top of unified.

Install

npm install @libretimes/markdown

Peer dependencies: react (>=19) and unified (>=10).

Import the stylesheet once in your app:

import "@libretimes/markdown/styles.css";
import "katex/dist/katex.min.css";

Rendering

The pipeline is asynchronous: the default rehype set includes rehype-pretty-code, whose shiki highlighter is async. So rendering can't go through react-markdown (it evaluates the tree with runSync). There are two entry points.

MarkdownRenderer (Server Component)

An async React Server Component. Use it on server-rendered pages, such as publication pages.

import { MarkdownRenderer } from "@libretimes/markdown";

export default function Page({ body }: { body: string }) {
  return <MarkdownRenderer content={body} language="en" />;
}
Prop Type Default
content string required
language Language (en/ru/fr/de/zh) "en"
className string "prose dark:prose-invert max-w-none"

The default className expects Tailwind Typography (prose). Override it for bare-element styling.

renderMarkdown (anywhere)

The async core: markdown in, sanitized HTML out. Use it when you can't render an async server component -- for example a client-side live preview, where you call it inside an effect and inject the result.

"use client";
import { useEffect, useState } from "react";
import { renderMarkdown } from "@libretimes/markdown";

function Preview({ body }: { body: string }) {
  const [html, setHtml] = useState("");
  useEffect(() => {
    let cancelled = false;
    renderMarkdown(body, { language: "en" }).then((out) => {
      if (!cancelled) setHtml(out);
    });
    return () => { cancelled = true; };
  }, [body]);

  return (
    <div
      className="prose dark:prose-invert max-w-none"
      dangerouslySetInnerHTML={{ __html: html }}
    />
  );
}

The output is sanitized by rehype-sanitize inside the pipeline, so it's safe to inject.

Plugin access

For a custom pipeline, the building blocks are exported individually: getMarkdownPlugins, getRemarkPlugins, getRehypePlugins, sanitizeSchema, the individual plugins, and the statement registry/labels. See src/index.ts.

Design

Why rendering is async, why it returns HTML instead of a React tree, and when to change that: docs/rendering.md.

Keywords