const SUPPORTS_UNWRAP_TRANSFORM = 1;
/**
- * @var string $mText The output text
+ * @var string|null $mText The output text
*/
- public $mText;
+ public $mText = null;
/**
* @var array $mLanguageLinks List of the full text of language links,
const SLOW_AR_TTL = 3600; // adaptive TTL for "slow" pages
const MIN_AR_TTL = 15; // min adaptive TTL (for sanity, pool counter, and edit stashing)
+ /**
+ * @param string|null $text HTML. Use null to indicate that this ParserOutput contains only
+ * meta-data, and the HTML output is undetermined, as opposed to empty. Passing null
+ * here causes hasText() to return false.
+ * @param array $languageLinks
+ * @param array $categoryLinks
+ * @param bool $unused
+ * @param string $titletext
+ */
public function __construct( $text = '', $languageLinks = [], $categoryLinks = [],
$unused = false, $titletext = ''
) {
$this->mTitleText = $titletext;
}
+ /**
+ * Returns true if text was passed to the constructor, or set using setText(). Returns false
+ * if null was passed to the $text parameter of the constructor to indicate that this
+ * ParserOutput only contains meta-data, and the HTML output is undetermined.
+ *
+ * @since 1.32
+ *
+ * @return bool Whether this ParserOutput contains rendered text. If this returns false, the
+ * ParserOutput contains meta-data only.
+ */
+ public function hasText() {
+ return ( $this->mText !== null );
+ }
+
/**
* Get the cacheable text with <mw:editsection> markers still in it. The
* return value is suitable for writing back via setText() but is not valid
* @since 1.27
*/
public function getRawText() {
+ if ( $this->mText === null ) {
+ throw new LogicException( 'This ParserOutput contains no text!' );
+ }
+
return $this->mText;
}
* the scheme-specific-part of the href is the (percent-encoded) value
* of the `data-mw-deduplicate` attribute.
* @return string HTML
+ * @return-taint escaped
*/
public function getText( $options = [] ) {
$options += [
'deduplicateStyles' => true,
'wrapperDivClass' => $this->getWrapperDivClass(),
];
- $text = $this->mText;
+ $text = $this->getRawText();
Hooks::runWithoutAbort( 'ParserOutputPostCacheTransform', [ $this, &$text, &$options ] );
);
}
+ // Hydrate slot section header placeholders generated by RevisionRenderer.
+ $text = preg_replace_callback(
+ '#<mw:slotheader>(.*?)</mw:slotheader>#',
+ function ( $m ) {
+ $role = htmlspecialchars_decode( $m[1] );
+ // TODO: map to message, using the interface language. Set lang="xyz" accordingly.
+ $headerText = $role;
+ return $headerText;
+ },
+ $text
+ );
return $text;
}
return $this->mExternalLinks;
}
+ public function setNoGallery( $value ) {
+ $this->mNoGallery = (bool)$value;
+ }
public function getNoGallery() {
return $this->mNoGallery;
}
[ 'mParseStartTime' ]
);
}
+
+ // TODO remove this method once old parser cache objects have expired, probably mid-October 2018
+ public function __wakeup() {
+ // T203716 remove wrapper that was added by logic in an older version of this class,
+ // where the wrapper was included in mText. This might sometimes remove a wrapper that's
+ // genuine content (manually added to a system message), but that will work out OK, see below.
+ $text = $this->getRawText();
+ $start = Html::openElement( 'div', [
+ 'class' => 'mw-parser-output'
+ ] );
+ $startLen = strlen( $start );
+ $end = Html::closeElement( 'div' );
+ $endPos = strrpos( $text, $end );
+ $endLen = strlen( $end );
+ if ( substr( $text, 0, $startLen ) === $start && $endPos !== false
+ // if the closing div is followed by real content, bail out of unwrapping
+ && preg_match( '/^(?>\s*<!--.*?-->)*\s*$/s', substr( $text, $endPos + $endLen ) )
+ ) {
+ $text = substr( $text, $startLen );
+ $text = substr( $text, 0, $endPos - $startLen ) .
+ substr( $text, $endPos - $startLen + $endLen );
+ $this->setText( $text );
+ // We found a wrapper to remove, so the ParserOutput was probably created by the
+ // code path that now contains an addWrapperDivClass( 'mw-parser-output' ) call,
+ // but it did not contain it when this object was cached, so we need to fix the
+ // wrapper class variable.
+ // If this was a message with a manually added wrapper, we are technically wrong about
+ // this but we were wrong about the unwrapping as well so it will work out just right,
+ // except when this is a normal page view of such a message page, in which case
+ // it will be single-wrapped instead of double-wrapped (harmless) or something wants
+ // render the message with unwrap=true (in which case the message won't be wrapped even
+ // though it should, but the few code paths using unwrap=true only do it for real pages).
+ $this->clearWrapperDivClass();
+ $this->addWrapperDivClass( 'mw-parser-output' );
+ }
+ }
+
+ /**
+ * Merges internal metadata such as flags, accessed options, and profiling info
+ * from $source into this ParserOutput. This should be used whenever the state of $source
+ * has any impact on the state of this ParserOutput.
+ *
+ * @param ParserOutput $source
+ */
+ public function mergeInternalMetaDataFrom( ParserOutput $source ) {
+ $this->mOutputHooks = self::mergeList( $this->mOutputHooks, $source->getOutputHooks() );
+ $this->mWarnings = self::mergeMap( $this->mWarnings, $source->mWarnings ); // don't use getter
+ $this->mTimestamp = $this->useMaxValue( $this->mTimestamp, $source->getTimestamp() );
+
+ if ( $this->mSpeculativeRevId && $source->mSpeculativeRevId
+ && $this->mSpeculativeRevId !== $source->mSpeculativeRevId
+ ) {
+ wfLogWarning(
+ 'Inconsistent speculative revision ID encountered while merging parser output!'
+ );
+ }
+
+ $this->mSpeculativeRevId = $this->useMaxValue(
+ $this->mSpeculativeRevId,
+ $source->getSpeculativeRevIdUsed()
+ );
+ $this->mParseStartTime = $this->useEachMinValue(
+ $this->mParseStartTime,
+ $source->mParseStartTime
+ );
+
+ $this->mFlags = self::mergeMap( $this->mFlags, $source->mFlags );
+ $this->mAccessedOptions = self::mergeMap( $this->mAccessedOptions, $source->mAccessedOptions );
+
+ // TODO: maintain per-slot limit reports!
+ if ( empty( $this->mLimitReportData ) ) {
+ $this->mLimitReportData = $source->mLimitReportData;
+ }
+ if ( empty( $this->mLimitReportJSData ) ) {
+ $this->mLimitReportJSData = $source->mLimitReportJSData;
+ }
+ }
+
+ /**
+ * Merges HTML metadata such as head items, JS config vars, and HTTP cache control info
+ * from $source into this ParserOutput. This should be used whenever the HTML in $source
+ * has been somehow mered into the HTML of this ParserOutput.
+ *
+ * @param ParserOutput $source
+ */
+ public function mergeHtmlMetaDataFrom( ParserOutput $source ) {
+ // HTML and HTTP
+ $this->mHeadItems = self::mergeMixedList( $this->mHeadItems, $source->getHeadItems() );
+ $this->mModules = self::mergeList( $this->mModules, $source->getModules() );
+ $this->mModuleScripts = self::mergeList( $this->mModuleScripts, $source->getModuleScripts() );
+ $this->mModuleStyles = self::mergeList( $this->mModuleStyles, $source->getModuleStyles() );
+ $this->mJsConfigVars = self::mergeMap( $this->mJsConfigVars, $source->getJsConfigVars() );
+ $this->mMaxAdaptiveExpiry = min( $this->mMaxAdaptiveExpiry, $source->mMaxAdaptiveExpiry );
+
+ // "noindex" always wins!
+ if ( $this->mIndexPolicy === 'noindex' || $source->mIndexPolicy === 'noindex' ) {
+ $this->mIndexPolicy = 'noindex';
+ } elseif ( $this->mIndexPolicy !== 'index' ) {
+ $this->mIndexPolicy = $source->mIndexPolicy;
+ }
+
+ // Skin control
+ $this->mNewSection = $this->mNewSection || $source->getNewSection();
+ $this->mHideNewSection = $this->mHideNewSection || $source->getHideNewSection();
+ $this->mNoGallery = $this->mNoGallery || $source->getNoGallery();
+ $this->mEnableOOUI = $this->mEnableOOUI || $source->getEnableOOUI();
+ $this->mPreventClickjacking = $this->mPreventClickjacking || $source->preventClickjacking();
+
+ // TODO: we'll have to be smarter about this!
+ $this->mSections = array_merge( $this->mSections, $source->getSections() );
+ $this->mTOCHTML = $this->mTOCHTML . $source->mTOCHTML;
+
+ // XXX: we don't want to concatenate title text, so first write wins.
+ // We should use the first *modified* title text, but we don't have the original to check.
+ if ( $this->mTitleText === null || $this->mTitleText === '' ) {
+ $this->mTitleText = $source->mTitleText;
+ }
+
+ // class names are stored in array keys
+ $this->mWrapperDivClasses = self::mergeMap(
+ $this->mWrapperDivClasses,
+ $source->mWrapperDivClasses
+ );
+
+ // NOTE: last write wins, same as within one ParserOutput
+ $this->mIndicators = self::mergeMap( $this->mIndicators, $source->getIndicators() );
+
+ // NOTE: include extension data in "tracking meta data" as well as "html meta data"!
+ // TODO: add a $mergeStrategy parameter to setExtensionData to allow different
+ // kinds of extension data to be merged in different ways.
+ $this->mExtensionData = self::mergeMap(
+ $this->mExtensionData,
+ $source->mExtensionData
+ );
+ }
+
+ /**
+ * Merges dependency tracking metadata such as backlinks, images used, and extension data
+ * from $source into this ParserOutput. This allows dependency tracking to be done for the
+ * combined output of multiple content slots.
+ *
+ * @param ParserOutput $source
+ */
+ public function mergeTrackingMetaDataFrom( ParserOutput $source ) {
+ $this->mLanguageLinks = self::mergeList( $this->mLanguageLinks, $source->getLanguageLinks() );
+ $this->mCategories = self::mergeMap( $this->mCategories, $source->getCategories() );
+ $this->mLinks = self::merge2D( $this->mLinks, $source->getLinks() );
+ $this->mTemplates = self::merge2D( $this->mTemplates, $source->getTemplates() );
+ $this->mTemplateIds = self::merge2D( $this->mTemplateIds, $source->getTemplateIds() );
+ $this->mImages = self::mergeMap( $this->mImages, $source->getImages() );
+ $this->mFileSearchOptions = self::mergeMap(
+ $this->mFileSearchOptions,
+ $source->getFileSearchOptions()
+ );
+ $this->mExternalLinks = self::mergeMap( $this->mExternalLinks, $source->getExternalLinks() );
+ $this->mInterwikiLinks = self::merge2D(
+ $this->mInterwikiLinks,
+ $source->getInterwikiLinks()
+ );
+
+ // TODO: add a $mergeStrategy parameter to setProperty to allow different
+ // kinds of properties to be merged in different ways.
+ $this->mProperties = self::mergeMap( $this->mProperties, $source->getProperties() );
+
+ // NOTE: include extension data in "tracking meta data" as well as "html meta data"!
+ // TODO: add a $mergeStrategy parameter to setExtensionData to allow different
+ // kinds of extension data to be merged in different ways.
+ $this->mExtensionData = self::mergeMap(
+ $this->mExtensionData,
+ $source->mExtensionData
+ );
+ }
+
+ private static function mergeMixedList( array $a, array $b ) {
+ return array_unique( array_merge( $a, $b ), SORT_REGULAR );
+ }
+
+ private static function mergeList( array $a, array $b ) {
+ return array_values( array_unique( array_merge( $a, $b ), SORT_REGULAR ) );
+ }
+
+ private static function mergeMap( array $a, array $b ) {
+ return array_replace( $a, $b );
+ }
+
+ private static function merge2D( array $a, array $b ) {
+ $values = [];
+ $keys = array_merge( array_keys( $a ), array_keys( $b ) );
+
+ foreach ( $keys as $k ) {
+ if ( empty( $a[$k] ) ) {
+ $values[$k] = $b[$k];
+ } elseif ( empty( $b[$k] ) ) {
+ $values[$k] = $a[$k];
+ } elseif ( is_array( $a[$k] ) && is_array( $b[$k] ) ) {
+ $values[$k] = array_replace( $a[$k], $b[$k] );
+ } else {
+ $values[$k] = $b[$k];
+ }
+ }
+
+ return $values;
+ }
+
+ private static function useEachMinValue( array $a, array $b ) {
+ $values = [];
+ $keys = array_merge( array_keys( $a ), array_keys( $b ) );
+
+ foreach ( $keys as $k ) {
+ if ( is_array( $a[$k] ?? null ) && is_array( $b[$k] ?? null ) ) {
+ $values[$k] = self::useEachMinValue( $a[$k], $b[$k] );
+ } else {
+ $values[$k] = self::useMinValue( $a[$k] ?? null, $b[$k] ?? null );
+ }
+ }
+
+ return $values;
+ }
+
+ private static function useMinValue( $a, $b ) {
+ if ( $a === null ) {
+ return $b;
+ }
+
+ if ( $b === null ) {
+ return $a;
+ }
+
+ return min( $a, $b );
+ }
+
+ private static function useMaxValue( $a, $b ) {
+ if ( $a === null ) {
+ return $b;
+ }
+
+ if ( $b === null ) {
+ return $a;
+ }
+
+ return max( $a, $b );
+ }
+
}