PDF document

CLI Beta

Added in version 0.35.0.

Every key, with its type and default: [build.pdf].

The [build.pdf] section configures the single PDF produced by ubc build pdf: the page size and margins, the base type size, the two pieces of page furniture, the external PlantUML renderer, and the quality bar the build is held to.

A key that [build.pdf] or any of its sub-tables does not know is refused when the configuration is read — the two exceptions being [build.pdf.theme.palette] and [build.pdf.theme.classes], whose keys are names of your own choosing (see theme.* below). The refusal names the line it is written on and lists the names that would have been accepted — deny, page_size, margins, base_font_size, title_page, cover, toc, appendices, pagination, typography, running_header, header, footer, theme, fonts, emoji, hidden_needs, plantuml, mermaid and highlight in this table, and the keys documented under each heading below in the sub-tables. A misspelling is therefore a failed build with a line to jump to, rather than a setting that quietly does nothing.

The builder is Beta: the typeset output, the theming surface and the command’s options may still change between minor releases. Pin the minor version you build with — a key a later release adds is refused, as above, by a ubc that predates it. For a guide to running it, see Building a PDF.

Minimal example

Everything has a default, so the section is optional: ubc build pdf builds an A4 document at 10 pt with 20 mm margins, a title page, a table of contents and a running header, with no [build.pdf] table at all. What a project usually writes is the deviation from that:

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

[build.pdf.margins]
top = 25.0
bottom = 25.0

Options

deny

Type: string (optional, default: "warning")

The build’s quality bar: exit 1 when any finding at or above this level survives [lint] narrowing — "none", "info", "warning" or "error". One bar covers both counts the command reports (index diagnostics and render-phase build warnings), and the PDF is still written before the exit code is decided. The --deny command-line flag overrides the key for one run; "none" never fails the threshold (an explicit --max-warnings budget still applies).

Because constructs that are not typeset yet are reported as warnings, the default bar fails a build that contains one. That is deliberate — the finding is real, and the document has a grey box in it — and Warnings and the quality gate covers the three ways to live with it, narrowest first.

page_size

Type: string (optional, default: "a4")

The page size of the document: a4 (210 × 297 mm) or letter (8.5 × 11 in).

An enum rather than a dimension string, so there are no units to spell and no invalid geometry to express. The pages are this size upright, and a block classed landscape is printed on pages of this size turned, with the same margins — unless they leave that page a text area under 50 mm tall (see Landscape pages).

margins

Type: table of float (optional, defaults below)

The page margins, in millimetres. Each of top, right, bottom and left defaults to 20.0 and may be set on its own:

[build.pdf.margins]
top = 25.0

A negative value is refused, as is any combination leaving a content box narrower or shorter than 50 mm — the configuration is rejected when it is read, naming the key, rather than producing an unreadable document.

The margins are also where the page furniture is drawn: by default the page number and its total in the bottom one and the document’s title in the top one, both in muted ink, and header and footer below change either. Each band stays just inside the page edge when its own margin is too small to hold it, so a tight margin costs the header’s hairline rule rather than the title.

base_font_size

Type: float (optional, default: 10.0)

The base text size, in points. Must be between 6.0 and 24.0.

Every other size in the document derives from it: headings scale up from it, code and the page number scale down from it, and the line spacing and the gaps between blocks are multiples of it. So this one key is the whole type scale — raising it to 11.0 sets the entire document a step larger, not just its body text.

title_page

Type: bool (optional, default: true)

Open the document with a title page.

On unless title_page = false. When on, the document starts with a cover carrying the project name — or, when no name is set, the root document’s title, or its file name when that document has no title — the version from [project] version when there is one, and how many of the project’s needs and documents the file contains when the build was given a need selection. A date is added only when the SOURCE_DATE_EPOCH environment variable is set, so that two builds of the same sources stay byte-identical.

The cover carries no page number — the pages after it are numbered from 1, and a PDF reader’s own page box shows the same numbers — and it takes the first entry in the document outline, so a reader can navigate back to it.

What the body is numbered from is the front matter’s extent: the cover’s sheets, however many it takes, plus the sheets of a toc table when one is printed. So the first body sheet prints 1 under a one-sheet cover, under a cover that runs onto a second sheet, and under a cover followed by six sheets of contents alike.

Changed in version 0.36.0: The body used to be numbered from the sheet after the FIRST one, whatever the cover took. A project whose cover spills onto a second sheet — reachable with a large base_font_size or a long lines list below — therefore renumbers: its first body sheet now prints 1 where it printed 2, and its PDF page labels move with it. A project whose cover is one sheet prints exactly what it printed.

[build.pdf]
title_page = false

Turning it off removes the cover: the body is the same document in the same order, and the page numbers do not move, because the cover was never counted in them.

--title-page and --no-title-page decide it for a single run and beat this key in both directions — see the command’s options.

cover.*

What the title page carries.

Added in version 0.36.0.

Unset — the default — is the cover this builder has always drawn: the project’s name, its version, the counts of a need selection when one is active, and the date of a build that pinned SOURCE_DATE_EPOCH. Every key below adds to that or takes one line away from it. The whole table is ignored when title_page = false (a layout dump still lists the keys you wrote).

[build.pdf.cover]
logo = "assets/mark.svg"
subtitle = "Release candidate"
author = "The Documentation Team"
lines = ["Confidential", "(c) 2026 Example Ltd"]
counts = false

The keys

Key

What it does

logo

An image file to print above the title. A path relative to this ubproject.toml, or absolute. PNG, JPEG, GIF, WebP, SVG and PDF files are all drawn, and an SVG’s text is set in this builder’s own faces rather than in the machine’s, so a build server draws the same mark an author does. Unset or "" means no picture — and in a configuration that extends another, "" turns off a picture the other set, where leaving the key out inherits it.

logo_height

How tall to print the logo, in millimetres. Unset — the default — fits the picture into the box described below and never enlarges it. Set, the picture is printed at this height with its proportions kept, and the only remaining limit is the width of the text. Must be greater than 0 and at most 100.0; a value outside that is refused when the configuration is read. It does nothing on its own when no logo is set.

subtitle

A line of text printed under the title, in muted ink at the ordinary text size. Unset, "" or blank prints nothing there; in a configuration that extends another, "" turns off a line the other set, and leaving the key out inherits it.

author

A line of text printed under the subtitle, in the same ink and size — a person, a team or an organisation, in whatever form suits. Unset, "" or blank prints nothing there, and it extends exactly as subtitle does.

lines

Extra lines printed at the foot of the cover’s text, one paragraph each, in the order written — the place an imprint, a classification or a copyright line belongs. An entry that is "" or blank prints nothing, so a line can be emptied without renumbering the rest. An entry that is a web or mail address on its own — https://, http:// or mailto: — is printed as a link, in the document’s link colour, and is clickable; an address inside a sentence is printed as text. Everything on the line is part of the address, a closing full stop included, so write the address alone. At most 16 entries; a longer list is refused when the configuration is read, pointing at the table. Unset or [] means no extra lines.

title

Print the project’s name as the cover’s title. On unless title = false. Turn it off when the logo is a wordmark that already says the name, so the cover does not say it twice. The document’s title in the PDF’s metadata and outline is unaffected — only the printed line goes. If nothing else would be left on the cover, the title stays.

version

Print the Version line. On unless version = false. The line is absent anyway when [project] version is unset, so this turns off a line a project that HAS a version would get.

date

Print the date line. On unless date = false. The date comes only from the SOURCE_DATE_EPOCH environment variable, so the line is absent anyway on a build that pinned none.

counts

Print the selection-count line. On unless counts = false. The line states how many of the project’s needs and documents the file contains, and it is printed only under --need-id or --need-filter. Turning it off removes the one mark on the document saying it is an extract; the command still reports the selection on its own output.

The order, and what it is not

The pieces are printed in one fixed order — the logo, the title, the subtitle, the author, then the version, the counts and the date, then your own lines — and each starts at the left edge of the text. Every one of them is optional, the title included: a piece you have not configured, or have switched off, simply is not there and the pieces under it move up. There is no alignment, position or size key: this table says what the cover says, not where each piece sits.

The cover’s ink is not here either. What the document is painted in is [build.pdf.theme.colors] below, keyed by role like every other colour.

The logo’s size

Without logo_height the picture is fitted into a fixed box — two fifths of the text width, and six times base_font_size high — keeping its proportions, and a picture smaller than that box is drawn at its own size rather than blown up. Both numbers move with the page: change the margins, the page size or the base text size and the box follows.

With logo_height the picture is printed at the height you state, enlarged or reduced, and the two-fifths rule no longer applies — it is the rule for a mark whose size the builder chooses, and you have chosen. What still applies is the width of the text: a mark too wide for the page is scaled down to fit it. Millimetres, like the page margins, because this is a physical size on paper rather than a size of type.

A file that is not there, or that cannot be read as a picture, is reported as build.pdf_cover_logo_unreadable naming the path and the reason, and left out. The cover still prints, and no placeholder box is drawn in its place.

A configured cover paginates

It is typeset like any other content: where it does not fit on one sheet it runs onto the next, and those sheets carry neither the running header nor a page number. The 16-entry bound on lines is a sanity limit and not a promise of one sheet — at base_font_size = 24 a cover has room for about ten lines of its own, against about thirty at the default size.

toc.*

A table of contents in the front matter.

Added in version 0.36.0.

Unset — the default — is a table of contents: the document opens with a table listing its headings and the page each one starts on, on its own sheets before the body and after the title page. enabled = false takes it away and gives the document this builder drew before the table existed.

[build.pdf.toc]
depth = 2
title = "Table of Contents"

The keys

Key

What it does

enabled

Whether the table is printed at all. On unless enabled = false.

depth

How many heading levels the table lists. Defaults to 3, and must be 1 or more. A project that wants every heading listed writes a large number.

title

The heading printed above the table. Defaults to Contents. "" prints the table with no heading at all; in a configuration that extends another, "" turns off a heading the other set, and leaving the key out inherits it.

What the table lists

One entry per section heading, in reading order, each the heading’s text, a dotted leader and the page the heading starts on, and each entry is a link that jumps there. The title page is not listed — a table of contents that opened on the cover would be listing the sheet the reader is holding. A long heading wraps rather than being cut short, its continuation lines indented one step further in than the first, because an entry a reader cannot read is an entry they cannot find.

depth counts levels from the root document’s title, which is level 1. So the default of 3 lists that title, the titles of the documents the root toctree reaches, and those documents’ own top-level sections — and likewise the appendices, under their heading. The numbers printed are the page numbers the pages themselves print — what a reader would cite — and not the sheet positions in the file.

What a contents sheet carries, and what it does not

The table’s sheets are front matter, like the cover: they carry neither the running header nor a page number, and a PDF reader’s own page box shows them numbered i, ii, iii … while the body is numbered from 1 on the sheet after them. The heading is set in the same type as a top-level heading and takes no entry in the document outline — a reader’s bookmark pane lists the document’s own headings, which is what the table itself is.

