Differences from a Sphinx build¶
ubCode has two rendering surfaces of its own:
the live preview in the editor, which re-renders your document on every keystroke,
and ubc build html, which builds a static site
from the whole project (in alpha).
Neither is a full sphinx-build,
and the two do not diverge from one in the same places:
the preview deliberately skips the slower, whole-project transforms
so that it can answer within a keystroke,
while the build runs the project-wide index pass
and so resolves much of what the preview only guesses at.
This page is the honest register of the resulting user-visible differences,
so you can trust what you see and know when to reach for a real build.
How to read this page
Each entry names the surfaces it applies to.
An entry that names ubc build html says how the built site behaves —
sometimes that it shares the difference, sometimes that it does not.
One that speaks only of the preview has not been audited against the
built site: expect the build to share the preview’s difference unless an
entry says otherwise.
Rendering is the default scope.
A few differences also reach indexing and diagnostics —
what ubc check and the editor report —
and those say so in their status line.
Unless one does, the difference changes what you are shown and nothing else.
Preview entries describe the Rendered mode (in Structural mode every directive and role renders as a uniform placeholder; see Authoring).
The main entries follow the same shape — what you write, what the preview shows, what a Sphinx build does, and a short status note; smaller differences are grouped into bullet lists.
Group |
What it covers |
|---|---|
Constructs still shown as placeholders, document topmatter and
|
|
|
|
sphinx-design components and tabs, tables, code blocks, images, diagrams, and directives or roles ubCode does not recognise. |
|
Need cards and the view directives, external |
|
Differences in what the built site contains rather than in how a page renders — today, page redirects. |
|
What is being closed, and when to run a real |
Rendering and transforms¶
Not yet rendered¶
These constructs are recognised and shown as self-describing placeholders, but the transform that gives them their final form does not run in the preview yet.
- Math
You write a formula with the
:math:role, a.. math::block, or MyST dollar math ($x$/$$x$$).The preview shows the formula typeset (KaTeX), as does a built site — including multi-part blocks (blank-line-separated equations align, Sphinx-style),
:no-wrap:raw environments, and:label:/:name:anchors with a ¶ permalink in built output. A formula KaTeX cannot parse stays as its raw source, with the error in the tooltip.A Sphinx build additionally numbers labelled equations and resolves
:eq:/:math:numref:cross-references.Status — typeset now, numbering deferred: equation numbers (
(1)),math_number_all,math_eqref_formatand the:eq:role need the math-domain numbering machinery, which ubCode does not implement yet. Diagnostics are unaffected by that deferral. The deferral is widest for the MySTamsmathextension (bare\begin{align}…\end{align}environments): Sphinx numbers every environment written without a*and gives it anidplus a ¶ permalink, where ubCode numbers none of them and mints no anchor. That id is not stable output in a Sphinx build either — it is a fresh random UUID every time the page is parsed, so on every clean build and whenever the page is edited (an incremental rebuild that does not re-read the page reuses the cached one) — so nothing a reader could link to is lost by the deferral.In the built site’s search index, raw LaTeX is excluded from the indexed terms — from page and section body text, and from the terms taken from a heading that contains math — so a formula’s control sequences (
\frac,\partial) never become search words. The displayed strings still show the LaTeX source: a result whose title or breadcrumb contains math shows the formula source rather than nothing (Sphinx’s own search UI shows it too), and a title in the navigation sidebar, a renderedtoctreeentry (or a:doc:link) or the previous/next links shows that source rather than a typeset formula. Those surfaces do carry the rest of a title’s markup — an inline literal,**bold**and the like all render there — but math is deliberately left flat on them: the typesetter is loaded per page from the page’s own body, so a page that merely links to a formula-titled page never loads it, and half-rendered formulas would be worse than the source text. The On this page column beside the page (the drawer aubc build htmlsite draws from the page’s own headings) is not one of them: it keeps the heading’s markup, so its formulas are typeset like the body. A.. contents::table of contents keeps it too.- Raw HTML in a heading
You write raw HTML inside a section title or heading — MyST’s inline
<b>bold</b>, or reStructuredText’s:raw-html:`…`role.A Sphinx build puts that markup into the page and into the navigation sidebar of every page in the project, live. A heading containing a
<script>therefore runs on every page of the site, and an unclosed tag leaks into the rest of the navigation.ubCode renders it in the page exactly as Sphinx does, but shows it as its own source text in the sidebar, the previous/next links and a rendered
toctree— the surfaces a heading reaches from every other page. Styling markup such as<b>therefore reads as<b>in those places rather than emboldening the entry. Deliberate: those surfaces are the site’s chrome, and one heading should not be able to restyle — or run code in — every page that merely links to it.
- Needs flow diagram (
needflow) You write a
.. needflow::traceability graph.A Sphinx build draws it from the resolved need set via the configured Graphviz or PlantUML engine (an SVG/PNG image).
The preview and
ubc build htmldraw the same graph as a Mermaidflowchart, rendered client-side by the bundled Mermaid runtime (the list, table, sequence and chart views render their real output too — the charts as inline SVG, with no client-side runtime at all; see below).Status — implemented, with these deliberate differences from Sphinx-Needs (the output is self-consistent, not byte-identical to either upstream engine):
Portable presentation options — three options state an intent rather than one engine’s syntax, so the same document reads the same whatever draws the graph. All three behave identically in Sphinx-Needs; the two rows below them are the only places the two tools deliberately differ.
:direction:— where the graph grows:down(the default),up,rightorleft. The engine tokensTB/TD,BT,LRandRLare accepted as aliases. Mermaid draws all four natively, so nothing degrades. It supersedes the direction a:config:sets, which keeps working as the lower-precedence source; when a:direction:and the direction a:config:sets disagree, the:direction:option wins and aneeds.option_conflictwarning says so. Agreement is silent.:show_link_names:— the existing flag, widened to take an optional value:none,outgoing,incomingortype. Written bare it still meansoutgoing, exactly as before, so no existing document changes. The value is what lets one diagram disagree with a project default: a flag could only ever turn labels on, soneeds.flow_show_links = "outgoing"could not be switched off for a single diagram —:show_link_names: nonenow does that.:show_legend:— the existing flag, widened to take an optional key: the name of a [needs.flow_legends] entry, which says what the legend lists (parts— an ordered list oftypes/linkssections, where["links", "types"]really does draw the link table first) and where it would like to be drawn (placement). Written bare it takes the project’sneeds.flow_show_legend, and then the engine default. The value is a key and never an inline setting, deliberately: a project may name a legendtypes, and an option accepting both would need a rule to say which one:show_legend: typesmeant.Presence stays per-directive.
needs.flow_show_legendselects which legend, never whether there is one, so no project setting can add a legend to a diagram that did not ask for one. An unknown key is reported against the directive and the default legend is drawn anyway, so a typo costs the styling and not the legend.
Each has a project-wide default under [needs]:
flow_direction,flow_show_linksandflow_show_legend. A malformed value raisesneeds.option_invalidand is treated as unset, so the lower-precedence source (or the project default) still decides.Note
:direction:is not yet accepted by Sphinx-Needs. A document using it builds under ubCode but a Sphinx build reports an unknown option and drops the whole directive, until the matching Sphinx-Needs release lands (acceptance is in flight, following the route ubCode’s earlier view options took into Sphinx-Needs 8.4.0::cypher:,:width:and:height:are parsed there and ignored, while:max_items:went further and is genuinely implemented). Until then, keep a portable document on:config:.:show_legend:and:show_link_names:are the exceptions, and deliberately so: they are the options both projects already have, so a bare one of either is portable today. Sphinx-Needs is widening both in step — the same four label values under the same “bare meansoutgoing” rule, and the same legend keys — but until that release lands, either option with a value is refused by Sphinx the way:direction:is.The legend is a table beside the diagram, not a block inside it. A bare
:show_legend:draws Sphinx-Needs’ in-diagram legend there and ubCode’s external table here.Mermaid’s flowchart syntax has no legend construct at all, so there is nowhere inside the picture to put one; the external table carries the identical rows and differs only in where it sits.
placement = "internal"is honoured by Sphinx-Needs and silently drawn external by ubCode. No warning: the value is correct for the configuration it is shared with, and there would be nothing for an author to do about it.placementunset means the engine’s default placement, which isinternalfor Sphinx-Needs andexternalhere — one axis each engine answers for itself, the same shape as the engine default for a legend nobody named. So a sharedubproject.tomlthat sets onlypartslands differently in the two tools by design, and neither is overriding the other.Engine — ubCode draws Mermaid by default, and can draw
needflowwith a real PlantUML instead. A project that configures a PlantUML executable and sets[build.plantuml] needflow = truehas everyneedflowinubc build htmlandubc build pdflaid out by PlantUML itself, with each node a link to its need — see External PlantUML renderer.:engine:(graphviz/plantuml) overrides that per diagram, in either direction:plantumldraws this diagram with the executable even when the key is off, andgraphvizkeeps this diagram on ubCode’s own engine even when the key is on (ubCode has no Graphviz pathway, sographvizmeans “not PlantUML” here). With no executable configured both values are accepted and ignored, so a source file shared with a Sphinx build raises nothing.[needs] flow_engineis read as the low-precedence default for the key, so oneubproject.tomlneed not say the same thing twice. The editor preview always converts to Mermaid, whatever the project configured, so a built page and the preview show different renderings of the same diagram — the same tradeneedumlalready makes. It is a fidelity choice rather than a functional one, and four intents cannot be carried over exactly::direction: upand:direction: leftare drawn on their axis mate (downandright) with aneeds.flow_direction_degradedwarning, because PlantUML lays a diagram out on one axis per statement; a need type whosestyleis not one of PlantUML’s own node keywords is drawn as arectangle(Sphinx-Needs passes the value through and loses the whole diagram to a syntax error); a link type’sstyle_start/style_endare accepted and ignored, as they are on the Mermaid arm — an edge is always drawn-->-headed; and astyleword PlantUML does not know is dropped rather than passed through, so the edge draws as a plain line instead of costing the diagram. The legend stays a table beside the diagram, and parts and child needs stay un-nested, on both arms. A%in a need title, in a link type’s name or in the output of [needs] diagram_template is drawn as the literal%the author wrote. PlantUML runs a preprocessor before it parses a diagram, and that preprocessor expands%fn(…)built-ins —%version(),%filename(),%date("yyyy")and the rest — inside a quoted label; several other forms refuse the whole diagram outright. Sphinx-Needs hands the label straight to PlantUML, so a template that deliberately writes%version()expands there and draws literally here. The one place a built-in is honoured is a [needs.flow_configs] block, whose content is spliced verbatim exactly as Sphinx-Needs splices it — configuration is yours to write, a need’s title is data. An&is escaped for the same reason: PlantUML decodes numeric character references in a label, so an unescaped in a title would break the label up.A diagram the executable could not draw falls back to the Mermaid flowchart with a
needs.flow_external_render_failedwarning, so a failure is visible without being fatal.:debug:on such a diagram shows two blocks: the PlantUML that was attempted, labelled with the failure, and then the Mermaid that was drawn in its place — the warning’s line number refers to the first of them. Aneedflowreached through aneedextractorneedimportcard is always the Mermaid flowchart, whatever the project configured: imported content is re-rendered as a self-contained document with no host page to resolve against, and the PlantUML pathway draws only the diagrams the index captured on a real page. So a diagram written inside a need’s body can appear drawn where the need itself is rendered and as Mermaid inside a card that extracts it — the same source, two renderings, on one page.:config:names entries of needs.flow_configs. Two of the names need no entry at all:lefttorightandtoptobottomare read as the layout direction itself, on both arms, and neither warns when the project defines no block under them. Sphinx-Needs ships those two asneeds_flow_configsentries; ubCode ships no entries at all, so the two spellings are built in instead. A project that does define a block under either name still gets its content inserted, as for any other name. Every other name is looked up, and a name with no entry is reported. The direction a:config:sets is the low-precedence source for:direction:above, and it is read from the entry’s CONTENT — a block containingleft to right directionortop to bottom directionsets that direction, which is how Sphinx-Needs reads it too, so the common idiom of one project-wide style block carrying a direction statement gives the same axis in both tools. A block setting both statements resolves to top-to-bottom, again as in Sphinx-Needs, and so does a:config:naming two blocks that disagree — the search is over the blocks’ joined content, so the order they are named in does not change the answer. A block defined under one of the two direction names is read like any other block: its content decides, and the name does not override it. One case still differs deliberately. A direction statement that is commented out (' left to right direction) is inert to ubCode, which draws what it derived; Sphinx-Needs finds the text inside the comment, treats the direction as already set and suppresses its own statement, so it draws the project default instead. ubCode is the self-consistent one there, and that is the reason it emits its direction statement unconditionally. On the Mermaid arm the entry content is otherwise ignored: only the direction it sets is read. On the PlantUML arm each named entry’s content is inserted at the top of the generated diagram, as Sphinx-Needs does, and the resolved:direction:is written after it, so an explicit direction still wins.:alt:is accepted and ignored (it applies to a raster image, not an SVG).:scale:is accepted and warns, pointing at:width:/:height:below: it scales a raster image, which is not what ubCode draws.Size —
:width:and:height:size the rendered diagram, in the preview and inubc build htmlalike. They take a CSS length or percentage (a bare number is pixels), and any CSS unit is accepted —60vhand30remare ordinary here, because the diagram is a CSS-sized box rather than an image. A value that is not a size at all raises a warning and the diagram renders without that dimension. Sphinx-Needs has no size option on any of its diagram directives. Setting:height:also lifts the default height bound described under Diagrams below.Link types — with no
:link_types:option the diagram draws edges of every configured link type (exceptparent_needs, which is never drawn), matching Sphinx-Needs’ effective behaviour: itsneedflowdirective defaults:link_types:to all link fields, so theneeds_flow_link_typesconfig (default["links"]) is inert forneedflowin Sphinx-Needs 8.4.0 and 8.5.0 — ubCode intentionally has no equivalent key.Node cap — a default cap of 100 drawn nodes (with a shown-vs-total notice) keeps a large graph readable and within Mermaid’s edge limits;
:max_items: 0draws them all. The default is configurable project-wide with needs.views_max_items, and a build that caps a diagram also emits abuild.need_view_truncatedwarning. Sphinx-Needs does not implement this option.Flat, need-level graph — a need’s parts are not drawn as their own nodes: they are not matched as separate items (the project-wide convention the
:need_count:role also follows), so an edge to a part collapses to that part’s base need. The parent/child hierarchy is likewise not drawn as nesting (Sphinx-Needs clusters child needs inside their parent).Click-through — on the Mermaid arm nodes are not yet click-through links (the Mermaid runtime is initialised in its strict security mode, which strips link bindings); use the surrounding need cards / references to navigate. A diagram drawn by a PlantUML executable does carry them: each node is a real link to its need’s card, resolved the same way a
:need:reference in prose is.Caption — a
:caption:renders as a caption paragraph rather than a docutilsfigure, and its inline markup is not parsed (as for diagram captions above).No automatic anchor; use
:name:. A diagram with a:name:carries a stableidyou can:ref:in every body state, and a¶in its caption — but the caption is written only when the diagram is DRAWN, so a named view that is empty or failed keeps itsidand shows neither. An unnamed one carries none of it, and ubCode never mints the positional id a Sphinx build does. See Anchors on views and diagrams for the whole rule, including the:name:values that normalise to nothing.``:root_depth:`` — the
:root_id:walk bounds by shortest-path distance from the root (a breadth-first walk), rather than Sphinx-Needs’ order-dependent last-recorded distance; the two agree except at the exact depth boundary of a diamond-shaped graph.``:border_color:`` — applied as a literal colour (a bare 3- or 6-digit hex such as
FF0000is accepted, as Sphinx-Needs does; a per-need variant expression is not resolved yet).
- Needs sequence diagram (
needsequence) You write a
.. needsequence::message flow, starting from one or more:start:needs.A Sphinx build draws it via PlantUML (an SVG/PNG image); Sphinx-Needs offers no other engine for this directive.
The preview and
ubc build htmldraw the same walk as a MermaidsequenceDiagram, rendered client-side by the bundled Mermaid runtime — unless the project configured a PlantUML executable and set[build.plantuml] needsequence = true, in which caseubc build htmlandubc build pdfhave that draw it, from the same walk. See External PlantUML renderer. The key is separate fromneedflow’s, and [needs] flow_engine does not default it: Sphinx-Needs’needsequenceis PlantUML-only and has no engine option, so there is nothing for that key — or for a:engine:on the directive, which does not exist — to choose between. The editor preview always uses the Mermaid conversion, whatever the project configured. A diagram the executable could not draw falls back to the Mermaid one with aneeds.sequence_external_render_failedwarning.The walk itself matches Sphinx-Needs: from each
:start:need, the needs it links to are the messages, and each message need’s own links name the receivers. So a message need is never drawn as a lifeline — its title becomes the arrow label, which is what lets the diagram show a mediated relationship (specification → programme objective, labelled with the requirement that connects them).Status — implemented, with these deliberate differences from Sphinx-Needs (the output is self-consistent, not byte-identical to the PlantUML engine):
Link types — with no
:link_types:option the walk followslinksonly, matching Sphinx-Needs, whoseneedsequencehard-codes that default. Note this is not theneedflowrule, which defaults to every configured link type.parent_needsmay be walked here, whereneedflownever draws it.One walk across all starts — several
:start:ids share a single set of already-visited needs, so each lifeline is declared once and no subtree is walked twice; arrows between the starts’ subtrees are still drawn. Sphinx-Needs restarts its bookkeeping per:start:id, which redraws a shared subtree and declares its participants again.Every drawn need is declared — including a need that only receives messages, which is shown with its title. Sphinx-Needs declares only senders and lets PlantUML auto-create the rest, so a receive-only lifeline appears under its raw ID while its neighbours show titles. A need whose messages exist but whose receivers are all filtered away or unresolvable keeps its lifeline, with no arrow attached — Sphinx-Needs does the same, and it is how the diagram says “this need was reached, and its messages went nowhere”. Truncation is the exception: the
:max_items:cap never adds a lifeline the surviving messages do not touch.Nothing to draw shows the empty-result paragraph — a
:start:need with no links, an unknown:start:id, or a:filter:that admits no receiver each render the “no needs” wording (or your:filter_warning:) rather than a diagram. Sphinx-Needs renders an empty diagram frame for the first of those.Errors are diagnostics, never a failed build — all three of the conditions below are fatal to a Sphinx build (a missing or blank
:start:and an unknown:start:id raiseNeedSequenceException; an unknown:link_types:name warns and then dies on aKeyError). None of them stops a ubCode build, but what you are told differs:a missing or blank
:start:warns (needs.option_invalid) and the directive renders the empty-result paragraph;an unknown ``:link_types:`` name warns, with a did-you-mean over the project’s configured link types, and that type is dropped from the walk;
an unknown ``:start:`` entry is dropped SILENTLY — no diagnostic anywhere. If every entry drops, all you see is the empty-result paragraph. This is the one place the directive fails without telling you, so a mistyped
:start:looks exactly like a filter that matched nothing; check the ID against the project when a sequence comes out empty.
Titles and IDs are escaped — a title containing
#,;or a Mermaid init directive, and an ID that collides with another after sanitisation or matches a Mermaid keyword, are all made safe. Sphinx-Needs escapes nothing, so such a title or ID produces broken PlantUML. The PlantUML arm escapes them too, for its own grammar: a real newline or a"in a title is a whole-diagram failure there, and two IDs that fold alike would otherwise overwrite one lifeline’s label in silence. A Mermaid init directive needs no counterpart guard on that arm — the form is inert to PlantUML — but a%does: PlantUML expands%version()and its other built-in calls inside a quoted title, so a%anywhere in a title or a message label is escaped on that arm as well, and renders as the%the author wrote. Without that, a title mentioning one of those names either lost the whole diagram or published text nobody wrote.Legend —
:show_legend:lists the types of the needs actually drawn, matchingneedflow. Sphinx-Needs lists every configured need type, including the service types it adds itself.``:show_filters:`` — echoes only
:filter:, the one selection surface this directive actually honours. Sphinx-Needs also printsstatus,tagsandtypes, which its own selection never consulted.Accepted and ignored —
:status:,:tags:,:types:,:sort_by:,:highlight:and:show_link_names:have no effect in Sphinx-Needs’needsequenceeither (it never reads them); ubCode warns so the mismatch is visible rather than silent.:config:carries PlantUML configuration, which cannot apply to a Mermaid sequence diagram, and also warns — and it keeps warning even when the diagram is drawn by a PlantUML executable, because ubCode does not record which blocks the option named: aneedsequence’s:config:is diagnosed and dropped rather than stored, where aneedflow’s is stored. (Aneedflowdrawn by PlantUML therefore splices its:config:blocks; aneedsequencedoes not.):scale:warns too, pointing at:width:/:height:below: it scales a raster image, which is not what ubCode draws.Size —
:width:and:height:size the rendered diagram, exactly as forneedflowabove (a CSS length or percentage, any CSS unit, a bare number is pixels), and:height:lifts the default height bound described under Diagrams below. Sphinx-Needs has no size option here either.Message cap — a default cap of 100 drawn messages (with a shown-vs-total notice) keeps a deep walk readable;
:max_items: 0draws them all. The default is configurable project-wide with needs.views_max_items, and a build that caps a diagram also emits abuild.need_view_truncatedwarning. Sphinx-Needs does not implement this option.Caption — the directive argument renders as a caption paragraph rather than a docutils
figure, and its inline markup is not parsed (as forneedflowabove).No click-through — lifelines are not click-through links, for the same reason as
needflow.
- Needs chart views (
needpie/needbar) You write a
.. needpie::or.. needbar::chart — one filter per content line for a pie, a separator-split grid of filters for a bar, each entry counted over the needs it matches (a bare non-negative integer is taken as a literal value instead, as upstream does).A Sphinx build draws the chart with Matplotlib and writes it into the output as an image file referenced by an
<img>.The preview and
ubc build htmldraw the same chart as an inline SVG written straight into the page, followed by a legend table carrying every number the picture shows. That table is always written, but it is shown only with:legend:: without the flag it is visually hidden — still read by assistive technology, still selectable and still indexed for search, simply not drawn. There is no image file, no client-side runtime and no extra request: the chart renders with JavaScript disabled and in a printed page.Status — implemented, with these deliberate differences from Sphinx-Needs (the output is self-consistent, not pixel-identical):
Counts cover needs only — this is the difference most likely to change a number you are reading. A Sphinx-Needs build counts needs and their parts in every slice and cell; ubCode counts needs. A filter over the
is_partcontext constant therefore renders 0 here (need parts are not first-class filter items — the parts themselves are modelled, and aneedtablewith:show_parts:renders a row for each), and any project using parts should expect its chart values to be lower than a Sphinx build’s. This matches the:need_count:role, which has always counted needs only — the same filter, evaluated by the same code path, cannot disagree with a chart cell.Bad input warns and degrades; it never fails the build — a Sphinx-Needs build turns fifteen ordinary authoring mistakes into a hard abort with no HTML at all (most of them as raw Matplotlib messages). ubCode reports each one as an ordinary warning (
needs.filter_invalidorneeds.option_invalid, on the directive or the option that caused it) and still draws the chart: an unparseable filter counts 0 in place, keeping the slice’s position and its label; a shortneedbarrow is padded with zero-valued cells, or with empty column names when the short row is the one:xlabels: FROM_DATAconsumes; a:labels:/:xlabels:/:ylabels:/:explode:list that does not match the data is padded with numbered labels or truncated; an unusable:colors:value falls back to the theme palette, and an unusable:text_color:value to the theme’s text colour; a:colors:list shorter than the data continues from the theme palette for the rest (a Sphinx-Needs build cycles the author’s own colours around a pie instead, and pads a bar with its default cycle); an emptyneedbardata cell counts 0 and a blank line betweenneedpiefilters is skipped (a Sphinx-Needs build silently counts every need in both of those cases). Theneedbarhalf of that is a DATA-cell rule only: an empty cell in a row or column aFROM_DATAsentinel consumes is a label, so it is left as an unnamed series and never warned about, exactly as upstream leaves it.Deterministic output — the same sources produce byte-identical chart markup on every build: fixed-precision geometry, ordered data, and element ids from a per-document counter, with no clock and no randomness anywhere in the path. Across platforms the guarantee is evidenced rather than asserted — the pinned chart fixtures are rendered and byte-compared on Linux, Windows and macOS in CI, so a platform difference would show up as a failing test. A Sphinx-Needs build’s chart images are not byte-reproducible — each carries the build’s wall-clock time, and a bar chart’s internal element ids are freshly random per build — so two builds of unchanged sources differ.
Correct on an incremental build — a page’s chart re-renders exactly when a number, label or colour it draws actually changes, including when the need that moved lives in a different document. A Sphinx-Needs build leaves such a chart stale until its own page is touched (as it does for
needtableand:need_count:).Accessible by construction — the SVG carries
role="img"and is named and described by id. The accessible name is the chart’s title argument when there is one, and the generated summary when there is not, so a chart is never left unnamed. The description is that summary: for a pie it names each slice and its value, and for a bar it gives the shape (how many series over how many categories) and points at the table, because reading a whole grid aloud is worse than reaching it as a table. The table below the plot is the data: every label and value as ordinary selectable text, with a share column for a pie. A Sphinx-Needs build emits an<img>whosealtis the image file’s own path forneedpiealways, and forneedbarwhenever no title is given; the series is not reachable as text at all.Option fidelity —
:style:is accepted and ignored: it names a Matplotlib style-sheet preset, which describes nothing about ubCode’s markup (an unknown name ends a Sphinx build; here it does nothing).:shadow:is likewise accepted and ignored — a themed drop shadow costs markup weight for a purely cosmetic effect.:filter-func:(needpieonly) is accepted, never executed, and warned atneeds.filter_unsupported, as it is on every other view directive; the chart still renders from its content lines.:show_top_sum:labels each column with its total — on a grouped chart a Sphinx-Needs build instead labels every individual bar with its own value, which is what:show_sum:already does here, and on a stacked chart it labels only the last layer, with that layer’s own value rather than the stack’s total.:text_color:follows upstream’s reach, which differs per directive: on aneedpieit recolours the in-wedge value labels only (the title keeps the theme colour), on aneedbarit recolours every text in the chart. Note the hazard that upstream cannot have: its chart is a baked image with its own background, while ubCode paints on the page, whose background follows the theme — so a colour chosen against a light page can be unreadable in dark. For the value labels that hazard is measured away: a hex:text_color:is given a contrast halo computed from the colour itself (the near-black or near-white that contrasts more with it), painted under the glyphs, so the digits keep their shape whatever the theme or the bar underneath them does. A named colour is not resolved — naming a colour is not knowing its value — and keeps the page-coloured halo, exactly as a named:colors:entry does. The hazard remains for the texts that sit on the page and so carry no halo: aneedbar’s title, tick and axis-title labels. Leave it unset to follow the theme.Where ubCode is stricter or simply works — a rotation option takes any finite angle, including a negative one, clamped to one turn (a Sphinx-Needs build silently ignores anything that is not a non-negative integer, so
-45does nothing there);:ylabels: FROM_DATAon its own works here and always fails a Sphinx build;:separator:is taken literally in both, so quotes are part of the separator. An all-zeroneedpierenders the “no needs” paragraph (with:filter_warning:as its wording), matching upstream; an all-zeroneedbarstill draws, with zero-height bars, also matching upstream — it has no all-zero empty state and no:filter_warning:option, and ubCode does not invent either. (A bar whose grid is empty rather than zero — stripped to nothing by:xlabels: FROM_DATAon a one-row body, say — does render the default paragraph: the empty arm itself is not chart-kind-specific, only the all-zero trigger is.)Layout limits — the plot has a fixed aspect and cannot measure text, so a long label may overlap its neighbour in the plot area. The legend table below the chart always carries the full text, but it is only visible with
:legend:; without the flag the full text is there for assistive technology and for search, and a reader looking at an overlapping label has to add the flag to see it. In-wedge values are omitted for slices under 5% and for empty slices, where they would collide.The filter options are a SCOPE, not a selection — both charts take
:filter:,:status:,:tags:and:types:, and a scope is not a slicer. This entry used to read “No filter options — as upstream, neither chart takes:filter:,:status:,:tags:or:types:: a chart’s selection lives in its content, one filter per cell.” The premise still holds — the selection does live in the content — which is exactly why a directive-level option cannot be a selection here and does something else instead: the needs it selects are the universe every content line is counted in, so a slice is that line’s filter intersected with the scope. The lines still do the slicing: add a line and you add a slice, exactly as without a scope. A bare integer line is unaffected (it is its own value, not a count of anything), and a scope on an empty body is still an empty chart — a scope is not a slice.The ubCode-only
:max_items:is still not offered, and would be actively wrong — a chart renders counts rather than per-need items, so a cap would silently change the numbers rather than truncate a list. That is the useful contrast: a scope narrows the universe a number is counted over, a cap changes the number.and both charts take a ubCode-only
:cypher:SCOPE — the same concept in the other spelling, reading the same way.:cypher:takes precedence when both are written, so carrying both is the recommended portable form: ubCode evaluates the query, a Sphinx build reads the python surfaces, and the two engines then show the same numbers — subject to Counts cover needs only above, which applies to a paired scope exactly as it applies to a content line. Both sides of the intersection are counted needs-only here and needs-and-parts there, so a project using parts still sees lower ubCode values; what the pairing removes is the divergence caused by the scope being read by one engine and not the other, not the divergence in what a count includes. Written alone, a:cypher:is the one compatibility option whose no-op changes a rendered number: a Sphinx build ignores it and counts each line over the whole project. Some scopes have no python spelling at all — a link traversal, which is the reason to reach for the option in the first place — and for those the divergence stands, accepted and documented rather than warned about.needbarused to offer neither spelling; both are now read there, and a bar’s cells scope exactly as a pie’s slices do. Sphinx-Needs acceptance status: the accept-and-ignore change for:cypher:on both charts shipped in Sphinx-Needs 8.5.0 (sphinx-needs#1818), and the four python scope options are the chart’s scope there from that same release onwards (sphinx-needs#1831); on 8.4.0 and earlier a Sphinx build reportsunknown optionfor any of the five and drops the directive, so the view disappears from the page (an ordinary build still reports success — only-Wfails). The reading, with an example, is in A chart’s filter options SCOPE its slices.
- Needs gantt chart (
needgantt) You write a
.. needgantt::selecting needs with the ordinary filter surfaces — each selected need becomes a task whose LENGTH comes from a need field (:duration_option:, defaultduration) and whose PROGRESS comes from another (:completion_option:, defaultcompletion); its POSITION comes from its links, through the three constraint families:starts_after_links:(defaultlinks),:starts_with_links:and:ends_with_links:. There is no date on a need::start_date:is the only calendar in the whole feature, and it is optional.A Sphinx build concatenates that into
@startgantttext and hands it to PlantUML, which solves the schedule and bakes an image.The preview and
ubc build htmlrender an inline SVG written straight into the page — ubCode solves the schedule itself, with a deterministic topological longest-path pass — with an elbow connector per dependency, a progress track on every task that has one, and (under:show_legend:) a task table carrying every task’s start, end, duration, completion and link as ordinary selectable text. There is no image file, no client-side runtime and no extra request, so a chart renders with JavaScript disabled and in print.Status — implemented, with these deliberate differences from Sphinx-Needs:
Every task is drawn once. This is the largest visible difference, and it is a Sphinx-Needs defect rather than a design choice on either side. Its generated PlantUML declares each task as
[title] as [id]but then addresses that task’s completion and colour lines by[title]; PlantUML binds a bracketed name it has not seen as an alias to a brand-new zero-length task, so each of those lines silently creates a phantom bar. Becausetype_coloris populated for every configured need type by default, in practice every task in every Sphinx-Needs gantt is currently drawn twice. Two needs that share a title collide there for the same reason; here they are two independent tasks.The two need fields must be declared, and are never invented. A Sphinx build adds
durationandcompletionas integer fields to every need type of every project — whether or not aneedganttis ever written — from needs_duration_option and needs_completion_option. ubCode does not: declare them under[needs.fields], or point the project-wide[needs] duration_option/completion_optionat fields you already have. Aneedganttnaming an undeclared or non-numeric field reportsneeds.option_invalidwith a did-you-mean and still DRAWS — every task one day long, no progress — where a Sphinx build logs aconfigwarning and deletes the directive from the page.Warn and degrade, never abort. A malformed
:start_date:and an unknown:timeline:each raiseNeedGanttExceptionin a Sphinx build — aBaseExceptionsubclass noexcept Exceptioncatches — and take the whole build down with a traceback. Here each is a warning: the chart falls back to relative day numbering, or to the daily axis, and still draws.A constraint cycle is reported, not drawn. PlantUML renders a cycle (including a need that links to itself) without complaint, so the picture a reader saw was silently meaningless. ubCode names the tasks involved and shows the view’s error block instead.
A dangling constraint target is reported, not fatal. A link naming a need that does not exist is an unguarded dictionary lookup in a Sphinx build — a
KeyErrorthat ends it. Here the constraint is dropped, the missing ids are named in a notice above the chart, and everything else draws. A target that is a real need but is filtered out of this chart is dropped silently, because there is no bar to anchor to — so that task is drawn at its unconstrained position. Be aware of what that means: a:filter:written slightly too narrowly (astatus == 'open'that drops a completed predecessor, a:types:that drops one tier) turns a chained schedule into a set of tasks all starting on day one, and the chart carries no signal that it happened. It is deliberately not reported — an inline notice on every filtered chart would be noise, and a filter is an instruction rather than a mistake — but if a chart’s tasks all start together and you expected a chain, this is the first thing to check.The month you wrote is the month you get. A Sphinx build indexes a zero-based month table with a one-based month number, so
:start_date: 2020-03-25reaches PlantUML as “the 25th of April 2020” — one month late — and any December start date raisesIndexErrorand ends the build.Task bars link to their needs. The Sphinx-Needs documentation says they do; its code emits no link anywhere, so no gantt it has ever produced had one. Here each bar is a plain SVG
<a href>— no client runtime — and the task table’s name cell carries the same link.A need type with no configured colour takes a theme palette slot, so it still reads as a distinct series and still re-themes with the page; a type with a colour uses it, as upstream does.
:no_color:puts every bar on the neutral theme fill.The task table is
:show_legend:-gated, and IS the legend. By default a chart is just the chart, as a Sphinx build’s is. With:show_legend:a table follows it — one row per task, with a colour swatch, the need’s own link, its type, and its start, end, duration and completion as text — rather than the separate colour-per-type tableneedflowandneedsequenceemit. One table, because it already carries a per-task swatch: a second would be a second authority on the same colours, and could not name a palette slot at all, since that is a stylesheet token rather than a value. Whether or not the table is drawn, the chart’s<desc>names up to 25 tasks with their dates and then summarises the rest (…and N more; set :show_legend: for the full table.), so a chart of any size is readable as text and a chart beyond that size says where the whole of it is. A 500-task<desc>would be longer than the page it describes, which is why the cap exists;:show_legend:is the uncapped option.Row order follows the document, not the id. With no
:sort_by:, tasks are drawn in the order the needs were written — page by page, and within a page in directive order — which is the order a Sphinx build’sprocess_filtersreturns and the order a plan reads in. (ubCode’s other views default to id order, because a list has no authored cascade to preserve; a schedule does.) Pages are ordered by path rather than by the toctree, which is a deterministic stand-in rather than a claim of parity with Sphinx’s read order; needs with no need-directive origin — imported, external,src-trace— carry nopage_order, so they sort after the authored ones, by id. (A traced need’s card does sit at its.. src-trace::site — the schedule simply does not use that position, which is a deliberate limit rather than an absence.)Dependencies are drawn. Every constraint the schedule honours gets an elbow connector from the anchor it pins to towards the anchor it pins, in a neutral colour — the Sphinx-Needs output does the same through PlantUML. A constraint the schedule could not honour (see the conflict rule below) gets no arrow, because the picture does not show that relationship; it is named in the
:debug:dump instead.Progress is a track, not a second shade. A task with a completion draws as an outline in its own colour with the done portion filled solid inside it, so “half done” reads at a glance in either theme. A task with no completion draws as a plain solid bar, as upstream does.
No automatic anchor; use
:name:. A chart with a:name:carries a stableidyou can:ref:, and a¶in its caption when it has one — aneedganttgiven no argument has no caption, and unlikeneedflowa captioned one draws its caption in every body state, so its¶is there even when the filter matched nothing. An unnamed chart carries neither, and ubCode never mints the positional id a Sphinx build does. See Anchors on views and diagrams for the whole rule — it is the same for every view and diagram, including the:name:values that normalise to nothing.A completion value outside 0–100 is reported as written but clamped when drawn, so the fill cannot overflow its bar. Neither tool range-checks the field; a Sphinx build passes
500to PlantUML verbatim. A completion of0draws no overlay, matching upstream, which emits its completion line only for a truthy value.A missing, zero or negative duration becomes one day — upstream’s own substitution, kept so a project moved between the two tools draws the same bars. A fractional duration is rounded, also as upstream does.
:config:and:scale:are accepted and reported.:config:carries a raw PlantUML snippet (read, oddly, from needs_flow_configs —needflow’s own table) which cannot reach an SVG ubCode draws;:scale:sizes a raster image, and is redirected to the ubCode-only:width:/:height:. Both reportneeds.option_unsupported.:link_types:,:show_link_names:and:highlight:have no effect in either tool — a Sphinx build declares them on this directive and never reads them (a gantt’s link vocabulary is the three*_linksoptions). ubCode reportsneeds.option_unsupportedso the no-op is not silent.:debug:shows the solved schedule, one line per task, rather than generated diagram source — there is none, and the schedule is what a reader debugging a surprising chart needs.No
:max_items:and no:cypher:. Capping a schedule drops predecessors out from under the tasks that remain, so every surviving bar moves: the result is not a shortened chart but a wrong one.parent_needsis not a constraint link type — the parent-child edge is containment, not precedence, and scheduling a child after its parent’s end is never what an author means. This followsneedflowrather thanneedsequence.Day one is the earliest BAR, not “the project”. The schedule is solved in relative offsets and then slid so that the earliest task sits on day one — and the slide applies to every task, including tasks that carry no links at all. So an
:ends_with_links:chain that has to reach back before the start moves the whole chart, and a task you never linked to anything can change which day it is drawn on because of another task’s links. On a dated chart this means:start_date:anchors the earliest bar rather than the project: a Sphinx build’sProject starts the …line is a project anchor, and this is not. The alternative — clamping only the tasks that ran negative — would break the relative offsets the constraints encode, which is why the whole solution moves.Where two constraints on one task conflict, the later position wins. A task’s start is the maximum over everything constraining it (that is what makes the schedule independent of the order the links happen to be walked in), so a need carrying links in two families — say
:ends_with_links:to a short task and:starts_after_links:to a long one — takes the later of the two, and the:ends_with_links:it cannot also satisfy is simply not honoured. A dropped constraint of this kind is named in the:debug:schedule dump, which is the surface for it; the chart itself shows the position that won.Layout limits — the plot has a fixed width and cannot measure text, so a long task title is ellipsised in the left gutter; the task table below always carries the full title, unabridged and linked. An in-bar completion percentage is omitted for bars too narrow to hold it.
No “today” marker, deliberately: it would move the page’s bytes every midnight, which would defeat the incremental build’s byte-comparison and make a rebuilt site differ from itself for no authored reason.
- Needs report (
needreport) You write a
.. needreport::with any of:types:,:links:,:options:and:usage:— an introspection of the project’s own vocabulary rather than a view over its needs.A Sphinx build renders a packaged Jinja template into reStructuredText and splices it back into the page, producing four
.. dropdown::sections; a:template:option (or needs_report_template) replaces that template with one of your own.The preview and
ubc build htmldo the same: with no template configured they compose those four sections themselves — the same.. dropdown::construct, the same tables, the same columns — and with a:template:or a[needs] report_templatethey render your template against the same five names a Sphinx build supplies (types,links,options,usageandreport_directive), splicing its reStructuredText back into the page. A section you did not ask for arrives as an empty value rather than as a missing name, so a template can always reference all five.Status — implemented, with these deliberate differences from Sphinx-Needs:
The counts in
usageare real — a Sphinx build’s are permanently zero, because its directive runs while documents are still being read and no correct count exists yet (useblocks/sphinx-needs#1110, which its own documentation states as a warning). ubCode evaluates the report from a finished index, sousage.needs_amountandusage["needs_types"]carry the numbers you would expect.If you want a template that renders identically in both systems, use
:need_count:roles for the numbers, as the Sphinx-Needs packaged template does — they work the same way here, and ubCode tracks what they resolve to so a warm rebuild cannot serve a stale one.A
:template:may not leave the project’s source root — a path that climbs out of it is refused withneeds.report_template_outside_projectand the default report is rendered instead. A Sphinx build reads it. The reason is that ubCode resolves and reads this path in the editor, on every preview, where a Sphinx build only ever reads it inside a build you chose to run: unclamped, opening a checked-out repository would read a file that repository named, with no command run at all.[needs] report_templateis not clamped — it is written by the project owner in the project’s own configuration file, the same trust boundary[variants] data_file(or[needs] variant_data_file) and[reports] directoryalready sit on.A template that cannot be used falls back to the default report — and says why, under one of three codes:
needs.report_template_not_found(no such file),needs.report_template_outside_project(the clamp above), orneeds.report_template_failed(a Jinja syntax or evaluation error). The first two are reported while the document is indexed, so they appear in the editor — on the:template:option itself, or on the directive name when the template came from[needs] report_templateand there is no option to point at; the third needs the whole project’s data and appears in the build output. A Sphinx build renders nothing at all in these cases, which is the honest answer when your only “default” is the packaged template — ubCode has a real one to fall back to, so it uses it rather than losing the section.The path base for the configuration key differs —
[needs] report_templatetakes a relative path from the directory holdingubproject.toml, and accepts an absolute one, which is what every other path-valued ubCode key does. A Sphinx build resolves needs_report_template against the source directory always, stripping a leading/rather than honouring it — so an absolute path is appended to the source directory instead of being read from where it points. The directive’s own:template:follows the ordinary Sphinx convention in both systems: relative to the document, with a leading/meaning source-root-relative.There is no
needs_render_context— a template sees the five names above and nothing else. In particularreport_directiveis fixed atdropdown: a Sphinx build recomputes it because no loaded extension may providedropdown, and ubCode always does (see below).needs_template_folder plays no part — it is the lookup root for need-content templates (the
:template:option on a directive such as.. req::), a different feature ubCode does not implement, and it has never had a role inneedreportresolution in either system.Give the template file an extension your project does not build —
.needor.txt, not.rstor.md. This is the same advice the Sphinx-Needs documentation gives, for the same reason: a template inside the documentation tree with a document extension is also read as a document of its own.A template composes its own text, so ubCode stops escaping it for you — the default report escapes every value it interpolates, which is what makes it impossible for a need type titled
.. raw:: htmlto open a directive inside the generated page. Your template decides that instead. What still holds either way is the fence: generated report content is parsed in its own arena with nestedneedimport/needreportand need-type directives refused and external content denied, so the blast radius is the page’s own rendering.Counts cover needs only — as for the chart views and the
:need_count:role, and for the same reason: one filter, one counting path, so a report row and a:need_count:written beside it cannot disagree. A Sphinx-Needs build counts needs and their parts here, and includes needs from external sources; ubCode counts needs, external ones included. A project using parts should expect a lower total.Need types are listed in config declaration order, matching a Sphinx build: ubCode resolves
[[needs.types]]into a name-keyed table, so the order you wrote them in is restored from a recorded ordinal rather than read off the table. Re-ordering the entries therefore changes the report — and is a configuration change, so it re-renders every page. The extra field names in theNeed Fieldssection are the one list still shown alphabetically where a Sphinx build shows them in declaration order.The vocabulary is ubCode’s — a Sphinx-Needs build lists three need types and eight extra fields its bundled services inject (
issue/pr/commit;query,specific,url,avatar,closed_at, …) even in a project that uses none of them, plus the twoneedganttfieldsdurationandcompletion— ten extra names in all. ubCode ships no services, and — unlike a Sphinx build — never invents the twoneedganttfields either (needganttrenders here; see its own entry above), so those rows are simply absent unless the project declared them itself. The classification is identical for every field a project does declare.The
dropdownprovider is always there — a Sphinx-Needs build aborts if no loaded extension providesdropdown(useblocks/sphinx-needs#899), which is why its documentation tells you to enablesphinx_design. ubCode renders.. dropdown::natively and unconditionally, so the failure cannot happen — and the report’s sections get the same ported sphinx-design presentation an authored.. dropdown::already gets, in both the preview and a built site.A report with no options warns and renders nothing — matching a Sphinx build’s behaviour, under
needs.report_no_sectionsso[lint] ignorecan silence it (a Sphinx build’s warning cannot be). A section you did ask for that has nothing to show is omitted rather than rendered empty, also matching.Prefixes and titles render as plain text — a Sphinx-Needs build wraps the ID prefix in single backticks, which its default role renders as a citation reference. Every cell here is escaped text, so a need type titled
*draft*reads as those characters rather than turning the row italic.
- Needs diagrams (
needuml/needarch) You write a
.. needuml::whose body is a Jinja template that produces PlantUML, or a.. needarch::— the same directive, written inside a need, which gainsneed()andimport()for reading that need.A Sphinx build renders the body against the project’s needs, hands the produced PlantUML to a PlantUML server through
sphinxcontrib-plantuml, and embeds the returned image.The preview and
ubc build htmlrender the same body with the same engine: the same callables (need,flow,filter,ref,umlandimport), the same four-layer context precedence, the same:key:-based recursion guard, and the same [needs] diagram_template. Then they part company: ubCode has no PlantUML server, so the produced diagram is transpiled to Mermaid and the page draws it client-side.Everything below follows from that one decision, or from the port’s standing warn-and-degrade rule.
The diagram is text in the page, not an image — no PlantUML server, no network call, no
_images/copy, and the same mermaid bundle every other diagram on the site uses. It is selectable, searchable and theme-aware, and it re-draws at the reader’s resolution. The cost is coverage: a PlantUML construct Mermaid has no equivalent for cannot be drawn. The whole diagram is then reported (needs.uml_unconvertible) and the produced PlantUML shown in its place, rather than left as a blank space — add:debug:to any diagram to see the same source. Diagram families outside Mermaid’s reach (gantt,mindmap,wbs,salt,ditaa, …) are the usual cause. A diagram that does draw but loses some of its lines on the way is reported too, under its own code (needs.uml_partially_converted), naming the lines that are missing — quoted as they appear in the message, so a[lint] message-ignoresrule can match them. That code covers lines ubCode could not read. A construct it reads and then deliberately does not draw — atitleon a component diagram, say, which Mermaid has no keyword for — is not reported and never will be: the diagram is drawn, and that part of it is a documented omission. Two codes rather than one, because they need different answers: the first means there is no picture, the second that the picture you can see is incomplete.Changed in version 0.37.0:
needs.uml_partially_convertedadded. Before it, a diagram that lost part of its content built without a word.:scale:sizes the diagram’s box, as a percentage width, where a Sphinx build scales the rendered bitmap. A value that is not a whole-number percentage is reported (needs.uml_scale_invalid) and the diagram drawn at 100%; a Sphinx build swallowed the error in silence until Sphinx-Needs 8.5.0, which reports it as well (scale value must be a number. "not-a-number" found).The caption is a
<figcaption>— the directive’s argument. A Sphinx build emits it as a docutilstitle, which renders as a heading in the middle of the page and, on a page whose first element is aneeduml, is taken as the document title.:config:resolves against [needs.flow_configs], and a name that table does not define is reported (needs.uml_config_unknown) and contributes nothing to the diagram; a Sphinx build passed it over in silence until Sphinx-Needs 8.5.0, which reports it as well (config name … is not defined in needs_flow_configs.). Because the diagram is transpiled, a block’s lines reach the produced PlantUML but only the ones Mermaid has an equivalent for change what is drawn —skinparamin particular is dropped.:debug:shows the block in place.:extra:splits each pair on its first colon, so a value that contains one — a URL, a timestamp — survives. A Sphinx build splits on every colon and ends the build with an unpackingValueError. A pair with no colon at all is reported (needs.uml_extra_invalid) and dropped, and the rest of the diagram renders.:save:is accepted and not yet written. The option is read and its path validated — an absolute path is reported (needs.uml_save_path), and a\separator or a..segment reported as advice (needs.uml_save_path_unportable) — but no.pumlfile is produced. A Sphinx build writes one into the output directory.A
needarchoutside a need is reported, not fatal (needs.arch_outside_need): the diagram renders withoutneed()andimport(), which have nothing to read. A Sphinx build raisesNeedumlExceptionand the build ends.Every other failure degrades too. A template error, an unknown id, an
import()naming something the need does not have, a diagram nested past the depth cap — each is reported under its ownneeds.uml_*code, which[lint] ignorecan silence, and the diagram renders as far as it got. A Sphinx build raises on most of them. Two of these are reported by Sphinx-Needs as well from 8.5.0 onwards, where 8.4.0 accepted them in silence: aref()given both or neither ofoptionandtext(needs.uml_ref_label) and animport()naming an option the need does not have (needs.uml_import_option_unknown). Both are warnings here.A keyless self-referencing
needarchrenders once. A Sphinx build registers the directive’s own guard entry under its:key:—Nonewhen it has none — whileuml()defaults to"diagram", so the two never match and the diagram renders twice (useblocks/sphinx-needs#1334). ubCode registers the keyuml()would ask for.filter()returns needs in id order. A Sphinx build returns them in its needs-view (insertion) order. ubCode’s needs index is a hash map whose iteration order is seeded per process, so any order but a sorted one would differ between two builds of identical sources — and the produced diagram is folded into the page fingerprint.There is no Python-method surface. A Sphinx build’s template can call arbitrary methods on the objects it is given, and
{{ need }}renders a Pythonrepr. Templates here see the same need projection aubc reporttemplate sees.needs_uml_process_max_timehas no equivalent, deliberately: it is a post-hoc warning about time already spent, and ubCode bounds the work instead — a fuel budget, a nesting cap, a produced-output cap, and a recursion guard.needs_build_needumls, the whole-project
.pumldump, is a separate Sphinx builder upstream. In ubCode an export is its own command rather than a side effect of building a site, so it is out of scope here.
- Needs extract (
needextract) You write a
.. needextract::naming one need by id, or a filter selecting several, to re-present needs that are defined elsewhere at a place that suits the reader.A Sphinx build copies each selected need’s already-parsed body out of the document that defines it and splices it into this page, then re-mints that need’s HTML anchor — and every anchor written inside its body — on this page as well.
The preview and
ubc build htmlrender a view: the same card a need gets on its own page, with its current metadata and its body, but no anchor of the need’s is repeated here.The differences worth knowing:
An extract creates no need. This is not a difference from a Sphinx build — the copies are absent from its
needs.jsontoo, and invisible to every filter and count, including one written on the very page that shows them. It is stated because the Sphinx-Needs documentation calls them “copies of filtered needs”, which reads as though they were needs.The card carries no copy of the need’s anchor, and neither does anything inside it: an explicit target, a
:name:you gave a figure or a table, anindexentry — all of them render, and none of them becomes a destination on the extract page. A Sphinx build re-mints every one of them, with no warning, so extracting one need twice on a page publishes the sameidtwice and extracting a need on its own page publishes it beside itself. Nothing links to those duplicates in either system — a reference to the need, or to a label inside it, resolves to the page that defines it — so what this removes is dead anchors, not live ones. What the site does get is one anchor of its own,ub-needextract-1,ub-needextract-2and so on in the order the directives appear, so you can link to the extract itself. A Sphinx build reports a.. contents::inside a need’s body as an error and renders no listing there. ubCode renders it, listing this page’s sections, with its entries anchored on the card rather than on the page, so it moves no anchor of the page’s own; a need’s body shown byneedimportbehaves the same. The card’s ID chip is a link back to the need’s own page — except for an external need, which has no rendered page in this project to point at, so its chip stays plain text.Content that would create needs is refused. A need type,
needimport,needreport,list2needorsrc-tracewritten inside a need’s body renders as a placeholder in the extract and creates nothing, with oneneeds.import.nested_generativewarning per directive naming what was refused. A Sphinx build renders such content a second time, as a full card with the same anchor as the original — indistinguishable from a second definition.A
needtableand aneedextractare NOT in that refused set, because neither creates a need. Aneedtableinside extracted content renders live here, against the whole project, and re-renders when what it selects changes; a nestedneedextractrenders as the plain directive placeholder with no warning, because an extract does not extract a second time. Both of them end a Sphinx build with a Python traceback.Everything else in the body renders in full, including four things a Sphinx build cannot do here: a footnote (which ends its build with a
KeyError), numbered figures (whose numbers vanish from the copy while a:numref:inside it prints the original’s), a substitution defined in the source document (which degrades to raw|markup|), and math on a different page from the need (useblocks/sphinx-needs#890). All four work here for one reason: the body is re-parsed and rendered through the same pipeline every other page goes through, rather than copied as an already-parsed tree that has seen neither document’s processing.Assets in extracted content follow the SOURCE need’s policy: a need authored in this project renders its images and figures, resolved against the need’s own document rather than against the page doing the extracting — so a need in a subdirectory shows the same image wherever it is viewed. A need imported from another project renders them only if you gave that source
allow_assets = true; and an external need’s are refused, with a warning naming the need rather than a silent gap.A relative reference may reach anywhere inside the project: one written as
../shared/logo.pngresolves against the need’s own document, exactly as it does on that document’s own page. Only a reference that leaves the project’s source directory is refused, with a warning naming the need; a Sphinx build copies such a file and renders it with no warning about the reference at all, so this is the one place ubCode is deliberately stricter here.One consequence is worth knowing about. You write the reference once, but a view resolves it to a project-relative spelling of its own while the need’s own page keeps the one you wrote — so the two pages ask for the same file under two different paths, and it is published twice, the second copy under the collision suffix (
logo.pngandlogo1.png). Both images render and both hrefs resolve; only the bytes are duplicated.One limit applies to an authored need’s assets, and only inside an extract — on the need’s own page every reference renders normally. The directory holding the need’s document must have a name that is spellable as a mount base, which rules out whitespace, a backslash, or a
#,?or:anywhere in it; a need in such a directory has its assets refused in views of it, with a warning naming the need.A Markdown heading inside a need’s body is a rubric rather than a section, and it stays one in the extract: the copy does not become a heading in the extracting page’s structure, does not appear in that page’s contents, and carries no anchor of its own.
Hidden and external needs can be extracted. A Sphinx build refuses both — a
:hide:need never stores a body for it to copy, and an external need has none at all — and warns once per need, so a broad filter in a project with external needs fills the log with warnings you cannot act on. ubCode has the stored content either way and renders it.Two limits apply to an EXTERNAL need specifically. Its card’s ID chip is plain text rather than a link, because the need has no rendered page in this project. And a
:need:reference written inside its content is resolved as written, without the source’sid_prefixapplied — so if the external project’s own ids are unprefixed and you gave the source a prefix, such a reference can resolve to a different need or to none. An extract is the first place ubCode renders external need content, so this is a limit of that content rather than a change to it; a need imported withneedimporthas its references re-pointed correctly.One warning per directive, not per need. A selection containing needs with no content at all reports
needs.extract_no_contentonce, naming how many there were and the first few ids.Options that end a Sphinx build report a warning here. A mistyped id (
needs.extract_missing_id), and an argument together with:filter:(needs.extract_argument_conflict, which the Sphinx-Needs documentation already tells you not to write) both warn and render the empty state, where a Sphinx build warns and then aborts on the next statement it reaches.needs_include_needs = falseaborts a Sphinx build with no warning at all. It has no equivalent here at all: it is aconf.pysetting, and ubCode does not readconf.py— see Supported toolchain, which lists it under the excluded configuration.A filter matching nothing shows
:filter_warning:if you gave one, andNo needs passed the filtersotherwise — the same empty state every other view directive uses.:show_filters:appends the used-filter line, matching a Sphinx build (which implements the option but does not document it).The site’s own
:layout:and:style:are applied to every card it draws. Until ubCode 0.33 they were read and reported instead (needs.option_unsupported), on the reasoning that a per-site override would need a second design resolver. That turned out not to be so — the resolver the need-level:layout:already goes through took the job — and the option is now honoured, in the editor preview and the built site alike. Precedence is a Sphinx-Needs build’s own: the site’s value beats the resolved value on the need (so it beats aneedextendon another page, a[needs.fields.*]default, a variant, a dynamic function and an importedneeds.json), an absent or empty option falls through to the need’s own, and a written:layout: cleanis a real override rather than an absent one — visible once the project moves its default_layout. A site’s:style:replaces the need’s tokens for that site’s cards rather than adding to them. Two things diverge, both in the tolerant direction. An unknown design name is reported once for the SITE (build.need_layout_unknown, naming the directive rather than any need) and applies no override at all, where a Sphinx build raises and takes the whole build down. A rejected:style:token is reported once for the site (build.need_style_invalid) and the valid tokens still apply, where a Sphinx build injects the token unchecked. The editor preview applies both options and stays silent about both findings, exactly as it does for the need-level ones. An extract stays a view: the override changes how the cards are drawn and nothing about the needs, so the meta rows keep the need’s own stored values andneeds.jsonis untouched. The one card element that follows the override rather than the need is the footer echo of a design that echoes them —complete’slayout: … / style: …line. ubCode’s footer names the design and tokens the card is actually drawn with, so inside an overriding extract it readslayout: completewhere the need’s own value isclean; a Sphinx build’s footer echoes the need’s stored field values there instead. That is this arc’s one declared divergence, and it follows from a pre-existing one rather than being new: ubCode’s echo has always printed the resolved design name where a Sphinx build prints the raw stored field (which is empty for a need that set none), so the two already differ for everycompletecard with no:layout:of its own (a design with no footer, like the defaultclean, echoes nothing in either engine).:export_id:is deprecated in Sphinx-Needs and is reported and ignored, exactly as it is on the other view directives.An extract’s cards are not indexed for search. They are a second view of needs already indexed at their own cards, and a card here has no anchor of its own to navigate to, so a search hit would lead nowhere. Prose around the directive is indexed normally.
- Needs from a list (
list2need) You write a
.. list2need::whose body is an indented bullet list, two spaces per nesting level, with:types:naming one need type per level. Each bullet becomes a need.A Sphinx build renders each item through a template into reStructuredText and splices the generated text back into the parser.
The preview and
ubc build htmlparse the list directly and create the needs from it. They are ordinary needs from that point on: filters,needtable,needs.json, schema validation,needextend, hover, goto-definition and find-references all work on them, and a:need:role pointing at one resolves.Status — implemented; these differences are deliberate:
Each need points at its own item. Goto-definition on
REQ-1selects theREQ-1token on the line you wrote it on, and every diagnostic below lands on the offending item or option rather than on the block header. One diagnostic is the exception, and it is one you are likely to meet:needs.id_regex— raised when an id does not match needs_id_regex, which hand-written(REQ-1)-style ids do not under the default pattern — is anchored at the block’slist2needtoken rather than at the item, because it is raised by machinery shared with every need directive. Several failing ids in one block therefore give you several warnings at the same position — each one names the offending id, which is what tells you which bullet it is about.A Sphinx build cannot do this: the line number its needs carry comes from where the generated text was spliced in, which is why they routinely report a line past the end of the file (useblocks/sphinx-needs#1349), and why every need after a
list2needin the same file is shifted too. ubCode’s line numbers are the true ones, for the generated needs and for everything after them.It works in a Markdown document. A Sphinx build creates no needs at all there — its mechanism needs a parser API that MyST only mocks — so there is nothing to be incompatible with. Because that means such a document builds here and produces nothing there, each Markdown block reports one informational
needs.list2need_myst_hostnote.:presentation:lays the cards out, and a value it cannot use falls back to the default.nested— the default, here and in a Sphinx build — renders a deeper item’s card inside the card of the item above it, after that card’s own content;standalonekeeps every card a sibling, in document order. A value that is neither is reported (needs.list2need_presentation) and then treated as unset, so the block rendersnestedrather than silently picking the other layout. A Sphinx build aborts on that value instead, so there is nothing to be incompatible with.The option is a data choice as well as a layout one, in both engines: a nested item records the item above it as its parent, and a standalone one records nothing — and cannot be made to, since an inline
((parent_needs="…"))is refused like the directive option below. ubCode derivesparent_needs— the id of the nearest enclosing need — and projectsparent_needfrom it, for alist2needitem and for a need directive you write inside another need’s body alike, so a nested item’sneeds.jsonentry carries both fields here as it does in a Sphinx build. The meta rows on the card readparent needsandchild needs, the same words a Sphinx build writes. ubCode labels every link row by that type’s configuredoutgoing/incomingtitle, so this is general rather than special-cased for the builtin: name your own link type and the card shows the names you chose. The presentation still differs — a table row here against an inlineparent needs:line there, so no trailing colon — and a link type declared with no titles keeps ubCode’s older labels, the bare type name and the name plus(incoming), where a Sphinx build would default to the name and<name> incoming. One detail remains worth knowing before you compare outputs:parent_needs_backis computed for:filter:expressions,needtablecolumns and the card, but is not written to the export file, the same as every other*_backfield here.:links-down:remains the layout-independent alternative where you want an edge that does not depend on how the cards are drawn.You cannot write the field yourself.
:parent_needs:on a need directive, and((parent_needs="…"))on alist2needitem, are both refused withneeds.option_derivedand the written value is dropped — the field is a fact about where the need sits, so an authored value could only contradict the document around it. That applies to the builtin link, which is whatparent_needsis unless your project declares something else of that name: declaring[needs.fields.parent_needs]replaces it with an ordinary field of your own, and the builtin link is then never registered at all, so the name is yours to write and nothing here refuses it. A Sphinx build accepts the option, lets the value reach the need, and then overwrites it from the nesting with no message at all, so the stored data is the same either way and only the diagnostic is new. One upgrade note follows from it: a project that declared[needs.links.parent_needs]itself and authored values into it kept those values before, and loses them now — they are derived from nesting instead. Move such an edge to:links:or to a link type of your own.A hidden item can still hold the items nested under it. A Sphinx build refuses to nest anything inside a
((hide="true"))item, because it removes a hidden need from the page entirely and a card placed inside one would be rendered nowhere. ubCode removes nothing — a hidden card is the whole card inside a collapsed disclosure — so the item you wrote underneath one stays where you wrote it, inside that disclosure, and its own anchor still reaches it.An item’s content is markup, and it is parsed with *this* document’s grammar.
**bold**is bold, a:need:role is a live cross-reference that counts towards traceability like any other, a:need_part:creates a real part, and a directive written under the item is a directive. Almost everything a need directive’s body can hold, an item’s content can hold. The one exception is reStructuredText-only: a directive you declared yourself under[parse.extend_directives], written inside an item’s content in a.rstdocument, shows its argument, options and body as text rather than parsing them. It is not reported — the names you declare under[parse.extend_directives]and[parse] ignore_directivessilence the unknown-directive warning inside item content exactly as they do everywhere else — it simply is not parsed. In a Markdown document the same directive parses in full, options and all, exactly as it does in a need’s body. A Sphinx build parses it in a reStructuredText document, where the comparison exists, so this is a gap rather than a difference of taste.A directive nothing knows — a typo, say — is reported inside an item’s content exactly as it is in a need’s body, in both hosts.
In a Markdown document the content is Markdown, not reStructuredText: write
{need}`REQ-1`rather than:need:`REQ-1`, and a fenced directive inside an item needs an outer fence one backtick longer than the inner one. A Sphinx build parses item content with the document’s own parser too, for the same reason — but it creates no needs at all from alist2needin a Markdown document, so there is no Sphinx behaviour here to be compared with.Every position inside that content is a real position in your file: a diagnostic raised inside an item’s content — by ubCode or by the parser itself — names the line and column you wrote it on, so the squiggle lands where you are looking. Two lines report their whole width rather than an exact column: one containing a tab (whose width the two engines resolve differently, so the column cannot be trusted —
source.tab_in_linereports the tab itself), and, in a Markdown document, a block-level report, because a Markdown block records only its line.One thing is refused rather than parsed: a need-creating directive inside an item’s content. A
.. req::, a nested.. list2need::, a.. needimport::— any directive that would create needs — is reported (needs.import.nested_generative) and not executed, because generating needs from generated content is the one shape that can recurse. Its own text stays on the page, shown as you wrote it, so the refusal is visible rather than a silent deletion; everything else in the same content still builds. A Sphinx build creates the need instead, so a document that relies on that will have one need fewer here.A malformed list warns; it never aborts the build. Twelve inputs stop a Sphinx build dead, most with a bare Python traceback and no line number: a
-or numbered bullet, a bare*, content before the first bullet, an indent that is not a multiple of two, an item deeper than:types:declares, too few:links-down:types, an unknown type or link name, an invalid:presentation:, a missing:types:, and an empty body. Here each is aneeds.list2need_*warning naming the line or the option, and the item — or, for a missing:types:or an empty body, the block — is skipped. The rest of the list still builds. This matters most in the editor, where analysis re-runs on every keystroke: a half-typed list must degrade to a squiggle, not kill the file — and the empty body is precisely the half-typed case, an author who has written the directive and not yet the list.A thirteenth input is not an abort but has the same effect: an unknown option on the directive itself. docutils rejects the directive outright, so a Sphinx build creates no needs at all from that block — the whole list disappears, with an error where the block was. ubCode reports the option under
block.directive_unknown_option, drops that one option, and builds the list.A skipped indentation level does not fabricate a link. A Sphinx build’s downward-link walk stops only on an exactly equal level, so jumping from level 0 to level 2 lets it run past a de-indent and link an item to a need in a different subtree — while the real parent of that need gets no link at all. ubCode stops at the de-indent and reports the skipped level (
needs.list2need_level). Yourneeds.jsonwill therefore be missing that one link relative to a Sphinx build, and that is the point: it was never a real one.A continuation line at column 0 keeps its first word. A Sphinx build silently deletes it, though its own documentation promises the line is added to the item above. The reach is wider than the missing word: a kept token can complete an
((…))option region that a Sphinx build never sees, so the FIELD set can differ too, not just the content text.Ids are identical to a Sphinx build’s, quirks included. Auto-generated ids are stable identity — they appear in
needs.json, in every cross-reference and in every traceability link — so the id rules are reproduced exactly, including the ones that are traps::delimiter:defaults to., so a title is everything up to the first full stop andv1.2truncates it. There is no escape.the
(ID)scan is not anchored, so an ordinary parenthetical becomes the id —* Some (parenthetical) titlegets the idparentheticaland loses those words from its title. ubCode reports this asneeds.list2need_ambiguous_idwhere a Sphinx build is silent, but produces the same id.only the first
((…))option region is read, and it spans greedily to the last))on the line, so a second region on the same item swallows the prose between them. Reported asneeds.list2need_option_area; again the same result.writing a literal
()suppresses the id and switches the need to a different hash function — one that honours[needs] id_from_titlewhere the ordinarylist2needone ignores it. Both are reproduced, so a project with that key set gets the same two id schemes in one list that a Sphinx build gives it.
The two informational reports above are graded below warning: there is nothing in your document to fix, and a Sphinx build accepts the same markup in silence.
Five generation artifacts are not reproduced, because they are accidents of building text and re-parsing it rather than behaviour anyone relies on:
a
()-suppressed id together with inline options turns those options into body text — here the options parse;a directive written inside a nested item’s content is mangled by the re-indentation — here the content is content;
:links-down:together with any inline option glues the first option onto the generated links line, so a Sphinx build ends up with a link named after your option’s text and without the option — here the link and the field are both set, separately;an empty option name (
((="open"))) generates a::line that docutils rejects, and a Sphinx build drops the whole need — here the need is created and the nameless option reported;a continuation line starting with
:is stored with three extra spaces of indentation. That last one is not purely an artifact: a Sphinx build strips the indentation off every continuation line, and those three spaces are what put the options of a directive written across an item’s continuation lines back under that directive. ubCode keeps the indentation you wrote instead, measured from the item’s own column, which serves that case and the ones the three spaces cannot reach — an indented directive body, a nested list, an option that does not start with:. The two engines agree on the directive’s options and part company on its body: a Sphinx build strips that body to column 0, so a.. code-block::written under an item renders there as an empty block followed by a stray paragraph, where ubCode renders the block you wrote. They differ in the STORED string too, and in the other direction — a Sphinx build’sneeds.jsonKEEPS the three spaces ('.. image:: logo.png\n :align: center'), while ubCode’s stores a plain line join without them.
A tab in an item’s indent is refused in a Markdown document, with
needs.list2need_indentnaming the tab, and the item is skipped. Markdown keeps the raw bytes, and the two markup languages resolve a tab to different widths — 4 columns in Markdown against reStructuredText’s 8 — so honouring it would put the item on a level you never wrote and on which the two hosts disagree. A Sphinx build creates no needs in a Markdown document at all, so there is nothing to be incompatible with.In reStructuredText there is no tab to refuse, because neither engine ever sees one: docutils expands tabs before the directive’s content is read and ubCode expands them the same way. A tab-indented item therefore lands on whatever column it expands to and is then handled like any other indentation — which may still skip it, for the ordinary reason that the column is odd. `` <tab>* item`` becomes an indent of 5, so
needs.list2need_indentreports “must be a multiple of 2 (found 5)” and skips the item, alongside ubCode’s pre-existingsource.tab_in_linelint. Both engines read the same level out of the same bytes, so this is parity rather than a divergence — at docutils’ default tab width of 8. A project that setstab_width: 4in adocutils.confchanges what a Sphinx build reads and not what ubCode reads; ubCode’s expansion is fixed at 8, so a tab-indented item can land on a different level in the two engines under that setting.
- Traced needs (
src-trace) You write a
.. src-trace::naming a codelinks project, optionally scoped to one:file:or:directory:, to bring the needs written as one-line comment markers in your source files into the documentation. This is a Sphinx-Codelinks directive rather than a Sphinx-Needs one, so the comparison below is against that extension.A Sphinx-Codelinks build runs the analysis when it reaches the directive and adds one need per marker at that position.
The preview and
ubc build htmlrun the analysis when they index the project and draw one need card per traced need at the directive’s position. What a reader sees is the same shape; the differences worth knowing:The cards are ordered by need id, ascending. A Sphinx-Codelinks build orders them by the marker’s line number across all files, so a marker on line 1 of
zebra.pycomes before one on line 40 ofalpha.py, and adding a file re-interleaves every other file’s cards. Ordering by id is stable under adding a file and moving a marker, and it is the order a reader scanning for an id wants; it is also the one rule ubCode applies to every set of generated cards,needimport’s included.The source URL rows are plain text. With
set_local_urlorset_remote_urlon, both engines put the URL in a field and show it as a metadata row. Sphinx-Codelinks also registers aneeds_string_linksrule for that field itself, so its row is clickable with no configuration; ubCode does not yet, so its row is the URL as text. Forremote-urlyou can add the rule yourself — Making the source URLs clickable has it, an identity template because ubCode stores the fully resolved URL in the field. Forlocal-urlthere is no rule that works: it is afile://URL, and ubCode’s string links render onlyhttps:,http:andmailto:targets. The next bullet is why that gap is not closeable by configuration.local-urlpoints at the file on your disk. ubCode writes afile://URL to the real source file. A Sphinx-Codelinks build copies each traced source file into the output tree, generates a syntax-highlighted page for it, and pointslocal-urlat that page with a line anchor — so its link works for a reader who has only the published site, and ubCode’s works for a reader who has the checkout.A marker’s git commit is read when the file is analysed, not when the site is built.
remote_url_pattern’s{commit}is substituted into the stored field during the codelinks pass, and that pass re-runs only for files whose modification time changed — so committing without editing a traced file leaves the previous commit’s SHA in the URL until something in that file changes or you build with--no-cache.Only one directive draws a card for a given need. If two
.. src-trace::sites cover the same marker, the one on the earliest page by path (and earliest on that page) draws the card and the other renders an empty container, with aneeds.duplicatediagnostic naming both. A Sphinx-Codelinks build keeps the site read FIRST, and for the second warnscodelinks.duplicate_needat the marker’s line, naming the document that already created the need (it is not created again).Two markers with one id: the earlier line wins in both tools, one-line and multi-line needs alike. ubCode reports
needs.duplicatenaming both sites; a Sphinx-Codelinks build reportscodelinks.duplicate_needat the skipped marker, naming its kind. A need directive with the same id always wins in ubCode, where in a Sphinx build the first one read wins, so a multi-line need beats a.. req::written later on the same page.
With
get_multiline_needson, both engines turn a multi-line need into a need with its body; the differences:A body is restricted in ubCode. It cannot read files (
include,literalinclude,csv-tablewith:file:,raw), its images and other assets are not resolved, and a need directive written in it is reported and not executed. A Sphinx-Codelinks build refuses nothing yet: a page-relativeincludein a body is read, and a.. req::in a body creates a second need, recorded against the page.A body does not see the page. ubCode parses it on its own — no page substitutions, no
rst_prolog— so a block reads the same on every page that traces it. A Sphinx-Codelinks build parses a reStructuredText body on a reStructuredText page inside that page.A label in a body is a page label in both, but ubCode numbers the page first. A Sphinx-Codelinks build parses the body inside the page, in document order, so a body before a section can take the section’s anchor and a body’s footnotes shift the page’s own ids and footnote numbers. ubCode gives the page’s own ids first and the body’s after them: a body label whose name or anchor the page already has is reported (
std.duplicate_target) and does not register. Sphinx reports a duplicate NAME and drops both (Duplicate explicit target name); a body label whose anchor a later section would take keeps the anchor there, and the section is renumbered (id4), with no warning. Two blocks on one page with one label: ubCode keeps the first in reading order — thesrc-tracedirectives in document order, the cards by need id within a directive — and Sphinx neither.A docutils named reference (
label_) from the page to a body label is still reported as unknown in ubCode, whose:ref:,{ref}and#labellinks reach it; in a Sphinx build it resolves.A body’s footnotes are numbered on their own in ubCode, from
[1]in each card; in a Sphinx build they continue the page’s numbering.A hidden block’s labels register in ubCode, whose hidden card is drawn collapsed; a Sphinx build drops a hidden need, so a
:ref:to its label does not resolve there.A glossary term, std object or citation in a body registers in both, but when the page defines the same name, ubCode keeps the page’s and reports the body’s, where a Sphinx build keeps whichever comes later on the page; and a body term or object whose anchor a label, a section or a page term or object already takes is reported in ubCode, where a Sphinx build gives it a numbered anchor.
Every finding is at the source line, the directive beside it. A Sphinx build reports docutils’ and Sphinx’s own messages there too, but myst-parser’s name the page with the body’s line, and a value that does not convert is reported at the open line rather than at its option’s line.
A markup no parser claims falls back to the page’s markup in both, with a warning at the directive: once per markup per directive in both, matched against the suffix your
[parse.parsers]include globs imply (case-insensitively, longest first) rather than Sphinx’s exactsource_suffixkey.A diagram in a body is not drawn. A
needumlorneedarchwritten in a multi-line need’s body shows on the card as its source, with abuild.directive_unhandledwarning at its line, and is not stored in the need’sarch; written in an ordinary need directive it is drawn and stored. A Sphinx-Codelinks build stores it for the block too (an unkeyed diagram underdiagram), and aneedumlkeyeddiagramin the body stops the Sphinx build, where ubCode reports nothing about the key.:template:,:pre_template:,:post_template:and:jinja_content:are accepted and do nothing, as on any need.A hidden block draws its card collapsed, as a hidden need does in ubCode; a Sphinx build draws none.
Anchors on views and diagrams¶
One rule for the whole family, stated once here because it is identical for every directive it applies to.
A Sphinx-Needs build mints a positional id for each view —
needgantt-<docname>-3, needflow-<docname>-1, and so on — and hangs a
¶ permalink off it.
The number renumbers as soon as a chart is added or moved above it,
so the deep link the page advertises silently retargets.
ubCode mints none.
No positional id, no serial, and no ¶ you did not ask for.
Instead, :name: is the anchor mechanism:
give a view or a diagram a :name: and it gets a stable cross-reference
target you can :ref:, on the same standard-target mechanism every
:name:-bearing directive uses.
In a built site (ubc build html) a named directive renders
an
idon its outermost element, normalised the same way every other:name:is (My Planbecomesmy-plan) — which, for a name that normalises to a non-empty slug, is exactly the anchor ubCode publishes for it inobjects.inv, so an intersphinx reference from another project lands on it too. See the empty-normalisation note below for the names that do not;a
¶permalink in its caption, when it renders one — a diagram with:caption:, aneedganttgiven an argument (its caption is drawn in every state, including the empty one), or aneedflowgiven an argument that draws a diagram. Aneedflowrenders its caption only in its drawn state, so a named one that is empty, failed or not yet hydrated keeps itsidand shows no caption and no¶. A view with no argument has no caption at all, so it gets theidand no¶— which is what.. note::and.. container::do as well.
Exactly 44 directives accept :name: and register a target for it:
admonition, attention, caution, code, code-block,
compound, container, csv-table, danger, digraph, div,
dropdown, error, figure, graph, graphviz, hint,
image, important, include, index, line-block,
list-table, literalinclude, math, mermaid, needflow,
needgantt, note, parsed-literal, plantuml, rubric,
seealso, sidebar, sourcecode, tab-item, table,
target-notes, tip, toctree, todo, topic, uml,
warning.
Two of them deliberately publish nothing: a plain .. include:: and a
:code: one.
docutils only names the block its :literal: and :code: branches
produce — the plain branch splices text and creates no element — so a
:ref: to a plain include is an undefined label in a Sphinx build too, and
ubCode matches that.
The :literal: branch anchors, and so does literalinclude.
:code: is not implemented in ubCode — it is reported as an unsupported
option and the file is spliced as plain text — so there is no code block to
name, and ubCode publishes no anchor for it where a Sphinx build does.
toctree anchors its wrapper, as a Sphinx build does.
target-notes accepts :name: here and anchors the block it renders;
docutils’ own target-notes cannot carry the option at all.
The index directive is the opposite case — its anchor is the only thing it
renders at all.
Extending :name: to the remaining views (needsequence, needtable,
needpie, needbar, needlist) is tracked in
useblocks/ubcode#2975.
card and grid are not on the list, in either engine: sphinx-design
declares no name option on them, and sphinx-build reports
unknown option: "name".
Four further points belong to the same rule:
A
:name:that normalises to nothing still gets an anchor — a numbered one. Under[parse] id_style = "docutils"the slug is built by transliterating what it can and then dropping every character that is not an ASCII letter or digit, so the rule is whether the slug comes out empty — not whether the name had ASCII in it. A purely numeric name (:name: 123) empties, because a leading run of digits is stripped as well; so, in practice, does every name written in a non-Latin script: Chinese, Japanese, Korean, Cyrillic, Greek, Arabic, Hebrew, Thai and so on. Some ASCII-free names do NOT empty, because the transliteration reaches them::name: fislugs tofiand:name: ßtosz. There is no readable slug to be had, so the element takes the page’s sharedid1/id2/ … counter instead, exactly as a Sphinx build does. The anchor is real: the page emits it,objects.invpublishes it, and:ref:reaches it, from this project and from another one through intersphinx. What you lose is the readable URL —#id1says nothing about what it points at, and it moves if you add another such element earlier on the page. So an ASCII:name:with your own display text (:ref:`交付计划 <delivery-plan>`) is still the form you want — see the:ref:note below. By default (id_style = "github", see Parsing) a name in any script keeps its letters (:name: 交付计划anchors at#交付计划), and only a name that loses every character — punctuation, or emoji without a variation selector — falls back to the numberedid1; an emoji written with U+FE0F (:name: ✔️) keeps that selector as its id — an invisible U+FE0F (#%EF%B8%8Fin a URL), or a-1/-2serial of it.A
:ref:to a named view or diagram shows the label, not the caption. A Sphinx build substitutes the target’s caption text into a bare:ref:; ubCode never does, for any:name:-bearing directive — write:ref:`See the plan <my-plan>`for your own wording.The editor preview emits no anchors at all — not for these directives, not for headings, not for figures. Anchors are a built-site surface; the preview has nothing to deep-link to.
An unnamed directive is unchanged: no
id, no¶, nothing.
Smart quotes¶
ubCode educates prose the way a Sphinx build does — docutils’ own algorithm, for reStructuredText and Markdown alike, on by default, with Sphinx’s key names (see Smart quotes ([build] smartquotes)). The differences:
The
conf.pyis not read. A project that setssmartquotes = False(orsmartquotes_action, orlanguage) inconf.pygets ubCode’s default until it sets[build] smartquotes/smartquotes_action/[project] languageinubproject.tomlas well.A backslash escape never turns a quote. An escaped character is kept as written, and for the quotes beside it, it is simply the character it is:
("\*args")shows(“*args”)anda "\ *x*\ " bshowsa “x” b, where a Sphinx build of a reStructuredText page shows(”*args”)anda “x“ b, because docutils lets its own escape markers decide the quotes next to them. An escaped quote, apostrophe, hyphen, full stop, backtick or backslash stays as written in text, in both syntaxes (a role’s explicit title loses its backslashes before quotes are decided, in a Sphinx build too, so:ref:`a \"b\" c <lbl>`showsa “b” c):\"x\"stays"x", which is CommonMark’s meaning of the escape; a Sphinx build curls it in a MyST page, because MyST-Parser drops the backslash before the transform runs. The reverse holds for an escaped backslash before a quote in a MyST page:c\\"x"keeps its backslash and curls the quote (c\”x”), as both builds do in reStructuredText, where a Sphinx build of a MyST page drops the backslash and keeps that quote straight (c"x”). In reStructuredText, text that spells one of docutils’ escape entities stays as written too:a " bshowsa " b, where a Sphinx build showsa " b. A role’s content follows the rules of its syntax’s text. In reStructuredText it is unescaped once, as docutils does, so:menuselection:`C:\\Users --> Settings`showsC:\Users ‣ Settingsin both builds, and an escaped quote, apostrophe, hyphen, full stop, backtick or backslash stays as written in every role,:guilabel:,:menuselection:and:abbr:included, where a Sphinx build curls an escaped quote in those three (:math:and:raw:keep their backslashes in both builds). In a MyST page the content of a prose role ({emphasis},{strong},{guilabel}, …) is unescaped as MyST text, so{strong}`p\*q`showsp*q, where a Sphinx build showsp\*q: MyST-Parser hands a role its content as written, and a Sphinx build drops only the backslash of an escaped quote, apostrophe, hyphen, full stop, backtick or backslash, in the roles whose quotes it educates, with smart quotes on. A code role’s content ({code},{kbd},{samp},{command}, …) stays as written in a MyST page, as in a Sphinx build.Quotes look past inline markup, line ends and paragraph breaks. ubCode reads a block of text as the text it renders as, so a quote beside emphasis, a reference or inline code sees the character on its other side:
("*x*")shows(“x”), where a Sphinx build shows(”x”), because docutils decides each piece of text without looking past its end. Inline code itself is never changed. A line break counts as the space it renders as, so a quote that ends a line after a space opens (He said "withhello" there.on the next line showsHe said “andhello” there.) in both syntaxes; a Sphinx build closes it in reStructuredText. The break between two paragraphs of one version note (versionadded,versionchangedand the like, which docutils reads as one block of text) counts as a space as well: a second paragraph that begins with a quote opens it, where a Sphinx build usually closes it.MyST’s
smartquotesextension uses docutils’ algorithm. markdown-it’s own rule pairs quotes, leaves an unmatched one straight and always uses English quote characters; ubCode educates a document whose parser lists the extension with the same rule every document gets, in the quote characters of[project] language(English ones for a language smart quotes do not support).The target’s title in an implicit reference is educated on its own. An implicit
:ref:,:doc:or:term:shows the target’s title educated as a block of its own, with a space before it, from the title as it is stored — its text, without its escapes or its markup; Sphinx shows the title as it was educated where it was written. The two differ when the title starts or ends with a quote that its surrounding text would have decided differently, holds an inline literal with quotes, dashes or ellipses in it, or holds an escaped quote, apostrophe, hyphen, full stop, backtick or backslash (a titleEsc \"x\" titleshowsEsc “x” titlein the link,Esc "x" titlein Sphinx). This is the one place a backslash does not keep a character as written: the title itself keeps it, a link’s text to that title does not.Two kinds of reference text are educated where Sphinx keeps them raw. An implicit
:any:that resolves to a glossary term shows the term educated (“quoted” term), where Sphinx keeps the role’s own text, raw and in code style; and a Markdown link without text to a heading’s slug,[](#a-heading)or[](other.md#a-heading), shows the heading educated, where Sphinx shows the heading text as MyST-Parser stored it when parsing, raw. An:any:to a label or a document, and a link to a document or a label, show the educated title in both.Inventory display names stay as written. Sphinx’s
objects.invcarries the educated section and document titles; ubCode’s carries the text as written.An unsupported language, or an unknown action letter, is reported. For a
[project] languagewith no quote characters of its own, both engines educate nothing; ubCode also says so, asconfig.smartquotes_unsupported_language, where Sphinx is silent. Likewise both ignore a lettersmartquotes_actiondoes not know and apply the others, and ubCode reports it asconfig.invalid_smartquotes_action.An empty
smartquotes_actionmeans the default.smartquotes_action = ""reads as unset, so quotes, dashes and ellipses are educated ("qDe"); Sphinx’ssmartquotes_action = ''educates nothing."0"(orsmartquotes = false) is the way to say nothing in ubCode.An extracted need’s own text is raw, as sphinx-needs renders it. sphinx-needs renders a
needextractcopy from one taken before any transform ran, so the copy’s text stays raw there even though the same content is educated in the need’s own box; a:ref:or:doc:inside it still shows the educated title, because sphinx-needs resolves the copy’s references afterwards into the target’s title, while a:term:keeps the copy’s own text, raw. ubCode follows all three, and would drop the rule if sphinx-needs changed it.The plain-text form of a French title drops its no-break spaces.
frpads its guillemets with a no-break space (« x »). The page itself keeps it, but the page<title>, the search index’s titles and the PDF’s cover and running header are derived from the title’s plain-text form, which folds every kind of space into an ordinary one; Sphinx keeps the no-break space there.Word characters are Python 3.12’s. Which characters count as letters and digits beside a quote follows Python 3.12’s regular expressions (Unicode 15.0), as docutils’ do under that Python; a character added to Unicode later is not counted, where a Sphinx build on a newer Python may count it.
Some Sphinx and docutils knobs are not supported: per-block
language-xxclasses, adocutils.confsmart_quotessetting, thealtquote sets andsmartquotes_excludes. ubCode has nomanortextbuilder to exclude, and the language half ofsmartquotes_excludesis[project] language.
Document structure¶
- Document topmatter
You write a document that opens with a field list — bibliographic fields such as
:Author:,:Version:or:Date:, or arbitrary ones such as:orphan:,:tocdepth: 2or:sd_hide_title:— or, in Markdown, a YAML front-matter block.The preview shows nothing for it: the block is consumed into that document’s metadata, exactly as a Sphinx build does. A
:dedication:or:abstract:field is the exception — each renders as a titled topic block, again matching Sphinx, in both reStructuredText and Markdown. A field list anywhere else — after the document title, or mid-document — is ordinary markup and still renders as a field list, also as in Sphinx, and so is a field list at the start of an imported need’s content (that content is nested into the importing document, never read as a document of its own).A Sphinx build does the same. It also consumes some of that metadata:
:orphan:is honoured (a document marked orphan is exempt from the “not included in any toctree” warning), whiletocdepth, the search-exclusion keys, and theme keys such assd_hide_titleandhide-tocshape the built page.Status — topmatter extraction is implemented for both reStructuredText and Markdown,
:orphan:is wired through to the toctree check, andubc build htmlconsumes the other four keys:sd_hide_titlehides the page’s first title (it stays in the page, and its anchor, the browser title and the search index are all unchanged),hide-tocomits the “on this page” panel,tocdepthlimits how deep that panel goes, andnosearch(orno-search) keeps the page’s prose out of the search index. All four test for the presence of the key, sohide-toc: falsein Markdown front matter still hides the panel — as it does in Sphinx. A need defined on anosearchpage is still searchable as a need: needs are data objects, and Sphinx keeps a page’s domain objects in its own search index for exactly the same reason — only the prose goes. Two consumption divergences are known:tocdepthshapes the page’s own panel only, not the site navigation, which lists document titles and so has no section depth to cut; andtocdepth: 1on a page with several top-level headings lists them all here, where Sphinx’s furo theme hides the panel entirely. The live preview hides ansd_hide_titletitle just as the build does. Two more follow from how Markdown pages that skip a heading level are modelled (a page whose first heading is not#):sd_hide_titledoes nothing there, andtocdepthcounts one level deeper than Sphinx does. Three smaller extraction divergences are known: field names are matched against the English bibliographic names only; RCS keyword substitution ($Date: … $) is not applied; an escaped separator in:authors:(Doe\, Jane) still splits, because escapes are resolved before the metadata is extracted. In Markdown, adedication/abstractvalue is shown as plain text — Sphinx re-renders it as inline Markdown, so emphasis inside one is not formatted.rst_prolog/rst_epilogYou write content in
rst_prologorrst_epilog(in your parsing configuration).The preview shows the prologue and epilogue spliced as their own blocks at the very start and end of each RST document, at the document root — not merged into the document’s section structure. A prologue that begins with a heading never swallows the rest of the document. State set there, such as a default role, still applies to the whole document. A malformed prolog is silently not shown.
A Sphinx build appends the epilogue text inside the document’s trailing section, and a prologue that opens a section can absorb the whole document.
Status — the splice is structural rather than a raw text concatenation. Indexing and diagnostics are unaffected by where the splice lands; the background index splices the prologue and epilogue too, so state and substitutions defined there apply to both the preview and the index.
Page-local table of contents (contents)¶
.. contents:: ({contents} in Markdown) renders the page’s own headings
as a nested list, as a Sphinx build does:
a bare directive lists the page’s H1 with everything beneath it,
:local: roots the list at the section it is written in,
:depth: cuts it, :class: adds classes to the <nav>,
and :backlinks: — which defaults to entry in a Sphinx build too —
turns every listed heading into a link back to its entry.
A table of contents that finds no headings to list renders nothing at all,
which is also what a Sphinx build does.
In the editor preview the same list is drawn without links,
because headings carry no anchors there.
Three differences from a Sphinx build:
An invalid option value is accepted silently and the option is treated as unset, where a Sphinx build stops with a directive error:
:depth: notanumbermeans no depth limit,:backlinks: bogusmeans theentrydefault, a:name:is ignored (the directive has no such option upstream), and body content — which the directive does not take — is dropped. The same applies to acontentswritten inside a body element such as a.. note::: a Sphinx build errors, and ubCode renders the list where you wrote it.A heading that contains a cross-reference is listed with the reference flattened to its text — as a Sphinx build lists it — but keeps a plain heading, where a Sphinx build also links it back to the table of contents. The reason is that linking it would nest one link inside another; a Sphinx build only avoids that by accident of timing (its reference is still an unresolved placeholder when the table of contents is generated, so the guard against nesting does not see it).
The anchors a page mints are assigned in document order as the page is written out, so a
.. contents::that precedes a section of the same name takes that name and, underid_style = "docutils", the section falls through to anidN; a Sphinx build assigns section anchors first and the table of contents afterwards, so it is the section that keeps the name. (The exactidNa displaced section lands on depends on how many anchors the page has already minted.) It only shows up when a heading’s slug collides with the table of contents’ own — or, on a page with two headings of the same text, in whichidnumber the duplicate ends up with. By default (id_style = "github") the displaced section takes the-1serial of its own slug rather than anidN.
Footnotes and citations¶
Footnotes are numbered, linked and back-linked on both surfaces — the built site and the live preview alike.
Auto footnotes ([#]_) take their numbers in definition order,
each the next integer that is not already a footnote or citation label on the page,
so a document mixing [1]_ and [3]_ with three [#]_ definitions numbers those 2, 4 and 5.
(An earlier version of this page said 2, 5 and 6, which counted a fourth, named definition
that is not in the example; a Sphinx build numbers this one 2, 4, 5, and so does ubCode.)
A named auto ([#name]_) resolves by name however many times it is referenced;
an unnamed one is paired with the autonumbered references in reference order,
not with whichever definition happens to be nearest.
An unnamed auto can also be referenced by the number it received — write [2]_
and it links to whichever [#] ended up as 2 — though that reference adds no
back-link, so a definition reached only that way is still reported as unreferenced.
(A named auto’s number is not referenceable this way; it keeps its own name.
A named auto whose name is a digit string, .. [#1], is the exception: a plain [1]_
does reach it, and does add a back-link.)
Symbol footnotes ([*]_) walk * † ‡ § ¶ # ♠ ♥ ♦ ♣ and then repeat the symbol,
so the eleventh is **.
Every reference links to its definition and every definition links back to each of its
references, whichever of the two the page happens to write first.
A footnote written inside content that a needextract, needimport or
needreport directive brings onto the page is numbered within that card,
starting again at 1 as it does on the page it was written on, and its link stays
inside the card — so a page showing one need twice shows that need’s footnote as
[1] in both copies, and the page’s own footnotes are numbered as if the cards
were not there.
Where two definitions claim the same name, neither is usable:
both are still numbered and rendered, but nothing resolves to either,
a reference to that name reports duplicate target name,
and neither definition is reported unreferenced.
That is docutils’ own rule, and it applies to reStructuredText.
Markdown never reaches it — MyST catches a repeated [^label]: in its own pass and
discards the second definition, so the reference resolves to the first.
ubCode resolves the reference to the first one too, but keeps and renders the second
(see the differences below).
A citation reference ([CIT2002]_) resolves across documents:
a citation label is a standard-domain label,
so a bibliography page’s definitions are reachable from every page that cites them,
with goto-definition and find-references in the editor.
A citation defined on the same page as the reference also gets a backlink,
which is Sphinx’s own asymmetry — a cross-page reference never produces one.
Inside an extracted or imported card the same rule holds within the card:
a reference in the card back-links a definition in the card’s own content,
may reach a definition in the page’s own prose without back-linking it
(the card shows another document’s text, so that link would not be the page author’s),
and is never reached from the page’s prose.
Seven diagnostics come with the numbering:
std.footnote_unreferenced— a footnote definition nothing references.std.citation_unreferenced— a citation definition no page in the project references. Reported once every page has been read, at the definition’s own line: a citation label is a project-wide name, so a definition on a bibliography page may be answered only by a reference on another page, and the question cannot be settled while reading one document.ref.unknown_footnote— a[1]_or[#name]_matching no definition and no auto-assigned number.ref.duplicate_footnote— a reference to a name two definitions claim.ref.too_many_autonumbered/ref.too_many_symbol_refs— more references than definitions.std.ref— a citation reference no page in the project defines.
Each reports the same situation a Sphinx build reports.
Two differ in their text: std.ref’s wording, and the label std.citation_unreferenced quotes —
both noted among the differences below.
All seven are warnings.
A Sphinx build grades the four docutils-derived ones (unknown target name,
duplicate target name, and the two too many … references) as errors;
ubCode has no error grade for reference diagnostics, so they arrive in the warning count
and, under --deny warning, still fail the build.
In Markdown, footnote definitions are collected to the foot of the page and re-sorted by
number, behind a horizontal rule — MyST’s myst_footnote_sort and myst_footnote_transition,
spelled footnote_sort and footnote_transition under [parse.parsers.md] and both on by
default. footnote_sort = false turns off the collection and the first-reference numbering,
exactly as MyST’s does; footnote_transition = false keeps the collection and drops the rule.
The rule ubCode writes is <hr class="footnotes" />; a Sphinx build writes
<hr class="footnotes docutils" />, the extra class being one docutils adds to every transition
and ubCode adds to none. The rule is omitted on a page that holds nothing but footnote definitions;
ubCode also writes it when the page’s only other content is an included fragment that itself holds
nothing but footnote definitions, or a front matter MyST consumes entirely (one whose only key is myst:),
where a Sphinx build does not.
A definition written inside a body excluded by a falsy only is dropped with that body here,
where a Sphinx build still renders it at the foot of the page — it moves the definition out of the
body before deciding to discard it — so the reference is left as a plain marker with no link.
See Footnotes for the keys in full.
Ten differences from a Sphinx build remain:
In Markdown, a repeated
[^label]:definition is kept and rendered, numbered after the first, and it draws two warnings — that the name is a duplicate, and that the second definition is unreferenced. MyST discards the second definition and warns once. The reference resolves to the first definition on both sides, so only the extra footnote and the extra warning differ. The collected block makes it more visible than it was: both definitions land in it, so the page shows two entries where a Sphinx build shows one.A reference that resolves to nothing keeps its raw label and simply carries no link. A Sphinx build replaces it with the source text marked as problematic, inside a link to a system message it then deletes — a dead link in its own output. The diagnostic says what is wrong either way. The label is kept in the syntax it was written in, so in Markdown the marker still reads
[^label]and in reStructuredText[#label]. In Markdown the visible text is a Sphinx build’s, except that a zero-padded label loses its padding here as it does everywhere else ([^07]shows[^7]); only the wrapper differs otherwise; in reStructuredText a Sphinx build shows the full source of the reference, trailing underscore included ([#label]_), so the wrapper and one character differ.A zero-padded label loses its padding:
[^01]in Markdown and.. [01]in reStructuredText both render1, where a Sphinx build keeps the authored01on either syntax. The consequence is larger than a rendering loss, and an earlier version of this page described only the loss:01and1become the same label, so[01]_silently resolves to a.. [1]definition, adds a second back-link to it, and suppresses the unknown target name finding a Sphinx build reports for it. It also shifts the numbering of every later auto footnote, the same way the non-ASCII-digit bullet below describes: a Sphinx build reserves the label string007, so an auto may still take 7, where ubCode reserves the number 7 and its autos step over it. On[^007]followed by seven autos the last one is 7 upstream and 8 here.A Markdown label written with non-ASCII digits (
[^٣]) stays a name rather than becoming a manual number, and a name takes an auto number — so it consumes one, and every later auto footnote on the page shifts up by one. On[^٣],[^a],[^b]a Sphinx build numbers٣, 1, 2 and ubCode numbers 1, 2, 3.A citation reference the project cannot resolve reports
std.ref(undefined label), where a Sphinx build words the same finding citation not found. Same severity, same suppression family. Both that message andstd.citation_unreferencedname the label normalised — lower-cased, with runs of whitespace collapsed — so a.. [CIT2002]is reported ascit2002, where a Sphinx build echoes the case you wrote.A
:ref:role pointing at a citation counts as a reference to it, so that citation is never reported unreferenced. A Sphinx build counts bracket references only: it reports the citation as unreferenced even on a page carrying the role, and then fails to create the cross-reference at all. Counting it follows from a citation label being an ordinary standard-domain label here, which is what makes the role resolve in the first place. A hyperlink reference (cited_) counts on neither side — it is a different family of reference on both.A citation defined in a file spliced in by
.. include::is reported at that file, on the definition’s own line. A Sphinx build names the including page instead, with a line number that indexes the fragment while the file name indexes the page, so the pair points at whatever the host happens to have on that line — or past its end. The finding fires either way; only the file named differs, and ubCode names the one goto-definition opens.A citation reference written inside a body excluded by a falsy
.. only::expression does not count as a reference, so a citation referenced only from there is reported unreferenced. A Sphinx build registers the reference while reading the page and discards the body afterwards, so it stays silent. The mirror case diverges the other way: a definition inside an excluded body is invisible here, so a citation defined only there is never reported, where a Sphinx build registers the definition while reading the page and reports it.A footnote reference written in a heading is dropped from every plain-text rendering of that heading — the page
<title>, a parent’s toctree entry, the sidebar and prev/next labels, thestd:docdisplay text in the published inventory, and the page title the search index stores — while the label is defined, which is what a Sphinx build does too. On an undefined label both builds keep the marker in all of them. The one exception is a heading whose whole text is a defined marker: ubCode falls back to the raw source and shows[#]_, where a Sphinx build shows the empty string, writes an emptystd:docdisplay name and leaves the page out of its search index altogether. The page’s own.. contents::listing now agrees with a Sphinx build’s on every shape: a defined reference is dropped, an undefined autonumbered[#]_or symbol[*]_past its supply is kept, and an undefined named or numbered one is dropped. The “On this page” panel keeps every undefined marker, as it always has. A citation reference is kept by both listings on both sides, but ubCode writes the label as a bare[LABEL]span where a Sphinx build wraps it in a link to the citation and gives it an id — which is also what stops the page emitting one id twice, whether or not the label resolves. The heading itself still shows the marker, and so does the heading text stored against each search result, where a Sphinx build keeps the label too. For a Markdown[^label]that matches a Sphinx build exactly; for a reStructuredText one it matches but for the trailing underscore, which ubCode never renders. The heading’s own anchor moves toward a Sphinx build’s for the same reason: the marker stops contributing text to the slug, sotitle-with-ref-xbecomestitle-with-ref, which is what a Sphinx build has always written.A footnote finding whose reference or definition sits inside a file spliced in by
.. include::names that fragment and the site’s own line and column, with the host page carried alongside it as theincluded-fromlocation — which is where a Sphinx build reports it too, and matches the citation family described above. ubCode names the line the text is written on under every clip —:start-line:,:start-after:, the two together, and with or without:end-line:/:end-before:. A Sphinx build does not, and the two engines differ in opposite directions. For reStructuredText docutils reports the line counted from the start of the slice: a construct on line 18 of a fragment included with:start-line: 10is reported at line 8, a defect docutils carries in its own source as# TODO: if startline != 0, line numbers are wrong.For MyST a Sphinx build names one line below the line the text is on, whatever:start-line:says — measured constant for:start-line:0, 1, 2 and 10 — so on a sliced{include}the two engines differ by exactly one, the same constant an unsliced one already differs by. Under:start-after:a Sphinx build’s MyST number is unusable: myst-parser adds the character position and length of the search string to a line number, so a reference on line 18 of one measured fragment is reported at line 62. None of this is specific to footnotes — it is the position every finding reported from spliced content carries.
Hyperlink references¶
A named reference such as target_ or `phrase`_ resolves through the
document’s own target table on both surfaces —
the built site and the live preview alike.
A reference to an external link target (.. _t: https://example.com) renders
the real URL,
a reference to an indirect target (.. _alias: t_) follows the chain to its
resolved end,
an anonymous reference (anon__) takes the anonymous target at its own
position in the document,
and a reference to an explicit internal target or a section title links to that
target’s anchor on the page.
Resolution also feeds diagnostics:
a dangling or ambiguous named reference is reported
(ref.unknown_target, ref.duplicate_target, ref.circular_indirect,
ref.nonexistent_indirect, ref.anonymous_mismatch)
in indexing, ubc check, and the build warning count.
These are warnings, where a Sphinx build grades the docutils-derived ones as errors;
under --deny warning they still fail the build.
Whether a nested bracket inside link text cancels the outer link is decided by a
different test here than upstream.
markdown-it asks whether any inline rule would consume a token at that bracket;
ubCode asks whether the bracket looks like a link, plus — since a footnote reference is the
only other construct that binds [ — whether the footnote rule would match there.
So a footnote reference inside link text now cancels the link on both sides, and five shapes
still differ: a nested shortcut reference that resolves ([see [plain] text](url) with
[plain]: url) keeps the outer link here and loses it upstream, while four shapes that
consume nothing — [plain][undef], [plain](not a url), an undefined [plain][] and an
unclosed [plain](x — lose the outer link here and keep it upstream.
A nested image is exempt on both sides, because upstream consumes the whole image in one
step and never looks inside its alt text.
ubCode stands its footnote test down for the same region, keyed on a ! before the [,
which is upstream’s outcome wherever an image token is actually consumed and leaves three
shapes short: an ![ that never closes, an escaped \![, and a reference-style
![alt][img] whose [img]: definition is missing.
Upstream consumes no image in any of the three, reaches the marker and drops the outer link,
where ubCode keeps it.
One residue is preview-only.
Where two things on a page claim the same name,
ubc build html links to the exact anchor it minted for the winner
(#name-1 by default, #id3 under id_style = "docutils"),
while the preview keeps the plain slug (#name).
The exact-anchor table is produced by the whole-document anchor capture that the
preview deliberately does not run on every keystroke,
so a colliding name is the one case where the two surfaces still disagree.
Substitutions¶
|name| substitution references expand in the preview:
your own .. |name| replace:: definitions,
and the Sphinx specials |version|, |release|, and |today|
(fed by the version, release, today, and today_fmt keys
of the [project] table in ubproject.toml).
A few differences from a Sphinx build remain:
An undefined or circular substitution keeps its literal
|name|chip and raises a diagnostic; a Sphinx build reports these as errors in the build log instead.Definitions whose expansion would grow without bound are cut off with an oversized diagnostic — a guard with no Sphinx equivalent (a Sphinx build can take unboundedly long on such input).
Glossaries¶
A reStructuredText .. glossary:: renders as a definition list of terms,
each carrying a term-… anchor and a ¶ permalink,
with :sorted: applied in the same way a Sphinx build applies it
(case-insensitively, on each entry’s first term, keeping a shared definition together).
An entry written without a definition is kept and its term registered,
rendered with an empty definition, exactly as in a Sphinx build.
Two differences from a Sphinx build remain, both about the anchor and the classifier:
A term’s classifier is kept, but nothing renders it — and there is no general index for it to sort. In
Term : classifier— spaces required on both sides — the part after the separator is a classifier, and a Sphinx build uses it for one thing only: the sort key of that term’s entry in the general index. ubCode records it as the index key of the term’s entry, soTerm : bandTerm : zare distinguishable to ubCode, but ubCode builds no general index, so the key has nowhere to be shown. Not rendering it matches a Sphinx build, and neither engine puts it in the built site’s search (both drop the key from the search index). The classifier stays out of the term’s anchor and out of the:sorted:ordering on both engines. Only the first classifier is kept, exactly as a Sphinx build keeps only the first. (A classifier in an ordinary, non-glossary definition list is unaffected.) One consequence worth knowing: markup written inside a classifier — an inline target, or a cross-reference — is still parsed and indexed even though it renders nowhere, so an anchor defined there is unreachable and a broken reference there is still reported. A Sphinx build splits the classifier off before parsing and never sees it.Term anchors are lowercased. ubCode derives a term’s anchor with the standard docutils identifier rules, so
Zebrabecomes#term-zebra; a Sphinx build uses its own variant of those rules for glossary terms and emits#term-Zebra, preserving capitals, dots and underscores. Consequences:Foo_barandFoo.barshare one ubCode anchor (a Sphinx build keeps them apart), a term beginning with digits loses them (123abc→#term-abc), and two same-named terms on a page get-1,-2suffixes where a Sphinx build numbers themterm-0,term-1. This affects only the emitted identifier and theobjects.invURI::term:references resolve case-insensitively on both sides, so a reference that resolves in a Sphinx build resolves in ubCode too.
One further, cosmetic difference:
The list carries no ``simple`` class. A Sphinx build emits
<dl class="simple glossary">; ubCode emits<dl class="glossary">. Thesimpleclass is a general docutils behaviour for compact definition lists, which ubCode does not emit on any definition list.
A Markdown (MyST) {glossary} is read with the same body grammar,
which is not Markdown’s definition-list syntax:
a term goes on its own unindented line
and its definition on the indented lines below it,
with a blank line between entries,
exactly as in reStructuredText.
That matches a Sphinx build,
which hands a {glossary} fence’s raw lines to the same directive
whatever the file’s format,
so the deflist extension makes no difference to a {glossary}
whether it is enabled or not.
The definition’s content is parsed as Markdown.
Because the definition-list spelling is a plausible thing to type there,
ubCode reports it — glossary.definition_as_term, on the : … line.
That is a finding a Sphinx build does not raise,
not a difference in what is registered:
the line becomes a term on both engines,
and both publish it in objects.inv.
One thing that does not carry across the two flavours:
an indented glossary body.
reStructuredText de-indents a directive’s content before the grammar sees it,
so an extra level of indentation is harmless there;
Markdown strips only the fence’s own indent,
so an indented {glossary} body reads as definitions with no term
and produces an empty glossary.
ubCode matches each engine’s own behaviour here —
a Sphinx build does the same on both sides —
so this is a difference between the two flavours, not from Sphinx.
Index entries¶
.. index:: and the :index: role render the way a Sphinx build renders
them — for the directive, nothing you can see:
a Sphinx build skips the index node in its HTML writer,
so all the directive leaves behind is an anchor
(:name: when you give one, index-0, index-1, … otherwise —
one sequence shared with the role, in document order).
Where that anchor sits differs in a way no link notices:
ubCode emits an empty <span> at the directive, while a Sphinx build usually
attaches the same id to the next paragraph instead.
Either way #index-0 lands on a real element, so references resolve on both.
The :index: role renders its display text as plain prose behind the same
kind of anchor: :index:`Widgets <pair: widget; hardware>` shows Widgets,
and :index:`!widget` shows widget.
Four things follow that are worth knowing:
The editor preview shows the directive as a small chip listing the entries you wrote (and the
:name:, when it changes the anchor), so an otherwise invisible construct is visible while you author it. The built site does not — matching a Sphinx build.The entries are collected and checked, but there is still no general index page. ubCode parses each entry the way a Sphinx build parses it — the type prefixes (
single:,pair:,double:,triple:,see:,seealso:), the leading!that marks a main entry, and the comma shorthand — and files it against the anchor its construct minted. What a Sphinx build then does and ubCode does not is build agenindex.htmlpage, so there is still no index page to link to. A malformed entry is reported: the wrong number of;-separated parts, and one of the long-removed prefixes —module:,keyword:,operator:,object:,exception:,statement:,builtin:. A Sphinx build only warns for the first; the second raises an uncaught error that aborts the whole build. ubCode reports both as ordinary diagnostics and keeps building, which is a divergence in the forgiving direction. One quirk is upstream’s and is reproduced faithfully: the!is read at the very start of an entry, before any type prefix, so! single: widgetis a main entry whilesingle: !widgetkeeps the!as part of the value and is not one.A term that appears only in an index entry is findable by search. A Sphinx site’s search has a dedicated index-entry channel (
indexentriesin itssearchindex.js, since Sphinx 5.2), and a ubCode-built site now carries the same one, matched the way a Sphinx site matches it: the whole query has to be a substring of the whole entry and at least half its length, scored by how much of the entry you typed. So a word you put in.. index::and nowhere else in your prose is findable — which is much of what the directive is for. Main entries (a leading!, and every glossary term) rank with the ordinary results; the rest are listed after them. A term’s classifier is not searchable on either engine. In the editor, the same entries are searchable through thesearch_referencesMCP tool, asindex:entryhits that tell you where a phrase is already indexed.A ubCode-built
objects.invdoes not advertise the index pages ubCode does not build. The inventory a built site publishes for other projects to pointintersphinx_mappingat carries asearchlabel —search.htmlis generated — and nogenindex,modindexorpy-modindexlabel, because no such page is written. A Sphinx build advertises all four unconditionally: measured, turninghtml_use_indexoff removesgenindex.htmland changes its inventory not at all, and a prose-only project with no Python still advertisesmodindexandpy-modindex. So in another project,:external:std:label:`genindex`(or a:ref:through an intersphinx mapping) that would have silently produced a link to a missing page against a Sphinx-built inventory instead reports an unresolved reference against a ubCode-built one. That is the intended trade: a warning you can act on rather than a 404 nobody detects. From inside your own project the same rule decides what links, and it is the rule rather than a fixed list::ref:`search`renders a real link to the generated search page (withSearch Pageas its text, or your own title in the:ref:`Find things <search>`form), exactly as a Sphinx build does — while that page is being built. Setenabled = falseunder[build.html.search]and there is nosearch.html, so ubCode seeds nothing: the reference goes back to an inert placeholder chip and thesearchrow leavesobjects.invwith it. That is the same principle applied consistently, not a special case — ubCode advertises and links exactly the pages it writes.:ref:`genindex`renders a placeholder chip in every configuration, because there is no such page to link to at all. None of these warn, in any configuration. If your project defines its ownsearchlabel, yours wins — for the reference and for the inventory row alike, as in a Sphinx build.ubc build targetsfollows the same rule, so the two commands’ inventories agree row for row.
One quirk is copied deliberately, because a Sphinx build has it too:
in the :index: role’s no-title form the entry prefixes are not
interpreted, so :index:`pair: a; b` puts the literal words pair: a; b
into your prose and files them as a single single entry.
Write the explicit-title form when you mean a typed entry
(:index:`a and b <pair: a; b>`).
ubCode adds an advisory here that a Sphinx build does not have. Because the no-title form almost never means what it looks like, a no-title
:index:whose text opens with one of the six entry-type keywords and a colon (single:,pair:,double:,triple:,see:,seealso:) reportsindex.role_type_prefix, naming the rewrite. It is info-level, so it never failsubc check: a project a Sphinx build accepts must stay a projectubc checkaccepts, and Sphinx is silent on this construct. Silence it entirely withignore = ["index.role_type_prefix"]under[lint]. The rendering and the filed entry are unchanged either way — it is advice about a construct, not a change to it.
The :any: role¶
The :any: role (and its MyST {any} twin) resolves a target without
naming what kind of thing it is.
ubCode walks the same ladder a Sphinx build does, in the same order — a
document, then a project-wide label, then a glossary term, then any
loaded intersphinx inventory — and where more than one of the first three
matches it warns and links the first, exactly as Sphinx does:
more than one target found for 'any' cross-reference 'guide': could be :doc:`The Guide` (guide) or :std:ref:`A Guide Section` (handbook#guide)
A target nothing matches reports 'any' reference target not found: <target>.
Both messages share one code, std.any, which mirrors Sphinx giving both one
suppression key: silence them together with ignore = ["std.any"] under
[lint].
default_role = "any" works, and is worth knowing about: under it every
bare-backtick span in the project becomes an :any: reference, and behaves
identically to writing the role out.
The same ladder serves sphinx-design’s cross-reference directives.
A button-ref whose :ref-type: is any — which is the default, so this
is every button that does not say otherwise — and a card whose :link-type:
is any walk exactly the rungs above, exactly as a Sphinx build does.
One rung is sensitive to whether you wrote your own link text:
a label with no section title after it can only be reached by a link that
supplies its own text, because there is no title to display.
For a button that means giving it content;
for a card it means giving it :link-alt:.
Without one, the link reports std.any and stays a placeholder — the same
answer a Sphinx build gives.
The ladder serves imported need content too.
An :any: role or a sphinx-design any link written inside the content
of a need brought in by needimport walks the same four rungs.
The document rung is the one that depends on where the reference is written, and
an imported need has no file of its own — so it is resolved against the page
whose needimport produced the card, which is also the page the link is
rendered on.
The other three rungs are project-wide and answer the same wherever the need is
imported.
A reference nothing matches reports std.any like any other, anchored at the
needimport directive and naming the need whose content it is in:
'any' reference target not found: nosuchtarget (in content of imported need 'REQ_1')
If your imported data carries any references you cannot fix at the source,
ignore = ["std.any"] under [lint] silences the code project-wide, and
per-file-ignores narrows it to the page that imports them.
Six differences from a Sphinx build:
A mixed-case glossary term resolves here, and does not in Sphinx.
:any:`Zebra`links to aZebraglossary entry in ubCode. A Sphinx build reports it as not found, because its:any:ladder looks the term up lowercased in a store that kept the case you wrote — its own:term:role has a second lookup that recovers from this, and its:any:ladder does not use it. ubCode’s term lookup normalises both sides, so the reference simply resolves. Nothing that resolves in a Sphinx build fails here as a result; you may see a reference work in ubCode that a Sphinx build warns about.The ladder reaches fewer kinds of target, because fewer kinds exist. A Sphinx build’s
:any:can also reach a command-line option, a grammar token, and every object of the Python, C, C++, JavaScript and reStructuredText domains. ubCode does not index those, so:any:cannot reach them — the same reason the:option:and:token:roles are not yet supported. Where a target space arrives, its:any:rung arrives with it, and there are two worked examples now: configuration values and environment variables are both indexed, so:any:reaches both — after the glossary-term rung and before the inventories. Where one name is both, the configuration value wins and the ambiguity warning lists it first, which is the order a Sphinx build uses too.The generated-page labels are a chip, not a link — for the same names
:ref:already chips. A Sphinx build links:any:`genindex`,:any:`modindex`and:any:`py-modindex`to the pages it generates for them. ubCode generates none of those pages, so there is nothing to link to and the reference renders its placeholder instead. It does not warn — the names are exempt from the dangling check on both roles, so a project using the sphinx-quickstart boilerplate still builds clean.:any:`search`is the exception and it does link, exactly as:ref:`search`does, because ubCode really does build that page (unless you set[build.html.search] enabled = false, in which case it is a silent placeholder too).An ambiguous sphinx-design link that supplies its own text names its candidates more usefully here. When a
button-refwith content, or a card link with:link-alt:, finds more than one target, ubCode lists each candidate by the title that candidate resolved to, exactly as it does for an:any:role written without its own text. A Sphinx build lists them by the link’s own text instead, so every candidate in the message reads the same and none of them names what actually collided. This applies only to a link that supplies its own text: for one that does not, a Sphinx build captions from the resolved titles too, and the two messages agree. The link itself is identical in both: same winner, same target, and one suppression key per engine —std.anyhere,ref.anyin Sphinx.Each candidate in the ambiguity message says where it resolved to. The parenthesis after each candidate —
(guide),(handbook#guide)in the message above — is the page the link would go to, and, after the#, the target as you wrote it. That second part is deliberately the name from your own sources rather than the fragment the page actually emits, so you can search for it: the emitted id is the[parse] id_styleslug of the name for a label (#my-targetformy target) when that slug is still free — on a collision it is a serial,#my-target-1under the default (github) and an opaque#id1underdocutils— and it always carries aterm-prefix for a glossary term (#term-my-target). So do not read the parenthesis as a URL. A Sphinx build prints the candidate’s title alone. That is enough when the titles differ, and useless when they do not: if you wrote your own link text (:any:`The Guide <guide>`), a Sphinx build captions every candidate with that text, so the message readscould be :doc:`The Guide` or :std:ref:`The Guide`and names nothing that collided. The role prefix and the location together tell the candidates apart. A whole document and an anchored target differ in the location — including when they live on the same page, which a page name alone could not show. A label and a glossary term that share one name on one page print the same location, and there the role prefix (:std:ref:against:std:term:) is what separates them. A titleless document is captioned<no title>, which is what a Sphinx build calls it too — and so is its:doc:link text, everywhere else the document is named.An ambiguous match across two intersphinx inventories is reported here, and a Sphinx build is silent about it. If a target exists in nothing local but in more than one loaded inventory, ubCode links the same one a Sphinx build does and adds an
intersphinx.ambiguousnote saying which project won and which also matched:multiple inventory matches for 'shared': took 'zulu' (std:label), also matched by 'alpha' (std:label)
It is info-level, so it never fails a build: a project a Sphinx build accepts stays a project
ubc build htmlandubc checkaccept. It is worth saying because of how the winner is chosen: the inventories are merged in project-name order and the alphabetically last one wins, so adding an inventory whose key sorts after an existing one silently moves every colliding link — and renaming a project key does the same. The order the projects appear inubproject.tomlhas no effect at all, on either engine. To pin a target to one project, qualify it::ref:`zulu:shared`or:external+zulu:ref:`shared`. See the lint reference for how to silence it. Four names are exempt on:ref:and:any:sites and carry no notice there —genindex,modindex,py-modindexandsearch— because every generatedobjects.invships them, so any two inventories collide on them before you write a line. A Sphinx build resolves those four against its own generated pages; ubCode builds none of them exceptsearch, so such a reference does follow the name-order winner out to somebody else’s site, silently. The notice is therefore not exhaustive. An explicit external form is not exempt::external:ref:`genindex`asks for an inventory answer by name, and reports the collision like any other. The same collision reached through a Markdown link is reported at warning level instead — see Markdown link cross-references for why the two halves differ.
Configuration values¶
.. confval:: documents a configuration value, and :confval: links to
one.
Both work, on reStructuredText and Markdown pages alike, and what a page
renders matches what a Sphinx build renders.
Seven things below are worth knowing: four genuine differences (one of them in
ubCode’s favour), and three places where the behaviour matches a Sphinx build
in a way you might not expect it to.
The lookup is exact — and that is parity, not strictness.
:confval:`Needs_Types`finds a value defined asNeeds_Typesand nothing else. A differently-cased spelling does not resolve, and neither does one carrying a leading~: the tilde is part of the name you asked for. This is worth stating because:term:is not like this — a glossary term resolves case-insensitively — and a Sphinx build draws the same distinction between the two roles for the same reason.A value defined twice is reported, and the last definition wins. Two
.. confval::directives for one name raisestd.duplicate_confvalat the second one, naming the first, and the link goes to the second — matching a Sphinx build, which warns and then overwrites. This holds whether the two definitions are in different files or in one file, and every surface agrees on it: the link’s address, the entry inobjects.inv, and where “go to definition” takes you in the editor. Silence it withignore = ["std.duplicate_confval"]under[lint]; a Sphinx build’s own copy of this warning carries no category, so itssuppress_warningscannot reach it.A cross-reference inside
:type:or:default:renders as a link. A Sphinx build crashes on this — it aborts with anAssertionErrorfrom its HTML writer — so there is no output to match and ubCode renders the sensible thing. Emphasis, literals and plain text behave identically in both.The value is not listed in the page’s contents, and
:no-contents-entry:therefore does nothing. A Sphinx build adds each configuration value to its own navigation tree (though not to a.. contents::directive, which lists neither engine’s values). ubCode’s navigation lists sections only, so the flag has nothing to suppress. It is still accepted, so a page written for a Sphinx build needs no edits.A link to a value is styled with ubCode’s own class. A resolved
:confval:renders the name in monospace inside a link, as a Sphinx build does, but the link carriesclass="ub-xref"— the single class every ubCode cross-reference carries — where a Sphinx build writesclass="reference internal"and gives the inner<code>the classesxref std std-confval docutils literal notranslate. Only a stylesheet written against Sphinx’s class names is affected; the address, the text and the monospace are the same.A repeated signature line still appears — and that is parity, not a leak. Writing the same name twice in one
.. confval::argument renders both lines, and only the first gets an anchor and a permalink. The same holds for two spellings of one name (a banda b): both lines appear, each with its own spacing, and one of them is the target. A Sphinx build does exactly this — it de-duplicates the target, never the signature — so a page written for one renders the same here.A duplicated anchor is numbered differently. Two values with one name on one page get
#confval-nameand#confval-name-1here, where a Sphinx build writes#confval-nameand#confval-0. A reference links to the second definition in both engines — the one the duplicate warning is about — so what differs is only that anchor’s spelling:#confval-name-1here,#confval-0there.
:no-index: behaves as a Sphinx build’s does, and so does
:no-index-entry:, which is accepted and has no effect on a configuration
value in either engine.
:no-typesetting: also behaves as a Sphinx build’s does in the way that
matters — the value renders nothing, its anchor survives, and :confval:
still links to it — but the anchor sits on a different element: an empty
<span> of its own here, and the element that follows the directive in a
Sphinx build.
No link’s address changes either way.
Environment variables¶
.. envvar:: documents an environment variable, and :envvar: links to
one.
Both work, on reStructuredText and Markdown pages alike, and what a page
renders matches what a Sphinx build renders.
Almost everything true of configuration values
above is true here too — the lookup is exact, a value defined twice is reported
and the last definition wins, the anchor keeps capitals and dots, a repeated
signature line still appears, and a duplicated anchor is numbered
#envvar-name-1 where a Sphinx build writes #envvar-0.
Four things are worth knowing that are not shared with configuration values.
A broken
:envvar:link is not reported — and that is a match, not an oversight. Writing:envvar:`HOEM`for a variable calledHOMEproduces no warning at all, at any level. A Sphinx build does the same: its:envvar:role is the one member of this family registered without warn-on-dangling, so a missing environment variable is silent there too unless you run it in nitpicky mode, which ubCode has no equivalent of.:confval:is the contrast — a broken one does raisestd.confval— so the two roles genuinely differ here, in both engines. What you do get is a visible marker: the reference renders as a small bordered chip carrying the role name and the text you wrote, where a Sphinx build renders it as ordinary monospace text. So a mistyped variable is visible here and invisible there, and neither engine writes a warning about it. The chip shows your text exactly as you typed it, which is also visible on the deliberately unlinked form::envvar:`!HOME`shows!HOMEhere, where a Sphinx build strips the!and showsHOME.A link to a variable adds an entry to the index — twice. Every
:envvar:reference (including a broken one) contributes an invisible anchor at its own position and two index entries: the variable’s name on its own, and the name nested under environment variable. A definition contributes only the nested one. This is a Sphinx build’s behaviour exactly, and it is the reason a reference is searchable and not only a link. Two consequences are worth knowing. The anchor is numbered from the same counter the:index:role and the.. index::directive use, so adding or removing an:envvar:reference changes the anchor numbers of the index markers after it on the same page — again matching a Sphinx build. And the!form is exempt::envvar:`!HOME`contributes no anchor and no entries, so it does not disturb the numbering.The value indexed is the target, never your link text.
:envvar:`the home directory <HOME>`filesHOME, notthe home directory, and a run of spaces inside a name is collapsed to one before it is filed. A Sphinx build files the same string.:type:and:default:are not accepted on.. envvar::— and ubCode ignores them where a Sphinx build refuses the whole directive. A Sphinx build rejects an option the directive does not declare: it reportsERROR: Error in "envvar" directive:and nothing at all is produced for that block — no entry on the page, no index entry, and no target for:envvar:to link to. ubCode drops the unrecognised option in silence and then builds the block as though it had not been written, so the variable is documented, indexed, linkable and exported as usual. Neither engine renders it as a field of the variable, so nothing is misrendered — but the same source gives you a documented variable here and a missing one there, which matters if you build with both. This leniency is how ubCode treats an unrecognised option on most directives; the Sphinx-Needs ones,.. need::and.. list2need::among them, report it instead.
A duplicate definition raises std.duplicate_envvar, its own code rather
than the configuration-value one, so silencing one does not silence the other:
put ignore = ["std.duplicate_envvar"] under [lint].
A Sphinx build’s own copy of this warning carries no category, so its
suppress_warnings cannot reach it.
A target stacked above an external target¶
An empty explicit target written directly above an external or indirect one inherits that target’s destination:
.. _upper:
.. _lower: https://example.com/lower
upper_ and lower_ both link to https://example.com/lower, on both
engines — the upper target has been consumed, and neither engine gives it an
anchor of its own.
So it is not a cross-reference target either.
ubc build html publishes no objects.inv entry for upper and
:ref:`upper` is reported as undefined label (std.ref) and rendered
as plain text, which is what sphinx-build does with the same source.
Note
This changed in ubCode 0.37.0.
Earlier versions registered upper as an ordinary target: objects.inv
carried page.html#upper, and :ref:`upper` rendered as a link to an
anchor the page has never emitted.
If a project relied on that link, the build now reports std.ref for it —
give the element its own target below the external one, or silence the code
with ignore = ["std.ref"] under [lint] while you migrate.
One sub-case is stricter here than in a Sphinx build. Where the lower target sits inside a directive Sphinx failed to parse, a Sphinx build still publishes an inventory entry for the upper name — an anchor its own page does not emit — while ubCode publishes nothing.
Cross-references in titles¶
A cross-reference written inside a title carries its own advisory.
A title is not shown only in the page’s own heading: it is re-used by the
sidebar navigation, the prev/next footer, an in-page toctree entry and the
page’s contents list, and every one of those is already a link.
An anchor cannot nest inside an anchor, so on those surfaces the reference is
shown as plain text — the same shape a Sphinx build with a standard theme
produces.
In the bare form (Using :ref:`target` here) a document title’s
navigation entry also shows the raw target name instead of the target’s title,
because the display text is not resolved at that point.
ubCode adds an advisory here that a Sphinx build does not have. A cross-reference role (
:ref:,:doc:,:term:,:external:…) or a MyST link written in a page or section title reportstitle.cross_reference, naming the surfaces it affects and the rewrite: move the reference into the body, or — for a role — use the explicit-text form (:ref:`text <target>`). A MyST link is told only to move the reference: once the link is parsed its destination as written is no longer recoverable, so a spelled-out form could point somewhere other than the author wrote. It is info-level, so it never failsubc check: a project a Sphinx build accepts must stay a projectubc checkaccepts, and Sphinx is silent on this construct. Silence it withignore = ["title.cross_reference"]— or the whole family,ignore = ["title.*"]— under[lint]. The rendering is unchanged either way; it is advice about a construct, not a change to it. Need roles (:need:and:need_part:) are deliberately excluded: sphinx-needs supports a need reference in a heading and keeps a regression test for it, so ubCode does not second-guess it.
Directives that render nothing¶
Six directives are recognised, accepted, and deliberately contribute nothing to the page body. That is not a gap in every case — for most of them it is what a Sphinx HTML build produces too:
sectionauthor/moduleauthor/codeauthorA Sphinx build renders these only when
show_authorsis switched on, and it defaults to off, so a default-configured build shows nothing either. ubCode has noshow_authorsequivalent yet, so the author line cannot be turned on — the one difference, and the reason to keep writing them is the metadata, not the output.header/footerA Sphinx HTML build drops these entirely: docutils moves their content out of the page body into separate writer parts, and Sphinx’s HTML builder only ever emits the body. ubCode matches that exactly. Other docutils writers (LaTeX, manpage) do render them, so this is specific to HTML output rather than a general limitation.
metaNothing in the body — which is what a Sphinx build produces too — and the tags themselves go where they belong, in the page’s
<head>. See Page <head> meta tags for what is written and how it is checked.
Before this, all six printed a framed placeholder box into the page, so authoring metadata appeared in the reader’s prose. That is what changed.
A page can override its browser title¶
.. title:: Some Other Title (and the MyST {title} fence)
sets what the browser tab and the search results show,
without renaming the page anywhere else.
The heading stays the heading: the sidebar entry, the previous/next labels,
the parent page’s table-of-contents caption and the objects.inv export
all keep showing your H1.
That two-channel split is Sphinx’s own, and ubCode reproduces it.
The directive renders nothing into the page — before this it printed a framed placeholder box, which is markup a Sphinx build never produces.
Details worth knowing, all matching a Sphinx build:
The last one wins. Two
.. title::directives on a page leave the second in the tab, with no warning.Position does not matter. It can sit at the bottom of the file.
The text is taken literally.
.. title:: *not emphasis*puts the asterisks in the tab — nothing about the argument is parsed as markup.An included file’s ``.. title::`` sets the including page’s title.
Writing it with no argument reports
block.directive_no_argumentand the page keeps its heading — which is also what a Sphinx build does.
Three things it is not, or does differently:
MyST front matter
title:is a different feature — upstream turns it into anH1(only whenmyst_title_to_headeris on), so it renames the page everywhere rather than only in the tab. ubCode does not implement it.Giving the directive a body reports
block.directive_content_not_permittedand the title still applies. A Sphinx build is harsher: its content gate replaces the whole directive with an error before the title is ever read, so the override does not apply there. ubCode keeps the argument — the forgiving direction.On a page with no heading at all, a Sphinx build lets the override leak into the heading channel:
objects.invand the neighbouring pages’<link rel="prev">/<link rel="next">titles carry it. ubCode keeps the channels separate there too, so those surfaces read<no title>— the page really has no heading, and the tab title is a different channel saying so. Such a page gets no table-of-contents link on either side, and both report it (toctree.no_titlehere), because the warning is about the missing heading and not about the tab title; the previous/next chain still links to it.
Acknowledgements (acks)¶
.. acks:: renders its bullet list on its own —
no wrapper element and no injected “Acknowledgements” heading,
which is exactly what a Sphinx build renders.
The body must be a single bullet list;
anything else falls back to the generic placeholder,
where a Sphinx build warns and drops the directive.
As with every list in ubCode, the <ul> carries no docutils simple class.
Version markers¶
The four hyphenated spellings Sphinx 9 added —
version-added, version-changed, version-deprecated,
version-removed —
work and are indistinguishable from versionadded, versionchanged,
deprecated and versionremoved,
down to the CSS class on the rendered block.
Sphinx collapses the alias before anything downstream sees it, and so does ubCode.
Inline roles¶
:manpage: renders the shape a Sphinx build renders with no manpages_url
configured — <em class="manpage">ls(1)</em>, the whole reference as
emphasised text, section number included.
Sphinx turns it into a link when you set manpages_url in conf.py;
ubCode has no equivalent key, so it always renders the unlinked form.
One small difference in the same row:
Sphinx reads a leading ! as “do not link this one” and strips it,
while ubCode renders the ! — it only exists to switch off a link
that is never emitted here.
:mailheader: and :newsgroup: match a Sphinx build exactly.
Structure and inline detail¶
Some finer points of block and inline rendering differ from a Sphinx build:
Broken inline markup inside a directive argument or option value is not reported: an unterminated
**in an admonition title renders without the “Inline strong start-string without end-string” warnings a Sphinx build emits (the text itself is never swallowed).Nested line blocks render flattened rather than progressively indented.
.. container:: insand.. container:: delrender as plain containers — insertion and deletion semantics are not applied.Attribution lines (
-- author) inepigraph,highlights, andpull-quotestay plain paragraphs rather than becoming attributions.An unsupported option on a structural block makes the whole directive fall back to the generic placeholder.
In a Markdown (MyST) document, a directive’s argument and its inline-typed
option values are parsed with the MyST inline grammar, matching a Sphinx
build: *emphasis*, ` `code` `, links, and {ref} and its sibling
roles all work in a {admonition} title, a {rubric} argument, a
{code-block} :caption:, a {topic} argument, a table caption or a
sphinx-design card title, and a dangling reference in one is reported.
$maths$ works there too when the dollarmath extension is enabled —
a slot sees exactly the extensions the surrounding prose sees, so maths in a
title is on the same footing as maths in a paragraph.
Backslash escapes work as they do in prose: \{ref}`x` in a title is the
literal text, not a reference.
Link and footnote definitions are document-scoped, so a [label] in a
directive title resolves against a [label]: … line anywhere in the file,
including below the directive.
Two details differ from a reStructuredText document, and neither is a choice:
reStructuredText role syntax does not work in a Markdown slot.
` :ref:`x` `in a{card}title is the literal text:ref:followed by an inline code span, exactly as a Sphinx build renders it. Write` {ref}`x` `instead.A dangling reference is reported one line below the fence. A Sphinx build anchors a Markdown slot’s reference at the fence line plus one, where the reStructuredText equivalent anchors at the directive line itself; ubCode matches the build for a top-level directive. A version marker’s trailing text adds one more line, again matching. Inside a nested directive (a
:::{grid-item-card}in a::::{grid}, say) ubCode reports the line you wrote the reference on plus one, where a Sphinx build reports one line further down — the same one-line offset it applies to ordinary prose inside a nested directive, so ubCode’s number is the one to edit at.
Note
Writing a role inside a backtick-fenced directive’s argument does not
work in MyST at all, whatever the renderer: CommonMark forbids a backtick in
a fence’s info string, so ` ```{list-table} title {ref}`x` ` is not a
fence and the whole block degrades to a paragraph.
Use a ~~~ or ::: fence if you need one.
Note
A few references in a Markdown slot render but are not resolved or reported.
A {csv-table}’s cell content is re-parsed as reStructuredText
whatever the document’s format, so a reference written in a cell neither
renders nor warns; an {external}-family reference in any slot renders
but is not registered, so it never reports when it dangles;
and a Markdown link to another page ([text](other.md)) is not checked
inside a slot — in ordinary prose it is
(see Markdown link cross-references).
One scoping rule to know: inside a file you pull in with {include},
the host document’s link definitions apply to that file’s own
directive titles, not the partial’s.
A [label] defined in the partial itself stays literal there.
Footnotes are not affected: a [^label] is a reference wherever it is
written, and it resolves against every definition on the finished page —
the partial’s own included.
The Markdown block break (+++)¶
You write
+++on a line of its own in a Markdown (MyST) document — the MyST block break, which marks a boundary between blocks of text (notebook tooling uses it as a cell separator, and reads the text after it as that block’s metadata).The preview shows a dashed horizontal rule, so the break is visible while you write and distinguishable from a plain thematic break (
---), which draws a solid one.A Sphinx build renders nothing at all for it, and neither does
ubc build html. MyST treats a block break as a comment, and a Sphinx build drops comments from the page.Status — the dashed rule is a deliberate ubCode-only authoring affordance, on the same footing as the chips for
.. index::, the state-changing directives, block hyperlink targets and.. needextend::.
A plain thematic break is unaffected:
--- still renders a real <hr>, in the preview and in a built page.
Markdown link cross-references¶
A Markdown link destination that names project content becomes a
cross-reference — see the authoring guide for the
forms.
Ten points differ from a sphinx-build with myst-parser,
and each is deliberate:
A broken reference never removes your content. Where a destination resolves to nothing, ubCode keeps the link and its text and reports the failure. A Sphinx build deletes both in three cases: a failed
project:or document reference with no text of its own renders nothing at all, a no-matchinv:search renders nothing at all, and a document found with a fragment that was not renders an empty link. Text silently vanishing from a built page is not a behaviour worth matching.A
#fragmentreaches a plain heading, in either document.[](#some-heading)resolves against the referring page’s own headings and[](other.md#some-heading)against the target document’s, with no explicit target and no configuration (except a heading whose slug is the id of a need on the same page — see “A need keeps its id on its own page” below). The link then reads the heading’s own words, so[](#some-heading)shows Some Heading rather than the raw id. The target document may be reStructuredText:[](guide.rst#installation)works the same way.A heading is still outranked by everything above it in the ladder — an explicit
(target)=, a project-wide label, a document of that name and an intersphinx inventory each win over a same-named heading on the referring page, which is what a Sphinx build does at its default configuration.Where the two engines differ. A Sphinx build reaches a heading fragment only through MyST-Parser’s own heading-anchor layer: with
myst_heading_anchorsset, both the releasedmyst-parser(5.1.0) and its development tree resolve[](#some-heading)and[](other.md#some-heading)for a Markdown target whose heading is within the configured depth. At the default setting the released parser resolves neither form, and the development tree resolves only the same-page one, through itsreflocalidfallback (measured at commit723cffc).A fragment naming a plain section in a reStructuredText document resolves in ubCode alone, at any upstream setting: a Sphinx build has no slug metadata for such a document, so the fragment warns and drops the link text while emitting an href that happens to work. An explicit target inside that document is reached by the development tree’s own std-label lookup, still with empty link text; the released 5.1.0 warns there too.
A fragment naming nothing at all still reports the failure and keeps its text.
The reserved schemes win, and a clash is reported.
inv,pathandprojectcannot be redefined underurl_schemes; an entry naming one raises a configuration warning and is ignored. A Sphinx build silently letsmyst_url_schemesshadow them, so declaring apathscheme turns everypath:cross-reference in the project into an external link.The four web schemes always stay external.
http,https,mailtoandftpmerge into whateverurl_schemesyou declare rather than being replaced by it. In a Sphinx buildmyst_url_schemes = ["doi"]un-recogniseshttps:, and an unrecognised scheme is not an external link but a target-name lookup — so every[text](https://…)in the project becomes a dangling reference.New-tab links get a
relthat works. Withlinks_external_new_tabon, ubCode writesrel="noreferrer noopener"besidetarget="_blank". A Sphinx build writesnoreferer— oner— which is not a defined link type, so browsers ignore it and onlynoopenertakes effect: the referrer suppression the attribute is there to provide never happens. The spelling is corrected rather than copied.{{params}}in aurl_schemestemplate is always empty, so{{path}}keeps any;segments. A Sphinx build’s template variables come from Python’s own URL parser, which splits a trailing;parameterssegment off only for schemes on a hard-coded list (http,ftp,tel, …) — that is, never for the custom schemes the feature exists to serve.A destination that looks like a file is diagnosed as one.
[](notes.txt)with no such file is reported as a download that cannot be read, and[](nope.md)as an unknown document, each naming the thing you appear to have meant. A Sphinx build asks the filesystem first and, finding nothing, retries the whole destination as a target name, so it reports a missing target callednotes.txt. Relatedly,project:always means a document here — so an extension-less<project:other>resolves, where a Sphinx build’s suffix-driven lookup misses it and emits a literalproject:otherlink.A heading has one anchor, and
[parse] id_styledecides how it is spelled. ubCode mints exactly one id per heading, and a#fragmentmatches that id.myst_heading_anchorsadds a second, GitHub-flavoured slug layer in a Sphinx build instead.By default (
id_style = "github", see Parsing) a heading’s one id IS its GitHub slug, so a link written for GitHub or for MyST-Parser’s heading anchors, such as[](#my-heading)or[](#my-heading-1), reaches the same heading for a plain-text heading on a page where nothing else claims its slug — repeated headings included (Dup Slug×2 are#dup-slugand#dup-slug-1). The differences that remain:Depth. ubCode anchors every heading, where
myst_heading_anchors = Nstops at level N.One counter for the whole page. Every id on a page — headings,
:name:and{#id}anchors, targets, footnotes,.. contents::— shares one-1/-2counter, where GitHub counts headings only. An element earlier on the page that takes a heading’s slug, in its base spelling or a serial one, shifts that heading and every later heading of the same slug one serial down, and a link written for GitHub then lands on a DIFFERENT element with no warning: a{#dup}paragraph followed by## Duptwice gives the paragraph#dupand the headings#dup-1and#dup-2, so[](#dup)reaches the paragraph and[](#dup-1)the FIRST heading, where GitHub and MyST-Parser mean the second. A figure:name: Dupor a.. contents:: Dupbefore aDupheading does the same (the heading becomes#dup-1).Math in a heading. MyST-Parser slugs a heading without its math; ubCode slugs the title text with the math source included — the same input a Sphinx build’s own id uses — so
## Slug Derivation $a=1$ `code`is#slug-derivation-a1-codehere, and MyST’s[](#slug-derivation--code)does not resolve.An emoji with a variation selector, or an NFD title. ubCode keeps the invisible U+FE0F and any combining marks, exactly as GitHub does; MyST-Parser drops both, so a heading
## ‼️ Breaking Changesis an invisible U+FE0F followed by-breaking-changeshere (#%EF%B8%8F-breaking-changesin a URL), and a## Résumé Noteswritten with combining accents keeps them, where MyST-Parser gives#breaking-changesand#resume-notes— so its[](#-breaking-changes)and[](#resume-notes)do not resolve here.An empty slug. A heading or name with nothing left to slug (a
🚀heading,:name: !!!) takes the numberedid1,id2, … under both styles, where GitHub gives""and-1; a later headingId1is then#id1-1, so a GitHub-written[](#id1)reaches the first empty-slug element instead.
Under the opt-out,
id_style = "docutils", there are no GitHub-style heading slugs: the one id is the docutils one a Sphinx build also mints. The two layers agree on ordinary wording — bothMy Heading!andMy Headingare#my-headingeither way — and part company in two places:Repeated headings. Where two headings share a slug, a Sphinx build numbers the later ones (
#my-heading-1), so only the first is reachable here.Headings containing math, code or other markup. The GitHub slug is built from the heading’s plain text, while a docutils id keeps what the markup renders — a heading
Slug Derivation $a=1$ `code`is#slug-derivation--codein a Sphinx build and#slug-derivation-a-1-codehere.
A deep link written to a numbered
#idNdoes not follow a change ofid_style— those ids are positional under"docutils"and become-1/-2serials of the element’s own slug under"github", except for an element whose slug is empty, which keeps a positionalidNunder both styles — so write links to names and slugs, never to#idN.Migrating from the
"docutils"anchors. A site that published docutils anchors (a Sphinx build, or ubCode before"github"became the default) can take the new default and keep its old deep links: [build.html] legacy_anchor_redirects = [“docutils”] gives every page a map from each id’s docutils spelling to its new one,idNincluded, and the site’s script forwards a reader who arrives with an old fragment (["github"]does the same the other way round, on a"docutils"project). Sphinx has no equivalent. What the map does not cover:objects.inv: an intersphinx consumer gets the NEW anchors on its next refresh — which a project linking through intersphinx follows by itself;the PDF: its named destinations are the new ids, and a PDF has no redirect mechanism;
a reader without JavaScript, and a crawler: the forwarding happens in the browser, so they land at the top of the page;
an old anchor that the new style gives to a DIFFERENT element on the same page — a positional
#id1now taken by a footnote, or one title now spelled the way another used to be (under"github",Release 1.0is#release-10, the old anchor ofRelease 10): the page’s own anchor always wins, so the link lands on that other element, and no entry, generated or written by hand, can override an anchor the page carries — the usual outcome for an old anchor the page still carries, not a rare one;MyST-Parser’s SECOND heading slug (
myst_heading_anchors): a site that came from Sphinx and MyST-Parser also published GitHub-style heading anchors, which ubCode’s"github"ids match only where the two counters agree (see One counter for the whole page above); there is no"myst"member of the list yet.
An ambiguous match across two intersphinx inventories is a warning here, and only an info in the reStructuredText roles. Where a destination —
[](#target)or[](inv:…)— matches an object in more than one loaded inventory, ubCode reportsintersphinx.refat warning level, which fails a default build. That matchesmyst-parser, which warns on exactly this (myst.iref_ambiguous). The same collision reached through:ref:,:any:or:external:reportsintersphinx.ambiguousat info level instead, because a Sphinx build is silent there — see The :any: role. Each half follows its own upstream rather than being made uniform: making them match would mean either removing a warningmyst-parseritself emits, or failing builds a Sphinx build accepts. Silence the Markdown half withignore = ["intersphinx.ref"], the reStructuredText half withignore = ["intersphinx.ambiguous"].The ambiguity message names the search, not just the target. For a bare
[](#shared)ubCode printsmultiple inventory matches for '*:*:*:shared': …, spelling out the wildcard filter it actually ran;myst-parserprintsMultiple matches found for 'shared'for that form and switches to the filter form only for aninv:destination. The severity and the list of matches are the same on both engines; the codes differ (intersphinx.refhere,myst.iref_ambiguousthere), and ubCode uses the one wording for both destination forms wheremyst-parseruses two.
Markdown attributes¶
Added in version 0.35.0.
The attrs_inline and attrs_block extensions read
{#id .class key=value} groups — see
the authoring guide for the syntax and the
per-element keys. Eight points differ from a sphinx-build with
myst-parser, and each is deliberate:
An unterminated group keeps your text. You write
` `a`{.a `(no closing brace). The preview shows<code>a</code>{.a— the characters you typed. A Sphinx build shows<code>a</code>: its attribute parser succeeds on a group that runs off the end, so the braces are deleted. Content loss is not a formatting quirk, so ubCode declines the group.Classes accumulate on spans and links. You write
[a]{.x}{.y}or[a](u){.x}{.y}. The preview showsclass="x y". A Sphinx build showsclass="y": it reads the existing class off the closing token, which carries none, and replaces it. Its own documentation promises the opposite (“if multiple classes are given, they are combined”), and so does djot, the syntax’s stated source. Code spans and images already accumulate upstream, so this also makes the four element kinds agree.A span inside a link label keeps the link. You write
[a [b]{.c} d](u). The preview shows a link whose text contains a styled span. A Sphinx build shows the whole thing as literal text — an artefact of how its label scanner meets the span rule, which ubCode’s structural scanner does not have.A group before a directive fence becomes directive options. You write
{#note-a .lead}above a{note}fence. The preview shows the admonition carrying that id and class:#idis the directive’s:name:,.classis appended to its:class:, and every other key becomes an option. An option the author wrote out wins over the group’s. A Sphinx build drops the group silently — althoughmyst-parseralready ships this exact mapping for itsfence_as_directivemode.{eval-rst}fences are excluded: their body is another parser’s, so there is no option set to map onto, and the group is reported instead.Mistakes are reported.
attrs.invalid_value,attrs.invalid_choice,attrs.no_target,attrs.unsupported_targetand the informationalattrs.unsupported_key(see Linting) each cover a case a Sphinx build passes over in silence — or, for an imagew=/h=/a=with a bad value, crashes on. Each is positioned on the value you wrote, not on the element’s line.A link’s
titlerenders on a document link. You write[a](other.md){title="Team handbook"}. The preview shows<a … title="Team handbook">. A Sphinx build shows notitle: the doctree carries it, but the writer Sphinx uses for a document reference drops the attribute — while the writer it uses for an in-page anchor link keeps it, so[a](#anchor){title="…"}agrees on both. ubCode renders what you wrote in both places.A compact paragraph keeps its wrapper when you attribute it. You write a list with no blank lines between its items, and put a group inside one of them:
- {#step-one} Unpack the archive. - Run the installer.
The preview shows
<li><p id="step-one">Unpack the archive.</p></li>for the attributed item and the compact<li>Run the installer.</li>for the other. ubCode normally unwraps a tight item’s paragraph, followingmarkdown-it; an authored id or class outranks that, because the id is a cross-reference target and dropping the element would leave every[](#step-one)pointing at nothing. A Sphinx build wraps every list item’s content in a<p>, tight or not, so it never faces the choice — the attributed items agree, and only the plain one differs.The same rule covers the other places ubCode compacts a paragraph away: a tight definition list’s
<dd>, and the first body paragraph ofdeprecated/versionadded/versionchanged, which is normally merged into the version label’s own paragraph. Attribute any of them and the paragraph keeps its element.For the version directives that is a difference in your favour. A Sphinx build rebuilds the first body paragraph from scratch when it merges the version label into it, and the new paragraph carries neither the id nor the classes you wrote — so a reference to that id does not resolve upstream at all. ubCode keeps your paragraph, with its id and classes, as its own paragraph after the label’s.
A duplicate id keeps the first. You write
{#dup}twice in one file. The preview shows the first as the target and reportsstd.duplicate_target— exactly what(label)=already does. A Sphinx build unregisters the name entirely, so:ref:`dup`then fails to resolve at all — and on two headings it does not warn at all: it aborts, withValueErrorinside docutils, and writes no HTML.An empty or comment-only group still marks the span in the source, but renders no element. You write
[a]{}or[a]{%c%}. The preview showsa— an inline wrapper carrying neither a class nor an id is transparent in ubCode’s renderer, which predates attributes and is pinned elsewhere. A Sphinx build shows<span>a</span>. The group is still consumed on both engines; only the empty element differs.An inline code span’s language uses ubCode’s own class. You write
` `x`{l=python} `. The preview shows<code class="code language-python">, the samelanguage-…convention thecoderole already uses, which the highlighter picks up. A Sphinx build shows<code class="code docutils literal highlight highlight-python">with Pygments spans inside. ubCode’s<code>has never carried thedocutils literalclasses, so emitting them only for an attributed span would disagree with every other code span on the page.An explicit ``rel`` or ``target`` outranks the config default. You write
[a](https://x){target=_self}in a project withlinks_external_new_tab. The preview showstarget="_self". A Sphinx build overwrites it with_blank: the config is applied to the token before the group is read.
Content inclusion¶
Included files¶
.. include:: expands inline in the Rendered preview.
A few things differ from how a Sphinx build splices the same file textually:
State set inside an included file — a
.. default-role::, for example — is scoped to that file in the preview. A Sphinx build lets that state leak into the rest of the host document.A section heading in an included file that uses the same underline level as the host nests one level deeper in the preview than in a Sphinx build.
Clicking inside spliced content does not jump to the included file; sync-scroll maps spliced content back to the
.. include::line in the host document.Include options the preview does not support are flagged with a diagnostic and ignored; the file is still spliced in.
Markdown documents splice too.
A MyST {include}, {literalinclude} or {csv-table} with :file:
reads its file on every surface — the Rendered preview, the background index,
ubc check and ubc build html — exactly as the reStructuredText forms do.
Three things are specific to a Markdown host:
The included file is parsed as Markdown, whatever its extension. A fragment is parsed in the syntax of the document that includes it, which is what both Sphinx and
myst-parserdo: including a.rstfile into a Markdown document yields paragraphs and plain text, not directives. (The reverse also holds and is unchanged: a.. include::of a.mdfile is parsed as reStructuredText.)An included file’s headings nest inside the include, not into the page. They keep their relative structure — a
##under a#stays nested — but they never close the host’s sections, and an included heading never becomes the page’s title. They do appear in the page’s “on this page” contents, and a target under an included heading keeps that heading as its title. A Sphinx build integrates them into the page at their literal levels, so an included#can open a section beside the page title; it also means an included file that starts at##renders one heading level deeper here than it does there.:heading-offset:therefore has no effect and raises a warning, as do:relative-docs:and:relative-images:.An included file’s front matter is ignored, as in a Sphinx build.
A file that includes itself, directly or through a chain,
reports directive.include_circular and shows an error message in place of
that one include; the rest of the page renders normally.
The message names the file the include would re-enter,
relative to the project source directory,
so it reads the same on every machine that builds the project.
This applies to both formats and to every surface —
the preview, ubc check, the background index and ubc build html.
A Sphinx build with myst-parser has no cycle detection at all
and crashes instead.
.. literalinclude:: renders the named file as a code block —
in the Rendered preview, in ubc build html, and validated by ubc check.
Its selection options — :lines:, :start-after: / :start-at:,
:end-before: / :end-at:, :dedent:, :prepend: / :append:
and :tab-width: — apply exactly as in a Sphinx build, in the same order.
Three options are not applied; each raises a warning
and the file still renders with the remaining options applied:
:pyobject:— selecting a Python class or function by name.:diff:— rendering a unified diff against a second file.:encoding:— files are always read as UTF-8.utf-8andutf-8-sigare honoured (they are what a Sphinx build uses by default); any other value warns and the file is still read as UTF-8.
A leading byte-order mark is stripped, as in a Sphinx build —
unless you ask for :encoding: utf-8 explicitly, which keeps it.
A :tab-width: above 1000 is clamped.
A missing file, a marker that is not found, a line spec that selects nothing,
and a conflicting option pair each show a visible error message in place of the block —
the equivalent of the error a Sphinx build renders into the page.
A file that exists but is not valid UTF-8 is reported as an encoding problem,
not as a missing file.
Line-number options behave exactly as they do for code blocks (see below),
including :lineno-match:, which numbers the included slice with the numbers
its lines carry in the file.
It is mutually exclusive with :lineno-start:, :prepend: and :append:,
as in a Sphinx build.
The background index now splices .. include:: too:
a target, need, or glossary term defined in an included file
is registered in the host document,
so a :ref: or :term: to it resolves from anywhere in the project —
including from another page, and whether or not the included file
is itself one of your documents.
In ubc build html such a reference links the host page,
because that is the page the target’s anchor is rendered on,
and objects.inv advertises the same page.
Goto-definition and the hydrated title still point at the included file
where the target was written.
A :doc: written inside an included file
is resolved relative to the page it is included into,
as in a Sphinx build.
A default-role (or similar state) set inside an included file therefore
applies within that file’s spliced content on both the preview and the index.
A :need: to a need defined in an included file behaves the same way:
in ubc build html it links the host page,
which is where the need’s card — and so its anchor — is rendered.
The need’s docname (the value a :filter: or a Cypher n.docname sees)
is likewise the host document, as in a Sphinx build.
If the same file is included by more than one page,
its targets and need cards are rendered on every one of them,
but a reference resolves to a single page — the first host in path order,
chosen the same way for the page link and, for targets, for objects.inv
so the two always agree.
Needs carry one extra caveat, because a need id may only be defined once.
A need defined in a file that is included by more than one page,
or in a file that is BOTH included and one of your own documents
(an included .rst under your source directory rather than, say, an .inc partial),
is defined more than once — which is reported as a duplicate need,
and a :need: to it links whichever of those pages comes first in path order.
That page does not drift as you edit:
it is decided by your sources alone,
so an incremental rebuild and a --no-cache build always name the same one.
Keeping a shared need in a partial that your source directory does not pick up
and that only one page includes avoids the duplicate entirely,
and is the recommended shape.
Deleting an .. include:: line — or deleting a page that had one —
also removes the needs and targets that include had contributed,
so nothing is left linking to a card the page no longer renders.
A problem inside an included file is reported against that file,
at its own line and column, as sphinx-build reports it —
so the position always indexes into the file you are asked to open,
even when the including page is shorter than the file it includes.
Because the same content is checked once per including page,
ubc check adds a second included-from line naming that page,
and a report you see more than once is the same problem reached from
more than one host.
Either path can be used to silence it:
a per-file-ignores entry matches whether it names the included file
or the page that includes it.
Note
Both the preview and the background index splice
rst_prolog / rst_epilog,
so they agree on spliced content.
Between indexing passes, the per-keystroke checks hold back
undefined substitution diagnostics —
a reference might be defined in your prologue —
until the next background pass catches up.
Raw content¶
.. raw:: html passes its body through to the output verbatim,
in a block position and inside a substitution definition
(.. |br| raw:: html) —
which is what makes an rst_prolog-defined |br| render as a real line break
on every page that references it.
As in every Sphinx build, raw HTML is enabled by default and is emitted unescaped,
so treat a raw block as content you are responsible for.
Two controls exist for when that assumption does not hold;
both are described under Trust controls below.
The format argument is a list:
.. raw:: html latexemits in an HTML build, and.. raw:: latexis silently dropped, exactly as docutils does.ubc build pdfdraws no raw block either (a placeholder, when[parse] raw_enabled = false), with one exception: a.. raw:: latexblock whose whole content is\newpage,\clearpage,\pagebreakor\cleardoublepagebreaks the page there (see Page breaks you write).:class:wraps the payload in a<div>(block) or<span>(substitution) carrying those classes; without the option there is no wrapper markup at all.:file:reads the named file and records it as a dependency, so editing that file re-renders the page that embeds it.:url:is not supported — it would need a network fetch at build time. It raises a warning and the directive renders nothing.:encoding:is accepted but files are always read as UTF-8 (a non-UTF-8 value warns); a leading byte-order mark is stripped.Where docutils would insert a red error block into the page — a missing
:file:, a file and body content, neither of them — ubCode raises a warning and renders nothing, rather than injecting an error into the byte stream you are controlling.The inline
rawrole works, in its docutils form — a role derived from it with a:format::.. role:: raw-html(raw) :format: html A line break: :raw-html:`<br>` and a bold span: :raw-html:`<b>x</b>`.
The payload passes through verbatim, wrapped in a
<span>carrying the role’s classes (the role name unless you give it a:class:), and the same format-list rule applies: a role whose:format:does not namehtmlrenders nothing in an HTML build. The:format:option is only meaningful on araw-derived role; on any other base role it is ignored.Two differences from docutils: the bare
:raw:`text`form warns (role.raw_no_format) and renders nothing, where docutils raises an error and prints the unusable role into the page — the bare form cannot work in either, because a raw payload with no output format named is addressed to no writer. And a:format:value is matched case-insensitively, so:format: HTMLemits where docutils’ writer would not. A:format:written with no value at all renders nothing in both; ubCode additionally warns once per use site, where docutils is silent.
Trust controls¶
Raw content assumes the author of a page is a party you trust to write HTML into your output. Two independent controls apply when that is not true.
Disabling the directive — set raw_enabled to false
(see Parsing):
[parse]
raw_enabled = false
Every .. raw::, and its MyST {raw} twin, then passes nothing through:
it raises a directive.raw_disabled warning
and renders a placeholder showing its own source, escaped, instead —
a framed block, or an inline chip where the directive defines a substitution
(so a |br|-style definition stays valid inside the paragraphs that use it).
The :file: form is not read at all, so no dependency on it is recorded.
This is the docutils setting of the same name (which Sphinx never changes),
and it applies wherever a document is processed —
ubc build, ubc check, and the preview alike.
It is meant for pipelines that build contributions you do not control,
such as an auto-deployed preview of an external pull request.
The raw role is gated by the same switch,
because docutils’ own check there reads
“raw (and derived) roles disabled”:
a raw-derived role then renders nothing
and raises role.raw_disabled, its own code,
so you can silence the directive’s warning and keep the role’s, or the reverse.
Beyond those two, the switch changes nothing.
HTML written directly in a Markdown document
(<div>…</div> in a paragraph, or an HTML block)
is neither a directive nor a role, so raw_enabled does not affect it —
this is the docutils setting’s own boundary, which Sphinx never widens.
Escaping in an untrusted workspace — the preview panel can render raw HTML
escaped rather than injected, showing the markup as text.
Unlike raw_enabled this is not project configuration
(a repository you do not trust also supplies its own ubproject.toml):
it is driven by the editor’s own trust state, per session,
and it covers both syntaxes —
the raw directive and Markdown’s native raw HTML.
Note
In VS Code this control is currently redundant: the ubCode extension does not declare support for untrusted workspaces, so VS Code disables the extension entirely in one — a stronger guarantee than escaping, with no preview at all. The escaping path exists in the language server, and the editor client already reports its trust state on every render, so the two stay in step if that posture is ever relaxed.
Conditional content¶
.. only:: evaluates its tag expression
(and / or / not, parentheses, and the a if b else c form)
against the build’s tags: html, format_html and builder_html,
plus everything in the top-level build_tags key
(see Configuring a project with ubproject.toml).
A truthy expression includes the body transparently;
a falsy one excludes it from the built output entirely,
while the preview shows it as a collapsed, greyed block so you can see it exists.
An expression that cannot be parsed raises only.invalid_expression
and the body is kept — the same fail-open direction a Sphinx build takes
(it evaluates only in a post-transform that keeps the children on any
error), so a typo can never silently delete a page’s content.
.. if:: (sphinx-needs) works the same way against
variant data instead of build tags.
Section headings are allowed in the body of either directive, as they are in a Sphinx build.
In reStructuredText they behave exactly as upstream:
the body’s adornment styles are levelled from scratch,
so the first style used inside the body is its level 1
whatever that style means outside.
only additionally follows Sphinx in raising the block:
if the body’s first style is one the document already uses,
the whole only is re-parented to the level that style implies,
so the section becomes a sibling of the surrounding one rather than a child.
if does no such raise — sphinx-needs does not either —
so a heading there always stays where it is written.
Headings inside any other directive body
(note, container, a list item, a table cell, …)
remain invalid and are reported as block.title_disallowed,
matching docutils.
In Markdown a heading is a token rather than an adornment,
so the levelling has no counterpart and two things differ.
A heading at the top level of a {only} or {if} body
opens a real section, and it stays inside the enclosing one —
myst-parser raises it to the absolute level its # count implies,
which reorders the surrounding content; ubCode does not.
The body keeps its own relative nesting,
so ## followed by ### is still a parent and a child.
And a heading in any other container —
{note}, {card}, a block quote, a list item, a need body —
is not an error at all: it renders as a rubric,
an informal heading that keeps the heading’s anchor
but does not open a section and does not appear in the page contents.
That is myst-parser’s own rule.
Plain CommonMark and GFM documents are unaffected —
they have no rubric, and a heading nested in a block quote or list item
stays an ordinary heading.
Warning
Content inside an excluded only block is not indexed.
A Sphinx build parses the body at read time and removes it only when writing,
so a :ref: into a target defined inside a falsy only still resolves there —
at an anchor that is not on the page.
ubCode decides earlier and discards the body entirely,
so targets, sections, needs and glossary terms written inside an excluded block
are not registered, and an inbound reference to one renders as an
unresolved reference chip.
That reference is silent by design: ubCode remembers the names an excluded
body would have defined and suppresses the std.ref / needs.ref warning
for exactly those names, rather than reporting a target the author did write.
A reference to a name that is nowhere in the project still warns as usual.
Define anything you reference from elsewhere outside the only block.
An only block is an element of its own to what surrounds it, where an if is not.
A content-less rst-class written just before an only block reports
directive.class_unsupported_target and the class is dropped;
a Sphinx build puts the class on the first element inside a true only
and drops it silently before a false one.
A tab-set that holds an only block warns at the set
(“All children of a ‘tab-set’ should be a ‘tab-item’”), as a Sphinx build does,
and leaves out the tab-item directives inside it, which a Sphinx build shows when the only is true.
An if or a choose behaves as in Sphinx-Needs in both places:
the class lands on the first element it shows, or on the next element after one that shows nothing,
and its tabs show in the set, with nothing reported at the set.
Two exceptions remain for the class.
When the if or the branch taken opens with an rst-class of its own for that same first element,
both classes land, but in the other order —
class="inner outer" where a Sphinx-Needs build writes class="outer inner" —
a recorded difference.
And when it opens with an .. include::, the class is refused,
as it is before an .. include:: anywhere (see CSS classes).
Two files, one document¶
A document’s name comes from its path with the suffix removed,
so guide.rst and guide.md both claim the document guide.
Only one of them can be that document,
and ubCode picks the same one a Sphinx build picks:
the first in path order — here guide.md, because m sorts before r.
(It is path order, not the order your parsers are declared in.)
The other file is then not a document at all.
It is not parsed as one, and it contributes nothing:
no page, no :ref: targets, no glossary terms, no needs, no toctree entries.
A :ref: to a label written in it is reported as an undefined label,
exactly as in a Sphinx build,
rather than linking an anchor the served page does not have.
The file is still a file, though —
a third document that includes it with .. include:: still splices its content.
Only “is this a document of its own” changes.
Both engines warn.
ubCode reports std.duplicate_docname against the ignored file,
naming every claimant and the one it is using,
with the winning file as a second location you can jump to.
One difference is deliberate:
a Sphinx build’s multiple files found for the document warning carries no
warning type, so suppress_warnings cannot reach it,
while ubCode’s is an ordinary code — lint.ignore and
lint.per-file-ignores both silence it.
Rename one of the two files if you meant to keep both.
Components and markup¶
sphinx-design components¶
The :octicon: role renders natively —
in the Rendered preview, in ubc build html, and validated by ubc check.
Write :octicon:`name`, :octicon:`name;height` or
:octicon:`name;height;classes`, as in a sphinx-design build;
it emits the same inline <svg class="sd-octicon sd-octicon-{name} …">,
so a stylesheet written against those classes keeps working.
Only a subset of the GitHub octicon set is embedded. The full set is 328 icons, an unreasonable weight to carry for a decorative role. A name outside the embedded subset raises
directive.octicon_unknownand the role keeps its self-describing chip, so your source stays visible.A card / dropdown / tab / button title is parsed as inline content (an
:octicon:role in agrid-item-cardtitle is the common shape), matching sphinx-design, which runs the same inline parser on it. A title that looks like a block (.. card:: - dash title *em*) keeps its dash literal and renders its emphasis, rather than becoming a list inside the title element.A height that is not a number followed by
px,emorremraisesdirective.octicon_invalid_height, and a class name containing anything but letters, digits,_and-raisesdirective.octicon_invalid_class— the rendered SVG is emitted verbatim, so an unsafe class name is refused rather than escaped. Both keep the chip.
The sphinx-design directives render natively too —
grid / grid-item / grid-item-card, card (with its
card-header / card-footer sub-directives), dropdown, div,
tab-set / tab-item, and button-link / button-ref.
They emit the same sd-* classes and element nesting sphinx-design does,
so a stylesheet written against those classes keeps working.
ubc build html ships its own minimal design stylesheet
(_static/ubc-design.css) covering the 12-column responsive grid,
the card, dropdown, tab and button chrome, and the spacing, sizing, display,
border and text utility families.
A site with no sphinx-design markup neither links nor writes it.
Its colours follow your project’s theme:
the accent, and a card’s border, text and header/footer colours,
are defined in terms of the --ubc-* custom properties
(see Theming with extra_css),
so a project that sets --ubc-brand gets branded buttons,
and cards that follow your surface, border and text tokens,
in light and dark alike, with no --sd-* override of its own.
A resting card is not brand-coloured — --ubc-brand reaches it
only through the hover border of a sd-card-hover card —
and a card’s background is left transparent, so it shows the page.
Setting a --sd-* property yourself still works — extra_css is loaded last.
The live preview loads the same stylesheet: the language server inlines it into any preview that contains sphinx-design markup, with those tokens mapped onto your editor’s theme colours, so a grid lays out in columns there too.
What differs from a sphinx-design build:
The accent colour is your project’s, not sphinx-design’s. Components take
--ubc-brand—#2563c9in a project that sets no theme of its own — where a stock sphinx-design build shows#0071bc, and they follow the light/dark palette, which a stock build does not.A card’s header and footer carry the recessed surface colour (
--ubc-bg-soft), where a stock sphinx-design build leaves them transparent. This is a parity change rather than a departure: the Sphinx theme these pages are modelled on colours the same marginals#f8f9fbin light and#1e2124in dark, against ubCode’s#f2f4f7and#1e2023. A card’s border, by contrast, is deliberately more visible than that theme’s (see the note above about backgrounds).A card’s header and footer come from sub-directives. Write
.. card-header::and.. card-footer::inside acardorgrid-item-card; they are hoisted into their slots wherever you put them. The legacy^^^header separator is not supported — usecard-header. Acard-headerorcard-footeroutside a card raises a warning and renders its content in place.The legacy
+++footer separator is honoured, but write exactly three. ubCode splits the parsed body where sphinx-design splits the raw lines, which cuts both ways. Better: a+++inside a code block, a literal block or a nested card is not treated as a separator here, where sphinx-design’s line scan claims it. Worse: a run of four or more+is a transition character in reStructuredText, so the parser claims it first — a standalone++++still splits, but a++++written directly under a line of text is read as a section underline and that line is dropped. That shape raisesdirective.card_separator_invalidtelling you to write+++or usecard-footer.A
grid-itemorgrid-item-cardoutside a grid raises a warning (as it does in a sphinx-design build) and still renders, as does agridchild that is neither.A dropdown’s default icon is not read from
conf.py. sphinx-design lets a project set per-directive option defaults through thesd_custom_directivesconfiguration value, which ubCode has no equivalent for, so dropdowns relying on it render without their configured icon. A:icon:naming an icon outside the embedded subset raisesdirective.octicon_unknownand the dropdown renders icon-less.A card or dropdown title is parsed as reStructuredText, on both host formats — so an
:octicon:role in agrid-item-cardtitle renders as an icon. Abutton-linkwith no content falls back to showing its URL verbatim; abutton-refwith no content shows the resolved title of its target, as in a sphinx-design build.A button’s content is inline-only. It becomes the link text, and a link may only contain inline markup, so a single wrapping paragraph is unwrapped. Block content (a list, an admonition, a nested directive) produces invalid nesting — sphinx-design has the same limitation, since it inline-parses the content lines. Keep button content to one line of inline markup.
A link’s text has no wrapping
<span>. sphinx-design wraps a button’s and a card link’s text in one because docutils requires a reference to hold an element child; no stylesheet rule targets it, so ubCode emits the text directly.A component container carries no trailing
docutilsclass. docutils’ writer appends that token to every container it writes; no sphinx-design selector uses it, so it is not reproduced. For the same reason thesd-card-textclass is not stamped onto a card’s or dropdown’s body paragraphs — the ubCode stylesheet reproduces its one effect with a.sd-card-body > p:last-childrule instead.``:img-background:`` is ignored (with a warning), and
card-carousel,article-info,tab-set-codeand the badge roles are not implemented — they still show the generic placeholder.
Tabs¶
tab-set and tab-item render as a real tab strip,
and switching tabs needs no JavaScript.
Each tab is a hidden radio button with a visible label,
which is how sphinx-design does it too:
the browser’s own radio-group behaviour does the switching,
so tabs work with scripting disabled, and in the live preview.
.. tab-set::
.. tab-item:: Linux
:sync: nix
Install with ``apt``.
.. tab-item:: Windows
:sync: win
:selected:
Install with ``winget``.
:selected: chooses the tab shown first — the first item if you set none,
and a second :selected: in one set warns and loses.
:sync: gives a tab a key: every tab on the page sharing that key
(within the set’s :sync-group:, tab unless you name one)
selects together, so a reader who picks Linux once sees it everywhere.
:name: puts an anchor on the tab’s label, so #your-name targets it.
:class-label: and :class-content: add classes to the tab’s label
and to its content panel.
:class-container: adds classes to a wrapper that a tab set dissolves,
so inside a set it reaches nothing in the HTML —
exactly as in a sphinx-design build.
It has an effect only on a tab-item written outside a set (see below).
:sync:, along with two related conveniences,
comes from a small script (_static/ubc-tabs.js)
that ubc build html links only on pages that contain a tab set:
tabs sharing a sync key switch together;
a link to an anchor inside a closed tab opens that tab — and every tab enclosing it, for nested sets — before scrolling to it;
the reader’s choice per sync group is remembered in browser storage, so it survives the next page and the next visit. The storage keys are prefixed with
ubc-tab-id-; build.html.sd_tabs_storage_prefix changes that prefix, and setting it to an empty string turns the remembering off entirely.
What differs from a sphinx-design build:
The storage-key prefix differs by default. sphinx-design uses
sphinx-design-tab-id-; ubCode usesubc-tab-id-so that two builds served from one domain do not overwrite each other’s remembered choices. Set build.html.sd_tabs_storage_prefix tosphinx-design-tab-id-if you want them shared.Hovering an unselected tab shows a muted form of your brand colour; a sphinx-design build shows the full selected-tab colour. Upstream gives the hover and selected states the same value, so hovering makes a tab look selected and the set briefly reads as having two selections. Here hover is softened, and it follows your theme: a project that sets
--ubc-brandgets a muted version of it, in light and dark alike. Proposed upstream as sphinx-design#301; if that lands, the two converge again.A tab label is parsed as reStructuredText on both host formats, as a card or dropdown title is.
A ``tab-item`` outside a ``tab-set`` warns and renders its label as a rubric above its content — the same shape sphinx-design produces for non-HTML output, and the one place
:class-container:lands. Its content is visible here; a sphinx-design build emits the same markup but hides it, because its stylesheet has no rule for a set-less item. We show it: the directive already warns, and losing the text you wrote is the worse outcome. Atab-setchild that is not atab-itemwarns and is dropped, except for a hyperlink target, which is kept so references to it still resolve. Both raisedirective.tab_structure_invalid(one warning per set, where sphinx-design warns once per stray child).An invalid option *value* warns instead of dropping your tab. A
:sync:or:sync-group:written with nothing after it, or a:selected:given a value (it is a flag and takes none), raisesdirective.design_invalid_optionand the tab still renders — unsynced, in the defaulttabgroup, or selected, respectively. A sphinx-design build treats each as a hard error and drops the whole tab item (or the whole directive). Deleting what you wrote over a one-character slip is the worse trade, especially in the live preview, which re-renders as you type.In the live preview the tabs switch, but the script’s three additions do not run. The preview renders inside the editor’s own webview, whose content-security policy admits no script the editor did not sign — so
:sync:, anchor-opening and the remembered choice areubc build htmlonly.``tab-set-code`` is not implemented and shows the generic placeholder.
Tables¶
.. tabularcolumns:: is a LaTeX-only marker and correctly renders nothing,
matching an HTML sphinx-build.
A table title (the directive argument on table, list-table and
csv-table) is parsed as inline content, so markup and references in it
render and resolve — matching a Sphinx build, whose make_title runs the
same inline parser.
That includes a title that looks like a block
(.. list-table:: - dash title *em*): the dash stays literal, because an
inline parse knows nothing about bullets, and the emphasis renders.
:widths: on table, list-table and csv-table takes
one positive whole number per column
— the same relative shares, read by the same code, as a needtable’s
:colwidths: below.
A value ubCode cannot apply now raises a warning and renders the table without
column widths, where before it was silently ignored:
an entry that is not a positive whole number
(including 0 and a negative, which Docutils rejects too),
one above 18446744073709551615,
a grid keyword on list-table or csv-table
(only table accepts it),
or a count that does not match the columns.
A Sphinx build makes every one of those a directive error that replaces the
whole table, so a table that used to vanish now renders with a warning.
Two details are worth knowing because they are easy to trip over and both match
Docutils exactly.
The separator is comma or whitespace, never both in one value:
if the value contains a comma it is split on commas only,
so :widths: 1, 2 3 is two entries and the second one (2 3) is not a
number.
And the keywords are matched exactly, so AUTO is not auto.
One divergence runs the other way, on table only.
:widths: grid asks for the widths the source grid table itself declares,
which ubCode does not carry through its document model,
so it renders no column group where a Sphinx build sizes the columns from the
=== rulers.
Give explicit numbers if you need those widths.
:width: (singular, the whole table’s width) is now validated where it
used to be passed through untouched.
It takes a number, optionally followed by em, ex, px, in,
cm, mm, pt, pc or % — Docutils’ list, matched
case-sensitively, so 3PT is not 3pt.
Anything else raises a warning and the table renders without a width:
auto, 100vw, a negative value,
or a value with anything after the unit such as 75%; position: fixed.
A space between the number and the unit is allowed and removed,
so :width: 3 pt renders width: 3pt — this one is a fix,
because width: 3 pt is not valid CSS and a browser discards it.
Code blocks¶
code-block (with its aliases sourcecode and code), :: literal blocks,
literalinclude and .. highlight:: render with syntax highlighting,
with a few differences from a Sphinx build:
ubc build htmlandubc build pdfboth highlight during the build, from a bundled collection of Sublime Text syntax definitions rather than from Pygments — about two hundred and twenty languages, plus ubCode’s ownconsole,pycon,cypherandmystgrammars, and nothing to install. The two agree on everything: the same engine, the same language table (see Languages) and the same names for the colours, so a construct gets the same class on the site and on paper. Colours differ from your Sphinx theme, and token boundaries differ from a Pygments lexer’s. A language name nothing answers to is reported by both (build.highlight_language_unknown), and Sphinx’sdefaultis drawn as Python — see Code blocks are syntax highlighted and its paper twin.The VS Code preview colours with the same engine, during the same render: the language server highlights the document and the preview draws the result, so a construct is coloured the same way in the editor, on the site and on paper. A language it does not know is drawn plain (never mangled), and the preview reports nothing about it — run a build to see the finding.
One listing is coloured more than a Sphinx build colours it, in every surface including the editor: a
rstorrestructuredtextblock. ubCode’s own reStructuredText grammar paints about 66 % of a listing’s non-whitespace characters (rst) and about 59 % (restructuredtext), measured over this project’s own documentation, where Pygments’ own reStructuredText lexer paints about 33 %. The extra is mostly two things Sphinx leaves plain: a directive’s ARGUMENT, and a directive OPTION’s VALUE — a quarter of the characters of this project’s own Sphinx-Needs listings, which no other highlighter colours at all. The body of a.. code-block::is also lexed in the language its argument names, which Sphinx’s own reStructuredText lexer (Pygments) does only when the directive carries no options. This is a deliberate over-paint: the number to match was the colour the editor’s own preview used to show, not the colour a Sphinx page shows.Some reStructuredText constructs read as a different KIND of thing than they did before ubCode’s grammar replaced the bundled one. A field or option marker such as
:id:reads as an attribute rather than as a tag name; a footnote label, a hyperlink target, a substitution and a simple reference all read as built-ins; a doctest>>>reads as a prompt, the same way it does in apyconlisting. Nothing is mangled, and — measured over this project’s own documentation, 48 characters against 6,054 the other way — nothing that the bundled grammar coloured is now plain except two constructs it read as something else: a substitution definition, which it could not parse and took for verbatim text, and a citation’s body, which its footnote rule took for a footnote’s text.A section title’s TEXT is coloured only where the highlighter can see that it is one, which means from an OVERLINE that begins a block: the highlighter reads one line at a time, so the text above a bare underline stays plain, and so does an over-and-underlined title placed straight after a directive, a comment or a literal block, because the block before it runs through the blank line. The adornment lines themselves are always coloured. This is the one place the colouring is thinner than the editor’s previous highlighter, and it is six characters of this project’s documentation.
:linenos:,:lineno-start:,:number-lines:and:emphasize-lines:are applied, as isliteralinclude’s:lineno-match:.:lineno-start:,:number-lines:and:lineno-match:each imply:linenos:, exactly as in a Sphinx build, and:emphasize-lines:counts the lines displayed, from 1, whatever numbers those lines carry.The line numbers are drawn beside the code rather than inside it, as CSS generated content. A Sphinx build interleaves them with the Pygments token spans as real text; ubCode keeps them outside the
<code>element so that the copy button copies the code and nothing else, and so that your browser’s find-in-page searches the code and not the gutter. Three consequences, all of them deliberate: the numbers can never be copied, selected or dragged into your clipboard (a Sphinx build shipsuser-select: nonefor the same reason, and can go no further because its numbers are text); they are hidden from screen readers, which read a Sphinx build’s aloud; and your browser’s find-in-page will not find a line number.:dedent:is applied on a code block as well as onliteralinclude. reStructuredText already removes a directive’s own content indentation, so:dedent:only has something to remove when the block’s body is indented further than the directive’s options are — which is exactly the case a Sphinx build applies it to. A Markdown fence keeps its body verbatim, so it dedents as written.A
:dedent:value that is not a whole number of columns (:dedent: two,:dedent: -1, and Python’s:dedent: 1_0) is reported and the block renders with its indentation unchanged. A Sphinx build fails the directive, so the code disappears from the page. This is the one option here that changes the TEXT you are shown, which is why it is reported rather than applied quietly.:force:is not applicable, rather than pending. It exists upstream to silence a Pygments lexer-failure warning; ubCode’s engine reports no lexing failure at all — a grammar that cannot make sense of a line simply leaves it uncoloured — so there is nothing for the option to suppress. It changes no output byte in a Sphinx build either... highlight::’s:linenothreshold: Nis applied: every literal block below it that is long enough is numbered from 1, unless the block asks for numbering itself. The threshold counts the lines you are shown — for aliteralincludethat is the selected slice, not the whole file. At:linenothreshold: 5a five-line code block is numbered and a four-line one is not, while aliteralinclude, an.. include::with:literal:or a plain Markdown fence is numbered one line earlier — from four (a{code-block}fence is not: it behaves as a code block). That one-line difference is what a Sphinx build does too: its test counts line endings rather than lines, and the trailing newline those sources carry reaches it one line sooner. (An included file whose last line has no trailing newline is numbered from five, like a code block.) An explicit language argument does not exempt a block.The threshold is how the blocks that have no options at all get line numbers: a plain
::literal block, a quoted literal block, a Markdown fence, an.. include::with:literal:. Nothing else can number those.Every
.. highlight::sets the threshold, so a later one without:linenothreshold:clears it — the same reset a Sphinx build performs. A value that is not a positive whole number is reported, and the threshold already in force keeps applying; a Sphinx build fails the directive there.A
parsed-literalis not numbered by a threshold, where a Sphinx build numbers it. Its body renders as parsed markup rather than as source lines, so there are no lines for the numbers to belong to.docutils’
.. include::also carries a:number-lines:, which is reported as unsupported wherever it is used; that one is still outstanding.A line-number option a Sphinx build rejects is reported and the block still renders, without that one decoration: an unparsable
:emphasize-lines:/:lineno-start:/:number-lines:, and:lineno-match:over a disjoint:lines:selection. A Sphinx build replaces the whole block with an error, so the code disappears from the page; a mistyped decoration should not cost you the code. An:emphasize-lines:naming lines past the end of the block behaves exactly as upstream: it warns, and the lines that do exist are emphasised... code::accepts:linenos:and:caption:here. A Sphinx build patches docutils’codedirective but keeps its narrower option set and errors on both.:caption:renders above the block, with its inline markup parsed — including references, which resolve and warn when dangling. This applies to Markdown as well as reStructuredText, each with its own grammar (see Markdown documents). A dangling reference in a caption is reported at the directive line; a Sphinx build reports it at the first line of the block’s content — three lines further down for a one-option block with a blank line after it, more with more options, and past the directive entirely when aliteralincludehas no content. Every other inline slot anchors at its host directive, which is where a Sphinx build reports it — including a version marker’s trailing text. The exception in the other direction is aline-blockdirective’s lines, where a Sphinx build’s own anchor is unusable (it reports an unrelated line, or none at all) and ours is the directive line.A caption that looks like a block rather than a phrase (
:caption: - dash caption,:caption: 1. numbered *em*) renders as literal text, markup and all. A Sphinx build keeps the dash literal but still parses the markup — inside a<li>with no surrounding list, which no browser renders as written. There is no valid output to match: emitting the list ourselves would put a<ul>inside the caption’s<p>, invalid in the other direction. The same applies to a wrapped caption whose continuation line is indented, which is read as a definition list. Unlike table and card titles, which are at parity here, this one is a deliberate divergence.An argument-less
code-block(or a plain::block) with no.. highlight::language and no configuredhighlight_languagestays unhighlighted; a Sphinx build falls back to highlighting it as Python (itsdefault).
Custom CSS targeting .hljs-* no longer applies
Until 0.35.x, ubc build html coloured code in the reader’s browser, and
the spans it coloured carried highlight.js class names (.hljs-keyword,
.hljs-string, and the rest).
The colouring now happens during the build, and a span carries one hl-
class instead — hl-keyword, hl-type, hl-function, hl-name,
hl-built-in, hl-label, hl-attr, hl-variable, hl-number,
hl-literal, hl-operator, hl-string, hl-escape,
hl-comment, hl-section, hl-bullet, hl-link,
hl-addition or hl-deletion, plus hl-strong and hl-emphasis
for a bold or italic run that takes no colour of its own.
A stylesheet in extra_css that targets the old
names selects nothing and fails silently, which is why this note exists
rather than a warning.
If you recoloured tokens by redeclaring the --ubc-hl-* custom properties,
the values are unchanged and they are still what ubc build pdf paints
from — but there are twenty-one of them now rather than thirteen, and a
property whose reach narrowed no longer paints everything it used to.
See the
stable-surface note for which ones moved.
The data-ubc-highlighted attribute the browser used to stamp on each
element is gone with the client highlighter, as is the hljs class the old
palette hung on.
Highlighted inline code — a role derived from :code: with a
:language: option — follows the same rules as the blocks above,
and ubc build html, ubc build pdf and the VS Code preview all colour it,
with the same engine they run over a block.
It is not a reStructuredText construct only:
in Markdown, a role derived from code with a {role} directive
works the same way,
and with attrs_inline a code span takes its language from an {l=…},
{language=…} or {lexer=…} attribute.
It has four differences of its own:
On paper, a diff token keeps its colour and not its tinted row.
ubc build pdfdraws each token in its colour inside the chip’s own background, and an inserted or deleted token of adiffinhl-addition-fgorhl-deletion-fgalone: thehl-addition-bg/hl-deletion-bgground a code block paints behind the row would break the chip’s background at the token.ubc build htmlpaints that ground inside the chip.A Sphinx build writes Pygments token spans into the HTML (
<span class="mi">1</span>) and ships apygments.cssto colour them, and standalone docutils writes them too but with its own longer class names (<span class="literal number integer">) — the two upstreams already disagree here. ubCode writes its own nineteen-class vocabulary instead —hl-keyword,hl-type,hl-function,hl-name,hl-built-in,hl-label,hl-attr,hl-variable,hl-number,hl-literal,hl-operator,hl-string,hl-escape,hl-comment,hl-section,hl-bullet,hl-link,hl-addition,hl-deletion, plushl-strongandhl-emphasisfor bold and italic runs — and colours it from the twenty-one--ubc-hl-*custom properties, so apygments.cssfile or apygments_stylesetting has no effect here. Redeclare those properties in an extra_css file instead; on paper the same roles are set under[build.pdf.theme.colors].The derived role’s own name and any
:class:tokens are dropped; both upstreams keep them alongside the language. (This is the same limitation custom classes have on other derived roles.).. role:: python(code)with no:language:renders plain inline code: the role’s name is never used as the language. This matches Sphinx. Current docutils development builds do fall back to the role name.
An unrecognised language is reported as build.highlight_language_unknown,
naming the token, counting the places it is written and offering the nearest
name the build does know; the code is drawn in full, without colours.
Both builders report it on a block and on an inline :code: role,
counted together under the one name, a chip in a section title once.
A Sphinx build logs a highlighting_failure warning and falls back to
unhighlighted output, which is the same behaviour;
standalone docutils also reports a warning,
then renders the role’s source text as a problematic node in place of the span.
The VS Code preview resolves the same names from the same table,
so it colours exactly what ubc build html colours;
it draws an unrecognised language plain without a finding of its own,
because the build is where the finding is reported.
A plain :code: role with no language is byte-identical to a docutils build
(a bare <code> element, no classes);
a Sphinx build adds its own code docutils literal notranslate classes
and wraps each word in a <span class="pre">.
Further differences apply to .. role:: definitions generally,
not only to :code::
Options must follow the signature without a blank line. docutils re-reads a role definition’s options out of its own content, skipping a leading blank line as it goes, so a
:class:or:language:separated from the signature by a blank line still applies there. ubCode parses the option block the way it does for every other directive, which ends at the first blank line, so those lines are content and the option is not applied. Write the options directly under the signature.A rejected body does not decline the definition. No docutils role accepts content, so a role definition with a body raises
Error in "role" directive: no content permittedupstream and the role is left undefined. ubCode still defines it, so later uses of the role render rather than being flagged a second time.A signature continued on the next line is joined into the signature. ubCode treats the signature as the directive argument, and a directive argument folds in its continuation lines, so
.. role:: customfollowed by an indented(code)definescustomwith thecodebase — silently. docutils declares no argument forroleat all and reads the signature back out of its own content, so a continuation line is content there: it raisesError in "role" directive: no content permittedand the role is left undefined. Keep the whole signature on one line.A valueless
:class:or:language:is ignored, not an error.:class:with no value falls back to the role name. docutils passes the missing value to its option parser, which raisesargument required but none suppliedand leaves the role undefined. The same best-effort stance applies to a repeated option, where ubCode keeps the first.A cycle between role definitions renders a placeholder chip. Redefining a role in terms of one that already inherits from it —
.. role:: a(emphasis),.. role:: b(a), then.. role:: a(b)— leaves a cycle, because ubcode resolves a base by name at the point of use. Such a chain is followed for at most 32 hops and then renders as a placeholder chip. Both upstreams bind the base role object when the definition is read, so the redefinition is harmless there and the role still renders (docutils renders:b:as the inherited emphasis).An option the base role does not recognise is accepted and ignored. Both upstreams validate a role definition’s options against its base role, report an error at the definition, and then do not define the role at all — so every later use of that role errors too, as an unknown interpreted text role. ubcode records the option, defines the role, and the option simply never takes effect.
:language:is only meaningful on acodebase and:format:only on arawbase (each does its real work there — see Raw content for:format:, which is the whole mechanism of the inlinerawrole). On any other base, docutils reportsunknown option: "language"/unknown option: "format"and myst-parser reportsUnknown option keys: ['format'] (allowed: ['class']), where ubCode accepts the definition and ignores the option.
Both differences apply in a MyST fence as well,
and both are now reported there:
role accepts no content,
so anything that lands in the fence body is flagged
directive.content_not_permitted (Linting),
the Markdown twin of reStructuredText’s block.directive_content_not_permitted.
What differs between the two syntaxes is only the outcome.
A blank line still ends the option block,
so an option written after one becomes body content and is dropped —
here :class: red is reported but never applies:
```{role} custom
:class: red
```
A body written below the options is reported and dropped in the same way,
and the :class: above it still applies:
```{role} custom
:class: applied
this body is reported and dropped
```
In both examples ubCode reports the rejected body and keeps the role
defined, so later uses of :custom: still render.
myst-parser refuses both outright —
Error in "role" directive — and leaves the role undefined,
so every later use warns Unknown interpreted text role.
That is the real difference; the report itself is new in Markdown,
and the dropped option is unchanged.
One case is still not reported, alongside the blank-line option above.
When a directive declares no argument at all
(todolist, target-notes, sectnum and the like),
text written on the fence line becomes the directive’s argument in ubCode
and is silently discarded,
where myst-parser folds that line into the body and so reports it:
```{todolist} inline text on the fence line
```
The same todolist written with the text as a real body is reported by
both. Only the fence-line route is silent.
One further difference is MyST-only: the signature must be the directive argument.
```{role} custom(code)
:language: python
```
The docutils-style form that puts the signature in the fence body is reported as a missing argument and defines no role, where myst-parser accepts it by handing the fence body straight to the docutils implementation.
Images¶
A local image whose file lives outside your workspace folder does not load in the preview; the preview can only read files within the workspace.
Remote images referenced by an
https://URL load directly.An image written inside a substitution definition is accepted. A Sphinx build rejects the construct outright when the image is named (
ERROR: Substitution definition contains illegal element <image>): docutils refuses a named element inside a substitution definition. An unnamed substitution image is fine in both engines. ubCode accepts either silently, which is the difference worth knowing — and its:name:does render as an anchor, so:ref:to it, and the entry ubCode publishes inobjects.inv, reach the image on the page.
:width:, :height: and a figure’s :figwidth: are validated
where they used to be split apart and passed through.
Each takes a number, optionally followed by
em, ex, px, in, cm, mm, pt or pc —
Docutils’ list, matched case-sensitively, so 3PT is not 3pt.
:width: and :figwidth: also take %; :height: does not.
That asymmetry is Docutils’, not ubCode’s:
its image directive reads the height with length_or_unitless
and the width with length_or_percentage_or_unitless,
so :height: 50% is a directive error in a Sphinx build
while :width: 50% is a length.
Anything the grammar refuses raises a warning
and the image renders without that dimension —
auto, 100vw, 10rem, a negative value,
or a value with anything after the unit such as 75%; position: fixed.
A Sphinx build refuses the same values and drops the whole image:
the directive becomes an error node, which Sphinx’s default
keep_warnings = False then strips from the page,
so the picture simply is not there and the reason is only in the build log.
(Bare Docutils leaves the error visible in the output.)
ubCode keeps the picture and drops the size.
The last of those is the one worth knowing:
before this, everything after the unit was carried into the style
attribute, so :width: 75%; position: fixed really did add a
position: fixed declaration to the image.
Raw <img> tags, with the MyST html_image extension enabled
(see Parsing), behave like an {image} directive with the same
options — so everything on this page about images applies to them too.
Four differences are specific to the tag form:
A missing or empty ``src`` leaves the markup on the page. ubCode warns (
md.html_image_no_src) and renders the HTML block exactly as written — every tag in it, whether or not that one was the problem. A Sphinx build raises an error and deletes the markup: the tag is replaced by the error message, so nothing image-shaped survives. ubCode’s rule is that a diagnostic never comes at the cost of your content.A valueless attribute is empty, not the word “None”.
<img src="a.png" alt>rendersalt="", which is what HTML says a valueless attribute means. A Sphinx build renders the literal textalt="None"there.A newline inside an attribute value stays inside it.
<img src="a.png" alt="x⏎:width: 99">keeps both lines as the alt text. A Sphinx build re-serialises the attributes as directive option lines before parsing them, so the second line becomes a real:width:option — the alt text silently resizes the image.A stray end tag leaves the block raw.
<img src="a.png">followed by</div>in the same HTML block renders as written here. A Sphinx build discards the</div>and converts the image, so markup you wrote disappears.
Two more follow from the tag behaving like the directive, and are the same
differences the directive has:
a src containing a space is kept as written
(a Sphinx build deletes the space, so a b.png names ab.png),
and an align value outside the allowed set warns and is still applied
as a CSS class (a Sphinx build warns and drops it).
Character references in an attribute value are decoded —
src="a&b.png" names a&b.png — but the ; is required.
src="a&b.png" names a file spelled exactly that way here, where
a Sphinx build decodes the legacy no-semicolon spelling too; the same rule
means a¬b.png stays itself rather than becoming a¬b.png.
A figure’s :figwidth: image is accepted and sizes nothing.
Docutils answers it by reading the image file’s intrinsic width,
which ubCode does not do while indexing —
and neither does Docutils itself without Pillow installed,
where it also emits no width.
A space between the number and the unit is allowed and removed
(:width: 3 pt is 3pt), as in a Sphinx build —
the value is normalised once, when the document is read,
so both the preview and ubc build html get the corrected form.
The number itself reaches the page exactly as you wrote it —
1.50cm stays 1.50cm and .5em stays .5em, as in a Sphinx build.
One consequence of that faithfulness: a width with no unit also reaches the
page as written, and width: 300 is not a length any browser applies,
where a Sphinx build appends the px
(#2827).
(ubCode’s Docutils-conformance renderer, which is not the mode described here,
re-formats them to 1.5cm and 0.5em.)
:scale: is the exception, and it is a layout difference rather than a
spelling one.
A Sphinx build multiplies the size and bakes the product into the length,
so :width: 10px with :scale: 66 becomes width: 6.6px
(written out in full as 6.6000000000000005px).
ubCode keeps the authored width and applies a CSS transform,
which scales what you see without changing the box the page lays out around —
so text flows around a 10px image here and a 6.6px one there.
One gap worth knowing: a .. figure:: :figwidth:
currently sets no width at all in the preview or in ubc build html
(#2826);
it is honoured only by that conformance renderer.
A * wildcard extension (.. image:: logo.*) selects the best available file,
exactly as a Sphinx html build does:
the pattern is globbed, candidates are classified by file extension,
the html priority svg > png > gif > jpeg picks the winner,
and within one type the shortest file name wins.
webp is never selectable for html output — as in Sphinx.
ubc build html copies and references the selected file,
and hovering the image argument in the editor previews that same file.
If nothing matches — or only unsupported types match —
the build emits the pattern as written
(minus any ?query/#fragment, which every asset reference is stripped of)
with a warning,
and ubc check reports image.not_found on the directive
(a Sphinx build only warns at build time; the background index here
also re-checks the glob when files appear or disappear beside it).
The build warning comes from rendering the page,
but it is reported on every build:
a page’s render warnings are stored with its rendered body,
so a build that reuses the page re-reports them
and --show-warnings shows the same list warm as it does under --fresh.
ubc check reports the unmatched glob independently,
from the index, on every run.
A Sphinx build repeats neither
(its image post-processing runs during the write phase,
and an unchanged document is not written) —
one of the few places ubCode is deliberately noisier.
Two deliberate differences from a Sphinx build:
candidates are classified by file extension only; Sphinx additionally sniffs the bytes of files with unknown extensions, so a real JPEG named
photo.jpegis selectable there but not here — name itphoto.jpg;language-specific candidates (
figure_language_filename) are not searched. This is not limited to translated projects: Sphinx’slanguagedefaults to"en"andfigure_language_filenameto"{root}.{language}{ext}", so in a project with no internationalisation set up at all, a Sphinx build looking forlogo.*searcheslogo.en.*first and prefers alogo.en.svgsitting beside yourlogo.svg. ubCode has no i18n machinery, so it pickslogo.svg. Both files are in the candidate set either way — only the winner within one image type differs — so this bites you only if you keep<name>.en.<ext>files beside<name>.<ext>ones.
A wildcard extension is an image feature: candidates are ranked by image
type, which cannot choose a video file.
So a .. video:: source must name its file exactly.
A source written as clip.* is emitted as written
(minus any ?query/#fragment) and reported as
build.video_glob_unsupported; nothing is copied for it.
A :poster: is a real image and resolves normally, wildcard included.
Two things to know about that report:
it is only
<stem>.*that counts as a wildcard extension. Any other*is not glob syntax here and never was:.. video:: media/*.mp4is treated as the literal filename*.mp4, so the build tries to copy it, fails, and reportsbuild.image_read_failedinstead — with a_images/*.mp4reference that loads nothing. The same is true of an image: only the final.*triggers selection.build.video_glob_unsupportedcomes from rendering the page, andubc build htmlis its only surface:ubc checkdoes not report it (the background index does not resolve video sources at all). It is nonetheless reported on every build, warm or fresh — a page’s render warnings are stored with its rendered body, so reusing the page re-reports them.
Selection is pinned to the html builder’s priority list (other output formats would re-open the choice), and the Rendered preview does not yet resolve the wildcard — the image does not load there, while the hover preview and the build resolve it.
Downloads¶
Added in version 0.33.0.
The :download: role publishes a file for the reader to save.
A local target is copied into _downloads/<hash>/<name>,
where <hash> is derived from the file’s project-relative path —
the same scheme a Sphinx build uses,
so an href written by either engine points at the same place.
The file name is kept as it is:
two files with the same name in different directories get different
<hash> directories rather than a numbered name,
because the name is what the reader’s browser saves.
One file inside the source directory linked from many pages is copied once,
however each page spelled the path.
assets/data.txt from the root,
../assets/data.txt from a subdirectory,
and even ../<source-dir>/assets/data.txt
— which climbs out of the source directory and back in —
are one download, not several:
the path is normalised before it is hashed,
as a Sphinx build normalises it.
That normalisation is done by reading the path,
not by following the filesystem.
So there are two cases it does not cover.
If a .. climbs through a symbolic link,
ubCode and a Sphinx build can name that file differently
and publish it under different hashes;
and two genuinely different files can end up
sharing one published name,
in which case one of them is published
and a page linking the other gets those bytes.
Both directions of that case need a .. crossing a link to happen at all.
The other case needs no symbolic link:
a target that points outside the source directory altogether
is hashed as each page wrote it,
so one outside file referenced from pages at different depths
is published once per spelling,
where a Sphinx build publishes it once.
A target containing :// is treated as an external URL:
it is linked and never copied.
That test is narrower than the one images use —
a mailto: or //host/x target is read as a path here,
exactly as a Sphinx build reads it,
which means it names no file and degrades as below.
Three differences worth knowing:
A target that names no readable file loses its link, keeping its text. There is no placeholder and no empty link; the file name renders as inline code and nothing more. A Sphinx build does the same, but reports it only when the site is built — ubCode reports it while you type, as
download.not_readable, on the line the role is written. A separatebuild.download_read_failedcovers the narrower case of a file that disappears during a build, after the page linking it was written.The rendered element is
<code class="xref download">, where a Sphinx build addsdocutils literal notranslateand wraps the text in<span class="pre">. ubCode drops that markup for every inline literal, not just this one; a stylesheet keyed onxrefordownloadstill selects it.A
:download:inside imported need content is never published, even withneeds.import_allow_assetsswitched on — where an imported.. image::is. The reference is refused with a diagnostic naming the key, and renders as its own text. Imported content comes from outside your project, and a download hands whatever it names to the reader as a file to keep, so this surface starts closed; see What works inside imported need content.
A leading ! switches the reference off — :download:`!notes.txt` renders
the text alone — and whitespace inside a target collapses to single spaces,
both as in a Sphinx build.
In the preview, a download link resolves to the real file on disk,
and clicking it opens that file
with the application your operating system associates with it.
That is a deliberate difference from a built site,
where the download attribute makes the browser save the file instead:
in the editor the file is already on disk beside your sources,
so the preview opens it rather than saving another copy.
The link is never followed as a web address —
the file is opened directly from where it sits on disk,
before any _downloads/ copy exists.
Diagrams¶
Mermaid and PlantUML diagram directives render, with a few option differences:
:width:and:height:size the diagram container, in the preview and inubc build htmlalike (a bare number is treated as pixels). The same two options are available on theneedflowandneedsequenceviews above, where they are a pure ubCode extension.sphinxcontrib-mermaiddeclares neither option, so a Sphinx build does not merely ignore them — it raisesunknown option: "width"and drops the whole diagram.Any CSS unit is accepted here —
100vwand10remsize the container correctly — which is more than an image or figure accepts, and deliberately so: the diagram is a CSS-sized box rather than an<img>, and a PlantUML build reads these options with Docutils’ image grammar only because its output is an image. A value that is not a size at all (bogus,-5px,1.5.5px) now raises a warning and the diagram renders unsized; before, it was dropped in silence.A very large diagram no longer grows the page without limit. A rendered diagram is at most 60% of the viewport height and scrolls past that, in the preview and in
ubc build htmlalike. A diagram that already fits is unaffected, an explicit:height:wins over the bound, and printing releases it so the whole diagram is printed. The same bound applies to every rendered diagram, including theneedflowandneedsequenceviews. Sphinx has no equivalent default:sphinxcontrib-mermaidsizes every diagram from two global config values (mermaid_width,mermaid_height) instead.:alt:,:config:,:title:, and:zoom:are parsed but not applied.:scale:is applied only when a real PlantUML drew the diagram (External PlantUML renderer), where it becomes a percentage width on the diagram’s box. Without that it is parsed and not applied, because a diagram the browser has not drawn yet has no size to scale. A Sphinx build scales the rendered bitmap instead, so the two are visually similar rather than identical, and an explicit:width:wins over it here.:html_format:and:latex_format:are accepted and ignored, including when a real PlantUML draws the diagram. ubCode inlines the SVG into the page rather than linking an image:pngwould throw away the point of the pathway,svg_img(an<img>) kills the links inside the diagram, andsvg_obj(an<object>) resolves relative links against the SVG’s own URL rather than the page’s, breaking page-relative traceability links. There is no LaTeX builder for:latex_format:to serve. They are ignored rather than warned about, so a project that also builds with Sphinx is not warned on every diagram for an option that is correct there.:name:gives the diagram a stable cross-reference target, rendered as anidin a built site (and a¶in the caption, when the diagram has a:caption:). A name whose slug is empty — punctuation or emoji only, and, under[parse] id_style = "docutils", most non-Latin-script names and any purely numeric one — still anchors, on the page’s numberedid1/id2counter rather than on a readable slug. See Anchors on views and diagrams.Caption text is rendered as plain text; inline markup inside a caption is not parsed (unlike a
code-block:caption:, which is).A diagram supplied by file path (
.. uml:: flow.puml) renders the file’s content, resolved like an image path (a leading/against the source directory, anything else against the document’s own directory). PlantUML is converted to Mermaid, approximately; a diagram the conversion cannot express falls back to showing the directive source, andubc build htmlreports it (Unrendered directives, roles and diagram files). With a real PlantUML configured the file is drawn by the executable instead, exactly as an inline diagram is.A diagram copied onto a page by
needextractkeeps the Mermaid conversion, even with a real PlantUML configured. The copy is rendered from the source need’s stored content rather than from the page’s own captured diagram sites, so there is nothing for the external renderer to have drawn it from. The diagram on its own page is drawn normally.A local
!includebehaves differently once a real PlantUML draws your diagrams, and it is the one case that gets worse rather than better. The Mermaid conversion’s preprocessor skips an include it cannot resolve and draws the rest of the diagram; PlantUML, in the sandbox ubCode runs it under, fails the diagram — so it falls back to the Mermaid conversion with a warning, andubc build htmlfails on any warning by default. Stdlib includes (!include <C4/C4_Context>,!theme cerulean, sprites) are compiled into PlantUML and work under the sandbox. A Sphinx build resolves local includes, because it does not sandbox PlantUML at all.
CSS classes¶
The rst-class and cssclass directives attach CSS classes to your content.
Both are the same directive under two names, exactly as in Sphinx.
- Where the class lands
You write
.. rst-class:: highlightedwith no content, followed by a paragraph, a list, a table or a section heading.The preview shows the class on the element that FOLLOWS — and when the directive is the last thing in a section, on the NEXT SECTION, not on anything inside the one it was written in. Comments, hyperlink targets, substitution definitions,
.. index::entries and the state-changing directives (.. highlight::,.. role::,.. default-role::,.. default-domain::) are skipped on the way there.A Sphinx build does the same. A class written inside a directive body stays inside that body, attaching to the body’s own first block rather than escaping past it.
Status — parity for the attachment rule itself. What a class can then land ON is narrower than a Sphinx build’s — see “Where the class CANNOT land” below; that is the remaining gap in this section, and it is reported rather than silent. Two of its members are not element-vocabulary gaps but differences in what the walk MEETS: before a directive ubCode does not recognise, Docutils skips its own invisible error node where ubCode’s visible placeholder stops the class, and an
.. include::is textual upstream where ubCode’s wrapper is a boundary — both are listed below with the rest.- Where the class CANNOT land
You write a content-less
rst-classbefore an element ubCode cannot put a class attribute on — a transition (----), a footnote or citation definition, a field list, asphinx-designcomponent, an.. include::, or a directive ubCode does not recognise.The preview shows the element unchanged, and reports
directive.class_unsupported_targetat therst-classline.A Sphinx build renders the class on any of them: Docutils writes every element’s classes through one shared code path, so the question never arises upstream.
Status — a known gap, and a deliberately LOUD one. ubCode’s HTML writer reads classes per element type, so a class stored on an element whose rendering cannot express it would disappear with no sign at all — which is worse than being told. The elements a class CAN land on are paragraphs, sections, bullet / numbered / field / definition lists, list items, tables, admonitions, images and figures, literal and math and raw blocks, block quotes, containers, compounds, topics, sidebars, rubrics and line blocks. (Field lists and list items joined that set with the MyST attribute family in 0.35.0, which needed the same declaration — two spellings of “put a class on this element” must not disagree.) Widening that list is additive work; report the element you need.
Two of those shapes are worth calling out, because a Sphinx build does something you might not expect: before an unrecognised directive, Docutils skips the error node it puts there and the class reaches the next real element, so upstream classes the paragraph AFTER the unknown directive where ubCode reports and drops.
- With content
You write
.. rst-class:: framedwith a body.The preview shows the body’s blocks with the class on each of them, and no wrapper element around them.
A Sphinx build does the same.
Status — full parity. Only the body’s TOP-LEVEL blocks are classed; a paragraph nested inside a body block quote is not, which is also what a Sphinx build does. If any top-level block of the body is one of the unsupported elements above, the whole directive falls back to the generic placeholder (which still renders the body) rather than classing some blocks and not others.
- Classes on a section in the preview
You write an
rst-classthat lands on a section.The preview shows nothing different.
A Sphinx build, and
ubc build html, render<section class="…" id="…">.Status — inherited from a wider gap: the preview does not emit a
<section>element at all (it renders the heading and the content, without the wrapper), so there is nothing for the class to live on. The class IS applied inubc build html.- A class on a list
You write
.. rst-class:: on-a-listbefore a bullet list.The preview shows
<ul class="on-a-list">.A Sphinx build renders
<ul class="on-a-list simple">.Status — the missing
simpleis the general list gap already noted forhlistandglossary, not something specific to classes.- Invalid class names
You write
.. rst-class:: 123— a name that normalises to nothing.The preview shows the generic directive placeholder and reports
directive.class_invalid_argument.A Sphinx build reports an error and drops the directive.
Status — the same outcome, reported rather than errored. Class names are normalised the same way Sphinx normalises them: split on whitespace, lowercased, and punctuation folded to
-, so:class: Foo_Bar BAZbecomesclass="foo-bar baz".- Nothing to attach to
You write a content-less
rst-classwith nothing after it.The preview shows nothing, and reports
directive.class_no_target.A Sphinx build renders a red error block quoting your directive’s source.
Status — reported, not printed into the page.
- Bare
.. class:: You write
.. class:: Widget.The preview shows the generic directive placeholder.
A Sphinx build documents a Python class — because
primary_domaindefaults topy,.. class::is the Python domain’s object directive, not the class-attaching one. Userst-class(orcssclass) to attach a class; that is exactly why Sphinx ships those names.Status — recognised but not rendered, like every other Python-domain object directive. ubCode records the default domain but does not yet dispatch unprefixed names to it.
Unknown directives and roles¶
- Unknown directives
You write a directive ubCode does not recognise, such as
.. wibble::.The preview shows a generic directive placeholder.
A Sphinx build reports an error and typically drops or marks the construct.
Status — unknown directives also raise a configurable lint diagnostic. Register the directive with
extend_directives, or silence it withignore_directives, in your parsing configuration; see Linting.- A need type named like a built-in directive
You write a
[[needs.types]]entry whosedirectiveis a built-in’s name —note,todo,tab-set,needtable,src-trace.ubCode gives the name to the need type, title and all, and reports it (
config.conflicting_directive_registration).A Sphinx build does the same for docutils’, Sphinx’s and sphinx-needs’ directives and the ports (sphinx-needs registers each type’s directive after every extension’s
setup()), usually with an[app.add_directive]warning — but NOT for a domain directive (thestddomain’senvvar,option,glossary, …, orclass, which resolves to the primarypydomain’s): there the domain keeps the name and the need cannot be written.Status — ubCode keeps the need for a domain name too. For a domain directive ubCode knows — the
stddomain’s andclass— its finding says that Sphinx does not; a name ubCode does not model (function,module, …) is simply the type’s here, with no finding, where Sphinx’spydomain keeps it.- A need type and a
[parse.extend_directives]entry of one name You write both.
ubCode gives the name to the need type and does not use the declaration, and reports it (
config.conflicting_directive_registration).A Sphinx build has no
extend_directives.- Unknown roles
You write text marked up with an inline role ubCode does not recognise, such as a
:wibble:role.The preview shows a placeholder chip naming the role and its content.
A Sphinx build reports an error and typically drops or marks the construct.
Status — the chip is self-describing, so you can spot the unrecognised role at a glance.
Needs and filtering¶
Needs and variants¶
Need directives render as need cards. How a card looks compared with a Sphinx-Needs build:
A hydrated card lists the fields you authored plus any added by
needextend, and not values that exist only as a[needs.fields.*]default. This is ubCode’s own divergence, and it is in the narrowing direction: a Sphinx-Needs layout iterates the fully-populated need, so itscleanshows configuration-defaulted values as ordinary rows.:layout: completeand:layout: debugrestore them, per need or per project; see the:layout:entry below.Cards and their links refresh after each background indexing pass completes — a bounded refresh, not one per keystroke.
A card’s fields, links and back-links sit in a collapsible disclosure, and
:collapse:decides whether it starts closed. ubCode honours the option more broadly than a Sphinx-Needs build does:It works in the editor preview and the HTML build alike, and in every card ubCode renders — including the cards a
needimportrenders and needs whosecollapsewas set from somewhere else entirely (aneedextendon another page, a[needs.fields.collapse]default or predicate, a variant, a dynamic function, or the value stored in an imported project’sneeds.json). In a Sphinx-Needs build the option does nothing at all unless the need’s active layout happens to callcollapse_button, and it is always inert on non-HTML builders such as LaTeX.The control is the browser’s own
<details>element, so no JavaScript is involved and none is needed: it works in a saved page and with scripting off. A Sphinx-Needs build ships a jQuery toggle and a pair of show/hide images.Before the background index has seen the need, the card renders with the disclosure open whatever
:collapse:says. ubCode reads the resolved value, not the text in front of it, and the resolved value is not knowable until the index exists; the card corrects itself on the next render, exactly as its field and link rows do.
Each card carries a hover
¶permalink in built output, the last thing on the header line, after the id — the same affordance a heading or a named caption has. It is the built site only — the¶belongs to the static build’s anchor output, which the editor preview does not render. A preview card still carries its ownid, so a need stays addressable there; what the preview does not draw is the permalink. Three card shapes carry none, each for its own reason: aneedextractview card, because it holds no copy of the need’s anchor to point at (theNeeds extract (needextract)entry earlier on this page states that rule); a header-less design, which has no header line to hang one on and keeps whatever permalink its own regions draw —focus’s slim id footer,focus_l/focus_r’s vertical id strip; and a:hide:need’s collapsed summary line, which ubCode declines deliberately: a<summary>maps to a disclosure button, whose subtree assistive technology commonly flattens into the button’s name, so a permalink there could be visible and focusable and still go unannounced. The need’s own anchor sits outside the disclosure, so a hidden need stays linkable either way. A Sphinx-Needs build has no¶on a card at all. Its layouts link the id chip itself, which on the need’s own card is a self-link, and its<<permalink()>>layout function — which no built-in upstream layout calls — points at apermalink.htmlredirector page ubCode does not ship (needs_permalink_fileandneeds_permalink_dataare excluded configuration).A need keeps its id on its own page. When a need’s id equals the anchor a section, a
:name:or a target on the same page would take, ubCode keeps the id on the need and gives the other element a de-duplicated anchor (its own anchor with a-1serial by default; underid_style = "docutils",id1, orname-1for a:name:), the one a second section with the same title would get. A:ref:or a named hyperlink reference to that element follows it, and nothing is reported; a Sphinx-Needs build publishes both elements under the same id, silently. The exception is a Markdown[text](#slug)link spelled as a displaced heading’s former anchor: it no longer reaches the heading and is reported asstd.ref, so give the heading a(label)=target and link to that instead. This is only reachable with a need id that contains lower-case letters — valid under anid_regexthat admits them; under the default regex such an id is itself reported byneeds.id_regex. A card rendered by aneedimportor asrc-traceis not covered and can still share its id with a section.:style:is honoured: each comma-separated token becomes aub-need-style-<token>class on the card, in the order you wrote them. ubCode ships styling — in the preview and the built site alike — for the twenty style names below, which are the set a Sphinx-Needs build’s ownneeds_style_*stylesheet defines, and they work as it documents them:fills:
green,red,yellow,blue,gray,discreet, plusimplemented/open/in_progressas aliases of green / red / yellow;frames:
green_border,red_border,yellow_border,blue_border,discreet_border;trailing stripes:
green_bar,red_bar,orange_bar,yellow_bar,blue_bar(noteorangeappears only here, and there is nogray_barordiscreet_bar);clean, which strips the card’s chrome.
It also tints the five
tr_<result>styles a test-report need carries (tr_passed,tr_failed,tr_error,tr_skipped,tr_disabled), as sphinx-test-reports’ own stylesheet does (Results tables).A token outside that set is still a valid token if it is a legal CSS identifier: it becomes a class and is available to your own CSS, but ubCode ships no styling for it, so on its own it does nothing visible. Five differences are worth knowing:
The class prefix is
ub-need-style-, notneeds_style_. ubCode’s card is a<div>tree, not the<table>of layout regions a Sphinx-Needs build emits, so upstream card CSS could not be reused whatever the prefix were called;ub-*is ubCode’s documented, stable class vocabulary, and custom CSS you write against it keeps working.Invalid tokens are rejected, not injected. A token must look like a CSS identifier (an ASCII letter or underscore, then ASCII letters, digits, underscores or hyphens — so
:style: caféis rejected). Anything else becomes no class at all, andubc build htmlreports it as build.need_style_invalid, naming the need and quoting each rejected token. Valid tokens on the same need still apply. A Sphinx-Needs build performs no validation here —:style: a bsilently adds a barebclass to the need’s table.The editor preview stays silent about a rejected token. It applies the same style classes the build does (each surface uses its own theme tokens, so the two are visually equivalent rather than pixel-identical), but it has no surface for render-time findings, so run
ubc build html(or read your CI log) to see the warning.Order is kept and duplicates collapse.
:style: blue, gray, blueyieldsub-need-style-blue ub-need-style-gray— first occurrence wins its position, and the repeat is dropped.Like
:collapse:, the value is the resolved one, so aneedextendon another page, a[needs.fields.style]default or predicate, a variant, a dynamic function, or a value stored in an imported project’sneeds.jsonall reach the card — and, also like:collapse:, a card carries no style classes until the background index has seen the need.
:layout:is honoured, as a set of declarative card designs — ten built in, plus any your project defines with [needs.card_layouts] — rather than the layout DSL a Sphinx-Needs build ships. Layouts render in the editor preview and the built site alike — the two share the card markup, so that parity is free — and, like:collapse:and:style:, the value is the resolved one, so aneedextendon another page, a[needs.fields.layout]default or predicate (Choosing a card design with [needs.fields.layout]), a variant, a dynamic function, or a value stored in an imported project’sneeds.jsonall reach the card. A need with no value at all takes the project’s [needs] default_layout — the ubCode spelling ofneeds_default_layout, and the same key in the same tableneeds_from_tomlmaps onto it, so oneubproject.tomlcannot meancompletetosphinx-buildandcleantoubc build html. There is one tier ABOVE the resolved value: a.. needextract::may write its own:layout:(and:style:), which draws every card that site renders, ahead of whatever each need resolves to for itself. TheNeeds extract (needextract)entry earlier on this page states the whole chain.A card renders as
clean— not as the project’s configured default — until the background index has seen the need. This is the same “plain until hydrated” contract:collapse:,:style:and:hide:already follow, and it is where ubCode visibly differs from a Sphinx build, which has no un-hydrated state to render: in a project whosedefault_layoutisfocus, a card you have just typed flashes as an ordinarycleancard for the moment before the index catches up. The card markup is produced by one writer shared with the built site, and that writer deliberately holds no project configuration — which is what makes the preview and the built site the same renderer rather than two.cleanThe default: header, a collapsible meta section listing the fields the need actually carries, then the content. This is the card ubCode has always rendered, and it carries no layout class at all — so writing
:layout: cleanproduces the same card as writing nothing, as long as the project’s default_layout isclean. That condition is upstream’s too, not a ubCode qualification: a Sphinx-Needs build resolves the option with a lookup chain that treats an empty value as unset and falls through toneeds_default_layout, while a writtencleandoes not fall through. So once the project moves its default, the card with no:layout:draws that design and:layout: cleanis a deliberate override — in both tools. What:layout: cleandoes not do is vanish:layoutis a layout-visible field, so the value you wrote still shows as a meta row, the same way an unknown name’s does.completeclean’s chrome over the full effective field set — including values that come only from a[needs.fields.*]default, whichcleanhides as configuration padding — plus a footer echoing the resolved layout and style, as upstream’scompletelayout does. Empty values are still skipped.layoutandstylemove OUT of the meta section here, because the footer is now their home — upstream’scompleteis the only built-in layout that names fields to exclude (an explicitexclude=["layout", "style"]), for the same reason.focusContent only: no header, no meta section. The card keeps its
idanchor anddata-*attributes and gains a slim footer carrying its permalink, so a focused need is still linkable and still appears in the built site’s search index under its own id and title. This is exactly upstream’sfocus_f, and ubCode says so rather than pretending otherwise: upstream’s barefocusdrops even the id, which would leave a card with nothing to link to. Sofocus_fresolves to this design by design — it is an identity, not an approximation — and it is upstream’s barefocusthat has no ubCode equivalent.:collapse:is inert here — there is no meta section to collapse.debugclean’s chrome over every field the index can resolve for the need — internal fields, empty values and configuration-defaulted values alike — with the meta section forced open, so:collapse:is ignored. The “what can I filter on” view, and the configuration-defaulted values are what make that true: a filter resolves a field through the same default tier, sopriority == "medium"selects a need whoseprioritywas never written on it. The three designs with a meta section are therefore a chain, bar one relocation: every row survives into the next design up, exceptlayout/style, whichcompletemoves to its footer (cleananddebugshow them as ordinary rows).A Sphinx-Needs build does the reverse on the collapse question: its
debuglayout shipscollapse_button(..., initial=True), which renders the card collapsed unconditionally. ubCode inverts that deliberately — a debug view that opens closed shows nothing.
The six sphinx-needs names that add a side column to one of those designs are implemented as designs of their own, image and all:
Name
What it renders
clean_l,clean_rcleanplus a column drawing the need’simagefield, on the left or the right, beside the whole card.clean_lp,clean_rpThe same column, but beside the header and meta section only — the content takes the card’s full width below it. (That is what upstream’s
_pgrids do too.)focus_l,focus_rfocus’s content-only card with the need’s id in a narrow vertical strip instead of a footer.Two upstream names remain documented identities rather than designs:
testrendersclean(upstream’stestlayout is that minimal card), andfocus_frendersfocus(see above). Both are stated in the editor’s completion list, so nothing about the mapping is silent.A need with no value for the field a column draws renders byte-identical to the design the column was added to — no column, no wrapper, and not even a layout class to catch a stylesheet on. That is deliberate: it is what upstream renders for such a need too.
Several further differences are worth knowing:
An unknown name is a warning, not a build failure. ubCode renders the project’s default design and reports build.need_layout_unknown, naming the rejected name, the design the card actually got, the valid set, and the needs affected. A Sphinx-Needs build raises
SphinxNeedLayoutException— which derives fromBaseException, so one mistyped layout ends the whole build. The lookup is case-sensitive in both::layout: Cleanis notclean. As with:style:, the editor preview stays silent about a rejected name — it renders the same default design the build does, but it has no surface for render-time findings, so runubc build html(or read your CI log) to see the warning.An empty value means unset, not “unknown”:
:layout:with nothing after it renders the project’s default design and warns about nothing. That matches upstream, whose resolution is anorchain over a value an empty string is falsy in.Which default, exactly, is worth being precise about, because an empty
:layout:is still a value: it is written on the need, so it suppresses a[needs.fields.layout]defaultorpredicatesrow (those fill a field that is ABSENT), and then resolves as unset straight to [needs] default_layout, thenclean. So writing:layout:on one need is how you opt that need out of a project-wide field default — not how you ask for it.An empty
[needs] default_layoutis unset here and fatal there.default_layout = ""means “useclean” in ubCode — every optional string key in this table treats an empty value as unset, and so does:layout:itself. In a Sphinx build the same line is a build-killer: the value is the last link of the resolution chain, so there is nothing left to fall through to, and every need in the project raisesSphinxNeedLayoutException. The divergence is deliberate — a configuration key whose empty value destroys the build is not a behaviour worth porting — but if you shareubproject.tomlwithsphinx-build, omit the key rather than emptying it.An unknown
[needs] default_layoutis reported once, at configuration time. ubCode names it underneeds.default_layoutand falls back toclean, so a typo in one line costs one warning however many needs the project has — where a Sphinx build discovers it per need, at render time, and aborts on the first one.A defaulted card’s
layout_echoshows the resolved design.complete’s footer printslayout: completefor a card that gotcompletefromdefault_layout, where a Sphinx build printslayout:and nothing after it — upstream’s footer cell reads the need’s stored field, which an unset:layout:leaves empty, while ubCode’s echoes the design the card was actually drawn with. ubCode’s is the more useful of the two, and it is what makes the echo worth having on precisely the cards that never named a design; the difference only became visible when a project could move its default.You can define your own designs, with [needs.card_layouts] — but not with upstream’s DSL. needs_layouts is deliberately not implemented: its layout definition is a grid of docutils table cells filled by
<<meta_all(...)>>-style callables, which describes a<table>card ubCode does not emit, and it is the mechanism behind the build-ending failure above. What ubCode ships instead is a declarative card spec — a named set of regions (header, meta, content, footer, side) and the typed elements each holds — which the ten built-in designs are written in too, so a design you define andcompleteare the same kind of thing, interpreted by the same code, in the preview and the built site alike. A Sphinx-Needs project can take the same vocabulary with that project’sneeds_card_layouts, which compiles it down toneeds_layouts.
The layout classes are
ub-need-layout-<design>(the design namedcleanemits none, and neither does a side-only design whose column drew nothing), and they compose freely with the:style:classes and the per-type accent. The exclusion follows the name, not the project’s default: underdefault_layout = "complete"a card with no:layout:carriesub-need-layout-completeand one with:layout: cleancarries no class. It has to work that way — ubCode’s stylesheet keys real rules on those classes (complete’s footer alignment, the whole geometry offocus_l/focus_r’s vertical id strip), so a defaulted card that suppressed its class would silently lose them. A Sphinx-Needs build makes no exclusion at all: it emitsneeds_layout_cleanon everycleancard.needs_string_links is honoured, as [needs.string_links] — a field value that matches a rule’s pattern renders as a link, in the need’s card, in a
needtable, and in the editor preview and the built site alike. The matching rules are ported as they stand — the,/;splitting, the first-declared-rule-wins precedence (with no fallthrough when its pattern does not match), the unanchored search that replaces the whole value, and the silence when nothing matches. Some behaviours deliberately differ, because the upstream one is a defect rather than a contract:No trailing separator. A Sphinx-Needs meta area up to release 8.3 emits a
;after the last item as well as between items; its ownneedtabledoes not. ubCode emits separators between items only, on both surfaces — and Sphinx-Needs agrees from its next release (fixed upstream in sphinx-needs PR 1718).A list field links on both surfaces. Upstream applies string links to a list field’s elements in a
needtablebut not in the need’s own meta area — the same field, two answers, on one page. ubCode links them in both.A failed template keeps the value. When a template raises, upstream logs a warning without a location and the field’s value disappears from the page. ubCode renders the plain text and reports
build.need_string_link_invalid.Whitespace-only items are dropped.
AB-1, , AB-2is two items, not three (upstream tests emptiness before stripping, and renders a phantom).A non-participating optional group renders empty, where upstream prints the literal
None: a PythonNonereaches its template through the object bridge and is formatted as a string. Only optional groups —(?P<a>[A-Z]+)(?:-(?P<b>\d+))?onAB— can tell the difference.
Further differences are consequences of the engines rather than choices about rendering:
A dangerous link target is refused, not emitted. A rendered
link_urlmust be anhttps:///http://URL, amailto:address, or a relative reference; anything else —javascript:,data:, a protocol-relative//host— draws no link and reports. Upstream emits whatever the template rendered. The check is on the template’s output, because a need field can arrive from an importedneeds.json.Rules are validated when the configuration loads. A missing key, an uncompilable pattern or an unparseable template is a warning against the rule, and the rest of the project keeps building; upstream raises out of the first need it renders, which ends the build even when no need carries the field. Two capabilities are missing on ubCode’s side, both stated by the diagnostic that reports them: patterns cannot use look-ahead, look-behind or backreferences — which are refused by name — nor a handful of rarer constructs (conditionals, atomic groups,
\Z,(?#...)), which are refused as ordinary parse errors; the engine trades them for a linear-time guarantee. And there is no needs_render_context, so a template sees the pattern’s named groups and nothing else — which also removes the upstream footgun where a global silently shadows a group namedvalue.
:hide:is honoured, as a collapsed card rather than as nothing at all. A hidden need renders its whole card — meta section, content, footer, nested needs and all — inside a closed disclosure whose summary is the card’s own header line. One compact line, expandable in place. Nothing is withheld; it is collapsed. It works in the editor preview and the built site alike, and, like:collapse:,:style:and:layout:, the value is the resolved one, so an authored:hide:, aneedextendon another page, a[needs.fields.hide]default or predicate (Hiding cards conditionally with [needs.fields.hide]), or a value stored in an imported project’sneeds.jsonall reach the card. A card renders expanded until the background index has seen the need.Note
A dynamic function or a variant written as the value of
:hide:does not currently reach the card: ubCode resolves neither back to a boolean, so the need falls through to the field’s default and renders uncollapsed. This is a limitation of boolean fields in general rather than of:hide:—:collapse:behaves the same way — and the string-valued fields beside them (:layout:,:style:) are unaffected. A Sphinx-Needs build does resolve both onhide(the field carriesallow_dfandallow_variants), so this is a real parity gap on ubCode’s side rather than a shared limitation. Tracked as issue #2910.Everything composes with it.
:hide:beats nothing: a hiddencompletecard is a complete card, its:style:still paints, its type accent still paints (unless a*_borderstyle claims that edge, exactly as on a visible card), and its:collapse:still decides whether its meta section starts closed — a second disclosure, nested inside the first, each with its own meaning. A mistyped:layout:or an invalid:style:token on a hidden need is reported exactly as it is on a visible one; hiding a need is not a way to silence it.The need stays linkable and findable, which a Sphinx-Needs build cannot manage:
the card’s
idis on its root element, outside the disclosure, so a:need:reference or a link row targeting the need itself lands on it with nothing to expand;anchors inside the card — a nested need’s id, a label written in the body — are reached by the browser opening the disclosure as it scrolls to the fragment. This is standard behaviour, verified in current Chromium, Firefox and WebKit; ubCode’s own shell script covers engines old enough to lack it. In the editor preview the cards render, but jumping the cursor into a collapsed card does not yet open it — the preview scrolls rather than navigating to a fragment, so nothing triggers the reveal;
the need keeps a full search-index record, its content included, since the body is published rather than withheld;
its
needlistentry is now a hyperlink. This is the one change to output that a project upgrading may notice: those entries used to render as plain text, because before this a hidden need had no anchor worth pointing at. Entries in aneedtable, and the nodes of aneedfloworneedsequence, are unaffected — they never unlinked hidden needs.
A Sphinx-Needs build can do none of that, and the reason is structural rather than a matter of taste: its
:hide:returns from need construction before thenodes.targetthat would carry the anchor is created (sphinx_needs/api/need.py:774-780against:796-798), and the need node it does keep is then removed from the doctree outright (sphinx_needs/directives/need.py:325-332). So a hidden need has no anchor there at all, and a:need:reference to one resolves — without a warning — to a target that does not exist (sphinx_needs/roles/need_ref.py:271-282builds the reference from the need’sdocname, consulting nothing abouthide). Itsneedlistspecial-cases hidden needs to plain text for exactly that reason (sphinx_needs/directives/needlist.py:117-118).Two further differences are worth knowing:
The body is parsed, and everything in it renders and is indexed. A Sphinx-Needs build returns before parsing a hidden need’s content, so — as its own source comment records —
partsandarchare never populated and child needs are never found. ubCode parses every need unconditionally, so a hidden need’s parts and nested needs are all in the index — and, since the card renders in full, a need nested in a hidden need has its own card and its own anchor. Note that:hide:applies to the need it is written on and never to the needs inside it: nest a visible need in a hidden one and it renders as an ordinary card, inside a collapsed one. Hide the children too if that is what you want.``hide`` is exported to
needs.json. A Sphinx-Needs build cannot: the field carriesexclude_json: True(sphinx_needs/data.py:150-159), so a downstream consumer of itsneeds.jsoncannot tell which needs were hidden. ubCode exports the value like any other field.
Printing a page renders hidden needs in their collapsed state — the summary lines are the compact view, which is the point of the design — so a printed page lists them without their bodies.
:hide:is not a confidentiality boundary and never was: a hidden need’s content is published — collapsed — and is searchable, quotable and present in the page source. Nor is it the “produce no output at all” mechanism;:delete:is (see Needs). A deleted need is dropped from the index entirely; a hidden one is a full member of it that simply starts collapsed.A need type’s
color([[needs.types]]) tints its cards, not just its diagram nodes: the card exposes it as a--ub-need-accentCSS custom property, and ubCode’s stylesheets use it to outline the type badge and paint the card’s leading edge (an explicit*_borderstyle frames all four edges and takes that one; a visible card’s type badge, where its layout shows one, keeps the accent as its outline). It applies only when you have chosen a colour: a type declared without one is not accented, and neither is a built-in type still carrying its shipped colour (those exist to fill diagram nodes, and every project gets them whether or not it asked — a Sphinx-Needs build does not colour cards from them either). Change a built-in’scolorand its cards accent normally. When a type colour accents a card lists the four shipped values, spells out which spellings of black and transparent count as unset, and says what to do when a colour of yours is being ignored — worth reading if you are migrating a[[needs.types]]block across, because those four values are exactly what you will be carrying.Only
#RGB,#RRGGBBand#RRGGBBAAare used for the card; any other value is ignored there, and diagrams are unaffected throughout — they keep every colour, including the built-in ones. A Sphinx-Needs build usescolorfor diagrams only, and documentsneeds_type_<Name>classes as the way to colour cards by type.:template:,:pre_template:,:post_template:and:jinja_content:are accepted and then ignored. Writing one on a need produces no diagnostic — it is a recognised option, not an unknown one — but nothing about the need or its card changes, and the value reaches neitherneeds.jsonnor a filter. A Sphinx-Needs build renders the need’s content through the named template.[needs.global_options]entries fortemplate,pre_templateandpost_templateare silently accepted and ignored the same way; ajinja_contententry there is instead reported as an unknown global option.A falsy
.. if::block, and each branch a.. choose::does not take, stays visible but collapsed and greyed out, labelled with its condition; a Sphinx-Needs build omits it entirely.A need referenced only inside a non-indexed include fragment (for example an extensionless or excluded file) keeps its placeholder chip, because the background index does not look inside such fragments.
A
needlistwith no:sort_by:lists its needs in id order; a Sphinx-Needs build lists them in document order. ubCode sorts by id so the output is reproducible build-to-build — the incremental index has no stable document-processing order to preserve. Giving:sort_by:a field name orders by that field in both.:need_count:— and everyneedpieslice,needbarcell andneedreportmetrics row, which all count through the same code path — counts needs only; a Sphinx-Needs build counts needs and their parts in the filtered case. (The plain total counts needs in both.) In particular, a count filtered on theis_partcontext constant renders 0 (need parts are not first-class filter items), where a Sphinx-Needs build counts the parts. The parts themselves are modelled — they resolve in a:need:role, they are validated, they are written toneeds.json, and aneedtablewith:show_parts:renders a row for each — they are simply not things a filter expression selects.:filter-func:is accepted but never executed on every view directive that offers it —needlist,needtable,needflow,needsequenceandneedpie. It names a Python callable for a Sphinx build to import and run; ubCode does not execute project Python, so the option is reported asneeds.filter_unsupportedand the directive still renders from its remaining filter surfaces (a chart, from its content lines). Use the portable:filter:expression,:status:/:tags:/:types:, or:cypher:where the directive offers them.:export_id:is deprecated in Sphinx-Needs, which warns about it too and points at itsneeds_debug_filtersconfiguration instead. ubCode ignores the option and reportsneeds.option_deprecated— its own code, deliberately not theneeds.filter_unsupportedabove, so that silencing “ubCode cannot run project Python” does not also silence “stop using this everywhere”. It is reported the same way on every view directive that handles the option:needlist,needtable,needflow,needsequenceandneedgantt. Delete the option.A
needtableand aneedlistcap their rendered rows/items at 100 by default and append an honest “Showing the first 100 of N needs” notice when the cap bites; a Sphinx-Needs build renders every matched need. The cap keeps an unfiltered view on a large project from dominating render (and live-preview) times. Set:max_items: 0for no cap, or:max_items: Nfor a specific limit; the cap applies after the filter and the sort. The 100 is the default of needs.views_max_items, which sets the cap project-wide for all four view directives. Whenever a cap bites,ubc build htmlalso emits abuild.need_view_truncatedwarning naming the directive and the shown-vs-total, so a truncation is visible in the build log too.A
needtableis interactive by default, as it is in a Sphinx-Needs build: the reader sorts by column, filters the rows, pages through them, hides columns, and copies or downloads the table as CSV. That is what:style: datatables— the Sphinx-Needs default, and ubCode’s through [needs] table_style — now means. It is not DataTables: the two tools ship the same hand-written asset against the same markup contract, whose design record (design/needstable-contract.mdin the Sphinx-Needs repository) is the reference for both. So a table’s rows, its:style_row:classes and the links inside its cells are the same nodes before and after — the widget enhances the table in place and never re-renders a cell.:style: tableis the opt-out and keeps the static table inside a scroll frame with a sticky header row; set[needs] table_style = "table"to make that the project-wide default. The rows per page come from:page_size:or [needs] table_page_size, and the sizes offered from [needs] table_page_sizes. The widget and its stylesheet (_static/needstable.jsand_static/needstable.css) are linked only on pages that hold an interactive table, so a page of prose — or one whose tables are all:style: table— downloads neither.The interactive table pages the rows the cap allowed.
:max_items:(and [needs] views_max_items) still truncate server-side, before the page is written, so a table over more needs than the cap allows shows the notice and pages within what it shows. Raise:max_items:— or set it to0— to page the whole selection.:style_row:works, and every row carries the Sphinx-Needs row classes —needon a need’s row andneed_parton a:show_parts:part row, with the:style_row:value appended after it, so the workedtr.needs_implemented { … }examples in the Sphinx-Needs documentation and any stylesheet you already have keep working. The value runs through the dynamic-function evaluator per need, so:style_row: needs_[[copy('status')]]colours each row by its own status, and whitespace in the result becomes_. Two limits, the second shared with a Sphinx build: ubCode resolves all seven Sphinx-Needs built-in dynamic functions (echo,test,copy,links_from_content,check_linked_values,calc_sumandlinks_from_filter) and renders??for anything else — where a Sphinx build also resolves the functions a project registers throughneeds_functions; and only the first function call in the value is resolved, which is Sphinx-Needs’ own behaviour, so a second, different call is left standing as literal text in both tools. Part rows do not get the:style_row:class, also matching a Sphinx build.When a function does not resolve, whether you are told depends on why. A Sphinx build warns on every failure; ubCode splits them, because one half is per-table and the other is per-row:
A call that cannot work for any need — it does not parse, it names a function ubCode has no built-in for, or its arguments are a shape that built-in cannot take — reports
needs.dynamic_functionsonce, on the directive, naming the call and saying that??rendered in its place. A typo’d function name is this case.A call that is well formed but fails for a particular need — most often
[[copy('status')]]on a need that has nostatus— renders??for that row and says nothing. A Sphinx build warns once per affected row; doing the same on a table of two hundred needs is the un-deduplicated warning spam ubCode set out to remove, so the??in the class is the only signal here.
:show_parts:works: each need’s parts render as extra rows directly under it, withIDshowing<need>.<part>andTITLEthe part’s content, both prefixed with needs.part_prefix (default"→ ", an arrow and a non-breaking space — the same default as Sphinx-Needs’needs_part_prefix). A part row’s outgoing-link columns are empty, its incoming-link columns show that part’s own incoming links,CONTENTshows the part’s content, andIS_PART/IS_NEEDaretrue/false— all as in a Sphinx build, whose part item is an overlay on the parent need. Every column the overlay does not touch shows the parent need’s value in both tools:STATUS,TYPE,TAGS, your own fields, anddocname/lineno/ theis_*origin fields. (If you declare your own field calledis_partoris_need, yours wins on a part row and on every need; a part tested by aninclude_partsrule answers its ownis_part,is_need,id_parentandid_complete. Sphinx-Needs refuses such a declaration outright —Field 'is_part' already used internallyat startup — so no Sphinx build that runs can observe the difference.)A Sphinx build also lists the parts a
:filter:passes as items of their own, in aneedtableand aneedlistalike, and so shows each part twice under:show_parts:; ubCode’s table rows are needs, and a part appears once, as a:show_parts:row.Three differences from a Sphinx build:
The part id links to the parent need’s anchor, not the part’s own — ubCode does not yet give a part its own anchor in a built page, so there is nothing narrower to point at. A Sphinx build links
#REQ_1.p1.The part rows are ordered by part id, where a Sphinx build orders them by position in the document (id order is reproducible for parts read from a
needs.jsonas well as from a page, the same reason:sort:breaks ties by id).:max_items:counts needs, so a need that survives the cap keeps all of its part rows — which is what a Sphinx build does too.
id_completeandid_parentare columns: the need’s own id on a need row, and on a part row<need>.<part>and the owning need’s id.partsis a column ubCode does not answer at all; it renders empty on a need row and on a part row alike, which is the same unsupported-column gap listed further down rather than anything specific to parts.parent_needused to be in that list and no longer is: it is answered from need nesting, as the first entry ofparent_needsin the sorted order the column beside it shows — so the two never disagree about which need comes first. On a part row it is still empty — a part has no nesting parent of its own; the owning need’s id isid_parent.:style_col:does nothing — in a Sphinx build either. Sphinx-Needs declares the option and stores it, but never reads it back, and it is absent from its own documentation. ubCode reportsneeds.option_unsupportedsaying so — the same stance it takes forneedgantt’s no-op options above — so the dead option is not silent; delete it.A
needtablewith no:sort:sorts by id (case-insensitively), matching a Sphinx-Needs build’s default; where two needs would tie on the sort key, ubCode breaks the tie by id rather than by document order (the same reproducibility reason asneedlistabove). The:columns:and:sort:options accept the same names as Sphinx-Needs; an unknown name raises a warning with a “did you mean” suggestion and renders an empty column (:columns:) or falls back to id order (:sort:).:columns:also accepts several of the Sphinx-Needs core fields —docname,lineno,is_external,is_import,is_need,is_part,id_parent,id_complete,is_modifiedandmodifications— plus ubCode’s own origin-kind fieldsis_directiveandis_src_trace. (contentwas accepted before too, but rendered an empty cell; it now renders the need’s body text.) A Sphinx-Needs build renders every core field as a column; the ones ubCode does not answer yet — among themexternal_url,type_nameandlineno_content— still warn and render empty. Service-injected fields such asserviceanduserstay unknown unless you declare them as fields inubproject.toml.The same names are now readable in a
:filter:expression, which has two knock-on effects if you were already using those spellings.linenopreviously fell through to a[needs.filter_data]key of that name; it no longer does. And a project that declares its own field or link calledcontentnow gets the need’s body in a:columns: contentcell, matching what a:filter:oncontenthas always read —id,typeandcontentare the need’s identity and nothing you declare overrides them. Your ownis_need,docnameand the like do still win, as before.:sort:accepts the core fields above, so:sort: linenoputs a table in source order and:sort: is_externalgroups the needs read from an external source last. (Needs pulled in byneedimportare a different origin — order those with:sort: is_import.):sort_by:reads the same names, though it still ignores an unrecognised key without warning, and — unlike:sort:— it matches the key case-sensitively, so:sort_by: LINENOsilently leaves the order alone. The two option vocabularies are otherwise the same, with two exceptions.id_completesorts (it is the default) and is now a column too. And a link’s presentation spellings are columns but not sort keys —<link>_back, theoutgoing/incomingaliases, and any customincoming/outgoinglabel you declared for it. Sort on the link’s own name instead, which orders by the same values its column shows.Sorting is now by value, not by text. A numeric field orders
2, 9, 10; previously every sort key was stringified, which ordered them10, 2, 9. This affects:sort:and:sort_by:on any numeric field you already use, so an existing table may legitimately change order. Where a key has no value at all those needs sort first, and where one field mixes types the order is booleans, then numbers, then text — a Sphinx-Needs build raisesTypeErroron that mixture rather than choosing, so ubCode defines an order instead of failing the build.:colwidths:sets the column widths, given as one positive whole number per column, separated by,or;. The numbers are relative shares of their total, so50,40,10and5,4,1lay out identically — the same normalisation alist-table:widths:gets, from the same code. (As with any table on the site, the widths are what the browser is asked for; an unbreakable cell can still push a column wider. A Sphinx-Needs build is in the same position.) Anything else raises a warning and renders the table without column widths: a value that is not a number at all (including the50%form the Sphinx-Needs documentation invites), a zero or negative one, one above 18446744073709551615, or a count that does not match the number of columns — with:columns:unset, that count is the six default columns. A Sphinx-Needs build fails on most of those, so a table that used to stop your build now renders with a warning. Some refusals run the other way, because ubCode reads a stricter grammar than Python’sint— ASCII digits with an optional+, no_separators, and that upper bound. A Sphinx-Needs build accepts a zero or negative width, rendering a zero or negative percentage, and has no upper bound at all, so those are refused here and not there. Refusing a zero is Docutils’ rule rather than Sphinx-Needs’, which validates nothing but the count: on Docutils 0.22 a zero-width column aborts the build when the page is written, so the widths Sphinx-Needs lets through are ones a current Docutils will not render at all. With no:colwidths:at all, ubCode leaves the widths to the browser; a Sphinx-Needs build always writes a column group, giving atitlecolumn three times the width of each of its neighbours (and equal widths when no column istitle).Two of those columns read differently from a Sphinx-Needs build. A boolean cell shows
true/falserather thanTrue/False— ubCode renders every boolean the same way wherever it comes from, including your own boolean fields and the built-incollapse/hide. And for a need written in an included fragment,docnamenames the page the card is rendered on whilelinenois the line in the fragment — the position your editor can actually open. A Sphinx-Needs build instead numbers lines in the spliced document, so itslinenois shifted for every need at or after aninclude.
See also
RST preview for how need cards hydrate, Variants for the variant authoring constructs, and the if directive for variant conditions.
Dynamic functions resolve all seven built-ins, and their filters¶
ubCode evaluates every one of Sphinx-Needs’ built-in dynamic functions —
echo, test, copy, links_from_content, check_linked_values
and calc_sum, and links_from_filter of Sphinx-Needs 9.0.0 —
with their filter arguments (filter_string on check_linked_values),
wherever a [[…]] may be written:
a need’s option and link values, a needtable’s :style_row:,
and the :ndf: and :need_func: roles.
Arguments may be literals or need.<attr> references.
Released versions of Sphinx-Needs accept need.<attr> only in an :ndf: role inside a need
and in a needtable’s :style_row:;
Sphinx-Needs 9.0.0 accepts it in option values too
(see “A need.<attr> in an option value” below).
What a Sphinx build still does and ubCode does not:
- A filter that reads beyond the candidate need
A filter naming
needsorcurrent_need, or reading ac.attribute such asc.this_doc(), and a filter given as aneed.<attr>, are not evaluated here: the call is reported once, at the site you wrote it, as an informationalneeds.dynamic_functionsfinding, and the value is left at its placeholder. A Sphinx build evaluates them in some calls, and not always as the reader:needsincalc_sum,copyandlinks_from_filter, ac.attribute incopyandlinks_from_filter, andcurrent_needin every call — though incalc_sumandcheck_linked_valuesit is the need tested, not the reader. For a sum over the needs passing a test of other needs, the portable form is a boolean derived field the sum filters on.- Functions your project registers
A function added through
needs_functionsis not resolved. ubCode cannot run project Python. The call is reported once at its site and renders??, or keeps the role’s chip where a role wrote it.
Where the two tools resolve the same call to different values, they do so in these places:
- A call that reads a value another call computes
ubCode resolves the calls in dependency order: a call reads the value the other call computes, a value on a cycle of reads keeps its empty value with a
needs.derive_cyclewarning, and a read that cannot be ordered isneeds.derive_scope(see When a call reads a value another call computes). The answer is the same on every build. Released versions of Sphinx-Needs have no defined answer here: their single resolution pass mutates needs in place in the order they were added, so the value depends on document order — and on whether the build was incremental — and where the pass reaches the reading need first, a text field stores the stringNone; neither finding is reported. Sphinx-Needs adopts the same dependency order, and the same two findings, in its 9.0.0 release; until then, for the ordinary case — a call reading fields that are not themselves calls — the two tools agree.- A sum over every need
calc_sum('hours')withoutlinks_onlyadds its summands in id order in ubCode, the ids compared character by character, and in the order the needs were added in a Sphinx build. Floating-point addition depends on that order, so a total can differ between the two tools in its last digits:0.1,0.2and0.3total0.6000000000000001added in that order, and0.6added in reverse. ubCode’s total is the same on every build; a Sphinx build’s depends on document order, and on whether the build was incremental. Adding in that id order is planned for Sphinx-Needs too.- An unknown
need_id copy('x', need_id='NOPE')andlinks_from_content(need_id='NOPE')both fail the call, reported asneeds.dynamic_functionnaming the id, and the field keeps its empty value — as a Sphinx build reports both (aKeyErrorfor one, “no stored node” for the other). Aneed_id— or acontent_linksrule’sfrom— naming an external need, which has no content, fails the same way in both. In a role, both render??here, where a Sphinx build renders??forcopyand[]forlinks_from_content, which it does not raise.- A filter a need cannot be evaluated on
A need lacking a field the filter compares (
hours > 3on a need withouthours) is treated alike by both tools:calc_sumstill counts it,check_linked_valuesstill tests it,copydrops it,links_from_contentfails the call, andlinks_from_filterand a derived link type’swherelet it not pass, silently. The finding differs in number only: a Sphinx build logsneeds.filterfor every such need undercalc_sumandcheck_linked_values(once per call undercopy), ubCode once per call, naming the needs.is_modifiedafter a dynamic functionA need whose own
[[…]]computed a value counts as modified in ubCode —is_modifiedis true in a:filter:, inubc queryand in a:columns:cell — where a Sphinx build marks only the needs aneedextendchanged. A derived link type’s result marks nothing in either tool.:need:references of a hidden needlinks_from_contentand acontent_linksderived link type read every:need:reference of a need’s content in ubCode, on a:hide:need too — as the carrier, or named by afrom/need_idfrom a visible need. Sphinx-Needs 9.0.0 reads none of a hidden need’s: the call and the rule give[]there, with aneeds.dynamic_functionwarning (links_from_content: no stored node for need 'HIDE_ONE', at the hidden need), until a later release decides how a hidden need’s content is processed. On any other need both tools read every reference, inside anonlyblock or a figure caption included.- A link list in an array field
A non-empty
[[links_from_filter(…)]]or[[links_from_content()]]written in a field declared as an array of strings stores the need ids in ubCode. Sphinx-Needs 9.0.0 refuses the value (dynamic function value <class 'list'> is not of type 'array' or item type 'string') and stores nothing — a defect in how it types the list, to be fixed there. The empty list passes in both.- A
need.<attr>outside any need An
:ndf:role written outside a need body cannot readneed.status, and renders??. A Sphinx build renders the same??and additionally warns.- A
need.<attr>in an option value :total: [[calc_sum(need.which)]]reads the need’s ownwhichin ubCode and sums the field it names; whenwhichis itself a call, what the call reads is not known in time, so it isneeds.derive_scopeand not run. Released versions of Sphinx-Needs refuse the value when they read the directive (Need could not be created: … Unsupported arg 0 value type), so the need does not exist there at all; written in aneedextendoption, the option is skipped with aneeds.needextendwarning and the extend’s other options still apply. Sphinx-Needs 9.0.0 acceptsneed.<attr>in option values, as ubCode does. To build with both tools until then, name the field in the call:[[calc_sum('hours')]].- A
need.<attr>that holds a list ubCode joins a list-valued selector into one name with
,and a space, so[[copy('title', need.links_back)]]on a need with one backlink copies that need’s title, where a Sphinx build fails the call. With two or more backlinks the call fails in both tools. With none it fails in ubCode, where a Sphinx build takes the empty list for noneed_idand copies the need’s own title.[[links_from_content(need.links_back)]]gives an empty list in ubCode whatever the list holds, where a Sphinx build fails the call for a non-empty list and reads the need’s own content links for an empty one.- The dead-link flags
has_dead_links(a link names a need that does not exist, or a part its need does not have) andhas_forbidden_dead_links(such a link is in a link type that does not allow dead links) are set once every link is final, with the backlinks, in both tools: a field’s call or variant condition reads them ([[copy('has_dead_links')]],<<[has_dead_links]: …>>), as doescalc_sum, and a link’s isneeds.derive_scope. A view directive’s:filter:reads them and a needtable shows them as a column, where a<link>_backfield is read. The differences that remain:They are not available in a
needextendfilter: ubCode reports such a filter and applies nothing, where Sphinx-Needs evaluates it againstFalsewith no warning, sonot has_dead_linksextends every need there andhas_dead_links == Truenone.ubCode does not write them to
needs.json, where Sphinx-Needs writes both on every need.A needtable cell reads
true/false, as every boolean cell does, where Sphinx-Needs printsTrue/False.A field the project declares with one of the two names is its own in ubCode, everywhere, where Sphinx-Needs rejects the directive option and reads its flag; a link type of the name is its own in ubCode too, where Sphinx-Needs accepts it and fails the build as soon as that flag is true on any need (any dead link for
has_dead_links, a forbidden one forhas_forbidden_dead_links; the flag is written over the link type’s value). Rename such a field or link type to build alike in both.An imported need whose
parent_needsnames a need that does not exist has a dead link in ubCode, which keeps the imported value, so both its flags are set; Sphinx-Needs recomputesparent_needsfrom the nesting of the needs, so the need has no parent there, and no dead link.
- A link field on a need that has none
copy('links')on a need with no:links:option copies an empty value in both tools — an empty list, which an array field holds and a text field refuses —check_linked_valuesfinds no match on such a need in both, and a variant condition reads an empty list there too:<<['T_ONE' in refs]: yes, no>>givesno. The storedneeds.jsonstill differs: Sphinx-Needs gives every configured link type a key on every need, where ubCode writes only the link types a need actually uses.check_linked_valuesand part linksReleased versions of Sphinx-Needs look a linked need up by its full link string, so a link to a need part (
ID.PART) — or to no need at all — fails the whole call, as it does acalc_sumwithlinks_only. ubCode resolves the parent need and compares against that instead, and skips a link to no need (the dead link is reported on its own), as Sphinx-Needs 9.0.0 does. It also compares values type-strictly, where Python’sintreatsTrueand1as equal — so a mixed list such as['a', 1]may match in a Sphinx build and not here.test()echoes aneed.<attr>as you wrote it[[test(need.id)]]on needREQ_1printsargs: (need.id)in ubCode andargs: (REQ_1)in a Sphinx build, which substitutes the attribute beforetestever formats it. For the same reasontest(need.nope)— an attribute the need does not carry — renders??in a Sphinx build and printsneed.nopehere.testis a debug helper, so this affects nothing but its own output.- Numbers and booleans a function returns
In an
:ndf:role or a:style_row:,calc_sum’s float renders22.0rather than22— matching Python’s ownstr(), which is what a Sphinx build prints — and a boolean rendersTrue/Falsefor the same reason. In the rendered text, very large and very small floats are the one place the two differ: Python switches to exponent form (1e+20for1e20,1e-05for1e-5) where ubCode writes the digits out (100000000000000000000.0and0.00001). In a need’s field a result is not rendered at all: as in a Sphinx build, it must have the field’s type, so a number, a boolean or a list in astringfield is aneeds.dynamic_functionwarning and the field stays empty (see When ubCode cannot resolve a call), as is a list in aninteger,numberorbooleanfield and an item of another type in an array field. A boolean is not a number either:truein anintegerornumberfield, or as an item of anintegerornumberarray, is the same warning here, where a Sphinx build stores it, because Python counts a boolean as an integer, and then reports the stored value as a schema violation. An integer is accepted in anumberfield and stored as a float (77.0), where a Sphinx build keeps the integer (77).- A function that returns nothing
copy('x')of a field the need does not set and that has no default returnsNone. In a field that may not be empty — anextra_optionsentry without a schema, or a field declarednullable = false— both tools warn (needs.dynamic_function) and leave the field empty. In a field that may be empty, ubCode leaves it empty (null) and reports nothing, where a Sphinx build stores the textNonein astringfield and[None]in an array field, which its schema validation then reports as an error; a planned Sphinx-Needs release leaves it empty too.
Derived fields need sphinx-needs 9.0.0¶
A derived field — a derive table on a
[needs.fields.<name>] or [needs.links.<name>] entry —
is read by sphinx-needs from its 9.0.0 release,
with the same kinds, roles and findings.
- A sphinx-needs older than 9.0.0
ignores the
derivekey without a warning, so a Sphinx build leaves the field authorable and unset where ubCode closes it: an option an author writes on it is stored there, and reported (needs.derive_authored) and ignored here. ubCode cannot see which sphinx-needs a project builds with, so it reportsconfig.derive_sphinx_minimum(info) once on every project that declaresderive; silence it once the project builds with sphinx-needs 9.0.0 or later.- Once both tools compute a rule
ubCode computes every kind with sphinx-needs 9.0.0’s values and findings, checked by the conformance corpus both tools run. Both order the reads the same way: a
whereor alinks_from_filternaming a field a value kind computes isneeds.derive_scopeand links nothing, since the field is computed after the links; and a call or a rule withoutafter = "derived"reading ahashfield, or one withafter = "derived", isneeds.derive_scopeand keeps its empty value. What still differs:A hidden need’s
:need:references are read by acontent_linksrule in ubCode and by none in sphinx-needs (see the entry on a hidden need’s references above).A hidden need’s parts are candidates of
include_partsin ubCode, which reads a hidden need’s content, and are not in sphinx-needs, which collects no part of a hidden need.In
needs.json, ubCode writes no<link>_backkey, and a link type a need does not carry is absent from that need, where sphinx-needs writes[]— a derived link type whose rule computed nothing included (#4035 writes every declared link type). The values a need carries are the same in both.A derived field or link type does not make its need
is_modifiedin either tool; a need’s own[[…]]does in ubCode (see “is_modifiedafter a dynamic function” above).A
derive_by_typeentry keyedissue,prorcommit, orneedin a project that declares no[[needs.types]], is accepted by sphinx-needs and refused in ubCode (config.derive_invalid, “not a need type”). sphinx-needs registers the GitHub service’s need types in every project, and its default need types are ubCode’s four plus a legacyneedtype, which ubCode does not have; ubCode knows only the project’s own need types (declare the type under[[needs.types]]and ubCode reads the entry too), and creates no service needs, so the rule has no need to compute on here. The values on the need types both tools know are the same. A cycle finding that lists need types names those extra types too in sphinx-needs. Aderivethat cycles on every need type ubCode knows is reported as cycling everywhere in ubCode (“The field keeps its empty value”) and per type in sphinx-needs (”… on every other need type the rule applies”) when sphinx-needs still has a type the cycle leaves out: aderive_by_typeentry on a field of the cycle naming a type only sphinx-needs registers, or entries naming every one of ubCode’s types. A plain cycle, with noderive_by_typeentry on the cycle’s fields, reads as cycling everywhere in both.
- The configuration finding
A rule that cannot be read is
config.derive_invalidin ubCode andneeds.derive_invalidin sphinx-needs: each tool keeps its own family for configuration findings, so oneignoreentry does not cover both.- A field named
parent_needs A
[needs.fields.parent_needs]entry replaces the built-inparent_needslink type in ubCode; sphinx-needs reports and ignores it. A field named after any other link type is refused by both tools.
needextend renders nothing in a built page¶
You write a
.. needextend::(or the MyST{needextend}fence), naming a need id or a filter, with one option per field you want changed.The preview shows the directive as a small chip listing the id or filter you named and the changes you asked for, so an otherwise invisible construct is visible while you author it.
A Sphinx build renders nothing at all for it, and neither does
ubc build html— matching a Sphinx-Needs build exactly.Status — the chip is a deliberate ubCode-only authoring affordance, on the same footing as the chips for
.. index::, the state-changing directives and block hyperlink targets.
The directive’s effect is untouched by any of this.
The extend is applied while your project is indexed,
before any page is rendered,
so every card on every page already shows its post-extend values —
including cards written above the needextend in the same file,
and cards on entirely different pages.
What a built page no longer carries is the directive’s own source:
the id or filter being extended,
and the raw option rows such as +tags —
addressed to an author, not to a reader.
Three things follow that are worth knowing:
A dead extend renders nothing either. A
needextendnaming an id that does not exist, or a filter that matches nothing, renders nothing in a built page just as a working one does. The dead id is still reported, asneeds.extend_missing_id, while you type; a filter that matches nothing stays silent, which is what a Sphinx-Needs build does too.No anchor is lost, because there was never one.
needextendmints no cross-reference target in ubCode, so nothing could have linked to the site. A Sphinx-Needs build does mint one in its document tree, but it does not survive to the page either: docutils moves the id onto the node Sphinx-Needs then deletes, so a real Sphinx build publishes no anchor for aneedextendat all.Site search is unaffected. The chip’s text was already excluded from ubCode’s search index, so removing it from built pages changes no search result.
Before this, a built page printed a framed placeholder box for every
needextend,
so the id being extended and the operators you wrote appeared in the reader’s
prose.
That is what changed.
External need sources¶
An [[needs.external]] source reads another project’s needs.json,
the same job Sphinx-Needs gives needs_external_needs.
Where the two differ, they differ in one consistent direction:
a Sphinx build raises and stops,
while ubCode reports and carries on with as much of the source as it can use —
a source that is momentarily wrong should not cost you the whole project’s index.
These differences reach indexing and diagnostics,
so they change what ubc check and the editor report,
not only what a page looks like.
- A source with no
base_url You write a
[[needs.external]]entry with apathbut nobase_url.ubCode reads the source’s needs and reports one warning for the source saying they have no
external_url, so every reference to them — tables, lists, the:need:role — renders as plain text instead of a link. A source that contributes no needs stays silent: there are no references to be unlinked.A Sphinx build refuses the source outright and fails the build with “base_url must be configured in external_needs item <n>”.
Status — deliberate. URL-less external sources have always been allowed here, so refusing them now would break projects that rely on it. The warning exists because “external needs render as plain text” is the symptom nobody attributes to a missing configuration key. Set
base_urlfor linked references in both.- A
target_urlthat fails to render You write a
target_urltemplate that parses but cannot be rendered against some need’s data — an unknown filter, or an operator applied to a field that is not the type it expects.ubCode ingests every need anyway; the ones whose URL failed simply have no
external_url, and their references render unlinked. The failures are reported once per distinct error message rather than once per need, each naming how many of the source’s needs it cost, the smallest affected id, and the error itself. Read the count as the diagnosis: all of them means the template is wrong, a few of them means those needs’ data is not what it expects.A Sphinx build raises the Jinja error and fails the build on the first need that hits it.
Status — deliberate, on the same “report, don’t stop” principle. A template that does not parse is a separate case, reported once for the source before any need is read.
- A
needs_schemafield_typeof the wrong JSON type You write (or export) a
needs.jsonwhoseneeds_schemagives some property afield_typethat is not a string — a number,null, or a list.ubCode cannot parse the file, so the source contributes no needs at all; the message names the JSON path of the offending property.
A Sphinx build never reads
field_type— it takes only each property’sdefault— so the value is inert whatever type it has.Status — quote the value (
"extra") to fix it. A property that simply omitsfield_type, or states a value ubCode does not recognise, is no longer a divergence in what gets read: it is reported once, naming the JSON path and the accepted values, and the source’s needs are read as before, that property’sdefaultapplied exactly as a Sphinx build would apply it. Only the wrong JSON type still stops the read.
The [test_reports] section¶
ubCode reads the [test_reports] section sphinx-test-reports reads, and adds the same need types and fields. Where the two read it differently:
- The section is the switch
You write
extensions = ["sphinx_test_reports"]inconf.py, and no[test_reports]table and no[parse] extensionsentry inubproject.toml.ubCode adds no test-report need type or field: it does not read
conf.py, so importing the converter’sneeds.jsonreportsInvalid need type: testcase.A Sphinx build registers the types and fields because the extension is loaded.
Status — deliberate: add a
[test_reports]table, an empty one will do, or mirror theconf.pylist into[parse] extensions; both tools then read the same settings.- Switched off with the section kept
You write
"sphinx_test_reports" = falseunder[parse.extensions]beside a[test_reports]section (or under a section inherited throughextend).ubCode keeps the section’s settings and withdraws the extension: no need types or fields, and its directives render as labelled placeholders, unless a need type of the same name owns them. Switched off, the extension ranks below everything else that claims a name: a
[[needs.types]]entry or a[parse.extend_directives]entry of the same name owns it. An explicitfalseunder either of its two names beats an explicittrueunder the other.A Sphinx build has no such switch: the extension is loaded or it is not, and an unloaded one leaves its names to whatever else registers them.
Status — a ubCode-only switch, so one
ubproject.tomlcan carry the settings for a build that does not load the extension.- The directive names belong to sphinx-test-reports
You write a
[[needs.types]]entry whosedirectiveistest-case(ortest-file,test-suite) while sphinx-test-reports is switched on.ubCode gives the name to sphinx-test-reports:
.. test-case::reads a test report, the type is kept, andconfig.conflicting_directive_registrationsays so.A Sphinx build gives the name to whichever registers it last, which for two
config-initedregistrars is the order ofextensions: listingsphinx_test_reportsaftersphinx_needs(the order sphinx-test-reports documents) gives it the name, the other order gives it to the type.Status — deliberate: ubCode reads no
conf.py, so it fixes the documented order.rootdirleft unsetYou write a
[test_reports]section withoutrootdir.ubCode reads relative report paths from the source directory,
[source] dir.A Sphinx build reads them from its configuration directory, the one holding
conf.py.Status — deliberate: ubCode has no
conf.py. The two agree wheneverconf.pysits in the source directory, the usual layout; otherwise setrootdir, which both tools read from the directory of theubproject.toml.- No
conf.pylayer You write
tr_*values inconf.py, or pass them withsphinx-build -D.ubCode reads only the section, with a
-coverride in place of-D:-cwins over theubproject.toml, which wins over the default.A Sphinx build reads
-D, then the section, thenconf.py, then the default.Status — move
tr_*values into the section, where both tools read them.- A need type the project declares itself
You write a
[[needs.types]]entry namedtestcase(ortestfile,testsuite) beside a[test_reports]section.ubCode uses your declaration unchanged, and reports
config.test_reports_type_declaredwhen its title, prefix, color or style differs from the section’s.A Sphinx build stops with “already exists as need type”.
- A link target that is not a link type
You write
property_link_types = { verifies = "verifies_link" }without declaringverifies_linkunder[needs.links].ubCode reports
config.test_reports_link_target_undeclared, and the property’s values are not linked.A Sphinx build’s test-report directives stop at the first test case carrying the property, with
InvalidNeedException; a Sphinx import of aneeds.jsonwarns and drops the key.- A need-type table that leaves keys out
You write
[test_reports.case]with only some of its six keys.ubCode takes each key left out from its default.
sphinx-test-reports refuses the configuration, naming the missing keys.
Status — write all six keys while the project is also built with Sphinx.
- A renamed directive
You write a
directiveother thantest-file,test-suiteortest-case.ubCode keeps the setting, keeps the default name, and reports
config.test_reports_rename_unsupported.A Sphinx build registers the directive under the new name.
Status — renaming is deferred, tracked in #3845.
The test-report directives¶
ubCode supports sphinx-test-reports’ test-file, test-suite and test-case directives
(Test reports: the directives), one need each,
and a test-file’s :auto_suites: / :auto_cases: expansion into a need per suite and case.
Where the two differ:
- A mistake in a directive
You write a
test-casewithout:suite:, a:suite:or:case:the report does not have, a:collapse:that is not true or false, no:file:, or a report that is not well-formed XML.ubCode reports it on the directive (a
needs.test_report_*warning), creates no need for that directive, and carries on; the directive keeps the generic directive box on the page. A directive that loses a duplicate-id contest keeps the directive box too: no second card and no duplicate HTML id, unlike twoneeddirectives sharing an id, which both render a card.A Sphinx build stops with an exception (
TestReportInvalidOptionError,TestReportFileNotSetError, anlxmlsyntax error, a duplicate id, …).Status — deliberate: one bad directive does not stop a build.
One difference runs the other way: an
extra_optionsname is matched as written, so:TestType: unitsets theTestTypefield, where a Sphinx build lowercases the option name and refuses it as unknown (a mixed-case extra option reaches a Sphinx need only through a report property).- An option docutils refuses
You write an option the directive does not take, an option with no value, a value on the
:auto_suites:or:auto_cases:flag, or no title.ubCode reports it (
block.directive_unknown_option,needs.test_report_option_invalidorblock.directive_no_argument) and creates no need for that directive.A Sphinx build reports a docutils error and drops the directive, creating no need either. Both tools treat an unknown option on a
needdirective differently: the need is kept and the option dropped.Status — the same in both.
- A report that does not exist
You write a
:file:naming no file, or naming something that is not a readable file.ubCode reports
needs.test_report_missingand renders the directive as the generic directive box.A Sphinx build warns (an untyped warning, so it cannot be suppressed by type); a
test-filerenders “Test file not found”, and atest-suiteortest-casethen stops the build.Status — deliberate.
- Counters for an attribute the report leaves out
You write a
test-fileortest-suiteover a report whose suites have noskippedattribute (googletest writes none).ubCode counts the skipped cases (
skipped: 1) and deducts them frompassed.A Sphinx build adds
-1for each missing attribute (skipped: -2) and leavespassedas the report states it.Status — deliberate: a counter is never negative. The same rule is proposed for sphinx-test-reports (sphinx-needs#2054).
- A test case’s evidence
You write a
test-casefor a case with several failures, or withsystem-erroutput.ubCode appends every failure, error and skip — each message left out when its text already says it, the text’s own indentation kept — then
System-outandSystem-err: the rendering thetest-reports build needsconverter writes.A Sphinx build appends the first part only, as
**Text**and**Message**, each line stripped of its indentation, thenSystem-out, and noSystem-err.Status — deliberate: one rendering for both routes, the converter’s (sphinx-needs#2055). ubCode keeps every byte of report text literal: whatever line terminator a line of it carries, it stays inside its literal block and is never read as markup.
- Generated ids and
id_regex You write a directive without
:id:, in a project with the default[needs] id_regex.ubCode gives it the generated id (
testfile86527, ortestcase__MathTest__Subtraction_srmht) and does not check it againstid_regex. An:id:you write is checked.A Sphinx build checks the generated id too, and stops: both forms contain lower-case letters.
Status — deliberate.
- A
:file:outsiderootdir You write
:file: ../../secret.xml, or an absolute path, leaving[test_reports] rootdir.ubCode reports
needs.test_report_path_outside_rootand does not read the file.A Sphinx build reads it.
Status — deliberate: the editor indexes a project as soon as it is opened, so a path written in a document may not read files outside the root its configuration names (the
needreport:template:rule). Pointrootdirat the reports instead.- Nested suites in an expansion
The two tools agree since sphinx-needs#2149 (unreleased at the time of writing): a need for every suite at every depth, with the same ids and links; a release before it stops with
KeyError: 'testsuites'at the first nested suite.- Two needs of one expansion with one id
You write a
test-filewith:auto_suites:over a report whose two suite names hash alike atsuite_id_length(the vendoredmany_testsuites.xmldoes at the default 3), or that holds one suite, or one case, twice.ubCode creates the first need, leaves the later one out — a suite with its cases and nested suites — and reports
needs.test_report_duplicate_idwith what would separate the two.A Sphinx build stops with
Suite ID … already existsorCase ID exists— or, for one case in two suites undertr_deterministic_case_ids, with sphinx-needs’A need with ID … already exists. [duplicate_id].Status — deliberate: the same outcome is proposed for sphinx-test-reports (sphinx-needs#2052).
- A
:suite:that names a nested suite The two tools agree since sphinx-needs#2149 (unreleased at the time of writing): a top-level suite of that name first, then the first at any depth; a release before it looks at the top-level suites only and stops with
Suite … not found.- A section titled like a test-report need’s id
You write a section titled exactly like the id of a need a test-report directive creates — one of the lower-case ids,
testcase__usage_test__usage_test_sxckt— on the page of that directive, with the default[parse] id_style = "github".ubCode gives the id to the need card and
<id>-1to the section in the built page, while:ref:links to the section andobjects.invname<id>, which lands on the card; it reportsneeds.test_report_id_shadows_section.A Sphinx build gives the section an id of docutils’ form (
testcase-usage-test-usage-test-sxckt), which a need id never equals, and reports nothing.Status — a gap, until the index reserves these ids as the build does (#3903).
- An expansion whose
test-fileloses its id You write a
test-filewith:auto_suites:whose:id:another need also has (areq).ubCode reports
needs.duplicate; thereqkeeps the id and thetest-filerenders as the generic directive box, followed by the cards of its expansion. Those suites and cases link to the id as their parent — so to thereq; and a suite whose id another need takes leaves its cases linking to that need.A Sphinx build stops when the
test-filecomes second (InvalidNeedException … [duplicate_id]); when the other need comes second, it drops that need with a warning (Need could not be created … [needs.create_need]) and keeps thetest-fileand its expansion — the opposite winner from ubCode, which lets thereqwin in either order.Status — a gap (#3904).
- Order in a
needgantt You write a
needganttover needs from a page that mixes need directives and test-report directives.ubCode draws the test-report needs after the page’s directive needs, by id.
A Sphinx build orders them in document order.
Status — a gap (#3878).
- Need parts in the body
You write a need part (the
:np:role) in the body of atest-case.ubCode records no part on the need, so a
:need:reference to the part (TC_SUB.p1) does not resolve, and reports the role asneeds.need_part(“Need part role not in need directive”): false here, since the directive does create a need, and a failure under--deny warning.A Sphinx build collects the part, as from any need’s content.
Status — a gap (#3877).
- A JSON report
You write a
:file:naming a JSON report (results.json).ubCode reports
needs.test_report_json_unsupportedand creates no need: JSON reports are not read yet ([test_reports] json_mappingis accepted but not applied).A Sphinx build reads it with its JSON parser, under
tr_json_mapping, and creates the need.Status — a gap, until JSON reports are read.
- A test case’s evidence on a MyST page
You write a
{test-case}in a MyST document.ubCode renders the evidence as reStructuredText (literal blocks), as the converter writes it, after the body written in MyST — on the page and in a
needextractof the need alike.A Sphinx build appends its own reStructuredText evidence (the first part only, see above) to the MyST body and parses the whole as MyST, so the evidence’s literal blocks do not render as such.
Status — by design: the evidence is reStructuredText whatever the page’s syntax.
The test-report tables¶
ubCode renders sphinx-test-reports’ test-results and test-env directives
(Results tables).
Where the two differ:
- The suites are real sections
The two tools agree since sphinx-needs#2148 (unreleased at the time of writing): a section per suite (per environment), with the same ids, in the table of contents; a release before it renders each title as a heading at the enclosing section’s level, and stops inside a list item (
AssertionError).- A
:ref:to a suite’s section You write a
:ref:to a suite’s name (:ref:`parent_testsuite`) withsphinx.ext.autosectionlabelon.ubCode labels no section
test-resultsortest-envgenerates: the reference reaches an authored section of that name, or isstd.ref.A Sphinx build with sphinx-needs#2148 (unreleased at the time of writing) labels the suite’s section, and the reference reaches it.
Status — a gap.
- Nested suites
The two tools agree since sphinx-needs#2149 (unreleased at the time of writing): every suite at every depth, each suite’s own cases before its nested suites; a release before it shows the outer suite with an empty table and drops everything below it.
- Counters and
Time: You write
test-resultsover a report whose suites leave out an attribute (failures,skipped,time).ubCode counts the missing counter from the suite’s cases, its nested suites’ included (
Failures: 1), and leaves theTime:line out.A Sphinx build prints
-1for each missing counter andTime: -1.0.Status — deliberate: a counter is never negative, as on the test-report needs (sphinx-needs#2054).
- The reason cell
You write
test-resultsover a case that failed with a message and a traceback.ubCode renders the message as a paragraph and the traceback as a literal block, its lines kept; a skip or error whose message is absent shows only its text.
A Sphinx build joins the two into one paragraph, where the browser runs the traceback’s lines together.
Status — deliberate.
- The table’s frame
You write either directive.
ubCode gives the
test-resultstable its 20 / 20 / 20 / 40 % column widths (test-env50 / 50),ub-test-results/ub-test-envon the table, and norow-odd/row-evenstriping; the row, cell and paragraph classes (tr_<result>, the column names) are the oracle’s. A JSON value is highlighted by ubCode’s own highlighter.A Sphinx build writes no column widths,
docutils align-defaulton the table, and Pygments markup.Status — ubCode’s own table writer; a stylesheet keyed on the
tr_*classes works in both.:data:order and the “not present” warningsYou write
:data: platform, host, nope.ubCode shows the rows in the option’s order, and reports
nopeonce for the directive; a name listed twice counts once, and an empty entry is skipped.A Sphinx build shows the rows in the file’s order, reports
nopeonce per environment, and reports a repeated name and an empty entry (option '') as not present.Status — deliberate (sphinx-needs#1969).
- Values in a
test-envtable You write a variable whose value is
false,null,0,""ortrue, or text with non-ASCII characters inside a list.ubCode writes each scalar as JSON writes it (
false,null,0,true), and non-ASCII text as it is.A Sphinx build leaves every false-ish value’s cell empty, writes
True, and escapes non-ASCII text in a JSON block (\u00f6).Status — deliberate.
- What stops a Sphinx build
You write a report that does not exist, is not well-formed XML, or has no suite; a
.jsonargument totest-results; an environment file that is not valid JSON, is not an object of objects, or starts with a byte-order mark; a non-numeric counter or time in a report.ubCode reports each one as a positioned warning (a note for a report with no suite or a value read past) and renders what it can; a byte-order mark is read past.
A Sphinx build stops with an exception (#2052’s list, and
TypeError/IndexError/ValueError).Status — deliberate (sphinx-needs#2052).
- An option under
test-results You write
:class: foounder.. test-results::.ubCode reports
block.directive_unknown_optionand does not render the directive.A Sphinx build reads the option line as part of the report’s path, and stops on a missing file.
Status — deliberate.
- Row colours in the PDF
You write
test-resultsin a project built withubc build pdf.ubCode draws the tables without the
tr_<result>row colours.A Sphinx PDF build (LaTeX) draws none either; the html builds colour the rows.
Status — ubCode’s PDF has no stylesheet to carry them.
The test-report directive¶
ubCode renders sphinx-test-reports’ test-report directive through its report template
(A whole report: test-report).
Where the two differ:
- The template’s language
You write a template with a format spec (
{id:>8}), a conversion ({id!r}), an index or an attribute ({content[0]},{id.upper}).ubCode reports
needs.test_report_template_invalidwith the construct, its line and column, and renders nothing of the template: a template is{key}for its ten keys and{{/}}.A Sphinx build formats the template with Python’s
str.formatand renders all four — an attribute as a memory address (<built-in method upper of str object at 0x…>).Status — deliberate; a template language with conditions and loops is proposed upstream (sphinx-needs#2142).
- What stops a Sphinx build
You write a template with an unknown key, a lone brace or a numbered field; a
report_templatethat does not exist, or is not valid inimport_encoding; atest-reportwithout:id:or:file:, with a:collapse:that is not true or false, or with the:id:of another report ortest-file.ubCode reports each one on the directive and carries on (a duplicate id is
needs.duplicate, the template’s generatedtest-filebeing an ordinary one).A Sphinx build stops with an exception at the first one (
KeyError,ValueError,IndexError,InvalidConfigurationError,UnicodeDecodeError,SphinxErrorfor no:id:,TestReportFileNotSetErrorfor no:file:, a bareExceptionfor:collapse:,duplicate_id).Status — deliberate.
- In a Markdown page
You write a
{test-report}fence in a MyST page.ubCode reports
needs.test_report_markdown_hostand renders nothing.A Sphinx build reports
Directive 'test-report' cannot be mockedand renders nothing.Status — the same outcome, as a positioned finding.
- A section in the template
You write a template with a section title.
ubCode nests the section one level under the section the
test-reportstands in, as it does for an.. include::d file’s section.A Sphinx build reads the title in the page’s own title styles: a section underlined like the page’s title becomes a sibling of it, and the page’s next sections move under it.
Status — the include rule (a registered divergence of
.. include::).- Where a finding in the template points
You write a template with a reStructuredText mistake, or a body with one.
ubCode reports a mistake in text the template wrote — on its own lines, or before or after
{content}on a body’s line — at the directive, naming the template and its line, and one in the body at the body’s own line. Inside a body line that holds a tab, columns right of the tab are tab-expanded.A Sphinx build names the page with the template’s line, or with a line past the page’s end.
Status — deliberate: every position is a real line.
{template_path}You write
{template_path}in a template, or use the shipped one, which does.ubCode writes the template’s path relative to the
ubproject.tomlthat set it,test_report_template.txtfor the shipped one.A Sphinx build writes the absolute path, on the machine that built it.
Status — deliberate: a page is the same wherever it is built.
- The shipped template’s tables
You write a
test-reportover a report of more than 100 cases, with the shipped template.ubCode lists every case: its copy of the template sets
:max_items: 0on its three tables.A Sphinx build lists every case: its tables have no cap.
Status — the same outcome; a template of your own follows
[needs] views_max_items.- A report inside a file the template includes
The two tools agree: with a template holding
.. include:: inner.txtand atest-reportininner.txt, both render the inner report once and report the inner template’s inclusion ofinner.txtas circular (directive.include_circularin ubCode). A report’s template should not include a report.{content}’s indentation and the template’s encodingYou write
{content}twice, or once after an escaped{{content}}; a template starting with a byte-order mark;import_encoding = "latin-1".ubCode indents each
{content}like its own line, drops the byte-order mark, and reads the encoding names of the WHATWG standard (latin1, notlatin-1).A Sphinx build indents every
{content}like the first line holding{content}, the escaped one included, renders the byte-order mark into the page, and reads Python’s codec names.Status — deliberate.
ubCode-only view filters¶
The needlist, needtable and needflow view directives accept a
ubCode-only :cypher: option —
a read-only Cypher selection
(a bare WHERE-style expression such as n.status = 'open',
or a full single-node-column query MATCH (n) ... RETURN n) —
in place of the portable python filter surfaces.
Two of the three directives read a richer query as more than a selection:
a needtable’s can name its columns, and a needflow’s can name its
edges. Both are covered in their own sub-sections below.
A fourth directive, needpie, takes the same option with a different
meaning. It has no selection for a query to replace — a chart’s selection is
its content, one filter per line — so the query scopes the chart instead.
That reading has its own sub-section below too.
You write a
needlist,needtableorneedflowwith a:cypher:option.The preview shows the needs the Cypher query selects, exactly as a
:filter:selection would.A Sphinx build reports an error on Sphinx-Needs 8.3.1 and earlier: those releases have no
:cypher:option, so the unknown option is a directive error. Sphinx-Needs 8.4.0 accepts the option as a no-op (sphinx-needs#1760), so from that release on a Sphinx build simply ignores it —needlist,needtableandneedfloweach declarecypherin their option spec and nothing reads it.Status —
:cypher:is a deliberate ubCode extension; Sphinx-Needs will not evaluate it, and the interoperability cost is accepted.
:cypher: takes precedence over the python filter surfaces
(:filter: / :status: / :tags: / :types:)
rather than conflicting with them.
When both are present ubCode evaluates the Cypher selection
and ignores the python surfaces.
Carrying both is therefore the recommended portable form, not a mistake: ubCode reads the Cypher, a Sphinx build reads the python surfaces, and one source builds under both tools.
.. needtable::
:cypher: MATCH (n:spec) WHERE n.status = 'open' RETURN n
:types: spec
:filter: status == 'open'
The block above is shown as source only.
For the same pairing rendered live —
by this Sphinx build and by ubc build html alike,
on the tutorial’s own model —
see Selecting with Cypher.
Keeping the two selections in sync is the author’s job.
ubCode does not check that a Cypher query and a python expression
select the same needs —
proving that in general is not possible,
so nothing warns about the pairing
(the needs.filter_conflict warning that used to greet it was retired).
A divergence shows up honestly,
as the two builders rendering different needs.
A needtable’s :cypher: can also name its COLUMNS¶
On a needtable, a :cypher: that returns more than the bare node
defines the table’s columns and headers
(see Projections in a needtable),
so a :cypher: table’s presentation is ubCode-only too,
not only its selection.
You write a
needtablewhose:cypher:projects several columns.The preview shows one column per
RETURNitem, headed by itsASalias or its expression text.A Sphinx build renders the table from
:columns:(or the configured default column set), ignoring the query entirely.Status — deliberate, and the reason
:columns:alongside a projection draws no warning: it is the Sphinx half of the same dual-builder form.
How close the two builders stay depends on the shape of the projection:
Projection |
Dual-builder story |
|---|---|
single anchor, per-need property columns only,
from a |
Portable. One row per need in both builders, and every projected
column has a The |
single anchor, but row-multiplying or computed
( |
ubCode-only, for the same two reasons as the row below, and worth
its own row because “one anchor” is not what makes a projection
portable. |
multi-variable
( |
ubCode-only. A traversal repeats its anchor once per match, so a
Sphinx build renders FEWER rows — and |
The discriminator is therefore does the query multiply rows, and can every
column be named in :columns: — not “how many variables does it mention”.
Three smaller differences inside a projected table:
A need’s LINKS are relationships, not node properties.
n.linksandn.satisfiesare therefore not properties at all: they project asnull— a blank cell — and the query is reported asneeds.cypher_vocabulary, on the:cypher:option, with a note that names the link type and shows the traversal. Traverse instead:MATCH (n)-[:satisfies]->(m) RETURN n, mrenders linked chips. (A need’s own FIELDS are properties and project normally, son.tagsis correct and renders its entries joined by a semicolon and a space, exactly as a:columns:cell does.)That is also the contrast with
n.id, which is a property and the node’s own: its value is a need id by construction, which is why it renders as a linked id cell while a list of link targets cannot.On a
:show_parts:part row, a projected column shows the PARENT’s value — so a projected column can disagree with the parity column of the same name in the same table. The parity path OVERRIDESID,TITLE,CONTENT,IS_PART,IS_NEEDand the link columns on a part row; a projection overrides none of them. A projectedn.titletherefore shows the parent’s title where:columns: TITLEshows the part’s own content.A null anchor fails the table. An
OPTIONAL MATCHthat matches nothing yields an all-null row, and a needtable row with no need behind it has no id cell and nothing for:style_row:to evaluate against. ubCode reports the failure rather than dropping the rows silently.
A needflow’s :cypher: can also name its EDGES¶
On a needflow, a :cypher: that returns paths or relationships
supplies the diagram’s edges as well as its nodes
(see Edges in a needflow),
so a :cypher: diagram’s shape is ubCode-only too,
not only its selection.
You write a
needflowwhose:cypher:returns paths or relationships —RETURN p,RETURN n, r, m, or a bareRETURN r.The preview shows exactly the edges the query matched, between the needs those edges touch.
A Sphinx build ignores the query and draws its own diagram from
:root_id:/:root_direction:/:root_depth:/:link_types:and the python filter surfaces.Status — deliberate, and there is no python dual form: a diagram whose edges come from a query cannot be expressed in the option model at all, so this is the one
:cypher:extension whose two builders draw different diagrams rather than the same one. That is also why the walk options draw no warning beside such a query: they are the Sphinx half of the pairing, ignored by ubCode and read by Sphinx-Needs, exactly as the python surfaces are beside any:cypher:.
Why the two diagrams differ at all is the point of the feature.
Without a graph-returning query — in ubCode and in Sphinx-Needs alike —
a needflow draws every allowed link between the needs it selected,
so the edge set is a function of the node set
and :link_types: is the only lever on it.
Moving that lever moves the node set with it,
because one option gates both the walk and the draw.
A query that returns paths gives the edges directly,
so the same needs can be drawn with different edges.
Sphinx-Needs has no equivalent, and will not gain one.
Carrying the walk options alongside is therefore the recommended form,
for the same reason carrying the python surfaces alongside a :cypher:
selection is: it gives a Sphinx build something to draw.
Expect the two pictures to differ — that is what the feature is for —
and keep them recognisably about the same needs.
.. needflow::
:cypher: MATCH p = (r {id: "T_CAR"})<-[:specifies]-()<-[:tests]-()
RETURN p
:root_id: T_CAR
:root_direction: incoming
:link_types: specifies, tests
Two smaller differences inside a query-drawn diagram:
A part edge loses its dotted styling. Parts are not nodes in the needs graph and a matched relationship carries only its type, so a query-given edge cannot know it crossed a part: it draws as a plain edge to the part’s parent need, where the option model draws it dotted. And a link to the need’s OWN part is not drawn — in the graph the two spellings are one relationship, so it arrives as a self-loop, and the default model suppresses that loop too.
An unbounded
*pattern is a build risk. A variable-length pattern enumerates paths rather than testing reachability, so its cost grows exponentially with the bound. A view directive runs under a short build budget, and an unbounded pattern over a couple of hundred densely linked needs will exceed it — reported asbuild.need_view_failed, with an error block on the page in place of the diagram. Bound the pattern (*1..3).
A chart’s filter options SCOPE its slices¶
On a needpie or a needbar, a directive-level filter option is not a
selection at all —
there is nothing for it to select, because a chart’s selection is its content,
one filter per line (needpie) or per separator-split cell (needbar).
It is a scope: the needs it selects are the universe those cells are counted
in, so each slice is that cell’s filter intersected with the scope.
One concept, two spellings.
The portable one is the four python surfaces :filter:, :status:,
:tags: and :types:;
the ubCode-only one is :cypher:, which takes precedence when both are
written.
Writing both is the recommended form, exactly as it is on needlist,
needtable and needflow:
ubCode evaluates the query, a Sphinx build reads the python surfaces, and one
source then counts the same numbers under either engine —
subject to the needs-only counting recorded in Needs chart views (needpie / needbar)
(ubCode counts needs, a Sphinx build counts needs and their parts), which a
paired scope neither causes nor cures.
You write a chart with a scope —
:cypher:, or the four python surfaces, or both — and one filter per content cell, as usual.The preview shows each cell counting only the needs that are BOTH in the scope and matched by that cell’s filter.
A Sphinx build reads the python surfaces and scopes by them, so a chart whose scope has a python spelling shows the same numbers in both engines — as far as the SCOPE goes; a project using need parts still counts more there than here (Needs chart views (needpie / needbar)). Given a bare
:cypher:and nothing else, it counts each cell over the whole project instead, so every slice is larger (or equal).On Sphinx-Needs 8.4.0 and earlier none of the five options is known on a chart: the build reports
unknown optionand drops the directive, so the view disappears from the page. The accept-and-ignore change for:cypher:on both charts shipped in Sphinx-Needs 8.5.0 (sphinx-needs#1818), and the four python surfaces are the chart’s scope there from that same release (sphinx-needs#1831).Status — deliberate. A paired scope is portable and its numbers agree. A bare
:cypher:is the one:cypher:extension whose no-op changes a rendered number rather than a selection or a shape; that divergence is accepted and documented, and nothing warns about it.
Two things follow from “scope, not slicer”, and both are what makes the option usable rather than a second filter language:
the lines keep slicing. Add a content line and you add a slice, remove one and you remove a slice, exactly as without a scope. A bare non-negative integer line is untouched — it is its own value, not a count of anything — so a fixed reference slice stays fixed inside a scope.
a scope alone draws nothing. A
needpiewhose body is empty renders the empty-result paragraph whether or not it carries a:cypher:.
The reason to reach for it is the reason :cypher: exists everywhere else:
a question about the links, which ubCode’s python filter dialect cannot ask.
Here the scope is the requirements that some specification realises,
and the two content lines then split that set by status:
.. needpie:: Realised requirements by status
:cypher: MATCH (n:req)<-[:links]-(:spec) RETURN n
:labels: open, closed
:legend:
status == 'open'
status == 'closed'
The block above is shown as source only, and no longer because a Sphinx build
would refuse it: this build pins Sphinx-Needs 8.5.0, which reads all five scope
options.
It stays source because its scope is a traversal, which has no python twin,
so rendering it would publish two different sets of numbers — the very
divergence this sub-section exists to record — and because the register’s
examples are written against a generic req / spec model that this
project does not define.
For a scope that does pair, rendered live by this Sphinx build and by
ubc build html alike, see Selecting with Cypher.
There is a portable python dual form for a scope, and it is the same four
options every other view uses.
This paragraph used to open “there is no portable python dual form for a scope
yet”, and gave the reason:
“a chart takes none in either tool today: the only place a Sphinx build could
read an equivalent restriction is inside each content line, which means
writing the scope into every line by hand.”
That is no longer the only place.
Both charts now read :filter:, :status:, :tags: and :types: as a
scope, and a Sphinx-Needs release carrying those options reads them the same
way, so the pairing that works on needlist, needtable and needflow
works here too:
.. needpie:: Open requirements by kind
:cypher: n.type = 'req' AND n.status = 'open'
:types: req
:status: open
:labels: safety, other
"safety" in tags
"safety" not in tags
The one thing the pairing cannot carry is a traversal.
ubCode’s python dialect judges each need on its own fields and cannot follow a
link, so a scope like MATCH (n:req)<-[:links]-(:spec) RETURN n has no python
spelling — in a content line or in a scope option — and there the two engines’
numbers still differ.
Folding a plain field test into every content line by hand remains possible
and is now pointless: write it once, in the scope option.
A Sphinx-Needs filter string can reach other needs, through the needs
view its filter context binds.
That form is named here only so it is not mistaken for a route:
ubCode does not accept it,
and even in a Sphinx build it does not do what a scoped slice needs.
Sphinx-Needs narrows the needs view by an id / type / status / tags
test anded beside the traversal before binding it,
so the natural one-line spelling —
status == 'open' and <traversal over needs> —
is evaluated against a narrowed universe and,
whenever that narrowing drops the needs the traversal walks through
(the usual case: a specification carries no status),
counts nothing.
The bare traversal alone works.
So expect the numbers to differ, and keep the two engines’ charts recognisably
about the same needs.
A mistyped label or property in a :cypher: scope is caught before it quietly
changes every slice: it is reported as needs.cypher_vocabulary, positioned on
the :cypher: option, exactly as on the other three directives.
An unparseable :filter: scope is reported as needs.filter_invalid on the
:filter: option, and is silent whenever a :cypher: is written beside it —
the option that loses the precedence is not evaluated, so it is not complained
about either.
A query that does not parse, or one that returns anything other than the matched
node itself, is reported as needs.cypher_invalid at edit time and again as
build.need_view_failed when the page renders —
and on the page the chart itself is replaced by an error block:
the failed scope is a recorded failure, which the writer shows in place of the chart,
and every filter slice would have counted 0 in any case.
Note that this replaces the whole chart:
the scope is resolved before any content line is looked at,
so even an all-literal pie (whose numbers no scope could change) shows the
error block rather than its literals.
That is the standing “nothing silent” posture — a chart is never left looking
like data when its selection failed — and not an accident.
A failing scope has four postures, and what separates them is which LAYER can see the defect — the handler while you type, the index-time vocabulary sweep, or only the evaluation itself — not which spelling you wrote it in:
the defect |
at edit time |
at build time |
on the page |
|---|---|---|---|
|
|
|
error block, no chart |
|
|
nothing |
the chart, every filter cell 0, literal cells kept |
either spelling parses but cannot be executed — an unknown field, a
type error, a |
nothing |
|
error block, no chart |
a |
|
|
the chart, every filter cell 0, literal cells kept |
Row two is the one whose defect is reported once: the complaint is already in
your editor, so a second build-side copy would be a second report about one
mistake, and suppressing it is the posture needlist has had all along.
Row three is the one to know about, because it is the only row with no warning
of any kind before the build.
Both spellings reach it: the check behind needs.filter_invalid is a parse
check, so :filter: bogus_field == 'x' is accepted while you write it; and the
only edit-time gate on a :cypher: reads the query’s form, so a query that
parses and returns a single node column — which is every well-formed query, a
link traversal included — passes it and can still fail against the index.
Either way the failure surfaces only when the build resolves the scope.
Row four is the one that looks like row one and behaves like row two, and it is
why “which spelling” is the wrong question to ask.
A mistyped label is not an execution failure at all: the query engine follows
openCypher null semantics, reads the missing name as null and returns an
empty result rather than raising — so the chart draws its zeros with the literal
cells kept, exactly as an unparseable :filter: does.
What saves you is the separate index-time vocabulary check described above, which
reports needs.cypher_vocabulary on the option in the editor and on the
build’s warning stream.
needbar reads a scope too, in both spellings.
This section used to end “needbar does not offer the option.
Nothing about the reading is pie-shaped — a bar’s cells would scope identically
— so this is a deliberate hold, not a limitation of the idea.”
The hold is over, on that same reasoning.
One difference between the two charts is worth knowing before you scope a bar.
A needpie whose slices sum to zero renders the empty-result paragraph (with
the :filter_warning: override, if set), so a scope that matched nothing says
so.
A needbar has no all-zero empty state and no :filter_warning: —
upstream parity — so it draws, with zero-height bars and no message.
The empty paragraph itself is not chart-kind-specific: a bar whose grid is
empty (rather than zero) renders it like a pie does.
What a bar lacks is the all-zero trigger, which is exactly the case a scope that
selects nothing produces.
On the page itself a mistyped bar scope is therefore the quietest mistake in the
family, which is why the vocabulary check above matters most here:
in the editor it is still a squiggle on the option.
Filter expression syntax¶
ubCode evaluates :filter: options and :need_count: targets with its own
filter engine, which accepts the whole portable subset Sphinx-Needs documents
(comparisons in either operand order, in / not in, is None,
and / or / not, var.* variant fields, search(), and a bare
True / False).
A few forms Sphinx-Needs accepts are deliberately not accepted here;
each raises a needs.filter_invalid warning (with the source column) and
renders an empty result, and each has a workaround.
Raw-string prefix¶
You write a filter using a raw-string regex, such as
search(r"[\w\s]+", title).A Sphinx build accepts it (Python treats
r"..."and"..."alike).Status — drop the
rprefix and writesearch("[\w\s]+", title). Regex escapes that are not also Python string escapes —\w,\d,\s— are preserved in an ordinary string, so the pattern is unchanged. But\b(word boundary),\nand\tdo change meaning without therprefix (Python reads them as backspace, newline, tab): keep those as an explicit character class or double the backslash (\\b). Note also thatsearch()patterns must be valid for ubCode’s regex engine, which does not support lookaround ((?=...),(?<=...)) or backreferences (\1) — such patterns raiseneeds.filter_invalid.
Tuple or set containers¶
You write a membership test against a tuple or set literal, such as
id in ("a", "b")orid in {"a", "b"}.A Sphinx build accepts the tuple/set form.
Status — use a list literal instead:
id in ["a", "b"].
Ordering comparisons against non-numbers¶
You write an ordering comparison (
</<=/>/>=) whose other operand is a string, boolean orNone— in either operand order, such asdeadline < "2024-06-01"or"beta" < status.A Sphinx build accepts it: Python applies the operator to the values at runtime, and only raises if the types are not orderable.
Status — the ordering operators support numeric literals only (
priority > 3,5 <= level); equality (==/!=) and membership (in) work for strings. Use a numeric field for ordering. Date-string ordering has no direct portable equivalent — model the date as a sortable numeric field, or select with equality /in.
Unrecognised function calls¶
You write a filter calling a function other than the recognised helpers, such as
bool(arch).A Sphinx build accepts it (its
evalhas the Python builtins available).Status — the only calls the engine recognises are
search(),len(),.lower(),.upper(),.startswith()and.endswith(). Forbool(arch), write the bare fieldarch— truthiness is supported (0,""and[]are falsy, as in Python).
Site output¶
Page redirects (sphinxext-rediraffe)¶
You write [build.html.redirects] in
ubproject.toml, a table mapping the path of a page that moved to the page
it became.
A Sphinx build does the same job through
sphinxext-rediraffe:
rediraffe_redirects names either a dict in conf.py or a
whitespace-separated redirects.txt,
and the extension writes a small forwarding page at each old location.
The preview has nothing to show — a redirect is a property of the built site,
not of a document.
ubc build html writes a forwarding page at each old location,
carrying the reader on with their ?query, and with their #fragment
unless the entry names a section of its own.
Status — implemented, with these deliberate differences from rediraffe:
A deleted entry deletes its page. rediraffe records the stubs it has written in
_rediraffe_redirected.jsonand only ever adds to that record, so a redirect you remove from the configuration leaves its forwarding page on the site forever. ubCode’s forwarding pages are ordinary managed output: remove the entry and the next build prunes the page, and any directory it emptied — in an output directory the build owns, which is the projection’s standing rule for every file it manages (a directory ubCode did not create is never pruned).An existing forwarding page is rewritten, not skipped. rediraffe takes a fast path for any stub already in its record, so a change to the template never reaches a site that was built once. ubCode recomputes every forwarding page’s bytes on every build and writes only what differs — so a fix reaches an existing site, while an unchanged redirect keeps its file and its timestamp.
A loop warns; it does not abort the build. rediraffe raises an
ExtensionErroron a circular redirect and the whole Sphinx build dies. ubCode reportsbuild.redirect_cycle, skips the entries in the loop, and builds the rest of the site — which still fails the default quality bar, so nothing is quietly accepted.A faulty table fails loudly. Every entry ubCode cannot use names itself in a warning — an unreadable key or target, a refused URL scheme and two spellings of one page are all
build.redirect_invalid. rediraffe’s file parser has silent failure modes — a file saved with a byte-order mark, for instance, writes a forwarding page under a mangled name and reports success.A key that names a real page never overwrites it, whatever the target. The author’s page keeps its own content and
build.redirect_source_existssays so.Targets are validated. A redirect whose chain ends at a document the build renders no page for raises
build.redirect_target_missingand writes nothing, rather than a page that forwards to a 404.The forwarding page replaces the history entry rather than pushing one, so Back from the new page returns where the reader came from instead of bouncing them forward again.
The reader’s URL is reassembled, not concatenated. A target may carry its own
?queryand#fragment; each component the target sets wins, and only an absent one is taken from the reader’s URL. Without JavaScript the fallback refresh reaches the target page but can carry neither.Fragments are understood, on both sides. A target may name a section (
"guide/new#install"), and so may a KEY — rediraffe has neither. A section-level entry cannot work through a forwarding page (a fragment never reaches the server), so it works in the reader’s browser instead: on a page that still exists it rides the site’s own script, and on a page that is itself redirected it travels in that page’s forwarding page. A section that is still there always wins, so an entry that has not really moved is inert rather than surprising — and says so (build.redirect_anchor_shadowed). The liveness check honours<a name>as well asid, matching what a browser resolves; rediraffe’s own anchormap proposal checksgetElementByIdalone.A reader is forwarded once, across both layers. If the page moved and the section moved, the forwarding page already carries the section’s new destination — its map pre-merges the map of the page it forwards to. Composing the two layers naively (which is what rediraffe’s anchormap proposal does) costs the reader two navigations.
Target sections are checked against the ids the target page actually renders — section slugs, the ids generated for repeated or empty titles, anchors written by hand in a
rawblock, and all — and reported asbuild.redirect_anchor_unknownwhen the page has no such anchor. Both levels are checked: an entry naming a section of its target is checked whether or not its KEY names one.The reader’s query rides every hop. Their
?highlight=or?q=is theirs whichever layer moved them — the forwarding page’s or the site script’s — unless the destination names a query of its own, which wins. rediraffe concatenates instead, which turns an external target’s own query into?a=1?b=2. Their fragment behaves differently, and deliberately: it rides a page redirect (nothing consumed it, so it is still theirs), while a section redirect replaces it with the destination the entry names — or with nothing, which means the top of the new page. Re-attaching it there would send the reader to the one anchor the entry exists to say has gone.
Not ported, deliberately:
no
redirects.txtfile form — the table lives inubproject.tomlwith the rest of the project’s configuration;no
rediraffecheckdiff/rediraffewritediffbuilders;no
rediraffe_template— the forwarding page is fixed;no
anchormapdirective — rediraffe’s open proposal authors section redirects in the moved page’s own source; ubCode keeps them in the same configuration table as the page redirects, so one place answers “where did this go”;no wildcard keys (which is a
sphinx-reredirectsfeature rather than a rediraffe one).
Listing "sphinxext.rediraffe" in [parse].extensions
is harmless and does nothing: it is not one of the extension ports, and
[build.html.redirects] is not gated on it.
Closing the gap¶
The preview grows release by release:
ubCode actively closes the gap with Sphinx and Sphinx-Needs —
substitutions, toctree rendering, resolved footnotes and citations,
and named hyperlink references have all moved off this page’s
“not yet rendered” list recently —
and the constructs still shown as placeholders
are tracked on the Roadmap.
When you need the final, fully-transformed output, run a full sphinx-build.