1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
|
<?php
// Suppress UnusedPluginSuppression because Phan on PHP 8.1 needs more
// suppressions than PHP 7.x due to tighter types on Element::insertBefore()
// and Element::appendChild(): see comments marked PHP81 below. The
// Unused*Suppression can be removed once MW moves to >= PHP 8.1.
// @phan-file-suppress UnusedPluginSuppression,UnusedPluginFileSuppression
namespace MediaWiki\OutputTransform\Stages;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Context\RequestContext;
use MediaWiki\OutputTransform\ContentDOMTransformStage;
use MediaWiki\Parser\ParserOptions;
use MediaWiki\Parser\ParserOutput;
use MediaWiki\Parser\ParserOutputFlags;
use MediaWiki\Title\TitleFactory;
use Psr\Log\LoggerInterface;
use Skin;
use Wikimedia\Parsoid\DOM\Document;
use Wikimedia\Parsoid\Utils\DOMCompat;
/**
* Add anchors and other heading formatting, and replace the section link placeholders.
* @internal
*/
class HandleParsoidSectionLinks extends ContentDOMTransformStage {
private TitleFactory $titleFactory;
public function __construct(
ServiceOptions $options, LoggerInterface $logger, TitleFactory $titleFactory
) {
parent::__construct( $options, $logger );
$this->titleFactory = $titleFactory;
}
public function shouldRun( ParserOutput $po, ?ParserOptions $popts, array $options = [] ): bool {
// Only run this stage if it is parsoid content
return ( $options['isParsoidContent'] ?? false );
}
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 (assuming section edit links are enabled)
// adding a <span> with the section edit link
// inside that <div>
//
// If COLLAPSIBLE_SECTIONS is set, then we also wrap a <div>
// around the section *contents*.
$toc = $po->getTOCData();
$sections = ( $toc !== null ) ? $toc->getSections() : [];
// use the TOC data to extract the headings:
foreach ( $sections as $section ) {
$fromTitle = $section->fromTitle;
if ( $fromTitle === null ) {
// T353489: don't wrap bare <h> tags
continue;
}
$h = $dom->getElementById( $section->anchor );
if ( $h === null ) {
$this->logger->error(
__METHOD__ . ': Heading missing for anchor',
$section->toLegacy()
);
continue;
}
$div = $dom->createElement( 'div' );
if ( ( $options['enableSectionEditLinks'] ?? true ) &&
!$po->getOutputFlag( ParserOutputFlags::NO_SECTION_EDIT_LINKS ) ) {
$editPage = $this->titleFactory->newFromTextThrow( $fromTitle );
$html = $skin->doEditSectionLink(
$editPage, $section->index, $h->textContent,
$skin->getLanguage()
);
DOMCompat::setInnerHTML( $div, $html );
}
// Reuse existing wrapper if present.
$maybeWrapper = $h->parentNode;
'@phan-var \Wikimedia\Parsoid\DOM\Element $maybeWrapper';
if (
DOMCompat::nodeName( $maybeWrapper ) === 'div' &&
DOMCompat::getClassList( $maybeWrapper )->contains( 'mw-heading' )
) {
// Transfer section edit link children to existing wrapper
// All contents of the div (the section edit link) will be
// inserted immediately following the <h> tag
$ref = $h->nextSibling;
while ( $div->firstChild !== null ) {
// @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal firstChild is non-null (PHP81)
$maybeWrapper->insertBefore( $div->firstChild, $ref );
}
$div = $maybeWrapper; // for use below
} else {
// Move <hX> to new wrapper: the div contents are currently
// the section edit link. We first replace the h with the
// div, then insert the <h> as the first child of the div
// so the section edit link is immediately following the <h>.
$div->setAttribute(
'class', 'mw-heading mw-heading' . $section->hLevel
);
$h->parentNode->replaceChild( $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 (PHP81)
$div->insertBefore( $h, $divFirstChild );
}
// Create collapsible section wrapper if requested.
if ( $po->getOutputFlag( ParserOutputFlags::COLLAPSIBLE_SECTIONS ) ) {
$contentsDiv = $dom->createElement( 'div' );
while ( $div->nextSibling !== null ) {
// @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal
$contentsDiv->appendChild( $div->nextSibling );
}
$div->parentNode->appendChild( $contentsDiv );
}
}
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;
}
}
|