The table survives title_page = false: it is its own sheets rather than part of the cover, so a project that wants a contents page and no cover gets one, and the table is then the document’s first sheets.

--toc and --no-toc decide it for a single run and beat enabled in both directions — see the command’s options.

appendices

Type: array of string (optional, default: [])

Added in version 0.37.0.

Documents to print after the body, as appendices, in the order listed, under one Appendices heading in the table of contents and the bookmarks. Each entry is a docname — the name a toctree entry uses, with no suffix and no leading /, such as appendix/glossary — of a document the root toctree does not reach.

[build.pdf]
appendices = ["glossary/index", "release-notes"]

A listed page’s own toctree comes with it. The pages its toctree lists — and the pages their toctrees list — follow it, beneath it and in toctree order, so one entry is an appendix of the pages its toctree reaches that nothing printed before; a page the body or an earlier entry already prints is not printed again. The listed page is set one level below the Appendices heading and the pages it lists one level below that, as a chapter of the body and its own pages are. Under the default pagination the appendices start on a new page, with the heading above the first appendix’s title, and [build.pdf.pagination] documents decides which of the later ones start a page of their own, as it does in the body; documents = "none" lets the heading and the appendices flow on after the body. Under documents = "top-level" and "all" the heading opens the page and the first appendix follows it. Under "none" the heading flows, unless break_before_level covers the level of the first appendix’s title: that title then starts a page and takes the heading along (break_before_level acts on the section headings of the documents themselves, not on the heading), and every later listed page whose title level it covers starts a page too.

An entry the build cannot honour is skipped and reported by name, as build.pdf_appendix_refused warnings, one per reason and entry (identical sentences share a line) — the list is never repaired in silence:

  • an entry that names no document — with a hint when dropping a .rst or .md suffix names a document that would be printed;

  • the root document, or a document the root toctree reaches: it stays where the body prints it and is not repeated;

  • a document an earlier entry’s toctree already brought in;

  • an entry listed a second time: only its first entry counts.

The toctrees of the pages an entry brings in are checked too: a toctree entry there that names no document, that lists a page already above it, or that lists its own page, is reported under the same code. An entry that is "" is a no-op.

A page another page pulls in is not refused for that. A listed document that another page pulls in with .. include:: prints as the appendix you listed and, when the page that pulls it in is printed, there too, as Sphinx prints it; list it only when you mean that. The build does not refuse it, because it cannot tell include:: from literalinclude:: (the index records a page’s dependencies without their kind), and refusing would block a page another page only shows as source.

Mark an appendix page :orphan:. ubc check and ubc build html do not act on [build.pdf] appendices, so listing a page here does not tell them it is meant to be outside every toctree: a listed page that no toctree lists and that is not :orphan: is still reported as toctree.not_included, as Sphinx reports a page listed in latex_appendices.

See Appendices for a worked example, and for how this differs from Sphinx’s latex_appendices.

pagination.*

Where pages break, and what is kept together on one.

Added in version 0.36.0.

Six keys: which documents start on a new page, a section heading level that always starts one, how many lines a heading keeps with it, the fewest lines a split leaves on each side of a page break, and whether a line ending in a colon stays with what it introduces. Unset — the default — is the document this builder draws without the table: the root document, the documents its toctree lists directly and each appendices entry start on a new page, no heading level forces one, a heading keeps three lines of what follows it, a split leaves at least two lines on each side, and a line ending in a colon stays with its block. The guide’s Where a page may break (Building a PDF) describes the rules themselves.

[build.pdf.pagination]
documents = "all"
break_before_level = 2
keep_introducers = false

The keys

Key

What it does

documents

Which documents start on a new page. "top-level" — the default — starts the root document, each document its toctree lists directly and each appendices entry on a new page, and lets deeper documents follow on where the text ends; "all" starts every document on a new page; "none" starts none, so each follows on where the one before it ends. Any other spelling is refused.

break_before_level

Start every section heading at this outline level or above on a new page. Defaults to 0, which starts none; 2 starts every level-2 heading on a new page. Levels are counted as toc.depth counts them. There is no upper bound.

heading_keep_lines

How many lines of what follows a heading share its page. Defaults to 3, and must be 1 or more; 1 keeps only the next line with the heading, or orphans lines of a block long enough to split.

orphans

The fewest lines a page break leaves at the foot of a page — lines of a paragraph, source lines of a code listing, rows of a table’s body. Defaults to 2, and must be 1 or more.

widows

The fewest lines a page break carries onto the next page, counted the same way. Defaults to 2, and must be 1 or more.

keep_introducers

Keep a paragraph ending in a colon on the page of the block it introduces. On unless keep_introducers = false.

Which documents start a page

The body starts on the page after the title page and the table of contents when the project prints them, whatever documents says, and the bibliography page is always a page of its own. A page a document starts carries no running header, since the document’s own title heads it — so under "all" a deeper document’s first page is unheaded too, as a top-level document’s always is. Under "none" a document’s title may fall anywhere on a page: the running header of that page names the document the page began with, and the table of contents and the outline point at the page the title is on. A document that ends on a landscape block is followed on a new, upright page, under "none" too: a landscape run always ends with the block it turns.

A heading level that always starts a page

break_before_level counts levels as the table of contents does: the root document’s title is level 1, and a document at toctree depth d sets its title at level d, at most 6 — a title deeper than that is set at level 6 — and its own sections from there, so a section heading’s level on paper can be deeper than on the site. The break is exactly a break-before class written on each such section (see Page breaks you write): a heading whose document already starts a new page is not given a second one, a class written on the same section is still one break, a page the break opens carries the running header (unless it is a document’s title that the break puts at the top of the page, as on any page a document begins), and a break directly under a heading with nothing else on its page is not made — so a document’s title directly followed by its first level-2 section keeps that section on the title’s page. A rubric is not a section heading and never breaks.

Keeps

heading_keep_lines counts the lines of the blocks under the heading: a heading over a one-line paragraph keeps the next block’s first lines as well, and a paragraph, a code listing, a table or a list long enough to be split keeps that many of its own lines with the heading, or orphans if that is more. A table’s header row is one of those lines, and a list’s are counted through its items. A caption ends the count: the table or figure it captions keeps by its own rule. The block’s own widows still holds under the heading, so a block shorter than heading_keep_lines and widows together is not split there: it moves whole with its heading when the two fit a page, and otherwise the heading keeps what fits. orphans and widows hold for paragraphs, code listings and table bodies alike: a block with fewer lines than the two together is not split when it fits on one page, so at the default a listing of three lines or a table of three rows stays whole. 1 lets a split leave a single line — or row — on that side, so 1 for both lets a listing or a table break between any two of its lines or rows. None of the counts has an upper bound, and none asks for more than a page can hold — a paragraph, a listing or a table alike: a heading that cannot share a page with that many lines keeps what fits, and a block too tall for any page is split where the page fills, so a large orphans or widows means “split only what no page can hold”. keep_introducers = false lets a page end on a line such as “For example:” with its example on the next.

running_header

Type: bool (optional, default: true)

Print each page’s document title in the top margin.

On unless running_header = false. When on, every page carries the title of the document its content belongs to — or that document’s file name when it has no title — above the text, set in muted ink with a hairline rule under it, as the counterpart of the page number in the bottom margin.

It is not printed on the page where a document begins, where its own heading is already the title, and not on a title page, which belongs to no document. A document that fits on one page is therefore never headed at all — there is no continuation to head, and its heading is the page’s title. A title too wide for the page is cut short with an ellipsis, and the rule is left out where a small top margin would put it inside the text.

It names the document and not the current section: that is the question a reader of a long concatenation actually has, and the section is already in the bookmark pane and in the heading above.

It is a shorthand for the header table below. running_header = false empties that table’s default slots, and any slot you write there still applies on top of it — so a project can turn the stock running head off and still put a page number in the top margin (with the header’s rule under it, unless divider.show = false).

header.*

What is printed in the top margin.

Added in version 0.36.0.

Three slots — left, center and right — with the band’s type beside them and a divider sub-table for the rule under it. Each slot is a line of text in which the names below, written in braces, are replaced by what the page knows. Unset — the default — prints the document’s title on the left with a hairline under it, which is what running_header has always drawn. A band is always set in the built-in family, whatever typography.font selects: the page’s furniture is placed by that family’s own metrics and stays where it is however the document is set.

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

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

What you may write in braces

Name

What it prints

{title}

The title of the document the page belongs to, or that document’s file name when it has no title. It is empty on the page a document begins on, where the document’s own heading is already the title — so a document that fits on one page is never headed.

{section}

The most recent section heading of that same document — the last one that starts at or above the top of the page’s text. Empty before the document’s first section, and it never carries across a document boundary. The document’s own title heading is not a section for this purpose.

{page}

The page’s printed number. A title page carries no number and the pages after it start at 1.

{pages}

How many numbered pages the whole PDF has — the build’s total, not the current document’s — so {page} of {pages} is right on the last one.

{project}

The project’s name, as the title page takes it: [project] name, else the root document’s title, else its file name.

{version}

[project] version, or empty when it is not set.

{date}

The build date, and only when the SOURCE_DATE_EPOCH environment variable is set — otherwise empty, so that two builds of the same sources stay byte-identical. There is no clock in this builder.

Everything else in a slot is printed as written, including free text around a name that resolves to nothing — so "{title} — {section}" prints a bare — on a page that has neither. {{ and }} are a literal { and }; any other name in braces, and any unmatched brace, is refused when the configuration is read, pointing at the table. A resolved slot is collapsed to one space-separated line, and cut short with an ellipsis when it is too wide for the room it has.

What a band draws, and where

A slot that resolves to nothing prints nothing, and a band with no printed slot draws nothing at all, its rule included. 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.

Each slot is cut to a fixed share of the measure and does not lend its space to another: 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 band too tall for its margin is kept on the sheet rather than printed off the paper. 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. A footer rule has about a sixth of the band’s type size to sit in before it would reach the tallest letter the band could carry — an accented capital such as Ẳ, not the page number’s own digits — so a divider.weight heavier than that (0.18 times the type size, 1.6 pt at the defaults) is left out rather than ruled through the text. A letter that stacks several combining marks can rise past that bound, and a footer rule is not protected against it.

left

Type: string (optional, default: "{title}")

Added in version 0.36.0.

What is printed at the left of the top margin. Set it to "" to print nothing there.

center

Type: string (optional, default: "")

Added in version 0.36.0.

What is printed in the middle of the top margin.

right

Type: string (optional, default: "")

Added in version 0.36.0.

What is printed at the right of the top margin.

size

Type: float (optional, default: 0.9)

Added in version 0.36.0.

The band’s type size, as a factor of base_font_size — so 0.9 is 9.0 pt under the default 10.0 pt base, and a project that changes its base moves the band with it. Must be between 0.1 and 3.0. Below the floor a band is too small to be type, and small enough that it can lose its divider rule or vanish altogether, so it is refused when the configuration is read rather than typeset as nothing.

