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

paper-view

v2.3.0

Published

Chunks up a document into paged media flows and applies print styles

Readme

PaperView

npm version License: MIT npm downloads

A JavaScript library for paged media viewing. Fork of paged.js with enhanced support for browser, Angular, and Vue.

PaperView Screenshot


Table of Contents


Features

  • Paged Media Rendering: Chunks documents into paginated flows with print styles
  • CSS @page Support: Full support for @page rules, margins, sizes, and named pages
  • Page Breaks: break-before, break-after, break-inside with left/right/recto/verso
  • Generated Content: Running headers, string-sets, target-counters, target-text
  • Page Numbering: Configurable page numbers with multiple positions and styles
  • PDF Export: Browser and Node.js PDF generation
  • Lazy Rendering: Render only visible pages for large documents (100+ pages)
  • Web Workers: Offload layout calculations to background threads
  • Image Handling: Automatic image overflow prevention
  • Overset Detection: Automatic detection and handling of content that doesn't fit
  • CSS Level 4 Selectors: :has() and :is() for powerful page matching
  • Running Headers: Full string-set support with first, last, start, first-except keywords
  • CSS Hyphens: Automatic hyphenation with dictionaries for 6 languages (en, es, fr, de, it, pt)
  • CSS Footnotes: Full float: footnote support with ::footnote-call, ::footnote-marker, and @footnote area
  • Spread Viewer: Book view with navigation, zoom, and touch support
  • Framework Support: Works with vanilla JS, Angular, and Vue.js

Installation

npm

npm install paper-view

yarn

yarn add paper-view

CDN

<script src="https://unpkg.com/paper-view/dist/paperview.js"></script>

Quick Start

Browser

<!DOCTYPE html>
<html>
<head>
  <script src="dist/paperview.js"></script>
  <link rel="stylesheet" href="dist/css/paper.css">
</head>
<body>
  <!-- Source content (hidden) -->
  <div id="content" style="display: none;">
    <h1>My Document</h1>
    <p>Your content goes here...</p>
  </div>

  <!-- Render target -->
  <div id="render"></div>

  <script>
    const content = document.getElementById('content');
    const renderTo = document.getElementById('render');

    const previewer = new PaperView.Previewer();
    previewer.preview(content, renderTo, []).then((flow) => {
      console.log(`Rendered ${flow.total} pages in ${flow.performance}ms`);
    });
  </script>
</body>
</html>

Angular

1. Add CSS to angular.json:

{
  "styles": [
    "node_modules/paper-view/dist/css/paper.css"
  ]
}

2. Import and use in your component:

import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { Previewer } from 'paper-view';

@Component({
  selector: 'app-root',
  template: `
    <div #content style="display: none;">
      <h1>My Document</h1>
      <p>Content here...</p>
    </div>
    <div #render></div>
  `
})
export class AppComponent implements OnInit {
  @ViewChild('content', { read: ElementRef }) contentRef!: ElementRef;
  @ViewChild('render', { read: ElementRef }) renderRef!: ElementRef;

  ngOnInit() {
    const previewer = new Previewer();
    previewer.preview(
      this.contentRef.nativeElement,
      this.renderRef.nativeElement,
      []
    );
  }
}

Vue.js

<template>
  <div id="app">
    <div ref="content" style="display: none;">
      <h1>My Document</h1>
      <p>Content here...</p>
    </div>
    <div ref="render"></div>
  </div>
</template>

<script>
import { Previewer } from 'paper-view';
import 'paper-view/dist/css/paper.css';

export default {
  name: 'App',
  mounted() {
    const previewer = new Previewer();
    previewer.preview(this.$refs.content, this.$refs.render, []);
  }
};
</script>

API Reference

Previewer

The main entry point for rendering documents.

const previewer = new Previewer(options);

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | viewMode | string | 'spread' | View mode: 'single' or 'spread' | | lazyRender | boolean | false | Enable lazy rendering | | lazyRenderOptions | object | {} | Lazy rendering configuration | | useWorkers | boolean | false | Enable Web Workers for layout | | workerOptions | object | {} | Web Workers configuration |

Methods

| Method | Description | |--------|-------------| | preview(content, renderTo, stylesheets) | Render content to target element | | exportPDF(options) | Export rendered document as PDF | | scrollToPage(index) | Scroll to page by index | | scrollToPageNumber(pageNumber) | Scroll to page by number | | getLazyRenderInfo() | Get lazy rendering statistics | | getWorkerStats() | Get Web Workers statistics | | getOversetInfo() | Get overset content information | | destroy() | Clean up resources |

