Skip to content

Nested :::tab inside :::tab silently flattens to siblings (content escapes the panel) #391

Description

@yumike

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:

  1. Inner becomes a sibling button of Outer in the same group rather than a nested tab.
  2. z (intended for the Outer panel, after the inner group) is emitted outside any tab panel.
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions