Skip to content
Closed
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
83 changes: 83 additions & 0 deletions src/Parser/InlineParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,16 @@ protected function parseDelimited(string $text, int $pos, string $delimiter, str
}
}

// Skip over link/image constructs [...](...) and [...][] or ![...](...) etc.
if ($char === '[' || ($char === '!' && ($text[$searchPos + 1] ?? '') === '[')) {
$linkEnd = $this->findLinkEnd($text, $char === '!' ? $searchPos + 1 : $searchPos);
if ($linkEnd !== null) {
$searchPos = $linkEnd;

continue;
}
}

// Skip escape sequences
if ($char === '\\' && $searchPos + 1 < $length) {
$searchPos += 2;
Expand Down Expand Up @@ -1535,6 +1545,79 @@ protected function findAutolinkEnd(string $text, int $pos): ?int
return null;
}

/**
* Find the end of a link/image construct starting at $pos (which points to '[').
* Handles [text](url), [text][ref], and [text][] forms.
*
* @return int|null Position after the link construct, or null if not a valid link
*/
protected function findLinkEnd(string $text, int $pos): ?int
{
$length = strlen($text);
if ($pos >= $length || $text[$pos] !== '[') {
return null;
}

// Find matching ]
$bracketDepth = 1;
$i = $pos + 1;
while ($i < $length && $bracketDepth > 0) {
if ($text[$i] === '[') {
$bracketDepth++;
} elseif ($text[$i] === ']') {
$bracketDepth--;
} elseif ($text[$i] === '\\' && $i + 1 < $length) {
$i++;
}
if ($bracketDepth > 0) {
$i++;
}
}

if ($bracketDepth !== 0) {
return null;
}

// $i is now at the closing ]
$afterBracket = $i + 1;

// Inline link: [text](url)
if ($afterBracket < $length && $text[$afterBracket] === '(') {
$parenDepth = 1;
$j = $afterBracket + 1;
while ($j < $length && $parenDepth > 0) {
if ($text[$j] === '(') {
$parenDepth++;
} elseif ($text[$j] === ')') {
$parenDepth--;
} elseif ($text[$j] === '\\' && $j + 1 < $length) {
$j++;
}
if ($parenDepth > 0) {
$j++;
}
}

if ($parenDepth === 0) {
return $j + 1;
}

return null;
}

// Reference link: [text][ref] or [text][]
if ($afterBracket < $length && $text[$afterBracket] === '[') {
$refEnd = strpos($text, ']', $afterBracket + 1);
if ($refEnd !== false) {
return $refEnd + 1;
}

return null;
}

return null;
}

/**
* Remove comments from attribute string: % ... % or % to end
*/
Expand Down
41 changes: 41 additions & 0 deletions tests/TestCase/Parser/InlineParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,47 @@ public function testAutolinkPrecedenceOverEmphasis(): void
$this->assertSame('http://example.com/a_b', $children[1]->getDestination());
}

public function testEmphasisWithUnderscoreInLinkUrl(): void
{
// Issue #70: underscores in link URLs should not break emphasis
$para = $this->parseInline('_[link](http://example.com?foo_bar=1), more text_');

$children = $para->getChildren();
$this->assertCount(1, $children);
$this->assertInstanceOf(Emphasis::class, $children[0]);

$emChildren = $children[0]->getChildren();
// Should contain a link node followed by text
$this->assertInstanceOf(Link::class, $emChildren[0]);
$this->assertSame('http://example.com?foo_bar=1', $emChildren[0]->getDestination());
}

public function testEmphasisWithUnderscoreInLinkUrlMoreCases(): void
{
// _hello [link](a_b) world_
$para = $this->parseInline('_hello [link](a_b) world_');

$children = $para->getChildren();
$this->assertCount(1, $children);
$this->assertInstanceOf(Emphasis::class, $children[0]);

$emChildren = $children[0]->getChildren();
$this->assertInstanceOf(Text::class, $emChildren[0]);
$this->assertSame('hello ', $emChildren[0]->getContent());
$this->assertInstanceOf(Link::class, $emChildren[1]);
$this->assertSame('a_b', $emChildren[1]->getDestination());
}

public function testStrongWithStarInLinkUrl(): void
{
// *[closed](hello*) - star in URL should not break strong
$para = $this->parseInline('*[link](http://example.com?q=a*b) text*');

$children = $para->getChildren();
$this->assertCount(1, $children);
$this->assertInstanceOf(Strong::class, $children[0]);
}

public function testEmphasisFollowedByCloseBrace(): void
{
// Emphasis opener cannot be followed by } (closer marker)
Expand Down
2 changes: 1 addition & 1 deletion tests/official/emphasis.test
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ _ab\_c_
```
_[bar_](url)
.
<p><em>[bar</em>](url)</p>
<p>_<a href="url">bar_</a></p>
```

```
Expand Down
2 changes: 1 addition & 1 deletion tests/official/links_and_images.test
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ starts first:
```
*[closed](hello*)
.
<p><strong>[closed](hello</strong>)</p>
<p>*<a href="hello*">closed</a></p>
```

Avoid this with a backslash escape:
Expand Down