Events

previewer.on('rendered', (flow) => {
  console.log(`Rendered ${flow.total} pages`);
});

previewer.on('page', (page) => {
  console.log(`Page ${page.index} rendered`);
});

previewer.on('chunkingComplete', (data) => {
  console.log(`Chunked ${data.totalPages} pages`);
});

previewer.on('oversetDetected', (data) => {
  console.log(`Overset on page ${data.pageIndex}`);
});

Chunker

Chunks documents into paged media flows.

import { Chunker } from 'paper-view';

const chunker = new Chunker(content, renderTo, options);

Polisher

Processes CSS stylesheets for paged media.

import { Polisher } from 'paper-view';

const polisher = new Polisher();
await polisher.add('styles.css');

Advanced Features

Page Numbering

Add configurable page numbers to your documents.

Basic Usage

const previewer = new Previewer();
const flow = await previewer.preview(content, renderTo, stylesheets);

// Enable page numbering
flow.setPageNumberingEnabled(true);

// Update configuration
flow.updatePageNumbering({
  position: 'bottom-center',
  style: 'decimal',
  template: 'Page {current} of {total}'
});

Configuration Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | enabled | boolean | false | Enable page numbering | | position | string | 'bottom-center' | Position: top-left, top-center, top-right, bottom-left, bottom-center, bottom-right | | style | string | 'decimal' | Style: decimal, upper-roman, lower-roman, upper-alpha, lower-alpha | | start | number | 1 | Starting page number | | template | string | '{current}' | Template with {current} and {total} |

CSS Integration

@page {
  pagedjs-page-numbering: enabled;
  pagedjs-page-numbering-position: top-right;
  pagedjs-page-numbering-style: upper-roman;
}

@page :first {
  pagedjs-page-numbering: disabled;
}

PDF Export

Export documents as PDF in browser or Node.js.

Browser Export

const previewer = new Previewer();
const flow = await previewer.preview(content, renderTo, stylesheets);

await previewer.exportPDF({
  filename: 'document.pdf',
  metadata: {
    title: 'My Document',
    author: 'John Doe'
  },
  onProgress: ({ stage, progress }) => {
    console.log(`${stage}: ${progress}%`);
  }
});

Node.js Export

const { NodePDFExporter } = require('paper-view/dist/paperview.node-pdf.js');

const pdfBuffer = await NodePDFExporter.fromHTML(html, css, {
  format: 'A4',
  margin: { top: '2cm', bottom: '2cm' }
});

await NodePDFExporter.save(pdfBuffer, 'output.pdf');

Lazy Rendering

Render only visible pages for large documents.

const previewer = new Previewer({
  viewMode: 'single',
  lazyRender: true,
  lazyRenderOptions: {
    bufferPages: 3,      // Pages to render around viewport
    unloadPages: 5,      // Pages away before unloading
    enableUnload: true   // Unload pages when out of view
  }
});

const flow = await previewer.preview(content, renderTo, stylesheets);

// Get render statistics
const info = previewer.getLazyRenderInfo();
console.log(info);
// { totalPages: 150, renderedPages: 12, progress: 8, chunkingComplete: true }

Web Workers

Offload layout calculations to background threads.

const previewer = new Previewer({
  useWorkers: true,
  workerOptions: {
    workerCount: 2  // Number of workers
  }
});

// Listen for worker events
previewer.on('workersReady', (data) => {
  console.log(`${data.workerCount} workers ready`);
});

previewer.on('workerTaskComplete', (data) => {
  console.log(`Task completed in ${data.duration}ms`);
});

Image Overflow Handling

Images are automatically constrained to fit within page boundaries.

/* Applied automatically */
img {
  max-width: 100%;
  height: auto;
  object-fit: contain;
}

Images that exceed the page area are scaled down while maintaining aspect ratio. Constrained images receive the pagedjs_image_constrained class.

Overset Content Handling

PaperView automatically detects and handles content that doesn't fit within the allocated pages (overset content). When content overflows a page, new pages are created and the remaining content is re-flowed.

Automatic Handling

Overset handling is automatic and runs after the initial render:

const previewer = new Previewer();
const flow = await previewer.preview(content, renderTo, stylesheets);

// Overset content is automatically detected and handled
// New pages are added as needed

Detecting Overset Content

Check if any pages have overset content:

