Field links with [needs.string_links]¶
Added in version 0.31.2.
Some need fields hold a value that means a link:
a ticket id, a commit hash, a URL to an external tracker.
[needs.string_links] turns those values into hyperlinks
wherever ubCode shows the field —
in the need’s card, in a needtable, in the built site and in the editor’s rendered preview alike —
without changing what the need actually stores.
A rule is a pattern plus two templates: values of the fields it names are matched against the pattern, and a match renders a link whose target and label come from the templates.
[needs.fields.ticket_url]
[needs.string_links.tracker]
regex = "^(?P<value>https://.+)$"
link_url = "{{value}}"
link_name = "Open in Tracker"
options = ["ticket_url"]
A need carrying :ticket_url: https://tracker.example.com/items/1
now shows that row as a link labelled Open in Tracker.
A need whose value does not match the pattern shows it as plain text, and nothing is reported —
a value that does not match is the ordinary case, not a fault.
The keys¶
Key |
What it does |
|---|---|
|
The pattern a value must match.
Its named capture groups — |
|
The link target, as a template over those groups. |
|
The link’s visible text, as a template over the same groups. |
|
The need fields this rule applies to. |
All four are required.
A rule missing one — or whose regex or templates do not compile —
is reported against its own configuration path and ignored,
and the fields it names keep rendering as plain text.
The rest of the project keeps building.
[config.string_link_incomplete]
--> ubproject.toml
needs.string_links.tracker: `regex` is missing or empty, so this rule
could never draw a link — this rule is ignored (a string link needs all
of `regex`, `link_url`, `link_name` and `options`)
An options entry naming a field the project never declared is reported too
(config.string_link_unknown_field) and dropped,
while the rule keeps its other fields:
a misspelled field name is the common case, and losing the working half of a rule to it
would cost more than it explains.
A worked example¶
A project that records ticket ids rather than URLs, several to a need:
[needs.fields.tickets]
[needs.string_links.tickets]
regex = "^(?P<tool>[A-Z]+)-(?P<num>\\d+)$"
link_url = "https://tickets.example.com/{{tool | lower}}/{{num}}"
link_name = "{{tool}} {{num}}"
options = ["tickets"]
.. req:: Rate limiting
:id: REQ_001
:tickets: AB-1, AB-2; ZZ-9
The row renders as three links — AB 1, AB 2, ZZ 9 — separated by ;,
each pointing at its own ticket.
Both templates see every named group the pattern captured,
so tool and num are available to the target and to the label.
How a value is matched¶
Splitting.
A field named in any rule’s options has its value split on , and ;,
each item stripped, and each item matched on its own.
That is what makes a multi-value field render as several links.
It also means there is no escape for a literal comma or semicolon
in a field a rule claims:
https://example.com/a,b is two items, not one.
The split is not undone: the items are re-joined with ; for display even when none
of them matched, so a claimed field holding alice, bob renders as alice; bob.
A field no rule names is never touched — not matched, not split, not re-joined.
One rule per field.
Where more than one rule lists the same field,
only the first rule declared is ever tried.
It is not retried against the others when its pattern does not match:
if the first rule’s pattern fails, the value renders as plain text
even though a later rule would have matched it.
Rules are tried in the order they appear in ubproject.toml.
Anchoring.
The pattern is searched for, not matched against the whole value,
so an unanchored pattern matches anywhere inside the item —
and when it does, the whole item is replaced by the link.
see JIRA-123 please, matched by (?P<id>[A-Z]+-\d+),
renders as one link and the surrounding words are gone.
Anchor with ^ and $ unless you mean the whole value to be replaced.
Empty labels.
A link_name that renders empty draws no link:
the value renders as plain text.
That is how a rule can decline a value it matched.
List fields.
A list-valued field (tags, or any field declared with an array schema)
links per element, in the need’s card and in a needtable alike.
Its elements are the items; they are not split further.
Which columns link.
In a needtable, the ID column and every link-type column keep their own navigation
and ignore string links entirely.
Every other column — including TITLE — goes through the rules.
The regular expression engine¶
Patterns are compiled by ubCode’s regular expression engine,
which guarantees a linear-time match — it cannot be made to hang on a pathological pattern.
The price is that some constructs Python’s re accepts are not supported.
Three are common enough that a pattern using one is reported by name
rather than as a bare parse failure:
look-ahead (
(?=...),(?!...))look-behind (
(?<=...),(?<!...))backreferences (
(?P=name),\1)
A few rarer ones — conditionals ((?(1)...)), atomic groups ((?>...)),
\Z and inline comments ((?#...)) — are refused too,
as ordinary parse errors carrying the engine’s own cause.
The named case reads:
[config.string_link_regex_invalid]
--> ubproject.toml
needs.string_links.tracker: `regex` uses look-ahead, which ubCode's
regular expression engine does not support, so this rule is ignored and
the fields it names keep rendering as plain text — rewrite the pattern
without it (the engine is the price of a guaranteed linear-time match)
Named groups, non-capturing groups, character classes, alternation and inline flags
((?i), (?s), (?m)) all work as usual.
Only named groups reach the templates; an unnamed group is invisible to them.
A named group that did not participate in the match renders as the empty string.
The template engine¶
Both templates are rendered with MiniJinja,
which is the engine sphinx-needs renders needs_string_links with too,
so a template that works there works here.
Its stock filters are available ({{tool | lower}}, {{id | replace("_", "-")}});
anything beyond them does not resolve.
An undefined variable renders as empty rather than raising.
The templates see the pattern’s named groups and nothing else.
ubCode has no equivalent of sphinx-needs’ needs_render_context,
so a template cannot reach a project-wide variable —
which also means a global cannot silently shadow a capture group named value.
A template that fails to render (an unknown filter is the usual cause)
leaves the value as plain text and reports
build.need_string_link_invalid, once per message however many needs it applies to.
Accepted link targets¶
A rendered link_url must be one of:
an
https://orhttp://URL,a
mailto:address,a relative reference or same-page fragment (
reports/index.html,#section).
A relative reference is refused only when its leading characters are scheme-shaped —
a letter followed by letters, digits, +, . or -, up to a colon,
as in notes:2024.html: by the URL grammar a leading word: is a scheme,
and there is no way to tell the two apart from the text alone.
Write ./notes:2024.html if you mean the path.
A colon later in the reference (docs/2024:notes.html, 2024:notes.html) is not
scheme-shaped and passes.
Anything else is refused, and no link is drawn —
javascript:, data:, file:, an unknown scheme,
a protocol-relative //host target, or a target carrying a control character.
The value renders as plain text and the build reports
build.need_string_link_invalid naming the rule and the refused target.
The check runs on what the template rendered, not on the field value,
so a dangerous scheme is refused whether it came from the value
or was written into link_url itself.
This matters because a need field is not always something your project wrote:
a need can arrive from an imported needs.json,
and escaping a value stops attribute break-out but does not stop a scheme.
Differences from sphinx-needs¶
The matching rules above are ported from sphinx-needs and measured against it. Some behaviours deliberately differ, each because the upstream one is a defect rather than a contract; they are listed with the rest in differences:
No trailing separator. Upstream’s 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 — as does Sphinx-Needs from its next release (fixed upstream in sphinx-needs PR 1718).List fields link everywhere. 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 on both.A failed template keeps the value. Upstream drops the field’s value out of the page entirely when a template raises. ubCode renders the plain text and reports it.
Whitespace-only items are dropped.
AB-1, , AB-2is two items, not three.A non-participating optional group renders empty. Upstream prints the literal
Nonethere.
Further differences are consequences of the engines: ubCode refuses a dangerous link target where sphinx-needs emits it, and ubCode validates a rule when the configuration loads where sphinx-needs raises out of the first need it renders (so a rule with a missing key ends an upstream build even when no need carries the field).
Upgrade cost¶
Every project pays a one-off cost on the release that adds this feature, whether or not it configures a rule: the resolved configuration gained the section, which moves the configuration hash that keys the index cache, and the build manifest’s schema version moved with the renderer — so the first build after upgrading re-indexes and re-renders once, then settles.
After that the cost is the ordinary one: adding or editing a rule changes the
configuration hash again, so that build re-indexes and re-renders once too.
That is the intended trade — it is also what makes an edit to a rule’s link_url
re-render every page that draws it.