aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--autoload.php1
-rw-r--r--includes/OutputTransform/DefaultOutputPipelineFactory.php2
-rw-r--r--includes/OutputTransform/Stages/HandleParsoidSectionLinks.php111
-rw-r--r--tests/phpunit/includes/api/ApiParseTest.php7
-rw-r--r--tests/phpunit/includes/content/WikitextContentHandlerIntegrationTest.php31
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&amp;action=edit&amp;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',