Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 252
0.00% covered (danger)
0.00%
0 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialMathSearch
0.00% covered (danger)
0.00%
0 / 252
0.00% covered (danger)
0.00%
0 / 19
4290
0.00% covered (danger)
0.00%
0 / 1
 exception_error_handler
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
56
 searchForm
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
2
 getSearchRows
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
12
 processInput
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 performSearch
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
132
 displayMathElements
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
90
 getElementById
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 printTerm
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 highlightHit
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 printSource
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 displayRevisionResults
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
56
 render
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 addTerm
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 enableMathStyles
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 addFormData
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getDefault
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3use MediaWiki\Extension\Math\MathLaTeXML;
4use MediaWiki\Extension\Math\MathMathML;
5use MediaWiki\HTMLForm\Field\HTMLSelectField;
6use MediaWiki\HTMLForm\HTMLForm;
7use MediaWiki\Logger\LoggerFactory;
8use MediaWiki\MediaWikiServices;
9use MediaWiki\SpecialPage\SpecialPage;
10
11/**
12 * MediaWiki MathSearch extension
13 *
14 * (c) 2012 Moritz Schubotz
15 * GPLv2 license; info in main package.
16 *
17 * @file
18 * @ingroup extensions
19 */
20class SpecialMathSearch extends SpecialPage {
21
22    private const GUI_PATH = '/modules/min/index.xhtml';
23
24    /** @var string */
25    private $mathpattern;
26    /** @var string */
27    private $textpattern;
28    /** @var string */
29    private $mathmlquery;
30    /** @var string */
31    private $mathEngine = 'basex';
32    /** @var string */
33    private $displayQuery;
34    /** @var MathEngineRest */
35    private $mathBackend;
36    /** @var int */
37    private $resultID = 0;
38    /** @var int|string */
39    private $noTerms = 1;
40    /** @var array<int,MathSearchTerm> */
41    private $terms = [];
42    /** @var int[] */
43    private $relevanceMap;
44    /** @var array<int,array<string,int|string>> */
45    private $defaults;
46
47    public static function exception_error_handler( $errno, $errstr, $errfile, $errline ) {
48        if ( !( error_reporting() & $errno ) ) {
49            // This error code is not included in error_reporting
50            return;
51        }
52        throw new ErrorException( $errstr, 0, $errno, $errfile, $errline );
53    }
54
55    function __construct() {
56        parent::__construct( 'MathSearch' );
57    }
58
59    /**
60     * The main function
61     * @param string|null $par
62     */
63    public function execute( $par ) {
64        set_error_handler( "SpecialMathSearch::exception_error_handler" );
65        $request = $this->getRequest();
66        $this->setHeaders();
67        $this->mathpattern = $request->getText( 'mathpattern', '' );
68        $this->textpattern = $request->getText( 'textpattern', '' );
69        $this->noTerms = $request->getText( 'wpnoTerms', $this->noTerms );
70        $isEncoded = $request->getBool( 'isEncoded', false );
71        if ( $isEncoded ) {
72            $this->mathpattern = htmlspecialchars_decode( $this->mathpattern );
73        }
74        if ( $this->mathpattern || $this->textpattern ) {
75            $i = 0;
76            if ( $this->mathpattern ) {
77                $i++;
78                $this->addFormData( $this->mathpattern, $i,
79                    MathSearchTerm::TYPE_MATH, MathSearchTerm::REL_AND );
80            }
81            if ( $this->textpattern ) {
82                $i++;
83                $this->addFormData( $this->textpattern, $i,
84                    MathSearchTerm::TYPE_TEXT, MathSearchTerm::REL_AND );
85            }
86            $this->noTerms = $i;
87            $form = $this->searchForm();
88            $form->prepareForm();
89            $res = $form->trySubmit();
90            $this->getOutput()->addHTML( $form->getHTML( $res ) );
91            // $this->performSearch();
92            // $this->performSearch();
93        } else {
94            $this->searchForm()->show();
95            if ( file_exists( __DIR__ . self::GUI_PATH ) ) {
96                $minurl = $this->getConfig()->get( 'ExtensionAssetsPath' ) . '/MathSearch' . self::GUI_PATH;
97                $this->getOutput()
98                    ->addHTML( "<p><a href=\"{$minurl}\">Test experimental math input interface</a></p>" );
99            }
100        }
101        restore_error_handler();
102    }
103
104    /**
105     * Generates the search input form
106     *
107     * @return HTMLForm
108     */
109    private function searchForm() {
110        # A formDescriptor Array to tell HTMLForm what to build
111        $formDescriptor = [
112            'mathEngine' => [
113                'label' => 'Math engine',
114                'class' => HTMLSelectField::class,
115                'options' => [
116                    'MathWebSearch' => 'mws',
117                    'BaseX' => 'basex'
118                ],
119                'default' => $this->mathEngine,
120            ],
121            'displayQuery' => [
122                'label' => 'Display search query',
123                'type' => 'check',
124                'default' => $this->displayQuery,
125            ],
126            'noTerms' => [
127                'label' => 'Number of search terms',
128                'type' => 'int',
129                'min' => 1,
130                'default' => $this->noTerms,
131            ],
132        ];
133        $formDescriptor = array_merge( $formDescriptor, $this->getSearchRows( $this->noTerms ) );
134        $htmlForm =    new HTMLForm( $formDescriptor, $this->getContext() );
135        $htmlForm->setSubmitText( 'Search' );
136        $htmlForm->setSubmitCallback( [ $this, 'processInput' ] );
137        $htmlForm->setHeaderHtml( "<h2>Input</h2>" );
138        // $htmlForm->show();
139        return $htmlForm;
140    }
141
142    private function getSearchRows( $cnt ) {
143        $out = [];
144        for ( $i = 1; $i <= $cnt; $i++ ) {
145            if ( $i == 1 ) {
146                // Hide the meaningless first relation from the user
147                $relType = 'hidden';
148            } else {
149                $relType = 'select';
150            }
151            $out["rel-$i"] = [
152                'label-message' => 'math-search-relation-label',
153                'options' => [
154                    $this->msg( 'math-search-relation-0' )->text() => 0,
155                    $this->msg( 'math-search-relation-1' )->text() => 1,
156                    $this->msg( 'math-search-relation-2' )->text() => 2 // ,
157                    // 'nor' => 3
158                ],
159                'type' => $relType,
160                'default' => $this->getDefault( $i, 'rel' ),
161                'section' => "term $i" // TODO: figure out how to localize section with parameter
162            ];
163            $out["type-$i"] = [
164                'label-message' => 'math-search-type-label',
165                'options' => [
166                    $this->msg( 'math-search-type-0' )->text() => 0,
167                    $this->msg( 'math-search-type-1' )->text() => 1,
168                    $this->msg( 'math-search-type-2' )->text() => 2
169                ],
170                'type' => 'select',
171                'section' => "term $i",
172                'default' => $this->getDefault( $i, 'type' )
173            ];
174            $out["expr-$i"] = [
175                'label-message' => 'math-search-expression-label',
176                'type' => 'text',
177                'section' => "term $i",
178                'default' => $this->getDefault( $i, 'expr' )
179            ];
180        }
181        return $out;
182    }
183
184    /**
185     * Processes the submitted Form input
186     * @param array $formData
187     * @return bool
188     */
189    public function processInput( $formData ) {
190        if ( $formData['noTerms'] != $this->noTerms ) {
191            $this->noTerms = $formData['noTerms'];
192            $this->searchForm();
193            return true;
194        }
195
196        for ( $i = 1; $i <= $this->noTerms; $i++ ) {
197            $this->addTerm( $i, $formData["rel-$i"], $formData["type-$i"],
198                $formData["expr-$i"] );
199        }
200
201        $this->displayQuery = $formData['displayQuery'];
202        $this->performSearch();
203    }
204
205    public function performSearch() {
206        $out = $this->getOutput();
207        $out->addWikiTextAsInterface( '==Results==' );
208        $out->addWikiTextAsInterface( 'You searched for the following terms:' );
209        switch ( $this->mathEngine ) {
210            case 'basex':
211                $this->mathBackend = new MathEngineBaseX( null );
212                break;
213            default:
214                $this->mathBackend = new MathEngineMws( null );
215        }
216        /** @var MathSearchTerm $term */
217        foreach ( $this->terms as $term ) {
218            if ( $term->getExpr() == "" ) {
219                continue;
220            }
221            $term->doSearch( $this->mathBackend );
222            $this->enableMathStyles();
223            $this->printTerm( $term );
224            if ( $term->getKey() == 1 ) {
225                $this->relevanceMap = $term->getRelevanceMap();
226
227            } else {
228                switch ( $term->getRel() ) {
229                    case $term::REL_AND:
230                        $this->relevanceMap =
231                            array_intersect( $this->relevanceMap, $term->getRelevanceMap() );
232                        break;
233                    case $term::REL_OR:
234                        $this->relevanceMap = $this->relevanceMap + $term->getRelevanceMap();
235                        break;
236                    case $term::REL_NAND:
237                        $this->relevanceMap =
238                            array_diff( $this->relevanceMap, $term->getRelevanceMap() );
239                    // case $term::REL_NOR: (too many results)
240                }
241            }
242        }
243        $formulaCount = 0;
244        if ( $this->relevanceMap != null ) {
245            $formulaCount = count( $this->relevanceMap, true );
246            foreach ( $this->relevanceMap as $revisionID ) {
247                $this->displayRevisionResults( $revisionID );
248            }
249        }
250        $this->getOutput()->addWikiTextAsInterface( "In total " . $formulaCount . ' results.' );
251    }
252
253    /**
254     * @param int $revisionID
255     * @param array[] $mathElements
256     * @param string $pagename
257     */
258    public function displayMathElements( $revisionID, $mathElements, $pagename ) {
259        $out = $this->getOutput();
260        foreach ( $mathElements as $anchorID => $answ ) {
261            $res = MathObject::constructformpage( $revisionID, $anchorID );
262            if ( !$res ) {
263                LoggerFactory::getInstance(
264                    'MathSearch'
265                )->error( "Failure: Could not get entry $anchorID for page $pagename (id $revisionID)" );
266                return;
267            }
268            $mml = $res->getMathml();
269            if ( !$mml ) {
270                LoggerFactory::getInstance( 'MathSearch' )
271                    ->error( "Failure: Could not get MathML $anchorID for page $pagename (id $revisionID)" );
272                continue;
273            }
274            $out->addWikiTextAsInterface( "====[[$pagename#$anchorID|Eq: $anchorID (Result " .
275                $this->resultID++ . ")]]====", false );
276            $out->addHTML( "<br />" );
277            $xpath = $answ[0]['xpath'];
278            // TODO: Remove hack and report to Prode that he fixes that
279            // $xmml->registerXPathNamespace('m', 'http://www.w3.org/1998/Math/MathML');
280            $xpath =
281                str_replace( '/m:semantics/m:annotation-xml[@encoding="MathML-Content"]',
282                    '', $xpath );
283            $dom = new DOMDocument;
284            $dom->preserveWhiteSpace = false;
285            $dom->validateOnParse = true;
286            $dom->loadXML( $mml );
287            $DOMx = new DOMXpath( $dom );
288            $hits = $DOMx->query( $xpath );
289            if ( $this->getConfig()->get( 'MathDebug' ) ) {
290                LoggerFactory::getInstance( 'MathSearch' )->debug( 'xPATH:' . $xpath );
291            }
292            if ( $hits !== null && $hits ) {
293                foreach ( $hits as $node ) {
294                    $this->highlightHit( $node, $dom, $mml );
295                }
296            }
297            if ( $mml != $res->getMathml() ) {
298                $renderer = new MathMathML( $mml, [ 'type' => 'pmml' ] );
299                $renderer->setMathml( $mml );
300                $renderer->render();
301                $out->addHTML( $renderer->getHtmlOutput() );
302                $renderer->writeCache();
303            } else {
304                $res->render();
305                $out->addHTML( $res->getHtmlOutput() );
306            }
307
308        }
309    }
310
311    /**
312     * Note that the default getElementById function
313     * <code>
314     *  $dom->getElementById( $id );
315     * </code>
316     * works for "xml:id" only,  but not for "id" which is extended to "math:id"
317     *     TODO: could be fixed with
318     * @link http://php.net/manual/de/domdocument.getelementbyid.php#86056
319     * @param string $id
320     * @param DOMDocument $doc
321     * @return DOMElement|null
322     */
323    private function getElementById( $id, $doc ) {
324        $xpath = new DOMXPath( $doc );
325        return $xpath->query( "//*[@id='$id']" )->item( 0 );
326    }
327
328    /**
329     * @param MathSearchTerm $term
330     */
331    public function printTerm( $term ) {
332        if ( $term->getType() == MathSearchTerm::TYPE_MATH ) {
333            $expr = "<math>{$term->getExpr()}</math>";
334        } else {
335            $expr = "<code>{$term->getExpr()}</code>";
336        }
337        $this->getOutput()->addWikiMsg( 'math-search-term',
338            $term->getKey(),
339            $expr,
340            $this->msg( "math-search-type-{$term->getType()}" )->text(),
341            $term->getRel() == '' ? '' : $this->msg( "math-search-relation-{$term->getRel()}" )->text(),
342            count( $term->getRelevanceMap() ) );
343    }
344
345    /**
346     * @param DOMNode $node
347     * @param DOMDocument $dom
348     * @param string &$mml
349     */
350    protected function highlightHit( $node, $dom, &$mml ) {
351        if ( $node == null || !$node->hasAttributes() ) {
352            return;
353        }
354        try {
355            $xRef = $node->attributes->getNamedItem( 'xref' );
356            if ( $xRef ) {
357                $domRes = $this->getElementById( $xRef->nodeValue, $dom );
358                if ( $domRes ) {
359                    $domRes->setAttribute( 'mathcolor', '#cc0000' );
360                    $mml = $domRes->ownerDocument->saveXML();
361                }
362            } else {
363                // CMML node has no corresponding PMML element
364                $fallback = $node->parentNode;
365                $this->highlightHit( $fallback, $dom, $mml );
366            }
367        } catch ( Exception $e ) {
368            LoggerFactory::getInstance(
369                'MathSearch'
370            )->error( 'Problem highlighting hit ' . $e->getMessage() );
371        }
372    }
373
374    /**
375     * @param string $src
376     * @param string $lang the language of the source snippet
377     */
378    private function printSource( $src, $lang = "xml" ) {
379        $out = $this->getOutput();
380        $out->addWikiTextAsInterface( '<source lang="' . $lang . '">' . $src . '</source>' );
381    }
382
383    /**
384     * Displays the equations for one page
385     *
386     * @param int $revisionID
387     *
388     * @return bool
389     */
390    function displayRevisionResults( $revisionID ) {
391        $out = $this->getOutput();
392        $revisionStoreRecord = MediaWikiServices::getInstance()->getRevisionLookup()->getRevisionById( $revisionID );
393        if ( !$revisionStoreRecord ) {
394            LoggerFactory::getInstance( 'MathSearch' )->error( 'invalid revision number' );
395            return false;
396        }
397        $title = $revisionStoreRecord->getPageAsLinkTarget(); # MCR migration note: this replaced Revision::getTitle
398        $pagename = (string)$title;
399        $mathElements = [];
400        $textElements = [];
401        /** @var MathSearchTerm $term */
402        foreach ( $this->terms as $term ) {
403            if ( $term->getExpr() == "" ) {
404                continue;
405            }
406            if ( $term->getType() == MathSearchTerm::TYPE_MATH ) {
407                $mathElements += $term->getRevisionResult( $revisionID );
408            } elseif ( $term->getType() == MathSearchTerm::TYPE_TEXT ) { // Forward compatible
409                /** @var SearchResult $textResult */
410                $textResult = $term->getRevisionResult( $revisionID );
411                // see: T90976
412                $textElements[] = $textResult->getTextSnippet( [ $term->getExpr() ] );
413                // $textElements[]=$textResult->getSectionSnippet();
414            }
415        }
416        $out->addWikiTextAsInterface( "=== [[Special:Permalink/$revisionID | $pagename]] ===" );
417        foreach ( $textElements as $textResult ) {
418            $out->addWikiTextAsInterface( $textResult );
419        }
420        LoggerFactory::getInstance( 'MathSearch' )->warning( "Processing results for $pagename" );
421        $this->displayMathElements( $revisionID, $mathElements, $pagename );
422        return true;
423    }
424
425    /**
426     * Renders the math search input to mathml
427     * @return bool
428     */
429    function render() {
430        $renderer = new MathLaTeXML( $this->mathpattern );
431        $renderer->setLaTeXMLSettings( 'profile=mwsquery' );
432        $renderer->setAllowedRootElements( [ 'query' ] );
433        $renderer->render( true );
434        $this->mathmlquery = $renderer->getMathml();
435
436        return $this->mathmlquery !== '';
437    }
438
439    /**
440     * @param int $i
441     * @param int $rel
442     * @param int $type
443     * @param string $expr
444     */
445    private function addTerm( $i, $rel, $type, $expr ) {
446        $this->terms[ $i ] = new MathSearchTerm( $i, $rel, $type, $expr );
447    }
448
449    private function enableMathStyles() {
450        $out = $this->getOutput();
451        $out->addModuleStyles(
452            [ 'ext.math.styles', 'ext.math.desktop.styles', 'ext.math.scripts' ]
453        );
454    }
455
456    private function addFormData( $mathpattern, $i, $TYPE_MATH, $REL_AND ) {
457        $this->defaults[$i]['type'] = $TYPE_MATH;
458        $this->defaults[$i]['rel'] = $REL_AND;
459        $this->defaults[$i]['expr'] = $mathpattern;
460    }
461
462    /**
463     * @param int $i
464     * @param string $what
465     * @return int|string
466     */
467    private function getDefault( $i, $what ) {
468        if ( isset( $this->defaults[$i][$what] ) ) {
469            return $this->defaults[$i][$what];
470        } else {
471            switch ( $what ) {
472                case 'expr':
473                    return '';
474                case 'type':
475                    return MathSearchTerm::TYPE_MATH;
476                case 'rel':
477                    return MathSearchTerm::REL_AND;
478            }
479            return "";
480        }
481    }
482
483    protected function getGroupName() {
484        return 'mathsearch';
485    }
486}