Skip to content

@nextgensoftwares/folio-*  ·  pre-1.0  ·  Apache-2.0

A document editor
with real pages.

Folio is an editor and layout engine for apps that work with books, reports and contracts. Page breaks, headers and footers, sections and tables behave like Word, and the PDF and Word files you export match the screen.

$ pnpm add @nextgensoftwares/folio-layout @nextgensoftwares/folio-fonts
Engineering Handbook3 · Layout

3.2 Incremental pagination

Each block measures into a flow of fragments plus the places a page may end. After an edit, unchanged blocks keep their measured flows, pagination resumes at the first page the edit can reach, and stops as soon as a new page starts where an old one did.

A keystroke in a book of several thousand pages therefore touches one or two pages. Unchanged pages are returned as the same objects, so a renderer skips them by identity.

Every rule below is data on a break, so the paginator stays one linear walk and host block types inherit pagination by returning a flow.

Table 3.1 Pagination rules, expressed as breaks

RuleExpressed asPenalty
Line boundarybreak0
Widows, orphansbreak1
keepWithNextboundary1
keepLinesTogetherevery line1

break · penalty 0

41 of 248
p. 41
3 · LayoutEngineering Handbook
carry · header repeated
RuleExpressed asPenalty
pageBreakBeforeboundaryforced
Header rowscarry—
Media, mathnone∞

Rows never split between their cells' lines unless a row is taller than a page; rows that cross a rowspan avoid breaking there.

3.3 Keep rules keepWithNext

Widows, orphans, keep-with-next and keep-lines-together are penalties on breaks, not special cases in the paginator. A heading stays with the paragraph after it; a two-line remainder never strands at a page top.

Penalty 1 means avoid, not never: a keep-together paragraph taller than a page still splits, exactly as Word does it.

Rects such as cell borders and quote rules are clipped at the page edge; everything else lands on the page that holds its top.

42 of 248
p. 42

Try it: click the page and start typing.

This is the full editor, <FolioApp>, set up in five lines. UI kit

Live editor

01  /  Why Folio

Built for long,
printable documents.

Most web editors show one long scrolling page and leave printing to the browser. Folio works out every page itself, so what you see is what prints.

Read the design notes →

  1. 01

    Same pages everywhere

    Text is measured with the real font files instead of the browser, so a document breaks into the same pages on screen, in a background worker and on your server.

    layoutDocument(doc, { measurer: fontEngine, theme })
  2. 02

    Fast on long books

    Typing only re-lays out the pages it affects. A keystroke in a book of thousands of pages touches one or two of them, and the result always matches a full layout.

    layoutDocument(next, { measurer, cache, previous })
  3. 03

    Word-style page rules

    Headings stay with the paragraph after them, single lines don't get stranded at the top or bottom of a page, and table header rows repeat. These are the same settings Word uses, so imported files keep them.

    { type: 'heading', attrs: { keepWithNext: true } }
  4. 04

    Plain JSON documents

    Documents are ProseMirror-style JSON, so existing ProseMirror and Tiptap content loads as is. Undo, tables and collaboration come from ProseMirror’s proven core.

    standardSchema.extend({ nodes: { callout } })
  5. 05

    Saves you can trust

    Large documents open in chunks and save as small edits. The server checks every edit against its own copy before accepting it, so a buggy client can never corrupt a book.

    push(docId, version, steps, clientID, { checksum })

02  /  Performance

Tested on a
7,000-page book.

We benchmark against a generated book that uses every kind of content: tables, code, images, math and Arabic text. These are the current numbers.

Measured in Chrome with React in development mode. More detail in Performance.

Document7,314 pages · 35,019 blockstables, code, images, math, Arabic
Open, cold layout2.3 sreal HarfBuzz shaping · was 20.7 s
Keystroke, engine9.5 mstransaction + layout + position index · was 49 ms
Keystroke, to frame28 ms avg · 38 ms p95keydown → painted frame · was 209 ms
Enter50 msshifts every later block · was 731 ms
JS heap≈ 460 MBwas 3.4 GB
First page, via API≈ 0.1 schunked, progressive open
Warm reopen2.5 sone 12 KB request, chunks from IndexedDB
Save checksum≈ 4 msper push · 60 ms cold, warmed at start
Line breaks vs Chrome205 / 206 blocksheights within 0.01 px

03  /  Quick start

Get your first
pages on screen.

Folio comes in layers: the layout engine turns a document into pages, and the editor and React components build on top of it. Use as much as you need.

sh
pnpm add @nextgensoftwares/folio-model @nextgensoftwares/folio-layout @nextgensoftwares/folio-fonts
# editor + React view
pnpm add @nextgensoftwares/folio-editor @nextgensoftwares/folio-react react react-dom
ts
import { readFileSync } from 'node:fs';
import { FontEngine } from '@nextgensoftwares/folio-fonts';
import { layoutDocument } from '@nextgensoftwares/folio-layout';

const fonts = await FontEngine.create({ fallback: ['Noto Sans Arabic'] });
fonts.addFont({ family: 'Geist', data: readFileSync('Geist-Variable.ttf') });
fonts.addFont({ family: 'Geist Mono', data: readFileSync('GeistMono-Variable.ttf') });
fonts.addFont({ family: 'Noto Sans Arabic', data: readFileSync('NotoSansArabic-Regular.ttf') });

const layout = layoutDocument(doc, {
  measurer: fonts,
  headerFooter: { footer: { enabled: true, showPageNumber: true } },
});

for (const page of layout.pages) {
  console.log(page.number, page.blocks); // { first, last } top-level block range
}
tsx
import { FolioEditor } from '@nextgensoftwares/folio-editor';
import { LayoutCache, layoutDocument, type DocumentLayout } from '@nextgensoftwares/folio-layout';
import { standardSchema, type FolioDocument } from '@nextgensoftwares/folio-model';
import { FolioStyles, FolioToolbar, FolioView } from '@nextgensoftwares/folio-react';

const cache = new LayoutCache();
let previous: DocumentLayout | undefined;
const layout = (d: FolioDocument) =>
  (previous = layoutDocument(d, { measurer: fonts, cache, ...(previous ? { previous } : {}) }));

const editor = new FolioEditor({ schema: standardSchema, doc, layout, measurer: fonts });

export const Editor = () => (
  <>
    <FolioStyles />
    <FolioView editor={editor} metrics={(f) => fonts.metrics(f)} zoom="fit-width"
      top={<FolioToolbar editor={editor} />} />
  </>
);

04  /  Automation

Works without
a browser.

Everything runs as plain function calls in Node. Scripts, tests and AI agents can create, check and lay out documents without a headless browser.

Model
Plain JSON, { type, attrs, content, text, marks }. validate(doc, schema) returns every issue with its path and never throws.
Layout
layoutDocument(doc, opts) runs headless in Node. Pages expose blocks: { first, last }; fragments carry the document path they came from.
Determinism
Same document, theme and font bytes give the same pages. Use a fixed-advance measurer in tests for round numbers.
Edits
ProseMirror steps as JSON; docChecksum proves two copies are identical.
Index
/llms.txt lists every package and links its API reference.

Released under the Apache License 2.0. Built with Folio? We would love a link back.