Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
96.25% |
77 / 80 |
|
66.67% |
6 / 9 |
CRAP | |
0.00% |
0 / 1 |
TextLibrary | |
96.25% |
77 / 80 |
|
66.67% |
6 / 9 |
31 | |
0.00% |
0 / 1 |
register | |
96.00% |
24 / 25 |
|
0.00% |
0 / 1 |
4 | |||
textUnstrip | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
processNoWikis | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
textUnstripNoWiki | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
killMarkers | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getEntityTable | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
jsonEncode | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
4.03 | |||
jsonDecode | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
5 | |||
reindexArrays | |
94.74% |
18 / 19 |
|
0.00% |
0 / 1 |
11.02 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Scribunto\Engines\LuaCommon; |
4 | |
5 | use MediaWiki\Json\FormatJson; |
6 | use MediaWiki\MainConfigNames; |
7 | use MediaWiki\MediaWikiServices; |
8 | use MediaWiki\Parser\CoreTagHooks; |
9 | |
10 | class 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:'; |
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 | } |