Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
6.67% covered (danger)
6.67%
4 / 60
CRAP
35.05% covered (danger)
35.05%
68 / 194
Parsoid\Config\Env
0.00% covered (danger)
0.00%
0 / 1
6.67% covered (danger)
6.67%
4 / 60
3184.35
35.05% covered (danger)
35.05%
68 / 194
 __construct
0.00% covered (danger)
0.00%
0 / 1
4.02
90.00% covered (success)
90.00%
27 / 30
 getSiteConfig
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getPageConfig
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getDataAccess
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 noDataAccess
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 nativeTemplateExpansionEnabled
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getUID
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getFID
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getWrapSections
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getPipelineFactory
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getRequestOffsetType
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getCurrentOffsetType
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 setCurrentOffsetType
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 resolveTitle
100.00% covered (success)
100.00%
1 / 1
14
100.00% covered (success)
100.00%
27 / 27
 titleToString
0.00% covered (danger)
0.00%
0 / 1
3.04
83.33% covered (warning)
83.33%
5 / 6
 normalizedTitleKey
0.00% covered (danger)
0.00%
0 / 1
3.14
75.00% covered (warning)
75.00%
3 / 4
 normalizeAndResolvePageTitle
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 makeTitle
0.00% covered (danger)
0.00%
0 / 1
11.10
37.50% covered (danger)
37.50%
3 / 8
 makeTitleFromText
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 1
 makeTitleFromURLDecodedStr
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
1 / 1
 makeLink
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 isValidLinkTarget
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 generateUID
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 newObjectId
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 newAboutId
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 setOrigDOM
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getOrigDOM
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 setDOMDiff
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getDOMDiff
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 1
 newFragmentId
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 referenceDataObject
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 3
 createDocument
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 5
 setVariable
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 setBehaviorSwitch
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getBehaviorSwitch
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getPageMainContent
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getFragmentMap
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getFragment
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 setFragment
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 recordLint
n/a
0 / 0
2
n/a
0 / 0
 anonymousFunction:740#3149
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getLints
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 log
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 bumpTimeUse
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 bumpCount
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 bumpWt2HtmlResourceUse
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 8
 bumpHtml2WtResourceUse
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 8
 getContentHandler
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 6
 langConverterEnabled
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 6
 shouldScrubWikitext
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getInputContentVersion
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 setInputContentVersion
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getOutputContentVersion
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 setOutputContentVersion
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 resolveContentVersion
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 5
 getHtmlVariantLanguage
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 1
 getWtVariantLanguage
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 1
 addOutputProperty
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 getOutputProperties
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 htmlVary
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 5
 htmlContentLanguage
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
<?php
declare( strict_types = 1 );
namespace Parsoid\Config;
// use Closure;
use Composer\Semver\Comparator;
use Composer\Semver\Semver;
use DOMDocument;
use DOMElement;
use DOMNode;
use Parsoid\ContentModelHandler;
use Parsoid\ResourceLimitExceededException;
use Parsoid\Tokens\Token;
use Parsoid\Logger\ParsoidLogger;
use Parsoid\Utils\DataBag;
use Parsoid\Utils\DOMCompat;
use Parsoid\Utils\DOMUtils;
// use Parsoid\Utils\PHPUtils;
use Parsoid\Utils\Title;
use Parsoid\Utils\TitleNamespace;
use Parsoid\Utils\TitleException;
use Parsoid\Utils\TokenUtils;
use Parsoid\Utils\Util;
use Parsoid\Wt2Html\Frame;
use Parsoid\Wt2Html\PageConfigFrame;
use Parsoid\Wt2Html\ParserPipelineFactory;
use Parsoid\Wt2Html\TT\Sanitizer;
use UnexpectedValueException;
// phpcs:disable MediaWiki.Commenting.FunctionComment.MissingDocumentationPublic
/**
 * Environment/Envelope class for Parsoid
 *
 * Carries around the SiteConfig and PageConfig during an operation
 * and provides certain other services.
 */
