mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Cite
synced 2024-11-28 00:40:12 +00:00
Port Cite extension
* All wt2wt, html2wt, and all but one html2html tests pass in hybrid mode when entire html2wt code is run in PHP Set "Serializer: true" in the html2wt section of phpconfig.yaml * The single failing html2html test is a <gallery> test which is presumably related to the unported <gallery> extension code, but not sure. Not investigating it now. * Update Parsoid Extension API to provide access to extension source without exposing internals. Change-Id: I6d6e21ad2324acfc4306b32c9055d6c088708c48
This commit is contained in:
parent
4e334fa727
commit
005176a355
|
@ -1,48 +1,37 @@
|
|||
<?php
|
||||
// phpcs:ignoreFile
|
||||
// phpcs:disable Generic.Files.LineLength.TooLong
|
||||
/* REMOVE THIS COMMENT AFTER PORTING */
|
||||
/**
|
||||
* This module implements `<ref>` and `<references>` extension tag handling
|
||||
* natively in Parsoid.
|
||||
* @module ext/Cite
|
||||
*/
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace Parsoid;
|
||||
namespace Parsoid\Ext\Cite;
|
||||
|
||||
use Parsoid\Ref as Ref;
|
||||
use Parsoid\References as References;
|
||||
use Parsoid\RefProcessor as RefProcessor;
|
||||
use DOMNode;
|
||||
use Parsoid\Config\Env;
|
||||
|
||||
/**
|
||||
* Native Parsoid implementation of the Cite extension
|
||||
* that ties together `<ref>` and `<references>`.
|
||||
*/
|
||||
class Cite {
|
||||
public function __construct() {
|
||||
$this->config = [
|
||||
/** @return array */
|
||||
public function getConfig(): array {
|
||||
return [
|
||||
'name' => 'cite',
|
||||
'domProcessors' => [
|
||||
'wt2htmlPostProcessor' => RefProcessor::class,
|
||||
'html2wtPreProcessor' => function ( ...$args ) {return $this->_html2wtPreProcessor( ...$args );
|
||||
}
|
||||
'html2wtPreProcessor' => function ( ...$args ) {
|
||||
return self::html2wtPreProcessor( ...$args );
|
||||
}
|
||||
],
|
||||
'tags' => [
|
||||
[
|
||||
'name' => 'ref',
|
||||
'toDOM' => Ref::toDOM,
|
||||
'class' => Ref::class,
|
||||
'fragmentOptions' => [
|
||||
'sealFragment' => true
|
||||
],
|
||||
'serialHandler' => Ref::serialHandler, // FIXME: Rename to toWikitext
|
||||
'lintHandler' => Ref::lintHandler
|
||||
]
|
||||
, // FIXME: Do we need (a) domDiffHandler (b) ... others ...
|
||||
],
|
||||
[
|
||||
'name' => 'references',
|
||||
'toDOM' => References::toDOM,
|
||||
'serialHandler' => References::serialHandler,
|
||||
'lintHandler' => References::lintHandler
|
||||
'class' => References::class,
|
||||
]
|
||||
],
|
||||
'styles' => [
|
||||
|
@ -51,7 +40,6 @@ class Cite {
|
|||
]
|
||||
];
|
||||
}
|
||||
public $config;
|
||||
|
||||
/**
|
||||
* html -> wt DOM PreProcessor
|
||||
|
@ -64,12 +52,11 @@ class Cite {
|
|||
* - for inserted refs, we might want to de-duplicate refs.
|
||||
* - for deleted refs, if the primary ref was deleted, we have to transfer
|
||||
* the primary ref designation to another instance of the named ref.
|
||||
*
|
||||
* @param Env $env
|
||||
* @param DOMNode $body
|
||||
*/
|
||||
public function _html2wtPreProcessor( $env, $body ) {
|
||||
private static function html2wtPreProcessor( Env $env, DOMNode $body ) {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
if ( gettype( $module ) === 'object' ) {
|
||||
$module->exports = $Cite;
|
||||
}
|
|
@ -1,26 +1,32 @@
|
|||
<?php // lint >= 99.9
|
||||
// phpcs:ignoreFile
|
||||
// phpcs:disable Generic.Files.LineLength.TooLong
|
||||
/* REMOVE THIS COMMENT AFTER PORTING */
|
||||
namespace Parsoid;
|
||||
<?php
|
||||
declare( strict_types = 1 );
|
||||
|
||||
$ParsoidExtApi = $module->parent->parent->require( './extapi.js' )->versionCheck( '^0.10.0' );
|
||||
$temp0 = $ParsoidExtApi;
|
||||
$ContentUtils = $temp0::ContentUtils;
|
||||
$DOMDataUtils = $temp0::DOMDataUtils;
|
||||
$WTUtils = $temp0::WTUtils;
|
||||
$Promise = $temp0::Promise;
|
||||
namespace Parsoid\Ext\Cite;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use DOMNode;
|
||||
use Exception;
|
||||
use Parsoid\Config\ParsoidExtensionAPI;
|
||||
use Parsoid\Ext\ExtensionTag;
|
||||
use Parsoid\Ext\SerialHandler;
|
||||
use Parsoid\Html2Wt\SerializerState;
|
||||
use Parsoid\Utils\ContentUtils;
|
||||
use Parsoid\Utils\DOMCompat;
|
||||
use Parsoid\Utils\DOMDataUtils;
|
||||
use Parsoid\Utils\DOMUtils;
|
||||
use Parsoid\Utils\WTUtils;
|
||||
|
||||
/**
|
||||
* Simple token transform version of the Ref extension tag.
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
class Ref {
|
||||
public static function toDOM( $state, $content, $args ) {
|
||||
class Ref implements ExtensionTag, SerialHandler {
|
||||
|
||||
/** @inheritDoc */
|
||||
public function toDOM( ParsoidExtensionAPI $extApi, string $txt, array $extExtArgs ): DOMDocument {
|
||||
// Drop nested refs entirely, unless we've explicitly allowed them
|
||||
if ( $state->parseContext->extTag === 'ref'
|
||||
&& !( $state->parseContext->extTagOpts && $state->parseContext->extTagOpts->allowNestedRef )
|
||||
if ( ( $extApi->parseContext['extTag'] ?? null ) === 'ref' &&
|
||||
empty( $extApi->parseContext['extTagOpts']['allowNestedRef'] )
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
@ -29,120 +35,140 @@ class Ref {
|
|||
// function. However, we're overly permissive here since we can't
|
||||
// distinguish when that's nested in another template.
|
||||
// The php preprocessor did our expansion.
|
||||
$allowNestedRef = $state->parseContext->inTemplate && $state->parseContext->extTag !== 'ref';
|
||||
$allowNestedRef = !empty( $extApi->parseContext['inTemplate'] ) &&
|
||||
( $extApi->parseContext['extTag'] ?? null ) !== 'ref';
|
||||
|
||||
return ParsoidExtApi::parseTokenContentsToDOM( $state, $args, '', $content, [
|
||||
return $extApi->parseTokenContentsToDOM(
|
||||
$extExtArgs,
|
||||
'',
|
||||
$txt,
|
||||
[
|
||||
// NOTE: sup's content model requires it only contain phrasing
|
||||
// content, not flow content. However, since we are building an
|
||||
// in-memory DOM which is simply a tree data structure, we can
|
||||
// nest flow content in a <sup> tag.
|
||||
'wrapperTag' => 'sup',
|
||||
'inTemplate' => $state->parseContext->inTemplate,
|
||||
'inTemplate' => $extApi->parseContext['inTemplate'] ?? null,
|
||||
'extTag' => 'ref',
|
||||
'extTagOpts' => [
|
||||
'allowNestedRef' => (bool)$allowNestedRef
|
||||
],
|
||||
'extTagOpts' => [ 'allowNestedRef' => $allowNestedRef ],
|
||||
// FIXME: One-off PHP parser state leak.
|
||||
// This needs a better solution.
|
||||
'inPHPBlock' => true
|
||||
'inPHPBlock' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public static function lintHandler( $ref, $env, $tplInfo, $domLinter ) {
|
||||
/** @inheritDoc */
|
||||
public function hasLintHandler(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function lintHandler(
|
||||
ParsoidExtensionAPI $extApi, DOMElement $ref, callable $defaultHandler
|
||||
): ?DOMNode {
|
||||
// Don't lint the content of ref in ref, since it can lead to cycles
|
||||
// using named refs
|
||||
if ( WTUtils::fromExtensionContent( $ref, 'references' ) ) { return $ref->nextSibling;
|
||||
}
|
||||
if ( WTUtils::fromExtensionContent( $ref, 'references' ) ) {
|
||||
return $ref->nextSibling;
|
||||
}
|
||||
|
||||
$linkBackId = preg_replace( '/[^#]*#/', '', $ref->firstChild->getAttribute( 'href' ), 1 );
|
||||
$refFirstChild = $ref->firstChild;
|
||||
DOMUtils::assertElt( $refFirstChild );
|
||||
$linkBackId = preg_replace( '/[^#]*#/', '', $refFirstChild->getAttribute( 'href' ), 1 );
|
||||
$refNode = $ref->ownerDocument->getElementById( $linkBackId );
|
||||
if ( $refNode ) {
|
||||
// Ex: Buggy input wikitext without ref content
|
||||
$domLinter( $refNode->lastChild, $env, ( $tplInfo->isTemplated ) ? $tplInfo : null );
|
||||
$defaultHandler( $refNode->lastChild );
|
||||
}
|
||||
return $ref->nextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
Ref::serialHandler = [
|
||||
'handle' => /* async */function ( $node, $state, $wrapperUnmodified ) use ( &$DOMDataUtils, &$ContentUtils ) {
|
||||
$startTagSrc = /* await */ $state->serializer->serializeExtensionStartTag( $node, $state );
|
||||
/** @inheritDoc */
|
||||
public function fromHTML(
|
||||
DOMElement $node, SerializerState $state, bool $wrapperUnmodified
|
||||
): string {
|
||||
$startTagSrc = $state->serializer->serializeExtensionStartTag( $node, $state );
|
||||
$dataMw = DOMDataUtils::getDataMw( $node );
|
||||
$env = $state->env;
|
||||
$env = $state->getEnv();
|
||||
$html = null;
|
||||
if ( !$dataMw->body ) {
|
||||
if ( empty( $dataMw->body ) ) {
|
||||
return $startTagSrc; // We self-closed this already.
|
||||
} else { // We self-closed this already.
|
||||
if ( gettype( $dataMw->body->html ) === 'string' ) {
|
||||
// First look for the extension's content in data-mw.body.html
|
||||
$html = $dataMw->body->html;
|
||||
} elseif ( gettype( $dataMw->body->id ) === 'string' ) {
|
||||
// If the body isn't contained in data-mw.body.html, look if
|
||||
// there's an element pointed to by body.id.
|
||||
$bodyElt = $node->ownerDocument->getElementById( $dataMw->body->id );
|
||||
if ( !$bodyElt && $env->page->editedDoc ) {
|
||||
// Try to get to it from the main page.
|
||||
// This can happen when the <ref> is inside another
|
||||
// extension, most commonly inside a <references>.
|
||||
// The recursive call to serializeDOM puts us inside
|
||||
// inside a new document.
|
||||
$bodyElt = $env->page->editedDoc->getElementById( $dataMw->body->id );
|
||||
}
|
||||
if ( $bodyElt ) {
|
||||
// n.b. this is going to drop any diff markers but since
|
||||
// the dom differ doesn't traverse into extension content
|
||||
// none should exist anyways.
|
||||
DOMDataUtils::visitAndStoreDataAttribs( $bodyElt );
|
||||
$html = ContentUtils::toXML( $bodyElt, [ 'innerXML' => true ] );
|
||||
DOMDataUtils::visitAndLoadDataAttribs( $bodyElt );
|
||||
} else {
|
||||
// Some extra debugging for VisualEditor
|
||||
$extraDebug = '';
|
||||
$firstA = $node->querySelector( 'a[href]' );
|
||||
if ( $firstA && preg_match( '/^#/', $firstA->getAttribute( 'href' ) || '' ) ) {
|
||||
$href = $firstA->getAttribute( 'href' ) || '';
|
||||
try {
|
||||
$ref = $node->ownerDocument->querySelector( $href );
|
||||
if ( $ref ) {
|
||||
$extraDebug += ' [own doc: ' . $ref->outerHTML . ']';
|
||||
}
|
||||
$ref = $env->page->editedDoc->querySelector( $href );
|
||||
if ( $ref ) {
|
||||
$extraDebug += ' [main doc: ' . $ref->outerHTML . ']';
|
||||
}
|
||||
} catch ( Exception $e ) {
|
||||
}// eslint-disable-line
|
||||
// eslint-disable-line
|
||||
if ( !$extraDebug ) {
|
||||
$extraDebug = ' [reference ' . $href . ' not found]';
|
||||
}
|
||||
if ( is_string( $dataMw->body->html ?? null ) ) {
|
||||
// First look for the extension's content in data-mw.body.html
|
||||
$html = $dataMw->body->html;
|
||||
} 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.
|
||||
$bodyElt = DOMCompat::getElementById( $node->ownerDocument, $dataMw->body->id );
|
||||
$editedDoc = $env->getPageConfig()->editedDoc ?? null;
|
||||
if ( !$bodyElt && $editedDoc ) {
|
||||
// Try to get to it from the main page.
|
||||
// This can happen when the <ref> is inside another
|
||||
// extension, most commonly inside a <references>.
|
||||
// The recursive call to serializeDOM puts us inside
|
||||
// inside a new document.
|
||||
$bodyElt = DOMCompat::getElementById( $editedDoc, $dataMw->body->id );
|
||||
}
|
||||
$env->log( 'error/' . $dataMw->name,
|
||||
'extension src id ' . $dataMw->body->id
|
||||
. ' points to non-existent element for:', $node->outerHTML,
|
||||
'. More debug info: ', $extraDebug
|
||||
);
|
||||
if ( $bodyElt ) {
|
||||
// n.b. this is going to drop any diff markers but since
|
||||
// the dom differ doesn't traverse into extension content
|
||||
// none should exist anyways.
|
||||
DOMDataUtils::visitAndStoreDataAttribs( $bodyElt );
|
||||
$html = ContentUtils::toXML( $bodyElt, [ 'innerXML' => true ] );
|
||||
DOMDataUtils::visitAndLoadDataAttribs( $bodyElt );
|
||||
} else {
|
||||
// Some extra debugging for VisualEditor
|
||||
$extraDebug = '';
|
||||
$firstA = DOMCompat::querySelector( $node, 'a[href]' );
|
||||
$href = $firstA->getAttribute( 'href' );
|
||||
if ( $firstA && preg_match( '/^#/', $href ) ) {
|
||||
try {
|
||||
$ref = DOMCompat::querySelector( $node->ownerDocument, $href );
|
||||
if ( $ref ) {
|
||||
$extraDebug .= ' [own doc: ' . DOMCompat::getOuterHTML( $ref ) . ']';
|
||||
}
|
||||
$ref = DOMCompat::querySelector( $env->getPageConfig()->editedDoc, $href );
|
||||
if ( $ref ) {
|
||||
$extraDebug .= ' [main doc: ' . DOMCompat::getOuterHTML( $ref ) . ']';
|
||||
}
|
||||
} catch ( Exception $e ) {
|
||||
}// eslint-disable-line
|
||||
// eslint-disable-line
|
||||
if ( !$extraDebug ) {
|
||||
$extraDebug = ' [reference ' . $href . ' not found]';
|
||||
}
|
||||
}
|
||||
$env->log(
|
||||
'error/' . $dataMw->name,
|
||||
'extension src id ' . $dataMw->body->id . ' points to non-existent element for:',
|
||||
DOMCompat::getOuterHTML( $node ),
|
||||
'. More debug info: ',
|
||||
$extraDebug
|
||||
);
|
||||
return ''; // Drop it!
|
||||
}
|
||||
} else { // Drop it!
|
||||
$env->log( 'error', 'Ref body unavailable for: ' . DOMCompat::getOuterHTML( $node ) );
|
||||
return ''; // Drop it!
|
||||
}
|
||||
} else { // Drop it!
|
||||
|
||||
$env->log( 'error', 'Ref body unavailable for: ' . $node->outerHTML );
|
||||
return ''; // Drop it!
|
||||
}// Drop it!
|
||||
} // Drop it!
|
||||
}
|
||||
|
||||
$src = /* await */ $state->serializer->serializeHTML( [
|
||||
'env' => $state->env,
|
||||
'extName' => $dataMw->name,
|
||||
// FIXME: One-off PHP parser state leak.
|
||||
$src = $state->serializer->serializeHTML(
|
||||
[
|
||||
'env' => $state->getEnv(),
|
||||
'extName' => $dataMw->name, // FIXME: One-off PHP parser state leak.
|
||||
// This needs a better solution.
|
||||
'inPHPBlock' => true
|
||||
], $html
|
||||
],
|
||||
$html
|
||||
);
|
||||
return $startTagSrc + $src . '</' . $dataMw->name . '>';
|
||||
return $startTagSrc . $src . '</' . $dataMw->name . '>';
|
||||
}
|
||||
|
||||
];
|
||||
|
||||
$module->exports = $Ref;
|
||||
/** @inheritDoc */
|
||||
public function before( DOMElement $node, DOMNode $otherNode, SerializerState $state ): ?array {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,69 +1,113 @@
|
|||
<?php
|
||||
// phpcs:ignoreFile
|
||||
// phpcs:disable Generic.Files.LineLength.TooLong
|
||||
/* REMOVE THIS COMMENT AFTER PORTING */
|
||||
namespace Parsoid;
|
||||
declare( strict_types = 1 );
|
||||
|
||||
$ParsoidExtApi = $module->parent->parent->parent->parent->require( './extapi.js' )->versionCheck( '^0.10.0' );
|
||||
$temp0 = $ParsoidExtApi;
|
||||
$DOMDataUtils = $temp0::DOMDataUtils;
|
||||
$DOMUtils = $temp0::DOMUtils;
|
||||
namespace Parsoid\Ext\Cite;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use Parsoid\Config\Env;
|
||||
use Parsoid\Utils\DOMDataUtils;
|
||||
use Parsoid\Utils\DOMUtils;
|
||||
use Parsoid\Utils\Title;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Helper class used by `<references>` implementation.
|
||||
* @class
|
||||
*/
|
||||
class RefGroup {
|
||||
public function __construct( $group ) {
|
||||
$this->name = $group || '';
|
||||
$this->refs = [];
|
||||
$this->indexByName = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var stdClass[]
|
||||
*/
|
||||
public $refs;
|
||||
|
||||
/**
|
||||
* @var stdClass[]
|
||||
*/
|
||||
public $indexByName;
|
||||
|
||||
public function renderLine( $env, $refsList, $ref ) {
|
||||
/**
|
||||
* RefGroup constructor.
|
||||
* @param string $group
|
||||
*/
|
||||
public function __construct( string $group = '' ) {
|
||||
$this->name = $group;
|
||||
$this->refs = [];
|
||||
$this->indexByName = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate leading linkbacks
|
||||
* @param string $href
|
||||
* @param string|null $group
|
||||
* @param string $text
|
||||
* @param DOMDocument $ownerDoc
|
||||
* @param Env $env
|
||||
* @return DOMElement
|
||||
*/
|
||||
private static function createLinkback(
|
||||
string $href, ?string $group, string $text, DOMDocument $ownerDoc, Env $env
|
||||
): DOMElement {
|
||||
$a = $ownerDoc->createElement( 'a' );
|
||||
$s = $ownerDoc->createElement( 'span' );
|
||||
$textNode = $ownerDoc->createTextNode( $text . ' ' );
|
||||
$title = Title::newFromText(
|
||||
$env->getPageConfig()->getTitle() . '#' . $href,
|
||||
$env->getSiteConfig()
|
||||
);
|
||||
$a->setAttribute( 'href', $env->makeLink( $title ) );
|
||||
$s->setAttribute( 'class', 'mw-linkback-text' );
|
||||
if ( $group ) {
|
||||
$a->setAttribute( 'data-mw-group', $group );
|
||||
}
|
||||
$s->appendChild( $textNode );
|
||||
$a->appendChild( $s );
|
||||
return $a;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Env $env
|
||||
* @param DOMElement $refsList
|
||||
* @param stdClass $ref
|
||||
*/
|
||||
public function renderLine( Env $env, DOMElement $refsList, stdClass $ref ): void {
|
||||
$ownerDoc = $refsList->ownerDocument;
|
||||
|
||||
// Generate the li and set ref content first, so the HTML gets parsed.
|
||||
// We then append the rest of the ref nodes before the first node
|
||||
$li = $ownerDoc->createElement( 'li' );
|
||||
$refDir = $ref->dir;
|
||||
$refTarget = $ref->target;
|
||||
$refContent = $ref->content;
|
||||
$refGroup = $ref->group;
|
||||
DOMDataUtils::addAttributes( $li, [
|
||||
'about' => '#' . $ref->target,
|
||||
'id' => $ref->target,
|
||||
'class' => ( [ 'rtl', 'ltr' ]->includes( $ref->dir ) ) ? 'mw-cite-dir-' . $ref->dir : null
|
||||
'about' => '#' . $refTarget,
|
||||
'id' => $refTarget,
|
||||
'class' => ( $refDir === 'rtl' || $refDir === 'ltr' ) ? 'mw-cite-dir-' . $refDir : null
|
||||
]
|
||||
);
|
||||
$reftextSpan = $ownerDoc->createElement( 'span' );
|
||||
DOMDataUtils::addAttributes( $reftextSpan, [
|
||||
'id' => 'mw-reference-text-' . $ref->target,
|
||||
'class' => 'mw-reference-text'
|
||||
DOMDataUtils::addAttributes(
|
||||
$reftextSpan,
|
||||
[
|
||||
'id' => 'mw-reference-text-' . $refTarget,
|
||||
'class' => 'mw-reference-text',
|
||||
]
|
||||
);
|
||||
if ( $ref->content ) {
|
||||
$content = $env->fragmentMap->get( $ref->content )[ 0 ];
|
||||
if ( $refContent ) {
|
||||
$content = $env->getFragment( $refContent )[0];
|
||||
DOMUtils::migrateChildrenBetweenDocs( $content, $reftextSpan );
|
||||
DOMDataUtils::visitAndLoadDataAttribs( $reftextSpan );
|
||||
}
|
||||
$li->appendChild( $reftextSpan );
|
||||
|
||||
// Generate leading linkbacks
|
||||
$createLinkback = function ( $href, $group, $text ) use ( &$ownerDoc, &$env ) {
|
||||
$a = $ownerDoc->createElement( 'a' );
|
||||
$s = $ownerDoc->createElement( 'span' );
|
||||
$textNode = $ownerDoc->createTextNode( $text . ' ' );
|
||||
$a->setAttribute( 'href', $env->page->titleURI . '#' . $href );
|
||||
$s->setAttribute( 'class', 'mw-linkback-text' );
|
||||
if ( $group ) {
|
||||
$a->setAttribute( 'data-mw-group', $group );
|
||||
}
|
||||
$s->appendChild( $textNode );
|
||||
$a->appendChild( $s );
|
||||
return $a;
|
||||
};
|
||||
if ( count( $ref->linkbacks ) === 1 ) {
|
||||
$linkback = $createLinkback( $ref->id, $ref->group, "↑" );
|
||||
$linkback = self::createLinkback( $ref->id, $refGroup, "↑", $ownerDoc, $env );
|
||||
$linkback->setAttribute( 'rel', 'mw:referencedBy' );
|
||||
$li->insertBefore( $linkback, $reftextSpan );
|
||||
} else {
|
||||
|
@ -72,10 +116,9 @@ class RefGroup {
|
|||
$span->setAttribute( 'rel', 'mw:referencedBy' );
|
||||
$li->insertBefore( $span, $reftextSpan );
|
||||
|
||||
$ref->linkbacks->forEach( function ( $lb, $i ) use ( &$span, &$createLinkback, &$ref ) {
|
||||
$span->appendChild( $createLinkback( $lb, $ref->group, $i + 1 ) );
|
||||
foreach ( $ref->linkbacks as $i => $lb ) {
|
||||
$span->appendChild( self::createLinkback( $lb, $refGroup, $i + 1, $ownerDoc, $env ) );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Space before content node
|
||||
|
@ -85,5 +128,3 @@ class RefGroup {
|
|||
$refsList->appendChild( $li );
|
||||
}
|
||||
}
|
||||
|
||||
$module->exports = $RefGroup;
|
||||
|
|
|
@ -1,25 +1,29 @@
|
|||
<?php
|
||||
// phpcs:ignoreFile
|
||||
// phpcs:disable Generic.Files.LineLength.TooLong
|
||||
/* REMOVE THIS COMMENT AFTER PORTING */
|
||||
namespace Parsoid;
|
||||
declare( strict_types = 1 );
|
||||
|
||||
use Parsoid\References as References;
|
||||
use Parsoid\ReferencesData as ReferencesData;
|
||||
namespace Parsoid\Ext\Cite;
|
||||
|
||||
use DOMElement;
|
||||
use Parsoid\Config\Env;
|
||||
|
||||
/**
|
||||
* wt -> html DOM PostProcessor
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
class RefProcessor {
|
||||
public function run( $body, $env, $options, $atTopLevel ) {
|
||||
|
||||
/**
|
||||
* @param DOMElement $body
|
||||
* @param Env $env
|
||||
* @param array $options
|
||||
* @param bool $atTopLevel
|
||||
*/
|
||||
public function run(
|
||||
DOMElement $body, Env $env, array $options = [], bool $atTopLevel = false
|
||||
): void {
|
||||
if ( $atTopLevel ) {
|
||||
$refsData = new ReferencesData( $env );
|
||||
References::_processRefs( $env, $refsData, $body );
|
||||
References::processRefs( $env, $refsData, $body );
|
||||
References::insertMissingReferencesIntoDOM( $refsData, $body );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$module->exports = $RefProcessor;
|
||||
|
|
|
@ -1,23 +1,31 @@
|
|||
<?php // lint >= 99.9
|
||||
// phpcs:ignoreFile
|
||||
// phpcs:disable Generic.Files.LineLength.TooLong
|
||||
/* REMOVE THIS COMMENT AFTER PORTING */
|
||||
namespace Parsoid;
|
||||
<?php
|
||||
declare( strict_types = 1 );
|
||||
|
||||
$ParsoidExtApi = $module->parent->parent->require( './extapi.js' )->versionCheck( '^0.10.0' );
|
||||
$temp0 = $ParsoidExtApi;
|
||||
$ContentUtils = $temp0::ContentUtils;
|
||||
$DOMDataUtils = $temp0::DOMDataUtils;
|
||||
$DOMUtils = $temp0::DOMUtils;
|
||||
$TokenUtils = $temp0::TokenUtils;
|
||||
$WTUtils = $temp0::WTUtils;
|
||||
$Promise = $temp0::Promise;
|
||||
namespace Parsoid\Ext\Cite;
|
||||
|
||||
/**
|
||||
* @class
|
||||
*/
|
||||
class References {
|
||||
public static function hasRef( $node ) {
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use DOMNode;
|
||||
use Parsoid\Config\Env;
|
||||
use Parsoid\Config\ParsoidExtensionAPI;
|
||||
use Parsoid\Ext\ExtensionTag;
|
||||
use Parsoid\Ext\SerialHandler;
|
||||
use Parsoid\Html2Wt\SerializerState;
|
||||
use Parsoid\Utils\ContentUtils;
|
||||
use Parsoid\Utils\DOMCompat;
|
||||
use Parsoid\Utils\DOMDataUtils;
|
||||
use Parsoid\Utils\DOMUtils;
|
||||
use Parsoid\Utils\Title;
|
||||
use Parsoid\Utils\TokenUtils;
|
||||
use Parsoid\Utils\WTUtils;
|
||||
use Wikimedia\Assert\Assert;
|
||||
|
||||
class References implements ExtensionTag, SerialHandler {
|
||||
/**
|
||||
* @param DOMNode $node
|
||||
* @return bool
|
||||
*/
|
||||
private static function hasRef( DOMNode $node ): bool {
|
||||
$c = $node->firstChild;
|
||||
while ( $c ) {
|
||||
if ( DOMUtils::isElt( $c ) ) {
|
||||
|
@ -33,52 +41,37 @@ class References {
|
|||
return false;
|
||||
}
|
||||
|
||||
public static function toDOM( $state, $content, $args ) {
|
||||
return ParsoidExtApi::parseTokenContentsToDOM( $state, $args, '', $content, [
|
||||
'wrapperTag' => 'div',
|
||||
'extTag' => 'references',
|
||||
'inTemplate' => $state->parseContext->inTemplate
|
||||
]
|
||||
)->then( function ( $doc ) use ( &$TokenUtils, &$args, &$state ) {
|
||||
$refsOpts = Object::assign( [
|
||||
'group' => null,
|
||||
'responsive' => null
|
||||
], TokenUtils::kvToHash( $args, true )
|
||||
);
|
||||
|
||||
$frag = References::createReferences( $state->env, $doc, $doc->body, $refsOpts, function ( $dp ) use ( &$state ) {
|
||||
$dp->src = ( $state->extToken->hasAttribute( 'source' ) ) ? $state->extToken->getAttribute( 'source' ) : null;
|
||||
// Redundant - also present on doc.body.firstChild, but feels cumbersome to use
|
||||
$dp->selfClose = $state->extToken->dataAttribs->selfClose;
|
||||
}
|
||||
);
|
||||
$doc->body->appendChild( $frag );
|
||||
|
||||
return $doc;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static function createReferences( $env, $doc, $body, $refsOpts, $modifyDp, $autoGenerated ) {
|
||||
/**
|
||||
* @param Env $env
|
||||
* @param DOMDocument $doc
|
||||
* @param DOMNode|null $body
|
||||
* @param array $refsOpts
|
||||
* @param callable|null $modifyDp
|
||||
* @param bool $autoGenerated
|
||||
* @return DOMElement
|
||||
*/
|
||||
private static function createReferences(
|
||||
Env $env, DOMDocument $doc, ?DOMNode $body, array $refsOpts,
|
||||
?callable $modifyDp, bool $autoGenerated = false
|
||||
): DOMElement {
|
||||
$ol = $doc->createElement( 'ol' );
|
||||
$ol->classList->add( 'mw-references' );
|
||||
$ol->classList->add( 'references' );
|
||||
DOMCompat::getClassList( $ol )->add( 'mw-references' );
|
||||
DOMCompat::getClassList( $ol )->add( 'references' );
|
||||
|
||||
if ( $body ) {
|
||||
DOMUtils::migrateChildren( $body, $ol );
|
||||
}
|
||||
|
||||
// Support the `responsive` parameter
|
||||
$rrOpts = $env->conf->wiki->responsiveReferences;
|
||||
$responsiveWrap = $rrOpts->enabled;
|
||||
if ( $refsOpts->responsive !== null ) {
|
||||
$responsiveWrap = $refsOpts->responsive !== '0';
|
||||
$rrOpts = $env->getSiteConfig()->responsiveReferences();
|
||||
$responsiveWrap = !empty( $rrOpts['enabled'] );
|
||||
if ( $refsOpts['responsive'] !== null ) {
|
||||
$responsiveWrap = $refsOpts['responsive'] !== '0';
|
||||
}
|
||||
|
||||
$frag = null;
|
||||
if ( $responsiveWrap ) {
|
||||
$div = $doc->createElement( 'div' );
|
||||
$div->classList->add( 'mw-references-wrap' );
|
||||
DOMCompat::getClassList( $div )->add( 'mw-references-wrap' );
|
||||
$div->appendChild( $ol );
|
||||
$frag = $div;
|
||||
} else {
|
||||
|
@ -94,64 +87,70 @@ class References {
|
|||
}
|
||||
|
||||
$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 ( $refsOpts['group'] ) { // No group for the empty string either
|
||||
$dp->group = $refsOpts['group'];
|
||||
$ol->setAttribute( 'data-mw-group', $refsOpts['group'] );
|
||||
}
|
||||
if ( gettype( $modifyDp ) === 'function' ) {
|
||||
if ( $modifyDp ) {
|
||||
$modifyDp( $dp );
|
||||
}
|
||||
|
||||
return $frag;
|
||||
}
|
||||
|
||||
public static function extractRefFromNode( $node, $refsData, $referencesAboutId, $referencesGroup, $nestedRefsHTML ) {
|
||||
$env = $refsData->env;
|
||||
private static function extractRefFromNode(
|
||||
DOMElement $node, ReferencesData $refsData, ?string $referencesAboutId = null,
|
||||
?string $referencesGroup = '', array $nestedRefsHTML = []
|
||||
): void {
|
||||
$env = $refsData->getEnv();
|
||||
$doc = $node->ownerDocument;
|
||||
$nestedInReferences = $referencesAboutId !== null;
|
||||
|
||||
// This is data-parsoid from the dom fragment node that's gone through
|
||||
// dsr computation and template wrapping.
|
||||
$nodeDp = DOMDataUtils::getDataParsoid( $node );
|
||||
$typeOf = $node->getAttribute( 'typeof' ) || '';
|
||||
$typeOf = $node->getAttribute( 'typeof' );
|
||||
$isTplWrapper = preg_match( '/\bmw:Transclusion\b/', $typeOf );
|
||||
$nodeType = preg_replace( '/mw:DOMFragment\/sealed\/ref/', '', $typeOf, 1 );
|
||||
$content = $nodeDp->html;
|
||||
$tplDmw = ( $isTplWrapper ) ? DOMDataUtils::getDataMw( $node ) : null;
|
||||
$tplDmw = $isTplWrapper ? DOMDataUtils::getDataMw( $node ) : null;
|
||||
|
||||
// This is the <sup> that's the meat of the sealed fragment
|
||||
$c = $env->fragmentMap->get( $content )[ 0 ];
|
||||
/** @var DOMElement $c */
|
||||
$c = $env->getFragment( $content )[0];
|
||||
DOMUtils::assertElt( $c );
|
||||
// All the actions that require loaded data-attributes on `c` are done
|
||||
// here so that we can quickly store those away for later.
|
||||
DOMDataUtils::visitAndLoadDataAttribs( $c );
|
||||
$cDp = DOMDataUtils::getDataParsoid( $c );
|
||||
$refDmw = DOMDataUtils::getDataMw( $c );
|
||||
if ( !$cDp->empty && self::hasRef( $c ) ) { // nested ref-in-ref
|
||||
self::_processRefs( $env, $refsData, $c );
|
||||
if ( empty( $cDp->empty ) && self::hasRef( $c ) ) { // nested ref-in-ref
|
||||
self::processRefs( $env, $refsData, $c );
|
||||
}
|
||||
DOMDataUtils::visitAndStoreDataAttribs( $c );
|
||||
|
||||
// Use the about attribute on the wrapper with priority, since it's
|
||||
// only added when the wrapper is a template sibling.
|
||||
$about = ( $node->hasAttribute( 'about' ) ) ? $node->getAttribute( 'about' ) :
|
||||
( $c->hasAttribute( 'about' ) ) ? $c->getAttribute( 'about' ) : '';
|
||||
$about = $node->hasAttribute( 'about' )
|
||||
? $node->getAttribute( 'about' )
|
||||
: $c->getAttribute( 'about' );
|
||||
|
||||
// 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?
|
||||
$group = $refDmw->attrs->group || $referencesGroup || '';
|
||||
$refName = $refDmw->attrs->name || '';
|
||||
$group = $refDmw->attrs['group'] ?? $referencesGroup ?? null;
|
||||
$refName = $refDmw->attrs['name'] ?? '';
|
||||
$ref = $refsData->add( $env, $group, $refName, $about, $nestedInReferences );
|
||||
|
||||
// Add ref-index linkback
|
||||
$linkBack = $doc->createElement( 'sup' );
|
||||
|
||||
// FIXME: Lot of useless work for an edge case
|
||||
if ( $cDp->empty ) {
|
||||
if ( !empty( $cDp->empty ) ) {
|
||||
// Discard wrapper if there was no input wikitext
|
||||
$content = null;
|
||||
if ( $cDp->selfClose ) {
|
||||
$refDmw->body = null;
|
||||
if ( !empty( $cDp->selfClose ) ) {
|
||||
unset( $refDmw->body );
|
||||
} else {
|
||||
$refDmw->body = [ 'html' => '' ];
|
||||
}
|
||||
|
@ -178,14 +177,15 @@ class References {
|
|||
DOMDataUtils::addAttributes( $linkBack, [
|
||||
'about' => $about,
|
||||
'class' => 'mw-ref',
|
||||
'id' => ( $nestedInReferences ) ? null :
|
||||
( ( $ref->name ) ? $ref->linkbacks[ count( $ref->linkbacks ) - 1 ] : $ref->id ),
|
||||
'id' => $nestedInReferences
|
||||
? null
|
||||
: ( $ref->name ? end( $ref->linkbacks ) : $ref->id ),
|
||||
'rel' => 'dc:references',
|
||||
'typeof' => $nodeType
|
||||
]
|
||||
);
|
||||
DOMDataUtils::addTypeOf( $linkBack, 'mw:Extension/ref' );
|
||||
$dataParsoid = [
|
||||
$dataParsoid = (object)[
|
||||
'src' => $nodeDp->src,
|
||||
'dsr' => $nodeDp->dsr,
|
||||
'pi' => $nodeDp->pi
|
||||
|
@ -199,9 +199,13 @@ class References {
|
|||
|
||||
// refLink is the link to the citation
|
||||
$refLink = $doc->createElement( 'a' );
|
||||
$title = Title::newFromText(
|
||||
$env->getPageConfig()->getTitle() . '#' . $ref->target,
|
||||
$env->getSiteConfig()
|
||||
);
|
||||
DOMDataUtils::addAttributes( $refLink, [
|
||||
'href' => $env->page->titleURI . '#' . $ref->target,
|
||||
'style' => 'counter-reset: mw-Ref ' . $ref->groupIndex . ';'
|
||||
'href' => $env->makeLink( $title ),
|
||||
'style' => 'counter-reset: mw-Ref ' . $ref->groupIndex . ';',
|
||||
]
|
||||
);
|
||||
if ( $ref->group ) {
|
||||
|
@ -212,8 +216,8 @@ class References {
|
|||
// 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 . ']'
|
||||
$refLinkSpan->appendChild( $doc->createTextNode(
|
||||
'[' . ( $ref->group ? $ref->group . ' ' : '' ) . $ref->groupIndex . ']'
|
||||
)
|
||||
);
|
||||
$refLink->appendChild( $refLinkSpan );
|
||||
|
@ -230,27 +234,36 @@ class References {
|
|||
// Keep the first content to compare multiple <ref>s with the same name.
|
||||
if ( !$ref->content ) {
|
||||
$ref->content = $content;
|
||||
$ref->dir = strtolower( $refDmw->attrs->dir || '' );
|
||||
$ref->dir = strtolower( $refDmw->attrs['dir'] ?? '' );
|
||||
}
|
||||
}
|
||||
|
||||
public static function insertReferencesIntoDOM( $refsNode, $refsData, $nestedRefsHTML, $autoGenerated ) {
|
||||
$env = $refsData->env;
|
||||
$isTplWrapper = preg_match( '/\bmw:Transclusion\b/', $refsNode->getAttribute( 'typeof' ) || '' );
|
||||
/**
|
||||
* @param DOMElement $refsNode
|
||||
* @param ReferencesData $refsData
|
||||
* @param array $nestedRefsHTML
|
||||
* @param bool $autoGenerated
|
||||
*/
|
||||
private static function insertReferencesIntoDOM(
|
||||
DOMElement $refsNode, ReferencesData $refsData, array $nestedRefsHTML, bool $autoGenerated = false
|
||||
): void {
|
||||
$env = $refsData->getEnv();
|
||||
$isTplWrapper = preg_match( '/\bmw:Transclusion\b/', $refsNode->getAttribute( 'typeof' ) );
|
||||
$dp = DOMDataUtils::getDataParsoid( $refsNode );
|
||||
$group = $dp->group || '';
|
||||
$group = $dp->group ?? '';
|
||||
if ( !$isTplWrapper ) {
|
||||
$dataMw = DOMDataUtils::getDataMw( $refsNode );
|
||||
if ( !count( Object::keys( $dataMw ) ) ) {
|
||||
if ( !count( (array)$dataMw ) ) {
|
||||
// FIXME: This can be moved to `insertMissingReferencesIntoDOM`
|
||||
Assert::invariant( $autoGenerated );
|
||||
$dataMw = [
|
||||
Assert::invariant( $autoGenerated, 'Expected non empty $dataMw or $autoGenerated is true' );
|
||||
$dataMw = (object)[
|
||||
'name' => 'references',
|
||||
'attrs' => [
|
||||
'group' => $group || null
|
||||
]
|
||||
]; // Dont emit empty keys
|
||||
|
||||
'attrs' => [],
|
||||
];
|
||||
// Dont emit empty keys
|
||||
if ( $group ) {
|
||||
$dataMw->attrs['group'] = $group;
|
||||
}
|
||||
DOMDataUtils::setDataMw( $refsNode, $dataMw );
|
||||
}
|
||||
|
||||
|
@ -259,22 +272,23 @@ class References {
|
|||
if ( $autoGenerated ) {
|
||||
$dataMw->autoGenerated = true;
|
||||
} elseif ( count( $nestedRefsHTML ) > 0 ) {
|
||||
$dataMw->body = [ 'html' => "\n" . implode( '', $nestedRefsHTML ) ];
|
||||
} elseif ( !$dp->selfClose ) {
|
||||
$dataMw->body = [ 'html' => '' ];
|
||||
$dataMw->body = (object)[ 'html' => "\n" . implode( $nestedRefsHTML ) ];
|
||||
} elseif ( empty( $dp->selfClose ) ) {
|
||||
$dataMw->body = (object)[ 'html' => '' ];
|
||||
} else {
|
||||
$dataMw->body = null;
|
||||
unset( $dataMw->body );
|
||||
}
|
||||
$dp->selfClose = null;
|
||||
// @phan-suppress-next-line PhanTypeObjectUnsetDeclaredProperty
|
||||
unset( $dp->selfClose );
|
||||
}
|
||||
|
||||
$refGroup = $refsData->getRefGroup( $group );
|
||||
|
||||
// Deal with responsive wrapper
|
||||
if ( $refsNode->classList->contains( 'mw-references-wrap' ) ) {
|
||||
$rrOpts = $env->conf->wiki->responsiveReferences;
|
||||
if ( $refGroup && count( $refGroup->refs ) > $rrOpts->threshold ) {
|
||||
$refsNode->classList->add( 'mw-references-columns' );
|
||||
if ( DOMCompat::getClassList( $refsNode )->contains( 'mw-references-wrap' ) ) {
|
||||
$rrOpts = $env->getSiteConfig()->responsiveReferences();
|
||||
if ( $refGroup && count( $refGroup->refs ) > $rrOpts['threshold'] ) {
|
||||
DOMCompat::getClassList( $refsNode )->add( 'mw-references-columns' );
|
||||
}
|
||||
$refsNode = $refsNode->firstChild;
|
||||
}
|
||||
|
@ -289,8 +303,9 @@ class References {
|
|||
}
|
||||
|
||||
if ( $refGroup ) {
|
||||
$refGroup->refs->forEach( function ( $ref ) use ( &$refGroup, &$env, &$refsNode ) {return $refGroup->renderLine( $env, $refsNode, $ref );
|
||||
} );
|
||||
foreach ( $refGroup->refs as $ref ) {
|
||||
$refGroup->renderLine( $env, $refsNode, $ref );
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the group from refsData
|
||||
|
@ -301,20 +316,32 @@ class References {
|
|||
* 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 ReferencesData $refsData
|
||||
* @param DOMNode $node
|
||||
*/
|
||||
public static function insertMissingReferencesIntoDOM( $refsData, $node ) {
|
||||
$env = $refsData->env;
|
||||
public static function insertMissingReferencesIntoDOM(
|
||||
ReferencesData $refsData, DOMNode $node
|
||||
): void {
|
||||
$env = $refsData->getEnv();
|
||||
$doc = $node->ownerDocument;
|
||||
|
||||
$refsData->refGroups->forEach( function ( $refsValue, $refsGroup ) use ( &$env, &$doc, &$node, &$refsData ) {
|
||||
$frag = References::createReferences( $env, $doc, null, [
|
||||
foreach ( $refsData->getRefGroups() as $refsGroup ) {
|
||||
$frag = self::createReferences(
|
||||
$env,
|
||||
$doc,
|
||||
null,
|
||||
[
|
||||
'group' => $refsGroup,
|
||||
'responsive' => null
|
||||
], function ( $dp ) {
|
||||
'responsive' => null,
|
||||
],
|
||||
function ( $dp ) use ( $env ) {
|
||||
// The new references come out of "nowhere", so to make selser work
|
||||
// propertly, add a zero-sized DSR pointing to the end of the document.
|
||||
$dp->dsr = [ count( $env->page->src ), count( $env->page->src ), 0, 0 ];
|
||||
}, true
|
||||
// property, add a zero-sized DSR pointing to the end of the document.
|
||||
$contentLength = mb_strlen( $env->getPageMainContent() );
|
||||
$dp->dsr = [ $contentLength, $contentLength, 0, 0 ];
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
// Add a \n before the <ol> so that when serialized to wikitext,
|
||||
|
@ -322,39 +349,38 @@ class References {
|
|||
$node->appendChild( $doc->createTextNode( "\n" ) );
|
||||
$node->appendChild( $frag );
|
||||
|
||||
References::insertReferencesIntoDOM( $frag, $refsData, [ '' ], true );
|
||||
self::insertReferencesIntoDOM( $frag, $refsData, [ '' ], true );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static function lintHandler( $refs, $env, $tplInfo, $domLinter ) {
|
||||
// Nothing to do
|
||||
//
|
||||
// FIXME: Not entirely true for scenarios where the <ref> tags
|
||||
// are defined in the references section that is itself templated.
|
||||
//
|
||||
// {{1x|<references>\n<ref name='x'><b>foo</ref>\n</references>}}
|
||||
//
|
||||
// In this example, the references tag has the right tplInfo and
|
||||
// when the <ref> tag is processed in the body of the article where
|
||||
// it is accessed, there is no relevant template or dsr info available.
|
||||
//
|
||||
// Ignoring for now.
|
||||
return $refs->nextSibling;
|
||||
}
|
||||
|
||||
public static function _processRefs( $env, $refsData, $node ) {
|
||||
/**
|
||||
* @param Env $env
|
||||
* @param ReferencesData $refsData
|
||||
* @param DOMElement $node
|
||||
*/
|
||||
public static function processRefs( Env $env, ReferencesData $refsData, DOMElement $node ): void {
|
||||
$child = $node->firstChild;
|
||||
while ( $child !== null ) {
|
||||
$nextChild = $child->nextSibling;
|
||||
if ( DOMUtils::isElt( $child ) ) {
|
||||
if ( $child instanceof DOMElement ) {
|
||||
if ( WTUtils::isSealedFragmentOfType( $child, 'ref' ) ) {
|
||||
self::extractRefFromNode( $child, $refsData );
|
||||
} elseif ( preg_match( ( '/(?:^|\s)mw:Extension\/references(?=$|\s)/' ), $child->getAttribute( 'typeof' ) || '' ) ) {
|
||||
} elseif (
|
||||
preg_match(
|
||||
'#(?:^|\s)mw:Extension/references(?=$|\s)#',
|
||||
$child->getAttribute( 'typeof' ) ?? ''
|
||||
)
|
||||
) {
|
||||
$referencesId = $child->getAttribute( 'about' ) || '';
|
||||
$referencesGroup = DOMDataUtils::getDataParsoid( $child )->group;
|
||||
$referencesGroup = DOMDataUtils::getDataParsoid( $child )->group ?? null;
|
||||
$nestedRefsHTML = [];
|
||||
self::_processRefsInReferences( $refsData, $child, $referencesId, $referencesGroup, $nestedRefsHTML );
|
||||
self::processRefsInReferences(
|
||||
$refsData,
|
||||
$child,
|
||||
$referencesId,
|
||||
$referencesGroup,
|
||||
$nestedRefsHTML
|
||||
);
|
||||
self::insertReferencesIntoDOM( $child, $refsData, $nestedRefsHTML );
|
||||
} else {
|
||||
// inline media -- look inside the data-mw attribute
|
||||
|
@ -385,17 +411,17 @@ class References {
|
|||
* See T214994
|
||||
* ----------------------------------------------------------------- */
|
||||
$dmw = DOMDataUtils::getDataMw( $child );
|
||||
$caption = $dmw->caption;
|
||||
$caption = $dmw->caption ?? null;
|
||||
if ( $caption ) {
|
||||
// Extract the caption HTML, build the DOM, process refs,
|
||||
// serialize to HTML, update the caption HTML.
|
||||
$captionDOM = ContentUtils::ppToDOM( $env, $caption );
|
||||
self::_processRefs( $env, $refsData, $captionDOM );
|
||||
self::processRefs( $env, $refsData, $captionDOM );
|
||||
$dmw->caption = ContentUtils::ppToXML( $captionDOM, [ 'innerXML' => true ] );
|
||||
}
|
||||
}
|
||||
if ( $child->hasChildNodes() ) {
|
||||
self::_processRefs( $env, $refsData, $child );
|
||||
self::processRefs( $env, $refsData, $child );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -409,56 +435,116 @@ class References {
|
|||
* <references> <ref>foo</ref> </references>
|
||||
* <references> <ref>bar</ref> </references>
|
||||
* ```
|
||||
* @private
|
||||
*
|
||||
* @param ReferencesData $refsData
|
||||
* @param DOMElement $node
|
||||
* @param string $referencesId
|
||||
* @param string|null $referencesGroup
|
||||
* @param array $nestedRefsHTML
|
||||
*/
|
||||
public static function _processRefsInReferences( $refsData, $node, $referencesId, $referencesGroup, $nestedRefsHTML ) {
|
||||
private static function processRefsInReferences(
|
||||
ReferencesData $refsData, DOMElement $node, string $referencesId,
|
||||
?string $referencesGroup, array $nestedRefsHTML
|
||||
): void {
|
||||
$child = $node->firstChild;
|
||||
while ( $child !== null ) {
|
||||
$nextChild = $child->nextSibling;
|
||||
if ( DOMUtils::isElt( $child ) ) {
|
||||
if ( $child instanceof DOMElement ) {
|
||||
if ( WTUtils::isSealedFragmentOfType( $child, 'ref' ) ) {
|
||||
self::extractRefFromNode( $child, $refsData, $referencesId, $referencesGroup, $nestedRefsHTML );
|
||||
self::extractRefFromNode(
|
||||
$child,
|
||||
$refsData,
|
||||
$referencesId,
|
||||
$referencesGroup,
|
||||
$nestedRefsHTML
|
||||
);
|
||||
} elseif ( $child->hasChildNodes() ) {
|
||||
self::_processRefsInReferences( $refsData, $child, $referencesId, $referencesGroup, $nestedRefsHTML );
|
||||
self::processRefsInReferences(
|
||||
$refsData,
|
||||
$child,
|
||||
$referencesId,
|
||||
$referencesGroup,
|
||||
$nestedRefsHTML
|
||||
);
|
||||
}
|
||||
}
|
||||
$child = $nextChild;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
References::serialHandler = [
|
||||
'handle' => /* async */function ( $node, $state, $wrapperUnmodified ) use ( &$DOMDataUtils ) {
|
||||
/** @inheritDoc */
|
||||
public function toDOM( ParsoidExtensionAPI $extApi, string $txt, array $extArgs ): DOMDocument {
|
||||
$doc = $extApi->parseTokenContentsToDOM(
|
||||
$extArgs,
|
||||
'',
|
||||
$txt,
|
||||
[
|
||||
'wrapperTag' => 'div',
|
||||
'extTag' => 'references',
|
||||
'inTemplate' => $extApi->parseContext['inTemplate'] ?? null,
|
||||
]
|
||||
);
|
||||
|
||||
$refsOpts = TokenUtils::kvToHash( $extArgs, true ) + [
|
||||
'group' => null,
|
||||
'responsive' => null,
|
||||
];
|
||||
|
||||
$docBody = DOMCompat::getBody( $doc );
|
||||
$frag = self::createReferences(
|
||||
$extApi->getEnv(),
|
||||
$doc,
|
||||
$docBody,
|
||||
$refsOpts,
|
||||
function ( $dp ) use ( $extApi, $docBody ) {
|
||||
$dp->src = $extApi->getExtSource();
|
||||
// Setting redundant info on fragment.
|
||||
// $docBody->firstChild info feels cumbersome to use downstream.
|
||||
$wrapperNode = $docBody->firstChild;
|
||||
DOMUtils::assertElt( $wrapperNode );
|
||||
if ( !empty( DOMDataUtils::getDataParsoid( $wrapperNode )->selfClose ) ) {
|
||||
$dp->selfClose = true;
|
||||
}
|
||||
}
|
||||
);
|
||||
DOMCompat::getBody( $doc )->appendChild( $frag );
|
||||
return $doc;
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function fromHTML(
|
||||
DOMElement $node, SerializerState $state, bool $wrapperUnmodified
|
||||
): string {
|
||||
$dataMw = DOMDataUtils::getDataMw( $node );
|
||||
if ( $dataMw->autoGenerated && $state->rtTestMode ) {
|
||||
if ( !empty( $dataMw->autoGenerated ) && $state->rtTestMode ) {
|
||||
// Eliminate auto-inserted <references /> noise in rt-testing
|
||||
return '';
|
||||
} else {
|
||||
$startTagSrc = /* await */ $state->serializer->serializeExtensionStartTag( $node, $state );
|
||||
if ( !$dataMw->body ) {
|
||||
$startTagSrc = $state->serializer->serializeExtensionStartTag( $node, $state );
|
||||
if ( empty( $dataMw->body ) ) {
|
||||
return $startTagSrc; // We self-closed this already.
|
||||
} else { // We self-closed this already.
|
||||
if ( gettype( $dataMw->body->html ) === 'string' ) {
|
||||
$src = /* await */ $state->serializer->serializeHTML( [
|
||||
'env' => $state->env,
|
||||
'extName' => $dataMw->name
|
||||
], $dataMw->body->html
|
||||
);
|
||||
return $startTagSrc + $src . '</' . $dataMw->name . '>';
|
||||
} else {
|
||||
$state->env->log( 'error',
|
||||
'References body unavailable for: ' . $node->outerHTML
|
||||
);
|
||||
return ''; // Drop it!
|
||||
}
|
||||
if ( is_string( $dataMw->body->html ) ) {
|
||||
$src = $state->serializer->serializeHTML(
|
||||
[
|
||||
'env' => $state->getEnv(),
|
||||
'extName' => $dataMw->name,
|
||||
],
|
||||
$dataMw->body->html
|
||||
);
|
||||
return $startTagSrc . $src . '</' . $dataMw->name . '>';
|
||||
} else {
|
||||
$state->getEnv()->log( 'error',
|
||||
'References body unavailable for: ' . DOMCompat::getOuterHTML( $node )
|
||||
);
|
||||
return ''; // Drop it!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, // Drop it!
|
||||
|
||||
// FIXME: LEAKY -- Should we expose newline constraints to extensions?
|
||||
'before' => function ( $node, $otherNode, $state ) use ( &$WTUtils ) {
|
||||
/** @inheritDoc */
|
||||
public function before( DOMElement $node, DOMNode $otherNode, SerializerState $state ): ?array {
|
||||
// Serialize new references tags on a new line.
|
||||
if ( WTUtils::isNewElt( $node ) ) {
|
||||
return [ 'min' => 1, 'max' => 2 ];
|
||||
|
@ -466,6 +552,28 @@ References::serialHandler = [
|
|||
return null;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
$module->exports = $References;
|
||||
/** @inheritDoc */
|
||||
public function hasLintHandler(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function lintHandler(
|
||||
ParsoidExtensionAPI $extApi, DOMElement $refs, callable $defaultHandler
|
||||
): ?DOMNode {
|
||||
// Nothing to do
|
||||
//
|
||||
// FIXME: Not entirely true for scenarios where the <ref> tags
|
||||
// are defined in the references section that is itself templated.
|
||||
//
|
||||
// {{1x|<references>\n<ref name='x'><b>foo</ref>\n</references>}}
|
||||
//
|
||||
// In this example, the references tag has the right tplInfo and
|
||||
// when the <ref> tag is processed in the body of the article where
|
||||
// it is accessed, there is no relevant template or dsr info available.
|
||||
//
|
||||
// Ignoring for now.
|
||||
return $refs->nextSibling;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +1,45 @@
|
|||
<?php
|
||||
// phpcs:ignoreFile
|
||||
// phpcs:disable Generic.Files.LineLength.TooLong
|
||||
/* REMOVE THIS COMMENT AFTER PORTING */
|
||||
namespace Parsoid;
|
||||
declare( strict_types = 1 );
|
||||
|
||||
$ParsoidExtApi = $module->parent->parent->parent->require( './extapi.js' )->versionCheck( '^0.10.0' );
|
||||
$temp0 = $ParsoidExtApi;
|
||||
$ContentUtils = $temp0::ContentUtils;
|
||||
$Sanitizer = $temp0::Sanitizer;
|
||||
namespace Parsoid\Ext\Cite;
|
||||
|
||||
$RefGroup = require './RefGroup.js';
|
||||
use Parsoid\Config\Env;
|
||||
use Parsoid\Utils\ContentUtils;
|
||||
use Parsoid\Wt2Html\TT\Sanitizer;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @class
|
||||
*/
|
||||
class ReferencesData {
|
||||
public function __construct( $env ) {
|
||||
|
||||
/**
|
||||
* @var Env
|
||||
*/
|
||||
private $env;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $index;
|
||||
|
||||
/**
|
||||
* @var RefGroup[]
|
||||
*/
|
||||
private $refGroups;
|
||||
|
||||
/**
|
||||
* ReferencesData constructor.
|
||||
* @param Env $env
|
||||
*/
|
||||
public function __construct( Env $env ) {
|
||||
$this->index = 0;
|
||||
$this->env = $env;
|
||||
$this->refGroups = new Map();
|
||||
$this->refGroups = [];
|
||||
}
|
||||
public $env;
|
||||
public $index;
|
||||
|
||||
public $refGroups;
|
||||
|
||||
public function makeValidIdAttr( $val ) {
|
||||
/**
|
||||
* @param string $val
|
||||
* @return bool|string
|
||||
*/
|
||||
public function makeValidIdAttr( string $val ) {
|
||||
// Looks like Cite.php doesn't try to fix ids that already have
|
||||
// a "_" in them. Ex: name="a b" and name="a_b" are considered
|
||||
// identical. Not sure if this is a feature or a bug.
|
||||
|
@ -36,33 +50,52 @@ class ReferencesData {
|
|||
return Sanitizer::escapeIdForAttribute( $val );
|
||||
}
|
||||
|
||||
public function getRefGroup( $groupName, $allocIfMissing ) {
|
||||
$groupName = $groupName || '';
|
||||
if ( !$this->refGroups->has( $groupName ) && $allocIfMissing ) {
|
||||
$this->refGroups->set( $groupName, new RefGroup( $groupName ) );
|
||||
/**
|
||||
* @param string $groupName
|
||||
* @param bool $allocIfMissing
|
||||
* @return RefGroup|null
|
||||
*/
|
||||
public function getRefGroup( string $groupName = '', bool $allocIfMissing = false ): ?RefGroup {
|
||||
if ( !isset( $this->refGroups[$groupName] ) && $allocIfMissing ) {
|
||||
$this->refGroups[$groupName] = new RefGroup( $groupName );
|
||||
}
|
||||
return $this->refGroups->get( $groupName );
|
||||
return $this->refGroups[$groupName] ?? null;
|
||||
}
|
||||
|
||||
public function removeRefGroup( $groupName ) {
|
||||
if ( $groupName !== null && $groupName !== null ) {
|
||||
/**
|
||||
* @param string|null $groupName
|
||||
*/
|
||||
public function removeRefGroup( ?string $groupName = null ): void {
|
||||
if ( $groupName !== null ) {
|
||||
// '' is a valid group (the default group)
|
||||
$this->refGroups->delete( $groupName );
|
||||
unset( $this->refGroups[$groupName] );
|
||||
}
|
||||
}
|
||||
|
||||
public function add( $env, $groupName, $refName, $about, $skipLinkback ) {
|
||||
/**
|
||||
* @param Env $env
|
||||
* @param string $groupName
|
||||
* @param string $refName
|
||||
* @param string $about
|
||||
* @param bool $skipLinkback
|
||||
* @return stdClass
|
||||
*/
|
||||
public function add(
|
||||
Env $env, string $groupName, string $refName, string $about, bool $skipLinkback
|
||||
): stdClass {
|
||||
$group = $this->getRefGroup( $groupName, true );
|
||||
$refName = $this->makeValidIdAttr( $refName );
|
||||
|
||||
$ref = null;
|
||||
if ( $refName && $group->indexByName->has( $refName ) ) {
|
||||
$ref = $group->indexByName->get( $refName );
|
||||
if ( $refName && isset( $group->indexByName[$refName] ) ) {
|
||||
$ref = $group->indexByName[$refName];
|
||||
if ( $ref->content && !$ref->hasMultiples ) {
|
||||
$ref->hasMultiples = true;
|
||||
// Use the non-pp version here since we've already stored attribs
|
||||
// before putting them in the map.
|
||||
$ref->cachedHtml = ContentUtils::toXML( $env->fragmentMap->get( $ref->content )[ 0 ], [ 'innerXML' => true ] );
|
||||
$ref->cachedHtml = ContentUtils::toXML(
|
||||
$env->getFragment( $ref->content )[0],
|
||||
[ 'innerXML' => true ]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// The ids produced Cite.php have some particulars:
|
||||
|
@ -71,14 +104,14 @@ class ReferencesData {
|
|||
// Notes (references) whose ref doesn't have a name are 'cite_note-' + index
|
||||
// Notes whose ref has a name are 'cite_note-' + name + '-' + index
|
||||
$n = $this->index;
|
||||
$refKey = ( 1 + $n ) . '';
|
||||
$refIdBase = 'cite_ref-' . ( ( $refName ) ? $refName . '_' . $refKey : $refKey );
|
||||
$noteId = 'cite_note-' . ( ( $refName ) ? $refName . '-' . $refKey : $refKey );
|
||||
$refKey = strval( 1 + $n );
|
||||
$refIdBase = 'cite_ref-' . ( $refName ? $refName . '_' . $refKey : $refKey );
|
||||
$noteId = 'cite_note-' . ( $refName ? $refName . '-' . $refKey : $refKey );
|
||||
|
||||
// bump index
|
||||
$this->index += 1;
|
||||
|
||||
$ref = [
|
||||
$ref = (object)[
|
||||
'about' => $about,
|
||||
'content' => null,
|
||||
'dir' => '',
|
||||
|
@ -86,7 +119,7 @@ class ReferencesData {
|
|||
'groupIndex' => count( $group->refs ) + 1,
|
||||
'index' => $n,
|
||||
'key' => $refIdBase,
|
||||
'id' => ( ( $refName ) ? $refIdBase . '-0' : $refIdBase ),
|
||||
'id' => $refName ? $refIdBase . '-0' : $refIdBase,
|
||||
'linkbacks' => [],
|
||||
'name' => $refName,
|
||||
'target' => $noteId,
|
||||
|
@ -96,7 +129,7 @@ class ReferencesData {
|
|||
];
|
||||
$group->refs[] = $ref;
|
||||
if ( $refName ) {
|
||||
$group->indexByName->set( $refName, $ref );
|
||||
$group->indexByName[$refName] = $ref;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,6 +138,18 @@ class ReferencesData {
|
|||
}
|
||||
return $ref;
|
||||
}
|
||||
}
|
||||
|
||||
$module->exports = $ReferencesData;
|
||||
/**
|
||||
* @return Env
|
||||
*/
|
||||
public function getEnv(): Env {
|
||||
return $this->env;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RefGroup[]
|
||||
*/
|
||||
public function getRefGroups(): array {
|
||||
return $this->refGroups;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue