Building a PDF

Added in version 0.35.0.

ubc build pdf renders your project into a single PDF — the documents your root toctree reaches, in the order it puts them, concatenated into one paginated file with an outline, internal links and page numbers.

It is ubc doing the typesetting, not Sphinx and not LaTeX: there is no conf.py, no extension loading, no Python environment and no TeX installation involved, so the PDF comes out of the same index the editor and ubc check already use — the same one ubc build html renders the site from.

Caution

ubc build pdf is in alpha.

The command works and is under active development, but the typeset output, the theming surface ([build.pdf.theme]) and the available options may still change between releases, so it is not yet recommended for production deliverables. Some constructs are not typeset yet and are drawn as labelled placeholder boxes — see What is not rendered yet for the list. Pin your ubc version if you depend on the output.

We would love your feedback.

Quickstart

Run the command from anywhere inside your project:

ubc build pdf

It discovers the closest ubproject.toml, indexes the project, and writes the PDF under that file’s directory:

Building index for: /home/me/project/ubproject.toml
Processing 3 files
3 document(s) rendered to 4 page(s); 0 placeholder(s).
PDF written to: /home/me/project/_build/pdf/index.pdf
Done in 0.07s

The count line is the one to read: how many documents went in, how many sheets came out, and how many constructs were drawn as placeholder boxes rather than typeset (What is not rendered yet). Four pages for three documents is the title page — it is on by default, and it is not counted as a document. On a terminal the run also draws its progress as it goes, one row per phase — see while it works.

The file name comes from [project] root_doc, so a project whose root document is manual writes _build/pdf/manual.pdf.

Principal options

Option

What it does

[PATH]

The project root to build. Defaults to the current directory.

-o, --output <FILE>

The file to write — not a directory, unlike ubc build html. Defaults to _build/pdf/<root_doc>.pdf under the project root; a relative path is resolved against the current directory, and missing parent directories are created.

-w, --show-warnings

Print the detail of every build and indexing warning. A build that fails the quality gate prints it anyway, so this matters mainly once the gate has been lowered (see Warnings and the quality gate).

--deny <LEVEL>

Move the failure threshold for this run: none, info, warning (the default) or error.

--max-warnings <N>

Fail if more than N warnings are found, counting both streams.

--no-cache

Index from scratch rather than reading the on-disk cache. It also re-renders every external PlantUML diagram and refreshes the shared store (see Rebuilds).

--title-page, --no-title-page

Open this run’s document with a title page, or without one. Either beats [build.pdf] title_page, so a project that has turned the cover off can still ask for one and a project that has turned it on can still ask for the plain output. Without either flag the configuration decides, and the cover is on when the configuration says nothing.

--toc, --no-toc

Print a table of contents in this run’s front matter, or leave it out. Either beats [build.pdf.toc] enabled, so a project that has turned the table off can still ask for one and a project that has left it on can still ask for the plain output. Without either flag the configuration decides, and there is a table when it says nothing.

--need-id <ID>

Include the document that holds this need, whole. Repeatable.

--need-filter <EXPR>

Include the documents that hold the needs this expression matches, whole.

--lint-extend-ignore <CODES>

Additional diagnostic codes to ignore, comma separated.

-c, --config <TOML>

A configuration override, repeatable — for example -c 'build.pdf.page_size = "letter"'.

-v / --verbose additionally prints a Timings: line — how long indexing, lowering, layout and emitting each took — which is what to quote when a build takes longer than you expected.

Run ubc build pdf --help for the complete list.

Note

--dump-layout and --debug-overlay are debugging aids, hidden from --help and carrying no stability promise: the first writes the internal page layout as text beside the PDF, the second draws every link’s hit-rectangle and every destination’s landing point onto the pages. Neither belongs in a deliverable.

Building a subset of needs

A formal deliverable is very often a filtered set of needs rather than the whole manual, so the selection is part of the command:

$ ubc build pdf --need-id REQ_1
$ ubc build pdf --need-filter 'type == "req" and status != "done"'

--need-filter takes the same Python-like expression over need fields that ubc query filter takes. --need-id is repeatable and the ids union, so --need-id A --need-id B is exactly --need-filter 'id in ["A", "B"]'. The two flags cannot be combined; use one expression instead.

Whole documents come in, not individual needs. A selected need brings the document that contains it into the PDF entire — its prose, its other needs and its pictures included — in toctree order, because a page of a deliverable that showed one need out of its section would be a different document from the one under review. The run says how far it widened:

1 document(s) rendered to 2 page(s); 0 placeholder(s).
Selection: 1 of 2 need(s) → 1 of 3 document(s) included whole.

That line is a report, not a warning: a deliberate selection is not a defect, and it must not fail your quality gate. Two things can still be reported beside it — references that pointed into a document the selection left out (they are drawn as plain text, and counted on their own line), and needs whose document no toctree reaches, which are not in the PDF at all.

Three exit codes are worth knowing: an id that names no need in the project is a usage error (2), so is an expression that will not compile; and a selection that matches nothing writes no file and exits 1.

The output

One file. Each document follows the previous one, starting on a fresh page down to the toctree’s second level; deeper documents flow on where the text left off, as a book’s subsections do. [build.pdf.pagination] documents changes which documents start a page — every one, or none (see PDF document). A document at toctree depth d sets its own title at level d and its level-n section at level d + n − 1, so a section that is level 2 on the site can be level 4 on paper — a title deeper than level 6 is set at level 6, and its sections nest from there.

