Paged.js

Paged.js is an open-source library to paginate content in the browser. Based on the W3C specifications, it’s a sort of polyfill for Paged Media and Generated Content for Paged Media CSS modules. The development was launch as an open-source community driven initiative and it’s still experimental. The core team behind paged.js includes Adam Hyde, Julie Blanc, Fred Chasen & Julien Taquet.

pagedjs-logo

Until we have formal accessible documentation for paged.js, here is a list of links for those who would like to start using paged.js:

  • If you want to add your 5 cents worth to the discussion or just contact the team, you can jump and discuss with us on our Mattermost channel.

You can also find below the features we are supporting right now. This text is an extract from the Editoria book.

Page rules

The page rules must be set up in the @print media query.

@media print{
                        /* write the page rules here */
}

Size

The size of the pages in a book can be defined by either width and height (in inches or millimeters) or a paper size such as A5 or Letter. It must be the same for all the pages in the book and will be inferred only from the root @page.

@page {
                        size: A5;
}
# or
@page {
                        size: 140mm
200mm;
}

Margins

The margin command defines the top, bottom, left, and right areas around the page’s content.

@page {
                        margin: 1in
2in .5in
2in;
}

Names

Single pages or groups can be named, for instance as “cover” or “backmatter.” Named pages can have their own, more specific, styles and margins, and even different styles from the main rule.

@page
backmatter {
                        margin: 20mm
30mm;
                        background: yellow;
}

In HTML, these page groups are defined by adding the page name to a CSS selector.

section.backmatter {
                        page: backmatter;
}

Page selectors

Blank pages

The blank selector styles pages that have no content, e.g., pages automatically added to make sure a new chapter begins on the desired left or right page.

@page :blank {
      @top-left { content: none; }
}

First page and nth page

There are selectors for styling the first page or a specific page, targeted by its number (named n in the specification).

@page :first {
                        background: yellow;
}
@page :nth(5) {
                        margin: 2in;
}

Left and right or recto and verso

Typically, pages across a spread (a pair of pages) have symmetrical margins and are centered on the gutter. If, however, the inner margin needs to be larger or smaller, the selector to style left and right pages can make that change.

@page
:left {
                        margin-right: 2in;
}
@page
:right {
                        margin-left: 2in;
}

Margin boxes

The margins of a page are divided into sixteen named boxes, each with its own border, padding, and content area. They’re set within the @page query. A box is named based on its position: for example, @top-left, @bottom-right-corner, or @left-middle (see all rules). By default, the size is determined by the page area. Margin boxes are typically used to display running headers, running footers, page numbers, and other content more likely to be found in a book than on a website. The content of the box is governed by CSS properties.

 

To select these margin boxes and add content to them, use the following example:

@page {
  @top-center {
        content: "Moby-Dick";
  }
}

Generated content

CSS counters

css-counter is a CSS property that lets you count elements within your content. For example, you might want to add a number before each figure caption. To do so, you would reset the counter for the <body> , increment it any time a caption appears in the content, and display that number in a ::before pseudo-element.

body {
                        counter-reset: figureNumber;
}
figcaption {
                        counter-increment: figureNumber;
}
figcaption::before {
                        content: counter(figureNumber)
}

Page-based counters

To define page numbers, paged.js uses a CSS counter that gets incremented for each new page.

To insert a page number on a page or retrieve the total number of pages in a document, the W3C proposes a specific counter named page. The counters declaration must be used within a content property in the margin-boxes declaration. The following example declares the page number in the bottom-left box:

@page {
  @bottom-left {
        content: counter(page);
  }
}

You can also add a bit of text before the page number:

@page {
  @bottom-left {
                        content: "page " counter(page);
  }
}

To tally the total number of pages in your document, write this:

@page {
  @bottom-left {
                        content: counter(pages);
  }
}

Repeated elements on different pages

Named string

Named strings are used to create running headers and footers: they copy text for reuse in margin boxes.

First, the text content of the element is cloned into a named string using string-set with a custom identifier (in the code below we call it “title,” but you can name it whatever makes sense as a variable). In the following example, each time a new <h1> appears in the HTML, the content of the named string gets updated with the text of that <h1>.

h1 {  string-set: title content(text) }

Next, the string() function copies the value of a named string to the document, via the content property.

@page {
  @bottom-left {
                        content: string(title)
  }
}

Running elements

Running elements are another way to create running headers and footers. Here the content, complete with style and structure, is copied from the text, assigned a custom identifier, and placed inside a margin box. This is useful for formatted text such as a word in italics.

The element’s position is set:

.title {
                        position: running(title);
}

Then it is placed into a margin box with the element() value via the content property:

@page {
@top-center {
content: element(title)
}
}

Controlling text fragmentation with page breaks

Sometimes there is a need to define how content gets divided into pages based on markup. To do so, paged media specifications include break-before, beak-inside, and break-after properties.

