Skip to content

✨ Add sd_custom_roles: config-defined badge roles with baked tooltips - #295

Merged
chrisjsewell merged 3 commits into
mainfrom
brief/26-custom-roles
Nov 10, 2026
Merged

chrisjsewell merged 3 commits into
mainfrom
brief/26-custom-roles

Conversation

@chrisjsewell

@chrisjsewell chrisjsewell commented Jul 16, 2026 •

Copy link
Copy Markdown
Member

Adds sd_custom_roles — the role counterpart to sd_custom_directives (#194), and the follow-up to the badge tooltips of #286. Repeated status badges get defined once in conf.py, fully TOML-representable:

sd_custom_roles = {
    "bdg-stable": {
        "inherit": "bdg-success",
        "tooltip": "A released, supported version",
    },
    "bdg-beta": {"inherit": "bdg-warning", "tooltip": "Interface may change"},
}

Then :bdg-stable:`v1.2` everywhere — no retyped ; tooltip suffixes. The docs present this as the primary pattern for repeated semantics, with the per-instance suffix as the one-off escape hatch (which still overrides a baked tooltip; precedence documented and tested).

Behaviour

  • v1 scope: the badge family (bdg, bdg-<color>, line/link/ref variants; extension point for icon/button roles noted in-code). A baked tooltip also applies to bare-form link/ref badges — where the suffix grammar deliberately refuses to split — and overrides a ref's automatic title.
  • Validation at config time: dict-of-dicts of primitives, unknown keys rejected naming the offender, unknown inherit warns and skips, unparseable role names rejected.
  • Registry safety (both review-caught): the clash guard now also covers docutils' canonical roles — a custom role named code, math or strong is warn+skipped instead of silently hijacking core rendering; and registrations are reconciled every build with ownership markers, so under in-process rebuilds (sphinx-autobuild) tooltip edits take effect, removed roles stop resolving, and nothing leaks between apps — matching sd_custom_directives behaviour. Both locked by regression tests (including a three-apps-in-one-process test that bypasses the fixture cleanup which had masked the leak).
  • A no-tooltip custom role is byte-identical in output to the role it inherits (tested), and all 69 built-in badge roles are byte-identical to main (the shared-constructor refactor is behaviour-preserving — review diffed an exhaustive page of every family).

Verification

  • Full test/lint/type matrix green (+13 dedicated tests incl. the meta-test syncs every SdConfig field requires); docs -nW with live examples rendering in the docs' own conf.py
  • Reviewer probe scripts (canonical-role hijack, in-process staleness/leak, role-name edge cases, grammar edges) all rerun clean post-fix

Add an ``sd_custom_roles`` config option (parallel to ``sd_custom_directives``)
to register badge roles that inherit a built-in badge role (``bdg``,
``bdg-<color>``, ``bdg-*-line``, ``bdg-link-*``, ``bdg-ref-*``) and optionally
bake in a default ``tooltip``. This is the recommended way to declare repeated
status badges (stable/beta/deprecated) once, instead of retyping the ``;
tooltip`` suffix at every use site.

- A baked tooltip is only the default: a per-instance ``; tooltip`` suffix
  overrides it. The shared badge-role constructor gains a ``tooltip`` keyword,
  used as the fallback when no suffix is present; a tooltip-less custom role is
  byte-identical to the role it inherits. A baked tooltip also applies to the
  bare form of link/ref badges (where the suffix grammar refuses to split) and
  overrides a reference's automatic title.
- Config validation: dict-of-dicts, unknown keys rejected, ``inherit`` a
  string, and the role name must be non-empty and whitespace-free (unicode,
  case and colons are accepted; docutils looks roles up case-insensitively).
- Registration reconciles on every ``config-inited`` against docutils'
  process-global role registry: our own names are refreshed (picking up
  tooltip/inherit edits in-process), names dropped from the config are
  unregistered (no stale leak into a later build or sibling app), and a name
  clashing with a foreign role - another extension's, or a docutils canonical
  role such as ``code``/``math``/``strong`` - is skipped with a warning.
- The field is fully TOML-representable and picked up by the config table.
- Docs frame the ``; tooltip`` suffix as the one-off escape hatch, with custom
  roles as the primary pattern (rst + myst examples, cross-linked with the
  custom directives docs, and a note recommending a ``bdg-`` name prefix).
@codecov

codecov Bot commented Jul 16, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.06173% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.43%. Comparing base (0511463) to head (1ffe374).

Files with missing lines Patch % Lines
sphinx_design/config.py 84.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #295      +/-   ##
==========================================
- Coverage   93.44%   93.43%   -0.02%     
==========================================
  Files          16       16              
  Lines        1618     1675      +57     
==========================================
+ Hits         1512     1565      +53     
- Misses        106      110       +4     
Flag Coverage Δ
pytests 93.43% <95.06%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Keep both sides: the role-registry and RSTState imports in
badges_buttons.py, with the shared badge-role constructor ahead of
__call__ (which keeps main's PLR0917 noqa); the aside and custom-roles
imports in extension.py; and in tests/test_misc.py the custom_roles
assertion closing test_config_toml_round_trip, then the accordion tests.
Drop this branch's CHANGELOG entry: the release PR writes the changelog.
@chrisjsewell
chrisjsewell merged commit 2f473aa into main Nov 10, 2026
29 checks passed
@chrisjsewell
chrisjsewell deleted the brief/26-custom-roles branch November 10, 2026 12:06
chrisjsewell added a commit to dipinknair/sphinx-design that referenced this pull request Nov 10, 2026
…ecutablebooks#293 executablebooks#294 executablebooks#295 and others)

Keep both sides in sphinx_design/extension.py (the new-tab
doctree-resolved handler and the sd_custom_roles config-inited hook) and
in tests/test_buttons.py (the new-tab tests and main's line-block
tests). cards.py merged cleanly: the stretched link is still built by
_create_link, which marks it for the new-tab handler.
chrisjsewell added a commit that referenced this pull request Nov 10, 2026
Remove the unreleased `sd_custom_roles` setting (#295) before it ships in 0.8.0:
its config value and validation, the role registration and the process-global registry handling,
its documentation and its tests.

The badge-role refactor from #295 stays: `_iter_badge_roles` is still the single source of the badge family,
and every badge role is built by one constructor that accepts a default tooltip.
In its place, the badge roles (plain, link and ref, in every colour) now accept a standard `tooltip` role option,
read from the docutils role-function options dictionary.
A reStructuredText custom role can preset it (`.. role:: bdg-stable(bdg-success)` with `:tooltip:`),
and an extension that presets a badge role passes it the same way.
A `; tooltip` suffix still wins over the option, which wins over the constructor default.

The docs describe the option, for authors and for extension authors,
and recommend the sphinx-ub-directives package for project-defined directive presets.
`sd_custom_directives` is unchanged.
chrisjsewell added a commit that referenced this pull request Nov 10, 2026
…ole option (#316)

Remove the unreleased `sd_custom_roles` setting (#295): its config value and validation,
the role registration and its handling of docutils' process-global role registry,
its documentation and its tests.

The badge-role refactor from #295 stays: `_iter_badge_roles` is the single source of the badge family,
and every badge role is built by one constructor that accepts a default tooltip.
In its place, the badge roles (plain, link and ref, in every colour) accept a standard `tooltip` role option,
read from the docutils role-function options dictionary, so a reStructuredText custom role
(`.. role:: bdg-stable(bdg-success)` with `:tooltip:`) or an extension that presets a badge role can set it.
A `; tooltip` suffix still wins over the option, which wins over the constructor default.
The option specification is kept per role instance and restored after each call,
because Sphinx's `SphinxRole` overwrites `options` with each call's options,
and docutils adds a `class` option to it when the role is registered.

As a side effect, a `.. role::` with `:class:` over a badge role the document had already used,
which failed before, now works.

The docs describe the option for authors (a `.. role::` per document,
or in `rst_prolog` for every reStructuredText document) and for extension authors.
They point to the sphinx-ub-directives package for presets beyond default option values;
role presets are planned there.
`sd_custom_directives` is unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant