Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
12.50% covered (danger)
12.50%
1 / 8
CRAP
3.49% covered (danger)
3.49%
3 / 86
PWrap
0.00% covered (danger)
0.00%
0 / 1
12.50% covered (danger)
12.50%
1 / 8
1267.67
3.49% covered (danger)
3.49%
3 / 86
 flatten
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 1
 pWrapOptional
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 3
 isSplittableTag
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 mergeRuns
0.00% covered (danger)
0.00%
0 / 1
110
0.00% covered (danger)
0.00%
0 / 31
 split
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 12
 pWrapDOM
0.00% covered (danger)
0.00%
0 / 1
90
0.00% covered (danger)
0.00%
0 / 26
 pWrapInsideTag
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 9
 run
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
<?php
declare( strict_types = 1 );
namespace Wikimedia\Parsoid\Wt2Html\PP\Processors;
use Wikimedia\Parsoid\Config\Env;
use Wikimedia\Parsoid\DOM\Comment;
use Wikimedia\Parsoid\DOM\DocumentFragment;
use Wikimedia\Parsoid\DOM\Element;
use Wikimedia\Parsoid\DOM\Node;
use Wikimedia\Parsoid\DOM\Text;
use Wikimedia\Parsoid\Utils\DOMCompat;
use Wikimedia\Parsoid\Utils\DOMDataUtils;
use Wikimedia\Parsoid\Utils\DOMUtils;
use Wikimedia\Parsoid\Utils\PHPUtils;
use Wikimedia\Parsoid\Wikitext\Consts;
use Wikimedia\Parsoid\Wt2Html\Wt2HtmlDOMProcessor;
class PWrap implements Wt2HtmlDOMProcessor {
    /**
     * Flattens an array with other arrays for elements into
     * an array without nested arrays.
     *
     * @param array[] $a
     * @return array
     */
    private function flatten( array $a ): array {
        return $a === [] ? [] : array_merge( ...$a );
    }
    /**
     * Is a P-wrapper optional for this node?
     *
     * The following nodes do not need p wrappers of their own:
     * - whitespace nodes
     * - comment nodes
     * - HTML metadata tags generated by wikitext (not always rendering-transparent)
     *   and these metatags don't need p-wrappers of their own. Both Remex and Parsoid
     *   have identical p-wrapping behavior on these tags. This is a superset of
     *   \\MediaWiki\Tidy\RemexCompatMunger::$metadataElements.
     *
     * @param Node $n
     * @return bool
     */
    public static function pWrapOptional( Node $n ): bool {
        return ( $n instanceof Text && preg_match( '/^\s*$/D', $n->nodeValue ) ) ||
            $n instanceof Comment ||
            isset( Consts::$HTML['MetaDataTags'][DOMCompat::nodeName( $n )] );
    }
    /**
     * Can we split the subtree rooted at $n into multiple adjacent
     * subtrees rooted in a clone of $n where each of those subtrees
     * get a contiguous subset of $n's children?
     *
     * This is probably equivalent to asking if this node supports the
     * adoption agency algorithm in the HTML5 spec.
     *
     * @param Node $n
     * @return bool
     */
    private function isSplittableTag( Node $n ): bool {
        // Seems safe to split span, sub, sup, cite tags
        //
        // However, if we want to mimic Parsoid and HTML5 spec
        // precisely, we should only use isFormattingElt(n)
        return DOMUtils::isFormattingElt( $n );
    }
    /**
     * Merge a contiguous run of split subtrees that have identical pwrap properties
     *
     * @param Element $n
     * @param array $a
     * @return array
     */
    private function mergeRuns( Element $n, array $a ): array {
        $ret = [];
        // This flag should be transferred to the rightmost
        // clone of this node in the loop below.
        $ndp = DOMDataUtils::getDataParsoid( $n );
        $origAIEnd = $ndp->autoInsertedEnd ?? null;
        $origEndTSR = $ndp->tmp->endTSR ?? null;
        $i = -1;
        foreach ( $a as $v ) {
            if ( $i < 0 ) {
                $ret[] = [ 'pwrap' => $v['pwrap'], 'node' => $n ];
                $i++;
            } elseif ( $ret[$i]['pwrap'] === null ) {
                // @phan-suppress-previous-line PhanTypeInvalidDimOffset
                $ret[$i]['pwrap'] = $v['pwrap'];
            } elseif ( $ret[$i]['pwrap'] !== $v['pwrap'] && $v['pwrap'] !== null ) {
                // @phan-suppress-previous-line PhanTypeInvalidDimOffset
                // @phan-suppress-next-line PhanTypeInvalidDimOffset
                $dp = DOMDataUtils::getDataParsoid( $ret[$i]['node'] );
                $dp->autoInsertedEnd = true;
                unset( $dp->tmp->endTSR );
                $cnode = $n->cloneNode();
                '@phan-var Element $cnode'; // @var Element $cnode
                if ( $n->hasAttribute( DOMDataUtils::DATA_OBJECT_ATTR_NAME ) ) {
                    DOMDataUtils::setNodeData( $cnode, DOMDataUtils::getNodeData( $n )->clone() );
                }
                $ret[] = [ 'pwrap' => $v['pwrap'], 'node' => $cnode ];
                $i++;
                DOMDataUtils::getDataParsoid( $ret[$i]['node'] )->autoInsertedStart = true;
            }
            $ret[$i]['node']->appendChild( $v['node'] );
        }
        if ( $i >= 0 ) {
            $dp = DOMDataUtils::getDataParsoid( $ret[$i]['node'] );
            if ( $origAIEnd ) {
                $dp->autoInsertedEnd = true;
                unset( $dp->tmp->endTSR );
            } else {
                unset( $dp->autoInsertedEnd );
                if ( $origEndTSR ) {
                    $dp->getTemp()->endTSR = $origEndTSR;
                }
            }
        }
        return $ret;
    }
    /**
     * Implements the split operation described in the algorithm below.
     *
     * The values of 'pwrap' here bear out in pWrapDOM below.
     *
     *  true: opens a paragaph or continues adding to a paragraph
     *  false: closes a paragraph
     *  null: agnostic, doesn't open or close a paragraph
     *
     * @param Node $n
     * @return array
     */
    private function split( Node $n ): array {
        if ( $this->pWrapOptional( $n ) ) {
            // Set 'pwrap' to null so p-wrapping doesn't break
            // a run of wrappable nodes because of these.
            return [ [ 'pwrap' => null, 'node' => $n ] ];
        } elseif ( $n instanceof Text ) {
            return [ [ 'pwrap' => true, 'node' => $n ] ];
        } elseif ( !$this->isSplittableTag( $n ) || count( $n->childNodes ) === 0 ) {
            // block tag OR non-splittable inline tag
            return [
                [ 'pwrap' => !DOMUtils::hasBlockTag( $n ), 'node' => $n ]
            ];
        } else {
            DOMUtils::assertElt( $n );
            // splittable inline tag
            // split for each child and merge runs
            $children = $n->childNodes;
            $splits = [];
            foreach ( $children as $child ) {
                $splits[] = $this->split( $child );
            }
            return $this->mergeRuns( $n, $this->flatten( $splits ) );
        }
    }
    /**
     * Wrap children of '$root' with paragraph tags
     * so that the final output has the following properties:
     *
     * 1. A paragraph will have at least one non-whitespace text
     *    node or an non-block element node in its subtree.
     *
     * 2. Two paragraph nodes aren't siblings of each other.
     *
     * 3. If a child of $root is not a paragraph node, it is one of:
     * - a white-space only text node
     * - a comment node
     * - a block element
     * - a splittable inline element which has some block node
     *   on *all* paths from it to all leaves in its subtree.
     * - a non-splittable inline element which has some block node
     *   on *some* path from it to a leaf in its subtree.
     *
     * This output is generated with the following algorithm
     *
     * 1. Block nodes are skipped over
     * 2. Non-splittable inline nodes that have a block tag
     *    in its subtree are skipped over.
     * 3. A splittable inline node, I, that has at least one block tag
     *    in its subtree is split into multiple tree such that
     *    - each new tree is $rooted in I
     *    - the trees alternate between two kinds
     *    (a) it has no block node inside
     *        => pwrap is true
     *    (b) all paths from I to its leaves have some block node inside
     *        => pwrap is false
     * 4. A paragraph tag is wrapped around adjacent runs of comment nodes,
     *    text nodes, and an inline node that has no block node embedded inside.
     *    This paragraph tag does not start with nodes for which p-wrapping is
     *    optional (as determined by the pWrapOptional helper). The current
     *    algorithm also ensures that it doesn't end with one of those either
     *    (if it impacts template / param / annotation range building).
     *
     * @param Element|DocumentFragment $root
     */
    private function pWrapDOM( Node $root ) {
        $state = new PWrapState();
        $c = $root->firstChild;
        while ( $c ) {
            $next = $c->nextSibling;
            if ( DOMUtils::isRemexBlockNode( $c ) ) {
                $state->reset();
            } else {
                $vs = $this->split( $c );
                foreach ( $vs as $v ) {
                    $n = $v['node'];
                    if ( $v['pwrap'] === false ) {
                        $state->reset();
                        $root->insertBefore( $n, $next );
                    } elseif ( $v['pwrap'] === null ) {
                        if ( $state->p ) {
                            $state->p->appendChild( $n );
                            $state->processOptionalNode( $n );
                        } else {
                            $root->insertBefore( $n, $next );
                        }
                    } elseif ( $v['pwrap'] === true ) {
                        if ( !$state->p ) {
                            $state->p = $root->ownerDocument->createElement( 'p' );
                            $root->insertBefore( $state->p, $next );
                        }
                        $state->p->appendChild( $n );
                    } else {
                        PHPUtils::unreachable( 'Unexpected value for pwrap.' );
                    }
                }
            }
            $c = $next;
        }
        $state->reset();
    }
    /**
     * This function walks the DOM tree $rooted at '$root'
     * and uses pWrapDOM to add appropriate paragraph wrapper
     * tags around children of nodes with tag name '$tagName'.
     *
     * @param Element|DocumentFragment $root
     * @param string $tagName
     */
    private function pWrapInsideTag( Node $root, string $tagName ) {
        $c = $root->firstChild;
        while ( $c ) {
            $next = $c->nextSibling;
            if ( $c instanceof Element ) {
                if ( DOMCompat::nodeName( $c ) === $tagName ) {
                    $this->pWrapDOM( $c );
                } else {
                    $this->pWrapInsideTag( $c, $tagName );
                }
            }
            $c = $next;
        }
    }
    /**
     * Wrap children of <body> as well as children of
     * <blockquote> found anywhere in the DOM tree.
     *
     * @inheritDoc
     */
    public function run(
        Env $env, Node $root, array $options = [], bool $atTopLevel = false
    ): void {
        '@phan-var Element|DocumentFragment $root';  // @var Element|DocumentFragment $root
        $this->pWrapDOM( $root );
        $this->pWrapInsideTag( $root, 'blockquote' );
    }
}