The band’s whole geometry scales with it: the line sits further from the text and takes its rule with it. Each band is placed so that its nearest ink — the head’s descender line, the page number’s cap line — is the same distance from its own edge of the text block. Both bands use the same rule, so the same size value puts either band the same distance from the text: set both tables’ size to move the two together, until a band is large enough that it has to be held inside the page edge. Before 0.36.0 the bottom band sat about half a line lower than the top one.

weight

Type: string (optional, default: "normal")

Added in version 0.36.0.

The face the band is set in: normal or bold. It names a face, not a numeric CSS weight — the built-in sans family carries these two.

italic

Type: bool (optional, default: false)

Added in version 0.36.0.

Set the band in the oblique face.

divider

Type: table (optional)

Added in version 0.36.0.

The rule between the header and the text.

show is a bool, on for the header, and weight is the rule’s thickness in points, defaulting to 0.5. A thickness must be greater than 0 and at most 5.0: write show = false rather than a zero thickness, so that there is one way to say “no rule”. It does not scale with size — a hairline is a physical printing fact rather than a size of type.

[build.pdf.header.divider]
show = true
weight = 0.5
footer.*

What is printed in the bottom margin.

Added in version 0.36.0.

The same seven keys as header, with the same names in braces and the same rules about empty slots, title pages and measure sharing. Two defaults differ: center is "{page} of {pages}", the running page number and the count of numbered pages in the file that was built, and divider.show is false, because a page number is a fragment rather than a head. Like the header, a footer band keeps the built-in family whatever typography.font selects.

{pages} counts the pages of the built file: a title page is not numbered and is not counted, so a one-page document under the default cover reads 1 of 1, and a --need-id or --need-filter extract counts only its own pages — a three-page extract of a long project reads 3 of 3 on its last sheet, not 3 of 400.

Writing center = "{page}" restores the bare number, and center = "" is how a project asks for no page numbers at all — there is no other spelling for it. The centre is wider than a bare number was, so a project that writes a footer side slot beside it shares the measure three ways and a long {title} there is cut a little earlier.

{title} is empty on a document’s opening page in this band too, so a footer set to the document title behaves exactly as the header does.

[build.pdf.footer]
left = "{project} {version}"
center = ""
right = "{page}"
theme.*

The colours the document is painted in — ubCode’s own palette by default — and the type and colours of every element that carries a class.

Added in version 0.36.0.

Three tables. [build.pdf.theme.palette] holds your own named colours, [build.pdf.theme.colors] says which of the document’s roles each colour paints, and [build.pdf.theme.classes] says how an element carrying a class is set — its type, and its own colours (see classes below).

Every key of colors is a name from the HTML site’s custom-property table without its --ubc- prefix, so a colour set here and the same token set in the site’s own theming mean the same thing on paper and on screen. A key that is not one of the names below is refused when the configuration is read, naming the line it is on.

palette

Type: table of string (optional, default: empty)

Added in version 0.36.0.

Your project’s own colours, by name. A key is a name of your choosing — letters, digits, _ and -, not starting with a digit — and a value is a hex colour, #rgb or #rrggbb.

A palette entry may not refer to another entry: the references run one way, from colors into palette, so there is no chain to follow and no cycle to detect.

A configuration that writes this table at all replaces it whole rather than merging with the palette of a configuration it extends — a partial merge would let @brand resolve through an entry the file that wrote it never read. The same rule governs a -c override: -c build.pdf.theme.palette.other=… replaces the file’s palette rather than adding to it.

colors

Type: table of string (optional, default: empty)

Added in version 0.36.0.

