What works inside imported need content

When you import needs with .. needimport::, ubCode reads each need’s content and treats it as markup, not as a dead string: cross-references resolve, need parts are extracted, and the body renders.

It does not treat it as your markup. The content came from another project’s export, so a handful of constructs are deliberately refused. This page lists every one of them, what you see instead, and how to lift the restriction where that is possible.

You will not have to find this page by accident. Every refusal reports itself at the .. needimport:: directive, and where a setting would allow the construct the message names that setting. If something in imported content did not render, look at the warning first — it should tell you the answer, and this page tells you why the rule exists.

Note

Nothing here applies to need content you write yourself. These rules are about content that arrives from outside the project.

The short version

Ordinary requirement bodies are unaffected. Emphasis, lists, tables with inline data, admonitions, code blocks and inline diagrams work exactly as they do in your own files, and so do cross-references — a :need:, :ref:, :doc: or :term: written inside imported content resolves against your project, whether or not anything else in it happens to reference the same target.

One cross-reference family is the exception: an explicit intersphinx reference (:external:…:) inside imported content does not resolve, and renders as plain text. Nothing else in the family behaves that way; if you need it, say so on the tracking issue.

What is refused is a small set of constructs that reach outside the content — either to a file on disk, or into the parse itself.

What you see on the page

Each .. needimport:: renders one card per imported need, at the directive’s own place in the document, in ascending ID order. A card looks like a need you wrote yourself: the same header, the same metadata table (with needextend overlays and incoming links applied), and the need’s content as its body.

Imported needs are also clickable from anywhere a need renders — a :need: role, a needlist row, a needtable cell, a link in another need’s metadata — and land on the card, on the page whose needimport produced it.

If the same ID arrives from two imports, only one card is rendered: the one the project already resolved the ID to. The other site shows nothing for that ID, and the existing duplicate-ID warning tells you about the clash. A locally authored need always wins over an imported one; between two imports, the earlier needimport wins.

In the editor preview

A rendered preview shows the first 50 needs per import and says so, so a page importing thousands stays responsive while you type. Raise, lower or disable it with needs.import_preview_max. This is a preview limit only: ubc build html always renders every imported need, because a truncated build would silently drop content from the published site.

Which parser renders the content

Each need’s content is parsed as the markup it declares: a need whose doctype field says .md renders as Markdown, even imported into an RST project. The declared suffix is matched against the parsers your project configures under [parse.parsers] — the same routing that decides how your own files parse, with the longest matching suffix winning — so the export says what the content is, while the parser and all of its settings are always your project’s.

Most exports declare nothing, and that is fine: with doctype absent or empty, the content is parsed with the flavour of the file the .. needimport:: itself is written in. A doctype that no configured parser claims produces one warning per source, naming the unclaimed suffix, and falls back the same way — so the parser in use is never a silent guess.

Two parsers can also claim the same suffix, told apart only by the directory their include globs name. Your own files are then routed by path, but a doctype carries no path, so it cannot choose between them: the content is parsed with the first parser in name order, and you get one warning per source (needs.import.doctype_ambiguous) naming every tied parser and the one that won. Give the parsers globs implying different suffixes if imported content should use a particular one.

If one source’s needs declare two different tied doctypes, only the first is reported — the fix is the same edit to your parser table either way.

If a :filter: matches nothing

An import whose :filter: selects none of the needs its source offered contributes nothing, and says so — once per source, with both counts. This is an informational message (needs.import.filter_empty), not a warning, so it does not fail ubc check: a filter that matches nothing this week may match something next.

It is worth knowing why a plausible-looking filter can match nothing. The expression is applied to each need’s raw JSON in the source — before tags are added and long before needextend runs, matching Sphinx-Needs — and a field the export never sets is read from the export’s own schema defaults, which are typically null. So status == "open" over a status-less export matches nothing rather than reporting an error. Check the filter’s field names and values against the JSON.

