0.35.x

0.35.0

Released:

11.09.2026

The story of this release, in five arcs: a PDF is now one command and one binaryubc build pdf (alpha) typesets the documents the root toctree reaches into a single paginated file, with no Sphinx and no LaTeX in the path: prose, tables that span pages, images, need cards, needlist and needtable, and needpie / needbar as vector charts, with anything not yet typeset drawn as a labelled placeholder box and counted; MyST grew attributes, images, equations and heading links{#id .class key=value} groups give any element an id, CSS classes and per-element options, raw <img> tags become real images that the build collects and checks, bare \begin{align} environments typeset without a $$ fence, and a #fragment link reaches a plain heading in either document, reStructuredText targets included; the needs domain got its interactive table — a needtable sorts, filters, pages, hides columns and exports to CSV in the browser, in ubc build html (alpha) and in the editor’s rendered preview alike; cross-references land where they should, and a finding names the file it is written in — a directive :name: whose slug was already taken now anchors the element you named, an .. envvar:: becomes a real object description, a target carrying a line wrap reaches its inventory, content a needextract pulls in no longer shifts the host page’s anchors, and a warning inside an included fragment, a prologue or a viewed need’s body names the file that holds the text rather than a plausible line of the page; and the tools tell you what they are doing — a live progress bar while a project is indexed, while a site or PDF is built and while its links are checked, Cypher path queries that no longer rebuild every element’s field map, and HTML anchors allocated once instead of by three walks over the same page. Ten new warning-level diagnostic codes reach ubc check, the editor and ubc build html, all on by default, so an unchanged project can see new warnings (see Breaking Changes); the PDF builder’s own twelve codes are reported only by ubc build pdf.

✨ PDF build (alpha)

  • ubc build pdf — the whole project as one PDF (alpha). The documents your root toctree reaches, concatenated into one paginated file with an outline, real internal links and page numbers, typeset from the same index the editor and ubc check use. It draws prose, headings and lists, tables that span pages with their header row redrawn, images and SVG, externally rendered PlantUML diagrams, admonitions and version-change boxes — in the site’s own colours — and needs as cards, with needlist and needtable. See Building a PDF.

  • --need-id and --need-filter build a selection, out of whole documents in toctree order. --need-filter takes the same Python-like expression over need fields that ubc query filter takes; --need-id is repeatable, with the ids unioned. An id that names no need, or an expression that will not compile, exits 2; a selection that matches nothing writes no file and exits 1. See Building a subset of needs.

  • needpie and needbar are vector pictures, drawn straight into the page in the palette the site uses — paths and real text, not a picture of a chart. With :legend: the legend follows as a table. See Charts are vector pictures.

  • Never quietly incomplete, and reproducible. A construct the builder cannot typeset yet — footnotes and citations, maths, contents, raw, video, download, todolist, needflow, needsequence, needgantt and Mermaid — is drawn as a labelled placeholder box and counted on a build.pdf_* warning naming it. The default bar is --deny warning, as for ubc build html, and two builds of unchanged sources are byte-identical. See What is not rendered yet and Warnings and the quality gate.

  • Configuration under [build.pdf]: the page size, the four margins, the base type size, the external PlantUML renderer and the quality bar. The file lands at _build/pdf/<root_doc>.pdf, opens with a title page and carries each document’s title in the top margin, both on by default. See PDF document. It is in alpha: the typeset output, the theming surface and the available options may still change between releases, so it is not yet recommended for production deliverables. There is no incremental PDF build yet — the index underneath it is incremental as ever.

✨ MyST

  • Attributes: {#id .class key=value}. With the attrs_inline and attrs_block extensions enabled, an attribute group gives an element an id, CSS classes and per-element options, inline or on a line of its own. An #id becomes a cross-reference target; on a directive fence the whole group becomes directive options. Five diagnostics report what MyST passes over in silence: attrs.invalid_value, attrs.invalid_choice, attrs.no_target, attrs.unsupported_target and the informational attrs.unsupported_key. See Attributes and Markdown attributes.

  • html_image: a raw <img> tag becomes a real image. A standalone <img src="pics/logo.png" width="50%"> behaves like an {image} directive: the file is collected into _images/, the src is rewritten, and a missing file is reported at build time. Six attributes beyond src are read as the matching {image} options and the rest are dropped; a tag with no src reports md.html_image_no_src and leaves its block raw. See Parsing.

  • amsmath: bare LaTeX environments. \begin{align}\end{align} and the other twelve amsmath environments, each also in its starred form, typeset as display maths with no $$ fence. Equation numbers are not emitted. See Parsing.

  • A #fragment reaches a heading. [](#some-heading) and [](other.md#some-heading) both resolve to a plain heading now, with no explicit (target)=, and the target document may be reStructuredText, so [](guide.rst#installation) reaches that section. A heading is the last rung, so an explicit target, a label, a document of that name and an inventory match all still win. See Cross-referencing with links.

✨ Needs

  • Interactive needtables. A needtable is now interactive by default: the reader sorts by column, filters the rows, pages through them, hides columns, and copies or downloads the table as CSV — in ubc build html and in the editor’s rendered preview alike; :style: table keeps the static table. It is the same asset Sphinx-Needs ships, against the same markup contract. New with it: the :page_size: option and the [needs] keys table_style, table_page_size and table_page_sizes. See Needs.

✨ Cross-references

  • Environment variables: .. envvar:: and the :envvar: role. The directive drew a placeholder and the role drew a chip; both now work, on reStructuredText and Markdown pages alike. An environment variable reaches everything a cross-reference target reaches: objects.inv, the :any: ladder, the general index, site search, and the editor’s hover and go-to-definition. A name defined twice reports std.duplicate_envvar; a dangling :envvar: stays silent, as in Sphinx.

✨ The command line

  • Progress while a project is indexed. Every command that indexes a project for a person — ubc check, every ubc build, ubc report, ubc schema and ubc diff — now shows a live progress bar on a terminal while the files are parsed:

    indexing 412/1001 [████████░░░░░░░░░░░░] 4s
    

    It counts the files this run actually parses, so a warm run with one edit reads 1/1. --verbose replaces it with one parsed <path> line per file; --quiet, a pipe, a file, a CI job and a TERM that is dumb, empty or unset each suppress it on their own. ubc query and the ubc agent verbs stay silent. See while it works.

    ubc build html (alpha) and ubc build pdf (alpha) then show one bar per build phase after the indexing one — rendering, assembling and finishing for the site, lowering, laying out and writing for the PDF — each row cleared before the next opens, and the last before the summary line. The same suppressor rules apply, and under --verbose the existing Timings: line reports each phase’s duration instead.

👌 Improvements

  • One PlantUML executable for every builder. [build.plantuml] names the renderer once for the project; a builder’s own table such as [build.html.plantuml] inherits every key it does not set and wins on every key it does. See the shared renderer table.

  • A diagram both builders draw is rendered once. ubc build html and ubc build pdf share one content-addressed store of externally rendered PlantUML diagrams, so running both costs one run per diagram.

  • The language server reports indexing progress as a percentage. Its ubcode indexing $/progress token now carries a percentage and an N/M files message — one report per whole point, strictly increasing — so an editor shows how far a cold index has got.

  • ubc build linkcheck says what it is doing while it does it (alpha). Before the first request it prints Checking 143 URLs on 37 hosts (98 from cache), and on a terminal it then draws the same progress bar the indexing phase draws:

    checked 37/45 [████████████████░░░░] 12s 9 hosts waiting
    
  • A failing ubc build linkcheck prints only the findings that failed it, so one dead link is not buried under the redirects that did not fail anything. The bar is the deny threshold, or warning when only --max-warnings tripped, and the lower of the two when both did; a run that withholds anything says how many and names the bar.

  • A cosmetic redirect is no longer reported by ubc build linkcheck. A site root redirecting to that site’s own landing page — https://example.test/ to https://example.test/en/stable/ — and a bare www. label appearing or disappearing now read as working, joining the trailing slash, the default port and the dropped fragment. Stored results are re-read under the new rule, so no URL is re-requested. See Link checking.

  • Much faster Cypher queries that navigate a path. A query that only navigates a path — nodes(p) or relationships(p) in a list construct, element equality, length(p), count(p) or p IS NULL — no longer rebuilds the full field map of every element on the path. Measured at 100k nodes in the change’s own benchmarks, such queries run 4 to 76 times faster, and the openCypher conformance suite is unchanged either side — the same scenarios pass, and every skip reason is identical.

  • HTML anchors are allocated once, while a document is lowered, instead of by three walks over the same page. Indexing is faster on every pass, and building or previewing a page that uses footnotes or citations is faster end to end; a page with neither is slightly slower to build. Anchors are otherwise unchanged, except that a section heading carrying an :index: or an :envvar: role now publishes one anchor rather than two.

  • A warning names the file the construct is written in. A finding inside an .. include::d fragment, or in rst_prolog / rst_epilog, is reported against the fragment at its own line; a finding inside a card shown by a needextract names the need’s own document. ubc check, both builders and ubc build linkcheck now print the identical string for the identical fault, and the ubc build html tail (showing N locations) reads (N shown).

  • The font licence notices travel with every copy of ubc. THIRD-PARTY-NOTICES.md and OFL-1.1.txt are staged beside the downloaded executable and at the root of the installed VS Code extension.

‼️ Breaking Changes

  • Unchanged pages render differently. Every needtable that sets no :style: of its own is now an interactive table rather than a static one; set [needs] table_style = "table", or :style: table on the directive, to keep the old one. Source comments and the MyST block break +++ stop reaching the built page. A toctree entry naming a titleless document disappears from the in-page list and the sidebar, and that document’s title reads <no title>. .. envvar:: and :envvar: draw their real output, so objects.inv gains a row per definition. A view directive whose :filter: met a need holding no value for a field renders its real selection instead of failing. An inline image writes its alt attribute before its src.

  • Some anchors move. Content pulled in by a needextract, needimport, src-trace or needreport now anchors under the card’s own namespace, and a host page’s own id{N} and index-{n} anchors shift back to the sequence it has with no view on it. A :name: whose slug was contested or comes out empty gets a different anchor. A :ref:, an objects.inv link and a :need: stay correct; a deep link copied by hand off a built page may need updating.

  • per-file-ignores means one thing on every channel: a finding is suppressed when a rule matches any file that finding names. An included fragment’s finding names the fragment and the page it was spliced into, so a rule naming either still silences it. The one family that gained a file is a card shown by a needextract: it names the need’s own document and not the page, so an existing page-scoped rule stops suppressing findings inside those cards — name the need’s home document instead. See Code matching and .* wildcards.

  • Unknown MyST extension names are now reported. [parse.parsers.<name>] extensions used to pass an unrecognised name through. Only the names ubCode implements take effect now, and any other is reported: config.extension_unimplemented for a MyST-Parser name ubCode has not implemented — substitution, linkify, html_admonition, smartquotes, replacements — and config.unknown_md_extension for a name MyST-Parser does not recognise either. attrs_image reports config.extension_deprecated and keeps working. Remove the entry, or silence the code under Linting. See Parsers and file routing.

  • New warnings may appear on an unchanged project. Ten new warning-level codes reach ubc check, the editor and ubc build html, and on the default gate (deny = "warning") a warning fails ubc check and ubc build html. Two fire on any project: toctree.no_title, for a titleless toctree target, and std.duplicate_envvar, for two .. envvar:: definitions of one name. Three fire on an existing [parse.parsers.<name>].extensions list: config.extension_unimplemented, config.unknown_md_extension and config.extension_deprecated. Five arrive only once a new MyST extension is enabled — attrs.invalid_value, attrs.invalid_choice, attrs.no_target, attrs.unsupported_target and md.html_image_no_src; the informational attrs.unsupported_key never fails a default gate. Two existing codes also fire anew on Markdown pages: every well-formed [^label] is a footnote reference now, so prose that merely looks like one — a bare regex character class in running text — reports ref.unknown_footnote and ref.too_many_autonumbered, and a case-mismatched [^Foo] against a [^foo]: definition does the same, with std.footnote_unreferenced for the definition. Silence a code individually or by family under [lint]: ignore = ["attrs.*"]. The twelve new build.pdf_* codes are not in this group: they are reported only by ubc build pdf.

  • One-time full re-index and re-render on upgrade. The cache format moved (6.167 → 6.180) and the build manifest with it (99 → 112). The first run after upgrading rebuilds the index and re-renders every page once — about the cost of a cold build — then incremental behaviour resumes. Self-healing. The link-check result cache is not discarded: its own version is unchanged, so ubc build linkcheck re-reads every stored verdict and requests nothing it did not have to.

🐛 Fixes

  • A :name: on a directive is now reached by its own name. When that name’s slug was already taken, the directive was renumbered but every reference to it kept landing on whatever won the slug, silently. :ref:, MyST links and objects.inv now all reach the element you named.

  • A cross-reference to a target after a needextract, needimport, src-trace or needreport now lands on it. Content one of these pulls in used to take its anchors from the host page, shifting everything below it, so such a reference landed on the wrong element in HTML, or on nothing at all in a PDF.

  • A need holding no value for a field no longer aborts the whole query. A filter such as 'x' in status used to fail for the entire project as soon as one need held no value for status — in ubc query filter, the editor’s needs panel, the MCP query route and every view directive’s :filter:. Such needs are treated as not matching now, and the encounter is reported once per query.

  • Built pages no longer publish your source comments. A comment written in the source reached the built page as an HTML comment, so authoring notes were readable in the page source. They are dropped now, and the MyST block break +++ renders nothing in a built page; the editor preview still draws its dashed rule for +++.

  • A cross-reference whose target carries a line wrap reaches its inventory. ubCode collapsed whitespace in a cross-reference target for standard-domain object kinds only, so a :ref:, :doc:, :term:, :any: or :external: reference written across two lines missed every objects.inv it should have matched.

  • A toctree entry naming a document with no title is reported, not rendered. Such an entry generates no link, exactly as in a Sphinx build, and now reports toctree.no_title. The document still builds, and reads the placeholder Sphinx shows for it. A .. title:: does not count as a heading; see A page can override its browser title.

  • Every MyST [^label] is a footnote reference, whether or not a definition was written nearby — and a reference resolves against every definition on the finished page, including across an {include} splice in either direction. A label is matched exactly, so [^Foo] does not answer a [^foo]:. See Footnotes.

  • Footnote parity. A footnote finding written inside an included fragment names the fragment and its own line instead of the top of the host page, so ubc check on the fragment reports it. A footnote marker inside link text no longer nests one link inside another, and the .. contents:: listing and the “On this page” panel keep and drop the markers a Sphinx build does. See Footnotes and citations.

  • A glossary term’s index entry reports the term’s own line, not the first line of the file, so ubc query search and the editor’s reference search no longer send you to the top of the page.

  • Incremental builds reconcile a page’s index entries. An edit made in a warm session kept the page’s pre-edit entries, so site search and ubc query search could show entries at stale lines, miss a second definition of a confval or envvar name, and keep serving a deleted .. index::.