PDF document

Added in version 0.35.0.

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 one exception being [build.pdf.theme.palette], 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, pagination, typography, running_header, header, footer, theme, fonts, emoji, plantuml 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.

Warning

ubc build pdf is in alpha. The command works and is under active development, but the typeset output, the theming surface and the command’s options may still change before it is announced as stable, so it is not yet recommended for production deliverables.

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. 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.

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 and the documents its toctree lists directly 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 and each document its toctree lists directly 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.

Added in version 0.36.0.

Two tables. [build.pdf.theme.palette] holds your own named colours, and [build.pdf.theme.colors] says which of the document’s roles each colour paints.

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.

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.

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, 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 listed families in order, sans families included: a code span can still be drawn in a proportional face where nothing else covers the character.

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:. Size only; how far the run is shifted off the baseline is not configurable.

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 non-section 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.

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

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 thirteen differences to write, not fourteen: 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. The heading rule, the subscript and superscript steps, 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, in the order listed, and before the bundled emoji face (see Emoji).

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

The built-in faces still win every character they cover. 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.

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 no built-in face can ever be right 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.

Text inside pictures takes the same faces: a chart, a gantt or an SVG whose labels need a listed font gets it, so the document and its diagrams cannot disagree about what they can draw.

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.

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.

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.

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.

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, and inside a code span, whose face has no joiner, so does the heart on fire. 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 those faces come from the emoji face there.

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 five 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, as it is in the heart on fire inside a code span. 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;

  • an emoji the body text face has and the code face lacks — most smileys in U+1F600 to U+1F64F — is left out inside code and reported, and its message still says that no font covers 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; inside a code span the monospaced face lacks ℹ ⚫ ⚪ and the ten faces, so a listed colour font draws those there. 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 not reported, as any uncovered character there is.

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.