The colour each role is painted in. A value is a hex colour (#rgb or #rrggbb), or @name naming an entry of palette. A role you do not set keeps its built-in colour; there is no need to restate a palette you are happy with. The empty string is not a value — link = "" is refused like any other value outside the grammar. Omit the key instead.

Unlike palette, this table merges key by key across an extend chain, so a configuration that recolours link keeps every other role the configuration it extends had set. One consequence is worth knowing: inside such a chain a key you do not set keeps the extending configuration’s value rather than the built-in one, and there is no spelling yet that reverts a single role to its built-in colour.

Document colours

Key

What it paints

fg

Body text, and the rule struck through deleted text.

fg-muted

Secondary text: the running header and a need card’s id chip.

fg-border

The stronger border colour, for boxes that read as edged: a topic’s or sidebar’s leading bar.

border

The default border and rule colour: a container’s frame, a table’s cell rules, a chart’s grid lines, and the rule between sections.

bg-soft

The recessed background: table header rows, key caps (:kbd:), placeholder boxes, and topic and sidebar boxes. Not a code block’s or an inline literal’s, which is code-bg.

brand

A need card’s type badge. A role of its own rather than a use of link: the site declares --ubc-brand and --ubc-link as separate tokens that happen to carry one value, so a project that recolours link alone leaves its badges where they were — on paper exactly as on the site. The badge’s outline is not painted by this; it is the type’s own [[needs.types]] color.

link

A link’s text.

link-underline

A link’s underline. Left unset it follows link — on paper the link’s colour, on the site the link’s current text colour in every state — so recolouring links recolours their underlines. Set it to decouple the two: the underline-style treatment, where the link text stays in the body colour and the underline carries the accent. The site’s --ubc-link-underline token behaves the same way, and this repository’s own documentation PDF uses this treatment; the worked example below shows the shape.

code-bg

The background of a code block, and of an inline literal. Not a key cap’s (:kbd:), whose fill is bg-soft.

code-fg

The ink of a code block’s lines, and of an inline literal. Not the line-number gutter’s, which is drawn in fg.

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, hl-addition-fg, hl-addition-bg, hl-deletion-fg, hl-deletion-bg

The syntax-highlighting palette — one key per kind of token a highlighted code block or inline chip distinguishes: keywords and storage words; the names of types, classes, structs, enums and namespaces; the names of functions and methods; tag names, selectors and quoted passages; built-ins, support functions and a console prompt; the names that anchor a cross-reference in a markup listing, and a label a language declares such as a goto target; attribute names; variables and parameters; numbers; language constants; operators; strings; escape sequences, in a string or in markup prose; comments; headings (set bold as well as coloured); list bullets; URLs and hyperlink targets in a markup listing; and an inserted or deleted line in a diff, which has a text colour and a row background each (inside an inline chip, the text colour alone). A run no rule claims keeps code-fg. These twenty-one mirror the site’s own --ubc-hl-* custom properties one for one, same names and same default values — the site’s way to change them is to redeclare those properties in an extra_css file, and this is paper’s. See Code blocks are syntax highlighted for what is highlighted.

Changed in version 0.37.0: New default colours, the same as the site’s: one hue per kind of token, each readable at WCAG AA contrast.

generic, note, tip, important, warning, danger, seealso, todo

One admonition accent each, by kind. Each key paints two surfaces: the box’s leading bar, and its title band, which is the same colour at a fifth strength over the page.

version-added, version-changed, version-deprecated, version-removed

The lead label’s text in a .. versionadded::, .. versionchanged::, .. deprecated:: or .. versionremoved:: box.

version-added-bar, version-changed-bar, version-deprecated-bar, version-removed-bar

The leading bar of the same four boxes.

brand, code-bg and code-fg are worth one sentence, because they are the three roles a reader would expect to follow link, bg-soft and fg, and deliberately do not: they default to the values those three tokens carry today, and an unset code-bg does not take a themed bg-soft (nor an unset code-fg a themed fg, nor an unset brand a themed link) — exactly as on the site, where the sheet declares each as a colour of its own rather than in terms of another. So a project that recolours bg-soft sets code-bg too if it wants the code ground to move with it, one that recolours fg sets code-fg too if it wants the code ink to move with it, and one that recolours link to rebrand its cards sets brand too — one extra line each, and until you write it your code blocks keep the built-in ground and ink and your type badges the built-in brand colour.

Page-furniture colours (paper only)

The site has no running head, no page number and no rule under a heading, so these five keys name no site token and colour nothing on the site. Each one follows an existing role while it is unset, which is why the shipped document looks the same whether they exist or not — and why a project that rebrands fg-muted gets a rebranded running head for free.

Key

What it paints

header-text

The text of the top margin’s band. Paper only; unset, it follows fg-muted.

header-rule

The rule under the top margin’s band. Paper only; unset, it follows border.

footer-text

The text of the bottom margin’s band, the page number included. Paper only; unset, it follows fg-muted, as header-text does. Write footer-text = "#1a1c1e" for the body’s own ink: the built-in body ink as a literal, because @name names a [build.pdf.theme.palette] entry and fg is a role, not a palette entry. A project that recolours fg therefore no longer recolours the page number with it — recolour fg-muted, or set footer-text to the colour you want.

footer-rule

The rule above the bottom margin’s band. Paper only; unset, it follows border.

heading-rule

The rule under a heading, when typography asks for one. Paper only; unset, it follows border — the colour every other hairline in the document is drawn in, and deliberately not the heading’s own ink, where a hairline reads as an underline rather than as a separator. One key colours every level.

Need-card :style: colours

Key

What it paints

need-green-fill, need-red-fill, need-yellow-fill, need-blue-fill, need-gray-fill, need-discreet-fill

The background of a need card carrying that :style: (green also covers its upstream alias implemented, red covers open and yellow covers in_progress). On paper the value is the opaque colour painted: the site tints the card translucently, and this is what that tint composites to over white.

need-green-line, need-red-line, need-yellow-line, need-orange-line, need-blue-line, need-discreet-line

The frame or trailing bar of a need card carrying the matching *_border / *_bar style. The set is asymmetric because the upstream palette is: there is no orange_border and no discreet_bar.

Chart colours

Key

What it paints

chart-1 … chart-8

The categorical palette a needpie or needbar draws from: the fill of a wedge, a bar or a legend swatch. A chart with more series than slots cycles back to slot 1.

chart-on-1 … chart-on-8

The text of a value label drawn on that slot’s fill.

chart-halo-1 … chart-halo-8

The halo stroked under that label, so its glyphs keep an edge against the fill.

Not keys here. bg and admonition-bg — a printed page’s ground is the sheet; bg-hover, link-hover, link-visited, highlight-on-target and toc-active — paper has no pointer, no browsing history, no :target and no reading position; announcement-* — the site’s banner has no printed form; content-width, content-gutter, header-height and radius — lengths rather than colours, and the page’s measure comes from page_size and margins; font and font-mono — typography keys rather than colours; see typography.font.

Worked example

[build.pdf.theme.palette]
brand = "#583eff"
brand-muted = "#b7a3ff"
ink = "#101418"

[build.pdf.theme.colors]
fg = "@ink"
# An underline-style link treatment: the text stays in the body
# colour and the underline carries the accent.
link = "@ink"
link-underline = "@brand-muted"
# The card type badge keeps the brand, which a themed `link` no
# longer reaches. Without this line it would fall back to the
# built-in blue beside the purple above.
brand = "@brand"
note = "#0a66c2"
chart-1 = "#583eff"

A hyphen is legal in a bare TOML key, so no key here needs quoting.

classes

Type: table of table (optional, default: the two built-in bundles below)

Added in version 0.37.0.

How an element carrying a class is set: one sub-table per class name — a bundle — each setting any of three type keys and a colors table. A key a bundle does not know is refused at the line it is written on.

Key

What it sets

size

A factor the element’s text size is multiplied by, between 0.1 and 4.0: 0.8 sets a body paragraph at 8 pt on a 10 pt page, and a heading inside the element at 0.8 × the heading’s own size.

weight

"bold" or "normal".

italic

true for the oblique face, false for the upright one. Monospaced text has no oblique face and stays upright.

colors

A table of the element’s own colours, keyed as colors above (see Colours, below).

The precedence is one line: a class’s size multiplies the size its element’s text would otherwise have, and its weight and italic set the face of the element’s body text — and of the element itself when it is a heading — while a heading, title, term, attribution, caption or table head inside the element keeps the face its own row gives it, and a code block’s or parsed literal’s lines take the size alone. So a section classed weight = "normal" sets its paragraphs regular and keeps its headings bold, a table classed italic = true slants its body cells and not its head, and a rubric classed weight = "normal" is a regular rubric. For a heading, a table head, a term and a title the weight is the site’s own rule too: its stylesheet gives each a weight of its own. A figure’s, table’s or listing’s caption and an attribution have none there, so a class’s font-weight reaches them on the site and not here; and for the slant the site differs throughout — its stylesheet gives a heading, a table head, a term or a title no slant of its own, so a class’s font-style slants them there, where here each keeps its own. A **strong** or *emphasis* you write inside the element still sets its face, as it does anywhere.

Which elements. A class reaches an element by the same channels a page-break class does (see Page breaks you write, and Type by class for a walk-through): an rst-class, a directive’s :class: option, a container’s arguments, a MyST attribute block {.name} or an inline attribute [text]{.name}, a :::{container}, a figure’s :figclass:, a needtable’s or needflow’s :class:, and a need’s :style:. A figure’s :class: is not one of them: docutils puts it on the figure’s picture, which has no text. Not read either: a .. contents:: :class:, a sphinx-design component’s class options, and the classes the builder adds to a block itself (align-*, admonition, rubric, …). A span of text takes a class from a custom role defined with no base role (.. role:: name, then the role :name: on the text it marks), the reStructuredText twin of [text]{.name}; and an inline the builder itself gives a class list, such as a disabled raw substitution’s chip, is read by that list, as a site rule would style it. A class written above a section title is the section’s, so it sets the heading and every block of the section — but a footnote or citation drawn away from it (below). A need’s :style: also names the card’s paint, so a bundle named after a paint (green, clean, red-border) sets the type of every card carrying it — and the paint still paints.

The name. Name the class as it reaches the element. reStructuredText normalises a class the way docutils’ make_id does — lower case; each run of characters other than letters and digits becomes one -; a leading run of digits or -, and a trailing -, is dropped — so :class: Fine_Print is the class fine-print, and so is a MyST directive’s :class: option; only a MyST attribute is kept as written — an inline one, or an attribute block above a block that is not a directive ({.Fine_Print} on a paragraph is the class Fine_Print); above a directive fence an attribute block becomes the directive’s :class: option and is normalised like one. On those channels the name is the one the site’s class attribute prints. A need’s :style: tokens and a needtable’s or needflow’s :class: are normalised the reStructuredText way wherever they are written — :style: red_border is the class red-border, :class: My_Table is my-table — which is not how the site prints them. A name must be letters, digits, _ and -, not starting with a digit or a -, and any other name is refused when the configuration is read: it is the grammar of every name the theme reads, the palette’s, and deliberately narrower than CSS’s own. No name is reserved: a bundle named landscape or break-before sets the type, and the class still turns or breaks the page.

Several classes on one element. Each distinct class counts once, however often the element repeats it. Every matching bundle’s size applies — the factors multiply — and for weight and for italic the later class in the element’s list wins, a class that does not set the key taking no part. An rst-class written above a directive comes after the directive’s own :class: or arguments in that list, which is docutils’ order, so the class written above wins.

Nesting compounds. A size = 0.8 table inside a size = 0.8 container is set at 0.8 × 0.8 = 0.64 of its size, as em compounds on the site: each size you write is bounded, and the product is what you nested.

A class no bundle names is left as it is, and a bundle no element carries does nothing; neither is reported. The layout dump written by --dump-layout lists every key the project wrote here under a -- classes -- header, which is where a misspelt bundle name shows; a class no bundle names shows nowhere.

What a class does not reach: a placeholder box standing in for something this builder cannot draw; a footnote or citation drawn away from where it is written — at a page foot, at the end of a MyST document or on the bibliography sheet — which keeps a note’s own size, while one drawn in place takes its section’s class; the leading paragraph of a versionadded, versionchanged, deprecated or versionremoved box, whose own class is dropped (the box itself takes none); and an item’s bullet or number: a class on an item’s paragraph — MyST - {.name} — sets that paragraph and not the item’s bullet, which keeps the list’s size and its place at the top of the item. A class on a code block sets its lines at the scaled size too, and one on a list its bullets and numbers and the gutter they are drawn in, so a number of one or two digits is drawn whole beside its item’s text. A table’s box follows its type: a class’s size scales its cell padding and the one-line height below which no row is set, as it scales its text, and whether a table is too wide for the page is judged at that size too — by a class written on the table or on any element around it, though not by a need’s :style:, which is read only once the need’s card is drawn.

Colours. A bundle’s colors table takes the keys of colors above, with the same values — a hex colour, or @name naming an entry of palette — and paints the element carrying the class and everything inside it: its text, its own bar, band, icon, frame or ground, and every block and span nested in it. The four page-band keys, header-text, header-rule, footer-text and footer-rule, are refused in a bundle, naming the key: the running header and footer belong to no element. An editor completing the configuration from its schema still offers the four there, since a bundle’s table is the same table.

The precedence is one line: a role in a bundle’s colors replaces the same role in [build.pdf.theme.colors] for every element carrying the class and everything inside it; of two classes on one element the one later in the element’s class list wins per role; a class inside a class, the inner’s; a role the element’s classes leave unset takes the enclosing element’s, then the document’s; @name names [build.pdf.theme.palette]. Per role and not per bundle, so one class’s fg-muted survives beside a later class’s fg. Of two classes the element’s order decides, where the site orders two rules of equal specificity by their place in its stylesheet: the rule weight and italic follow too.

A role follows another inside the element as it does for the document: a bundle that writes link recolours its links’ underlines too, unless link-underline is written, and one that writes border recolours its headings’ rules, unless heading-rule is. Only what a bundle writes moves: a role the document alone sets keeps the document’s colour inside the element, and a bundle that writes fg recolours the element’s text but not its links, its inline literals or its code, which keep their own roles, as on the site. The site reads --ubc-fg for text colour only on body and its own chrome, though (content inherits the computed colour), so a site class needs color: var(--ubc-fg) beside the property to repaint its text, where a bundle’s fg here reaches every run inside the element. A needpie, needbar or needgantt inside the element is drawn in its colours, the chart-* roles included. One more divergence from the site: a code block’s line numbers and a figure’s caption are drawn in fg here, where the site draws them in fg-muted, so a bundle’s fg recolours them here and its fg-muted does there.

What a bundle’s colours do not reach: the running header and footer; a footnote or citation drawn away from where it is written; a picture’s own colours — an image, or an SVG you wrote; a need type’s own color, on a card’s leading bar and its badge’s frame; and a placeholder box’s two lines of text (its fill and frame are reached). A bundle a need’s :style: names paints the need’s card and what is inside it — its frame and its text (and its fill, when the card’s :style: also names a paint token), and the text, rules and swatch frames of a chart’s legend there — except that a chart inside the card is drawn in the colours around the card, its picture (slices, title, labels and axes) and the fills of its legend’s swatches alike, and a classed element inside the card whose own classes set a colour takes the colours around the card for every role they leave unset.

The layout dump written by --dump-layout prints each colour a bundle wrote under -- classes --, as name.colors.key #rrggbb with an @name as the colour it names, and each paint that colour decides as the bare #rrggbb.

Built in. Two bundles are built in, at sphinx-simplepdf’s own values: ssp-tinier sets size = 0.75 and ssp-tiny size = 0.5. sphinx-simplepdf applies them to a table; here they apply wherever the class is written, as any bundle does, and a table’s box follows its type as for any bundle. sphinx-simplepdf sets its own, smaller padding for the two classes (.4rem / .05rem); here the padding scales with the type, so the two are not the same number. Write the same name to change one ([build.pdf.theme.classes.ssp-tiny] with size = 0.6), and size = 1.0 to turn one off.

In an extend chain the table merges bundle by bundle and then key by key, so a configuration that writes warn.size keeps the warn.weight and the other bundles of the configuration it extends, and a bundle’s colors merges key by key again. As with colors, there is no spelling that drops a bundle or un-sets one of its keys.

Reserved. A bundle’s own construct rows are not designed yet; a table-head key inside a bundle is refused today as an unknown key, so it can be added later without changing what a bundle already means.

[build.pdf.theme.palette]
brand = "#0b7a75"

[build.pdf.theme.classes.callout.colors]
generic = "@brand"

[build.pdf.theme.classes.aside.colors]
fg = "#5b6470"

[build.pdf.theme.classes.compact]
size = 0.85

[build.pdf.theme.classes.fine-print]
size = 0.8
italic = true

[build.pdf.theme.classes.shout]
size = 1.2
weight = "bold"
.. list-table::
   :class: compact
   :header-rows: 1

   * - Key
     - Value
   * - ``page_size``
     - ``a4``

.. rst-class:: fine-print

This paragraph is set at 0.8 × the body size, in the oblique face.

.. note::
   :class: shout

   The note's body is set bold at 1.2 × the body size;
   its title grows with it and keeps its own face.

.. admonition:: Before you start
   :class: callout

   The box's bar, title band and icon are drawn in the palette's ``brand``.

.. rst-class:: aside

This paragraph is set in a muted grey.
typography.*

How each construct is set: its size, its face, and two decorations.

Added in version 0.36.0.

Two keys at the table’s top level name the families, and one sub-table per construct decides everything else. Unset — the default — is the type this builder has always set, so a project that writes nothing gets the document it already had.

The two families: font and font-mono

Added in version 0.36.0.

font is the family every paragraph, heading, table, card, caption and the cover are set in; font-mono the family every code block, inline literal and :kbd: cap is set in. A value is a family name, exactly as a font file listed under [build.pdf] fonts declares it — this builder scans no system font directory, so a family it has not been given a file for is a family it has never heard of. Unset, "" or blank keeps the built-in family; in a configuration that extends another, "" turns off a family the other set, and leaving the key out inherits it.

The keys belong to this table, [build.pdf.typography]. A font written in [build.pdf] itself is refused, naming the key and listing the sixteen the outer table does accept, exactly as a misspelling here is refused at the line it is on. fonts and font are one letter apart and the first is a key in the outer table, which is the mistake worth knowing about — the difference now shows up as a failed build rather than as a document quietly set in the wrong family.

[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 must supply every face the layout asks for: regular, bold, italic and bold italic for font, regular and bold for font-mono. Those are the only questions asked of it: font-mono does not check that the family you name is monospaced, so a proportional family with a regular and a bold is accepted there and your code is then set proportionally. The faces may arrive as one file or as four — a family’s files are chained — but a family missing one of them is refused as a whole: the warning build.pdf_font_unselected names the value, says which face is missing and which families your files do declare, and the built-in family is used. Nothing is filled in face by face from the built-in family 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. A bold at weight 600 does not fill the bold slot either, although it would have drawn: the slots are weight 400 and weight 700 exactly. The build’s default --deny warning fails on that warning, so a mis-named family is caught rather than shipped; --deny error is the escape hatch, and the document builds either way.

Coverage still works behind a selection. A character the selected family has not got is drawn from the built-in faces exactly as before — the built-in sans face, then the built-in monospaced face — and then from the other families your fonts files declare — so an icon or CJK font listed for coverage goes on doing its job. For font-mono the fall-through is the built-in monospaced family and then the rest of the chain prose falls back through — the font family when one is selected, the built-in sans face, the other listed families in order, and the emoji face: a character no monospaced face has is drawn in a code span from the face that would draw it in prose.

What follows the selection and what does not. Text inside pictures follows it — a chart, a gantt, a PlantUML diagram, and any SVG text with no family of its own or a generic one (sans-serif, serif, monospace). An SVG whose text names a family your own fonts files declare keeps that family; one naming a family nothing declares (Arial, Helvetica Neue) has never resolved in this closed font set and falls back to the selected family as it used to fall back to the built-in one. The running header and footer do not follow it: the page’s furniture is placed by the built-in family’s own metrics, so it stays where it was whatever the document is set in. A code span is set upright whatever faces its family carries, which is a rule about the construct rather than about the faces available.

Every size is a factor, never a point size. base_font_size above stays the one key that scales the whole document. What a factor multiplies depends on where its construct sits: a BLOCK row — a heading, a caption, a table head, a card title, code — multiplies base_font_size itself, while the eight INLINE rows (literal, kbd, subscript, superscript, badge, id-chip, display, version-label) multiply the size of the text they sit in, so a :kbd: inside a level-one heading is 1.9 × 0.9 × base_font_size. What you give up by writing a key is the ability to predict every size on the page from one number; what you get is a name for each of them. A size must be between 0.1 and 4.0; a value outside that is refused when the configuration is read, which is what catches 19 typed for 1.9.

Every weight names a face, "normal" or "bold" — not a numeric weight, because the built-in family carries exactly two. italic selects the oblique face, and has no effect on a construct set in the monospaced family, which is always set upright whatever faces the family carries.

[build.pdf.typography.heading]
# Applies to every level a `heading-N` table does not override.
weight = "normal"

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

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

Headings: a shared table and six levels

[build.pdf.typography.heading] carries what every level has in common — weight, italic and rule — and heading-1 … heading-6 each carry a size of their own and may override any of those. The precedence is one line: a key on heading-N wins over the same key on heading, and either wins over the built-in value. A rule merges key by key, so a level that states only a weight keeps the show the shared table wrote.

heading-6 governs level 6 and every level deeper than six. A document’s headings run deeper than its sources’ do, because a document included at depth d renders its own title at level d (at most 6) and its sections from there.

The constructs, and their keys

Sub-table

Keys

Default

What it sets

heading

weight, italic, rule

"bold", off, no rule

Every heading level a heading-N table does not override.

heading-1 … heading-6

size, weight, italic, rule

1.9, 1.6, 1.35, 1.2, 1.1, 1.0

One heading level. heading-6 also governs every deeper level.

body

weight, italic

"normal", off

Every paragraph, wherever it appears: a list item, a table cell, a need card’s body, an admonition’s body, a block quote, the paragraph under a caption, a line block’s lines. A list’s bullet or number follows the paragraph it opens. It has no size key — base_font_size is the body size.

code

size

0.9

A code block’s lines, and the line numbers in its gutter. Size only: a listing’s weight is its :emphasize-lines: option, which is content rather than styling.

literal

size

0.9

An inline literal, and an object description’s signature. Size only: it keeps the face of the sentence around it.

kbd

size

0.9

A :kbd: key cap, and an option list’s option. Size only.

badge, id-chip

size

0.9

A need card’s type badge and its id chip. Size only: both are set in font-mono, and their colours are the brand and fg-muted roles in [build.pdf.theme.colors].

link

underline

on

Whether a link is underlined. See below.

subscript, superscript

size

0.7

A :sub: or a :sup:; superscript also sizes a footnote reference’s mark. Size only; how far the run is shifted off the baseline is not configurable: a third of the paragraph’s size plus 1 px up, a fifth plus 1 px down, as a browser shifts it, rounded to the nearest point. A :sub: or :sup: whose box then reaches past the line’s box grows that line, as on the site. At the default 0.7 and the default sizes, a subscript always does; a superscript does by a point in a footnote or a citation and in a level-2, -3 or -4 heading, but not in body text or a level-1, -5 or -6 heading. A footnote mark does not grow its line unless superscript is set above about 1.8.

math

size

1.21

Typeset maths, against the text around it: a display formula is 1.21 × the size of the text it sits in by default — base_font_size in the body, a footnote’s own size in a footnote, scaled by a class that sets a size — the size the HTML build’s maths is set at beside the same prose. Size only: maths is set in its own faces. A formula wider than the measure or taller than the page is drawn smaller to fit.

table-head

size, weight, italic

1.0, "bold", off

A table’s header cells.

caption

size, weight, italic

1.0, "bold", off

A table’s, a view’s or a code block’s caption.

figure-caption

size, weight, italic

1.0, "normal", on

A figure’s, a gantt’s, a diagram’s or a video’s caption, under its picture or the box standing in for it — the one construct here set in the oblique face.

card-title

size, weight, italic

1.0, "bold", off

A need card’s title.

lead-in

size, weight, italic

1.0, "bold", off

A topic’s, a sidebar’s or a .. contents:: title, a sphinx-design component’s title slot, a tab’s label.

admonition-title

size, weight, italic

1.0, "bold", off

An admonition’s title, a generic .. admonition::’s included.

definition-term

size, weight, italic

1.0, "bold", off

A definition list’s term.

quote-attribution

size, weight, italic

1.0, "normal", on

A block quote’s attribution line — the last paragraph of a quote, opened by two or three hyphens or an em dash.

placeholder-headline

size, weight, italic

0.9, "bold", off

A placeholder box’s first line, naming what was not rendered.

placeholder-detail

size, weight, italic

0.85, "normal", off

A placeholder box’s detail lines, set in the monospaced family, so italic has no effect on them.

display

size, weight, italic

2.4, "bold", off

The title page’s display line. Whether that line is printed at all is [build.pdf.cover] title.

version-label

size, weight, italic

1.0, "bold", off

A version-change box’s lead label — “Added in version 2.5” and its three siblings.

The rule under a heading

rule = { show = true, weight = 0.75 } draws a hairline across the width of the text under the heading. It is off by default. weight is a thickness in points — it does not scale with the heading’s size, because a hairline is a physical printing fact rather than a size of type — and it must be greater than 0 and at most 3.0. Write show = false rather than a zero thickness, so that there is one way to say “no rule”.

The rule separates a heading from the block that follows it, and is drawn centred in the gap the heading already leaves before that block; a heading with no block after it in the same container — the document, a container’s body, a list item, a table cell or a definition — draws none. Because the gap is already there, turning rules on adds no height and moves no page break. 3.0 is smaller than the narrowest gap any text size can produce, so even the thickest rule stays inside it: a rule can never reach into the block below, at any base_font_size. It is painted in the heading-rule colour role, which follows border while it is unset — set that key in [build.pdf.theme.colors] to give it a colour of its own.

A link’s underline

[build.pdf.typography.link] underline = false removes the rule drawn under a link. The link keeps its colour and stays clickable; on paper it is then distinguished by its colour alone, which is worth weighing against a document that may be printed in black and white. A project that has also set [build.pdf.theme.colors] link to its body colour — the underline-style treatment the colour table above describes — leaves a link marked by nothing: the two keys are worth reading together. The underline’s position and thickness are the typeface’s own and are not configurable.

The site’s own numbers

The defaults above are what this builder has always drawn, which is why a project that writes nothing gets the bytes it already had. The HTML site states several of them differently. The column below is there so a reader who wants the site’s number can write it, rather than having to measure it:

Construct

This builder

Site equivalent

heading-1

1.9

2.5

heading-2

1.6

2

heading-3

1.35

1.5

heading-4

1.2

1.25

heading-5

1.1

1.125

heading-6 [1]

1.0

1

literal

0.9

0.875

kbd

0.9

0.85

code

0.9

0.875

subscript

0.7

0.833, the browser’s smaller

superscript

0.7

0.8 — at which a superscript grows a body line too

figure-caption

1.0, regular oblique

0.9, regular upright in muted ink

admonition-title

1.0, bold

1.0, the medium face

badge

0.9, monospaced

1.0, a semi-bold face — the site sets the type badge’s weight and never its size

id-chip

0.9

0.85, the same shape as kbd above

version-label

1.0, bold

1.0, regular — the site distinguishes the label by colour alone

So there are fifteen differences to write, not sixteen: the site also sets a heading’s LINE HEIGHT differently (1.25 against this builder’s 1.4), and there is no key for that. The remaining constructs agree — quote-attribution among them, measured rather than assumed: the site sets an attribution in the oblique face at the size around it, which is this builder’s default too; and math, whose 1.21 is the site’s own (its maths stylesheet sets every formula at 1.21em). The heading rule, the two placeholder rows and the title page’s own display line have no counterpart on the site at all.

fonts

Type: array of string (optional, default: [])

Added in version 0.36.0.

Extra font files for the characters the built-in faces do not cover.

Paths are relative to this ubproject.toml (or absolute). Each file is read when the document is built and added to its fallback chain after the built-in faces (the sans and the monospaced ones), in the order listed, and before the bundled emoji face (see Emoji), in prose and in code alike.

[build.pdf]
fonts = ["fonts/icons.ttf", "fonts/NotoSansJP-Regular.otf"]

The built-in faces still win every character they cover, in prose and in code alike. A listed font is consulted only for a character none of them has, so there is no need to list the built-in faces and no way for a listed font to take over body text by accident. For the same reason this key does not change the typeface of your prose or your code: it is coverage, and face selection is typography.font and typography.font-mono, which name one of the families the files listed here declare. Where two listed fonts both cover a character, the first one listed draws it — which is what the order is for.

Listing one family as several files is the normal thing to do. A family’s regular file and its bold file declare the same family name and differ only in their attributes, and both are used: the bold span draws from the bold file. That is the arrangement to prefer for a text face you want at several weights (see the variable-font note below).

What cannot work is a second file in a slot the family already has — the same family, width, style and weight as a file listed before it. The earlier face is always at least as good a match, so the later one could never be chosen; it is refused, reported as build.pdf_font_unreadable naming the slot it clashes on, rather than loaded where nothing would consult it. If you hit this and the second file really does carry different glyphs, give it a family of its own. A file declaring the name of a built-in family (ubc-sans, ubc-mono or ubc-emoji) is refused for the same reason and on the name alone: those families already hold every slot the layout asks for. Rename the family inside such a file. A file listed twice is simply de-duplicated and costs nothing.

A file may declare several families, and a .ttc collection usually does; each of them joins the chain, in the file’s own face order. A collision refuses the whole file, its other families included — the faces of a family that cannot be reached in your prose would still be reached inside pictures, and a document whose text and diagrams disagree about what they can draw is worse than one missing face.

A font listed for coverage is best given a family of its own. Within one family, the shaping stack consults at most two faces for any one span — the best match for the style that span asks for, and the family’s default face. So a character that exists only in, say, the italic file of a four-face family is not found in a paragraph of regular text: the regular face is the better match, it does not have the character, and the italic one is never asked. A character in the family’s own default face is still found from any span, which is the other half of the same rule. This does not affect the ordinary arrangement above, where a family’s faces all carry the same characters and differ only in weight or slope. It matters when a file is listed to supply characters, which is what this key is for: put those characters in a font that stands alone — an icon, emoji or CJK face normally is one — rather than in one style of a family whose other styles do not have them. A character left out this way is reported, and its message says what happened — a font this build carries has this character, but not in a face this text could use — with (a listed font has it only in a style this text does not use — …) after the codepoint, or, for a private-use character, (a private-use character a listed font defines only in a style this text does not use — …) — the shape of an icon family whose regular and solid files share one family name. A character can also be lost when another face drew its cluster — a combining mark only a listed font has, after a letter the built-in sans has, the second consonant of a conjunct, or an icon followed by a variation selector (U+FE0F), which a copy-paste leaves behind it and an icon font normally does not map — and that loss gets its own message, the face that drew its cluster does not have this character, with a clause naming the face that has it and asking for a font that has the whole cluster. A character lost both ways in one document keeps the first message, with a clause true of both: (a listed font has it in a face this text did not reach — …), (a built-in face has it in a face this text did not reach; …) for a built-in face’s character, or (only the built-in monospaced face has it, which this text did not reach; …) for the monospaced face’s — except a private-use character, which keeps the style clause above.

The characters this is for are the ones a PDF cannot otherwise print:

  • a private-use character, such as an icon font’s glyphs. These have no meaning outside the font that defines them, so the built-in faces map no private-use codepoint and this key is the only route by which one can draw. A site built with ubc build html loads such a font through a CSS @font-face rule, which the PDF builder cannot see;

  • CJK, Thai, Khmer or Lao text — coverage the builder does not carry, because the files are large;

  • emoji beyond the bundled set. A minimal monochrome set is carried (Emoji), and a fuller emoji font listed here draws every emoji it covers that the built-in text faces do not draw themselves, with the bundled face filling the rest.

A character drawn from one of these files does not change the spacing of its line, and an inline literal holding one keeps the height of the code face’s box, as a browser sets them; the character itself may reach a little above or below the line.

Text inside pictures takes the same faces: a chart, a gantt or an SVG whose labels need a listed font gets it, offered at the label’s own weight and slope as the prose beside it is, and a character no face draws there is drawn as a hollow box and reported with the prose’s.

What is embedded, and what it costs:

  • only the glyphs actually used are embedded, so a 16 MB CJK face adds kilobytes to a document that draws a handful of characters from it;

  • a colour emoji font works, and is embedded as one raster image per distinct glyph drawn — about 6 KB each, measured — so such a document grows per distinct emoji rather than per occurrence. A monochrome emoji face is outlines instead, and costs well under half that;

  • a variable font is drawn at its default instance. Text is positioned for the weight that was asked for and drawn at the default one, so a bold heading set in such a face is spaced as bold and inked as regular. For a text face you want at several weights, list the static instances (a regular file and a bold file) rather than one variable file — two files of one family are exactly what this key expects;

  • .woff and .woff2 are not loadable. They are compressed web-font containers rather than font files; supply the .ttf or .otf the site’s own web fonts were built from.

A file that cannot be read, or that is not a font this builder can load, is reported once per file as build.pdf_font_unreadable, naming the path and the reason, and is skipped — the document still builds, missing only the characters that font would have supplied.

The glyphs are embedded in the PDF and travel with every copy of it, so the fonts’ licensing is yours to satisfy: ubCode neither checks nor records what a listed file permits.

emoji

Type: bool (optional, default: true)

Added in version 0.36.0.

Whether the bundled emoji face draws emoji.

On unless emoji = false. The builder carries a minimal monochrome emoji set — 226 common emoji from Noto Emoji — and uses it last: after the built-in faces and after every file listed under fonts, for an emoji none of them has. So it takes no character over from another face outside such a sequence (see Emoji), and a listed emoji font draws every emoji it covers except the few the built-in text faces draw themselves (listed there).

[build.pdf]
emoji = false

With it off, the document and its pictures carry no emoji face, and an emoji no other face covers is reported as a missing glyph (build.pdf_glyph_unavailable) again — the setting for a project whose deliverables must carry no emoji. Emoji below lists what is bundled and how to bring a fuller set.

hidden_needs

Type: "full" or "collapsed" (optional, default: "full")

How a hidden need (:hide:) is printed.

full, the default, prints its whole card — fields, body and any need nested in it — as a visible need’s card is printed. collapsed prints only the card’s header line — its type, title and id — which is how a browser prints the HTML site’s page, where a hidden need’s card is closed. A label, a need or a footnote reference written inside a collapsed card’s body is not printed, so a link to one lands on that header line, and nothing in that body is reported as a finding of the build. Any other value is refused.

:hide: is not a confidentiality boundary in either mode: the need’s content is still published, collapsed, on the HTML site (see the need-card differences).

[build.pdf]
hidden_needs = "collapsed"
plantuml.*

The external PlantUML renderer — off by default.

The same six keys as [build.plantuml] (enabled, needflow, needsequence, command, timeout, min_version). This table inherits every key it does not set from [build.plantuml] and wins on every key it does, enabled = false included — see Sharing the renderer between builders for naming the executable once for the whole project, and External PlantUML renderer for the whole surface, the requirements and the trade-offs.

[build.plantuml]
command = ["java", "-jar", "{config_dir}/libs/plantuml.jar"]

[build.pdf.plantuml]
enabled = true

With it configured, a needuml, needarch, uml or plantuml diagram is drawn into the PDF as a real picture, from the same content-addressed store ubc build html renders into — so a diagram both builders draw is rendered once between them. Without it, each of those diagrams is a placeholder box showing its source (Which diagrams draw on a default install has the whole table).

needflow and needsequence additionally need their own keys (needflow = true / needsequence = true, documented with the html table, off by default in both builders): those diagrams are ubCode’s own Mermaid rather than PlantUML you wrote, so enabling the executable does not by itself change the engine they are drawn with (a needflow’s own :engine: plantuml, or [needs] flow_engine, routes it there too). One the executable draws becomes a real picture with its caption under it; one it does not keeps its placeholder box, the caption under the box, and shows no source: the diagram is generated, not written, so there is nothing of the author’s to print.

mermaid.*

Drawing mermaid diagrams in a browser you already have — on by default.

The same keys as [build.mermaid] (browser, timeout, min_version, browser_args), plus enabled, which lives only here. This table inherits every key it does not set from [build.mermaid] and wins on every key it does. See Mermaid diagrams for the whole pathway.

highlight.*

Syntax highlighting for code blocks and inline code — on by default.

Two keys, enabled and aliases, either of which can be written here or once in [build.highlight] for the whole project. This table inherits every key it does not set from [build.highlight] and wins on every key it does — the same inheritance plantuml.* above has.

[build.pdf.highlight]
enabled = false
enabled

Colour the code in a code-block, a literalinclude, a fenced block and an inline chip that names a language. Defaults to true. false draws every listing and chip in one colour — the document’s own code-fg — as this builder did before it could highlight. It does not affect the line-number gutter, and it does not affect the band an :emphasize-lines: option draws behind a line.

aliases

Extra names for languages, on top of the built-in table. A table of string → string, empty by default.

[build.highlight]
aliases = { mylang = "python", ourdsl = "text" }

The value must name a language the build already knows — any name from Languages — or one of text, none, plain, plaintext, raw and output, which draw the block plain and report nothing. A value that names nothing is reported as config.highlight_alias_unknown, once per entry, and under the default --deny warning that stops the build. An entry wins over a built-in name of the same spelling, so default = "text" draws a block whose language is default — Sphinx’s fallback name, reached through .. highlight:: default or [parse] highlight_language = "default" — plain instead of as Python. Values are looked up once: a = "b" with b = "python" resolves a to b, which is not a language, and is reported. A format’s own table that writes aliases at all REPLACES the shared map for that format rather than adding to it.

The colours are not here. They are twenty-one hl-* roles in the theme.colors table above, beside every other colour the document is painted in, and they carry the site’s own token names and values.

Both builders read this table. ubc build html colours code during the build too, with the same engine and the same language table, so a name means the same thing on both surfaces and so does a decision to turn colour off. Write it once in [build.highlight] and it applies to the site as well. The VS Code preview reads this table too — the shared one. An alias you write in [build.highlight] colours the editor as well as both builds, but one written only under [build.pdf.highlight] does not: the preview reads the same expression ubc build html reads, so the editor and the site cannot disagree about a render they share. See Code blocks and Code blocks are syntax highlighted for what the PDF draws.

Mermaid diagrams

Added in version 0.37.0.

ubc build pdf draws every authored mermaid diagram — the .. mermaid:: directive and the MyST {mermaid} fence, inline or from a file (.. mermaid:: diagrams/flow.mmd) — as a real picture, with ubCode’s own Mermaid 11.17.2, the release the html site draws with, inside a Chromium-family browser you already have: Chrome, Edge, Chromium, Brave, or a chrome-headless-shell. No node, npm package or Mermaid CLI is involved, and ubCode ships no browser: without one, each diagram stays a placeholder box showing its source. A copy of a diagram shown by a needextract (or another view of a need) stays a placeholder box, as a PlantUML diagram’s copy does; the diagram itself is drawn where it is written.

It is on by default and needs no configuration where Chrome or Edge is installed. The build says how many diagrams it drew, how long that took, and which browser drew them and, when it ran, where it came from:

Mermaid: 2 diagrams drawn in 0.84s with Google Chrome 154.0.8037.58 at
/Applications/Google Chrome.app/Contents/MacOS/Google Chrome (found at an install location).

The line appears only when some diagram has a picture, and takes one of four forms:

  • Mermaid: 2 diagrams drawn in 0.84s with … — the browser drew them this build;

  • Mermaid: 1 diagram drawn in 0.75s (1 served from the cache) with … — it drew some, and the cache held the rest;

  • Mermaid: 2 diagrams served from the cache (drawn earlier with Google Chrome 154.0.8037.58). — the cache held every one, so the browser drew nothing this build and the line gives no duration;

  • Mermaid: 2 of 3 diagrams have a picture (1 drawn this build in 1.20s, 1 served from the cache) with … — the browser stopped or failed part of the batch, or drew nothing it was asked for; a finding says why.

The duration is the whole Mermaid step: finding and identifying the browser, starting it, drawing, and writing the cache. It is the mermaid figure of the --verbose Timings: line, and most of it is the browser’s start-up, which is why --no-cache — which draws every diagram again (see How it is drawn, below) — always pays it. The counts are of distinct diagrams: the same source written at two places is one diagram, drawn once. A cached picture that is damaged is drawn again, and counts as this build’s.

Which browser

The first of these wins:

  1. the browser the UBCODE_MERMAID_BROWSER environment variable names — the machine’s choice, so it beats a checked-out repository’s. It is a path, not a name to look up: a relative value is taken from the directory the build runs in, and a blank one counts as unset;

  2. [build.mermaid] browser: a path, taken relative to ubproject.toml ({config_dir} is replaced with its directory), or a bare name, looked up on the absolute entries of PATH;

  3. a browser found at its usual install location — Chrome, then Edge, Chromium and Brave, then Chrome’s Beta, Dev and Canary channels: under /Applications and ~/Applications on macOS; under %ProgramFiles%, %ProgramFiles(x86)% and %LocalAppData% on Windows, then the registry’s App Paths for chrome.exe and msedge.exe; under /opt and the snap and flatpak locations on Linux;

  4. a browser on an absolute entry of PATH (google-chrome, microsoft-edge, chromium, brave-browser and their variants; chrome.exe and msedge.exe on Windows). An empty, . or other relative entry is never searched: it would resolve against the directory the build runs in, so a checked-out repository could have its own executable run by a build that never asked for one.

Windows needs nothing: Edge comes with every Windows 10 and 11, and is found where it is installed. For the quietest, fastest build, point browser or UBCODE_MERMAID_BROWSER at a chrome-headless-shell — the headless-only build from Chrome for Testing, which starts in about half the time (measured on one Mac: one diagram in 0.6 s against 1.2 s for Chrome, a hundred on four pages in 2.3 s against 2.7 s):

[build.mermaid]
browser = "{config_dir}/tools/chrome-headless-shell/chrome-headless-shell"

The browser must be version 131 or newer; min_version can raise that bar, never lower it. A Linux snap Chromium is looked for but has not been verified to run under ubCode.

In a container, or when the build runs as root, Chromium’s sandbox often cannot start, and the build says so with the browser’s own first line of error output. browser_args = ["--no-sandbox"] is the usual remedy there; it turns a protection of the browser off, so set it only where it is needed — ubCode never adds it for you.

To stop ubc build pdf looking for a browser at all:

[build.pdf.mermaid]
enabled = false

For a single run, setting the environment variable UBCODE_MERMAID_DISCOVERY to off, 0, false or no (in any case) stops the search of install locations and PATH for a browser, and a Mermaid diagram’s placeholder then says so; on, 1, true and yes leave it on, and any other value leaves it on and says so. A browser that browser or UBCODE_MERMAID_BROWSER names is still used, and a bare browser name is still looked up on PATH.

How it is drawn

One browser draws every diagram of the build, on up to four pages at once, so a hundred diagrams take a few seconds rather than minutes, and one diagram that fails is reported on its own while the rest are drawn.

  • The browser is started headless, on a throwaway profile that is removed after the build, and ubCode drives it over a pipe: no port is ever opened, and the browser does not outlive the build.

  • No diagram runs script: Mermaid’s securityLevel is strict, and no Mermaid configuration of the project’s reaches it — ubCode’s own settings are the only ones applied. A diagram’s own %%{init}%% still applies to that diagram, but cannot loosen the security level.

  • Nothing a diagram asks for is fetched. An image node, a themeCSS url() or an HTML label with an <img> would make the browser fetch a URL on the machine running the build; none of those requests leaves the browser, and the diagram is reported instead. The page has no file of its own, so a diagram cannot read local files either.

    Behind “no script”, four controls each stop part of what a page could attempt, and none covers everything alone: every request is intercepted (only data: and about:blank continue), the page’s policy is connect-src 'none', every host name resolves to nothing (--host-resolver-rules, which also silences a branded browser’s own update, account and time services), and WebRTC may send no UDP (--force-webrtc-ip-handling-policy and --webrtc-ip-handling-policy, one for the headless shell, one for a full browser). Measured against a local listener, each control alone (✓ stopped):

    What a page tries

    interception

    connect-src

    resolver rule

    WebRTC policy

    a picture (image node, themeCSS, an HTML label’s <img>), Image

    ✓

    ✓

    fetch, EventSource

    ✓

    ✓

    ✓

    a WebSocket

    ✓

    ✓

    navigator.sendBeacon

    not reliably

    ✓

    ✓

    window.open

    ✓

    WebRTC to an IP address

    ✓

    The whole posture together let nothing out on every browser measured: the headless shell 154, Chrome 154 and Chrome for Testing 131 and 154 on macOS, and, in ubCode’s CI, the pinned headless shell 154 on Linux, Windows and macOS, Chrome for Testing 154 on Linux and macOS, Chrome 153 on Linux and Edge 152 on Windows. Chromium and Brave take the same switches but have not been measured, and a machine-wide enterprise policy for WebRTC sits above the command line.

  • A diagram is drawn the same way every time: in UTC and US English, with Mermaid’s random choices fixed, and with “today” (the line a gantt draws) set to the build’s date — SOURCE_DATE_EPOCH’s when it is set, otherwise the day the build runs.

  • Labels are drawn as SVG text (htmlLabels: false), measured in the same font the PDF draws them in — the document’s [build.pdf.typography] font, or the built-in DejaVu Sans — so a label fits its box. A diagram that sets its own fontFamily is measured in that font by the browser and drawn in the PDF’s, so its labels may not fit.

Drawn diagrams are cached in the project’s cache directory, beside the PlantUML ones, keyed by the diagram, ubCode’s Mermaid, the browser (its product, version and file name), the operating system and architecture, the font, browser_args — and, for a gantt only, the date, so a new day draws the gantts again and nothing else. A rebuild of unchanged diagrams starts no browser — unless a diagram failed last time: failures are not cached, so a browser starts on every build while any diagram fails. --no-cache draws every diagram again and refreshes the cache. Drawings are keyed by browser and operating system, not by the machine’s fonts: text in characters the PDF’s font does not cover falls back to the drawing machine’s own fonts, so a cache directory shared across machines — an actions/cache keyed only by the runner’s operating system, say — can serve a drawing whose fallback glyphs came from another machine. A browser that cannot tell its version without being started (Chrome for Testing’s chrome-headless-shell.exe on Windows) is started once to ask, and the answer is remembered in the cache. Every [build.mermaid] and [build.pdf.mermaid] key is part of the project’s configuration hash, so editing one re-indexes the project and draws every Mermaid diagram again.

The keys

[build.mermaid] holds the keys every builder shares, and [build.pdf.mermaid] the PDF’s own, which inherit every key they do not set from [build.mermaid]. enabled exists only in [build.pdf.mermaid].

Key

Default

Meaning

enabled

true

Draw mermaid diagrams. false never looks for a browser.

browser

""

The browser executable; "" looks for one (above). A path is taken relative to ubproject.toml, and {config_dir} is replaced with its directory; a bare name is looked up on the absolute entries of PATH.

timeout

30

Seconds ONE diagram may take. One that takes longer fails on its own; 0, and more than a day (86 400), are refused.

min_version

""

The oldest browser version accepted (131 built in; raise-only).

browser_args

[]

Extra switches for the browser, such as --no-sandbox.

browser_args cannot undo one of ubCode’s controls. A switch that would — a debugging port, another profile or proxy, the host resolver rules, the WebRTC policy, web security, an extension or app, headless mode, or the popup blocker — is refused in every spelling (--name, --name=value, -name, and /name on Windows, with spaces around it trimmed as the browser trims them), reported as build.mermaid_exec_failed, and never reaches the browser; so is an argument that is not a switch at all. An accepted switch is passed on trimmed, so the browser reads exactly what was checked. The refused switches are:

  • --remote-debugging-port

  • --remote-debugging-pipe

  • --remote-debugging-io-pipes

  • --remote-debugging-address

  • --remote-allow-origins

  • --user-data-dir

  • --host-resolver-rules

  • --proxy-server

  • --proxy-pac-url

  • --proxy-bypass-list

  • --disable-web-security

  • --allow-file-access-from-files

  • --headless

  • --force-webrtc-ip-handling-policy

  • --webrtc-ip-handling-policy

  • --profile-directory

  • --disable-popup-blocking

  • --load-extension

  • --disable-extensions-except

  • --load-apps

  • --load-and-launch-app

Diagrams the PDF cannot draw

A diagram that draws its text as HTML cannot be drawn in the PDF, and is refused rather than drawn without its labels: a journey diagram, and any diagram that turns htmlLabels back on, by a %%{init}%% directive or by its front matter’s config:. A zenuml diagram is not part of ubCode’s Mermaid, and fails as Mermaid reports it (“No diagram type detected”). Each keeps its placeholder box, with a build.mermaid_render_failed warning saying why. Mermaid’s C4 diagrams are drawn; ubCode repairs the one declaration Mermaid leaves out of them.

Mermaid draws the PDF’s labels as SVG text, and three of its limitations there show:

  • HTML inside a label is not markup: <br> still breaks the line, but another tag such as <b> is drawn as written. A Markdown string — A["`**bold** text`"] — draws bold in the PDF and on the site alike.

  • A mindmap’s root label can be cut off by the edge of its circle.

  • A gantt is laid out 800 pixels wide, so over a long span its date axis can crowd its labels.

What is reported

Code

When

build.pdf_diagram_unrendered

The diagram is a placeholder box, and the warning says why: no browser was found (install Chrome or Edge, or set browser), enabled = false, UBCODE_MERMAID_DISCOVERY=off with no browser named, or the browser could not be used this build, could not be started, or stopped before it drew these diagrams (the whole-build finding below that says why). Also a view copy of a diagram (needextract), with no reason added.

build.mermaid_render_failed

One diagram was not drawn: Mermaid rejected it (the message is Mermaid’s own first line — an image it could not load among them), what it drew refers to something outside itself (the message names the URL, which was not fetched), it took longer than timeout, or it was refused (above). Reported at the directive; the diagram keeps its box.

build.mermaid_exec_failed

The browser that browser or UBCODE_MERMAID_BROWSER names could not be used — not there, not starting, stopping mid-build (it then says how many distinct diagrams have a picture, and how many of those this build drew) — or a key cannot be used as written (timeout = 0 or more than a day, a bare browser name on no absolute PATH entry, a min_version that is not a version or is below the built-in minimum, a refused browser_args switch): these are reported in a build with a Mermaid diagram, whether or not a browser is found. Also a browser that was found failing because of a key the project set — the browser_args that reached it, when it would not start or stopped, or the timeout, when the batch ran out of time — naming that key: the project asked for it.

build.mermaid_version_unsupported

The named browser is older than the accepted minimum — or a found one is older than the min_version the project set.

build.mermaid_version_unparsed

The named browser reports a version ubCode cannot read.

build.mermaid_timeout

The whole Mermaid batch stopped responding and was stopped.

build.mermaid_discovered_unusable

A note, not a warning: a browser that was found — one nobody named — could not be used, or stopped short (it then says how many distinct diagrams have a picture). It says what was found and why. The diagrams’ own build.pdf_diagram_unrendered warnings still stand.

build.mermaid_request_blocked

A note: the browser was refused a request that no failed diagram explains. It lists the URLs; nothing was fetched.

Everything but the two notes is a warning, so it fails the default --deny warning. Only authored mermaid diagrams are drawn this way: a needflow or needsequence drawn with ubCode’s own Mermaid, and a uml converted to Mermaid, are still placeholder boxes in the PDF.

Smart quotes

Added in version 0.36.0.

The PDF typesets prose with the site’s keys, [build] smartquotes and smartquotes_action: the same text is educated, in the same quote characters ([project] language), and the same constructs are left as written. The cover, the metadata title, the outline and the running header name a document by its educated title, as the site’s <title> does. Only the document’s text is educated — nothing the layout draws itself, such as the table of contents’ dot leaders.

Emoji

Added in version 0.36.0.

ubc build pdf carries a minimal emoji set of its own: 226 sequences, monochrome, from Noto Emoji 3.002 — every one of them draws in every PDF with no configuration, and so do the 21 sequences built only from their parts (the last row). Its face comes last in every font order, after the built-in text faces and after every file listed under fonts, so it draws only an emoji that none of them has. One face draws each sequence, though: where the bundled face covers more of a ZWJ sequence than the text face does, it draws the whole sequence, and a member only the text face has is left out and reported — the couple with heart (U+1F469 U+200D U+2764 U+FE0F U+200D U+1F468) prints both people without the heart. A code span falls back through the same faces, after its monospaced one, so a sequence there is drawn by the face that would draw it in prose. Text inside pictures follows the same order.

What is bundled

Category

Sequences

Status and marks (28)

‼️ ✅ 🎯 🚦 🏁 ✔️ ☑️ ❌ ✖️ ❎ ⚠️ ℹ️ ❗ ❓ ❕ ❔ ⁉️ 🚫 ⛔ 🛑 🚧 🚨 💯 🆕 🆗 🆙 🔜 🔝

Shapes and colours (28)

🔴 🟠 🟡 🟢 🔵 🟣 🟤 ⚫ ⚪ 🟥 🟧 🟨 🟩 🟦 🟪 🟫 ⬛ ⬜ 🔶 🔷 🔸 🔹 🔺 🔻 🔘 ✳️ ✴️ ❇️

Arrows and media controls (26)

➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↔️ ↕️ 🔄 🔁 🔀 🔃 ⤴️ ⤵️ ▶️ ◀️ ⏩ ⏪ ⏭️ ⏮️ ⏸️ ⏹️ 🔼 🔽

Stars, highlights and markers (16)

✨ 📌 ⭐ 🌟 💫 🔥 💡 📍 🔖 🏷️ 🔗 📎 ➕ ➖ ➗ 🟰

Hands (16)

👌 👍 👎 👉 👈 👆 👇 ✋ 👋 👏 🙏 🤝 ✌️ 💪 ☝️ 🤞

Faces (16)

😀 🙂 😊 😉 😐 😕 🙁 😢 😅 🤔 😎 🙃 😬 🥳 😱 🤯

Objects (80)

🐛 📚 🔧 🧪 ♻️ 🤖 🧩 🖥️ 🚀 🧭 ⏳ 📝 📋 📊 📈 📉 📦 🛠️ ⚙️ 🔨 🔩 🧰 🧱 🔒 🔓 🔑 🔐 🔍 🔎 🔬 🔭 ⌛ ⏱️ ⏰ 📅 🗓️ 🗑️ 💬 📣 📢 🔔 🔕 📄 📃 📁 📂 🗂️ 💻 ⌨️ 🖱️ 🌐 ☁️ 🔌 🔋 📡 🐞 🦀 🐍 🎉 🎨 ✏️ 🖊️ 📜 📖 🧠 👀 👤 👥 🏗️ ⚡ 🌙 ☀️ 💾 🧾 📐 📏 ⚖️ 🛡️ 🧹 🪲

Keycaps (13)

#️⃣ *️⃣ 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟

Coder sequences (ZWJ) (3)

🧑‍💻 👨‍💻 👩‍💻

Also drawn, built from the parts above (21)

🙂‍↔️ 🙂‍↕️ 🧑 👨 👩 🧑‍⚖️ 👨‍⚖️ 👩‍⚖️ 🧑‍🔧 👨‍🔧 👩‍🔧 🧑‍🔬 👨‍🔬 👩‍🔬 🧑‍🎨 👨‍🎨 👩‍🎨 🧑‍🚀 👨‍🚀 👩‍🚀 🧑‍🤝‍🧑

Noto Emoji draws each profession’s three sequences (🧑, 👨 and 👩 with 💻 ⚖️ 🔧 🔬 🎨 🚀) with one gender-neutral glyph, so the 247 sequences it draws are 235 pictures.

The set is minimal on purpose: it is what a technical document commonly writes — this project’s own commit and changelog emoji among them — in about 100 KB of font data (about 62 KB in the binary), where nearly the whole monochrome repertoire would cost about 0.9 MB and a colour face about 10 MB. The emoji is drawn in the bundled face’s regular weight, also inside a bold heading.

Drawn by the text face, not by the emoji face

Some of the characters above are also in the built-in DejaVu text face, and a built-in face wins every character it has, except inside a ZWJ sequence the bundled face covers better (below). These draw in DejaVu’s own monochrome style — the same glyph with or without a variation selector (U+FE0F) asking for emoji presentation:

‼ ♻ ✔ ☑ ✖ ⚠ ℹ ⁉ ⚫ ⚪ ✳ ✴ ❇ ➡ ⬅ ⬆ ⬇ ↗ ↘ ↙ ↖ ↔ ↕ ▶ ◀ ✌ ☝ ⚙ ⌨ ☁ ✏ ⚡ ☀ ⚖

and the faces 😀 😊 😉 😐 😕 😢 😅 😎 🙃 😱. Inside a code span the monospaced text face draws fewer of them, and the sans text face draws the rest — ℹ, ⚫, ⚪ and those faces — as it does in prose: the emoji face draws none of them anywhere.

What is still reported

An emoji outside the set is omitted and reported as build.pdf_glyph_unavailable, named by its codepoint and followed by:

(an emoji outside the bundled set; list a fuller emoji font under [build.pdf] fonts — see the Emoji section of the [build.pdf] configuration page)

That covers most of the repertoire, and four shapes worth knowing:

  • a skin-tone modifier (U+1F3FB to U+1F3FF). The set draws the thumbs-up but not the modifier after it, so that sequence prints the thumb and reports the modifier;

  • a flag, which is a pair of regional indicators: the set has none, so both halves are reported;

  • a sequence the set draws in part, such as a ZWJ family: the members it has are drawn and only the missing ones are reported;

  • a character inside a sequence another face draws. One face draws each sequence: the first in the order that has all of it, else the one that covers most of it, its joiners counted, and on a tie the earlier one. In the heart on fire (U+2764 U+FE0F U+200D U+1F525) DejaVu and the bundle cover as much, so DejaVu draws the heart and the fire is reported, although the set draws it on its own; in the couple with heart the bundle covers more, so it draws both people and the heart DejaVu has is reported. Their messages say which face has the character and why it is missing:

    U+1F525 ‘🔥’ (an emoji the bundled set has, but its sequence was drawn by another face; list an emoji font with the whole sequence under [build.pdf] fonts): the face that drew its sequence does not have this character, so it was omitted at 1 site
    
    U+2764 ‘❤’ (an emoji the built-in text faces have, but its sequence was drawn by another face; list an emoji font with the whole sequence under [build.pdf] fonts): the face that drew its sequence does not have this character, so it was omitted at 1 site
    

    Listing an emoji font that has the whole sequence draws all of it.

A few characters Unicode counts as emoji although they are usually written as text — such as U+24C2, U+3030, U+303D, U+3297 and U+3299 — are named with the emoji message too when no face has them.

CJK, Thai, Khmer and Lao text is not bundled either, and keeps the ordinary advice to list a font that has it.

How to add your own set

List an emoji font under fonts:

[build.pdf]
fonts = ["fonts/NotoColorEmoji.ttf"]

A listed file wins every emoji it covers except the characters the built-in text face draws itself (listed above), and the bundled face fills the rest, so a colour face and the monochrome bundle can appear in one document — the bundle drawing only what the listed file lacks. With Noto Color Emoji listed, for example, 🤔 and ✅ print in colour while 😀 and ⚠️ keep DejaVu’s monochrome glyph. That is true of all 44 characters listed under “Drawn by the text face” above, and of a variation selector’s request for emoji presentation (⚠️ is U+26A0 U+FE0F): the text face keeps them even with a colour font listed, in prose and in code alike — inside a code span the sans text face draws the ones the monospaced face lacks. Choosing a face by the presentation a sequence asks for is a named follow-up, not a setting today. The file keeps its own family name; one that declares ubc-emoji is refused, as the fonts entry says.

Where to get one

  • Noto Emoji — monochrome, nearly the whole repertoire, SIL Open Font License 1.1, one file of about 2 MB (NotoEmoji[wght].ttf). It is a variable font and is drawn at its default weight (see the variable-font note).

  • Noto Color Emoji — colour, SIL Open Font License 1.1. Its bitmap (CBDT) build is about 10 MB, and such a colour face is embedded as one raster image per distinct glyph drawn (see what it costs); the numbers are that build’s.

Whichever you list, the glyphs used travel inside every copy of the PDF, so the fonts’ licensing is yours to satisfy.

No emoji at all

For a project whose deliverables must carry no emoji, turn the bundled face off:

[build.pdf]
emoji = false

The PDF and its pictures then carry no emoji face, and every emoji no other face covers in the text is reported, with a message saying the bundled set is off. Inside a chart or a diagram label such an emoji is drawn as a hollow box, and reported with the text’s.

Unrendered constructs

Some constructs are not typeset yet. Each is drawn as a labelled placeholder box — or, inline, as a mono chip — and counted on a build.pdf_* warning naming it, so the document stays honest and the counts are a backlog rather than a mystery.

No key here turns that off or changes it: it is a property of the builder’s coverage at this release, not a setting. The list, the codes and what to do about them are on the guide page: What is not rendered yet.

That is one list in one place, deliberately. It is the same shape ubc build html uses in reverse — there, the coverage warnings are documented on this configuration page and the guide links to them — and it is the placement the list itself argues for: not one of these findings is a [build.pdf] key’s, and duplicating the list across two pages would guarantee that the two drift.

The list is checked against the code, not maintained by hand alone: a test in ubc_build_pdf asserts that every construct class it draws as a placeholder is named on that page, and the per-node matrix behind it lives with the code in rust/ubc_build_pdf/design/coverage.md.