class Env {
    /**
     * Available HTML content versions.
     * @see https://www.mediawiki.org/wiki/Parsoid/API#Content_Negotiation
     * @see https://www.mediawiki.org/wiki/Specs/HTML/2.1.0#Versioning
     */
    const AVAILABLE_VERSIONS = [ '2.1.0', '999.0.0' ];
    /** @var SiteConfig */
    private $siteConfig;
    /** @var PageConfig */
    private $pageConfig;
    /** @var DataAccess */
    private $dataAccess;
    /**
     * The top-level frame for this conversion.  This largely wraps the
     * PageConfig.
     *
     * In the future we may replace PageConfig with the Frame, and add
     * a
     * @var Frame
     */
    public $topFrame;
    // XXX In the future, perhaps replace PageConfig with the Frame, and
    // add $this->currentFrame (relocated from TokenTransformManager) if/when
    // we've removed async parsing.
    /**
     * @var bool Are data accesses disabled?
     *
     * FIXME: This can probably moved to a NoDataAccess instance, rather than
     * being an explicit mode of Parsoid.  See T229469
     */
    private $noDataAccess;
    /**
     * @var bool Are we using native template expansion?
     *
     * Parsoid implements native template expansion, which is currently
     * only used during parser tests; in production, template expansion
     * is done via MediaWiki's legacy preprocessor.
     *
     * FIXME: Hopefully this distinction can be removed when we're entirely
     * in PHP land.
     */
    private $nativeTemplateExpansion;
    /** @phan-var array<string,int> */
    private $wt2htmlUsage = [];
    /** @phan-var array<string,int> */
    private $html2wtUsage = [];
    /** @var DOMDocument[] */
    private $liveDocs = [];
    /** @var bool */
    private $wrapSections = true;
    /** @var string */
    private $requestOffsetType = 'byte';
    /** @var string */
    private $currentOffsetType = 'byte';
    /** @var array<string,mixed> */
    private $behaviorSwitches = [];
    /**
     * Maps fragment id to the fragment forest (array of DOMNodes).
     * @var array<string,DOMNode[]>
     */
    private $fragmentMap = [];
    /**
     * @var int used to generate fragment ids as needed during parse
     */
    private $fid = 1;
    /** @var int used to generate uids as needed during this parse */
    private $uid = 1;
    /** @var array[] Lints recorded */
    private $lints = [];
    /** @var bool[] */
    public $traceFlags;
    /** @var bool[] */
    public $dumpFlags;
    /** @var bool[] */
    public $debugFlags;
    /** @var ParsoidLogger */
    private $parsoidLogger;
    /** @var float */
    public $startTime;
    /** @var bool */
    private $scrubWikitext = false;
    /**
     * The default content version that Parsoid assumes it's serializing or
     * updating in the pb2pb endpoints
     *
     * @var string
     */
    private $inputContentVersion;
    /**
     * The default content version that Parsoid will generate.
     *
     * @var string
     */
    private $outputContentVersion;
    /**
     * If non-null, the language variant used for Parsoid HTML;
     * we convert to this if wt2html, or from this if html2wt.
     * @var string
     */
    private $htmlVariantLanguage;
    /**
     * If non-null, the language variant to be used for wikitext.
     * If null, heuristics will be used to identify the original wikitext variant
     * in wt2html mode, and in html2wt mode new or edited HTML will be left unconverted.
     * @var string
     */
    private $wtVariantLanguage;
    /** @var ParserPipelineFactory */
    private $pipelineFactory;
    /**
     * FIXME Used in DedupeStyles::dedupe()
     * @var array
     */
    public $styleTagKeys = [];
    /** @var bool */
    public $pageBundle = false;
    /** @var bool */
    public $discardDataParsoid = false;
    /** @var DOMNode */
    private $origDOM;
    /** @var DOMDocument */
    private $domDiff;
    /**
     * Page properties (module resources primarily) that need to be output
     * @var array
     */
    private $outputProps = [];
    /**
     * PORT-FIXME: public currently
     * Cache of wikitext source for a title
     * @var array
     */
    public $pageCache = [];
    /**
     * PORT-FIXME: public currently
     * HTML Cache of expanded transclusions to support
     * reusing expansions from HTML of previous revision.
     * @var array
     */
    public $transclusionCache = [];
    /**
     * PORT-FIXME: public currently
     * HTML Cache of expanded media wikiext to support
     * reusing expansions from HTML of previous revision.
     * @var array
     */
    public $mediaCache = [];
    /**
     * PORT-FIXME: public currently
     * HTML Cache of expanded extension tags to support
     * reusing expansions from HTML of previous revision.
     * @var array
     */
    public $extensionCache = [];
    /**
     * @param SiteConfig $siteConfig
     * @param PageConfig $pageConfig
     * @param DataAccess $dataAccess
     * @param array|null $options
     *  - wrapSections: (bool) Whether `<section>` wrappers should be added.
     *  - pageBundle: (bool) Sets ids on nodes and stores data-* attributes in a JSON blob.
     *  - scrubWikitext: (bool) Indicates emit "clean" wikitext.
     *  - traceFlags: (array) Flags indicating which components need to be traced
     *  - dumpFlags: (bool[]) Dump flags
     *  - debugFlags: (bool[]) Debug flags
     *  - noDataAccess: boolean
     *  - nativeTemplateExpansion: boolean
     *  - discardDataParsoid: boolean
     *  - offsetType: 'byte' (default), 'ucs2', 'char'
     *                See `Parsoid\Wt2Html\PP\Processors\ConvertOffsets`.
     *  - titleShouldExist: (bool) Are we expecting page content to exist?
     */
    public function __construct(
        SiteConfig $siteConfig, PageConfig $pageConfig, DataAccess $dataAccess, array $options = null
    ) {
        $options = $options ?? [];
        $this->siteConfig = $siteConfig;
        $this->pageConfig = $pageConfig;
        $this->dataAccess = $dataAccess;
        $this->topFrame = new PageConfigFrame( $this, $pageConfig, $siteConfig,
            !empty( $options['titleShouldExist'] ) );
        if ( isset( $options['scrubWikitext'] ) ) {
            $this->scrubWikitext = !empty( $options['scrubWikitext'] );
        }
        if ( isset( $options['wrapSections'] ) ) {
            $this->wrapSections = !empty( $options['wrapSections'] );
        }
        if ( isset( $options['pageBundle'] ) ) {
            $this->pageBundle = !empty( $options['pageBundle'] );
        }
        $this->pipelineFactory = new ParserPipelineFactory( $this );
        $this->inputContentVersion = self::AVAILABLE_VERSIONS[0];
        $this->outputContentVersion = self::AVAILABLE_VERSIONS[0];
        $this->htmlVariantLanguage = $options['htmlVariantLanguage'] ?? null;
        $this->wtVariantLanguage = $options['wtVariantLanguage'] ?? null;
        $this->noDataAccess = !empty( $options['noDataAccess'] );
        $this->nativeTemplateExpansion = !empty( $options['nativeTemplateExpansion'] );
        $this->discardDataParsoid = !empty( $options['discardDataParsoid'] );
        $this->requestOffsetType = $options['offsetType'] ?? 'byte';
        $this->traceFlags = $options['traceFlags'] ?? [];
        $this->dumpFlags = $options['dumpFlags'] ?? [];
        $this->debugFlags = $options['debugFlags'] ?? [];
        $this->parsoidLogger = new ParsoidLogger( $this->siteConfig->getLogger(), [
            'logLevels' => $options['logLevels'] ?? [ 'fatal', 'error', 'warn', 'info' ],
            'debugFlags' => $this->debugFlags,
            'dumpFlags' => $this->dumpFlags,
            'traceFlags' => $this->traceFlags
        ] );
    }
    /**
     * Get the site config
     * @return SiteConfig
     */
    public function getSiteConfig(): SiteConfig {
        return $this->siteConfig;
    }
    /**
     * Get the page config
     * @return PageConfig
     */
    public function getPageConfig(): PageConfig {
        return $this->pageConfig;
    }
    /**
     * Get the data access object
     * @return DataAccess
     */
    public function getDataAccess(): DataAccess {
        return $this->dataAccess;
    }
    public function noDataAccess(): bool {
        return $this->noDataAccess;
    }
    public function nativeTemplateExpansionEnabled(): bool {
        return $this->nativeTemplateExpansion;
    }
    /**
     * Get the current uid counter value
     * @return int
     */
    public function getUID(): int {
        return $this->uid;
    }
    /**
     * Get the current fragment id counter value
     * @return int
     */
    public function getFID(): int {
        return $this->fid;
    }
    /**
     * Whether `<section>` wrappers should be added.
     * @todo Does this actually belong here? Should it be a behavior switch?
     * @return bool
     */
    public function getWrapSections(): bool {
        return $this->wrapSections;
    }
    public function getPipelineFactory(): ParserPipelineFactory {
        return $this->pipelineFactory;
    }
    /**
     * Return the external format of character offsets in source ranges.
     * Internally we always keep DomSourceRange and SourceRange information
     * as UTF-8 byte offsets for efficiency (matches the native string
     * representation), but for external use we can convert these to
     * other formats when we output wt2html or input for html2wt.
     *
     * @see Parsoid\Wt2Html\PP\Processors\ConvertOffsets
     * @return string 'byte', 'ucs2', or 'char'
     */
    public function getRequestOffsetType(): string {
        return $this->requestOffsetType;
    }
    /**
     * Return the current format of character offsets in source ranges.
     * This allows us to track whether the internal byte offsets have
     * been converted to the external format (as returned by
     * `getRequestOffsetType`) yet.
     *
     * @see Parsoid\Wt2Html\PP\Processors\ConvertOffsets
     * @return string 'byte', 'ucs2', or 'char'
     */
    public function getCurrentOffsetType(): string {
        return $this->currentOffsetType;
    }
    /**
     * Update the current offset type. Only
     * Parsoid\Wt2Html\PP\Processors\ConvertOffsets should be doing this.
     * @param string $offsetType 'byte', 'ucs2', or 'char'
     */
    public function setCurrentOffsetType( string $offsetType ) {
        $this->currentOffsetType = $offsetType;
    }
    /**
     * Resolve strings that are page-fragments or subpage references with
     * respect to the current page name.
     *
     * TODO: Handle namespaces relative links like [[User:../../]] correctly, they
     * shouldn't be treated like links at all.
     *
     * @param string $str Page fragment or subpage reference. Not URL encoded.
     * @param bool $resolveOnly If true, only trim and add the current title to
     *  lone fragments. TODO: This parameter seems poorly named.
     * @return string Resolved title
     */
    public function resolveTitle( string $str, bool $resolveOnly = false ): string {
        $origName = $str;
        $str = trim( $str ); // PORT-FIXME: Care about non-ASCII whitespace?
        $pageConfig = $this->getPageConfig();
        // Resolve lonely fragments (important if the current page is a subpage,
        // otherwise the relative link will be wrong)
        if ( $str !== '' && $str[0] === '#' ) {
            $str = $pageConfig->getTitle() . $str;
        }
        // Default return value
        $titleKey = $str;
        if ( $this->getSiteConfig()->namespaceHasSubpages( $pageConfig->getNs() ) ) {
            // Resolve subpages
            $reNormalize = false;
            if ( preg_match( '!^(?:\.\./)+!', $str, $relUp ) ) {
                $levels = strlen( $relUp[0] ) / 3;  // Levels are indicated by '../'.
                $titleBits = explode( '/', $pageConfig->getTitle() );
                if ( count( $titleBits ) <= $levels ) {
                    // Too many levels -- invalid relative link
                    return $origName;
                }
                $newBits = array_slice( $titleBits, 0, -$levels );
                if ( $str !== $relUp[0] ) {
                    $newBits[] = substr( $str, $levels * 3 );
                }
                $str = implode( '/', $newBits );
                $reNormalize = true;
            } elseif ( $str !== '' && $str[0] === '/' ) {
                // Resolve absolute subpage links
                $str = $pageConfig->getTitle() . $str;
                $reNormalize = true;
            }
            if ( $reNormalize && !$resolveOnly ) {
                // Remove final slashes if present.
                // See https://gerrit.wikimedia.org/r/173431
                $str = rtrim( $str, '/' );
                $titleKey = (string)$this->normalizedTitleKey( $str );
            }
        }
        // Strip leading ':'
        if ( $titleKey !== '' && $titleKey[0] === ':' && !$resolveOnly ) {
            $titleKey = substr( $titleKey, 1 );
        }
        return $titleKey;
    }
    /**
     * Convert a Title to a string
     * @param Title $title
     * @param bool $ignoreFragment
     * @return string
     */
    private function titleToString( Title $title, bool $ignoreFragment = false ): string {
        $ret = $title->getPrefixedDBKey();
        if ( !$ignoreFragment ) {
            $fragment = $title->getFragment() ?? '';
            if ( $fragment !== '' ) {
                $ret .= '#' . $fragment;
            }
        }
        return $ret;
    }
    /**
     * Get normalized title key for a title string.
     *
     * @param string $str Should be in url-decoded format.
     * @param bool $noExceptions Return null instead of throwing exceptions.
     * @param bool $ignoreFragment Ignore the fragment, if any.
     * @return string|null Normalized title key for a title string (or null for invalid titles).
     */
    public function normalizedTitleKey(
        string $str, bool $noExceptions = false, bool $ignoreFragment = false
    ): ?string {
        $title = $this->makeTitleFromURLDecodedStr( $str, 0, $noExceptions );
        if ( !$title ) {
            return null;
        }
        return $this->titleToString( $title, $ignoreFragment );
    }
    /**
     * Normalize and resolve the page title
     * @deprecated Just use $this->getPageConfig()->getTitle() directly
     * @return string
     */
    public function normalizeAndResolvePageTitle(): string {
        return $this->getPageConfig()->getTitle();
    }
    /**
     * Create a Title object
     * @param string $text URL-decoded text
     * @param int|TitleNamespace $defaultNs
     * @param bool $noExceptions
     * @return Title|null
     */
    private function makeTitle( string $text, $defaultNs = 0, bool $noExceptions = false ): ?Title {
        try {
            if ( preg_match( '!^(?:[#/]|\.\./)!', $text ) ) {
                $defaultNs = $this->getPageConfig()->getNs();
            }
            $text = $this->resolveTitle( $text );
            return Title::newFromText( $text, $this->getSiteConfig(), $defaultNs );
        } catch ( TitleException $e ) {
            if ( $noExceptions ) {
                return null;
            }
            throw $e;
        }
    }
    /**
     * Create a Title object
     * @see Title::newFromURL in MediaWiki
     * @param string $str URL-encoded text
     * @param int|TitleNamespace $defaultNs
     * @param bool $noExceptions
     * @return Title|null
     */
    public function makeTitleFromText(
        string $str, $defaultNs = 0, bool $noExceptions = false
    ): ?Title {
        return $this->makeTitle( Util::decodeURIComponent( $str ), $defaultNs, $noExceptions );
    }
    /**
     * Create a Title object
     * @see Title::newFromText in MediaWiki
     * @param string $str URL-decoded text
     * @param int|TitleNamespace $defaultNs
     * @param bool $noExceptions
     * @return Title|null
     */
    public function makeTitleFromURLDecodedStr(
        string $str, $defaultNs = 0, bool $noExceptions = false
    ): ?Title {
        return $this->makeTitle( $str, $defaultNs, $noExceptions );
    }
    /**
     * Make a link to a Title
     * @param Title $title
     * @return string
     */
    public function makeLink( Title $title ): string {
        return Sanitizer::sanitizeTitleURI(
            $this->getSiteConfig()->relativeLinkPrefix() . $this->titleToString( $title ),
            false
        );
    }
    /**
     * Test if an href attribute value could be a valid link target
     * @param string|(Token|string)[] $href
     * @return bool
     */
    public function isValidLinkTarget( $href ): bool {
        $href = TokenUtils::tokensToString( $href );
        // decode percent-encoding so that we can reliably detect
        // bad page title characters
        $hrefToken = Util::decodeURIComponent( $href );
        return $this->normalizedTitleKey( $this->resolveTitle( $hrefToken, true ), true ) !== null;
    }
    /**
     * Generate a new uid
     * @return int
     */
    public function generateUID(): int {
        return $this->uid++;
    }
    /**
     * Generate a new object id
     * @return string
     */
    public function newObjectId(): string {
        return "mwt" . $this->generateUID();
    }
    /**
     * Generate a new about id
     * @return string
     */
    public function newAboutId(): string {
        return "#" . $this->newObjectId();
    }
    /**
     * Store reference to original DOM (body)
     * @param DOMElement $domBody
     */
    public function setOrigDOM( DOMElement $domBody ): void {
        $this->origDOM = $domBody;
    }
    /**
     * Return reference to original DOM (body)
     * @return DOMElement
     */
    public function getOrigDOM(): DOMElement {
        return $this->origDOM;
    }
    /**
     * Store reference to DOM diff document
     * @param DOMDocument $doc
     */
    public function setDOMDiff( $doc ): void {
        $this->domDiff = $doc;
    }
    /**
     * Return reference to DOM diff document
     * @return DOMDocument|null
     */
    public function getDOMDiff(): ?DOMDocument {
        return $this->domDiff;
    }
    /**
     * Generate a new fragment id
     * @return string
     */
    public function newFragmentId(): string {
        return "mwf" . (string)$this->fid++;
    }
    /**
     * FIXME: This function could be given a better name to reflect what it does.
     *
     * @param DOMDocument $doc
     * @param DataBag|null $bag
     */
    public function referenceDataObject( DOMDocument $doc, ?DataBag $bag = null ): void {
        // `bag` is a deliberate dynamic property; see DOMDataUtils::getBag()
        // @phan-suppress-next-line PhanUndeclaredProperty dynamic property
        $doc->bag = $bag ?? new DataBag();
        // Prevent GC from collecting the PHP wrapper around the libxml doc
        $this->liveDocs[] = $doc;
    }
    /**
     * @param string $html
     * @return DOMDocument
     */
    public function createDocument( string $html = '' ): DOMDocument {
        $doc = DOMUtils::parseHTML( $html );
        // Cache the head and body.
        DOMCompat::getHead( $doc );
        DOMCompat::getBody( $doc );
        $this->referenceDataObject( $doc );
        return $doc;
    }
    /**
     * BehaviorSwitchHandler support function that adds a property named by
     * $variable and sets it to $state
     *
     * @deprecated Use setBehaviorSwitch() instead.
     * @param string $variable
     * @param mixed $state
     */
    public function setVariable( string $variable, $state ): void {
        $this->setBehaviorSwitch( $variable, $state );
    }
    /**
     * Record a behavior switch.
     *
     * @todo Does this belong here, or on some equivalent to MediaWiki's ParserOutput?
     * @param string $switch Switch name
     * @param mixed $state Relevant state data to record
     */
    public function setBehaviorSwitch( string $switch, $state ): void {
        $this->behaviorSwitches[$switch] = $state;
    }
    /**
     * Fetch the state of a previously-recorded behavior switch.
     *
     * @todo Does this belong here, or on some equivalent to MediaWiki's ParserOutput?
     * @param string $switch Switch name
     * @param mixed|null $default Default value if the switch was never set
     * @return mixed State data that was previously passed to setBehaviorSwitch(), or $default
     */
    public function getBehaviorSwitch( string $switch, $default = null ) {
        return $this->behaviorSwitches[$switch] ?? $default;
    }
    /**
     * FIXME: Once we remove the hardcoded slot name here,
     * the name of this method could be updated, if necessary.
     *
     * Shortcut method to get page source
     * @deprecated Use $this->topFrame->getSrcText()
     * @return string
     */
    public function getPageMainContent(): string {
        return $this->pageConfig->getRevisionContent()->getContent( 'main' );
    }
    /**
     * @return array<string,DOMNode[]>
     */
    public function getFragmentMap(): array {
        return $this->fragmentMap;
    }
    /**
     * @param string $id Fragment id
     * @return DOMNode[]
     */
    public function getFragment( string $id ): array {
        return $this->fragmentMap[$id];
    }
    /**
     * @param string $id Fragment id
     * @param DOMNode[] $forest DOM forest (contiguous array of DOM trees)
     *   to store against the fragment id
     */
    public function setFragment( string $id, array $forest ): void {
        $this->fragmentMap[$id] = $forest;
    }
    /**
     * Record a lint
     * @param string $type Lint type key
     * @param array $lintData Data for the lint.
     */
    public function recordLint( string $type, array $lintData ): void {
        // Parsoid-JS tests don't like getting null properties where JS had undefined.
        $lintData = array_filter( $lintData, function ( $v ) {
            return $v !== null;
        } );
        if ( empty( $lintData['dsr'] ) ) {
            $this->log( 'error/lint', "Missing DSR; msg=", $lintData );
            return;
        }
        // This will always be recorded as a native 'byte' offset
        $lintData['dsr'] = $lintData['dsr']->jsonSerialize();
        $this->lints[] = [ 'type' => $type ] + $lintData;
    }
    /**
     * Retrieve recorded lints
     * @return array[]
     */
    public function getLints(): array {
        return $this->lints;
    }
    /**
     * @param mixed ...$args
     */
    public function log( ...$args ): void {
        $this->parsoidLogger->log( ...$args );
    }
    /**
     * Update a profile timer.
     *
     * @param string $resource
     * @param mixed $time
     * @param mixed $cat
     */
    public function bumpTimeUse( string $resource, $time, $cat ): void {
        throw new \BadMethodCallException( 'not yet ported' );
    }
    /**
     * Update a profile counter.
     *
     * @param string $resource
     * @param int $n The amount to increment the counter; defaults to 1.
     */
    public function bumpCount( string $resource, int $n = 1 ): void {
        throw new \BadMethodCallException( 'not yet ported' );
    }
    /**
     * Bump usage of some limited parser resource
     * (ex: tokens, # transclusions, # list items, etc.)
     *
     * @param string $resource
     * @param int $count How much of the resource is used?
     * @throws ResourceLimitExceededException
     */
    public function bumpWt2HtmlResourceUse( string $resource, int $count = 1 ): void {
        $n = $this->wt2htmlUsage[$resource] ?? 0;
        $n += $count;
        $this->wt2htmlUsage[$resource] = $n;
        $wt2htmlLimits = $this->siteConfig->getWt2HtmlLimits();
        if (
            isset( $wt2htmlLimits[$resource] ) &&
            $n > $wt2htmlLimits[$resource]
        ) {
            // TODO: re-evaluate whether throwing an exception is really
            // the right failure strategy when Parsoid is integrated into MW
            // (T221238)
            throw new ResourceLimitExceededException( "wt2html: $resource limit exceeded: $n" );
        }
    }
    /**
     * Bump usage of some limited serializer resource
     * (ex: html size)
     *
     * @param string $resource
     * @param int $count How much of the resource is used? (defaults to 1)
     * @throws ResourceLimitExceededException
     */
    public function bumpHtml2WtResourceUse( string $resource, int $count = 1 ): void {
        $n = $this->html2wtUsage[$resource] ?? 0;
        $n += $count;
        $this->html2wtUsage[$resource] = $n;
        $html2wtLimits = $this->siteConfig->getHtml2WtLimits();
        if (
            isset( $html2wtLimits[$resource] ) &&
            $n > $html2wtLimits[$resource]
        ) {
            throw new ResourceLimitExceededException( "html2wt: $resource limit exceeded: $n" );
        }
    }
    /**
     * Get an appropriate content handler, given a contentmodel.
     *
     * @param string|null $forceContentModel An optional content model which
     *   will override whatever the source specifies.
     * @return ContentModelHandler An appropriate content handler
     */
    public function getContentHandler(
        ?string $forceContentModel = null
    ): ContentModelHandler {
        $contentmodel = $forceContentModel ?? $this->pageConfig->getContentModel();
        $handler = $this->siteConfig->getContentModelHandler( $contentmodel );
        if ( !$handler ) {
            $this->log( 'warn', "Unknown contentmodel $contentmodel" );
            $handler = $this->siteConfig->getContentModelHandler( 'wikitext' );
        }
        return $handler;
    }
    /**
     * Is the language converter enabled on this page?
     * @return bool
     */
    public function langConverterEnabled(): bool {
        $lang = $this->pageConfig->getPageLanguage();
        if ( !$lang ) {
            $lang = $this->siteConfig->lang();
        }
        if ( !$lang ) {
            $lang = 'en';
        }
        return $this->siteConfig->langConverterEnabled( $lang );
    }
    /**
     * Indicates emit "clean" wikitext compared to what we would if we didn't normalize HTML
     * @return bool
     */
    public function shouldScrubWikitext(): bool {
        return $this->scrubWikitext;
    }
    /**
     * The HTML content version of the input document (for html2wt and html2html conversions).
     * @see https://www.mediawiki.org/wiki/Parsoid/API#Content_Negotiation
     * @see https://www.mediawiki.org/wiki/Specs/HTML/2.1.0#Versioning
     * @return string A semver version number
     */
    public function getInputContentVersion(): string {
        return $this->inputContentVersion;
    }
    public function setInputContentVersion( string $version ) {
        $this->inputContentVersion = $version;
    }
    /**
     * The HTML content version of the input document (for html2wt and html2html conversions).
     * @see https://www.mediawiki.org/wiki/Parsoid/API#Content_Negotiation
     * @see https://www.mediawiki.org/wiki/Specs/HTML/2.1.0#Versioning
     * @return string A semver version number
     */
    public function getOutputContentVersion(): string {
        return $this->outputContentVersion;
    }
    public function setOutputContentVersion( string $version ): void {
        if ( !in_array( $version, self::AVAILABLE_VERSIONS, true ) ) {
            throw new UnexpectedValueException( 'Not an available content version.' );
        }
        $this->outputContentVersion = $version;
    }
    /**
     * See if any content version Parsoid knows how to produce satisfies the
     * the supplied version, when interpreted with semver caret semantics.
     * This will allow us to make backwards compatible changes, without the need
     * for clients to bump the version in their headers all the time.
     *
     * @param string $version
     * @return string|null
     */
    public function resolveContentVersion( $version ) {
        foreach ( self::AVAILABLE_VERSIONS as $i => $a ) {
            if ( Semver::satisfies( $a, "^{$version}" ) &&
                // The section wrapping in 1.6.x should have induced a major
                // version bump, since it requires upgrading clients to
                // handle it.  We therefore hardcode this in so that we can
                // fail hard.
                Comparator::greaterThanOrEqualTo( $version, '1.6.0' )
            ) {
                return $a;
            }
        }
        return null;
    }
    /**
     * If non-null, the language variant used for Parsoid HTML; we convert
     * to this if wt2html, or from this (if html2wt).
     *
     * @return string|null
     */
    public function getHtmlVariantLanguage(): ?string {
        return $this->htmlVariantLanguage;
    }
    /**
     * If non-null, the language variant to be used for wikitext.  If null,
     * heuristics will be used to identify the original wikitext variant
     * in wt2html mode, and in html2wt mode new or edited HTML will be left
     * unconverted.
     *
     * @return string|null
     */
    public function getWtVariantLanguage(): ?string {
        return $this->wtVariantLanguage;
    }
    /**
     * Update K=[V1,V2,...] that might need to be output as part of the
     * generated HTML.  Ex: module styles, modules scripts, ...
     *
     * @param string $key
     * @param array $value
     */
    public function addOutputProperty( string $key, array $value ): void {
        if ( !isset( $this->outputProps[$key] ) ) {
            $this->outputProps[$key] = [];
        }
        $this->outputProps[$key] = array_merge( $this->outputProps[$key], $value );
    }
    /**
     * @return array
     */
    public function getOutputProperties(): array {
        return $this->outputProps;
    }
    /**
     * Determine appropriate vary headers for the HTML form of this page.
     * @return string
     */
    public function htmlVary(): string {
        $varies = [ 'Accept' ]; // varies on Content-Type
        if ( $this->langConverterEnabled() ) {
            $varies[] = 'Accept-Language';
        }
        sort( $varies );
        return implode( ', ', $varies );
    }
    /**
     * Determine an appropriate content-language for the HTML form of this page.
     * @return string
     */
    public function htmlContentLanguage(): string {
        // PageConfig::htmlVariant is set iff we do variant conversion on the
        // HTML
        return $this->pageConfig->getVariant() ??
            $this->pageConfig->getPageLanguage();
    }
}