Page MenuHomeWickedGov Phorge

ParsoidExtensionAPI.php
No OneTemporary

Size
34 KB
Referenced Files
None
Subscribers
None

ParsoidExtensionAPI.php

<?php
declare( strict_types = 1 );
namespace Wikimedia\Parsoid\Ext;
use Closure;
use Wikimedia\Bcp47Code\Bcp47Code;
use Wikimedia\Parsoid\Config\Env;
use Wikimedia\Parsoid\Config\PageConfig;
use Wikimedia\Parsoid\Config\SiteConfig;
use Wikimedia\Parsoid\Core\ContentMetadataCollector;
use Wikimedia\Parsoid\Core\ContentMetadataCollectorStringSets as CMCSS;
use Wikimedia\Parsoid\Core\DomSourceRange;
use Wikimedia\Parsoid\Core\MediaStructure;
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\Html2Wt\ConstrainedText\WikiLinkText;
use Wikimedia\Parsoid\Html2Wt\LinkHandlerUtils;
use Wikimedia\Parsoid\Html2Wt\SerializerState;
use Wikimedia\Parsoid\NodeData\DataMwError;
use Wikimedia\Parsoid\Tokens\KV;
use Wikimedia\Parsoid\Tokens\SourceRange;
use Wikimedia\Parsoid\Utils\ContentUtils;
use Wikimedia\Parsoid\Utils\DOMCompat;
use Wikimedia\Parsoid\Utils\DOMDataUtils;
use Wikimedia\Parsoid\Utils\DOMUtils;
use Wikimedia\Parsoid\Utils\PipelineUtils;
use Wikimedia\Parsoid\Utils\Title;
use Wikimedia\Parsoid\Utils\TokenUtils;
use Wikimedia\Parsoid\Utils\Utils;
use Wikimedia\Parsoid\Utils\WTUtils;
use Wikimedia\Parsoid\Wikitext\Wikitext;
use Wikimedia\Parsoid\Wt2Html\DOM\Processors\AddMetaData;
use Wikimedia\Parsoid\Wt2Html\Frame;
/**
* Extensions are expected to use only these interfaces and strongly discouraged from
* calling Parsoid code directly. Code review is expected to catch these discouraged
* code patterns. We'll have to finish grappling with the extension and hooks API
* to go down this path seriously. Till then, we'll have extensions leveraging existing
* code as in the native extension code in this repository.
*/
class ParsoidExtensionAPI {
/** @var Env */
private $env;
/** @var ?Frame */
private $frame;
/** @var ?ExtensionTag */
public $extTag;
/**
* @var ?array TokenHandler options
*/
private $wt2htmlOpts;
/**
* @var ?array Serialiation options / state
*/
private $html2wtOpts;
/**
* @var ?SerializerState
*/
private $serializerState;
/**
* Errors collected while parsing. This is used to indicate that, although
* an extension is returning a dom fragment, errors were encountered while
* generating it and should be marked up with the mw:Error typeof.
*
* @var list<DataMwError>
*/
private $errors = [];
/**
* @param Env $env
* @param ?array $options
* - wt2html: used in wt->html direction
* - frame: (Frame)
* - parseOpts: (array)
* - extTag: (string)
* - extTagOpts: (array)
* - inTemplate: (bool)
* - extTag: (ExtensionTag)
* - html2wt: used in html->wt direction
* - state: (SerializerState)
*/
public function __construct(
Env $env, ?array $options = null
) {
$this->env = $env;
$this->wt2htmlOpts = $options['wt2html'] ?? null;
$this->html2wtOpts = $options['html2wt'] ?? null;
$this->serializerState = $this->html2wtOpts['state'] ?? null;
$this->frame = $this->wt2htmlOpts['frame'] ?? null;
$this->extTag = $this->wt2htmlOpts['extTag'] ?? null;
}
/**
* Collect errors while parsing. If processing can't continue, an
* ExtensionError should be thrown instead.
*
* $key and $params are basically the arguments to wfMessage, although they
* will be stored in the data-mw of the encapsulation wrapper.
*
* See https://www.mediawiki.org/wiki/Specs/HTML#Error_handling
*
* The returned fragment can be inserted in the dom and will be populated
* with the localized message. See T266666
*
* @unstable
* @param string $key
* @param mixed ...$params
* @return DocumentFragment
*/
public function pushError( string $key, ...$params ): DocumentFragment {
$this->errors[] = new DataMwError( $key, $params );
return WTUtils::createInterfaceI18nFragment( $this->getTopLevelDoc(), $key, $params );
}
/**
* Creates an internationalization (i18n) message that will be localized into the user
* interface language. The returned DocumentFragment contains, as a single child, a span
* element with the appropriate information for later localization.
* @param string $key message key for the message to be localized
* @param ?array $params parameters for localization
* @return DocumentFragment
*/
public function createInterfaceI18nFragment( string $key, ?array $params ): DocumentFragment {
return WTUtils::createInterfaceI18nFragment( $this->getTopLevelDoc(), $key, $params );
}
/**
* Creates an internationalization (i18n) message that will be localized into the page content
* language. The returned DocumentFragment contains, as a single child, a span
* element with the appropriate information for later localization.
* @param string $key message key for the message to be localized
* @param ?array $params parameters for localization
* @return DocumentFragment
*/
public function createPageContentI18nFragment( string $key, ?array $params ): DocumentFragment {
return WTUtils::createPageContentI18nFragment( $this->getTopLevelDoc(), $key, $params );
}
/**
* Creates an internationalization (i18n) message that will be localized into an arbitrary
* language. The returned DocumentFragment contains, as a single child, a span
* element with the appropriate information for later localization.
* The use of this method is discouraged; use ::createPageContentI18nFragment(...) and
* ::createInterfaceI18nFragment(...) where possible rather than, respectively,
* ::createLangI18nFragment($wgContLang, ...) and ::createLangI18nFragment($wgLang, ...).
* @param Bcp47Code $lang language in which the message will be localized
* @param string $key message key for the message to be localized
* @param ?array $params parameters for localization
* @return DocumentFragment
*/
public function createLangI18nFragment( Bcp47Code $lang, string $key, ?array $params ): DocumentFragment {
return WTUtils::createLangI18nFragment( $this->getTopLevelDoc(), $lang, $key, $params );
}
/**
* Adds to $element the internationalization information needed for the attribute $name to be
* localized in a later pass into the user interface language.
* @param Element $element element on which to add internationalization information
* @param string $name name of the attribute whose value will be localized
* @param string $key message key used for the attribute value localization
* @param ?array $params parameters for localization
*/
public function addInterfaceI18nAttribute(
Element $element, string $name, string $key, ?array $params
): void {
WTUtils::addInterfaceI18nAttribute( $element, $name, $key, $params );
}
/**
* Adds to $element the internationalization information needed for the attribute $name to be
* localized in a later pass into the page content language.
* @param Element $element element on which to add internationalization information
* @param string $name name of the attribute whose value will be localized
* @param string $key message key used for the attribute value localization
* @param array $params parameters for localization
*/
public function addPageContentI18nAttribute(
Element $element, string $name, string $key, array $params
): void {
WTUtils::addPageContentI18nAttribute( $element, $name, $key, $params );
}
/**
* Adds to $element the internationalization information needed for the attribute $name to be
* localized in a later pass into the provided language.
* The use of this method is discouraged; use ::addPageContentI18nAttribute(...) and
* ::addInterfaceI18nAttribute(...) where possible rather than, respectively,
* ::addLangI18nAttribute(..., $wgContLang, ...) and ::addLangI18nAttribute(..., $wgLang, ...).
* @param Element $element element on which to add internationalization information
* @param Bcp47Code $lang language in which the attribute will be localized
* @param string $name name of the attribute whose value will be localized
* @param string $key message key used for the attribute value localization
* @param array $params parameters for localization
*/
public function addLangI18nAttribute(
Element $element, Bcp47Code $lang, string $name, string $key, array $params
) {
WTUtils::addLangI18nAttribute( $element, $lang, $name, $key, $params );
}
/**
* @return list<DataMwError>
*/
public function getErrors(): array {
return $this->errors;
}
/**
* Returns the main document we're parsing. Extension content is parsed
* to fragments of this document.
*
* @return Document
*/
public function getTopLevelDoc(): Document {
return $this->env->topLevelDoc;
}
/**
* Get a new about id for marking extension output
* FIXME: This should never really be needed since the extension API
* handles this on behalf of extensions, but Cite has one use case
* where implicit <references /> output is added.
*
* @return string
*/
public function newAboutId(): string {
return $this->env->newAboutId();
}
/**
* Get the site configuration to let extensions customize
* their behavior based on how the wiki is configured.
*
* @return SiteConfig
*/
public function getSiteConfig(): SiteConfig {
return $this->env->getSiteConfig();
}
/**
* FIXME: Unsure if we need to provide this access yet
* Get the page configuration
* @return PageConfig
*/
public function getPageConfig(): PageConfig {
return $this->env->getPageConfig();
}
/**
* Get the ContentMetadataCollector corresponding to the top-level page.
* In Parsoid integrated mode this will typically be an instance of
* core's `ParserOutput` class.
*
* @return ContentMetadataCollector
*/
public function getMetadata(): ContentMetadataCollector {
return $this->env->getMetadata();
}
/**
* Get the URI to link to a title
* @param Title $title
* @return string
*/
public function getTitleUri( Title $title ): string {
return $this->env->makeLink( $title );
}
/**
* Get an URI for the current page
* @return string
*/
public function getPageUri(): string {
return $this->getTitleUri( $this->env->getContextTitle() );
}
/**
* Make a title from an input string
* @param string $str
* @param int $namespaceId
* @return ?Title
*/
public function makeTitle( string $str, int $namespaceId ): ?Title {
return $this->env->makeTitleFromText( $str, $namespaceId, true /* no exceptions */ );
}
/**
* Are we parsing in a template context?
* @return bool
*/
public function inTemplate(): bool {
return $this->wt2htmlOpts['parseOpts']['inTemplate'] ?? false;
}
/**
* Are we parsing for a preview?
* FIXME: Right now, we never do; when we do, this needs to be modified to reflect reality
* @unstable
* @return bool
*/
public function isPreview(): bool {
return false;
}
/**
* FIXME: Is this something that can come from the frame?
* If we are parsing in the context of a parent extension tag,
* return the name of that extension tag
* @return string|null
*/
public function parentExtTag(): ?string {
return $this->wt2htmlOpts['parseOpts']['extTag'] ?? null;
}
/**
* FIXME: Is this something that can come from the frame?
* If we are parsing in the context of a parent extension tag,
* return the parsing options set by that tag.
* @return array
*/
public function parentExtTagOpts(): array {
return $this->wt2htmlOpts['parseOpts']['extTagOpts'] ?? [];
}
/**
* Get the content DOM corresponding to an id
* @param string $contentId
* @return DocumentFragment
*/
public function getContentDOM( string $contentId ): DocumentFragment {
return $this->env->getDOMFragment( $contentId );
}
public function clearContentDOM( string $contentId ): void {
$this->env->removeDOMFragment( $contentId );
}
/**
* Parse wikitext to DOM
*
* @param string $wikitext
* @param array $opts
* - srcOffsets
* - processInNewFrame
* - clearDSROffsets
* - shiftDSRFn
* - parseOpts
* - extTag
* - extTagOpts
* - context "inline", "block", etc. Currently, only "inline" is supported
* @param bool $sol Whether tokens should be processed in start-of-line context.
* @return DocumentFragment
*/
public function wikitextToDOM(
string $wikitext, array $opts, bool $sol
): DocumentFragment {
if ( $wikitext === '' ) {
$domFragment = $this->getTopLevelDoc()->createDocumentFragment();
} else {
// Parse content to DOM and pass DOM-fragment token back to the main pipeline.
// The DOM will get unwrapped and integrated when processing the top level document.
$parseOpts = $opts['parseOpts'] ?? [];
$srcOffsets = $opts['srcOffsets'] ?? null;
$frame = $this->frame;
if ( !empty( $opts['processInNewFrame'] ) ) {
$frame = $frame->newChild( $frame->getTitle(), [], $wikitext );
$srcOffsets = new SourceRange( 0, strlen( $wikitext ) );
}
$domFragment = PipelineUtils::processContentInPipeline(
$this->env, $frame, $wikitext,
[
// Full pipeline for processing content
'pipelineType' => 'wikitext-to-fragment',
'pipelineOpts' => [
'expandTemplates' => true,
'extTag' => $parseOpts['extTag'],
'extTagOpts' => $parseOpts['extTagOpts'] ?? null,
'inTemplate' => $this->inTemplate(),
'inlineContext' => ( $parseOpts['context'] ?? '' ) === 'inline',
],
'srcOffsets' => $srcOffsets,
'sol' => $sol,
]
);
if ( !empty( $opts['clearDSROffsets'] ) ) {
$dsrFn = static function ( DomSourceRange $dsr ) {
return null;
};
} else {
$dsrFn = $opts['shiftDSRFn'] ?? null;
}
if ( $dsrFn ) {
ContentUtils::shiftDSR( $this->env, $domFragment, $dsrFn, $this );
}
}
return $domFragment;
}
/**
* Parse extension tag to DOM.
*
* If a wrapper tag is requested, beyond parsing the contents of the extension tag,
* this method wraps the contents in a custom wrapper element (ex: <div>), sanitizes
* the arguments of the extension args and sets some content flags on the wrapper.
*
* @param array $extArgs Args sanitized and applied to wrapper
* @param string $wikitext Wikitext content of the tag
* @param array $opts
* - srcOffsets
* - wrapperTag (skip OR pass null to not add any wrapper tag)
* - processInNewFrame
* - clearDSROffsets
* - shiftDSRFn
* - parseOpts
* - extTag
* - extTagOpts
* - context
* @return DocumentFragment
*/
public function extTagToDOM(
array $extArgs, string $wikitext, array $opts
): DocumentFragment {
$extTagOffsets = $this->extTag->getOffsets();
if ( !isset( $opts['srcOffsets'] ) ) {
$opts['srcOffsets'] = new SourceRange(
$extTagOffsets->innerStart(),
$extTagOffsets->innerEnd()
);
}
$domFragment = $this->wikitextToDOM( $wikitext, $opts, true /* sol */ );
if ( !empty( $opts['wrapperTag'] ) ) {
// Create a wrapper and migrate content into the wrapper
$wrapper = $domFragment->ownerDocument->createElement(
$opts['wrapperTag']
);
DOMUtils::migrateChildren( $domFragment, $wrapper );
$domFragment->appendChild( $wrapper );
// Sanitize args and set on the wrapper
Sanitizer::applySanitizedArgs( $this->env->getSiteConfig(), $wrapper, $extArgs );
// Mark empty content DOMs
if ( $wikitext === '' ) {
DOMDataUtils::getDataParsoid( $wrapper )->empty = true;
}
if ( !empty( $this->extTag->isSelfClosed() ) ) {
DOMDataUtils::getDataParsoid( $wrapper )->selfClose = true;
}
}
return $domFragment;
}
/**
* Set temporary data into the DOM node that will be discarded
* when DOM is serialized
*
* Use the tag name as the key for TempData management
*
* @param Element $node
* @param mixed $data
*/
public function setTempNodeData( Element $node, $data ): void {
$dataParsoid = DOMDataUtils::getDataParsoid( $node );
$tmpData = $dataParsoid->getTemp();
$key = $this->extTag->getName();
$tmpData->setTagData( $key, $data );
}
/**
* Get temporary data into the DOM node that will be discarded
* when DOM is serialized.
*
* This should only be used when the ExtensionTag is not available; otherwise access the newly created data
* directly.
*
* @param Element $node
* @param string $key to access TmpData
* @return mixed
* @unstable
*/
public function getTempNodeData( Element $node, string $key ) {
if ( $this->extTag ) {
throw new \RuntimeException(
'ExtensionTag is available. Data should be available directly through the DOM.'
);
}
$dataParsoid = DOMDataUtils::getDataParsoid( $node );
$tmpData = $dataParsoid->getTemp();
return $tmpData->getTagData( $key );
}
/**
* Process a specific extension arg as wikitext and return its DOM equivalent.
* By default, this method processes the argument value in inline context and normalizes
* every whitespace character to a single space.
* @param KV[] $extArgs
* @param string $key should be lower-case
* @param string $context
* @return ?DocumentFragment
*/
public function extArgToDOM(
array $extArgs, string $key, string $context = "inline"
): ?DocumentFragment {
$argKV = KV::lookupKV( $extArgs, strtolower( $key ) );
if ( $argKV === null || !$argKV->v ) {
return null;
}
if ( $context === "inline" ) {
// `normalizeExtOptions` can mess up source offsets as well as the string
// that ought to be processed as wikitext. So, we do our own whitespace
// normalization of the original source here.
//
// If 'context' is 'inline' below, it ensures indent-pre / p-wrapping is suppressed.
// So, the normalization is primarily for HTML string parity.
$argVal = preg_replace( '/[\t\r\n ]/', ' ', $argKV->vsrc );
} else {
$argVal = $argKV->vsrc;
}
return $this->wikitextToDOM(
$argVal,
[
'parseOpts' => [
'extTag' => $this->extTag->getName(),
'context' => $context,
],
'srcOffsets' => $argKV->valueOffset(),
],
false // inline context => no sol state
);
}
/**
* Convert the ext args representation from an array of KV objects
* to a plain associative array mapping arg name strings to arg value strings.
* @param array<KV> $extArgs
* @return array<string,string>
*/
public function extArgsToArray( array $extArgs ): array {
return TokenUtils::kvToHash( $extArgs );
}
/**
* This method finds a requested arg by key name and return its current value.
* If a closure is passed in to update the current value, it is used to update the arg.
*
* @param KV[] &$extArgs Array of extension args
* @param string $key Argument key whose value needs an update
* @param ?Closure $updater $updater will get the existing string value
* for the arg and is expected to return an updated value.
* @return ?string
*/
public function findAndUpdateArg(
array &$extArgs, string $key, ?Closure $updater = null
): ?string {
// FIXME: This code will get an overhaul when T250854 is resolved.
foreach ( $extArgs as $i => $kv ) {
$k = TokenUtils::tokensToString( $kv->k );
if ( strtolower( trim( $k ) ) === strtolower( $key ) ) {
$val = $kv->v;
if ( $updater ) {
$kv = clone $kv;
$kv->v = $updater( TokenUtils::tokensToString( $val ) );
$extArgs[$i] = $kv;
}
return $val;
}
}
return null;
}
/**
* This method adds a new argument to the extension args array
* @param KV[] &$extArgs
* @param string $key
* @param string $value
*/
public function addNewArg( array &$extArgs, string $key, string $value ): void {
$extArgs[] = new KV( $key, $value );
}
// TODO: Provide support for extensions to register lints
// from their customized lint handlers.
/**
* Forwards the logging request to the underlying logger
* @param string $prefix
* @param mixed ...$args
*/
public function log( string $prefix, ...$args ): void {
$this->env->log( $prefix, ...$args );
}
/**
* Extensions might be interested in examining (their) content embedded
* in data-mw attributes that don't otherwise show up in the DOM.
*
* Ex: inline media captions that aren't rendered, language variant markup,
* attributes that are transcluded. More scenarios might be added later.
*
* @param Element $elt The node whose data attributes need to be examined
* @param Closure $proc The processor that will process the embedded HTML
* Signature: (string) -> string
* This processor will be provided the HTML string as input
* and is expected to return a possibly modified string.
*/
public function processAttributeEmbeddedHTML( Element $elt, Closure $proc ): void {
ContentUtils::processAttributeEmbeddedHTML( $this, $elt, $proc );
}
/**
* Copy $from->childNodes to $to and clone the data attributes of $from
* to $to.
*
* @param Element $from
* @param Element $to
*/
public static function migrateChildrenAndTransferWrapperDataAttribs(
Element $from, Element $to
): void {
DOMUtils::migrateChildren( $from, $to );
DOMDataUtils::setDataParsoid(
$to, DOMDataUtils::getDataParsoid( $from )->clone()
);
DOMDataUtils::setDataMw(
$to, Utils::clone( DOMDataUtils::getDataMw( $from ) )
);
}
/**
* Equivalent of 'preprocess' from Parser.php in core.
* - expands templates
* - replaces magic variables
* This does not run any hooks however since that would be unexpected.
* This also doesn't support replacing template args from a frame.
*
* @param string $wikitext
* @return string preprocessed wikitext
*/
public function preprocessWikitext( string $wikitext ): string {
return Wikitext::preprocess( $this->env, $wikitext )['src'];
}
/**
* Parse input string into DOM.
* NOTE: This leaves the DOM in Parsoid-canonical state and is the preferred method
* to convert HTML to DOM that will be passed into Parsoid's processing code.
*
* @param string $html
* @param ?Document $doc XXX You probably don't want to be doing this
* @param ?array $options
* @return DocumentFragment
*/
public function htmlToDom(
string $html, ?Document $doc = null, ?array $options = []
): DocumentFragment {
return ContentUtils::createAndLoadDocumentFragment(
$doc ?? $this->getTopLevelDoc(), $html, $options
);
}
/**
* Serialize DOM element to string (inner/outer HTML is controlled by flag).
* If $releaseDom is set to true, the DOM will be left in non-canonical form
* and is not safe to use after this call. This is primarily a performance optimization.
*
* @param Node $node
* @param bool $innerHTML if true, inner HTML of the element will be returned
* This flag defaults to false
* @param bool $releaseDom if true, the DOM will not be in canonical form after this call
* This flag defaults to false
* @return string
*/
public function domToHtml(
Node $node, bool $innerHTML = false, bool $releaseDom = false
): string {
// FIXME: This is going to drop any diff markers but since
// the dom differ doesn't traverse into extension content (right now),
// none should exist anyways.
$html = ContentUtils::ppToXML( $node, [ 'innerXML' => $innerHTML ] );
if ( !$releaseDom ) {
DOMDataUtils::visitAndLoadDataAttribs( $node );
}
return $html;
}
/**
* Bit flags describing escaping / serializing context in html -> wt mode
*/
public const IN_SOL = 1;
public const IN_MEDIA = 2;
public const IN_LINK = 4;
public const IN_IMG_CAPTION = 8;
public const IN_OPTION = 16;
/**
* FIXME: This is a bit broken - shouldn't be needed ideally
* @param string $flag
*/
public function setHtml2wtStateFlag( string $flag ) {
$this->serializerState->{$flag} = true;
}
/**
* Emit the opening tag (including attributes) for the extension
* represented by this node.
*
* @param Element $node
* @return string
*/
public function extStartTagToWikitext( Element $node ): string {
$state = $this->serializerState;
return $state->serializer->serializeExtensionStartTag( $node, $state );
}
/**
* Convert the input DOM to wikitext.
*
* @param array $opts
* - extName: (string) Name of the extension whose body we are serializing
* - inPHPBlock: (bool) FIXME: This needs to be removed
* @param Element $node DOM to serialize
* @param bool $releaseDom If $releaseDom is set to true, the DOM will be left in
* non-canonical form and is not safe to use after this call. This is primarily a
* performance optimization. This flag defaults to false.
* @return mixed
*/
public function domToWikitext( array $opts, Element $node, bool $releaseDom = false ) {
// FIXME: WTS expects the input DOM to be a <body> element!
// Till that is fixed, we have to go through this round-trip!
// TODO: Move $node children to a fragment and call `$serializer->domToWikitext`
return $this->htmlToWikitext( $opts, $this->domToHtml( $node, $releaseDom ) );
}
/**
* Convert the HTML body of an extension to wikitext
*
* @param array $opts
* - extName: (string) Name of the extension whose body we are serializing
* - inPHPBlock: (bool) FIXME: This needs to be removed
* @param string $html HTML for the extension's body
* @return string
*/
public function htmlToWikitext( array $opts, string $html ): string {
// Type cast so phan has more information to ensure type safety
$state = $this->serializerState;
$opts['env'] = $this->env;
return $state->serializer->htmlToWikitext( $opts, $html );
}
/**
* Get the original source for an element.
*
* The callable, $checkIfOrigSrcReusable, is used to determine if the $elt
* is unedited and therefore valid to reuse source. This is assumed to be
* pretty specific to the callsite so no default is provided.
*
* @param Element $elt
* @param bool $inner
* @param callable $checkIfOrigSrcReusable
* @return string|null
*/
public function getOrigSrc(
Element $elt, bool $inner, callable $checkIfOrigSrcReusable
): ?string {
$state = $this->serializerState;
if ( !$state->selserMode || $state->inInsertedContent ) {
return null;
}
$dsr = DOMDataUtils::getDataParsoid( $elt )->dsr ?? null;
if ( !Utils::isValidDSR( $dsr, $inner ) ) {
return null;
}
if ( $checkIfOrigSrcReusable( $elt ) ) {
return $state->getOrigSrc(
$inner ? $dsr->innerRange() : $dsr
);
} else {
return null;
}
}
/**
* @param Element $elt
* @param int $context OR-ed bit flags specifying escaping / serialization context
* @return string
*/
public function domChildrenToWikitext( Element $elt, int $context ): string {
$state = $this->serializerState;
if ( $context & self::IN_IMG_CAPTION ) {
if ( $context & self::IN_OPTION ) {
$escapeHandler = 'mediaOptionHandler'; // Escapes "|" as well
} else {
$escapeHandler = 'wikilinkHandler'; // image captions show up in wikilink syntax
}
$out = $state->serializeCaptionChildrenToString( $elt,
[ $state->serializer->wteHandlers, $escapeHandler ] );
} else {
throw new \RuntimeException( 'Not yet supported!' );
}
return $out;
}
/**
* Escape any wikitext like constructs in a string so that when the output
* is parsed, it renders as a string. The escaping is sensitive to the context
* in which the string is embedded. For example, a "*" is not safe at the start
* of a line (since it will parse as a list item), but is safe if it is not in
* a start of line context. Similarly the "|" character is safe outside tables,
* links, and transclusions.
*
* @param string $str
* @param Node $node
* @param int $context OR-ed bit flags specifying escaping / serialization context
* @return string
*/
public function escapeWikitext( string $str, Node $node, int $context ): string {
if ( $context & ( self::IN_MEDIA | self::IN_LINK ) ) {
$state = $this->serializerState;
return $state->serializer->wteHandlers->escapeLinkContent(
$state, $str,
(bool)( $context & self::IN_SOL ),
$node,
(bool)( $context & self::IN_MEDIA )
);
} else {
throw new \RuntimeException( 'Not yet supported!' );
}
}
/**
* EXTAPI-FIXME: We have to figure out what it means to run a DOM pass
* (and what processors and what handlers apply) on content models that are
* not wikitext. For now, we are only storing data attribs back to the DOM
* and adding metadata to the page.
*
* @param Document $doc
*/
public function postProcessDOM( Document $doc ): void {
$env = $this->env;
// From CleanUp::saveDataParsoid
$body = DOMCompat::getBody( $doc );
DOMDataUtils::visitAndStoreDataAttribs( $body, [
'storeInPageBundle' => $env->pageBundle,
'env' => $env
] );
// Ugh! But, this whole method needs to go away anyway
( new AddMetaData( null ) )->run( $env, $body );
}
/**
* Produce the HTML rendering of a title string and media options as the
* wikitext parser would for a wikilink in the file namespace
*
* @param string $titleStr Image title string
* @param array $imageOpts Array of a mix of strings or arrays,
* the latter of which can signify that the value came from source.
* Where,
* [0] is the fully-constructed image option
* [1] is the full wikitext source offset for it
* @param ?string &$error Error string is set when the return is null.
* @param ?bool $forceBlock Forces the media to be rendered in a figure as
* opposed to a span.
* @param ?bool $suppressMediaFormats If any media format is present in
* $imageOpts, it won't be applied and will result in a linting error.
* @return ?Element
*/
public function renderMedia(
string $titleStr, array $imageOpts, ?string &$error = null,
?bool $forceBlock = false, ?bool $suppressMediaFormats = false
): ?Element {
$extTagName = $this->extTag->getName();
$extTagOpts = [ 'suppressMediaFormats' => $suppressMediaFormats ];
$fileNs = $this->getSiteConfig()->canonicalNamespaceId( 'file' );
$title = $this->makeTitle( $titleStr, 0 );
if ( $title === null || $title->getNamespace() !== $fileNs ) {
$error = "{$extTagName}_no_image";
return null;
}
$pieces = [ '[[' ];
// Since the above two chars aren't from source, the resulting figure
// won't have any dsr info, so we can omit an offset for the title as
// well. In any case, $titleStr may not necessarily be from source,
// see the special case in the gallery extension.
$pieces[] = $titleStr;
$pieces = array_merge( $pieces, $imageOpts );
if ( $forceBlock ) {
// We add "none" here so that this renders in the block form
// (ie. figure). It's a valid media option, so shouldn't turn into
// a caption. And since it's first wins, it shouldn't interfere
// with another horizontal alignment defined in $imageOpts.
// We just have to remember to strip the class below.
// NOTE: This will have to be adjusted with T305628
$pieces[] = '|none';
}
$pieces[] = ']]';
$shiftOffset = static function ( int $offset ) use ( $pieces ): ?int {
foreach ( $pieces as $p ) {
if ( is_string( $p ) ) {
$offset -= strlen( $p );
if ( $offset <= 0 ) {
return null;
}
} else {
if ( $offset <= strlen( $p[0] ) && isset( $p[1] ) ) {
return $p[1] + $offset;
}
$offset -= strlen( $p[0] );
if ( $offset <= 0 ) {
return null;
}
}
}
return null;
};
$imageWt = '';
foreach ( $pieces as $p ) {
$imageWt .= ( is_string( $p ) ? $p : $p[0] );
}
$domFragment = $this->wikitextToDOM(
$imageWt,
[
'parseOpts' => [
'extTag' => $extTagName,
'extTagOpts' => $extTagOpts,
'context' => 'inline',
],
// Create new frame, because $pieces doesn't literally appear
// on the page, it has been hand-crafted here
'processInNewFrame' => true,
// Shift the DSRs in the DOM by startOffset, and strip DSRs
// for bits which aren't the caption or file, since they
// don't refer to actual source wikitext
'shiftDSRFn' => static function ( DomSourceRange $dsr ) use ( $shiftOffset ) {
$start = $dsr->start === null ? null : $shiftOffset( $dsr->start );
$end = $dsr->end === null ? null : $shiftOffset( $dsr->end );
// If either offset is newly-invalid, remove entire DSR
if (
( $dsr->start !== null && $start === null ) ||
( $dsr->end !== null && $end === null )
) {
return null;
}
return new DomSourceRange(
$start, $end, $dsr->openWidth, $dsr->closeWidth
);
},
],
true // sol
);
$thumb = $domFragment->firstChild;
$validWrappers = [ 'figure' ];
// Downstream code expects a figcaption if we're forcing a block so we
// validate that we did indeed parse a figure. It might not have
// happened because $imageOpts has an unbalanced `]]` which closes
// the wikilink syntax before we get in our `|none`.
if ( !$forceBlock ) {
$validWrappers[] = 'span';
}
if ( !in_array( DOMCompat::nodeName( $thumb ), $validWrappers, true ) ) {
$error = "{$extTagName}_invalid_image";
return null;
}
DOMUtils::assertElt( $thumb );
// Detach the $thumb since the $domFragment is going out of scope
// See https://bugs.php.net/bug.php?id=39593
DOMCompat::remove( $thumb );
if ( $forceBlock ) {
$dp = DOMDataUtils::getDataParsoid( $thumb );
array_pop( $dp->optList );
$explicitNone = false;
foreach ( $dp->optList as $opt ) {
if ( $opt['ck'] === 'none' ) {
$explicitNone = true;
}
}
if ( !$explicitNone ) {
// FIXME: Should we worry about someone adding this with the
// "class=" option?
DOMCompat::getClassList( $thumb )->remove( 'mw-halign-none' );
}
}
return $thumb;
}
/**
* Serialize a MediaStructure to a title and media options string.
* The converse to ::renderMedia.
*
* @param MediaStructure $ms
* @return array Where,
* [0] is the media title string
* [1] is the string of media options
*/
public function serializeMedia( MediaStructure $ms ): array {
$ct = LinkHandlerUtils::figureToConstrainedText( $this->serializerState, $ms );
if ( $ct instanceof WikiLinkText ) {
// Remove the opening and closing square brackets
$text = substr( $ct->text, 2, -2 );
return array_pad( explode( '|', $text, 2 ), 2, '' );
} else {
// Note that $ct could be an AutoURLLinkText, not just null
return [ '', '' ];
}
}
/**
* @param array $modules
* @deprecated Use ::getMetadata()->appendOutputStrings( MODULE, ...) instead.
*/
public function addModules( array $modules ) {
$this->getMetadata()->appendOutputStrings( CMCSS::MODULE, $modules );
}
/**
* @param array $modulestyles
* @deprecated Use ::getMetadata()->appendOutputStrings(MODULE_STYLE, ...) instead.
*/
public function addModuleStyles( array $modulestyles ) {
$this->getMetadata()->appendOutputStrings( CMCSS::MODULE_STYLE, $modulestyles );
}
/**
* Get an array of attributes to apply to an anchor linking to $url
*/
public function getExternalLinkAttribs( string $url ): array {
return $this->env->getExternalLinkAttribs( $url );
}
/**
* Add a tracking category to the current page.
* @param string $key Message key (not localized)
*/
public function addTrackingCategory( string $key ): void {
$this->env->getDataAccess()->addTrackingCategory(
$this->env->getPageConfig(),
$this->env->getMetadata(),
$key
);
}
}

File Metadata

Mime Type
text/x-php
Expires
Sun, Oct 4, 00:04 (2 w, 4 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
67/b9/592f25c1d9084a27114cadf96544
Default Alt Text
ParsoidExtensionAPI.php (34 KB)

Event Timeline