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')) return;
23 require_once dirname(__FILE__
)."/textwheelruleset.php";
27 protected static $subwheel = array();
29 protected $compiled = array();
33 * @param TextWheelRuleSet $ruleset
35 public function TextWheel($ruleset = null) {
36 $this->setRuleSet($ruleset);
41 * @param TextWheelRuleSet $ruleset
43 public function setRuleSet($ruleset){
44 if (!is_object($ruleset))
45 $ruleset = new TextWheelRuleSet ($ruleset);
46 $this->ruleset
= $ruleset;
50 * Apply all rules of RuleSet to a text
55 public function text($t) {
56 $rules = & $this->ruleset
->getRules();
57 ## apply each in order
58 foreach ($rules as $name => $rule) #php4+php5
60 $this->apply($rules[$name], $t);
62 #foreach ($this->rules as &$rule) #smarter &reference, but php5 only
63 # $this->apply($rule, $t);
67 private function export($x) {
68 return addcslashes(var_export($x, true), "\n\r\t");
71 public function compile($b = null) {
72 $rules = & $this->ruleset
->getRules();
74 ## apply each in order
78 foreach ($rules as $name => $rule)
81 $this->initRule($rule);
82 if (is_string($rule->replace
)
83 AND isset($this->compiled
[$rule->replace
])
84 AND $fun = $this->compiled
[$rule->replace
]) {
85 $pre[] = "\n###\n## $name\n###\n" . $fun;
86 preg_match(',function (\w+),', $fun, $r);
87 $rule->compilereplace
= $r[1]; # ne pas modifier ->replace sinon on casse l'execution...
90 $r = "\t/* $name */\n";
93 $r .= "\t".'require_once '.TextWheel
::export($rule->require).';'."\n";
95 $r .= "\t".'if (strpos($t, '.TextWheel
::export($rule->if_str
).') === false)'."\n";
97 $r .= "\t".'if (stripos($t, '.TextWheel
::export($rule->if_stri
).') === false)'."\n";
99 $r .= "\t".'if (preg_match('.TextWheel
::export($rule->if_match
).', $t))'."\n";
101 if ($rule->func_replace
!== 'replace_identity') {
102 $fun = 'TextWheel::'.$rule->func_replace
;
104 case 'TextWheel::replace_all_cb':
105 $fun = $rule->replace
; # trim()...
107 case 'TextWheel::replace_preg':
108 $fun = 'preg_replace';
110 case 'TextWheel::replace_str':
111 $fun = 'str_replace';
113 case 'TextWheel::replace_preg_cb':
114 $fun = 'preg_replace_callback';
119 $r .= "\t".'$t = '.$fun.'('.TextWheel
::export($rule->match
).', '.TextWheel
::export($rule->replace
).', $t);'."\n";
124 $code = join ("\n", $comp);
125 $code = 'function '.$b.'($t) {' . "\n". $code . "\n\treturn \$t;\n}\n\n";
126 $code = join ("\n", $pre) . $code;
133 * Get an internal global subwheel
134 * read acces for annymous function only
139 public static function &getSubWheel($n){
140 return TextWheel
::$subwheel[$n];
144 * Create SubWheel (can be overriden in debug class)
145 * @param TextWheelRuleset $rules
148 protected function &createSubWheel(&$rules){
149 $tw = new TextWheel($rules);
154 * Initializing a rule a first call
155 * including file, creating function or wheel
158 * @param TextWheelRule $rule
160 protected function initRule(&$rule){
163 require_once $rule->require;
166 # optimization: strpos or stripos?
167 if (isset($rule->if_str
)) {
168 if (strtolower($rule->if_str
) !== strtoupper($rule->if_str
)) {
169 $rule->if_stri
= $rule->if_str
;
170 unset($rule->if_str
);
174 if ($rule->create_replace
){
175 $compile = $rule->replace
.'($t)';
176 $rule->replace
= create_function('$m', $rule->replace
);
177 $this->compiled
[$rule->replace
] = $compile;
178 $rule->create_replace
= false;
179 $rule->is_callback
= true;
181 elseif ($rule->is_wheel
){
182 $n = count(TextWheel
::$subwheel);
183 TextWheel
::$subwheel[] = $this->createSubWheel($rule->replace
);
184 $var = '$m['.intval($rule->pick_match
).']';
185 if ($rule->type
=='all' OR $rule->type
=='str' OR $rule->type
=='split' OR !isset($rule->match
))
187 $code = 'return TextWheel::getSubWheel('.$n.')->text('.$var.');';
188 $rule->replace
= create_function('$m', $code);
189 $cname = 'compiled_'.str_replace('-','_', $rule->name
);
190 $compile = TextWheel
::getSubWheel($n)->compile($cname);
191 $this->compiled
[$rule->replace
] = $compile;
192 $rule->is_wheel
= false;
193 $rule->is_callback
= true;
197 $rule->func_replace
= '';
198 if (isset($rule->replace
)) {
199 switch($rule->type
) {
201 $rule->func_replace
= 'replace_all';
204 $rule->func_replace
= 'replace_str';
205 // test if quicker strtr usable
206 if (!$rule->is_callback
207 AND is_array($rule->match
) AND is_array($rule->replace
)
208 AND $c = array_map('strlen',$rule->match
)
209 AND $c = array_unique($c)
212 AND $c = array_map('strlen',$rule->replace
)
213 AND $c = array_unique($c)
217 $rule->match
= implode('',$rule->match
);
218 $rule->replace
= implode('',$rule->replace
);
219 $rule->func_replace
= 'replace_strtr';
223 $rule->func_replace
= 'replace_split';
224 $rule->match
= array($rule->match
, is_null($rule->glue
)?
$rule->match
:$rule->glue
);
228 $rule->func_replace
= 'replace_preg';
231 if ($rule->is_callback
)
232 $rule->func_replace
.= '_cb';
234 if (!method_exists("TextWheel", $rule->func_replace
)){
235 $rule->disabled
= true;
236 $rule->func_replace
= 'replace_identity';
242 * Apply a rule to a text
244 * @param TextWheelRule $rule
248 protected function apply(&$rule, &$t, &$count=null) {
253 if (isset($rule->if_chars
) AND (strpbrk($t, $rule->if_chars
) === false))
256 if (isset($rule->if_match
) AND !preg_match($rule->if_match
, $t))
259 // init rule before testing if_str / if_stri as they are optimized by initRule
260 if (!isset($rule->func_replace
))
261 $this->initRule($rule);
263 if (isset($rule->if_str
) AND strpos($t, $rule->if_str
) === false)
266 if (isset($rule->if_stri
) AND stripos($t, $rule->if_stri
) === false)
269 $func = $rule->func_replace
;
270 TextWheel
::$func($rule->match
,$rule->replace
,$t,$count);
274 * No Replacement function
275 * fall back in case of unknown method for replacing
276 * should be called max once per rule
278 * @param mixed $match
279 * @param mixed $replace
283 protected static function replace_identity(&$match,&$replace,&$t,&$count){
287 * Static replacement of All text
288 * @param mixed $match
289 * @param mixed $replace
293 protected static function replace_all(&$match,&$replace,&$t,&$count){
294 # special case: replace $0 with $t
295 # replace: "A$0B" will surround the string with A..B
296 # replace: "$0$0" will repeat the string
297 if (strpos($replace, '$0')!==FALSE)
298 $t = str_replace('$0', $t, $replace);
304 * Call back replacement of All text
305 * @param mixed $match
306 * @param mixed $replace
310 protected static function replace_all_cb(&$match,&$replace,&$t,&$count){
315 * Static string replacement
317 * @param mixed $match
318 * @param mixed $replace
322 protected static function replace_str(&$match,&$replace,&$t,&$count){
323 if (!is_string($match) OR strpos($t,$match)!==FALSE)
324 $t = str_replace($match, $replace, $t, $count);
328 * Fast Static string replacement one char to one char
330 * @param mixed $match
331 * @param mixed $replace
335 protected static function replace_strtr(&$match,&$replace,&$t,&$count){
336 $t = strtr( $t, $match, $replace);
340 * Callback string replacement
342 * @param mixed $match
343 * @param mixed $replace
347 protected static function replace_str_cb(&$match,&$replace,&$t,&$count){
348 if (strpos($t,$match)!==FALSE)
349 if (count($b = explode($match, $t)) > 1)
350 $t = join($replace($match), $b);
354 * Static Preg replacement
356 * @param mixed $match
357 * @param mixed $replace
361 protected static function replace_preg(&$match,&$replace,&$t,&$count){
362 $t = preg_replace($match, $replace, $t, -1, $count);
366 * Callback Preg replacement
367 * @param mixed $match
368 * @param mixed $replace
372 protected static function replace_preg_cb(&$match,&$replace,&$t,&$count){
373 $t = preg_replace_callback($match, $replace, $t, -1, $count);
378 * Static split replacement : invalid
379 * @param mixed $match
380 * @param mixed $replace
384 protected static function replace_split(&$match,&$replace,&$t,&$count){
385 throw new InvalidArgumentException('split rule always needs a callback function as replace');
389 * Callback split replacement
390 * @param array $match
391 * @param mixed $replace
395 protected static function replace_split_cb(&$match,&$replace,&$t,&$count){
396 $a = explode($match[0], $t);
397 $t = join($match[1], array_map($replace,$a));
401 class TextWheelDebug
extends TextWheel
{
402 static protected $t; #tableaux des temps
403 static protected $tu; #tableaux des temps (rules utilises)
404 static protected $tnu; #tableaux des temps (rules non utilises)
405 static protected $u; #compteur des rules utiles
406 static protected $w; #compteur des rules appliques
410 * Timer for profiling
412 * @staticvar int $time
417 protected function timer($t='rien', $raw = false) {
419 $a=time(); $b=microtime();
420 // microtime peut contenir les microsecondes et le temps
422 if (count($b)==2) $a = end($b); // plus precis !
424 if (!isset($time[$t])) {
427 $p = ($a +
$b - $time[$t]) * 1000;
433 $s = sprintf("%d ", $x = floor($p/1000));
436 return $s . sprintf("%.3f ms", $p);
441 * Apply all rules of RuleSet to a text
446 public function text($t) {
447 $rules = & $this->ruleset
->getRules();
448 ## apply each in order
449 foreach ($rules as $name => $rule) #php4+php5
452 $name .= ' '.$rule->match
;
455 $this->apply($rule, $t);
456 TextWheelDebug
::$w[$name] ++
; # nombre de fois appliquee
457 $v = $this->timer($name, true); # timer
458 TextWheelDebug
::$t[$name] +
= $v;
460 TextWheelDebug
::$u[$name] ++
; # nombre de fois utile
461 TextWheelDebug
::$tu[$name] +
= $v;
463 TextWheelDebug
::$tnu[$name] +
= $v;
467 #foreach ($this->rules as &$rule) #smarter &reference, but php5 only
468 # $this->apply($rule, $t);
473 * Ouputs data stored for profiling/debuging purposes
475 public static function outputDebug(){
476 if (isset(TextWheelDebug
::$t)) {
477 $time = array_flip(array_map('strval', TextWheelDebug
::$t));
480 <div class='textwheeldebug'>
481 <style type='text/css'>
482 .textwheeldebug table { margin:1em 0; }
483 .textwheeldebug th,.textwheeldebug td { padding-left: 15px }
484 .textwheeldebug .prof-0 .number { padding-right: 60px }
485 .textwheeldebug .prof-1 .number { padding-right: 30px }
486 .textwheeldebug .prof-1 .name { padding-left: 30px }
487 .textwheeldebug .prof-2 .name { padding-left: 60px }
488 .textwheeldebug .zero { color:orange; }
489 .textwheeldebug .number { text-align:right; }
490 .textwheeldebug .strong { font-weight:bold; }
492 <table class='sortable'>
493 <caption>Temps par rule</caption>
494 <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";
496 foreach($time as $t => $r) {
497 $applications = intval(TextWheelDebug
::$u[$r]);
501 <td class='number strong'>".number_format(round($t*10)/10,1)."</td><td> ".spip_htmlspecialchars($r)."</td>
503 . (!$applications ?
" class='zero'" : "")
504 .">".$applications."/".intval(TextWheelDebug
::$w[$r])."</td>
505 <td class='number'>".($applications?
number_format(round(TextWheelDebug
::$tu[$r]/$applications*100)/100,2):"") ."</td>
506 <td class='number'>".(($nu = intval(TextWheelDebug
::$w[$r])-$applications)?
number_format(round(TextWheelDebug
::$tnu[$r]/$nu*100)/100,2):"") ."</td>
513 <caption>Temps total par rule</caption>
514 <thead><tr><th>temps</th><th>rule</th></tr></thead>\n";
515 ksort($GLOBALS['totaux']);
516 TextWheelDebug
::outputTotal($GLOBALS['totaux']);
518 # somme des temps des rules, ne tient pas compte des subwheels
519 echo "<p>temps total rules: ".round($total)." ms</p>\n";
524 public static function outputTotal($liste, $profondeur=0) {
526 foreach ($liste as $cause => $duree) {
527 if (is_array($duree)) {
528 TextWheelDebug
::outputTotal($duree, $profondeur+
1);
530 echo "<tr class='prof-$profondeur'>
531 <td class='number'><b>".intval($duree)."</b> ms</td>
532 <td class='name'>".spip_htmlspecialchars($cause)."</td>
539 * Create SubWheel (can be overriden in debug class)
540 * @param TextWheelRuleset $rules
543 protected function &createSubWheel(&$rules){
544 return new TextWheelDebug($rules);
554 if (!function_exists('stripos')) {
555 function stripos($haystack, $needle) {
556 return strpos($haystack, stristr( $haystack, $needle ));
561 * approximation of strpbrk for php4
562 * return false if no char of $char_list is in $haystack
564 if (!function_exists('strpbrk')) {
565 function strpbrk($haystack, $char_list) {
566 $result = strcspn($haystack, $char_list);
567 if ($result != strlen($haystack)) {