File: HandleParsoidSectionLinks.php

package info (click to toggle)
mediawiki 1%3A1.43.3%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 417,464 kB
  • sloc: php: 1,062,949; javascript: 664,290; sql: 9,714; python: 5,458; xml: 3,489; sh: 1,131; makefile: 64
file content (152 lines) | stat: -rw-r--r-- 5,706 bytes parent folder | download
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;
	}
}