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 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 151
AnnotationDOMRangeBuilder
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 11
2070
0.00% covered (danger)
0.00%
0 / 151
 __construct
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 wrapAnnotationsInTree
0.00% covered (danger)
0.00%
0 / 1
72
0.00% covered (danger)
0.00%
0 / 23
 makeUneditable
0.00% covered (danger)
0.00%
0 / 1
90
0.00% covered (danger)
0.00%
0 / 31
 moveRangeStart
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 20
 moveRangeEnd
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 25
 isExtended
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 7
 setMetaDataMwForRange
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 7
 matchMetaType
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 2
 getRangeId
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 updateDSRForFirstRangeNode
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 execute
0.00% covered (danger)
0.00%
0 / 1
56
0.00% covered (danger)
0.00%
0 / 30
<?php
declare( strict_types = 1 );
namespace Wikimedia\Parsoid\Wt2Html\PP\Processors;
use SplObjectStorage;
use Wikimedia\Parsoid\Core\DomSourceRange;
use Wikimedia\Parsoid\DOM\Document;
use Wikimedia\Parsoid\DOM\Element;
use Wikimedia\Parsoid\DOM\Node;
use Wikimedia\Parsoid\NodeData\DataParsoid;
use Wikimedia\Parsoid\Utils\DOMCompat;
use Wikimedia\Parsoid\Utils\DOMDataUtils;
use Wikimedia\Parsoid\Utils\DOMUtils;
use Wikimedia\Parsoid\Utils\WTUtils;
use Wikimedia\Parsoid\Wt2Html\Frame;
class AnnotationDOMRangeBuilder extends DOMRangeBuilder {
    /** @var MigrateTrailingNLs */
    private $migrateTrailingNls;
    /**
     * AnnotationDOMRangeBuilder constructor.
     * @param Document $document
     * @param Frame $frame
     */
    public function __construct( Document $document, Frame $frame ) {
        parent::__construct( $document, $frame );
        $this->migrateTrailingNls = new MigrateTrailingNLs();
    }
    /**
     * @param array $annRanges
     */
    private function wrapAnnotationsInTree( array $annRanges ): void {
        foreach ( $annRanges as $range ) {
            if ( DOMUtils::isFosterablePosition( $range->start ) ) {
                $newStart = $range->start;
                while ( DOMUtils::isFosterablePosition( $newStart ) ) {
                    $newStart = $newStart->parentNode;
                }
                $range->start = $newStart;
            }
            if ( DOMUtils::isFosterablePosition( $range->end ) ) {
                $newEnd = $range->end;
                while ( DOMUtils::isFosterablePosition( $newEnd ) ) {
                    $newEnd = $newEnd->parentNode;
                }
                $range->end = $newEnd;
            }
            if ( $range->startElem !== $range->start ) {
                $this->moveRangeStart( $range, $range->start );
            }
            if ( $range->endElem !== $range->end ) {
                $this->moveRangeEnd( $range, $range->end );
            }
        }
    }
    /**
     * Makes the DOM range between $range->startElem and $range->endElem uneditable by wrapping
     * it into a <div> (for block ranges) or <span> (for inline ranges) with the mw:ExtendedAnnRange
     * type.
     * @param DOMRangeInfo $range
     * @param int|null $actualRangeStart
     * @param int|null $actualRangeEnd
     */
    private function makeUneditable( DOMRangeInfo $range, ?int $actualRangeStart, ?int $actualRangeEnd ) {
        $parent = $range->startElem->parentNode;
        $node = $range->startElem;
        $inline = true;
        while ( $node !== $range->endElem ) {
            $node = $node->nextSibling;
            if ( $node instanceof Element && DOMUtils::hasBlockTag( $node ) ) {
                $inline = false;
                break;
            }
        }
        $wrap = $parent->ownerDocument->createElement( $inline ? 'span' : 'div' );
        $parent->insertBefore( $wrap, $range->startElem );
        $toMove = $range->startElem;
        while ( $toMove !== $range->endElem ) {
            $nextToMove = $toMove->nextSibling;
            $wrap->appendChild( $toMove );
            $toMove = $nextToMove;
        }
        $wrap->appendChild( $toMove );
        $wrap->setAttribute( "typeof", "mw:ExtendedAnnRange" );
        // Ensure template continuity is not broken
        $about = $range->startElem->getAttribute( "about" );
        if ( $about ) {
            $wrap->setAttribute( "about", $about );
        }
        $dp = new DataParsoid();
        $dp->autoInsertedStart = true;
        $dp->autoInsertedEnd = true;
        $dp->dsr = new DomSourceRange( $actualRangeStart, $actualRangeEnd, 0, 0 );
        DOMDataUtils::setDataParsoid( $wrap, $dp );
        $openRanges = [];
    }
    /**
     * Moves the start of the range to the designated node
     * @param DOMRangeInfo $range the range to modify
     * @param Node $node the new start of the range
     */
    private function moveRangeStart( DOMRangeInfo $range, Node $node ): void {
        $startMeta = $range->startElem;
        $startDataParsoid = DOMDataUtils::getDataParsoid( $startMeta );
        if ( $node instanceof Element ) {
            if ( DOMCompat::nodeName( $node ) === "p" && $node->firstChild === $startMeta ) {
                // If the first child of "p" is the meta, and it gets moved, then it got mistakenly
                // pulled inside the paragraph, and the paragraph dsr that gets computed includes
                // it - which may lead to the tag getting duplicated on roundtrip. Hence, we
                // adjust the dsr of the paragraph in that case. We also don't consider the meta
                // tag to have been moved in that case.
                $pDataParsoid = DOMDataUtils::getDataParsoid( $node );
                $pDataParsoid->dsr->start = $startDataParsoid->dsr->end;
            } else {
                $startDataParsoid->wasMoved = true;
            }
        }
        $node = $this->getStartConsideringFosteredContent( $node );
        $node->parentNode->insertBefore( $startMeta, $node );
        if ( $node instanceof Element ) {
            // Ensure template continuity is not broken
            $about = $node->getAttribute( "about" );
            if ( $about ) {
                $startMeta->setAttribute( "about", $about );
            }
        }
        $range->start = $startMeta;
    }
    /**
     * Moves the start of the range to the designated node
     * @param DOMRangeInfo $range the range to modify
     * @param Node $node the new start of the range
     */
    private function moveRangeEnd( DOMRangeInfo $range, Node $node ): void {
        $endMeta = $range->endElem;
        $endDataParsoid = DOMDataUtils::getDataParsoid( $endMeta );
        if ( $node instanceof Element ) {
            $endMetaWasLastChild = $node->lastChild === $endMeta;
            // Migrate $endMeta and ensure template continuity is not broken
            $node->parentNode->insertBefore( $endMeta, $node->nextSibling );
            $about = $node->getAttribute( "about" );
            if ( $about ) {
                $endMeta->setAttribute( "about", $about );
            }
            if ( ( DOMCompat::nodeName( $node ) === "p" ) && $endMetaWasLastChild ) {
                // If the last child of "p" is the meta, and it gets moved, then it got mistakenly
                // pulled inside the paragraph, and the paragraph dsr that gets computed includes
                // it - which may lead to the tag getting duplicated on roundtrip. Hence, we
                // adjust the dsr of the paragraph in that case. We also don't consider the meta
                // tag to have been moved in that case.
                $pDataParsoid = DOMDataUtils::getDataParsoid( $node );
                $pDataParsoid->dsr->end = $endDataParsoid->dsr->start;
                $prevLength = strlen( $node->textContent ?? '' );
                $this->migrateTrailingNls->doMigrateTrailingNLs( $node, $this->env );
                $newLength = strlen( $node->textContent ?? '' );
                if ( $prevLength != $newLength ) {
                    $pDataParsoid->dsr->end -= ( $prevLength - $newLength );
                }
            } else {
                $endDataParsoid->wasMoved = true;
                DOMDataUtils::setDataParsoid( $endMeta, $endDataParsoid );
            }
        }
        $range->end = $endMeta;
    }
    /**
     * Returns whether one of the ends of the range has been moved, which corresponds to an extended
     * range.
     * @param DOMRangeInfo $range
     * @return bool
     */
    private function isExtended( DOMRangeInfo $range ): bool {
        if ( $range->extendedByOverlapMerge ) {
            return true;
        }
        $startDataParsoid = DOMDataUtils::getDataParsoid( $range->startElem );
        $endDataParsoid = DOMDataUtils::getDataParsoid( $range->endElem );
        return ( $startDataParsoid->wasMoved ?? false ) || ( $endDataParsoid->wasMoved ?? false );
    }
    /**
     * Sets the data-mw attribute for meta tags of the provided range
     * @param DOMRangeInfo $range range whose start and end element needs to be to modified
     * @param bool $isExtended whether the range got extended
     */
    private function setMetaDataMwForRange( DOMRangeInfo $range, bool $isExtended ): void {
        $startDataMw = DOMDataUtils::getDataMw( $range->startElem );
        $endDataMw = DOMDataUtils::getDataMw( $range->endElem );
        $startDataMw->extendedRange = $isExtended;
        $startDataMw->wtOffsets = DOMDataUtils::getDataParsoid( $range->startElem )->tsr;
        $endDataMw->wtOffsets = DOMDataUtils::getDataParsoid( $range->endElem )->tsr;
        unset( $endDataMw->rangeId );
    }
    /**
     * Returns the meta type of the element if it exists and matches the type expected by the
     * current class, null otherwise
     * @param Element $elem the element to check
     * @return string|null
     */
    protected function matchMetaType( Element $elem ): ?string {
        // for this class we're interested in the annotation type
        return WTUtils::matchAnnotationMeta( $elem );
    }
    /**
     * Returns the range ID of a node - in the case of annotations, the "rangeId" property
     * of its "data-mw" attribute.
     * @param Element $node
     * @return string
     */
    protected function getRangeId( Element $node ): string {
        return DOMDataUtils::getDataMw( $node )->rangeId ?? '';
    }
    /**
     * @inheritDoc
     */
    protected function updateDSRForFirstRangeNode( Element $target, Element $source ): void {
        // nop
    }
    /**
     * @param Node $root
     */
    public function execute( Node $root ): void {
        try {
            $annRanges = $this->findWrappableMetaRanges( $root );
        } catch ( RangeBuilderException $e ) {
            $this->env->log( 'warn', 'The annotation ranges could not be fully detected. ' .
                ' Annotation processing cancelled. ' );
            return;
        }
        $rangesByType = [];
        foreach ( $annRanges as $range ) {
            $annType = WTUtils::extractAnnotationType( $range->startElem );
            if ( !isset( $rangesByType[$annType] ) ) {
                $rangesByType[$annType] = [];
            }
            $rangesByType[$annType][] = $range;
        }
        foreach ( $rangesByType as $singleTypeRange ) {
            $this->nodeRanges = new SplObjectStorage;
            $topRanges = $this->findTopLevelNonOverlappingRanges( $root, $singleTypeRange );
            $this->wrapAnnotationsInTree( $topRanges );
            foreach ( $topRanges as $range ) {
                $actualRangeStart = DOMDataUtils::getDataParsoid( $range->start )->dsr->start;
                $actualRangeEnd = DOMDataUtils::getDataParsoid( $range->end )->dsr->end;
                $isExtended = $this->isExtended( $range );
                if ( $isExtended ) {
                    $this->makeUneditable( $range, $actualRangeStart, $actualRangeEnd );
                }
                $this->setMetaDataMwForRange( $range, $isExtended );
            }
        }
    }
}