%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /www/varak.net/wiki.varak.net/extensions/MobileFrontend/includes/
Upload File :
Create Path :
Current File : /www/varak.net/wiki.varak.net/extensions/MobileFrontend/includes/MobileFormatter.php

<?php

use HtmlFormatter\HtmlFormatter;
use MobileFrontend\ContentProviders\IContentProvider;
use MobileFrontend\Transforms\MoveLeadParagraphTransform;
use MobileFrontend\Transforms\AddMobileTocTransform;
use MobileFrontend\Transforms\NoTransform;
use MobileFrontend\Transforms\LegacyMainPageTransform;
use MobileFrontend\Transforms\LazyImageTransform;

/**
 * Converts HTML into a mobile-friendly version
 */
class MobileFormatter extends HtmlFormatter {
	/**
	 * Class name for collapsible section wrappers
	 */
	const STYLE_COLLAPSIBLE_SECTION_CLASS = 'collapsible-block';

	/**
	 * Whether scripts can be added in the output.
	 * @var boolean $scriptsEnabled
	 */
	private $scriptsEnabled = true;

	/**
	 * The current revision id of the Title being worked on
	 * @var integer $revId
	 */
	private $revId;

	/** @var array $topHeadingTags Array of strings with possible tags,
		can be recognized as top headings. */
	public $topHeadingTags = [];

	/**
	 * @var LazyImageTransform $lazyTransform
	 */
	protected $lazyTransform;

	/**
	 * Saves a Title Object
	 * @var Title $title
	 */
	protected $title;

	/**
	 * Whether the table of contents is needed on this page
	 * @var boolean $isTOCEnabled
	 */
	protected $isTOCEnabled = false;

	/**
	 * Are sections expandable?
	 * @var boolean $expandableSections
	 */
	protected $expandableSections = false;
	/**
	 * Whether actual page is the main page and should be special cased
	 * @var boolean $mainPage
	 */
	protected $mainPage = false;

	/**
	 * Name of the transformation option
	 * @const string SHOW_FIRST_PARAGRAPH_BEFORE_INFOBOX
	 */
	const SHOW_FIRST_PARAGRAPH_BEFORE_INFOBOX = 'showFirstParagraphBeforeInfobox';

	/**
	 * @param string $html Text to process
	 * @param Title $title Title to which $html belongs
	 */
	public function __construct( $html, $title ) {
		parent::__construct( $html );

		$this->title = $title;
		$this->revId = $title->getLatestRevID();
		$config = MobileContext::singleton()->getMFConfig();
		$this->topHeadingTags = $config->get( 'MFMobileFormatterHeadings' );

		$this->lazyTransform = new LazyImageTransform(
			$config->get( 'MFLazyLoadSkipSmallImages' )
		);
	}

	/**
	 * Disables the generation of script tags in output HTML.
	 */
	public function disableScripts() {
		$this->scriptsEnabled = false;
	}
	/**
	 * Creates and returns a MobileFormatter
	 *
	 * @param MobileContext $context in which the page is being rendered. Needed to access page title
	 *  and MobileFrontend configuration.
	 * @param IContentProvider $provider
	 * @param bool $enableSections (optional)
	 *  whether to wrap the content of sections
	 *
	 * @return MobileFormatter
	 */
	public static function newFromContext( MobileContext $context,
		IContentProvider $provider,
		$enableSections = false
	) {
		$mfSpecialCaseMainPage = $context->getMFConfig()->get( 'MFSpecialCaseMainPage' );

		$title = $context->getTitle();
		$isMainPage = $title->isMainPage();
		$isFilePage = $title->inNamespace( NS_FILE );
		$html = self::wrapHTML( $provider->getHTML() );
		$formatter = new MobileFormatter( $html, $title );
		if ( $isMainPage ) {
			$formatter->enableExpandableSections( !$mfSpecialCaseMainPage );
		} else {
			$formatter->enableExpandableSections( $enableSections );
		}

		$formatter->setIsMainPage( $isMainPage && $mfSpecialCaseMainPage );
		$formatter->enableTOCPlaceholder( strpos( $html, 'toclevel' ) !== false );

		return $formatter;
	}

	/**
	 * Mark whether a placeholder table of contents should be included at the end of the lead
	 * section
	 * @param bool $flag should TOC be included?
	 */
	public function enableTOCPlaceholder( $flag = true ) {
		$this->isTOCEnabled = $flag;
	}

	/**
	 * Set support of page for expandable sections to $flag (standard: true)
	 * @todo kill with fire when there will be minimum of pre-1.1 app users remaining
	 * @param bool $flag should expandable sections be supported?
	 */
	public function enableExpandableSections( $flag = true ) {
		$this->expandableSections = $flag;
	}

