Repository navigation
The PDF is rendered by PDFreactor from the HTML export of nswow. The project supplies the CSS and a script.
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.
| 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 |
- PDFreactor lays the document out, runs the scripts and renders the modified DOM. Everything must run synchronously in
DOMContentLoadedorload.setTimeoutand Promises run too late and are effectively ignored. - Layout information (
ro.layout.getBoxDescriptions(el)) is only available inload.ro.layout.forceRelayout()lays the document out again synchronously.
-
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: nonedo not take part in the layout; replacing them does not shift anything.
-
: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 incontent, e.g. for dotted ToC leaders up to the page number. Usetarget-counter(attr(…), page)for page references. -
node.replaceWith(a, b)with several nodes does not work. Replace with one node and insert the next withafter(). - PDFreactor-specific properties use the
-ro-prefix.-ro-truncate-margin-before-break/-after-breakonly work inside@page.
- Use
@page :left/@page :rightwith margin boxes (@top-left-corner, …). - Page sizes come from
meta.pdfinsrl.config.json(margin,image-quality) and from@page(-ro-media-size, bleed, crop). Components can switch the page layout with thepdf-page-layoutproperty (see Component Properties). - Use
break-inside: avoidon table rows and on cells withrowspanto 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).
- An element with
position: running(<name>)leaves the flow and is shown withcontent: 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-headerwith__title/__subtitleitems, empty values omitted). That list is the running element. - Before deleting a component, check whether a PDF
@pagerule references it viaelement(…).
-
Render with a PDFreactor Docker container and the debug configuration (
pdf-configuration-debug.xml). WithappendLog, theconsole.logoutput 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.