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

@llui/markdown

v0.10.3

Published

LLui reactive Markdown rendering — parse to mdast, render real reactive DOM with custom per-node renderers and streaming-friendly keyed reuse

Readme

@llui/markdown

Reactive Markdown rendering for LLui.

Parses Markdown to a real mdast AST (micromark + GFM) and renders it through LLui's own authoring helpers as live reactive DOM — never an HTML string. That means:

  • Reactive — drive it with a Signal<string>; the view updates when the source changes.
  • Streaming-friendly — top-level blocks are content-hash-keyed, so a growing document (e.g. LLM output) reuses the DOM of unchanged earlier blocks and only rebuilds the changing tail.
  • Complete — full CommonMark + GFM (tables, strikethrough, task lists, autolinks, footnotes).
  • Extensible — override any node's rendering, or register custom node types via micromark/mdast extensions.
  • Safe by default — raw HTML is dropped and dangerous URL schemes (javascript:, data:, …) are neutralized unless you opt in.

Install

pnpm add @llui/markdown

@llui/dom is a peer dependency.

Usage

markdown() is a view helper — it returns a Mountable, so it composes like text() or any element helper:

import { component, div } from '@llui/dom'
import { markdown } from '@llui/markdown'
import '@llui/markdown/styles/theme.css' // optional default styling

interface State {
  source: string
}
type Msg = { type: 'edit'; value: string }

export const Doc = component<State, Msg>({
  init: () => ({ source: '# Hello\n\nThis is **live**.' }),
  update: (s, m) => (m.type === 'edit' ? { source: m.value } : s),
  // state.at('source') is a Signal<string> — the render reacts to it.
  view: ({ state }) => [div([markdown(state.at('source'))])],
})

A plain string works too (rendered once, no reactivity):

div([markdown('# Static\n\nRendered once.')])

Streaming / LLM output

Because top-level blocks are keyed by a hash of their source, feeding a growing string only rebuilds the part that changed:

// As tokens stream in, send the accumulated text:
update: (s, m) => (m.type === 'chunk' ? { source: s.source + m.text } : s)

Earlier blocks keep their exact DOM nodes; only the last (still-growing) block and any newly-completed blocks are built. No diffing of a virtual tree, no re-rendering settled content.

Custom rendering

Override any node type. Renderers receive the mdast node and a context for recursing into children:

import { markdown, defaultRenderers, type NodeRenderer } from '@llui/markdown'
import { a, h2 } from '@llui/dom'
import type { Heading } from 'mdast'

// Add anchor links to h2 headings; delegate other depths to the built-in.
const heading: NodeRenderer<Heading> = (node, ctx) => {
  if (node.depth !== 2) return defaultRenderers.heading(node, ctx)
  const text = node.children.map((c) => ('value' in c ? c.value : '')).join('')
  const id = text.toLowerCase().replace(/\s+/g, '-')
  return [h2({ id }, [a({ href: `#${id}` }, ctx.renderChildren(node))])]
}

markdown(source, { renderers: { heading } })

Overrides are merged over the built-in defaultRenderers; any type you don't override keeps its default. Export defaultRenderers if you want to delegate to the built-in for some cases.

Custom node types

Register a micromark syntax extension + matching mdast extension, then a renderer keyed by the new node type:

import { myExtension } from 'micromark-extension-mine'
import { myFromMarkdown } from 'mdast-util-mine'

markdown(source, {
  extensions: [myExtension()],
  mdastExtensions: [myFromMarkdown()],
  renderers: {
    myNode: (node, ctx) => [
      /* …build DOM… */
    ],
  },
})

Syntax highlighting

The core ships no highlighter (keeping it dependency-light). Plug one in with a custom code renderer and foreign() — the imperative-library boundary:

import { markdown, type NodeRenderer } from '@llui/markdown'
import { foreign } from '@llui/dom'
import { codeToHtml } from 'shiki'
import type { Code } from 'mdast'

const code: NodeRenderer<Code> = (node) => [
  foreign({
    tag: 'div',
    mount: ({ el }) => {
      el.className = 'shiki-host'
      codeToHtml(node.value, { lang: node.lang ?? 'text', theme: 'github-dark' }).then((html) => {
        el.innerHTML = html
      })
      return null
    },
  }),
]

markdown(source, { renderers: { code } })

The same shape works for highlight.js, Prism, CodeMirror, etc. — render synchronously into the foreign element's node, or hydrate asynchronously as above.

Security

| Concern | Default | Opt-in | | ---------------------------- | ------------------------------------------ | ---------------------------------------------------- | | Raw HTML (<div>) | Dropped | allowDangerousHtml: true → routed via unsafeHtml | | javascript: / data: URLs | Neutralized (link → text, image → dropped) | add the scheme to allowedProtocols | | Link rewriting | — | transformLink: (href, node) => string \| null |

allowedProtocols defaults to ['http', 'https', 'mailto', 'tel']. Relative URLs, query strings and #anchors are always allowed.

Options

| Option | Type | Default | | -------------------- | ----------------------------------- | --------------------- | | gfm | boolean | true | | renderers | Renderers | — | | extensions | micromark extensions | — | | mdastExtensions | mdast extensions | — | | allowDangerousHtml | boolean | false | | allowedProtocols | string[] | http/https/mailto/tel | | transformLink | (href, node) => string \| null | — | | class | string | 'markdown-body' | | keyOf | (node, index) => string \| number | content hash |

keyOf

By default each top-level block is keyed by a hash of its source — optimal for streaming. Override keyOf to give blocks stable identity across edits (e.g. for block-level transitions): each block is then wrapped in a stable div.markdown-block whose contents rebuild in place when the block's source changes.

Lower-level API

import { parseMarkdown, renderMarkdown } from '@llui/markdown'

const root = parseMarkdown('# Hi') // mdast Root
const nodes = renderMarkdown(root) // Renderable (snapshot, non-reactive)

Also exported: defaultRenderers, mergeRenderers, sanitizeUrl, resolveUrl, toKeyedBlocks, makeContext, collectDefinitions, resolveOptions.

License

MIT