diff options
-rw-r--r-- | autoload.php | 1 | ||||
-rw-r--r-- | includes/OutputTransform/DefaultOutputPipelineFactory.php | 2 | ||||
-rw-r--r-- | includes/OutputTransform/Stages/HandleParsoidSectionLinks.php | 111 | ||||
-rw-r--r-- | tests/phpunit/includes/api/ApiParseTest.php | 7 | ||||
-rw-r--r-- | tests/phpunit/includes/content/WikitextContentHandlerIntegrationTest.php | 31 |
5 files changed, 149 insertions, 3 deletions
diff --git a/autoload.php b/autoload.php index 4002ef4e0921..7ebcacf8ee2e 100644 --- a/autoload.php +++ b/autoload.php @@ -1668,6 +1668,7 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\OutputTransform\\Stages\\ExecutePostCacheTransformHooks' => __DIR__ . '/includes/OutputTransform/Stages/ExecutePostCacheTransformHooks.php', 'MediaWiki\\OutputTransform\\Stages\\ExpandToAbsoluteUrls' => __DIR__ . '/includes/OutputTransform/Stages/ExpandToAbsoluteUrls.php', 'MediaWiki\\OutputTransform\\Stages\\ExtractBody' => __DIR__ . '/includes/OutputTransform/Stages/ExtractBody.php', + 'MediaWiki\\OutputTransform\\Stages\\HandleParsoidSectionLinks' => __DIR__ . '/includes/OutputTransform/Stages/HandleParsoidSectionLinks.php', 'MediaWiki\\OutputTransform\\Stages\\HandleSectionLinks' => __DIR__ . '/includes/OutputTransform/Stages/HandleSectionLinks.php', 'MediaWiki\\OutputTransform\\Stages\\HandleTOCMarkers' => __DIR__ . '/includes/OutputTransform/Stages/HandleTOCMarkers.php', 'MediaWiki\\OutputTransform\\Stages\\HydrateHeaderPlaceholders' => __DIR__ . '/includes/OutputTransform/Stages/HydrateHeaderPlaceholders.php', diff --git a/includes/OutputTransform/DefaultOutputPipelineFactory.php b/includes/OutputTransform/DefaultOutputPipelineFactory.php index 582bc62ee3a7..55a7438d22a6 100644 --- a/includes/OutputTransform/DefaultOutputPipelineFactory.php +++ b/includes/OutputTransform/DefaultOutputPipelineFactory.php @@ -11,6 +11,7 @@ use MediaWiki\OutputTransform\Stages\DeduplicateStyles; use MediaWiki\OutputTransform\Stages\ExecutePostCacheTransformHooks; use MediaWiki\OutputTransform\Stages\ExpandToAbsoluteUrls; use MediaWiki\OutputTransform\Stages\ExtractBody; +use MediaWiki\OutputTransform\Stages\HandleParsoidSectionLinks; use MediaWiki\OutputTransform\Stages\HandleSectionLinks; use MediaWiki\OutputTransform\Stages\HandleTOCMarkers; use MediaWiki\OutputTransform\Stages\HydrateHeaderPlaceholders; @@ -63,6 +64,7 @@ class DefaultOutputPipelineFactory { ->addStage( new ExecutePostCacheTransformHooks( $this->hookContainer ) ) ->addStage( new AddWrapperDivClass( $this->langFactory, $this->contentLang ) ) ->addStage( new HandleSectionLinks( $this->logger, $this->titleFactory ) ) + ->addStage( new HandleParsoidSectionLinks( $this->logger, $this->titleFactory ) ) ->addStage( new HandleTOCMarkers( $this->tidy ) ) ->addStage( new DeduplicateStyles() ) ->addStage( new ExpandToAbsoluteUrls() ) diff --git a/includes/OutputTransform/Stages/HandleParsoidSectionLinks.php b/includes/OutputTransform/Stages/HandleParsoidSectionLinks.php new file mode 100644 index 000000000000..c4a3736e77b3 --- /dev/null +++ b/includes/OutputTransform/Stages/HandleParsoidSectionLinks.php @@ -0,0 +1,111 @@ +<?php + +namespace MediaWiki\OutputTransform\Stages; + +use MediaWiki\Context\RequestContext; +use MediaWiki\OutputTransform\ContentDOMTransformStage; +use MediaWiki\Parser\ParserOutput; +use MediaWiki\Parser\ParserOutputFlags; +use MediaWiki\Title\TitleFactory; +use ParserOptions; +use Psr\Log\LoggerInterface; +use Skin; +use Wikimedia\Parsoid\DOM\Document; +use Wikimedia\Parsoid\DOM\Element; +use Wikimedia\Parsoid\Utils\DOMCompat; + +/** + * Add anchors and other heading formatting, and replace the section link placeholders. + * @internal + */ +class HandleParsoidSectionLinks extends ContentDOMTransformStage { + + private LoggerInterface $logger; + private TitleFactory $titleFactory; + + public function __construct( LoggerInterface $logger, TitleFactory $titleFactory ) { + $this->logger = $logger; + $this->titleFactory = $titleFactory; + } + + public function shouldRun( ParserOutput $po, ?ParserOptions $popts, array $options = [] ): bool { + // Only run this stage if it is parsoid content *and* section edit + // links are enabled. + return ( + ( $options['isParsoidContent'] ?? false ) && + ( $options['enableSectionEditLinks'] ?? true ) && + !$po->getOutputFlag( ParserOutputFlags::NO_SECTION_EDIT_LINKS ) + ); + } + + public function transformDOM( + Document $dom, ParserOutput $po, ?ParserOptions $popts, array &$options + ): Document { + $skin = $this->resolveSkin( $options ); + $titleText = $po->getTitleText(); + // Transform: + // <section data-mw-section-id=...> + // <h2 id=...><span id=... typeof="mw:FallbackId"></span> ... </h2> + // ...section contents.. + // To: + // <section data-mw-section-id=...> + // <div class="mw-heading mw-heading2"> + // <h2 id=...><span id=... typeof="mw:FallbackId"></span> ... </h2> + // <span class="mw-editsection">...section edit link...</span> + // </div> + // That is, we're wrapping a <div> around the <h2> generated by + // Parsoid, and then adding a <span> with the section edit link + // inside that <div> + $toc = $po->getTOCData(); + $sections = ( $toc !== null ) ? $toc->getSections() : []; + // use the TOC data to extract the headings: + foreach ( $sections as $section ) { + $h = $dom->getElementById( $section->anchor ); + if ( $h === null ) { + $this->logger->error( + __METHOD__ . ': Heading missing for anchor', + $section->toLegacy() + ); + continue; + } + $div = $dom->createElement( 'div' ); + '@phan-var Element $div'; // assert Element + $div->setAttribute( + 'class', 'mw-heading mw-heading' . $section->hLevel + ); + $fromTitle = $section->fromTitle; + if ( $fromTitle !== null ) { + $editPage = $this->titleFactory->newFromTextThrow( $fromTitle ); + $html = $skin->doEditSectionLink( + $editPage, $section->index, $h->textContent, + $skin->getLanguage() + ); + DOMCompat::setInnerHTML( $div, $html ); + } + $h->parentNode->insertBefore( $div, $h ); + // Work around bug in phan (https://github.com/phan/phan/pull/4837) + // by asserting that $div->firstChild is non-null here. Actually, + // ::insertBefore will work fine if $div->firstChild is null (if + // "doEditSectionLink" returned nothing, for instance), but + // phan incorrectly thinks the second argument must be non-null. + $divFirstChild = $div->firstChild; + '@phan-var \DOMNode $divFirstChild'; // asserting non-null + $div->insertBefore( $h, $divFirstChild ); + } + return $dom; + } + + /** + * Extracts the skin from the $options array, with a fallback on request context skin + * @param array $options + * @return Skin + */ + private function resolveSkin( array $options ): Skin { + $skin = $options[ 'skin' ] ?? null; + if ( !$skin ) { + // T348853 passing $skin will be mandatory in the future + $skin = RequestContext::getMain()->getSkin(); + } + return $skin; + } +} diff --git a/tests/phpunit/includes/api/ApiParseTest.php b/tests/phpunit/includes/api/ApiParseTest.php index 22c416283122..2c8bcb4ee7ad 100644 --- a/tests/phpunit/includes/api/ApiParseTest.php +++ b/tests/phpunit/includes/api/ApiParseTest.php @@ -95,11 +95,16 @@ class ApiParseTest extends ApiTestCase { private function doAssertParsedTo( $expected, array $res, $warnings, callable $callback ) { $html = $res[0]['parse']['text']; - $expectedStart = '<div class="mw-content-ltr mw-parser-output" lang="en" dir="ltr">'; + $expectedStart = '<div class="mw-content-ltr mw-parser-output" lang="en" dir="ltr"'; $this->assertSame( $expectedStart, substr( $html, 0, strlen( $expectedStart ) ) ); $html = substr( $html, strlen( $expectedStart ) ); + # Parsoid-based transformations may add an ID attribute to the + # wrapper div + $possibleIdAttr = '/^( id="[^"]+")?>/'; + $html = preg_replace( $possibleIdAttr, '', $html ); + $possibleParserCache = '/\n<!-- Saved in (?>parser cache|RevisionOutputCache) (?>.*?\n -->)\n/'; $html = preg_replace( $possibleParserCache, '', $html ); diff --git a/tests/phpunit/includes/content/WikitextContentHandlerIntegrationTest.php b/tests/phpunit/includes/content/WikitextContentHandlerIntegrationTest.php index 40b8f49363f8..7f1f948bdd8a 100644 --- a/tests/phpunit/includes/content/WikitextContentHandlerIntegrationTest.php +++ b/tests/phpunit/includes/content/WikitextContentHandlerIntegrationTest.php @@ -72,7 +72,7 @@ class WikitextContentHandlerIntegrationTest extends TextContentHandlerIntegratio 'useParsoid', 'maxIncludeSize', 'interfaceMessage', 'disableContentConversion', 'suppressSectionEditLinks', 'isPreview', 'wrapclass' ], ], - 'options' => [ 'useParsoid' => true ] + 'options' => [ 'useParsoid' => true, 'suppressSectionEditLinks' => true ], ]; yield 'Parsoid render (redirect page)' => [ 'title' => 'WikitextContentTest_testGetParserOutput', @@ -89,7 +89,34 @@ class WikitextContentHandlerIntegrationTest extends TextContentHandlerIntegratio 'useParsoid', 'maxIncludeSize', 'interfaceMessage', 'disableContentConversion', 'suppressSectionEditLinks', 'isPreview', 'wrapclass' ], ], - 'options' => [ 'useParsoid' => true ] + 'options' => [ 'useParsoid' => true, 'suppressSectionEditLinks' => true ], + ]; + yield 'Parsoid render (section edit links)' => [ + 'title' => 'WikitextContentTest_testGetParserOutput', + 'model' => CONTENT_MODEL_WIKITEXT, + 'text' => "== Hello ==", + 'expectedHtml' => '<div class="mw-content-ltr mw-parser-output" lang="en" dir="ltr" id="mwAw"><section data-mw-section-id="0" id="mwAQ"></section><section data-mw-section-id="1" id="mwAg"><div class="mw-heading mw-heading2" id="mwBA"><h2 id="Hello">Hello</h2><span class="mw-editsection" id="mwBQ"><span class="mw-editsection-bracket" id="mwBg">[</span><a href="/w/index.php?title=WikitextContentTest_testGetParserOutput&action=edit&section=1" title="Edit section: Hello" id="mwBw"><span id="mwCA">edit</span></a><span class="mw-editsection-bracket" id="mwCQ">]</span></span></div></section></div>', + 'expectedFields' => [ + 'Links' => [ + ], + 'Sections' => [ + [ + 'toclevel' => 1, + 'level' => '2', + 'line' => 'Hello', + 'number' => '1', + 'index' => '1', + 'fromtitle' => 'WikitextContentTest_testGetParserOutput', + 'byteoffset' => 0, + 'anchor' => 'Hello', + 'linkAnchor' => 'Hello', + ], + ], + 'UsedOptions' => [ + 'useParsoid', 'maxIncludeSize', 'interfaceMessage', 'disableContentConversion', 'suppressSectionEditLinks', 'isPreview', 'wrapclass' + ], + ], + 'options' => [ 'useParsoid' => true ], ]; yield 'Links' => [ 'title' => 'WikitextContentTest_testGetParserOutput', |