6 * let's reinvent the wheel one last time
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
11 * It is dual-licensed for any use under the GNU/GPL2 and MIT licenses,
14 * (c) 2009 Fil - fil@rezo.net
15 * Documentation & http://zzz.rezo.net/-TextWheel-
17 * Usage: $wheel = new TextWheel(); echo $wheel->text($text);
21 if (!defined('_ECRIRE_INC_VERSION')) {
25 require_once dirname(__FILE__
) . "/textwheelruleset.php";
29 protected static $subwheel = array();
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();
38 * @param TextWheelRuleSet $ruleset
40 public function __construct($ruleset = null) {
41 $this->setRuleSet($ruleset);
47 * @param TextWheelRuleSet $ruleset
49 public function setRuleSet($ruleset) {
50 if (!is_object($ruleset)) {
51 $ruleset = new TextWheelRuleSet($ruleset);
53 $this->ruleset
= $ruleset;
57 * Apply all rules of RuleSet to a text
62 public function text($t) {
63 $rules = &$this->ruleset
->getRules();
64 ## apply each in order
65 foreach ($rules as $name => $rule) #php4+php5
67 $this->apply($rules[$name], $t);
69 #foreach ($this->rules as &$rule) #smarter &reference, but php5 only
70 # $this->apply($rule, $t);
74 private function export($x) {
75 return addcslashes(var_export($x, true), "\n\r\t");
78 public function compile($b = null) {
79 $rules = &$this->ruleset
->getRules();
81 ## apply each in order
85 foreach ($rules as $name => $rule) {
87 $this->initRule($rule);
89 and $compiledEntry = $this->ruleCompiledEntryName($rule->replace
)
90 and isset($this->compiled
[$compiledEntry])
91 and $fun = $this->compiled
[$compiledEntry]
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...
98 $r = "\t/* $name */\n";
100 if ($rule->require) {
101 $r .= "\t" . 'require_once ' . TextWheel
::export($rule->require) . ';' . "\n";
104 $r .= "\t" . 'if (strpos($t, ' . TextWheel
::export($rule->if_str
) . ') === false)' . "\n";
106 if ($rule->if_stri
) {
107 $r .= "\t" . 'if (stripos($t, ' . TextWheel
::export($rule->if_stri
) . ') === false)' . "\n";
109 if ($rule->if_match
) {
110 $r .= "\t" . 'if (preg_match(' . TextWheel
::export($rule->if_match
) . ', $t))' . "\n";
113 if ($rule->func_replace
!== 'replace_identity') {
114 $fun = 'TextWheel::' . $rule->func_replace
;
117 case 'TextWheel::replace_all_cb':
118 if (is_string($rule->replace
)) {
119 $fun = $rule->replace
;
121 elseif ($rule->compilereplace
) {
122 $fun = trim($rule->compilereplace
, "'");
125 $call = "\$t = $fun(\$t);";
128 case 'TextWheel::replace_preg':
129 $fun = 'preg_replace';
131 case 'TextWheel::replace_str':
132 $fun = 'str_replace';
134 case 'TextWheel::replace_preg_cb':
135 $fun = 'preg_replace_callback';
141 if (empty($rule->compilereplace
)) {
142 $rule->compilereplace
= TextWheel
::export($rule->replace
);
144 $call = '$t = ' . $fun . '(' . TextWheel
::export($rule->match
) . ', ' . $rule->compilereplace
. ', $t);';
151 $code = join("\n", $comp);
152 $code = 'function ' . $b . '($t) {' . "\n" . $code . "\n\treturn \$t;\n}\n\n";
153 $code = join("\n", $pre) . $code;
160 * Get an internal global subwheel
161 * read acces for annymous function only
166 public static function &getSubWheel($n) {
167 return TextWheel
::$subwheel[$n];
171 * Create SubWheel (can be overriden in debug class)
173 * @param TextWheelRuleset $rules
176 protected function &createSubWheel(&$rules) {
177 $tw = new TextWheel($rules);
186 protected function ruleCompiledEntryName($replace) {
187 if (is_array($replace)) {
188 return serialize($replace);
190 elseif (is_object($replace)) {
191 return get_class($replace) . ':' . spl_object_hash($replace);
197 * Initializing a rule a first call
198 * including file, creating function or wheel
201 * @param TextWheelRule $rule
203 protected function initRule(&$rule) {
205 if ($rule->require) {
206 require_once $rule->require;
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;
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;
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);
234 $rule->compilereplace
= "'$cname'";
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]);
241 $rule->compilereplace
= 'function ($m) { return '.$cname.'($m['.$pick_match.']) }';
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;
250 $rule->func_replace
= '';
251 if (isset($rule->replace
)) {
252 switch ($rule->type
) {
254 $rule->func_replace
= 'replace_all';
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)
265 and $c = array_map('strlen', $rule->replace
)
266 and $c = array_unique($c)
270 $rule->match
= implode('', $rule->match
);
271 $rule->replace
= implode('', $rule->replace
);
272 $rule->func_replace
= 'replace_strtr';
276 $rule->func_replace
= 'replace_split';
277 $rule->match
= array($rule->match
, is_null($rule->glue
) ?
$rule->match
: $rule->glue
);
281 $rule->func_replace
= 'replace_preg';
284 if ($rule->is_callback
) {
285 $rule->func_replace
.= '_cb';
288 if (!method_exists("TextWheel", $rule->func_replace
)) {
289 $rule->disabled
= true;
290 $rule->func_replace
= 'replace_identity';
296 * Apply a rule to a text
298 * @param TextWheelRule $rule
302 protected function apply(&$rule, &$t, &$count = null) {
304 if ($rule->disabled
) {
308 if (isset($rule->if_chars
) and (strpbrk($t, $rule->if_chars
) === false)) {
312 if (isset($rule->if_match
) and !preg_match($rule->if_match
, $t)) {
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);
321 if (isset($rule->if_str
) and strpos($t, $rule->if_str
) === false) {
325 if (isset($rule->if_stri
) and stripos($t, $rule->if_stri
) === false) {
329 $func = $rule->func_replace
;
330 TextWheel
::$func($rule->match
, $rule->replace
, $t, $count);
334 * No Replacement function
335 * fall back in case of unknown method for replacing
336 * should be called max once per rule
338 * @param mixed $match
339 * @param mixed $replace
343 protected static function replace_identity(&$match, &$replace, &$t, &$count) {
347 * Static replacement of All text
349 * @param mixed $match
350 * @param mixed $replace
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);
366 * Call back replacement of All text
368 * @param mixed $match
369 * @param mixed $replace
373 protected static function replace_all_cb(&$match, &$replace, &$t, &$count) {
378 * Static string replacement
380 * @param mixed $match
381 * @param mixed $replace
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);
392 * Fast Static string replacement one char to one char
394 * @param mixed $match
395 * @param mixed $replace
399 protected static function replace_strtr(&$match, &$replace, &$t, &$count) {
400 $t = strtr($t, $match, $replace);
404 * Callback string replacement
406 * @param mixed $match
407 * @param mixed $replace
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);
420 * Static Preg replacement
422 * @param mixed $match
423 * @param mixed $replace
428 protected static function replace_preg(&$match, &$replace, &$t, &$count) {
429 $t = preg_replace($match, $replace, $t, -1, $count);
431 throw new Exception('Memory error, increase pcre.backtrack_limit in php.ini');
436 * Callback Preg replacement
438 * @param mixed $match
439 * @param mixed $replace
444 protected static function replace_preg_cb(&$match, &$replace, &$t, &$count) {
445 $t = preg_replace_callback($match, $replace, $t, -1, $count);
447 throw new Exception('Memory error, increase pcre.backtrack_limit in php.ini');
453 * Static split replacement : invalid
455 * @param mixed $match
456 * @param mixed $replace
460 protected static function replace_split(&$match, &$replace, &$t, &$count) {
461 throw new InvalidArgumentException('split rule always needs a callback function as replace');
465 * Callback split replacement
467 * @param array $match
468 * @param mixed $replace
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));
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;
487 * Timer for profiling
489 * @staticvar int $time
494 protected function timer($t = 'rien', $raw = false) {
498 // microtime peut contenir les microsecondes et le temps
499 $b = explode(' ', $b);
500 if (count($b) == 2) {
504 if (!isset($time[$t])) {
507 $p = ($a +
$b - $time[$t]) * 1000;
515 $s = sprintf("%d ", $x = floor($p / 1000));
519 return $s . sprintf("%.3f ms", $p);
524 * Apply all rules of RuleSet to a text
529 public function text($t) {
530 $rules = &$this->ruleset
->getRules();
531 ## apply each in order
532 foreach ($rules as $name => $rule) #php4+php5
535 $name .= ' ' . $rule->match
;
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;
544 TextWheelDebug
::$u[$name]++
; # nombre de fois utile
545 TextWheelDebug
::$tu[$name] +
= $v;
547 TextWheelDebug
::$tnu[$name] +
= $v;
551 #foreach ($this->rules as &$rule) #smarter &reference, but php5 only
552 # $this->apply($rule, $t);
557 * Ouputs data stored for profiling/debuging purposes
559 public static function outputDebug() {
560 if (isset(TextWheelDebug
::$t)) {
561 $time = array_flip(array_map('strval', TextWheelDebug
::$t));
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; }
576 <table class='sortable'>
577 <caption>Temps par rule</caption>
578 <thead><tr><th>temps (ms)</th><th>rule</th><th>application</th><th>t/u (ms)</th><th>t/n-u (ms)</th></tr></thead>\n";
580 foreach ($time as $t => $r) {
581 $applications = intval(TextWheelDebug
::$u[$r]);
583 if (intval($t * 10)) {
585 <td class='number strong'>" . number_format(round($t * 10) / 10, 1) . "</td><td> " . spip_htmlspecialchars($r) . "</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,
591 <td class='number'>" . (($nu = intval(TextWheelDebug
::$w[$r]) - $applications) ?
number_format(round(TextWheelDebug
::$tnu[$r] / $nu * 100) / 100,
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']);
605 # somme des temps des rules, ne tient pas compte des subwheels
606 echo "<p>temps total rules: " . round($total) . " ms</p>\n";
611 public static function outputTotal($liste, $profondeur = 0) {
613 foreach ($liste as $cause => $duree) {
614 if (is_array($duree)) {
615 TextWheelDebug
::outputTotal($duree, $profondeur +
1);
617 echo "<tr class='prof-$profondeur'>
618 <td class='number'><b>" . intval($duree) . "</b> ms</td>
619 <td class='name'>" . spip_htmlspecialchars($cause) . "</td>
626 * Create SubWheel (can be overriden in debug class)
628 * @param TextWheelRuleset $rules
631 protected function &createSubWheel(&$rules) {
632 return new TextWheelDebug($rules);
641 if (!function_exists('stripos')) {
642 function stripos($haystack, $needle) {
643 return strpos($haystack, stristr($haystack, $needle));
648 * approximation of strpbrk for php4
649 * return false if no char of $char_list is in $haystack
651 if (!function_exists('strpbrk')) {
652 function strpbrk($haystack, $char_list) {
653 $result = strcspn($haystack, $char_list);
654 if ($result != strlen($haystack)) {