Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `</#id>` 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.
Expand Down
34 changes: 13 additions & 21 deletions src/Converter/HtmlToCarve.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
// `<figcaption>` 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);
Expand Down
36 changes: 20 additions & 16 deletions src/Renderer/HtmlRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<figure>`
* 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 `<figure>` 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 `<caption>`.
* No group caption, no trailing `<figcaption>`.
*/
Expand All @@ -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 .= "<figure class=\"carve-figure-panel\">\n"
$body .= "<figure class=\"carve-figure-panel\">\n"
. $this->indentBlock($table, 2) . "\n</figure>\n";
} else {
$inner .= rtrim($this->renderNode($child), "\n") . "\n";
$body .= rtrim($this->renderNode($child), "\n") . "\n";
}
}

$body = "<div class=\"carve-figure-panels\">\n";
$inner = rtrim($inner, "\n");
if ($inner !== '') {
$body .= $this->indentBlock($inner, 2) . "\n";
}
$body .= "</div>\n";

$caption = $node->getCaption();
if ($caption !== null) {
$body .= '<figcaption>' . $this->renderChildren($caption) . "</figcaption>\n";
}

return '<figure' . $attrs . ">\n" . $this->indentBlock(rtrim($body, "\n"), 2) . "\n</figure>\n";
$body = rtrim($body, "\n");
if ($body === '') {
return '<figure' . $attrs . ">\n</figure>\n";
}

return '<figure' . $attrs . ">\n" . $this->indentBlock($body, 2) . "\n</figure>\n";
}

protected function renderTable(Table $node): string
Expand Down
18 changes: 18 additions & 0 deletions tests/TestCase/Parser/ABareFigureFenceIsACompositeFigureTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
"<figure class=\"carve-figure-group\">\n</figure>\n",
(new CarveConverter())->convert("::: figure\n:::\n"),
);
}

public function testAnEmptyCaptionedGroupHoldsOnlyItsFigcaption(): void
{
$this->assertSame(
"<figure class=\"carve-figure-group\">\n <figcaption>Figure 1: G</figcaption>\n</figure>\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
Expand Down
Loading