. * * @file * @ingroup Skins */ declare( strict_types=1 ); namespace MediaWiki\Skins\Citizen\Partials; use DOMDocument; use DOMElement; use DOMNode; use DOMXpath; use HtmlFormatter\HtmlFormatter; use MediaWiki\MediaWikiServices; use MediaWiki\Title\Title; use Wikimedia\Parsoid\Utils\DOMCompat; use Wikimedia\Services\NoSuchServiceException; final class BodyContent extends Partial { /** * The code below is largely based on the extension MobileFrontend * All credits go to the author and contributors of the project */ /** * Class name for collapsible section wrappers */ public const STYLE_COLLAPSIBLE_SECTION_CLASS = 'section-collapsible'; /** * List of tags that could be considered as section headers. * @var array */ private $topHeadingTags = [ "h1", "h2", "h3", "h4", "h5", "h6" ]; /** * Helper function to decide if the page should be formatted * * @param Title $title * @return bool */ private function shouldFormatPage( $title ) { try { $mfCxt = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' ); // Check if page is in mobile view and let MF do the formatting return !$mfCxt->shouldDisplayMobileView(); } catch ( NoSuchServiceException $ex ) { // MobileFrontend not installed. Don't do anything } return $this->getConfigValue( 'CitizenEnableCollapsibleSections' ) === true && !$title->isMainPage() && $title->isContentPage(); } /** * Rebuild the body content * * @param string $bodyContent HTML of the body content from core * @return string html */ public function decorateBodyContent( $bodyContent ) { $title = $this->title; // Return the page if title is null if ( $title === null ) { return $bodyContent; } // Make section and sanitize the output if ( $this->shouldFormatPage( $title ) ) { $formatter = new HtmlFormatter( $bodyContent ); $doc = $formatter->getDoc(); // Make top level sections $this->makeSections( $doc, $this->getTopHeadings( $doc ) ); $formatter->filterContent(); $bodyContent = $formatter->getText(); } return $bodyContent; } /** * @param DOMNode|null $node * @return string|false Heading tag name if the node is a heading */ private function getHeadingName( $node ) { if ( !( $node instanceof DOMElement ) ) { return false; } // We accept both kinds of nodes that can be returned by getTopHeadings(): // a `
section-subheading
class to each of them, if it
* hasn't already been added.
*
* @param DOMElement[] $headings Heading elements
*/
protected function markSubHeadings( array $headings ) {
foreach ( $headings as $heading ) {
$class = $heading->getAttribute( 'class' );
if ( strpos( $class, 'section-subheading' ) === false ) {
$heading->setAttribute(
'class',
ltrim( $class . ' section-subheading' )
);
}
}
}
/**
* Gets all subheadings in the document in rank order.
*
* @param DOMDocument $doc
* @return DOMElement[]
*/
private function getSubHeadings( DOMDocument $doc ): array {
$found = false;
$subheadings = [];
foreach ( $this->topHeadingTags as $tagName ) {
$allTags = DOMCompat::querySelectorAll( $doc, $tagName );
$elements = [];
foreach ( $allTags as $el ) {
if ( $el->parentNode->getAttribute( 'class' ) !== 'toctitle' ) {
$elements[] = $el;
}
}
if ( $elements ) {
if ( !$found ) {
$found = true;
} else {
$subheadings = array_merge( $subheadings, $elements );
}
}
}
return $subheadings;
}
}