MediaWiki master
compareLanguageConverterOutput.php
Go to the documentation of this file.
1<?php
18use Wikimedia\Bcp47Code\Bcp47Code;
23
24// @codeCoverageIgnoreStart
25require_once __DIR__ . '/Maintenance.php';
26// @codeCoverageIgnoreEnd
27
35 public function __construct() {
36 parent::__construct();
37 $this->addDescription( 'Compares variant conversion output between Parser and HtmlOutputRendererHelper' );
38 $this->addArg(
39 'page-title',
40 'Name of the page to be parsed and compared',
41 true
42 );
43 $this->addArg(
44 'target-variant',
45 'Target variant language code to transform the content to',
46 true
47 );
48 }
49
51 public function execute() {
52 $mwInstance = $this->getServiceContainer();
53
54 $pageName = $this->getArg( 'page-title' );
55 $pageTitle = Title::newFromText( $pageName );
56
57 if ( !$pageTitle || !$pageTitle->exists() ) {
58 $this->fatalError( "Title with name $pageName not found" );
59 }
60
61 $targetVariantCode = $this->getArg( 'target-variant' );
62 $languageNameUtils = $mwInstance->getLanguageNameUtils();
63 if ( !$languageNameUtils->isValidBuiltInCode( $targetVariantCode ) ) {
64 $this->fatalError( "$targetVariantCode is not a supported variant" );
65 }
66 $targetVariant = $mwInstance->getLanguageFactory()->getLanguage(
67 $targetVariantCode
68 );
69
70 $user = User::newSystemUser( User::MAINTENANCE_SCRIPT_USER, [ 'steal' => true ] );
71 $baseLanguage = $pageTitle->getPageLanguage();
72
73 $parserOutput = $this->getParserOutput( $pageTitle, $baseLanguage, $targetVariant );
74 $parsoidOutput = $this->getParsoidOutput( $pageTitle, $targetVariant, $user );
75 $converterUsed = $this->getConverterUsed( $parsoidOutput );
76
77 $this->compareOutput( $parserOutput->getContentHolderText(), $parsoidOutput->getContentHolderText(),
78 $converterUsed );
79 return true;
80 }
81
82 private function newPageRestHelperFactory(): PageRestHelperFactory {
83 $services = $this->getServiceContainer();
84
85 return new PageRestHelperFactory(
86 new ServiceOptions( PageRestHelperFactory::CONSTRUCTOR_OPTIONS, $services->getMainConfig() ),
87 $services->getRevisionLookup(),
88 $services->getRevisionRenderer(),
89 $services->getTitleFormatter(),
90 $services->getPageStore(),
91 $services->getParsoidOutputStash(),
92 $services->getParserOutputAccess(),
93 $services->getParsoidSiteConfig(),
94 $services->getHtmlTransformFactory(),
95 $services->getContentHandlerFactory(),
96 $services->getLanguageFactory(),
97 $services->getRedirectStore(),
98 $services->getLanguageConverterFactory(),
99 $services->getTitleFactory(),
100 $services->getConnectionProvider(),
101 $services->getChangeTagsStore(),
102 StatsFactory::newNull(),
103 $services->getShadowPageLoader(),
104 $services->getPageBundleParserOutputConverter(),
105 );
106 }
107
108 private function getParserOptions( Language $language ): ParserOptions {
109 $parserOpts = ParserOptions::newFromAnon();
110 $parserOpts->setTargetLanguage( $language );
111 $parserOpts->disableContentConversion( false );
112 $parserOpts->disableTitleConversion( false );
113
114 return $parserOpts;
115 }
116
117 private function getParserOutput(
118 Title $pageTitle,
119 Language $baseLanguage,
120 Language $targetVariant
121 ): ParserOutput {
122 // We update the default language variant because we want Parser to
123 // perform variant conversion to it.
125 $wgDefaultLanguageVariant = $targetVariant->getCode();
126
127 $mwInstance = $this->getServiceContainer();
128
129 $languageFactory = $mwInstance->getLanguageFactory();
130 $parser = $mwInstance->getParser();
131 $parserOptions = $this->getParserOptions(
132 $languageFactory->getParentLanguage( $baseLanguage )
133 );
134
135 $content = $mwInstance->getRevisionLookup()
136 ->getRevisionByTitle( $pageTitle )
137 ->getContent( SlotRecord::MAIN );
138 $wikiContent = ( $content instanceof TextContent ) ? $content->getText() : '';
139
140 $po = $parser->parse( $wikiContent, $pageTitle, $parserOptions );
141 // TODO T371008 consider if using the Content framework makes sense instead of creating the pipeline
142 $pipeline = $mwInstance->getDefaultOutputPipeline();
143 $options = [ 'deduplicateStyles' => false ];
144 return $pipeline->run( $po, $parserOptions, $options );
145 }
146
147 private function getParsoidOutput(
148 Title $pageTitle,
149 Bcp47Code $targetVariant,
150 User $user
151 ): ParserOutput {
152 $parserOptions = ParserOptions::newFromAnon();
153 $htmlOutputRendererHelper = $this->newPageRestHelperFactory()->newHtmlOutputRendererHelper( $pageTitle, [
154 'stash' => false,
155 'flavor' => 'view',
156 ], $user, null, false, $parserOptions );
157 $htmlOutputRendererHelper->setVariantConversionLanguage( $targetVariant );
158
159 $po = $htmlOutputRendererHelper->getHtml();
160 $pipeline = $this->getServiceContainer()->getDefaultOutputPipeline();
161 $options = [ 'deduplicateStyles' => false ];
162 return $pipeline->run( $po, $parserOptions, $options );
163 }
164
165 private function getWords( string $output ): array {
166 $tagsRemoved = strip_tags( $output );
167 $words = preg_split( '/\s+/', trim( $tagsRemoved ), -1, PREG_SPLIT_NO_EMPTY );
168 return $words;
169 }
170
171 private function getBody( string $output ): string {
172 $dom = new DOMDocument();
173 // phpcs:disable Generic.PHP.NoSilencedErrors.Discouraged
174 @$dom->loadHTML( $output );
175 $body = $dom->getElementsByTagName( 'body' )->item( 0 );
176 if ( $body === null ) {
177 // Body element not present
178 return $output;
179 }
180
181 return $body->textContent;
182 }
183
184 private function compareOutput(
185 string $parserText,
186 string $parsoidText,
187 string $converterUsed
188 ): void {
189 $parsoidWords = $this->getWords( $this->getBody( $parsoidText ) );
190 $parserWords = $this->getWords( $parserText );
191
192 $parserWordCount = count( $parserWords );
193 $parsoidWordCount = count( $parsoidWords );
194 $this->output( "Word count: Parsoid: $parsoidWordCount; Parser: $parserWordCount\n" );
195
196 $this->outputSimilarity( $parsoidWords, $parserWords );
197 $this->output( "\n" );
198 $this->outputDiff( $parsoidWords, $parserWords, $converterUsed );
199 }
200
201 private function getConverterUsed( ParserOutput $parsoidOutput ): string {
202 $isCoreConverterUsed = strpos(
203 $parsoidOutput->getContentHolderText(),
204 'Variant conversion performed using the core LanguageConverter'
205 );
206
207 if ( $isCoreConverterUsed ) {
208 return 'Core LanguageConverter';
209 } else {
210 return 'Parsoid LanguageConverter';
211 }
212 }
213
214 // Inspired from: https://stackoverflow.com/a/55927237/903324
215 private function mb_sprintf( string $format, string ...$args ): string {
216 $params = $args;
217
218 return sprintf(
219 preg_replace_callback(
220 '/(?<=%|%-)\d+(?=s)/',
221 static function ( array $matches ) use ( &$params ) {
222 $value = array_shift( $params );
223
224 return (string)( strlen( $value ) - mb_strlen( $value ) + $matches[0] );
225 },
226 $format
227 ),
228 ...$args
229 );
230 }
231
232 private function outputSimilarity( array $parsoidWords, array $parserWords ): void {
233 $parsoidOutput = implode( ' ', $parsoidWords );
234 $parserOutput = implode( ' ', $parserWords );
235 $this->output(
236 'Total characters: Parsoid: ' . strlen( $parsoidOutput ) .
237 '; Parser: ' . strlen( $parserOutput ) . "\n"
238 );
239
240 $similarityPercent = 0;
241 $similarCharacters = similar_text( $parsoidOutput, $parserOutput, $similarityPercent );
242 $similarityPercent = round( $similarityPercent, 2 );
243
244 $this->output(
245 "Similarity via similar_text(): $similarityPercent%; Similar characters: $similarCharacters"
246 );
247 }
248
249 private function outputDiff( array $parsoidWords, array $parserWords, string $converterUsed ): void {
250 $out = str_repeat( '-', 96 ) . "\n";
251 $out .= sprintf( "| %5s | %-35s | %-35s | %-8s |\n", 'Line', 'Parsoid', 'Parser', 'Diff' );
252 $out .= sprintf( "| %5s | %-35s | %-35s | %-8s |\n", '', "($converterUsed)", '', '' );
253 $out .= str_repeat( '-', 96 ) . "\n";
254
255 try {
256 $diff = new Diff( $parsoidWords, $parserWords );
257 } catch ( ComplexityException $e ) {
258 $this->output( $e->getMessage() );
259 $this->fatalError( 'Encountered ComplexityException while computing diff' );
260 }
261
262 // Print the difference between the words
263 $wordDiffFormat = ( new ArrayDiffFormatter() )->format( $diff );
264 foreach ( $wordDiffFormat as $index => $wordDiff ) {
265 $action = $wordDiff['action'];
266 $old = $wordDiff['old'] ?? null;
267 $new = $wordDiff['new'] ?? null;
268
269 $out .= $this->mb_sprintf(
270 "| %5s | %-35s | %-35s | %-8s |\n",
271 str_pad( (string)( $index + 1 ), 5, ' ', STR_PAD_LEFT ),
272 mb_strimwidth( $old ?? '- N/A -', 0, 35, '…' ),
273 mb_strimwidth( $new ?? '- N/A -', 0, 35, '…' ),
274 $action
275 );
276 }
277
278 // Print the footer.
279 $out .= str_repeat( '-', 96 ) . "\n";
280 $this->output( "\n" . $out );
281 }
282}
283
284// @codeCoverageIgnoreStart
285$maintClass = CompareLanguageConverterOutput::class;
286require_once RUN_MAINTENANCE_IF_MAIN;
287// @codeCoverageIgnoreEnd
Maintenance script that compares variant conversion output between Parser and HtmlOutputRendererHelpe...
execute()
Do the actual work.All child classes will need to implement thisbool|null|void True for success,...
A class for passing options to services.
Content object implementation for representing flat text.
Base class for language-specific code.
Definition Language.php:65
getCode()
Get the internal language code for this language object.
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
addArg( $arg, $description, $required=true, $multi=false)
Add some args that are needed.
getArg( $argId=0, $default=null)
Get an argument.
fatalError( $msg, $exitCode=1)
Output a message and terminate the current script.
getServiceContainer()
Returns the main service container.
addDescription( $text)
Set the description text.
Set options of the Parser.
setTargetLanguage( $x)
Target language for the parse.
ParserOutput is a rendering of a Content object or a message.
getContentHolderText()
Returns the body fragment text of the ParserOutput.
Value object representing a content slot associated with a page revision.
Represents a title within MediaWiki.
Definition Title.php:69
getPageLanguage()
Get the language in which the content of this page is written in wikitext.
Definition Title.php:3502
exists( $flags=0)
Check if page exists.
Definition Title.php:3129
User class for the MediaWiki software.
Definition User.php:129
A pseudo-formatter that just passes along the Diff::$edits array.
Class representing a 'diff' between two sequences of strings.
Definition Diff.php:20
This is the primary interface for validating metrics definitions, caching defined metrics,...
$wgDefaultLanguageVariant
Config variable stub for the DefaultLanguageVariant setting, for use by phpdoc and IDEs.
array $params
The job parameters.