Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 116 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
MarkFosteredContent | |
0.00% |
0 / 116 |
|
0.00% |
0 / 7 |
1980 | |
0.00% |
0 / 1 |
createNodeWithAttributes | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
removeTransclusionShadows | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
42 | |||
insertTransclusionMetas | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
90 | |||
moveFosteredAnnotations | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
56 | |||
getFosterContentHolder | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
processRecursively | |
0.00% |
0 / 46 |
|
0.00% |
0 / 1 |
342 | |||
run | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace Wikimedia\Parsoid\Wt2Html\DOM\Processors; |
5 | |
6 | use Wikimedia\Assert\Assert; |
7 | use Wikimedia\Parsoid\Config\Env; |
8 | use Wikimedia\Parsoid\DOM\Comment; |
9 | use Wikimedia\Parsoid\DOM\Document; |
10 | use Wikimedia\Parsoid\DOM\DocumentFragment; |
11 | use Wikimedia\Parsoid\DOM\Element; |
12 | use Wikimedia\Parsoid\DOM\Node; |
13 | use Wikimedia\Parsoid\DOM\Text; |
14 | use Wikimedia\Parsoid\NodeData\DataParsoid; |
15 | use Wikimedia\Parsoid\NodeData\TempData; |
16 | use Wikimedia\Parsoid\Utils\DOMCompat; |
17 | use Wikimedia\Parsoid\Utils\DOMDataUtils; |
18 | use Wikimedia\Parsoid\Utils\DOMUtils; |
19 | use Wikimedia\Parsoid\Utils\WTUtils; |
20 | use Wikimedia\Parsoid\Wt2Html\Wt2HtmlDOMProcessor; |
21 | |
22 | /** |
23 | * Non-IEW (inter-element-whitespace) can only be found in <td> <th> and |
24 | * <caption> tags in a table. If found elsewhere within a table, such |
25 | * content will be moved out of the table and be "adopted" by the table's |
26 | * sibling ("foster parent"). The content that gets adopted is "fostered |
27 | * content". |
28 | * |
29 | * http://www.w3.org/TR/html5/syntax.html#foster-parent |
30 | * @module |
31 | */ |
32 | class MarkFosteredContent implements Wt2HtmlDOMProcessor { |
33 | /** |
34 | * Create a new DOM node with attributes. |
35 | * |
36 | * @param Document $document |
37 | * @param string $type |
38 | * @param array $attrs |
39 | * @return Element |
40 | */ |
41 | private static function createNodeWithAttributes( |
42 | Document $document, string $type, array $attrs |
43 | ): Element { |
44 | $node = $document->createElement( $type ); |
45 | DOMUtils::addAttributes( $node, $attrs ); |
46 | return $node; |
47 | } |
48 | |
49 | /** |
50 | * Cleans up transclusion shadows, keeping track of fostered transclusions |
51 | * |
52 | * @param Node $node |
53 | * @return bool |
54 | */ |
55 | private static function removeTransclusionShadows( Node $node ): bool { |
56 | $sibling = null; |
57 | $fosteredTransclusions = false; |
58 | if ( $node instanceof Element ) { |
59 | if ( DOMUtils::isMarkerMeta( $node, 'mw:TransclusionShadow' ) ) { |
60 | $node->parentNode->removeChild( $node ); |
61 | return true; |
62 | } elseif ( DOMDataUtils::getDataParsoid( $node )->getTempFlag( TempData::IN_TRANSCLUSION ) ) { |
63 | $fosteredTransclusions = true; |
64 | } |
65 | $node = $node->firstChild; |
66 | while ( $node ) { |
67 | $sibling = $node->nextSibling; |
68 | if ( self::removeTransclusionShadows( $node ) ) { |
69 | $fosteredTransclusions = true; |
70 | } |
71 | $node = $sibling; |
72 | } |
73 | } |
74 | return $fosteredTransclusions; |
75 | } |
76 | |
77 | /** |
78 | * Inserts metas around the fosterbox and table |
79 | * |
80 | * @param Env $env |
81 | * @param Node $fosterBox |
82 | * @param Element $table |
83 | */ |
84 | private static function insertTransclusionMetas( |
85 | Env $env, Node $fosterBox, Element $table |
86 | ): void { |
87 | $aboutId = $env->newAboutId(); |
88 | |
89 | // Ensure we have depth entries for 'aboutId'. |
90 | $docDataBag = DOMDataUtils::getBag( $table->ownerDocument ); |
91 | $docDataBag->transclusionMetaTagDepthMap[$aboutId]['start'] = |
92 | $docDataBag->transclusionMetaTagDepthMap[$aboutId]['end'] = |
93 | DOMUtils::nodeDepth( $table ); |
94 | |
95 | // You might be asking yourself, why is $table->dataParsoid->tsr->end always |
96 | // present? The earlier implementation searched the table's siblings for |
97 | // their tsr->start. However, encapsulation doesn't happen when the foster box, |
98 | // and thus the table, are in the transclusion. |
99 | $s = self::createNodeWithAttributes( $fosterBox->ownerDocument, 'meta', [ |
100 | 'about' => $aboutId, |
101 | 'id' => substr( $aboutId, 1 ), |
102 | 'typeof' => 'mw:Transclusion', |
103 | ] |
104 | ); |
105 | $dp = new DataParsoid; |
106 | $dp->tsr = clone DOMDataUtils::getDataParsoid( $table )->tsr; |
107 | $dp->setTempFlag( TempData::FROM_FOSTER ); |
108 | DOMDataUtils::setDataParsoid( $s, $dp ); |
109 | $fosterBox->parentNode->insertBefore( $s, $fosterBox ); |
110 | |
111 | $e = self::createNodeWithAttributes( $table->ownerDocument, 'meta', [ |
112 | 'about' => $aboutId, |
113 | 'typeof' => 'mw:Transclusion/End', |
114 | ] |
115 | ); |
116 | |
117 | $sibling = $table->nextSibling; |
118 | $beforeText = null; |
119 | |
120 | // Skip past the table end, mw:shadow and any transclusions that |
121 | // start inside the table. There may be newlines and comments in |
122 | // between so keep track of that, and backtrack when necessary. |
123 | while ( $sibling ) { |
124 | if ( !WTUtils::isTplStartMarkerMeta( $sibling ) && |
125 | ( WTUtils::isEncapsulatedDOMForestRoot( $sibling ) || |
126 | DOMUtils::isMarkerMeta( $sibling, 'mw:TransclusionShadow' ) |
127 | ) |
128 | ) { |
129 | $sibling = $sibling->nextSibling; |
130 | $beforeText = null; |
131 | } elseif ( $sibling instanceof Comment || $sibling instanceof Text ) { |
132 | if ( !$beforeText ) { |
133 | $beforeText = $sibling; |
134 | } |
135 | $sibling = $sibling->nextSibling; |
136 | } else { |
137 | break; |
138 | } |
139 | } |
140 | |
141 | $table->parentNode->insertBefore( $e, $beforeText ?: $sibling ); |
142 | } |
143 | |
144 | /** |
145 | * @param Node $e |
146 | * @param Node $firstFosteredNode |
147 | * @param Element|DocumentFragment $tableParent |
148 | * @param ?Node $tableNextSibling |
149 | */ |
150 | private static function moveFosteredAnnotations( |
151 | Node $e, Node $firstFosteredNode, $tableParent, ?Node $tableNextSibling |
152 | ): void { |
153 | if ( WTUtils::isAnnotationStartMarkerMeta( $e ) && $e !== $firstFosteredNode ) { |
154 | '@phan-var Element $e'; |
155 | DOMDataUtils::getDataParsoid( $e )->wasMoved = true; |
156 | $firstFosteredNode->parentNode->insertBefore( $e, $firstFosteredNode ); |
157 | } elseif ( WTUtils::isAnnotationEndMarkerMeta( $e ) ) { |
158 | '@phan-var Element $e'; |
159 | DOMDataUtils::getDataParsoid( $e )->wasMoved = true; |
160 | $tableParent->insertBefore( $e, $tableNextSibling ); |
161 | } elseif ( $e instanceof Element && $e->hasChildNodes() ) { |
162 | // avoid iterating over a mutated DOMNodeList |
163 | $childNodeList = iterator_to_array( $e->childNodes ); |
164 | foreach ( $childNodeList as $child ) { |
165 | self::moveFosteredAnnotations( $child, $firstFosteredNode, $tableParent, $tableNextSibling ); |
166 | } |
167 | } |
168 | } |
169 | |
170 | private static function getFosterContentHolder( Document $doc, bool $inPTag ): Element { |
171 | $fosterContentHolder = $doc->createElement( $inPTag ? 'span' : 'p' ); |
172 | $dp = new DataParsoid; |
173 | $dp->fostered = true; |
174 | // Set autoInsertedStart for bug-compatibility with the old ProcessTreeBuilderFixups code |
175 | $dp->autoInsertedStart = true; |
176 | |
177 | DOMDataUtils::setDataParsoid( $fosterContentHolder, $dp ); |
178 | return $fosterContentHolder; |
179 | } |
180 | |
181 | /** |
182 | * Searches for FosterBoxes and does two things when it hits one: |
183 | * - Marks all nextSiblings as fostered until the accompanying table. |
184 | * - Wraps the whole thing (table + fosterbox) with transclusion metas if |
185 | * there is any fostered transclusion content. |
186 | * |
187 | * @param Node $node |
188 | * @param Env $env |
189 | */ |
190 | private static function processRecursively( Node $node, Env $env ): void { |
191 | $c = $node->firstChild; |
192 | |
193 | while ( $c ) { |
194 | $sibling = $c->nextSibling; |
195 | $fosteredTransclusions = false; |
196 | |
197 | if ( DOMUtils::hasNameAndTypeOf( $c, 'table', 'mw:FosterBox' ) ) { |
198 | $inPTag = DOMUtils::hasNameOrHasAncestorOfName( $c->parentNode, 'p' ); |
199 | $fosterContentHolder = self::getFosterContentHolder( $c->ownerDocument, $inPTag ); |
200 | |
201 | $fosteredElements = []; |
202 | // mark as fostered until we hit the table |
203 | while ( $sibling && |
204 | ( !( $sibling instanceof Element ) || DOMCompat::nodeName( $sibling ) !== 'table' ) |
205 | ) { |
206 | $fosteredElements[] = $sibling; |
207 | $next = $sibling->nextSibling; |
208 | if ( $sibling instanceof Element ) { |
209 | // TODO: Note the similarity here with the p-wrapping pass. |
210 | // This can likely be combined in some more maintainable way. |
211 | if ( |
212 | DOMUtils::isRemexBlockNode( $sibling ) || |
213 | PWrap::pWrapOptional( $sibling ) |
214 | ) { |
215 | // Block nodes don't need to be wrapped in a p-tag either. |
216 | // Links, includeonly directives, and other rendering-transparent |
217 | // nodes dont need wrappers. sol-transparent wikitext generate |
218 | // rendering-transparent nodes and we use that helper as a proxy here. |
219 | DOMDataUtils::getDataParsoid( $sibling )->fostered = true; |
220 | // If the foster content holder is not empty, |
221 | // close it and get a new content holder. |
222 | if ( $fosterContentHolder->hasChildNodes() ) { |
223 | $sibling->parentNode->insertBefore( $fosterContentHolder, $sibling ); |
224 | $fosterContentHolder = self::getFosterContentHolder( $sibling->ownerDocument, $inPTag ); |
225 | } |
226 | } else { |
227 | $fosterContentHolder->appendChild( $sibling ); |
228 | } |
229 | |
230 | if ( self::removeTransclusionShadows( $sibling ) ) { |
231 | $fosteredTransclusions = true; |
232 | } |
233 | } else { |
234 | $fosterContentHolder->appendChild( $sibling ); |
235 | } |
236 | $sibling = $next; |
237 | } |
238 | |
239 | $table = $sibling; |
240 | |
241 | // we should be able to reach the table from the fosterbox |
242 | Assert::invariant( |
243 | $table instanceof Element && DOMCompat::nodeName( $table ) === 'table', |
244 | "Table isn't a sibling. Something's amiss!" |
245 | ); |
246 | |
247 | if ( $fosterContentHolder->hasChildNodes() ) { |
248 | $table->parentNode->insertBefore( $fosterContentHolder, $table ); |
249 | } |
250 | |
251 | // we have fostered transclusions |
252 | // wrap the whole thing in a transclusion |
253 | if ( $fosteredTransclusions ) { |
254 | self::insertTransclusionMetas( $env, $c, $table ); |
255 | } |
256 | |
257 | // We have two possibilities here for the insertion of more than one meta tag after the table. |
258 | // We can either keep them in the order of traversal (by keeping a reference to the initial |
259 | // $table->nextSibling), or in reverse order of traversal (by updating $table->nextSibling to |
260 | // the inserted meta. |
261 | // This has different consequences depending on whether multiple ranges are nested or not. |
262 | // If the fosterbox initially contains <ann1><ann2></ann2></ann1>, the end result for the first |
263 | // possibility becomes <ann1><ann2>TABLE</ann2></ann1>. If the fosterbox initially contains |
264 | // <ann1></ann1><ann2></ann2>, the end result becomes <ann1><ann2>TABLE</ann1></ann2>. The |
265 | // consequences are inverted if we insert in reverse order of traversal. |
266 | // Note that this is only relevant if the annotations are of different types and that, right |
267 | // now, we only have two types of annotation (namely <translate> and <tvar>), and <tvar> can |
268 | // only exist nested in <translate>. Hence, we choose to insert in traversal order so that we can |
269 | // preserve existing nesting order. |
270 | // (The last option would be to keep a stack of opening metas in the foster table and to re-add |
271 | // them in inverse order at the end of the table. This would add significant code complexity for |
272 | // what seems like marginal benefits at best as long as we do not have more annotation types.) |
273 | $tableNextSibling = $table->nextSibling; |
274 | $tableParent = $table->parentNode; |
275 | // this needs to happen after inserting the transclusion meta so that they get |
276 | // included in the transclusion |
277 | foreach ( $fosteredElements as $elem ) { |
278 | '@phan-var Element $elem'; |
279 | self::moveFosteredAnnotations( |
280 | $elem, $fosteredElements[0], $tableParent, $tableNextSibling |
281 | ); |
282 | } |
283 | |
284 | // remove the foster box |
285 | $c->parentNode->removeChild( $c ); |
286 | |
287 | } elseif ( DOMUtils::isMarkerMeta( $c, 'mw:TransclusionShadow' ) ) { |
288 | $c->parentNode->removeChild( $c ); |
289 | } elseif ( $c instanceof Element ) { |
290 | if ( $c->hasChildNodes() ) { |
291 | self::processRecursively( $c, $env ); |
292 | } |
293 | } |
294 | |
295 | $c = $sibling; |
296 | } |
297 | } |
298 | |
299 | /** |
300 | * @inheritDoc |
301 | */ |
302 | public function run( |
303 | Env $env, Node $root, array $options = [], bool $atTopLevel = false |
304 | ): void { |
305 | self::processRecursively( $root, $env ); |
306 | } |
307 | } |