Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
81.82% covered (warning)
81.82%
243 / 297
44.44% covered (danger)
44.44%
12 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 1
PageContentHandler
81.82% covered (warning)
81.82%
243 / 297
44.44% covered (danger)
44.44%
12 / 27
105.03
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getContentClass
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 canBeUsedOn
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 serializeContent
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 serializeContentInJson
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
3.00
 serializeContentInWikitext
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
3.00
 unserializeContent
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
5.93
 preSaveTransform
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 supportsPreloadContent
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 preloadTransform
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 unserializeContentInJson
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
3
 unserializeContentInWikitext
97.62% covered (success)
97.62%
41 / 42
0.00% covered (danger)
0.00%
0 / 1
8
 cleanDeprecatedWrappers
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 getActionOverrides
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getSlotDiffRendererWithOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 makeEmptyContent
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 merge3
96.43% covered (success)
96.43%
27 / 28
0.00% covered (danger)
0.00%
0 / 1
8
 getAutosummary
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 makeRedirectContent
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 supportsRedirects
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isParserCacheSupported
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSecondaryDataUpdates
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getDeletionUpdates
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getIndexTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildIndexQualityStatsUpdate
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 getPageLanguage
36.36% covered (danger)
36.36%
4 / 11
0.00% covered (danger)
0.00%
0 / 1
8.12
 fillParserOutput