Where a page may break. A page breaks where the text runs out, but not where that would leave a reader with something cut off. A heading keeps at least three lines of what follows it (heading_keep_lines, as Sphinx’s LaTeX preamble asks for), a paragraph ending in a colon keeps the block it introduces (keep_introducers), a caption stays with its picture (or with the box standing in for one), a code block’s caption with the first lines of its listing, an admonition’s title (a generic admonition’s or a topic’s too) with the first line of its body, even when that line is a table’s caption, and a need card’s header line with what follows it in the card. A paragraph, a code listing or a table splits only where at least two lines — or two rows — stay on each side of the break (orphans and widows), so a listing under four lines, or a table of three rows or fewer, is not split when it fits on one page together with everything above it that it keeps with. Each of those counts, and the colon rule, is a key of the [build.pdf.pagination] table, where the defaults above can be changed. A document that starts on a fresh page starts at the top of the text area; a document that has nothing to print takes no page of its own; and a section heading with nothing under it before the next document — a Contents section that holds only a toctree — moves to the top of that document’s first page when it fits there with what follows, is not alone on its own page, and has no rubric or centered line kept directly above it; otherwise it stays where it is. A rubric or a centered line that ends a document stays in its own document too. .. rubric:: Footnotes is not drawn on paper, as Sphinx’s LaTeX output has none: the footnotes it would title are set at the page foot. That is a rubric that is the single word Footnotes, plain or in one span of inline markup, holding no footnote or citation marker. A role inside it is read as this builder reads it before resolution — for some roles the title written, for others the raw text, for a few the text the role renders — so where Sphinx’s resolved text differs, the verdict can differ; the PDF builder’s divergence register (rust/ubc_build_pdf/design/architecture.md, item 162) lists those shapes. A MyST **Footnotes** rubric and a named link with an embedded URL reading Footnotes (the anonymous form is skipped, as in Sphinx) are not drawn either, where Sphinx draws them. A link to its :name:, to a label above it, or to an inline target that is its text, lands where it stood — or, with break-after, at the top of the page the break opens, as any label directly above a page break does — and a page-break class written on it still breaks the page there. Any other rubric above a document’s footnotes sits above them or, when it does not fit under them, at the top of that document’s next page. Where no page can keep a rule — a run of headings taller than a page, a heading and its line above a picture as tall as the page, or a short listing or table below a heading it cannot share a page with — the page breaks where the text runs out, except that a short listing or table that opens a box, a list item, a definition or a table cell below a heading, a heading’s kept line, a line ending in a colon or a need card’s header it cannot share a page with may be moved to a fresh page with it and split there. You can also ask for a break, or for a block to stay on one page — see Page breaks you write — or for every section heading down to a level to start a page (break_before_level).

The outline is the navigation. Every document and section takes a bookmark, so a reader’s PDF viewer shows the project’s tree in its sidebar. Internal :ref: and :doc: references become real link annotations that jump within the file; a reference that does not resolve is drawn as a mono chip rather than as prose, because a PDF has no address bar to recover with — except a Markdown link written with its own text, which prints that text, unlinked, while the build’s warning names the target it could not find.

The title page and the running header are on by default. The cover carries the project name (or the root document’s title), the version from [project] version, and the resolved counts of a need selection when one is active. [build.pdf.cover] adds to that — a logo, a subtitle, an author, lines of your own — and can take any of the three stock lines away. Every continuation page carries its document’s title in the top margin, in muted ink under a hairline rule — not the page a document opens on, where its own heading is already the title, so a document that fits on one page is never headed. The bottom margin carries the page number in the same muted ink, reading n of N, set as far from the text as the title above it. Set [build.pdf] title_page = false or running_header = false to turn the front matter or the running head off (see PDF document), or pass --title-page / --no-title-page to decide the cover for one run — either flag beats the key.

A table of contents is printed unless you turn it off. It takes its own sheets after the title page, listing every section heading down to three levels by default with the page it starts on, each entry a link that jumps there. [build.pdf.toc] enabled = false — or --no-toc for one run — does without it. depth and title say how deep it goes and what heading it carries, and the table survives title_page = false: it is its own sheets rather than part of the cover, so a project can have a contents page and no title page (see PDF document).

The front matter is unnumbered and the body is numbered from 1. The cover and the table of contents are front matter: they carry neither the running head nor a page number, a reader’s page box numbers them i, ii, iii …, and the body starts at 1 on the sheet after them. The page numbers a reader cites are therefore the same whether or not the front matter is there, and the PDF’s own page labels agree with the printed numbers, so “go to page 12” in a viewer lands on the page that says 12. The total beside each number counts the pages of the file you built: the front matter is not among them, so a one-page document reads 1 of 1, and an extract built with --need-id or --need-filter counts only the extract’s own pages.

Changed in version 0.36.0: The page number reads n of N where it read n, is set in the running head’s muted ink where it was body ink, and is set as far from the text as the head above it. [build.pdf.footer] center = "{page}" restores the bare number and [build.pdf.theme.colors] footer-text = "#1a1c1e" the body ink — a literal, since @name names a palette entry and fg is a role.

Changed in version 0.36.0: The body is numbered from 1 on the sheet after the whole front matter, where the offset used to be one sheet whatever the front matter took. Only one shape of project moves: one whose cover spills onto a second sheet — reachable with a large base_font_size or a long [build.pdf.cover] lines — whose first body sheet now prints 1 where it printed 2, with its PDF page labels moving to match. A project whose cover is one sheet prints exactly what it printed.

What each margin prints is yours to say.

Added in version 0.36.0.

[build.pdf.header] and [build.pdf.footer] describe the two bands, three slots apiece — left, center and right — plus the band’s type and a rule. Each slot is a line of text in which seven names, written in braces, are replaced by what the page knows: {title} (the page’s document), {section} (the most recent heading in it), {page}, {pages}, {project}, {version} and {date}. Everything else is printed as written, and {{ and }} are literal braces.

Writing the defaults out gives exactly the document you already have:

[build.pdf.header]
left = "{title}"

[build.pdf.footer]
center = "{page} of {pages}"

A more usual thing to want is the page number in the top margin beside the title, nothing in the bottom one, and no rule:

[build.pdf.header]
left = "{title}"
right = "{page} of {pages}"
size = 0.85
italic = true

[build.pdf.header.divider]
show = false

[build.pdf.footer]
center = ""

Seven things about the bands are fixed, and worth knowing before you write a slot:

  • Neither band is ever printed on a title page, whatever the slots say — free text and {project} included, and on every sheet of it when the title page runs to more than one.

  • {title} is empty on the page a document begins on, where that document’s own heading is already the title. A document that fits on one page is therefore never headed, and this holds in the footer as well as in the header.

  • A band with no printed slot draws nothing at all, its rule included — which is how you ask for a document with no page numbers.

  • Each slot is cut to a fixed share of the measure and closed with an ellipsis: one slot on its own gets the whole width, a middle slot gets a third of it, centred, and each side then gets half of what the middle and a one-size gutter on each side leave. With no middle slot the two sides share the measure less one gutter, half each. A short slot does not lend its space to a long one.

  • A band too tall for its margin is kept on the sheet rather than printed off the paper — but where a margin is much too small for its band, the band can be drawn over the lines of text nearest it — the first lines for the header, the last for the footer — and nothing reports it. Only the rule is protected: a band’s rule is left out where it would land inside the text, or across the band’s own line.

  • {date} needs SOURCE_DATE_EPOCH. There is no clock in this builder — that is what makes two builds of unchanged sources byte-identical — so with no epoch pinned the name resolves to nothing.

  • weight names a face, normal or bold, and not a numeric CSS weight: those are the two the built-in sans family carries.

running_header = false is a shorthand for emptying the header’s default slots, and it keeps working: a slot you write in [build.pdf.header] still applies on top of it — with the header’s rule under it, unless divider.show = false, since the bool empties the slots and nothing else. The colours are roles rather than keys here — header-text, header-rule, footer-text and footer-rule under [build.pdf.theme.colors], each following an existing role until you set it.

