Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 98
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
HandleLinkNeighbours
0.00% covered (danger)
0.00%
0 / 98
0.00% covered (danger)
0.00%
0 / 4
3080
0.00% covered (danger)
0.00%
0 / 1
 getLinkPrefix
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getLinkTrail
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 findAndHandleNeighbour
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
506
 handler
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 1
756
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Wt2Html\PP\Handlers;
5
6use Wikimedia\Parsoid\Config\Env;
7use Wikimedia\Parsoid\DOM\Element;
8use Wikimedia\Parsoid\DOM\Text;
9use Wikimedia\Parsoid\Utils\DOMCompat;
10use Wikimedia\Parsoid\Utils\DOMDataUtils;
11use Wikimedia\Parsoid\Utils\DOMUtils;
12use Wikimedia\Parsoid\Utils\WTUtils;
13
14class HandleLinkNeighbours {
15    /**
16     * Function for fetching the link prefix based on a link node.
17     * The content will be reversed, so be ready for that.
18     *
19     * @param Env $env
20     * @param Element $aNode
21     * @return ?array
22     */
23    private static function getLinkPrefix( Env $env, Element $aNode ): ?array {
24        $regex = $env->getSiteConfig()->linkPrefixRegex();
25        if ( !$regex ) {
26            return null;
27        }
28
29        $baseAbout = WTUtils::isEncapsulatedDOMForestRoot( $aNode ) ? DOMCompat::getAttribute( $aNode, 'about' ) : null;
30        return self::findAndHandleNeighbour( $env, false, $regex, $aNode, $baseAbout );
31    }
32
33    /**
34     * Function for fetching the link trail based on a link node.
35     *
36     * @param Env $env
37     * @param Element $aNode
38     * @return ?array
39     */
40    private static function getLinkTrail( Env $env, Element $aNode ): ?array {
41        $regex = $env->getSiteConfig()->linkTrailRegex();
42        if ( !$regex ) {
43            return null;
44        }
45
46        $baseAbout = WTUtils::isEncapsulatedDOMForestRoot( $aNode ) ? DOMCompat::getAttribute( $aNode, 'about' ) : null;
47        return self::findAndHandleNeighbour( $env, true, $regex, $aNode, $baseAbout );
48    }
49
50    /**
51     * Abstraction of both link-prefix and link-trail searches.
52     *
53     * @param Env $env
54     * @param bool $goForward
55     * @param string $regex
56     * @param Element $aNode
57     * @param ?string $baseAbout
58     * @return array
59     */
60    private static function findAndHandleNeighbour(
61        Env $env, bool $goForward, string $regex, Element $aNode, ?string $baseAbout
62    ): array {
63        $nbrs = [];
64        $node = $goForward ? $aNode->nextSibling : $aNode->previousSibling;
65        while ( $node !== null ) {
66            $nextSibling = $goForward ? $node->nextSibling : $node->previousSibling;
67            $fromTpl = WTUtils::isEncapsulatedDOMForestRoot( $node );
68            $unwrappedSpan = null;
69            if ( $node instanceof Element && DOMCompat::nodeName( $node ) === 'span' &&
70                !WTUtils::isLiteralHTMLNode( $node ) &&
71                // <span> comes from the same template we are in
72                $fromTpl && $baseAbout !== null && DOMCompat::getAttribute( $node, 'about' ) === $baseAbout &&
73                // Not interested in <span>s wrapping more than 1 node
74                ( !$node->firstChild || $node->firstChild->nextSibling === null )
75            ) {
76                // With these checks here, we are not going to support link suffixes
77                // or link trails coming from a different transclusion than the link itself.
78                // {{1x|[[Foo]]}}{{1x|bar}} won't be link-trailed. Similarly for prefixes.
79                // But, we want support {{1x|Foo[[bar]]}} style link prefixes where the
80                // "Foo" is wrapped in a <span> and carries the transclusion info.
81                if ( !$node->hasAttribute( 'typeof' ) ||
82                    ( !$goForward && !$aNode->hasAttribute( 'typeof' ) )
83                ) {
84                    $unwrappedSpan = $node;
85                    $node = $node->firstChild;
86                }
87            }
88
89            if ( $node instanceof Text && preg_match( $regex, $node->nodeValue, $matches ) && $matches[0] !== '' ) {
90                $nbr = [ 'node' => $node, 'src' => $matches[0], 'fromTpl' => $fromTpl ];
91
92                // Link prefix node is templated => migrate transclusion info to $aNode
93                if ( $unwrappedSpan && $unwrappedSpan->hasAttribute( 'typeof' ) ) {
94                    DOMUtils::addTypeOf( $aNode, DOMCompat::getAttribute( $unwrappedSpan, 'typeof' ) ?? '' );
95                    DOMDataUtils::setDataMw( $aNode, DOMDataUtils::getDataMw( $unwrappedSpan ) );
96                }
97
98                if ( $nbr['src'] === $node->nodeValue ) {
99                    // entire node matches linkprefix/trail
100                    $node->parentNode->removeChild( $node );
101                    if ( $unwrappedSpan ) { // The empty span is useless now
102                        $unwrappedSpan->parentNode->removeChild( $unwrappedSpan );
103                    }
104
105                    // Continue looking at siblings
106                    $nbrs[] = $nbr;
107                } else {
108                    // part of node matches linkprefix/trail
109                    $nbr['node'] = $node->ownerDocument->createTextNode( $matches[0] );
110                    $tn = $node->ownerDocument->createTextNode( preg_replace( $regex, '', $node->nodeValue ) );
111                    $node->parentNode->replaceChild( $tn, $node );
112
113                    // No need to look any further beyond this point
114                    $nbrs[] = $nbr;
115                    break;
116                }
117            } else {
118                break;
119            }
120
121            $node = $nextSibling;
122        }
123
124        return $nbrs;
125    }
126
127    /**
128     * Workhorse function for bringing linktrails and link prefixes into link content.
129     * NOTE that this function mutates the node's siblings on either side.
130     *
131     * @param Element $node
132     * @param Env $env
133     * @return bool|Element
134     */
135    public static function handler( Element $node, Env $env ) {
136        if ( !DOMUtils::matchRel( $node, '#^mw:WikiLink(/Interwiki)?$#D' ) ) {
137            return true;
138        }
139
140        $firstTplNode = WTUtils::findFirstEncapsulationWrapperNode( $node );
141        $inTpl = $firstTplNode !== null && DOMUtils::hasTypeOf( $firstTplNode, 'mw:Transclusion' );
142
143        // Find link prefix neighbors
144        $dp = DOMDataUtils::getDataParsoid( $node );
145        $prefixNbrs = self::getLinkPrefix( $env, $node );
146        if ( !empty( $prefixNbrs ) ) {
147            $prefix = '';
148            $dataMwCorrection = '';
149            $dsrCorrection = 0;
150            foreach ( $prefixNbrs as $nbr ) {
151                $node->insertBefore( $nbr['node'], $node->firstChild );
152                $prefix = $nbr['src'] . $prefix;
153                if ( !$nbr['fromTpl'] ) {
154                    $dataMwCorrection = $nbr['src'] . $dataMwCorrection;
155                    $dsrCorrection += strlen( $nbr['src'] );
156                }
157            }
158
159            // Set link prefix
160            if ( $prefix !== '' ) {
161                $dp->prefix = $prefix;
162            }
163
164            // Correct DSR values
165            if ( $firstTplNode ) {
166                // If this is part of a template, update dsr on that node!
167                $dp = DOMDataUtils::getDataParsoid( $firstTplNode );
168            }
169            if ( $dsrCorrection !== 0 && !empty( $dp->dsr ) ) {
170                if ( $dp->dsr->start !== null ) {
171                    $dp->dsr->start -= $dsrCorrection;
172                }
173                if ( $dp->dsr->openWidth !== null ) {
174                    $dp->dsr->openWidth += $dsrCorrection;
175                }
176            }
177
178            // Update template wrapping data-mw info, if necessary
179            if ( $dataMwCorrection !== '' && $inTpl ) {
180                $dataMW = DOMDataUtils::getDataMw( $firstTplNode );
181                if ( isset( $dataMW->parts ) ) {
182                    array_unshift( $dataMW->parts, $dataMwCorrection );
183                }
184            }
185        }
186
187        // Find link trail neighbors
188        $dp = DOMDataUtils::getDataParsoid( $node );
189        $trailNbrs = self::getLinkTrail( $env, $node );
190        if ( !empty( $trailNbrs ) ) {
191            $trail = '';
192            $dataMwCorrection = '';
193            $dsrCorrection = 0;
194            foreach ( $trailNbrs as $nbr ) {
195                $node->appendChild( $nbr['node'] );
196                $trail .= $nbr['src'];
197                if ( !$nbr['fromTpl'] ) {
198                    $dataMwCorrection .= $nbr['src'];
199                    $dsrCorrection += strlen( $nbr['src'] );
200                }
201            }
202
203            // Set link trail
204            if ( $trail !== '' ) {
205                $dp->tail = $trail;
206            }
207
208            // Correct DSR values
209            if ( $firstTplNode ) {
210                // If this is part of a template, update dsr on that node!
211                $dp = DOMDataUtils::getDataParsoid( $firstTplNode );
212            }
213            if ( $dsrCorrection !== 0 && !empty( $dp->dsr ) ) {
214                if ( $dp->dsr->end !== null ) {
215                    $dp->dsr->end += $dsrCorrection;
216                }
217                if ( $dp->dsr->closeWidth !== null ) {
218                    $dp->dsr->closeWidth += $dsrCorrection;
219                }
220            }
221
222            // Update template wrapping data-mw info, if necessary
223            if ( $dataMwCorrection !== '' && $inTpl ) {
224                $dataMW = DOMDataUtils::getDataMw( $firstTplNode );
225                if ( isset( $dataMW->parts ) ) {
226                    $dataMW->parts[] = $dataMwCorrection;
227                }
228            }
229
230            // If $trailNbs is not empty, $node's tail siblings have been consumed
231            return $node;
232        }
233
234        return true;
235    }
236}