diff --git a/src/Parsoid/Ref.php b/src/Parsoid/Ref.php index 610b80b4c..c118af7f9 100644 --- a/src/Parsoid/Ref.php +++ b/src/Parsoid/Ref.php @@ -94,9 +94,28 @@ class Ref extends ExtensionTagHandler { 'inPHPBlock' => true ]; + // ref follow nodes should already have their content spans stored in followContent if ( is_string( $dataMw->body->html ?? null ) ) { // First look for the extension's content in data-mw.body.html $src = $extApi->htmlToWikitext( $html2wtOpts, $dataMw->body->html ); + } elseif ( isset( $dataMw->attrs->follow ) ) { + $src = ''; + if ( $node->hasAttribute( 'about' ) ) { + $about = $node->getAttribute( 'about' ); + $bodyElt = []; + $followNode = DOMCompat::getElementById( $node->ownerDocument, $dataMw->body->id ) + ?? null; + if ( $followNode != null ) { + $bodyElt = DOMCompat::querySelectorAll( $followNode, + "span[typeof~='mw:Cite/Follow'][about='" . $about . "']" ); + } + // ensure that $bodyElt was found + if ( count( $bodyElt ) > 0 ) { + $src = $extApi->domToWikitext( $html2wtOpts, $bodyElt[0], true ); + $src = ltrim( $src, ' ' ); + } + } + } elseif ( is_string( $dataMw->body->id ?? null ) ) { // If the body isn't contained in data-mw.body.html, look if // there's an element pointed to by body.id. @@ -145,6 +164,14 @@ class Ref extends ExtensionTagHandler { return ''; // Drop it! } + // Search for spans with follow content + if ( isset( $bodyElt->firstChild->nextSibling ) && + DOMUtils::hasTypeOf( $bodyElt->firstChild->nextSibling, 'mw:Cite/Follow' ) ) { + $clonedNode = $bodyElt->cloneNode(); + $clonedNode->appendChild( $bodyElt->firstChild ); + $bodyElt = $clonedNode; + } + $src = $extApi->domToWikitext( $html2wtOpts, $bodyElt, true ); } else { $extApi->log( 'error', 'Ref body unavailable for: ' . DOMCompat::getOuterHTML( $node ) ); diff --git a/src/Parsoid/References.php b/src/Parsoid/References.php index 2ec444a63..619a39912 100644 --- a/src/Parsoid/References.php +++ b/src/Parsoid/References.php @@ -144,17 +144,73 @@ class References extends ExtensionTagHandler { // FIXME(SSS): Need to clarify semantics here. // If both the containing elt as well as the nested // elt has a group attribute, what takes precedence? - $group = $refDmw->attrs->group ?? $referencesGroup ?? ''; + $groupName = $refDmw->attrs->group ?? $referencesGroup ?? ''; // NOTE: This will have been trimmed in Utils::getExtArgInfo()'s call // to TokenUtils::kvToHash() and ExtensionHandler::normalizeExtOptions() $refName = $refDmw->attrs->name ?? ''; + $follow = $refDmw->attrs->follow ?? ''; + + $hasRefName = strlen( $refName ) > 0; + $hasFollow = strlen( $follow ) > 0; // Add ref-index linkback $linkBack = $doc->createElement( 'sup' ); + // prechecking various combinations of un-named refs, named refs, follow and error cases to + // properly determine how to proceed with adding a ref or not and generating appropriate code + if ( $hasRefName ) { + if ( $hasFollow ) { + // having both a ref name and follow is an error, the ref should be represented + // and the follow present, but not causing any effect, but add and + // verify follow round trips anyway + $debugging = 0; + } else { + // normal named ref, so add normally + $debugging = 1; + } + } else { + // Either an un-named ref or a follow + if ( $hasFollow ) { + // Is a follow ref, so check if a named ref has already been defined + $group = $refsData->getRefGroup( $groupName, true ); + if ( isset( $group->indexByName[$follow] ) ) { + // Yes the named ref is defined, so lets check if it has defined content + $ref = $group->indexByName[$follow]; + if ( $ref->contentId ) { + // The named ref did define content, so attach follow content normally + $debugging = 2; + } else { + // The follow content follows a named ref that did not define content + // and therefore the follow content cannot be appended and must be + // handled differently + $debugging = 3; + } + } else { + // this follow precedes a named ref being defined or possibly never being defined + // which is a type of error but still must round trip, so we must handle this case + // with special code and not call the add function. + $debugging = 4; + + $html = $extApi->domToHtml( $c, true, true ); + $refDmw->body = (object)[ 'html' => $html ]; + DOMDataUtils::setDataMw( $linkBack, $refDmw ); + DOMUtils::addTypeOf( $linkBack, 'mw:Extension/ref mw:Error' ); + $node->parentNode->replaceChild( $linkBack, $node ); + return; + // do not define a ref + + } + // Is an unnamed ref and not a follow, so add the ref normally + } else { + // normal un-named ref, so add normally + $debugging = 5; + } + } + $ref = $refsData->add( - $extApi, $group, $refName, $about, $nestedInReferences, $linkBack + $extApi, $groupName, $refName, $follow, $contentId, $about, $nestedInReferences, + $linkBack ); $errs = []; @@ -163,6 +219,10 @@ class References extends ExtensionTagHandler { // FIXME: See T260082 for a more complete description of cause and deeper fix $missingContent = ( !empty( $cDp->empty ) || trim( $refDmw->body->extsrc ?? '' ) === '' ); + if ( $refName !== '' && strlen( $follow ) > 0 ) { + $errs[] = [ 'key' => 'cite_error_ref_too_many_keys' ]; + } + if ( $missingContent ) { // Check for missing name and content to generate error code if ( $refName === '' ) { @@ -203,6 +263,10 @@ class References extends ExtensionTagHandler { } $lastLinkback = $ref->linkbacks[count( $ref->linkbacks ) - 1] ?? null; + + if ( strlen( $follow ) > 0 ) { + $linkBack->setAttribute( 'style', 'display: none;' ); + } DOMUtils::addAttributes( $linkBack, [ 'about' => $about, 'class' => 'mw-ref', @@ -215,7 +279,6 @@ class References extends ExtensionTagHandler { if ( count( $errs ) > 0 ) { DOMUtils::addTypeOf( $linkBack, 'mw:Error' ); } - $dataParsoid = new stdClass; if ( isset( $nodeDp->src ) ) { $dataParsoid->src = $nodeDp->src; @@ -256,8 +319,11 @@ class References extends ExtensionTagHandler { '[' . ( $ref->group ? $ref->group . ' ' : '' ) . $ref->groupIndex . ']' ) ); - $refLink->appendChild( $refLinkSpan ); - $linkBack->appendChild( $refLink ); + + if ( strlen( $follow ) == 0 || count( $errs ) > 0 ) { + $refLink->appendChild( $refLinkSpan ); + $linkBack->appendChild( $refLink ); + } $node->parentNode->replaceChild( $linkBack, $node ); diff --git a/src/Parsoid/ReferencesData.php b/src/Parsoid/ReferencesData.php index 6f8495ff8..7cf456576 100644 --- a/src/Parsoid/ReferencesData.php +++ b/src/Parsoid/ReferencesData.php @@ -5,6 +5,7 @@ namespace Wikimedia\Parsoid\Ext\Cite; use DOMElement; use stdClass; +use Wikimedia\Parsoid\Ext\DOMUtils; use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI; class ReferencesData { @@ -53,14 +54,16 @@ class ReferencesData { * @param ParsoidExtensionAPI $extApi * @param string $groupName * @param string $refName + * @param string $follow + * @param string $contentId * @param string $about * @param bool $skipLinkback * @param DOMElement $linkBack * @return stdClass */ public function add( - ParsoidExtensionAPI $extApi, string $groupName, string $refName, - string $about, bool $skipLinkback, DOMElement $linkBack + ParsoidExtensionAPI $extApi, string $groupName, string $refName, string $follow, + string $contentId, string $about, bool $skipLinkback, DOMElement $linkBack ): stdClass { $group = $this->getRefGroup( $groupName, true ); // Looks like Cite.php doesn't try to fix ids that already have @@ -72,6 +75,41 @@ class ReferencesData { // in Parsoid: ExtensionHandler#normalizeExtOptions $refName = $extApi->sanitizeHTMLId( $refName ); $hasRefName = strlen( $refName ) > 0; + $hasFollow = strlen( $follow ) > 0; + + // Is this a follow ref that has been preceeded by a named ref which defined + // content or did not define content, or a self closed named ref without content + if ( !$hasRefName && $hasFollow && isset( $group->indexByName[$follow] ) ) { + $ref = $group->indexByName[$follow]; + $contentSup = $extApi->getContentDOM( $contentId ); + $ownerDoc = $contentSup->ownerDocument; + $span = $ownerDoc->createElement( 'span' ); + DOMUtils::addTypeOf( $span, 'mw:Cite/Follow' ); + $span->setAttribute( 'about', $about ); + $spaceNode = $ownerDoc->createTextNode( ' ' ); + $span->appendChild( $spaceNode ); + DOMUtils::migrateChildren( $contentSup, $span ); + + // contentSup is now empty and can be used as a container for migrate children + $contentSup->appendChild( $span ); + + // If the named ref has defined content + if ( $ref->contentId ) { + $refContent = $extApi->getContentDOM( $ref->contentId ); + ParsoidExtensionAPI::migrateChildrenBetweenDocs( $contentSup, $refContent, false ); + } else { + // Otherwise we have a follow that comes after named ref without content + // So create a sup in a fragment, set that into the environment and migrate + // the follow content into the fragment + $ref->contentId = $contentId; + } + return $ref; + } + + // Must check for error case where $hasRefName and $hasFollow are both present + // which still needs to create the ref, but also flag it with an error due to + // [ 'key' => 'cite_error_ref_too_many_keys' ]; as trapped in References.php + // but which needs to preserve the extra key for round tripping if ( $hasRefName && isset( $group->indexByName[$refName] ) ) { $ref = $group->indexByName[$refName];