Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 267
0.00% covered (danger)
0.00%
0 / 51
CRAP
0.00% covered (danger)
0.00%
0 / 1
ParsoidExtensionAPI
0.00% covered (danger)
0.00%
0 / 267
0.00% covered (danger)
0.00%
0 / 51
10506
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 pushError
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 createInterfaceI18nFragment
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createPageContentI18nFragment
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createLangI18nFragment
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addInterfaceI18nAttribute
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addPageContentI18nAttribute
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addLangI18nAttribute
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getErrors
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTopLevelDoc
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 newAboutId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSiteConfig
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPageConfig
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMetadata
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTitleUri
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPageUri
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 makeTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 inTemplate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isPreview
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 parentExtTag
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 parentExtTagOpts
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getContentDOM
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 clearContentDOM
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 wikitextToDOM
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
72
 extTagToDOM
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
30
 setTempNodeData
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getTempNodeData
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 extArgToDOM
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 extArgsToArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 findAndUpdateArg
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 addNewArg
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 log
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 processAttributeEmbeddedHTML
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 migrateChildrenAndTransferWrapperDataAttribs
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 preprocessWikitext
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 htmlToDom
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 domToHtml
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 setHtml2wtStateFlag
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 extStartTagToWikitext
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 domToWikitext
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 htmlToWikitext
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getOrigSrc
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 domChildrenToWikitext
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 escapeWikitext
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 postProcessDOM
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 renderMedia
0.00% covered (danger)
0.00%
0 / 70
0.00% covered (danger)
0.00%
0 / 1
600
 serializeMedia
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 addModules
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addModuleStyles
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExternalLinkAttribs
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addTrackingCategory
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Ext;
5
6use Closure;
7use Wikimedia\Bcp47Code\Bcp47Code;
8use Wikimedia\Parsoid\Config\Env;
9use Wikimedia\Parsoid\Config\PageConfig;
10use Wikimedia\Parsoid\Config\SiteConfig;
11use Wikimedia\Parsoid\Core\ContentMetadataCollector;
12use Wikimedia\Parsoid\Core\ContentMetadataCollectorStringSets as CMCSS;
13use Wikimedia\Parsoid\Core\DomSourceRange;
14use Wikimedia\Parsoid\Core\MediaStructure;
15use Wikimedia\Parsoid\Core\Sanitizer;
16use Wikimedia\Parsoid\DOM\Document;
17use Wikimedia\Parsoid\DOM\DocumentFragment;
18use Wikimedia\Parsoid\DOM\Element;
19use Wikimedia\Parsoid\DOM\Node;
20use Wikimedia\Parsoid\Fragments\PFragment;
21use Wikimedia\Parsoid\Fragments\WikitextPFragment;
22use Wikimedia\Parsoid\Html2Wt\ConstrainedText\WikiLinkText;
23use Wikimedia\Parsoid\Html2Wt\LinkHandlerUtils;
24use Wikimedia\Parsoid\Html2Wt\SerializerState;
25use Wikimedia\Parsoid\NodeData\DataMwError;
26use Wikimedia\Parsoid\Tokens\KV;
27use Wikimedia\Parsoid\Tokens\SourceRange;
28use Wikimedia\Parsoid\Utils\ContentUtils;
29use Wikimedia\Parsoid\Utils\DOMCompat;
30use Wikimedia\Parsoid\Utils\DOMDataUtils;
31use Wikimedia\Parsoid\Utils\DOMUtils;
32use Wikimedia\Parsoid\Utils\PipelineUtils;
33use Wikimedia\Parsoid\Utils\Title;
34use Wikimedia\Parsoid\Utils\TokenUtils;
35use Wikimedia\Parsoid\Utils\Utils;
36use Wikimedia\Parsoid\Utils\WTUtils;
37use Wikimedia\Parsoid\Wikitext\Wikitext;
38use Wikimedia\Parsoid\Wt2Html\DOM\Processors\AddMetaData;
39use Wikimedia\Parsoid\Wt2Html\Frame;
40
41/**
42 * Extensions are expected to use only these interfaces and strongly discouraged from
43 * calling Parsoid code directly. Code review is expected to catch these discouraged
44 * code patterns. We'll have to finish grappling with the extension and hooks API
45 * to go down this path seriously. Till then, we'll have extensions leveraging existing
46 * code as in the native extension code in this repository.
47 */
48class ParsoidExtensionAPI {
49    /** @var Env */
50    private $env;
51
52    /** @var ?Frame */
53    private $frame;
54
55    /** @var ?ExtensionTag */
56    public $extTag;
57
58    /**
59     * @var ?array TokenHandler options
60     */
61    private $wt2htmlOpts;
62
63    /**
64     * @var ?array Serialiation options / state
65     */
66    private $html2wtOpts;
67
68    /**
69     * @var ?SerializerState
70     */
71    private $serializerState;
72
73    /**
74     * Errors collected while parsing.  This is used to indicate that, although
75     * an extension is returning a dom fragment, errors were encountered while
76     * generating it and should be marked up with the mw:Error typeof.
77     *
78     * @var list<DataMwError>
79     */
80    private $errors = [];
81
82    /**
83     * @param Env $env
84     * @param ?array $options
85     *  - wt2html: used in wt->html direction
86     *    - frame: (Frame)
87     *    - parseOpts: (array)
88     *      - extTag: (string)
89     *      - extTagOpts: (array)
90     *      - inTemplate: (bool)
91     *    - extTag: (ExtensionTag)
92     *  - html2wt: used in html->wt direction
93     *    - state: (SerializerState)
94     */
95    public function __construct(
96        Env $env, ?array $options = null
97    ) {
98        $this->env = $env;
99        $this->wt2htmlOpts = $options['wt2html'] ?? null;
100        $this->html2wtOpts = $options['html2wt'] ?? null;
101        $this->serializerState = $this->html2wtOpts['state'] ?? null;
102        $this->frame = $this->wt2htmlOpts['frame'] ?? $env->topFrame ?? null;
103        $this->extTag = $this->wt2htmlOpts['extTag'] ?? null;
104    }
105
106    /**
107     * Collect errors while parsing.  If processing can't continue, an
108     * ExtensionError should be thrown instead.
109     *
110     * $key and $params are basically the arguments to wfMessage, although they
111     * will be stored in the data-mw of the encapsulation wrapper.
112     *
113     * See https://www.mediawiki.org/wiki/Specs/HTML#Error_handling
114     *
115     * The returned fragment can be inserted in the dom and will be populated
116     * with the localized message.  See T266666
117     *
118     * @unstable
119     * @param string $key
120     * @param mixed ...$params
121     * @return DocumentFragment
122     */
123    public function pushError( string $key, ...$params ): DocumentFragment {
124        $this->errors[] = new DataMwError( $key, $params );
125        return WTUtils::createInterfaceI18nFragment( $this->getTopLevelDoc(), $key, $params );
126    }
127
128    /**
129     * Creates an internationalization (i18n) message that will be localized into the user
130     * interface language. The returned DocumentFragment contains, as a single child, a span
131     * element with the appropriate information for later localization.
132     * @param string $key message key for the message to be localized
133     * @param ?array $params parameters for localization
134     * @return DocumentFragment
135     */
136    public function createInterfaceI18nFragment( string $key, ?array $params ): DocumentFragment {
137        return WTUtils::createInterfaceI18nFragment( $this->getTopLevelDoc(), $key, $params );
138    }
139
140    /**
141     * Creates an internationalization (i18n) message that will be localized into the page content
142     * language. The returned DocumentFragment contains, as a single child, a span
143     * element with the appropriate information for later localization.
144     * @param string $key message key for the message to be localized
145     * @param ?array $params parameters for localization
146     * @return DocumentFragment
147     */
148    public function createPageContentI18nFragment( string $key, ?array $params ): DocumentFragment {
149        return WTUtils::createPageContentI18nFragment( $this->getTopLevelDoc(), $key, $params );
150    }
151
152    /**
153     * Creates an internationalization (i18n) message that will be localized into an arbitrary
154     * language. The returned DocumentFragment contains, as a single child, a span
155     * element with the appropriate information for later localization.
156     * The use of this method is discouraged; use ::createPageContentI18nFragment(...) and
157     * ::createInterfaceI18nFragment(...) where possible rather than, respectively,
158     * ::createLangI18nFragment($wgContLang, ...) and ::createLangI18nFragment($wgLang, ...).
159     * @param Bcp47Code $lang language in which the message will be localized
160     * @param string $key message key for the message to be localized
161     * @param ?array $params parameters for localization
162     * @return DocumentFragment
163     */
164    public function createLangI18nFragment( Bcp47Code $lang, string $key, ?array $params ): DocumentFragment {
165        return WTUtils::createLangI18nFragment( $this->getTopLevelDoc(), $lang, $key, $params );
166    }
167
168    /**
169     * Adds to $element the internationalization information needed for the attribute $name to be
170     * localized in a later pass into the user interface language.
171     * @param Element $element element on which to add internationalization information
172     * @param string $name name of the attribute whose value will be localized
173     * @param string $key message key used for the attribute value localization
174     * @param ?array $params parameters for localization
175     */
176    public function addInterfaceI18nAttribute(
177        Element $element, string $name, string $key, ?array $params
178    ): void {
179        WTUtils::addInterfaceI18nAttribute( $element, $name, $key, $params );
180    }
181
182    /**
183     * Adds to $element the internationalization information needed for the attribute $name to be
184     * localized in a later pass into the page content language.
185     * @param Element $element element on which to add internationalization information
186     * @param string $name name of the attribute whose value will be localized
187     * @param string $key message key used for the attribute value localization
188     * @param array $params parameters for localization
189     */
190    public function addPageContentI18nAttribute(
191        Element $element, string $name, string $key, array $params
192    ): void {
193        WTUtils::addPageContentI18nAttribute( $element, $name, $key, $params );
194    }
195
196    /**
197     * Adds to $element the internationalization information needed for the attribute $name to be
198     * localized in a later pass into the provided language.
199     * The use of this method is discouraged; use ::addPageContentI18nAttribute(...) and
200     * ::addInterfaceI18nAttribute(...) where possible rather than, respectively,
201     * ::addLangI18nAttribute(..., $wgContLang, ...) and ::addLangI18nAttribute(..., $wgLang, ...).
202     * @param Element $element element on which to add internationalization information
203     * @param Bcp47Code $lang language in which the  attribute will be localized
204     * @param string $name name of the attribute whose value will be localized
205     * @param string $key message key used for the attribute value localization
206     * @param array $params parameters for localization
207     */
208    public function addLangI18nAttribute(
209        Element $element, Bcp47Code $lang, string $name, string $key, array $params
210    ) {
211        WTUtils::addLangI18nAttribute( $element, $lang, $name, $key, $params );
212    }
213
214    /**
215     * @return list<DataMwError>
216     */
217    public function getErrors(): array {
218        return $this->errors;
219    }
220
221    /**
222     * Returns the main document we're parsing.  Extension content is parsed
223     * to fragments of this document.
224     *
225     * @return Document
226     */
227    public function getTopLevelDoc(): Document {
228        return $this->env->getTopLevelDoc();
229    }
230
231    /**
232     * Get a new about id for marking extension output
233     * FIXME: This should never really be needed since the extension API
234     * handles this on behalf of extensions, but Cite has one use case
235     * where implicit <references /> output is added.
236     *
237     * @return string
238     */
239    public function newAboutId(): string {
240        return $this->env->newAboutId();
241    }
242
243    /**
244     * Get the site configuration to let extensions customize
245     * their behavior based on how the wiki is configured.
246     *
247     * @return SiteConfig
248     */
249    public function getSiteConfig(): SiteConfig {
250        return $this->env->getSiteConfig();
251    }
252
253    /**
254     * FIXME: Unsure if we need to provide this access yet
255     * Get the page configuration
256     * @return PageConfig
257     */
258    public function getPageConfig(): PageConfig {
259        return $this->env->getPageConfig();
260    }
261
262    /**
263     * Get the ContentMetadataCollector corresponding to the top-level page.
264     * In Parsoid integrated mode this will typically be an instance of
265     * core's `ParserOutput` class.
266     *
267     * @return ContentMetadataCollector
268     */
269    public function getMetadata(): ContentMetadataCollector {
270        return $this->env->getMetadata();
271    }
272
273    /**
274     * Get the URI to link to a title
275     * @param Title $title
276     * @return string
277     */
278    public function getTitleUri( Title $title ): string {
279        return $this->env->makeLink( $title );
280    }
281
282    /**
283     * Get an URI for the current page
284     * @return string
285     */
286    public function getPageUri(): string {
287        return $this->getTitleUri( $this->env->getContextTitle() );
288    }
289
290    /**
291     * Make a title from an input string
292     * @param string $str
293     * @param int $namespaceId
294     * @return ?Title
295     */
296    public function makeTitle( string $str, int $namespaceId ): ?Title {
297        return $this->env->makeTitleFromText( $str, $namespaceId, true /* no exceptions */ );
298    }
299
300    /**
301     * Are we parsing in a template context?
302     * @return bool
303     */
304    public function inTemplate(): bool {
305        return $this->wt2htmlOpts['parseOpts']['inTemplate'] ?? false;
306    }
307
308    /**
309     * Are we parsing for a preview?
310     * FIXME: Right now, we never do; when we do, this needs to be modified to reflect reality
311     * @unstable
312     * @return bool
313     */
314    public function isPreview(): bool {
315        return false;
316    }
317
318    /**
319     * FIXME: Is this something that can come from the frame?
320     * If we are parsing in the context of a parent extension tag,
321     * return the name of that extension tag
322     * @return string|null
323     */
324    public function parentExtTag(): ?string {
325        return $this->wt2htmlOpts['parseOpts']['extTag'] ?? null;
326    }
327
328    /**
329     * FIXME: Is this something that can come from the frame?
330     * If we are parsing in the context of a parent extension tag,
331     * return the parsing options set by that tag.
332     * @return array
333     */
334    public function parentExtTagOpts(): array {
335        return $this->wt2htmlOpts['parseOpts']['extTagOpts'] ?? [];
336    }
337
338    /**
339     * Get the content DOM corresponding to an id
340     * @param string $contentId
341     * @return DocumentFragment
342     */
343    public function getContentDOM( string $contentId ): DocumentFragment {
344        return $this->env->getDOMFragment( $contentId );
345    }
346
347    public function clearContentDOM( string $contentId ): void {
348        $this->env->removeDOMFragment( $contentId );
349    }
350
351    /**
352     * Parse wikitext to DOM
353     *
354     * @param string|PFragment $wikitextOrPFragment
355     * @param array $opts
356     * - srcOffsets
357     * - processInNewFrame
358     * - clearDSROffsets
359     * - shiftDSRFn
360     * - parseOpts
361     *   - extTag
362     *   - extTagOpts
363     *   - context "inline", "block", etc. Currently, only "inline" is supported
364     * @param bool $sol Whether tokens should be processed in start-of-line context.
365     * @return DocumentFragment
366     */
367    public function wikitextToDOM(
368        $wikitextOrPFragment, array $opts, bool $sol
369    ): DocumentFragment {
370        if ( is_string( $wikitextOrPFragment ) ) {
371            $srcOffsets = $opts['srcOffsets'] ?? null;
372            if ( $srcOffsets !== null && !$srcOffsets instanceof DomSourceRange ) {
373                $srcOffsets = DomSourceRange::fromTsr( $srcOffsets );
374            }
375            $pFragment = WikitextPFragment::newFromWt(
376                $wikitextOrPFragment, $srcOffsets
377            );
378        } else {
379            $pFragment = $wikitextOrPFragment;
380        }
381        if ( $pFragment->isEmpty() ) {
382            $domFragment = $this->getTopLevelDoc()->createDocumentFragment();
383        } else {
384            // Parse content to DOM and pass DOM-fragment token back to the main pipeline.
385            // The DOM will get unwrapped and integrated  when processing the top level document.
386            [ $wikitext, $pFragmentMap ] =
387                PipelineUtils::pFragmentToParsoidFragmentMarkers( $pFragment );
388            $srcOffsets = $pFragment->getSrcOffsets() ?? $opts['srcOffsets'] ?? null;
389            $parseOpts = $opts['parseOpts'] ?? [];
390            $frame = $this->frame;
391            if ( !empty( $opts['processInNewFrame'] ) ) {
392                $frame = $frame->newChild( $frame->getTitle(), [], $wikitext );
393                $srcOffsets = new SourceRange( 0, strlen( $wikitext ) );
394            }
395            $this->env->addToPFragmentMap( $pFragmentMap );
396            $domFragment = PipelineUtils::processContentInPipeline(
397                $this->env, $frame, $wikitext,
398                [
399                    // Full pipeline for processing content
400                    'pipelineType' => 'wikitext-to-fragment',
401                    'pipelineOpts' => [
402                        'expandTemplates' => true,
403                        'extTag' => $parseOpts['extTag'] ?? null,
404                        'extTagOpts' => $parseOpts['extTagOpts'] ?? null,
405                        'inTemplate' => $this->inTemplate(),
406                        'inlineContext' => ( $parseOpts['context'] ?? '' ) === 'inline',
407                    ],
408                    'srcOffsets' => $srcOffsets,
409                    'sol' => $sol,
410                ]
411            );
412
413            if ( !empty( $opts['clearDSROffsets'] ) ) {
414                $dsrFn = static function ( DomSourceRange $dsr ) {
415                    return null;
416                };
417            } else {
418                $dsrFn = $opts['shiftDSRFn'] ?? null;
419            }
420
421            if ( $dsrFn ) {
422                ContentUtils::shiftDSR( $this->env, $domFragment, $dsrFn, $this );
423            }
424        }
425        return $domFragment;
426    }
427
428    /**
429     * Parse extension tag to DOM.
430     *
431     * If a wrapper tag is requested, beyond parsing the contents of the extension tag,
432     * this method wraps the contents in a custom wrapper element (ex: <div>), sanitizes
433     * the arguments of the extension args and sets some content flags on the wrapper.
434     *
435     * @param array $extArgs Args sanitized and applied to wrapper
436     * @param string $wikitext Wikitext content of the tag
437     * @param array $opts
438     * - srcOffsets
439     * - wrapperTag (skip OR pass null to not add any wrapper tag)
440     * - processInNewFrame
441     * - clearDSROffsets
442     * - shiftDSRFn
443     * - parseOpts
444     *   - extTag
445     *   - extTagOpts
446     *   - context
447     * @return DocumentFragment "prepared and loaded"
448     */
449    public function extTagToDOM(
450        array $extArgs, string $wikitext, array $opts
451    ): DocumentFragment {
452        $extTagOffsets = $this->extTag->getOffsets();
453        if ( !isset( $opts['srcOffsets'] ) ) {
454            $opts['srcOffsets'] = new SourceRange(
455                $extTagOffsets->innerStart(),
456                $extTagOffsets->innerEnd()
457            );
458        }
459
460        $domFragment = $this->wikitextToDOM( $wikitext, $opts, true /* sol */ );
461        if ( !empty( $opts['wrapperTag'] ) ) {
462            // Create a wrapper and migrate content into the wrapper
463            $wrapper = $domFragment->ownerDocument->createElement(
464                $opts['wrapperTag']
465            );
466            DOMUtils::migrateChildren( $domFragment, $wrapper );
467            $domFragment->appendChild( $wrapper );
468
469            // Sanitize args and set on the wrapper
470            Sanitizer::applySanitizedArgs( $this->env->getSiteConfig(), $wrapper, $extArgs );
471
472            // Mark empty content DOMs
473            if ( $wikitext === '' ) {
474                DOMDataUtils::getDataParsoid( $wrapper )->empty = true;
475            }
476
477            if ( !empty( $this->extTag->isSelfClosed() ) ) {
478                DOMDataUtils::getDataParsoid( $wrapper )->selfClose = true;
479            }
480        }
481
482        return $domFragment;
483    }
484
485    /**
486     * Set temporary data into the DOM node that will be discarded
487     * when DOM is serialized
488     *
489     * Use the tag name as the key for TempData management
490     *
491     * @param Element $node
492     * @param mixed $data
493     */
494    public function setTempNodeData( Element $node, $data ): void {
495        $dataParsoid = DOMDataUtils::getDataParsoid( $node );
496        $tmpData = $dataParsoid->getTemp();
497        $key = $this->extTag->getName();
498        $tmpData->setTagData( $key, $data );
499    }
500
501    /**
502     * Get temporary data into the DOM node that will be discarded
503     * when DOM is serialized.
504     *
505     * This should only be used when the ExtensionTag is not available; otherwise access the newly created data
506     * directly.
507     *
508     * @param Element $node
509     * @param string $key to access TmpData
510     * @return mixed
511     * @unstable
512     */
513    public function getTempNodeData( Element $node, string $key ) {
514        if ( $this->extTag ) {
515            throw new \RuntimeException(
516                'ExtensionTag is available. Data should be available directly through the DOM.'
517            );
518        }
519        $dataParsoid = DOMDataUtils::getDataParsoid( $node );
520        $tmpData = $dataParsoid->getTemp();
521        return $tmpData->getTagData( $key );
522    }
523
524    /**
525     * Process a specific extension arg as wikitext and return its DOM equivalent.
526     * By default, this method processes the argument value in inline context and normalizes
527     * every whitespace character to a single space.
528     * @param KV[] $extArgs
529     * @param string $key should be lower-case
530     * @param string $context
531     * @return ?DocumentFragment
532     */
533    public function extArgToDOM(
534        array $extArgs, string $key, string $context = "inline"
535    ): ?DocumentFragment {
536        $argKV = KV::lookupKV( $extArgs, strtolower( $key ) );
537        if ( $argKV === null || !$argKV->v ) {
538            return null;
539        }
540
541        if ( $context === "inline" ) {
542            // `normalizeExtOptions` can mess up source offsets as well as the string
543            // that ought to be processed as wikitext. So, we do our own whitespace
544            // normalization of the original source here.
545            //
546            // If 'context' is 'inline' below, it ensures indent-pre / p-wrapping is suppressed.
547            // So, the normalization is primarily for HTML string parity.
548            $argVal = preg_replace( '/[\t\r\n ]/', ' ', $argKV->vsrc );
549        } else {
550            $argVal = $argKV->vsrc;
551        }
552
553        return $this->wikitextToDOM(
554            $argVal,
555            [
556                'parseOpts' => [
557                    'extTag' => $this->extTag->getName(),
558                    'context' => $context,
559                ],
560                'srcOffsets' => $argKV->valueOffset(),
561            ],
562            false // inline context => no sol state
563        );
564    }
565
566    /**
567     * Convert the ext args representation from an array of KV objects
568     * to a plain associative array mapping arg name strings to arg value strings.
569     * @param array<KV> $extArgs
570     * @return array<string,string>
571     */
572    public function extArgsToArray( array $extArgs ): array {
573        return TokenUtils::kvToHash( $extArgs );
574    }
575
576    /**
577     * This method finds a requested arg by key name and return its current value.
578     * If a closure is passed in to update the current value, it is used to update the arg.
579     *
580     * @param KV[] &$extArgs Array of extension args
581     * @param string $key Argument key whose value needs an update
582     * @param ?Closure $updater $updater will get the existing string value
583     *   for the arg and is expected to return an updated value.
584     * @return ?string
585     */
586    public function findAndUpdateArg(
587        array &$extArgs, string $key, ?Closure $updater = null
588    ): ?string {
589        // FIXME: This code will get an overhaul when T250854 is resolved.
590        foreach ( $extArgs as $i => $kv ) {
591            $k = TokenUtils::tokensToString( $kv->k );
592            if ( strtolower( trim( $k ) ) === strtolower( $key ) ) {
593                $val = $kv->v;
594                if ( $updater ) {
595                    $kv = clone $kv;
596                    $kv->v = $updater( TokenUtils::tokensToString( $val ) );
597                    $extArgs[$i] = $kv;
598                }
599                return $val;
600            }
601        }
602
603        return null;
604    }
605
606    /**
607     * This method adds a new argument to the extension args array
608     * @param KV[] &$extArgs
609     * @param string $key
610     * @param string $value
611     */
612    public function addNewArg( array &$extArgs, string $key, string $value ): void {
613        $extArgs[] = new KV( $key, $value );
614    }
615
616    // TODO: Provide support for extensions to register lints
617    // from their customized lint handlers.
618
619    /**
620     * Forwards the logging request to the underlying logger
621     * @param string $prefix
622     * @param mixed ...$args
623     */
624    public function log( string $prefix, ...$args ): void {
625        $this->env->log( $prefix, ...$args );
626    }
627
628    /**
629     * Extensions might be interested in examining (their) content embedded
630     * in data-mw attributes that don't otherwise show up in the DOM.
631     *
632     * Ex: inline media captions that aren't rendered, language variant markup,
633     *     attributes that are transcluded. More scenarios might be added later.
634     *
635     * @param Element $elt The node whose data attributes need to be examined
636     * @param Closure $proc The processor that will process the embedded HTML
637     *        Signature: (string) -> string
638     *        This processor will be provided the HTML string as input
639     *        and is expected to return a possibly modified string.
640     */
641    public function processAttributeEmbeddedHTML( Element $elt, Closure $proc ): void {
642        ContentUtils::processAttributeEmbeddedHTML( $this, $elt, $proc );
643    }
644
645    /**
646     * Copy $from->childNodes to $to and clone the data attributes of $from
647     * to $to.
648     *
649     * @param Element $from
650     * @param Element $to
651     */
652    public static function migrateChildrenAndTransferWrapperDataAttribs(
653        Element $from, Element $to
654    ): void {
655        DOMUtils::migrateChildren( $from, $to );
656        DOMDataUtils::setDataParsoid(
657            $to, clone DOMDataUtils::getDataParsoid( $from )
658        );
659        DOMDataUtils::setDataMw(
660            $to, clone DOMDataUtils::getDataMw( $from )
661        );
662    }
663
664    /**
665     * Equivalent of 'preprocess' from Parser.php in core.
666     * - expands templates
667     * - replaces magic variables
668     * This does not run any hooks however since that would be unexpected.
669     * This also doesn't support replacing template args from a frame.
670     *
671     * @param string $wikitext
672     * @return array{error:bool,src?:string,fragment?:PFragment}
673     *  - 'error' did we hit resource limits?
674     *  - 'src' expanded wikitext OR error message to print
675     *     FIXME: Maybe error message should be localizable
676     *  - 'fragment' Optional fragment (wikitext plus strip state)
677     */
678    public function preprocessWikitext( string $wikitext ) {
679        return Wikitext::preprocess( $this->env, $wikitext );
680    }
681
682    /**
683     * Parse input string into DOM.
684     * NOTE: This leaves the DOM in Parsoid-canonical state and is the preferred method
685     * to convert HTML to DOM that will be passed into Parsoid's processing code.
686     *
687     * @param string $html
688     * @param ?Document $doc XXX You probably don't want to be doing this
689     * @param ?array $options
690     * @return DocumentFragment
691     */
692    public function htmlToDom(
693        string $html, ?Document $doc = null, ?array $options = []
694    ): DocumentFragment {
695        return ContentUtils::createAndLoadDocumentFragment(
696            $doc ?? $this->getTopLevelDoc(), $html, $options
697        );
698    }
699
700    /**
701     * Serialize DOM element to string (inner/outer HTML is controlled by flag).
702     * If $releaseDom is set to true, the DOM will be left in non-canonical form
703     * and is not safe to use after this call. This is primarily a performance optimization.
704     *
705     * @param Node $node
706     * @param bool $innerHTML if true, inner HTML of the element will be returned
707     *    This flag defaults to false
708     * @param bool $releaseDom if true, the DOM will not be in canonical form after this call
709     *    This flag defaults to false
710     * @return string
711     */
712    public function domToHtml(
713        Node $node, bool $innerHTML = false, bool $releaseDom = false
714    ): string {
715        // FIXME: This is going to drop any diff markers but since
716        // the dom differ doesn't traverse into extension content (right now),
717        // none should exist anyways.
718        $html = ContentUtils::ppToXML( $node, [ 'innerXML' => $innerHTML ] );
719        if ( !$releaseDom ) {
720            DOMDataUtils::visitAndLoadDataAttribs( $node );
721        }
722        return $html;
723    }
724
725    /**
726     * Bit flags describing escaping / serializing context in html -> wt mode
727     */
728    public const IN_SOL = 1;
729    public const IN_MEDIA = 2;
730    public const IN_LINK = 4;
731    public const IN_IMG_CAPTION = 8;
732    public const IN_OPTION = 16;
733
734    /**
735     * FIXME: This is a bit broken - shouldn't be needed ideally
736     * @param string $flag
737     */
738    public function setHtml2wtStateFlag( string $flag ) {
739        $this->serializerState->{$flag} = true;
740    }
741
742    /**
743     * Emit the opening tag (including attributes) for the extension
744     * represented by this node.
745     *
746     * @param Element $node
747     * @return string
748     */
749    public function extStartTagToWikitext( Element $node ): string {
750        $state = $this->serializerState;
751        return $state->serializer->serializeExtensionStartTag( $node, $state );
752    }
753
754    /**
755     * Convert the input DOM to wikitext.
756     *
757     * @param array $opts
758     *  - extName: (string) Name of the extension whose body we are serializing
759     *  - inPHPBlock: (bool) FIXME: This needs to be removed
760     * @param Element $node DOM to serialize
761     * @param bool $releaseDom If $releaseDom is set to true, the DOM will be left in
762     *  non-canonical form and is not safe to use after this call. This is primarily a
763     *  performance optimization.  This flag defaults to false.
764     * @return mixed
765     */
766    public function domToWikitext( array $opts, Element $node, bool $releaseDom = false ) {
767        // FIXME: WTS expects the input DOM to be a <body> element!
768        // Till that is fixed, we have to go through this round-trip!
769        // TODO: Move $node children to a fragment and call `$serializer->domToWikitext`
770        return $this->htmlToWikitext( $opts, $this->domToHtml( $node, $releaseDom ) );
771    }
772
773    /**
774     * Convert the HTML body of an extension to wikitext
775     *
776     * @param array $opts
777     *  - extName: (string) Name of the extension whose body we are serializing
778     *  - inPHPBlock: (bool) FIXME: This needs to be removed
779     * @param string $html HTML for the extension's body
780     * @return string
781     */
782    public function htmlToWikitext( array $opts, string $html ): string {
783        // Type cast so phan has more information to ensure type safety
784        $state = $this->serializerState;
785        $opts['env'] = $this->env;
786        return $state->serializer->htmlToWikitext( $opts, $html );
787    }
788
789    /**
790     * Get the original source for an element.
791     *
792     * The callable, $checkIfOrigSrcReusable, is used to determine if the $elt
793     * is unedited and therefore valid to reuse source.  This is assumed to be
794     * pretty specific to the callsite so no default is provided.
795     *
796     * @param Element $elt
797     * @param bool $inner
798     * @param callable $checkIfOrigSrcReusable
799     * @return string|null
800     */
801    public function getOrigSrc(
802        Element $elt, bool $inner, callable $checkIfOrigSrcReusable
803    ): ?string {
804        $state = $this->serializerState;
805        if ( !$state->selserMode || $state->inInsertedContent ) {
806            return null;
807        }
808        $dsr = DOMDataUtils::getDataParsoid( $elt )->dsr ?? null;
809        if ( !Utils::isValidDSR( $dsr, $inner ) ) {
810            return null;
811        }
812        if ( $checkIfOrigSrcReusable( $elt ) ) {
813            return $state->getOrigSrc(
814                $inner ? $dsr->innerRange() : $dsr
815            );
816        } else {
817            return null;
818        }
819    }
820
821    /**
822     * @param Element $elt
823     * @param int $context OR-ed bit flags specifying escaping / serialization context
824     * @return string
825     */
826    public function domChildrenToWikitext( Element $elt, int $context ): string {
827        $state = $this->serializerState;
828        if ( $context & self::IN_IMG_CAPTION ) {
829            if ( $context & self::IN_OPTION ) {
830                $escapeHandler = 'mediaOptionHandler'; // Escapes "|" as well
831            } else {
832                $escapeHandler = 'wikilinkHandler'; // image captions show up in wikilink syntax
833            }
834            $out = $state->serializeCaptionChildrenToString( $elt,
835                [ $state->serializer->wteHandlers, $escapeHandler ] );
836        } else {
837            throw new \RuntimeException( 'Not yet supported!' );
838        }
839        return $out;
840    }
841
842    /**
843     * Escape any wikitext like constructs in a string so that when the output
844     * is parsed, it renders as a string. The escaping is sensitive to the context
845     * in which the string is embedded. For example, a "*" is not safe at the start
846     * of a line (since it will parse as a list item), but is safe if it is not in
847     * a start of line context. Similarly the "|" character is safe outside tables,
848     * links, and transclusions.
849     *
850     * @param string $str
851     * @param Node $node
852     * @param int $context OR-ed bit flags specifying escaping / serialization context
853     * @return string
854     */
855    public function escapeWikitext( string $str, Node $node, int $context ): string {
856        if ( $context & ( self::IN_MEDIA | self::IN_LINK ) ) {
857            $state = $this->serializerState;
858            return $state->serializer->wteHandlers->escapeLinkContent(
859                $state, $str,
860                (bool)( $context & self::IN_SOL ),
861                $node,
862                (bool)( $context & self::IN_MEDIA )
863            );
864        } else {
865            throw new \RuntimeException( 'Not yet supported!' );
866        }
867    }
868
869    /**
870     * EXTAPI-FIXME: We have to figure out what it means to run a DOM pass
871     * (and what processors and what handlers apply) on content models that are
872     * not wikitext. For now, we are only storing data attribs back to the DOM
873     * and adding metadata to the page.
874     *
875     * @param Document $doc
876     */
877    public function postProcessDOM( Document $doc ): void {
878        // Ugh! But, this whole method needs to go away anyway
879        ( new AddMetaData( null ) )->run( $this->env, DOMCompat::getBody( $doc ) );
880    }
881
882    /**
883     * Produce the HTML rendering of a title string and media options as the
884     * wikitext parser would for a wikilink in the file namespace
885     *
886     * @param string $titleStr Image title string
887     * @param array $imageOpts Array of a mix of strings or arrays,
888     *   the latter of which can signify that the value came from source.
889     *   Where,
890     *     [0] is the fully-constructed image option
891     *     [1] is the full wikitext source offset for it
892     * @param ?string &$error Error string is set when the return is null.
893     * @param ?bool $forceBlock Forces the media to be rendered in a figure as
894     *   opposed to a span.
895     * @param ?bool $suppressMediaFormats If any media format is present in
896     *   $imageOpts, it won't be applied and will result in a linting error.
897     * @return ?Element
898     */
899    public function renderMedia(
900        string $titleStr, array $imageOpts, ?string &$error = null,
901        ?bool $forceBlock = false, ?bool $suppressMediaFormats = false
902    ): ?Element {
903        $extTagName = $this->extTag->getName();
904        $extTagOpts = [ 'suppressMediaFormats' => $suppressMediaFormats ];
905
906        $fileNs = $this->getSiteConfig()->canonicalNamespaceId( 'file' );
907
908        $title = $this->makeTitle( $titleStr, 0 );
909        if ( $title === null || $title->getNamespace() !== $fileNs ) {
910            $error = "{$extTagName}_no_image";
911            return null;
912        }
913
914        $pieces = [ '[[' ];
915        // Since the above two chars aren't from source, the resulting figure
916        // won't have any dsr info, so we can omit an offset for the title as
917        // well.  In any case, $titleStr may not necessarily be from source,
918        // see the special case in the gallery extension.
919        $pieces[] = $titleStr;
920        $pieces = array_merge( $pieces, $imageOpts );
921
922        if ( $forceBlock ) {
923            // We add "none" here so that this renders in the block form
924            // (ie. figure).  It's a valid media option, so shouldn't turn into
925            // a caption.  And since it's first wins, it shouldn't interfere
926            // with another horizontal alignment defined in $imageOpts.
927            // We just have to remember to strip the class below.
928            // NOTE: This will have to be adjusted with T305628
929            $pieces[] = '|none';
930        }
931
932        $pieces[] = ']]';
933
934        $shiftOffset = static function ( int $offset ) use ( $pieces ): ?int {
935            foreach ( $pieces as $p ) {
936                if ( is_string( $p ) ) {
937                    $offset -= strlen( $p );
938                    if ( $offset <= 0 ) {
939                        return null;
940                    }
941                } else {
942                    if ( $offset <= strlen( $p[0] ) && isset( $p[1] ) ) {
943                        return $p[1] + $offset;
944                    }
945                    $offset -= strlen( $p[0] );
946                    if ( $offset <= 0 ) {
947                        return null;
948                    }
949                }
950            }
951            return null;
952        };
953
954        $imageWt = '';
955        foreach ( $pieces as $p ) {
956            $imageWt .= ( is_string( $p ) ? $p : $p[0] );
957        }
958
959        $domFragment = $this->wikitextToDOM(
960            $imageWt,
961            [
962                'parseOpts' => [
963                    'extTag' => $extTagName,
964                    'extTagOpts' => $extTagOpts,
965                    'context' => 'inline',
966                ],
967                // Create new frame, because $pieces doesn't literally appear
968                // on the page, it has been hand-crafted here
969                'processInNewFrame' => true,
970                // Shift the DSRs in the DOM by startOffset, and strip DSRs
971                // for bits which aren't the caption or file, since they
972                // don't refer to actual source wikitext
973                'shiftDSRFn' => static function ( DomSourceRange $dsr ) use ( $shiftOffset ) {
974                    $start = $dsr->start === null ? null : $shiftOffset( $dsr->start );
975                    $end = $dsr->end === null ? null : $shiftOffset( $dsr->end );
976                    // If either offset is newly-invalid, remove entire DSR
977                    if (
978                        ( $dsr->start !== null && $start === null ) ||
979                        ( $dsr->end !== null && $end === null )
980                    ) {
981                        return null;
982                    }
983                    return new DomSourceRange(
984                        $start, $end, $dsr->openWidth, $dsr->closeWidth
985                    );
986                },
987            ],
988            true  // sol
989        );
990
991        $thumb = $domFragment->firstChild;
992        $validWrappers = [ 'figure' ];
993        // Downstream code expects a figcaption if we're forcing a block so we
994        // validate that we did indeed parse a figure.  It might not have
995        // happened because $imageOpts has an unbalanced `]]` which closes
996        // the wikilink syntax before we get in our `|none`.
997        if ( !$forceBlock ) {
998            $validWrappers[] = 'span';
999        }
1000        if ( !in_array( DOMCompat::nodeName( $thumb ), $validWrappers, true ) ) {
1001            $error = "{$extTagName}_invalid_image";
1002            return null;
1003        }
1004        DOMUtils::assertElt( $thumb );
1005
1006        // Detach the $thumb since the $domFragment is going out of scope
1007        // See https://bugs.php.net/bug.php?id=39593
1008        DOMCompat::remove( $thumb );
1009
1010        if ( $forceBlock ) {
1011            $dp = DOMDataUtils::getDataParsoid( $thumb );
1012            array_pop( $dp->optList );
1013            $explicitNone = false;
1014            foreach ( $dp->optList as $opt ) {
1015                if ( $opt['ck'] === 'none' ) {
1016                    $explicitNone = true;
1017                }
1018            }
1019            if ( !$explicitNone ) {
1020                // FIXME: Should we worry about someone adding this with the
1021                // "class=" option?
1022                DOMCompat::getClassList( $thumb )->remove( 'mw-halign-none' );
1023            }
1024        }
1025
1026        return $thumb;
1027    }
1028
1029    /**
1030     * Serialize a MediaStructure to a title and media options string.
1031     * The converse to ::renderMedia.
1032     *
1033     * @param MediaStructure $ms
1034     * @return array Where,
1035     *   [0] is the media title string
1036     *   [1] is the string of media options
1037     */
1038    public function serializeMedia( MediaStructure $ms ): array {
1039        $ct = LinkHandlerUtils::figureToConstrainedText( $this->serializerState, $ms );
1040        if ( $ct instanceof WikiLinkText ) {
1041            // Remove the opening and closing square brackets
1042            $text = substr( $ct->text, 2, -2 );
1043            return array_pad( explode( '|', $text, 2 ), 2, '' );
1044        } else {
1045            // Note that $ct could be an AutoURLLinkText, not just null
1046            return [ '', '' ];
1047        }
1048    }
1049
1050    /**
1051     * @param array $modules
1052     * @deprecated Use ::getMetadata()->appendOutputStrings( MODULE, ...) instead.
1053     */
1054    public function addModules( array $modules ) {
1055        $this->getMetadata()->appendOutputStrings( CMCSS::MODULE, $modules );
1056    }
1057
1058    /**
1059     * @param array $modulestyles
1060     * @deprecated Use ::getMetadata()->appendOutputStrings(MODULE_STYLE, ...) instead.
1061     */
1062    public function addModuleStyles( array $modulestyles ) {
1063        $this->getMetadata()->appendOutputStrings( CMCSS::MODULE_STYLE, $modulestyles );
1064    }
1065
1066    /**
1067     * Get an array of attributes to apply to an anchor linking to $url
1068     */
1069    public function getExternalLinkAttribs( string $url ): array {
1070        return $this->env->getExternalLinkAttribs( $url );
1071    }
1072
1073    /**
1074     * Add a tracking category to the current page.
1075     * @param string $key Message key (not localized)
1076     */
1077    public function addTrackingCategory( string $key ): void {
1078        $this->env->getDataAccess()->addTrackingCategory(
1079            $this->env->getPageConfig(),
1080            $this->env->getMetadata(),
1081            $key
1082        );
1083    }
1084}