Skip to content
Changelog

Changelog

All user visible changes of XTS, newest first. The version at the top may not be released yet.

0.1.10 (2026-10-13)

  • New attribute column-fill on Flow: balance spreads the end of a flow over the frames of its last page (#72).
    With column-fill="balance", the part of a Flow after its last forced break that ends on a page is set in the frames of that page as high as it takes to hold it, so that a run of columns that ends part way down the page ends at about the same height in every column. The pages before fill their frames as before, and bottom and a Flow that follows directly start below the longest frame. The default auto fills one frame after the other, as always.
  • A block beside a float runs on under it, as in CSS; new display: flow-root (htmlbag v0.0.91).
    A block with a border or a background beside a float was narrowed as a whole. Now only its lines keep clear of the float, while the border and the background run on under it, and a block that starts past the float is not narrowed at all. A margin, border or padding of the boxes in between takes from the room the float needs instead of adding to it, so the text no longer moves further away from the float the deeper it is nested. A float in a block with padding stands at the padding edge. display: flow-root keeps a block clear of a float as a whole, as before.
  • A paragraph’s padding-left is taken off once (htmlbag v0.0.91).
    The lines of a paragraph with padding-left but no border or background ended short of the right edge by the padding, and beside a float or with a text-indent the padding was lost for the whole paragraph. The lines now run from the padding to the right edge, below a float as well, and a text-indent adds to the padding. A float without a width that has padding is as wide as its content plus its padding.

0.1.9 (2026-10-13)

  • Links, backgrounds and languages stay on their text in right to left paragraphs (boxesandglue v0.2.87).
    A link, an inline background, a text color, a text decoration or an element with lang inside a right to left paragraph cut the line into pieces that were reordered one by one: the words came out in logical order from left to right, and the spaces at the edges of the run were lost. They stay in place now. A link that runs from Arabic into English gets one area for each visually contiguous part, as CSS splits an inline box at a change of direction.
  • A left and a right float stand side by side (htmlbag v0.0.90).
    A float that started while another one was still beside the text went below it, even when it floated to the other side. A left and a right float now stand side by side, each beside its own edge of the text, as long as they leave room for text between them. Two floats on the same side still stack. A page break no longer leaves the shorter of two such floats alone at the bottom of a page.
  • A negative margin lets the text run over a float, as in CSS (htmlbag v0.0.90).
    A float’s negative margin on the side of the text fell back to -bag-float-gutter, and the text kept its distance. Now the text runs over the float by that much, or past it altogether. Only a margin of zero still gets the gutter. Where the text runs over the float, the float covers it.

0.1.8 (2026-10-09)

  • An object that reaches into the rest of the grid at the right no longer warns.
    When the cells do not fit evenly into the page area, the grid ends before the right margin. An object that reached past the last column into this rest gave the warning “object protrudes into the right margin”, even when it ended before the margin. The warning now compares the object’s exact right edge with the margin, as it already did at the bottom.
  • An image can show a part of its source with object-view-box: inset() (htmlbag v0.0.89).
    The CSS property object-view-box takes none or inset() on an <img> with a bitmap, PDF or SVG source. The part becomes the natural size of the image, so width, height and max-width size it as they size a whole image. xywh(), rect() and the round corners of inset() are ignored with a warning.
  • A margin at a break inside a block split across pages or areas is truncated (htmlbag v0.0.89).
    When a block with blocks inside was split and the break fell at the margin between two of them, the margin stayed at the top of the next page or area, and the text started lower there. The margin is truncated now, as a margin between two blocks of the flow itself always was.
  • An SVG drawn much smaller than its viewBox keeps its exact size (svgreader v0.0.6).
    The transformation of an SVG was written with four decimals. It scales every coordinate drawn under it, so a large viewBox placed small ended visibly short of its box: 3000 units at 85pt lost 0.14pt. It is written with six decimals now, and the drawing fills its box.
  • A ::before or ::after on an element with -bag-bookmark or page-break-inside no longer stops the document (htmlbag v0.0.87).
    Generated content took the settings of its element along, among them the internal ones for -bag-bookmark and page-break-inside, and the document stopped with “Unknown setting”. The generated content of such an element is set now.
  • A table caption is set above its table (htmlbag v0.0.87).
    The <caption> of an HTML table was dropped without a word. It is set as a block above the table and keeps with it at a page break. Under PDF/UA it is tagged as the first child of the table.
  • A link whose text comes from ::before or ::after is clickable (htmlbag v0.0.87).
    A cross reference such as <a class="ref" href="#sec"></a> with its text from target-counter() in ::before got no link annotation. The generated content of a link carries the link now; a leader does not.
  • SVG text without a font-family is set (htmlbag v0.0.87).
    Text in an SVG that names no font family, or one the document does not know, was dropped with a warning. An inline <svg> now takes the font of the element around it, an SVG in <img> the font family serif, as a browser does.
  • PDF/UA-2: links to an element get a structure destination, and a figure with a caption is tagged as one (htmlbag v0.0.87, boxes and glue v0.2.85).
    Under PDF/UA-2 the action of a link to an element with an id points to the structure element of its target, as ISO 14289-2 requires. A <figure> with an image and a <figcaption> is tagged as a section holding the image as a figure with its alternative text and the caption, in reading order; before, the caption had no structure element.
  • MathML: the minus sign, displaystyle and mspace (boxes and glue v0.2.85).
    <mo>-</mo> is set with the minus sign instead of the hyphen of the math font, and the solidus without the space of a binary operator. <mstyle displaystyle="true">, and the attribute on <math>, set their content in display style, so a fraction in a line of text can be set at full size. <mspace width> in em or as a named space adds space.
  • The CSS reference lists the column properties of htmlbag, which xts does not lay out (htmlbag v0.0.87).
    htmlbag reads column-count, column-gap, column-span, column-fill and column-rule and lays out columns for glu. Text that xts sets with them stays in one column, with a warning; columns in xts are areas with several frames.

0.1.7 (2026-10-08)

  • A Flow that ends in the row where the Flow before it ended no longer lets the next Flow print over it (#73).
    A Flow that follows another one starts at its exact end, which may lie partway down a grid row. When its content also ended within that row, it recorded no end, and a third Flow started where the second one had started and printed over it. The end is recorded now, so each Flow starts below the one before it.
  • -bag-text-box-trim-at-break lets the last line before a page break fit by its text (htmlbag v0.0.86).
    With -bag-text-box-trim-at-break: trim-end on a block, the last line before an unforced page or frame break may fit by its text: the space below the text edge of the block’s font, such as the half-leading, is taken off that line, so a paragraph whose last line fits by its text but not by its full line height stays on the page instead of moving on. Nothing else changes: a block ending mid-page keeps its height, its first line is not trimmed, and a forced break trims nothing. A block that a break-after: avoid moves on with the next one stays untrimmed. The property is not inherited, and under box-decoration-break: clone with text-box-trim: trim-end it changes nothing, since every part is trimmed there already.
  • box-decoration-break: clone repeats a block’s border and padding on every page (htmlbag v0.0.86).
    The CSS property box-decoration-break takes slice, the initial value and the behavior so far, and clone. With clone, a block with a border, padding or background that is split across pages or frames gets its padding and border on both sides of every part. Tables are not cloned yet.
  • Under box-decoration-break: clone, text-box-trim trims every part of a split paragraph (htmlbag v0.0.86).
    As CSS has it, a paragraph with box-decoration-break: clone and text-box-trim is trimmed at every break, not only at its start and end: with trim-end the last line before a break may fit by its text, and with trim-start the first line of each continued part is trimmed. A container of other blocks under clone is still trimmed only at its start and end.
  • text-box-trim on a table cell trims its first and last line (htmlbag v0.0.86).
    On a table cell, text-box-trim now trims the first line of the cell’s content and its last, as on a container, so a row can end at the text of its cells. It does not reach into a table nested in the cell.
  • A justified line before a forced line break no longer runs past the measure (boxes and glue v0.2.84).
    When none of the possible line ends before a forced line break gave an acceptable line, the line breaker took the one it had looked at last. In a justified paragraph that could set a line far looser than needed, or join the words up to the break into one line wider than the column, although a line that fits was possible. It now takes the line end with the best result for the paragraph, and one whose line fits before one whose line is too wide.
  • Long ragged paragraphs no longer take time and memory growing with their square (boxes and glue v0.2.83).
    A paragraph set ragged, with thousands of words, took time and memory that grew with the square of its length, gigabytes for a very long one. It now takes a small part of that. The breaks stay the same, except that a paragraph whose first lines are wider than the rest no longer gets a stray line skip above its first line.

0.1.6 (2026-10-07)

  • A Flow in a slate works while the body is a Flow (htmlbag v0.0.85, #71).
    A slate with a Flow, built in AtPageCreation or AtPageShipout while the body is itself a Flow running over pages, failed with the error that a flow is already running, and the slate stayed empty on the pages made during the body’s flow. The slate’s flow now runs inside the body’s, and the body goes on as without it, so a running header or footer can be a Flow on every page.
  • text-box-trim on a container trims its first and last line (htmlbag v0.0.85).
    On a container of other blocks, such as a div around paragraphs, text-box-trim now trims the first line of its first block and the last line of its last block, through nested containers, unless padding or a border on that block lies between. A table cell is not trimmed yet.
  • A paragraph alone in a Flow keeps its widows (htmlbag v0.0.85).
    A paragraph that was the only block of a Flow or of HTML text, or of the part after a forced break, broke without widow protection, so its last line could stand alone in the next frame or on the next page. It now keeps widows at every break, and the rest that moves into a frame of another width is broken again at that width.

0.1.5 (2026-10-07)

  • Table borders are painted over all cell backgrounds (htmlbag v0.0.84, boxes and glue v0.2.82).
    A table painted each cell’s background and then its borders, cell by cell, so the background of the next cell came after the borders beside it. Adobe Acrobat rounds the edges of filled areas to its pixel grid, and a thin border next to a shaded cell, such as a zebra stripe or a header row, then vanished partly or wholly. A table now paints its borders after all of its cell backgrounds, as CSS 2.1 asks, but still before anything placed over it later. This holds for Table and for tables in HTML text. In PDF/UA the borders are layout artifacts instead of content of their cells.
  • Tables with cell spacing pass PDF/UA, and PDF/A-3 with PDF/UA declares its schema (boxes and glue v0.2.82).
    The space between table cells, as from border-spacing, left an empty text operator outside of tagged content, which veraPDF and pdfa11y reject in PDF/UA. It is gone. A document that is PDF/A-3 and PDF/UA at once now declares the PDF/UA identification schema in its XMP metadata, as PDF/A-3 requires; veraPDF rejected the PDF/A part without it.
  • An SVG image floats like any other image (htmlbag v0.0.84).
    An img with an SVG file and float: left or float: right in HTML text was set inline, at the start of a paragraph and as a block before one alike. It now floats as a raster image does.
  • Fonts with CFF outlines no longer break in some subsets (textshape v0.0.18).
    The subset of an OpenType font with CFF outlines, such as TeX Gyre Heros, could come out with every offset in its Top DICT one byte short, depending on the glyphs a document uses. FreeType then rejected the font as a broken table and viewers showed a system font instead.
  • CSS text-box-trim trims a block to its text (htmlbag v0.0.83).
    The CSS properties text-box-trim (trim-start, trim-end, trim-both), text-box-edge and the shorthand text-box take the space above a block’s first line and below its last line, such as the half-leading, off down to the text edges of the block’s font, so the block ends at its text. They are not inherited. Split across pages, only the start of the first part and the end of the last part are trimmed. A container of other blocks and a table cell are not trimmed yet.
  • -bag-trace: boxmodel shows the box model of chosen elements.
    The CSS property -bag-trace: boxmodel paints the box model overlay (margin, border, padding and content) on the elements a rule matches, as the global box model trace does on every block. It is not inherited, and none turns it off again in a more specific rule.
  • Floats in HTML text shrink to fit, float at the start of a paragraph and are painted over the blocks beside them (htmlbag v0.0.83, boxes and glue v0.2.81).
    A float without a width shrinks to its longest line instead of taking the full measure, and where it leaves no room beside it the text goes below it. A float at the start of a paragraph, such as an image with float: left before the paragraph’s text, now floats; before, it was set inline. A paragraph with a border or padding still sets it inline. A float is painted over the background and border of the blocks beside it, as in CSS, where those used to cover it.
  • A list item with lang no longer stops the run.
    An HTML li with a lang attribute, -bag-bookmark or page-break-inside stopped the run with an “Unknown setting” error, because its marker took over settings only htmlbag knows (htmlbag v0.0.83).
  • A Flow in a slate stacks its blocks as one object.
    A Flow in a Slate was an error. It now stacks its blocks as one object, as wide as the slate’s frame, with the same children, rules and warnings as on a page: the blocks keep their exact heights and their margins collapse, where a PlaceObject per block rounds each one up to whole rows, but nothing goes on to a next frame or page. bottom and sd:slate-height() give its exact end, and a PlaceObject after it starts on the next whole row. A forced break with page, column, left or right has nothing to break to there and is ignored with one warning per flow; area is an error in a slate. A Mark or Bookmark between the blocks takes the page the slate is placed on (#59).
  • A Go program can choose how paragraphs break.
    A program that runs XTS can register breakers in XTSConfig.Breakers, by name, as it registers line models in XTSConfig.LineModels. The CSS property -bag-line-breaker selects one for a paragraph, which then chooses the paragraph’s breaks among its legal breakpoints in place of Knuth-Plass; the lines are measured and set as before. auto, the default, keeps Knuth-Plass.
  • A link target in a line sits at the line’s top, and an inline background is one band (boxes and glue v0.2.80).
    With boxes and glue v0.2.80 and baseline-pdf v1.1.26: a named destination inside a line, from a Mark with pdftarget, a Bookmark in a Paragraph or an id on an element in HTML text, was placed the line’s depth above the line, so a viewer jumping there showed a sliver of the line before it; it is now at the top of the line. A background-color on an inline element takes its height from that element’s own font, as in a browser, so text inside it in another font or size no longer makes the band step up and down. A PDF no longer claims a Unicode mapping for a glyph that is only part of another, such as the dieresis of an “ä” in a TrueType font.
  • A margin no longer starts a page on its own, and a CSS height splits at the page end (htmlbag v0.0.81).
    With htmlbag v0.0.81 and boxes and glue v0.2.79: when a page ended exactly full, the bottom margin of its last block went to the next page on its own, and a block taller than a page then moved one page further and left a page with nothing but that margin; a margin at the top of a page after an automatic break is now dropped, as in CSS. The space a CSS height adds below a block’s content is split at the page end like the content, rather than going whole to the next page and running below its content area.
  • A Mark in an Action inside a Paragraph takes the page of its line.
    A Mark in an Action inside a Paragraph, or inside B, Span and the other text elements, was dropped, so sd:page-number() never found it. It now goes into the line it is in, as an invisible character, and takes that line’s page; with pdftarget it is a named destination there. A Bookmark works the same way and is now allowed in the text of a Paragraph. In a Flow both are possible inside a paragraph, also when the flow breaks it across pages; elsewhere inside a block they remain an error (#67).
  • A Mark in a slate takes the page the slate is placed on.
    A Mark in a Slate got the page that was current when the slate was composed, so sd:page-number() gave that page even when the slate was placed on a later one. A mark now takes the page it is shipped out on; a slate placed more than once leaves the mark on the last of its pages, as a second Mark of the same name does (#68).

0.1.4 (2026-10-05)

  • Floats get -bag-float-gutter, and a table moves with position: relative (htmlbag v0.0.80).
    With htmlbag v0.0.80 and boxes and glue v0.2.78: the new CSS property -bag-float-gutter sets the space between a float and the text beside it where the float declares no margin on that side (9pt by default, 0 gives a browser’s spacing); a table with position: relative is moved by left or right, as other blocks are; a float taller than a page no longer leaves empty space on the next page, or a blank page at the end; and a line model from XTSConfig.LineModels gets the synthetic oblique in LineModelStyles.Font where font-synthesis-style slants the upright.
  • A Flow runs on to the bottom margin below the grid’s last row.
    In a frame that reaches the grid’s last row, a Flow’s band ended at that row’s bottom, so the rest of the page area below it, when its height is not a whole number of rows, was never used. The band now runs on to the bottom margin there; a frame that ends higher, or rows taken by an object such as a footer, keep the row edge. A line that ends between the last row and the bottom margin gives no “protrudes into the bottom margin” warning (#58).
  • Text and HTML follow the browser more closely in margins, backgrounds and link targets (htmlbag v0.0.79).
    With htmlbag v0.0.79 and boxes and glue v0.2.77: margin: auto centers a block with a width (both sides) or moves it to the right (margin-left: auto); a block image is placed by its margins, applied once, and no longer by the text-align it inherits; an inline background such as background-color on a span covers the font’s ascent and descent, as in a browser, rather than the em box; a block with break-inside: avoid stays whole; and every element with an id, also a div or a span, is a named PDF destination that a link can point to.
  • Mark no longer documents append and shiftup, which had no effect.
    The reference and the schema listed the attributes append and shiftup of Mark, but XTS read them and did nothing with them: a mark never collected several pages and its anchor was never raised. Both are removed until they are implemented, and a layout that sets them gets the warning about an unknown attribute (#56).
  • ProcessNode passes on what its records return, and their errors.
    The blocks made in a Record called through ProcessNode were thrown away, so a Flow over ProcessNode came out empty without a warning, and the same held in a Paragraph, a table cell or a variable around it. ProcessNode now returns what its records return, in order, as ForAll does with its body, so a record per chapter can fill a flow, its Mark included. An error in such a record, such as an unknown area, was logged and the run went on; it now ends the run as in a ForAll. The schema allows ProcessNode and CallTemplate inside a Flow (#64).
  • sd:slate-height while a slate is composed no longer draws objects twice.
    Measuring a slate with sd:slate-height() while its Contents were still being placed kept links from that measurement, so the next object on the slate brought an earlier one back: it was drawn twice, or, with the objects one below the other, the run did not end (#62).
  • A Flow below content on the page moves a block that does not fit on.
    A Flow that starts below what the page already holds, such as a second Flow or a PlaceObject, set a block that did not fit in the space left there anyway, past the bottom of the area with a warning. The block now moves on to the next frame or page, as it does after another block of the same flow (#53, #61).
  • XTS warns about attributes that a command does not know.
    An attribute that XTS does not read, such as a misspelled <PlaceObject colum="3">, was ignored without a word. XTS now writes a warning with the line, the command and, when one is close, the attribute that was probably meant: Layout line 12: unknown attribute "colum" on PlaceObject, did you mean "column"? Attributes in a namespace, such as xml:lang, are not checked. Attribute names are now matched exactly, so the spelling without the hyphen that XTS used to accept, such as backgroundcolor for background-color, is ignored with this warning; layouts that use it need the hyphen (#61).
  • break-inside: avoid keeps a paragraph of a Flow in one frame.
    A Paragraph (or a bordered block) directly in a Flow split between two frames although it had break-inside: avoid; the property worked only on a block inside a container. Such a block now moves to the next frame whole when it fits into an empty one (htmlbag#75).
  • In a Flow, break-before and break-after with recto and verso pick the page side.
    A forced break with recto or verso in a Flow started a new page without looking at its side, unlike right and left. recto now goes on to the next right (odd) page and verso to the next left (even) one (#60).
  • Objects are placed on the cells of a grid with gaps.
    With gaps between the grid cells (dx and dy of SetGrid or Grid), an object in column or row n was placed one gap too far left or up, and in a positioning frame that does not start in the first column or row, the gaps before the frame were not counted at all. Objects, frames and flows now start on their cells as the grid trace draws them. Layouts with dx or dy change.
  • The grid trace draws every frame of an area where it is.
    With <Trace grid="yes"/>, all frames of an area were outlined at the place of the area’s current frame, so an area with two columns showed one of them twice and the other not at all. Every frame is now drawn at its own position, and the frames are drawn in a fixed order, so two runs with the trace on give the same PDF.
  • An object without coordinates no longer lands on a cell that is taken in its last column.
    When XTS looked for a free place for an object without row and column, it checked every column the object covers but the last one, and none at all for an object one column wide. Such an object could be put over something placed before. Every column is checked now, so the object moves down to the next row where it is free.
  • An object can be placed in the last row of a frame.
    The search for a free row never looked at the last row of a frame, so an object without row and column that fits exactly into it, as well as the cursor of NextRow, went on to the next frame or page instead. The last row is now used like every other, which changes layouts that relied on the extra break: such an object stays on the page.
  • Loop passes on what its children make.
    A Loop dropped the result of its children, so rows made in a Loop inside a Table, or text inside a Paragraph, were lost without a warning and the table stayed empty. A Loop now returns what its children return in every round, as ForAll and While do.
  • Mark and Bookmark work between the blocks of a Flow.
    A Mark between the blocks of a <Flow>, also in an Action, marks the page of the block that follows it, wherever the flow puts that block, also when it moves on to the next frame or page; after the last block, it marks the page where the flow ends. With pdftarget it is a named destination at the top of that block. A Bookmark between the blocks points to the top of the block that follows it, where before it was left out. Inside a block of the flow, such as a Paragraph, both are an error for now (#54).
  • A registered line model gets the paragraph’s font.
    A line model registered through XTSConfig.LineModels gets the paragraph’s own font in htmlbag.LineModelStyles.Font, with the face, size and vertical metrics its glyphs are set in, so it can give a line without glyphs, such as the one between two Br, the height of the paragraph’s font (CSS 2.1 §10.8.1).
  • PlaceObject keeps a table whole; a table that breaks goes into a Flow.
    A table placed with PlaceObject was split across frames and pages. It is now kept whole, as any other object: when it does not fit below what is in the frame, it moves to the next frame or page as one piece, and a table taller than every frame of its area is an error that names the PlaceObject and points to Flow. A table that should break goes into a <Flow>, which breaks it between rows with its head and foot repeated and keeps the rows a rowspan joins together. This changes layouts that relied on the split (#47).
  • New command Flow pours paragraphs and tables into an area, page after page.
    The children of a <Flow>, such as Paragraph, Table, HTML, Ul and Ol, are laid out as one sequence of blocks into the frames of an area, starting new pages as needed. Paragraphs break between lines with widows and orphans, also inside a container such as a list item, a div or a bordered box, tables between rows with their head and foot repeated, break-after: avoid keeps a block with the next, and break-before or break-after go to the next frame (column) or page (page, left, right), also on a block inside a container. Rows that hold objects are passed over, and the blocks are not rounded to the grid: a flow starts at the exact bottom of the objects above it, and a Flow that follows another directly continues exactly where it ended, in every frame of its area that is free from there. A child with an id shows up in --dumpoutput once per part. PlaceObject, ClearPage, NextFrame and NextRow are an error inside a flow. The attribute bottom sets a variable to where the flow ends (#47).
  • A run no longer uses the Records of an earlier run in the same process.
    The Record commands of every layout were kept for the lifetime of the process, so a second RunXTS with a different layout, as in xts watch or a program that calls XTS several times, fell back to a Record of an earlier layout when its own layout had no match, and kept the earlier layout trees in memory. The Records, the cache of files found on disk and the numbering of destinations now belong to the run, and every run starts as a fresh process does (#51).
  • Ragged text is set at its natural width.
    With text-align left, right, center, start or end (the default), the word spaces of a line could shrink by up to a third and the glyphs were condensed by the font expansion, so a line kept a word that does not fit at its natural width. Ragged lines are now set at their natural width and such a word moves to the next line, so line breaks in ragged text change. Justified text is unchanged, and -bag-font-expansion only acts on justified text.
  • letter-spacing also widens the spaces.
    A space now gets the letter-spacing like any other character, so a letter-spaced line no longer sets its words closer than its letters. This holds for preserved spaces under white-space: pre and pre-wrap as well.
  • New CSS property -bag-horizontal-scale.
    Draws the glyphs narrower or wider by a fixed factor, a percentage or a number (90% or 0.9). It is inherited and does not compound, so 100% inside a scaled element returns to normal width. The line breaker sees the scaled width.
  • New CSS property -bag-fixed-height for table rows.
    On a Tr, -bag-fixed-height takes a length and makes the row exactly that high: content that does not fit draws past the row, and the row never breaks inside. Unlike height, which stays a minimum, it also limits the row. It is not inherited and has no effect on a Td.

0.1.3 (2026-10-02)

  • font-variant-caps: small-caps sets small capitals.
    A text with font-variant-caps: small-caps uses the small capitals of the font (OpenType feature smcp) when it has them. Otherwise the small capitals are synthesized: every letter that changes when uppercased becomes its capital at 70% of the size, with the case mapping of the language, so ß becomes SS. font-synthesis-small-caps: none switches the synthesis off, and the font shorthand reads and resets small-caps as well.
  • A paragraph in a table cell draws its background and border.
    A background-color or border on a Paragraph inside a Td had no effect, only the cell itself drew one. The paragraph’s box is now drawn as on the page.
  • A background-color on a span that holds only spaces is painted.
    A Span with a background-color whose text is only spaces painted nothing. The background now covers the space, as high as the font, like on a span with text.
  • A collapsed column border is drawn in one piece.
    With border-collapse: collapse, a vertical border between two columns was drawn as one segment per row, which some PDF viewers show with fine gaps between the rows. Where the rows agree on the border’s width and color, it is now drawn as one line from top to bottom.
  • Hyphenation is two to three times faster.
    The hyphenation patterns are now matched once per word and language, so a word that appears again is hyphenated from a cache. The hyphenation points do not change.
  • A table is moved by its margin-left.
    A margin-left on a Table had no effect. The table is now moved to the right by it, with its borders, also when it is split across pages and its header is repeated.
  • A negative margin-left moves a paragraph in a table cell to the left.
    A Paragraph in a Td with a negative margin-left was widened but stayed at the left edge of the cell, so its text ran out on the right. It now starts left of the cell’s content edge by that amount, as in CSS.
  • New command Tab inserts a tab.
    A <Tab/> in a Paragraph or another text command moves the text after it to the next stop of -bag-tab-stops, and without stops it advances by tab-size. Unlike a tab character in the text, which white-space: normal collapses to a space where no stops are set, it always stays a tab. The new manual page on tab stops shows a table of contents and a price list (#39).
  • A Slate draws its objects in the order they were placed.
    A Slate sorted its objects by their vertical position before drawing them, so an object placed later but higher up, such as a background with allocate="no", was drawn under the objects below its top edge. A slate now draws its objects in the order they were placed, as a page does, and layer="behind" still puts an object under the others. The positions of the objects and the size of the slate do not change (#49).
  • Go callers can register line models for -bag-leading-model.
    The new XTSConfig.LineModels field maps -bag-leading-model names to htmlbag.LineModelFunc values, which XTS registers with its CSS builder through CSSBuilder.RegisterLineModel before the layout is read. A reserved or empty name makes RunXTS return an error. Layouts that do not use a registered name are not affected.
  • A Span with position: relative moves by top or bottom.
    A Span with position: relative is moved down by top or up by bottom after the lines are set, and the lines and the neighboring text stay where they are. top wins over bottom, nested offsets add up, and a percentage has no effect. Before, the offsets were ignored. left and right still have no effect on inline text.
  • Raised text keeps its position across a color change.
    Inside a Span with vertical-align, text after a color change, such as a nested Span with color: red, was drawn on the baseline instead of raised. The same happened to the horizontal scaling of font expansion. Both now stay in effect.
  • New PlaceObject attribute layer draws an object behind the others.
    With layer="behind", an object is drawn under everything placed on the page since its AtPageCreation ran, but over what that drew, and in a Slate under the slate’s other objects. It is meant for decoration such as panels, tints and rules. layer="front" is the default.
  • In a Slate, allocate=“no” makes an object take no room.
    An object placed with allocate="no" in a Slate, at lengths or in the grid, is drawn where it is placed, negative offsets included, without making the slate larger or moving the objects placed after it. Before, it still counted towards the size of the slate.
  • An object placed at lengths in a Slate is placed in the slate.
    A PlaceObject with column and row given as lengths inside a Slate was drawn on the current page, measured from the page’s top left corner. It now goes into the slate, measured from the slate’s top left corner as grid positions already are, and the slate grows to hold it.
  • A tab after a Br is kept where tab stops apply.
    With -bag-tab-stops in force, a tab right after a Br was dropped with the white space that starts the new line, so the text after it began at the margin instead of at the first stop. The tab is now kept.
  • Nested tables keep their place among the paragraphs of a cell.
    A table inside a Td was set above all of the cell’s paragraphs, and a second nested table in the same cell replaced the first. Each nested table now appears where it stands in the cell, so text and tables can alternate.
  • sd:current-row(), sd:number-of-columns() and sd:number-of-rows() work before the first page.
    Before anything had created a page, these functions read a grid that did not exist yet and XTS stopped with a panic. Like sd:current-page() and the other grid functions, they now set up the page first and report its grid. A page type test that cannot be evaluated, such as one that does not give true or false, or one that asks for the grid while the first page is being set up, is reported as an error and does not match, instead of stopping XTS.
  • xts compare catches a missing page and a page of a different size.
    xts compare only looked at as many pages as the new PDF has, so a reference with more pages passed, and with ImageMagick 6, as on Ubuntu, two pages of different sizes counted as equal. A page missing on either side and a page whose size differs from the reference now count as bad pages. Rendering errors of the new PDF are reported instead of being ignored.
  • A tab stop’s leader is drawn in a Paragraph.
    With -bag-tab-stops and leader(), the leader showed only in <HTML> text and came out blank in a Paragraph, because the tab lost its leader when the lines were copied for placing (#38).
  • margin-left and margin-right of a paragraph work in a table cell.
    A paragraph with a side margin was indented on the page but not in a Td. It is now indented in a cell too, and the height of the cell does not change.
  • The right border of a table cell stands at the edge of the cell.
    Content narrower than the cell, such as a fixed-width box or a paragraph with a side margin, put the right padding and border just after the content instead of at the edge of the cell.
  • New CSS property font-synthesis-style slants the upright for a missing italic.
    With font-synthesis-style: auto (or font-synthesis: style) a font family without an italic sets italic text in its upright slanted by 12 degrees, as browsers do. Unlike CSS, the default is none, so existing layouts keep the upright.
  • A missing font style is reported once, not for every run of text.
    A family without an italic logged “Style italic not found” for each italic run of text. The warning now comes once for each missing weight and style.
  • A span that holds only white space no longer breaks the line in two.
    A paragraph whose only content was white space inside a span, such as a colored space or a lone tab, could break at the invisible start and end of the span and set as two lines.
  • A table with fixed columns no longer collapses on x86 computers.
    When a line or row was one scaled point too narrow or too wide for its glue, the arithmetic produced a value that is 0 on ARM and a huge negative number on x86. On x86 a table with fixed column widths could set every column to nothing. The glue now keeps its natural width in that case, as in TeX.
  • Text selection and extraction get the right line height for fonts whose units per em are not 1000.
    The ascent, descent and font bounding box in the PDF font descriptor were written in the units of the font instead of 1/1000 of the em, so viewers took each line of a 2048-unit TrueType font for twice its height and could merge table rows when copying text. Nothing drawn changes.
  • position() and last() in ForAll and ProcessNode count the right items.
    A path such as ../x or count(../rec) left position() at 1 and last() at the number of nodes of its last step, for the rest of the expression and the rest of the loop body. goxpath 1.0.21 fixes this. ForAll and ProcessNode now set last() to the number of items they go through and give the outer position() and last() back when they end. Also, an expression that sets up the first page in its middle, such as concat(sd:current-page(), sd:current-page()), no longer loses its namespaces and fails with “Could not find namespace for prefix”.
  • Trace gridallocation gives the same PDF on every run.
    The allocated grid cells were drawn in a random order, so a layout with <Trace gridallocation="yes"/> gave a different PDF on every run, even with --suppressinfo. They are now drawn in the order of the cells.
  • A finished run no longer keeps its document in memory.
    Every page started a goroutine that handed out its marker ids and never ended. It held the page, and the page the whole document, so a process that renders more than once, such as xts watch or a run with --runs, grew by every document it had made. The marker ids now come from a counter on the page.

0.1.2 (2026-09-25)

  • Justified lines end at the right edge of the text instead of running past it.
    Glyph widths were measured with the font size divided by the units per em rounded down, so every glyph was measured slightly narrower than it is drawn: 0.3% for a font with 2048 units per em, 0.05% for one with 1000. Justified lines printed past their measure by that amount. Glyphs are now measured at their exact width, so text comes out slightly wider and a few lines break differently. Also, an OpenType font with CFF outlines whose units per em are not 1000 was drawn at the wrong size and spacing, four times too large at 4000 units per em.
  • New CSS property -bag-tab-stops sets tab stops in HTML text.
    A comma separated list of stops, each a length or a percentage of the line width, optionally followed by start, end, center, decimal or decimal(",") and a leader(), for example -bag-tab-stops: 12mm, 100% end leader(dotted) for a contents line. A tab in the text advances to the next stop. With stops set a tab survives any white-space mode, whitespace with a newline in it stays source formatting. The leader() function in content now also accepts the keywords dotted, solid and space.
  • A table does not break between rows a cell spans.
    When a table was split across frames or pages, the break could fall between rows joined by a cell with rowspan. The spanning cell is drawn whole with its first row, so it hung past the bottom of the frame and left a gap beside the rows continued in the next one. The rows a rowspan joins now go to the next frame together; a group taller than a frame is still broken inside, so none of its rows are lost.
  • Text that exactly fills its line is no longer pushed 1pt to the right.
    A line whose contents were exactly as wide as the measure had its glue stretched fully instead of not at all. In tables that hit the widest cell of a column, the one the column width is taken from: its text started 1pt further right than the other cells and ran 1pt past the cell. Such lines are now set at their natural width. Also, a line breaking at a soft hyphen right before a space was chosen as if the hyphen were set and the space kept on the next line; both lines are now measured the way they are set, without the hyphen and the space.
  • An object that no longer fits continues where there is room: in the next frame of its area, or below what a new page already carries.
    When an object placed in an area with several frames did not fit the rest of a frame, it went to row 1 of the next frame without checking that row was free, so a frame that starts beside content placed earlier (a full-width box above two columns) was written over. It now moves frame by frame to the first that has room; an object taller than every frame takes the next free one. On a new page the same search runs over its frames, so the first object no longer covers what <AtPageCreation> placed; a table continuing into the next frame or onto a new page does the same. This changes documents with a single area too: after a page break, placement used the previous page’s area, so the objects after the first on a new page could be set in the wrong frame or column; they now follow the first as they did on page 1. <NextRow> had the same page-break problem and is fixed too.
  • A CSS float stays with its text at a page break.
    A float reserved no height of its own, so at a page break a margin note or a floated figure could stay at the bottom of the page while the paragraph beside it moved to the next one. The band a figure leaves beside itself also ran across the break and narrowed the first lines on the next page next to nothing. A float now stays on the same page as the block beside it, or both move to the next page together, and the band ends at the page break: the lines of a split paragraph on the next page run at full width again. Also, the id attribute of a paragraph, table row or table cell in HTML content is now carried onto the box it produces and shows up as attr-id in the dump output.
  • A table without a TableHead breaks across frames and pages too.
    A table taller than the frame that had no <TableHead> was placed as a single object and drawn past the bottom margin. It is now split row by row like a table with a head, continuing in the next frame of the area or on a new page; there are just no header rows to repeat. A table with only a <TableFoot> is split as well, with its footer repeated. This also changes a short table without a head that does not fit the space left in the frame: it starts where it is and breaks, as a table with a head already did, where before it moved whole to the next frame (#29).

0.1.1 (2026-09-22)

  • CSS floats can be placed towards or away from the binding with float: inside and float: outside.
    In HTML content, float: outside and float: inside pick the side of the float from the page it lands on: the outer edge on right pages is the right edge, on left pages the left edge. The declared margin-left and margin-right are read as written for a right page and swapped on left pages, so one rule with a negative outer margin places a margin note in the outer margin of every page. clear accepts the two values as well. A float now starts level with the block after it (the margin between the two blocks is laid out before the float), and a margin note followed by a floated figure shares its position instead of pushing the figure down.
  • Right-to-left paragraphs keep their insets and alignment on the correct side.
    In a right-to-left paragraph the lines beside a float on the right ran underneath the float, text-align end and center came out flush right, the first-line indent went to the left edge, a line ending in a forced break was flush left, and so was the last line of a justified paragraph. Hanging punctuation at the end of a right-to-left line was painted over the next glyph. All of these now follow the paragraph direction: insets are physical, indents and alignment are resolved from the line start.
  • Tables are laid out about twice as fast.
    Every paragraph in a table cell was shaped and broken into lines four times: twice to find the column widths and twice to build the row. The column widths are now measured on the shaped text without breaking it into lines, and a cell is built once. A catalog of 112 pages of nested tables takes 0.4 seconds instead of 0.9. Along the way a miscalculation was fixed: a column whose cell breaks its lines with <Br> came out too wide from the third line on, it now follows the longest line.

0.1.0 (2026-09-18)

  • height on a table row or cell sets the minimum height of the row.
    A CSS height on <Tr> or <Td>, or on tr, td and th in HTML, was ignored. It is now the minimum height of the row, as CSS 2.1 prescribes for tables: a row with less content grows to the height, a row whose cells need more room keeps its natural height, nothing is clipped. The height counts the whole cell including padding and borders, and valign places the content within the extra room. min-height and max-height have no defined meaning on table rows in CSS and are still ignored.
  • border-collapse and border-spacing work on tables, the default is separate as in CSS.
    Both properties were parsed before but had no effect: tables always drew their borders in a collapsing manner. Now a table follows CSS. The default is the separated model: every cell draws all of its borders and border-spacing (one length, or horizontal and vertical, 2pt by default) keeps the cells apart, also from the edge of the table. border-collapse: collapse draws the border between two cells once, the wider one wins and brings its color, and a border on the table itself merges into the cells on the edge instead of being drawn around them a second time. A style sheet that relied on the old behavior without saying so shows gaps and doubled borders between the cells, like a browser would; table { border-collapse: collapse } restores the old look.
  • list-style-type knows the alphabetic and roman styles.
    An ordered list can be numbered with lower-alpha, upper-alpha (or lower-latin, upper-latin), lower-roman, upper-roman, lower-greek and decimal-leading-zero; before, every style other than decimal and the three bullets fell back to a bullet. The same styles are the optional last argument of counter(), counters(), target-counter() and target-counters() in a content value, as in counter(chapter, upper-roman).
  • SVG text takes its font from an enclosing group.
    A <text> in an SVG image without font-family, font-size, font-weight, font-style or text-anchor of its own now takes them from the nearest <g> or the <svg> root that sets them, as SVG prescribes; before, such a text was not rendered at all. A text whose font family XTS does not know is still dropped, but now with a warning that names the family and the text.
  • background-color is painted on spans and other inline elements.
    A background-color on a <Span>, or on an inline element such as <span> or <code> in HTML, had no effect: only paragraphs, table cells and other block elements got a background. The color is now painted behind the text of the span, as high as the font, and follows the text across line breaks, one box per line. Nested spans paint the inner color on top of the outer one, an underline stays visible on top of the background, and a span whose color is the one of its paragraph paints nothing extra. The manual page “Text Formatting” no longer claims that a background is only painted on block elements.
  • Releases follow the 0.MINOR.PATCH scheme and the version attribute of Layout checks against it.
    Starting with this release the version number of XTS says what an update means: a patch release (0.1.0 to 0.1.1) fixes bugs only, a minor release (0.1 to 0.2) adds commands, attributes and functions, may change line and page breaks of an existing layout and may remove features that were deprecated in the previous minor release. The manual chapter “Versions and Compatibility” describes the rules. The version attribute of <Layout> takes the version the layout was written for, such as version="0.1": the parts are compared from the left, a layout that asks for a newer version than the running XTS stops with an error, an older request passes, and development builds accept every version. A two part version such as 0.1 crashed XTS before, and a development build rejected every version. The reference of the attribute still described the speedata Publisher.

0.0.30 (2026-09-17)

  • allocate=“no” no longer moves the cursor for full width objects.
    A <PlaceObject allocate="no"> whose object reached the right edge of the area still moved the cursor to the row below it, so the next object placed without coordinates ended up under a full width background instead of on top of it. The reference promises that the cursor position is not changed, and now it is not. Narrow objects were not affected.
  • xts compare can look for a reference PDF with another name.
    The new option --reference NAME makes xts compare look for NAME.pdf in every test directory instead of reference.pdf. This allows a collection of examples whose expected output is stored as result.pdf to be checked with the same command.
  • CSS @page margin boxes render headers and footers on master pages. ↗
    A @page name { } rule in a <StyleSheet> now belongs to the <DefineMasterPage> with the same name, the generic @page rule is the base for every master page. The margin boxes (@top-left, @bottom-center, …) are rendered into the page margin when the page is shipped out, after <AtPageShipout>, so counter(page) is final and no grid cell is consumed. Static headers and footers no longer need a hook. The margin attribute of <DefineMasterPage> is now optional: when present it defines the grid and wins over the @page margins with a warning, without it the margins come from the @page rule, and without either the margin is 1cm. The selectors :first, :left and :right are not evaluated, the test attribute selects the master page.
  • StyleSheet accepts @-bag-color, spot colors get their ink name. ↗
    Colors defined with the @-bag-color at-rule in a <StyleSheet> are registered with the document, so CSS and layout commands can use them by name. <DefineColor> with model="spotcolor" now passes the colorname attribute as the ink name and the optional c, m, y and k attributes as the fallback tint. Both were ignored before, so the Separation color space was written without a name.
  • xts watch sees subdirectories and always re-runs on a save. ↗
    The watch mode did not notice a change to a file in a subdirectory of the layout directory, such as css/style.css, and a save of the unchanged layout afterwards was dropped as well. The watcher now observes all subdirectories, including ones created later, and a save after a run always starts a new run. Files written while a run is in progress are still ignored when their content did not change, so a Lua filter that rewrites its data file does not loop.
  • The layout schemas carry annotations for the speedata language server. ↗
    The RELAX NG schemas layoutschema-en.rng and layoutschema-de.rng now contain lsp: annotations for the language server used by the speedata Visual Studio Code extension: links to the reference page, outline entries, definitions and references of named things such as colors and areas, conditional attributes and predefined names. The XSD schemas are unchanged.
  • Slates built before the first placement and nested slates work. ↗
    A <Slate> created before anything was placed on page 1 got an empty grid: text blocks without an explicit width came out with zero width and objects without column and row overprinted each other. A slate nested in another slate placed the rest of the outer slate on the page instead of the outer slate, and crashed when combined with the first problem (#31, #32).

0.0.29 (2026-09-14)

  • xts compare exits with an error when a page differs from its reference. ↗
    The compare command reported differing pages on standard output and in compare-report.html, but always exited with status 0, so a QA run could not fail. It now exits with status 1 when at least one directory does not match its reference, when the report cannot be written, and when no directory is given.
  • CSS shorthands in StyleSheet are expanded, several XPath axis fixes. ↗
    A shorthand such as border: 1pt solid red in a layout style sheet was dropped as an unknown property, it now arrives as its longhands. The XPath engine fixes self::A (matched every element), self::A or self::B and (@A, @B)[1] (the context was not restored between operands), preceding:: and following:: (only looked at siblings) and the name tests prefix:* and *:name.
  • Floats keep the text clear of their own margins. ↗
    A floated element in HTML content now holds the text clear of its own margins on every side instead of the default gutter alone. A percentage width on a floated image resolves against the container instead of the intrinsic size of the image, and a float stays inside its container whatever margin sits below it.
  • Text flows beside left and right floats, CSS errors name the selector. ↗
    In HTML content and paragraphs text now flows beside elements with float: left and float: right. CSS parse errors name the selector and the line in the style sheet. A relative url() in an @page rule resolves against the style sheet that declares it instead of the document directory, and the color of text-decoration is resolved where it is declared.
  • New subcommand xts watch re-runs the publishing process on file changes. ↗
    The new subcommand xts watch runs the publishing process once and again whenever an input file changes. It observes the current directory, the directories of the layout and data files and all extra directories. Events are debounced, files written by xts itself and editor artifacts are ignored, and errors in a run do not stop the loop (#30).

0.0.28 (2026-09-11)

  • Trace hyphenation shows the hyphenation points again. ↗
    The hyphenation trace did not descend into the vertical lists that wrap the lines of a paragraph, so <Trace hyphenation="yes"> drew nothing.
  • Trace gains the boxmodel and hboxes overlays. ↗
    The new attribute boxmodel on <Trace> draws the translucent CSS box model overlay known from the browser developer tools (content, padding, border and margin), hboxes outlines the line boxes.

0.0.27 (2026-09-02)

  • PositioningArea accepts a Switch to select frames per page. ↗
    A <Switch> inside <PositioningArea> is evaluated when the page is created, so a <Case> with sd:current-page() can define different frames for even and odd pages. If no branch matches, the area does not exist on that page.
  • Column supports align, valign and background-color, Td supports align. ↗
    The new attributes align, valign and background-color on <Column> are distributed onto the cells of the column, aware of colspan and rowspan. <Td> gets align. Settings on the cell win over settings on the column.
  • ClearPage can open the next page on the left or right side. ↗
    The new attribute openon="right" or openon="left" on <ClearPage> inserts a blank page when the next page would open on the wrong side. Right pages have odd page numbers.
  • A slate can define its own grid and place objects on it. ↗
    A <Grid> child of <Slate> with the attributes of <SetGrid> defines the grid of the slate, and <PlaceObject> with column and row inside a slate positions the object on that grid. A <PlaceObject> with an unknown slate name reports an error instead of crashing, as do sd:slate-width() and sd:slate-height() on an empty slate.
  • Breaking: the Lua xlsx worksheet call takes (row, column). ↗
    The worksheet accessor of the Lua xlsx module took (column, row) while the manual documented (row, column). The code now follows the manual, Lua filters that read spreadsheets need to swap the two arguments.
  • Required attributes are enforced, CSS-like attributes use dashed names. ↗
    Attributes the schema marks as required are now checked at run time: select on <Attribute>, <ForAll> and <Mark>, level on <Bookmark>, name on <DefineColor>, elementname on <SaveXML> and test on <Case>. CSS-like attributes use dashed names such as background-color and max-width, the old spellings keep working. <SaveXML> gains the documented attributes attribute.
  • Constructed elements are navigable with XPath, several smaller fixes. ↗
    Structures built with <Element> and <Attribute> can be navigated with XPath. <OutputAt> with halign="right" aligns to the width of the area instead of a fixed six grid columns. sd:odd() handles negative numbers, sd:file-exists() checks the file system, the Lua filter script is looked up via the extra directories, the Lua runtime.finalizer runs after the publishing run, and the --trace help names gridallocation. The manual was audited against the implementation and examples from the speedata Publisher era were repaired.

0.0.26 (2026-09-01)

  • New command TableFoot repeats rows at the bottom of every table part. ↗
    The new command <TableFoot> mirrors <TableHead>: its rows are set at the end of the table and, when the table is split across frames or pages, above every break as well.
  • A table with a TableHead breaks across frames and pages. ↗ ↗ ↗ ↗ ↗ ↗
    A table taller than the frame was drawn past the bottom margin. A table placed on the grid that has a <TableHead> is now split: rows fill the frame, the table continues in the next frame of the area or on a new page, and the header rows are repeated at the top of every continuation. A lone header is never left at the bottom of a frame, frame and background are drawn per table part, and the column position is kept when the table continues. A table without a head is placed as a single object as before, as is an absolutely positioned table (#29).
  • expand-text works in mixed XHTML content of HTML. ↗
    The attribute expand-text on <HTML> was ignored when the content mixed XHTML and XTS elements, so {$width} in an attribute value or in text reached the output with the braces intact. The setting now applies on both paths, the default stays no (#28).

0.0.25 (2026-09-01)

  • CSS white-space and text-decoration are supported, the interword space comes from the font. ↗
    All five values of the CSS property white-space are honored, text-decoration supports overline and line-through with text-decoration-style and text-decoration-color. The width of the space between words now comes from the space glyph of the font instead of a fixed value, which changes the line breaks of existing layouts slightly. Centered and right aligned table cells no longer measure zero width, and several blocks in a cell stack instead of packing side by side.

0.0.24 (2026-09-01)

  • PlaceObject gets an id attribute for the XML dump. ↗
    The optional attribute id on <PlaceObject> is stamped on the placed object and appears as attr-id next to the box geometry in the output of --dumpoutput, so external tools can map output boxes back to the layout element. The value goes through the usual {} interpolation. It is not a CSS id and does not create a link target (#26).
  • The subcommand list-fonts has been removed. ↗
    The subcommand xts list-fonts, which printed @font-face rules for the fonts in the search path, is gone.
  • The XSD schemas are generated natively, the content model is permissive. ↗
    The XSD layout schemas are now generated directly from commands.xml instead of being converted from RELAX NG with a Java tool. Since XSD cannot express the interleaved content model, the XSD allows the child elements of a command in any order and number, which keeps editor completion working. Validation with the RELAX NG schema remains exact.
  • Value keeps inline markup, imagenotfound=error no longer aborts the run. ↗
    Mixed content in <Value> such as <Value>foo<br/>bar</Value> lost its child elements, the <br/> was dropped and the text glued together. Inline markup is now passed on to the enclosing paragraph. <Image> with imagenotfound="error" inserts the file-not-found placeholder like warning and still produces a PDF, the error only makes the process exit with a non-zero status.

0.0.23 (2026-06-29)

  • PDF/UA, PDF/A and PDF/X are selected with command line flags and configuration keys. ↗
    The conformance classes are configured with the orthogonal flags --pdfua, --pdfa and --pdfx or the matching keys in xts.cfg. The format attribute of <PDFOptions> has been removed. <HTML> accepts <math> with MathML content, and the manual gains a chapter on accessible PDF.
  • Typed values, Template and CallTemplate, and a Programming manual. ↗
    Every command is classified as constructor, action or control flow. A value context, that is <SetVariable> with the new as attribute and the body of <Function>, rejects layout actions such as <PlaceObject>. The as attribute takes a sequence type in the style of XSLT and XQuery. Constructors return queryable data, so count($head) and $head/@width work on a variable that holds <Column> elements. The new commands <Template> and <CallTemplate> offer reusable behavior with parameters. The manual is restructured into Layout Guide, Programming and Reference.
  • Color fonts are supported. ↗
    Fonts with color glyphs (COLR/CPAL, sbix, CBDT and SVG-in-OpenType) such as emoji fonts render in color.
  • New output formats PDF/UA-1 and PDF/UA-2. ↗
    XTS can write tagged, accessible PDF that conforms to PDF/UA-1 and PDF/UA-2.
  • New command AttachFile, placeholder for missing images. ↗
    The new command <AttachFile> embeds a file in the PDF. When an image file is not found, a placeholder is inserted instead of leaving the space empty.