Status
The spurious "unclosed" warning on valid multi-tab groups (originally "Bug A" in this issue) is fixed in PR #455 / commit 2fa8848: ContainerDirective gained a default opened_scope() -> bool method, and the processor now pushes onto active_containers only when the handler reports it opened a new scope. TabsDirective returns false for continuation (:::tab) openers, so a correctly-closed multi-tab group no longer leaves phantom entries for finalize() to warn about.
This issue now tracks the remaining defect: nested :::tab does not work.
Remaining bug: nesting :::tab inside :::tab silently flattens to siblings
TabsDirective (crates/rw-renderer/src/tabs/directive.rs) models a two-level structure — a tab group containing tab panels — on top of the container protocol's single-level start/end. It uses self.stack.is_empty() in start() to pick the level:
- stack empty → open a new tab group
- stack non-empty → append a sibling tab to the current group
So a :::tab that is meant to be nested inside another :::tab is indistinguishable from a sibling, and gets flattened.
Repro
:::tab[Outer]
x
:::tab[Inner]
y
:::
z
:::
Current output (post-#455)
1 warning: "stray ::: with no opening directive"
<div class="tabs" id="tabs-0">
<div class="tabs-buttons" role="tablist">
<button ... >Outer</button>
<button ... >Inner</button> <!-- flattened into a sibling, not nested -->
</div>
<div role="tabpanel" ...><p>x</p></div>
<div role="tabpanel" ... hidden><p>y</p></div>
</div>
<p>z</p> <!-- escaped the tab panel entirely -->
<p>:::</p> <!-- trailing close rendered as literal text -->
So:
Inner becomes a sibling button of Outer in the same group rather than a nested tab.
z (intended for the Outer panel, after the inner group) is emitted outside any tab panel.
- The first
::: closes the whole group; the second ::: finds nothing open and is left as a literal <p>:::</p> paragraph, with one stray ::: warning.
The flattening of Inner and the escape of z are silent (no warning points at them); only the leftover ::: warns, and confusingly as "stray", not as a nesting error.
Note: before #455 this nested case warned nothing at all (it pushed/popped "tab" symmetrically). #455 corrected the accounting, so the leftover ::: now surfaces as a stray ::: warning — louder, but the structural flattening is unchanged.
Root cause
TabsDirective infers nesting depth from its own stack, but stack only reaches depth 1 (the group), never the panel level — start() pushes only in the new-group branch. There is no way, within the flat :::name … ::: protocol, for a second :::tab to mean "open a child" versus "open a sibling". The directive is overloading one delimiter for two structural roles.
Relevant lines:
crates/rw-renderer/src/tabs/directive.rs — the if self.stack.is_empty() branch in start() (new-group vs sibling), and the push that happens only in the new-group branch.
Suggested fix
Model the two levels in the syntax — a separate group container with one :::tab per panel, each panel explicitly closed:
:::tabs
:::tab[macOS]
...
:::
:::tab[Linux]
...
:::
:::
tabs (group) and tab (panel) become separate ContainerDirectives, each with exactly one matching close, so the processor's accounting is correct by construction and genuine nesting becomes expressible. Cost: a markdown syntax change (and a migration for existing :::tab[…] … :::tab[…] … ::: content).
The opened_scope() mechanism added in #455 fixed the flat model's warning accounting but deliberately did not attempt to model two levels; closing this issue means the two-container redesign (or an equivalent that makes panel nesting first-class).
Impact
Status
The spurious "unclosed" warning on valid multi-tab groups (originally "Bug A" in this issue) is fixed in PR #455 / commit
2fa8848:ContainerDirectivegained a defaultopened_scope() -> boolmethod, and the processor now pushes ontoactive_containersonly when the handler reports it opened a new scope.TabsDirectivereturnsfalsefor continuation (:::tab) openers, so a correctly-closed multi-tab group no longer leaves phantom entries forfinalize()to warn about.This issue now tracks the remaining defect: nested
:::tabdoes not work.Remaining bug: nesting
:::tabinside:::tabsilently flattens to siblingsTabsDirective(crates/rw-renderer/src/tabs/directive.rs) models a two-level structure — a tab group containing tab panels — on top of the container protocol's single-level start/end. It usesself.stack.is_empty()instart()to pick the level:So a
:::tabthat is meant to be nested inside another:::tabis indistinguishable from a sibling, and gets flattened.Repro
Current output (post-#455)
So:
Innerbecomes a sibling button ofOuterin the same group rather than a nested tab.z(intended for theOuterpanel, after the inner group) is emitted outside any tab panel.:::closes the whole group; the second:::finds nothing open and is left as a literal<p>:::</p>paragraph, with onestray :::warning.The flattening of
Innerand the escape ofzare silent (no warning points at them); only the leftover:::warns, and confusingly as "stray", not as a nesting error.Root cause
TabsDirectiveinfers nesting depth from its ownstack, butstackonly reaches depth 1 (the group), never the panel level —start()pushes only in the new-group branch. There is no way, within the flat:::name … :::protocol, for a second:::tabto mean "open a child" versus "open a sibling". The directive is overloading one delimiter for two structural roles.Relevant lines:
crates/rw-renderer/src/tabs/directive.rs— theif self.stack.is_empty()branch instart()(new-group vs sibling), and the push that happens only in the new-group branch.Suggested fix
Model the two levels in the syntax — a separate group container with one
:::tabper panel, each panel explicitly closed:tabs(group) andtab(panel) become separateContainerDirectives, each with exactly one matching close, so the processor's accounting is correct by construction and genuine nesting becomes expressible. Cost: a markdown syntax change (and a migration for existing:::tab[…] … :::tab[…] … :::content).The
opened_scope()mechanism added in #455 fixed the flat model's warning accounting but deliberately did not attempt to model two levels; closing this issue means the two-container redesign (or an equivalent that makes panel nesting first-class).Impact
:::tabsyntax — are impossible to express; the renderer silently flattens them and leaks content out of the intended panel, plus emits a misleadingstray :::warning.