Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
0.00% |
0 / 6 |
CRAP | |
0.00% |
0 / 92 |
MarkFosteredContent | |
0.00% |
0 / 1 |
|
0.00% |
0 / 6 |
1332 | |
0.00% |
0 / 92 |
createNodeWithAttributes | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 3 |
|||
removeTransclusionShadows | |
0.00% |
0 / 1 |
42 | |
0.00% |
0 / 15 |
|||
insertTransclusionMetas | |
0.00% |
0 / 1 |
90 | |
0.00% |
0 / 28 |
|||
getFosterContentHolder | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 6 |
|||
processRecursively | |
0.00% |
0 / 1 |
306 | |
0.00% |
0 / 38 |
|||
run | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
<?php | |
declare( strict_types = 1 ); | |
namespace Wikimedia\Parsoid\Wt2Html\PP\Processors; | |
use Wikimedia\Assert\Assert; | |
use Wikimedia\Parsoid\Config\Env; | |
use Wikimedia\Parsoid\DOM\Comment; | |
use Wikimedia\Parsoid\DOM\Document; | |
use Wikimedia\Parsoid\DOM\Element; | |
use Wikimedia\Parsoid\DOM\Node; | |
use Wikimedia\Parsoid\DOM\Text; | |
use Wikimedia\Parsoid\NodeData\DataParsoid; | |
use Wikimedia\Parsoid\NodeData\TempData; | |
use Wikimedia\Parsoid\Utils\DOMCompat; | |
use Wikimedia\Parsoid\Utils\DOMDataUtils; | |
use Wikimedia\Parsoid\Utils\DOMUtils; | |
use Wikimedia\Parsoid\Utils\WTUtils; | |
use Wikimedia\Parsoid\Wt2Html\Wt2HtmlDOMProcessor; | |
/** | |
* Non-IEW (inter-element-whitespace) can only be found in <td> <th> and | |
* <caption> tags in a table. If found elsewhere within a table, such | |
* content will be moved out of the table and be "adopted" by the table's | |
* sibling ("foster parent"). The content that gets adopted is "fostered | |
* content". | |
* | |
* http://www.w3.org/TR/html5/syntax.html#foster-parent | |
* @module | |
*/ | |
class MarkFosteredContent implements Wt2HtmlDOMProcessor { | |
/** | |
* Create a new DOM node with attributes. | |
* | |
* @param Document $document | |
* @param string $type | |
* @param array $attrs | |
* @return Element | |
*/ | |
private static function createNodeWithAttributes( | |
Document $document, string $type, array $attrs | |
): Element { | |
$node = $document->createElement( $type ); | |
DOMUtils::addAttributes( $node, $attrs ); | |
return $node; | |
} | |
/** | |
* Cleans up transclusion shadows, keeping track of fostered transclusions | |
* | |
* @param Node $node | |
* @return bool | |
*/ | |
private static function removeTransclusionShadows( Node $node ): bool { | |
$sibling = null; | |
$fosteredTransclusions = false; | |
if ( $node instanceof Element ) { | |
if ( DOMUtils::isMarkerMeta( $node, 'mw:TransclusionShadow' ) ) { | |
$node->parentNode->removeChild( $node ); | |
return true; | |
} elseif ( DOMDataUtils::getDataParsoid( $node )->getTempFlag( TempData::IN_TRANSCLUSION ) ) { | |
$fosteredTransclusions = true; | |
} | |
$node = $node->firstChild; | |
while ( $node ) { | |
$sibling = $node->nextSibling; | |
if ( self::removeTransclusionShadows( $node ) ) { | |
$fosteredTransclusions = true; | |
} | |
$node = $sibling; | |
} | |
} | |
return $fosteredTransclusions; | |
} | |
/** | |
* Inserts metas around the fosterbox and table | |
* | |
* @param Env $env | |
* @param Node $fosterBox | |
* @param Element $table | |
*/ | |
private static function insertTransclusionMetas( | |
Env $env, Node $fosterBox, Element $table | |
): void { | |
$aboutId = $env->newAboutId(); | |
// You might be asking yourself, why is $table->dataParsoid->tsr->end always | |
// present? The earlier implementation searched the table's siblings for | |
// their tsr->start. However, encapsulation doesn't happen when the foster box, | |
// and thus the table, are in the transclusion. | |
$s = self::createNodeWithAttributes( $fosterBox->ownerDocument, 'meta', [ | |
'about' => $aboutId, | |
'id' => substr( $aboutId, 1 ), | |
'typeof' => 'mw:Transclusion', | |
] | |
); | |
$dp = new DataParsoid; | |
$dp->tsr = clone DOMDataUtils::getDataParsoid( $table )->tsr; | |
$dp->setTempFlag( TempData::FROM_FOSTER ); | |
DOMDataUtils::setDataParsoid( $s, $dp ); | |
$fosterBox->parentNode->insertBefore( $s, $fosterBox ); | |
$e = self::createNodeWithAttributes( $table->ownerDocument, 'meta', [ | |
'about' => $aboutId, | |
'typeof' => 'mw:Transclusion/End', | |
] | |
); | |
$sibling = $table->nextSibling; | |
$beforeText = null; | |
// Skip past the table end, mw:shadow and any transclusions that | |
// start inside the table. There may be newlines and comments in | |
// between so keep track of that, and backtrack when necessary. | |
while ( $sibling ) { | |
if ( !WTUtils::isTplStartMarkerMeta( $sibling ) && | |
( WTUtils::hasParsoidAboutId( $sibling ) || | |
DOMUtils::isMarkerMeta( $sibling, 'mw:TransclusionShadow' ) | |
) | |
) { | |
$sibling = $sibling->nextSibling; | |
$beforeText = null; | |
} elseif ( $sibling instanceof Comment || $sibling instanceof Text ) { | |
if ( !$beforeText ) { | |
$beforeText = $sibling; | |
} | |
$sibling = $sibling->nextSibling; | |
} else { | |
break; | |
} | |
} | |
$table->parentNode->insertBefore( $e, $beforeText ?: $sibling ); | |
} | |
/** | |
* @param Document $doc | |
* @param bool $inPTag | |
* @return Element | |
*/ | |
private static function getFosterContentHolder( Document $doc, bool $inPTag ): Element { | |
$fosterContentHolder = $doc->createElement( $inPTag ? 'span' : 'p' ); | |
$dp = new DataParsoid; | |
$dp->fostered = true; | |
// Set autoInsertedStart for bug-compatibility with the old ProcessTreeBuilderFixups code | |
$dp->autoInsertedStart = true; | |
DOMDataUtils::setDataParsoid( $fosterContentHolder, $dp ); | |
return $fosterContentHolder; | |
} | |
/** | |
* Searches for FosterBoxes and does two things when it hits one: | |
* - Marks all nextSiblings as fostered until the accompanying table. | |
* - Wraps the whole thing (table + fosterbox) with transclusion metas if | |
* there is any fostered transclusion content. | |
* | |
* @param Node $node | |
* @param Env $env | |
*/ | |
private static function processRecursively( Node $node, Env $env ): void { | |
$c = $node->firstChild; | |
while ( $c ) { | |
$sibling = $c->nextSibling; | |
$fosteredTransclusions = false; | |
if ( DOMUtils::hasNameAndTypeOf( $c, 'table', 'mw:FosterBox' ) ) { | |
$inPTag = DOMUtils::hasNameOrHasAncestorOfName( $c->parentNode, 'p' ); | |
$fosterContentHolder = self::getFosterContentHolder( $c->ownerDocument, $inPTag ); | |
// mark as fostered until we hit the table | |
while ( $sibling && | |
( !( $sibling instanceof Element ) || DOMCompat::nodeName( $sibling ) !== 'table' ) | |
) { | |
$next = $sibling->nextSibling; | |
if ( $sibling instanceof Element ) { | |
// TODO: Note the similarity here with the p-wrapping pass. | |
// This can likely be combined in some more maintainable way. | |
if ( | |
DOMUtils::isRemexBlockNode( $sibling ) || | |
WTUtils::emitsSolTransparentSingleLineWT( $sibling ) | |
) { | |
// Block nodes don't need to be wrapped in a p-tag either. | |
// Links, includeonly directives, and other rendering-transparent | |
// nodes dont need wrappers. sol-transparent wikitext generate | |
// rendering-transparent nodes and we use that helper as a proxy here. | |
DOMDataUtils::getDataParsoid( $sibling )->fostered = true; | |
// If the foster content holder is not empty, | |
// close it and get a new content holder. | |
if ( $fosterContentHolder->hasChildNodes() ) { | |
$sibling->parentNode->insertBefore( $fosterContentHolder, $sibling ); | |
$fosterContentHolder = self::getFosterContentHolder( $sibling->ownerDocument, $inPTag ); | |
} | |
} else { | |
$fosterContentHolder->appendChild( $sibling ); | |
} | |
if ( self::removeTransclusionShadows( $sibling ) ) { | |
$fosteredTransclusions = true; | |
} | |
} else { | |
$fosterContentHolder->appendChild( $sibling ); | |
} | |
$sibling = $next; | |
} | |
$table = $sibling; | |
// we should be able to reach the table from the fosterbox | |
Assert::invariant( | |
$table instanceof Element && DOMCompat::nodeName( $table ) === 'table', | |
"Table isn't a sibling. Something's amiss!" | |
); | |
if ( $fosterContentHolder->hasChildNodes() ) { | |
$table->parentNode->insertBefore( $fosterContentHolder, $table ); | |
} | |
// we have fostered transclusions | |
// wrap the whole thing in a transclusion | |
if ( $fosteredTransclusions ) { | |
self::insertTransclusionMetas( $env, $c, $table ); | |
} | |
// remove the foster box | |
$c->parentNode->removeChild( $c ); | |
} elseif ( DOMUtils::isMarkerMeta( $c, 'mw:TransclusionShadow' ) ) { | |
$c->parentNode->removeChild( $c ); | |
} elseif ( $c instanceof Element ) { | |
if ( $c->hasChildNodes() ) { | |
self::processRecursively( $c, $env ); | |
} | |
} | |
$c = $sibling; | |
} | |
} | |
/** | |
* @inheritDoc | |
*/ | |
public function run( | |
Env $env, Node $root, array $options = [], bool $atTopLevel = false | |
): void { | |
self::processRecursively( $root, $env ); | |
} | |
} |