	/**
	 * Change mainPage (is this the main page) to $value (standard: true)
	 * This enables special casing for the main page.
	 * @deprecated
	 * @param bool $value
	 */
	public function setIsMainPage( $value = true ) {
		$this->mainPage = $value;
	}

	/**
	 * Performs various transformations to the content to make it appropiate for mobile devices.
	 * @param bool $removeDefaults Whether default settings at $wgMFRemovableClasses should be used
	 * @param bool $removeReferences Whether to remove references from the output
	 * @param bool $removeImages Whether to move images into noscript tags
	 * @param bool $showFirstParagraphBeforeInfobox Whether the first paragraph from the lead
	 *  section should be shown before all infoboxes that come earlier.
	 * @return array
	 */
	public function filterContent(
		$removeDefaults = true, $removeReferences = false, $removeImages = false,
		$showFirstParagraphBeforeInfobox = false
	) {
		$ctx = MobileContext::singleton();
		$config = $ctx->getMFConfig();
		$doc = $this->getDoc();

		$isSpecialPage = $this->title->isSpecialPage();
		$mfRemovableClasses = $config->get( 'MFRemovableClasses' );
		$removableClasses = $mfRemovableClasses['base'];
		if ( $ctx->isBetaGroupMember() ) {
			$removableClasses = array_merge( $removableClasses, $mfRemovableClasses['beta'] );
		}

		// Don't remove elements in special pages
		if ( !$isSpecialPage && $removeDefaults ) {
			$this->remove( $removableClasses );
		}

		if ( $this->removeMedia ) {
			$this->doRemoveImages();
		}

		// Apply all removals before continuing with transforms (see T185040 for example)
		$removed = parent::filterContent();
		$transformOptions = [
			'images' => $removeImages,
			'references' => $removeReferences,
			self::SHOW_FIRST_PARAGRAPH_BEFORE_INFOBOX => $showFirstParagraphBeforeInfobox
		];
		// Sectionify the content and transform it if necessary per section
		if ( !$this->mainPage && $this->expandableSections ) {
			list( $headings, $subheadings ) = $this->getHeadings( $doc );
			$this->makeHeadingsEditable( $subheadings );
			$this->makeSections( $doc, $headings, $transformOptions );
		} else {
			// Otherwise apply the per-section transformations to the document as a whole
			$this->filterContentInSection( $doc, $doc, 0, $transformOptions );
		}
		if ( $transformOptions['references'] ) {
			$this->doRewriteReferencesLinksForLazyLoading( $doc );
		}
		return $removed;
	}

	/**
	 * Apply filtering per element (section) in a document.
	 * @param DOMElement|DOMDocument $el
	 * @param DOMDocument $doc
	 * @param int $sectionNumber Which section is it on the document
	 * @param array $options options about the transformations per section
	 */
	private function filterContentInSection(
		$el, DOMDocument $doc, $sectionNumber, $options = []
	) {
		if ( !$this->removeMedia && $options['images'] && $sectionNumber > 0 ) {
			$this->lazyTransform->apply( $el );
		}
		if ( $options['references'] ) {
			$this->doRewriteReferencesListsForLazyLoading( $el, $doc );
		}
	}

	/**
	 * Replaces any references links with a link to Special:MobileCite
	 *
	 * @param DOMDocument $doc Document to create and replace elements in
	 */
	private function doRewriteReferencesLinksForLazyLoading( DOMDocument $doc ) {
		$citePath = "$this->revId";

		$xPath = new DOMXPath( $doc );
		$nodes = $xPath->query(
			// sup.reference > a
			'//sup[contains(concat(" ", normalize-space(./@class), " "), " reference ")]/a[1]' );

		foreach ( $nodes as $node ) {
			$fragment = $node->getAttribute( 'href' );
			$node->setAttribute(
				'href',
				SpecialPage::getTitleFor( 'MobileCite', $citePath )->getLocalUrl() . $fragment
			);
		}
	}

	/**
	 * Replaces any references list with a link to Special:MobileCite
	 *
	 * @param DOMElement|DOMDocument $el Element or document to rewrite references in.
	 * @param DOMDocument $doc Document to create elements in
	 */
	private function doRewriteReferencesListsForLazyLoading( $el, DOMDocument $doc ) {
		$citePath = "$this->revId";
		$isReferenceSection = false;

		// Accessing by tag is cheaper than class
		$nodes = $el->getElementsByTagName( 'ol' );
		// PHP's DOM classes are recursive
		// but since we are manipulating the DOMList we have to
		// traverse it backwards
		// see http://php.net/manual/en/class.domnodelist.php
		for ( $i = $nodes->length - 1; $i >= 0; $i-- ) {
			$list = $nodes->item( $i );

			// Use class to decide it is a list of references
			if ( strpos( $list->getAttribute( 'class' ), 'references' ) !== false ) {
				// Only mark the section as a reference container if we're transforming a section, not the
				// document.
				$isReferenceSection = $el instanceof DOMElement;

				$parent = $list->parentNode;
				$placeholder = $doc->createElement( 'a',
					wfMessage( 'mobile-frontend-references-list' ) );
				$placeholder->setAttribute( 'class', 'mf-lazy-references-placeholder' );
				// Note to render a reference we need to know only its reference
				// Note: You can have multiple <references> tag on the same page, we render all of these in
				// the special page together.
				$placeholder->setAttribute( 'href',
					SpecialPage::getTitleFor( 'MobileCite', $citePath )->getLocalUrl() );
				$parent->replaceChild( $placeholder, $list );
			}
		}

		// Mark section as having references
		if ( $isReferenceSection ) {
			$el->setAttribute( 'data-is-reference-section', '1' );
		}
	}

