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