Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 217
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialLaTeXTranslator
0.00% covered (danger)
0.00%
0 / 217
0.00% covered (danger)
0.00%
0 / 15
1722
0.00% covered (danger)
0.00%
0 / 1
 log
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 __construct
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
30
 processInput
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getTranslations
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 calculateTranslations
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
6
 getDependencyGraphFromContext
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 calculateDependencyGraphFromContext
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
6
 printSource
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 displayResults
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
20
 printList
0.00% covered (danger)
0.00%
0 / 11
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
 printColHeader
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 printColFooter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 displayTests
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
72
1<?php
2
3use MediaWiki\Logger\LegacyLogger;
4use MediaWiki\Logger\LoggerFactory;
5use MediaWiki\MediaWikiServices;
6use MediaWiki\Revision\RevisionRecord;
7use MediaWiki\Revision\SlotRecord;
8use Psr\Log\LogLevel;
9
10class SpecialLaTeXTranslator extends SpecialPage {
11
12    private const VERSION = '1.0.0';
13
14    private $cache;
15    private $dgUrl;
16    private $compUrl;
17    private $httpFactory;
18    private $logger;
19    private $context;
20    private $tex;
21    /** @var bool */
22    private $purge;
23    /**
24     * @var false|mixed|string
25     */
26    private $dependencyGraph;
27
28    private function log( $level, $message, array $context = [] ) {
29        $this->logger->log( $level, $message, $context );
30        if ( $this->getConfig()->get( 'ShowDebug' ) ) {
31            $msg = LegacyLogger::interpolate( $message, $context );
32            $this->getOutput()->addWikiTextAsContent( "Log $level:" . $msg );
33        }
34    }
35
36    function __construct() {
37        parent::__construct( 'LaTeXTranslator' );
38        $mw = MediaWikiServices::getInstance();
39        $this->cache = $mw->getMainWANObjectCache();
40        // provisional Hack to get the URL
41        $provisionalUrl = $mw->getMainConfig()->get( 'MathSearchTranslationUrl' );
42        $this->dgUrl =
43            preg_replace( '/translation/', 'generateAnnotatedDependencyGraph', $provisionalUrl );
44        $this->compUrl =
45            preg_replace( '/translation/', 'generateTranslatedComputedMoi', $provisionalUrl );
46        $this->httpFactory = $mw->getHttpRequestFactory();
47        $this->logger = LoggerFactory::getInstance( 'MathSearch' );
48    }
49
50    /**
51     * Returns corresponding Mathematica translations of LaTeX functions
52     * @param string|null $par
53     */
54    function execute( $par ) {
55        $pid = $this->getRequest()->getVal( 'pid' ); // Page ID
56        $eid = $this->getRequest()->getVal( 'eid' ); // Equation ID
57        $this->setHeaders();
58        $output = $this->getOutput();
59        $output->addWikiMsg( 'math-tex2nb-intro' );
60        if ( $pid && $eid ) {
61            $revisionRecord =
62                MediaWikiServices::getInstance()->getRevisionLookup()->getRevisionById( $pid );
63            $contentModel =
64                $revisionRecord->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel();
65            if ( $contentModel !== CONTENT_MODEL_WIKITEXT ) {
66                throw new RuntimeException( "Only CONTENT_MODEL_WIKITEXT supported for translation." );
67            }
68
69            $content = $revisionRecord->getContent( SlotRecord::MAIN );
70            if ( !$content instanceof TextContent ) {
71                throw new RuntimeException( "Translation supports only TextContent" );
72            }
73            $this->context = $content->getText();
74            $mo = MathObject::newFromRevisionText( $pid, $eid );
75            $this->tex = $mo->getTex();
76            $this->displayResults();
77        } else {
78            $this->tex = '(z)_n = \frac{\Gamma(z+n)}{\Gamma(z)}';
79            $this->context = 'The Gamma function
80<math>\Gamma(z)</math>
81and the Pochhammer symbol
82<math>(a)_n</math>
83are often used together.';
84        }
85        $formDescriptor = [
86            'input' => [
87                'label-message' => 'math-tex2nb-input',
88                'class' => 'HTMLTextField',
89                'default' => $this->tex,
90            ],
91            'wikitext' => [
92                'label-message' => 'math-tex2nb-wikitext',
93                'class' => 'HTMLTextAreaField',
94                'default' => $this->context,
95            ],
96            'purge' => [
97                'label-message' => 'math-tex2nb-purge',
98                'class' => 'HTMLCheckField',
99            ],
100        ];
101        $htmlForm = new HTMLForm( $formDescriptor, $this->getContext() );
102        $htmlForm->setSubmitText( 'Translate' );
103        $htmlForm->setSubmitCallback( [ $this, 'processInput' ] );
104        $htmlForm->setHeaderText( '<h2>' . $this->msg( 'math-tex2nb-header' )->escaped() .
105            '</h2>' );
106        $htmlForm->show();
107    }
108
109    /**
110     * Processes the submitted Form input
111     * @param array $formData
112     * @return bool
113     */
114    public function processInput( $formData ) {
115        $this->tex = $formData['input'];
116        $this->context = $formData['wikitext'];
117        $this->purge = $formData['purge'];
118
119        return $this->displayResults();
120    }
121
122    /**
123     * @return false|mixed
124     */
125    private function getTranslations(): string {
126        $hash =
127            $this->cache->makeGlobalKey( self::class,
128                sha1( self::VERSION . '-F-' . $this->dependencyGraph . $this->tex ) );
129        if ( $this->purge ) {
130            $value = $this->calculateTranslations();
131            $this->cache->set( $hash, $value );
132        }
133
134        return $this->cache->getWithSetCallback( $hash, WANObjectCache::TTL_INDEFINITE,
135            [ $this, 'calculateTranslations' ] );
136    }
137
138    /**
139     * @return string
140     */
141    public function calculateTranslations(): string {
142        $this->log( LogLevel::INFO, "Cache miss. Calculate translation." );
143        $q = rawurlencode( $this->tex );
144        $url = "{$this->compUrl}?latex=$q";
145        $options = [
146            'method' => 'POST',
147            'postData' => $this->dependencyGraph,
148        ];
149        $req = $this->httpFactory->create( $url, $options, __METHOD__ );
150        $req->setHeader( 'Content-Type', 'application/json' );
151        $req->execute();
152        $statusCode = $req->getStatus();
153        if ( $statusCode === 200 ) {
154            return $req->getContent();
155        }
156        $e = new RuntimeException( 'Calculation endpoint failed. Error:' . $req->getContent() );
157        $this->logger->error( 'Calculation "{url}" returned ' .
158            'HTTP status code "{statusCode}" for post data "{postData}".: {exception}.', [
159            'url' => $url,
160            'statusCode' => $statusCode,
161            'postData' => $this->dependencyGraph,
162            'exception' => $e,
163        ] );
164        throw $e;
165    }
166
167    private function getDependencyGraphFromContext(): string {
168        $hash =
169            $this->cache->makeGlobalKey( self::class,
170                sha1( self::VERSION . '-DG-' . $this->context ) );
171        $this->log( LogLevel::DEBUG, "DG Hash is {hash}", [ 'hash' => $hash ] );
172        if ( $this->purge ) {
173            $this->log( LogLevel::INFO, 'Cache purging requested' );
174            $value = $this->calculateDependencyGraphFromContext();
175            $this->cache->set( $hash, $value );
176        }
177
178        return $this->cache->getWithSetCallback( $hash, 31556952,
179            [ $this, 'calculateDependencyGraphFromContext' ] );
180    }
181
182    /**
183     * @return false|mixed
184     */
185    public function calculateDependencyGraphFromContext(): string {
186        $this->log( LogLevel::INFO, "Cache miss. Calculate dependency graph." );
187        $url = $this->dgUrl;
188        $q = rawurlencode( $this->context );
189        $postData = "content=$q";
190        $options = [
191            'method' => 'POST',
192            'postData' => $postData,
193            'timeout' => 240,
194        ];
195        $req = $this->httpFactory->create( $url, $options, __METHOD__ );
196        $req->execute();
197        $statusCode = $req->getStatus();
198        if ( $statusCode === 200 ) {
199            return $req->getContent();
200        }
201        $e = new RuntimeException( 'Dependency graph endpoint failed.' );
202        $this->log( LogLevel::ERROR, 'Dependency graph "{url}" returned ' .
203            'HTTP status code "{statusCode}" for post data "{postData}": {exception}.', [
204            'url' => $url,
205            'statusCode' => $statusCode,
206            'postData' => $postData,
207            'exception' => $e,
208        ] );
209        throw $e;
210    }
211
212    private function printSource(
213        $source, $description = "", $language = "text", $linestart = true, $collapsible = true
214    ) {
215        $inline = ' inline ';
216        $out = $this->getOutput();
217        if ( $description ) {
218            $description .= ": ";
219        }
220        if ( $collapsible ) {
221            $this->printColHeader( $description );
222            $description = '';
223            $inline = '';
224        }
225        $out->addWikiTextAsInterface( "$description<syntaxhighlight lang=\"$language\" $inline>" .
226            $source .
227            '</syntaxhighlight>', $linestart );
228        if ( $collapsible ) {
229            $this->printColFooter();
230        }
231    }
232
233    /**
234     * @return bool
235     */
236    public function displayResults(): bool {
237        $output = $this->getOutput();
238        $output->addWikiMsg( 'math-tex2nb-latex' );
239        $this->printSource( $this->tex, '', 'latex', false, false );
240        $output->addWikiTextAsContent( "<math>$this->tex</math>", false );
241        $output->addWikiMsg( 'math-tex2nb-mathematica' );
242        try {
243            $this->dependencyGraph = $this->getDependencyGraphFromContext();
244            $calulation = $this->getTranslations();
245        } catch ( Exception $exception ) {
246            $expected_error =
247                'The given context (dependency graph) did not contain sufficient information';
248            if ( strpos( $exception->getText(), $expected_error ) !== false ) {
249                FormulaInfo::DisplayTranslations( $this->tex );
250
251                return false;
252            } else {
253                $output->addWikiTextAsContent( "Could not consider context: {$exception->getMessage()}" );
254                FormulaInfo::DisplayTranslations( $this->tex );
255
256                return false;
257            }
258        }
259        $insights = json_decode( $calulation );
260        $this->printSource( $insights->semanticFormula, "Semantic latex", 'latex', false, false );
261        $output->addWikiTextAsContent( "Confidence: " . $insights->confidence, false );
262
263        foreach ( $insights->translations as $key => $value ) {
264            $output->addWikiTextAsContent( "=== $key ===" );
265            $this->printSource( $value->translation, "Translation", '$key', false, false );
266            $output->addWikiTextAsContent( "==== Information ====" );
267            $info = $value->translationInformation;
268            $this->printList( $info->subEquations, "Sub Equations" );
269            $this->printList( $info->freeVariables, "Free variables" );
270            $this->printList( $info->constraints, "Constraints" );
271            $this->printList( $info->tokenTranslations, "Symbol info" );
272
273            // $this->printList($value);
274            $output->addWikiTextAsContent( "==== Tests ====" );
275            $output->addWikiTextAsContent( "=====  Symbolic =====" );
276
277            $this->displayTests( $value->symbolicResults->testCalculationsGroup );
278            $output->addWikiTextAsContent( "=====  Numeric =====" );
279
280            $this->displayTests( $value->numericResults->testCalculationsGroups );
281        }
282
283        $output->addWikiTextAsContent( "=== Dependency Graph Information ===" );
284        $mathprint = static function ( $x ) {
285            return "* <math>$x</math>";
286        };
287        $this->printList( $insights->includes, "Includes", $mathprint );
288        $this->printList( $insights->isPartOf, "Is part of", $mathprint );
289        $this->printList( $insights->definiens, 'Description',
290            static function ( $x ) { return "{$x->definition}";
291            } );
292        $this->printSource( $calulation, 'Complete translation information', 'json' );
293        return false;
294    }
295
296    private function printList( $list, $description, $callable = false ): void {
297        if ( !$list || empty( $list ) ) {
298            return;
299        }
300        $output = $this->getOutput();
301        $this->printColHeader( $description );
302        foreach ( $list as $key => $value ) {
303            if ( $callable === false ) {
304                $callable = static function ( $x, $y ) { return "$x";
305                };
306            }
307            $value = $callable( $value, $key );
308            $output->addWikiTextAsContent( $value );
309        }
310            $this->printColFooter();
311    }
312
313    protected function getGroupName() {
314        return 'mathsearch';
315    }
316
317    private function printColHeader( string $description ): void {
318        $out = $this->getOutput();
319        $out->addHTML( '<div class="toccolours mw-collapsible mw-collapsed"  style="text-align: left">' );
320        $out->addWikiTextAsContent( $description );
321        $out->addHTML( '<div class="mw-collapsible-content">' );
322    }
323
324    private function printColFooter(): void {
325        $this->getOutput()->addHTML( '</div></div>' );
326    }
327
328    private function displayTests( $group ) {
329        if ( !is_array( $group ) ) {
330            return;
331        }
332        foreach ( $group as $testGroup ) {
333            if ( !$testGroup->testExpression ) {
334                continue;
335            }
336            $this->printSource( $testGroup->testExpression, "Test expression", "text",
337                true, false );
338            foreach ( $testGroup->testCalculations as $calculation ) {
339                $calcRes = json_encode( $calculation, JSON_PRETTY_PRINT );
340                $description = $calculation->result;
341                if ( isset( $calculation->testExpression ) ) {
342                    $description .= ": " . $calculation->testExpression;
343                }
344                if ( isset( $calculation->testValues ) ) {
345                    $description .= ":";
346                    foreach ( $calculation->testValues as $k => $v ) {
347                        $description .= " $k = $v";
348                    }
349                }
350                $this->printSource( $calcRes, $description, 'json' );
351            }
352        }
353    }
354}