// Get overset information
const oversetInfo = previewer.getOversetInfo();
console.log(oversetInfo);
// {
//   hasOverset: false,
//   oversetPageCount: 0,
//   oversetPages: []
// }

Listening for Overset Events

Subscribe to overset events for debugging or custom handling:

previewer.on('oversetDetected', (data) => {
  console.log(`Overset on page ${data.pageIndex}, iteration ${data.iteration}`);
  console.log('Break token:', data.breakToken);
});

previewer.on('oversetMaxIterations', (data) => {
  console.warn(`Overset handling stopped after ${data.iterations} iterations`);
  console.warn('Some content may be truncated');
});

Manual Overset Handling

For advanced use cases, you can manually handle overset content:

const chunker = flow; // Chunker instance

// Find pages with overset content
const oversetPages = chunker.oversetPages();

if (oversetPages.length > 0) {
  console.log(`${oversetPages.length} pages have overset content`);

  // Manually trigger overset handling
  await chunker.handleOverset(chunker.source, {
    maxIterations: 100  // Custom max iterations
  });
}

Page Overset Property

Each page has an overset property that indicates if it has remaining content:

flow.pages.forEach((page, index) => {
  if (page.overset) {
    console.log(`Page ${index + 1} has overset content`);
    console.log('Remaining content starts at:', page.endToken);
  }
});

Configuration

CSS @page Rules

@page {
  size: A4;
  margin: 2cm;
}

@page :first {
  margin-top: 4cm;
}

@page :left {
  margin-left: 3cm;
}

@page :right {
  margin-right: 3cm;
}

Named Pages

@page chapter {
  size: A5;
  margin: 1.5cm;
}

.chapter {
  page: chapter;
}

Page Breaks

h1 {
  break-before: page;
}

figure {
  break-inside: avoid;
}

.chapter-end {
  break-after: right;
}

CSS Level 4 Selectors: :has() and :is()

PaperView supports CSS Level 4 :has() and :is() selectors for powerful page matching based on content.

:has() Selector

Style pages that contain specific elements:

/* Style pages that contain h1 elements */
@page:has(h1) {
  margin-top: 3cm;
  background: #f9f9f9;
}

/* Style pages with figures */
@page:has(img.figure) {
  margin: 1.5cm;
}

/* Style pages with tables */
@page:has(table) {
  size: landscape;
}

/* Combine with named pages */
@page chapter:has(h1) {
  @top-center {
    content: "Chapter Start";
  }
}

:is() Selector

Apply styles to pages matching any of multiple conditions:

/* Style both first and blank pages */
@page:is(:first, :blank) {
  margin: 3cm;
}

/* Style first, left, and right pages differently */
@page:is(:first, :left, :right) {
  bleed: 6mm;
  marks: crop cross;
}

/* Combine with named pages */
@page chapter:is(:first, :nth(1)) {
  margin-top: 5cm;
}

Specificity Order

Selectors are applied based on specificity (highest to lowest):

  1. :has() - Content-based matching (highest)
  2. :first, :blank - Position-based
  3. :is() - Multi-selector matching
  4. :nth() - Position-based
  5. Named pages (e.g., @page chapter)
  6. :left, :right - Spread position
  7. * - Default (lowest)

Browser Compatibility

The :has() selector requires browser support for :has() in matches(). For older browsers, PaperView automatically falls back to querySelector() for matching.

Running Headers and String-Sets

PaperView supports CSS Paged Media string-set for running headers and footers with advanced keyword support.

Basic Usage

/* Set a string from h1 elements */
h1 {
  string-set: chapter content();
}

/* Use it in page margins */
@page {
  @top-center {
    content: string(chapter);
  }
}

String-Set Keywords

PaperView supports all CSS Paged Media string-set keywords:

| Keyword | Description | |---------|-------------| | content() or content(first) | First value on the page (default) | | content(last) | Last value on the page | | content(start) | First value from the first page, or first value from current page | | content(first-except) | First value except on the first page (empty on first page) |

Examples

/* Use first heading on each page */
h1 {
  string-set: chapter content(first);
}

/* Use last heading on each page (useful for section endings) */
h2 {
  string-set: section content(last);
}

/* Use title from first page throughout document */
h1.title {
  string-set: doctitle content(start);
}

/* Show chapter title except on first page */
h1.chapter {
  string-set: chapter content(first-except);
}

@page {
  @top-left {
    content: string(doctitle);
  }
  @top-right {
    content: string(chapter);
  }
}

