[SPIP] ~maj v3.2.9-->v3.2.11
[lhc/web/www.git] / www / plugins-dist / textwheel / engine / textwheel.php
1 <?php
2
3 /*
4 * TextWheel 0.1
5 *
6 * let's reinvent the wheel one last time
7 *
8 * This library of code is meant to be a fast and universal replacement
9 * for any and all text-processing systems written in PHP
10 *
11 * It is dual-licensed for any use under the GNU/GPL2 and MIT licenses,
12 * as suits you best
13 *
14 * (c) 2009 Fil - fil@rezo.net
15 * Documentation & http://zzz.rezo.net/-TextWheel-
16 *
17 * Usage: $wheel = new TextWheel(); echo $wheel->text($text);
18 *
19 */
20
21 if (!defined('_ECRIRE_INC_VERSION')) {
22 return;
23 }
24
25 require_once dirname(__FILE__) . "/textwheelruleset.php";
26
27 class TextWheel {
28 protected $ruleset;
29 protected static $subwheel = array();
30
31 // Experimental : projet de compilation PHP d'une wheel
32 // pour generation d'un fichier php execute a la place de ->text()
33 protected $compiled = array();
34
35 /**
36 * Constructor
37 *
38 * @param TextWheelRuleSet $ruleset
39 */
40 public function __construct($ruleset = null) {
41 $this->setRuleSet($ruleset);
42 }
43
44 /**
45 * Set RuleSet
46 *
47 * @param TextWheelRuleSet $ruleset
48 */
49 public function setRuleSet($ruleset) {
50 if (!is_object($ruleset)) {
51 $ruleset = new TextWheelRuleSet($ruleset);
52 }
53 $this->ruleset = $ruleset;
54 }
55
56 /**
57 * Apply all rules of RuleSet to a text
58 *
59 * @param string $t
60 * @return string
61 */
62 public function text($t) {
63 $rules = &$this->ruleset->getRules();
64 ## apply each in order
65 foreach ($rules as $name => $rule) #php4+php5
66 {
67 $this->apply($rules[$name], $t);
68 }
69 #foreach ($this->rules as &$rule) #smarter &reference, but php5 only
70 # $this->apply($rule, $t);
71 return $t;
72 }
73
74 private function export($x) {
75 return addcslashes(var_export($x, true), "\n\r\t");
76 }
77
78 public function compile($b = null) {
79 $rules = &$this->ruleset->getRules();
80
81 ## apply each in order
82 $pre = array();
83 $comp = array();
84
85 foreach ($rules as $name => $rule) {
86 $rule->name = $name;
87 $this->initRule($rule);
88 if ($rule->replace
89 and $compiledEntry = $this->ruleCompiledEntryName($rule->replace)
90 and isset($this->compiled[$compiledEntry])
91 and $fun = $this->compiled[$compiledEntry]
92 ) {
93 $pre[] = "\n###\n## $name\n###\n" . $fun;
94 preg_match(',function (\w+), ', $fun, $r);
95 $rule->compilereplace = "'".$r[1]."'"; # ne pas modifier ->replace sinon on casse l'execution...
96 }
97
98 $r = "\t/* $name */\n";
99
100 if ($rule->require) {
101 $r .= "\t" . 'require_once ' . TextWheel::export($rule->require) . ';' . "\n";
102 }
103 if ($rule->if_str) {
104 $r .= "\t" . 'if (strpos($t, ' . TextWheel::export($rule->if_str) . ') === false)' . "\n";
105 }
106 if ($rule->if_stri) {
107 $r .= "\t" . 'if (stripos($t, ' . TextWheel::export($rule->if_stri) . ') === false)' . "\n";
108 }
109 if ($rule->if_match) {
110 $r .= "\t" . 'if (preg_match(' . TextWheel::export($rule->if_match) . ', $t))' . "\n";
111 }
112
113 if ($rule->func_replace !== 'replace_identity') {
114 $fun = 'TextWheel::' . $rule->func_replace;
115 $call = '';
116 switch ($fun) {
117 case 'TextWheel::replace_all_cb':
118 if (is_string($rule->replace)) {
119 $fun = $rule->replace;
120 }
121 elseif ($rule->compilereplace) {
122 $fun = trim($rule->compilereplace, "'");
123 };
124 if ($fun) {
125 $call = "\$t = $fun(\$t);";
126 }
127 break;
128 case 'TextWheel::replace_preg':
129 $fun = 'preg_replace';
130 break;
131 case 'TextWheel::replace_str':
132 $fun = 'str_replace';
133 break;
134 case 'TextWheel::replace_preg_cb':
135 $fun = 'preg_replace_callback';
136 break;
137 default:
138 break;
139 }
140 if (!$call) {
141 if (empty($rule->compilereplace)) {
142 $rule->compilereplace = TextWheel::export($rule->replace);
143 }
144 $call = '$t = ' . $fun . '(' . TextWheel::export($rule->match) . ', ' . $rule->compilereplace . ', $t);';
145 }
146 $r .= "\t$call\n";
147 }
148
149 $comp[] = $r;
150 }
151 $code = join("\n", $comp);
152 $code = 'function ' . $b . '($t) {' . "\n" . $code . "\n\treturn \$t;\n}\n\n";
153 $code = join("\n", $pre) . $code;
154
155 return $code;
156 }
157
158
159 /**
160 * Get an internal global subwheel
161 * read acces for annymous function only
162 *
163 * @param int $n
164 * @return TextWheel
165 */
166 public static function &getSubWheel($n) {
167 return TextWheel::$subwheel[$n];
168 }
169
170 /**
171 * Create SubWheel (can be overriden in debug class)
172 *
173 * @param TextWheelRuleset $rules
174 * @return TextWheel
175 */
176 protected function &createSubWheel(&$rules) {
177 $tw = new TextWheel($rules);
178
179 return $tw;
180 }
181
182 /**
183 * @param $replace
184 * @return string
185 */
186 protected function ruleCompiledEntryName($replace) {
187 if (is_array($replace)) {
188 return serialize($replace);
189 }
190 elseif (is_object($replace)) {
191 return get_class($replace) . ':' . spl_object_hash($replace);
192 }
193 return $replace;
194 }
195
196 /**
197 * Initializing a rule a first call
198 * including file, creating function or wheel
199 * optimizing tests
200 *
201 * @param TextWheelRule $rule
202 */
203 protected function initRule(&$rule) {
204 # language specific
205 if ($rule->require) {
206 require_once $rule->require;
207 }
208
209 # optimization: strpos or stripos?
210 if (isset($rule->if_str)) {
211 if (strtolower($rule->if_str) !== strtoupper($rule->if_str)) {
212 $rule->if_stri = $rule->if_str;
213 $rule->if_str = null;
214 }
215 }
216
217 if ($rule->create_replace) {
218 // DEPRECATED : rule->create_replace, on ne peut rien faire de mieux ici
219 // mais c'est voue a disparaitre
220 $compile = $rule->replace . '($t)';
221 $rule->replace = create_function('$m', $rule->replace);
222 $this->compiled[$this->ruleCompiledEntryName($rule->replace)] = $compile;
223 $rule->create_replace = false;
224 $rule->is_callback = true;
225 }
226 elseif ($rule->is_wheel) {
227 $rule_number = count(TextWheel::$subwheel);
228 TextWheel::$subwheel[] = $this->createSubWheel($rule->replace);
229 $cname = 'compiled_' . str_replace('-', '_', $rule->name) . '_' . substr(md5(spl_object_hash($rule)),0,7);
230 if ($rule->type == 'all' or $rule->type == 'str' or $rule->type == 'split' or !isset($rule->match)) {
231 $rule->replace = function ($m) use ($rule_number) {
232 return TextWheel::getSubWheel($rule_number)->text($m);
233 };
234 $rule->compilereplace = "'$cname'";
235 }
236 else {
237 $pick_match = intval($rule->pick_match);
238 $rule->replace = function ($m) use ($rule_number, $pick_match) {
239 return TextWheel::getSubWheel($rule_number)->text($m[$pick_match]);
240 };
241 $rule->compilereplace = 'function ($m) { return '.$cname.'($m['.$pick_match.']) }';
242 }
243 $rule->is_wheel = false;
244 $rule->is_callback = true;
245 $compile = TextWheel::getSubWheel($rule_number)->compile($cname);
246 $this->compiled[$this->ruleCompiledEntryName($rule->replace)] = $compile;
247 }
248
249 # optimization
250 $rule->func_replace = '';
251 if (isset($rule->replace)) {
252 switch ($rule->type) {
253 case 'all':
254 $rule->func_replace = 'replace_all';
255 break;
256 case 'str':
257 $rule->func_replace = 'replace_str';
258 // test if quicker strtr usable
259 if (!$rule->is_callback
260 and is_array($rule->match) and is_array($rule->replace)
261 and $c = array_map('strlen', $rule->match)
262 and $c = array_unique($c)
263 and count($c) == 1
264 and reset($c) == 1
265 and $c = array_map('strlen', $rule->replace)
266 and $c = array_unique($c)
267 and count($c) == 1
268 and reset($c) == 1
269 ) {
270 $rule->match = implode('', $rule->match);
271 $rule->replace = implode('', $rule->replace);
272 $rule->func_replace = 'replace_strtr';
273 }
274 break;
275 case 'split':
276 $rule->func_replace = 'replace_split';
277 $rule->match = array($rule->match, is_null($rule->glue) ? $rule->match : $rule->glue);
278 break;
279 case 'preg':
280 default:
281 $rule->func_replace = 'replace_preg';
282 break;
283 }
284 if ($rule->is_callback) {
285 $rule->func_replace .= '_cb';
286 }
287 }
288 if (!method_exists("TextWheel", $rule->func_replace)) {
289 $rule->disabled = true;
290 $rule->func_replace = 'replace_identity';
291 }
292 # /end
293 }
294
295 /**
296 * Apply a rule to a text
297 *
298 * @param TextWheelRule $rule
299 * @param string $t
300 * @param int $count
301 */
302 protected function apply(&$rule, &$t, &$count = null) {
303
304 if ($rule->disabled) {
305 return;
306 }
307
308 if (isset($rule->if_chars) and (strpbrk($t, $rule->if_chars) === false)) {
309 return;
310 }
311
312 if (isset($rule->if_match) and !preg_match($rule->if_match, $t)) {
313 return;
314 }
315
316 // init rule before testing if_str / if_stri as they are optimized by initRule
317 if (!isset($rule->func_replace)) {
318 $this->initRule($rule);
319 }
320
321 if (isset($rule->if_str) and strpos($t, $rule->if_str) === false) {
322 return;
323 }
324
325 if (isset($rule->if_stri) and stripos($t, $rule->if_stri) === false) {
326 return;
327 }
328
329 $func = $rule->func_replace;
330 TextWheel::$func($rule->match, $rule->replace, $t, $count);
331 }
332
333 /**
334 * No Replacement function
335 * fall back in case of unknown method for replacing
336 * should be called max once per rule
337 *
338 * @param mixed $match
339 * @param mixed $replace
340 * @param string $t
341 * @param int $count
342 */
343 protected static function replace_identity(&$match, &$replace, &$t, &$count) {
344 }
345
346 /**
347 * Static replacement of All text
348 *
349 * @param mixed $match
350 * @param mixed $replace
351 * @param string $t
352 * @param int $count
353 */
354 protected static function replace_all(&$match, &$replace, &$t, &$count) {
355 # special case: replace $0 with $t
356 # replace: "A$0B" will surround the string with A..B
357 # replace: "$0$0" will repeat the string
358 if (strpos($replace, '$0') !== false) {
359 $t = str_replace('$0', $t, $replace);
360 } else {
361 $t = $replace;
362 }
363 }
364
365 /**
366 * Call back replacement of All text
367 *
368 * @param mixed $match
369 * @param mixed $replace
370 * @param string $t
371 * @param int $count
372 */
373 protected static function replace_all_cb(&$match, &$replace, &$t, &$count) {
374 $t = $replace($t);
375 }
376
377 /**
378 * Static string replacement
379 *
380 * @param mixed $match
381 * @param mixed $replace
382 * @param string $t
383 * @param int $count
384 */
385 protected static function replace_str(&$match, &$replace, &$t, &$count) {
386 if (!is_string($match) or strpos($t, $match) !== false) {
387 $t = str_replace($match, $replace, $t, $count);
388 }
389 }
390
391 /**
392 * Fast Static string replacement one char to one char
393 *
394 * @param mixed $match
395 * @param mixed $replace
396 * @param string $t
397 * @param int $count
398 */
399 protected static function replace_strtr(&$match, &$replace, &$t, &$count) {
400 $t = strtr($t, $match, $replace);
401 }
402
403 /**
404 * Callback string replacement
405 *
406 * @param mixed $match
407 * @param mixed $replace
408 * @param string $t
409 * @param int $count
410 */
411 protected static function replace_str_cb(&$match, &$replace, &$t, &$count) {
412 if (strpos($t, $match) !== false) {
413 if (count($b = explode($match, $t)) > 1) {
414 $t = join($replace($match), $b);
415 }
416 }
417 }
418
419 /**
420 * Static Preg replacement
421 *
422 * @param mixed $match
423 * @param mixed $replace
424 * @param string $t
425 * @param int $count
426 * @throws Exception
427 */
428 protected static function replace_preg(&$match, &$replace, &$t, &$count) {
429 $t = preg_replace($match, $replace, $t, -1, $count);
430 if (is_null($t)) {
431 throw new Exception('Memory error, increase pcre.backtrack_limit in php.ini');
432 }
433 }
434
435 /**
436 * Callback Preg replacement
437 *
438 * @param mixed $match
439 * @param mixed $replace
440 * @param string $t
441 * @param int $count
442 * @throws Exception
443 */
444 protected static function replace_preg_cb(&$match, &$replace, &$t, &$count) {
445 $t = preg_replace_callback($match, $replace, $t, -1, $count);
446 if (is_null($t)) {
447 throw new Exception('Memory error, increase pcre.backtrack_limit in php.ini');
448 }
449 }
450
451
452 /**
453 * Static split replacement : invalid
454 *
455 * @param mixed $match
456 * @param mixed $replace
457 * @param string $t
458 * @param int $count
459 */
460 protected static function replace_split(&$match, &$replace, &$t, &$count) {
461 throw new InvalidArgumentException('split rule always needs a callback function as replace');
462 }
463
464 /**
465 * Callback split replacement
466 *
467 * @param array $match
468 * @param mixed $replace
469 * @param string $t
470 * @param int $count
471 */
472 protected static function replace_split_cb(&$match, &$replace, &$t, &$count) {
473 $a = explode($match[0], $t);
474 $t = join($match[1], array_map($replace, $a));
475 }
476 }
477
478 class TextWheelDebug extends TextWheel {
479 protected static $t; #tableaux des temps
480 protected static $tu; #tableaux des temps (rules utilises)
481 protected static $tnu; #tableaux des temps (rules non utilises)
482 protected static $u; #compteur des rules utiles
483 protected static $w; #compteur des rules appliques
484 public static $total;
485
486 /**
487 * Timer for profiling
488 *
489 * @staticvar int $time
490 * @param string $t
491 * @param bool $raw
492 * @return int/strinf
493 */
494 protected function timer($t = 'rien', $raw = false) {
495 static $time;
496 $a = time();
497 $b = microtime();
498 // microtime peut contenir les microsecondes et le temps
499 $b = explode(' ', $b);
500 if (count($b) == 2) {
501 $a = end($b);
502 } // plus precis !
503 $b = reset($b);
504 if (!isset($time[$t])) {
505 $time[$t] = $a + $b;
506 } else {
507 $p = ($a + $b - $time[$t]) * 1000;
508 unset($time[$t]);
509 if ($raw) {
510 return $p;
511 }
512 if ($p < 1000) {
513 $s = '';
514 } else {
515 $s = sprintf("%d ", $x = floor($p / 1000));
516 $p -= ($x * 1000);
517 }
518
519 return $s . sprintf("%.3f ms", $p);
520 }
521 }
522
523 /**
524 * Apply all rules of RuleSet to a text
525 *
526 * @param string $t
527 * @return string
528 */
529 public function text($t) {
530 $rules = &$this->ruleset->getRules();
531 ## apply each in order
532 foreach ($rules as $name => $rule) #php4+php5
533 {
534 if (is_int($name)) {
535 $name .= ' ' . $rule->match;
536 }
537 $this->timer($name);
538 $b = $t;
539 $this->apply($rule, $t);
540 TextWheelDebug::$w[$name]++; # nombre de fois appliquee
541 $v = $this->timer($name, true); # timer
542 TextWheelDebug::$t[$name] += $v;
543 if ($t !== $b) {
544 TextWheelDebug::$u[$name]++; # nombre de fois utile
545 TextWheelDebug::$tu[$name] += $v;
546 } else {
547 TextWheelDebug::$tnu[$name] += $v;
548 }
549
550 }
551 #foreach ($this->rules as &$rule) #smarter &reference, but php5 only
552 # $this->apply($rule, $t);
553 return $t;
554 }
555
556 /**
557 * Ouputs data stored for profiling/debuging purposes
558 */
559 public static function outputDebug() {
560 if (isset(TextWheelDebug::$t)) {
561 $time = array_flip(array_map('strval', TextWheelDebug::$t));
562 krsort($time);
563 echo "
564 <div class='textwheeldebug'>
565 <style type='text/css'>
566 .textwheeldebug table { margin:1em 0; }
567 .textwheeldebug th,.textwheeldebug td { padding-left: 15px }
568 .textwheeldebug .prof-0 .number { padding-right: 60px }
569 .textwheeldebug .prof-1 .number { padding-right: 30px }
570 .textwheeldebug .prof-1 .name { padding-left: 30px }
571 .textwheeldebug .prof-2 .name { padding-left: 60px }
572 .textwheeldebug .zero { color:orange; }
573 .textwheeldebug .number { text-align:right; }
574 .textwheeldebug .strong { font-weight:bold; }
575 </style>
576 <table class='sortable'>
577 <caption>Temps par rule</caption>
578 <thead><tr><th>temps&nbsp;(ms)</th><th>rule</th><th>application</th><th>t/u&nbsp;(ms)</th><th>t/n-u&nbsp;(ms)</th></tr></thead>\n";
579 $total = 0;
580 foreach ($time as $t => $r) {
581 $applications = intval(TextWheelDebug::$u[$r]);
582 $total += $t;
583 if (intval($t * 10)) {
584 echo "<tr>
585 <td class='number strong'>" . number_format(round($t * 10) / 10, 1) . "</td><td> " . spip_htmlspecialchars($r) . "</td>
586 <td"
587 . (!$applications ? " class='zero'" : "")
588 . ">" . $applications . "/" . intval(TextWheelDebug::$w[$r]) . "</td>
589 <td class='number'>" . ($applications ? number_format(round(TextWheelDebug::$tu[$r] / $applications * 100) / 100,
590 2) : "") . "</td>
591 <td class='number'>" . (($nu = intval(TextWheelDebug::$w[$r]) - $applications) ? number_format(round(TextWheelDebug::$tnu[$r] / $nu * 100) / 100,
592 2) : "") . "</td>
593 </tr>";
594 }
595 }
596 echo "</table>\n";
597
598 echo "
599 <table>
600 <caption>Temps total par rule</caption>
601 <thead><tr><th>temps</th><th>rule</th></tr></thead>\n";
602 ksort($GLOBALS['totaux']);
603 TextWheelDebug::outputTotal($GLOBALS['totaux']);
604 echo "</table>";
605 # somme des temps des rules, ne tient pas compte des subwheels
606 echo "<p>temps total rules: " . round($total) . "&nbsp;ms</p>\n";
607 echo "</div>\n";
608 }
609 }
610
611 public static function outputTotal($liste, $profondeur = 0) {
612 ksort($liste);
613 foreach ($liste as $cause => $duree) {
614 if (is_array($duree)) {
615 TextWheelDebug::outputTotal($duree, $profondeur + 1);
616 } else {
617 echo "<tr class='prof-$profondeur'>
618 <td class='number'><b>" . intval($duree) . "</b>&nbsp;ms</td>
619 <td class='name'>" . spip_htmlspecialchars($cause) . "</td>
620 </tr>\n";
621 }
622 }
623 }
624
625 /**
626 * Create SubWheel (can be overriden in debug class)
627 *
628 * @param TextWheelRuleset $rules
629 * @return TextWheel
630 */
631 protected function &createSubWheel(&$rules) {
632 return new TextWheelDebug($rules);
633 }
634
635 }
636
637
638 /**
639 * stripos for php4
640 */
641 if (!function_exists('stripos')) {
642 function stripos($haystack, $needle) {
643 return strpos($haystack, stristr($haystack, $needle));
644 }
645 }
646
647 /**
648 * approximation of strpbrk for php4
649 * return false if no char of $char_list is in $haystack
650 */
651 if (!function_exists('strpbrk')) {
652 function strpbrk($haystack, $char_list) {
653 $result = strcspn($haystack, $char_list);
654 if ($result != strlen($haystack)) {
655 return $result;
656 }
657
658 return false;
659 }
660 }