From 9311d469facab4f8276cf052827a5a3b878d8af1 Mon Sep 17 00:00:00 2001 From: Jeroen De Dauw Date: Thu, 16 Jul 2026 16:45:21 +0200 Subject: [PATCH 1/4] Syntax-highlight fenced code blocks via Extension:SyntaxHighlight Fixes https://github.com/ProfessionalWiki/NativeMarkdown/issues/37 A fenced code block whose info string names a language (```python) is now highlighted server-side by Extension:SyntaxHighlight (Pygments) when that extension is installed, matching how a wikitext block renders. Without it -- or for a block with no info string, an unknown language, an over-size block or a highlighter error -- the block keeps the default
 rendering, so this is a silent progressive
enhancement with no hard dependency and no new configuration.

Delegation goes through a pure CodeHighlighter port with a SyntaxHighlight
adapter in Persistence and a no-op fallback, selected in the composition root by
ExtensionRegistry. Highlight attempts are capped per render (mirroring the
template-expansion cap) because each block can shell out to Pygments and there
is no expensive-function budget in the ContentHandler context. The required
ResourceLoader modules (ext.pygments, ext.pygments.view) are registered on the
ParserOutput only when a block was actually highlighted.

Possible follow-up: a tracking category for pages whose fenced languages could
not be highlighted.

Co-Authored-By: Claude Fable 5 
---
 .github/workflows/ci.yml                      |  17 +-
 .github/workflows/installMediaWiki.sh         |   4 +
 README.md                                     |  13 ++
 phpstan.neon                                  |   4 +
 psalm.xml                                     |   4 +
 src/Application/CodeHighlighter.php           |  40 +++++
 .../CommonMark/HighlightedCodeRenderer.php    |  33 ++++
 src/Application/MarkdownRenderer.php          |  67 +++++++-
 src/Application/NoOpCodeHighlighter.php       |  25 +++
 src/Application/RenderedMarkdown.php          |   6 +-
 src/EntryPoints/MarkdownContentHandler.php    |  11 ++
 src/NativeMarkdownExtension.php               |  18 +++
 .../SyntaxHighlightCodeHighlighter.php        |  59 +++++++
 .../Application/FixtureRenderingTest.php      |   2 +
 .../Application/MarkdownRendererTest.php      | 146 +++++++++++++++++-
 tests/phpunit/Application/XssSafetyTest.php   |   2 +
 .../MarkdownContentHandlerTest.php            |  26 ++++
 .../SyntaxHighlightCodeHighlighterTest.php    |  44 ++++++
 .../TestDoubles/FakeCodeHighlighter.php       |  45 ++++++
 19 files changed, 556 insertions(+), 10 deletions(-)
 create mode 100644 src/Application/CodeHighlighter.php
 create mode 100644 src/Application/CommonMark/HighlightedCodeRenderer.php
 create mode 100644 src/Application/NoOpCodeHighlighter.php
 create mode 100644 src/Persistence/SyntaxHighlightCodeHighlighter.php
 create mode 100644 tests/phpunit/Persistence/SyntaxHighlightCodeHighlighterTest.php
 create mode 100644 tests/phpunit/TestDoubles/FakeCodeHighlighter.php

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4b6a390..ee75fbd 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -40,7 +40,7 @@ jobs:
             mediawiki
             !mediawiki/extensions/
             !mediawiki/vendor/
-          key: mw_${{ matrix.mw }}-php${{ matrix.php }}-v1
+          key: mw_${{ matrix.mw }}-php${{ matrix.php }}-v2
 
       - name: Cache Composer cache
         uses: actions/cache@v4
@@ -66,6 +66,11 @@ jobs:
 
       - run: composer update
 
+      # Optional soft dependency: rendered separately from the MW cache because
+      # extensions/ is excluded from it, so this clone runs on every job.
+      - name: Clone SyntaxHighlight
+        run: git clone --depth 1 -b ${{ matrix.mw }} https://github.com/wikimedia/mediawiki-extensions-SyntaxHighlight_GeSHi extensions/SyntaxHighlight_GeSHi
+
       - name: Run update.php
         run: php maintenance/update.php --quick
 
@@ -146,7 +151,7 @@ jobs:
             mediawiki
             !mediawiki/extensions/
             !mediawiki/vendor/
-          key: mw_static_analysis-v1
+          key: mw_static_analysis-v2
 
       - name: Cache Composer cache
         uses: actions/cache@v4
@@ -173,6 +178,12 @@ jobs:
       - name: Composer update
         run: composer update --no-progress --no-interaction --prefer-dist --optimize-autoloader
 
+      # SyntaxHighlight is only a soft dependency, but the Persistence adapter
+      # references its class, so phpstan and psalm need its source to resolve it.
+      - name: Clone SyntaxHighlight
+        working-directory: mediawiki
+        run: git clone --depth 1 -b ${{ matrix.mw }} https://github.com/wikimedia/mediawiki-extensions-SyntaxHighlight_GeSHi extensions/SyntaxHighlight_GeSHi
+
       - name: PHPStan
         run: php vendor/bin/phpstan analyse --error-format=checkstyle --no-progress | cs2pr
 
@@ -210,7 +221,7 @@ jobs:
             mediawiki
             !mediawiki/extensions/
             !mediawiki/vendor/
-          key: mw_static_analysis-v1
+          key: mw_static_analysis-v2
 
       - name: Cache Composer cache
         uses: actions/cache@v4
diff --git a/.github/workflows/installMediaWiki.sh b/.github/workflows/installMediaWiki.sh
index a5a1f3e..95d044c 100755
--- a/.github/workflows/installMediaWiki.sh
+++ b/.github/workflows/installMediaWiki.sh
@@ -28,6 +28,10 @@ echo '$wgShowExceptionDetails = true;' >> LocalSettings.php
 echo '$wgShowDBErrorBacktrace = true;' >> LocalSettings.php
 echo '$wgDevelopmentWarnings = true;' >> LocalSettings.php
 
+# Optional soft dependency: enables the integration tests that exercise the
+# SyntaxHighlight adapter (they skip themselves when it is not loaded).
+echo 'wfLoadExtension( "SyntaxHighlight_GeSHi" );' >> LocalSettings.php
+
 echo 'wfLoadExtension( "'$EXTENSION_NAME'" );' >> LocalSettings.php
 
 cat <> composer.local.json
