around the which includes 'c'.
// 2. Don't create the pseudo-section by setting '$currSection = null'
// But, this can leave some content outside any top-level section.
// 'c' will not be in any section.
// The code below implements strategy 1.
$nestedHighestSectionLevel = $this->wrapSectionsInDOM( $state, null, $node );
if ( $currSection && $nestedHighestSectionLevel <= $currSection['level'] ) {
$currSection['container']->setAttribute( 'data-mw-section-id', '-1' );
$currSection = $this->createNewSection( $state, $rootNode, $sectionStack,
$tplInfo, $currSection, $node, $nestedHighestSectionLevel, true );
$addedNode = true;
}
}
if ( $currSection && !$addedNode ) {
$currSection['container']->appendChild( $node );
}
if ( $tplInfo && $tplInfo['first'] === $node ) {
$tplInfo['firstSection'] = $currSection;
}
// Track exit from templated output
if ( $tplInfo && $tplInfo['last'] === $node ) {
// The opening $node and closing $node of the template
// are in different sections! This might require resolution.
if ( $currSection !== $tplInfo['firstSection'] ) {
$tplInfo['lastSection'] = $currSection;
$state['tplsAndExtsToExamine'][] = $tplInfo;
}
$tplInfo = null;
$state['inTemplate'] = false;
}
$node = $next;
}
// The last section embedded in a non-body DOM element
// should always be marked non-editable since it will have
// the closing tag (ex:
) showing up in the source editor
// which we cannot support in a visual editing $environment.
if ( $currSection && !DOMUtils::isBody( $rootNode ) ) {
$currSection['container']->setAttribute( 'data-mw-section-id', '-1' );
}
return $highestSectionLevel;
}
/**
* Get opening/closing DSR offset for the subtree rooted at $node.
* This handles scenarios where $node is a section or template wrapper
* and if a section, when it has leading/trailing non-element nodes
* that don't have recorded DSR values.
*
* @param array $tplInfo
* @param DOMElement $node
* @param bool $start
* @return int
*/
private function getDSR( array $tplInfo, DOMElement $node, bool $start ): int {
if ( $node->nodeName !== 'section' ) {
$nodeDsr = DOMDataUtils::getDataParsoid( $node )->dsr ?? null;
$tmplDsr = DOMDataUtils::getDataParsoid( $tplInfo['first'] )->dsr;
if ( $start ) {
return $nodeDsr->start ?? $tmplDsr->start;
} else {
return $nodeDsr->end ?? $tmplDsr->end;
}
}
$offset = 0;
$c = $start ? $node->firstChild : $node->lastChild;
while ( $c ) {
if ( !( $c instanceof DOMElement ) ) {
$offset += strlen( $c->textContent );
} else {
return $this->getDSR( $tplInfo, $c, $start ) + ( $start ? -$offset : $offset );
}
$c = $start ? $c->nextSibling : $c->previousSibling;
}
return -1;
}
/**
* Section wrappers and template/extension wrappers can conflict because
* of partial overlaps. This method identifies those conflicts and fixes up
* the template/extension encapsulation by expanding those ranges as necessary.
* This algorithm is not fully foolproof and there are known edge case bugs.
* See phabricator for these open bugs.
*
* @param array &$state
*/
private function resolveTplExtSectionConflicts( array &$state ) {
foreach ( $state['tplsAndExtsToExamine'] as $tplInfo ) {
// could be null
if ( isset( $tplInfo['firstSection'] ) &&
isset( $tplInfo['firstSection']['container'] )
) {
$s1 = $tplInfo['firstSection']['container'];
} else {
$s1 = null;
}
// guaranteed to be non-null
$s2 = $tplInfo['lastSection']['container'];
// Find a common ancestor of s1 and s2 (could be s1)
$s2Ancestors = DOMUtils::pathToRoot( $s2 );
$s1Ancestors = [];
$ancestor = null;
$i = 0;
if ( $s1 ) {
$ancestor = $s1;
while ( !in_array( $ancestor, $s2Ancestors, true ) ) {
$s1Ancestors[] = $ancestor;
$ancestor = $ancestor->parentNode;
}
// ancestor is now the common ancestor of s1 and s2
$s1Ancestors[] = $ancestor;
$i = array_search( $ancestor, $s2Ancestors, true );
}
if ( !$s1 || $ancestor === $s1 ) {
// Scenario 1: s1 is s2's ancestor OR s1 doesn't exist.
// In either case, s2 only covers part of the transcluded content.
// But, s2 could also include content that follows the transclusion.
// If so, append the content of the section after the last $node
// to data-mw.parts.
if ( $tplInfo['last']->nextSibling ) {
$newTplEndOffset = $this->getDSR( $tplInfo, $s2, false );
// The next line will succeed because it traverses non-tpl content
$tplDsr = &DOMDataUtils::getDataParsoid( $tplInfo['first'] )->dsr;
$tplEndOffset = $tplDsr->end;
$dmw = DOMDataUtils::getDataMw( $tplInfo['first'] );
if ( DOMUtils::hasTypeOf( $tplInfo['first'], 'mw:Transclusion' ) ) {
if ( $dmw->parts ) {
$dmw->parts[] = $this->getSrc( $state['frame'], $tplEndOffset, $newTplEndOffset );
}
} else { /* Extension */
// https://phabricator.wikimedia.org/T184779
$dmw->extSuffix = $this->getSrc( $state['frame'], $tplEndOffset, $newTplEndOffset );
}
// Update DSR
$tplDsr->end = $newTplEndOffset;
// Set about attributes on all children of s2 - add span wrappers if required
$span = null;
for ( $n = $tplInfo['last']->nextSibling; $n; $n = $n->nextSibling ) {
if ( $n instanceof DOMElement ) {
$n->setAttribute( 'about', $tplInfo['about'] );
$span = null;
} else {
if ( !$span ) {
$span = $state['doc']->createElement( 'span' );
$span->setAttribute( 'about', $tplInfo['about'] );
$n->parentNode->replaceChild( $span, $n );
}
$span->appendChild( $n );
$n = $span; // to ensure n->nextSibling is correct
}
}
}
} else {
// Scenario 2: s1 and s2 are in different subtrees
// Find children of the common ancestor that are on the
// path from s1 -> ancestor and s2 -> ancestor
Assert::invariant(
count( $s1Ancestors ) >= 2 && $i >= 1,
'Scenario assumptions violated.'
);
$newS1 = $s1Ancestors[count( $s1Ancestors ) - 2]; // length >= 2 since we know ancestors != s1
$newS2 = $s2Ancestors[$i - 1]; // i >= 1 since we know s2 is not s1's ancestor
$newAbout = $state['env']->newAboutId(); // new about id for the new wrapping layer
// Ensure that all children from newS1 and newS2 have about attrs set
for ( $n = $newS1; $n !== $newS2->nextSibling; $n = $n->nextSibling ) {
$n->setAttribute( 'about', $newAbout );
}
// $newS2 is $s2, or its ancestor
DOMUtils::assertElt( $s2 );
DOMUtils::assertElt( $newS2 );
// Update transclusion info
$dsr1 = $this->getDSR( $tplInfo, $newS1, true ); // Traverses non-tpl content => will succeed
$dsr2 = $this->getDSR( $tplInfo, $newS2, false ); // Traverses non-tpl content => will succeed
$tplDP = DOMDataUtils::getDataParsoid( $tplInfo['first'] );
$tplDsr = &$tplDP->dsr;
$dmw = Utils::clone( DOMDataUtils::getDataMw( $tplInfo['first'] ) );
if ( DOMUtils::hasTypeOf( $tplInfo['first'], 'mw:Transclusion' ) ) {
if ( $dmw->parts ) {
array_unshift( $dmw->parts, $this->getSrc( $state['frame'], $dsr1, $tplDsr->start ) );
$dmw->parts[] = $this->getSrc( $state['frame'], $tplDsr->end, $dsr2 );
}
DOMDataUtils::setDataMw( $newS1, $dmw );
DOMUtils::addTypeOf( $newS1, 'mw:Transclusion' );
// Copy the template's parts-information object
// which has white-space information for formatting
// the transclusion and eliminates dirty-diffs.
$dp = (object)[ 'pi' => $tplDP->pi, 'dsr' => new DomSourceRange( $dsr1, $dsr2, null, null ) ];
DOMDataUtils::setDataParsoid( $newS1, $dp );
} else { /* extension */
// https://phabricator.wikimedia.org/T184779
$dmw->extPrefix = $this->getSrc( $state['frame'], $dsr1, $tplDsr->start );
$dmw->extSuffix = $this->getSrc( $state['frame'], $tplDsr->end, $dsr2 );
DOMDataUtils::setDataMw( $newS1, $dmw );
$newS1->setAttribute( 'typeof', $tplInfo['first']->getAttribute( 'typeof' ) );
$dp = (object)[ 'dsr' => new DomSourceRange( $dsr1, $dsr2, null, null ) ];
DOMDataUtils::setDataParsoid( $newS1, $dp );
}
}
}
}
/**
* DOM Postprocessor entry function to walk DOM rooted at $root
* and add wrappers as necessary.
* Implements the algorithm documented @ mw:Parsing/Notes/Section_Wrapping
*
* @inheritDoc
*/
public function run(
Env $env, DOMElement $root, array $options = [], bool $atTopLevel = false
): void {
if ( !$env->getWrapSections() ) {
return;
}
$doc = $root->ownerDocument;
$leadSection = [
'container' => $doc->createElement( 'section' ),
'debug_id' => 0,
// lowest possible level since we don't want
// any nesting of h-tags in the lead section
'level' => 6,
'lead' => true
];
$leadSection['container']->setAttribute( 'data-mw-section-id', '0' );
// Global $state
$state = [
'env' => $env,
'frame' => $options['frame'],
'count' => 1,
'doc' => $doc,
'rootNode' => $root,
'sectionNumber' => 0,
'inTemplate' => false,
'tplsAndExtsToExamine' => []
];
$this->wrapSectionsInDOM( $state, $leadSection, $root );
// There will always be a lead section, even if sometimes it only
// contains whitespace + comments.
$root->insertBefore( $leadSection['container'], $root->firstChild );
// Resolve template conflicts after all sections have been added to the DOM
$this->resolveTplExtSectionConflicts( $state );
}
}