Multiple String-Sets

You can define multiple string-sets in one declaration:

h1 {
  string-set: chapter content(first), chapter-last content(last);
}

@page {
  @top-left {
    content: string(chapter);
  }
  @top-right {
    content: string(chapter-last);
  }
}

Fallback Behavior

If no matching element exists on a page, the previous page's value is used (persistent behavior), ensuring running headers remain consistent across pages without headings.

CSS Hyphens Support

PaperView provides comprehensive support for the CSS hyphens property with automatic hyphenation using language-specific dictionaries.

Basic Usage

/* Enable automatic hyphenation */
p {
  hyphens: auto;
}

/* Only hyphenate at explicit soft hyphens (&shy; or \u00AD) */
p {
  hyphens: manual;
}

/* Disable hyphenation */
p {
  hyphens: none;
}

Hyphens Modes

| Value | Description | |-------|-------------| | auto | Automatic hyphenation using language dictionaries | | manual | Only break at soft hyphens (&shy; or \u00AD) | | none | No hyphenation |

Hyphenation Properties

PaperView supports all CSS hyphenation-related properties:

/* Custom hyphen character */
body {
  hyphenate-character: "\2010";  /* Hyphen bullet: ‐ */
}

/* Minimum characters before/after hyphen and minimum word length */
body {
  hyphenate-limit-chars: 3 2 6;  /* before after total */
}

/* Maximum consecutive hyphenated lines */
body {
  hyphenate-limit-lines: 2;
}

/* Unsqueezed space threshold to trigger hyphenation */
body {
  hyphenate-limit-zone: 20%;
}

Language Support

Hyphenation uses the lang attribute on elements to select the appropriate dictionary:

<article lang="en">
  <p>This text will be hyphenated using English rules.</p>
</article>

<article lang="es">
  <p>Este texto se separará usando reglas en español.</p>
</article>

<article lang="de">
  <p>Dieser Text wird nach deutschen Regeln getrennt.</p>
</article>

Supported languages: English (en), Spanish (es), French (fr), German (de), Italian (it), Portuguese (pt)

How It Works

The hyphenation engine uses the Knuth-Liang algorithm, which matches word patterns against language-specific dictionaries to identify valid syllable boundaries. The system includes:

  1. Pattern-based hyphenation: Compact pattern dictionaries for each language
  2. Exception words: Hundreds of common words with predefined hyphenation points
  3. Browser API fallback: Uses Intl.Segmenter when available for better performance
  4. Result caching: Hyphenated words are cached to avoid redundant calculations

Example

See examples/hyphens-example.html for an interactive demo showing different hyphenation modes, languages, and configuration options.

CSS Footnotes

PaperView implements CSS Paged Media footnotes support, allowing you to create professional-quality documents with footnotes.

Basic Usage

/* Mark elements as footnotes */
.footnote {
  float: footnote;
}

/* Style the footnote area */
@page {
  @footnote {
    border-top: 1px solid #333;
    padding-top: 6pt;
    margin-top: 12pt;
    font-size: 9pt;
  }
}

/* Style footnote markers */
::footnote-call {
  color: #0066cc;
  font-size: 0.75em;
}

::footnote-marker {
  font-weight: bold;
}
<p>This is some text with a footnote.<span class="footnote">This is the footnote content.</span></p>

Float: Footnote

The float: footnote property removes the element from normal flow and places it in the footnote area at the bottom of the page. A footnote-call (superscript number) is automatically inserted at the original location.

Footnote Display

The footnote-display property controls how footnotes are rendered:

| Value | Description | |-------|-------------| | block (default) | Each footnote displayed as a block with marker | | inline | Footnotes displayed inline, one after another | | compact | Compact display minimizing vertical space |

.footnote {
  float: footnote;
  footnote-display: block;
}

Footnote Policy

The footnote-policy property determines behavior when there is insufficient space:

| Value | Description | |-------|-------------| | line (default) | Footnote may move to next page if area is full | | keep | Reference and footnote kept together on same page | | block | Same as keep, for block-level elements |

.footnote {
  float: footnote;
  footnote-policy: keep;
}

@footnote Area

The @footnote at-rule within @page allows styling the footnote area:

@page {
  @footnote {
    border-top: 1px solid #333;
    padding-top: 6pt;
    font-size: 9pt;
    line-height: 1.3;
  }
}

Pseudo-elements