How each construct is set is a table too. [build.pdf.typography] holds one sub-table per construct — the six heading levels and what they share, every paragraph’s face, the monospaced sizes, a link’s underline, and the constructs the builder styles itself (a table head, a caption, a card title, a tab label, an admonition title, a definition term). Every size is a factor rather than a point size, and base_font_size stays the one key that scales the whole document: a block construct’s factor multiplies it, while literal, kbd, subscript and superscript multiply the size of the text they sit in. Every weight names a face rather than a number.

[build.pdf.typography.heading-1]
size = 2.2
rule = { show = true }

[build.pdf.typography.link]
underline = false

Two of those keys draw something new, and only when you ask. rule puts a hairline under a heading, in the heading-rule colour role and centred in the gap the heading already leaves before the block that follows it, so turning rules on moves no page break — and a heading with no block after it in the same container (the document, a container’s body, a list item, a table cell, a definition) draws none, having no block to be separated from. underline = false takes the rule out from under a link — the link stays clickable and stays in the link colour, so on paper it is then told apart by its colour alone, which is worth weighing for a document that may be printed in black and white. The typeface itself is two keys at the top of that table. font names the family your prose is set in — the cover included — and font-mono the family your code is set in, and a value is a family name as one of the font files you list under [build.pdf] fonts declares it — so selecting a family is two steps, listing the files and naming the family (in [build.pdf.typography]: a font written in [build.pdf] beside fonts is refused when the configuration is read):

[build.pdf]
fonts = [
  "fonts/BrandSans-Regular.ttf",
  "fonts/BrandSans-Bold.ttf",
  "fonts/BrandSans-Italic.ttf",
  "fonts/BrandSans-BoldItalic.ttf",
]

[build.pdf.typography]
font = "Brand Sans"

A family has to supply every face the layout asks for — regular, bold, italic and bold italic for font, regular and bold for font-mono — whether those arrive as one file or as four. A family missing one of them, or a name no file you listed declares, is refused as a whole and reported as build.pdf_font_unselected; the document is then set in the built-in family rather than in a mixture of the two, and the default --deny warning bar fails the command so that a mis-named family is caught rather than shipped. A character your family has not got still comes from the built-in faces, so coverage is unaffected; charts, gantts and diagrams follow the selection, while the running header and footer keep the built-in family and stay where they are.

Every key and its default is listed in the configuration reference, beside the HTML site’s own number for each construct.

Code blocks are syntax highlighted. A code-block, a literalinclude, a :: literal block and a fenced block are coloured from the language the block names, by a bundled collection of Sublime Text syntax definitions — around two hundred and twenty languages, the set the bat project curates, plus three ubCode writes itself: Shell Session (Sphinx’s console), Python Console (pycon, and what a doctest block is given) and Cypher. Nothing is installed and nothing is downloaded: the set travels in the ubc binary, so a document colours the same way on every machine that builds it. A parsed-literal is drawn as a listing with its markup — its substitutions expanded, its links clickable, bold in the bold monospaced face and emphasis upright — and its own text is never coloured; an inline code chip inside it keeps its colours.

A language is named the way you already name it. python, py, bash, sh, console, toml, rst, cypher and the rest resolve through ubCode’s own table, and ubc build html reads the same one — see Languages for every name you can write. The VS Code preview resolves the same table, so a name one build knows, the preview knows too. One of the table’s rows is an approximation and is worth knowing: Sphinx’s default means Python, and silently plain text if Python does not lex, and only the first half of that is reproducible here, so default is Python. text, none, plain, plaintext, raw and output are deliberately uncoloured and are not a failure. [build.highlight] aliases adds names of your own — see the configuration reference. A name nothing answers to is reported as build.highlight_language_unknown, naming the token and up to three of the places it is written, and the block is drawn in full without colours. That finding is a warning, which the default --deny warning treats as a failure — as sphinx-build -W treats Sphinx’s own misc.highlighting_failure — so a project that names a language this build has no grammar for stops building until the name is corrected or the code is silenced with ignore under [lint]. The names that are guaranteed to resolve are the website’s, which a test keeps in step; Pygments’ own names for languages the bundled collection carries were swept in once as well, but not every Pygments name has a grammar here — one that does not needs [lint] ignore, or a .. highlight:: under a name that does.

Inline code that names a language is coloured the same way. A role derived from :code: with a :language: option (.. role:: py(code) with :language: python, then :py:`x = 1`), and a Markdown code span given a language with attrs_inline (an {l=python} attribute), are coloured from the same table and the same collection as a block, inside the chip’s own background — as ubc build html colours them. The uncoloured names stay plain on a chip too, and a chip that names no language — the bare :code: role, :literal:, a double-backquoted literal — is drawn in one colour as it always was. A name nothing answers to is reported on a chip exactly as on a block: the same build.highlight_language_unknown, counted together with the blocks that name it, and a chip in a section title counted once. So a project whose only unrecognised name sits on a chip gets that warning from this builder too, as it already did from ubc build html. Three things differ from ubc build html. A diff token in a chip is drawn in its colour without the tinted row the site paints behind it, because the chip’s own background is what shows it is code; a chip inside a link — a .. contents:: entry for a title that carries one — is drawn in the link colour, as every word of a link is on paper, where the site colours its tokens; and a chip whose source runs over a line break is coloured here as one line, where the site colours it line by line, so a # comment on its first line runs to the chip’s end on paper (a Markdown code span, whose line endings are spaces on both, and a chip in a parsed-literal, cut line by line, are unaffected).

The colours are theme roles. Twenty-one of them — hl-keyword, hl-type, hl-function, hl-name, hl-built-in, hl-label, hl-attr, hl-variable, hl-number, hl-literal, hl-operator, hl-string, hl-escape, hl-comment, hl-section, hl-bullet, hl-link, and an ink and a ground for each side of a diff (hl-addition-fg and hl-addition-bg, hl-deletion-fg and hl-deletion-bg) — under [build.pdf.theme.colors], beside code-bg and code-fg and set the same way. They carry the website’s own token names and default values, so the two surfaces agree without either being configured; the site’s way to change them is to redeclare the matching --ubc-hl-* custom properties in an extra_css file. A run no rule claims keeps code-fg. A diff’s inserted and deleted rows carry a tinted ground as well as an ink, drawn behind the coloured words themselves rather than across the whole row. The website tints the same runs, to the same extent — the two surfaces agree here exactly. [build.pdf.highlight] enabled = false draws every listing in one colour, as this builder did before it could highlight.

Two things a colour scheme can ask for are not drawn on paper. Italics: a monospaced span is set upright here whatever the scheme says, because the embedded mono family has no oblique face and a code span being upright is a rule about the construct. And an underline: in this document an underline means a link, so a scheme that underlines a token would make a word look clickable. Weight IS drawn — a heading inside a Markdown listing is bold.