A source that was empty to begin with is never blamed on its filter.

If an :ids: entry names nothing

An :ids: list that names a need the source does not contain reports it (needs.import.ids_unmatched), naming the unmatched ids — once per source, with a handful named and a count of the rest. The other ids still import normally, which is exactly why this is worth reporting: one typo among five is invisible in the output.

This is a warning, where the filter message above is only a note. The difference is deliberate: a filter is a predicate, and matching nothing may simply be the truth today; an id is a name, and one that resolves to nothing is a mistake in either your directive or the source.

Refused, but unlockable

What

What you see instead

Why

.. image:: and .. figure::

The generic directive placeholder, plus a warning naming the setting. A Markdown ![alt](pic.png) shows its alt text instead.

An imported reference names a path that means something in the exporting project. Resolving it against your pages would let a third party’s JSON point at any file you publish.

Diagrams from a file (.. uml:: flow.puml, .. mermaid:: x.mmd)

Same as above. Inline diagram bodies are unaffected and keep rendering.

Only the file form reaches outside the content. A diagram written inline is self-contained, so there is nothing to police.

Any other construct that names an asset — a .. video:: source or :poster:, a sphinx-design card’s :img-top: / :img-bottom:

The construct still renders; the asset reference is dropped, with the same warning naming the setting.

The rule is about the reference, not the directive that carries it: every asset reference in imported content goes through one policy, so a newly supported directive cannot quietly become an exception.

To allow them, name the source and set allow_assets on it. The project-wide key stays off; the per-source entry is the unlock:

[[needs.import_sources]]
json_path = "specs/needs.json"       # or json_url = "https://…"
allow_assets = true                  # ← this is the unlock
assets_root = "_assets/specs"        # if the assets are not beside the JSON

needs.import_allow_assets flips the project-wide default the other way, if you would rather opt out per source than in.

The policy belongs to the source, not to the .. needimport:: directive. Two directives on different pages that import the same file must resolve its assets the same way, so the setting lives with the file it describes. It is also why this is configuration rather than a directive option: needimport is a directive Sphinx-Needs also defines, and an option only ubCode understood would break the same document under sphinx-build.

Once allowed, the references render as your own do: the file is copied into the site’s _images/ directory, the <img> gets a real src, a diagram file is read and drawn, and editing the file re-renders the pages that show it. There is no second mechanism — an unlocked asset is an asset.

Important

What you are granting, stated plainly. allow_assets = true gives that source the same authority over your source tree that an image you wrote yourself has. A relative reference resolves against the directory holding the source’s own needs.json, but an absolute /logo.png resolves against [project] srcdir — so the source can reference any file published under your source directory, and that file is copied into the built site.

The unlock does not create a new capability; it extends an existing one to a new principal. For a source you do not control, set assets_root as well: that narrows the reach to one subtree, in both reference forms.

Where imported assets resolve

Against the JSON’s own directory, not the importing page’s. That is a deliberate difference from Sphinx-Needs, which resolves them against whichever document did the importing — so the same file resolves differently on every page that imports it. Keying on the JSON’s location means an exporting project can ship assets next to its needs.json and have every consumer find them. assets_root re-roots this if your layout differs.

A /-prefixed path is your project’s own rule, unchanged: it resolves against [project] srcdir. With assets_root set, both forms resolve inside it — which is how you narrow a third-party source to one subtree of your tree.

What the mount can reach, and what it cannot

Exactly the directory it names, and below. A reference that climbs out of it with .. is refused with a warning that names the reference and the directory it was checked against, in every spelling of .. — it is never resolved. Everything else behaves as an authored reference does, including a missing file, which reports the same “could not read” warning your own missing image would.

Two limits worth knowing. Without assets_root, the base for an absolute /x is srcdir itself (see the note above), so “outside the mount” means outside your source tree rather than outside the JSON’s folder. And a symlink inside the mount resolves wherever it points, exactly as it would for an image you wrote yourself — if that matters for a source you do not control, do not link out of the tree.

