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 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 92
PHandler
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 8
4970
0.00% covered (danger)
0.00%
0 / 92
 __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
6
0.00% covered (danger)
0.00%
0 / 2
 before
0.00% covered (danger)
0.00%
0 / 1
462
0.00% covered (danger)
0.00%
0 / 27
 after
0.00% covered (danger)
0.00%
0 / 1
156
0.00% covered (danger)
0.00%
0 / 15
 currWikitextLineHasBlockNode
0.00% covered (danger)
0.00%
0 / 1
156
0.00% covered (danger)
0.00%
0 / 18
 newWikitextLineMightHaveBlockNode
0.00% covered (danger)
0.00%
0 / 1
72
0.00% covered (danger)
0.00%
0 / 14
 treatAsPPTransition
0.00% covered (danger)
0.00%
0 / 1
72
0.00% covered (danger)
0.00%
0 / 8
 isPPTransition
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 6
<?php
declare( strict_types = 1 );
namespace Wikimedia\Parsoid\Html2Wt\DOMHandlers;
use stdClass;
use Wikimedia\Parsoid\Config\WikitextConstants;
use Wikimedia\Parsoid\DOM\Element;
use Wikimedia\Parsoid\DOM\Node;
use Wikimedia\Parsoid\DOM\Text;
use Wikimedia\Parsoid\Html2Wt\SerializerState;
use Wikimedia\Parsoid\Utils\DOMCompat;
use Wikimedia\Parsoid\Utils\DOMDataUtils;
use Wikimedia\Parsoid\Utils\DOMUtils;
use Wikimedia\Parsoid\Utils\WTUtils;
class PHandler extends DOMHandler {
    public function __construct() {
        // Counterintuitive but seems right.
        // Otherwise the generated wikitext will parse as an indent-pre
        // escapeWikitext nowiking will deal with leading space for content
        // inside the p-tag, but forceSOL suppresses whitespace before the p-tag.
        parent::__construct( true );
    }
    /** @inheritDoc */
    public function handle(
        Element $node, SerializerState $state, bool $wrapperUnmodified = false
    ): ?Node {
        // XXX: Handle single-line mode by switching to HTML handler!
        $state->serializeChildren( $node );
        return $node->nextSibling;
    }
    /** @inheritDoc */
    public function before( Element $node, Node $otherNode, SerializerState $state ): array {
        $otherNodeName = DOMCompat::nodeName( $otherNode );
        $tableCellOrBody = [ 'td', 'th', 'body' ];
        if ( $node->parentNode === $otherNode
            && ( DOMUtils::isListItem( $otherNode ) || in_array( $otherNodeName, $tableCellOrBody, true ) )
        ) {
            if ( in_array( $otherNodeName, $tableCellOrBody, true ) ) {
                return [ 'min' => 0, 'max' => 1 ];
            } else {
                return [ 'min' => 0, 'max' => 0 ];
            }
        } elseif ( ( $otherNode === DOMUtils::previousNonDeletedSibling( $node )
                // p-p transition
                && $otherNode instanceof Element // for static analyzers
                && $otherNodeName === 'p'
                && ( DOMDataUtils::getDataParsoid( $otherNode )->stx ?? null ) !== 'html' )
            || ( self::treatAsPPTransition( $otherNode )
                && $otherNode === DOMUtils::previousNonSepSibling( $node )
                // A new wikitext line could start at this P-tag. We have to figure out
                // if 'node' needs a separation of 2 newlines from that P-tag. Examine
                // previous siblings of 'node' to see if we emitted a block tag
                // there => we can make do with 1 newline separator instead of 2
                // before the P-tag.
                && !$this->currWikitextLineHasBlockNode( $state->currLine, $otherNode ) )
            || ( WTUtils::isMarkerAnnotation( DOMUtils::nextNonSepSibling( $otherNode ) )
                && DOMUtils::nextNonSepSibling( DOMUtils::nextNonSepSibling( $otherNode ) ) === $node )
        ) {
            return [ 'min' => 2, 'max' => 2 ];
        } elseif ( self::treatAsPPTransition( $otherNode )
            || ( DOMUtils::isWikitextBlockNode( $otherNode )
                && DOMCompat::nodeName( $otherNode ) !== 'blockquote'
                && $node->parentNode === $otherNode )
            // new p-node added after sol-transparent wikitext should always
            // get serialized onto a new wikitext line.
            || ( WTUtils::emitsSolTransparentSingleLineWT( $otherNode )
                && WTUtils::isNewElt( $node ) )
        ) {
            if ( !DOMUtils::hasNameOrHasAncestorOfName( $otherNode, 'figcaption' ) ) {
                return [ 'min' => 1, 'max' => 2 ];
            } else {
                return [ 'min' => 0, 'max' => 2 ];
            }
        } else {
            return [ 'min' => 0, 'max' => 2 ];
        }
    }
    /** @inheritDoc */
    public function after( Element $node, Node $otherNode, SerializerState $state ): array {
        if ( !( $node->lastChild && DOMCompat::nodeName( $node->lastChild ) === 'br' )
            && self::isPPTransition( $otherNode )
            // A new wikitext line could start at this P-tag. We have to figure out
            // if 'node' needs a separation of 2 newlines from that P-tag. Examine
            // previous siblings of 'node' to see if we emitted a block tag
            // there => we can make do with 1 newline separator instead of 2
            // before the P-tag.
             && !$this->currWikitextLineHasBlockNode( $state->currLine, $node, true )
            // Since we are going to emit newlines before the other P-tag, we know it
            // is going to start a new wikitext line. We have to figure out if 'node'
            // needs a separation of 2 newlines from that P-tag. Examine following
            // siblings of 'node' to see if we might emit a block tag there => we can
            // make do with 1 newline separator instead of 2 before the P-tag.
             && !$this->newWikitextLineMightHaveBlockNode( $otherNode )
        ) {
            return [ 'min' => 2, 'max' => 2 ];
        } elseif ( DOMUtils::atTheTop( $otherNode ) ) {
            return [ 'min' => 0, 'max' => 2 ];
        } elseif ( self::treatAsPPTransition( $otherNode )
            || ( DOMUtils::isWikitextBlockNode( $otherNode )
                && DOMCompat::nodeName( $otherNode ) !== 'blockquote'
                && $node->parentNode === $otherNode )
        ) {
            if ( !DOMUtils::hasNameOrHasAncestorOfName( $otherNode, 'figcaption' ) ) {
                return [ 'min' => 1, 'max' => 2 ];
            } else {
                return [ 'min' => 0, 'max' => 2 ];
            }
        } else {
            return [ 'min' => 0, 'max' => 2 ];
        }
    }
    // IMPORTANT: Do not start walking from line.firstNode forward. Always
    // walk backward from node. This is because in selser mode, it looks like
    // line.firstNode doesn't always correspond to the wikitext line that is
    // being processed since the previous emitted node might have been an unmodified
    // DOM node that generated multiple wikitext lines.
    /**
     * @param ?stdClass $line See SerializerState::$currLine
     * @param Node $node
     * @param bool $skipNode
     * @return bool
     */
    private function currWikitextLineHasBlockNode(
        ?stdClass $line, Node $node, bool $skipNode = false
    ): bool {
        $parentNode = $node->parentNode;
        if ( !$skipNode ) {
            // If this node could break this wikitext line and emit
            // non-ws content on a new line, the P-tag will be on that new line
            // with text content that needs P-wrapping.
            if ( preg_match( '/\n[^\s]/', $node->textContent ) ) {
                return false;
            }
        }
        $node = DOMUtils::previousNonDeletedSibling( $node );
        while ( !$node || !DOMUtils::atTheTop( $node ) ) {
            while ( $node ) {
                // If we hit a block node that will render on the same line, we are done!
                if ( WTUtils::isBlockNodeWithVisibleWT( $node ) ) {
                    return true;
                }
                // If this node could break this wikitext line, we are done.
                // This is conservative because textContent could be looking at descendents
                // of 'node' that may not have been serialized yet. But this is safe.
                if ( str_contains( $node->textContent, "\n" ) ) {
                    return false;
                }
                $node = DOMUtils::previousNonDeletedSibling( $node );
                // Don't go past the current line in any case.
                if ( !empty( $line->firstNode ) && $node &&
                    DOMUtils::isAncestorOf( $node, $line->firstNode )
                ) {
                    return false;
                }
            }
            $node = $parentNode;
            $parentNode = $node->parentNode;
        }
        return false;
    }
    /**
     * @param Node $node
     * @return bool
     */
    private function newWikitextLineMightHaveBlockNode( Node $node ): bool {
        $node = DOMUtils::nextNonDeletedSibling( $node );
        while ( $node ) {
            if ( $node instanceof Text ) {
                // If this node will break this wikitext line, we are done!
                if ( preg_match( '/\n/', $node->nodeValue ) ) {
                    return false;
                }
            } elseif ( $node instanceof Element ) {
                // These tags will always serialize onto a new line
                if (
                    isset( WikitextConstants::$HTMLTagsRequiringSOLContext[DOMCompat::nodeName( $node )] ) &&
                    !WTUtils::isLiteralHTMLNode( $node )
                ) {
                    return false;
                }
                // We hit a block node that will render on the same line
                if ( WTUtils::isBlockNodeWithVisibleWT( $node ) ) {
                    return true;
                }
                // Go conservative
                return false;
            }
            $node = DOMUtils::nextNonDeletedSibling( $node );
        }
        return false;
    }
    /**
     * Node is being serialized before/after a P-tag.
     * While computing newline constraints, this function tests
     * if node should be treated as a P-wrapped node.
     * @param Node $node
     * @return bool
     */
    private static function treatAsPPTransition( Node $node ): bool {
        // Treat text/p similar to p/p transition
        // If an element, it should not be a:
        // * block node or literal HTML node
        // * template wrapper
        // * mw:Includes or Annotation meta or a SOL-transparent link
        return $node instanceof Text
            || ( !DOMUtils::atTheTop( $node )
                && !DOMUtils::isWikitextBlockNode( $node )
                && !WTUtils::isLiteralHTMLNode( $node )
                && !WTUtils::isEncapsulationWrapper( $node )
                && !WTUtils::isSolTransparentLink( $node )
                && !DOMUtils::matchTypeOf( $node, '#^mw:Includes/#' )
                && !DOMUtils::matchTypeOf( $node, '#^mw:Annotation/#' ) );
    }
    /**
     * Test if $node is a P-wrapped node or should be treated as one.
     *
     * @param ?Node $node
     * @return bool
     */
    public static function isPPTransition( ?Node $node ): bool {
        if ( !$node ) {
            return false;
        }
        return ( $node instanceof Element // for static analyzers
                && DOMCompat::nodeName( $node ) === 'p'
                && ( DOMDataUtils::getDataParsoid( $node )->stx ?? '' ) !== 'html' )
            || self::treatAsPPTransition( $node );
    }
}