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

@liquefy/cascade.print

v2.0.0

Published

Cascade for print: documents laid out reactively onto a sequence of papers - paragraphs broken into lines and placed page by page, in micrometers.

Readme

cascade.print

Cascade for print: text laid out reactively onto a sequence of papers - paragraphs broken into lines and placed page by page - shown, edited at a caret, and printed. No DOM in the layout: every length is an integer in micrometers, and text is measured by whatever measurer it's given.

cascade.print knows nothing of any document model. A model says what its sections and paragraphs are - which paper a section is on, what a paragraph's text and fonts are - and cascade.print does the rest. The Cascade demo's word processor (cascade.application/demo/src/pages/wordProcessor) is one such model: Word's, with paragraph and character styles.

import { Component, observable } from "@liquefy/cascade.component";
import { Section, Paragraph, PaperSequence, contentWidth, monospaceMeasurer, paperSizes, margins, mm } from "@liquefy/cascade.print";

const font = { family: "Georgia, serif", size: 11, weight: 400, italic: false };

// A paragraph of the model: what it's made of.
class NoteParagraph extends Paragraph {
  content() {
    return { spans: [{ text: this.source.text, font }], font, spaceAfter: mm(3) };
  }
}

// A section of the model: its paper, and its paragraphs on it.
class NotesSection extends Section {
  pageFormat() {
    return { ...paperSizes.A4, margins: margins(mm(25)) };
  }
  build() {
    const format = this.pageFormat();
    return this.notes.map((note) =>
      new NoteParagraph({ key: note.id, source: note, width: contentWidth(format), format }));
  }
}

// The measurer, provided to the paragraphs as `textMeasurer`.
class Notes extends Component {
  provide() {
    return { textMeasurer: monospaceMeasurer() };
  }
  build() {
    return new NotesSection({ notes: this.notes });
  }
}

const papers = new PaperSequence();
new Notes({ notes: observable([observable({ id: "a", text: "Hello, paper." })]) }).renderOnto(papers);
papers.pages.length;   // how many papers
papers.linesOf(0);     // the lines on the first, positioned on it

Two steps per paragraph

A Paragraph is laid out in two repeaters of its own (see src/Paragraph.js):

  1. Breaking into lines - its content() measured and divided for the width it's given. Independent of where the paragraph is placed, so it reruns only when what content() read changes, or the width.
  2. Placing the lines - its render, onto the paper sequence, from where the paragraph before it left off (PaperSequence.flow), onto a new paper when one is full.

So an edit breaks one paragraph again, and the paragraphs after it are only placed again - and only until one ends where it did before.

content() returns the paragraph's text with its fonts already resolved, and how it's laid out:

{
  spans: [{ text, font }],   // font: { family, size, weight, italic }, size in points
  font,                      // the paragraph's own - the height of an empty one
  align, lineSpacing, indentLeft, indentRight, firstLineIndent, spaceBefore, spaceAfter,
}

Every placed line keeps the paragraph's source - typically the model's own paragraph object - and its range in the paragraph's text: what positions refer to.

Positions

A position is a place in a paragraph's text - { paragraph, offset, lineEnd }, paragraph being a paragraph's source - never a place on paper, so a new layout never makes it wrong (src/positions.js). Between positions and papers: hitTest() - the place in the text nearest a point on a paper - caretAt() and selectionRects() - where a place, or a stretch, of the text is on the papers - and the moves that depend on the layout: lineStart, lineEnd, lineAbove, lineBelow.

Measurers

A measurer answers measure(text, font) (a width) and metrics(font) ({ ascent, descent }), in µm. monospaceMeasurer() needs no fonts, for tests and anywhere without a browser.

In the browser

@liquefy/cascade.print/dom (needs @liquefy/cascade.dom):

  • domMeasurer() - measures text on a canvas; the DOM as a measuring device only. A font finishing loading makes every paragraph break its lines again.
  • paperSequenceView({ sequence, zoom }) - the papers on screen, at their real size (µm become CSS millimeters), white with a shadow on grey. Each paper reads only its own lines.
  • paperEditor({ sequence, measurer, editing, zoom }) - the papers with a caret and a selection: click, drag, Shift, double and triple click, the arrows, Home, End, Ctrl/Cmd+A; typing, Backspace, Delete, Enter. Keyboard input goes through a hidden textarea, so input methods work. The editor knows nothing of the model: what an edit does is the model's, given as editing - insertText, deleteBackward, deleteForward, splitParagraph, deleteBetween, moveLeft, moveRight, documentStart, documentEnd, orderedRange, wordAt, paragraphAt, each taking and returning positions. For a toolbar: selection(), apply(change), and shortcuts/onShortcut for Ctrl/Cmd plus a key.
  • printPaperSequence(sequence, { title }) - the browser's print dialog, one sheet per paper at its own size, printed from a hidden document of its own so nothing of the app around the papers gets onto paper.

The layout and the view are two roots: a model's sections rendered onto a PaperSequence, and a view rendering that sequence onto the DOM (see the demo's WordProcessorPage).