From 16ca8a5f459767532f10ab6a53f4cf012e2104c2 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sat, 15 Aug 2026 19:49:29 +0200 Subject: [PATCH] fix: a caret line inside an open caption is caption text The caption collector gathered continuation lines until a blank line or a new block, and a line starting with the caption marker counted as a new block. So the first caption ended there and the second parsed as another caption, which on a table REPLACED the first - its text was discarded with nothing to show for it. djot.js keeps it: an adjacent caret line is an ordinary continuation line of the open caption, and its caret is literal text. This library tracks djot.js, so parity decides it. Before, the caption read `Second`. Now it reads `First\n^ Second`. Everything else is unchanged and now has a test: a blank line between the two caret lines still means the second caption replaces the first, a plain continuation line still continues the caption, and two captioned tables in one document still keep their own captions. The collector is shared, so an image and a block quote caption swallow an adjacent caret line too. That is deliberate: a caption is one block, and its continuation rule cannot depend on what it attaches to. Neither target lost text before, so this only makes them consistent. --- docs/reference/enhancements.md | 2 +- src/Parser/BlockParser.php | 2 +- tests/TestCase/DjotConverterTest.php | 44 ++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/docs/reference/enhancements.md b/docs/reference/enhancements.md index 70b1ab8..5143024 100644 --- a/docs/reference/enhancements.md +++ b/docs/reference/enhancements.md @@ -1008,7 +1008,7 @@ The `^ caption text` syntax adds captions to images, tables, and block quotes: - `^ ` marker at start of line triggers caption parsing - Can interrupt paragraphs (no blank line required before caption) - Blank line between element and caption is allowed for readability -- Multi-line captions supported (continues until blank line or new block) +- Multi-line captions supported (continues until blank line or new block; an adjacent `^ ` line is caption text with its caret preserved) - Full roundtrip support in HtmlToDjot converter **Multi-line caption example:** diff --git a/src/Parser/BlockParser.php b/src/Parser/BlockParser.php index 35cb232..ab8bfc5 100644 --- a/src/Parser/BlockParser.php +++ b/src/Parser/BlockParser.php @@ -3728,7 +3728,7 @@ protected function tryParseCaption(Node $parent, array $lines, int $start): ?int break; } // Stop at block-level elements - if ($this->startsNewBlock($nextLine)) { + if ($this->startsNewBlock($nextLine) && !preg_match('/^\^ /', $nextLine)) { break; } // Stop at new table diff --git a/tests/TestCase/DjotConverterTest.php b/tests/TestCase/DjotConverterTest.php index 96be866..aa42b4d 100644 --- a/tests/TestCase/DjotConverterTest.php +++ b/tests/TestCase/DjotConverterTest.php @@ -380,6 +380,15 @@ public function testTableCaptionWithBlankLine(): void $this->assertStringContainsString('Caption after blank line', $result); } + public function testBlankSeparatedSecondCaptionReplacesTheFirst(): void + { + $djot = "| A | B |\n|---|---|\n| 1 | 2 |\n^ First\n\n^ Second"; + $expected = "\n\n\n\n\n\n" + . "\n\n\n\n
Second
AB
12
\n"; + + $this->assertSame($expected, $this->converter->convert($djot)); + } + public function testTableCaptionMultiline(): void { $djot = "| A | B |\n|---|---|\n| 1 | 2 |\n^ This is a long caption\nthat continues on the next line"; @@ -390,6 +399,41 @@ public function testTableCaptionMultiline(): void $this->assertStringContainsString("This is a long caption\nthat continues on the next line", $result); } + public function testAdjacentCaptionMarkerIsLiteralTableCaptionText(): void + { + $djot = "| A | B |\n|---|---|\n| 1 | 2 |\n^ First\n^ Second"; + $expected = "\n\n\n\n\n\n" + . "\n\n\n\n
First\n^ Second
AB
12
\n"; + + $this->assertSame($expected, $this->converter->convert($djot)); + } + + public function testAdjacentCaptionMarkerIsLiteralImageCaptionText(): void + { + $djot = "![alt](image.png)\n^ First\n^ Second"; + $expected = "
\n\"alt\"
First\n^ Second
\n
\n"; + + $this->assertSame($expected, $this->converter->convert($djot)); + } + + public function testAdjacentCaptionMarkerIsLiteralBlockquoteCaptionText(): void + { + $djot = "> q\n^ First\n^ Second"; + $expected = "
\n
\n

q

\n
\n
First\n^ Second
\n" + . "
\n"; + + $this->assertSame($expected, $this->converter->convert($djot)); + } + + public function testTwoCaptionedTablesKeepTheirOwnCaptions(): void + { + $djot = "| A |\n|---|\n| 1 |\n^ First\n\n| B |\n|---|\n| 2 |\n^ Second"; + $expected = "\n\n\n\n\n\n\n\n
First
A
1
\n" + . "\n\n\n\n\n\n\n\n
Second
B
2
\n"; + + $this->assertSame($expected, $this->converter->convert($djot)); + } + public function testReferenceLink(): void { $djot = "[Example][ex]\n\n[ex]: https://example.com";