Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.25% covered (success)
96.25%
77 / 80
66.67% covered (warning)
66.67%
6 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
TextLibrary
96.25% covered (success)
96.25%
77 / 80
66.67% covered (warning)
66.67%
6 / 9
31
0.00% covered (danger)
0.00%
0 / 1
 register
96.00% covered (success)
96.00%
24 / 25
0.00% covered (danger)
0.00%
0 / 1
4
 textUnstrip
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 processNoWikis
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 textUnstripNoWiki
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 killMarkers
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getEntityTable
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 jsonEncode
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 jsonDecode
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 reindexArrays
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
11.02
1<?php
2
3namespace MediaWiki\Extension\Scribunto\Engines\LuaCommon;
4
5use CoreTagHooks;
6use FormatJson;
7use MediaWiki\MainConfigNames;
8use MediaWiki\MediaWikiServices;
9
10class TextLibrary extends LibraryBase {
11    // Matches Lua mw.text constants
12    private const JSON_PRESERVE_KEYS = 1;
13    private const JSON_TRY_FIXING = 2;
14    private const JSON_PRETTY = 4;
15
16    public function register() {
17        $lib = [
18            'unstrip' => [ $this, 'textUnstrip' ],
19            'unstripNoWiki' => [ $this, 'textUnstripNoWiki' ],
20            'killMarkers' => [ $this, 'killMarkers' ],
21            'getEntityTable' => [ $this, 'getEntityTable' ],
22            'jsonEncode' => [ $this, 'jsonEncode' ],
23            'jsonDecode' => [ $this, 'jsonDecode' ],
24        ];
25        $opts = [
26            'comma' => wfMessage( 'comma-separator' )->inContentLanguage()->text(),
27            'and' => wfMessage( 'and' )->inContentLanguage()->text() .
28                wfMessage( 'word-separator' )->inContentLanguage()->text(),
29            'ellipsis' => wfMessage( 'ellipsis' )->inContentLanguage()->text(),
30            'nowiki_protocols' => [],
31        ];
32
33        $urlProtocols = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::UrlProtocols );
34        foreach ( $urlProtocols as $prot ) {
35            if ( substr( $prot, -1 ) === ':' ) {
36                // To convert the protocol into a case-insensitive Lua pattern,
37                // we need to replace letters with a character class like [Xx]
38                // and insert a '%' before various punctuation.
39                $prot = preg_replace_callback( '/([a-zA-Z])|([()^$%.\[\]*+?-])/', static function ( $m ) {
40                    if ( $m[1] ) {
41                        return '[' . strtoupper( $m[1] ) . strtolower( $m[1] ) . ']';
42                    } else {
43                        return '%' . $m[2];
44                    }
45                }, substr( $prot, 0, -1 ) );
46                $opts['nowiki_protocols']["($prot):"] = '%1&#58;';
47            }
48        }
49
50        return $this->getEngine()->registerInterface( 'mw.text.lua', $lib, $opts );
51    }
52
53    /**
54     * Handler for textUnstrip
55     * @internal
56     * @param string $s
57     * @return string[]
58     */
59    public function textUnstrip( $s ) {
60        $this->checkType( 'unstrip', 1, $s, 'string' );
61        $stripState = $this->getParser()->getStripState();
62        return [ $stripState->killMarkers( $stripState->unstripNoWiki( $s ) ) ];
63    }
64
65    /**
66     * @param string $text
67     * @return string
68     */
69    public function processNoWikis( string $text ): string {
70        $content = preg_replace( "#</?nowiki[^>]*>#i", '', $text );
71        return $content ? CoreTagHooks::nowiki( $content, [], $this->getParser() )[0] : '';
72    }
73
74    /**
75     * Handler for textUnstripNoWiki
76     * @internal
77     * @param string $s
78     * @param bool $getOrigTextWhenPreprocessing
79     * @return string[]
80     */
81    public function textUnstripNoWiki( $s, $getOrigTextWhenPreprocessing ) {
82        $this->checkType( 'unstripNoWiki', 1, $s, 'string' );
83        if ( !$getOrigTextWhenPreprocessing ) {
84            return [ $this->getParser()->getStripState()->replaceNoWikis( $s, [ $this, "processNowikis" ] ) ];
85        } else {
86            return [ $this->getParser()->getStripState()->unstripNoWiki( $s ) ];
87        }
88    }
89
90    /**
91     * Handler for killMarkers
92     * @internal
93     * @param string $s
94     * @return string[]
95     */
96    public function killMarkers( $s ) {
97        $this->checkType( 'killMarkers', 1, $s, 'string' );
98        return [ $this->getParser()->getStripState()->killMarkers( $s ) ];
99    }
100
101    /**
102     * Handler for getEntityTable
103     * @internal
104     * @return array[]
105     */
106    public function getEntityTable() {
107        $table = array_flip(
108            get_html_translation_table( HTML_ENTITIES, ENT_QUOTES | ENT_HTML5, "UTF-8" )
109        );
110        return [ $table ];
111    }
112
113    /**
114     * Handler for jsonEncode
115     * @internal
116     * @param mixed $value
117     * @param string|int $flags
118     * @return string[]
119     */
120    public function jsonEncode( $value, $flags ) {
121        $this->checkTypeOptional( 'mw.text.jsonEncode', 2, $flags, 'number', 0 );
122        $flags = (int)$flags;
123        if ( !( $flags & self::JSON_PRESERVE_KEYS ) && is_array( $value ) ) {
124            $value = self::reindexArrays( $value, true );
125        }
126        $ret = FormatJson::encode( $value, (bool)( $flags & self::JSON_PRETTY ), FormatJson::ALL_OK );
127        if ( $ret === false ) {
128            throw new LuaError( 'mw.text.jsonEncode: Unable to encode value' );
129        }
130        return [ $ret ];
131    }
132
133    /**
134     * Handler for jsonDecode
135     * @internal
136     * @param string $s
137     * @param string|int $flags
138     * @return array
139     */
140    public function jsonDecode( $s, $flags ) {
141        $this->checkType( 'mw.text.jsonDecode', 1, $s, 'string' );
142        $this->checkTypeOptional( 'mw.text.jsonDecode', 2, $flags, 'number', 0 );
143        $flags = (int)$flags;
144        $opts = FormatJson::FORCE_ASSOC;
145        if ( $flags & self::JSON_TRY_FIXING ) {
146            $opts |= FormatJson::TRY_FIXING;
147        }
148        $status = FormatJson::parse( $s, $opts );
149        if ( !$status->isOk() ) {
150            throw new LuaError( 'mw.text.jsonDecode: ' . $status->getMessage()->text() );
151        }
152        $val = $status->getValue();
153        if ( !( $flags & self::JSON_PRESERVE_KEYS ) && is_array( $val ) ) {
154            $val = self::reindexArrays( $val, false );
155        }
156        return [ $val ];
157    }
158
159    /** Recursively reindex array with integer keys to 0-based or 1-based
160     * @param array $arr
161     * @param bool $isEncoding
162     * @return array
163     * @internal
164     */
165    public static function reindexArrays( array $arr, $isEncoding ) {
166        if ( $isEncoding ) {
167            ksort( $arr, SORT_NUMERIC );
168            $next = 1;
169        } else {
170            $next = 0;
171        }
172        $isSequence = true;
173        foreach ( $arr as $k => &$v ) {
174            if ( is_array( $v ) ) {
175                $v = self::reindexArrays( $v, $isEncoding );
176            }
177
178            if ( $isSequence ) {
179                if ( is_int( $k ) ) {
180                    $isSequence = $next++ === $k;
181                } elseif ( $isEncoding && ctype_digit( $k ) ) {
182                    // json_decode currently doesn't return integer keys for {}
183                    $isSequence = $next++ === (int)$k;
184                } else {
185                    $isSequence = false;
186                }
187            }
188        }
189        if ( $isSequence ) {
190            if ( $isEncoding ) {
191                return array_values( $arr );
192            } else {
193                return $arr ? array_combine( range( 1, count( $arr ) ), $arr ) : $arr;
194            }
195        }
196        return $arr;
197    }
198
199}