Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 260 |
|
0.00% |
0 / 50 |
CRAP | |
0.00% |
0 / 1 |
ParsoidExtensionAPI | |
0.00% |
0 / 260 |
|
0.00% |
0 / 50 |
10100 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
pushError | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
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 / 31 |
|
0.00% |
0 / 1 |
30 | |||
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 |
56 | |||
domChildrenToWikitext | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
escapeWikitext | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
postProcessDOM | |
0.00% |
0 / 7 |
|
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 |
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\DomSourceRange; |
13 | use Wikimedia\Parsoid\Core\MediaStructure; |
14 | use Wikimedia\Parsoid\Core\Sanitizer; |
15 | use Wikimedia\Parsoid\DOM\Document; |
16 | use Wikimedia\Parsoid\DOM\DocumentFragment; |
17 | use Wikimedia\Parsoid\DOM\Element; |
18 | use Wikimedia\Parsoid\DOM\Node; |
19 | use Wikimedia\Parsoid\Html2Wt\ConstrainedText\WikiLinkText; |
20 | use Wikimedia\Parsoid\Html2Wt\LinkHandlerUtils; |
21 | use Wikimedia\Parsoid\Html2Wt\SerializerState; |
22 | use Wikimedia\Parsoid\Tokens\KV; |
23 | use Wikimedia\Parsoid\Tokens\SourceRange; |
24 | use Wikimedia\Parsoid\Utils\ContentUtils; |
25 | use Wikimedia\Parsoid\Utils\DOMCompat; |
26 | use Wikimedia\Parsoid\Utils\DOMDataUtils; |
27 | use Wikimedia\Parsoid\Utils\DOMUtils; |
28 | use Wikimedia\Parsoid\Utils\PipelineUtils; |
29 | use Wikimedia\Parsoid\Utils\Title; |
30 | use Wikimedia\Parsoid\Utils\TokenUtils; |
31 | use Wikimedia\Parsoid\Utils\Utils; |
32 | use Wikimedia\Parsoid\Utils\WTUtils; |
33 | use Wikimedia\Parsoid\Wikitext\Wikitext; |
34 | use Wikimedia\Parsoid\Wt2Html\DOMPostProcessor; |
35 | use 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 | */ |
44 | class 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[] = $err; |
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' => 'text/x-mediawiki/full', |
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 PP 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 | } |