Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
7.09% |
21 / 296 |
|
1.85% |
1 / 54 |
CRAP | |
0.00% |
0 / 1 |
ParsoidExtensionAPI | |
7.09% |
21 / 296 |
|
1.85% |
1 / 54 |
10171.10 | |
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 / 41 |
|
0.00% |
0 / 1 |
56 | |||
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 | |||
updateAllArgs | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
normalizeWhiteSpaceInArgs | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
7 | |||
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 | |||
processAttributeEmbeddedDom | |
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 | [ |
387 | 'frame' => $frame, |
388 | 'wikitext' => $wikitext, |
389 | 'srcOffsets' => $srcOffsets, |
390 | ] = PipelineUtils::preparePFragment( |
391 | $this->env, $this->frame, $pFragment, $opts |
392 | ); |
393 | $parseOpts = $opts['parseOpts'] ?? []; |
394 | $domFragment = PipelineUtils::processContentInPipeline( |
395 | $this->env, $frame, $wikitext, |
396 | [ |
397 | // Full pipeline for processing content |
398 | 'pipelineType' => 'wikitext-to-fragment', |
399 | 'pipelineOpts' => [ |
400 | 'expandTemplates' => true, |
401 | 'extTag' => $parseOpts['extTag'] ?? null, |
402 | 'extTagOpts' => $parseOpts['extTagOpts'] ?? null, |
403 | 'inTemplate' => $this->inTemplate(), |
404 | 'inlineContext' => ( $parseOpts['context'] ?? '' ) === 'inline', |
405 | ], |
406 | 'srcOffsets' => $srcOffsets, |
407 | 'sol' => $sol, |
408 | ] |
409 | ); |
410 | |
411 | if ( !empty( $opts['clearDSROffsets'] ) ) { |
412 | $dsrFn = static function ( DomSourceRange $dsr ) { |
413 | return null; |
414 | }; |
415 | } else { |
416 | $dsrFn = $opts['shiftDSRFn'] ?? null; |
417 | } |
418 | |
419 | if ( $dsrFn ) { |
420 | ContentUtils::shiftDSR( $this->env, $domFragment, $dsrFn, $this ); |
421 | } |
422 | } |
423 | return $domFragment; |
424 | } |
425 | |
426 | /** |
427 | * Parse extension tag to DOM. |
428 | * |
429 | * If a wrapper tag is requested, beyond parsing the contents of the extension tag, |
430 | * this method wraps the contents in a custom wrapper element (ex: <div>), sanitizes |
431 | * the arguments of the extension args and sets some content flags on the wrapper. |
432 | * |
433 | * @param array $extArgs Args sanitized and applied to wrapper |
434 | * @param string $wikitext Wikitext content of the tag |
435 | * @param array $opts |
436 | * - srcOffsets |
437 | * - wrapperTag (skip OR pass null to not add any wrapper tag) |
438 | * - processInNewFrame |
439 | * - clearDSROffsets |
440 | * - shiftDSRFn |
441 | * - parseOpts |
442 | * - extTag |
443 | * - extTagOpts |
444 | * - context |
445 | * @return DocumentFragment "prepared and loaded" |
446 | */ |
447 | public function extTagToDOM( |
448 | array $extArgs, string $wikitext, array $opts |
449 | ): DocumentFragment { |
450 | $extTagOffsets = $this->extTag->getOffsets(); |
451 | if ( !isset( $opts['srcOffsets'] ) ) { |
452 | $opts['srcOffsets'] = new SourceRange( |
453 | $extTagOffsets->innerStart(), |
454 | $extTagOffsets->innerEnd() |
455 | ); |
456 | } |
457 | |
458 | $domFragment = $this->wikitextToDOM( $wikitext, $opts, true /* sol */ ); |
459 | if ( !empty( $opts['wrapperTag'] ) ) { |
460 | // Create a wrapper and migrate content into the wrapper |
461 | $wrapper = $domFragment->ownerDocument->createElement( |
462 | $opts['wrapperTag'] |
463 | ); |
464 | DOMUtils::migrateChildren( $domFragment, $wrapper ); |
465 | $domFragment->appendChild( $wrapper ); |
466 | |
467 | // Sanitize args and set on the wrapper |
468 | Sanitizer::applySanitizedArgs( $this->env->getSiteConfig(), $wrapper, $extArgs ); |
469 | |
470 | // Mark empty content DOMs |
471 | if ( $wikitext === '' ) { |
472 | DOMDataUtils::getDataParsoid( $wrapper )->empty = true; |
473 | } |
474 | |
475 | if ( !empty( $this->extTag->isSelfClosed() ) ) { |
476 | DOMDataUtils::getDataParsoid( $wrapper )->selfClose = true; |
477 | } |
478 | } |
479 | |
480 | return $domFragment; |
481 | } |
482 | |
483 | /** |
484 | * Set temporary data into the DOM node that will be discarded |
485 | * when DOM is serialized |
486 | * |
487 | * Use the tag name as the key for TempData management |
488 | * |
489 | * @param Element $node |
490 | * @param mixed $data |
491 | */ |
492 | public function setTempNodeData( Element $node, $data ): void { |
493 | $dataParsoid = DOMDataUtils::getDataParsoid( $node ); |
494 | $tmpData = $dataParsoid->getTemp(); |
495 | $key = $this->extTag->getName(); |
496 | $tmpData->setTagData( $key, $data ); |
497 | } |
498 | |
499 | /** |
500 | * Get temporary data into the DOM node that will be discarded |
501 | * when DOM is serialized. |
502 | * |
503 | * This should only be used when the ExtensionTag is not available; otherwise access the newly created data |
504 | * directly. |
505 | * |
506 | * @param Element $node |
507 | * @param string $key to access TmpData |
508 | * @return mixed |
509 | * @unstable |
510 | */ |
511 | public function getTempNodeData( Element $node, string $key ) { |
512 | if ( $this->extTag ) { |
513 | throw new \RuntimeException( |
514 | 'ExtensionTag is available. Data should be available directly through the DOM.' |
515 | ); |
516 | } |
517 | $dataParsoid = DOMDataUtils::getDataParsoid( $node ); |
518 | $tmpData = $dataParsoid->getTemp(); |
519 | return $tmpData->getTagData( $key ); |
520 | } |
521 | |
522 | /** |
523 | * Process a specific extension arg as wikitext and return its DOM equivalent. |
524 | * By default, this method processes the argument value in inline context and normalizes |
525 | * every whitespace character to a single space. |
526 | * @param KV[] $extArgs |
527 | * @param string $key should be lower-case |
528 | * @param string $context |
529 | * @return ?DocumentFragment |
530 | */ |
531 | public function extArgToDOM( |
532 | array $extArgs, string $key, string $context = "inline" |
533 | ): ?DocumentFragment { |
534 | $argKV = KV::lookupKV( $extArgs, strtolower( $key ) ); |
535 | if ( $argKV === null || !$argKV->v ) { |
536 | return null; |
537 | } |
538 | |
539 | if ( $context === "inline" ) { |
540 | // `normalizeExtOptions` can mess up source offsets as well as the string |
541 | // that ought to be processed as wikitext. So, we do our own whitespace |
542 | // normalization of the original source here. |
543 | // |
544 | // If 'context' is 'inline' below, it ensures indent-pre / p-wrapping is suppressed. |
545 | // So, the normalization is primarily for HTML string parity. |
546 | $argVal = preg_replace( '/[\t\r\n ]/', ' ', $argKV->vsrc ); |
547 | } else { |
548 | $argVal = $argKV->vsrc; |
549 | } |
550 | |
551 | return $this->wikitextToDOM( |
552 | $argVal, |
553 | [ |
554 | 'parseOpts' => [ |
555 | 'extTag' => $this->extTag->getName(), |
556 | 'context' => $context, |
557 | ], |
558 | 'srcOffsets' => $argKV->valueOffset(), |
559 | ], |
560 | false // inline context => no sol state |
561 | ); |
562 | } |
563 | |
564 | /** |
565 | * Convert the ext args representation from an array of KV objects |
566 | * to a plain associative array mapping arg name strings to arg value strings. |
567 | * @param array<KV> $extArgs |
568 | * @return array<string,string> |
569 | */ |
570 | public function extArgsToArray( array $extArgs ): array { |
571 | return TokenUtils::kvToHash( $extArgs ); |
572 | } |
573 | |
574 | /** |
575 | * This method finds a requested arg by key name and return its current value. |
576 | * If a closure is passed in to update the current value, it is used to update the arg. |
577 | * |
578 | * @param KV[] &$extArgs Array of extension args |
579 | * @param string $key Argument key whose value needs an update |
580 | * @param ?Closure $updater $updater will get the existing string value |
581 | * for the arg and is expected to return an updated value. |
582 | * @return ?string |
583 | */ |
584 | public function findAndUpdateArg( |
585 | array &$extArgs, string $key, ?Closure $updater = null |
586 | ): ?string { |
587 | // FIXME: This code will get an overhaul when T250854 is resolved. |
588 | foreach ( $extArgs as $i => $kv ) { |
589 | $k = TokenUtils::tokensToString( $kv->k ); |
590 | if ( strtolower( trim( $k ) ) === strtolower( $key ) ) { |
591 | $val = $kv->v; |
592 | if ( $updater ) { |
593 | $kv = clone $kv; |
594 | $kv->v = $updater( TokenUtils::tokensToString( $val ) ); |
595 | $extArgs[$i] = $kv; |
596 | } |
597 | return $val; |
598 | } |
599 | } |
600 | |
601 | return null; |
602 | } |
603 | |
604 | /** |
605 | * Updates all arguments, similar to findAndUpdateArg, but applies the closure to all arguments |
606 | * @param KV[] &$extArgs Array of extension args |
607 | * @param ?Closure $updater $updater will get the existing string value |
608 | * for the key and value of the arg and is expected to return an updated value. |
609 | */ |
610 | public function updateAllArgs( array &$extArgs, ?Closure $updater = null ): void { |
611 | if ( !$updater ) { |
612 | return; |
613 | } |
614 | foreach ( $extArgs as $i => $kv ) { |
615 | $k = TokenUtils::tokensToString( $kv->k ); |
616 | $val = $kv->v; |
617 | $kv = clone $kv; |
618 | $kv->v = $updater( $k, TokenUtils::tokensToString( $val ) ); |
619 | $extArgs[$i] = $kv; |
620 | } |
621 | } |
622 | |
623 | /** |
624 | * Normalizes spaces from extension tag arguments, except for those keyed by values in $exceptions |
625 | * @param KV[] &$extArgs Array of extension args |
626 | * @param array[] $action array that is either empty or has one key, 'except' or 'only', which defines the |
627 | * attributes that should be respectively excluded or only included from the normalization |
628 | */ |
629 | public function normalizeWhiteSpaceInArgs( array &$extArgs, array $action = [] ) { |
630 | $except = $action['except'] ?? null; |
631 | $only = $action['only'] ?? null; |
632 | |
633 | if ( $except && $only ) { |
634 | $this->log( 'warn', 'normalizeWhiteSpaceInArgs should not have both except and only parameters' ); |
635 | return; |
636 | } |
637 | |
638 | if ( $except ) { |
639 | $closure = static function ( $key, $value ) use ( $except ) { |
640 | if ( in_array( strtolower( trim( $key ) ), $except, true ) ) { |
641 | return $value; |
642 | } else { |
643 | return trim( preg_replace( '/[\r\n\t ]+/', ' ', $value ) ); |
644 | } |
645 | }; |
646 | } elseif ( $only ) { |
647 | $closure = static function ( $key, $value ) use ( $only ) { |
648 | if ( in_array( strtolower( trim( $key ) ), $only, true ) ) { |
649 | return trim( preg_replace( '/[\r\n\t ]+/', ' ', $value ) ); |
650 | } else { |
651 | return $value; |
652 | } |
653 | }; |
654 | } else { |
655 | $closure = static function ( $key, $value ) { |
656 | return trim( preg_replace( '/[\r\n\t ]+/', ' ', $value ) ); |
657 | }; |
658 | } |
659 | |
660 | $this->updateAllArgs( $extArgs, $closure ); |
661 | } |
662 | |
663 | /** |
664 | * This method adds a new argument to the extension args array |
665 | * @param KV[] &$extArgs |
666 | * @param string $key |
667 | * @param string $value |
668 | */ |
669 | public function addNewArg( array &$extArgs, string $key, string $value ): void { |
670 | $extArgs[] = new KV( $key, $value ); |
671 | } |
672 | |
673 | // TODO: Provide support for extensions to register lints |
674 | // from their customized lint handlers. |
675 | |
676 | /** |
677 | * Forwards the logging request to the underlying logger |
678 | * @param string $prefix |
679 | * @param mixed ...$args |
680 | */ |
681 | public function log( string $prefix, ...$args ): void { |
682 | $this->env->log( $prefix, ...$args ); |
683 | } |
684 | |
685 | /** |
686 | * Extensions might be interested in examining (their) content embedded |
687 | * in attributes that don't otherwise show up in the DOM. |
688 | * |
689 | * Ex: inline media captions that aren't rendered, language variant markup, |
690 | * attributes that are transcluded. More scenarios might be added later. |
691 | * @deprecated |
692 | * Don't use this directly: use ::processAttributeEmbeddedDom(). |
693 | * This method may omit content which is embedded natively as |
694 | * DocumentFragments instead of as HTML strings. |
695 | * |
696 | * @param Element $elt The node whose data attributes need to be examined |
697 | * @param Closure $proc The processor that will process the embedded HTML |
698 | * Signature: (string) -> string |
699 | * This processor will be provided the HTML string as input |
700 | * and is expected to return a possibly modified string. |
701 | */ |
702 | public function processAttributeEmbeddedHTML( Element $elt, Closure $proc ): void { |
703 | // @phan-suppress-next-line PhanDeprecatedFunction |
704 | ContentUtils::processAttributeEmbeddedHTML( $this, $elt, $proc ); |
705 | } |
706 | |
707 | /** |
708 | * Extensions might be interested in examining (their) content embedded |
709 | * in attributes that don't otherwise show up in the DOM. |
710 | * |
711 | * Ex: inline media captions that aren't rendered, language variant markup, |
712 | * attributes that are transcluded. More scenarios might be added later. |
713 | * |
714 | * @param Element $elt The node whose data attributes need to be examined |
715 | * @param callable(DocumentFragment):bool $proc |
716 | * The processor that will process the embedded HTML. |
717 | * This processor will be provided a DocumentFragment |
718 | * and is expected to return true if that fragment was modified. |
719 | */ |
720 | public function processAttributeEmbeddedDom( Element $elt, callable $proc ): void { |
721 | ContentUtils::processAttributeEmbeddedDom( $this, $elt, $proc ); |
722 | } |
723 | |
724 | /** |
725 | * Copy $from->childNodes to $to and clone the data attributes of $from |
726 | * to $to. |
727 | * |
728 | * @param Element $from |
729 | * @param Element $to |
730 | */ |
731 | public static function migrateChildrenAndTransferWrapperDataAttribs( |
732 | Element $from, Element $to |
733 | ): void { |
734 | DOMUtils::migrateChildren( $from, $to ); |
735 | DOMDataUtils::setDataParsoid( |
736 | $to, clone DOMDataUtils::getDataParsoid( $from ) |
737 | ); |
738 | DOMDataUtils::setDataMw( |
739 | $to, clone DOMDataUtils::getDataMw( $from ) |
740 | ); |
741 | } |
742 | |
743 | /** |
744 | * Equivalent of 'preprocess' from Parser.php in core. |
745 | * - expands templates |
746 | * - replaces magic variables |
747 | * This does not run any hooks however since that would be unexpected. |
748 | * This also doesn't support replacing template args from a frame. |
749 | * |
750 | * @param string $wikitext |
751 | * @return array{error:bool,src?:string,fragment?:PFragment} |
752 | * - 'error' did we hit resource limits? |
753 | * - 'src' expanded wikitext OR error message to print |
754 | * FIXME: Maybe error message should be localizable |
755 | * - 'fragment' Optional fragment (wikitext plus strip state) |
756 | */ |
757 | public function preprocessWikitext( string $wikitext ) { |
758 | return Wikitext::preprocess( $this->env, $wikitext ); |
759 | } |
760 | |
761 | /** |
762 | * Parse input string into DOM. |
763 | * NOTE: This leaves the DOM in Parsoid-canonical state and is the preferred method |
764 | * to convert HTML to DOM that will be passed into Parsoid's processing code. |
765 | * |
766 | * @param string $html |
767 | * @param ?Document $doc XXX You probably don't want to be doing this |
768 | * @param ?array $options |
769 | * @return DocumentFragment |
770 | */ |
771 | public function htmlToDom( |
772 | string $html, ?Document $doc = null, ?array $options = [] |
773 | ): DocumentFragment { |
774 | return ContentUtils::createAndLoadDocumentFragment( |
775 | $doc ?? $this->getTopLevelDoc(), $html, $options |
776 | ); |
777 | } |
778 | |
779 | /** |
780 | * Serialize DOM element to string (inner/outer HTML is controlled by flag). |
781 | * If $releaseDom is set to true, the DOM will not be "prepared and loaded" |
782 | * and is not safe to use after this call. This is primarily a performance optimization. |
783 | * |
784 | * @param Node $node |
785 | * @param bool $innerHTML if true, inner HTML of the element will be returned |
786 | * This flag defaults to false |
787 | * @param bool $releaseDom if true, the DOM will not be in canonical form after this call |
788 | * This flag defaults to false |
789 | * @return string |
790 | */ |
791 | public function domToHtml( |
792 | Node $node, bool $innerHTML = false, bool $releaseDom = false |
793 | ): string { |
794 | // FIXME: This is going to drop any diff markers but since |
795 | // the dom differ doesn't traverse into extension content (right now), |
796 | // none should exist anyways. |
797 | // FIXME: This roundtrip discards data-parsoid.tmp data from $node. |
798 | // Maybe it meant to set keepTmp, but that flag is currently broken. |
799 | $html = ContentUtils::ppToXML( $node, [ 'innerXML' => $innerHTML, 'fragment' => true ] ); |
800 | if ( !$releaseDom ) { |
801 | DOMDataUtils::visitAndLoadDataAttribs( $node ); |
802 | } |
803 | return $html; |
804 | } |
805 | |
806 | /** |
807 | * Bit flags describing escaping / serializing context in html -> wt mode |
808 | */ |
809 | public const IN_SOL = 1; |
810 | public const IN_MEDIA = 2; |
811 | public const IN_LINK = 4; |
812 | public const IN_IMG_CAPTION = 8; |
813 | public const IN_OPTION = 16; |
814 | |
815 | /** |
816 | * FIXME: This is a bit broken - shouldn't be needed ideally |
817 | * @param string $flag |
818 | */ |
819 | public function setHtml2wtStateFlag( string $flag ) { |
820 | $this->serializerState->{$flag} = true; |
821 | } |
822 | |
823 | /** |
824 | * Emit the opening tag (including attributes) for the extension |
825 | * represented by this node. |
826 | * |
827 | * @param Element $node |
828 | * @return string |
829 | */ |
830 | public function extStartTagToWikitext( Element $node ): string { |
831 | $state = $this->serializerState; |
832 | return $state->serializer->serializeExtensionStartTag( $node, $state ); |
833 | } |
834 | |
835 | /** |
836 | * Convert the input DOM to wikitext. |
837 | * |
838 | * @param array $opts |
839 | * - extName: (string) Name of the extension whose body we are serializing |
840 | * - inPHPBlock: (bool) FIXME: This needs to be removed |
841 | * @param Element $node DOM to serialize |
842 | * @param bool $releaseDom If $releaseDom is set to true, the DOM will be left in |
843 | * non-canonical form and is not safe to use after this call. This is primarily a |
844 | * performance optimization. This flag defaults to false. |
845 | * @return mixed |
846 | */ |
847 | public function domToWikitext( array $opts, Element $node, bool $releaseDom = false ) { |
848 | // FIXME: WTS expects the input DOM to be a <body> element! |
849 | // Till that is fixed, we have to go through this round-trip! |
850 | // TODO: Move $node children to a fragment and call `$serializer->domToWikitext` |
851 | return $this->htmlToWikitext( $opts, $this->domToHtml( $node, $releaseDom ) ); |
852 | } |
853 | |
854 | /** |
855 | * Convert the HTML body of an extension to wikitext |
856 | * |
857 | * @param array $opts |
858 | * - extName: (string) Name of the extension whose body we are serializing |
859 | * - inPHPBlock: (bool) FIXME: This needs to be removed |
860 | * @param string $html HTML for the extension's body |
861 | * @return string |
862 | */ |
863 | public function htmlToWikitext( array $opts, string $html ): string { |
864 | // Type cast so phan has more information to ensure type safety |
865 | $state = $this->serializerState; |
866 | $opts['env'] = $this->env; |
867 | return $state->serializer->htmlToWikitext( $opts, $html ); |
868 | } |
869 | |
870 | /** |
871 | * Get the original source for an element. |
872 | * |
873 | * The callable, $checkIfOrigSrcReusable, is used to determine if the $elt |
874 | * is unedited and therefore valid to reuse source. This is assumed to be |
875 | * pretty specific to the callsite so no default is provided. |
876 | * |
877 | * @param Element $elt |
878 | * @param bool $inner |
879 | * @param callable $checkIfOrigSrcReusable |
880 | * @return string|null |
881 | */ |
882 | public function getOrigSrc( |
883 | Element $elt, bool $inner, callable $checkIfOrigSrcReusable |
884 | ): ?string { |
885 | $state = $this->serializerState; |
886 | if ( !$state->selserMode || $state->inInsertedContent ) { |
887 | return null; |
888 | } |
889 | $dsr = DOMDataUtils::getDataParsoid( $elt )->dsr ?? null; |
890 | if ( !Utils::isValidDSR( $dsr, $inner ) ) { |
891 | return null; |
892 | } |
893 | if ( $checkIfOrigSrcReusable( $elt ) ) { |
894 | return $state->getOrigSrc( |
895 | $inner ? $dsr->innerRange() : $dsr |
896 | ); |
897 | } else { |
898 | return null; |
899 | } |
900 | } |
901 | |
902 | /** |
903 | * @param Element $elt |
904 | * @param int $context OR-ed bit flags specifying escaping / serialization context |
905 | * @return string |
906 | */ |
907 | public function domChildrenToWikitext( Element $elt, int $context ): string { |
908 | $state = $this->serializerState; |
909 | if ( $context & self::IN_IMG_CAPTION ) { |
910 | if ( $context & self::IN_OPTION ) { |
911 | $escapeHandler = 'mediaOptionHandler'; // Escapes "|" as well |
912 | } else { |
913 | $escapeHandler = 'wikilinkHandler'; // image captions show up in wikilink syntax |
914 | } |
915 | $out = $state->serializeCaptionChildrenToString( $elt, |
916 | [ $state->serializer->wteHandlers, $escapeHandler ] ); |
917 | } else { |
918 | throw new \RuntimeException( 'Not yet supported!' ); |
919 | } |
920 | return $out; |
921 | } |
922 | |
923 | /** |
924 | * Escape any wikitext like constructs in a string so that when the output |
925 | * is parsed, it renders as a string. The escaping is sensitive to the context |
926 | * in which the string is embedded. For example, a "*" is not safe at the start |
927 | * of a line (since it will parse as a list item), but is safe if it is not in |
928 | * a start of line context. Similarly the "|" character is safe outside tables, |
929 | * links, and transclusions. |
930 | * |
931 | * @param string $str |
932 | * @param Node $node |
933 | * @param int $context OR-ed bit flags specifying escaping / serialization context |
934 | * @return string |
935 | */ |
936 | public function escapeWikitext( string $str, Node $node, int $context ): string { |
937 | if ( $context & ( self::IN_MEDIA | self::IN_LINK ) ) { |
938 | $state = $this->serializerState; |
939 | return $state->serializer->wteHandlers->escapeLinkContent( |
940 | $state, $str, |
941 | (bool)( $context & self::IN_SOL ), |
942 | $node, |
943 | (bool)( $context & self::IN_MEDIA ) |
944 | ); |
945 | } else { |
946 | throw new \RuntimeException( 'Not yet supported!' ); |
947 | } |
948 | } |
949 | |
950 | /** |
951 | * EXTAPI-FIXME: We have to figure out what it means to run a DOM pass |
952 | * (and what processors and what handlers apply) on content models that are |
953 | * not wikitext. For now, we are only storing data attribs back to the DOM |
954 | * and adding metadata to the page. |
955 | * |
956 | * @param Document $doc |
957 | */ |
958 | public function postProcessDOM( Document $doc ): void { |
959 | // Ugh! But, this whole method needs to go away anyway |
960 | ( new AddMetaData( null ) )->run( $this->env, DOMCompat::getBody( $doc ) ); |
961 | } |
962 | |
963 | /** |
964 | * Produce the HTML rendering of a title string and media options as the |
965 | * wikitext parser would for a wikilink in the file namespace |
966 | * |
967 | * @param string $titleStr Image title string |
968 | * @param array $imageOpts Array of a mix of strings or arrays, |
969 | * the latter of which can signify that the value came from source. |
970 | * Where, |
971 | * [0] is the fully-constructed image option |
972 | * [1] is the full wikitext source offset for it |
973 | * @param ?string &$error Error string is set when the return is null. |
974 | * @param ?bool $forceBlock Forces the media to be rendered in a figure as |
975 | * opposed to a span. |
976 | * @param ?bool $suppressMediaFormats If any media format is present in |
977 | * $imageOpts, it won't be applied and will result in a linting error. |
978 | * @return ?Element |
979 | */ |
980 | public function renderMedia( |
981 | string $titleStr, array $imageOpts, ?string &$error = null, |
982 | ?bool $forceBlock = false, ?bool $suppressMediaFormats = false |
983 | ): ?Element { |
984 | $extTagName = $this->extTag->getName(); |
985 | $extTagOpts = [ 'suppressMediaFormats' => $suppressMediaFormats ]; |
986 | |
987 | $fileNs = $this->getSiteConfig()->canonicalNamespaceId( 'file' ); |
988 | |
989 | $title = $this->makeTitle( $titleStr, 0 ); |
990 | if ( $title === null || $title->getNamespace() !== $fileNs ) { |
991 | $error = "{$extTagName}_no_image"; |
992 | return null; |
993 | } |
994 | |
995 | $pieces = [ '[[' ]; |
996 | // Since the above two chars aren't from source, the resulting figure |
997 | // won't have any dsr info, so we can omit an offset for the title as |
998 | // well. In any case, $titleStr may not necessarily be from source, |
999 | // see the special case in the gallery extension. |
1000 | $pieces[] = $titleStr; |
1001 | $pieces = array_merge( $pieces, $imageOpts ); |
1002 | |
1003 | if ( $forceBlock ) { |
1004 | // We add "none" here so that this renders in the block form |
1005 | // (ie. figure). It's a valid media option, so shouldn't turn into |
1006 | // a caption. And since it's first wins, it shouldn't interfere |
1007 | // with another horizontal alignment defined in $imageOpts. |
1008 | // We just have to remember to strip the class below. |
1009 | // NOTE: This will have to be adjusted with T305628 |
1010 | $pieces[] = '|none'; |
1011 | } |
1012 | |
1013 | $pieces[] = ']]'; |
1014 | |
1015 | $shiftOffset = static function ( int $offset ) use ( $pieces ): ?int { |
1016 | foreach ( $pieces as $p ) { |
1017 | if ( is_string( $p ) ) { |
1018 | $offset -= strlen( $p ); |
1019 | if ( $offset <= 0 ) { |
1020 | return null; |
1021 | } |
1022 | } else { |
1023 | if ( $offset <= strlen( $p[0] ) && isset( $p[1] ) ) { |
1024 | return $p[1] + $offset; |
1025 | } |
1026 | $offset -= strlen( $p[0] ); |
1027 | if ( $offset <= 0 ) { |
1028 | return null; |
1029 | } |
1030 | } |
1031 | } |
1032 | return null; |
1033 | }; |
1034 | |
1035 | $imageWt = ''; |
1036 | foreach ( $pieces as $p ) { |
1037 | $imageWt .= ( is_string( $p ) ? $p : $p[0] ); |
1038 | } |
1039 | |
1040 | $domFragment = $this->wikitextToDOM( |
1041 | $imageWt, |
1042 | [ |
1043 | 'parseOpts' => [ |
1044 | 'extTag' => $extTagName, |
1045 | 'extTagOpts' => $extTagOpts, |
1046 | 'context' => 'inline', |
1047 | ], |
1048 | // Create new frame, because $pieces doesn't literally appear |
1049 | // on the page, it has been hand-crafted here |
1050 | 'processInNewFrame' => true, |
1051 | // Shift the DSRs in the DOM by startOffset, and strip DSRs |
1052 | // for bits which aren't the caption or file, since they |
1053 | // don't refer to actual source wikitext |
1054 | 'shiftDSRFn' => static function ( DomSourceRange $dsr ) use ( $shiftOffset ) { |
1055 | $start = $dsr->start === null ? null : $shiftOffset( $dsr->start ); |
1056 | $end = $dsr->end === null ? null : $shiftOffset( $dsr->end ); |
1057 | // If either offset is newly-invalid, remove entire DSR |
1058 | if ( |
1059 | ( $dsr->start !== null && $start === null ) || |
1060 | ( $dsr->end !== null && $end === null ) |
1061 | ) { |
1062 | return null; |
1063 | } |
1064 | return new DomSourceRange( |
1065 | $start, $end, $dsr->openWidth, $dsr->closeWidth |
1066 | ); |
1067 | }, |
1068 | ], |
1069 | true // sol |
1070 | ); |
1071 | |
1072 | $thumb = $domFragment->firstChild; |
1073 | $validWrappers = [ 'figure' ]; |
1074 | // Downstream code expects a figcaption if we're forcing a block so we |
1075 | // validate that we did indeed parse a figure. It might not have |
1076 | // happened because $imageOpts has an unbalanced `]]` which closes |
1077 | // the wikilink syntax before we get in our `|none`. |
1078 | if ( !$forceBlock ) { |
1079 | $validWrappers[] = 'span'; |
1080 | } |
1081 | if ( !in_array( DOMCompat::nodeName( $thumb ), $validWrappers, true ) ) { |
1082 | $error = "{$extTagName}_invalid_image"; |
1083 | return null; |
1084 | } |
1085 | DOMUtils::assertElt( $thumb ); |
1086 | |
1087 | // Detach the $thumb since the $domFragment is going out of scope |
1088 | // See https://bugs.php.net/bug.php?id=39593 |
1089 | DOMCompat::remove( $thumb ); |
1090 | |
1091 | if ( $forceBlock ) { |
1092 | $dp = DOMDataUtils::getDataParsoid( $thumb ); |
1093 | array_pop( $dp->optList ); |
1094 | $explicitNone = false; |
1095 | foreach ( $dp->optList as $opt ) { |
1096 | if ( $opt['ck'] === 'none' ) { |
1097 | $explicitNone = true; |
1098 | } |
1099 | } |
1100 | if ( !$explicitNone ) { |
1101 | // FIXME: Should we worry about someone adding this with the |
1102 | // "class=" option? |
1103 | DOMCompat::getClassList( $thumb )->remove( 'mw-halign-none' ); |
1104 | } |
1105 | } |
1106 | |
1107 | return $thumb; |
1108 | } |
1109 | |
1110 | /** |
1111 | * Serialize a MediaStructure to a title and media options string. |
1112 | * The converse to ::renderMedia. |
1113 | * |
1114 | * @param MediaStructure $ms |
1115 | * @return array Where, |
1116 | * [0] is the media title string |
1117 | * [1] is the string of media options |
1118 | */ |
1119 | public function serializeMedia( MediaStructure $ms ): array { |
1120 | $ct = LinkHandlerUtils::figureToConstrainedText( $this->serializerState, $ms ); |
1121 | if ( $ct instanceof WikiLinkText ) { |
1122 | // Remove the opening and closing square brackets |
1123 | $text = substr( $ct->text, 2, -2 ); |
1124 | return array_pad( explode( '|', $text, 2 ), 2, '' ); |
1125 | } else { |
1126 | // Note that $ct could be an AutoURLLinkText, not just null |
1127 | return [ '', '' ]; |
1128 | } |
1129 | } |
1130 | |
1131 | /** |
1132 | * @param array $modules |
1133 | * @deprecated Use ::getMetadata()->appendOutputStrings( MODULE, ...) instead. |
1134 | */ |
1135 | public function addModules( array $modules ) { |
1136 | $this->getMetadata()->appendOutputStrings( CMCSS::MODULE, $modules ); |
1137 | } |
1138 | |
1139 | /** |
1140 | * @param array $modulestyles |
1141 | * @deprecated Use ::getMetadata()->appendOutputStrings(MODULE_STYLE, ...) instead. |
1142 | */ |
1143 | public function addModuleStyles( array $modulestyles ) { |
1144 | $this->getMetadata()->appendOutputStrings( CMCSS::MODULE_STYLE, $modulestyles ); |
1145 | } |
1146 | |
1147 | /** |
1148 | * Get an array of attributes to apply to an anchor linking to $url |
1149 | */ |
1150 | public function getExternalLinkAttribs( string $url ): array { |
1151 | return $this->env->getExternalLinkAttribs( $url ); |
1152 | } |
1153 | |
1154 | /** |
1155 | * Add a tracking category to the current page. |
1156 | * @param string $key Message key (not localized) |
1157 | */ |
1158 | public function addTrackingCategory( string $key ): void { |
1159 | $this->env->getDataAccess()->addTrackingCategory( |
1160 | $this->env->getPageConfig(), |
1161 | $this->env->getMetadata(), |
1162 | $key |
1163 | ); |
1164 | } |
1165 | } |