Merge "Drop zh-tw message "saveprefs""
[lhc/web/wiklou.git] / languages / ConverterRule.php
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 * @ingroup Language
20 */
21
22 /**
23 * Parser for rules of language conversion , parse rules in -{ }- tag.
24 * @ingroup Language
25 * @author fdcn <fdcn64@gmail.com>, PhiLiP <philip.npc@gmail.com>
26 */
27 class ConverterRule {
28 public $mText; // original text in -{text}-
29 public $mConverter; // LanguageConverter object
30 public $mRuleDisplay = '';
31 public $mRuleTitle = false;
32 public $mRules = '';// string : the text of the rules
33 public $mRulesAction = 'none';
34 public $mFlags = array();
35 public $mVariantFlags = array();
36 public $mConvTable = array();
37 public $mBidtable = array();// array of the translation in each variant
38 public $mUnidtable = array();// array of the translation in each variant
39
40 /**
41 * Constructor
42 *
43 * @param string $text The text between -{ and }-
44 * @param LanguageConverter $converter
45 */
46 public function __construct( $text, $converter ) {
47 $this->mText = $text;
48 $this->mConverter = $converter;
49 }
50
51 /**
52 * Check if variants array in convert array.
53 *
54 * @param array|string $variants Variant language code
55 * @return string Translated text
56 */
57 public function getTextInBidtable( $variants ) {
58 $variants = (array)$variants;
59 if ( !$variants ) {
60 return false;
61 }
62 foreach ( $variants as $variant ) {
63 if ( isset( $this->mBidtable[$variant] ) ) {
64 return $this->mBidtable[$variant];
65 }
66 }
67 return false;
68 }
69
70 /**
71 * Parse flags with syntax -{FLAG| ... }-
72 * @private
73 */
74 function parseFlags() {
75 $text = $this->mText;
76 $flags = array();
77 $variantFlags = array();
78
79 $sepPos = strpos( $text, '|' );
80 if ( $sepPos !== false ) {
81 $validFlags = $this->mConverter->mFlags;
82 $f = StringUtils::explode( ';', substr( $text, 0, $sepPos ) );
83 foreach ( $f as $ff ) {
84 $ff = trim( $ff );
85 if ( isset( $validFlags[$ff] ) ) {
86 $flags[$validFlags[$ff]] = true;
87 }
88 }
89 $text = strval( substr( $text, $sepPos + 1 ) );
90 }
91
92 if ( !$flags ) {
93 $flags['S'] = true;
94 } elseif ( isset( $flags['R'] ) ) {
95 $flags = array( 'R' => true );// remove other flags
96 } elseif ( isset( $flags['N'] ) ) {
97 $flags = array( 'N' => true );// remove other flags
98 } elseif ( isset( $flags['-'] ) ) {
99 $flags = array( '-' => true );// remove other flags
100 } elseif ( count( $flags ) == 1 && isset( $flags['T'] ) ) {
101 $flags['H'] = true;
102 } elseif ( isset( $flags['H'] ) ) {
103 // replace A flag, and remove other flags except T
104 $temp = array( '+' => true, 'H' => true );
105 if ( isset( $flags['T'] ) ) {
106 $temp['T'] = true;
107 }
108 if ( isset( $flags['D'] ) ) {
109 $temp['D'] = true;
110 }
111 $flags = $temp;
112 } else {
113 if ( isset( $flags['A'] ) ) {
114 $flags['+'] = true;
115 $flags['S'] = true;
116 }
117 if ( isset( $flags['D'] ) ) {
118 unset( $flags['S'] );
119 }
120 // try to find flags like "zh-hans", "zh-hant"
121 // allow syntaxes like "-{zh-hans;zh-hant|XXXX}-"
122 $variantFlags = array_intersect( array_keys( $flags ), $this->mConverter->mVariants );
123 if ( $variantFlags ) {
124 $variantFlags = array_flip( $variantFlags );
125 $flags = array();
126 }
127 }
128 $this->mVariantFlags = $variantFlags;
129 $this->mRules = $text;
130 $this->mFlags = $flags;
131 }
132
133 /**
134 * Generate conversion table.
135 * @private
136 */
137 function parseRules() {
138 $rules = $this->mRules;
139 $bidtable = array();
140 $unidtable = array();
141 $variants = $this->mConverter->mVariants;
142 $varsep_pattern = $this->mConverter->getVarSeparatorPattern();
143
144 // Split according to $varsep_pattern, but ignore semicolons from HTML entities
145 $rules = preg_replace( '/(&[#a-zA-Z0-9]+);/', "$1\x01", $rules );
146 $choice = preg_split( $varsep_pattern, $rules );
147 $choice = str_replace( "\x01", ';', $choice );
148
149 foreach ( $choice as $c ) {
150 $v = explode( ':', $c, 2 );
151 if ( count( $v ) != 2 ) {
152 // syntax error, skip
153 continue;
154 }
155 $to = trim( $v[1] );
156 $v = trim( $v[0] );
157 $u = explode( '=>', $v, 2 );
158 // if $to is empty (which is also used as $from in bidtable),
159 // strtr() could return a wrong result.
160 if ( count( $u ) == 1 && $to !== '' && in_array( $v, $variants ) ) {
161 $bidtable[$v] = $to;
162 } elseif ( count( $u ) == 2 ) {
163 $from = trim( $u[0] );
164 $v = trim( $u[1] );
165 // if $from is empty, strtr() could return a wrong result.
166 if ( array_key_exists( $v, $unidtable )
167 && !is_array( $unidtable[$v] )
168 && $from !== ''
169 && in_array( $v, $variants ) ) {
170 $unidtable[$v] = array( $from => $to );
171 } elseif ( $from !== '' && in_array( $v, $variants ) ) {
172 $unidtable[$v][$from] = $to;
173 }
174 }
175 // syntax error, pass
176 if ( !isset( $this->mConverter->mVariantNames[$v] ) ) {
177 $bidtable = array();
178 $unidtable = array();
179 break;
180 }
181 }
182 $this->mBidtable = $bidtable;
183 $this->mUnidtable = $unidtable;
184 }
185
186 /**
187 * @private
188 *
189 * @return string
190 */
191 function getRulesDesc() {
192 $codesep = $this->mConverter->mDescCodeSep;
193 $varsep = $this->mConverter->mDescVarSep;
194 $text = '';
195 foreach ( $this->mBidtable as $k => $v ) {
196 $text .= $this->mConverter->mVariantNames[$k] . "$codesep$v$varsep";
197 }
198 foreach ( $this->mUnidtable as $k => $a ) {
199 foreach ( $a as $from => $to ) {
200 $text .= $from . '⇒' . $this->mConverter->mVariantNames[$k] .
201 "$codesep$to$varsep";
202 }
203 }
204 return $text;
205 }
206
207 /**
208 * Parse rules conversion.
209 * @private
210 *
211 * @param string $variant
212 *
213 * @return string
214 */
215 function getRuleConvertedStr( $variant ) {
216 $bidtable = $this->mBidtable;
217 $unidtable = $this->mUnidtable;
218
219 if ( count( $bidtable ) + count( $unidtable ) == 0 ) {
220 return $this->mRules;
221 } else {
222 // display current variant in bidirectional array
223 $disp = $this->getTextInBidtable( $variant );
224 // or display current variant in fallbacks
225 if ( $disp === false ) {
226 $disp = $this->getTextInBidtable(
227 $this->mConverter->getVariantFallbacks( $variant ) );
228 }
229 // or display current variant in unidirectional array
230 if ( $disp === false && array_key_exists( $variant, $unidtable ) ) {
231 $disp = array_values( $unidtable[$variant] );
232 $disp = $disp[0];
233 }
234 // or display frist text under disable manual convert
235 if ( $disp === false && $this->mConverter->mManualLevel[$variant] == 'disable' ) {
236 if ( count( $bidtable ) > 0 ) {
237 $disp = array_values( $bidtable );
238 $disp = $disp[0];
239 } else {
240 $disp = array_values( $unidtable );
241 $disp = array_values( $disp[0] );
242 $disp = $disp[0];
243 }
244 }
245 return $disp;
246 }
247 }
248
249 /**
250 * Similar to getRuleConvertedStr(), but this prefers to use original
251 * page title if $variant === $this->mConverter->mMainLanguageCode
252 * and may return false in this case (so this title conversion rule
253 * will be ignored and the original title is shown).
254 *
255 * @since 1.22
256 * @param string $variant The variant code to display page title in
257 * @return string|bool The converted title or false if just page name
258 */
259 function getRuleConvertedTitle( $variant ) {
260 if ( $variant === $this->mConverter->mMainLanguageCode ) {
261 // If a string targeting exactly this variant is set,
262 // use it. Otherwise, just return false, so the real
263 // page name can be shown (and because variant === main,
264 // there'll be no further automatic conversion).
265 $disp = $this->getTextInBidtable( $variant );
266 if ( $disp ) {
267 return $disp;
268 }
269 if ( array_key_exists( $variant, $this->mUnidtable ) ) {
270 $disp = array_values( $this->mUnidtable[$variant] );
271 $disp = $disp[0];
272 }
273 // Assigned above or still false.
274 return $disp;
275 } else {
276 return $this->getRuleConvertedStr( $variant );
277 }
278 }
279
280 /**
281 * Generate conversion table for all text.
282 * @private
283 */
284 function generateConvTable() {
285 // Special case optimisation
286 if ( !$this->mBidtable && !$this->mUnidtable ) {
287 $this->mConvTable = array();
288 return;
289 }
290
291 $bidtable = $this->mBidtable;
292 $unidtable = $this->mUnidtable;
293 $manLevel = $this->mConverter->mManualLevel;
294
295 $vmarked = array();
296 foreach ( $this->mConverter->mVariants as $v ) {
297 /* for bidirectional array
298 fill in the missing variants, if any,
299 with fallbacks */
300 if ( !isset( $bidtable[$v] ) ) {
301 $variantFallbacks =
302 $this->mConverter->getVariantFallbacks( $v );
303 $vf = $this->getTextInBidtable( $variantFallbacks );
304 if ( $vf ) {
305 $bidtable[$v] = $vf;
306 }
307 }
308
309 if ( isset( $bidtable[$v] ) ) {
310 foreach ( $vmarked as $vo ) {
311 // use syntax: -{A|zh:WordZh;zh-tw:WordTw}-
312 // or -{H|zh:WordZh;zh-tw:WordTw}-
313 // or -{-|zh:WordZh;zh-tw:WordTw}-
314 // to introduce a custom mapping between
315 // words WordZh and WordTw in the whole text
316 if ( $manLevel[$v] == 'bidirectional' ) {
317 $this->mConvTable[$v][$bidtable[$vo]] = $bidtable[$v];
318 }
319 if ( $manLevel[$vo] == 'bidirectional' ) {
320 $this->mConvTable[$vo][$bidtable[$v]] = $bidtable[$vo];
321 }
322 }
323 $vmarked[] = $v;
324 }
325 /* for unidirectional array fill to convert tables */
326 if ( ( $manLevel[$v] == 'bidirectional' || $manLevel[$v] == 'unidirectional' )
327 && isset( $unidtable[$v] )
328 ) {
329 if ( isset( $this->mConvTable[$v] ) ) {
330 $this->mConvTable[$v] = $unidtable[$v] + $this->mConvTable[$v];
331 } else {
332 $this->mConvTable[$v] = $unidtable[$v];
333 }
334 }
335 }
336 }
337
338 /**
339 * Parse rules and flags.
340 * @param string $variant Variant language code
341 */
342 public function parse( $variant = null ) {
343 if ( !$variant ) {
344 $variant = $this->mConverter->getPreferredVariant();
345 }
346
347 $this->parseFlags();
348 $flags = $this->mFlags;
349
350 // convert to specified variant
351 // syntax: -{zh-hans;zh-hant[;...]|<text to convert>}-
352 if ( $this->mVariantFlags ) {
353 // check if current variant in flags
354 if ( isset( $this->mVariantFlags[$variant] ) ) {
355 // then convert <text to convert> to current language
356 $this->mRules = $this->mConverter->autoConvert( $this->mRules,
357 $variant );
358 } else {
359 // if current variant no in flags,
360 // then we check its fallback variants.
361 $variantFallbacks =
362 $this->mConverter->getVariantFallbacks( $variant );
363 if ( is_array( $variantFallbacks ) ) {
364 foreach ( $variantFallbacks as $variantFallback ) {
365 // if current variant's fallback exist in flags
366 if ( isset( $this->mVariantFlags[$variantFallback] ) ) {
367 // then convert <text to convert> to fallback language
368 $this->mRules =
369 $this->mConverter->autoConvert( $this->mRules,
370 $variantFallback );
371 break;
372 }
373 }
374 }
375 }
376 $this->mFlags = $flags = array( 'R' => true );
377 }
378
379 if ( !isset( $flags['R'] ) && !isset( $flags['N'] ) ) {
380 // decode => HTML entities modified by Sanitizer::removeHTMLtags
381 $this->mRules = str_replace( '=&gt;', '=>', $this->mRules );
382 $this->parseRules();
383 }
384 $rules = $this->mRules;
385
386 if ( !$this->mBidtable && !$this->mUnidtable ) {
387 if ( isset( $flags['+'] ) || isset( $flags['-'] ) ) {
388 // fill all variants if text in -{A/H/-|text}- is non-empty but without rules
389 if ( $rules !== '' ) {
390 foreach ( $this->mConverter->mVariants as $v ) {
391 $this->mBidtable[$v] = $rules;
392 }
393 }
394 } elseif ( !isset( $flags['N'] ) && !isset( $flags['T'] ) ) {
395 $this->mFlags = $flags = array( 'R' => true );
396 }
397 }
398
399 $this->mRuleDisplay = false;
400 foreach ( $flags as $flag => $unused ) {
401 switch ( $flag ) {
402 case 'R':
403 // if we don't do content convert, still strip the -{}- tags
404 $this->mRuleDisplay = $rules;
405 break;
406 case 'N':
407 // process N flag: output current variant name
408 $ruleVar = trim( $rules );
409 if ( isset( $this->mConverter->mVariantNames[$ruleVar] ) ) {
410 $this->mRuleDisplay = $this->mConverter->mVariantNames[$ruleVar];
411 } else {
412 $this->mRuleDisplay = '';
413 }
414 break;
415 case 'D':
416 // process D flag: output rules description
417 $this->mRuleDisplay = $this->getRulesDesc();
418 break;
419 case 'H':
420 // process H,- flag or T only: output nothing
421 $this->mRuleDisplay = '';
422 break;
423 case '-':
424 $this->mRulesAction = 'remove';
425 $this->mRuleDisplay = '';
426 break;
427 case '+':
428 $this->mRulesAction = 'add';
429 $this->mRuleDisplay = '';
430 break;
431 case 'S':
432 $this->mRuleDisplay = $this->getRuleConvertedStr( $variant );
433 break;
434 case 'T':
435 $this->mRuleTitle = $this->getRuleConvertedTitle( $variant );
436 $this->mRuleDisplay = '';
437 break;
438 default:
439 // ignore unknown flags (but see error case below)
440 }
441 }
442 if ( $this->mRuleDisplay === false ) {
443 $this->mRuleDisplay = '<span class="error">'
444 . wfMessage( 'converter-manual-rule-error' )->inContentLanguage()->escaped()
445 . '</span>';
446 }
447
448 $this->generateConvTable();
449 }
450
451 /**
452 * Checks if there are conversion rules.
453 * @return bool
454 */
455 public function hasRules() {
456 return $this->mRules !== '';
457 }
458
459 /**
460 * Get display text on markup -{...}-
461 * @return string
462 */
463 public function getDisplay() {
464 return $this->mRuleDisplay;
465 }
466
467 /**
468 * Get converted title.
469 * @return string
470 */
471 public function getTitle() {
472 return $this->mRuleTitle;
473 }
474
475 /**
476 * Return how deal with conversion rules.
477 * @return string
478 */
479 public function getRulesAction() {
480 return $this->mRulesAction;
481 }
482
483 /**
484 * Get conversion table. (bidirectional and unidirectional
485 * conversion table)
486 * @return array
487 */
488 public function getConvTable() {
489 return $this->mConvTable;
490 }
491
492 /**
493 * Get conversion rules string.
494 * @return string
495 */
496 public function getRules() {
497 return $this->mRules;
498 }
499
500 /**
501 * Get conversion flags.
502 * @return array
503 */
504 public function getFlags() {
505 return $this->mFlags;
506 }
507 }