Runs in flowing text: tags, Markdown and the list
Until 1.5 a paragraph was set in one face. A bold amount inside a sentence meant a table cell or a line assembled by hand from textWidth. Since 1.5 the string handed to text may carry its own bold, italic and bold italic words, underlined and struck ones, links, and headings of three levels, and the line breaker runs over all of it: every piece is measured in the face it is set in — a bold word is wider than the same word regular — and drawn with its own Tf, and a word that crosses a run boundary is still one word to the breaker and to the hyphenator. There are two notations for it, tags and Markdown, and one list underneath both.
What a run does not change: the size, the colour, the spacing. Those belong to the paragraph, which is what keeps every line one height. A heading does change the size, and that is why a heading is not a run but a paragraph of its own: 1.6, 1.3 and 1.15 times the block's size, bold, with a leading grown by the same factor, and never the last line of a page or column — where a height limit would cut right under it, the heading goes with the rest. In a tagged document it is an H1, H2 or H3 element. Kerning ends at a run boundary, because bold and regular have no pair table between them in any face.
A list item is a paragraph the same way. It is indented by 1.6 times the block's size on every line, with a label in front of its first line — •, or a number the package counts — right-aligned, half the size before the text, in the block's face and colour. Consecutive items of one kind are one list; a paragraph without the option, a heading or the other kind ends it, and the next list counts from 1 again. A run inside an item is nothing special. One level, no nesting. In a tagged document a list is an L element with ListNumbering Disc or Decimal, holding an LI per item with the label as Lbl and the text as LBody — the structure ISO 32000-2 asks for, checked green by veraPDF for PDF/UA-1 and UA-2. A tagged list is refused beside -paginate and -columns, whose marks are per page; untagged, both roads take it. The face has to have the bullet glyph U+2022, or the digits and the full stop a number needs — a face without them is refused by name (TCLPDF TEXT RUNS BULLET, NUMBER).
1. Tags
$doc text {The <strong>amount</strong> is payable by <em>31 October</em>; the <s>old price</s> no longer applies.} -at {20 40} -width 170 -align justify -markup tags
| Tag | What it sets |
|---|---|
<b>, <strong> |
bold |
<i>, <em> |
italic |
<u> |
underlined — a stroke in the text's colour, placed and weighted from the face |
<s> |
struck through |
<a href="…"> |
a link annotation over the run on every line it spans, underlined |
<p>; <h1>, <h2>, <h3> |
a paragraph; a heading, which is always a paragraph of its own |
<ul>, <ol> holding <li> |
a bullet list, a numbered list; every <li> an item paragraph |
Fourteen names, in any case, and nothing else between angle brackets is a tag. <b> and <strong>, <i> and <em> set alike; what differs is what a tagged document is told. A < in front of a space, a digit, = or the end is text — a < b, < 5 % — while a < in front of a letter or / has to open a known tag. The one escape is < for a literal < in front of a letter, with > and & beside it for symmetry; no other entity is read, and & on its own is text. A line feed stays a paragraph break exactly as without markup, so a caller who wants nothing but <b> changes nothing else, and a string without a single tag sets the bytes the plain block sets. Nesting is strict. A closing tag closes the innermost open one; a tag still open at the end, a paragraph tag inside a run and a heading inside a paragraph are refused; a tag repeated inside itself is allowed and does nothing. <ul> and <ol> hold <li> and nothing else; white space directly inside them is swallowed, a line feed inside an <li> is refused because an item is one paragraph, and what follows </ul> starts a new paragraph as after </p>. <a> takes href="…" in double quotes and nothing else takes an attribute. Every fault is refused with the name and the position: TCLPDF TEXT MARKUP UNKNOWN, NESTING, OPEN, HREF or ATTRIBUTE.
Why tags, and why these names. The vocabulary is closed, so the one character to watch is a < in front of a letter, where *, _, # and [ occur in every text. Opening and closing are explicit, so a fault is a refusal with a position and not a guess. The names are the structure vocabulary of ISO 32000-2, 14.8.4: in a tagged document <h1> is an H1 element, and <strong>, <em> and <a> name the Strong, Em and Link they stand for — carried with the run, not written as elements yet (see the url run below). And [Text](url) inside a double-quoted Tcl string is a command substitution.
2. Markdown
Text that already comes written in Markdown — from a ticket, a wiki page, a template kept by hand — is read with -markup markdown, the second notation, as strictly as the tags.
| Written | Set as |
|---|---|
**bold** |
bold, as <strong> |
*italic* |
italic, as <em> |
***both*** |
bold italic |
~~struck~~ |
struck through, as <s> |
[text](url) |
a link, as <a href="url"> |
#, ##, ### at the start of the string or after a line feed |
a heading 1, 2 or 3, reaching to the next line feed |
-, * or + at the start of a line |
a bullet item, reaching to the next line feed |
1., 2. … at the start of a line — the number does not matter |
a numbered item; the package counts |
That is the whole vocabulary. There is no underline — Markdown has no spelling for it, which is what -markup tags is for. Everything else Markdown knows is text, set as it was typed: nested lists, code (backticks are text, and what stands between them is read like any other text), images ( is not a link), quotes, tables and autolinks — and _, always, so that snake_case is no trap. A [ in front of a letter opens a link, so [1] and [sic] in running text are refused and are written \[1]; a list marker counts only in column one, so 5 - 3 and 12. March inside a line stay text, and a line that has to begin with one is written \- x or 12\. March; a backslash in front of *, ~, [, ], #, -, +, . or \ writes that character. There is no nesting by indentation: leading spaces belong to the item's text.
Strict, deliberately against CommonMark. A *, **, ~~ or [ still open at the end of the text is TCLPDF TEXT MARKDOWN OPEN, a closing delimiter that does not close the innermost open run is NESTING, [text] without (url) or with an empty address is LINK, and four or more # are HEADING — each with the position. A delimiter in front of a space or the end while no run is open is text (5 * 3), the counterpart of the < rule. A run that quietly falls back to text because of one forgotten * is the silence this package refuses everywhere else.
3. The list underneath
Both notations are translated into one thing, and -runs 1 takes that thing directly: a list of {text options} pairs. A program that assembles a paragraph from data — a name from one column, an amount from another — writes this list and needs no notation at all.
set runs [list "Runs in flowing text" {heading 1} \
"The " {} "amount of 1,234.56 EUR" {style bold} " is payable by " {} "31 October" {style italic} \
"; terms at " {} "fossil.sowaswie.de/tclpdf" {url https://fossil.sowaswie.de/tclpdf}]
$doc text $runs -at {20 34} -width 170 -align justify -runs 1
The options of a run are style bold, italic or both, underline and strike 0 or 1, url with the address, heading 1 to 3, and item bullet or number for a list item; a run with no options is {}. The two translators are plain procedures, ::tclpdf::markup::parse and ::tclpdf::markdown::parse, which answer the list a string becomes; they share the step that emits it, so the same paragraph written both ways sets the same bytes. textHeight and textLines take the same list, and textLines answers every line as such a list — the pieces of the line with the options they were set with — so example 01.17 does not claim that the notations agree, it measures it: both break into the same lines, runs and options alike, the underline apart. -markup and -runs 1 exclude each other, and runs need -width; a block set -direction rtl or ttb, or one beside -avoid, is refused under TCLPDF TEXT RUNS. Beside -width, -breakHyphen is taken for runs as well, provided the string stays one line at that width (TCLPDF TEXT BREAKHYPHEN lines) — that is how a table draws the lines of a cell.
A url run writes the link annotation and nothing more — no Link structure element, and not the Contents PDF/UA asks of every link annotation. A tagged document with a url run therefore cannot claim PDF/UA; the write names the annotations. A link that has to pass is set with link and structure Link. That was so in 1.5 already.
4. Cells
Since 1.6 a table cell takes the same runs. A cell dictionary holds runs — the list of pairs, as a program writes it from data — in place of text. markup is a style key like hyphenate, with the values plain (the default), tags and markdown: a column of descriptions says it once, a section (-headStyle …) or the whole table (-style) may say it, and a cell may say otherwise. The string always stays under text.
set client "Weber & Sohn"
$doc table -at {20 40} -width 170 -theme striped \
-head {{Date {text "**Activity**" markup markdown} Hours}} \
-body [list \
{2026-09-02 "**Schmidt AG** - the estimate of ~~4 h~~ *2 h* held" 2} \
[list 2026-09-04 [list runs [list $client {style bold} " - on-site training, " {} "invoiced separately" {style italic}]] 4] \
{2026-09-05 {text "<b>Schmidt AG</b> - <u>final acceptance</u> signed" markup tags} 1} \
{2026-09-06 {text "the **bold** markers are meant literally here" markup plain} 0.5}] \
-columns {{width 24} {markup markdown} {width 18 align decimal}}
The activity column is Markdown by its column description; the name from a variable goes in as a run, with nothing to escape; one cell says markup tags for the underline Markdown cannot spell, and one says markup plain and keeps its asterisks — plain and not an empty value, which on a cell means that the cell says nothing, as an empty align does. A text in which the notation finds nothing to mark is a plain cell, so the figures of a decimal column stay figures under a table that says markup markdown once. The cell is measured as it is drawn: a bold word is wider, the row is as tall as the lines the cell breaks into, the automatic column width measures the pieces the same way, and hyphenate breaks inside a run as it breaks plain text. Its first baseline is the baseline of the plain cell beside it. In a tagged document the cell stays a TD or TH, and a repeated head of runs is an artifact like a plain one.
A list in a cell is a list as in flowing text. item, start and continued mean in a cell what they mean under text -runs 1, whether written as runs or read from <ul>, <ol> and <li> or from - and 1. in Markdown: every item is a paragraph of the cell, indented and labelled as at the top of this page, consecutive items of one kind are one list, and the row grows with the lines the items break into. A position text that says "it covers in particular:" and then lists what it covers keeps its dashes as a list rather than as literal characters. In a tagged document the TD holds an L with an LI, Lbl and LBody per item.
Refused by name, before anything is drawn: text and runs in one cell, or a list of odd length (TCLPDF TABLE CELL runs); runs beside a markup on the cell, and {markup {markdown …}} written as a pair (… CELL markup); a value other than the three (TCLPDF TABLE STYLE markup); a body cell of runs under align decimal, whose line of pieces has no one separator to hang on (… CELL decimal); a heading — heading, written or read from <h1> or # — because it changes the size and the lines of a cell are one height (… CELL paragraph heading); a right-to-left cell (TCLPDF TEXT RUNS DIRECTION); and a notation left open, with the code and position text -markup gives it. Nothing falls back to plain text in silence. A caller who wants that fallback tries the translation in -didParseCell and sets markup plain on the cell where it fails, as the last table of example 04.05 does. didParseCell sees the cell as it was written; the two draw hooks see it measured — the pairs under runs, for a markup cell the pairs the notation became, and lines as a list of pair lists, in which every line of a list item carries the item's {item …}.
5. Embedded faces need a family
A standard face knows its bold and oblique: -style bold on Helvetica is Helvetica-Bold. An embedded face is one alias and one face, and a bold run had nothing to reach. font family ties the faces under one name, and from then on every bold run and every heading resolves to the bold face:
$doc font embed dejavu assets/fonts/DejaVuSans.ttf
$doc font embed dejavuBold assets/fonts/DejaVuSans-Bold.ttf
$doc font family body -regular dejavu -bold dejavuBold
-regular is required, -bold, -italic and -boldItalic may be left out; a style on an embedded alias that no family covers is refused under TCLPDF TEXT RUNS. A style that names a face the family does not have is refused by name rather than set in the regular face — that silence is the very thing this call ends. The family above has no italic, and a run asking for one stops the block with TCLPDF FONT FAMILY STYLE body italic:
tclpdf: family "body" has no italic face - register one with [font family body ... -italic alias], or set this text without the style
Examples and reference
| What it shows | Source | |
|---|---|---|
| 01.17-markup.pdf | one paragraph in tags and in Markdown, each as typed and as set, and the measured proof that both break into the same lines | 01.17-markup.tcl |
| 01.18-runs.pdf | the list written by hand, set in an embedded family, with the refusal of the italic face the family does not have | 01.18-runs.tcl |
| 01.19-lists.pdf | bullet and numbered lists in tags and in Markdown, each as typed and as set, with runs inside the items | 01.19-lists.tcl |
| 04.05-table-markup.pdf | runs and markup in cells: a Markdown column, a cell of runs from data, a cell of tags, two cells with a list, the refusals, and a fallback written as a hook | 04.05-table-markup.tcl |
The man-page has the full rules in its section Text: under text the options -runs and -markup (once for tags, once for markdown), and beside them font family, textHeight and textLines; the cells in its section Tables, under Runs and markup in a cell. The complete list of codes is in its section Error codes.