Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 7
CRAP
15.94% covered (danger)
15.94%
22 / 138
MetaHandler
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 7
3598.42
15.94% covered (danger)
15.94%
22 / 138
 __construct
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 handle
0.00% covered (danger)
0.00%
0 / 1
300.16
31.43% covered (danger)
31.43%
22 / 70
 needToWriteStartMeta
0.00% covered (danger)
0.00%
0 / 1
72
0.00% covered (danger)
0.00%
0 / 14
 needToWriteEndMeta
0.00% covered (danger)
0.00%
0 / 1
90
0.00% covered (danger)
0.00%
0 / 14
 needNewLineSepBeforeMeta
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 5
 before
0.00% covered (danger)
0.00%
0 / 1
210
0.00% covered (danger)
0.00%
0 / 19
 after
0.00% covered (danger)
0.00%
0 / 1
132
0.00% covered (danger)
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 );
            }
            // 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 [];
        }
    }
}