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

flomark

v0.0.6

Published

Render markdown while it is still streaming.

Readme

flomark

Render markdown while it is still streaming.

flomark is built for LLM chat interfaces where an assistant message arrives as small text chunks. It incrementally processes an append-only markdown string, keeps completed blocks stable, and only updates the live trailing block while tokens are still arriving.

Why

Most markdown renderers assume the full document is available. In an LLM chat app, the document is constantly growing, and reparsing or rerendering the whole assistant message on every token can become wasteful.

flomark provides a small framework-agnostic streaming core with React and Solid adapters on top. The adapters share the same parser behavior while integrating with each framework's rendering model.

Design Criteria

  • Incrementally process append-only markdown as chunks arrive.
  • Use marked's lexer for markdown tokenization.
  • Keep finalized blocks stable so completed content does not rerender on every token.
  • Re-tokenize only the mutable trailing region while streaming.
  • Share one framework-agnostic core across framework adapters.
  • Provide ergonomic React and Solid APIs without splitting them into separate packages.
  • Render basic semantic HTML nodes only, with no styling included.
  • Escape raw HTML by default and block unsafe URL schemes in default renderers.

Bring your own CSS, typography system, syntax highlighter, or component overrides if you want a specific visual style.

Install

npm install flomark

Core API

Use the core parser directly when you want framework-independent control over a markdown stream.

import { StreamingMarkdownParser } from "flomark";

const parser = new StreamingMarkdownParser();

parser.write("# Hello\n\nThis is stream");
parser.write("ing markdown.");

const { frozen, pending } = parser.snapshot();

parser.end();

A snapshot contains:

  • frozen: completed markdown blocks whose identity and rendered output stay stable.
  • pending: the trailing in-progress block, if any, which may change as more text arrives.

Each block exposes the original marked token used to render it. flomark does not invent a second markdown AST.

import type { MarkdownBlock } from "flomark";

function inspect(block: MarkdownBlock) {
  console.log(block.token.type);
}

React

Use StreamingMarkdown when your app stores each assistant message as one growing string. Append each model token to message.content, then set isStreaming to false when the response finishes.

import { StreamingMarkdown } from "flomark/react";

export function AssistantMessage({ message }) {
  return (
    <article className="prose">
      <StreamingMarkdown
        content={message.content}
        isStreaming={message.isStreaming}
        resetKey={message.id}
      />
    </article>
  );
}

For direct control over chunks, use the hook and render the frozen/pending split yourself.

import { StreamingBlocks, useStreamingMarkdown } from "flomark/react";

export function AssistantMessage() {
  const { frozen, pending, write, end, reset } = useStreamingMarkdown();

  // Call write(token) as chunks arrive.
  // Call end() when the model finishes.
  // Call reset() before reusing the hook for a different message.

  return <StreamingBlocks frozen={frozen} pending={pending} />;
}

Solid

Use the Solid renderer the same way: pass a cumulative, append-only markdown string to StreamingMarkdown.

import { StreamingMarkdown } from "flomark/solid";

export function AssistantMessage(props) {
  return (
    <article class="prose">
      <StreamingMarkdown
        content={props.message.content}
        isStreaming={props.message.isStreaming}
        resetKey={props.message.id}
      />
    </article>
  );
}

Hook-based Solid usage exposes accessors for the completed blocks and pending block.

import { StreamingBlocks, useStreamingMarkdown } from "flomark/solid";

export function AssistantMessage() {
  const { frozen, pending, write, end, reset } = useStreamingMarkdown();

  // Call write(token) as chunks arrive.
  // Call end() when the model finishes.
  // Call reset() before reusing the hook for a different message.

  return <StreamingBlocks frozen={frozen()} pending={pending()} />;
}

Streaming Model

<StreamingMarkdown content> expects content to grow by appending new text. This matches most chat apps, where each model token is appended to the current assistant message.

When switching to a different message or replacing the text instead of appending to it, pass a new resetKey so flomark starts with a fresh parser instance.

<StreamingMarkdown
  content={message.content}
  isStreaming={message.streaming}
  resetKey={message.id}
/>

When isStreaming becomes false, the trailing in-progress block is finalized.

Markdown can be ambiguous while text is still arriving. A paragraph may become a list, a table may need another line, and a fenced code block may remain open until its closing fence arrives. For that reason, flomark only freezes blocks that are confidently complete; the trailing block may be re-tokenized and rerendered until the stream ends.

Custom Rendering

Pass a components map to override how individual markdown token types render.

Custom components receive the original marked token as token, along with any already-sanitized props that the default renderer would use for that node.

<StreamingMarkdown
  content={content}
  isStreaming={isStreaming}
  components={{
    code: ({ token }) => (
      <pre data-language={token.lang || undefined}>
        <code>{token.text}</code>
      </pre>
    ),
    link: ({ href, title, children, token }) => (
      <a href={href} title={title} data-token-type={token.type}>
        {children}
      </a>
    ),
  }}
/>

Component names match marked token types where practical, such as paragraph, heading, list, list_item, blockquote, code, codespan, strong, em, link, image, table, hr, and br.

Safety

Default renderers are safe for untrusted model output:

  • Raw HTML markdown is rendered as escaped text, not inserted as real HTML.
  • Dangerous href and src schemes are dropped.
  • Allowed URL forms are http:, https:, mailto:, tel:, same-page fragments, and relative URLs.
  • javascript:, data:, vbscript:, and other unknown schemes are blocked.

If you override components, keep using the sanitized props passed by flomark when possible. If you render token.raw, call dangerouslySetInnerHTML, or otherwise insert untrusted model output as real HTML, sanitize it yourself first.

Package Shape

import { StreamingMarkdownParser } from "flomark";
import { StreamingMarkdown } from "flomark/react";
import { StreamingMarkdown } from "flomark/solid";

React and Solid are optional peer dependencies. Importing one adapter should not require installing or bundling the other.