Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
6.67% covered (danger)
6.67%
5 / 75
10.00% covered (danger)
10.00%
2 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
MathIdGenerator
6.67% covered (danger)
6.67%
5 / 75
10.00% covered (danger)
10.00%
2 / 20
1275.63
0.00% covered (danger)
0.00%
0 / 1
 newFromRevisionRecord
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 getKeys
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 __construct
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 newFromRevisionId
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 newFromTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIdList
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 formatIds
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 parserKey2fId
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 getInputHash
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIdsFromContent
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getContentIdMap
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 guessIdFromContent
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 getMathTags
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getWikiText
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRevisionId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setUseCustomIds
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTagFromId
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
 getUniqueFromId
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
 formatKey
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getUserInputTex
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3use MediaWiki\Extension\Math\MathSource;
4use MediaWiki\MediaWikiServices;
5use MediaWiki\Parser\Parser;
6use MediaWiki\Parser\Sanitizer;
7use MediaWiki\Revision\RevisionRecord;
8use MediaWiki\Revision\SlotRecord;
9use MediaWiki\Title\Title;
10
11class MathIdGenerator {
12
13    public const CONTENT_POS = 1;
14    public const ATTRIB_POS = 2;
15
16    private string $wikiText;
17    /**
18     * @var array<string,array{0:string,1:string|null,2:array,3:string}> Filtered result from
19     *  {@see Parser::extractTagsAndParams} with only <math> tags
20     */
21    private array $mathTags;
22    private int $revisionId;
23    /** @var int[] */
24    private $contentAccessStats = [];
25    private string $format = "math.%d.%d";
26    private bool $useCustomIds = false;
27    /** @var int[]|null */
28    private $keys;
29    /** @var array<string,?string[]>|null */
30    private $contentIdMap;
31
32    public static function newFromRevisionRecord( RevisionRecord $revisionRecord ): MathIdGenerator {
33        $contentModel = $revisionRecord
34            ->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )
35            ->getModel();
36        if ( $contentModel !== CONTENT_MODEL_WIKITEXT ) {
37            throw new RuntimeException( "MathIdGenerator supports only CONTENT_MODEL_WIKITEXT" );
38        }
39        $content = $revisionRecord->getContent( SlotRecord::MAIN );
40        if ( !$content instanceof TextContent ) {
41            throw new RuntimeException( "MathIdGenerator supports only TextContent" );
42        }
43        return new self(
44            $content->getText(),
45            $revisionRecord->getId()
46        );
47    }
48
49    /**
50     * @return array<string,int> Array mapping key names to their position
51     */
52    public function getKeys() {
53        $this->keys ??= array_flip( array_keys( $this->mathTags ) );
54        return $this->keys;
55    }
56
57    public function __construct( string $wikiText, int $revisionId = 0 ) {
58        $wikiText = Sanitizer::removeHTMLcomments( $wikiText );
59        $this->wikiText =
60            Parser::extractTagsAndParams( [ 'nowki', 'syntaxhighlight', 'math' ], $wikiText,
61                $tags );
62        $this->mathTags = array_filter( $tags, static function ( $v ) {
63            return $v[0] === 'math';
64        } );
65        $this->revisionId = $revisionId;
66    }
67
68    public static function newFromRevisionId( int $revId ): MathIdGenerator {
69        $revisionRecord = MediaWikiServices::getInstance()
70            ->getRevisionLookup()
71            ->getRevisionById( $revId );
72
73        return self::newFromRevisionRecord( $revisionRecord );
74    }
75
76    public static function newFromTitle( Title $title ): MathIdGenerator {
77        return self::newFromRevisionId( $title->getLatestRevID() );
78    }
79
80    public function getIdList() {
81        return $this->formatIds( $this->mathTags );
82    }
83
84    /**
85     * @param array<string,mixed> $mathTags
86     *
87     * @return string[]
88     */
89    public function formatIds( $mathTags ) {
90        return array_map( [ $this, 'parserKey2fId' ], array_keys( $mathTags ) );
91    }
92
93    /**
94     * @param string $key
95     *
96     * @return string|null
97     */
98    public function parserKey2fId( $key ) {
99        if ( $this->useCustomIds ) {
100            if ( isset( $this->mathTags[$key][self::ATTRIB_POS]['id'] ) ) {
101                return $this->mathTags[$key][self::ATTRIB_POS]['id'];
102            }
103        }
104        if ( isset( $this->mathTags[$key] ) ) {
105            return $this->formatKey( $key );
106        }
107    }
108
109    public function getInputHash( $inputTex ) {
110        return pack( "H32", md5( $inputTex ) );
111    }
112
113    /**
114     * @param string $content
115     *
116     * @return ?string[]
117     */
118    public function getIdsFromContent( $content ) {
119        $contentIdMap = $this->getContentIdMap();
120        if ( array_key_exists( $content, $contentIdMap ) ) {
121            return $contentIdMap[$content];
122        }
123        return [];
124    }
125
126    /**
127     * @return array<string,?string[]>
128     */
129    public function getContentIdMap() {
130        if ( !$this->contentIdMap ) {
131            $this->contentIdMap = [];
132            foreach ( $this->mathTags as $key => $tag ) {
133                $userInputTex = $this->getUserInputTex( $tag );
134                $this->contentIdMap[$userInputTex][] = $this->parserKey2fId( $key );
135            }
136        }
137        return $this->contentIdMap;
138    }
139
140    public function guessIdFromContent( string $content ): ?string {
141        $allIds = $this->getIdsFromContent( $content );
142        $size = count( $allIds );
143        if ( $size == 0 ) {
144            return null;
145        }
146        if ( $size == 1 ) {
147            return $allIds[0];
148        }
149        if ( array_key_exists( $content, $this->contentAccessStats ) ) {
150            $this->contentAccessStats[$content]++;
151        } else {
152            $this->contentAccessStats[$content] = 0;
153        }
154        $currentIndex = $this->contentAccessStats[$content] % $size;
155        return $allIds[$currentIndex];
156    }
157
158    /**
159     * @return array
160     */
161    public function getMathTags(): array {
162        return $this->mathTags;
163    }
164
165    public function getWikiText(): string {
166        return $this->wikiText;
167    }
168
169    public function getRevisionId(): int {
170        return $this->revisionId;
171    }
172
173    public function setUseCustomIds( bool $useCustomIds ): void {
174        $this->useCustomIds = $useCustomIds;
175    }
176
177    /**
178     * @param string $eid
179     * @return array|null
180     */
181    public function getTagFromId( string $eid ): ?array {
182        foreach ( $this->mathTags as $key => $mathTag ) {
183            if ( $eid == $this->formatKey( $key ) ) {
184                return $mathTag;
185            }
186            if ( isset( $mathTag[self::ATTRIB_POS]['id'] ) && $eid == $mathTag[self::ATTRIB_POS]['id'] ) {
187                return $mathTag;
188            }
189        }
190        return null;
191    }
192
193    public function getUniqueFromId( string $eid ): ?string {
194        foreach ( $this->mathTags as $key => $mathTag ) {
195            if ( $eid == $this->formatKey( $key ) ) {
196                return $key;
197            }
198            if ( isset( $mathTag[self::ATTRIB_POS]['id'] ) && $eid == $mathTag[self::ATTRIB_POS]['id'] ) {
199                return $key;
200            }
201        }
202        return null;
203    }
204
205    private function formatKey( string $key ): string {
206        $keys = $this->getKeys();
207        return sprintf( $this->format, $this->revisionId, $keys[$key] );
208    }
209
210    /**
211     * @param array{1:string,2:array} $tag
212     * @return string
213     */
214    public function getUserInputTex( array $tag ): string {
215        return ( new MathSource( $tag[self::CONTENT_POS], $tag[self::ATTRIB_POS] ) )->getUserInputTex();
216    }
217}