MediaWiki master
ParsoidParser.php
Go to the documentation of this file.
1<?php
2declare( strict_types = 1 );
3
5
24use Wikimedia\Assert\Assert;
25use Wikimedia\Parsoid\Config\PageConfig;
26use Wikimedia\Parsoid\Config\SiteConfig;
27use Wikimedia\Parsoid\Parsoid;
28
41class ParsoidParser /* eventually this will extend \Parser */ {
42 public function __construct(
43 private Parsoid $parsoid,
44 private readonly PageConfigFactory $pageConfigFactory,
45 private readonly LanguageConverterFactory $languageConverterFactory,
46 private readonly SiteConfig $siteConfig,
47 private readonly DataAccess $dataAccess,
48 private readonly NamespaceInfo $namespaceInfo,
49 private readonly TrackingCategories $trackingCategories,
50 ) {
51 }
52
60 private function genParserOutput(
61 PageConfig $pageConfig, ParserOptions $options, ?ParserOutput $previousOutput
62 ): ParserOutput {
63 $parserOutput = new ParserOutput();
64
65 // Parsoid itself does not vary output by parser options right now.
66 // But, ensure that any option use by extensions, parser functions,
67 // recursive parses, or (in the unlikely future scenario) Parsoid itself
68 // are recorded as used.
69 $options->registerWatcher( $parserOutput->recordOption( ... ) );
70
71 // The enable/disable logic here matches that in Parser::internalParseHalfParsed(),
72 // although __NOCONTENTCONVERT__ is handled internal to Parsoid.
73 //
74 // T349137: It might be preferable to handle __NOCONTENTCONVERT__ here rather than
75 // by inspecting the DOM inside Parsoid. That will come in a separate patch.
76 $htmlVariantLanguage = null;
77 if ( !( $options->getDisableContentConversion() || $options->getInterfaceMessage() ) ) {
78 // NOTES (some of these are TODOs for read views integration)
79 // 1. This html variant conversion is a pre-cache transform. HtmlOutputRendererHelper
80 // has another variant conversion that is a post-cache transform based on the
81 // 'Accept-Language' header. If that header is set, there is really no reason to
82 // do this conversion here. So, eventually, we are likely to either not pass in
83 // the htmlVariantLanguage option below OR disable language conversion from the
84 // wt2html path in Parsoid and this and the Accept-Language variant conversion
85 // both would have to be handled as post-cache transforms.
86 //
87 // 2. Parser.php calls convert() which computes a preferred variant from the
88 // target language. But, we cannot do that unconditionally here because REST API
89 // requests specify the exact variant via the 'Content-Language' header.
90 //
91 // For Parsoid page views, either the callers will have to compute the
92 // preferred variant and set it in ParserOptions OR the REST API will have
93 // to set some other flag indicating that the preferred variant should not
94 // be computed. For now, I am adding a temporary hack, but this should be
95 // replaced with something more sensible (T267067).
96 //
97 // 3. Additionally, Parsoid's callers will have to set targetLanguage in ParserOptions
98 // to mimic the logic in Parser.php (missing right now).
99 $langCode = $pageConfig->getPageLanguageBcp47();
100 // TEMPORARY HACK
101 if ( $options->getRenderReason() === 'page_view' || $options->getRenderReason() === 'page_view_old' ) {
102 $langFactory = MediaWikiServices::getInstance()->getLanguageFactory();
103 $lang = $langFactory->getLanguage( $langCode );
104 $langConv = $this->languageConverterFactory->getLanguageConverter( $lang );
105 $htmlVariantLanguage = $langFactory->getLanguage( $langConv->getPreferredVariant() );
106 } else {
107 $htmlVariantLanguage = $langCode;
108 }
109 }
110 $oldPageConfig = null;
111 $oldPageBundle = null;
112
113 // T371713: Temporary statistics collection code to determine
114 // feasibility of Parsoid selective update
115 $sampleRate = MediaWikiServices::getInstance()->getMainConfig()->get(
117 );
118 $doSample = ( $sampleRate && mt_rand( 1, $sampleRate ) === 1 );
119 if ( $doSample && $previousOutput !== null && $previousOutput->getCacheRevisionId() ) {
120 // Allow fetching the old wikitext corresponding to the
121 // $previousOutput
122 $oldPageConfig = $this->pageConfigFactory->createFromParserOptions(
123 $options,
124 Title::newFromLinkTarget( $pageConfig->getLinkTarget() ),
125 $previousOutput->getCacheRevisionId(),
126 $previousOutput->getLanguage()
127 );
128 $oldPageBundle =
130 $previousOutput, $this->siteConfig, bodyOnly: false,
131 );
132 }
133 $defaultOptions = [
134 'pageBundle' => true,
135 'wrapSections' => true,
136 'logLinterData' => true,
137 // The canonical ParserOutput form is body-only; Parsoid emits the
138 // body fragment and the full document is reconstructed on demand
139 // from an HtmlPageBundle (getPageBundle()). Legacy full-document
140 // cache entries are still handled by the lazy strip in ContentHolder.
141 'body_only' => true,
142 'htmlVariantLanguage' => $htmlVariantLanguage,
143 // We're doing language conversion in postprocessing now.
144 'skipLanguageConversionPass' => true,
145 'offsetType' => 'byte',
146 'outputContentVersion' => Parsoid::defaultHTMLVersion(),
147 'previousOutput' => $oldPageBundle,
148 'previousInput' => $oldPageConfig,
149 // The following are passed for metrics & labelling
150 'sampleStats' => $doSample,
151 'renderReason' => $options->getRenderReason(),
152 'userAgent' => RequestContext::getMain()->getRequest()->getHeader( 'User-Agent' ),
153 ];
154
155 $parserOutput->resetParseStartTime();
156
157 // This can throw ClientError or ResourceLimitExceededException.
158 // Callers are responsible for figuring out how to handle them.
159 $pageBundle = $this->parsoid->wikitext2html(
160 $pageConfig,
161 $defaultOptions,
162 $headers,
163 $parserOutput );
164
166 $pageBundle, $parserOutput,
167 title: $pageConfig->getLinkTarget(),
168 siteConfig: $this->siteConfig,
169 );
170
171 // Register a watcher again because the $parserOutput arg
172 // and $parserOutput return value above are different objects!
173 $options->registerWatcher( $parserOutput->recordOption( ... ) );
174
175 $parserOutput->setFromParserOptions( $options );
176
177 $parserOutput->recordTimeProfile();
178 $this->dataAccess->makeLimitReport( $pageConfig, $options, $parserOutput );
179
180 // T371713: Collect statistics on parsing time -vs- presence of
181 // $previousOutput
182 $stats = MediaWikiServices::getInstance()->getStatsFactory();
183 $labels = [
184 'type' => $previousOutput === null ? 'full' : 'selective',
185 'wiki' => WikiMap::getCurrentWikiId(),
186 'reason' => $options->getRenderReason() ?: 'unknown',
187 'has_async_content' =>
188 $parserOutput->getOutputFlag( ParserOutputFlags::HAS_ASYNC_CONTENT )
189 ? 'true' : 'false',
190 'async_not_ready' =>
191 $parserOutput->getOutputFlag( ParserOutputFlags::ASYNC_NOT_READY )
192 ? 'true' : 'false',
193 ];
194 $stats
195 ->getCounter( 'Parsoid_parse_cpu_seconds' )
196 ->setLabels( $labels )
197 ->incrementBy( $parserOutput->getTimeProfile( 'cpu' ) );
198 $stats
199 ->getCounter( 'Parsoid_parse_total' )
200 ->setLabels( $labels )
201 ->increment();
202
203 return $this->addMetadata( $parserOutput, $pageConfig );
204 }
205
213 public function addMetadata( ParserOutput $parserOutput, PageConfig $pageConfig ): ParserOutput {
214 // Add Parsoid skinning module
215 $parserOutput->addModuleStyles( [ 'mediawiki.skinning.content.parsoid' ] );
216
217 // (T10068) Allow control over whether robots index a page.
218 # __NOINDEX__ always overrides __INDEX__, see T16899
219 foreach ( [ 'noindex', 'index' ] as $indexSwitch ) {
220 if (
221 $parserOutput->getPageProperty( $indexSwitch ) !== null &&
222 $this->namespaceInfo->canUseNoindex(
223 $pageConfig->getLinkTarget()->getNamespace()
224 )
225 ) {
226 $parserOutput->setIndexPolicy( $indexSwitch );
227 // Tracking categories are 'index-category', 'noindex-category'
228 $this->trackingCategories->addTrackingCategory(
229 $parserOutput,
230 $indexSwitch . '-category',
231 Title::newFromLinkTarget( $pageConfig->getLinkTarget() ),
232 );
233 }
234 }
235
236 // Record base uri
237 $parserOutput->setExtensionData(
238 'core:base-uri', $this->siteConfig->baseURI()
239 );
240
241 // Record Parsoid version in extension data; this allows
242 // us to use the onRejectParserCacheValue hook to selectively
243 // expire "bad" generated content in the event of a rollback.
244 $parserOutput->setExtensionData(
245 'core:parsoid-version', Parsoid::version()
246 );
247 $parserOutput->setExtensionData(
248 'core:html-version', Parsoid::defaultHTMLVersion()
249 );
250 // Export Parsoid HTML version to client gadgets as well
251 $parserOutput->setJsConfigVar(
252 'wgParsoidHtmlVersion', Parsoid::defaultHTMLVersion()
253 );
254 // TEMPORARY during transition to new LanguageConverter
255 // Ensure we can distinguish ParserOutputs created using the
256 // older language converter implementation.
257 $parserOutput->setExtensionData(
258 'core:parsoid-languageconverter', 'postprocess'
259 );
260
261 return $parserOutput;
262 }
263
284 public function parse(
285 $text, PageReference $page, ParserOptions $options,
286 bool $linestart = true, bool $clearState = true, ?int $revId = null,
287 ?ParserOutput $previousOutput = null
288 ): ParserOutput {
289 Assert::invariant( $linestart, '$linestart=false is not yet supported' );
290 Assert::invariant( $clearState, '$clearState=false is not yet supported' );
291 $title = Title::newFromPageReference( $page );
292 $lang = $options->getTargetLanguage();
293 if ( $lang === null && $options->getInterfaceMessage() ) {
294 $lang = $options->getUserLangObj();
295 }
296 $pageConfig = $revId === null || $revId === 0 ? null : $this->pageConfigFactory->createFromParserOptions(
297 $options, // T392113: transfers current revision record callback
298 $title,
299 $revId,
300 $lang // defaults to title page language if null
301 );
302 $content = null;
303 if ( $text instanceof TextContent ) {
304 $content = $text;
305 $text = $content->getText();
306 }
307 if ( !( $pageConfig && $pageConfig->getPageMainContent() === $text ) ) {
308 // This is a bit awkward! But we really need to parse $text, which
309 // may or may not correspond to the $revId provided!
310 // T332928 suggests one solution: splitting the "have revid"
311 // callers from the "bare text, no associated revision" callers.
312 $revisionRecord = new MutableRevisionRecord( $title );
313 if ( $revId !== null ) {
314 $revisionRecord->setId( $revId );
315 }
316 $revisionRecord->setSlot(
317 SlotRecord::newUnsaved(
318 SlotRecord::MAIN,
319 $content ?? new WikitextContent( $text )
320 )
321 );
322 $pageConfig = $this->pageConfigFactory->createFromParserOptions(
323 $options,
324 $title,
325 $revisionRecord,
326 $lang // defaults to title page language if null
327 );
328 }
329
330 return $this->genParserOutput( $pageConfig, $options, $previousOutput );
331 }
332}
This class performs some operations related to tracking categories, such as adding a tracking categor...
Content object implementation for representing flat text.
Content object for wiki text pages.
Group all the pieces relevant to the context of a request into one instance.
An interface for creating language converters.
A class containing constants representing the names of configuration variables.
const ParsoidSelectiveUpdateSampleRate
Name constant for the ParsoidSelectiveUpdateSampleRate setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
recordOption(string $option)
Tags a parser option for use in the cache key for this parser output.
Set options of the Parser.
getDisableContentConversion()
Whether content conversion should be disabled.
getRenderReason()
Returns reason for rendering the content.
getInterfaceMessage()
Parsing an interface message in the user language?
getTargetLanguage()
Target language for the parse.
getUserLangObj()
Get the user language used by the parser for this page and split the parser cache.
registerWatcher( $callback)
Registers a callback for tracking which ParserOptions which are used.
ParserOutput is a rendering of a Content object or a message.
setIndexPolicy( $policy)
Update the index policy of the robots meta tag.
getTimeProfile(string $clock)
Returns the time that elapsed between the most recent call to resetParseStartTime() and the first cal...
setExtensionData( $key, $value)
Attaches arbitrary data to this ParserObject.
setJsConfigVar(string $key, $value)
Add a variable to be set in mw.config in JavaScript.
getOutputFlag(ParserOutputFlags|string $flag)
Provides a uniform interface to various boolean flags stored in the ParserOutput.
recordTimeProfile()
Record the time since resetParseStartTime() was last called.
getPageProperty(string $name)
Look up a page property.
getLanguage()
Get the primary language code of the output.
resetParseStartTime()
Resets the parse start timestamps for future calls to getTimeProfile() and recordTimeProfile().
setFromParserOptions(ParserOptions $parserOptions)
Transfer parser options which affect post-processing from ParserOptions to this ParserOutput.
Implement Parsoid's abstract class for data access.
Helper class used by MediaWiki to create Parsoid PageConfig objects.
static htmlPageBundleFromParserOutput(ParserOutput $parserOutput, SiteConfig $siteConfig, bool $bodyOnly=false,)
Returns a Parsoid HtmlPageBundle equivalent to the given ParserOutput.
static parserOutputFromPageBundle(HtmlPageBundle $pageBundle, ?ParserOutput $originalParserOutput=null, ParsoidLinkTarget|PageReference|null $title=null, ?SiteConfig $siteConfig=null,)
Creates a ParserOutput object containing the relevant data from the given HtmlPageBundle object.
Parser implementation which uses Parsoid.
parse( $text, PageReference $page, ParserOptions $options, bool $linestart=true, bool $clearState=true, ?int $revId=null, ?ParserOutput $previousOutput=null)
Convert wikitext to HTML Do not call this function recursively.
addMetadata(ParserOutput $parserOutput, PageConfig $pageConfig)
Add Parsoid-specific metadata to the final ParserOutput.
__construct(private Parsoid $parsoid, private readonly PageConfigFactory $pageConfigFactory, private readonly LanguageConverterFactory $languageConverterFactory, private readonly SiteConfig $siteConfig, private readonly DataAccess $dataAccess, private readonly NamespaceInfo $namespaceInfo, private readonly TrackingCategories $trackingCategories,)
Value object representing a content slot associated with a page revision.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Represents a title within MediaWiki.
Definition Title.php:69
Tools for dealing with other locally-hosted wikis.
Definition WikiMap.php:19
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.