Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.18% covered (warning)
82.18%
853 / 1038
70.42% covered (warning)
70.42%
100 / 142
CRAP
0.00% covered (danger)
0.00%
0 / 1
ParserOutput
82.26% covered (warning)
82.26%
853 / 1037
70.42% covered (warning)
70.42%
100 / 142
1371.45
0.00% covered (danger)
0.00%
0 / 1
 __construct
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 getContentHolder
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setContentHolder
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasText
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 runOutputPipeline
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
2
 addCacheMessage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addWrapperDivClass
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 clearWrapperDivClass
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWrapperDivClass
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setSpeculativeRevIdUsed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSpeculativeRevIdUsed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setSpeculativePageIdUsed
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSpeculativePageIdUsed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setRevisionTimestampUsed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRevisionTimestampUsed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setRevisionUsedSha1Base36
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
4.59
 getRevisionUsedSha1Base36
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLanguageLinksInternal
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getCategoryNames
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCategoryMap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCategorySortKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIndicators
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getTitleText
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTOCData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCacheMessage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSections
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getLinkList
86.42% covered (warning)
86.42%
70 / 81
0.00% covered (danger)
0.00%
0 / 1
39.25
 appendLinkList
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
2
 hasLinks
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 hasImages
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExternalLinks
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setNoGallery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNoGallery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHeadItems
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getModules
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getModuleStyles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getJsConfigVars
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 getWarnings
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWarningMsgs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIndexPolicy
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getRevisionTimestamp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLimitReportData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLimitReportJSData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEnableOOUI
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExtraCSPDefaultSrcs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExtraCSPScriptSrcs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExtraCSPStyleSrcs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 clearLanguageLinks
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setTitleText
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTOCData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setSections
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setIndexPolicy
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 setRevisionTimestamp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addCategory
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 setCategories
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 setIndicator
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 setIndicatorDom
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 setEnableOOUI
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addLanguageLink
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
5.02
 addWarningMsgVal
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
3.00
 addWarningMsg
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setNewSection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setHideNewSection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHideNewSection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNewSection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isLinkInternal
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 addExternalLink
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
4.01
 addLink
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
6
 addImage
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 addTemplate
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 addInterwikiLink
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 addExistenceDependency
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 addHeadItem
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 addModules
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addModuleStyles
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addJsConfigVars
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 setJsConfigVar
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 appendJsConfigVar
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 addOutputPageMetadata
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 setDisplayTitle
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getDisplayTitle
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getTitle
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 setTitle
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 getLanguage
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 setLanguage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRedirectHeader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setRedirectHeader
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setRenderId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRenderId
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getAllFlags
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setPageProperty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setNumericPageProperty
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 setUnsortedPageProperty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPageProperty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 unsetPageProperty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPageProperties
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setOutputFlag
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 getOutputFlag
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 appendOutputStrings
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
2
 getOutputStrings
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 setExtensionData
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 appendExtensionData
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 getExtensionData
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getTimes
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 resetParseStartTime
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 clearParseStartTime
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 recordTimeProfile
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 getTimeProfile
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setLimitReportData
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 hasReducedExpiry
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getCacheExpiry
44.12% covered (danger)
44.12%
15 / 34
0.00% covered (danger)
0.00%
0 / 1
12.28
 setPreventClickjacking
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPreventClickjacking
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 updateRuntimeAdaptiveExpiry
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 addExtraCSPDefaultSrc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addExtraCSPStyleSrc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addExtraCSPScriptSrc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 finalizeAdaptiveCacheExpiry
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 setFromParserOptions
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
6.04
 mergeInternalMetaDataFrom
69.77% covered (warning)
69.77%
30 / 43
0.00% covered (danger)
0.00%
0 / 1
24.99
 mergeHtmlMetaDataFrom