Three cases need assets_root

A remote source (json_url) has no local directory to resolve against, and ubCode does not fetch remote assets — so vendor them into your project and point assets_root at them. The same applies to a local needs.json that sits outside srcdir: there is no directory inside your project for a relative reference to mean.

The third is a local needs.json that is inside srcdir but whose folder name cannot be used as a base: names containing whitespace, a backslash, :, # or ? are read differently by different parts of the build, so resolving against one would be a guess. Rather than mount at a folder you did not name, ubCode refuses and asks for an explicit assets_root.

In all three the warning says which one it is, and names assets_root.

assets_root itself must stay inside srcdir, and is held to the same naming rule — it is a base too. One that leads outside, or that is spelled ambiguously, is reported against your configuration once, naming the entry by its index, and that source’s asset references are refused rather than resolved somewhere unexpected.

In an untrusted workspace, the preview refuses them

Trusting a workspace and allowing a source’s assets are two different statements, and both are required before the editor reads a file because imported content asked it to. In an untrusted workspace the rendered preview therefore shows the placeholder and says the session refused it — the same posture raw takes there. ubc build html is unaffected: running a build is itself an explicit act.

A URL in imported content stays a URL — but only a real one

An .. image:: https://… (or a data: URI) inside imported content is emitted exactly as it is written, the same as in your own files: ubCode never fetches it, and readers’ browsers load it from wherever it points. If that matters for a source you do not control, do not unlock it.

A protocol-relative reference (//host/file.png) is treated as a project path instead, and so is checked against the mount like any other path. It has no working meaning in imported content, and reading it as a URL would let a third party’s data choose a host your readers fetch from.

Refused, with no setting to lift it

What

What you see instead

Why

.. include:: and .. literalinclude::

The generic directive placeholder, plus a warning.

These pull bytes into the parse, where they can open a directive block, define a substitution or end a table. A wrong image is visible and recoverable; a wrong include silently changes what the surrounding markup means, and the page gives no clue where it came from.

.. csv-table:: with :file: (inline CSV data is fine)

Same as above.

Same reason — it reads a file into the parse.

.. raw::, even with raw_enabled = true

Same as above.

Enabling raw says you trust your own files. It does not say anything about a third party’s JSON, so the two decisions are kept separate.

Need directives inside imported content (a .. spec:: in an imported body)

The placeholder plus a warning. No need is created.

That nested need already exists as its own entry in the same export, so running it again would only re-create a need you are already importing. Sphinx-Needs ends up in the same place, but by way of a duplicate-ID warning.

Nested .. needimport:: or .. src-trace::

The placeholder plus a warning. Nothing is read.

A recursive import. Sphinx-Needs has no depth limit and no cycle guard here, so a self-referential chain is stopped only by chance; refusing at the first level bounds it by construction.

Two differences you will not get a warning about

These are absences rather than refusals — there is no construct to attach a message to. They are listed so you can find them when something looks off.

Substitutions do not resolve

|version|, |release|, |today|, and your own rst_prolog substitutions all expand to nothing. Imported content is parsed on its own, with no host document around it, which is what makes an import mean the same thing on every page that uses it. (A relative :doc: reference is not affected — it resolves against the page whose needimport produced the card.)

:id_prefix: does not rewrite the content text

References resolve correctly: a :need: role inside a prefixed import that names a sibling by its raw ID resolves to the prefixed need, and becomes a working link. That holds for every ID the source contributed, including one that lost an ID clash to a need defined elsewhere. What differs is an ID mentioned in prose — ubCode leaves it as written, where Sphinx-Needs rewrites it. ubCode applies the prefix when resolving a reference rather than by substituting text, so it also cannot mangle REQ_1 inside REQ_10, which the Sphinx-Needs implementation notes as a known weakness of its own approach.

See also

Coming from Sphinx-Needs for the rest of the migration story.