2019-05-25 22:24:47 +00:00
|
|
|
<?php
|
|
|
|
declare( strict_types = 1 );
|
2019-04-02 22:06:21 +00:00
|
|
|
|
2020-02-03 18:52:06 +00:00
|
|
|
namespace Wikimedia\Parsoid\Ext\Cite;
|
2019-05-25 22:24:47 +00:00
|
|
|
|
|
|
|
use DOMElement;
|
|
|
|
use DOMNode;
|
|
|
|
use Exception;
|
2020-02-03 18:52:06 +00:00
|
|
|
use Wikimedia\Parsoid\Config\ParsoidExtensionAPI;
|
|
|
|
use Wikimedia\Parsoid\Ext\ExtensionTag;
|
|
|
|
use Wikimedia\Parsoid\Utils\DOMCompat;
|
|
|
|
use Wikimedia\Parsoid\Utils\DOMDataUtils;
|
|
|
|
use Wikimedia\Parsoid\Utils\DOMUtils;
|
|
|
|
use Wikimedia\Parsoid\Utils\WTUtils;
|
2019-04-02 22:06:21 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Simple token transform version of the Ref extension tag.
|
|
|
|
*/
|
2019-07-11 17:22:29 +00:00
|
|
|
class Ref extends ExtensionTag {
|
2019-05-25 22:24:47 +00:00
|
|
|
|
|
|
|
/** @inheritDoc */
|
2019-11-22 18:27:55 +00:00
|
|
|
public function toDOM( ParsoidExtensionAPI $extApi, string $txt, array $extArgs ) {
|
2019-04-02 22:06:21 +00:00
|
|
|
// Drop nested refs entirely, unless we've explicitly allowed them
|
2020-01-17 13:12:15 +00:00
|
|
|
$parentExtTag = $extApi->parentExtTag();
|
|
|
|
if ( $parentExtTag === 'ref' && empty( $extApi->parentExtTagOpts()['allowNestedRef'] ) ) {
|
2019-08-09 20:48:03 +00:00
|
|
|
return null;
|
2019-04-02 22:06:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// The one supported case for nested refs is from the {{#tag:ref}} parser
|
|
|
|
// 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.
|
2020-01-17 13:12:15 +00:00
|
|
|
$allowNestedRef = !empty( $extApi->inTemplate() ) && $parentExtTag !== 'ref';
|
2019-04-02 22:06:21 +00:00
|
|
|
|
2019-05-25 22:24:47 +00:00
|
|
|
return $extApi->parseTokenContentsToDOM(
|
2019-11-22 18:27:55 +00:00
|
|
|
$extArgs,
|
2019-05-25 22:24:47 +00:00
|
|
|
'',
|
|
|
|
$txt,
|
|
|
|
[
|
2019-04-02 22:06:21 +00:00
|
|
|
// 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',
|
2019-04-19 19:38:27 +00:00
|
|
|
'pipelineOpts' => [
|
|
|
|
'extTag' => 'ref',
|
|
|
|
'extTagOpts' => [ 'allowNestedRef' => $allowNestedRef ],
|
|
|
|
// FIXME: One-off PHP parser state leak.
|
|
|
|
// This needs a better solution.
|
|
|
|
'inPHPBlock' => true,
|
|
|
|
],
|
2019-04-02 22:06:21 +00:00
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-05-25 22:24:47 +00:00
|
|
|
/** @inheritDoc */
|
|
|
|
public function lintHandler(
|
|
|
|
ParsoidExtensionAPI $extApi, DOMElement $ref, callable $defaultHandler
|
|
|
|
): ?DOMNode {
|
2019-04-02 22:06:21 +00:00
|
|
|
// Don't lint the content of ref in ref, since it can lead to cycles
|
|
|
|
// using named refs
|
2019-05-25 22:24:47 +00:00
|
|
|
if ( WTUtils::fromExtensionContent( $ref, 'references' ) ) {
|
|
|
|
return $ref->nextSibling;
|
|
|
|
}
|
2019-04-02 22:06:21 +00:00
|
|
|
|
2019-05-25 22:24:47 +00:00
|
|
|
$refFirstChild = $ref->firstChild;
|
|
|
|
DOMUtils::assertElt( $refFirstChild );
|
|
|
|
$linkBackId = preg_replace( '/[^#]*#/', '', $refFirstChild->getAttribute( 'href' ), 1 );
|
2019-04-02 22:06:21 +00:00
|
|
|
$refNode = $ref->ownerDocument->getElementById( $linkBackId );
|
|
|
|
if ( $refNode ) {
|
|
|
|
// Ex: Buggy input wikitext without ref content
|
2019-05-25 22:24:47 +00:00
|
|
|
$defaultHandler( $refNode->lastChild );
|
2019-04-02 22:06:21 +00:00
|
|
|
}
|
2019-06-05 20:09:31 +00:00
|
|
|
return $ref->nextSibling;
|
2019-04-02 22:06:21 +00:00
|
|
|
}
|
|
|
|
|
2019-05-25 22:24:47 +00:00
|
|
|
/** @inheritDoc */
|
2020-01-16 14:55:36 +00:00
|
|
|
public function fromDOM(
|
2020-01-17 13:12:15 +00:00
|
|
|
ParsoidExtensionAPI $extApi, DOMElement $node, bool $wrapperUnmodified
|
|
|
|
) {
|
|
|
|
$startTagSrc = $extApi->serializeExtensionStartTag( $node );
|
2019-04-02 22:06:21 +00:00
|
|
|
$dataMw = DOMDataUtils::getDataMw( $node );
|
|
|
|
$html = null;
|
2019-06-24 18:10:35 +00:00
|
|
|
if ( !isset( $dataMw->body ) ) {
|
2019-04-02 22:06:21 +00:00
|
|
|
return $startTagSrc; // We self-closed this already.
|
|
|
|
} else { // We self-closed this already.
|
2019-05-25 22:24:47 +00:00
|
|
|
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 );
|
2020-02-24 22:06:41 +00:00
|
|
|
$editedDoc = $extApi->getPageConfig()->editedDoc ?? null;
|
2019-05-25 22:24:47 +00:00
|
|
|
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 );
|
|
|
|
}
|
|
|
|
if ( $bodyElt ) {
|
2020-02-10 22:34:56 +00:00
|
|
|
$html = ParsoidExtensionAPI::innerHTML( $bodyElt );
|
2019-05-25 22:24:47 +00:00
|
|
|
} 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 ) . ']';
|
|
|
|
}
|
2020-02-24 22:06:41 +00:00
|
|
|
$ref = DOMCompat::querySelector( $editedDoc, $href );
|
2019-05-25 22:24:47 +00:00
|
|
|
if ( $ref ) {
|
|
|
|
$extraDebug .= ' [main doc: ' . DOMCompat::getOuterHTML( $ref ) . ']';
|
|
|
|
}
|
|
|
|
} catch ( Exception $e ) {
|
2019-06-24 18:10:35 +00:00
|
|
|
// We are just providing VE with debugging info.
|
|
|
|
// So, ignore all exceptions / errors in this code.
|
|
|
|
}
|
|
|
|
|
2019-05-25 22:24:47 +00:00
|
|
|
if ( !$extraDebug ) {
|
|
|
|
$extraDebug = ' [reference ' . $href . ' not found]';
|
2019-04-02 22:06:21 +00:00
|
|
|
}
|
|
|
|
}
|
2020-01-17 13:12:15 +00:00
|
|
|
$extApi->log(
|
2019-05-25 22:24:47 +00:00
|
|
|
'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!
|
2019-04-02 22:06:21 +00:00
|
|
|
}
|
2019-05-25 22:24:47 +00:00
|
|
|
} else { // Drop it!
|
2020-01-17 13:12:15 +00:00
|
|
|
$extApi->log( 'error', 'Ref body unavailable for: ' . DOMCompat::getOuterHTML( $node ) );
|
2019-04-02 22:06:21 +00:00
|
|
|
return ''; // Drop it!
|
2019-05-25 22:24:47 +00:00
|
|
|
} // Drop it!
|
2019-04-02 22:06:21 +00:00
|
|
|
}
|
|
|
|
|
2020-01-17 13:12:15 +00:00
|
|
|
$src = $extApi->serializeHTML(
|
2019-05-25 22:24:47 +00:00
|
|
|
[
|
2020-01-17 13:12:15 +00:00
|
|
|
'extName' => $dataMw->name,
|
|
|
|
// FIXME: One-off PHP parser state leak.
|
2019-04-02 22:06:21 +00:00
|
|
|
// This needs a better solution.
|
|
|
|
'inPHPBlock' => true
|
2019-05-25 22:24:47 +00:00
|
|
|
],
|
|
|
|
$html
|
2019-04-02 22:06:21 +00:00
|
|
|
);
|
2019-05-25 22:24:47 +00:00
|
|
|
return $startTagSrc . $src . '</' . $dataMw->name . '>';
|
2019-04-02 22:06:21 +00:00
|
|
|
}
|
2019-05-25 22:24:47 +00:00
|
|
|
}
|