96.15% covered (success)
96.15%
50 / 52
0.00% covered (danger)
0.00%
0 / 1
11
 mergeTrackingMetaDataFrom
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 collectMetadata
70.15% covered (warning)
70.15%
94 / 134
0.00% covered (danger)
0.00%
0 / 1
120.18
 mergeMixedList
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 mergeList
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 mergeMap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 makeMapStrategy
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 collectMapStrategy
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 mergeMapStrategy
84.62% covered (warning)
84.62%
22 / 26
0.00% covered (danger)
0.00%
0 / 1
14.71
 useEachMinValue
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 useEachTotalValue
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 useMaxValue
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 toJsonArray
97.78% covered (success)
97.78%
44 / 45
0.00% covered (danger)
0.00%
0 / 1
3
 newFromJsonArray
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 jsonClassHintFor
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 initFromJson
84.54% covered (warning)
84.54%
82 / 97
0.00% covered (danger)
0.00%
0 / 1
28.50
 detectAndEncodeBinary
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 detectAndDecodeBinary
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 __clone
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getContentHolderText
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setContentHolderText
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 normalizeForObjectEquality
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare( strict_types = 1 );
3
4/**
5 * @license GPL-2.0-or-later
6 * @file
7 */
8
9namespace MediaWiki\Parser;
10
11use DateTimeImmutable;
12use DateTimeZone;
13use InvalidArgumentException;
14use LogicException;
15use MediaWiki\DAO\WikiAwareEntity;
16use MediaWiki\Edit\ParsoidRenderID;
17use MediaWiki\Json\JsonDeserializable;
18use MediaWiki\MainConfigNames;
19use MediaWiki\MediaWikiServices;
20use MediaWiki\Message\Message;
21use MediaWiki\Output\OutputPage;
22use MediaWiki\Page\PageReference;
23use MediaWiki\Title\TitleValue;
24use MediaWiki\Utils\MWTimestamp;
25use UnhandledMatchError;
26use Wikimedia\Assert\Assert;
27use Wikimedia\Bcp47Code\Bcp47Code;
28use Wikimedia\Bcp47Code\Bcp47CodeValue;
29use Wikimedia\JsonCodec\Hint;
30use Wikimedia\Message\MessageSpecifier;
31use Wikimedia\Message\MessageValue;
32use Wikimedia\Parsoid\Core\ContentMetadataCollector;
33use Wikimedia\Parsoid\Core\ContentMetadataCollectorCompat;
34use Wikimedia\Parsoid\Core\HtmlPageBundle;
35use Wikimedia\Parsoid\Core\LinkTarget as ParsoidLinkTarget;
36use Wikimedia\Parsoid\Core\MergeStrategy;
37use Wikimedia\Parsoid\Core\TOCData;
38use Wikimedia\Parsoid\DOM\DocumentFragment;
39
40/**
41 * ParserOutput is a rendering of a Content object or a message.
42 * Content objects and messages often contain wikitext, but not always.
43 *
44 * `ParserOutput` object combine the HTML rendering of Content objects
45 * or messages, available via `::getContentHolderText()`, with various bits of
46 * metadata generated during rendering, which may include categories,
47 * links, page properties, and extension data, among others.
48 *
49 * `ParserOutput` objects corresponding to the content of page revisions
50 * are created by the `ParserOutputAccess` service, which
51 * automatically caches them via `ParserCache` where appropriate and
52 * produces new output via `ContentHandler` as needed.
53 *
54 * In addition, wikitext from system messages as well as odd bits of
55 * wikitext rendered to create special pages and other UX elements are
56 * rendered to `ParserOutput` objects.  In these cases the metadata
57 * from the `ParserOutput` is generally discarded and the
58 * `ParserOutput` is not cached.  `ParserOptions::setIsMessage(true)`
59 * is usually used when rendering system messages.
60 * `ParserOptions::setInterfaceMessage(true)` is usually used when
61 * rendering system messages in the user interface language,
62 * and occasionally for the other odd bits of wikitext as well.
63 * These options are not used as consistently as one would hope.
64 *
65 * A `ParserOutput` object corresponding to a given revision may be a
66 * combination of the renderings of multiple "slots":
67 * the Multi-Content Revisions (MCR) work allows articles to be
68 * composed from multiple `Content` objects.  Each `Content` renders
69 * to a `ParserOutput`, and those `ParserOutput`s are merged by
70 * `RevisionRenderer::combineSlotOutput()` to create the final article
71 * output.
72 *
73 * Similarly, `OutputPage` maintains metadata overlapping
74 * with the metadata kept by `ParserOutput` (T301020) and may merge
75 * several `ParserOutput`s using `OutputPage::addParserOutput()` to
76 * create the final output page.  Parsoid parses certain transclusions
77 * in independent top-level contexts using
78 * `Parser::parseExtensionTagAsTopLevelDoc()` and these also result in
79 * `ParserOutput`s which are merged via
80 * `ParserOutput::collectMetadata()`.
81 *
82 * Future plans for incremental parsing and asynchronous rendering may
83 * result in several of these component `ParserOutput` objects being
84 * cached independently and then recombined asynchronously, so
85 * operations on `ParserOutput` objects should be compatible with that
86 * model (T300979).
87 *
88 * @ingroup Parser
89 */
90class ParserOutput extends CacheTime implements ContentMetadataCollector {
91    // This is used to break cyclic dependencies and allow a measure
92    // of compatibility when new methods are added to ContentMetadataCollector
93    // by Parsoid.
94    use ContentMetadataCollectorCompat;
95
96    /**
97     * @internal
98     * @since 1.45
99     */
100    public const PARSOID_PAGE_BUNDLE_KEY = 'parsoid-page-bundle';
101
102    /**
103     * @internal
104     * @since 1.38
105     */
106    public const MW_MERGE_STRATEGY_KEY = '_mw-strategy';
107
108    /**
109     * Merge strategy to use for ParserOutput accumulators: "union"
110     * means that values are strings, stored as a set, and exposed as
111     * a PHP associative array mapping from values to `true`.
112     *
113     * This constant should be treated as @internal until we expose
114     * alternative merge strategies for external use.
115     * @internal
116     * @since 1.38
117     * @deprecated since 1.45; use MergeStrategy::UNION
118     */
119    public const MW_MERGE_STRATEGY_UNION = MergeStrategy::UNION;
120
121    private ContentHolder $contentHolder;
122
123    /**
124     * @var array<string,string> Array mapping interwiki prefix to (non DB key) Titles (e.g. 'fr' => 'Test page')
125     */
126    private array $mLanguageLinkMap = [];
127
128    /**
129     * @var array<string,string> Map of category names to sort keys
130     */
131    private array $mCategories = [];
132
133    /**
134     * @var array<string> Page status indicators, usually displayed in top-right corner.
135     * This is a list of indicator IDs; the actual content is stored in
136     * the ContentHolder.
137     */
138    private array $mIndicatorIds = [];
139
140    /**
141     * @var string Title text of the chosen language variant, as HTML.
142     */
143    private string $mTitleText;
144
145    /**
146     * @var array<int,array<string,int>> 2-D map of NS/DBK to ID for the links in the document.
147     *  ID=zero for broken.
148     */
149    private array $mLinks = [];
150
151    /**
152     * @var array<string,int> Keys are DBKs for the links to special pages in the document.
153     * @since 1.35
154     */
155    private array $mLinksSpecial = [];
156
157    /**
158     * @var array<int,array<string,int>> 2-D map of NS/DBK to ID for the template references.
159     *  ID=zero for broken.
160     */
161    private array $mTemplates = [];
162
163    /**
164     * @var array<int,array<string,int>> 2-D map of NS/DBK to rev ID for the template references.
165     *  ID=zero for broken.
166     */
167    private array $mTemplateIds = [];
168
169    /**
170     * @var array<string,int> DB keys of the images used, in the array key only
171     */
172    private array $mImages = [];
173
174    /**
175     * @var array<string,array<string,string>> DB keys of the images used mapped to sha1 and MW timestamp.
176     */
177    private array $mFileSearchOptions = [];
178
179    /**
180     * @var array<string,int> External link URLs, in the key only.
181     */
182    private array $mExternalLinks = [];
183
184    /**
185     * @var array<string,array<string,int>> 2-D map of prefix/DBK (in keys only)
186     *  for the inline interwiki links in the document.
187     */
188    private array $mInterwikiLinks = [];
189
190    /**
191     * @var array<int,array<string,bool>> 2-D map of NS/DBK to true for #ifexist and similar
192     */
193    private array $existenceLinks = [];
194
195    /**
196     * @var array<string|int,string> Items to put in the <head> section
197     */
198    private array $mHeadItems = [];
199
200    /**
201     * @var array<string,true> Modules to be loaded by ResourceLoader
202     */
203    private array $mModuleSet = [];
204
205    /**
206     * @var array<string,true> Modules of which only the CSS will be loaded by ResourceLoader.
207     */
208    private array $mModuleStyleSet = [];
209
210    /**
211     * @var array JavaScript config variable for mw.config combined with this page.
212     */
213    private array $mJsConfigVars = [];
214
215    /**
216     * @var array<string,int> Warning text to be returned to the user.
217     *  Wikitext formatted, in the key only.
218     * @deprecated since 1.45; use ::$mWarningMsgs instead
219     */
220    private array $mWarnings = [];
221
222    /**
223     * @var array<string,MessageValue> *Unformatted* warning messages and
224     * arguments to be returned to the user.
225     */
226    private array $mWarningMsgs = [];
227
228    /**
229     * @var ?TOCData Table of contents data, or null if it hasn't been set.
230     */
231    private ?TOCData $mTOCData = null;
232
233    /**
234     * @var array<string,int|float|string> Name/value pairs to be cached in the DB.
235     */
236    private array $mProperties = [];
237
238    /**
239     * @var ?string Timestamp of the revision.
240     */
241    private ?string $mTimestamp = null;
242
243    /**
244     * @var array<string,mixed> extra data used by extensions.
245     */
246    private array $mExtensionData = [];
247
248    /**
249     * @var array Parser limit report data.
250     */
251    private array $mLimitReportData = [];
252
253    /** @var array Parser limit report data for JSON */
254    private array $mLimitReportJSData = [];
255
256    /** @var string Debug message added by ParserCache */
257    private string $mCacheMessage = '';
258
259    /**
260     * @var array Timestamps for getTimeProfile().
261     */
262    private array $mParseStartTime = [];
263
264    /**
265     * @var array Durations for getTimeProfile().
266     */
267    private array $mTimeProfile = [];
268
269    /**
270     * @var list<string> Extra script-src for CSP
271     */
272    private array $mExtraScriptSrcs = [];
273
274    /**
275     * @var list<string> Extra default-src for CSP [Everything but script and style]
276     */
277    private array $mExtraDefaultSrcs = [];
278
279    /**
280     * @var list<string> Extra style-src for CSP
281     */
282    private array $mExtraStyleSrcs = [];
283
284    /**
285     * @var array<string,true> Generic flags.
286     */
287    private $mFlags = [];
288
289    private const SPECULATIVE_FIELDS = [
290        'speculativePageIdUsed',
291        'mSpeculativeRevId',
292        'revisionTimestampUsed',
293    ];
294
295    /** @var int|null Assumed rev ID for {{REVISIONID}} if no revision is set */
296    private ?int $mSpeculativeRevId = null;
297    /** @var int|null Assumed page ID for {{PAGEID}} if no revision is set */
298    private ?int $speculativePageIdUsed = null;
299    /** @var string|null Assumed rev timestamp for {{REVISIONTIMESTAMP}} if no revision is set */
300    private ?string $revisionTimestampUsed = null;
301
302    /** @var string|null SHA-1 base 36 hash of any self-transclusion */
303    private ?string $revisionUsedSha1Base36 = null;
304
305    /** string CSS classes to use for the wrapping div, stored in the array keys.
306     * If no class is given, no wrapper is added.
307     * @var array<string,true>
308     */
309    private array $mWrapperDivClasses = [];
310
311    /**
312     * @var ?int Upper bound of expiry based on parse duration;
313     *    null means "infinite" or "not set"
314     */
315    private ?int $mMaxAdaptiveExpiry = null;
316
317    // finalizeAdaptiveCacheExpiry() uses TTL = MAX( m * PARSE_TIME + b, MIN_AR_TTL)
318    // Current values imply that m=3933.333333 and b=-333.333333
319    // See https://www.nngroup.com/articles/website-response-times/
320    private const PARSE_FAST_SEC = 0.100; // perceived "fast" page parse
321    private const PARSE_SLOW_SEC = 1.0; // perceived "slow" page parse
322    private const FAST_AR_TTL = 60; // adaptive TTL for "fast" pages
323    private const SLOW_AR_TTL = 3600; // adaptive TTL for "slow" pages
324    private const MIN_AR_TTL = 15; // min adaptive TTL (for pool counter, and edit stashing)
325
326    /**
327     * @param string|null $text HTML. Use null to indicate that this ParserOutput contains only
328     *        meta-data, and the HTML output is undetermined, as opposed to empty. Passing null
329     *        here causes hasText() to return false. In 1.39 the default value changed from ''
330     *        to null.
331     * @param array $languageLinks
332     * @param array $categoryLinks
333     * @param bool $unused
334     * @param string $titletext
335     */
336    public function __construct( ?string $text = null, array $languageLinks = [], array $categoryLinks = [],
337        $unused = false, string $titletext = ''
338    ) {
339        if ( $text === null ) {
340            $this->contentHolder = ContentHolder::createEmpty();
341        } else {
342            $this->contentHolder = ContentHolder::createFromLegacyString( $text );
343        }
344        $this->mCategories = $categoryLinks;
345        $this->mTitleText = $titletext;
346        foreach ( $languageLinks as $ll ) {
347            $this->addLanguageLink( $ll );
348        }
349        // If the content handler does not specify an alternative (by
350        // calling ::resetParseStartTime() at a later point) then use
351        // the creation of the ParserOutput as the "start of parse" time.
352        $this->resetParseStartTime();
353    }
354
355    /**
356     * Return the ContentHolder storing the HTML/DOM contents of this
357     * ParserOutput.
358     * @unstable
359     * @since 1.45
360     */
361    public function getContentHolder(): ContentHolder {
362        return $this->contentHolder;
363    }
364
365    /**
366     * @internal Use __construct or PageBundleParserOutputConverter.
367     * @since 1.45
368     */
369    public function setContentHolder( ContentHolder $contentHolder ) {
370        $this->contentHolder = $contentHolder;
371    }
372
373    /**
374     * Returns true if text was passed to the constructor, or set using setText(). Returns false
375     * if null was passed to the $text parameter of the constructor to indicate that this
376     * ParserOutput only contains meta-data, and the HTML output is undetermined.
377     *
378     * @since 1.32
379     *
380     * @return bool Whether this ParserOutput contains rendered text. If this returns false, the
381     *         ParserOutput contains meta-data only.
382     */
383    public function hasText(): bool {
384        return $this->contentHolder->has( ContentHolder::BODY_FRAGMENT );
385    }
386
387    /*
388     * @unstable This method is transitional and will be replaced by a method
389     * in another class, maybe ContentRenderer.  It allows us to break our
390     * porting work into two steps; in the first we bring ParserOptions to
391     * to each callsite to ensure it is made available to the
392     * postprocessing pipeline.  In the second we move this functionality
393     * into the Content hierarchy and out of ParserOutput, which should become
394     * a pure value object.
395     *
396     * @param ParserOptions $popts
397     * @param array $options (since 1.31) Transformations to apply to the HTML
398     *      - allowClone: (bool) Whether to clone the ParserOutput before
399     *     applying transformations. Default is true.
400     *  - allowTOC: (bool) Show the TOC, assuming there were enough headings
401     *     to generate one and `__NOTOC__` wasn't used. Default is true,
402     *     but might be statefully overridden.
403     *  - injectTOC: (bool) Replace the TOC_PLACEHOLDER with TOC contents;
404     *     otherwise the marker will be left in the article (and the skin
405     *     will be responsible for replacing or removing it).  Default is
406     *     true.
407     *  - enableSectionEditLinks: (bool) Include section edit links, assuming
408     *     section edit link tokens are present in the HTML. Default is true,
409     *     but might be statefully overridden.
410     *  - userLang: (Language) Language object used for localizing UX messages,
411     *    for example the heading of the table of contents. If omitted, will
412     *    use the language of the main request context.
413     *  - skin: (Skin) Skin object used for transforming section edit links.
414     *  - unwrap: (bool) Return text without a wrapper div. Default is false,
415     *    meaning a wrapper div will be added if getWrapperDivClass() returns
416     *    a non-empty string.
417     *  - wrapperDivClass: (string) Wrap the output in a div and apply the given
418     *    CSS class to that div. This overrides the output of getWrapperDivClass().
419     *    Setting this to an empty string has the same effect as 'unwrap' => true.
420     *  - deduplicateStyles: (bool) When true, which is the default, `<style>`
421     *    tags with the `data-mw-deduplicate` attribute set are deduplicated by
422     *    value of the attribute: all but the first will be replaced by `<link
423     *    rel="mw-deduplicated-inline-style" href="mw-data:..."/>` tags, where
424     *    the scheme-specific-part of the href is the (percent-encoded) value
425     *    of the `data-mw-deduplicate` attribute.
426     *  - absoluteURLs: (bool) use absolute URLs in all links. Default: false
427     *  - includeDebugInfo: (bool) render PP limit report in HTML. Default: false
428     *  It is planned to eventually deprecate this $options array and to be able to
429     *  pass its content in the $popts ParserOptions.
430     * @return ParserOutput
431     */
432    public function runOutputPipeline( ParserOptions $popts, array $options = [] ): ParserOutput {
433        $pipeline = MediaWikiServices::getInstance()->getDefaultOutputPipeline();
434        $options += [
435            'allowClone' => true,
436            'allowTOC' => true,
437            'injectTOC' => true,
438            'enableSectionEditLinks' => true,
439            'userLang' => null,
440            'skin' => null,
441            'unwrap' => false,
442            'wrapperDivClass' => $this->getWrapperDivClass(),
443            'deduplicateStyles' => true,
444            'absoluteURLs' => false,
445            'includeDebugInfo' => false,
446        ];
447        return $pipeline->run( $this, $popts, $options );
448    }
449
450    /**
451     * Adds a comment notice about cache state to the text of the page
452     * @param string $msg
453     * @internal used by ParserCache
454     */
455    public function addCacheMessage( string $msg ): void {
456        $this->mCacheMessage .= $msg;
457    }
458
459    /**
460     * Add a CSS class to use for the wrapping div. If no class is given, no wrapper is added.
461     *
462     * @param string $class
463     */
464    public function addWrapperDivClass( $class ): void {
465        $this->mWrapperDivClasses[$class] = true;
466    }
467
468    /**
469     * Clears the CSS class to use for the wrapping div, effectively disabling the wrapper div
470     * until addWrapperDivClass() is called.
471     */
472    public function clearWrapperDivClass(): void {
473        $this->mWrapperDivClasses = [];
474    }
475
476    /**
477     * Returns the class (or classes) to be used with the wrapper div for this output.
478     * If there is no wrapper class given, no wrapper div should be added.
479     * The wrapper div is added automatically by getText().
480     */
481    public function getWrapperDivClass(): string {
482        return implode( ' ', array_keys( $this->mWrapperDivClasses ) );
483    }
484
485    /**
486     * @param int $id
487     * @since 1.28
488     */
489    public function setSpeculativeRevIdUsed( $id ): void {
490        $this->mSpeculativeRevId = $id;
491    }
492
493    /**
494     * @return int|null
495     * @since 1.28
496     */
497    public function getSpeculativeRevIdUsed(): ?int {
498        return $this->mSpeculativeRevId;
499    }
500
501    /**
502     * @param int $id
503     * @since 1.34
504     */
505    public function setSpeculativePageIdUsed( $id ): void {
506        $this->speculativePageIdUsed = $id;
507    }
508
509    /**
510     * @return int|null
511     * @since 1.34
512     */
513    public function getSpeculativePageIdUsed() {
514        return $this->speculativePageIdUsed;
515    }
516
517    /**
518     * @param string $timestamp TS::MW timestamp
519     * @since 1.34
520     */
521    public function setRevisionTimestampUsed( $timestamp ): void {
522        $this->revisionTimestampUsed = $timestamp;
523    }
524
525    /**
526     * @return string|null TS::MW timestamp or null if not used
527     * @since 1.34
528     */
529    public function getRevisionTimestampUsed() {
530        return $this->revisionTimestampUsed;
531    }
532
533    /**
534     * @param string $hash Lowercase SHA-1 base 36 hash
535     * @since 1.34
536     */
537    public function setRevisionUsedSha1Base36( $hash ): void {
538        if ( $hash === null ) {
539            return; // e.g. RevisionRecord::getSha1() returned null
540        }
541
542        if (
543            $this->revisionUsedSha1Base36 !== null &&
544            $this->revisionUsedSha1Base36 !== $hash
545        ) {
546            $this->revisionUsedSha1Base36 = ''; // mismatched
547        } else {
548            $this->revisionUsedSha1Base36 = $hash;
549        }
550    }
551
552    /**
553     * @return string|null Lowercase SHA-1 base 36 hash, null if unused, or "" on inconsistency
554     * @since 1.34
555     */
556    public function getRevisionUsedSha1Base36() {
557        return $this->revisionUsedSha1Base36;
558    }
559
560    /**
561     * @return list<string>
562     */
563    private function getLanguageLinksInternal(): array {
564        $result = [];
565        foreach ( $this->mLanguageLinkMap as $lang => $title ) {
566            $result[] = "$lang:$title";
567        }
568        return $result;
569    }
570
571    /**
572     * Return the names of the categories on this page.
573     * Unlike ::getCategories(), sort keys are *not* included in the
574     * return value.
575     * @return array<string> The names of the categories
576     * @since 1.38
577     */
578    public function getCategoryNames(): array {
579        # Note that numeric category names get converted to 'int' when
580        # stored as array keys; stringify the keys to ensure they
581        # return to original string form so as not to confuse callers.
582        return array_map( 'strval', array_keys( $this->mCategories ) );
583    }
584
585    /**
586     * Return category names and sort keys as a map.
587     *
588     * BEWARE that numeric category names get converted to 'int' when stored
589     * as array keys.  Because of this, use of this method is not recommended
590     * in new code; using ::getCategoryNames() and ::getCategorySortKey() will
591     * be less error-prone.
592     *
593     * @return array<string|int,string>
594     * @internal
595     */
596    public function getCategoryMap(): array {
597        return $this->mCategories;
598    }
599
600    /**
601     * Return the sort key for a given category name, or `null` if the
602     * category is not present in this ParserOutput.  Returns the
603     * empty string if the category is to use the default sort key.
604     *
605     * @note The effective sort key in the database may vary from what
606     * is returned here; see note in ParserOutput::addCategory().
607     *
608     * @param string $name The category name
609     * @return ?string The sort key for the category, or `null` if the
610     *  category is not present in this ParserOutput
611     * @since 1.40
612     */
613    public function getCategorySortKey( string $name ): ?string {
614        // This API avoids exposing the fact that numeric string category
615        // names are going to be converted to 'int' when used as array
616        // keys for the `mCategories` field.
617        return $this->mCategories[$name] ?? null;
618    }
619
620    /**
621     * @return array<string,string> Maps identifiers to HTML contents
622     * @since 1.25
623     */
624    public function getIndicators(): array {
625        $result = [];
626        foreach ( $this->mIndicatorIds as $id ) {
627            $fragmentName = "indicator:{$id}";
628            $contents = $this->contentHolder->getAsHtmlString( $fragmentName );
629            Assert::invariant( $contents !== null, "fragments should exist" );
630            $result[$id] = $contents;
631        }
632        return $result;
633    }
634
635    public function getTitleText(): string {