diff --git a/README.md b/README.md
index 1c5ef39..d0339ba 100644
--- a/README.md
+++ b/README.md
@@ -68,6 +68,18 @@ For Markdown syntax highlighting in the editor, also install the [CodeEditor ext
   alt="The wiki edit form with Markdown syntax highlighting"
 />
 
+For syntax highlighting of fenced code blocks in the rendered page, install the
+[SyntaxHighlight extension] (bundled with MediaWiki). When it is installed, a fenced block whose info string
+names a language is highlighted server-side with Pygments, exactly like a wikitext `` block;
+without it, code blocks render as plain preformatted text.
+
+````markdown
+```python
+def greet(name):
+    print(f"Hello {name}")
+```
+````
+
 ## Configuration
 
 New pages use the Markdown content model where the wiki's configuration says so. Defaults apply to page
@@ -246,6 +258,7 @@ Initial release for MediaWiki 1.43+ with these features:
 [Composer install]: https://professional.wiki/en/articles/installing-mediawiki-extensions-with-composer
 [LocalSettings.php]: https://www.mediawiki.org/wiki/Manual:LocalSettings.php
 [CodeEditor extension]: https://www.mediawiki.org/wiki/Extension:CodeEditor
+[SyntaxHighlight extension]: https://www.mediawiki.org/wiki/Extension:SyntaxHighlight
 [Scribunto]: https://www.mediawiki.org/wiki/Extension:Scribunto
 [MediaWiki MCP Server]: https://github.com/ProfessionalWiki/MediaWiki-MCP-Server
 [Extension:WikiMarkdown]: https://www.mediawiki.org/wiki/Extension:WikiMarkdown
diff --git a/phpstan.neon b/phpstan.neon
index 14c5380..17222a3 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -7,6 +7,10 @@ parameters:
 		- ../../includes
 		- ../../tests/phpunit
 		- ../../vendor
+		# Optional Extension:SyntaxHighlight, referenced by the Persistence adapter
+		# but not part of the scanned MediaWiki core. Cloned in CI; present in the
+		# dev env. Scanned for symbols only, so its own code is not analysed.
+		- ../../extensions/SyntaxHighlight_GeSHi/includes
 	bootstrapFiles:
 		- ../../includes/AutoLoader.php
 	treatPhpDocTypesAsCertain: false
diff --git a/psalm.xml b/psalm.xml
index 9da5ad4..4ae1951 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -17,6 +17,10 @@
 		
 		
+		
+		
 	
 	
 		
diff --git a/src/Application/CodeHighlighter.php b/src/Application/CodeHighlighter.php
new file mode 100644
index 0000000..655cf9b
--- /dev/null
+++ b/src/Application/CodeHighlighter.php
@@ -0,0 +1,40 @@
+` rendering, so highlighting is always
+ * an enhancement over, never a replacement for, plain code blocks.
+ */
+interface CodeHighlighter {
+
+	/**
+	 * @param string $code The raw code, exactly as written between the fences.
+	 * @param string $language The first word of the fence's info string.
+	 * @return string|null Ready-to-embed HTML for the highlighted block, or null
+	 *   when the code cannot be highlighted (unknown language, highlighter
+	 *   unavailable or failing, size limit exceeded, empty code).
+	 */
+	public function highlight( string $code, string $language ): ?string;
+
+	/**
+	 * ResourceLoader modules the highlighted HTML needs to be interactive.
+	 *
+	 * @return string[]
+	 */
+	public function modules(): array;
+
+	/**
+	 * ResourceLoader style modules the highlighted HTML needs to look right.
+	 *
+	 * @return string[]
+	 */
+	public function styleModules(): array;
+
+}
diff --git a/src/Application/CommonMark/HighlightedCodeRenderer.php b/src/Application/CommonMark/HighlightedCodeRenderer.php
new file mode 100644
index 0000000..df9238b
--- /dev/null
+++ b/src/Application/CommonMark/HighlightedCodeRenderer.php
@@ -0,0 +1,33 @@
+` block. Stateless: the highlighting decision is made in
+ * the pipeline and only its result is read back here at render time.
+ */
+final class HighlightedCodeRenderer implements NodeRendererInterface {
+
+	/**
+	 * Data-bag key under which MarkdownRenderer stashes the highlighted HTML of a
+	 * fenced code block, read back here at render time.
+	 */
+	public const HIGHLIGHTED_HTML_KEY = 'native_markdown_highlighted_html';
+
+	public function render( Node $node, ChildNodeRendererInterface $childRenderer ): ?string {
+		/** @var string|null $highlightedHtml */
+		$highlightedHtml = $node->data->get( self::HIGHLIGHTED_HTML_KEY, null );
+
+		return $highlightedHtml;
+	}
+
+}
diff --git a/src/Application/MarkdownRenderer.php b/src/Application/MarkdownRenderer.php
index 311c845..e0c7a1b 100644
--- a/src/Application/MarkdownRenderer.php
+++ b/src/Application/MarkdownRenderer.php
@@ -8,6 +8,7 @@
 use League\CommonMark\Exception\CommonMarkException;
 use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
 use League\CommonMark\Extension\DefaultAttributes\DefaultAttributesExtension;
+use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
 use League\CommonMark\Extension\CommonMark\Node\Block\Heading;
 use League\CommonMark\Extension\CommonMark\Node\Inline\Image;
 use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
@@ -29,6 +30,7 @@
 use League\CommonMark\Util\HtmlFilter;
 use ProfessionalWiki\NativeMarkdown\Application\CommonMark\FileEmbedNode;
 use ProfessionalWiki\NativeMarkdown\Application\CommonMark\FileEmbedNodeRenderer;
+use ProfessionalWiki\NativeMarkdown\Application\CommonMark\HighlightedCodeRenderer;
 use ProfessionalWiki\NativeMarkdown\Application\CommonMark\ImageLinkRenderer;
 use ProfessionalWiki\NativeMarkdown\Application\CommonMark\MarkdownLinkRenderer;
 use ProfessionalWiki\NativeMarkdown\Application\CommonMark\SpacedLinkParser;
@@ -68,6 +70,16 @@ final class MarkdownRenderer {
 	 */
 	private const MAX_TEMPLATE_EXPANSIONS_PER_RENDER = 100;
 
+	/**
+	 * Upper bound on syntax-highlight attempts per page render. Each attempt can
+	 * shell out to an external highlighter (pygmentize), and unlike wikitext's
+	 * `` there is no wikitext-parser expensive-function budget
+	 * bounding it in the ContentHandler context, so the count is capped here. 100
+	 * is generous for real pages while turning the worst case into a fixed number
+	 * of invocations; blocks past the cap render with the default escaped renderer.
+	 */
+	private const MAX_HIGHLIGHTED_BLOCKS_PER_RENDER = 100;
+
 	private MarkdownParser $parser;
 	private HtmlRenderer $htmlRenderer;
 	private FrontMatterParser $frontMatterParser;
@@ -83,6 +95,7 @@ public function __construct(
 		private readonly WikiTitleParser $titleParser,
 		private readonly PageLinkRenderer $pageLinkRenderer,
 		private readonly FileEmbedRenderer $fileEmbedRenderer,
+		private readonly CodeHighlighter $codeHighlighter,
 		bool $allowExternalImages,
 		int $maxNestingLevel,
 		private readonly ?string $tocPlaceholderHtml,
@@ -136,6 +149,10 @@ private function newEnvironment(
 		$environment->addRenderer( WikiLinkNode::class, new WikiLinkNodeRenderer( $this->pageLinkRenderer ) );
 		$environment->addRenderer( FileEmbedNode::class, new FileEmbedNodeRenderer( $this->fileEmbedRenderer ) );
 		$environment->addRenderer( TocPlaceholder::class, new TocPlaceholderRenderer() );
+		// Runs above league's default FencedCodeRenderer, but is a no-op unless the
+		// pipeline stashed highlighted HTML on the node, in which case it returns
+		// that; otherwise it returns null and the default renderer takes over.
+		$environment->addRenderer( FencedCode::class, new HighlightedCodeRenderer(), self::RENDERER_OVERRIDE_PRIORITY );
 		$environment->addRenderer(
 			Link::class,
 			new MarkdownLinkRenderer( $this->pageLinkRenderer, $this->externalUrlDetector, $noFollowExternalLinks ),
@@ -206,6 +223,10 @@ private function renderDocument(
 		$this->preloadFileLookups( $files, $generateHtml );
 		$sections = $this->processHeadings( $document );
 
+		// Only while rendering HTML: a metadata-only parse needs no highlighting,
+		// and skipping it avoids the highlighter's cost when the HTML is discarded.
+		$highlighted = $generateHtml && $this->highlightCodeBlocks( $document );
+
 		return new RenderedMarkdown(
 			html: $generateHtml ? $this->renderHtml( $document, $sections ) : '',
 			links: $links,
@@ -213,10 +234,50 @@ private function renderDocument(
 			files: $files,
 			sections: $sections,
 			externalLinks: $this->collectExternalLinks( $document ),
-			frontMatter: $frontMatter
+			frontMatter: $frontMatter,
+			modules: $highlighted ? $this->codeHighlighter->modules() : [],
+			styleModules: $highlighted ? $this->codeHighlighter->styleModules() : []
 		);
 	}
 
+	/**
+	 * Delegates each fenced block that has an info string to the code highlighter,
+	 * up to MAX_HIGHLIGHTED_BLOCKS_PER_RENDER attempts, and stashes each non-null
+	 * result on its node for HighlightedCodeRenderer to emit. Blocks with no info
+	 * string, blocks the highlighter declines, and every block past the cap keep
+	 * the default escaped rendering.
+	 *
+	 * @return bool Whether at least one block was highlighted, so the caller
+	 *   registers the highlighter's ResourceLoader modules only when they matter.
+	 */
+	private function highlightCodeBlocks( Document $document ): bool {
+		$attemptCount = 0;
+		$anyHighlighted = false;
+
+		foreach ( $this->nodesOfType( $document, FencedCode::class ) as $node ) {
+			$language = $node->getInfoWords()[0] ?? '';
+
+			if ( $language === '' ) {
+				continue;
+			}
+
+			if ( $attemptCount >= self::MAX_HIGHLIGHTED_BLOCKS_PER_RENDER ) {
+				break;
+			}
+
+			$attemptCount++;
+
+			$html = $this->codeHighlighter->highlight( $node->getLiteral(), $language );
+
+			if ( $html !== null ) {
+				$node->data->set( HighlightedCodeRenderer::HIGHLIGHTED_HTML_KEY, $html );
+				$anyHighlighted = true;
+			}
+		}
+
+		return $anyHighlighted;
+	}
+
 	/**
 	 * Replaces the raw wikitext of each template call with the expander's HTML,
 	 * up to MAX_TEMPLATE_EXPANSIONS_PER_RENDER calls. Unbalanced block calls, and
@@ -334,7 +395,9 @@ private function escapedSourceFallback( string $markdown, bool $generateHtml ):
 			files: [],
 			sections: [],
 			externalLinks: [],
-			frontMatter: null
+			frontMatter: null,
+			modules: [],
+			styleModules: []
 		);
 	}
 
diff --git a/src/Application/NoOpCodeHighlighter.php b/src/Application/NoOpCodeHighlighter.php
new file mode 100644
index 0000000..713fa3a
--- /dev/null
+++ b/src/Application/NoOpCodeHighlighter.php
@@ -0,0 +1,25 @@
+|null $frontMatter
+	 * @param string[] $modules ResourceLoader modules the rendered HTML needs
+	 * @param string[] $styleModules ResourceLoader style modules the rendered HTML needs
 	 */
 	public function __construct(
 		public readonly string $html,
@@ -25,7 +27,9 @@ public function __construct(
 		public readonly array $files,
 		public readonly array $sections,
 		public readonly array $externalLinks,
-		public readonly ?array $frontMatter
+		public readonly ?array $frontMatter,
+		public readonly array $modules,
+		public readonly array $styleModules
 	) {
 	}
 
diff --git a/src/EntryPoints/MarkdownContentHandler.php b/src/EntryPoints/MarkdownContentHandler.php
index 514eb23..e54f3a6 100644
--- a/src/EntryPoints/MarkdownContentHandler.php
+++ b/src/EntryPoints/MarkdownContentHandler.php
@@ -142,6 +142,7 @@ protected function fillParserOutput( Content $content, ContentParseParams $cpoPa
 		$this->registerFiles( $output, $rendered );
 		$this->registerExternalLinks( $output, $rendered );
 		$this->registerFrontMatter( $output, $rendered );
+		$this->registerModules( $output, $rendered );
 
 		if ( $templateExpander !== null ) {
 			$templateExpander->mergeInto( $output );
@@ -212,6 +213,16 @@ private function registerFrontMatter( ParserOutput $output, RenderedMarkdown $re
 		}
 	}
 
+	/**
+	 * Loads the ResourceLoader modules the rendered HTML needs, such as the
+	 * syntax-highlighting styles for highlighted code blocks. Empty when the
+	 * render produced nothing that needs a module.
+	 */
+	private function registerModules( ParserOutput $output, RenderedMarkdown $rendered ): void {
+		$output->addModules( $rendered->modules );
+		$output->addModuleStyles( $rendered->styleModules );
+	}
+
 	/**
 	 * Sets the table of contents from the page's own markdown headings,
 	 * authoritatively: it overwrites any TOCData and SHOW_TOC flag a transcluded
diff --git a/src/NativeMarkdownExtension.php b/src/NativeMarkdownExtension.php
index 4fcc9f9..ae3248f 100644
--- a/src/NativeMarkdownExtension.php
+++ b/src/NativeMarkdownExtension.php
@@ -8,13 +8,17 @@
 use MediaWiki\Page\PageReference;
 use MediaWiki\Parser\Parser;
 use MediaWiki\Parser\ParserOptions;
+use MediaWiki\Registration\ExtensionRegistry;
+use ProfessionalWiki\NativeMarkdown\Application\CodeHighlighter;
 use ProfessionalWiki\NativeMarkdown\Application\MarkdownDefaultPolicy;
 use ProfessionalWiki\NativeMarkdown\Application\MarkdownRenderer;
+use ProfessionalWiki\NativeMarkdown\Application\NoOpCodeHighlighter;
 use ProfessionalWiki\NativeMarkdown\Application\RedirectSyntax;
 use ProfessionalWiki\NativeMarkdown\Persistence\MediaWikiFileEmbedRenderer;
 use ProfessionalWiki\NativeMarkdown\Persistence\MediaWikiPageLinkRenderer;
 use ProfessionalWiki\NativeMarkdown\Persistence\MediaWikiTemplateExpander;
 use ProfessionalWiki\NativeMarkdown\Persistence\MediaWikiTitleParser;
+use ProfessionalWiki\NativeMarkdown\Persistence\SyntaxHighlightCodeHighlighter;
 
 /**
  * Composition root. All object graph wiring happens here; MediaWikiServices
@@ -69,6 +73,7 @@ private function newMarkdownRenderer(): MarkdownRenderer {
 				$services->getLinkRenderer(),
 				$this->defaultThumbnailWidth( $services )
 			),
+			codeHighlighter: $this->newCodeHighlighter(),
 			allowExternalImages: (bool)$services->getMainConfig()->get( 'NativeMarkdownAllowExternalImages' ),
 			maxNestingLevel: self::MAX_NESTING_LEVEL,
 			tocPlaceholderHtml: Parser::TOC_PLACEHOLDER,
@@ -78,6 +83,19 @@ private function newMarkdownRenderer(): MarkdownRenderer {
 		);
 	}
 
+	/**
+	 * Delegates fenced-code highlighting to Extension:SyntaxHighlight when it is
+	 * installed, and to a no-op otherwise, so highlighting is a silent
+	 * enhancement rather than a hard dependency.
+	 */
+	private function newCodeHighlighter(): CodeHighlighter {
+		if ( ExtensionRegistry::getInstance()->isLoaded( 'SyntaxHighlight' ) ) {
+			return new SyntaxHighlightCodeHighlighter();
+		}
+
+		return new NoOpCodeHighlighter();
+	}
+
 	/**
 	 * The width a bare `thumb` embed renders at, mirroring how the wikitext
 	 * parser sizes a thumbnail: the default thumbnail preference indexed into
diff --git a/src/Persistence/SyntaxHighlightCodeHighlighter.php b/src/Persistence/SyntaxHighlightCodeHighlighter.php
new file mode 100644
index 0000000..710abe1
--- /dev/null
+++ b/src/Persistence/SyntaxHighlightCodeHighlighter.php
@@ -0,0 +1,59 @@
+` uses, so a code block looks the
+ * same whether it is written in Markdown or wikitext.
+ */
+final class SyntaxHighlightCodeHighlighter implements CodeHighlighter {
+
+	public function highlight( string $code, string $language ): ?string {
+		// Trim the same way SyntaxHighlight's own tag hook does, for parity with a
+		// wikitext code block. No Parser is passed: there is none in ContentHandler
+		// context, and MarkdownRenderer's per-render cap replaces the wikitext
+		// parser's expensive-function budget.
+		$trimmedCode = rtrim( trim( $code, "\n" ) );
+
+		// Empty code has nothing to highlight; SyntaxHighlight would still wrap it
+		// in an empty highlight div, so short-circuit to the default rendering.
+		if ( $trimmedCode === '' ) {
+			return null;
+		}
+
+		try {
+			$status = SyntaxHighlight::highlight( $trimmedCode, $language );
+		} catch ( \Exception ) {
+			// unwrap() throws a RuntimeException on unexpected Pygments output.
+			return null;
+		}
+
+		// A non-good status covers an unknown language, an exceeded size limit and
+		// a Pygments invocation failure: all should degrade to default rendering.
+		if ( !$status->isGood() ) {
+			return null;
+		}
+
+		// highlight()'s contract makes the caller responsible for loading
+		// ext.pygments; modules() and styleModules() below provide that.
+		/** @psalm-suppress MixedAssignment Status::getValue() is untyped; guarded by is_string(). */
+		$html = $status->getValue();
+
+		return is_string( $html ) ? $html : null;
+	}
+
+	public function modules(): array {
+		return [ 'ext.pygments.view' ];
+	}
+
+	public function styleModules(): array {
+		return [ 'ext.pygments' ];
+	}
+
+}
diff --git a/tests/phpunit/Application/FixtureRenderingTest.php b/tests/phpunit/Application/FixtureRenderingTest.php
index ebd11f4..381161d 100644
--- a/tests/phpunit/Application/FixtureRenderingTest.php
+++ b/tests/phpunit/Application/FixtureRenderingTest.php
@@ -6,6 +6,7 @@
 
 use PHPUnit\Framework\TestCase;
 use ProfessionalWiki\NativeMarkdown\Application\MarkdownRenderer;
+use ProfessionalWiki\NativeMarkdown\Application\NoOpCodeHighlighter;
 use ProfessionalWiki\NativeMarkdown\Tests\TestDoubles\FakeFileEmbedRenderer;
 use ProfessionalWiki\NativeMarkdown\Tests\TestDoubles\FakePageLinkRenderer;
 use ProfessionalWiki\NativeMarkdown\Tests\TestDoubles\FakeWikiTitleParser;
@@ -45,6 +46,7 @@ private function render( string $markdown ): string {
 			titleParser: new FakeWikiTitleParser(),
 			pageLinkRenderer: new FakePageLinkRenderer(),
 			fileEmbedRenderer: new FakeFileEmbedRenderer(),
+			codeHighlighter: new NoOpCodeHighlighter(),
 			allowExternalImages: false,
 			maxNestingLevel: 100,
 			tocPlaceholderHtml: null,
diff --git a/tests/phpunit/Application/MarkdownRendererTest.php b/tests/phpunit/Application/MarkdownRendererTest.php
index 4bdd585..93fec75 100644
--- a/tests/phpunit/Application/MarkdownRendererTest.php
+++ b/tests/phpunit/Application/MarkdownRendererTest.php
@@ -5,12 +5,15 @@
 namespace ProfessionalWiki\NativeMarkdown\Tests\Application;
 
 use PHPUnit\Framework\TestCase;
+use ProfessionalWiki\NativeMarkdown\Application\CodeHighlighter;
 use ProfessionalWiki\NativeMarkdown\Application\MarkdownRenderer;
+use ProfessionalWiki\NativeMarkdown\Application\NoOpCodeHighlighter;
 use ProfessionalWiki\NativeMarkdown\Application\PageLinkRenderer;
 use ProfessionalWiki\NativeMarkdown\Application\RenderedMarkdown;
 use ProfessionalWiki\NativeMarkdown\Application\Section;
 use ProfessionalWiki\NativeMarkdown\Application\TemplateExpander;
 use ProfessionalWiki\NativeMarkdown\Tests\FrontMatterBombs;
+use ProfessionalWiki\NativeMarkdown\Tests\TestDoubles\FakeCodeHighlighter;
 use ProfessionalWiki\NativeMarkdown\Tests\TestDoubles\FakeFileEmbedRenderer;
 use ProfessionalWiki\NativeMarkdown\Tests\TestDoubles\FakePageLinkRenderer;
 use ProfessionalWiki\NativeMarkdown\Tests\TestDoubles\FakeTemplateExpander;
@@ -22,6 +25,8 @@
 /**
  * @covers \ProfessionalWiki\NativeMarkdown\Application\MarkdownRenderer
  * @covers \ProfessionalWiki\NativeMarkdown\Application\RenderedMarkdown
+ * @covers \ProfessionalWiki\NativeMarkdown\Application\NoOpCodeHighlighter
+ * @covers \ProfessionalWiki\NativeMarkdown\Application\CommonMark\HighlightedCodeRenderer
  * @covers \ProfessionalWiki\NativeMarkdown\Application\FileEmbed
  * @covers \ProfessionalWiki\NativeMarkdown\Application\HeadingAnchorBuilder
  * @covers \ProfessionalWiki\NativeMarkdown\Application\CommonMark\WikiLinkParser
@@ -51,12 +56,14 @@ private function newRenderer(
 		bool $allowExternalImages = false,
 		?PageLinkRenderer $pageLinkRenderer = null,
 		?string $tocPlaceholderHtml = null,
-		bool $templateTransclusion = false
+		bool $templateTransclusion = false,
+		?CodeHighlighter $codeHighlighter = null
 	): MarkdownRenderer {
 		return new MarkdownRenderer(
 			titleParser: new FakeWikiTitleParser(),
 			pageLinkRenderer: $pageLinkRenderer ?? new FakePageLinkRenderer(),
 			fileEmbedRenderer: new FakeFileEmbedRenderer(),
+			codeHighlighter: $codeHighlighter ?? new NoOpCodeHighlighter(),
 			allowExternalImages: $allowExternalImages,
 			maxNestingLevel: 100,
 			tocPlaceholderHtml: $tocPlaceholderHtml,
@@ -73,10 +80,16 @@ private function render(
 		bool $generateHtml = true,
 		?string $tocPlaceholderHtml = null,
 		bool $templateTransclusion = false,
-		?TemplateExpander $templateExpander = null
+		?TemplateExpander $templateExpander = null,
+		?CodeHighlighter $codeHighlighter = null
 	): RenderedMarkdown {
-		return $this->newRenderer( $allowExternalImages, $pageLinkRenderer, $tocPlaceholderHtml, $templateTransclusion )
-			->render( $markdown, $generateHtml, $templateExpander );
+		return $this->newRenderer(
+			$allowExternalImages,
+			$pageLinkRenderer,
+			$tocPlaceholderHtml,
+			$templateTransclusion,
+			$codeHighlighter
+		)->render( $markdown, $generateHtml, $templateExpander );
 	}
 
 	private function extractPlainText( string $markdown ): string {
@@ -649,6 +662,7 @@ public function testFileLookupsArePreloadedInOneBatchBeforeRendering(): void {
 			titleParser: new FakeWikiTitleParser(),
 			pageLinkRenderer: new FakePageLinkRenderer(),
 			fileEmbedRenderer: $fileEmbedRenderer,
+			codeHighlighter: new NoOpCodeHighlighter(),
 			allowExternalImages: false,
 			maxNestingLevel: 100,
 			tocPlaceholderHtml: null,
@@ -1285,4 +1299,128 @@ public function testCapBoundaryExpandsTheHundredthCallButLeavesTheHundredFirstLi
 		$this->assertStringContainsString( '

{{T101}}

', $html ); } + public function testFencedCodeWithLanguageIsRenderedByTheHighlighter(): void { + $html = $this->render( + "```python\nprint('hi')\n```", + codeHighlighter: new FakeCodeHighlighter() + )->html; + + $this->assertStringContainsString( '
HIGHLIGHTED
', $html ); + $this->assertStringNotContainsString( 'html; + + $this->assertStringContainsString( '
', $html );
+	}
+
+	public function testFencedCodeWithoutInfoStringIsNotHighlighted(): void {
+		$highlighter = new FakeCodeHighlighter();
+
+		$this->render( "```\nplain code\n```", codeHighlighter: $highlighter );
+
+		$this->assertSame( [], $highlighter->calls );
+	}
+
+	public function testIndentedCodeBlockIsNotHighlighted(): void {
+		$highlighter = new FakeCodeHighlighter();
+
+		$this->render( "    indented code line\n", codeHighlighter: $highlighter );
+
+		$this->assertSame( [], $highlighter->calls );
+	}
+
+	public function testFirstInfoWordIsPassedAsTheLanguage(): void {
+		$highlighter = new FakeCodeHighlighter();
+
+		$this->render( "```python line-numbers\ncode\n```", codeHighlighter: $highlighter );
+
+		$this->assertSame( 'python', $highlighter->calls[0]['language'] );
+	}
+
+	public function testFencedCodeLiteralIsPassedToTheHighlighter(): void {
+		$highlighter = new FakeCodeHighlighter();
+
+		$this->render( "```js\nconst x = 1;\n```", codeHighlighter: $highlighter );
+
+		$this->assertSame( "const x = 1;\n", $highlighter->calls[0]['code'] );
+	}
+
+	public function testMetadataOnlyRenderDoesNotHighlightCode(): void {
+		$highlighter = new FakeCodeHighlighter();
+
+		$this->render( "```python\nprint('hi')\n```", generateHtml: false, codeHighlighter: $highlighter );
+
+		$this->assertSame( [], $highlighter->calls );
+	}
+
+	public function testHighlighterModulesAreReportedWhenABlockIsHighlighted(): void {
+		$result = $this->render(
+			"```python\nprint('hi')\n```",
+			codeHighlighter: new FakeCodeHighlighter(
+				modules: [ 'ext.demo.view' ],
+				styleModules: [ 'ext.demo' ]
+			)
+		);
+
+		$this->assertSame( [ 'ext.demo.view' ], $result->modules );
+		$this->assertSame( [ 'ext.demo' ], $result->styleModules );
+	}
+
+	public function testNoModulesWhenDocumentHasNoHighlightableCode(): void {
+		$result = $this->render(
+			'Just a paragraph with no code.',
+			codeHighlighter: new FakeCodeHighlighter()
+		);
+
+		$this->assertSame( [], $result->modules );
+		$this->assertSame( [], $result->styleModules );
+	}
+
+	public function testNoModulesWhenEveryBlockDeclinesHighlighting(): void {
+		$result = $this->render(
+			"```python\nprint('hi')\n```",
+			codeHighlighter: new FakeCodeHighlighter( html: null )
+		);
+
+		$this->assertSame( [], $result->modules );
+		$this->assertSame( [], $result->styleModules );
+	}
+
+	/**
+	 * A stack of distinct fenced blocks ```lang1 .. ```lang$count, each with a
+	 * one-word language so every block is a highlight candidate.
+	 */
+	private function fencedCodeBlocks( int $count ): string {
+		return implode(
+			"\n\n",
+			array_map(
+				static fn ( int $number ) => "```lang$number\ncode $number\n```",
+				range( 1, $count )
+			)
+		);
+	}
+
+	public function testHighlightAttemptsAreCappedPerRender(): void {
+		$highlighter = new FakeCodeHighlighter();
+
+		$this->render( $this->fencedCodeBlocks( 101 ), codeHighlighter: $highlighter );
+
+		$this->assertCount( 100, $highlighter->calls );
+	}
+
+	public function testFencedBlockPastTheHighlightCapKeepsDefaultRendering(): void {
+		$html = $this->render(
+			$this->fencedCodeBlocks( 101 ),
+			codeHighlighter: new FakeCodeHighlighter()
+		)->html;
+
+		$this->assertStringNotContainsString( 'class="language-lang100"', $html );
+		$this->assertStringContainsString( 'class="language-lang101"', $html );
+	}
+
 }
diff --git a/tests/phpunit/Application/XssSafetyTest.php b/tests/phpunit/Application/XssSafetyTest.php
index b34a956..4e1e41b 100644
--- a/tests/phpunit/Application/XssSafetyTest.php
+++ b/tests/phpunit/Application/XssSafetyTest.php
@@ -6,6 +6,7 @@
 
 use PHPUnit\Framework\TestCase;
 use ProfessionalWiki\NativeMarkdown\Application\MarkdownRenderer;
+use ProfessionalWiki\NativeMarkdown\Application\NoOpCodeHighlighter;
 use ProfessionalWiki\NativeMarkdown\Tests\TestDoubles\FakeFileEmbedRenderer;
 use ProfessionalWiki\NativeMarkdown\Tests\TestDoubles\FakePageLinkRenderer;
 use ProfessionalWiki\NativeMarkdown\Tests\TestDoubles\FakeWikiTitleParser;
@@ -94,6 +95,7 @@ private function render( string $markdown, bool $allowExternalImages = false ):
 			titleParser: new FakeWikiTitleParser(),
 			pageLinkRenderer: new FakePageLinkRenderer(),
 			fileEmbedRenderer: new FakeFileEmbedRenderer(),
+			codeHighlighter: new NoOpCodeHighlighter(),
 			allowExternalImages: $allowExternalImages,
 			maxNestingLevel: 100,
 			tocPlaceholderHtml: null,
diff --git a/tests/phpunit/EntryPoints/MarkdownContentHandlerTest.php b/tests/phpunit/EntryPoints/MarkdownContentHandlerTest.php
index 363db02..a6e102f 100644
--- a/tests/phpunit/EntryPoints/MarkdownContentHandlerTest.php
+++ b/tests/phpunit/EntryPoints/MarkdownContentHandlerTest.php
@@ -8,6 +8,7 @@
 use MediaWiki\MainConfigNames;
 use MediaWiki\Page\PageReferenceValue;
 use MediaWiki\Parser\ParserOutput;
+use MediaWiki\Registration\ExtensionRegistry;
 use MediaWikiIntegrationTestCase;
 use ProfessionalWiki\NativeMarkdown\EntryPoints\MarkdownContent;
 use ProfessionalWiki\NativeMarkdown\Tests\FrontMatterBombs;
@@ -218,6 +219,31 @@ public function testInterwikiFragmentOnlyLinkKeepsPrefixInLabel(): void {
 		$this->assertStringContainsString( '>wikipedia:#History', $output->getRawText() );
 	}
 
+	public function testFencedCodeBlockWithLanguageIsSyntaxHighlighted(): void {
+		$this->skipIfSyntaxHighlightMissing();
+
+		$output = $this->getParserOutput( "```python\nprint('hi')\n```" );
+
+		$this->assertStringContainsString( 'mw-highlight-lang-python', $output->getRawText() );
+		$this->assertContains( 'ext.pygments', $output->getModuleStyles() );
+		$this->assertContains( 'ext.pygments.view', $output->getModules() );
+	}
+
+	public function testFencedCodeBlockWithoutLanguageIsNotHighlighted(): void {
+		$this->skipIfSyntaxHighlightMissing();
+
+		$output = $this->getParserOutput( "```\nplain code\n```" );
+
+		$this->assertStringContainsString( '
plain code', $output->getRawText() );
+		$this->assertNotContains( 'ext.pygments', $output->getModuleStyles() );
+	}
+
+	private function skipIfSyntaxHighlightMissing(): void {
+		if ( !ExtensionRegistry::getInstance()->isLoaded( 'SyntaxHighlight' ) ) {
+			$this->markTestSkipped( 'Extension:SyntaxHighlight is not installed' );
+		}
+	}
+
 	private function configureWikipediaInterwiki(): void {
 		$globalScope = 2;
 		$this->overrideConfigValues( [
diff --git a/tests/phpunit/Persistence/SyntaxHighlightCodeHighlighterTest.php b/tests/phpunit/Persistence/SyntaxHighlightCodeHighlighterTest.php
new file mode 100644
index 0000000..683a386
--- /dev/null
+++ b/tests/phpunit/Persistence/SyntaxHighlightCodeHighlighterTest.php
@@ -0,0 +1,44 @@
+isLoaded( 'SyntaxHighlight' ) ) {
+			$this->markTestSkipped( 'Extension:SyntaxHighlight is not installed' );
+		}
+	}
+
+	private function highlight( string $code, string $language ): ?string {
+		return ( new SyntaxHighlightCodeHighlighter() )->highlight( $code, $language );
+	}
+
+	public function testKnownLanguageIsHighlightedIntoPygmentsMarkup(): void {
+		$html = $this->highlight( 'print("hi")', 'python' );
+
+		$this->assertNotNull( $html );
+		$this->assertStringContainsString( 'mw-highlight-lang-python', $html );
+		$this->assertStringContainsString( 'assertNull( $this->highlight( 'print("hi")', 'definitelynotalanguage' ) );
+	}
+
+	public function testEmptyCodeIsNotHighlighted(): void {
+		$this->assertNull( $this->highlight( '', 'python' ) );
+	}
+
+}
diff --git a/tests/phpunit/TestDoubles/FakeCodeHighlighter.php b/tests/phpunit/TestDoubles/FakeCodeHighlighter.php
new file mode 100644
index 0000000..e4b72e8
--- /dev/null
+++ b/tests/phpunit/TestDoubles/FakeCodeHighlighter.php
@@ -0,0 +1,45 @@
+ */
+	public array $calls = [];
+
+	/**
+	 * @param string[] $modules
+	 * @param string[] $styleModules
+	 */
+	public function __construct(
+		private readonly ?string $html = '
HIGHLIGHTED
', + private readonly array $modules = [ 'test.pygments.view' ], + private readonly array $styleModules = [ 'test.pygments' ] + ) { + } + + public function highlight( string $code, string $language ): ?string { + $this->calls[] = [ 'code' => $code, 'language' => $language ]; + + return $this->html; + } + + public function modules(): array { + return $this->modules; + } + + public function styleModules(): array { + return $this->styleModules; + } + +} From c08e2dd1030b18967d992f8ffc4e720fa8d7b976 Mon Sep 17 00:00:00 2001 From: Jeroen De Dauw Date: Thu, 16 Jul 2026 18:33:23 +0200 Subject: [PATCH 2/4] Reset skin inline-code styling inside fenced code blocks Fixes https://github.com/ProfessionalWiki/NativeMarkdown/issues/36 Skins style bare `code` for inline use, giving it a background, a border and padding. Since `code` is an inline element, that background paints once per line box, so a code block showed a pill behind every one of its lines inside the box `pre` already draws. Wikitext never nests the two elements, so no skin resets this, while CommonMark renders every code block as `pre > code`. Ships the extension's first style module, loaded on every markdown page: it is tiny, it also covers indented code blocks, and it gives future markdown-specific styling a home. Co-Authored-By: Claude Fable 5 --- extension.json | 11 +++++++++++ modules/ext.nativeMarkdown.content.css | 15 +++++++++++++++ src/EntryPoints/MarkdownContentHandler.php | 10 ++++++---- .../EntryPoints/MarkdownContentHandlerTest.php | 6 ++++++ 4 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 modules/ext.nativeMarkdown.content.css diff --git a/extension.json b/extension.json index 190f6ca..5211d72 100644 --- a/extension.json +++ b/extension.json @@ -26,6 +26,17 @@ ] }, + "ResourceFileModulePaths": { + "localBasePath": "modules", + "remoteExtPath": "NativeMarkdown/modules" + }, + + "ResourceModules": { + "ext.nativeMarkdown.content": { + "styles": "ext.nativeMarkdown.content.css" + } + }, + "AutoloadNamespaces": { "ProfessionalWiki\\NativeMarkdown\\": "src/", "ProfessionalWiki\\NativeMarkdown\\Tests\\": "tests/phpunit/" diff --git a/modules/ext.nativeMarkdown.content.css b/modules/ext.nativeMarkdown.content.css new file mode 100644 index 0000000..5b2cfb5 --- /dev/null +++ b/modules/ext.nativeMarkdown.content.css @@ -0,0 +1,15 @@ +/*! + * Base styles for markdown content. + * + * Skins style bare `code` for inline use, giving it a background, a border and + * padding. Since `code` is an inline element, that background paints once per + * line box, so a code block ends up with a pill behind each of its lines inside + * the box `pre` already draws. Wikitext never nests the two elements, so no skin + * resets this; CommonMark renders every code block as `pre > code`. + */ +.mw-parser-output pre > code { + background-color: transparent; + border: 0; + border-radius: 0; + padding: 0; +} diff --git a/src/EntryPoints/MarkdownContentHandler.php b/src/EntryPoints/MarkdownContentHandler.php index e54f3a6..69876c7 100644 --- a/src/EntryPoints/MarkdownContentHandler.php +++ b/src/EntryPoints/MarkdownContentHandler.php @@ -27,6 +27,8 @@ final class MarkdownContentHandler extends TextContentHandler { private const FRONT_MATTER_DATA_KEY = 'nativemarkdown-front-matter'; + private const CONTENT_STYLES_MODULE = 'ext.nativeMarkdown.content'; + // Matches the threshold used by the wikitext parser ("enough" headings for a ToC). private const TOC_SECTION_THRESHOLD = 4; @@ -214,13 +216,13 @@ private function registerFrontMatter( ParserOutput $output, RenderedMarkdown $re } /** - * Loads the ResourceLoader modules the rendered HTML needs, such as the - * syntax-highlighting styles for highlighted code blocks. Empty when the - * render produced nothing that needs a module. + * Loads the base styles every markdown page needs, since skins do not style + * CommonMark's markup correctly on their own, plus the modules the render + * itself needs, such as the highlighter's for highlighted code blocks. */ private function registerModules( ParserOutput $output, RenderedMarkdown $rendered ): void { + $output->addModuleStyles( array_merge( [ self::CONTENT_STYLES_MODULE ], $rendered->styleModules ) ); $output->addModules( $rendered->modules ); - $output->addModuleStyles( $rendered->styleModules ); } /** diff --git a/tests/phpunit/EntryPoints/MarkdownContentHandlerTest.php b/tests/phpunit/EntryPoints/MarkdownContentHandlerTest.php index a6e102f..36b79e8 100644 --- a/tests/phpunit/EntryPoints/MarkdownContentHandlerTest.php +++ b/tests/phpunit/EntryPoints/MarkdownContentHandlerTest.php @@ -219,6 +219,12 @@ public function testInterwikiFragmentOnlyLinkKeepsPrefixInLabel(): void { $this->assertStringContainsString( '>wikipedia:#History', $output->getRawText() ); } + public function testMarkdownPageLoadsContentStyles(): void { + $output = $this->getParserOutput( 'Just some prose, without a code block in sight.' ); + + $this->assertContains( 'ext.nativeMarkdown.content', $output->getModuleStyles() ); + } + public function testFencedCodeBlockWithLanguageIsSyntaxHighlighted(): void { $this->skipIfSyntaxHighlightMissing(); From 63a57a0ec199ce6c7ccfb761d49cb282d9f224af Mon Sep 17 00:00:00 2001 From: Jeroen De Dauw Date: Thu, 16 Jul 2026 18:37:35 +0200 Subject: [PATCH 3/4] Clone SyntaxHighlight over any cached copy in CI The MediaWiki cache turned out to keep extensions/, despite the `!mediawiki/extensions/` exclusion, so once the cache was warm the restored tree already contained SyntaxHighlight and the clone step failed on a non-empty destination. It only passed the first time round because the cache was still cold. Remove the directory before cloning, so the step is correct with a cold or a warm cache without needing a cache key bump, and drop the comment claiming the cache excludes extensions/. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee75fbd..2e00ba0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,10 +66,13 @@ jobs: - run: composer update - # Optional soft dependency: rendered separately from the MW cache because - # extensions/ is excluded from it, so this clone runs on every job. + # Optional soft dependency, not part of the checkout, so clone it on every + # job. Removed first: a restored MediaWiki cache can already hold a copy, + # which would make the clone fail on a non-empty directory. - name: Clone SyntaxHighlight - run: git clone --depth 1 -b ${{ matrix.mw }} https://github.com/wikimedia/mediawiki-extensions-SyntaxHighlight_GeSHi extensions/SyntaxHighlight_GeSHi + run: | + rm -rf extensions/SyntaxHighlight_GeSHi + git clone --depth 1 -b ${{ matrix.mw }} https://github.com/wikimedia/mediawiki-extensions-SyntaxHighlight_GeSHi extensions/SyntaxHighlight_GeSHi - name: Run update.php run: php maintenance/update.php --quick @@ -180,9 +183,12 @@ jobs: # SyntaxHighlight is only a soft dependency, but the Persistence adapter # references its class, so phpstan and psalm need its source to resolve it. + # Removed first: a restored MediaWiki cache can already hold a copy. - name: Clone SyntaxHighlight working-directory: mediawiki - run: git clone --depth 1 -b ${{ matrix.mw }} https://github.com/wikimedia/mediawiki-extensions-SyntaxHighlight_GeSHi extensions/SyntaxHighlight_GeSHi + run: | + rm -rf extensions/SyntaxHighlight_GeSHi + git clone --depth 1 -b ${{ matrix.mw }} https://github.com/wikimedia/mediawiki-extensions-SyntaxHighlight_GeSHi extensions/SyntaxHighlight_GeSHi - name: PHPStan run: php vendor/bin/phpstan analyse --error-format=checkstyle --no-progress | cs2pr From a03b281d3c47a86098961158a080da646f2f58fb Mon Sep 17 00:00:00 2001 From: Jeroen De Dauw Date: Thu, 16 Jul 2026 18:58:50 +0200 Subject: [PATCH 4/4] Adopt the style module naming from #39 Renames ext.nativeMarkdown.content to ext.nativeMarkdown.styles and moves its CSS from modules/ to resources/, matching both Karsten's independent fix in #39 and the house convention WikibaseFacetedSearch and Maps follow. Sets remoteExtPath to NativeMarkdown/resources so it agrees with localBasePath, which matters as soon as the CSS references a file. Drops the border-radius reset: it does nothing once the border is gone and the background is transparent. Co-Authored-By: Claude Fable 5 --- extension.json | 8 ++++---- .../ext.nativeMarkdown.styles.css | 1 - src/EntryPoints/MarkdownContentHandler.php | 2 +- tests/phpunit/EntryPoints/MarkdownContentHandlerTest.php | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) rename modules/ext.nativeMarkdown.content.css => resources/ext.nativeMarkdown.styles.css (96%) diff --git a/extension.json b/extension.json index 5211d72..74c03d5 100644 --- a/extension.json +++ b/extension.json @@ -27,13 +27,13 @@ }, "ResourceFileModulePaths": { - "localBasePath": "modules", - "remoteExtPath": "NativeMarkdown/modules" + "localBasePath": "resources", + "remoteExtPath": "NativeMarkdown/resources" }, "ResourceModules": { - "ext.nativeMarkdown.content": { - "styles": "ext.nativeMarkdown.content.css" + "ext.nativeMarkdown.styles": { + "styles": "ext.nativeMarkdown.styles.css" } }, diff --git a/modules/ext.nativeMarkdown.content.css b/resources/ext.nativeMarkdown.styles.css similarity index 96% rename from modules/ext.nativeMarkdown.content.css rename to resources/ext.nativeMarkdown.styles.css index 5b2cfb5..3f3d6b0 100644 --- a/modules/ext.nativeMarkdown.content.css +++ b/resources/ext.nativeMarkdown.styles.css @@ -10,6 +10,5 @@ .mw-parser-output pre > code { background-color: transparent; border: 0; - border-radius: 0; padding: 0; } diff --git a/src/EntryPoints/MarkdownContentHandler.php b/src/EntryPoints/MarkdownContentHandler.php index 69876c7..de8510b 100644 --- a/src/EntryPoints/MarkdownContentHandler.php +++ b/src/EntryPoints/MarkdownContentHandler.php @@ -27,7 +27,7 @@ final class MarkdownContentHandler extends TextContentHandler { private const FRONT_MATTER_DATA_KEY = 'nativemarkdown-front-matter'; - private const CONTENT_STYLES_MODULE = 'ext.nativeMarkdown.content'; + private const CONTENT_STYLES_MODULE = 'ext.nativeMarkdown.styles'; // Matches the threshold used by the wikitext parser ("enough" headings for a ToC). private const TOC_SECTION_THRESHOLD = 4; diff --git a/tests/phpunit/EntryPoints/MarkdownContentHandlerTest.php b/tests/phpunit/EntryPoints/MarkdownContentHandlerTest.php index 36b79e8..764ac85 100644 --- a/tests/phpunit/EntryPoints/MarkdownContentHandlerTest.php +++ b/tests/phpunit/EntryPoints/MarkdownContentHandlerTest.php @@ -222,7 +222,7 @@ public function testInterwikiFragmentOnlyLinkKeepsPrefixInLabel(): void { public function testMarkdownPageLoadsContentStyles(): void { $output = $this->getParserOutput( 'Just some prose, without a code block in sight.' ); - $this->assertContains( 'ext.nativeMarkdown.content', $output->getModuleStyles() ); + $this->assertContains( 'ext.nativeMarkdown.styles', $output->getModuleStyles() ); } public function testFencedCodeBlockWithLanguageIsSyntaxHighlighted(): void {