Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
0.00% |
0 / 7 |
CRAP | |
15.83% |
22 / 139 |
MetaHandler | |
0.00% |
0 / 1 |
|
0.00% |
0 / 7 |
3706.29 | |
15.83% |
22 / 139 |
__construct | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
handle | |
0.00% |
0 / 1 |
300.16 | |
31.43% |
22 / 70 |
|||
needToWriteStartMeta | |
0.00% |
0 / 1 |
90 | |
0.00% |
0 / 15 |
|||
needToWriteEndMeta | |
0.00% |
0 / 1 |
90 | |
0.00% |
0 / 14 |
|||
needNewLineSepBeforeMeta | |
0.00% |
0 / 1 |
30 | |
0.00% |
0 / 5 |
|||
before | |
0.00% |
0 / 1 |
210 | |
0.00% |
0 / 19 |
|||
after | |
0.00% |
0 / 1 |
132 | |
0.00% |
0 / 14 |
<?php | |
declare( strict_types = 1 ); | |
namespace Wikimedia\Parsoid\Html2Wt\DOMHandlers; | |
use Wikimedia\Parsoid\DOM\Element; | |
use Wikimedia\Parsoid\DOM\Node; | |
use Wikimedia\Parsoid\DOM\Text; | |
use Wikimedia\Parsoid\Html2Wt\SerializerState; | |
use Wikimedia\Parsoid\Html2Wt\WTSUtils; | |
use Wikimedia\Parsoid\Utils\DOMCompat; | |
use Wikimedia\Parsoid\Utils\DOMDataUtils; | |
use Wikimedia\Parsoid\Utils\DOMUtils; | |
use Wikimedia\Parsoid\Utils\Utils; | |
use Wikimedia\Parsoid\Utils\WTUtils; | |
class MetaHandler extends DOMHandler { | |
public function __construct() { | |
parent::__construct( false ); | |
} | |
/** @inheritDoc */ | |
public function handle( | |
Element $node, SerializerState $state, bool $wrapperUnmodified = false | |
): ?Node { | |
$property = $node->getAttribute( 'property' ) ?? ''; | |
$dp = DOMDataUtils::getDataParsoid( $node ); | |
$dmw = DOMDataUtils::getDataMw( $node ); | |
if ( isset( $dp->src ) && | |
DOMUtils::matchTypeOf( $node, '#^mw:Placeholder(/|$)#' ) | |
) { | |
$this->emitPlaceholderSrc( $node, $state ); | |
return $node->nextSibling; | |
} | |
// Check for property before type so that page properties with | |
// templated attrs roundtrip properly. | |
// Ex: {{DEFAULTSORT:{{1x|foo}} }} | |
if ( $property ) { | |
preg_match( '#^mw\:PageProp/(.*)$#D', $property, $switchType ); | |
if ( $switchType ) { | |
$out = $switchType[1]; | |
$cat = preg_match( '/^(?:category)?(.*)/', $out, $catMatch ); | |
if ( $cat && isset( Utils::magicMasqs()[$catMatch[1]] ) ) { | |
$contentInfo = $state->serializer->serializedAttrVal( $node, 'content' ); | |
if ( WTUtils::hasExpandedAttrsType( $node ) ) { | |
$out = '{{' . $contentInfo['value'] . '}}'; | |
} elseif ( isset( $dp->src ) ) { | |
$colon = strpos( $dp->src, ':', 2 ); | |
$out = preg_replace( '/^([^:}]+).*$/D', "$1", $dp->src, 1 ); | |
if ( ( $colon === false ) && ( $contentInfo['value'] === '' ) ) { | |
$out .= '}}'; | |
} else { | |
$out .= ':' . $contentInfo['value'] . '}}'; | |
} | |
} else { | |
$magicWord = mb_strtoupper( $catMatch[1] ); | |
$out = '{{' . $magicWord . ':' . $contentInfo['value'] . '}}'; | |
} | |
} else { | |
$out = $state->getEnv()->getSiteConfig()->getMagicWordWT( | |
$switchType[1], $dp->magicSrc ?? '' ); | |
} | |
$state->emitChunk( $out, $node ); | |
} else { | |
( new FallbackHTMLHandler )->handle( $node, $state ); | |
} | |
} elseif ( WTUtils::isAnnotationStartMarkerMeta( $node ) ) { | |
$annType = WTUtils::extractAnnotationType( $node ); | |
if ( $this->needToWriteStartMeta( $state, $node ) ) { | |
$datamw = DOMDataUtils::getDataMw( $node ); | |
$attrs = ""; | |
if ( isset( $datamw->attrs ) ) { | |
foreach ( get_object_vars( $datamw->attrs ) as $k => $v ) { | |
if ( $v === "" ) { | |
$attrs .= ' ' . $k; | |
} else { | |
$attrs .= ' ' . $k . '="' . $v . '"'; | |
} | |
} | |
} | |
// Follow-up on attributes sanitation to happen in T295168 | |
$state->emitChunk( '<' . $annType . $attrs . '>', $node ); | |
$state->openAnnotationRange( $annType, $datamw->extendedRange ?? false ); | |
} | |
} elseif ( WTUtils::isAnnotationEndMarkerMeta( $node ) ) { | |
if ( $this->needToWriteEndMeta( $state, $node ) ) { | |
$annType = WTUtils::extractAnnotationType( $node ); | |
$state->emitChunk( '</' . $annType . '>', $node ); | |
$state->closeAnnotationRange( $annType ); | |
} | |
} else { | |
switch ( $node->getAttribute( 'typeof' ) ?? '' ) { | |
case 'mw:Includes/IncludeOnly': | |
// Remove the dp.src when older revisions of HTML expire in RESTBase | |
$state->emitChunk( $dmw->src ?? $dp->src ?? '', $node ); | |
break; | |
case 'mw:Includes/IncludeOnly/End': | |
// Just ignore. | |
break; | |
case 'mw:Includes/NoInclude': | |
$state->emitChunk( $dp->src ?? '<noinclude>', $node ); | |
break; | |
case 'mw:Includes/NoInclude/End': | |
$state->emitChunk( $dp->src ?? '</noinclude>', $node ); | |
break; | |
case 'mw:Includes/OnlyInclude': | |
$state->emitChunk( $dp->src ?? '<onlyinclude>', $node ); | |
break; | |
case 'mw:Includes/OnlyInclude/End': | |
$state->emitChunk( $dp->src ?? '</onlyinclude>', $node ); | |
break; | |
case 'mw:DiffMarker/inserted': | |
case 'mw:DiffMarker/deleted': | |
case 'mw:DiffMarker/moved': | |
case 'mw:Separator': | |
// just ignore it | |
break; | |
default: | |
( new FallbackHTMLHandler() )->handle( $node, $state ); | |
} | |
} | |
return $node->nextSibling; | |
} | |
/** | |
* Decides if we need to write an annotation start meta at the place we encounter it | |
* @param SerializerState $state | |
* @param Element $node | |
* @return bool | |
*/ | |
private function needToWriteStartMeta( SerializerState $state, Element $node ): bool { | |
if ( !$state->selserMode ) { | |
return true; | |
} | |
if ( WTUtils::isMovedMetaTag( $node ) ) { | |
$nextContentSibling = DOMCompat::getNextElementSibling( $node ); | |
// If the meta tag has been moved, it comes from its next element.... "almost". | |
// First exception is if we have several marker annotations in a row - then we need | |
// to pass them all. Second exception is if we have fostered content: then we're | |
// interested in what happens in the table, which happens _after_ the fostered content. | |
while ( $nextContentSibling !== null && | |
( WTUtils::isMarkerAnnotation( $nextContentSibling ) || | |
!empty( DOMDataUtils::getDataParsoid( $nextContentSibling )->fostered ) | |
) | |
) { | |
$nextContentSibling = DOMCompat::getNextElementSibling( $nextContentSibling ); | |
} | |
if ( $nextContentSibling !== null ) { | |
// When the content from which the meta tag comes gets | |
// deleted or modified, we emit _now_ so that we don't risk losing it. The range | |
// stays extended in the round-tripped version of the wikitext. | |
$nextdiffdata = DOMDataUtils::getDataParsoidDiff( $nextContentSibling ); | |
if ( DOMUtils::isDiffMarker( $nextContentSibling ) || | |
( $nextdiffdata->diff ?? null ) ) { | |
return true; | |
} | |
return !WTSUtils::origSrcValidInEditedContext( $state, $nextContentSibling ); | |
} | |
} | |
return true; | |
} | |
/** | |
* Decides if we need to write an annotation end meta at the place we encounter it | |
* @param SerializerState $state | |
* @param Element $node | |
* @return bool | |
*/ | |
private function needToWriteEndMeta( SerializerState $state, Element $node ): bool { | |
if ( !$state->selserMode ) { | |
return true; | |
} | |
if ( WTUtils::isMovedMetaTag( $node ) ) { | |
$prevElementSibling = DOMCompat::getPreviousElementSibling( $node ); | |
while ( $prevElementSibling !== null && | |
WTUtils::isMarkerAnnotation( $prevElementSibling ) | |
) { | |
$prevElementSibling = DOMCompat::getPreviousElementSibling( $prevElementSibling ); | |
} | |
if ( $prevElementSibling ) { | |
$prevdiffdata = DOMDataUtils::getDataParsoidDiff( $prevElementSibling ); | |
if ( | |
DOMUtils::isDiffMarker( $prevElementSibling ) || | |
$prevdiffdata !== null && $prevdiffdata->diff !== null | |
) { | |
return true; | |
} | |
return !WTSUtils::origSrcValidInEditedContext( $state, $prevElementSibling ); | |
} | |
} | |
return true; | |
} | |
/** | |
* We create a newline (or two) if: | |
* * the previous element is a block element | |
* * the previous element is text, AND we're not in an inline-text situation: this | |
* corresponds to text having been added in VE without creating a paragraph, which happens | |
* when inserting a new line before the <meta> tag in VE. The "we're not in an inline text" | |
* is a heuristic and doesn't work for the ends of line for instance, but it shouldn't add | |
* semantic whitespace either. | |
* @param Node $meta | |
* @param Node $otherNode | |
* @return bool | |
*/ | |
private function needNewLineSepBeforeMeta( Node $meta, Node $otherNode ) { | |
return ( $otherNode !== $meta->parentNode | |
&& ( $otherNode instanceof Element && | |
DOMUtils::isWikitextBlockNode( $otherNode ) || | |
( $otherNode instanceof Text && | |
DOMUtils::isWikitextBlockNode( DOMUtils::nextNonSepSibling( $meta ) ) | |
) | |
) ); | |
} | |
/** @inheritDoc */ | |
public function before( Element $node, Node $otherNode, SerializerState $state ): array { | |
if ( WTUtils::isAnnotationStartMarkerMeta( $node ) ) { | |
if ( $this->needNewLineSepBeforeMeta( $node, $otherNode ) ) { | |
return [ 'min' => 2 ]; | |
} else { | |
return []; | |
} | |
} | |
if ( WTUtils::isAnnotationEndMarkerMeta( $node ) ) { | |
if ( $this->needNewLineSepBeforeMeta( $node, $otherNode ) ) { | |
return [ | |
'min' => 1 | |
]; | |
} else { | |
return []; | |
} | |
} | |
$type = $node->getAttribute( 'typeof' ) ?: $node->getAttribute( 'property' ) ?: null; | |
if ( $type && str_contains( $type, 'mw:PageProp/categorydefaultsort' ) ) { | |
if ( $otherNode instanceof Element | |
&& DOMCompat::nodeName( $otherNode ) === 'p' | |
&& ( DOMDataUtils::getDataParsoid( $otherNode )->stx ?? null ) !== 'html' | |
) { | |
// Since defaultsort is outside the p-tag, we need 2 newlines | |
// to ensure that it go back into the p-tag when parsed. | |
return [ 'min' => 2 ]; | |
} else { | |
return [ 'min' => 1 ]; | |
} | |
} elseif ( WTUtils::isNewElt( $node ) && | |
// Placeholder and annotation metas or <*include*> tags don't need to be serialized on | |
// their own line | |
!DOMUtils::matchTypeOf( $node, '#^mw:(Placeholder|Includes|Annotation)(/|$)#' ) | |
) { | |
return [ 'min' => 1 ]; | |
} else { | |
return []; | |
} | |
} | |
/** @inheritDoc */ | |
public function after( Element $node, Node $otherNode, SerializerState $state ): array { | |
if ( WTUtils::isAnnotationEndMarkerMeta( $node ) ) { | |
if ( $otherNode !== $node->parentNode && $otherNode instanceof Element && | |
DOMUtils::isWikitextBlockNode( $otherNode ) ) { | |
return [ 'min' => 2 ]; | |
} else { | |
return []; | |
} | |
} | |
if ( WTUtils::isAnnotationStartMarkerMeta( $node ) ) { | |
if ( $otherNode !== $node->parentNode && $otherNode instanceof Element && | |
DOMUtils::isWikitextBlockNode( $otherNode ) ) { | |
return [ 'min' => 1 ]; | |
} else { | |
return []; | |
} | |
} | |
// No diffs | |
if ( WTUtils::isNewElt( $node ) && | |
// Placeholder and annotation metas or <*include*> tags don't need to be serialized on | |
// their own line | |
!DOMUtils::matchTypeOf( $node, '#^mw:(Placeholder|Includes|Annotation)(/|$)#' ) | |
) { | |
return [ 'min' => 1 ]; | |
} else { | |
return []; | |
} | |
} | |
} |