Token boundaries are the website’s. The site, the preview and this builder colour from the same grammars and the same class table, so a construct carries the same class on paper as on the page.

:emphasize-lines: draws a band. The named lines are drawn on a tint of the brand colour across the whole row, gutter included — the website’s own treatment, and Sphinx’s on paper — rather than in a bold face. Bold is what a keyword is now, so a bold row and a bold word would be two different things wearing one face. The band follows a line that wraps, to the end of its last fragment, and [build.pdf.highlight] enabled = false does not turn it off: emphasis is content, and colouring is styling.

Charts are vector pictures. A needpie or needbar is drawn straight into the page in the same eight-colour palette the site uses, at the size its canvas asks for and scaled down to fit where it has to be. It is not a picture of a chart: the shapes are paths and the labels are real text, so a value in a chart is selectable and searchable like any other word. With :legend: the legend follows the plot as a table, one row per slice or series, each with its colour swatch; without the flag the table is not drawn, exactly as it is not shown on the site.

A needgantt is drawn the same way, and adds three things a pie or a bar does not. Its bars are links: clicking a task in the PDF goes to that need’s card, wherever in the document it is drawn, and a bar whose need is not in this deliverable simply carries no link rather than one that leads nowhere. With :show_legend: the task table follows the plot, one row per task with its colour swatch, dates, duration and completion. And because a gantt grows taller with every row it schedules, a very tall one is scaled down far enough that its own labels stop being readable — see the readable-type finding below. :align: is honoured; :width: and :height: are not, because on paper they would change the scale the type floor is measured against.

Counts and dynamic functions print their values. A :need_count:, :ndf: or :need_func: role prints the number or the text the site prints, computed from the same index — and inside a need’s body, against that need, so a card and its needextract copy print the same. One the index cannot evaluate keeps its source as a chip and is reported as ubc build html reports it: needs.filter_invalid or needs.dynamic_functions while indexing, build.need_view_failed for a filter that fails when it runs. A function that runs but fails prints ??, silently, as the site does.

