Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 98 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 1 |
HandleLinkNeighbours | |
0.00% |
0 / 98 |
|
0.00% |
0 / 4 |
3080 | |
0.00% |
0 / 1 |
getLinkPrefix | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
getLinkTrail | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
findAndHandleNeighbour | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
506 | |||
handler | |
0.00% |
0 / 56 |
|
0.00% |
0 / 1 |
756 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace Wikimedia\Parsoid\Wt2Html\PP\Handlers; |
5 | |
6 | use Wikimedia\Parsoid\Config\Env; |
7 | use Wikimedia\Parsoid\DOM\Element; |
8 | use Wikimedia\Parsoid\DOM\Text; |
9 | use Wikimedia\Parsoid\Utils\DOMCompat; |
10 | use Wikimedia\Parsoid\Utils\DOMDataUtils; |
11 | use Wikimedia\Parsoid\Utils\DOMUtils; |
12 | use Wikimedia\Parsoid\Utils\WTUtils; |
13 | |
14 | class 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 | } |