	/**
	 * Replaces images with [annotations from alt]
	 */
	private function doRemoveImages() {
		$doc = $this->getDoc();
		$domElemsToReplace = [];
		foreach ( $doc->getElementsByTagName( 'img' ) as $element ) {
			$domElemsToReplace[] = $element;
		}
		/** @var $element DOMElement */
		foreach ( $domElemsToReplace as $element ) {
			$alt = $element->getAttribute( 'alt' );
			if ( $alt === '' ) {
				$alt = '[' . wfMessage( 'mobile-frontend-missing-image' )->inContentLanguage()->text() . ']';
			} else {
				$alt = '[' . $alt . ']';
			}
			$replacement = $doc->createElement( 'span', htmlspecialchars( $alt ) );
			$replacement->setAttribute( 'class', 'mw-mf-image-replacement' );
			$element->parentNode->replaceChild( $replacement, $element );
		}
	}

	/**
	 * Performs final transformations to mobile format and returns resulting HTML
	 *
	 * @param DOMElement|string|null $element ID of element to get HTML from or
	 *   false to get it from the whole tree
	 * @return string Processed HTML
	 */
	public function getText( $element = null ) {
		if ( $this->mainPage ) {
			$transform = new LegacyMainPageTransform();
			$doc = $this->getDoc();
			$transform->apply( $doc->getElementsByTagName( 'body' )->item( 0 ) );
		}

		return parent::getText( $element );
	}

	/**
	 * Splits the body of the document into sections demarcated by the $headings elements.
	 * Also moves the first paragraph in the lead section above the infobox.
	 *
	 * All member elements of the sections are added to a <code><div></code> so
	 * that the section bodies are clearly defined (to be "expandable" for
	 * example).
	 *
	 * @param DOMDocument $doc representing the HTML of the current article. In the HTML the sections
	 *  should not be wrapped.
	 * @param DOMElement[] $headings The headings returned by
	 *  {@see MobileFormatter::getHeadings}
	 * @param array $transformOptions Options to pass when transforming content per section
	 */
	protected function makeSections( DOMDocument $doc, array $headings, array $transformOptions ) {
		$noTransform = new NoTransform();
		$tocTransform = $this->isTOCEnabled ? new AddMobileTocTransform() : $noTransform;
		$leadTransform = $transformOptions[ self::SHOW_FIRST_PARAGRAPH_BEFORE_INFOBOX ] ?
				new MoveLeadParagraphTransform( $this->title, $this->revId ) : $noTransform;

		// Find the parser output wrapper div
		$xpath = new DOMXPath( $doc );
		$containers = $xpath->query( 'body/div[@class="mw-parser-output"][1]' );
		if ( !$containers->length ) {
			// No wrapper? This could be an old parser cache entry, or perhaps the
			// OutputPage contained something that was not generated by the parser.
			// Try using the <body> as the container.
			$containers = $xpath->query( 'body' );
			if ( !$containers->length ) {
				throw new Exception( "HTML lacked body element even though we put it there ourselves" );
			}
		}

		$container = $containers->item( 0 );
		$containerChild = $container->firstChild;
		$firstHeading = reset( $headings );
		$firstHeadingName = $firstHeading ? $firstHeading->nodeName : false;
		$sectionNumber = 0;
		$sectionBody = $this->createSectionBodyElement( $doc, $sectionNumber, false );

		while ( $containerChild ) {
			$node = $containerChild;
			$containerChild = $containerChild->nextSibling;

			// If we've found a top level heading, insert the previous section if
			// necessary and clear the container div.
			// Note well the use of DOMNode#nodeName here. Only DOMElement defines
			// DOMElement#tagName.  So, if there's trailing text - represented by
			// DOMText - then accessing #tagName will trigger an error.
			if ( $node->nodeName === $firstHeadingName ) {
				// The heading we are transforming is always 1 section ahead of the
				// section we are currently processing
				$this->prepareHeading( $doc, $node, $sectionNumber + 1, $this->scriptsEnabled );
				if ( $sectionBody->hasChildNodes() ) {
					// Apply transformations to the section body
					$this->filterContentInSection( $sectionBody, $doc, $sectionNumber, $transformOptions );
				}
				// Insert the previous section body and reset it for the new section
				$container->insertBefore( $sectionBody, $node );

				if ( $sectionNumber === 0 ) {
					$tocTransform->apply( $sectionBody );
					$leadTransform->apply( $sectionBody );
				}
				$sectionNumber += 1;
				$sectionBody = $this->createSectionBodyElement( $doc, $sectionNumber, $this->scriptsEnabled );
				continue;
			}

			// If it is not a top level heading, keep appending the nodes to the
			// section body container.
			$sectionBody->appendChild( $node );
		}

		// If the document had the lead section only:
		if ( $sectionNumber == 0 ) {
			$leadTransform->apply( $sectionBody );
		}

		if ( $sectionBody->hasChildNodes() ) {
			// Apply transformations to the last section body
			$this->filterContentInSection( $sectionBody, $doc, $sectionNumber, $transformOptions );
		}
		// Append the last section body.
		$container->appendChild( $sectionBody );
	}

