Skip to content

Commit 4cbb5f8

Browse files
committed
More tests.
1 parent 400870f commit 4cbb5f8

File tree

2 files changed

+130
-1
lines changed

2 files changed

+130
-1
lines changed

src/Parser/BlockParser.php

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2215,7 +2215,15 @@ protected function tryParseParagraph(Node $parent, array $lines, int $start): in
22152215
while ($i < $count) {
22162216
$nextLine = $lines[$i];
22172217

2218-
if ($this->isBlankLine($nextLine) || $this->startsNewBlock($nextLine)) {
2218+
// Check for unclosed brace in content - if present, don't break on new block
2219+
// This handles cases like: text{a=x\n# not-a-heading
2220+
$hasUnclosedBrace = $this->hasUnclosedBrace($content);
2221+
2222+
if ($this->isBlankLine($nextLine)) {
2223+
break;
2224+
}
2225+
2226+
if (!$hasUnclosedBrace && $this->startsNewBlock($nextLine)) {
22192227
break;
22202228
}
22212229

@@ -2259,6 +2267,53 @@ protected function startsNewBlock(string $line): bool
22592267
return (bool)preg_match('/^(#{1,6}\s|[-*+]\s|`{3,}|\|)/', $line);
22602268
}
22612269

2270+
/**
2271+
* Check if text has an unclosed brace (for attribute blocks)
2272+
*/
2273+
protected function hasUnclosedBrace(string $text): bool
2274+
{
2275+
$depth = 0;
2276+
$inQuote = false;
2277+
$quoteChar = '';
2278+
$len = strlen($text);
2279+
2280+
for ($i = 0; $i < $len; $i++) {
2281+
$char = $text[$i];
2282+
2283+
// Handle escape sequences
2284+
if ($char === '\\' && $i + 1 < $len) {
2285+
$i++;
2286+
2287+
continue;
2288+
}
2289+
2290+
// Handle quotes
2291+
if (!$inQuote && ($char === '"' || $char === "'")) {
2292+
$inQuote = true;
2293+
$quoteChar = $char;
2294+
2295+
continue;
2296+
}
2297+
2298+
if ($inQuote && $char === $quoteChar) {
2299+
$inQuote = false;
2300+
2301+
continue;
2302+
}
2303+
2304+
// Count braces only outside quotes
2305+
if (!$inQuote) {
2306+
if ($char === '{') {
2307+
$depth++;
2308+
} elseif ($char === '}') {
2309+
$depth--;
2310+
}
2311+
}
2312+
}
2313+
2314+
return $depth > 0;
2315+
}
2316+
22622317
/**
22632318
* @return array<string>
22642319
*/

tests/TestCase/DjotConverterTest.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2033,4 +2033,78 @@ public function testShortcutReferenceLink(): void
20332033

20342034
$this->assertStringContainsString('href="https://example.com"', $result);
20352035
}
2036+
2037+
public function testUnclosedLinkEmphasisBoundary(): void
2038+
{
2039+
// Emphasis should NOT cross [text]( boundary when link is unclosed
2040+
$djot = '[x_y](x_';
2041+
$result = $this->converter->convert($djot);
2042+
2043+
// Should be literal text, not emphasis
2044+
$this->assertSame("<p>[x_y](x_</p>\n", $result);
2045+
}
2046+
2047+
public function testUnclosedLinkWithValidEmphasis(): void
2048+
{
2049+
// Emphasis entirely within (url) part should still work
2050+
$djot = "[unclosed](hello *a\nb*";
2051+
$result = $this->converter->convert($djot);
2052+
2053+
// The *a\nb* is entirely in the url part, so emphasis applies
2054+
$this->assertStringContainsString('<strong>a', $result);
2055+
$this->assertStringContainsString('b</strong>', $result);
2056+
}
2057+
2058+
public function testReferenceDefinitionAttributes(): void
2059+
{
2060+
// Attributes before reference definition apply to the link
2061+
$djot = "{title=foo}\n[ref]: /url\n\n[ref][]";
2062+
$result = $this->converter->convert($djot);
2063+
2064+
$this->assertStringContainsString('title="foo"', $result);
2065+
$this->assertStringContainsString('href="/url"', $result);
2066+
// Attributes should be on the link, not the paragraph
2067+
$this->assertStringNotContainsString('<p title=', $result);
2068+
}
2069+
2070+
public function testReferenceDefinitionAttributeOverride(): void
2071+
{
2072+
// Inline attributes override definition attributes
2073+
$djot = "{title=foo}\n[ref]: /url\n\n[ref][]{title=bar}";
2074+
$result = $this->converter->convert($djot);
2075+
2076+
$this->assertStringContainsString('title="bar"', $result);
2077+
$this->assertStringNotContainsString('title="foo"', $result);
2078+
}
2079+
2080+
public function testAttributeOrderIdClassFirst(): void
2081+
{
2082+
// Attributes should be ordered: id first, class second, then others
2083+
$djot = 'hi{#myid .myclass key="value"}';
2084+
$result = $this->converter->convert($djot);
2085+
2086+
// Check that id comes before class, and class comes before key
2087+
$this->assertMatchesRegularExpression('/id="myid".*class="myclass".*key="value"/', $result);
2088+
}
2089+
2090+
public function testUnclosedBraceParagraphContinuation(): void
2091+
{
2092+
// Unclosed { means next line is continuation, not new block
2093+
$djot = "text{a=x\n# not-a-heading";
2094+
$result = $this->converter->convert($djot);
2095+
2096+
// Should be single paragraph, not paragraph + heading
2097+
$this->assertSame("<p>text{a=x\n# not-a-heading</p>\n", $result);
2098+
$this->assertStringNotContainsString('<h1', $result);
2099+
}
2100+
2101+
public function testUnclosedBraceAtStartOfLine(): void
2102+
{
2103+
// Unclosed { at start of line also continues
2104+
$djot = "{a=x\n# not-a-heading";
2105+
$result = $this->converter->convert($djot);
2106+
2107+
$this->assertSame("<p>{a=x\n# not-a-heading</p>\n", $result);
2108+
$this->assertStringNotContainsString('<h1', $result);
2109+
}
20362110
}

0 commit comments

Comments
 (0)