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
1when 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--denycommand-line flag overrides the key for one run;"none"never fails the threshold (an explicit--max-warningsbudget 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) orletter(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
landscapeis 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:
tableoffloat(optional, defaults below)The page margins, in millimetres. Each of
top,right,bottomandleftdefaults to20.0and 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
headerandfooterbelow 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.0and24.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.0sets 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] versionwhen 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 theSOURCE_DATE_EPOCHenvironment 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
toctable when one is printed. So the first body sheet prints1under 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_sizeor a longlineslist below — therefore renumbers: its first body sheet now prints1where it printed2, 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-pageand--no-title-pagedecide 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 whentitle_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
logoAn 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_heightHow 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 than0and at most100.0; a value outside that is refused when the configuration is read. It does nothing on its own when nologois set.subtitleA 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.authorA 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 assubtitledoes.linesExtra 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://ormailto:— 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.titlePrint the project’s name as the cover’s title. On unless
title = false. Turn it off when thelogois 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.versionPrint the
Versionline. On unlessversion = false. The line is absent anyway when[project] versionis unset, so this turns off a line a project that HAS a version would get.datePrint the date line. On unless
date = false. The date comes only from theSOURCE_DATE_EPOCHenvironment variable, so the line is absent anyway on a build that pinned none.countsPrint 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-idor--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_heightthe picture is fitted into a fixed box — two fifths of the text width, and six timesbase_font_sizehigh — 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_heightthe 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_unreadablenaming 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
linesis a sanity limit and not a promise of one sheet — atbase_font_size = 24a 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 = falsetakes 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
enabledWhether the table is printed at all. On unless
enabled = false.depthHow many heading levels the table lists. Defaults to
3, and must be1or more. A project that wants every heading listed writes a large number.titleThe 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.
depthcounts levels from the root document’s title, which is level 1. So the default of3lists 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 from1on 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.--tocand--no-tocdecide it for a single run and beatenabledin 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
documentsWhich 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_levelStart every section heading at this outline level or above on a new page. Defaults to
0, which starts none;2starts every level-2 heading on a new page. Levels are counted astoc.depthcounts them. There is no upper bound.heading_keep_linesHow many lines of what follows a heading share its page. Defaults to
3, and must be1or more;1keeps only the next line with the heading, ororphanslines of a block long enough to split.orphansThe 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 be1or more.widowsThe fewest lines a page break carries onto the next page, counted the same way. Defaults to
2, and must be1or more.keep_introducersKeep 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
documentssays, 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_levelcounts 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 abreak-beforeclass 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. Arubricis not a section heading and never breaks.Keeps
heading_keep_linescounts 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, ororphansif 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 ownwidowsstill holds under the heading, so a block shorter thanheading_keep_linesandwidowstogether is not split there: it moves whole with its heading when the two fit a page, and otherwise the heading keeps what fits.orphansandwidowshold 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.1lets a split leave a single line — or row — on that side, so1for 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 largeorphansorwidowsmeans “split only what no page can hold”.keep_introducers = falselets 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
headertable below.running_header = falseempties 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, unlessdivider.show = false).- header.*
What is printed in the top margin.
Added in version 0.36.0.
Three slots —
left,centerandright— with the band’s type beside them and adividersub-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 whatrunning_headerhas always drawn. A band is always set in the built-in family, whatevertypography.fontselects: 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_EPOCHenvironment 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 adivider.weightheavier 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— so0.9is 9.0 pt under the default 10.0 pt base, and a project that changes its base moves the band with it. Must be between0.1and3.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
sizevalue puts either band the same distance from the text: set both tables’sizeto 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:
normalorbold. 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.
showis abool, on for the header, andweightis the rule’s thickness in points, defaulting to0.5. A thickness must be greater than 0 and at most5.0: writeshow = falserather than a zero thickness, so that there is one way to say “no rule”. It does not scale withsize— 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:centeris"{page} of {pages}", the running page number and the count of numbered pages in the file that was built, anddivider.showisfalse, because a page number is a fragment rather than a head. Like the header, a footer band keeps the built-in family whatevertypography.fontselects.{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 reads1 of 1, and a--need-idor--need-filterextract counts only its own pages — a three-page extract of a long project reads3 of 3on its last sheet, not3 of 400.Writing
center = "{page}"restores the bare number, andcenter = ""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
colorsis 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:
tableofstring(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,#rgbor#rrggbb.A palette entry may not refer to another entry: the references run one way, from
colorsintopalette, 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@brandresolve through an entry the file that wrote it never read. The same rule governs a-coverride:-c build.pdf.theme.palette.other=…replaces the file’s palette rather than adding to it.- colors
Type:
tableofstring(optional, default: empty)Added in version 0.36.0.
The colour each role is painted in. A value is a hex colour (
#rgbor#rrggbb), or@namenaming an entry ofpalette. 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 anextendchain, so a configuration that recolourslinkkeeps 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
fgBody text, and the rule struck through deleted text.
fg-mutedSecondary text: the running header and a need card’s id chip.
fg-borderThe stronger border colour, for boxes that read as edged: a topic’s or sidebar’s leading bar.
borderThe 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-softThe 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 iscode-bg.brandA need card’s type badge. A role of its own rather than a use of
link: the site declares--ubc-brandand--ubc-linkas separate tokens that happen to carry one value, so a project that recolourslinkalone 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.linkA link’s text.
link-underlineA 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-underlinetoken behaves the same way, and this repository’s own documentation PDF uses this treatment; the worked example below shows the shape.code-bgThe background of a code block, and of an inline literal. Not a key cap’s (
:kbd:), whose fill isbg-soft.code-fgThe 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-bgThe 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
gototarget; 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 adiff, which has a text colour and a row background each (inside an inline chip, the text colour alone). A run no rule claims keepscode-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,todoOne 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-removedThe lead label’s text in a
.. versionadded::,.. versionchanged::,.. deprecated::or.. versionremoved::box.version-added-bar,version-changed-bar,version-deprecated-bar,version-removed-barThe leading bar of the same four boxes.
brand,code-bgandcode-fgare worth one sentence, because they are the three roles a reader would expect to followlink,bg-softandfg, and deliberately do not: they default to the values those three tokens carry today, and an unsetcode-bgdoes not take a themedbg-soft(nor an unsetcode-fga themedfg, nor an unsetbranda themedlink) — 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 recoloursbg-softsetscode-bgtoo if it wants the code ground to move with it, one that recoloursfgsetscode-fgtoo if it wants the code ink to move with it, and one that recolourslinkto rebrand its cards setsbrandtoo — 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-mutedgets a rebranded running head for free.Key
What it paints
header-textThe text of the top margin’s band. Paper only; unset, it follows
fg-muted.header-ruleThe rule under the top margin’s band. Paper only; unset, it follows
border.footer-textThe text of the bottom margin’s band, the page number included. Paper only; unset, it follows
fg-muted, asheader-textdoes. Writefooter-text = "#1a1c1e"for the body’s own ink: the built-in body ink as a literal, because@namenames a[build.pdf.theme.palette]entry andfgis a role, not a palette entry. A project that recoloursfgtherefore no longer recolours the page number with it — recolourfg-muted, or setfooter-textto the colour you want.footer-ruleThe rule above the bottom margin’s band. Paper only; unset, it follows
border.heading-ruleThe 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:coloursKey
What it paints
need-green-fill,need-red-fill,need-yellow-fill,need-blue-fill,need-gray-fill,need-discreet-fillThe background of a need card carrying that
:style:(greenalso covers its upstream aliasimplemented,redcoversopenandyellowcoversin_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-lineThe frame or trailing bar of a need card carrying the matching
*_border/*_barstyle. The set is asymmetric because the upstream palette is: there is noorange_borderand nodiscreet_bar.Chart colours
Key
What it paints
chart-1…chart-8The categorical palette a
needpieorneedbardraws 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-8The text of a value label drawn on that slot’s fill.
chart-halo-1…chart-halo-8The halo stroked under that label, so its glyphs keep an edge against the fill.
Not keys here.
bgandadmonition-bg— a printed page’s ground is the sheet;bg-hover,link-hover,link-visited,highlight-on-targetandtoc-active— paper has no pointer, no browsing history, no:targetand no reading position;announcement-*— the site’s banner has no printed form;content-width,content-gutter,header-heightandradius— lengths rather than colours, and the page’s measure comes frompage_sizeandmargins;fontandfont-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:
fontandfont-monoAdded in version 0.36.0.
fontis the family every paragraph, heading, table, card, caption and the cover are set in;font-monothe 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]. Afontwritten 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.fontsandfontare 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 forfont-mono. Those are the only questions asked of it:font-monodoes 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 warningbuild.pdf_font_unselectednames 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 warningfails on that warning, so a mis-named family is caught rather than shipped;--deny erroris 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
fontsfiles declare — so an icon or CJK font listed for coverage goes on doing its job. Forfont-monothe 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 ownfontsfiles 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
sizeis a factor, never a point size.base_font_sizeabove 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— multipliesbase_font_sizeitself, 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 is1.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. Asizemust be between0.1and4.0; a value outside that is refused when the configuration is read, which is what catches19typed for1.9.Every
weightnames a face,"normal"or"bold"— not a numeric weight, because the built-in family carries exactly two.italicselects 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,italicandrule— andheading-1…heading-6each carry asizeof their own and may override any of those. The precedence is one line: a key onheading-Nwins over the same key onheading, and either wins over the built-in value. Arulemerges key by key, so a level that states only aweightkeeps theshowthe shared table wrote.heading-6governs 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
headingweight,italic,rule"bold", off, no ruleEvery heading level a
heading-Ntable does not override.heading-1…heading-6size,weight,italic,rule1.9,1.6,1.35,1.2,1.1,1.0One heading level.
heading-6also governs every deeper level.bodyweight,italic"normal", offEvery 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
sizekey —base_font_sizeis the body size.codesize0.9A 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.literalsize0.9An inline literal, and an object description’s signature. Size only: it keeps the face of the sentence around it.
kbdsize0.9A
:kbd:key cap, and an option list’s option. Size only.badge,id-chipsize0.9A need card’s type badge and its id chip. Size only: both are set in
font-mono, and their colours are thebrandandfg-mutedroles in[build.pdf.theme.colors].linkunderlineon
Whether a link is underlined. See below.
subscript,superscriptsize0.7A
:sub:or a:sup:. Size only; how far the run is shifted off the baseline is not configurable.table-headsize,weight,italic1.0,"bold", offA table’s header cells.
captionsize,weight,italic1.0,"bold", offA table’s, a view’s or a code block’s caption.
figure-captionsize,weight,italic1.0,"normal", onA 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-titlesize,weight,italic1.0,"bold", offA need card’s title.
lead-insize,weight,italic1.0,"bold", offA non-section title, a sphinx-design component’s title slot, a tab’s label.
admonition-titlesize,weight,italic1.0,"bold", offAn admonition’s title.
definition-termsize,weight,italic1.0,"bold", offA definition list’s term.
quote-attributionsize,weight,italic1.0,"normal", onA block quote’s attribution line — the last paragraph of a quote, opened by two or three hyphens or an em dash.
placeholder-headlinesize,weight,italic0.9,"bold", offA placeholder box’s first line, naming what was not rendered.
placeholder-detailsize,weight,italic0.85,"normal", offA placeholder box’s detail lines, set in the monospaced family, so
italichas no effect on them.displaysize,weight,italic2.4,"bold", offThe title page’s display line. Whether that line is printed at all is
[build.pdf.cover] title.version-labelsize,weight,italic1.0,"bold", offA 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.weightis a thickness in points — it does not scale with the heading’ssize, because a hairline is a physical printing fact rather than a size of type — and it must be greater than0and at most3.0. Writeshow = falserather 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.0is 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 anybase_font_size. It is painted in theheading-rulecolour role, which followsborderwhile 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 = falseremoves 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] linkto 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-11.92.5heading-21.62heading-31.351.5heading-41.21.25heading-51.11.125heading-6[1]1.01literal0.90.875kbd0.90.85code0.90.875figure-caption1.0, regular oblique0.9, regular upright in muted inkadmonition-title1.0, bold1.0, the medium facebadge0.9, monospaced1.0, a semi-bold face — the site sets the type badge’s weight and never its sizeid-chip0.90.85, the same shape askbdaboveversion-label1.0, bold1.0, regular — the site distinguishes the label by colour aloneSo there are thirteen differences to write, not fourteen: the site also sets a heading’s LINE HEIGHT differently (
1.25against this builder’s1.4), and there is no key for that. The remaining constructs agree —quote-attributionamong 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:
arrayofstring(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_unreadablenaming 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-monoorubc-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
.ttccollection 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 htmlloads such a font through a CSS@font-facerule, 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;
.woffand.woff2are not loadable. They are compressed web-font containers rather than font files; supply the.ttfor.otfthe 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 underfonts, 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 = falseincluded — 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,umlorplantumldiagram is drawn into the PDF as a real picture, from the same content-addressed storeubc build htmlrenders 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.needflowandneedsequenceadditionally 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 (aneedflow’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,
enabledandaliases, 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 inheritanceplantuml.*above has.[build.pdf.highlight] enabled = false
enabledColour the code in a
code-block, aliteralinclude, a fenced block and an inline chip that names a language. Defaults totrue.falsedraws every listing and chip in one colour — the document’s owncode-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.aliasesExtra 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,rawandoutput, which draw the block plain and report nothing. A value that names nothing is reported asconfig.highlight_alias_unknown, once per entry, and under the default--deny warningthat stops the build. An entry wins over a built-in name of the same spelling, sodefault = "text"draws a block whose language isdefault— Sphinx’s fallback name, reached through.. highlight:: defaultor[parse] highlight_language = "default"— plain instead of as Python. Values are looked up once:a = "b"withb = "python"resolvesatob, which is not a language, and is reported. A format’s own table that writesaliasesat 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 thetheme.colorstable 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 htmlcolours 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 expressionubc build htmlreads, 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+1F3FBtoU+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+1F600toU+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.