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

fleuron

v0.16.0

Published

Paged-media layout in a worker: markdown and CSS in, a display structure or PDF bytes out

Readme

fleuron

Paged-media layout in a worker: markdown and CSS in, a display structure or PDF bytes out.

fleuron is a layout engine for book-shaped documents, compiled to WebAssembly. It shapes text, breaks and hyphenates lines, fragments the result into pages, and paints the preview and the PDF from the same numbers. It touches no DOM and opens no files.

npm install fleuron

On screen

import { Preview } from 'fleuron';

const preview = await Preview.mount(document.querySelector('#book'));
await preview.setStyle(css);
await preview.setMarkdown(markdown);

preview.page = 12;
preview.zoom = 1.5;

Preview starts the worker, loads the module into it, keeps the session, fetches the fonts the book was set in, and paints a page as SVG. The encoded buffer, the worker messages and the display structure are handled internally, and all three stay exported.

fleuron-react is the same thing as a component, with no engine logic of its own.

Links

A click on a link in the preview follows it. A link to a place in the book turns to the page of that place. A link to a url opens the url in a new window.

onLink lets the host decide what a click does. The preview calls it with the link and the click event, before it follows the link. If it returns false, the page does not turn. If the host gives an onLink, the preview opens no url.

const preview = await Preview.mount(document.querySelector('#book'), {
  onLink: (link) => {
    if (link.to.kind === 'uri') {
      openTab(link.to.url);
    }
  },
});

A host that draws pages with paintPage reads page.links. Each link has one box for each line, in points, and a target. A target in the book carries the element, its box, and the index of its page in the book, which is the first of the range that fetches that page. linkAt(page, x, y) gives the link at a point in page points. The painter marks each box with a transparent rect[data-link] that takes no pointer events. paintPage(page, { links: false }) leaves the marks out.

In a worker

// fleuron.worker.js
import { createEngine } from 'fleuron';

const engine = createEngine();
self.onmessage = ({ data }) => {
  void engine.then((ready) =>
    ready.submit(data, (response, transfer) => self.postMessage(response, transfer)),
  );
};
// the host
import { Client, paintPage, styleOp } from 'fleuron';

const worker = new Worker(new URL('./fleuron.worker.js', import.meta.url), { type: 'module' });
const client = new Client({ post: (request, transfer) => worker.postMessage(request, transfer) });
worker.onmessage = ({ data }) => client.receive(data);

const output = await client.preview([
  { op: 'markdown', name: 'manuscript.md', text: markdown },
  styleOp(css),
]);
if (output !== null) {
  element.innerHTML = paintPage(output.pages[0], { fonts: output.fonts });
}

null means a later render overtook this one, so there is nothing to paint. Every render raises a generation, the worker echoes it back, and a reply that arrives behind the current one is dropped.

The package ships the worker in the shape above, so a host that wants no worker file of its own can point at fleuron/worker.

Sending what changed

The module keeps a session between calls: the content tree, the styling, and every stage between them and the page. A second render pays for the edit rather than for the book.

await client.preview([styleOp('@page { margin-bottom: 84pt }')]);
await client.preview([{ op: 'edit', name: 'ch03.md', text }]);
await client.apply([{ op: 'font', bytes }]);

A stylesheet that only moves the page box re-fragments over lines that are already broken. A keystroke in one chapter reparses that file and leaves every other section's lines alone. Font bytes cross once and stay registered. client.stages reports how many times each stage has run, which shows when a cache served.

Faces from a stylesheet

A @font-face rule gives a face a family name, a weight and a style. The following example loads a face through a rule:

const face = await fetch('/fonts/Junicode-Cond.otf');

const preview = await Preview.mount(document.querySelector('#book'), {
  fonts: { 'Junicode-Cond.otf': new Uint8Array(await face.arrayBuffer()) },
});
await preview.setStyle(`
  @font-face {
    font-family: "Junicode Cond";
    src: url("Junicode-Cond.otf");
    font-weight: 400;
    font-style: normal;
  }

  book { font-family: "Junicode Cond", serif; }
`);

The key in fonts is the string that url() holds. It does not have to be a real URL. The engine registers the face under the family, the weight and the style that the rule declares, not under the name in the file.

The file and the sheet can arrive in either order. preview.addFont(bytes, url) sends a file after the preview is mounted. In a worker of your own, the op is { op: 'font', url, bytes }. fleuron-react takes the same record as its fonts prop.

A rule whose url has no bytes gives a warning, and the text uses the next family in its font-family list.

Batch

import { decodeDisplayList, initWasm, render, renderPdf } from 'fleuron';

await initWasm();
const output = decodeDisplayList(render(markdown, css));
const pdf = renderPdf(markdown, css);

The display structure

client.preview hands back pages of text runs, rules and images, in points, origin top left. Each text run has the string it was shaped from and each glyph a byte range into it, which is what a painter needs for selection and copy-and-paste.

paintPage draws one of them as SVG. Each run becomes one <text> with an x for every character in it, so the browser places the glyphs where the engine put them instead of working out positions of its own. exportPdf writes the same pages as PDF.

The bytes underneath are postcard with a version in front of them. decodeDisplayList reads them, exported for a host that moves them around itself. Nothing about using the package requires touching them.

What the host owns

The engine reads no paths, so the host fetches the font bytes and sends them once. client.fontBytes(id) hands back the file a face was registered from, which is how a painter draws with the bundled one.

Layout never decodes an image. It places one from the size the host gives it, and the host draws the pixels.

The host starts the worker. A book-scale manuscript is hundreds of milliseconds of work, and that much time on the main thread drops interactions.

MIT or Apache-2.0.