Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 364
MockApiHelper
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 9
14280
0.00% covered (danger)
0.00%
0 / 364
 __construct
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 7
 setApiPrefix
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 addArticle
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 makeRequest
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 21
 imageInfo
0.00% covered (danger)
0.00%
0 / 1
3192
0.00% covered (danger)
0.00%
0 / 167
 processQuery
0.00% covered (danger)
0.00%
0 / 1
1560
0.00% covered (danger)
0.00%
0 / 118
 parse
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 27
 preProcess
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 14
 fetchTemplateData
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 6
<?php
declare( strict_types = 1 );
// phpcs:disable Generic.Files.LineLength.TooLong
namespace Wikimedia\Parsoid\ParserTests;
use Error;
use Wikimedia\Parsoid\Config\Api\ApiHelper;
/**
 * This class supports the implementation of Parser Tests in a standalone mode
 * and without network access.
 *
 * In standalone mode, the config and data transformations needed by Parsoid
 * cannot come from MediaWiki's database or its core classes.
 *
 * Without network access, we cannot fetch site configs or do data transformations
 * on a remote wiki. This class supports this by intercepting network requests
 * and returning mock responses based on cached site configs, hardcoded network
 * responses and config,
 *
 * So, this API helper should be used with the Parsoid\Config\Api* set of config classes
 * (and any subclasses derived from them).
 *
 * A lot of the responses here are tuned to what ParserTests needed. But, presumably
 * this can be used by PHP Unit tests as long as the specific set of mocked responses
 * satisfies the needs of those tests. Ideally, this class should NOT be updated for
 * anything but the needs of running parser tests.
 *
 * Alternatively, PHP Unit tests could bypass the Api* classes altogether and use
 * a (sub)set of mocked classes (Env, SiteConfig, PageConfig, DataAccess) if those
 * classes and the data they provide satisfies the needs of those tests.
 */
