Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 109
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialMathStatus
0.00% covered (danger)
0.00%
0 / 109
0.00% covered (danger)
0.00%
0 / 15
1260
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
56
 runNativeTest
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 runMathMLTest
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 runMathLaTeXMLTest
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 testSpecialCaseText
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 testMathMLIntegration
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 testPmmlInput
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 testLaTeXMLIntegration
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 testLaTeXMLLinebreak
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 assertTrue
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 assertContains
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 assertEquals
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 printDiff
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\Math;
4
5use MediaWiki\Exception\UserNotLoggedIn;
6use MediaWiki\Extension\Math\Render\RendererFactory;
7use MediaWiki\Extension\Math\Widget\MathTestInputForm;
8use MediaWiki\Logger\LoggerFactory;
9use MediaWiki\Message\Message;
10use MediaWiki\Registration\ExtensionRegistry;
11use MediaWiki\SpecialPage\UnlistedSpecialPage;
12use Psr\Log\LoggerInterface;
13
14/**
15 * MediaWiki math extension
16 *
17 * @copyright 2002-2015 Tomasz Wegrzanowski, Brooke Vibber, Moritz Schubotz,
18 * and other MediaWiki contributors
19 * @license GPL-2.0-or-later
20 * @author Moritz Schubotz
21 */
22class SpecialMathStatus extends UnlistedSpecialPage {
23    private readonly LoggerInterface $logger;
24
25    public function __construct(
26        private readonly MathConfig $mathConfig,
27        private readonly RendererFactory $rendererFactory,
28    ) {
29        parent::__construct( 'MathStatus' );
30
31        $this->logger = LoggerFactory::getInstance( 'Math' );
32    }
33
34    /**
35     * @param null|string $query
36     */
37    public function execute( $query ) {
38        $this->setHeaders();
39
40        if ( !( $this->getUser()->isNamed() ) ) {
41            // This page is primarily of interest to developers.
42            // This action is comparable to previewing or parsing a small snippet of wikitext.
43            // If using RESTBase instead of native MML, this page makes HTTP requests to it.
44            // Optimization: Avoid uncached math parsing for logged-out users.
45            throw new UserNotLoggedIn();
46        }
47
48        $out = $this->getOutput();
49        $enabledMathModes = $this->mathConfig->getValidRenderingModeNames();
50        $req = $this->getRequest();
51        $tex = $req->getText( 'wptex' );
52
53        if ( $tex === '' ) {
54            $out->addWikiMsg( 'math-status-introduction', count( $enabledMathModes ) );
55
56            foreach ( $enabledMathModes as $modeNr => $modeName ) {
57                $out->wrapWikiMsg( '=== $1 ===', $modeName );
58                $this->getOutput()->addWikiMsgArray( 'math-test-start', [ $modeName ] );
59                switch ( $modeNr ) {
60                    case MathConfig::MODE_MATHML:
61                        $this->runMathMLTest();
62                        break;
63                    case MathConfig::MODE_LATEXML:
64                        $this->runMathLaTeXMLTest();
65                        break;
66                    case MathConfig::MODE_NATIVE_MML:
67                        $this->runNativeTest( $modeName );
68                }
69                $this->getOutput()->addWikiMsgArray( 'math-test-end', [ $modeName ] );
70            }
71        }
72
73        $form = new MathTestInputForm( $this, $enabledMathModes, $this->rendererFactory );
74        $form->show();
75    }
76
77    private function runNativeTest( Message $modeMessage ): void {
78        $renderer = $this->rendererFactory->getRenderer( "a+b", [], MathConfig::MODE_NATIVE_MML );
79        if ( !$this->assertTrue( $renderer->render(), 'Rendering of a+b in ' . $modeMessage->parse() ) ) {
80            return;
81        }
82        $real = str_replace( "\n", '', $renderer->getHtmlOutput() );
83        $expected = '<mo stretchy="false">+</mo>';
84        $this->assertContains( $expected, $real, "Checking the presence of '+' in the MathML output" );
85    }
86
87    private function runMathMLTest(): void {
88        $this->testSpecialCaseText();
89        $this->testMathMLIntegration();
90        $this->testPmmlInput();
91    }
92
93    private function runMathLaTeXMLTest(): void {
94        $this->testLaTeXMLIntegration();
95        $this->testLaTeXMLLinebreak();
96    }
97
98    public function testSpecialCaseText() {
99        $renderer = $this->rendererFactory->getRenderer( 'x^2+\text{a sample Text}', [], MathConfig::MODE_MATHML );
100        $expected = 'a sample Text</mtext>';
101        if ( !$this->assertTrue( $renderer->render(), 'Rendering the input "x^2+\text{a sample Text}"' ) ) {
102            return;
103        }
104        $this->assertContains(
105            $expected, $renderer->getHtmlOutput(), 'Comparing to the reference rendering'
106        );
107    }
108
109    /**
110     * Checks the basic functionality
111     * i.e. if the span element is generated right.
112     */
113    public function testMathMLIntegration() {
114        $renderer = $this->rendererFactory->getRenderer( "a+b", [], MathConfig::MODE_MATHML );
115        if ( !$this->assertTrue( $renderer->render(), "Rendering of a+b in plain MathML mode" ) ) {
116            return;
117        }
118        $real = str_replace( "\n", '', $renderer->getHtmlOutput() );
119        $expected = '<mo>+</mo>';
120        $this->assertContains( $expected, $real, "Checking the presence of '+' in the MathML output" );
121        $this->assertContains(
122            '<svg xmlns:xlink="http://www.w3.org/1999/xlink" ',
123            $renderer->getSvg(),
124            "Check that the generated SVG image contains the xlink namespace"
125        );
126    }
127
128    /**
129     * Checks the experimental option to 'render' MathML input
130     */
131    public function testPmmlInput() {
132        // sample from 'Navajo Coal Combustion and Respiratory Health Near Shiprock,
133        // New Mexico' in ''Journal of Environmental and Public Health'' , vol. 2010p.
134        // authors  Joseph E. Bunnell;  Linda V. Garcia;  Jill M. Furst;
135        // Harry Lerch;  Ricardo A. Olea;  Stephen E. Suitt;  Allan Kolker
136        // phpcs:ignore Generic.Files.LineLength.TooLong
137        $inputSample = '<msub>  <mrow>  <mi> P</mi> </mrow>  <mrow>  <mi> i</mi>  <mi> j</mi> </mrow> </msub>  <mo> =</mo>  <mfrac>  <mrow>  <mn> 100</mn>  <msub>  <mrow>  <mi> d</mi> </mrow>  <mrow>  <mi> i</mi>  <mi> j</mi> </mrow> </msub> </mrow>  <mrow>  <mn> 6.75</mn>  <msub>  <mrow>  <mi> r</mi> </mrow>  <mrow>  <mi> j</mi> </mrow> </msub> </mrow> </mfrac>  <mo> ,</mo> </math>';
138        $attribs = [ 'type' => 'pmml' ];
139        $renderer = $this->rendererFactory->getRenderer( $inputSample, $attribs, MathConfig::MODE_MATHML );
140        $this->assertEquals( 'pmml', $renderer->getInputType(), 'Checking if MathML input is supported' );
141        if ( !$this->assertTrue( $renderer->render(), 'Rendering Presentation MathML sample' ) ) {
142            return;
143        }
144        $real = $renderer->getHtmlOutput();
145        $this->assertContains( 'mode=mathml', $real, 'Checking if the link to SVG image is in correct mode' );
146    }
147
148    /**
149     * Checks the basic functionality
150     * i.e. if the span element is generated right.
151     */
152    public function testLaTeXMLIntegration() {
153        $renderer = $this->rendererFactory->getRenderer( "a+b", [], MathConfig::MODE_LATEXML );
154        if ( !$this->assertTrue( $renderer->render(), "Rendering of a+b in LaTeXML mode" ) ) {
155            return;
156        }
157        // phpcs:ignore Generic.Files.LineLength.TooLong
158        $expected = '<math xmlns="http://www.w3.org/1998/Math/MathML" ';
159        $real = preg_replace( "/\n\\s*/", '', $renderer->getHtmlOutput() );
160        $this->assertContains( $expected, $real,
161            "Comparing the output to the MathML reference rendering" .
162              $renderer->getLastError() );
163    }
164
165    /**
166     * Checks LaTeXML line break functionality
167     * i.e. if a long line contains a mtr element.
168     * http://www.w3.org/TR/REC-MathML/chap3_5.html#sec3.5.2
169     */
170    public function testLaTeXMLLinebreak() {
171        $mathDefaultLaTeXMLSetting = $this->getConfig()->get( 'MathDefaultLaTeXMLSetting' );
172        $tex = '';
173        $testMax = ceil( $mathDefaultLaTeXMLSetting[ 'linelength' ] / 2 );
174        for ( $i = 0; $i < $testMax; $i++ ) {
175            $tex .= "$i+";
176        }
177        $tex .= $testMax;
178        $renderer = new MathLaTeXML( $tex, [ 'display' => 'linebreak' ] );
179        $renderer->setPurge();
180        if ( !$this->assertTrue( $renderer->render(), "Rendering of linebreak test in LaTeXML mode" ) ) {
181            return;
182        }
183        $expected = 'mtr';
184        $real = preg_replace( "/\n\\s*/", '', $renderer->getHtmlOutput() );
185        $this->assertContains( $expected, $real, "Checking for linebreak" .
186              $renderer->getLastError() );
187    }
188
189    private function assertTrue( bool $expression, string $message = '' ): bool {
190        if ( $expression ) {
191            $this->getOutput()->addWikiMsgArray( 'math-test-success', [ $message ] );
192            return true;
193        } else {
194            $this->getOutput()->addWikiMsgArray( 'math-test-fail', [ $message ] );
195            return false;
196        }
197    }
198
199    private function assertContains( string $expected, string $real, string $message = '' ) {
200        if ( !$this->assertTrue( strpos( $real, $expected ) !== false, $message ) ) {
201            $this->printDiff( $expected, $real, 'math-test-contains-diff' );
202        }
203    }
204
205    /**
206     * @param array|string $expected
207     * @param array|string $real
208     * @param string $message
209     */
210    private function assertEquals( $expected, $real, string $message = '' ): bool {
211        if ( is_array( $expected ) ) {
212            foreach ( $expected as $alternative ) {
213                if ( $alternative === $real ) {
214                    $this->getOutput()->addWikiMsgArray( 'math-test-success', [ $message ] );
215                    return true;
216                }
217            }
218            // non of the alternatives matched
219            $this->getOutput()->addWikiMsgArray( 'math-test-fail', [ $message ] );
220            return false;
221        }
222        if ( !$this->assertTrue( $expected === $real, $message ) ) {
223            $this->printDiff( $expected, $real, 'math-test-equals-diff' );
224            return false;
225        }
226        return true;
227    }
228
229    private function printDiff( string $expected, string $real, string $message = '' ) {
230        if ( ExtensionRegistry::getInstance()->isLoaded( "SyntaxHighlight" ) ) {
231            $expected = "<syntaxhighlight lang=\"xml\">$expected</syntaxhighlight>";
232            $real = "<syntaxhighlight lang=\"xml\">$real</syntaxhighlight>";
233            $this->getOutput()->addWikiMsgArray( $message, [ $real, $expected ] );
234        } else {
235            $this->logger->warning( 'Can not display expected and real value.' .
236                'SyntaxHighlight is not installed.' );
237        }
238    }
239
240    protected function getGroupName(): string {
241        return 'other';
242    }
243}