Interim commit.
[yaffs-website] / web / modules / contrib / advagg / advagg_js_minify / jspacker.inc
1 <?php
2 // @codingStandardsIgnoreFile
3 // @ignore comment_docblock_file:file
4 // @ignore style_curly_braces:file
5 // @ignore style_string_spacing:file
6 // @ignore style_else_spacing:file
7 // @ignore comment_comment_docblock_missing:file
8 // @ignore comment_comment_eg:file
9 // @ignore production_code:file
10 // @ignore druplart_unary:file
11 // @ignore style_uppercase_constants:file
12 // @ignore comment_comment_space:file
13 // @ignore druplart_conditional_assignment:file
14 // @ignore style_paren_spacing:file
15 // @ignore style_no_tabs:file
16 // @ignore :file
17
18 /* 9 April 2008. version 1.1
19  *
20  * This is the php version of the Dean Edwards JavaScript's Packer,
21  * Based on :
22  *
23  * ParseMaster, version 1.0.2 (2005-08-19) Copyright 2005, Dean Edwards
24  * a multi-pattern parser.
25  * KNOWN BUG: erroneous behavior when using escapeChar with a replacement
26  * value that is a function
27  *
28  * packer, version 2.0.2 (2005-08-19) Copyright 2004-2005, Dean Edwards
29  *
30  * License: http://creativecommons.org/licenses/LGPL/2.1/
31  *
32  * Ported to PHP by Nicolas Martin.
33  *
34  * ----------------------------------------------------------------------
35  * changelog:
36  * 1.1 : correct a bug, '\0' packed then unpacked becomes '\'.
37  * ----------------------------------------------------------------------
38  *
39  * examples of usage :
40  * $myPacker = new JavaScriptPacker($script, 62, true, false);
41  * $packed = $myPacker->pack();
42  *
43  * or
44  *
45  * $myPacker = new JavaScriptPacker($script, 'Normal', true, false);
46  * $packed = $myPacker->pack();
47  *
48  * or (default values)
49  *
50  * $myPacker = new JavaScriptPacker($script);
51  * $packed = $myPacker->pack();
52  *
53  *
54  * params of the constructor :
55  * $script:       the JavaScript to pack, string.
56  * $encoding:     level of encoding, int or string :
57  *                0,10,62,95 or 'None', 'Numeric', 'Normal', 'High ASCII'.
58  *                default: 62.
59  * $fastDecode:   include the fast decoder in the packed result, boolean.
60  *                default : true.
61  * $specialChars: if you are flagged your private and local variables
62  *                in the script, boolean.
63  *                default: false.
64  *
65  * The pack() method return the compressed JavasScript, as a string.
66  *
67  * see http://dean.edwards.name/packer/usage/ for more information.
68  *
69  * Notes :
70  * # need PHP 5 . Tested with PHP 5.1.2, 5.1.3, 5.1.4, 5.2.3
71  *
72  * # The packed result may be different than with the Dean Edwards
73  *   version, but with the same length. The reason is that the PHP
74  *   function usort to sort array don't necessarily preserve the
75  *   original order of two equal member. The Javascript sort function
76  *   in fact preserve this order (but that's not require by the
77  *   ECMAScript standard). So the encoded keywords order can be
78  *   different in the two results.
79  *
80  * # Be careful with the 'High ASCII' Level encoding if you use
81  *   UTF-8 in your files...
82  */
83
84
85 class JavaScriptPacker {
86   // constants
87   const IGNORE = '$1';
88
89   // validate parameters
90   private $_script = '';
91   private $_encoding = 62;
92   private $_fastDecode = true;
93   private $_specialChars = false;
94
95   private $LITERAL_ENCODING = array(
96     'None' => 0,
97     'Numeric' => 10,
98     'Normal' => 62,
99     'High ASCII' => 95,
100   );
101
102   public function __construct($_script, $_encoding = 62, $_fastDecode = true, $_specialChars = false) {
103     $this->_script = $_script . "\n";
104     if (array_key_exists($_encoding, $this->LITERAL_ENCODING)) {
105       $_encoding = $this->LITERAL_ENCODING[$_encoding];
106     }
107     $this->_encoding = min((int) $_encoding, 95);
108     $this->_fastDecode = $_fastDecode;
109     $this->_specialChars = $_specialChars;
110   }
111
112   public function pack() {
113     $this->_addParser('_basicCompression');
114     if ($this->_specialChars) {
115       $this->_addParser('_encodeSpecialChars');
116     }
117     if ($this->_encoding) {
118       $this->_addParser('_encodeKeywords');
119     }
120
121     // go!
122     return $this->_pack($this->_script);
123   }
124
125   // apply all parsing routines
126   private function _pack($script) {
127     for ($i = 0; isset($this->_parsers[$i]); $i++) {
128       $script = call_user_func(array(&$this, $this->_parsers[$i]), $script);
129     }
130     return $script;
131   }
132
133   // keep a list of parsing functions, they'll be executed all at once
134   private $_parsers = array();
135   private function _addParser($parser) {
136     $this->_parsers[] = $parser;
137   }
138
139   // zero encoding - just removal of white space and comments
140   private function _basicCompression($script) {
141     $parser = new ParseMaster();
142     // make safe
143     $parser->escapeChar = '\\';
144     // protect strings
145     $parser->add('/\'[^\'\\n\\r]*\'/', self::IGNORE);
146     $parser->add('/"[^"\\n\\r]*"/', self::IGNORE);
147     // remove comments
148     $parser->add('/\\/\\/[^\\n\\r]*[\\n\\r]/', ' ');
149     $parser->add('/\\/\\*[^*]*\\*+([^\\/][^*]*\\*+)*\\//', ' ');
150     // protect regular expressions
151     $parser->add('/\\s+(\\/[^\\/\\n\\r\\*][^\\/\\n\\r]*\\/g?i?)/', '$2'); // IGNORE
152     $parser->add('/[^\\w\\x24\\/\'"*)\\?:]\\/[^\\/\\n\\r\\*][^\\/\\n\\r]*\\/g?i?/', self::IGNORE);
153     // remove: ;;; doSomething();
154     if ($this->_specialChars) {
155       $parser->add('/;;;[^\\n\\r]+[\\n\\r]/');
156     }
157     // remove redundant semi-colons
158     $parser->add('/\\(;;\\)/', self::IGNORE); // protect for (;;) loops
159     $parser->add('/;+\\s*([};])/', '$2');
160     // apply the above
161     $script = $parser->exec($script);
162
163     // remove white-space
164     $parser->add('/(\\b|\\x24)\\s+(\\b|\\x24)/', '$2 $3');
165     $parser->add('/([+\\-])\\s+([+\\-])/', '$2 $3');
166     $parser->add('/\\s+/', '');
167     // done
168     return $parser->exec($script);
169   }
170
171   private function _encodeSpecialChars($script) {
172     $parser = new ParseMaster();
173     // replace: $name -> n, $$name -> na
174     $parser->add('/((\\x24+)([a-zA-Z$_]+))(\\d*)/',
175                                          array('fn' => '_replace_name')
176                 );
177     // replace: _name -> _0, double-underscore (__name) is ignored
178     $regexp = '/\\b_[A-Za-z\\d]\\w*/';
179     // build the word list
180     $keywords = $this->_analyze($script, $regexp, '_encodePrivate');
181     // quick ref
182     $encoded = $keywords['encoded'];
183
184     $parser->add($regexp,
185                         array(
186       'fn' => '_replace_encoded',
187       'data' => $encoded,
188     )
189                 );
190     return $parser->exec($script);
191   }
192
193   private function _encodeKeywords($script) {
194     // escape high-ascii values already in the script (i.e. in strings)
195     if ($this->_encoding > 62) {
196       $script = $this->_escape95($script);
197     }
198     // create the parser
199     $parser = new ParseMaster();
200     $encode = $this->_getEncoder($this->_encoding);
201     // for high-ascii, don't encode single character low-ascii
202     $regexp = ($this->_encoding > 62) ? '/\\w\\w+/' : '/\\w+/';
203     // build the word list
204     $keywords = $this->_analyze($script, $regexp, $encode);
205     $encoded = $keywords['encoded'];
206
207     // encode
208     $parser->add($regexp,
209                         array(
210       'fn' => '_replace_encoded',
211       'data' => $encoded,
212     )
213                 );
214     if (empty($script)) {
215       return $script;
216     }
217     else {
218       //$res = $parser->exec($script);
219       //$res = $this->_bootStrap($res, $keywords);
220       //return $res;
221       return $this->_bootStrap($parser->exec($script), $keywords);
222     }
223   }
224
225   private function _analyze($script, $regexp, $encode) {
226     // analyse
227     // retrieve all words in the script
228     $all = array();
229     preg_match_all($regexp, $script, $all);
230     $_sorted = array(); // list of words sorted by frequency
231     $_encoded = array(); // dictionary of word->encoding
232     $_protected = array(); // instances of "protected" words
233     $all = $all[0]; // simulate the javascript comportement of global match
234     if (!empty($all)) {
235       $unsorted = array(); // same list, not sorted
236       $protected = array(); // "protected" words (dictionary of word->"word")
237       $value = array(); // dictionary of charCode->encoding (eg. 256->ff)
238       $this->_count = array(); // word->count
239       $i = count($all);
240       $j = 0; //$word = null;
241       // count the occurrences - used for sorting later
242       do {
243         --$i;
244         $word = '$' . $all[$i];
245         if (!isset($this->_count[$word])) {
246           $this->_count[$word] = 0;
247           $unsorted[$j] = $word;
248           // make a dictionary of all of the protected words in this script
249 //  these are words that might be mistaken for encoding
250           //if (is_string($encode) && method_exists($this, $encode))
251           $values[$j] = call_user_func(array(&$this, $encode), $j);
252           $protected['$' . $values[$j]] = $j++;
253         }
254         // increment the word counter
255         $this->_count[$word]++;
256       } while ($i > 0);
257       // prepare to sort the word list, first we must protect
258 //  words that are also used as codes. we assign them a code
259 //  equivalent to the word itself.
260       // e.g. if "do" falls within our encoding range
261 //      then we store keywords["do"] = "do";
262       // this avoids problems when decoding
263       $i = count($unsorted);
264       do {
265         $word = $unsorted[--$i];
266         if (isset($protected[$word]) /*!= null*/) {
267           $_sorted[$protected[$word]] = substr($word, 1);
268           $_protected[$protected[$word]] = true;
269           $this->_count[$word] = 0;
270         }
271       } while ($i);
272
273       // sort the words by frequency
274       // Note: the javascript and php version of sort can be different :
275       // in php manual, usort :
276       // " If two members compare as equal,
277       // their order in the sorted array is undefined."
278       // so the final packed script is different of the Dean's javascript version
279       // but equivalent.
280       // the ECMAscript standard does not guarantee this behaviour,
281       // and thus not all browsers (e.g. Mozilla versions dating back to at
282       // least 2003) respect this.
283       usort($unsorted, array(&$this, '_sortWords'));
284       $j = 0;
285       // because there are "protected" words in the list
286 //  we must add the sorted words around them
287       do {
288         if (!isset($_sorted[$i])) {
289           $_sorted[$i] = substr($unsorted[$j++], 1);
290         }
291         $_encoded[$_sorted[$i]] = $values[$i];
292       } while (++$i < count($unsorted));
293     }
294     return array(
295       'sorted' => $_sorted,
296       'encoded' => $_encoded,
297       'protected' => $_protected,
298     );
299   }
300
301   private $_count = array();
302   private function _sortWords($match1, $match2) {
303     return $this->_count[$match2] - $this->_count[$match1];
304   }
305
306   // build the boot function used for loading and decoding
307   private function _bootStrap($packed, $keywords) {
308     $ENCODE = $this->_safeRegExp('$encode\\($count\\)');
309
310     // $packed: the packed script
311     $packed = "'" . $this->_escape($packed) . "'";
312
313     // $ascii: base for encoding
314     $ascii = min(count($keywords['sorted']), $this->_encoding);
315     if ($ascii == 0) {
316       $ascii = 1;
317     }
318
319     // $count: number of words contained in the script
320     $count = count($keywords['sorted']);
321
322     // $keywords: list of words contained in the script
323     foreach ($keywords['protected'] as $i => $value) {
324       $keywords['sorted'][$i] = '';
325     }
326     // convert from a string to an array
327     ksort($keywords['sorted']);
328     $keywords = "'" . implode('|', $keywords['sorted']) . "'.split('|')";
329
330     $encode = ($this->_encoding > 62) ? '_encode95' : $this->_getEncoder($ascii);
331     $encode = $this->_getJSFunction($encode);
332     $encode = preg_replace('/_encoding/', '$ascii', $encode);
333     $encode = preg_replace('/arguments\\.callee/', '$encode', $encode);
334     $inline = '\\$count' . ($ascii > 10 ? '.toString(\\$ascii)' : '');
335
336     // $decode: code snippet to speed up decoding
337     if ($this->_fastDecode) {
338       // create the decoder
339       $decode = $this->_getJSFunction('_decodeBody');
340       if ($this->_encoding > 62) {
341         $decode = preg_replace('/\\\\w/', '[\\xa1-\\xff]', $decode);
342       }
343       // perform the encoding inline for lower ascii values
344       elseif ($ascii < 36) {
345         $decode = preg_replace($ENCODE, $inline, $decode);
346       }
347       // special case: when $count==0 there are no keywords. I want to keep
348 //  the basic shape of the unpacking funcion so i'll frig the code...
349       if ($count == 0) {
350         $decode = preg_replace($this->_safeRegExp('($count)\\s*=\\s*1'), '$1=0', $decode, 1);
351       }
352     }
353
354     // boot function
355     $unpack = $this->_getJSFunction('_unpack');
356     if ($this->_fastDecode) {
357       // insert the decoder
358       $this->buffer = $decode;
359       $unpack = preg_replace_callback('/\\{/', array(&$this, '_insertFastDecode'), $unpack, 1);
360     }
361     $unpack = preg_replace('/"/', "'", $unpack);
362     if ($this->_encoding > 62) { // high-ascii
363       // get rid of the word-boundaries for regexp matches
364       $unpack = preg_replace('/\'\\\\\\\\b\'\s*\\+|\\+\s*\'\\\\\\\\b\'/', '', $unpack);
365     }
366     if ($ascii > 36 || $this->_encoding > 62 || $this->_fastDecode) {
367       // insert the encode function
368       $this->buffer = $encode;
369       $unpack = preg_replace_callback('/\\{/', array(&$this, '_insertFastEncode'), $unpack, 1);
370     }
371     else {
372       // perform the encoding inline
373       $unpack = preg_replace($ENCODE, $inline, $unpack);
374     }
375     // pack the boot function too
376     $unpackPacker = new JavaScriptPacker($unpack, 0, false, true);
377     $unpack = $unpackPacker->pack();
378
379     // arguments
380     $params = array($packed, $ascii, $count, $keywords);
381     if ($this->_fastDecode) {
382       $params[] = 0;
383       $params[] = '{}';
384     }
385     $params = implode(',', $params);
386
387     // the whole thing
388     return 'eval(' . $unpack . '(' . $params . "))\n";
389   }
390
391   private $buffer;
392   private function _insertFastDecode($match) {
393     return '{' . $this->buffer . ';';
394   }
395   private function _insertFastEncode($match) {
396     return '{$encode=' . $this->buffer . ';';
397   }
398
399   // mmm.. ..which one do i need ??
400   private function _getEncoder($ascii) {
401     return $ascii > 10 ? $ascii > 36 ? $ascii > 62 ?
402                        '_encode95' : '_encode62' : '_encode36' : '_encode10';
403   }
404
405   // zero encoding
406   // characters: 0123456789
407   private function _encode10($charCode) {
408     return $charCode;
409   }
410
411   // inherent base36 support
412   // characters: 0123456789abcdefghijklmnopqrstuvwxyz
413   private function _encode36($charCode) {
414     return base_convert($charCode, 10, 36);
415   }
416
417   // hitch a ride on base36 and add the upper case alpha characters
418   // characters: 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
419   private function _encode62($charCode) {
420     $res = '';
421     if ($charCode >= $this->_encoding) {
422       $res = $this->_encode62((int) ($charCode / $this->_encoding));
423     }
424     $charCode = $charCode % $this->_encoding;
425
426     if ($charCode > 35) {
427       return $res . chr($charCode + 29);
428     }
429     else {
430       return $res . base_convert($charCode, 10, 36);
431     }
432   }
433
434   // use high-ascii values
435   // characters: ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþ
436   private function _encode95($charCode) {
437     $res = '';
438     if ($charCode >= $this->_encoding) {
439       $res = $this->_encode95($charCode / $this->_encoding);
440     }
441
442     return $res . chr(($charCode % $this->_encoding) + 161);
443   }
444
445   private function _safeRegExp($string) {
446     return '/' . preg_replace('/\$/', '\\\$', $string) . '/';
447   }
448
449   private function _encodePrivate($charCode) {
450     return "_" . $charCode;
451   }
452
453   // protect characters used by the parser
454   private function _escape($script) {
455     return preg_replace('/([\\\\\'])/', '\\\$1', $script);
456   }
457
458   // protect high-ascii characters already in the script
459   private function _escape95($script) {
460     return preg_replace_callback(
461                         '/[\\xa1-\\xff]/',
462                         array(&$this, '_escape95Bis'),
463                         $script
464                 );
465   }
466   private function _escape95Bis($match) {
467     return '\x' . ((string) dechex(ord($match)));
468   }
469
470
471   private function _getJSFunction($aName) {
472     if (defined('self::JSFUNCTION' . $aName)) {
473       return constant('self::JSFUNCTION' . $aName);
474     }
475     else {
476       return '';
477     }
478   }
479
480   // JavaScript Functions used.
481   // Note : In Dean's version, these functions are converted
482   // with 'String(aFunctionName);'.
483   // This internal conversion complete the original code, ex :
484   // 'while (aBool) anAction();' is converted to
485   // 'while (aBool) { anAction(); }'.
486   // The JavaScript functions below are corrected.
487
488   // unpacking function - this is the boot strap function
489 //  data extracted from this packing routine is passed to
490 //  this function when decoded in the target
491   // NOTE ! : without the ';' final.
492   const JSFUNCTION_unpack =
493
494 'function($packed, $ascii, $count, $keywords, $encode, $decode) {
495     while ($count--) {
496         if ($keywords[$count]) {
497             $packed = $packed.replace(new RegExp(\'\\\\b\' + $encode($count) + \'\\\\b\', \'g\'), $keywords[$count]);
498         }
499     }
500     return $packed;
501 }';
502   /*
503    'function($packed, $ascii, $count, $keywords, $encode, $decode) {
504    while ($count--)
505    if ($keywords[$count])
506    $packed = $packed.replace(new RegExp(\'\\\\b\' + $encode($count) + \'\\\\b\', \'g\'), $keywords[$count]);
507    return $packed;
508    }';
509    */
510
511   // code-snippet inserted into the unpacker to speed up decoding
512   const JSFUNCTION_decodeBody =
513 //_decode = function() {
514 // does the browser support String.replace where the
515 //  replacement value is a function?
516
517 '    if (!\'\'.replace(/^/, String)) {
518         // decode all the values we need
519         while ($count--) {
520             $decode[$encode($count)] = $keywords[$count] || $encode($count);
521         }
522         // global replacement function
523         $keywords = [function ($encoded) {return $decode[$encoded]}];
524         // generic match
525         $encode = function () {return \'\\\\w+\'};
526         // reset the loop counter -  we are now doing a global replace
527         $count = 1;
528     }
529 ';
530   //};
531   /*
532    '    if (!\'\'.replace(/^/, String)) {
533    // decode all the values we need
534    while ($count--) $decode[$encode($count)] = $keywords[$count] || $encode($count);
535    // global replacement function
536    $keywords = [function ($encoded) {return $decode[$encoded]}];
537    // generic match
538    $encode = function () {return\'\\\\w+\'};
539    // reset the loop counter -  we are now doing a global replace
540    $count = 1;
541    }';
542    */
543
544   // zero encoding
545   // characters: 0123456789
546   const JSFUNCTION_encode10 =
547 'function($charCode) {
548     return $charCode;
549 }'; //;';
550
551   // inherent base36 support
552   // characters: 0123456789abcdefghijklmnopqrstuvwxyz
553   const JSFUNCTION_encode36 =
554 'function($charCode) {
555     return $charCode.toString(36);
556 }'; //;';
557
558   // hitch a ride on base36 and add the upper case alpha characters
559   // characters: 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
560   const JSFUNCTION_encode62 =
561 'function($charCode) {
562     return ($charCode < _encoding ? \'\' : arguments.callee(parseInt($charCode / _encoding))) +
563     (($charCode = $charCode % _encoding) > 35 ? String.fromCharCode($charCode + 29) : $charCode.toString(36));
564 }';
565
566   // use high-ascii values
567   // characters: ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþ
568   const JSFUNCTION_encode95 =
569 'function($charCode) {
570     return ($charCode < _encoding ? \'\' : arguments.callee($charCode / _encoding)) +
571         String.fromCharCode($charCode % _encoding + 161);
572 }';
573
574 }
575
576
577 class ParseMaster {
578   public $ignoreCase = false;
579   public $escapeChar = '';
580
581   // constants
582   const EXPRESSION = 0;
583   const REPLACEMENT = 1;
584   const LENGTH = 2;
585
586   // used to determine nesting levels
587   private $GROUPS = '/\\(/'; //g
588   private $SUB_REPLACE = '/\\$\\d/';
589   private $INDEXED = '/^\\$\\d+$/';
590   private $TRIM = '/([\'"])\\1\\.(.*)\\.\\1\\1$/';
591   private $ESCAPE = '/\\\./'; //g
592   private $QUOTE = '/\'/';
593   private $DELETED = '/\\x01[^\\x01]*\\x01/'; //g
594
595   public function add($expression, $replacement = '') {
596     // count the number of sub-expressions
597 //  - add one because each pattern is itself a sub-expression
598     $length = 1 + preg_match_all($this->GROUPS, $this->_internalEscape((string) $expression), $out);
599
600     // treat only strings $replacement
601     if (is_string($replacement)) {
602       // does the pattern deal with sub-expressions?
603       if (preg_match($this->SUB_REPLACE, $replacement)) {
604         // a simple lookup? (e.g. "$2")
605         if (preg_match($this->INDEXED, $replacement)) {
606           // store the index (used for fast retrieval of matched strings)
607           $replacement = (int) (substr($replacement, 1)) - 1;
608         }
609         else { // a complicated lookup (e.g. "Hello $2 $1")
610           // build a function to do the lookup
611           $quote = preg_match($this->QUOTE, $this->_internalEscape($replacement))
612                                                  ? '"' : "'";
613           $replacement = array(
614             'fn' => '_backReferences',
615             'data' => array(
616               'replacement' => $replacement,
617               'length' => $length,
618               'quote' => $quote,
619             ),
620           );
621         }
622       }
623     }
624     // pass the modified arguments
625     if (!empty($expression)) {
626       $this->_add($expression, $replacement, $length);
627     }
628     else {
629       $this->_add('/^$/', $replacement, $length);
630     }
631   }
632
633   public function exec($string) {
634     // execute the global replacement
635     $this->_escaped = array();
636
637     // simulate the _patterns.toSTring of Dean
638     $regexp = '/';
639     foreach ($this->_patterns as $reg) {
640       $regexp .= '(' . substr($reg[self::EXPRESSION], 1, -1) . ')|';
641     }
642     $regexp = substr($regexp, 0, -1) . '/';
643     $regexp .= ($this->ignoreCase) ? 'i' : '';
644
645     $string = $this->_escape($string, $this->escapeChar);
646     $string = preg_replace_callback(
647                         $regexp,
648                         array(
649       &$this,
650       '_replacement',
651     ),
652                         $string
653                 );
654     $string = $this->_unescape($string, $this->escapeChar);
655
656     return preg_replace($this->DELETED, '', $string);
657   }
658
659   public function reset() {
660     // clear the patterns collection so that this object may be re-used
661     $this->_patterns = array();
662   }
663
664   // private
665   private $_escaped = array(); // escaped characters
666   private $_patterns = array(); // patterns stored by index
667
668   // create and add a new pattern to the patterns collection
669   private function _add() {
670     $arguments = func_get_args();
671     $this->_patterns[] = $arguments;
672   }
673
674   // this is the global replace function (it's quite complicated)
675   private function _replacement($arguments) {
676     if (empty($arguments)) {
677       return '';
678     }
679
680     $i = 1;
681     $j = 0;
682     // loop through the patterns
683     while (isset($this->_patterns[$j])) {
684       $pattern = $this->_patterns[$j++];
685       // do we have a result?
686       if (isset($arguments[$i]) && ($arguments[$i] != '')) {
687         $replacement = $pattern[self::REPLACEMENT];
688
689         if (is_array($replacement) && isset($replacement['fn'])) {
690
691           if (isset($replacement['data'])) {
692             $this->buffer = $replacement['data'];
693           }
694           return call_user_func(array(&$this, $replacement['fn']), $arguments, $i);
695
696         }
697         elseif (is_int($replacement)) {
698           return $arguments[$replacement + $i];
699
700         }
701         $delete = ($this->escapeChar == '' ||
702                                            strpos($arguments[$i], $this->escapeChar) === false)
703                                         ? '' : "\x01" . $arguments[$i] . "\x01";
704         return $delete . $replacement;
705
706         // skip over references to sub-expressions
707       }
708       else {
709         $i += $pattern[self::LENGTH];
710       }
711     }
712   }
713
714   private function _backReferences($match, $offset) {
715     $replacement = $this->buffer['replacement'];
716     $quote = $this->buffer['quote'];
717     $i = $this->buffer['length'];
718     while ($i) {
719       $replacement = str_replace('$' . $i--, $match[$offset + $i], $replacement);
720     }
721     return $replacement;
722   }
723
724   private function _replace_name($match, $offset) {
725     $length = strlen($match[$offset + 2]);
726     $start = $length - max($length - strlen($match[$offset + 3]), 0);
727     return substr($match[$offset + 1], $start, $length) . $match[$offset + 4];
728   }
729
730   private function _replace_encoded($match, $offset) {
731     return $this->buffer[$match[$offset]];
732   }
733
734
735   // php : we cannot pass additional data to preg_replace_callback,
736   // and we cannot use &$this in create_function, so let's go to lower level
737   private $buffer;
738
739   // encode escaped characters
740   private function _escape($string, $escapeChar) {
741     if ($escapeChar) {
742       $this->buffer = $escapeChar;
743       return preg_replace_callback(
744                                 '/\\' . $escapeChar . '(.)' . '/',
745                                 array(&$this, '_escapeBis'),
746                                 $string
747                         );
748
749     }
750     else {
751       return $string;
752     }
753   }
754   private function _escapeBis($match) {
755     $this->_escaped[] = $match[1];
756     return $this->buffer;
757   }
758
759   // decode escaped characters
760   private function _unescape($string, $escapeChar) {
761     if ($escapeChar) {
762       $regexp = '/' . '\\' . $escapeChar . '/';
763       $this->buffer = array(
764         'escapeChar' => $escapeChar,
765         'i' => 0,
766       );
767       return preg_replace_callback(
768                                 $regexp,
769                                 array(&$this, '_unescapeBis'),
770                                 $string
771                         );
772
773     }
774     else {
775       return $string;
776     }
777   }
778   private function _unescapeBis() {
779     if (isset($this->_escaped[$this->buffer['i']])
780                          && $this->_escaped[$this->buffer['i']] != '') {
781       $temp = $this->_escaped[$this->buffer['i']];
782     }
783     else {
784       $temp = '';
785     }
786     $this->buffer['i']++;
787     return $this->buffer['escapeChar'] . $temp;
788   }
789
790   private function _internalEscape($string) {
791     return preg_replace($this->ESCAPE, '', $string);
792   }
793 }