diff --git a/docs/extensions/index.md b/docs/extensions/index.md
index a9d58b3..0edbe05 100644
--- a/docs/extensions/index.md
+++ b/docs/extensions/index.md
@@ -1021,9 +1021,26 @@ $tocExtension = new TableOfContentsExtension(
cssClass: 'toc', // CSS class for nav element
position: 'top', // 'top', 'bottom', or null for manual placement
separator: '
', // Optional HTML between TOC and content
+ collapsible: true, // Wrap in a / disclosure
+ summary: 'Contents', // Disclosure label (default 'Table of Contents')
+ open: false, // Start expanded when true (default collapsed)
);
```
+**Collapsible:** with `collapsible: true` the TOC is wrapped in a
+``/`` disclosure (closed unless `open: true`), with the heading
+list directly inside it:
+
+```html
+
+Table of Contents
+
+
+```
+
+When `collapsible` is off (the default) the output is the unchanged
+``.
+
**Auto-insertion:**
```php
diff --git a/src/Extension/TableOfContentsExtension.php b/src/Extension/TableOfContentsExtension.php
index 5fb6de2..2fffbec 100644
--- a/src/Extension/TableOfContentsExtension.php
+++ b/src/Extension/TableOfContentsExtension.php
@@ -39,6 +39,9 @@
* listType: 'ol', // Use ordered list
* position: 'top', // Auto-insert at 'top', 'bottom', or null for manual
* separator: " \n", // Optional separator between TOC and content
+ * collapsible: true, // Wrap in a / disclosure
+ * summary: 'Contents', // Disclosure label (default 'Table of Contents')
+ * open: false, // Start expanded when true (default collapsed)
* );
* ```
*/
@@ -58,6 +61,10 @@ class TableOfContentsExtension implements ResettableExtensionInterface
* @param string $cssClass CSS class for the TOC container
* @param string|null $position Auto-insert position: 'top', 'bottom', or null for manual placement
* @param string $separator HTML separator between TOC and content (when position is set)
+ * @param bool $collapsible Wrap the TOC in a ``/`` disclosure so it can be
+ * collapsed. Off by default; when off the output is the unchanged ``.
+ * @param string $summary Summary label for the disclosure (only used when $collapsible is true).
+ * @param bool $open Render the disclosure expanded by default (only used when $collapsible is true).
*/
public function __construct(
protected int $minLevel = 1,
@@ -66,6 +73,9 @@ public function __construct(
protected string $cssClass = 'toc',
protected ?string $position = null,
protected string $separator = '',
+ protected bool $collapsible = false,
+ protected string $summary = 'Table of Contents',
+ protected bool $open = false,
) {
}
@@ -169,9 +179,19 @@ protected function renderTocHtml(array $headings): string
return '';
}
- $html = '' . "\n";
+ if (!$this->collapsible) {
+ return '' . "\n"
+ . $this->renderTocList($headings)
+ . ' ' . "\n";
+ }
+
+ // Collapsible: the heading list sits directly inside a
+ // disclosure so it can be toggled, closed by default unless $open.
+ $open = $this->open ? ' open' : '';
+ $html = '' . "\n";
+ $html .= '' . StringUtil::escapeHtml($this->summary) . ' ' . "\n";
$html .= $this->renderTocList($headings);
- $html .= ' ' . "\n";
+ $html .= ' ' . "\n";
return $html;
}
diff --git a/tests/TestCase/Extension/TableOfContentsExtensionTest.php b/tests/TestCase/Extension/TableOfContentsExtensionTest.php
index 385eee8..098c36f 100644
--- a/tests/TestCase/Extension/TableOfContentsExtensionTest.php
+++ b/tests/TestCase/Extension/TableOfContentsExtensionTest.php
@@ -430,4 +430,64 @@ public function testSkippedHeadingLevelsFollowedByShallowerHeadingReuseSameNeste
$this->assertStringNotContainsString("\n\n", $html);
$this->assertStringContainsString('Three ' . "\nTwo ", $html);
}
+
+ public function testNonCollapsibleTocKeepsPlainNav(): void
+ {
+ $converter = new DjotConverter();
+ $tocExtension = new TableOfContentsExtension();
+ $converter->addExtension($tocExtension);
+
+ $converter->convert("# One\n\n## Two\n");
+ $html = $tocExtension->getTocHtml();
+
+ $this->assertStringStartsWith('', $html);
+ $this->assertStringNotContainsString('addExtension($tocExtension);
+
+ $converter->convert("# One\n\n## Two\n");
+ $html = $tocExtension->getTocHtml();
+
+ // Closed by default (no `open`), list sits directly inside .
+ $this->assertStringStartsWith(
+ '' . "\n" . 'Table of Contents ' . "\n" . '',
+ $html,
+ );
+ $this->assertStringEndsWith(' ' . "\n" . ' ' . "\n", $html);
+ $this->assertStringNotContainsString('assertStringNotContainsString('addExtension($tocExtension);
+
+ $converter->convert("# One\n");
+ $html = $tocExtension->getTocHtml();
+
+ $this->assertStringStartsWith(
+ '' . "\n" . 'Contents ',
+ $html,
+ );
+ }
+
+ public function testCollapsibleSummaryIsHtmlEscaped(): void
+ {
+ $converter = new DjotConverter();
+ $tocExtension = new TableOfContentsExtension(collapsible: true, summary: 'A & B ');
+ $converter->addExtension($tocExtension);
+
+ $converter->convert("# One\n");
+ $html = $tocExtension->getTocHtml();
+
+ $this->assertStringContainsString('A & <b>B</b> ', $html);
+ $this->assertStringNotContainsString('B ', $html);
+ }
}