From f860fabf2d15bb5f0c8436197e899f318c292721 Mon Sep 17 00:00:00 2001 From: Marc Reichel Date: Thu, 24 Sep 2026 22:29:32 +0200 Subject: [PATCH] #35420 fix(confluence): Keep link text when removing Confluence links Co-Authored-By: Claude Opus 5.5 (1M context) --- src/MacroReplacer/LinkMacroReplacer.php | 39 +++++++++++ tests/MacroReplacer/LinkMacroReplacerTest.php | 65 +++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 src/MacroReplacer/LinkMacroReplacer.php create mode 100644 tests/MacroReplacer/LinkMacroReplacerTest.php diff --git a/src/MacroReplacer/LinkMacroReplacer.php b/src/MacroReplacer/LinkMacroReplacer.php new file mode 100644 index 0000000..bfefe28 --- /dev/null +++ b/src/MacroReplacer/LinkMacroReplacer.php @@ -0,0 +1,39 @@ + with its link text, dropping the link itself. + * + * Must run before {@see OtherMacroRemover}, which would otherwise remove the link together with its text. + */ +class LinkMacroReplacer implements MacroReplacerInterface +{ + public function replace(string $haystack): string + { + return preg_replace_callback( + '/]*>(.*?)<\/ac:link>/is', + function ($match) { + $linkContent = $match[1]; + + if (preg_match('/]*>(.*?)<\/ac:link-body>/is', $linkContent, $body)) { + return $body[1]; + } + + if (preg_match('/]*>\s*\s*<\/ac:plain-text-link-body>/is', $linkContent, $body)) { + return htmlspecialchars($body[1], ENT_QUOTES | ENT_HTML5); + } + + // Without a link body, Confluence displays the title of the linked page or attachment. + if (preg_match('/ri:(?:content-title|filename)="([^"]*)"/i', $linkContent, $title)) { + return $title[1]; + } + + return ''; + }, + $haystack + ) ?? $haystack; + } +} diff --git a/tests/MacroReplacer/LinkMacroReplacerTest.php b/tests/MacroReplacer/LinkMacroReplacerTest.php new file mode 100644 index 0000000..62b324b --- /dev/null +++ b/tests/MacroReplacer/LinkMacroReplacerTest.php @@ -0,0 +1,65 @@ + + */ + public static function links(): array + { + return [ + 'plain text link body' => [ + '

See here

', + '

See contracts & more here

', + ], + 'rich text link body' => [ + '

See Contracts here

', + '

See Contracts here

', + ], + 'page link without body' => [ + '

See here

', + '

See Contracts here

', + ], + 'attachment link without body' => [ + '

See here

', + '

See manual.pdf here

', + ], + 'user link without body' => [ + '

Ask here

', + '

Ask here

', + ], + 'multiple links' => [ + ', ', + 'A, B', + ], + ]; + } + + #[DataProvider('links')] + public function testReplacesLinkWithItsText(string $input, string $expected): void + { + $this->assertSame($expected, (new LinkMacroReplacer())->replace($input)); + } + + public function testLinkTextSurvivesOtherMacroRemover(): void + { + $input = '

See here

'; + + $output = (new OtherMacroRemover())->replace((new LinkMacroReplacer())->replace($input)); + + $this->assertSame('

See Contracts here

', $output); + } +}