Файловый менеджер - Редактировать - /var/www/html/Ext.zip
Ðазад
PK ! ���S S Y.plnu �[��� # !!!!!!! DO NOT EDIT THIS FILE !!!!!!! # This file is machine-generated by lib/unicore/mktables from the Unicode # database, Version 13.0.0. Any changes made here will be lost! # !!!!!!! INTERNAL PERL USE ONLY !!!!!!! # This file is for internal use by core Perl only. The format and even the # name or existence of this file are subject to change without notice. Don't # use it directly. Use Unicode::UCD to access the Unicode character data # base. return <<'END'; V64 183 184 720 722 1600 1601 2042 2043 2901 2902 3654 3655 3782 3783 6154 6155 6211 6212 6823 6824 7222 7223 7291 7292 12293 12294 12337 12342 12445 12447 12540 12543 40981 40982 42508 42509 43471 43472 43494 43495 43632 43633 43741 43742 43763 43765 65392 65393 70493 70494 71110 71113 72344 72345 92994 92996 94176 94178 94179 94180 123196 123198 125252 125255 END PK ! ���c$ c$ JSON/JSON.phpnu �Iw�� <?php /** * This implements the "json" content model as an extension, to allow editing * JSON data structures using Visual Editor. It represents the JSON * structure as a nested table. */ declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext\JSON; use JsonException; use Wikimedia\Assert\Assert; use Wikimedia\Parsoid\Core\ContentModelHandler; use Wikimedia\Parsoid\Core\SelectiveUpdateData; use Wikimedia\Parsoid\DOM\Document; use Wikimedia\Parsoid\DOM\Element; use Wikimedia\Parsoid\Ext\DOMUtils; use Wikimedia\Parsoid\Ext\ExtensionModule; use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI; use Wikimedia\Parsoid\Ext\PHPUtils; use Wikimedia\Parsoid\Utils\DOMCompat; /** * Native Parsoid implementation of the "json" contentmodel. */ class JSON extends ContentModelHandler implements ExtensionModule { private const PARSE_ERROR_HTML = "<table typeof=\"mw:Error\" data-mw='{\"errors\":[{\"key\":\"bad-json\"}]}'>"; /** @inheritDoc */ public function getConfig(): array { return [ 'name' => 'JSON content', 'contentModels' => [ 'json' => self::class, ], ]; } /** * @param Element $parent * @param array|object|string $val */ private function rootValueTable( Element $parent, $val ): void { if ( is_array( $val ) ) { // Wrap arrays in another array so they're visually boxed in a // container. Otherwise they are visually indistinguishable from // a single value. self::arrayTable( $parent, [ $val ] ); return; } if ( $val && is_object( $val ) ) { self::objectTable( $parent, (array)$val ); return; } DOMCompat::setInnerHTML( $parent, '<table class="mw-json mw-json-single-value"><tbody><tr><td>' ); self::primitiveValue( DOMCompat::querySelector( $parent, 'td' ), $val ); } private function objectTable( Element $parent, array $val ): void { DOMCompat::setInnerHTML( $parent, '<table class="mw-json mw-json-object"><tbody>' ); $tbody = $parent->firstChild->firstChild; DOMUtils::assertElt( $tbody ); $keys = array_keys( $val ); if ( count( $keys ) ) { foreach ( $val as $k => $v ) { self::objectRow( $tbody, (string)$k, $v ); } } else { DOMCompat::setInnerHTML( $tbody, '<tr><td class="mw-json-empty">' ); } } /** * @param Element $parent * @param ?string $key * @param mixed $val */ private function objectRow( Element $parent, ?string $key, $val ): void { $tr = $parent->ownerDocument->createElement( 'tr' ); if ( $key !== null ) { $th = $parent->ownerDocument->createElement( 'th' ); $th->textContent = $key; $tr->appendChild( $th ); } self::valueCell( $tr, $val ); $parent->appendChild( $tr ); } private function arrayTable( Element $parent, array $val ): void { DOMCompat::setInnerHTML( $parent, '<table class="mw-json mw-json-array"><tbody>' ); $tbody = $parent->firstChild->firstChild; DOMUtils::assertElt( $tbody ); if ( count( $val ) ) { foreach ( $val as $v ) { self::objectRow( $tbody, null, $v ); } } else { DOMCompat::setInnerHTML( $tbody, '<tr><td class="mw-json-empty">' ); } } /** * @param Element $parent * @param mixed $val */ private function valueCell( Element $parent, $val ): void { $td = $parent->ownerDocument->createElement( 'td' ); if ( is_array( $val ) ) { self::arrayTable( $td, $val ); } elseif ( $val && is_object( $val ) ) { self::objectTable( $td, (array)$val ); } else { DOMCompat::getClassList( $td )->add( 'value' ); self::primitiveValue( $td, $val ); } $parent->appendChild( $td ); } /** * @param Element $parent * @param string|int|bool|null $val */ private function primitiveValue( Element $parent, $val ): void { if ( $val === null ) { DOMCompat::getClassList( $parent )->add( 'mw-json-null' ); $parent->textContent = 'null'; return; } elseif ( is_bool( $val ) ) { DOMCompat::getClassList( $parent )->add( 'mw-json-boolean' ); $parent->textContent = $val ? 'true' : 'false'; return; } elseif ( is_int( $val ) || is_float( $val ) ) { DOMCompat::getClassList( $parent )->add( 'mw-json-number' ); } elseif ( is_string( $val ) ) { DOMCompat::getClassList( $parent )->add( 'mw-json-string' ); } $parent->textContent = (string)$val; } /** * JSON to HTML. * Implementation matches that from includes/content/JsonContent.php in * mediawiki core, except that we distinguish value types. * @param ParsoidExtensionAPI $extApi * @param ?SelectiveUpdateData $selectiveUpdateData * @return Document */ public function toDOM( ParsoidExtensionAPI $extApi, ?SelectiveUpdateData $selectiveUpdateData = null ): Document { // @phan-suppress-next-line PhanDeprecatedFunction not ready for this yet $jsonText = $extApi->getPageConfig()->getPageMainContent(); $document = $extApi->getTopLevelDoc(); $body = DOMCompat::getBody( $document ); try { $src = json_decode( $jsonText, false, 6, JSON_THROW_ON_ERROR ); self::rootValueTable( $body, $src ); } catch ( JsonException $e ) { DOMCompat::setInnerHTML( $body, self::PARSE_ERROR_HTML ); } // We're responsible for running the standard DOMPostProcessor on our // resulting document. $extApi->postProcessDOM( $document ); return $document; } /** * RootValueTableFrom * @param Element $el * @return array|false|int|string|null */ private function rootValueTableFrom( Element $el ) { if ( DOMUtils::hasClass( $el, 'mw-json-single-value' ) ) { return self::primitiveValueFrom( DOMCompat::querySelector( $el, 'tr > td' ) ); } elseif ( DOMUtils::hasClass( $el, 'mw-json-array' ) ) { return self::arrayTableFrom( $el )[0]; } else { return self::objectTableFrom( $el ); } } /** * @param Element $el * @return array */ private function objectTableFrom( Element $el ) { Assert::invariant( DOMUtils::hasClass( $el, 'mw-json-object' ), 'Expected mw-json-object' ); $tbody = $el; if ( $tbody->firstChild ) { $child = $tbody->firstChild; DOMUtils::assertElt( $child ); if ( DOMCompat::nodeName( $child ) === 'tbody' ) { $tbody = $child; } } $rows = $tbody->childNodes; $obj = []; $empty = count( $rows ) === 0; if ( !$empty ) { $child = $rows->item( 0 )->firstChild; DOMUtils::assertElt( $child ); if ( DOMUtils::hasClass( $child, 'mw-json-empty' ) ) { $empty = true; } } if ( !$empty ) { for ( $i = 0; $i < count( $rows ); $i++ ) { $item = $rows->item( $i ); DOMUtils::assertElt( $item ); self::objectRowFrom( $item, $obj, null ); } } return $obj; } private function objectRowFrom( Element $tr, array &$obj, ?int $key ): void { $td = $tr->firstChild; if ( $key === null ) { $key = $td->textContent; $td = $td->nextSibling; } DOMUtils::assertElt( $td ); $obj[$key] = self::valueCellFrom( $td ); } private function arrayTableFrom( Element $el ): array { Assert::invariant( DOMUtils::hasClass( $el, 'mw-json-array' ), 'Expected ms-json-array' ); $tbody = $el; if ( $tbody->firstChild ) { $child = $tbody->firstChild; DOMUtils::assertElt( $child ); if ( DOMCompat::nodeName( $child ) === 'tbody' ) { $tbody = $child; } } $rows = $tbody->childNodes; $arr = []; $empty = count( $rows ) === 0; if ( !$empty ) { $child = $rows->item( 0 )->firstChild; DOMUtils::assertElt( $child ); if ( DOMUtils::hasClass( $child, 'mw-json-empty' ) ) { $empty = true; } } if ( !$empty ) { for ( $i = 0; $i < count( $rows ); $i++ ) { $item = $rows->item( $i ); DOMUtils::assertElt( $item ); self::objectRowFrom( $item, $arr, $i ); } } return $arr; } /** * @param Element $el * @return array|object|false|float|int|string|null */ private function valueCellFrom( Element $el ) { Assert::invariant( DOMCompat::nodeName( $el ) === 'td', 'Expected tagName = td' ); $table = $el->firstChild; if ( $table instanceof Element ) { if ( DOMUtils::hasClass( $table, 'mw-json-array' ) ) { return self::arrayTableFrom( $table ); } elseif ( DOMUtils::hasClass( $table, 'mw-json-object' ) ) { return self::objectTableFrom( $table ); } } else { return self::primitiveValueFrom( $el ); } } /** * @param Element $el * @return false|float|int|string|null */ private function primitiveValueFrom( Element $el ) { if ( DOMUtils::hasClass( $el, 'mw-json-null' ) ) { return null; } elseif ( DOMUtils::hasClass( $el, 'mw-json-boolean' ) ) { return str_contains( $el->textContent, 'true' ); } elseif ( DOMUtils::hasClass( $el, 'mw-json-number' ) ) { return floatval( $el->textContent ); } elseif ( DOMUtils::hasClass( $el, 'mw-json-string' ) ) { return (string)$el->textContent; } else { return null; // shouldn't happen. } } /** * DOM to JSON. * @param ParsoidExtensionAPI $extApi * @param ?SelectiveUpdateData $selectiveUpdateData * @return string */ public function fromDOM( ParsoidExtensionAPI $extApi, ?SelectiveUpdateData $selectiveUpdateData = null ): string { $body = DOMCompat::getBody( $extApi->getTopLevelDoc() ); $t = $body->firstChild; DOMUtils::assertElt( $t ); Assert::invariant( $t && DOMCompat::nodeName( $t ) === 'table', 'Expected tagName = table' ); self::rootValueTableFrom( $t ); return PHPUtils::jsonEncode( self::rootValueTableFrom( $t ) ); } } PK ! <$v�� � PHPUtils.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext; use Wikimedia\Parsoid\Utils\PHPUtils as PHPU; /** * This class contains sundry helpers unrelated to core Parsoid */ class PHPUtils { /** * Convert an iterable to an array. * * This function is similar to *but not the same as* the built-in * iterator_to_array, because arrays are iterable but not Traversable! * * This function is also present in the wmde/iterable-functions library, * but it's short enough that we don't need to pull in an entire new * dependency here. * * @see https://stackoverflow.com/questions/44587973/php-iterable-to-array-or-traversable * @see https://github.com/wmde/iterable-functions/blob/master/src/functions.php * * @phan-template T * @param iterable<T> $iterable * @return array<T> */ public static function iterable_to_array( iterable $iterable ): array { // phpcs:ignore MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName,Generic.Files.LineLength.TooLong return PHPU::iterable_to_array( $iterable ); } /** * json_encode wrapper function * - unscapes slashes and unicode * * @param mixed $o * @return string */ public static function jsonEncode( $o ): string { return PHPU::jsonEncode( $o ); } /** * If a string starts with a given prefix, remove the prefix. Otherwise, * return the original string. Like preg_replace( "/^$prefix/", '', $subject ) * except about 1.14x faster in the replacement case and 2x faster in * the no-op case. * * Note: adding type declarations to the parameters adds an overhead of 3%. * The benchmark above was without type declarations. * * @param string $subject * @param string $prefix * @return string */ public static function stripPrefix( $subject, $prefix ) { return PHPU::stripPrefix( $subject, $prefix ); } /** * If a string ends with a given suffix, remove the suffix. Otherwise, * return the original string. Like preg_replace( "/$suffix$/", '', $subject ) * except faster. * * @param string $subject * @param string $suffix * @return string */ public static function stripSuffix( $subject, $suffix ) { return PHPU::stripSuffix( $subject, $suffix ); } } PK ! ~~��� � Pre/Pre.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext\Pre; use Wikimedia\Parsoid\Core\Sanitizer; use Wikimedia\Parsoid\DOM\DocumentFragment; use Wikimedia\Parsoid\Ext\DOMDataUtils; use Wikimedia\Parsoid\Ext\ExtensionModule; use Wikimedia\Parsoid\Ext\ExtensionTagHandler; use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI; use Wikimedia\Parsoid\Ext\PHPUtils; use Wikimedia\Parsoid\Ext\Utils; /** * The `<pre>` extension tag shadows the html pre tag, but has different * semantics. It treats anything inside it as plaintext. */ class Pre extends ExtensionTagHandler implements ExtensionModule { /** @inheritDoc */ public function getConfig(): array { return [ 'name' => '<pre>', 'tags' => [ [ 'name' => 'pre', 'handler' => self::class, ] ] ]; } /** @inheritDoc */ public function sourceToDom( ParsoidExtensionAPI $extApi, string $txt, array $extArgs ): DocumentFragment { $domFragment = $extApi->htmlToDom( '' ); $doc = $domFragment->ownerDocument; $pre = $doc->createElement( 'pre' ); Sanitizer::applySanitizedArgs( $extApi->getSiteConfig(), $pre, $extArgs ); DOMDataUtils::getDataParsoid( $pre )->stx = 'html'; // Support nowikis in pre. Do this before stripping newlines, see test, // "<pre> with <nowiki> inside (compatibility with 1.6 and earlier)" $txt = preg_replace( '/<nowiki\s*>(.*?)<\/nowiki\s*>/s', '$1', $txt ); // Strip leading newline to match legacy php parser. This is probably because // it doesn't do xml serialization accounting for `newlineStrippingElements` // Of course, this leads to indistinguishability between n=0 and n=1 // newlines, but that only seems to affect parserTests output. Rendering // is the same, and the newline is preserved for rt in the `extSrc`. $txt = PHPUtils::stripPrefix( $txt, "\n" ); // `extSrc` will take care of rt'ing these $txt = Utils::decodeWtEntities( $txt ); $pre->appendChild( $doc->createTextNode( $txt ) ); $domFragment->appendChild( $pre ); return $domFragment; } } PK ! P�ؒ� � LST/LST.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext\LST; use Wikimedia\Parsoid\DOM\Element; use Wikimedia\Parsoid\Ext\DOMDataUtils; use Wikimedia\Parsoid\Ext\DOMUtils; use Wikimedia\Parsoid\Ext\ExtensionModule; use Wikimedia\Parsoid\Ext\ExtensionTagHandler; use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI; use Wikimedia\Parsoid\Utils\DOMCompat; class LST extends ExtensionTagHandler implements ExtensionModule { /** @inheritDoc */ public function getConfig(): array { return [ 'name' => 'LST', 'tags' => [ [ 'name' => 'labeledsectiontransclusion', 'handler' => self::class, ], [ 'name' => 'labeledsectiontransclusion/begin', 'handler' => self::class, ], [ 'name' => 'labeledsectiontransclusion/end', 'handler' => self::class, ] ] ]; } /** @inheritDoc */ public function domToWikitext( ParsoidExtensionAPI $extApi, Element $node, bool $wrapperUnmodified ) { // TODO: We're keeping this serial handler around to remain backwards // compatible with stored content version 1.3.0 and below. Remove it // when those versions are no longer supported. $dp = DOMDataUtils::getDataParsoid( $node ); $src = null; $content = DOMCompat::getAttribute( $node, 'content' ) ?? ''; if ( isset( $dp->src ) ) { $src = $dp->src; } elseif ( DOMUtils::matchTypeOf( $node, '/begin/' ) ) { $src = '<section begin="' . $content . '" />'; } elseif ( DOMUtils::matchTypeOf( $node, '/end/' ) ) { $src = '<section end="' . $content . '" />'; } else { $extApi->log( 'error', 'LST <section> without content in: ' . DOMCompat::getOuterHTML( $node ) ); $src = '<section />'; } return $src; } } PK ! �8�~� � WTUtils.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext; use Wikimedia\Parsoid\DOM\Element; use Wikimedia\Parsoid\DOM\Node; use Wikimedia\Parsoid\Utils\WTUtils as WTU; /** * This class provides helpers needed by extensions. * These helpers help with extracting wikitext information from the DOM. */ class WTUtils { /** * Is $node a sealed DOMFragment of a specific extension? */ public static function isSealedFragmentOfType( Node $node, string $name ): bool { return WTU::isSealedFragmentOfType( $node, $name ); } public static function hasVisibleCaption( Element $node ): bool { return WTU::hasVisibleCaption( $node ); } public static function textContentFromCaption( Node $node ): string { return WTU::textContentFromCaption( $node ); } public static function fromEncapsulatedContent( Node $node ): bool { return WTU::fromEncapsulatedContent( $node ); } } PK ! �1s� Utils.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext; use Wikimedia\Parsoid\Config\SiteConfig; use Wikimedia\Parsoid\Utils\Utils as U; /** * This class provides sundry helpers needed by extensions. */ class Utils { /** * Entity-escape anything that would decode to a valid wikitext entity. * * Note that HTML5 allows certain "semicolon-less" entities, like * `¶`; these aren't allowed in wikitext and won't be escaped * by this function. * * @param string $text * @return string */ public static function escapeWtEntities( string $text ): string { return U::escapeWtEntities( $text ); } /** * Decode HTML5 entities in wikitext. * * NOTE that wikitext only allows semicolon-terminated entities, while * HTML allows a number of "legacy" entities to be decoded without * a terminating semicolon. This function deliberately does not * decode these HTML-only entity forms. * * @param string $text * @return string */ public static function decodeWtEntities( string $text ): string { return U::decodeWtEntities( $text ); } /** * Parse media dimensions * * @param SiteConfig $siteConfig * @param string $str media dimension string to parse * @param bool $onlyOne If set, returns null if multiple dimenstions are present * @param bool $localized Defaults to false; set to true if the $str * has already been matched against `img_width` to localize the `px` * suffix. * @return array{x:int,y?:int}|null */ public static function parseMediaDimensions( SiteConfig $siteConfig, string $str, bool $onlyOne = false, bool $localized = false ): ?array { return U::parseMediaDimensions( $siteConfig, $str, $onlyOne, $localized ); } /** * Encode all characters as entity references. This is done to make * characters safe for wikitext (regardless of whether they are * HTML-safe). Typically only called with single-codepoint strings. * @param string $s * @return string */ public static function entityEncodeAll( string $s ): string { return U::entityEncodeAll( $s ); } /** * Validate media parameters * More generally, this is defined by the media handler in core * @param ?int $num * @return bool */ public static function validateMediaParam( ?int $num ): bool { return U::validateMediaParam( $num ); } } PK ! &��s� � ExtensionTagHandler.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext; use Closure; use Wikimedia\Parsoid\DOM\DocumentFragment; use Wikimedia\Parsoid\DOM\Element; /** * A Parsoid extension module may register handlers for one or more * extension tags. The only method which is generally * required by all extension tags is `sourceToDom` (but Translate * doesn't even implement that). All other methods have default do-nothing * implementations; override them iff you wish to implement those * features. Default implementations consistently return `false` * to indicate not-implemented (in some cases `null` would be a * valid return value, and in other cases `null` would be a likely * "accidental" return value which we'd like to catch and flag). */ abstract class ExtensionTagHandler { /** * Convert an extension tag's content to DOM * @param ParsoidExtensionAPI $extApi * @param string $src Extension tag content * @param array $extArgs Extension tag arguments * The extension tag arguments should be treated as opaque objects * and any necessary inspection should be handled through the API. * @return DocumentFragment|false|null * `DocumentFragment` if returning some parsed content * `false` to fallback to the default handler for the content * `null` to drop the instance completely */ public function sourceToDom( ParsoidExtensionAPI $extApi, string $src, array $extArgs ) { return false; /* Use default wrapper */ } /** * Extensions might embed HTML in attributes in their own custom * representation (whether in data-mw or elsewhere). * * Core Parsoid will need a way to traverse such content. This method * is a way for extension tag handlers to provide this functionality. * Parsoid will only call this method if the tag's config sets the * options['wt2html']['embedsHTMLInAttributes'] property to true. * * @param ParsoidExtensionAPI $extApi * @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( ParsoidExtensionAPI $extApi, Element $elt, Closure $proc ): void { // Nothing to do by default } /** * Lint handler for this extension. * * If the extension has lints it wants to expose, it should use $extApi * to register those lints. Alternatively, the extension might simply * inspect its DOM and invoke the default lint handler on a DOM tree * that it wants inspected. For example, <ref> nodes often only have * a pointer (the id attribute) to its content, and its lint handler would * look up the DOM tree and invoke the default lint handler on that tree. * * @param ParsoidExtensionAPI $extApi * @param Element $rootNode Extension content's root node * @param callable $defaultHandler Default lint handler * - Default lint handler has signature $defaultHandler( Element $elt ): void * @return bool Return `false` to indicate that this * extension has no special lint handler (the default lint handler will * be used. Return `true` to indicate linting should proceed with the * next sibling. */ public function lintHandler( ParsoidExtensionAPI $extApi, Element $rootNode, callable $defaultHandler ) { /* Use default linter */ return false; } /** * Serialize a DOM node created by this extension to wikitext. * @param ParsoidExtensionAPI $extApi * @param Element $node * @param bool $wrapperUnmodified * @return string|false Return false to use the default serialization. */ public function domToWikitext( ParsoidExtensionAPI $extApi, Element $node, bool $wrapperUnmodified ) { /* Use default serialization */ return false; } /** * XXX: Experimental * * Call $domDiff on corresponding substrees of $origNode and $editedNode * * @param ParsoidExtensionAPI $extApi * @param callable $domDiff * @param Element $origNode * @param Element $editedNode * @return bool */ public function diffHandler( ParsoidExtensionAPI $extApi, callable $domDiff, Element $origNode, Element $editedNode ): bool { return false; } } PK ! �B� � Indicator/Indicator.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext\Indicator; use Closure; use Wikimedia\Parsoid\DOM\DocumentFragment; use Wikimedia\Parsoid\DOM\Element; use Wikimedia\Parsoid\Ext\DiffDOMUtils; use Wikimedia\Parsoid\Ext\DOMDataUtils; use Wikimedia\Parsoid\Ext\DOMUtils; use Wikimedia\Parsoid\Ext\ExtensionModule; use Wikimedia\Parsoid\Ext\ExtensionTagHandler; use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI; use Wikimedia\Parsoid\Utils\DOMCompat; /** * Implements the php parser's `indicator` hook natively. */ class Indicator extends ExtensionTagHandler implements ExtensionModule { /** @inheritDoc */ public function getConfig(): array { return [ 'name' => 'Indicator', 'tags' => [ [ 'name' => 'indicator', 'handler' => self::class, 'options' => [ 'wt2html' => [ 'embedsHTMLInAttributes' => true, 'customizesDataMw' => true, ], 'outputHasCoreMwDomSpecMarkup' => true ], ] ], ]; } /** @inheritDoc */ public function processAttributeEmbeddedHTML( ParsoidExtensionAPI $extApi, Element $elt, Closure $proc ): void { $dmw = DOMDataUtils::getDataMw( $elt ); if ( isset( $dmw->html ) ) { $dmw->html = $proc( $dmw->html ); } } /** @inheritDoc */ public function sourceToDom( ParsoidExtensionAPI $extApi, string $content, array $args ): DocumentFragment { $dataMw = $extApi->extTag->getDefaultDataMw(); $kvArgs = $extApi->extArgsToArray( $args ); $name = $kvArgs['name'] ?? ''; if ( trim( $name ) === '' ) { $out = $extApi->pushError( 'invalid-indicator-name' ); DOMDataUtils::setDataMw( $out->firstChild, $dataMw ); return $out; } // Convert indicator wikitext to DOM $domFragment = $extApi->extTagToDOM( [] /* No args to apply */, $content, [ 'parseOpts' => [ 'extTag' => 'indicator' ], ] ); // Strip an outer paragraph if it is the sole paragraph without additional attributes $content = DiffDOMUtils::firstNonSepChild( $domFragment ); if ( $content && DOMCompat::nodeName( $content ) === 'p' && DiffDOMUtils::nextNonSepSibling( $content ) === null && $content instanceof Element && // Needed to mollify Phan DOMDataUtils::noAttrs( $content ) ) { DOMUtils::migrateChildren( $content, $domFragment, $content->nextSibling ); $domFragment->removeChild( $content ); } // Save HTML and remove content from the fragment $dataMw->html = $extApi->domToHtml( $domFragment, true ); $c = $domFragment->firstChild; while ( $c ) { $domFragment->removeChild( $c ); $c = $domFragment->firstChild; } // Use a meta tag whose data-mw we will stuff this HTML into later. // NOTE: Till T214994 is resolved, this HTML will not get processed // by all the top-level DOM passes that may need to process this (ex: linting) $meta = $domFragment->ownerDocument->createElement( 'meta' ); DOMDataUtils::setDataMw( $meta, $dataMw ); // Append meta $domFragment->appendChild( $meta ); return $domFragment; } } PK ! #�D>� � Nowiki/Nowiki.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext\Nowiki; use Wikimedia\Assert\Assert; use Wikimedia\Parsoid\DOM\Comment; use Wikimedia\Parsoid\DOM\DocumentFragment; use Wikimedia\Parsoid\DOM\Element; use Wikimedia\Parsoid\DOM\Text; use Wikimedia\Parsoid\Ext\DiffDOMUtils; use Wikimedia\Parsoid\Ext\DiffUtils; use Wikimedia\Parsoid\Ext\DOMDataUtils; use Wikimedia\Parsoid\Ext\DOMUtils; use Wikimedia\Parsoid\Ext\ExtensionModule; use Wikimedia\Parsoid\Ext\ExtensionTagHandler; use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI; use Wikimedia\Parsoid\Ext\Utils; use Wikimedia\Parsoid\NodeData\DataParsoid; use Wikimedia\Parsoid\Utils\DOMCompat; /** * Nowiki treats anything inside it as plain text. */ class Nowiki extends ExtensionTagHandler implements ExtensionModule { /** @inheritDoc */ public function getConfig(): array { return [ 'name' => '<nowiki>', 'tags' => [ [ 'name' => 'nowiki', 'handler' => self::class, ] ] ]; } /** @inheritDoc */ public function sourceToDom( ParsoidExtensionAPI $extApi, string $txt, array $extArgs ): DocumentFragment { $domFragment = $extApi->htmlToDom( '' ); $doc = $domFragment->ownerDocument; $span = $doc->createElement( 'span' ); DOMUtils::addTypeOf( $span, 'mw:Nowiki' ); foreach ( preg_split( '/(&[#0-9a-zA-Z]+;)/', $txt, -1, PREG_SPLIT_DELIM_CAPTURE ) as $i => $t ) { if ( $i % 2 === 1 ) { $cc = Utils::decodeWtEntities( $t ); if ( $cc !== $t ) { // This should match the output of the "htmlentity" rule // in the tokenizer. $entity = $doc->createElement( 'span' ); DOMUtils::addTypeOf( $entity, 'mw:Entity' ); $dp = new DataParsoid; $dp->src = $t; $dp->srcContent = $cc; DOMDataUtils::setDataParsoid( $entity, $dp ); $entity->appendChild( $doc->createTextNode( $cc ) ); $span->appendChild( $entity ); continue; } // else, fall down there } $span->appendChild( $doc->createTextNode( $t ) ); } DOMCompat::normalize( $span ); $domFragment->appendChild( $span ); return $domFragment; } /** @inheritDoc */ public function domToWikitext( ParsoidExtensionAPI $extApi, Element $node, bool $wrapperUnmodified ) { if ( !$node->hasChildNodes() ) { $extApi->setHtml2wtStateFlag( 'hasSelfClosingNowikis' ); // FIXME return '<nowiki/>'; } $str = '<nowiki>'; for ( $child = $node->firstChild; $child; $child = $child->nextSibling ) { $out = null; if ( $child instanceof Element ) { if ( DiffUtils::isDiffMarker( $child ) ) { /* ignore */ } elseif ( DOMCompat::nodeName( $child ) === 'span' && DOMUtils::hasTypeOf( $child, 'mw:Entity' ) && DiffDOMUtils::hasNChildren( $child, 1 ) ) { $dp = DOMDataUtils::getDataParsoid( $child ); if ( isset( $dp->src ) && $dp->srcContent === $child->textContent ) { // Unedited content $out = $dp->src; } else { // Edited content $out = Utils::entityEncodeAll( $child->firstChild->nodeValue ); } // DisplaySpace is added in a final post-processing pass so, // even though it isn't emitted in the extension handler, we // need to deal with the possibility of its presence // FIXME(T254501): Should avoid the need for this } elseif ( DOMCompat::nodeName( $child ) === 'span' && DOMUtils::hasTypeOf( $child, 'mw:DisplaySpace' ) && DiffDOMUtils::hasNChildren( $child, 1 ) ) { $out = ' '; } else { /* This is a hacky fallback for what is essentially * undefined behavior. No matter what we emit here, * this won't roundtrip html2html. */ $extApi->log( 'error/html2wt/nowiki', 'Invalid nowiki content' ); $out = $child->textContent; } } elseif ( $child instanceof Text ) { $out = $child->nodeValue; } else { Assert::invariant( $child instanceof Comment, "Expected a comment here" ); /* Comments can't be embedded in a <nowiki> */ $extApi->log( 'error/html2wt/nowiki', 'Discarded invalid embedded comment in a <nowiki>' ); $out = ''; } // Always escape any nowikis found in $out if ( $out ) { // Inlined helper that previously existed in Parsoid's WT Utils $str .= preg_replace( '#<(/?nowiki\s*/?\s*)>#i', '<$1>', $out ); } } return $str . '</nowiki>'; } } PK ! z��d� � DOMUtils.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext; use Wikimedia\Parsoid\DOM\Element; use Wikimedia\Parsoid\DOM\Node; use Wikimedia\Parsoid\Utils\DOMUtils as DU; /** * This class provides DOM helpers useful for extensions. */ class DOMUtils { /** * Test if a node matches a given typeof. * @param Node $node node * @param string $type type * @return bool */ public static function hasTypeOf( Node $node, string $type ): bool { return DU::hasTypeOf( $node, $type ); } /** * @param Element $element * @param string $regex Partial regular expression, e.g. "foo|bar" * @return bool */ public static function hasClass( Element $element, string $regex ): bool { return DU::hasClass( $element, $regex ); } /** * Determine whether the node matches the given `typeof` attribute value. * * @param Node $n The node to test * @param string $typeRe Regular expression matching the expected value of * the `typeof` attribute. * @return ?string The matching `typeof` value, or `null` if there is * no match. */ public static function matchTypeOf( Node $n, string $typeRe ): ?string { return DU::matchTypeOf( $n, $typeRe ); } /** * Add a type to the typeof attribute. If the elt already has an existing typeof, * it makes that attribute a string of space separated types. * @param Element $elt * @param string $type type */ public static function addTypeOf( Element $elt, string $type ): void { DU::addTypeOf( $elt, $type ); } /** * Remove a type from the typeof attribute. * @param Element $elt * @param string $type type */ public static function removeTypeOf( Element $elt, string $type ): void { DU::removeTypeOf( $elt, $type ); } /** * Determine whether the node matches the given rel attribute value. * * @param Node $n * @param string $rel Expected value of "rel" attribute, as a literal string. * @return ?string The match if there is one, null otherwise */ public static function matchRel( Node $n, string $rel ): ?string { return DU::matchRel( $n, $rel ); } /** * Add a type to the rel attribute. This method should almost always * be used instead of `setAttribute`, to ensure we don't overwrite existing * rel information. * * @param Element $node node * @param string $rel type */ public static function addRel( Element $node, string $rel ): void { DU::addRel( $node, $rel ); } /** * Assert that this is a DOM element node. * This is primarily to help phan analyze variable types. * @phan-assert Element $node * @param ?Node $node * @return bool Always returns true */ public static function assertElt( ?Node $node ): bool { return DU::assertElt( $node ); } /** * Move 'from'.childNodes to 'to' adding them before 'beforeNode' * If 'beforeNode' is null, the nodes are appended at the end. * @param Node $from Source node. Children will be removed. * @param Node $to Destination node. Children of $from will be added here * @param ?Node $beforeNode Add the children before this node. */ public static function migrateChildren( Node $from, Node $to, ?Node $beforeNode = null ): void { DU::migrateChildren( $from, $to, $beforeNode ); } /** * Add attributes to a node element. * * @param Element $elt element * @param array $attrs attributes */ public static function addAttributes( Element $elt, array $attrs ): void { DU::addAttributes( $elt, $attrs ); } /** * Find an ancestor of $node with nodeName $name. * * @param Node $node * @param string $name * @return ?Element */ public static function findAncestorOfName( Node $node, string $name ): ?Element { return DU::findAncestorOfName( $node, $name ); } } PK ! �|H� � DiffDOMUtils.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext; use Wikimedia\Parsoid\DOM\Node; use Wikimedia\Parsoid\Utils\DiffDOMUtils as DDU; /** */ class DiffDOMUtils { /** * Test the number of children this node has without using * `DOMNode::$childNodes->count()`. This walks the sibling list and so * takes O(`nchildren`) time -- so `nchildren` is expected to be small * (say: 0, 1, or 2). * * Skips all diff markers by default. * @param Node $node * @param int $nchildren * @param bool $countDiffMarkers * @return bool */ public static function hasNChildren( Node $node, int $nchildren, bool $countDiffMarkers = false ): bool { return DDU::hasNChildren( $node, $nchildren, $countDiffMarkers ); } /** * Get the first child element or non-IEW text node, ignoring * whitespace-only text nodes, comments, and deleted nodes. * * @param Node $node * @return Node|null */ public static function firstNonSepChild( Node $node ): ?Node { return DDU::firstNonSepChild( $node ); } /** * Get the next non separator sibling node. * * @param Node $node * @return Node|null */ public static function nextNonSepSibling( Node $node ): ?Node { return DDU::nextNonSepSibling( $node ); } } PK ! !�}� � DiffUtils.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext; use Wikimedia\Parsoid\DOM\Element; use Wikimedia\Parsoid\DOM\Node; use Wikimedia\Parsoid\Html2Wt\DiffUtils as DU; /** */ class DiffUtils { /** * Check a node to see whether it's a diff marker. */ public static function isDiffMarker( ?Node $node, ?string $mark = null ): bool { return DU::isDiffMarker( $node, $mark ); } /** * Check that the diff markers on the node exist. */ public static function hasDiffMarkers( Node $node ): bool { return DU::hasDiffMarkers( $node ); } public static function subtreeUnchanged( Element $node ): bool { return DU::subtreeUnchanged( $node ); } } PK ! lL��� � ExtensionModule.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext; /** * A Parsoid native extension module. This bundles up the configuration * for a number of different ExtensionTagHandlers, ContentModelHandlers, * and DomProcessors into one registered object. The only method required * is `getConfig`. * * FIXME: This might be created on-demand by configuration data specified * in extension.json. */ interface ExtensionModule { /** * Return information about this extension module. * FIXME: Add more expected fields or create a class for this * FIXME: The 'name' is expected to be the same as the name defined * at the top level of extension.json. * @return array{name:string} */ public function getConfig(): array; } PK ! X|� AnnotationStripper.phpnu �Iw�� <?php namespace Wikimedia\Parsoid\Ext; /** * A Parsoid extension module defining annotations should define an AnnotationStripper * that allows Parsoid to strip annotation markup from an arbitrary string, typically in * the content of non-wikitext extensions (such as SyntaxHighlight) in the wt2html direction. */ interface AnnotationStripper { /** * Strip annotation markup from the provided string $s * @param string $s * @return string */ public function stripAnnotations( string $s ): string; } PK ! 3�{� � ExtensionError.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext; use Exception; use Wikimedia\Parsoid\NodeData\DataMwError; /** * Exception thrown to halt extension tag content parsing and produce standard * error output. * * $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 */ class ExtensionError extends Exception { /** * @var DataMwError */ public $err; /** * @param string $key * @param mixed ...$params */ public function __construct( string $key = 'mw-extparse-error', ...$params ) { parent::__construct(); $this->err = new DataMwError( $key, $params ); } } PK ! �M��K K WTSUtils.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext; use Wikimedia\Parsoid\DOM\Element; use Wikimedia\Parsoid\Html2Wt\WTSUtils as WTSU; class WTSUtils { public static function getShadowInfo( Element $node, string $name, ?string $curVal ): array { return WTSU::getShadowInfo( $node, $name, $curVal ); } } PK ! Q`�@� � ExtensionTag.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext; use Wikimedia\Parsoid\Core\DomSourceRange; use Wikimedia\Parsoid\NodeData\DataMw; use Wikimedia\Parsoid\Tokens\Token; use Wikimedia\Parsoid\Utils\Utils; /** * Wrapper so that the internal token isn't exposed */ class ExtensionTag { /** @var Token */ private $extToken; public function __construct( Token $extToken ) { $this->extToken = $extToken; } /** * Return the name of the extension tag */ public function getName(): string { return $this->extToken->getAttributeV( 'name' ); } /** * Return the source offsets for this extension tag usage */ public function getOffsets(): ?DomSourceRange { return $this->extToken->dataParsoid->extTagOffsets ?? null; } /** * Return the full extension source */ public function getSource(): ?string { return $this->extToken->getAttributeV( 'source' ); } /** * Is this extension tag self-closed? */ public function isSelfClosed(): bool { return !empty( $this->extToken->dataParsoid->selfClose ); } public function getDefaultDataMw(): DataMw { return Utils::getExtArgInfo( $this->extToken ); } } PK ! `,�� DOMProcessor.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext; use Wikimedia\Parsoid\DOM\DocumentFragment; use Wikimedia\Parsoid\DOM\Element; use Wikimedia\Parsoid\DOM\Node; /** * A Parsoid extension module may contain one or more DOMProcessors, * which allow Parsoid to post-process the DOM in the wt2html direction, * or pre-process the DOM in the html2wt direction. * * @phan-file-suppress PhanEmptyPublicMethod */ abstract class DOMProcessor { /** * Post-process DOM in the wt2html direction. * * @param ParsoidExtensionAPI $extApi * @param DocumentFragment|Element $root The root of the tree to process * @param array $options */ public function wtPostprocess( ParsoidExtensionAPI $extApi, Node $root, array $options ): void { /* do nothing by default */ } /** * Pre-process DOM in the html2wt direction. * * @param ParsoidExtensionAPI $extApi * @param Element $root */ public function htmlPreprocess( ParsoidExtensionAPI $extApi, Element $root ): void { /* do nothing by default */ } } PK ! ���, DOMDataUtils.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext; use stdClass; use Wikimedia\Parsoid\DOM\Element; use Wikimedia\Parsoid\NodeData\DataMw; use Wikimedia\Parsoid\NodeData\DataParsoid; use Wikimedia\Parsoid\Utils\DOMDataUtils as DDU; /** * This class provides DOM data helpers needed by extensions. * These helpers support fetching / updating attributes of DOM nodes. */ class DOMDataUtils { /** * Get data parsoid info from DOM element * @param Element $elt * @return DataParsoid ( this is mostly used for type hinting ) */ public static function getDataParsoid( Element $elt ): DataParsoid { return DDU::getDataParsoid( $elt ); } /** * Set data parsoid info on a DOM element * @param Element $elt * @param ?DataParsoid $dp data-parsoid */ public static function setDataParsoid( Element $elt, ?DataParsoid $dp ): void { DDU::setDataParsoid( $elt, $dp ); } /** * Get data meta wiki info from a DOM element * @param Element $elt * @return ?DataMw */ public static function getDataMw( Element $elt ): ?DataMw { return DDU::getDataMw( $elt ); } /** * Check if there is meta wiki info on a DOM element * @param Element $elt * @return bool */ public static function dataMwExists( Element $elt ): bool { return DDU::validDataMw( $elt ); } /** * Set data meta wiki info from a DOM element * @param Element $elt * @param ?DataMw $dmw data-mw */ public static function setDataMw( Element $elt, ?DataMw $dmw ): void { DDU::setDataMw( $elt, $dmw ); } /** * Get data diff info from a DOM element. * @param Element $elt * @return ?stdClass */ public static function getDataParsoidDiff( Element $elt ): ?stdClass { return DDU::getDataParsoidDiff( $elt ); } /** * Set data diff info on a DOM element. * @param Element $elt * @param ?stdClass $diffObj data-parsoid-diff object */ public static function setDataParsoidDiff( Element $elt, ?stdClass $diffObj ): void { DDU::setDataParsoidDiff( $elt, $diffObj ); } /** * Does this node have any attributes? This method is the preferred way of * interrogating this property since Parsoid DOMs might have Parsoid-internal * attributes added. * @param Element $elt * @return bool */ public static function noAttrs( Element $elt ): bool { return DDU::noAttrs( $elt ); } /** * Clones a node and its data bag * @param Element $elt * @param bool $deep * @return Element */ public static function cloneNode( Element $elt, bool $deep ): Element { return DDU::cloneNode( $elt, $deep ); } } PK ! ���0� � ParsoidExtensionAPI.phpnu �Iw�� <?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 ); } } PK ! ��6�� � Gallery/TraditionalMode.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext\Gallery; use Wikimedia\Parsoid\DOM\Document; use Wikimedia\Parsoid\DOM\DocumentFragment; use Wikimedia\Parsoid\DOM\Element; use Wikimedia\Parsoid\Ext\DOMDataUtils; use Wikimedia\Parsoid\Ext\DOMUtils; use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI; use Wikimedia\Parsoid\Utils\DOMCompat; class TraditionalMode extends Mode { /** * Create a TraditionalMode singleton. * @param ?string $mode Only used by subclasses. */ protected function __construct( ?string $mode = null ) { parent::__construct( $mode ?? 'traditional' ); $this->scale = 1; $this->padding = (object)[ 'thumb' => 30, 'box' => 5, 'border' => 8 ]; } /** @var float */ protected $scale; /** @var \stdClass */ protected $padding; private function appendAttr( Element $ul, string $k, string $v ): void { $val = DOMCompat::getAttribute( $ul, $k ); $val = ( $val === null || trim( $val ) === '' ) ? $v : "$val $v"; $ul->setAttribute( $k, $val ); } /** * Attributes in this method are applied to the list element in the order * that matches the legacy parser. * * 1. Default * 2. Inline * 3. Additional * * The order is particularly important for appending to the style attribute * since editors do not always terminate with a semi-colon. */ private function ul( Opts $opts, DocumentFragment $domFragment ): Element { $ul = $domFragment->ownerDocument->createElement( 'ul' ); $cl = 'gallery mw-gallery-' . $this->mode; $ul->setAttribute( 'class', $cl ); $this->perRow( $opts, $ul ); foreach ( $opts->attrs as $k => $v ) { $this->appendAttr( $ul, $k, $v ); } $this->setAdditionalOptions( $opts, $ul ); $domFragment->appendChild( $ul ); return $ul; } protected function perRow( Opts $opts, Element $ul ): void { if ( $opts->imagesPerRow > 0 ) { $padding = $this->padding; $total = $opts->imageWidth + $padding->thumb + $padding->box + $padding->border; $total *= $opts->imagesPerRow; $this->appendAttr( $ul, 'style', 'max-width: ' . $total . 'px;' ); } } protected function setAdditionalOptions( Opts $opts, Element $ul ): void { } private function caption( Opts $opts, Element $ul, DocumentFragment $caption ): void { $doc = $ul->ownerDocument; $li = $doc->createElement( 'li' ); $li->setAttribute( 'class', 'gallerycaption' ); DOMUtils::migrateChildren( $caption, $li ); $ul->appendChild( $doc->createTextNode( "\n" ) ); $ul->appendChild( $li ); } /** @inheritDoc */ public function dimensions( Opts $opts ): string { return "{$opts->imageWidth}x{$opts->imageHeight}px"; } /** * @param Opts $opts * @param Element $wrapper * @return int|float */ protected function scaleMedia( Opts $opts, Element $wrapper ) { return $opts->imageWidth; } /** * @param float|int $width * @return float|int */ protected function thumbWidth( $width ) { return $width + $this->padding->thumb; } /** * @param float|int $height * @return float|int */ protected function thumbHeight( $height ) { return $height + $this->padding->thumb; } /** * @param float|int $width * @param float|int $height * @param bool $hasError * @return string */ protected function thumbStyle( $width, $height, bool $hasError ): string { $style = []; if ( !$hasError ) { $style[] = 'width: ' . $this->thumbWidth( $width ) . 'px;'; } if ( $hasError || $this->mode === 'traditional' ) { $style[] = 'height: ' . $this->thumbHeight( $height ) . 'px;'; } return implode( ' ', $style ); } /** * @param float|int $width * @return float|int */ protected function boxWidth( $width ) { return $this->thumbWidth( $width ) + $this->padding->box; } /** * @param float|int $width * @param float|int $height * @return string */ protected function boxStyle( $width, $height ): string { return 'width: ' . $this->boxWidth( $width ) . 'px;'; } protected function galleryText( Document $doc, Element $box, ?Element $gallerytext, float $width ): void { $div = $doc->createElement( 'div' ); $div->setAttribute( 'class', 'gallerytext' ); if ( $gallerytext ) { ParsoidExtensionAPI::migrateChildrenAndTransferWrapperDataAttribs( $gallerytext, $div ); } $box->appendChild( $div ); } private function line( Opts $opts, Element $ul, ParsedLine $o ): void { $doc = $ul->ownerDocument; $width = $this->scaleMedia( $opts, $o->thumb ); $height = $opts->imageHeight; $box = $doc->createElement( 'li' ); $box->setAttribute( 'class', 'gallerybox' ); $box->setAttribute( 'style', $this->boxStyle( $width, $height ) ); DOMDataUtils::getDataParsoid( $box )->dsr = $o->dsr; $thumb = $doc->createElement( 'div' ); $thumb->setAttribute( 'class', 'thumb' ); $thumb->setAttribute( 'style', $this->thumbStyle( $width, $height, $o->hasError ) ); $wrapper = $doc->createElement( 'span' ); $wrapper->setAttribute( 'typeof', $o->rdfaType ); ParsoidExtensionAPI::migrateChildrenAndTransferWrapperDataAttribs( $o->thumb, $wrapper ); $thumb->appendChild( $wrapper ); $box->appendChild( $thumb ); $this->galleryText( $doc, $box, $o->gallerytext, $width ); $ul->appendChild( $doc->createTextNode( "\n" ) ); $ul->appendChild( $box ); } /** @inheritDoc */ public function render( ParsoidExtensionAPI $extApi, Opts $opts, ?DocumentFragment $caption, array $lines ): DocumentFragment { $domFragment = $extApi->htmlToDom( '' ); $ul = $this->ul( $opts, $domFragment ); if ( $caption ) { $this->caption( $opts, $ul, $caption ); } foreach ( $lines as $l ) { $this->line( $opts, $ul, $l ); } $ul->appendChild( $domFragment->ownerDocument->createTextNode( "\n" ) ); return $domFragment; } public function getModuleStyles(): array { return [ 'mediawiki.page.gallery.styles' ]; } } PK ! j�3�O O Gallery/Mode.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext\Gallery; use Wikimedia\Parsoid\DOM\DocumentFragment; use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI; abstract class Mode { /** * The name of this mode. * @var string */ protected $mode; /** * Construct a (singleton) mode. * @param string $name The name of this mode, all lowercase. */ protected function __construct( string $name ) { $this->mode = $name; } public function getModules(): array { return []; } public function getModuleStyles(): array { return []; } /** * Format the dimensions as a string. */ abstract public function dimensions( Opts $opts ): string; /** * Render HTML for the given lines in this mode. * @param ParsoidExtensionAPI $extApi * @param Opts $opts * @param ?DocumentFragment $caption * @param ParsedLine[] $lines * @return DocumentFragment */ abstract public function render( ParsoidExtensionAPI $extApi, Opts $opts, ?DocumentFragment $caption, array $lines ): DocumentFragment; /** * Return the Mode object with the given name, * or null if the name is invalid. * @param string $name * @return Mode|null */ public static function byName( string $name ): ?Mode { static $modesByName = null; if ( $modesByName === null ) { $modesByName = [ 'traditional' => new TraditionalMode(), 'nolines' => new NoLinesMode(), 'slideshow' => new SlideshowMode(), 'packed' => new PackedMode(), 'packed-hover' => new PackedHoverMode(), 'packed-overlay' => new PackedOverlayMode() ]; } return $modesByName[$name] ?? null; } } PK ! )1� � Gallery/PackedHoverMode.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext\Gallery; class PackedHoverMode extends PackedMode { /** * Create a PackedHoverMode singleton. * @param ?string $mode Only used by subclasses. */ protected function __construct( ?string $mode = null ) { parent::__construct( $mode ?? 'packed-hover' ); } /** @inheritDoc */ protected function useTraditionalGalleryText(): bool { return false; } } PK ! x�� � Gallery/PackedOverlayMode.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext\Gallery; class PackedOverlayMode extends PackedMode { /** * Create a PackedOverlayMode singleton. * @param ?string $mode Only used by subclasses. */ protected function __construct( ?string $mode = null ) { parent::__construct( $mode ?? 'packed-overlay' ); } /** @inheritDoc */ protected function useTraditionalGalleryText(): bool { return false; } } PK ! �,��I2 I2 Gallery/Gallery.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext\Gallery; use stdClass; use Wikimedia\Assert\UnreachableException; use Wikimedia\Parsoid\Core\ContentMetadataCollectorStringSets as CMCSS; use Wikimedia\Parsoid\Core\DomSourceRange; use Wikimedia\Parsoid\Core\MediaStructure; use Wikimedia\Parsoid\DOM\DocumentFragment; use Wikimedia\Parsoid\DOM\Element; use Wikimedia\Parsoid\DOM\Text; use Wikimedia\Parsoid\Ext\DiffDOMUtils; use Wikimedia\Parsoid\Ext\DiffUtils; use Wikimedia\Parsoid\Ext\DOMDataUtils; use Wikimedia\Parsoid\Ext\DOMUtils; use Wikimedia\Parsoid\Ext\ExtensionModule; use Wikimedia\Parsoid\Ext\ExtensionTagHandler; use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI; use Wikimedia\Parsoid\Ext\Utils; use Wikimedia\Parsoid\Utils\DOMCompat; /** * Implements the php parser's `renderImageGallery` natively. * * Params to support (on the extension tag): * - showfilename * - caption * - mode * - widths * - heights * - perrow * * A proposed spec is at: https://phabricator.wikimedia.org/P2506 */ class Gallery extends ExtensionTagHandler implements ExtensionModule { /** @inheritDoc */ public function getConfig(): array { return [ 'name' => 'Gallery', 'tags' => [ [ 'name' => 'gallery', 'handler' => self::class, 'options' => [ 'wt2html' => [ 'customizesDataMw' => true, ], 'outputHasCoreMwDomSpecMarkup' => true ], ] ], ]; } /** * Parse the gallery caption. * @param ParsoidExtensionAPI $extApi * @param array $extArgs * @return ?DocumentFragment */ private function pCaption( ParsoidExtensionAPI $extApi, array $extArgs ): ?DocumentFragment { return $extApi->extArgToDOM( $extArgs, 'caption' ); } /** * Parse a single line of the gallery. * @param ParsoidExtensionAPI $extApi * @param string $line * @param int $lineStartOffset * @param Opts $opts * @return ParsedLine|null */ private static function pLine( ParsoidExtensionAPI $extApi, string $line, int $lineStartOffset, Opts $opts ): ?ParsedLine { // Regexp from php's `renderImageGallery` if ( !preg_match( '/^([^|]+)(\|(?:.*))?$/D', $line, $matches ) ) { return null; } $oTitleStr = $matches[1]; $imageOptStr = $matches[2] ?? ''; $fileNs = $extApi->getSiteConfig()->canonicalNamespaceId( 'file' ); // WikiLinkHandler effectively decodes entities in titles by having // PEG decode entities and preserving the decoding while stringifying. // Match that behavior here by decoding entities in the title string. $decodedTitleStr = Utils::decodeWtEntities( $oTitleStr ); $noPrefix = false; $title = $extApi->makeTitle( $decodedTitleStr, 0 ); if ( $title === null || $title->getNamespace() !== $fileNs ) { // Try again, this time with a default namespace $title = $extApi->makeTitle( $decodedTitleStr, $fileNs ); $noPrefix = true; } if ( $title === null || $title->getNamespace() !== $fileNs ) { return null; } if ( $noPrefix ) { // Take advantage of $fileNs to give us the right namespace, since, // the explicit prefix isn't necessary in galleries but for the // wikilink syntax it is. Ex, // // <gallery> // Test.png // </gallery> // // vs [[File:Test.png]], here the File: prefix is necessary // // Note, this is no longer from source now $titleStr = $title->getPrefixedDBKey(); } else { $titleStr = $oTitleStr; } // A somewhat common editor mistake is to close a gallery line with // trailing square brackets, perhaps as a result of converting a file // from wikilink syntax. Unfortunately, the implementation in // renderMedia is not robust in the face of stray brackets. To boot, // media captions can contain wiklinks. if ( !preg_match( '/\[\[/', $imageOptStr, $m ) ) { $imageOptStr = preg_replace( '/]]$/D', '', $imageOptStr ); } $mode = Mode::byName( $opts->mode ); $imageOpts = [ [ $imageOptStr, $lineStartOffset + strlen( $oTitleStr ) ], // T305628: Dimensions are last one wins so ensure this takes // precedence over anything in $imageOptStr "|{$mode->dimensions( $opts )}", ]; $thumb = $extApi->renderMedia( $titleStr, $imageOpts, $error, // Force block for an easier structure to manipulate, otherwise // we have to pull the caption out of the data-mw true, // Suppress media formats since they aren't valid gallery media // options and we don't want to deal with rendering differences true ); if ( !$thumb || DOMCompat::nodeName( $thumb ) !== 'figure' ) { return null; } if ( $noPrefix ) { // Fiddling with the shadow attribute below, rather than using // DOMDataUtils::setShadowInfoIfModified, since WikiLinkHandler::renderFile // always sets a shadow (at minimum for the relative './') and that // method preserves the original source from the first time it's called, // though there's a FIXME to remove that behaviour. $media = $thumb->firstChild->firstChild; $dp = DOMDataUtils::getDataParsoid( $media ); $dp->sa['resource'] = $oTitleStr; } $doc = $thumb->ownerDocument; $rdfaType = DOMCompat::getAttribute( $thumb, 'typeof' ) ?? ''; // Detach figcaption as well $figcaption = DOMCompat::querySelector( $thumb, 'figcaption' ); DOMCompat::remove( $figcaption ); if ( $opts->showfilename ) { $file = $title->getPrefixedDBKey(); $galleryfilename = $doc->createElement( 'a' ); $galleryfilename->setAttribute( 'href', $extApi->getTitleUri( $title ) ); $galleryfilename->setAttribute( 'class', 'galleryfilename galleryfilename-truncate' ); $galleryfilename->setAttribute( 'title', $file ); $galleryfilename->appendChild( $doc->createTextNode( $file ) ); $figcaption->insertBefore( $galleryfilename, $figcaption->firstChild ); } $gallerytext = null; for ( $capChild = $figcaption->firstChild; $capChild !== null; $capChild = $capChild->nextSibling ) { if ( $capChild instanceof Text && preg_match( '/^\s*$/D', $capChild->nodeValue ) ) { // skip blank text nodes continue; } // Found a non-blank node! $gallerytext = $figcaption; break; } $dsr = new DomSourceRange( $lineStartOffset, $lineStartOffset + strlen( $line ), null, null ); return new ParsedLine( $thumb, $gallerytext, $rdfaType, $dsr ); } /** @inheritDoc */ public function sourceToDom( ParsoidExtensionAPI $extApi, string $content, array $args ): DocumentFragment { $attrs = $extApi->extArgsToArray( $args ); $opts = new Opts( $extApi, $attrs ); $offset = $extApi->extTag->getOffsets()->innerStart(); // Prepare the lines for processing $lines = explode( "\n", $content ); $lines = array_map( static function ( $line ) use ( &$offset ) { $lineObj = [ 'line' => $line, 'offset' => $offset ]; $offset += strlen( $line ) + 1; // For the nl return $lineObj; }, $lines ); $caption = $opts->caption ? $this->pCaption( $extApi, $args ) : null; $lines = array_map( function ( $lineObj ) use ( $extApi, $opts ) { return $this->pLine( $extApi, $lineObj['line'], $lineObj['offset'], $opts ); }, $lines ); // Drop invalid lines like "References: 5." $lines = array_filter( $lines, static function ( $lineObj ) { return $lineObj !== null; } ); $mode = Mode::byName( $opts->mode ); $extApi->getMetadata()->appendOutputStrings( CMCSS::MODULE, $mode->getModules() ); $extApi->getMetadata()->appendOutputStrings( CMCSS::MODULE_STYLE, $mode->getModuleStyles() ); $domFragment = $mode->render( $extApi, $opts, $caption, $lines ); $dataMw = $extApi->extTag->getDefaultDataMw(); // Remove extsrc from native extensions if ( // Self-closed tags don't have a body but unsetting on it induces one isset( $dataMw->body ) ) { unset( $dataMw->body->extsrc ); } // Remove the caption since it's redundant with the HTML // and we prefer editing it there. unset( $dataMw->attrs->caption ); DOMDataUtils::setDataMw( $domFragment->firstChild, $dataMw ); return $domFragment; } private function contentHandler( ParsoidExtensionAPI $extApi, Element $node ): string { $content = "\n"; for ( $child = $node->firstChild; $child; $child = $child->nextSibling ) { switch ( $child->nodeType ) { case XML_ELEMENT_NODE: DOMUtils::assertElt( $child ); // Ignore if it isn't a "gallerybox" if ( DOMCompat::nodeName( $child ) !== 'li' || !DOMUtils::hasClass( $child, 'gallerybox' ) ) { break; } $oContent = $extApi->getOrigSrc( $child, false, [ DiffUtils::class, 'subtreeUnchanged' ] ); if ( $oContent !== null ) { $content .= $oContent . "\n"; break; } $div = DOMCompat::querySelector( $child, '.thumb' ); if ( !$div ) { break; } $gallerytext = DOMCompat::querySelector( $child, '.gallerytext' ); if ( $gallerytext ) { $showfilename = DOMCompat::querySelector( $gallerytext, '.galleryfilename' ); if ( $showfilename ) { DOMCompat::remove( $showfilename ); // Destructive to the DOM! } } $thumb = DiffDOMUtils::firstNonSepChild( $div ); $ms = MediaStructure::parse( $thumb ); if ( $ms ) { // Unlike other inline media, the caption isn't found in the data-mw // of the container element. Hopefully this won't be necessary after T268250 $ms->captionElt = $gallerytext; // Destructive to the DOM! But, a convenient way to get the serializer // to ignore the fake dimensions that were added in pLine when parsing. DOMCompat::getClassList( $ms->containerElt )->add( 'mw-default-size' ); [ $line, $options ] = $extApi->serializeMedia( $ms ); if ( $options ) { $line .= '|' . $options; } } else { // TODO: Previously (<=1.5.0), we rendered valid titles // returning mw:Error (apierror-filedoesnotexist) as // plaintext. Continue to serialize this content until // that version is no longer supported. $line = $div->textContent; if ( $gallerytext ) { $caption = $extApi->domChildrenToWikitext( $gallerytext, $extApi::IN_IMG_CAPTION ); // Drop empty captions if ( !preg_match( '/^\s*$/D', $caption ) ) { $line .= '|' . $caption; } } } // Ensure that this only takes one line since gallery // tag content is split by line $line = str_replace( "\n", ' ', $line ); $content .= $line . "\n"; break; case XML_TEXT_NODE: case XML_COMMENT_NODE: // Ignore it break; default: throw new UnreachableException( 'should not be here!' ); } } return $content; } /** @inheritDoc */ public function domToWikitext( ParsoidExtensionAPI $extApi, Element $node, bool $wrapperUnmodified ) { $dataMw = DOMDataUtils::getDataMw( $node ); $dataMw->attrs ??= new stdClass; // Handle the "gallerycaption" first $galcaption = DOMCompat::querySelector( $node, 'li.gallerycaption' ); if ( $galcaption ) { $dataMw->attrs->caption = $extApi->domChildrenToWikitext( $galcaption, $extApi::IN_IMG_CAPTION | $extApi::IN_OPTION ); // Destructive to the DOM! // However, removing it simplifies some of the logic below. // Hopefully this won't be necessary after T268250 DOMCompat::remove( $galcaption ); } // Not having a body is a signal that the extension tag was parsed // as self-closed but, when serializing, we should make sure that // no content was added, otherwise it's uneditable. // // This relies on the caption having been removed above if ( DiffDOMUtils::firstNonSepChild( $node ) !== null ) { $dataMw->body ??= new stdClass; } $startTagSrc = $extApi->extStartTagToWikitext( $node ); if ( !isset( $dataMw->body ) ) { return $startTagSrc; // We self-closed this already. } else { $content = $extApi->getOrigSrc( $node, true, // The gallerycaption is nested as a list item but shouldn't // be considered when deciding if the body can be reused. // Hopefully this won't be necessary after T268250 // // Even though we've removed the caption from the DOM above, // it was present during DOM diff'ing, so a call to // DiffUtils::subtreeUnchanged is insufficient. static function ( Element $elt ): bool { for ( $child = $elt->firstChild; $child; $child = $child->nextSibling ) { if ( DiffUtils::hasDiffMarkers( $child ) ) { return false; } } return true; } ); if ( $content === null ) { $content = $this->contentHandler( $extApi, $node ); } return $startTagSrc . $content . '</' . $dataMw->name . '>'; } } /** @inheritDoc */ public function diffHandler( ParsoidExtensionAPI $extApi, callable $domDiff, Element $origNode, Element $editedNode ): bool { return call_user_func( $domDiff, $origNode, $editedNode ); } } PK ! �_ _ Gallery/PackedMode.phpnu �Iw�� <?php declare( strict_types = 1 ); namespace Wikimedia\Parsoid\Ext\Gallery; use Wikimedia\Parsoid\DOM\Document; use Wikimedia\Parsoid\DOM\Element; use Wikimedia\Parsoid\Ext\DOMUtils; use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI; use Wikimedia\Parsoid\Utils\DOMCompat; class PackedMode extends TraditionalMode { /** * Create a PackedMode singleton. * @param ?string $mode Only used by subclasses. */ protected function __construct( ?string $mode = null ) { parent::__construct( $mode ?? 'packed' ); $this->scale = 1.5; $this->padding = (object)[ 'thumb' => 0, 'box' => 2, 'border' => 8 ]; } /** @inheritDoc */ protected function perRow( Opts $opts, Element $ul ): void { /* do nothing */ } /** @inheritDoc */ public function dimensions( Opts $opts ): string { $height = floor( $opts->imageHeight * $this->scale ); // The legacy parser does this so that the width is not the contraining factor $width = floor( ( $opts->imageHeight * 10 + 100 ) * $this->scale ); return "{$width}x{$height}px"; } /** @inheritDoc */ public function scaleMedia( Opts $opts, Element $wrapper ) { $elt = $wrapper->firstChild->firstChild; DOMUtils::assertElt( $elt ); $width = DOMCompat::getAttribute( $elt, 'width' ); if ( !is_numeric( $width ) ) { $width = $opts->imageWidth; } else { $width = intval( $width, 10 ); $width /= $this->scale; } $elt->setAttribute( 'width', strval( ceil( $width ) ) ); $elt->setAttribute( 'height', "$opts->imageHeight" ); return $width; } protected function useTraditionalGalleryText(): bool { return true; } /** @inheritDoc */ protected function galleryText( Document $doc, Element $box, ?Element $gallerytext, float $width ): void { if ( $this->useTraditionalGalleryText() ) { parent::galleryText( $doc, $box, $gallerytext, $width ); return; } if ( !$gallerytext ) { return; } $div = $doc->createElement( 'div' ); $div->setAttribute( 'class', 'gallerytext' ); ParsoidExtensionAPI::migrateChildrenAndTransferWrapperDataAttribs( $gallerytext, $div ); $wrapper = $doc->createElement( 'div' ); $wrapper->setAttribute( 'class', 'gallerytextwrapper' ); $wrapper->setAttribute( 'style', 'width: ' . ceil( $width - 20 ) . 'px;' ); $wrapper->appendChild( $div ); $box->appendChild( $wrapper ); } public function getModules(): array { return [ 'mediawiki.page.gallery' ]; } } PK ! �;o'