Page MenuHomeWickedGov Phorge

References.php
No OneTemporary

Size
29 KB
Referenced Files
None
Subscribers
None

References.php

<?php
declare( strict_types = 1 );
// phpcs:disable MediaWiki.WhiteSpace.SpaceBeforeSingleLineComment.NewLineComment
// @phan-file-suppress MediaWikiNoEmptyIfDefined Phan thinks the @property docs in the DataParsoid
// class are a guarantee, but they aren't
namespace Cite\Parsoid;
use Closure;
use MediaWiki\Config\Config;
use stdClass;
use Wikimedia\Parsoid\Core\DomSourceRange;
use Wikimedia\Parsoid\DOM\DocumentFragment;
use Wikimedia\Parsoid\DOM\Element;
use Wikimedia\Parsoid\DOM\Node;
use Wikimedia\Parsoid\Ext\DOMDataUtils;
use Wikimedia\Parsoid\Ext\DOMUtils;
use Wikimedia\Parsoid\Ext\ExtensionTagHandler;
use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI;
use Wikimedia\Parsoid\Ext\PHPUtils;
use Wikimedia\Parsoid\Ext\WTUtils;
use Wikimedia\Parsoid\NodeData\DataMw;
use Wikimedia\Parsoid\NodeData\DataMwError;
use Wikimedia\Parsoid\NodeData\DataParsoid;
use Wikimedia\Parsoid\Utils\DOMCompat;
/**
* @license GPL-2.0-or-later
*/
class References extends ExtensionTagHandler {
private Config $mainConfig;
public function __construct( Config $mainConfig ) {
$this->mainConfig = $mainConfig;
}
private static function hasRef( Node $node ): bool {
$c = $node->firstChild;
while ( $c ) {
if ( $c instanceof Element ) {
if ( WTUtils::isSealedFragmentOfType( $c, 'ref' ) ) {
return true;
}
if ( self::hasRef( $c ) ) {
return true;
}
}
$c = $c->nextSibling;
}
return false;
}
private function createReferences(
ParsoidExtensionAPI $extApi, DocumentFragment $domFragment,
array $refsOpts, ?callable $modifyDp, bool $autoGenerated = false
): Element {
$doc = $domFragment->ownerDocument;
$ol = $doc->createElement( 'ol' );
DOMCompat::getClassList( $ol )->add( 'mw-references', 'references' );
DOMUtils::migrateChildren( $domFragment, $ol );
// Support the `responsive` parameter
if ( $refsOpts['responsive'] !== null ) {
$responsiveWrap = $refsOpts['responsive'] !== '0';
} else {
$responsiveWrap = (bool)$this->mainConfig->get( 'CiteResponsiveReferences' );
}
if ( $responsiveWrap ) {
$div = $doc->createElement( 'div' );
DOMCompat::getClassList( $div )->add( 'mw-references-wrap' );
$div->appendChild( $ol );
$frag = $div;
} else {
$frag = $ol;
}
if ( $autoGenerated ) {
// FIXME: This is very much trying to copy ExtensionHandler::onDocument
DOMUtils::addAttributes( $frag, [
'typeof' => 'mw:Extension/references',
'about' => $extApi->newAboutId()
] );
$dataMw = new DataMw( [
'name' => 'references',
'attrs' => new stdClass
] );
// Dont emit empty keys
if ( $refsOpts['group'] ) {
$dataMw->attrs->group = $refsOpts['group'];
}
DOMDataUtils::setDataMw( $frag, $dataMw );
}
$dp = DOMDataUtils::getDataParsoid( $frag );
if ( $refsOpts['group'] ) { // No group for the empty string either
$dp->group = $refsOpts['group'];
$ol->setAttribute( 'data-mw-group', $refsOpts['group'] );
}
if ( $modifyDp ) {
$modifyDp( $dp );
}
// These module namess are copied from Cite extension.
// They are hardcoded there as well.
$metadata = $extApi->getMetadata();
$metadata->addModules( [ 'ext.cite.ux-enhancements' ] );
$metadata->addModuleStyles( [ 'ext.cite.parsoid.styles', 'ext.cite.styles' ] );
return $frag;
}
private function extractRefFromNode(
ParsoidExtensionAPI $extApi, Element $node, ReferencesData $refsData
): void {
$doc = $node->ownerDocument;
$errs = [];
// This is data-parsoid from the dom fragment node that's gone through
// dsr computation and template wrapping.
$nodeDp = DOMDataUtils::getDataParsoid( $node );
$isTplWrapper = DOMUtils::hasTypeOf( $node, 'mw:Transclusion' );
$contentId = $nodeDp->html;
$tplDmw = $isTplWrapper ? DOMDataUtils::getDataMw( $node ) : null;
// This is the <sup> that's the meat of the sealed fragment
$c = $extApi->getContentDOM( $contentId )->firstChild;
DOMUtils::assertElt( $c );
$cDp = DOMDataUtils::getDataParsoid( $c );
$refDmw = DOMDataUtils::getDataMw( $c );
// Use the about attribute on the wrapper with priority, since it's
// only added when the wrapper is a template sibling.
$about = DOMCompat::getAttribute( $node, 'about' ) ??
DOMCompat::getAttribute( $c, 'about' );
'@phan-var string $about'; // assert that $about is non-null
// FIXME(SSS): Need to clarify semantics here.
// If both the containing <references> elt as well as the nested <ref>
// elt has a group attribute, what takes precedence?
$groupName = $refDmw->attrs->group ?? $refsData->referencesGroup;
$group = $refsData->getRefGroup( $groupName );
if (
$refsData->inReferencesContent() &&
$groupName !== $refsData->referencesGroup
) {
$errs[] = new DataMwError(
'cite_error_references_group_mismatch',
[ $refDmw->attrs->group ]
);
}
// NOTE: This will have been trimmed in Utils::getExtArgInfo()'s call
// to TokenUtils::kvToHash() and ExtensionHandler::normalizeExtOptions()
$refName = $refDmw->attrs->name ?? '';
$followName = $refDmw->attrs->follow ?? '';
$refDir = strtolower( $refDmw->attrs->dir ?? '' );
// Add ref-index linkback
$linkBack = $doc->createElement( 'sup' );
$ref = null;
$hasRefName = strlen( $refName ) > 0;
$hasFollow = strlen( $followName ) > 0;
$validFollow = false;
if ( $hasFollow ) {
// Always wrap follows content so that there's no ambiguity
// where to find it when roundtripping
$span = $doc->createElement( 'span' );
DOMUtils::addTypeOf( $span, 'mw:Cite/Follow' );
$span->setAttribute( 'about', $about );
$span->appendChild(
$doc->createTextNode( ' ' )
);
DOMUtils::migrateChildren( $c, $span );
$c->appendChild( $span );
}
$html = '';
$contentDiffers = false;
if ( $hasRefName ) {
if ( $hasFollow ) {
// Presumably, "name" has higher precedence
$errs[] = new DataMwError( 'cite_error_ref_follow_conflicts' );
}
if ( isset( $group->indexByName[$refName] ) ) {
$ref = $group->indexByName[$refName];
// If there are multiple <ref>s with the same name, but different content,
// the content of the first <ref> shows up in the <references> section.
// in order to ensure lossless RT-ing for later <refs>, we have to record
// HTML inline for all of them.
if ( $ref->contentId ) {
if ( $ref->cachedHtml === null ) {
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
$refContent = $extApi->getContentDOM( $ref->contentId )->firstChild;
$ref->cachedHtml = $extApi->domToHtml( $refContent, true, false );
}
// See the test, "Forward-referenced ref with magical follow edge case"
// Ideally, we should strip the mw:Cite/Follow wrappers before comparing
// But, we are going to ignore this edge case as not worth the complexity.
$html = $extApi->domToHtml( $c, true, false );
$contentDiffers = ( $html !== $ref->cachedHtml );
}
} else {
if ( $refsData->inReferencesContent() ) {
$errs[] = new DataMwError(
'cite_error_references_missing_key',
[ $refDmw->attrs->name ]
);
}
}
} else {
if ( $hasFollow ) {
// This is a follows ref, so check that a named ref has already
// been defined
if ( isset( $group->indexByName[$followName] ) ) {
$validFollow = true;
$ref = $group->indexByName[$followName];
} else {
// FIXME: This key isn't exactly appropriate since this
// is more general than just being in a <references>
// section and it's the $followName we care about, but the
// extension to the legacy parser doesn't have an
// equivalent key and just outputs something wacky.
$errs[] = new DataMwError(
'cite_error_references_missing_key',
[ $refDmw->attrs->follow ]
);
}
} elseif ( $refsData->inReferencesContent() ) {
$errs[] = new DataMwError( 'cite_error_references_no_key' );
}
}
// Process nested ref-in-ref
//
// Do this before possibly adding the a ref below or
// migrating contents out of $c if we have a valid follow
if ( empty( $cDp->empty ) && self::hasRef( $c ) ) {
if ( $contentDiffers ) {
$refsData->pushEmbeddedContentFlag();
}
$this->processRefs( $extApi, $refsData, $c );
if ( $contentDiffers ) {
$refsData->popEmbeddedContentFlag();
// If we have refs and the content differs, we need to
// reserialize now that we processed the refs. Unfortunately,
// the cachedHtml we compared against already had its refs
// processed so that would presumably never match and this will
// always be considered a redefinition. The implementation for
// the legacy parser also considers this a redefinition so
// there is likely little content out there like this :)
$html = $extApi->domToHtml( $c, true, true );
}
}
if ( $validFollow ) {
// Migrate content from the follow to the ref
if ( $ref->contentId ) {
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
$refContent = $extApi->getContentDOM( $ref->contentId )->firstChild;
DOMUtils::migrateChildren( $c, $refContent );
} else {
// Otherwise, we have a follow that comes after a named
// ref without content so use the follow fragment as
// the content
// This will be set below with `$ref->contentId = $contentId;`
}
} else {
// If we have !$ref, one might have been added in the call to
// processRefs, ie. a self-referential ref. We could try to look
// it up again, but Parsoid is choosing not to support that.
// Even worse would be if it tried to redefine itself!
if ( !$ref ) {
$ref = $refsData->add( $extApi, $groupName, $refName, $refDir );
}
// Handle linkbacks
if ( $refsData->inEmbeddedContent() ) {
$ref->embeddedNodes[] = $about;
} else {
$ref->nodes[] = $linkBack;
$ref->linkbacks[] = $ref->key . '-' . count( $ref->linkbacks );
}
}
if ( isset( $refDmw->attrs->dir ) ) {
if ( $refDir !== 'rtl' && $refDir !== 'ltr' ) {
$errs[] = new DataMwError( 'cite_error_ref_invalid_dir', [ $refDir ] );
} elseif ( $ref->dir !== '' && $ref->dir !== $refDir ) {
$errs[] = new DataMwError( 'cite_error_ref_conflicting_dir', [ $ref->name ] );
}
}
// FIXME: At some point this error message can be changed to a warning, as Parsoid Cite now
// supports numerals as a name without it being an actual error, but core Cite does not.
// Follow refs do not duplicate the error which can be correlated with the original ref.
if ( ctype_digit( $refName ) ) {
$errs[] = new DataMwError( 'cite_error_ref_numeric_key' );
}
// Check for missing content, added ?? '' to fix T259676 crasher
// FIXME: See T260082 for a more complete description of cause and deeper fix
$missingContent = ( !empty( $cDp->empty ) || trim( $refDmw->body->extsrc ?? '' ) === '' );
if ( $missingContent ) {
// Check for missing name and content to generate error code
//
// In references content, refs should be used for definition so missing content
// is an error. It's possible that no name is present (!hasRefName), which also
// gets the error "cite_error_references_no_key" above, so protect against that.
if ( $refsData->inReferencesContent() ) {
$errs[] = new DataMwError(
'cite_error_empty_references_define',
[ $refDmw->attrs->name ?? '', $refDmw->attrs->group ?? '' ]
);
} elseif ( !$hasRefName ) {
if ( !empty( $cDp->selfClose ) ) {
$errs[] = new DataMwError( 'cite_error_ref_no_key' );
} else {
$errs[] = new DataMwError( 'cite_error_ref_no_input' );
}
}
if ( !empty( $cDp->selfClose ) ) {
unset( $refDmw->body );
} else {
// Empty the <sup> since we've serialized its children and
// removing it below asserts everything has been migrated out
DOMCompat::replaceChildren( $c );
$refDmw->body = (object)[ 'html' => $refDmw->body->extsrc ?? '' ];
}
} else {
if ( $ref->contentId && !$validFollow ) {
// Empty the <sup> since we've serialized its children and
// removing it below asserts everything has been migrated out
DOMCompat::replaceChildren( $c );
}
if ( $contentDiffers ) {
// TODO: Since this error is being placed on the ref, the
// key should arguably be "cite_error_ref_duplicate_key"
$errs[] = new DataMwError(
'cite_error_references_duplicate_key',
[ $refDmw->attrs->name ]
);
$refDmw->body = (object)[ 'html' => $html ];
} else {
$refDmw->body = (object)[ 'id' => 'mw-reference-text-' . $ref->target ];
}
}
$class = 'mw-ref reference';
if ( $validFollow ) {
$class .= ' mw-ref-follow';
}
$lastLinkback = $ref->linkbacks[count( $ref->linkbacks ) - 1] ?? null;
DOMUtils::addAttributes( $linkBack, [
'about' => $about,
'class' => $class,
'id' => ( $refsData->inEmbeddedContent() || $validFollow ) ?
null : ( $ref->name ? $lastLinkback : $ref->id ),
'rel' => 'dc:references',
'typeof' => DOMCompat::getAttribute( $node, 'typeof' ),
]
);
DOMUtils::removeTypeOf( $linkBack, 'mw:DOMFragment/sealed/ref' );
DOMUtils::addTypeOf( $linkBack, 'mw:Extension/ref' );
$dataParsoid = new DataParsoid;
if ( isset( $nodeDp->src ) ) {
$dataParsoid->src = $nodeDp->src;
}
if ( isset( $nodeDp->dsr ) ) {
$dataParsoid->dsr = $nodeDp->dsr;
}
if ( isset( $nodeDp->pi ) ) {
$dataParsoid->pi = $nodeDp->pi;
}
DOMDataUtils::setDataParsoid( $linkBack, $dataParsoid );
$dmw = $isTplWrapper ? $tplDmw : $refDmw;
DOMDataUtils::setDataMw( $linkBack, $dmw );
// FIXME(T214241): Should the errors be added to data-mw if
// $isTplWrapper? Here and other calls to addErrorsToNode.
if ( $errs ) {
self::addErrorsToNode( $linkBack, $errs );
}
// refLink is the link to the citation
$refLink = $doc->createElement( 'a' );
DOMUtils::addAttributes( $refLink, [
'href' => $extApi->getPageUri() . '#' . $ref->target,
'style' => 'counter-reset: mw-Ref ' . $ref->groupIndex . ';',
] );
if ( $ref->group ) {
$refLink->setAttribute( 'data-mw-group', $ref->group );
}
// refLink-span which will contain a default rendering of the cite link
// for browsers that don't support counters
$refLinkSpan = $doc->createElement( 'span' );
$refLinkSpan->setAttribute( 'class', 'mw-reflink-text' );
$refLinkSpan->appendChild( $doc->createTextNode(
'[' . ( $ref->group ? $ref->group . ' ' : '' ) . $ref->groupIndex . ']'
) );
$refLink->appendChild( $refLinkSpan );
$linkBack->appendChild( $refLink );
// Checking if the <ref> is nested in a link
$aParent = DOMUtils::findAncestorOfName( $node, 'a' );
if ( $aParent !== null ) {
// If we find a parent link, we hoist the reference up, just after the link
// But if there's multiple references in a single link, we want to insert in order -
// so we look for other misnested references before inserting
$insertionPoint = $aParent->nextSibling;
while ( $insertionPoint instanceof Element &&
DOMCompat::nodeName( $insertionPoint ) === 'sup' &&
!empty( DOMDataUtils::getDataParsoid( $insertionPoint )->misnested )
) {
$insertionPoint = $insertionPoint->nextSibling;
}
$aParent->parentNode->insertBefore( $linkBack, $insertionPoint );
// set misnested to true and DSR to zero-sized to avoid round-tripping issues
$dsrOffset = DOMDataUtils::getDataParsoid( $aParent )->dsr->end ?? null;
// we created that node hierarchy above, so we know that it only contains these nodes,
// hence there's no need for a visitor
self::setMisnested( $linkBack, $dsrOffset );
self::setMisnested( $refLink, $dsrOffset );
self::setMisnested( $refLinkSpan, $dsrOffset );
$parentAbout = DOMCompat::getAttribute( $aParent, 'about' );
if ( $parentAbout !== null ) {
$linkBack->setAttribute( 'about', $parentAbout );
}
$node->parentNode->removeChild( $node );
} else {
// if not, we insert it where we planned in the first place
$node->parentNode->replaceChild( $linkBack, $node );
}
// Keep the first content to compare multiple <ref>s with the same name.
if ( $ref->contentId === null && !$missingContent ) {
$ref->contentId = $contentId;
// Use the dir parameter only from the full definition of a named ref tag
$ref->dir = $refDir;
} else {
DOMCompat::remove( $c );
$extApi->clearContentDOM( $contentId );
}
}
/**
* Sets a node as misnested and its DSR as zero-width.
*/
private static function setMisnested( Element $node, ?int $offset ) {
$dataParsoid = DOMDataUtils::getDataParsoid( $node );
$dataParsoid->misnested = true;
$dataParsoid->dsr = new DomSourceRange( $offset, $offset, null, null );
}
/**
* @param Element $node
* @param list<DataMwError> $errs
*/
private static function addErrorsToNode( Element $node, array $errs ): void {
DOMUtils::addTypeOf( $node, 'mw:Error' );
$dmw = DOMDataUtils::getDataMw( $node );
$dmw->errors = is_array( $dmw->errors ?? null ) ?
array_merge( $dmw->errors, $errs ) : $errs;
}
private function insertReferencesIntoDOM(
ParsoidExtensionAPI $extApi, Element $refsNode,
ReferencesData $refsData, bool $autoGenerated = false
): void {
$isTplWrapper = DOMUtils::hasTypeOf( $refsNode, 'mw:Transclusion' );
$dp = DOMDataUtils::getDataParsoid( $refsNode );
$group = $dp->group ?? '';
$refGroup = $refsData->getRefGroup( $group );
// Iterate through the ref list to back-patch typeof and data-mw error
// information into ref for errors only known at time of references
// insertion. Refs in the top level dom will be processed immediately,
// whereas embedded refs will be gathered for batch processing, since
// we need to parse embedded content to find them.
if ( $refGroup ) {
$autoGeneratedWithGroup = ( $autoGenerated && $group !== '' );
foreach ( $refGroup->refs as $ref ) {
$errs = [];
// Mark all refs that are part of a group that is autogenerated
if ( $autoGeneratedWithGroup ) {
$errs[] = new DataMwError(
'cite_error_group_refs_without_references',
[ $group ]
);
}
// Mark all refs that are named without content
if ( ( $ref->name !== '' ) && $ref->contentId === null ) {
// TODO: Since this error is being placed on the ref,
// the key should arguably be "cite_error_ref_no_text"
$errs[] = new DataMwError(
'cite_error_references_no_text',
[ $ref->name ]
);
}
if ( $errs ) {
foreach ( $ref->nodes as $node ) {
self::addErrorsToNode( $node, $errs );
}
foreach ( $ref->embeddedNodes as $about ) {
$refsData->embeddedErrors[$about] = $errs;
}
}
}
}
// Note that `$sup`s here are probably all we really need to check for
// errors caught with `$refsData->inReferencesContent()` but it's
// probably easier to just know that state while they're being
// constructed.
$nestedRefsHTML = array_map(
static function ( Element $sup ) use ( $extApi ) {
return $extApi->domToHtml( $sup, false, true ) . "\n";
},
PHPUtils::iterable_to_array( DOMCompat::querySelectorAll(
$refsNode, 'sup[typeof~=\'mw:Extension/ref\']'
) )
);
if ( !$isTplWrapper ) {
$dataMw = DOMDataUtils::getDataMw( $refsNode );
// Mark this auto-generated so that we can skip this during
// html -> wt and so that clients can strip it if necessary.
if ( $autoGenerated ) {
$dataMw->autoGenerated = true;
} elseif ( $nestedRefsHTML ) {
$dataMw->body = (object)[ 'html' => "\n" . implode( $nestedRefsHTML ) ];
} elseif ( empty( $dp->selfClose ) ) {
$dataMw->body = (object)[ 'html' => '' ];
} else {
unset( $dataMw->body );
}
unset( $dp->selfClose );
}
// Deal with responsive wrapper
if ( DOMUtils::hasClass( $refsNode, 'mw-references-wrap' ) ) {
// NOTE: The default Cite implementation hardcodes this threshold to 10.
// We use a configurable parameter here primarily for test coverage purposes.
// See citeParserTests.txt where we set a threshold of 1 or 2.
$rrThreshold = $this->mainConfig->get( 'CiteResponsiveReferencesThreshold' ) ?? 10;
if ( $refGroup && count( $refGroup->refs ) > $rrThreshold ) {
DOMCompat::getClassList( $refsNode )->add( 'mw-references-columns' );
}
$refsNode = $refsNode->firstChild;
}
DOMUtils::assertElt( $refsNode );
// Remove all children from the references node
//
// Ex: When {{Reflist}} is reused from the cache, it comes with
// a bunch of references as well. We have to remove all those cached
// references before generating fresh references.
DOMCompat::replaceChildren( $refsNode );
if ( $refGroup ) {
foreach ( $refGroup->refs as $ref ) {
$refGroup->renderLine( $extApi, $refsNode, $ref );
}
}
// Remove the group from refsData
$refsData->removeRefGroup( $group );
}
/**
* Process `<ref>`s left behind after the DOM is fully processed.
* We process them as if there was an implicit `<references />` tag at
* the end of the DOM.
*
* @param ParsoidExtensionAPI $extApi
* @param ReferencesData $refsData
* @param Node $node
*/
public function insertMissingReferencesIntoDOM(
ParsoidExtensionAPI $extApi, ReferencesData $refsData, Node $node
): void {
$doc = $node->ownerDocument;
foreach ( $refsData->getRefGroups() as $groupName => $refsGroup ) {
$domFragment = $doc->createDocumentFragment();
$frag = $this->createReferences(
$extApi,
$domFragment,
[
// Force string cast here since in the foreach above, $groupName
// is an array key. In that context, number-like strings are
// silently converted to a numeric value!
// Ex: In <ref group="2" />, the "2" becomes 2 in the foreach
'group' => (string)$groupName,
'responsive' => null,
],
static function ( $dp ) use ( $extApi ) {
// The new references come out of "nowhere", so to make selser work
// properly, add a zero-sized DSR pointing to the end of the document.
$content = $extApi->getPageConfig()->getRevisionContent()->getContent( 'main' );
$contentLength = strlen( $content );
$dp->dsr = new DomSourceRange( $contentLength, $contentLength, 0, 0 );
},
true
);
// Add a \n before the <ol> so that when serialized to wikitext,
// each <references /> tag appears on its own line.
$node->appendChild( $doc->createTextNode( "\n" ) );
$node->appendChild( $frag );
$this->insertReferencesIntoDOM( $extApi, $frag, $refsData, true );
}
}
private function processEmbeddedRefs(
ParsoidExtensionAPI $extApi, ReferencesData $refsData, string $str
): string {
$domFragment = $extApi->htmlToDom( $str );
$this->processRefs( $extApi, $refsData, $domFragment );
return $extApi->domToHtml( $domFragment, true, true );
}
public function processRefs(
ParsoidExtensionAPI $extApi, ReferencesData $refsData, Node $node
): void {
$child = $node->firstChild;
while ( $child !== null ) {
$nextChild = $child->nextSibling;
if ( $child instanceof Element ) {
if ( WTUtils::isSealedFragmentOfType( $child, 'ref' ) ) {
$this->extractRefFromNode( $extApi, $child, $refsData );
} elseif ( DOMUtils::hasTypeOf( $child, 'mw:Extension/references' ) ) {
if ( !$refsData->inReferencesContent() ) {
$refsData->referencesGroup =
DOMDataUtils::getDataParsoid( $child )->group ?? '';
}
$refsData->pushEmbeddedContentFlag( 'references' );
if ( $child->hasChildNodes() ) {
$this->processRefs( $extApi, $refsData, $child );
}
$refsData->popEmbeddedContentFlag();
if ( !$refsData->inReferencesContent() ) {
$refsData->referencesGroup = '';
$this->insertReferencesIntoDOM( $extApi, $child, $refsData, false );
}
} else {
$refsData->pushEmbeddedContentFlag();
// Look for <ref>s embedded in data attributes
$extApi->processAttributeEmbeddedHTML( $child,
function ( string $html ) use ( $extApi, $refsData ) {
return $this->processEmbeddedRefs( $extApi, $refsData, $html );
}
);
$refsData->popEmbeddedContentFlag();
if ( $child->hasChildNodes() ) {
$this->processRefs( $extApi, $refsData, $child );
}
}
}
$child = $nextChild;
}
}
/**
* Traverse into all the embedded content and mark up the refs in there
* that have errors that weren't known before the content was serialized.
*
* Some errors are only known at the time when we're inserting the
* references lists, at which point, embedded content has already been
* serialized and stored, so we no longer have live access to it. We
* therefore map about ids to errors for a ref at that time, and then do
* one final walk of the dom to peak into all the embedded content and
* mark up the errors where necessary.
*
* @param ParsoidExtensionAPI $extApi
* @param ReferencesData $refsData
* @param Node $node
*/
public function addEmbeddedErrors(
ParsoidExtensionAPI $extApi, ReferencesData $refsData, Node $node
): void {
$processEmbeddedErrors = function ( string $html ) use ( $extApi, $refsData ) {
// Similar to processEmbeddedRefs
$domFragment = $extApi->htmlToDom( $html );
$this->addEmbeddedErrors( $extApi, $refsData, $domFragment );
return $extApi->domToHtml( $domFragment, true, true );
};
$processBodyHtml = static function ( Element $n ) use ( $processEmbeddedErrors ) {
$dataMw = DOMDataUtils::getDataMw( $n );
if ( isset( $dataMw->body->html ) ) {
$dataMw->body->html = $processEmbeddedErrors(
$dataMw->body->html
);
}
};
$child = $node->firstChild;
while ( $child !== null ) {
$nextChild = $child->nextSibling;
if ( $child instanceof Element ) {
if ( DOMUtils::hasTypeOf( $child, 'mw:Extension/ref' ) ) {
$processBodyHtml( $child );
$about = DOMCompat::getAttribute( $child, 'about' );
'@phan-var string $about'; // assert $about is non-null
$errs = $refsData->embeddedErrors[$about] ?? null;
if ( $errs ) {
self::addErrorsToNode( $child, $errs );
}
} elseif ( DOMUtils::hasTypeOf( $child, 'mw:Extension/references' ) ) {
$processBodyHtml( $child );
} else {
$extApi->processAttributeEmbeddedHTML(
$child, $processEmbeddedErrors
);
}
if ( $child->hasChildNodes() ) {
$this->addEmbeddedErrors( $extApi, $refsData, $child );
}
}
$child = $nextChild;
}
}
/** @inheritDoc */
public function sourceToDom(
ParsoidExtensionAPI $extApi, string $txt, array $extArgs
): DocumentFragment {
$domFragment = $extApi->extTagToDOM(
$extArgs,
$txt,
[
'parseOpts' => [ 'extTag' => 'references' ],
]
);
$refsOpts = $extApi->extArgsToArray( $extArgs ) + [
'group' => null,
'responsive' => null,
];
// Detect invalid parameters on the references tag
$knownAttributes = [ 'group', 'responsive' ];
foreach ( $refsOpts as $key => $value ) {
if ( !in_array( strtolower( (string)$key ), $knownAttributes, true ) ) {
$extApi->pushError( 'cite_error_references_invalid_parameters' );
break;
}
}
$frag = $this->createReferences(
$extApi,
$domFragment,
$refsOpts,
static function ( $dp ) use ( $extApi ) {
$dp->src = $extApi->extTag->getSource();
// Setting redundant info on fragment.
// $docBody->firstChild info feels cumbersome to use downstream.
if ( $extApi->extTag->isSelfClosed() ) {
$dp->selfClose = true;
}
}
);
$domFragment->appendChild( $frag );
return $domFragment;
}
/** @inheritDoc */
public function processAttributeEmbeddedHTML(
ParsoidExtensionAPI $extApi, Element $elt, Closure $proc
): void {
$dataMw = DOMDataUtils::getDataMw( $elt );
if ( isset( $dataMw->body->html ) ) {
$dataMw->body->html = $proc( $dataMw->body->html );
}
}
/** @inheritDoc */
public function domToWikitext(
ParsoidExtensionAPI $extApi, Element $node, bool $wrapperUnmodified
) {
$dataMw = DOMDataUtils::getDataMw( $node );
// Autogenerated references aren't considered erroneous (the extension to the legacy
// parser also generates them) and are not suppressed when serializing because apparently
// that's the behaviour Parsoid clients want. However, autogenerated references *with
// group attributes* are errors (the legacy extension doesn't generate them at all) and
// are suppressed when serialized since we considered them an error while parsing and
// don't want them to persist in the content.
if ( !empty( $dataMw->autoGenerated ) && ( $dataMw->attrs->group ?? '' ) !== '' ) {
return '';
} else {
$startTagSrc = $extApi->extStartTagToWikitext( $node );
if ( empty( $dataMw->body ) ) {
return $startTagSrc; // We self-closed this already.
} else {
if ( isset( $dataMw->body->html ) ) {
$src = $extApi->htmlToWikitext(
[ 'extName' => $dataMw->name ],
$dataMw->body->html
);
return $startTagSrc . $src . '</' . $dataMw->name . '>';
} else {
$extApi->log( 'error',
'References body unavailable for: ' . DOMCompat::getOuterHTML( $node )
);
return ''; // Drop it!
}
}
}
}
/** @inheritDoc */
public function lintHandler(
ParsoidExtensionAPI $extApi, Element $refs, callable $defaultHandler
): bool {
$dataMw = DOMDataUtils::getDataMw( $refs );
if ( isset( $dataMw->body->html ) ) {
$fragment = $extApi->htmlToDom( $dataMw->body->html );
$defaultHandler( $fragment );
}
return true;
}
/** @inheritDoc */
public function diffHandler(
ParsoidExtensionAPI $extApi, callable $domDiff, Element $origNode,
Element $editedNode
): bool {
$origDataMw = DOMDataUtils::getDataMw( $origNode );
$editedDataMw = DOMDataUtils::getDataMw( $editedNode );
if ( isset( $origDataMw->body->html ) && isset( $editedDataMw->body->html ) ) {
$origFragment = $extApi->htmlToDom(
$origDataMw->body->html, $origNode->ownerDocument,
[ 'markNew' => true ]
);
$editedFragment = $extApi->htmlToDom(
$editedDataMw->body->html, $editedNode->ownerDocument,
[ 'markNew' => true ]
);
return call_user_func( $domDiff, $origFragment, $editedFragment );
}
// FIXME: Similar to DOMDiff::subtreeDiffers, maybe $editNode should
// be marked as inserted to avoid losing any edits, at the cost of
// more normalization
return false;
}
}

File Metadata

Mime Type
text/x-php
Expires
Wed, Aug 19, 06:03 (13 h, 20 m ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
fb/5c/d951741ff7dd41cfa4838d38659c
Default Alt Text
References.php (29 KB)

Event Timeline