class MockApiHelper extends ApiHelper {
    // configuration to match PHP parserTests
    private const IMAGE_BASE_URL = 'http://example.com/images';
    private const IMAGE_DESC_URL = self::IMAGE_BASE_URL;
    private const FILE_PROPS = [
        'Foobar.jpg' => [
            'size' => 7881,
            'width' => 1941,
            'height' => 220,
            'bits' => 8,
            'mime' => 'image/jpeg'
        ],
        'Thumb.png' => [
            'size' => 22589,
            'width' => 135,
            'height' => 135,
            'bits' => 8,
            'mime' => 'image/png'
        ],
        'Foobar.svg' => [
            'size' => 12345,
            'width' => 240,
            'height' => 180,
            'bits' => 24,
            'mime' => 'image/svg+xml'
        ],
        'Bad.jpg' => [
            'size' => 12345,
            'width' => 320,
            'height' => 240,
            'bits' => 24,
            'mime' => 'image/jpeg',
        ],
        'LoremIpsum.djvu' => [
            'size' => 3249,
            'width' => 2480,
            'height' => 3508,
            'bits' => 8,
            'mime' => 'image/vnd.djvu',
            'mediatype' => 'OFFICE',
            'pagecount' => 5,
        ],
        'Video.ogv' => [
            'size' => 12345,
            'width' => 320,
            'height' => 240,
            'bits' => 0,
            # duration comes from
            # TimedMediaHandler/tests/phpunit/mocks/MockOggHandler::getLength()
            'duration' => 4.3666666666667,
            'mime' => 'video/ogg; codecs="theora"',
            'mediatype' => 'VIDEO',
            'title' => 'Original Ogg file, 320 × 240 (590 kbps)',
            'shorttitle' => 'Ogg source',
            # hacky way to get seek parameters to return the correct info
            'extraParams' => [
                'seek=1.2' => 'seek%3D1.2',
                'seek=85' => 'seek%3D3.3666666666667', # hard limited by duration
            ],
        ],
        'Audio.oga' => [
            'size' => 12345,
            'width' => 0,
            'height' => 0,
            'bits' => 0,
            # duration comes from
            # TimedMediaHandler/tests/phpunit/mocks/MockOggHandler::getLength()
            'duration' => 0.99875,
            'mime' => 'audio/ogg; codecs="vorbis"',
            'mediatype' => 'AUDIO',
            'title' => 'Original Ogg file (41 kbps)',
            'shorttitle' => 'Ogg source',
        ]
    ];
    private $articleCache = [];
    private $cachedConfigs = [];
    private static $MAIN_PAGE = [
        'query' => [
            'pages' => [
                [
                    'pageid' => 1,
                    'ns' => 0,
                    'title' => 'Main Page',
                    'revisions' => [
                        [
                            'revid' => 1,
                            'parentid' => 0,
                            'slots' => [
                                'main' => [
                                    'contentmodel' => 'wikitext',
                                    'contentformat' => 'text/x-wiki',
                                    'content' => "<strong>MediaWiki has been successfully installed.</strong>\n\nConsult the [//meta.wikimedia.org/wiki/Help:Contents User's Guide] for information on using the wiki software.\n\n== Getting started ==\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Configuration settings list]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Localise MediaWiki for your language]"
                                ]
                            ]
                        ]
                    ]
                ]
            ]
        ]
    ];
    // Old response structure, pre-mcr
    private static $OLD_RESPONSE = [
        'query' => [
            'pages' => [
                [
                    'pageid' => 999,
                    'ns' => 0,
                    'title' => 'Old Response',
                    'revisions' => [
                        [
                            'revid' => 999,
                            'parentid' => 0,
                            'contentmodel' => 'wikitext',
                            'contentformat' => 'text/x-wiki',
                            '*' => "<strong>MediaWiki was successfully installed.</strong>\n\nConsult the [//meta.wikimedia.org/wiki/Help:Contents User's Guide] for information on using the wiki software.\n\n== Getting started ==\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Configuration settings list]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]\n* [//www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Localise MediaWiki for your language]"
                        ]
                    ]
                ]
            ]
        ]
    ];
    private static $JUNK_PAGE = [
        'query' => [
            'pages' => [
                [
                    'pageid' => 2,
                    'ns' => 0,
                    'title' => 'Junk Page',
                    'revisions' => [
                        [
                            'revid' => 2,
                            'parentid' => 0,
                            'slots' => [
                                'main' => [
                                    'contentmodel' => 'wikitext',
                                    'contentformat' => 'text/x-wiki',
                                    'content' => '2. This is just some junk. See the comment above.'
                                ]
                            ]
                        ]
                    ]
                ]
            ]
        ]
    ];
    private static $LARGE_PAGE = [
        'query' => [
            'pages' => [
                [
                    'pageid' => 3,
                    'ns' => 0,
                    'title' => 'Large_Page',
                    'revisions' => [
                        [
                            'revid' => 3,
                            'parentid' => 0,
                            'slots' => [
                                'main' => [
                                    'contentmodel' => 'wikitext',
                                    'contentformat' => 'text/x-wiki',
                                    /* content will be set separately */
                                ]
                            ]
                        ]
                    ]
                ]
            ]
        ]
    ];
    private static $REUSE_PAGE = [
        'query' => [
            'pages' => [
                [
                    'pageid' => 100,
                    'ns' => 0,
                    'title' => 'Reuse_Page',
                    'revisions' => [
                        [
                            'revid' => 100,
                            'parentid' => 0,
                            'slots' => [
                                'main' => [
                                    'contentmodel' => 'wikitext',
                                    'contentformat' => 'text/x-wiki',
                                    'content' => '{{colours of the rainbow}}'
                                ]
                            ]
                        ]
                    ]
                ]
            ]
        ]
    ];
    private static $JSON_PAGE = [
        'query' => [
            'pages' => [
                [
                    'pageid' => 101,
                    'ns' => 0,
                    'title' => 'JSON_Page',
                    'revisions' => [
                        [
                            'revid' => 101,
                            'parentid' => 0,
                            'slots' => [
                                'main' => [
                                    'contentmodel' => 'json',
                                    'contentformat' => 'text/json',
                                    'content' => '[1]'
                                ]
                            ]
                        ]
                    ]
                ]
            ]
        ]
    ];
    private static $LINT_PAGE = [
        'query' => [
            'pages' => [
                [
                    'pageid' => 102,
                    'ns' => 0,
                    'title' => 'Lint Page',
                    'revisions' => [
                        [
                            'revid' => 102,
                            'parentid' => 0,
                            'slots' => [
                                'main' => [
                                    'contentmodel' => 'wikitext',
                                    'contentformat' => 'text/x-wiki',
                                    'content' => "{|\nhi\n|ho\n|}"
                                ]
                            ]
                        ]
                    ]
                ]
            ]
        ]
    ];
    private static $REDLINKS_PAGE = [
        'query' => [
            'pages' => [
                [
                    'pageid' => 103,
                    'ns' => 0,
                    'title' => 'Redlinks Page',
                    'revisions' => [
                        [
                            'revid' => 103,
                            'parentid' => 0,
                            'slots' => [
                                'main' => [
                                    'contentmodel' => 'wikitext',
                                    'contentformat' => 'text/x-wiki',
                                    'content' => '[[Special:Version]] [[Doesnotexist]] [[Redirected]]'
                                ]
                            ]
                        ]
                    ]
                ]
            ]
        ]
    ];
    private static $VARIANT_PAGE = [
        'query' => [
            'pages' => [
                [
                    'pageid' => 104,
                    'ns' => 0,
                    'pagelanguage' => 'sr',
                    'pagelanguagedir' => 'ltr',
                    'title' => 'Variant Page',
                    'revisions' => [
                        [
                            'revid' => 104,
                            'parentid' => 0,
                            'slots' => [
                                'main' => [
                                    'contentmodel' => 'wikitext',
                                    'contentformat' => 'text/x-wiki',
                                    'content' => "абвг abcd"
                                ]
                            ]
                        ]
                    ]
                ]
            ]
        ]
    ];
    private static $NOVARIANT_PAGE = [
        'query' => [
            'pages' => [
                [
                    'pageid' => 105,
                    'ns' => 0,
                    'pagelanguage' => 'sr',
                    'pagelanguagedir' => 'ltr',
                    'title' => 'No Variant Page',
                    'revisions' => [
                        [
                            'revid' => 105,
                            'parentid' => 0,
                            'slots' => [
                                'main' => [
                                    'contentmodel' => 'wikitext',
                                    'contentformat' => 'text/x-wiki',
                                    'content' => "абвг abcd\n__NOCONTENTCONVERT__"
                                ]
                            ]
                        ]
                    ]
                ]
            ]
        ]
    ];
    private static $REVISION_PAGE = [
        'query' => [
            'pages' => [
                [
                    'pageid' => 63,
                    'ns' => 0,
                    'title' => 'Revision ID',
                    'revisions' => [
                        [
                            'revid' => 63,
                            'parentid' => 0,
                            'slots' => [
                                'main' => [
                                    'contentmodel' => 'wikitext',
                                    'contentformat' => 'text/x-wiki',
                                    'content' => '{{REVISIONID}}'
                                ]
                            ]
                        ]
                    ]
                ]
            ]
        ]
    ];
    private static $missingTitles = [ 'Doesnotexist' ];
    private static $specialTitles = [
        'Special:Version',
        'Special:BookSources',
        'Special:BookSources/isbn=4-00-026157-6',
        'Special:BookSources/0978739256',
    ];
    private static $redirectTitles = [ 'Redirected' ];
    private static $disambigTitles = [ 'Disambiguation' ];
    private const FNAMES = [
        'Image:Foobar.jpg' => 'Foobar.jpg',
        'Datei:Foobar.jpg' => 'Foobar.jpg',
        'File:Foobar.jpg' => 'Foobar.jpg',
        'Archivo:Foobar.jpg' => 'Foobar.jpg',
        'Mynd:Foobar.jpg' => 'Foobar.jpg',
        "Датотека:Foobar.jpg" => 'Foobar.jpg',
        'Image:Foobar.svg' => 'Foobar.svg',
        'File:Foobar.svg' => 'Foobar.svg',
        'Файл:Foobar.svg' => 'Foobar.svg',
        'Datei:Foobar.svg' => 'Foobar.svg',
        'Image:Thumb.png' => 'Thumb.png',
        'File:Thumb.png' => 'Thumb.png',
        'File:LoremIpsum.djvu' => 'LoremIpsum.djvu',
        'File:Video.ogv' => 'Video.ogv',
        'File:Audio.oga' => 'Audio.oga',
        'File:Bad.jpg' => 'Bad.jpg',
    ];
    private const PNAMES = [
        'Image:Foobar.jpg' => 'File:Foobar.jpg',
        'Image:Foobar.svg' => 'File:Foobar.svg',
        'Image:Thumb.png' => 'File:Thumb.png'
    ];
    // FIXME: Get this info from pagelanguage of a revision for these pages
    private const PAGELANGS = [
        'Rupage' => 'ru',
        'Depage' => 'de',
    ];
    // File is present in these langs
    private const FILELANGS = [
        'Foobar.svg' => [ 'en', 'ru' ],
    ];
    // This templatedata description only provides a subset of fields
    // that mediawiki API returns. Parsoid only uses the format and
    // paramOrder fields at this point, so keeping these lean.
    private static $templateData = [
        'Template:NoFormatWithParamOrder' => [
            'paramOrder' => [ 'f0', 'f1', 'unused2', 'f2', 'unused3' ]
        ],
        'Template:InlineTplNoParamOrder' => [
            'format' => 'inline'
        ],
        'Template:BlockTplNoParamOrder' => [
            'format' => 'block'
        ],
        'Template:InlineTplWithParamOrder' => [
            'format' => 'inline',
            'paramOrder' => [ 'f1', 'f2' ]
        ],
        'Template:BlockTplWithParamOrder' => [
            'format' => 'block',
            'paramOrder' => [ 'f1', 'f2' ]
        ],
        'Template:WithParamOrderAndAliases' => [
            'params' => [
                'f1' => [ 'aliases' => [ 'f4', 'f3' ] ]
            ],
            'paramOrder' => [ 'f1', 'f2' ]
        ],
        'Template:InlineFormattedTpl_1' => [
            'format' => '{{_|_=_}}'
        ],
        'Template:InlineFormattedTpl_2' => [
            'format' => "\n{{_ | _ = _}}"
        ],
        'Template:InlineFormattedTpl_3' => [
            'format' => '{{_| _____ = _}}'
        ],
        'Template:BlockFormattedTpl_1' => [
            'format' => "{{_\n| _ = _\n}}"
        ],
        'Template:BlockFormattedTpl_2' => [
            'format' => "\n{{_\n| _ = _\n}}\n"
        ],
        'Template:BlockFormattedTpl_3' => [
            'format' => "{{_|\n _____ = _}}"
        ]
    ];
    /** @var string wiki prefix for which we are mocking the api access */
    private $prefix = 'enwiki';
    /**
     * @param ?string $prefix
     */
    public function __construct( ?string $prefix = null ) {
        if ( $prefix ) {
            $this->prefix = $prefix;
        }
        // PORT-FIXME: Need to get this value
        // $wtSizeLimit = $parsoidOptions->limits->wt2html->maxWikitextSize;
        $wtSizeLimit = 1000000;
        $mainSlot = &self::$LARGE_PAGE['query']['pages'][0]['revisions'][0]['slots']['main'];
        $mainSlot['content'] = str_repeat( 'a', $wtSizeLimit + 1 );
    }
    /**
     * Update prefix
     * @param string $prefix
     */
    public function setApiPrefix( string $prefix ): void {
        $this->prefix = $prefix;
    }
    /**
     * Register an article defined in parsertests so that we can return
     * the proper known/missing information about that title.
     * @param string $key The normalized title of the article
     * @param Article $article The contents of the article
     */
    public function addArticle( string $key, Article $article ): void {
        $this->articleCache[$key] = $article;
    }
    /**
     * @param array $params
     * @return array
     */
    public function makeRequest( array $params ): array {
        switch ( $params['action'] ?? null ) {
            case 'query':
                return $this->processQuery( $params );
            case 'parse':
                return $this->parse( $params['text'], !empty( $params['onlypst'] ) );
            case 'templatedata':
                return $this->fetchTemplateData( $params );
            case 'expandtemplates':
                $ret = $this->preProcess( $params['titles'] ?? $params['title'], $params['text'], $params['revid'] ?? null );
                if ( $ret ) {
                    $ret += [
                        'categories' => [],
                        'modules' => [],
                        'modulestyles' => []
                    ];
                }
                return $ret;
            default:
                return []; // FIXME: Maybe some error
        }
    }
    /**
     * @param string $filename
     * @param ?int $twidth
     * @param ?int $theight
     * @param ?string $extraParam optional iiurlparam, used for video/pdf/etc
     * @param ?string $contexttitle optional iibadfilecontexttitle
     * @return ?array
     */
    private function imageInfo(
        string $filename, ?int $twidth, ?int $theight, ?string $extraParam,
        ?string $contexttitle
    ): ?array {
        $normPageName = self::PNAMES[$filename] ?? $filename;
        $normFileName = self::FNAMES[$filename] ?? $filename;
        $props = self::FILE_PROPS[$normFileName] ?? null;
        if ( $props === null ) {
            // We don't have info for this file
            return null;
        }
        $md5 = md5( $normFileName );
        $md5prefix = $md5[0] . '/' . $md5[0] . $md5[1] . '/';
        $baseurl = self::IMAGE_BASE_URL . '/' . $md5prefix . $normFileName;
        $height = $props['height'];
        $width = $props['width'];
        $turl = self::IMAGE_BASE_URL . '/thumb/' . $md5prefix . $normFileName;
        $durl = self::IMAGE_DESC_URL . '/' . $normFileName;
        $mediatype = $props['mediatype'] ??
            ( $props['mime'] === 'image/svg+xml' ? 'DRAWING' : 'BITMAP' );
        $info = [
            'size' => $props['size'],
            'height' => $height,
            'width' => $width,
            'url' => $baseurl,
            'descriptionurl' => $durl,
            'mediatype' => $mediatype,
            'mime' => $props['mime']
        ];
        if ( isset( $props['duration'] ) ) {
            $info['duration'] = $props['duration'];
        }
        if ( isset( $props['pagecount'] ) ) {
            $info['pagecount'] = $props['pagecount'];
        }
        if ( ( $mediatype === 'VIDEO' || $mediatype === 'DRAWING' ) && !$twidth && !$theight ) {
            $twidth = $width;
            $theight = $height;
        }
        preg_match( '/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/i', $extraParam ?? '', $matches );
        $lang = $matches[1] ?? null;
        $pagelang = self::PAGELANGS[$contexttitle] ?? 'en';
        $filelangs = self::FILELANGS[$normFileName] ?? [ 'en' ];
        // Set $lang based on the targetlang, if the file is present in that lang
        if (
            $lang === null &&
            $mediatype === 'DRAWING' &&
            $pagelang !== 'en' &&
            in_array( $pagelang, $filelangs, true )
        ) {
            $lang = $pagelang;
            $extraParam = "lang{$lang}-{$twidth}px";
        }
        if ( $theight || $twidth ) {
            if ( $theight === null ) {
                // File::scaleHeight in PHP
                $theight = round( $height * $twidth / $width );
            } elseif ( $twidth === null ) {
                // MediaHandler::fitBoxWidth in PHP
                // This is crazy!
                $idealWidth = $width * $theight / $height;
                $roundedUp = ceil( $idealWidth );
                if ( round( $roundedUp * $height / $width ) > $theight ) {
                    $twidth = floor( $idealWidth );
                } else {
                    $twidth = $roundedUp;
                }
            } else {
                if ( round( $height * $twidth / $width ) > $theight ) {
                    $twidth = ceil( $width * $theight / $height );
                } else {
                    $theight = round( $height * $twidth / $width );
                }
            }
            $urlWidth = $twidth;
            if ( $twidth > $width ) {
                // The PHP api won't enlarge a bitmap ... but the batch api will.
                // But, to match the PHP sections, don't scale.
                if ( $mediatype !== 'DRAWING' ) {
                    $urlWidth = $width;
                }
            }
            $thumbBaseUrl = $turl;
            $page = null;
            if ( $urlWidth !== $width || $mediatype === 'AUDIO' || $mediatype === 'VIDEO' || $mediatype === 'OFFICE' || $mediatype === 'DRAWING' ) {
                $turl .= '/';
                if ( preg_match( '/^page(\d+)-(\d+)px$/', $extraParam ?? '', $matches ) ) {
                    $turl .= $extraParam;
                    $page = (int)$matches[1];
                } elseif ( $mediatype === 'OFFICE' ) {
                    $turl .= 'page1-' . $urlWidth . 'px';
                    $page = 1;
                } elseif ( $lang !== null ) {
                    // Explicit English just gets the default path
                    if ( $lang === 'en' ) {
                        $turl .= $urlWidth . 'px';
                        $lang = null;
                    } else {
                        $turl .= $extraParam;
                    }
                } else {
                    $turl .= $urlWidth . 'px';
                }
                $turl .= '-';
                if ( $mediatype === 'VIDEO' ) {
                    // Hack in a 'seek' option, if provided (T258767)
                    if ( str_starts_with( $extraParam ?? '', 'seek' ) ) {
                        $turl .= $props['extraParams'][$extraParam] ?? '';
                    }
                    $turl .= '-';
                }
                $turl .= $normFileName;
                switch ( $mediatype ) {
                    case 'AUDIO':
                        // No thumbs are generated for audio
                        $turl = self::IMAGE_BASE_URL . '/w/resources/assets/file-type-icons/fileicon-ogg.png';
                        break;
                    case 'VIDEO':
                    case 'OFFICE':
                        $turl .= '.jpg';
                        break;
                    case 'DRAWING':
                        $turl .= '.png';
                        break;
                }
            } else {
                $turl = $baseurl;
            }
            $info['thumbwidth'] = $twidth;
            $info['thumbheight'] = $theight;
            $info['thumburl'] = $turl;
            // src set info; added to core API result as part of T226683
            foreach ( [ 1.5, 2 ] as $scale ) {
                $turl = $baseurl;
                if (
                    round( $twidth * $scale ) < $width ||
                    $mediatype === 'DRAWING' ||
                    $mediatype === 'OFFICE'
                ) {
                    $turl = $thumbBaseUrl . '/';
                    if ( $page !== null ) {
                        $turl .= "page{$page}-";
                    }
                    if ( $lang !== null ) {
                        $turl .= "lang{$lang}-";
                    }
                    $turl .= round( $twidth * $scale ) . 'px-' . $normFileName;
                    if ( $mediatype === 'VIDEO' || $mediatype === 'OFFICE' ) {
                        $turl .= '.jpg';
                    } elseif ( $mediatype === 'DRAWING' ) {
                        $turl .= '.png';
                    }
                }
                if ( $info['thumburl'] !== $turl && $mediatype !== 'AUDIO' ) {
                    $info['responsiveUrls']["$scale"] = $turl;
                }
            }
        }
        // Make this look like a TMH response
        if ( isset( $props['title'] ) || isset( $props['shorttitle'] ) ) {
            $info['derivatives'] = [
                [
                    'src' => $info['url'],
                    'type' => $info['mime'],
                    'width' => strval( $info['width'] ),
                    'height' => strval( $info['height'] ),
                ]
            ];
            if ( isset( $props['title'] ) ) {
                $info['derivatives'][0]['title'] = $props['title'];
            }
            if ( isset( $props['shorttitle'] ) ) {
                $info['derivatives'][0]['shorttitle'] = $props['shorttitle'];
            }
        }
        return [
            'result' => $info,
            'normPageName' => $normPageName
        ];
    }
    /**
     * @param array $params
     * @return array
     */
    private function processQuery( array $params ): array {
        if ( ( $params['meta'] ?? null ) === 'siteinfo' ) {
            if ( !isset( $this->cachedConfigs[$this->prefix] ) ) {
                $this->cachedConfigs[$this->prefix] = json_decode(
                    file_get_contents( __DIR__ . "/../../baseconfig/2/$this->prefix.json" ), true );
            }
            return $this->cachedConfigs[$this->prefix];
        }
        $revid = $params['revids'] ?? null;
        if ( ( $params['prop'] ?? null ) === 'revisions' ) {
            if ( $revid === '1' || $params['titles'] === 'Main_Page' ) {
                return self::$MAIN_PAGE;
            } elseif ( $revid === '2' || $params['titles'] === 'Junk_Page' ) {
                return self::$JUNK_PAGE;
            } elseif ( $revid === '3' || $params['titles'] === 'Large_Page' ) {
                return self::$LARGE_PAGE;
            } elseif ( $revid === '63' || $params['titles'] === 'Revision_ID' ) {
                return self::$REVISION_PAGE;
            } elseif ( $revid === '100' || $params['titles'] === 'Reuse_Page' ) {
                return self::$REUSE_PAGE;
            } elseif ( $revid === '101' || $params['titles'] === 'JSON_Page' ) {
                return self::$JSON_PAGE;
            } elseif ( $revid === '102' || $params['titles'] === 'Lint_Page' ) {
                return self::$LINT_PAGE;
            } elseif ( $revid === '103' || $params['titles'] === 'Redlinks_Page' ) {
                return self::$REDLINKS_PAGE;
            } elseif ( $revid === '104' || $params['titles'] === 'Variant_Page' ) {
                return self::$VARIANT_PAGE;
            } elseif ( $revid === '105' || $params['titles'] === 'No_Variant_Page' ) {
                return self::$NOVARIANT_PAGE;
            } elseif ( $revid === '999' || $params['titles'] === 'Old_Response' ) {
                return self::$OLD_RESPONSE;
            } else {
                return [ 'query' => [ 'pages' => [
                            [
                                'ns' => 6,
                                'title' => json_encode( $params['titles'] ),
                                'missing' => true,
                                'imagerepository' => true
                            ]
                        ]
                    ]
                ];
            }
        }
        if ( ( $params['prop'] ?? null ) === 'info' ) {
            $ret = [];
            $titles = preg_split( '/\|/', $params['titles'] );
            foreach ( $titles as $t ) {
                $props = [ 'title' => $t ];
                $key = str_replace( ' ', '_', $t ); # poor man's normalization
                $definedInPt = isset( $this->articleCache[$key] );
                if ( in_array( $t, self::$missingTitles, true ) ||
                     !$definedInPt ) {
                    $props['missing'] = true;
                }
                if ( in_array( $t, self::$specialTitles, true ) ) {
                    $props['special'] = true;
                    $props['missing'] = false;
                }
                if ( in_array( $t, self::$redirectTitles, true ) ) {
                    $props['redirect'] = true;
                    $props['missing'] = false;
                }
                if ( in_array( $t, self::$disambigTitles, true ) ) {
                    $props['linkclasses'] = [ 'mw-disambig' ];
                    $props['missing'] = false;
                }
                $ret[] = $props;
            }
            return [ 'query' => [ 'pages' => $ret ] ];
        }
        if ( ( $params['prop'] ?? null ) === 'imageinfo' ) {
            $response = [ 'query' => [] ];
            $filename = $params['titles']; // assumes this is a single file
            $tonum = static function ( $x ) {
                return $x ? (int)$x : null;
            };
            $ii = self::imageInfo(
                $filename,
                isset( $params['iiurlwidth'] ) ? $tonum( $params['iiurlwidth'] ) : null,
                isset( $params['iiurlheight'] ) ? $tonum( $params['iiurlheight'] ) : null,
                $params['iiurlparam'] ?? null,
                $params['iibadfilecontexttitle'] ?? null
            );
            if ( $ii === null ) {
                $p = [
                    'ns' => 6,
                    'title' => $filename,
                    'imagerepository' => true,
                    'imageinfo' => [ [
                        'size' => 0,
                        'width' => 0,
                        'height' => 0,
                        'filemissing' => true,
                        'mime' => null,
                        'mediatype' => null
                    ] ]
                ];
                $p['missing'] = $p['imageinfo']['filemissing'] = true;
                $p['badfile'] = false;
            } else {
                if ( $filename !== $ii['normPageName'] ) {
                    $response['query']['normalized'] = [
                        [ 'from' => $filename, 'to' => $ii['normPageName'] ]
                    ];
                }
                $p = [
                    'pageid' => 1,
                    'ns' => 6,
                    'title' => $ii['normPageName'],
                    'imageinfo' => [ $ii['result'] ]
                ];
                $p['badfile'] = ( $filename === 'File:Bad.jpg' );
            }
            $response['query']['pages'] = [ $p ];
            return $response;
        }
        return [ "error" => new Error( 'Uh oh!' ) ];
    }
    /**
     * @param string $text
     * @param bool $onlypst
     * @return array
     */
    private function parse( string $text, bool $onlypst ): array {
        // We're performing a subst
        if ( $onlypst ) {
            return [ 'text' => preg_replace( '/\{\{subst:1x\|([^}]+)\}\}/', '$1', $text, 1 ) ];
        }
        $res = null;
        // Render to html the contents of known extension tags
        // These are the only known extensions (besides native extensions)
        // used in parser tests currently. This would need to be updated
        // as more templates are added OR we need to rely on true parsing.
        preg_match( '#<([A-Za-z][^\t\n\v />\0]*)#', $text, $match );
        switch ( $match[1] ?? '' ) {
            // FIXME: this isn't really used by the mocha tests
            // since some mocha tests hit the production db, but
            // when we fix that, they should go through this.
            case 'templatestyles':
                $res = "<style data-mw-deduplicate='TemplateStyles:r123456'>small { font-size: 120% } big { font-size: 80% }</style>"; // Silliness
                break;
            case 'translate':
                $res = $text;
                break;
            case 'indicator':
            case 'section':
                $res = "";
                break;
            default:
                throw new Error( 'Unhandled extension type encountered in: ' . $text );
        }
        $parse = [
            'text' => $res,
            'categories' => [],
            'modules' => [],
            'modulestyles' => []
        ];
        return [ 'parse' => $parse ];
    }
    /**
     * @param string $title
     * @param string $text
     * @param ?int $revid
     * @return ?array
     */
    private function preProcess(
        string $title, string $text, ?int $revid
    ): ?array {
        // These are the only known templates in current parser tests.
        // This would need to be updated as more templates are added OR we need
        // to rely on true (instead of mock) preprocessing.
        preg_match( '/{{1x\|(.*?)}}/', $text, $match );
        if ( $match ) {
            return [ 'wikitext' => $match[1] ];
        } elseif ( $text === '{{colours of the rainbow}}' ) {
            return [ 'wikitext' => 'purple' ];
        } elseif ( $text === '{{REVISIONID}}' ) {
            return [ 'wikitext' => (string)$revid ];
        } else {
            error_log( "UNKNOWN TEMPLATE: $text for $title\n" );
            return null;
        }
    }
    /**
     * @param array $params
     * @return array
     */
    private function fetchTemplateData( array $params ): array {
        return [
            // Assumes that titles is a single title
            // (which is how Parsoid uses this)
            'pages' => [
                '1' => self::$templateData[$params['titles'] ?? ''] ?? []
            ]
        ];
    }
}