break-before adds a page break before the element; break-after adds a page break after the element.

Here is the list of options:

  • break-before: page pushes the element (and the following content) to the next available page
  • break-before: right pushes the element to the next right page
  • break-before: left pushes the element to the next left page
  • break-before: recto pushes the element to the next recto page
  • break-before: verso pushes the element to the next verso page
  • break-before: avoid ensures that no page break appears between two specified elements

For example, this sequence will create a page break before each h1 element:

h1 {
                        break-before: page;
}

This code, in contrast, will push the h1 to the next right page, creating a blank page if needed:

h1 {
                        break-before: right;
}

This snippet will keep any HTML element that comes after an h1 on the same page as the h1, moving them both to the next page if necessary.

h1 {
                        break-after: avoid;
}

The last option is the break-inside property, which ensures that the element won’t be separated across multiple pages. If you want to be sure that your block quotes will never be divided, write this:

blockquote {
                        break-inside: avoid;
}

Cross-references

To build items such as an index or a table of contents, the export function has to find the pages on which the relevant elements appear inside the book. To do so, paged media specifications include a target-counter property.

For cross-references, links are used that target anchors in the book:

<p>see the <a href="#anchor-name">Title of the chapter</a></p>

Later in the book, the chapter title will appear with the anchor, set using an ID property.

<h1 id="anchor-name">title of the chapter</h1>

The target-counter property is used in ::before and ::after pseudo-elements and set into the content property. As a page counter, it can include some text:

a::after {
                        content: ", page "
target-counter(attr(href), page );
}

In the PDF, this code will be rendered as “see title of the chapter, page 12”.

Extending Paged.js

There are several ways to extend the rendering of Paged.js. Selecting the best method will depend on how the code will be called and what it needs to access.

When creating a script or library that is specifically aimed at extending the functionality of paged.js, it is best to use hooks and a handler class.

Paged.js has various points in the parsing of content, transforming of CSS, rendering, and layout of HTML that you can hook into and make changes to before further code is run.

A handler is a JavaScript class that defines functions that are called when a hook in Paged.js is ready to defer to your code. All of the core modules for support of paged media specifications and generated content are implemented as handlers. To create your own handler, you extend this same handler class.

class
MyHandler
extends
Paged.Handler
{
                            constructor(chunker, polisher, caller) {
                            super(chunker, polisher, caller);
    }
}

The handler also exposes the underlying tools for fragmenting text (Chunker) and transforming CSS (Polisher)—see below.

Within this class, you can define methods for each of the hooks, and specify when they will be run in the code. A return that is asynchronous will delay the next code using await.

class
MyHandler
extends
Paged.Handler
{
                            constructor(chunker, polisher, caller) {
                            super(chunker, polisher, caller);
    }
    afterPageLayout(pageFragment, page, breakToken) {
                            console.log(pageFragment, page, breakToken);
    }
}

Paged.js contains the following asynchronous hooks:

Chunker

  • beforeParsed(content runs on content before it is parsed and given IDs
  • afterParsed(parsed) runs after the content has been parsed but before rendering has started
  • beforePageLayout(page) runs when a new page has been created
  • afterPageLayout(pageElement, page, breakToken) runs after a single page has gone through layout, and allows adjusting the breakToken
  • afterRendered(pages) runs after all pages have finished rendering

Polisher

  • beforeTreeParse(text, sheet) runs on the text of the style sheet
  • onUrl(urlNode) runs any time a CSS URL is parsed.
  • onAtPage(atPageNode) runs any time a CSS @page is parsed
  • onRule(ruleNode) runs any time a CSS rule is parsed
  • onDeclaration(declarationNode, ruleNode) runs any time a CSS declaration is parsed
  • onContent(contentNode, declarationNode, ruleNode) runs any time a CSS content declaration is parsed

Finally, the new handler needs to be registed in order to be used.

Paged.registerHandlers(MyHandler);

This can be registered anytime before the preview has started and will persist through any instances of Paged.Previewer that are created.

If a JavaScript library, such as MathJax, needs access to the content before it is paginated, you can delay pagination until that script has completed its work. This will give the library full access to the content of the book but has the disadvantage of needing to render the entire book before rendering each page, which can cause a significant delay.

Given that the polyfill will remove the page contents as soon as possible, adding a window.PagedConfig will allow you to pass a Promise that will delay until it is resolved.

let promise = new
Promise((resolve, reject) {
  someLongTask(resolve);
});
window.PagedConfig = {
                            before: () => {
                            return promise;
  }
};

It is also possible to delay rendering of the polyfill until called by passing auto: false.

window.PagedConfig = {
                            auto: false
};
window.PagedPolyfill.preview();

When the Previewer class is used directly, the preview() method can be called at any point that is appropriate.