Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 99 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 1 |
HandleLinkNeighbours | |
0.00% |
0 / 99 |
|
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 / 57 |
|
0.00% |
0 / 1 |
756 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace Wikimedia\Parsoid\Wt2Html\DOM\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\DTState; |
13 | use Wikimedia\Parsoid\Utils\WTUtils; |
14 | |
15 | class 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 | } |