	/**
	 * Prepare section headings, add required classes and onclick actions
	 *
	 * @param DOMDocument $doc
	 * @param DOMElement $heading
	 * @param integer $sectionNumber
	 * @param bool $isCollapsible
	 */
	private function prepareHeading( DOMDocument $doc, $heading, $sectionNumber, $isCollapsible ) {
		$className = $heading->hasAttribute( 'class' ) ? $heading->getAttribute( 'class' ) . ' ' : '';
		$heading->setAttribute( 'class', $className . 'section-heading' );
		if ( $isCollapsible ) {
			$heading->setAttribute( 'onclick', 'javascript:mfTempOpenSection(' . $sectionNumber . ')' );
		}

		// prepend indicator - this avoids a reflow by creating a placeholder for a toggling indicator
		$indicator = $doc->createElement( 'div' );
		$indicator->setAttribute( 'class', MobileUI::iconClass( '', 'element', 'indicator' ) );
		$heading->insertBefore( $indicator, $heading->firstChild );
	}

	/**
	 * Creates a Section body element
	 *
	 * @param DOMDocument $doc
	 * @param int $sectionNumber
	 * @param bool $isCollapsible
	 *
	 * @return DOMElement
	 */
	private function createSectionBodyElement( DOMDocument $doc, $sectionNumber, $isCollapsible ) {
		$sectionClass = 'mf-section-' . $sectionNumber;
		if ( $isCollapsible ) {
			// TODO: Probably good to rename this to the more generic 'section'.
			// We have no idea how the skin will use this.
			$sectionClass .= ' ' . self::STYLE_COLLAPSIBLE_SECTION_CLASS;
		}

		// FIXME: The class `/mf\-section\-[0-9]+/` is kept for caching reasons
		// but given class is unique usage is discouraged. [T126825]
		$sectionBody = $doc->createElement( 'div' );
		$sectionBody->setAttribute( 'class', $sectionClass );
		$sectionBody->setAttribute( 'id', 'mf-section-' . $sectionNumber );
		return $sectionBody;
	}

	/**
	 * Marks the headings as editable by adding the <code>in-block</code>
	 * class to each of them, if it hasn't already been added.
	 *
	 * FIXME: <code>in-block</code> isn't semantic in that it isn't
	 * obviously connected to being editable.
	 *
	 * @param DOMElement[] $headings Heading elements
	 */
	protected function makeHeadingsEditable( array $headings ) {
		foreach ( $headings as $heading ) {
			$class = $heading->getAttribute( 'class' );
			if ( strpos( $class, 'in-block' ) === false ) {
				$heading->setAttribute(
					'class',
					ltrim( $class . ' in-block' )
				);
			}
		}
	}

	/**
	 * Gets all headings in the document in rank order.
	 *
	 * Note well that the rank order is defined by the
	 * <code>MobileFormatter#topHeadingTags</code> property.
	 *
	 * @param DOMDocument $doc
	 * @return array A two-element array where the first is the highest
	 *  rank headings and the second is all other headings
	 */
	private function getHeadings( DOMDocument $doc ) {
		$headings = $subheadings = [];

		foreach ( $this->topHeadingTags as $tagName ) {
			$allTags = $doc->getElementsByTagName( $tagName );
			$elements = [];
			foreach ( $allTags as $el ) {
				if ( $el->parentNode->getAttribute( 'class' ) !== 'toctitle' ) {
					$elements[] = $el;
				}
			}

			if ( !$headings ) {
				$headings = $elements;
			} else {
				$subheadings = array_merge( $subheadings, $elements );
			}
		}

		return [ $headings, $subheadings ];
	}
}

Zerion Mini Shell 1.0