diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bfcd55f..6ab38e27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,8 +116,9 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). from its label's sequence and a panel id resolves `` as the group number plus a letter ("Figure 2a"). An opener carrying a quoted title or `[label]` stays a generic container, and groups do not nest. HTML renders the - corpus-pinned `carve-figure-group` / `carve-figure-panels` / - `carve-figure-panel` shape; the Markdown, plain-text and ANSI targets degrade + corpus-pinned flat `carve-figure-group` / `carve-figure-panel` shape (panels + directly inside the group figure, per HTML's figure content model and + Pandoc's subfigure output); the Markdown, plain-text and ANSI targets degrade deterministically; `carve fmt` writes the authored form back; the AST wire carries `figure_group` with inline `caption` content; and the HTML importer turns the rendered shape back into `::: figure` source. diff --git a/src/Converter/HtmlToCarve.php b/src/Converter/HtmlToCarve.php index 337a5b5a..24ffdac7 100644 --- a/src/Converter/HtmlToCarve.php +++ b/src/Converter/HtmlToCarve.php @@ -3769,31 +3769,23 @@ protected function processFigureGroup(DOMElement $node): string { $attrs = $this->formatBlockAttributesWithoutClass($node, 'carve-figure-group'); - $panelsDiv = null; + // FLAT shape: panels and preserved stray content are DIRECT children + // of the group figure - no wrapper element - and the group's own + // `
` is the direct child handled below (a panel's caption + // sits inside the panel figure, so it never matches here). + $content = ''; foreach ($node->childNodes as $child) { + if ($child instanceof DOMElement && strtolower($child->tagName) === 'figcaption') { + continue; + } if ( $child instanceof DOMElement - && strtolower($child->tagName) === 'div' - && $this->hasClass($child, 'carve-figure-panels') + && strtolower($child->tagName) === 'figure' + && $this->hasClass($child, 'carve-figure-panel') ) { - $panelsDiv = $child; - - break; - } - } - - $content = ''; - if ($panelsDiv !== null) { - foreach ($panelsDiv->childNodes as $child) { - if ( - $child instanceof DOMElement - && strtolower($child->tagName) === 'figure' - && $this->hasClass($child, 'carve-figure-panel') - ) { - $content .= $this->processFigurePanel($child); - } else { - $content .= $this->processNode($child); - } + $content .= $this->processFigurePanel($child); + } else { + $content .= $this->processNode($child); } } $content = trim($content); diff --git a/src/Renderer/HtmlRenderer.php b/src/Renderer/HtmlRenderer.php index 8a27f695..b486134b 100644 --- a/src/Renderer/HtmlRenderer.php +++ b/src/Renderer/HtmlRenderer.php @@ -1667,10 +1667,10 @@ protected static function withLeadingClass(array $attrs, string $leadingClass): * A composite figure (PART 9 §4c, markup-carve/carve#1122). * * The corpus pins the byte shape (318-composite-figures): the group class - * leads, the panels div is UNCONDITIONAL - zero panels still wrap the - * preserved stray content - and a Figure panel renders as the `
` - * its host already produces with `carve-figure-panel` leading its classes. - * A table does not render as a figure on its own, so its panel wrapper is + * leads, panels and preserved stray content are DIRECT children of the + * group figure, and a Figure panel renders as the `
` its host + * already produces with `carve-figure-panel` leading its classes. A table + * does not render as a figure on its own, so its panel wrapper is * explicit and the table keeps its own attributes and its own ``. * No group caption, no trailing `
`. */ @@ -1680,32 +1680,36 @@ protected function renderFigureGroup(FigureGroup $node): string self::withLeadingClass($this->getRenderableAttributes($node), 'carve-figure-group'), ); - $inner = ''; + // FLAT: panels and preserved stray content nest DIRECTLY inside the + // group figure, the group caption last. HTML's figure content model is + // one figcaption first-or-last plus flow content, and a figure is + // itself flow content, so the wrapper div added nothing the element + // does not already provide - and Pandoc's subfigure HTML output has + // the same flat shape. + $body = ''; foreach ($node->getChildren() as $child) { if ($child instanceof Figure) { - $inner .= $this->renderFigure($child, 'carve-figure-panel'); + $body .= $this->renderFigure($child, 'carve-figure-panel'); } elseif ($child instanceof Table) { $table = rtrim($this->renderTable($child), "\n"); - $inner .= "
\n" + $body .= "
\n" . $this->indentBlock($table, 2) . "\n
\n"; } else { - $inner .= rtrim($this->renderNode($child), "\n") . "\n"; + $body .= rtrim($this->renderNode($child), "\n") . "\n"; } } - $body = "
\n"; - $inner = rtrim($inner, "\n"); - if ($inner !== '') { - $body .= $this->indentBlock($inner, 2) . "\n"; - } - $body .= "
\n"; - $caption = $node->getCaption(); if ($caption !== null) { $body .= '
' . $this->renderChildren($caption) . "
\n"; } - return '\n" . $this->indentBlock(rtrim($body, "\n"), 2) . "\n
\n"; + $body = rtrim($body, "\n"); + if ($body === '') { + return '\n
\n"; + } + + return '\n" . $this->indentBlock($body, 2) . "\n
\n"; } protected function renderTable(Table $node): string diff --git a/tests/TestCase/Parser/ABareFigureFenceIsACompositeFigureTest.php b/tests/TestCase/Parser/ABareFigureFenceIsACompositeFigureTest.php index efe6b372..17121a3b 100644 --- a/tests/TestCase/Parser/ABareFigureFenceIsACompositeFigureTest.php +++ b/tests/TestCase/Parser/ABareFigureFenceIsACompositeFigureTest.php @@ -56,6 +56,24 @@ public function testAnEmptyGroupIsAValidParseToo(): void $this->assertSame([], $node->getChildren()); } + public function testAnEmptyGroupRendersTheBareContainerEmptyBodyShape(): void + { + // The oracle keeps the PART 10 §4 exception the BARE div takes: an + // empty uncaptioned group closes on the next line, no blank body line. + $this->assertSame( + "
\n
\n", + (new CarveConverter())->convert("::: figure\n:::\n"), + ); + } + + public function testAnEmptyCaptionedGroupHoldsOnlyItsFigcaption(): void + { + $this->assertSame( + "
\n
Figure 1: G
\n
\n", + (new CarveConverter())->convert("::: figure\n:::\n^ Figure #: G\n"), + ); + } + public function testAQuotedTitleKeepsTheOpenerAGenericContainer(): void { // `figure_group` has no title slot BY DESIGN (§4c): the group's one diff --git a/tests/spec b/tests/spec index 35f24049..427b125c 160000 --- a/tests/spec +++ b/tests/spec @@ -1 +1 @@ -Subproject commit 35f2404937fb9efe8419399468587786c0fcfbd2 +Subproject commit 427b125cea2834a9e1f222f6669a2634c970ad54