Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 148
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
CompareLanguageConverterOutput
0.00% covered (danger)
0.00%
0 / 145
0.00% covered (danger)
0.00%
0 / 13
462
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
 newPageRestHelperFactory
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
2
 getParserOptions
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getParserOutput
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 getParsoidOutput
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getWords
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getBody
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 compareOutput
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 getConverterUsed
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 mb_sprintf
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 outputSimilarity
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 outputDiff
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 * @ingroup Maintenance
20 */
21
22use MediaWiki\Config\ServiceOptions;
23use MediaWiki\Content\TextContent;
24use MediaWiki\Parser\ParserOutput;
25use MediaWiki\Rest\Handler\Helper\PageRestHelperFactory;
26use MediaWiki\Revision\SlotRecord;
27use MediaWiki\Title\Title;
28use MediaWiki\User\User;
29use Wikimedia\Bcp47Code\Bcp47Code;
30use Wikimedia\Diff\ArrayDiffFormatter;
31use Wikimedia\Diff\ComplexityException;
32use Wikimedia\Diff\Diff;
33
34require_once __DIR__ . '/Maintenance.php';
35
36/**
37 * Maintenance script that compares variant conversion output between Parser and
38 * HtmlOutputRendererHelper.
39 *
40 * @ingroup Maintenance
41 */
42class CompareLanguageConverterOutput extends Maintenance {
43    public function __construct() {
44        parent::__construct();
45        $this->addDescription( 'Compares variant conversion output between Parser and HtmlOutputRendererHelper' );
46        $this->addArg(
47            'page-title',
48            'Name of the page to be parsed and compared',
49            true
50        );
51        $this->addArg(
52            'target-variant',
53            'Target variant language code to transform the content to',
54            true
55        );
56    }
57
58    public function execute() {
59        $mwInstance = $this->getServiceContainer();
60
61        $pageName = $this->getArg( 'page-title' );
62        $pageTitle = Title::newFromText( $pageName );
63
64        if ( !$pageTitle || !$pageTitle->exists() ) {
65            $this->fatalError( "Title with name $pageName not found" );
66        }
67
68        $targetVariantCode = $this->getArg( 'target-variant' );
69        $languageNameUtils = $mwInstance->getLanguageNameUtils();
70        if ( !$languageNameUtils->isValidBuiltInCode( $targetVariantCode ) ) {
71            $this->fatalError( "$targetVariantCode is not a supported variant" );
72        }
73        $targetVariant = $mwInstance->getLanguageFactory()->getLanguage(
74            $targetVariantCode
75        );
76
77        $user = User::newSystemUser( User::MAINTENANCE_SCRIPT_USER, [ 'steal' => true ] );
78        $baseLanguage = $pageTitle->getPageLanguage();
79
80        $parserOutput = $this->getParserOutput( $pageTitle, $baseLanguage, $targetVariant );
81        $parsoidOutput = $this->getParsoidOutput( $pageTitle, $targetVariant, $user );
82        $converterUsed = $this->getConverterUsed( $parsoidOutput );
83
84        $this->compareOutput( $parserOutput, $parsoidOutput, $converterUsed );
85
86        return true;
87    }
88
89    private function newPageRestHelperFactory(): PageRestHelperFactory {
90        $services = $this->getServiceContainer();
91
92        $factory = new PageRestHelperFactory(
93            new ServiceOptions( PageRestHelperFactory::CONSTRUCTOR_OPTIONS, $services->getMainConfig() ),
94            $services->getRevisionLookup(),
95            $services->getTitleFormatter(),
96            $services->getPageStore(),
97            $services->getParsoidOutputStash(),
98            new NullStatsdDataFactory(),
99            $services->getParserOutputAccess(),
100            $services->getParsoidSiteConfig(),
101            $services->getParsoidParserFactory(),
102            $services->getHtmlTransformFactory(),
103            $services->getContentHandlerFactory(),
104            $services->getLanguageFactory(),
105            $services->getRedirectStore(),
106            $services->getLanguageConverterFactory()
107        );
108        return $factory;
109    }
110
111    private function getParserOptions( Language $language ): ParserOptions {
112        $parserOpts = ParserOptions::newFromAnon();
113        $parserOpts->setTargetLanguage( $language );
114        $parserOpts->disableContentConversion( false );
115        $parserOpts->disableTitleConversion( false );
116
117        return $parserOpts;
118    }
119
120    private function getParserOutput(
121        Title $pageTitle,
122        Language $baseLanguage,
123        Language $targetVariant
124    ): ParserOutput {
125        // We update the default language variant because we want Parser to
126        // perform variant conversion to it.
127        global $wgDefaultLanguageVariant;
128        $wgDefaultLanguageVariant = $targetVariant->getCode();
129
130        $mwInstance = $this->getServiceContainer();
131
132        $languageFactory = $mwInstance->getLanguageFactory();
133        $parser = $mwInstance->getParser();
134        $parserOptions = $this->getParserOptions(
135            $languageFactory->getParentLanguage( $baseLanguage )
136        );
137
138        $content = $mwInstance->getRevisionLookup()
139            ->getRevisionByTitle( $pageTitle )
140            ->getContent( SlotRecord::MAIN );
141        $wikiContent = ( $content instanceof TextContent ) ? $content->getText() : '';
142
143        return $parser->parse( $wikiContent, $pageTitle, $parserOptions );
144    }
145
146    private function getParsoidOutput(
147        Title $pageTitle,
148        Bcp47Code $targetVariant,
149        User $user
150    ): ParserOutput {
151        $htmlOutputRendererHelper = $this->newPageRestHelperFactory()->newHtmlOutputRendererHelper( $pageTitle, [
152            'stash' => false,
153            'flavor' => 'view',
154        ], $user );
155        $htmlOutputRendererHelper->setVariantConversionLanguage( $targetVariant );
156
157        return $htmlOutputRendererHelper->getHtml();
158    }
159
160    private function getWords( string $output ): array {
161        $tagsRemoved = strip_tags( $output );
162        $words = preg_split( '/\s+/', trim( $tagsRemoved ), -1, PREG_SPLIT_NO_EMPTY );
163        return $words;
164    }
165
166    private function getBody( string $output ): string {
167        $dom = new DOMDocument();
168        // phpcs:disable Generic.PHP.NoSilencedErrors.Discouraged
169        @$dom->loadHTML( $output );
170        $body = $dom->getElementsByTagName( 'body' )->item( 0 );
171        if ( $body === null ) {
172            // Body element not present
173            return $output;
174        }
175
176        return $body->textContent;
177    }
178
179    private function compareOutput(
180        ParserOutput $parserOutput,
181        ParserOutput $parsoidOutput,
182        string $converterUsed
183    ): void {
184        $parsoidText = $parsoidOutput->getText( [ 'deduplicateStyles' => false ] );
185        $parserText = $parserOutput->getText( [ 'deduplicateStyles' => false ] );
186
187        $parsoidWords = $this->getWords( $this->getBody( $parsoidText ) );
188        $parserWords = $this->getWords( $parserText );
189
190        $parserWordCount = count( $parserWords );
191        $parsoidWordCount = count( $parsoidWords );
192        $this->output( "Word count: Parsoid: $parsoidWordCount; Parser: $parserWordCount\n" );
193
194        $this->outputSimilarity( $parsoidWords, $parserWords );
195        $this->output( "\n" );
196        $this->outputDiff( $parsoidWords, $parserWords, $converterUsed );
197    }
198
199    private function getConverterUsed( ParserOutput $parsoidOutput ): string {
200        $isCoreConverterUsed = strpos(
201            $parsoidOutput->getRawText(),
202            'Variant conversion performed using the core LanguageConverter'
203        );
204
205        if ( $isCoreConverterUsed ) {
206            return 'Core LanguageConverter';
207        } else {
208            return 'Parsoid LanguageConverter';
209        }
210    }
211
212    // Inspired from: https://stackoverflow.com/a/55927237/903324
213    private function mb_sprintf( string $format, ...$args ): string {
214        $params = $args;
215
216        return sprintf(
217            preg_replace_callback(
218                '/(?<=%|%-)\d+(?=s)/',
219                static function ( array $matches ) use ( &$params ) {
220                    $value = array_shift( $params );
221
222                    return (string)( strlen( $value ) - mb_strlen( $value ) + $matches[0] );
223                },
224                $format
225            ),
226            ...$args
227        );
228    }
229
230    private function outputSimilarity( array $parsoidWords, array $parserWords ): void {
231        $parsoidOutput = implode( ' ', $parsoidWords );
232        $parserOutput = implode( ' ', $parserWords );
233        $this->output(
234            'Total characters: Parsoid: ' . strlen( $parsoidOutput ) .
235            '; Parser: ' . strlen( $parserOutput ) . "\n"
236        );
237
238        $similarityPercent = 0;
239        $similarCharacters = similar_text( $parsoidOutput, $parserOutput, $similarityPercent );
240        $similarityPercent = round( $similarityPercent, 2 );
241
242        $this->output(
243            "Similarity via similar_text(): $similarityPercent%; Similar characters: $similarCharacters"
244        );
245    }
246
247    private function outputDiff( array $parsoidWords, array $parserWords, string $converterUsed ): void {
248        $out = str_repeat( '-', 96 ) . "\n";
249        $out .= sprintf( "| %5s | %-35s | %-35s | %-8s |\n", 'Line', 'Parsoid', 'Parser', 'Diff' );
250        $out .= sprintf( "| %5s | %-35s | %-35s | %-8s |\n", '', "($converterUsed)", '', '' );
251        $out .= str_repeat( '-', 96 ) . "\n";
252
253        try {
254            $diff = new Diff( $parsoidWords, $parserWords );
255        } catch ( ComplexityException $e ) {
256            $this->output( $e->getMessage() );
257            $this->error( 'Encountered ComplexityException while computing diff' );
258        }
259
260        // Print the difference between the words
261        $wordDiffFormat = ( new ArrayDiffFormatter() )->format( $diff );
262        foreach ( $wordDiffFormat as $index => $wordDiff ) {
263            $action = $wordDiff['action'];
264            $old = $wordDiff['old'] ?? null;
265            $new = $wordDiff['new'] ?? null;
266
267            $out .= $this->mb_sprintf(
268                "| %5s | %-35s | %-35s | %-8s |\n",
269                str_pad( (string)( $index + 1 ), 5, ' ', STR_PAD_LEFT ),
270                mb_strimwidth( $old ?? '- N/A -', 0, 35, '…' ),
271                mb_strimwidth( $new ?? '- N/A -', 0, 35, '…' ),
272                $action
273            );
274        }
275
276        // Print the footer.
277        $out .= str_repeat( '-', 96 ) . "\n";
278        $this->output( "\n" . $out );
279    }
280}
281
282$maintClass = CompareLanguageConverterOutput::class;
283require_once RUN_MAINTENANCE_IF_MAIN;