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.
- Footnotes and citations
You write footnote and citation references such as
[1]_,[#]_, or[CIT2002]_, with their matching definitions.The preview shows each reference as a superscript label, but the label is not linked or resolved to its definition.
A Sphinx build numbers auto footnotes, resolves each reference, and renders bidirectional links between reference and definition.
Status — reference resolution is a whole-document transform the preview does not run. Indexing and diagnostics are unaffected.
- Hyperlink references
You write a named reference such as
target_or`phrase`_to a target defined in the same document — an external link target (.. _t: https://example.com), an indirect target (.. _alias: t_), an explicit internal target, or a section title.The preview shows the reference linked to a slug guessed from its name (
#target): correct for a same-page section or internal target, but a dead link for an external or indirect target.A Sphinx build (and
ubc build html) resolves the reference through the document’s target table — a reference to an external target renders the real URL, a reference to an indirect target follows the chain to its resolved end, and a reference to no target (or to a duplicated name) is reported as an error.Status —
ubc build htmlresolves external and indirect references, and this does affect diagnostics: a dangling or ambiguous named reference now raises an error (ref.unknown_target,ref.duplicate_target,ref.circular_indirect,ref.nonexistent_indirect,ref.anonymous_mismatch) in indexing,ubc check, and the build warning count. The live preview keeps the slug guess (reference resolution is a whole-document transform it does not run per keystroke).- 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.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 two PlantUML:config:direction names, which keep working as the lower-precedence source; when a:direction:and the direction a:config:names 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 same route the ubCode-only:cypher:/:max_items:/:width:/:height:options took). 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 always renders Mermaid.
:engine:(graphviz/plantuml) is accepted and ignored.:config:(Graphviz/PlantUML style snippets) is likewise accepted and ignored, except the two PlantUML direction names —lefttorightselects a left-to-right flowchart andtoptobottoma top-down one (the default) — which are the low-precedence source for:direction:above.: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.3.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).No click-through — 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.
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:name:``d diagram carries a stable ``idyou 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.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.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.: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 empty-state and no:filter_warning:option, and ubCode does not invent one.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.No filter options — as upstream, neither chart takes
:filter:,:status:,:tags:or:types:; the ubCode-only:cypher:and:max_items:are not offered either. A chart renders counts rather than per-need items, so a cap would silently change the numbers rather than truncate a list.
- 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 directive origin — imported, external,src-trace— have no position in this project’s documents and sort after the authored ones, by id.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:name:``d chart carries a stable ``idyou 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[needs] variant_data_fileand[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 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.
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.
The directives that accept :name: today are the figure-class directives
(figure, table, csv-table, list-table, math,
code-block, image, the admonitions, container and their kin),
the diagram directives mermaid / plantuml / uml,
the two views needflow and needgantt,
the index directive, whose anchor is the only thing it renders at all,
and the directives ubCode does not render yet but still registers a target for
— graphviz, digraph and graph —
whose anchor rides the placeholder block they render as.
Extending it to the remaining views (needsequence, needtable,
needpie, needbar, needlist) is tracked in
useblocks/ubcode#2975.
Four further points belong to the same rule:
A
:name:that normalises to nothing gets no anchor, and the target ubCode publishes for it lands nowhere on the page. The slug is built by dropping every character that is not an ASCII letter or digit, so a name with no ASCII letters at all normalises to the empty string — which is every purely numeric name (:name: 123) and, in practice, every name written in a non-Latin script: Chinese, Japanese, Korean, Cyrillic, Greek, Arabic, Hebrew, Thai and so on. The page then carries noid, while the entry ubCode publishes inobjects.invfalls back to a positionalid<n>anchor the page never emits — so:ref:to it, from this project or from another one through intersphinx, lands nowhere. Nothing warns about it. Give such a view an ASCII:name:and write your own display text (:ref:`交付计划 <delivery-plan>`), which is the form you want anyway — see the:ref:note below. A Sphinx build has the same restriction on the slug itself (the rule is docutils’make_id), but resolves the reference against its own document target table rather than against the emitted anchor, so it does not produce this particular dead link.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.
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 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.
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.
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.
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. Such a page gets no table-of-contents link either way, though 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:
An escaped backslash (
\\) inside aparsed-literalbody — or in ordinary prose — is dropped along with the line break that follows it, where a Sphinx build keeps both. A pre-existing limitation of the inline parser’s escape handling; it is listed here because<pre>is where a lost line break shows.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 and footnote definitions apply to that file’s own
directive titles, not the partial’s.
A [label] defined in the partial itself stays literal there.
Markdown link cross-references¶
A Markdown link destination that names project content becomes a
cross-reference — see the authoring guide for the
forms.
Seven 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
#fragmentdoes not yet reach a plain heading. A fragment reaches an explicit(target)=, a label or a document, but a heading that is no target at all is not resolved — neither[](#some-heading)on the same page nor[](other.md#some-heading)across files. Both report the failure and keep their text. Give the heading an explicit target to link to it:(some-heading)= ## Some Heading
A Sphinx build resolves the same-page form but not the cross-file one: it can only see the target document’s MyST slug metadata, so a fragment naming a plain heading — and every fragment into a reStructuredText document — warns and drops the link text while emitting an href that happens to work. Heading anchors are being designed as one piece; see issue #3082.
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.There are no GitHub-style heading slugs. A heading anchor is the one docutils-parity id ubCode mints for it, and a
#fragmentmatches that.myst_heading_anchorsadds a second, GitHub-flavoured slug layer in a Sphinx build, and the two layers agree on ordinary wording — bothMy Heading!andMy Headingare#my-headingeither way. They 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.
An equivalent of that setting is a possible future addition.
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.: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.
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:
The preview highlights client-side with a bundled set of common languages, not Pygments — colours differ from your Sphinx theme, and a language Pygments knows but the preview does not is shown as plain preformatted text (never mangled).
The bundled set includes
rst/restructuredtext,myst, andcypher, highlighted with community grammars rather than Pygments lexers, so token boundaries can differ slightly from a Sphinx build.: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 cannot, because the preview’s client-side highlighter reads the
<code>element’s own text as its input and replaces its contents. 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 highlights client-side in permissive mode and never emits that warning, 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).
Highlighted inline code — a role derived from :code: with a
:language: option — follows the same client-side rules as the blocks above
(non-Pygments colours; an unrecognised language renders plain),
with three differences of its own:
The element carries only the language class, and the colouring happens in the browser. A Sphinx build writes Pygments token spans into the HTML itself (
<span class="mi">1</span>), and standalone docutils writes them too but with its own longer class names (<span class="literal number integer">) — the two upstreams already disagree here.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 by neither the preview nor
ubc build html —
a Sphinx build logs a highlighting_failure warning before falling back to
unhighlighted output,
and standalone docutils also reports a warning,
then renders the role’s source text as a problematic node in place of the span.
Because highlighting happens in the browser,
neither the preview nor a build knows which languages the bundled grammars
cover, so both degrade silently.
The same holds for an unrecognised language on a code block.
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.A
:name:on an image written inside a substitution definition is registered as a cross-reference target but never rendered as an anchor, so:ref:to it — and the entry ubCode publishes inobjects.inv— lands nowhere on the page.A Sphinx build rejects that construct outright (
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 it silently, which is the difference worth knowing. If you need the anchor, name a plain.. image::or.. figure::instead — there:name:does render an id.
: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.
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:,:scale:,:config:,:title:, and:zoom:are parsed but not applied.: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:) — unless the name normalises to nothing, which every non-Latin-script name does. 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).
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 / definition lists, tables, admonitions, images and figures, literal and math and raw blocks, block quotes, containers, compounds, topics, sidebars, rubrics and line blocks. 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; and before a field list upstream renders
<dl class="… field-list simple">.- 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.- 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.
: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.
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.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, 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. 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 stays visible but collapsed and greyed out, labelled with its condition; a Sphinx-Needs build omits the block 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
needtablerenders as a static table inside a scroll frame with a sticky header row, whatever its:style:(DATATABLES, the Sphinx-Needs default, renders the same static table). Interactive client-side sorting, filtering, and pagination are a planned enhancement; the scroll frame and sticky header are the interim.: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 the four built-in dynamic functions (echo,test,copy,links_from_content) and renders??for anything else — where a Sphinx build also resolves project-defined functions and its larger built-in set; 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 as it does everywhere else. Sphinx-Needs refuses such a declaration outright —Field 'is_part' already used internallyat startup — so no Sphinx build that runs can observe the difference.)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_complete,id_parentandpartsare columns ubCode does not answer at all; they render 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, and the owning need’s id isid_parent, which stays unanswered.: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,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 themid_complete,external_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) but is not a column. 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.
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.
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.
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 has since accepted the option as a no-op (sphinx-needs#1760, unreleased as of 8.3.1), so on a release carrying that change a Sphinx build simply ignores 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'
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.
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 any ?query and #fragment they arrived with.
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. A target may name a section (
"guide/new#install"). A key may not — a fragment is never sent to a server — and such an entry is refused withbuild.redirect_invalidrather than silently ignored.
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 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 and toctree rendering moved off this page’s
“not yet rendered” list recently —
and the constructs still shown as placeholders,
such as resolved footnotes,
are tracked on the Roadmap.
When you need the final, fully-transformed output, run a full sphinx-build.