Skip to content
Michael Dörer edited this page Oct 12, 2026 · 1 revision

PDF

The PDF is rendered by PDFreactor from the HTML export of nswow. The project supplies the CSS and a script.

Pipeline

src/entries/pdf.ts is the entry of the pdf build. It imports all PDF styles and runs the PDF scripts:

import '#imports/pdf.scss'
import PDFNotes from 'srl/pdf/PDFNotes.ts'
import PDFNestedContainers from 'srl/pdf/PDFNestedContainers.ts'
import PDFSetPageNumbers from 'srl/pdf/PDFSetPageNumbers.ts'

document.addEventListener('DOMContentLoaded', () => {
  new PDFNotes({ noteClass: '.srl-category-notes_group, .srl-category-notes_holding' })
  new PDFNestedContainers({ selector: '.srl-nested-container' })
  new PDFSetPageNumbers({
    tocItemClass: '.srl-pdf-toc__item:not(.srl-pdf-toc__item--blank)',
    tocItemPageNumberClass: '.srl-pdf-toc__page-number',
  })
})

The result is .output/pdf/pdf.css and pdf.js. The PDFreactor configuration references them (userStyleSheets, userScripts with beforeDocumentScripts). For customer-specific PDFs, see CLI#customer-builds--c.

Helpers in srl/pdf/

Helper Options Effect
PDFNestedContainers selector (default .srl-nested-container) Adds <class>-first / <class>-last to each container, based on the component class of its first/last child (layout wrappers such as srl-grid are skipped). The srl.spacer-component-margin rules use these classes in the PDF build.
PDFNotes noteClass (comma-separated selectors) Same first/last marking for note groups; marks the last note per selector with last-note.
PDFSetPageNumbers tocItemClass, tocItemPageNumberClass Copies the href of each ToC item to data-page-number of its page number element. The CSS then shows the page with content: target-counter(attr(data-page-number), page).
Awesomizr.js – PDFreactor's helper library

Script timing

  • PDFreactor lays the document out, runs the scripts and renders the modified DOM. Everything must run synchronously in DOMContentLoaded or load. setTimeout and Promises run too late and are effectively ignored.
  • Layout information (ro.layout.getBoxDescriptions(el)) is only available in load. ro.layout.forceRelayout() lays the document out again synchronously.

Performance

  • Reading layout after any DOM change triggers a full relayout, once per read. Read everything you need first (a compact snapshot: page index, left/right, lines), then change the DOM, then read again only after an explicit forceRelayout(). For page-side logic: read all sides first, then write all classes.
  • A snapshot of all boxes of a large document can exhaust PDFreactor's memory. Keep it to the data you need, e.g. tables only down to tr.
  • Moving DOM subtrees is expensive (roughly 0.3 ms per node). When splitting, move only what goes to later pages.
  • Pure DOM changes without layout reads (replacing hidden elements, adding classes) are cheap. Run them first, before any layout-dependent script.
  • Elements with display: none do not take part in the layout; replacing them does not shift anything.

CSS and DOM support

  • :has() is not supported. Set a class on the element instead (in the template or from the PDF script) and select on that.
  • leader('.') works in content, e.g. for dotted ToC leaders up to the page number. Use target-counter(attr(…), page) for page references.
  • node.replaceWith(a, b) with several nodes does not work. Replace with one node and insert the next with after().
  • PDFreactor-specific properties use the -ro- prefix. -ro-truncate-margin-before-break / -after-break only work inside @page.

Page layout

  • Use @page :left / @page :right with margin boxes (@top-left-corner, …).
  • Page sizes come from meta.pdf in srl.config.json (margin, image-quality) and from @page (-ro-media-size, bleed, crop). Components can switch the page layout with the pdf-page-layout property (see Component Properties).
  • Use break-inside: avoid on table rows and on cells with rowspan to keep spanning rows together.
  • Split tall grid elements only between lines (text) or rows (tables). Clone the wrapper path and the table colgroup/thead, and mark continuations with a class.
  • PDF-only components carry data-remove-from-web|word|xhtml="complete" (see Component Library#output-specific-markup).

Running headers

  • An element with position: running(<name>) leaves the flow and is shown with content: element(<name>) in a margin box.
  • A page shows the first running element assigned on it, otherwise the last one from earlier pages. A running element keeps its value until the next one appears, so empty values must be explicit in the content (e.g. an empty subtitle).
  • A pattern that works: keep the editable title/subtitle components hidden (display: none) and let a small script replace them, in document order, with one list element (ul.srl-pdf-header with __title / __subtitle items, empty values omitted). That list is the running element.
  • Before deleting a component, check whether a PDF @page rule references it via element(…).

Testing locally

  • Render with a PDFreactor Docker container and the debug configuration (pdf-configuration-debug.xml). With appendLog, the console.log output is appended to the PDF. Use it to find root causes instead of guessing.

  • Build only the PDF without deleting the last full build: npx srl build -t pdf --no-clean.

  • Without a licence, PDFreactor adds a watermark and an "Evaluation Version" page after page 1, which the page counter does not count. For spread checks, insert two blank pages at the start of the test export, so the info page sits between them and page 3 starts a spread. Keep a copy of the untouched export.

    <body>
      <div class="srl-pdf-pagebreak" style="height: 1pt;"></div>
      <div class="srl-pdf-pagebreak" style="height: 1pt;"></div>
      …
  • Render each experiment into its own PDF, compare spreads visually and diff the extracted text against the reference so no content is lost: gs -sDEVICE=txtwrite -o out.txt report.pdf. Large reports take minutes; run them in the background.

  • State page-side rules concretely ("content on the side opposite the page number") and confirm them before changing code. "Inner/outer" is ambiguous when the evaluation page shifts the physical page numbers.

Clone this wiki locally