PaperView supports the following footnote pseudo-elements:

  • ::footnote-call: The reference marker in the main text (superscript number)
  • ::footnote-marker: The number marker in the footnote area
::footnote-call {
  color: #0066cc;
  vertical-align: super;
  font-size: 0.75em;
}

::footnote-marker {
  font-weight: bold;
  color: #333;
}

How It Works

  1. Elements with float: footnote are identified during CSS parsing
  2. Each footnote is replaced in the text flow with a footnote-call span
  3. Original footnote content is collected and placed in the footnote area
  4. Footnotes are numbered sequentially using CSS counters
  5. The footnote area is styled according to @footnote rules

Example

See examples/footnotes-example.html for an interactive demo showing footnotes with different display modes and policies.

Spread Viewer (Book View)

PaperView includes an enhanced spread viewer that provides a book-like reading experience with navigation, zoom, and mode switching.

Basic Usage

const previewer = new PaperView.Previewer({
  viewMode: "single",
  spreadViewer: true,
  spreadViewerOptions: {
    spreadMode: "book",    // "book" or "scroll"
    zoom: 1,               // Initial zoom level
    minZoom: 0.5,          // Minimum zoom
    maxZoom: 3,            // Maximum zoom
    zoomStep: 0.25,        // Zoom increment
    enableKeyboard: true,  // Keyboard navigation
    enableTouch: true      // Touch/swipe support
  }
});

const flow = await previewer.preview(content, renderTo, stylesheets);

Features

| Feature | Description | |---------|-------------| | Book View | Side-by-side page spreads with spine effect | | Scroll View | All pages in a scrollable column | | Arrow Navigation | Click arrows or use keyboard to navigate spreads | | Zoom | Mouse wheel (Ctrl+scroll), buttons, or keyboard shortcuts | | Touch/Swipe | Swipe left/right on mobile to change spreads | | Page Indicator | Shows current spread and page numbers |

Keyboard Shortcuts

| Key | Action | |-----|--------| | / PageUp | Previous spread | | / PageDown | Next spread | | Home | First spread | | End | Last spread | | Ctrl + + | Zoom in | | Ctrl + - | Zoom out | | Ctrl + 0 | Reset zoom |

Programmatic Control

// Get the spread viewer instance
const spreadViewer = previewer.getSpreadViewer();

// Navigate
spreadViewer.nextSpread();
spreadViewer.prevSpread();
spreadViewer.goToSpread(5);

// Zoom
spreadViewer.zoomIn();
spreadViewer.zoomOut();
spreadViewer.setZoom(1.5);
spreadViewer.zoomToFit();

// Switch mode
previewer.setSpreadViewerMode("book");  // or "scroll"

// Get state
const state = spreadViewer.getState();
console.log(state);
// { currentSpread: 2, totalSpreads: 10, zoom: 1, mode: "book" }

// Listen for events
spreadViewer.on("spreadChange", (data) => {
  console.log(`Now viewing spread ${data.spread}`);
  console.log(`Pages: ${data.pages}`);
});

spreadViewer.on("zoomChange", (data) => {
  console.log(`Zoom: ${data.zoom * 100}%`);
});

Development

Install Dependencies

npm install

Build

npm run build

Development Server

Run local dev-server with livereload at http://localhost:9090/:

npm start

Run Tests

# Unit tests
npm run tests

# Integration tests
npm run specs

# Lint
npm run lint

Project Structure

paper-view/
├── src/
│   ├── index.js              # Main entry point
│   ├── paperview.js          # Core PaperView class
│   ├── chunker/              # Content chunking
│   ├── polisher/             # CSS processing
│   ├── modules/              # Feature modules
│   ├── export/               # PDF export
│   └── polyfill/             # Polyfills & Previewer
├── examples/                 # Example projects
├── tests/                    # Unit tests
├── specs/                    # Integration tests
└── dist/                     # Built bundles

Examples

Complete examples are available in the examples/ directory:

| Example | Description | |---------|-------------| | index.html | Basic browser usage | | page-numbering-example.html | Page numbering with controls | | pdf-export-example.html | PDF export with options | | pdf-export-node-example.js | Node.js PDF generation | | lazy-render-example.html | Lazy rendering for large docs | | worker-layout-example.html | Web Workers for layout | | spread-viewer-example.html | Book view with navigation & zoom | | hyphens-example.html | CSS hyphens with multiple languages | | footnotes-example.html | CSS footnotes with float: footnote |


License

MIT License