Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 267 |
|
0.00% |
0 / 51 |
CRAP | |
0.00% |
0 / 1 |
ParsoidExtensionAPI | |
0.00% |
0 / 267 |
|
0.00% |
0 / 51 |
10506 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
pushError | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
createInterfaceI18nFragment | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
createPageContentI18nFragment | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
createLangI18nFragment | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addInterfaceI18nAttribute | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addPageContentI18nAttribute | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addLangI18nAttribute | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getErrors | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTopLevelDoc | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
newAboutId | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSiteConfig | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPageConfig | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getMetadata | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTitleUri | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPageUri | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
makeTitle | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
inTemplate | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isPreview | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
parentExtTag | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
parentExtTagOpts | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getContentDOM | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
clearContentDOM | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
wikitextToDOM | |
0.00% |
0 / 42 |
|
0.00% |
0 / 1 |
72 | |||
extTagToDOM | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
30 | |||
setTempNodeData | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getTempNodeData | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
extArgToDOM | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
20 | |||
extArgsToArray | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
findAndUpdateArg | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
addNewArg | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
log | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
processAttributeEmbeddedHTML | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
migrateChildrenAndTransferWrapperDataAttribs | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
preprocessWikitext | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
htmlToDom | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
domToHtml | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
setHtml2wtStateFlag | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
extStartTagToWikitext | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
domToWikitext | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
htmlToWikitext | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getOrigSrc | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
42 | |||
domChildrenToWikitext | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
escapeWikitext | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
postProcessDOM | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
renderMedia | |
0.00% |
0 / 70 |
|
0.00% |
0 / 1 |
600 | |||
serializeMedia | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
addModules | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addModuleStyles | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getExternalLinkAttribs | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addTrackingCategory | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace Wikimedia\Parsoid\Ext; |
5 | |
6 | use Closure; |
7 | use Wikimedia\Bcp47Code\Bcp47Code; |
8 | use Wikimedia\Parsoid\Config\Env; |
9 | use Wikimedia\Parsoid\Config\PageConfig; |
10 | use Wikimedia\Parsoid\Config\SiteConfig; |
11 | use Wikimedia\Parsoid\Core\ContentMetadataCollector; |
12 | use Wikimedia\Parsoid\Core\ContentMetadataCollectorStringSets as CMCSS; |
13 | use Wikimedia\Parsoid\Core\DomSourceRange; |
14 | use Wikimedia\Parsoid\Core\MediaStructure; |
15 | use Wikimedia\Parsoid\Core\Sanitizer; |
16 | use Wikimedia\Parsoid\DOM\Document; |
17 | use Wikimedia\Parsoid\DOM\DocumentFragment; |
18 | use Wikimedia\Parsoid\DOM\Element; |
19 | use Wikimedia\Parsoid\DOM\Node; |
20 | use Wikimedia\Parsoid\Fragments\PFragment; |
21 | use Wikimedia\Parsoid\Fragments\WikitextPFragment; |
22 | use Wikimedia\Parsoid\Html2Wt\ConstrainedText\WikiLinkText; |
23 | use Wikimedia\Parsoid\Html2Wt\LinkHandlerUtils; |
24 | use Wikimedia\Parsoid\Html2Wt\SerializerState; |
25 | use Wikimedia\Parsoid\NodeData\DataMwError; |
26 | use Wikimedia\Parsoid\Tokens\KV; |
27 | use Wikimedia\Parsoid\Tokens\SourceRange; |
28 | use Wikimedia\Parsoid\Utils\ContentUtils; |
29 | use Wikimedia\Parsoid\Utils\DOMCompat; |
30 | use Wikimedia\Parsoid\Utils\DOMDataUtils; |
31 | use Wikimedia\Parsoid\Utils\DOMUtils; |
32 | use Wikimedia\Parsoid\Utils\PipelineUtils; |
33 | use Wikimedia\Parsoid\Utils\Title; |
34 | use Wikimedia\Parsoid\Utils\TokenUtils; |
35 | use Wikimedia\Parsoid\Utils\Utils; |
36 | use Wikimedia\Parsoid\Utils\WTUtils; |
37 | use Wikimedia\Parsoid\Wikitext\Wikitext; |
38 | use Wikimedia\Parsoid\Wt2Html\DOM\Processors\AddMetaData; |
39 | use 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 | */ |
48 | class 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 | } |