A footnote’s note is set at the foot of the page of its first mark. Where the site leaves a .. [#label] definition on the page it was written on, a bound file can put it where a reader of that page needs it: the note is drawn at the bottom of the sheet the first [#label]_ mark falls on, under a short rule, in the same smaller muted type the site sets a footnote in. Later marks for the same note are marks only — one note, drawn once, and every mark is a link to it. A note too tall for the space left runs on to the foot of the next sheet, the way a long footnote does in any book, and where several notes land on one sheet they are ordered by their marks.

A mark written in a repeated table header owes its note on the sheet the header first draws; the copies of that header on the sheets the table continues onto carry the mark without repeating the note. A footnote written inside a need’s body and shown in a card is drawn inside the card, on the need’s own page and in every needextract copy alike, so a card stays a self-contained view of the need it shows. In Markdown that holds for the needextract copies only: MyST collects every footnote definition to the end of the document it is written in, so a MyST need’s own footnote has already left the card by the time the card is built — on the need’s home page its note goes to the page foot, while each needextract copy keeps its own note inside the card. And a definition nothing references stays where it was written — there is no page to send it to — which is also where std.footnote_unreferenced points the author. In Markdown “where it was written” is where MyST collects it: the end of its document, behind the transition rule, where MyST draws one.

Citations gather on a bibliography sheet at the end. Where the site leaves a .. [LABEL] definition on the page it was written on, one bound file can carry a reference list, so every definition in the deliverable is drawn as an entry on a single Bibliography sheet after the last document — with its own bookmark and its own row in the table of contents. A project that writes no citation gets no such sheet. The entries are listed in the order their definitions are written: document by document in toctree order, and top to bottom within a document. An entry is set as a footnote’s note is, its bracketed [LABEL] running in to the definition’s own text, and a [LABEL]_ reference in your prose is a link to it. A reference reaches the definition on its own page when there is one, and otherwise the same target the site links that reference to: the first target the project index captured under that label, which is not necessarily the first entry on the sheet, and — if a section or an explicit .. _target: claims the same name first — may not be a citation at all. The build says so when that happens, with std.duplicate_target. What is guaranteed is that the two builds never disagree: whatever a [LABEL]_ links to on the site, it links to here. A reference nothing defines anywhere draws its bracketed label as plain text rather than as a link that goes nowhere. An entry links back to the references written in its own document, as [LABEL] when there is one and [LABEL] (1,2) when there are several; a definition reached only from other documents carries no back-link, which is what the site does too. A citation defined inside a need’s body and shown by a needextract is drawn inside the card, as the site draws it, and the sheet lists the original once rather than every copy. On the need’s own page the card shows the reference and not the text: the definition there is the original, so it is the one that goes to the sheet. One need shown three times therefore reads two ways — twice with its citation inside the card, once pointing forward at the back matter — which is the price of gathering the entries in one place.

Two builds of unchanged sources are byte-identical. There is no wall clock in the file: the cover carries a date only when the SOURCE_DATE_EPOCH environment variable is set, which is also what makes a dated build reproducible. Fonts are embedded from a closed set carried in the binary, so the same sources produce the same document on any machine.

Page breaks you write

Added in version 0.36.0.

A page breaks where the text runs out (above), and where you ask it to. Three class names ask:

  • break-before starts the block on a new page;

  • break-after starts whatever follows the block on a new page;

  • keep-together keeps the block on one page, when one page can hold it and no footnote running on from the page before takes the room.

Any way of putting a class on a block works, and each spelling means the same:

.. rst-class:: break-before

This paragraph starts a new page.

.. container:: keep-together

   These two paragraphs stay on one page.

   Unless together they are taller than a page.

.. list-table::
   :class: break-after

   * - A table
     - after which a new page starts
{.break-before}
A paragraph that starts a new page.

:::{container} keep-together
Kept on one page.
:::

A MyST attribute block needs the MyST flavour and the attrs_block extension, and the ::: form of a container needs colon_fence too, as they do for any other class — the configuration the builder’s own page-break test project uses (tests/build_pdf_fixtures/breaks/ubproject.toml in the ubc_build_pdf crate):

[parse.parsers.md]
flavour = "myst"
extensions = ["attrs_block", "colon_fence"]

break-before and break-after are sphinx-simplepdf’s names and the CSS property names, so a source written for its PDFs breaks here in the same places, and the site prints them (below). keep-together is new here — its meaning is CSS break-inside: avoid — and sphinx-simplepdf’s dont-break is honoured as its alias. In reStructuredText — an rst-class, a :class: option or a container’s arguments — and in a MyST directive’s :class: option or container argument, break_before and break_after work too, as they do there: docutils normalises a class name, so an underscore reads as a hyphen and a trailing comma is dropped. Only a MyST attribute block is copied as written, as MyST-Parser copies it, so {.break_before} is no class this builder reads and breaks nothing (on the site it is the class break_before, which no print rule matches either).

Where the break falls. A class is on one element, and the break falls at that element’s edges: break-before before its first line, break-after after its last. A class written above a section title is the section’s, so break-after falls after the section’s last subsection, not after its heading. An rst-class with content of its own puts the class on each block of that content, so break-before there starts each of them on a new page: wrap the blocks in a container to break once, before all of them. A figure’s :class: and its :figclass: are both the figure’s here. A heading written directly above a block you break before moves to the new page with it, as a heading always keeps with what follows it — unless a footnote running on from the page before takes that page’s room, when the heading may stand alone above the note, as it can at an overflow; and unless the heading, with what it keeps, is all that is on its page: a document title keeps its first line or two, so a break directly under a title is not made, nor one under the box title, colon line or card header that follows a one-line first paragraph, and the block follows the heading there — a break on that second block itself is made. A box’s title above the first block you break before in the box, a line ending in a colon above the block it introduces, and a need card’s header above the first block of its body go the same way. break-after ends the page after the block, whatever keeps with it; break-before and a raw \newpage take a heading, a colon line, a box title or a card header above the block along. A page that would print nothing is never printed: a break before a document’s first block, a break after its last, and two breaks in a row are each one break. A page that a break opens is a continuation page: it carries the document’s running head, and a footnote still running on from the page before it continues at its foot.

Breaking before every heading of a level. [build.pdf.pagination] break_before_level is the configured twin of break-before on a section: break_before_level = 2 breaks before every level-2 section heading (levels counted as the table of contents counts them, the root document’s title being level 1), as sphinx-simplepdf breaks before every h2. It is the same break, with the same rules: one written on a section the level already reaches is still one break, a heading whose document already starts a new page is not given a second one, and a break directly under a heading with nothing else on its page is not made, so a document’s title keeps its first level-2 section on its page. The one difference is the page a break opens at a document’s title — which the level reaches when the document follows on, under documents = "none" or below the top level — that page is where the document begins, so it carries no running header. A rubric is no section and never breaks, and neither does a section where no page can end; no finding is raised for either, as the key is a setting and not a class you wrote there.

Where you may write one. Anywhere a page could already end between two lines — at the top level of a document, inside a list item, a definition, a note or another admonition, a topic, a quote, a container, or a need’s body: a box broken inside ends on this page and continues, frame and indent included, on the next. Two places cannot end a page, and a class written in one is refused: a table cell (an hlist column is one), which is laid out as one row, and the body of a footnote or a citation, which is set at a page foot or on the bibliography page. The block is drawn as if it carried no class, and the build reports build.pdf_break_unplaceable at the line of the classed block — for a container, the line of the first block inside it.

The aliases. Three more spellings are honoured, and will stay honoured, because sphinx-simplepdf styles them:

  • dont-break means keep-together;

  • break on a table — table, list-table, csv-table, a grid table under rst-class, or a needtable’s :class: — means break-before;

  • a need’s :style: break means break-before on its card (and a needextract’s :style: break before every card it shows, as sphinx-needs classes each of them). A need cannot take an rst-class, so :style: is its channel for every page-layout class: break-before, break-after, keep-together and dont-break work there too, beside the bare break. None of them paints, and a colour written beside them still paints.

Two more of its classes do nothing here, deliberately: ssp-table-wrap lets a browser engine break a word inside a table cell, which this builder already does in every table (a word too long for its cell is broken rather than drawn past the cell’s edge), so there is nothing for the class to switch on; and ssp-tiny / ssp-tinier set one table’s type smaller, where this builder sets every table at the document’s own text size (base_font_size scales it with everything else) and has no size for one table.

A kept block that does not fit is split. keep-together asks for one page, and a block taller than a page — or than a page less the heading kept above it — cannot have one. Such a block is laid out as if it carried no class, splitting where the page fills, and the build reports build.pdf_keep_together_oversized at the line of the classed block (for a container, the first line inside it). The fit is measured on an empty page foot: a kept block that one page could hold, but that opens a page whose foot a footnote from the page before still fills, is split there without a report. A block marked keep-together with a break written inside it keeps nothing: the break wins.

The one raw spelling. A .. raw:: latex block whose content is exactly \newpage, \clearpage, \pagebreak or \cleardoublepage is a page break too:

.. raw:: latex

   \newpage

It is the one spelling Sphinx writes into the LaTeX it hands to TeX, which is where a page break in a PDF built through Sphinx LaTeX comes from — for a class it writes nothing a page break follows from (a container’s class becomes an inert sphinxuseclass environment) — so a source written for that route asks for a break here in the same place. It is the only exception to raw content drawing nothing (see What is not rendered yet): any other LaTeX — a \vspace, a second command after \newpage, a \pagebreak[4] — is left out as every raw block is, and so is a block written for more than one format (.. raw:: latex html). A project that sets [parse] raw_enabled = false has no raw blocks at all, so there the spelling is a placeholder like every other raw block, and breaks nothing.

Printing the website. ubc build html styles the same classes for print, so printing a page of the site from a browser breaks where the PDF does: break-before and a table’s break before the block, break-after after it, and keep-together and dont-break not inside it. Three things print differently from the PDF: a figure’s :class: lands on its picture, where a browser does not break an inline picture (a centred figure’s picture is a block, and does) — write :figclass: to class the figure itself; a need’s :style: break does not break at all (wrap the need in a container classed break-before to break before it on both); and a class in a footnote’s body, which this builder refuses, prints a break on the site. Sphinx’s own html builders emit the classes too, and style none of them.

Landscape pages

Added in version 0.36.0.

A block too wide for the page can be printed on pages turned on their side. Two class names ask for it:

  • landscape prints the block on landscape pages;

  • portrait prints it on portrait pages — which changes something only inside a landscape section (below): a page is portrait already everywhere else.

They are written like the page-break classes, through any way of putting a class on a block, and each spelling means the same:

.. list-table::
   :class: landscape
   :header-rows: 1

   * - Identifier
     - ...

.. rst-class:: landscape

A wide section
--------------

Every block of the section is on landscape pages.

.. container:: landscape

   A paragraph and the table after it, on landscape pages together.
{.landscape}
| Identifier | Title | Status |
|---|---|---|
| REQ_1 | ... | open |

sphinx-simplepdf’s ssp-landscape and ssp-portrait are honoured as their aliases, and will stay honoured.

What a landscape page is here. It is the configured page turned: A4 landscape is 297 × 210 mm, a genuinely wider page, with the four margins keeping their meaning on the page as it is displayed, so its text area is 257 × 170 mm at the default margins (728.50 × 481.89 pt against 481.89 × 728.50 pt portrait). The running head and the page number are laid along its long edge — the head at its usual distance from the top, over the whole landscape width, and the page number at the foot of the landscape page — as sphinx-simplepdf lays its landscape pages. A viewer shows the page the right way up. A printer receives a page wider than it is tall, and the print dialog’s auto-rotate turns it onto the paper; which way it turns is the driver’s choice, so in a bound, printed copy the tops of the landscape pages may face the spine or the outer edge — the PDF does not say. (Rotated pages, which say it, need a change to the PDF library this builder uses.) Page numbers, the table of contents and the page labels count a landscape page as one page, like any other.

What goes landscape. A block — a table, a code block, a picture, a container — or a whole section, when the class is written above its title: the section’s heading, its text and its subsections are all printed at the landscape width, and the section after it is portrait again. Mark the table, not the section: a landscape line of running text holds roughly 140 characters, against 90 on a portrait page, which is more than twice the width prose reads comfortably at. A block marked landscape starts on a new page and whatever follows it starts on another, portrait again — the class lasts exactly as long as the block. A heading written directly above it moves onto the landscape page with it, as a heading keeps with what follows it anywhere (a rubric does not: it is not a section heading, and stays on the portrait page); two marked blocks in a row share the landscape pages, with no portrait page between them; a document whose first block is marked opens on a landscape page; and a block marked landscape that would fit a portrait page is printed landscape all the same: the class is honoured as written. Inside a landscape section a block marked portrait gets portrait pages of its own, and the section continues on landscape pages after it. When the lines kept together above a portrait block — the heading, a paragraph ending in a colon that no break may split, or the two lines of such a paragraph the widow rule keeps — are wider than a portrait line, they keep the block on the landscape pages with them, drawn as if it had no class: a portrait page could not hold them. When every one of them fits a portrait line, they move onto the portrait page with the block, as a heading moves onto a landscape page. A line ending in a colon is not kept with a portrait section, which opens with its own heading, nor with a block whose first line — for a picture, the picture — does not fit on a landscape page under the lines kept together above it — the heading, a paragraph ending in a colon that no break may split, or the two lines of such a paragraph the widow rule keeps — so neither stays on the landscape pages because of one. A heading directly above a block too tall to share a page with it — a picture fitted to the page’s height — is left at the end of the page before it, as it would be above such a picture anywhere. A break-before, break-after or keep-together beside landscape or portrait works as it always does, and never costs a second page break: a break-before portrait under a heading too wide for a portrait line still starts a new page, a landscape one, with the heading above the block.

Where it is refused. A landscape page holds blocks at the top level of a document only — a section, a block, or a container of them. Inside a list item, a definition, a field list, a quote, a note or another admonition, a topic, a sidebar, a figure, a need’s card, or a sphinx-design grid, card or tab, the box around the block would have to end on one page and continue on a page of another size, so the class is refused: the block is printed at the width around it, and the build reports build.pdf_landscape_refused at its line, naming the class and the place. A class naming the shape the page already has — landscape in a list inside a landscape section — changes nothing, and is not reported. In a table cell or a footnote’s body it is refused with the other page-layout classes, as build.pdf_break_unplaceable (see Page breaks you write). A landscape class is refused as well where the configured margins leave a landscape page a text area under 50 mm tall — on A4, a top and a bottom margin that together exceed 160 mm — because 50 mm is the smallest text area the configuration accepts on a portrait page: the block is printed portrait, and build.pdf_landscape_refused names the margins.

Tables too wide for a portrait page. Every column needs its two cell paddings and one em of text — 18 pt at the default size, so a portrait page holds 26 columns and a landscape one 40. A table too wide for a portrait page but not for a landscape one — a needtable as well — is printed on a landscape page, without a class and without a report: the table is drawn, and its page is the record. Past that it is a placeholder, reported as build.pdf_table_too_wide naming the widest width it could have had. Mark such a table landscape yourself to see which of the two it is: the source then says where the table goes, whatever its column count, and a finding for it names the landscape width it was measured against. A table marked portrait is never turned — past the portrait width it is a placeholder — and neither is a table nested where no page can turn — in a list, a note, a card — nor one at margins that leave a landscape page with a text area under 50 mm tall, the smallest the configuration accepts on a portrait page. A table marked portrait is tested at the portrait width when it is read, before the page is laid out, so one that the lines above it keep on the landscape pages, and that was too wide, stays a placeholder.

Footnotes whose mark is on a landscape page are set at its foot at the landscape width, under a rule 0.4 of that width — all but a note whose mark is in a line carried onto the landscape page with a marked block, such as a title directly above a marked table: that note keeps the portrait width, narrower than the foot, never wider. A note whose mark is in a heading that moves onto a portrait page with a portrait block is set at the portrait width. A table in a footnote’s body is tested at the portrait width wherever the mark is, because the page the note lands on is not known when the body is read: a note whose mark is on a landscape page, holding a table the landscape foot could hold, shows that table as a placeholder. A note still running on at the end of a landscape block finishes on landscape pages before the portrait page opens: a note never continues on a page of the other shape.

Pictures are fitted into the landscape text area, which is WIDER and SHORTER than a portrait one: a wide screenshot is printed larger, and a tall one — a long gantt chart, a portrait photograph — smaller, because it is fitted to the landscape page’s height. A picture that shrinks on a landscape page is better left portrait.

Printing the website. ubc build html styles landscape and ssp-landscape for print as a named landscape page, so printing a page of the site from a browser puts the marked block on landscape pages too, for exactly the block’s extent.

In Sphinx and sphinx-simplepdf. Sphinx’s builders emit the class and turn nothing: the html builders style no landscape class, and the LaTeX writer writes nothing for a table’s or a section’s class. A container’s class becomes a sphinxuseclass environment, so a LaTeX project should be able to print .. container:: landscape on turned pages itself, with two preamble lines (not tested here: no TeX toolchain runs in this project’s checks):

\usepackage{pdflscape}
\newenvironment{sphinxclasslandscape}{\begin{landscape}}{\end{landscape}}

sphinx-simplepdf honours ssp-landscape as a named landscape page, and there the landscape pages go on after the marked block until one fills, unless the block is also classed break_after: sources written for it that mark only the block print the same block landscape here, and return to portrait right after it. Its ssp-portrait breaks the page before the classed block there, and the portrait pages then go on the same way; in a portrait document it changes nothing here.

What is not rendered yet

A construct the builder cannot typeset yet is drawn as a labelled placeholder box — or, inline, as a mono chip, its text cut at 48 characters — and counted on a build.pdf_* warning naming it. Nothing an author wrote is dropped silently, with the exceptions named in this section and .. rubric:: Footnotes (see The output, above): a document that omits something without saying so looks complete, which is worse than one that is visibly incomplete. Each finding names the file the construct is written in — for an .. include::``d fragment, the fragment, with the including document beside it as an ``included-from location, and for a card shown by a needextract, the need’s own document — which is the same rule ubc check and ubc build html follow, and the one a per-file-ignores entry is matched against.

At this release those are:

  • maths, both .. math:: blocks and inline :math: — drawn as its TeX source in the mono face and counted on build.pdf_math_unrendered, which is readable to the audience that writes it;

  • video and download — neither has a paper form (a video’s :caption: is drawn under its box);

  • a diagram no external PlantUML executable draws, on build.pdf_diagram_unrendered: every Mermaid diagram, and a needuml, needarch, uml, plantuml, needflow or needsequence diagram in a project that has not configured the executable (see the shared PlantUML table). With it configured, the first four are drawn, and a needflow or needsequence is too once its own key routes it there ([build.plantuml] needflow / needsequence, or a needflow’s :engine: plantuml); what is left is what no external engine here produces — a mermaid diagram, and a needs view drawn with ubCode’s own Mermaid. A needflow or needsequence box carries its caption and no source: the diagram is generated, not written;

  • todolist;

  • a table with more columns than a page can hold — a needtable too — on build.pdf_table_too_wide naming the columns, the width they need and the width there was. Every column needs its two cell paddings and one em of text, so at the default size a portrait page holds 26 columns and a landscape one 40. A top-level table too wide for a portrait page but not for a landscape one is not a placeholder and not a finding: it is printed on a landscape page (see Landscape pages), and the finding for one too wide even for that names the landscape width. A table nested in a list, a note, a card or a footnote’s body is not turned, so its finding names the portrait width, as does every table at margins that leave a landscape page a text area under 50 mm tall;

  • any directive or role ubCode recognises and has no PDF renderer for — build.pdf_directive_unhandled and build.pdf_role_unhandled.

Raw blocks and roles are not on that list: they draw nothing at all, and raise no finding — but for one LaTeX spelling, which is a page break (see Page breaks you write). They are, with .. only:: content (below), the only constructs dropped because the author addressed them elsewhere rather than because the builder cannot typeset them — comments and targets also draw nothing, and the toctree draws nothing in-page because it is the PDF’s document order (above); none of them is a payload addressed to another writer. A .. raw:: html block names in its argument the writer its payload is written for, and a :raw-html: role carries the same name, fixed when the role was defined (.. role:: raw-html(raw) with :format: html). Either way the payload is addressed to a writer, so a writer it does not name has been told to leave it out — which is what Sphinx’s own LaTeX, text, man-page and Texinfo builders do with a format written for someone else, without a word. There is no format this builder could honour: it typesets directly, with no writer language an author could address.

That is not a raw directive’s or role’s payload going missing in silence: the rule above is about constructs this builder cannot typeset yet, each of which is a backlog entry with a box to prove it, and raw output is not one of them. The same applies to HTML written directly in a Markdown document — an inline tag, a <br>, or a block of HTML: MyST lowers it to the same raw node, so the PDF omits it. Inline tags around text keep the text; an inline tag with no text of its own, such as an <img>, leaves nothing at all; a hyperlink written as <a href=…> keeps its words but not its destination; and an HTML block and its contents do not appear at all, where the HTML site renders them — there the payload dropped is the author’s own. The same is true of sphinx-design’s :octicon: role, which an author types in prose. A role dropped mid-sentence leaves the spaces that surrounded it side by side, as Sphinx’s text builder does; on paper that is one extra space — 3.18 pt at 10 pt in the body face — where a browser or TeX would collapse it, and a <br> written between two words with no space around it fuses them.

Conditional content follows the site’s tags. .. only:: is evaluated against the tags the HTML build uses — html, format_html and builder_html, plus the project’s build_tags (see Configuring a project with ubproject.toml) — so .. only:: html content is printed, and .. only:: latex, .. only:: pdf or .. only:: not html content is not, with no finding. To print a pdf branch, add the tag for the PDF run alone: ubc build pdf -c 'build_tags = ["pdf"]'. The option replaces the project’s list for that run, so name any tags the project already sets as well. .. if:: is evaluated against variant data, as on the site.

These further findings are not placeholder boxes and are worth reading differently:

  • A character no font available to the build covers is omitted, and reported as build.pdf_glyph_unavailable with the codepoint and a count. The count is one per place the character was left out: a character in the running header or footer is counted once for every page that band is printed on, and a heading’s once more for its entry in the table of contents. The built-in text faces carry no CJK, Thai, Khmer or Lao glyphs, and the bundled emoji face draws a minimal set of common emoji and no more (see Emoji): a heading written with a leading sparkle emoji (U+2728) is typeset with it, while an emoji outside that set — a skin-tone modifier, a flag, most of the repertoire — is left out and reported. Omitting is deliberate — a row of ▯ in a formal deliverable reads as a broken product — but it is never silent. The codepoint is named here rather than pasted, so that documenting the behaviour does not itself add a site to this project’s own count.

    The answer is [build.pdf] fonts: list a font file that has the character and it is drawn — for emoji, a fuller emoji font, which the Emoji section says where to get. Listing one family as several files — a regular and a bold — is expected and both are used; what is refused is a file whose family name is one this builder already uses, or that fills a slot (family, width, style and weight) a file listed before it already fills.

    If you listed a font for the character and still see this finding, read the build.pdf_font_unreadable line first: a file that was refused or could not be read supplies nothing, so its characters are then reported as uncovered exactly as if it had never been listed, and that line — not this one — carries the remedy.

    Each message carries the remedy that is true of the character it names, and only where there is one. A private-use character (an icon font’s glyphs) says that nothing but the font defining it can ever draw it — such a codepoint has no meaning outside that font. A control character or a Unicode noncharacter is advised nothing at all: no conformant font maps either, so no font list can make one appear. An emoji is named as one: outside the bundled set, the message points at a fuller emoji font; one a built-in face has, left out of a sequence another face drew, is said to be exactly that; with [build.pdf] emoji = false, it says the bundled set is off.

    Text inside pictures takes the same faces, so a chart or diagram label draws exactly the characters the prose around it can. One divergence is worth knowing: where the prose OMITS an uncovered character, a picture draws the hollow box for it, because the SVG renderer positions a glyph this builder never sees.

  • A font file that could not be used is skipped, and reported as build.pdf_font_unreadable naming the path and the reason — it is not there, or nothing in it registers as a face (.woff and .woff2 are compressed web-font containers, not font files, and say so). The document still builds; what is missing is the characters that file would have supplied.

  • A family that could not be used is refused, and reported as build.pdf_font_unselected naming the value [build.pdf.typography] font or font-mono was given and what was wrong with it — no font file this project lists declares such a family (the families that ARE available are listed), or the family it declares has not got every face the layout asks for, which it names. The document is set in the built-in family, whole: a family is never filled in face by face from the built-in one and no face is synthesised, because a document whose emphasis silently came from another typeface than its body is worse than one set in the family it already had. It is the build.pdf_font_unreadable bargain once more — the deliverable is never the casualty — and the default --deny warning still fails the command, which is what catches a typo before it ships.

  • A cover logo that could not be drawn is left out, and reported as build.pdf_cover_logo_unreadable naming the path and the reason — it is not there, it could not be read, or it is not a picture this builder can draw. The title page still prints, laid out exactly as it would be for a project that named no logo at all; no placeholder box is drawn where the picture would have been. It is the build.pdf_font_unreadable bargain applied to a picture: a file present on your machine and absent on a build server costs the reader the mark, never the document (the default --deny warning bar still fails the command — pass --deny error if the mark is to be optional in CI).

  • A picture drawn too small to read is still drawn, and reported as build.pdf_picture_text_too_small naming the directive, the row count where it has one, the scale it was drawn at and the size its smallest labels print at. A synthesised chart or gantt is one atomic picture scaled to fit the space it is in, so a sixty-row needgantt on an A4 page is drawn at about two thirds of its natural size and its labels land near five points. The picture is still complete and to scale — drawing a partial one would be worse — but a reader who cannot read it can widen the measure, split the view, or set a larger page.

  • A code block naming a language nothing answers to is drawn without colours, and reported as build.highlight_language_unknown naming the token and where it is written. Aggregated per NAME, so twelve blocks written pyhton are one line saying twelve. The block itself is complete — the text is exactly what you wrote — and what is missing is the colouring, which is why this is a warning and not an error. It is not raised for a block that names no language, nor for one that names text, none, plain, plaintext, raw or output: those are uncoloured on purpose and are not failures. Nor for a build with [build.pdf.highlight] enabled = false, where no language is looked at at all. The name is not build.pdf_* like its neighbours here, deliberately: the answer it reports is the highlighter’s rather than this builder’s, so the same finding will be the same name when the website highlights too.

  • A footnote too tall for any page foot is still drawn, and reported once per build as build.pdf_note_oversized with the count. A note is set at the foot of the page of its first mark and runs on to the next sheet when it does not fit, cutting between its own lines — but a note whose smallest part is taller than a whole foot has nowhere to cut, and a picture inside a note is the one thing that makes one. It is drawn at the foot all the same and runs past the bottom of the page, because the alternative is losing the text: scale the picture down, or move it out of the note.

  • A page-layout class where no page can end is ignored, and reported as build.pdf_break_unplaceable naming the class and the place — a table cell, or a footnote’s or a citation’s body — at the line of the classed block (for a container, the first line inside it). The block is drawn as if it carried no class (see Page breaks you write).

  • A landscape class below the top level is ignored (and so is a portrait one inside a landscape section), and reported as build.pdf_landscape_refused naming the class and the place — a list item, a definition, an admonition, a need’s card, a figure… — at the line of the classed block. A page changes shape only between top-level blocks, so the block is printed at the width around it (see Landscape pages). The same finding names the margins when they leave a landscape page a text area under 50 mm tall, and the block is printed portrait.

  • A block marked keep-together that no page can hold is split, and reported as build.pdf_keep_together_oversized at the line of the classed block: it is taller than a page, or than a page less the heading kept above it, so it is laid out as if unmarked and breaks where the page fills.

  • Documents no toctree reaches are excluded, and reported once per build as build.pdf_documents_excluded with the count and up to three names. This is a deliberate divergence from ubc build html, which renders every source file: a page nobody links to is a harmless extra URL on a website, while a document silently appended to a signed deliverable is a defect.

The per-construct matrix behind this list — every AST node type, whether it is rendered, skipped or a placeholder — is maintained with the code in rust/ubc_build_pdf/design/coverage.md.

Warnings and the quality gate

Like ubc build html, the build reports two independent counts — indexing warnings (the ordinary project diagnostics ubc check reports) and build warnings raised by the render phase itself, all carrying a build.pdf_* code — and holds both to one bar. Any warning-or-worse finding fails the command with exit 1, and prints its detail without needing -w:

$ ubc build pdf
Building index for: /home/me/project/ubproject.toml
Processing 1 file
1 document(s) rendered to 3 page(s); 1 placeholder(s).
Found 1 build warning(s).
warning[build.pdf_block_unsupported]
  --> index.rst:7
  video: 1 site rendered as a placeholder box
PDF written to: /home/me/project/_build/pdf/index.pdf
Build failed the quality gate: 1 warning(s) found, and any warning-or-worse finding fails this build (the default --deny warning).
  Every finding that failed the gate is a construct the PDF builder does not render yet (drawn as a labelled placeholder box); no project diagnostic is among them.
  Silence a code with ignore = ["<code>"] under [lint] in ubproject.toml.
  Or relax the bar: --deny error (errors only), --deny none (never fail).
Done with warnings in 0.17s

The second line is the one to notice. It appears only when every finding under the bar is a construct the builder does not render yet — no indexing diagnostic among them, and no dropped glyph or unrenderable picture — which is the case where a red build reports known gaps in the builder, not defects in your project. A single broken cross-reference, and it is gone.

The PDF is still written. The gate is a judgement on the artifact, not a refusal to produce it, so a pipeline can publish a failing build for inspection.

Three ways to live with it, in the order to reach for them:

[lint]
ignore = ["build.pdf_glyph_unavailable"]

Narrowing beats relaxing: a code silenced through lint configuration — by code, by file or by message — never reaches the gate at all, and --lint-extend-ignore <CODES> does the same for a single run.

[build.pdf]
deny = "error"

declares the project’s bar once, so CI just runs the command. And --deny on the command line wins over the key for one run, in both directions:

$ ubc build pdf --deny none     # I know — just build it
$ ubc build pdf --deny error    # errors only, without editing tracked config

Rebuilds

There is no incremental PDF build yet. Pagination makes the page the wrong unit and the document the right one, and every run currently lowers, lays out and emits the whole file. The index underneath it is incremental exactly as it is for every other command, which is where a large project’s time goes, so a second run over unchanged sources is still much faster than the first.

One thing is shared: externally rendered PlantUML diagrams live in a content-addressed store under the project’s cache directory, the same one ubc build html writes, so a diagram both builders draw is rendered once between them. --no-cache bypasses the read of that store and re-renders every diagram — which is the recovery from swapping a PlantUML executable for one that reports the same version.

Configuring the document

Page size, margins, base type size, the two front-matter keys and the quality bar live under [build.pdf] in ubproject.toml:

[build.pdf]
page_size = "letter"
base_font_size = 11.0

[build.pdf.margins]
top = 25.0

See the PDF document configuration page for every option.