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