From d3865c037a38683826aae47a296f4a70b6ab2ce5 Mon Sep 17 00:00:00 2001 From: Jeroen De Dauw Date: Fri, 17 Jul 2026 17:59:55 +0200 Subject: [PATCH] Preserve typed link text in pipe-less wikilinks Fixes https://github.com/ProfessionalWiki/NativeMarkdown/issues/47 Pipe-less `[[...]]` links now display the target text as typed (like wikitext and Obsidian) instead of the normalized title; the resolved title still drives the href and link registration. Linktrail (`[[page]]s`) is deliberately not implemented: it is a language-dependent wikitext-ism absent from markdown ecosystems, and `[[page|pages]]` covers the need. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Application/CommonMark/WikiLinkNode.php | 9 ++-- .../Application/MarkdownRendererTest.php | 49 +++++++++++++++++++ .../MarkdownContentHandlerTest.php | 7 +++ .../TestDoubles/FakeWikiTitleParser.php | 12 ++++- 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/Application/CommonMark/WikiLinkNode.php b/src/Application/CommonMark/WikiLinkNode.php index fed5b78..4f01db1 100644 --- a/src/Application/CommonMark/WikiLinkNode.php +++ b/src/Application/CommonMark/WikiLinkNode.php @@ -24,15 +24,16 @@ public function __construct( parent::__construct(); } + /** + * Pipe-less links keep the target text exactly as typed (colon-stripped and + * trimmed by the parser), matching how MediaWiki wikitext and Obsidian render + * them. The resolved title drives the href and link registration, not the label. + */ public function displayLabel(): string { if ( $this->label !== null && $this->label !== '' ) { return $this->label; } - if ( $this->resolvedTitle !== null ) { - return $this->resolvedTitle->textWithFragment(); - } - return $this->target; } diff --git a/tests/phpunit/Application/MarkdownRendererTest.php b/tests/phpunit/Application/MarkdownRendererTest.php index 8b36608..3336adf 100644 --- a/tests/phpunit/Application/MarkdownRendererTest.php +++ b/tests/phpunit/Application/MarkdownRendererTest.php @@ -409,6 +409,55 @@ public function testWikiLinkRendersAndIsCollected(): void { $this->assertSame( 'Some Page', $result->links[0]->prefixedText ); } + public function testPipelessWikiLinkDisplaysTypedTextNotNormalizedTitle(): void { + $result = $this->render( 'See [[page]] here' ); + + $this->assertSame( + "

See page here

\n", + $result->html + ); + $this->assertSame( 'Page', $result->links[0]->prefixedText ); + } + + public function testPipelessWikiLinkKeepsTypedTextWithFragment(): void { + $result = $this->render( '[[page#section]]' ); + + $this->assertSame( + "

page#section

\n", + $result->html + ); + $this->assertSame( 'section', $result->links[0]->fragment ); + } + + public function testColonPrefixedCategoryLinkUsesTypedTextWithoutLeadingColon(): void { + $result = $this->render( '[[:Category:some category]]' ); + + $this->assertSame( + "

Category:some category

\n", + $result->html + ); + $this->assertSame( [], $result->categories ); + $this->assertCount( 1, $result->links ); + } + + public function testColonPrefixedFileLinkUsesTypedTextWithoutLeadingColon(): void { + $result = $this->render( '[[:File:some file.png]]' ); + + $this->assertSame( + "

File:some file.png

\n", + $result->html + ); + $this->assertSame( [], $result->files ); + $this->assertCount( 1, $result->links ); + } + + public function testPipedWikiLinkUsesLabelNotTypedTarget(): void { + $this->assertSame( + "

Label

\n", + $this->render( '[[page|Label]]' )->html + ); + } + public function testWikiLinkWithLabelUsesLabel(): void { $this->assertSame( "

the label

\n", diff --git a/tests/phpunit/EntryPoints/MarkdownContentHandlerTest.php b/tests/phpunit/EntryPoints/MarkdownContentHandlerTest.php index 764ac85..f9a3a24 100644 --- a/tests/phpunit/EntryPoints/MarkdownContentHandlerTest.php +++ b/tests/phpunit/EntryPoints/MarkdownContentHandlerTest.php @@ -42,6 +42,13 @@ public function testRegistersInternalLinks(): void { $this->assertArrayHasKey( 'Another_One', $output->getLinks()[NS_MAIN] ); } + public function testPipelessLinkDisplaysTypedTextWhileRegisteringNormalizedTitle(): void { + $output = $this->getParserOutput( 'See [[phase one matrix]] here' ); + + $this->assertStringContainsString( '>phase one matrix', $output->getRawText() ); + $this->assertArrayHasKey( 'Phase_one_matrix', $output->getLinks()[NS_MAIN] ); + } + public function testRendersMissingPageAsRedLink(): void { $output = $this->getParserOutput( '[[Surely This Page Is Missing]]' ); diff --git a/tests/phpunit/TestDoubles/FakeWikiTitleParser.php b/tests/phpunit/TestDoubles/FakeWikiTitleParser.php index f1731c6..702afca 100644 --- a/tests/phpunit/TestDoubles/FakeWikiTitleParser.php +++ b/tests/phpunit/TestDoubles/FakeWikiTitleParser.php @@ -10,7 +10,8 @@ /** * Pure-PHP stand-in for the MediaWiki TitleParser adapter. Understands just enough * title syntax for the pipeline tests: Category:/File: prefixes (case-insensitive, - * like MediaWiki), the "wikipedia" interwiki prefix, #fragments, and rejection of + * like MediaWiki), the "wikipedia" interwiki prefix, #fragments, first-letter + * capitalization (MediaWiki's $wgCapitalLinks default), and rejection of * characters MediaWiki considers invalid in titles. */ final class FakeWikiTitleParser implements WikiTitleParser { @@ -48,6 +49,7 @@ public function parse( string $titleText ): ?WikiTitle { } [ $namespace, $prefix, $text ] = $this->splitNamespace( $pageText ); + $text = $this->capitalizeFirst( $text ); return new WikiTitle( namespace: $namespace, @@ -84,4 +86,12 @@ private function splitNamespace( string $pageText ): array { return [ self::MAIN_NAMESPACE, '', $pageText ]; } + private function capitalizeFirst( string $text ): string { + if ( $text === '' ) { + return ''; + } + + return mb_strtoupper( mb_substr( $text, 0, 1 ) ) . mb_substr( $text, 1 ); + } + }