84.91% covered (warning)
84.91%
45 / 53
0.00% covered (danger)
0.00%
0 / 1
6.12
1<?php
2
3namespace ProofreadPage\Page;
4
5use MediaWiki\Category\TrackingCategories;
6use MediaWiki\Content\Content;
7use MediaWiki\Content\Renderer\ContentParseParams;
8use MediaWiki\Content\TextContentHandler;
9use MediaWiki\Content\Transform\PreloadTransformParams;
10use MediaWiki\Content\Transform\PreSaveTransformParams;
11use MediaWiki\Content\WikitextContent;
12use MediaWiki\Content\WikitextContentHandler;
13use MediaWiki\Context\IContextSource;
14use MediaWiki\Html\Html;
15use MediaWiki\MediaWikiServices;
16use MediaWiki\Parser\ParserOutput;
17use MediaWiki\Revision\SlotRenderingProvider;
18use MediaWiki\Title\Title;
19use MWContentSerializationException;
20use ProofreadPage\Context;
21use ProofreadPage\Index\IndexTemplateStyles;
22use ProofreadPage\Index\UpdateIndexQualityStats;
23use ProofreadPage\MultiFormatSerializerUtils;
24use UnexpectedValueException;
25
26/**
27 * @license GPL-2.0-or-later
28 *
29 * Content handler for a Page: pages
30 */
31class PageContentHandler extends TextContentHandler {
32
33    use MultiFormatSerializerUtils;
34
35    protected WikitextContentHandler $wikitextContentHandler;
36    private TrackingCategories $trackingCategories;
37
38    /**
39     * @param string $modelId
40     */
41    public function __construct( $modelId = CONTENT_MODEL_PROOFREAD_PAGE ) {
42        parent::__construct( $modelId, [ CONTENT_FORMAT_WIKITEXT, CONTENT_FORMAT_JSON ] );
43        $services = MediaWikiServices::getInstance();
44        $this->wikitextContentHandler = $services->getContentHandlerFactory()
45            ->getContentHandler( CONTENT_MODEL_WIKITEXT );
46        $this->trackingCategories = $services->getTrackingCategories();
47    }
48
49    /**
50     * @return string
51     */
52    protected function getContentClass() {
53        return PageContent::class;
54    }
55
56    /**
57     * @inheritDoc
58     */
59    public function canBeUsedOn( Title $title ) {
60        return parent::canBeUsedOn( $title ) &&
61            $title->getNamespace() === Context::getDefaultContext()->getPageNamespaceId();
62    }
63
64    /**
65     * @param PageContent $content
66     * @param string|null $format
67     * @return string
68     * @suppress PhanParamSignatureMismatch Intentional mismatching Content
69     */
70    public function serializeContent( Content $content, $format = null ) {
71        $this->checkFormat( $format );
72
73        switch ( $format ) {
74            case CONTENT_FORMAT_JSON:
75                return $this->serializeContentInJson( $content );
76            default:
77                return $this->serializeContentInWikitext( $content );
78        }
79    }
80
81    /**
82     * @param PageContent $content
83     * @return string
84     */
85    private function serializeContentInJson( PageContent $content ) {
86        $level = $content->getLevel();
87        $user = $level->getUser();
88
89        if ( $user ) {
90            if ( $user->isHidden() ) {
91                $userName = wfMessage( 'rev-deleted-user' )->inContentLanguage()->text();
92            } else {
93                $userName = $user->getName();
94            }
95        } else {
96            $userName = null;
97        }
98
99        return json_encode( [
100            'header' => $content->getHeader()->serialize(),
101            'body' => $content->getBody()->serialize(),
102            'footer' => $content->getFooter()->serialize(),
103            'level' => [
104                'level' => $level->getLevel(),
105                'user' => $userName
106            ]
107        ] );
108    }
109
110    /**
111     * @param PageContent $content
112     * @return string
113     */
114    private function serializeContentInWikitext( PageContent $content ) {
115        $level = $content->getLevel();
116        $user = $level->getUser();
117
118        if ( $user ) {
119            if ( $user->isHidden() ) {
120                $userName = wfMessage( 'rev-deleted-user' )->inContentLanguage()->text();
121            } else {
122                $userName = $user->getName();
123            }
124        } else {
125            $userName = null;
126        }
127
128        $text =
129            '<noinclude>' .
130                '<pagequality level="' . $level->getLevel() . '" user="' . $userName . '" />' .
131                $content->getHeader()->serialize() .
132            '</noinclude>' .
133            $content->getBody()->serialize() .
134            '<noinclude>' .
135                $content->getFooter()->serialize() .
136            '</noinclude>';
137
138        return $text;
139    }
140
141    /**
142     * @param string $text
143     * @param string|null $format
144     * @return PageContent
145     */
146    public function unserializeContent( $text, $format = null ) {
147        if ( $format === null ) {
148            $format = self::guessDataFormat( $text, true );
149        }
150
151        switch ( $format ) {
152            case CONTENT_FORMAT_JSON:
153                return $this->unserializeContentInJson( $text );
154            case CONTENT_FORMAT_WIKITEXT:
155                return $this->unserializeContentInWikitext( $text );
156            default:
157                throw new UnexpectedValueException(
158                    "Format ' . $format . ' is not supported for content model " . $this->getModelID()
159                );
160        }
161    }
162
163    /**
164     * @inheritDoc
165     */
166    public function preSaveTransform(
167        Content $content,
168        PreSaveTransformParams $pstParams
169    ): Content {
170        '@phan-var PageContent $content';
171
172        $contentHandlerFactory = MediaWikiServices::getInstance()->getContentHandlerFactory();
173        $contentClass = $this->getContentClass();
174        $header = $content->getHeader();
175        $body = $content->getBody();
176        $footer = $content->getFooter();
177
178        return new $contentClass(
179            $contentHandlerFactory->getContentHandler( $header->getModel() )
180                ->preSaveTransform( $header, $pstParams ),
181            $contentHandlerFactory->getContentHandler( $body->getModel() )
182                ->preSaveTransform( $body, $pstParams ),
183                $contentHandlerFactory->getContentHandler( $footer->getModel() )
184                ->preSaveTransform( $footer, $pstParams ),
185            $content->getLevel()
186        );
187    }
188
189    /**
190     * @inheritDoc
191     */
192    public function supportsPreloadContent(): bool {
193        return true;
194    }
195
196    /**
197     * @inheritDoc
198     */
199    public function preloadTransform(
200        Content $content,
201        PreloadTransformParams $pltparams
202    ): Content {
203        '@phan-var PageContent $content';
204        $contentHandlerFactory = MediaWikiServices::getInstance()->getContentHandlerFactory();
205        $contentClass = $this->getContentClass();
206        $header = $content->getHeader();
207        $body = $content->getBody();
208        $footer = $content->getFooter();
209
210        return new $contentClass(
211            $contentHandlerFactory->getContentHandler( $header->getModel() )
212                ->preloadTransform( $header, $pltparams ),
213            $contentHandlerFactory->getContentHandler( $body->getModel() )
214                ->preloadTransform( $body, $pltparams ),
215            $contentHandlerFactory->getContentHandler( $footer->getModel() )
216                ->preloadTransform( $footer, $pltparams ),
217            $content->getLevel()
218        );
219    }
220
221    /**
222     * @param string $text
223     * @return PageContent
224     * @throws MWContentSerializationException
225     * @suppress PhanTypeMismatchArgument
226     */
227    private function unserializeContentInJson( $text ) {
228        $array = json_decode( $text, true );
229
230        if ( !is_array( $array ) ) {
231            throw new MWContentSerializationException(
232                'The serialization is an invalid JSON array.'
233            );
234        }
235        self::assertArrayKeyExistsInSerialization( 'header', $array );
236        self::assertArrayKeyExistsInSerialization( 'body', $array );
237        self::assertArrayKeyExistsInSerialization( 'footer', $array );
238        self::assertArrayKeyExistsInSerialization( 'level', $array );
239        self::assertArrayKeyExistsInSerialization( 'level', $array['level'] );
240
241        $user = array_key_exists( 'user', $array['level'] )
242            ? PageLevel::getUserFromUserName( $array['level']['user'] )
243            : null;
244
245        return new PageContent(
246            $this->wikitextContentHandler->unserializeContent( $array['header'] ),
247            $this->wikitextContentHandler->unserializeContent( $array['body'] ),
248            $this->wikitextContentHandler->unserializeContent( $array['footer'] ),
249            new PageLevel( $array['level']['level'], $user )
250        );
251    }
252
253    /**
254     * @param string $text
255     * @return PageContent
256     * @suppress PhanTypeMismatchArgument
257     */
258    private function unserializeContentInWikitext( $text ) {
259        $header = '';
260        $footer = '';
261        $proofreader = '';
262        $level = 1;
263
264        $cleanHeader = false;
265        $cleanBody = false;
266        $cleanFooter = false;
267
268        if ( preg_match( '/^<noinclude>(.*?)\n*<\/noinclude>(.*)<noinclude>(.*?)<\/noinclude>$/s',
269            $text, $m )
270        ) {
271            $header = $m[1];
272            $body = $m[2];
273            $footer = $m[3];
274            $cleanFooter = true;
275        } elseif ( preg_match( '/^<noinclude>(.*?)\n*<\/noinclude>(.*?)$/s', $text, $m ) ) {
276            $header = $m[1];
277            $body = $m[2];
278            $cleanBody = true;
279        } else {
280            $body = $text;
281        }
282
283        if ( preg_match(
284            '/^<pagequality level="([0-4])" user="(.*?)" *(?:\/>|> *<\/pagequality>)(.*?)$/s',
285            $header, $m )
286        ) {
287            $level = intval( $m[1] );
288            $proofreader = $m[2];
289            $header = $m[3];
290            $cleanHeader = true;
291        } elseif (
292            preg_match( '/^\{\{PageQuality\|([0-4])(?:\|(.*?))?\}\}(.*)/is', $header, $m )
293        ) {
294            $level = intval( $m[1] );
295            $proofreader = $m[2];
296            $header = $m[3];
297            $cleanHeader = true;
298        }
299
300        if ( $cleanHeader ) {
301            if ( $cleanFooter ) {
302                [ $header, $footer ] = $this->cleanDeprecatedWrappers( $header, $footer );
303            } elseif ( $cleanBody ) {
304                [ $header, $body ] = $this->cleanDeprecatedWrappers( $header, $body );
305            } else {
306                // notice that second parameter is unused
307                [ $header, ] = $this->cleanDeprecatedWrappers( $header, '' );
308            }
309        }
310
311        return new PageContent(
312            $this->wikitextContentHandler->unserializeContent( $header ),
313            $this->wikitextContentHandler->unserializeContent( $body ),
314            $this->wikitextContentHandler->unserializeContent( $footer ),
315            new PageLevel( $level, PageLevel::getUserFromUserName( $proofreader ) )
316        );
317    }
318
319    /**
320     * @param string $header
321     * @param string $footer
322     * @return string[]
323     */
324    protected function cleanDeprecatedWrappers( $header, $footer ) {
325        $cleanedHeader = false;
326        if ( preg_match( '/^(.*?)<div class="pagetext">(.*?)$/s', $header, $mt ) ) {
327            $header = $mt[2];
328            $cleanedHeader = true;
329        } elseif ( preg_match( '/^(.*?)<div>(.*?)$/s', $header, $mt ) ) {
330            $header = $mt[2];
331            $cleanedHeader = true;
332        }
333
334        if ( $cleanedHeader && preg_match( '/^(.*?)<\/div>$/s', $footer, $mt ) ) {
335            $footer = $mt[1];
336        }
337
338        return [ $header, $footer ];
339    }
340
341    /**
342     * @inheritDoc
343     */
344    public function getActionOverrides() {
345        return [
346            'edit' => PageEditAction::class,
347            'submit' => PageSubmitAction::class,
348            'view' => PageViewAction::class
349        ];
350    }
351
352    /**
353     * @inheritDoc
354     */
355    protected function getSlotDiffRendererWithOptions( IContextSource $context, $options = [] ) {
356        return new PageSlotDiffRenderer( $context );
357    }
358
359    /**
360     * @inheritDoc
361     * @suppress PhanTypeMismatchArgument
362     */
363    public function makeEmptyContent() {
364        return new PageContent(
365            $this->wikitextContentHandler->makeEmptyContent(),
366            $this->wikitextContentHandler->makeEmptyContent(),
367            $this->wikitextContentHandler->makeEmptyContent(),
368            new PageLevel()
369        );
370    }
371
372    /**
373     * @param PageContent $oldContent
374     * @param PageContent $myContent
375     * @param PageContent $yourContent
376     * @return PageContent|false
377     * @suppress PhanParamSignatureMismatch Intentional mismatching Content
378     */
379    public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) {
380        $this->checkModelID( $oldContent->getModel() );
381        $this->checkModelID( $myContent->getModel() );
382        $this->checkModelID( $yourContent->getModel() );
383
384        if ( $myContent->getLevel()->getLevel() !== $yourContent->getLevel()->getLevel() ) {
385            return false;
386        }
387
388        $wikitextHandler = MediaWikiServices::getInstance()
389            ->getContentHandlerFactory()
390            ->getContentHandler( CONTENT_MODEL_WIKITEXT );
391        $mergedHeader = $myContent->getHeader()->equals( $yourContent->getHeader() )
392            ? $myContent->getHeader()
393            : $wikitextHandler->merge3(
394                $oldContent->getHeader(), $myContent->getHeader(), $yourContent->getHeader()
395            );
396        $mergedBody = $myContent->getBody()->equals( $yourContent->getBody() )
397            ? $myContent->getBody()
398            : $wikitextHandler->merge3(
399                $oldContent->getBody(), $myContent->getBody(), $yourContent->getBody()
400            );
401        $mergedFooter = $myContent->getFooter()->equals( $yourContent->getFooter() )
402            ? $yourContent->getFooter()
403            : $wikitextHandler->merge3(
404                $oldContent->getFooter(), $myContent->getFooter(), $yourContent->getFooter()
405            );
406
407        if ( !$mergedHeader || !$mergedBody || !$mergedFooter ) {
408            return false;
409        }
410
411        return new PageContent(
412            $mergedHeader, $mergedBody, $mergedFooter, $yourContent->getLevel()
413        );
414    }
415
416    /**
417     * @inheritDoc
418     */
419    public function getAutosummary(
420        ?Content $oldContent = null, ?Content $newContent = null, $flags = 0
421    ) {
422        $summary = parent::getAutosummary( $oldContent, $newContent, $flags );
423
424        if ( $newContent instanceof PageContent &&
425            ( $oldContent === null || ( $oldContent instanceof PageContent &&
426            !$newContent->getLevel()->equals( $oldContent->getLevel() ) ) )
427        ) {
428            $summary = trim( '/* ' . $newContent->getLevel()->getLevelCategoryName() .
429                ' */ ' . $summary );
430        }
431
432        return $summary;
433    }
434
435    /**
436     * @inheritDoc
437     * @todo is it the right content for redirects?
438     * @suppress PhanTypeMismatchArgument
439     */
440    public function makeRedirectContent( Title $destination, $text = '' ) {
441        return new PageContent(
442            $this->wikitextContentHandler->makeEmptyContent(),
443            $this->wikitextContentHandler->makeRedirectContent( $destination, $text ),
444            $this->wikitextContentHandler->makeEmptyContent(),
445            new PageLevel()
446        );
447    }
448
449    /**
450     * @inheritDoc
451     */
452    public function supportsRedirects() {
453        return true;
454    }
455
456    /**
457     * @inheritDoc
458     */
459    public function isParserCacheSupported() {
460        return true;
461    }
462
463    /**
464     * @inheritDoc
465     */
466    public function getSecondaryDataUpdates(
467        Title $title,
468        Content $content,
469        $role,
470        SlotRenderingProvider $slotOutput
471    ) {
472        $updates = parent::getSecondaryDataUpdates( $title, $content, $role, $slotOutput );
473
474        $indexTitle = $this->getIndexTitle( $title );
475        if ( $indexTitle !== null ) {
476            $indexTitle->invalidateCache();
477            $updates[] = $this->buildIndexQualityStatsUpdate( $title, $indexTitle, $content );
478        }
479
480        return $updates;
481    }
482
483    /**
484     * @inheritDoc
485     */
486    public function getDeletionUpdates( Title $title, $role ) {
487        $updates = parent::getDeletionUpdates( $title, $role );
488
489        $indexTitle = $this->getIndexTitle( $title );
490        if ( $indexTitle !== null ) {
491            $updates[] = $this->buildIndexQualityStatsUpdate( $title, $indexTitle );
492        }
493
494        return $updates;
495    }
496
497    /**
498     * @param Title $pageTitle
499     * @return Title|null
500     */
501    private function getIndexTitle( Title $pageTitle ): ?Title {
502        return Context::getDefaultContext()->getIndexForPageLookup()->getIndexForPageTitle( $pageTitle );
503    }
504
505    /**
506     * @param Title $pageTitle
507     * @param Title $indexTitle
508     * @param Content|null $pageContent
509     * @return UpdateIndexQualityStats
510     */
511    private function buildIndexQualityStatsUpdate(
512        Title $pageTitle,
513        Title $indexTitle,
514        ?Content $pageContent = null
515    ): UpdateIndexQualityStats {
516        $context = Context::getDefaultContext();
517        $newLevel = ( $pageContent instanceof PageContent )
518            ? $pageContent->getLevel()->getLevel()
519            : null;
520        return new UpdateIndexQualityStats(
521            MediaWikiServices::getInstance()->getDBLoadBalancer(),
522            $context->getPageQualityLevelLookup(),
523            $context->getPaginationFactory()->getPaginationForIndexTitle( $indexTitle ),
524            $indexTitle,
525            $pageTitle,
526            $newLevel
527        );
528    }
529
530    /**
531     * @inheritDoc
532     */
533    public function getPageLanguage( Title $title, ?Content $content = null ) {
534        $context = Context::getDefaultContext();
535        $indexTitle = $context->getIndexForPageLookup()->getIndexForPageTitle( $title );
536        if ( $indexTitle ) {
537            $indexContent = $context->getIndexContentLookup()->getIndexContentForTitle( $indexTitle );
538            $indexLang = $context->getCustomIndexFieldsParser()->getContentLanguage( $indexContent );
539            if ( $indexLang ) {
540                // if unrecognized, uses $wgContentLanguage
541                $services = MediaWikiServices::getInstance();
542                return $services->getLanguageNameUtils()->isKnownLanguageTag( $indexLang ) ?
543                    $services->getLanguageFactory()->getLanguage( $indexLang ) :
544                    $services->getContentLanguage();
545            }
546        }
547        return parent::getPageLanguage( $title, $content );
548    }
549
550    /**
551     * @inheritDoc
552     */
553    protected function fillParserOutput(
554        Content $content,
555        ContentParseParams $cpoParams,
556        ParserOutput &$parserOutput
557    ) {
558        '@phan-var PageContent $content';
559        $title = Title::castFromPageReference( $cpoParams->getPage() );
560        '@phan-var Title $title';
561        if ( $content->isRedirect() ) {
562            $parserOutput = $this->wikitextContentHandler->getParserOutput( $content->getBody(), $cpoParams );
563            return;
564        }
565
566        $context = Context::getDefaultContext();
567        $indexTitle = $context->getIndexForPageLookup()->getIndexForPageTitle( $title );
568
569        // create content
570        $wikitext = trim(
571            $content->getHeader()->getText()
572            . "\n\n"
573            . $content->getBody()->getText()
574            . $content->getFooter()->getText()
575        );
576
577        $indexTs = null;
578        if ( $indexTitle !== null ) {
579            $indexTs = new IndexTemplateStyles( $indexTitle );
580            // newline so that following wikitext that needs to start on a newline
581            // like tables, lists, etc, can do so.
582            $wikitext = $indexTs->getIndexTemplateStyles( '.pagetext' ) . "\n" . $wikitext;
583        }
584        $wikitextContent = new WikitextContent( $wikitext );
585
586        $parserOutput = new ParserOutput();
587        $this->wikitextContentHandler->fillParserOutputInternal( $wikitextContent, $cpoParams, $parserOutput );
588        $this->trackingCategories->addTrackingCategory(
589            $parserOutput,
590            $content->getLevel()->getLevelCategoryKey(),
591            $title
592        );
593        $parserOutput->setNumericPageProperty(
594            'proofread_page_quality_level',
595            $content->getLevel()->getLevel()
596        );
597
598        $poText = $parserOutput
599            ->runOutputPipeline( $cpoParams->getParserOptions(), [ 'enableSectionEditLinks' => false ] )
600            ->getContentHolderText();
601
602        // html container
603        $html = Html::openElement( 'div',
604            [ 'class' => 'prp-page-qualityheader quality' . $content->getLevel()->getLevel() ] ) .
605            wfMessage( 'proofreadpage_quality' . $content->getLevel()->getLevel() . '_message' )
606                ->title( $title )->inContentLanguage()->parse() .
607            Html::closeElement( 'div' ) .
608            Html::openElement( 'div', [ 'class' => 'pagetext' ] ) .
609            $poText .
610            Html::closeElement( 'div' );
611        $parserOutput->setText( $html );
612
613        $pageDisplayHandler = new PageDisplayHandler( $context );
614        $jsVars = $pageDisplayHandler->getPageJsConfigVars( $title, $content );
615        foreach ( $jsVars as $key => $value ) {
616            $parserOutput->setJsConfigVar( $key, $value );
617        }
618
619        // add modules
620        $parserOutput->addModuleStyles( [ 'ext.proofreadpage.base' ] );
621
622        // add scan image to dependencies
623        $parserOutput->addImage( strtok( $title->getDBkey(), '/' ) );
624
625        // add the styles.css as a dependency (even if it doesn't exist yet)
626        if ( $indexTs !== null ) {
627            $stylesPage = $indexTs->getTemplateStylesPage();
628
629            if ( $stylesPage ) {
630                $parserOutput->addTemplate(
631                    $stylesPage,
632                    $stylesPage->getArticleID(),
633                    $stylesPage->getLatestRevID() );
634            }
635        }
636    }
637}