Page MenuHomeWickedGov Phorge

References.php
No OneTemporary

Size
30 KB
Referenced Files
None
Subscribers
None

References.php

<?php
declare( strict_types = 1 );
// phpcs:disable MediaWiki.WhiteSpace.SpaceBeforeSingleLineComment.NewLineComment
namespace Cite\Parsoid;
use Cite\MarkSymbolRenderer;
use Cite\Validator;
use MediaWiki\Config\Config;
use MediaWiki\Html\HtmlHelper;
use MediaWiki\MediaWikiServices;
use stdClass;
use Wikimedia\Message\MessageParam;
use Wikimedia\Message\MessageValue;
use Wikimedia\Parsoid\Core\DomSourceRange;
use Wikimedia\Parsoid\Core\Sanitizer;
use Wikimedia\Parsoid\DOM\Document;
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\ParsoidExtensionAPI;
use Wikimedia\Parsoid\Ext\PHPUtils;
use Wikimedia\Parsoid\Ext\WTUtils;
use Wikimedia\Parsoid\NodeData\DataMw;
use Wikimedia\Parsoid\NodeData\DataMwBody;
use Wikimedia\Parsoid\NodeData\DataMwError;
use Wikimedia\Parsoid\NodeData\DataParsoid;
use Wikimedia\Parsoid\Utils\DOMCompat;
use Wikimedia\RemexHtml\HTMLData;
use Wikimedia\RemexHtml\Serializer\SerializerNode;
/**
* @license GPL-2.0-or-later
*/
class References {
private const CONFLICT_NONE = 0;
/** A conflict in e.g. the data-mw metadata, but not in the visible content */
private const CONFLICT_INVISIBLE = 1;
/** A conflict in the content that is visible to the user */
private const CONFLICT_VISIBLE = 2;
private Config $mainConfig;
private MarkSymbolRenderer $markSymbolRenderer;
private ParsoidValidator $validator;
/** @var array<string,array<string,string>>
* @internal Local copy of ref body HTML for conflict detection. Top level
* key is the ref group name, second level key is the ref name
*/
private array $conflictCache = [];
public function __construct( Config $mainConfig ) {
$this->mainConfig = $mainConfig;
$this->markSymbolRenderer = MediaWikiServices::getInstance()->getService( 'Cite.MarkSymbolRenderer' );
$this->validator = new ParsoidValidator();
}
private static function hasRef( ParsoidExtensionAPI $extApi, Node $node ): bool {
$c = $node->firstChild;
while ( $c ) {
if ( $c instanceof Element ) {
if ( WTUtils::isSealedFragmentOfType( $c, 'ref' ) ) {
return true;
}
$hasEmbeddedRefs = false;
$extApi->processAttributeEmbeddedDom( $c,
function ( DocumentFragment $df ) use ( $extApi, &$hasEmbeddedRefs ) {
$hasEmbeddedRefs = $hasEmbeddedRefs || self::hasRef( $extApi, $df );
return true;
}
);
if ( $hasEmbeddedRefs ) {
return true;
}
if ( self::hasRef( $extApi, $c ) ) {
return true;
}
}
$c = $c->nextSibling;
}
return false;
}
public function createEmptyReferenceListFragment(
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->setExtAttrib( '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 processNestedRefInRef(
ParsoidExtensionAPI $extApi,
Element $refFragment,
ReferencesData $referencesData,
bool $hasDifferingHtml
): ?string {
$refFragmentDp = DOMDataUtils::getDataParsoid( $refFragment );
if ( !empty( $refFragmentDp->empty ) || !self::hasRef( $extApi, $refFragment ) ) {
return null;
}
// This prevents nested list-defined references from erroneously giving "group mismatch"
// errors.
$referencesData->incrementRefDepth();
$this->processRefs( $extApi, $referencesData, $refFragment );
$referencesData->decrementRefDepth();
if ( $hasDifferingHtml ) {
// 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 :)
return $extApi->domToHtml( $refFragment, true, true );
}
return null;
}
private function extractRefFromNode(
ParsoidExtensionAPI $extApi, Element $node, ReferencesData $referencesData
): void {
$doc = $node->ownerDocument;
$errs = [];
// This is data-parsoid from the dom fragment node that's gone through
// DomSourceRange computation and template wrapping.
$nodeDp = DOMDataUtils::getDataParsoid( $node );
$contentId = $nodeDp->html;
// Extract the ref fragment and ensure it's valid
$refFragment = $extApi->getContentDOM( $contentId )->firstChild;
DOMUtils::assertElt( $refFragment );
$refFragmentDp = DOMDataUtils::getDataParsoid( $refFragment );
$refDataMw = DOMDataUtils::getDataMw( $refFragment );
// Validate attribute keys
$isSubreferenceSupported = $this->mainConfig->get( 'CiteSubReferencing' );
$status = Validator::filterRefArguments( $refDataMw->getExtAttribs() ?? [], $isSubreferenceSupported );
$arguments = $status->getValue();
// Check for missing content, added ?? '' to fix T259676 crasher
// FIXME: See T260082 for a more complete description of cause and deeper fix
$isOnlyWhitespace = trim( $refDataMw->body->extsrc ?? '' ) === '';
$isEmptyBody = !empty( $refFragmentDp->empty ) || $isOnlyWhitespace;
// FIXME: Duplication required for isKnown, but the Validator is supposed to do this.
$groupName = $arguments['group'] ?? $referencesData->referenceListGroup() ?? '';
$validator = new Validator(
$referencesData->referenceListGroup(),
false,
false
);
$text = !empty( $refFragmentDp->selfClose ) ? null : ( $isEmptyBody ? '' : 'dummy' );
$status->merge( $validator->validateRef( $text, $arguments ), true );
if ( $status->isOK() ) {
$arguments = $status->getValue();
}
foreach ( $status->getMessages() as $msg ) {
// FIXME: This is only temporary, eventually we want all validation results to be used
switch ( $msg->getKey() ) {
// TODO: Remove one by one so they are handled by the default instead
// Currently 3 failures
case 'cite_error_ref_no_key':
// Currently 22 failures
case 'cite_error_references_missing_key':
break;
default:
$errs[] = new DataMwError( $msg->getKey(), array_map(
static fn ( $p ) => $p instanceof MessageParam ? $p->getValue() : $p,
$msg->getParams()
) );
}
}
// Extract and validate attribute values
$refName = (string)$arguments['name'];
$followName = (string)$arguments['follow'];
$refDir = strtolower( (string)$arguments['dir'] );
$details = $arguments['details'] ?? '';
$hasBody = isset( $refDataMw->body );
// Handle 'about' attribute with priority since it's
// only added when the wrapper is a template sibling.
$about = DOMCompat::getAttribute( $node, 'about' ) ??
DOMCompat::getAttribute( $refFragment, 'about' );
'@phan-var string $about'; // assert that $about is non-null
$hasDetails = $details !== '' && $refName;
$hasValidFollow = false;
// Wrap the attribute 'follow'
if ( $followName ) {
$followSpan = $this->wrapFollower( $doc, $refFragment );
$followSpan->setAttribute( 'about', $about );
$refFragment->appendChild( $followSpan );
}
$refGroup = $referencesData->getOrCreateRefGroup( $groupName );
$ref = $refGroup->lookupRefByName( $refName );
$conflicts = self::CONFLICT_NONE;
// No point in doing conflict detection on <ref /> without body
if ( $hasBody ) {
$conflicts = $this->checkForConflictingContent(
$extApi, $groupName, $refName, $ref->contentId ?? null, $refFragment );
}
// Handle the attributes 'name' and 'follow'
if ( $refName ) {
$nameErrorMessage = $this->validator->validateName( $refName, $refGroup, $referencesData );
if ( $nameErrorMessage ) {
$errs[] = $nameErrorMessage;
}
} else {
if ( $followName ) {
// Check that the followed ref exists
$followErrorMessage = $this->validator->validateFollow( $followName, $refGroup );
if ( $followErrorMessage ) {
$errs[] = $followErrorMessage;
} else {
$hasValidFollow = true;
$ref = $refGroup->lookupRefByName( $followName );
}
}
}
// Split subref and main ref; add main ref as a list-defined reference
if ( $hasDetails ) {
$mainRefExists = (bool)$ref;
// Create new, empty main ref
$ref ??= $referencesData->addRef( $refGroup, $refName, $refDir );
if ( $hasBody ) {
if ( !$mainRefExists ) {
// Create a main ref and transfer the tag body to it,
$ref->isMainWithDetails = true;
$ref->contentId = $contentId;
}
// @phan-suppress-next-line PhanUndeclaredProperty
$refDataMw->mainBody = ParsoidAnchorFormatter::getNoteTextIdentifier( $ref );
// Flag to help reserialize main ref content into the subref when saving.
// @phan-suppress-next-line PhanUndeclaredProperty
$refDataMw->isMainRefBodyWithDetails = '1';
}
// Switch $ref to a newly-created subref
$ref = $referencesData->addRef( $refGroup, $refName, $refDir, $details );
// Move content to main ref.
$contentId = null;
$ref->contentId = null;
// Move details attribute into subref content.
$ref->externalFragment = $extApi->wikitextToDOM( $details, [
'processInNewFrame' => true,
'parseOpts' => [ 'context' => 'inline' ]
], true );
// Subref points to the main ref by name.
// FIXME: should have already asserted that refName exists for all details, see T387193
$refDataMw->setExtAttrib( 'name', null );
// @phan-suppress-next-line PhanUndeclaredProperty
$refDataMw->mainRef = $refName;
$refName = '';
}
$refFragmentHtml = $this->processNestedRefInRef( $extApi, $refFragment, $referencesData,
$conflicts !== self::CONFLICT_NONE ) ??
// Fall back to the normal behavior without any nested <ref>
$extApi->domToHtml( $refFragment, true );
if ( $this->isNestedInSupWithSameGroupAndName( $node, $groupName, $refName ) ) {
$errs[] = new DataMwError( 'cite_error_included_ref' );
ErrorUtils::addErrorsToNode( $node, $errs );
}
// Add ref-index linkback
$linkBackSup = $doc->createElement( 'sup' );
$isVisibleNode = false;
if ( $hasValidFollow ) {
// Migrate content from the follow to the ref
if ( $ref->contentId ) {
$refContent = $extApi->getContentDOM( $ref->contentId )->firstChild;
DOMUtils::migrateChildren( $refFragment, $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!
$ref ??= $referencesData->addRef( $refGroup, $refName, $refDir );
// Handle linkbacks
if ( $referencesData->inEmbeddedContent() ) {
$ref->embeddedNodes[] = $about;
// While indicator content is embedded throughout the parsing
// pipeline, it gets added back into the page in a post-processing
// step, so consider it visible for the sake of linkbacks.
if ( $referencesData->inIndicatorContext() ) {
$ref->visibleNodes++;
$isVisibleNode = true;
}
} else {
$ref->nodes[] = $linkBackSup;
$ref->visibleNodes++;
$isVisibleNode = true;
}
}
// Guaranteed from this point on
'@phan-var RefGroupItem $ref';
if ( isset( $arguments['dir'] ) ) {
$dirError = $this->validator->validateDir( $refDir, $ref );
if ( $dirError ) {
$errs[] = $dirError;
}
}
$hasMissingContent = $isEmptyBody && !$ref->externalFragment;
if ( $hasMissingContent ) {
if ( !$refName ) {
if ( !empty( $refFragmentDp->selfClose ) ) {
$errs[] = new DataMwError( 'cite_error_ref_no_key' );
}
}
if ( !empty( $refFragmentDp->selfClose ) ) {
unset( $refDataMw->body );
} else {
// Empty the <sup> since we've serialized its children and
// removing it below asserts everything has been migrated out
DOMCompat::replaceChildren( $refFragment );
$refDataMw->body = DataMwBody::new( [
'html' => $refDataMw->body->extsrc ?? '',
] );
}
} else {
if ( $ref->contentId && !$hasValidFollow ) {
// Empty the <sup> since we've serialized its children and
// removing it below asserts everything has been migrated out
DOMCompat::replaceChildren( $refFragment );
}
if ( $conflicts !== self::CONFLICT_NONE ) {
if ( $refFragmentHtml != '' && $conflicts === self::CONFLICT_VISIBLE ) {
$errs[] = new DataMwError( 'cite_error_references_duplicate_key', [ $refName ] );
}
$refDataMw->body = DataMwBody::new( [
'html' => $refFragmentHtml,
] );
} else {
$refDataMw->body = DataMwBody::new( [
'id' => ParsoidAnchorFormatter::getNoteTextIdentifier( $ref ),
] );
}
}
$this->addLinkBackAttributes(
$linkBackSup,
$isVisibleNode ? ParsoidAnchorFormatter::getBackLinkIdentifier( $ref ) : null,
DOMCompat::getAttribute( $node, 'typeof' ),
$about,
$hasValidFollow
);
$this->addLinkBackData(
$linkBackSup,
$nodeDp,
DOMUtils::hasTypeOf( $node, 'mw:Transclusion' ) ?
DOMDataUtils::getDataMw( $node ) :
$refDataMw
);
// FIXME(T214241): Should the errors be added to data-mw if
// $isTplWrapper? Here and other calls to addErrorsToNode.
ErrorUtils::addErrorsToNode( $linkBackSup, $errs );
// refLink is the link to the citation
$refLink = $doc->createElement( 'a' );
DOMUtils::addAttributes( $refLink, [
'href' => $extApi->getPageUri() . '#' .
Sanitizer::escapeIdForLink( ParsoidAnchorFormatter::getNoteIdentifier( $ref ) )
] );
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' );
$openBracket = $doc->createElement( 'span', '[' );
$openBracket->setAttribute( 'class', 'cite-bracket' );
$refLinkSpan->appendChild( $openBracket );
$refLinkSpan->appendChild( $doc->createTextNode(
$this->markSymbolRenderer->makeLabel( $ref->group, $ref->numberInGroup, $ref->subrefIndex )
) );
$closeBracket = $doc->createElement( 'span', ']' );
$closeBracket->setAttribute( 'class', 'cite-bracket' );
$refLinkSpan->appendChild( $closeBracket );
$refLink->appendChild( $refLinkSpan );
$linkBackSup->appendChild( $refLink );
// Checking if the <ref> is nested in a link
$aParent = DOMUtils::findAncestorOfName( $node, 'a' );
if ( $aParent ) {
$this->insertChildOutsideOfLink( $linkBackSup, $aParent );
// 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( $linkBackSup, $dsrOffset );
self::setMisnested( $refLink, $dsrOffset );
self::setMisnested( $refLinkSpan, $dsrOffset );
$node->parentNode->removeChild( $node );
} else {
// if not, we insert it where we planned in the first place
$node->parentNode->replaceChild( $linkBackSup, $node );
}
// Keep the first content to compare multiple <ref>s with the same name.
if ( $ref->contentId === null && !$hasMissingContent ) {
$ref->contentId = $contentId;
// Use the dir parameter only from the full definition of a named ref tag
$ref->dir = $refDir;
} elseif ( $contentId ) {
DOMCompat::remove( $refFragment );
$extApi->clearContentDOM( $contentId );
}
}
private function insertChildOutsideOfLink( Element $linkBackSup, Element $aParent ): void {
// 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( $linkBackSup, $insertionPoint );
// Mark the two now un-nested elements as a DOM forest that belongs together
$parentAbout = DOMCompat::getAttribute( $aParent, 'about' );
if ( $parentAbout !== null ) {
$linkBackSup->setAttribute( 'about', $parentAbout );
}
}
private function isNestedInSupWithSameGroupAndName( Element $node, string $groupName, string $refName ): bool {
// Nothing to compare with
if ( !$refName ) {
return false;
}
$supNode = DOMUtils::findAncestorOfName( $node, 'sup' );
while ( $supNode ) {
$dataMw = DOMDataUtils::getDataMw( $supNode );
if ( $dataMw &&
( $dataMw->getExtAttrib( 'group' ) ?? '' ) === $groupName &&
( $dataMw->getExtAttrib( 'name' ) ?? '' ) === $refName
) {
return true;
}
$supNode = DOMUtils::findAncestorOfName( $supNode, 'sup' );
}
return false;
}
/**
* wrap the content of the follow attribute
* so that there is no ambiguity
* where to find it when round tripping
*/
private function wrapFollower( Document $doc, Node $refFragment ): Element {
$followSpan = $doc->createElement( 'span' );
DOMUtils::addTypeOf( $followSpan, 'mw:Cite/Follow' );
$followSpan->appendChild(
$doc->createTextNode( ' ' )
);
DOMUtils::migrateChildren( $refFragment, $followSpan );
return $followSpan;
}
/**
* 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 );
}
public function insertReferencesIntoDOM(
ParsoidExtensionAPI $extApi, Element $refsNode,
ReferencesData $refsData, bool $autoGenerated = false
): void {
$isTemplateWrapper = DOMUtils::hasTypeOf( $refsNode, 'mw:Transclusion' );
$nodeDp = DOMDataUtils::getDataParsoid( $refsNode );
$groupName = $nodeDp->group ?? '';
$refGroup = $refsData->lookupRefGroup( $groupName );
// 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 ) {
foreach ( $refGroup->refs as $ref ) {
$errs = [];
// Mark all refs that are named without content
if ( $ref->name !== null && $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 ) {
ErrorUtils::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 fn ( Element $sup ) => $extApi->domToHtml( $sup, false, true ) . "\n",
PHPUtils::iterable_to_array( DOMCompat::querySelectorAll(
$refsNode, 'sup[typeof~=\'mw:Extension/ref\']'
) )
);
// Any main+details refs should have the main part appear in the
// reference list.
// TODO: clean up the various use cases for this: eg. don't add
// the main ref if it's already present.
if ( $refGroup ) {
$doc = $refsNode->ownerDocument;
foreach ( $refGroup->refs as $ref ) {
if ( $ref->isMainWithDetails && $ref->contentId ) {
$sup = $doc->createElement( 'sup' );
DOMUtils::addAttributes( $sup, [
'typeof' => 'mw:Extension/ref',
] );
$dataMw = new DataMw( [
'name' => 'ref',
'attrs' => (object)[
'name' => $ref->name,
'group' => $ref->group,
],
'body' => DataMwBody::new( [
'id' => ParsoidAnchorFormatter::getNoteTextIdentifier( $ref ),
] ),
'isMainWithDetails' => '1',
] );
DOMDataUtils::setDataMw( $sup, $dataMw );
$refFragment = $extApi->getContentDOM( $ref->contentId )->firstChild->firstChild;
if ( $refFragment ) {
DOMCompat::setInnerHTML( $sup, $extApi->domToHtml( $refFragment, false, false ) );
$nestedRefsHTML[] = $extApi->domToHtml( $sup, false, false );
}
}
}
}
if ( !$isTemplateWrapper ) {
$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;
}
if ( $nestedRefsHTML ) {
$dataMw->body = DataMwBody::new( [
'html' => "\n" . implode( $nestedRefsHTML ),
] );
} elseif ( !$autoGenerated && empty( $nodeDp->selfClose ) ) {
$dataMw->body = DataMwBody::new( [
'html' => '',
] );
} else {
unset( $dataMw->body );
}
unset( $nodeDp->selfClose );
}
$hasResponsiveWrapper = false;
// 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;
$hasResponsiveWrapper = true;
}
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 );
}
if ( $autoGenerated && $groupName !== '' ) {
$errorUtils = new ErrorUtils( $extApi );
$frag = $errorUtils->renderParsoidError(
new MessageValue( 'cite_error_group_refs_without_references', [ $groupName ] ) );
$span = DOMCompat::getFirstElementChild( $frag );
$refsNode->parentNode->insertBefore( $span, $refsNode->nextSibling );
$refsNodeAbout = DOMCompat::getAttribute( $refsNode, 'about' );
if ( $refsNodeAbout ) {
$span->setAttribute( 'about', $refsNodeAbout );
}
$destNode = $hasResponsiveWrapper ? DOMCompat::getParentElement( $refsNode ) : $refsNode;
ErrorUtils::addErrorsToNode( $destNode,
[ new DataMwError( 'cite_error_group_refs_without_references', [ $groupName ] )
] );
}
}
// Remove the group from refsData
$refsData->removeRefGroup( $groupName );
}
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' ) ) {
$inReferenceList = $refsData->inReferenceList();
if ( !$inReferenceList ) {
$refsData->referencesGroup =
DOMDataUtils::getDataParsoid( $child )->group ?? '';
}
$refsData->pushEmbeddedContentFlag();
if ( $child->hasChildNodes() ) {
$this->processRefs( $extApi, $refsData, $child );
}
$refsData->popEmbeddedContentFlag();
if ( !$inReferenceList ) {
$refsData->referencesGroup = null;
$this->insertReferencesIntoDOM( $extApi, $child, $refsData, false );
}
} else {
if ( DOMUtils::hasTypeOf( $child, 'mw:Extension/indicator' ) ) {
$refsData->pushEmbeddedContentFlag( 'indicator' );
} else {
$refsData->pushEmbeddedContentFlag();
}
// Look for <ref>s embedded in data attributes
$extApi->processAttributeEmbeddedDom( $child,
function ( DocumentFragment $df ) use ( $extApi, $refsData ) {
$this->processRefs( $extApi, $refsData, $df );
return true;
}
);
$refsData->popEmbeddedContentFlag();
if ( $child->hasChildNodes() ) {
$this->processRefs( $extApi, $refsData, $child );
}
}
}
$child = $nextChild;
}
}
private function addLinkBackData(
Element $linkBackSup,
DataParsoid $nodeDp,
?DataMw $dataMw
): void {
$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( $linkBackSup, $dataParsoid );
DOMDataUtils::setDataMw( $linkBackSup, $dataMw );
}
private function addLinkBackAttributes(
Element $linkBackSup,
?string $id,
?string $typeof,
?string $about,
bool $hasValidFollow
): void {
$class = 'mw-ref reference';
if ( $hasValidFollow ) {
$class .= ' mw-ref-follow';
}
DOMUtils::addAttributes( $linkBackSup, [
'about' => $about,
'class' => $class,
'id' => $hasValidFollow ? null : $id,
'rel' => 'dc:references',
'typeof' => $typeof,
] );
DOMUtils::removeTypeOf( $linkBackSup, 'mw:DOMFragment/sealed/ref' );
DOMUtils::addTypeOf( $linkBackSup, 'mw:Extension/ref' );
}
/**
* Compare named <ref> content with previous contents.
*
* 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 roundtripping
* for later <ref>s, we have to record HTML inline for all of them.
*
* @return int One of the self::CONFLICT_… constants specifying the type of conflict detected
*/
private function checkForConflictingContent(
ParsoidExtensionAPI $extApi,
string $refGroupName,
string $refName,
?string $existingContentId,
Element $newRefFragment
): int {
if ( !$refName || !$existingContentId ) {
return self::CONFLICT_NONE;
}
$this->conflictCache[$refGroupName][$refName] ??=
self::normalizeRef( $extApi->domToHtml( $extApi->getContentDOM( $existingContentId )->firstChild, true ) );
$existingHtml = $this->conflictCache[$refGroupName][$refName];
$html = $extApi->domToHtml( $newRefFragment, true );
// Optimized for performance: usually there is no conflict
if ( $this->normalizeRef( $html ) === $existingHtml ) {
return self::CONFLICT_NONE;
} elseif ( $this->normalizeRef( $html, true ) ===
$this->normalizeRef( $existingHtml, true )
) {
return self::CONFLICT_INVISIBLE;
} else {
return self::CONFLICT_VISIBLE;
}
}
/**
* This method removes the data-parsoid and about attributes from the HTML string passed in parameters, so
* that it doesn't interfere for the comparison of identical references.
* Remex does not implement the removal of "foreign" attributes, which means that these attributes cannot be
* removed on math and svg elements (T380977), and that trying to do so crashes the rendering.
* To avoid this, we only apply the normalization to nodes in the HTML namespace. This is wider than the
* exact definition of foreign attributes in Remex, but the other integration points of non-foreign content
* would be embedded in foreign content anyway - whose data-parsoid/about attributes would not be stripped
* anyway, so there's no need to process them.
* This means that identical references containing math or svg tags will be detected as being different.
* This is probably a rare enough corner case. If it is not, implementing the handling of foreign attributes
* (as started in Idf30b3afa00743fd78b015ff080cac29e1673f09) is a path to re-consider.
* The extra parameter $checkContent allows to further suppress error messages for references that are not
* technically identical from a round-tripping perspective (hence the need to still consider them distinct) but
* "close enough" to not bother editors with a confusing error message. This is in particular the case for
* references that involve various levels of templating, which means involve different variation of
* data-mw presence/absence.
*/
private function normalizeRef( string $s, bool $removeMetadata = false ): string {
return HtmlHelper::modifyElements( $s,
static function ( SerializerNode $node ) use ( $removeMetadata ): bool {
return $node->namespace == HTMLData::NS_HTML
&& ( isset( $node->attrs['data-parsoid'] )
|| isset( $node->attrs['about'] )
|| ( $removeMetadata
&& ( isset( $node->attrs['data-mw'] ) || isset( $node->attrs['typeof'] ) )
)
);
},
static function ( SerializerNode $node ) use ( $removeMetadata ): SerializerNode {
unset( $node->attrs['data-parsoid'] );
unset( $node->attrs['about'] );
if ( $removeMetadata ) {
unset( $node->attrs['data-mw'] );
unset( $node->attrs['typeof'] );
}
return $node;
}
);
}
}

File Metadata

Mime Type
text/x-php
Expires
Sun, Oct 4, 01:50 (4 w, 1 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
8b/ca/04ee18bf5e6423be77db9cd76334
Default Alt Text
References.php (30 KB)

Event Timeline