Suppressing a gated directive crashes the build instead of hiding it. Reproduced on main (f721c3f) with Sphinx 8.2.3.
Symptom
NotImplementedError: <class 'sphinx.writers.html5.HTML5Translator'> departing unknown node type: solution_end_node
The build aborts. There is no warning first — the extension reports nothing and Sphinx raises from the writer.
Three ways to trigger it
All three share one root cause. Minimal reproductions, each a complete project:
1. hide_solutions = True with any gated solution. This is the one most likely to bite users, since hide_solutions is a documented project-wide switch — turning it on breaks the build for any book that uses gated solutions anywhere.
```{solution-start} ex1
:label: sol1
Gated answer.
**2. `:hidden:` on `{solution-start}`** → same `solution_end_node` crash.
**3. `:hidden:` on `{exercise-start}`** → the same crash with `exercise_end_node`.
Non-gated `hide_solutions` and non-gated `:hidden:` both work correctly; only the gated forms crash.
## Root cause
`SolutionDirective.run()` returns `[]` early when `hide_solutions` is set, and again later when `node["hidden"]` is true. `SolutionStartDirective` inherits that `run()`, so in both cases **no `solution_start_node` is emitted**.
`MergeGatedSolutions` finds no start node, so it never runs and never consumes the end marker. But `SolutionEndDirective.run()` unconditionally returns a `solution_end_node`, which is now orphaned in the doctree.
`solution_end_node` is registered with a bare `app.add_node(solution_end_node)` and has no visitor for any builder, because it is only ever meant to be consumed by the merge transform. The writer reaches it and raises.
`exercise_end_node` has the identical shape, which is why `:hidden:` on `{exercise-start}` fails the same way.
## Suggested fix
Two candidate approaches:
- Register no-op visitors for `solution_end_node` and `exercise_end_node` that raise `SkipNode`. Cheap, and defends against any other path that leaves a marker orphaned.
- Or have the `-end` directives consult the same config/flag and return nothing when the corresponding start was suppressed. More precise, but the end directive does not currently know its partner's options.
The first is the smaller, more robust change; the second is closer to intent. Either way, the intervening content also needs a decision — with `hide_solutions` the author expects the solution body gone, and today that content sits between the markers as ordinary body text rather than inside a solution node.
## Test coverage
`grep -rn hide_solutions tests/` returns nothing — **`hide_solutions` has no test coverage at all**, gated or otherwise, which is why this has gone unnoticed. Worth adding alongside the fix.
Suppressing a gated directive crashes the build instead of hiding it. Reproduced on
main(f721c3f) with Sphinx 8.2.3.Symptom
The build aborts. There is no warning first — the extension reports nothing and Sphinx raises from the writer.
Three ways to trigger it
All three share one root cause. Minimal reproductions, each a complete project:
1.
hide_solutions = Truewith any gated solution. This is the one most likely to bite users, sincehide_solutionsis a documented project-wide switch — turning it on breaks the build for any book that uses gated solutions anywhere.Gated answer.