1<?php
2
3/**
4 * Shortcut to ref, HTML mode
5 *
6 * @param   mixed $args
7 * @return  void|string
8 */
9function r()
10{
11  // arguments passed to this function
12  $args = func_get_args();
13
14  // options (operators) gathered by the expression parser;
15  // this variable gets passed as reference to getInputExpressions(), which will store the operators in it
16  $options = array();
17
18  // names of the arguments that were passed to this function
19  $expressions = ref::getInputExpressions($options);
20  $capture = in_array('@', $options, true);
21
22  // something went wrong while trying to parse the source expressions?
23  // if so, silently ignore this part and leave out the expression info
24  if(func_num_args() !== count($expressions))
25  {
26    $expressions = null;
27  }
28
29  // use HTML formatter only if we're not in CLI mode, or if return was requested
30  $format = (php_sapi_name() !== 'cli') || $capture ? 'html' : 'cliText';
31
32  // IE goes funky if there's no doctype
33  if(!$capture && ($format === 'html') && !headers_sent() && (!ob_get_level() || ini_get('output_buffering')))
34  {
35    echo('<!DOCTYPE HTML><html><head><title>REF</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /></head><body>');
36  }
37
38  $ref = new ref($format);
39
40  // Observium specific paths
41  ref::config('stylePath',  $GLOBALS['config']['html_dir'] . '/css/ref.css');
42  ref::config('scriptPath', $GLOBALS['config']['html_dir'] . '/js/ref.js');
43
44  if ($capture)
45  {
46    ob_start();
47  }
48
49  foreach ($args as $index => $arg)
50  {
51    $ref->query($arg, $expressions ? $expressions[$index] : null);
52  }
53
54  // return the results if this function was called with the error suppression operator
55  if ($capture)
56  {
57    return ob_get_clean();
58  }
59
60  // stop the script if this function was called with the bitwise not operator
61  if (in_array('~', $options, true) && ($format === 'html'))
62  {
63    echo('</body></html>');
64    exit(0);
65  }
66}
67
68
69
70/**
71 * Shortcut to ref, plain text mode
72 *
73 * @param   mixed $args
74 * @return  void|string
75 */
76function rt()
77{
78  $args        = func_get_args();
79  $options     = array();
80  $output      = '';
81  $expressions = ref::getInputExpressions($options);
82  $capture     = in_array('@', $options, true);
83  $ref         = new ref((php_sapi_name() !== 'cli') || $capture ? 'text' : 'cliText');
84
85  if (func_num_args() !== count($expressions))
86  {
87    $expressions = null;
88  }
89
90  if (!headers_sent())
91  {
92    header('Content-Type: text/plain; charset=utf-8');
93  }
94
95  if ($capture)
96  {
97    ob_start();
98  }
99
100  foreach ($args as $index => $arg)
101  {
102    $ref->query($arg, $expressions ? $expressions[$index] : null);
103  }
104
105  if ($capture)
106  {
107    return ob_get_clean();
108  }
109
110  if (in_array('~', $options, true))
111  {
112    exit(0);
113  }
114}
115
116
117/**
118 * REF is a nicer alternative to PHP's print_r() / var_dump().
119 *
120 * @version  1.0
121 * @author   digitalnature - http://digitalnature.eu
122 */
123class ref
124{
125
126  const
127
128    MARKER_KEY = '_phpRefArrayMarker_';
129
130
131  protected static
132
133    /**
134     * CPU time used for processing
135     *
136     * @var  array
137     */
138    $time = 0,
139
140    /**
141     * Configuration (+ default values)
142     *
143     * @var  array
144     */
145    $config = array(
146
147                // initially expanded levels (for HTML mode only)
148                'expLvl'               => 1,
149
150                // depth limit (0 = no limit);
151                // this is not related to recursion
152                'maxDepth'             => 6,
153
154                // show the place where r() has been called from
155                'showBacktrace'        => true,
156
157                // if passed from high level function -> debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)
158                'Backtrace'            => NULL,
159
160                // display iterator contents
161                'showIteratorContents' => false,
162
163                // display extra information about resources
164                'showResourceInfo'     => true,
165
166                // display method and parameter list on objects
167                'showMethods'          => true,
168
169                // display private properties / methods
170                'showPrivateMembers'   => false,
171
172                // peform string matches (date, file, functions, classes, json, serialized data, regex etc.)
173                // note: seriously slows down queries on large amounts of data
174                'showStringMatches'    => true,
175
176                // shortcut functions used to access the query method below;
177                // if they are namespaced, the namespace must be present as well (methods are not supported)
178                'shortcutFunc'         => array('r', 'rt'),
179
180                // custom/external formatters (as associative array: format => className)
181                'formatters'           => array(),
182
183                // stylesheet path (for HTML only);
184                // 'false' means no styles
185                'stylePath'            => '{:dir}/ref.css',
186
187                // javascript path (for HTML only);
188                // 'false' means no js
189                'scriptPath'           => '{:dir}/ref.js',
190
191                // display url info via cURL
192                'showUrls'             => false,
193
194                // stop evaluation after this amount of time (seconds)
195                'timeout'              => 10,
196
197                // whether to produce W3c-valid HTML,
198                // or unintelligible, but optimized markup that takes less space
199                'validHtml'            => false,
200              ),
201
202    /**
203     * Some environment variables
204     * used to determine feature support
205     *
206     * @var  array
207     */
208    $env = array(),
209
210    /**
211     * Timeout point
212     *
213     * @var  bool
214     */
215    $timeout = -1,
216
217    $debug = array(
218      'cacheHits' => 0,
219      'objects'   => 0,
220      'arrays'    => 0,
221      'scalars'   => 0,
222    );
223
224
225  protected
226
227    /**
228     * Output formatter of this instance
229     *
230     * @var  RFormatter
231     */
232    $fmt = null,
233
234    /**
235     * Start time of the current instance
236     *
237     * @var  float
238     */
239        $startTime = 0,
240
241    /**
242     * Internally created objects
243     *
244     * @var  SplObjectStorage
245     */
246    $intObjects = null;
247
248
249  /**
250   * Constructor
251   *
252   * @param   string|RFormatter $format      Output format ID, or formatter instance defaults to 'html'
253   */
254  public function __construct($format = 'html')
255  {
256
257    static $didIni = false;
258
259    if (!$didIni)
260    {
261      $didIni = true;
262      foreach (array_keys(static::$config) as $key)
263      {
264        $iniVal = get_cfg_var('ref.' . $key);
265        if ($iniVal !== false)
266        {
267          static::$config[$key] = $iniVal;
268        }
269      }
270
271    }
272
273    if ($format instanceof RFormatter)
274    {
275      $this->fmt = $format;
276
277    } else {
278      $format = isset(static::$config['formatters'][$format]) ? static::$config['formatters'][$format] : 'R' . ucfirst($format) . 'Formatter';
279
280      if (!class_exists($format, false))
281      {
282        throw new \Exception(sprintf('%s class not found', $format));
283      }
284
285      $this->fmt = new $format();
286    }
287
288    if (static::$env)
289    {
290      return;
291    }
292
293    static::$env = array(
294
295      // php 5.4+ ?
296      'is54'         => version_compare(PHP_VERSION, '5.4') >= 0,
297
298      // php 5.4.6+ ?
299      'is546'        => version_compare(PHP_VERSION, '5.4.6') >= 0,
300
301      // php 5.6+
302      'is56'         => version_compare(PHP_VERSION, '5.6') >= 0,
303
304      // php 7.0+ ?
305      'is7'          => version_compare(PHP_VERSION, '7.0') >= 0,
306
307      // curl extension running?
308      'curlActive'   => function_exists('curl_version'),
309
310      // is the 'mbstring' extension active?
311      'mbStr'        => function_exists('mb_detect_encoding'),
312
313      // @see: https://bugs.php.net/bug.php?id=52469
314      'supportsDate' => (strncasecmp(PHP_OS, 'WIN', 3) !== 0) || (version_compare(PHP_VERSION, '5.3.10') >= 0),
315    );
316  }
317
318
319  /**
320   * Enforce proper use of this class
321   *
322   * @param   string $name
323   */
324  public function __get($name)
325  {
326    throw new \Exception(sprintf('No such property: %s', $name));
327  }
328
329
330  /**
331   * Enforce proper use of this class
332   *
333   * @param   string $name
334   * @param   mixed $value
335   */
336  public function __set($name, $value)
337  {
338    throw new \Exception(sprintf('Cannot set %s. Not allowed', $name));
339  }
340
341
342  /**
343   * Generate structured information about a variable/value/expression (subject)
344   *
345   * Output is flushed to the screen
346   *
347   * @param   mixed $subject
348   * @param   string $expression
349   */
350  public function query($subject, $expression = null)
351  {
352    if (static::$timeout > 0)
353    {
354      return;
355    }
356
357    $this->startTime = microtime(true);
358
359    $this->intObjects = new \SplObjectStorage();
360
361    $this->fmt->startRoot();
362    $this->fmt->startExp();
363    $this->evaluateExp($expression);
364    $this->fmt->endExp();
365    $this->evaluate($subject);
366    $this->fmt->endRoot();
367    $this->fmt->flush();
368
369    static::$time += microtime(true) - $this->startTime;
370  }
371
372
373  /**
374   * Executes a function the given number of times and returns the elapsed time.
375   *
376   * Keep in mind that the returned time includes function call overhead (including
377   * microtime calls) x iteration count. This is why this is better suited for
378   * determining which of two or more functions is the fastest, rather than
379   * finding out how fast is a single function.
380   *
381   * @param   int $iterations      Number of times the function will be executed
382   * @param   callable $function   Function to execute
383   * @param   mixed &$output       If given, last return value will be available in this variable
384   * @return  double               Elapsed time
385   */
386  public static function timeFunc($iterations, $function, &$output = null)
387  {
388
389    $time = 0;
390
391    for ($i = 0; $i < $iterations; $i++)
392    {
393      $start  = microtime(true);
394      $output = call_user_func($function);
395      $time  += microtime(true) - $start;
396    }
397
398    return round($time, 4);
399  }
400
401
402
403  /**
404   * Timer utility
405   *
406   * First call of this function will start the timer.
407   * The second call will stop the timer and return the elapsed time
408   * since the timer started.
409   *
410   * Multiple timers can be controlled simultaneously by specifying a timer ID.
411   *
412   * @since   1.0
413   * @param   int $id          Timer ID, optional
414   * @param   int $precision   Precision of the result, optional
415   * @return  void|double      Elapsed time, or void if the timer was just started
416   */
417  public static function timer($id = 1, $precision = 4)
418  {
419
420    static
421      $timers = array();
422
423    // check if this timer was started, and display the elapsed time if so
424    if (isset($timers[$id]))
425    {
426      $elapsed = round(microtime(true) - $timers[$id], $precision);
427      unset($timers[$id]);
428      return $elapsed;
429    }
430
431    // ID doesn't exist, start new timer
432    $timers[$id] = microtime(true);
433  }
434
435
436  /**
437   * Parses a DocBlock comment into a data structure.
438   *
439   * @link    http://pear.php.net/manual/en/standards.sample.php
440   * @param   string $comment    DocBlock comment (must start with /**)
441   * @param   string|null $key   Field to return (optional)
442   * @return  array|string|null  Array containing all fields, array/string with the contents of
443   *                             the requested field, or null if the comment is empty/invalid
444   */
445  public static function parseComment($comment, $key = null)
446  {
447
448    $description = '';
449    $tags        = array();
450    $tag         = null;
451    $pointer     = '';
452    $padding     = 0;
453    $comment     = preg_split('/\r\n|\r|\n/', '* ' . trim($comment, "/* \t\n\r\0\x0B"));
454
455    // analyze each line
456    foreach ($comment as $line)
457    {
458
459      // drop any wrapping spaces
460      $line = trim($line);
461
462      // drop "* "
463      if ($line !== '')
464      {
465        $line = substr($line, 2);
466      }
467
468      if (strpos($line, '@') !== 0)
469      {
470
471        // preserve formatting of tag descriptions,
472        // because they may span across multiple lines
473        if ($tag !== null)
474        {
475          $trimmed = trim($line);
476
477          if ($padding !== 0)
478          {
479            $trimmed = static::strPad($trimmed, static::strLen($line) - $padding, ' ', STR_PAD_LEFT);
480          } else {
481            $padding = static::strLen($line) - static::strLen($trimmed);
482          }
483
484          $pointer .= "\n{$trimmed}";
485          continue;
486        }
487
488        // tag definitions have not started yet; assume this is part of the description text
489        $description .= "\n{$line}";
490        continue;
491      }
492
493      $padding = 0;
494      $parts = explode(' ', $line, 2);
495
496      // invalid tag? (should we include it as an empty array?)
497      if (!isset($parts[1]))
498      {
499        continue;
500      }
501
502      $tag = substr($parts[0], 1);
503      $line = ltrim($parts[1]);
504
505      // tags that have a single component (eg. link, license, author, throws...);
506      // note that @throws may have 2 components, however most people use it like "@throws ExceptionClass if whatever...",
507      // which, if broken into two values, leads to an inconsistent description sentence
508      if (!in_array($tag, array('global', 'param', 'return', 'var')))
509      {
510        $tags[$tag][] = $line;
511        end($tags[$tag]);
512        $pointer = &$tags[$tag][key($tags[$tag])];
513        continue;
514      }
515
516      // tags with 2 or 3 components (var, param, return);
517      $parts    = explode(' ', $line, 2);
518      $parts[1] = isset($parts[1]) ? ltrim($parts[1]) : null;
519      $lastIdx  = 1;
520
521      // expecting 3 components on the 'param' tag: type varName varDescription
522      if ($tag === 'param')
523      {
524        $lastIdx = 2;
525        if (in_array($parts[1][0], array('&', '$'), true))
526        {
527          $line     = ltrim(array_pop($parts));
528          $parts    = array_merge($parts, explode(' ', $line, 2));
529          $parts[2] = isset($parts[2]) ? ltrim($parts[2]) : null;
530        } else {
531          $parts[2] = $parts[1];
532          $parts[1] = null;
533        }
534      }
535
536      $tags[$tag][] = $parts;
537      end($tags[$tag]);
538      $pointer = &$tags[$tag][key($tags[$tag])][$lastIdx];
539    }
540
541    // split title from the description texts at the nearest 2x new-line combination
542    // (note: loose check because 0 isn't valid as well)
543    if (strpos($description, "\n\n"))
544    {
545      list($title, $description) = explode("\n\n", $description, 2);
546
547    // if we don't have 2 new lines, try to extract first sentence
548    } else {
549      // in order for a sentence to be considered valid,
550      // the next one must start with an uppercase letter
551      $sentences = preg_split('/(?<=[.?!])\s+(?=[A-Z])/', $description, 2, PREG_SPLIT_NO_EMPTY);
552
553      // failed to detect a second sentence? then assume there's only title and no description text
554      $title = isset($sentences[0]) ? $sentences[0] : $description;
555      $description = isset($sentences[1]) ? $sentences[1] : '';
556    }
557
558    $title = ltrim($title);
559    $description = ltrim($description);
560
561    $data = compact('title', 'description', 'tags');
562
563    if (!array_filter($data))
564    {
565      return null;
566    }
567
568    if ($key !== null)
569    {
570      return isset($data[$key]) ? $data[$key] : null;
571    }
572
573    return $data;
574  }
575
576
577  /**
578   * Split a regex into its components
579   *
580   * Based on "Regex Colorizer" by Steven Levithan (this is a translation from javascript)
581   *
582   * @link     https://github.com/slevithan/regex-colorizer
583   * @link     https://github.com/symfony/Finder/blob/master/Expression/Regex.php#L64-74
584   * @param    string $pattern
585   * @return   array
586   */
587  public static function splitRegex($pattern)
588  {
589
590    // detection attempt code from the Symfony Finder component
591    $maybeValid = false;
592    if (preg_match('/^(.{3,}?)([imsxuADU]*)$/', $pattern, $m))
593    {
594      $start = substr($m[1], 0, 1);
595      $end   = substr($m[1], -1);
596
597      if (($start === $end && !preg_match('/[*?[:alnum:] \\\\]/', $start)) || ($start === '{' && $end === '}'))
598      {
599        $maybeValid = true;
600      }
601    }
602
603    if (!$maybeValid)
604    {
605      throw new \Exception('Pattern does not appear to be a valid PHP regex');
606    }
607
608    $output              = array();
609    $capturingGroupCount = 0;
610    $groupStyleDepth     = 0;
611    $openGroups          = array();
612    $lastIsQuant         = false;
613    $lastType            = 1;      // 1 = none; 2 = alternator
614    $lastStyle           = null;
615
616    preg_match_all('/\[\^?]?(?:[^\\\\\]]+|\\\\[\S\s]?)*]?|\\\\(?:0(?:[0-3][0-7]{0,2}|[4-7][0-7]?)?|[1-9][0-9]*|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|c[A-Za-z]|[\S\s]?)|\((?:\?[:=!]?)?|(?:[?*+]|\{[0-9]+(?:,[0-9]*)?\})\??|[^.?*+^${[()|\\\\]+|./', $pattern, $matches);
617
618    $matches = $matches[0];
619
620    $getTokenCharCode = function($token)
621    {
622      if (strlen($token) > 1 && $token[0] === '\\')
623      {
624        $t1 = substr($token, 1);
625
626        if (preg_match('/^c[A-Za-z]$/', $t1))
627        {
628          return strpos("ABCDEFGHIJKLMNOPQRSTUVWXYZ", strtoupper($t1[1])) + 1;
629        }
630
631        if (preg_match('/^(?:x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4})$/', $t1))
632        {
633          return intval(substr($t1, 1), 16);
634        }
635
636        if (preg_match('/^(?:[0-3][0-7]{0,2}|[4-7][0-7]?)$/', $t1))
637        {
638          return intval($t1, 8);
639        }
640
641        $len = strlen($t1);
642
643        if ($len === 1 && strpos('cuxDdSsWw', $t1) !== false)
644        {
645          return null;
646        }
647
648        if ($len === 1)
649        {
650          switch ($t1)
651          {
652            case 'b': return 8;
653            case 'f': return 12;
654            case 'n': return 10;
655            case 'r': return 13;
656            case 't': return 9;
657            case 'v': return 11;
658            default: return $t1[0];
659          }
660        }
661      }
662
663      return ($token !== '\\') ? $token[0] : null;
664    };
665
666    foreach ($matches as $m)
667    {
668
669      if ($m[0] === '[')
670      {
671        $lastCC         = null;
672        $cLastRangeable = false;
673        $cLastType      = 0;  // 0 = none; 1 = range hyphen; 2 = short class
674
675        preg_match('/^(\[\^?)(]?(?:[^\\\\\]]+|\\\\[\S\s]?)*)(]?)$/', $m, $parts);
676
677        array_shift($parts);
678        list($opening, $content, $closing) = $parts;
679
680        if (!$closing)
681        {
682          throw new \Exception('Unclosed character class');
683        }
684
685        preg_match_all('/[^\\\\-]+|-|\\\\(?:[0-3][0-7]{0,2}|[4-7][0-7]?|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|c[A-Za-z]|[\S\s]?)/', $content, $ccTokens);
686        $ccTokens     = $ccTokens[0];
687        $ccTokenCount = count($ccTokens);
688        $output[]     = array('chr' => $opening);
689
690        foreach($ccTokens as $i => $cm)
691        {
692
693          if ($cm[0] === '\\')
694          {
695            if (preg_match('/^\\\\[cux]$/', $cm))
696            {
697              throw new \Exception('Incomplete regex token');
698            }
699
700            if (preg_match('/^\\\\[dsw]$/i', $cm))
701            {
702              $output[]     = array('chr-meta' => $cm);
703              $cLastRangeable  = ($cLastType !== 1);
704              $cLastType       = 2;
705
706            }
707            else if ($cm === '\\')
708            {
709              throw new \Exception('Incomplete regex token');
710
711            } else {
712              $output[]       = array('chr-meta' => $cm);
713              $cLastRangeable = $cLastType !== 1;
714              $lastCC         = $getTokenCharCode($cm);
715            }
716
717          }
718          else if($cm === '-')
719          {
720            if ($cLastRangeable)
721            {
722              $nextToken = ($i + 1 < $ccTokenCount) ? $ccTokens[$i + 1] : false;
723
724              if ($nextToken)
725              {
726                $nextTokenCharCode = $getTokenCharCode($nextToken[0]);
727
728                if ((!is_null($nextTokenCharCode) && $lastCC > $nextTokenCharCode) || $cLastType === 2 || preg_match('/^\\\\[dsw]$/i', $nextToken[0]))
729                {
730                  throw new \Exception('Reversed or invalid range');
731                }
732
733                $output[]       = array('chr-range' => '-');
734                $cLastRangeable = false;
735                $cLastType      = 1;
736
737              } else {
738                $output[] = $closing ? array('chr' => '-') : array('chr-range' => '-');
739              }
740
741            } else {
742              $output[]        = array('chr' => '-');
743              $cLastRangeable  = ($cLastType !== 1);
744            }
745
746          } else {
747            $output[]       = array('chr' => $cm);
748            $cLastRangeable = strlen($cm) > 1 || ($cLastType !== 1);
749            $lastCC         = $cm[strlen($cm) - 1];
750          }
751        }
752
753        $output[] = array('chr' => $closing);
754        $lastIsQuant  = true;
755
756      }
757      else if ($m[0] === '(')
758      {
759        if (strlen($m) === 2)
760        {
761          throw new \Exception('Invalid or unsupported group type');
762        }
763
764        if (strlen($m) === 1)
765        {
766          $capturingGroupCount++;
767        }
768
769        $groupStyleDepth = ($groupStyleDepth !== 5) ? $groupStyleDepth + 1 : 1;
770        $openGroups[]    = $m; // opening
771        $lastIsQuant     = false;
772        $output[]        = array("g{$groupStyleDepth}" => $m);
773
774      }
775      else if ($m[0] === ')')
776      {
777        if (!count($openGroups))
778        {
779          throw new \Exception('No matching opening parenthesis');
780        }
781
782        $output[]        = array('g' . $groupStyleDepth => ')');
783        $prevGroup       = $openGroups[count($openGroups) - 1];
784        $prevGroup       = isset($prevGroup[2]) ? $prevGroup[2] : '';
785        $lastIsQuant     = !preg_match('/^[=!]/', $prevGroup);
786        $lastStyle       = "g{$groupStyleDepth}";
787        $lastType        = 0;
788        $groupStyleDepth = ($groupStyleDepth !== 1) ? $groupStyleDepth - 1 : 5;
789
790        array_pop($openGroups);
791        continue;
792
793      }
794      else if ($m[0] === '\\')
795      {
796        if (isset($m[1]) && preg_match('/^[1-9]/', $m[1]))
797        {
798          $nonBackrefDigits = '';
799          $num = substr(+$m, 1);
800
801          while ($num > $capturingGroupCount)
802          {
803            preg_match('/[0-9]$/', $num, $digits);
804            $nonBackrefDigits = $digits[0] . $nonBackrefDigits;
805            $num = floor($num / 10);
806          }
807
808          if ($num > 0)
809          {
810            $output[] = array('meta' =>  "\\{$num}", 'text' => $nonBackrefDigits);
811
812          } else {
813            preg_match('/^\\\\([0-3][0-7]{0,2}|[4-7][0-7]?|[89])([0-9]*)/', $m, $pts);
814            $output[] = array('meta' => '\\' . $pts[1], 'text' => $pts[2]);
815          }
816
817          $lastIsQuant = true;
818
819        }
820        else if (isset($m[1]) && preg_match('/^[0bBcdDfnrsStuvwWx]/', $m[1]))
821        {
822
823          if (preg_match('/^\\\\[cux]$/', $m))
824          {
825            throw new \Exception('Incomplete regex token');
826          }
827
828          $output[]    = array('meta' => $m);
829          $lastIsQuant = (strpos('bB', $m[1]) === false);
830
831        }
832        else if ($m === '\\')
833        {
834          throw new \Exception('Incomplete regex token');
835
836        } else {
837          $output[]    = array('text' => $m);
838          $lastIsQuant = true;
839        }
840
841      }
842      else if (preg_match('/^(?:[?*+]|\{[0-9]+(?:,[0-9]*)?\})\??$/', $m))
843      {
844        if (!$lastIsQuant)
845        {
846          throw new \Exception('Quantifiers must be preceded by a token that can be repeated');
847        }
848
849        preg_match('/^\{([0-9]+)(?:,([0-9]*))?/', $m, $interval);
850
851        if ($interval && (+$interval[1] > 65535 || (isset($interval[2]) && (+$interval[2] > 65535))))
852        {
853          throw new \Exception('Interval quantifier cannot use value over 65,535');
854        }
855
856        if ($interval && isset($interval[2]) && (+$interval[1] > +$interval[2]))
857        {
858          throw new \Exception('Interval quantifier range is reversed');
859        }
860
861        $output[]     = array($lastStyle ? $lastStyle : 'meta' => $m);
862        $lastIsQuant  = false;
863
864      }
865      else if ($m === '|')
866      {
867        if ($lastType === 1 || ($lastType === 2 && !count($openGroups)))
868        {
869          throw new \Exception('Empty alternative effectively truncates the regex here');
870        }
871
872        $output[]    = count($openGroups) ? array("g{$groupStyleDepth}" => '|') : array('meta' => '|');
873        $lastIsQuant = false;
874        $lastType    = 2;
875        $lastStyle   = '';
876        continue;
877
878      }
879      else if ($m === '^' || $m === '$')
880      {
881        $output[]    = array('meta' => $m);
882        $lastIsQuant = false;
883
884      }
885      else if ($m === '.')
886      {
887        $output[]    = array('meta' => '.');
888        $lastIsQuant = true;
889
890      } else {
891        $output[]    = array('text' => $m);
892        $lastIsQuant = true;
893      }
894
895      $lastType  = 0;
896      $lastStyle = '';
897    }
898
899    if ($openGroups)
900    {
901      throw new \Exception('Unclosed grouping');
902    }
903
904    return $output;
905  }
906
907
908  /**
909   * Set or get configuration options
910   *
911   * @param   string $key
912   * @param   mixed|null $value
913   * @return  mixed
914   */
915  public static function config($key, $value = null)
916  {
917
918    if (!array_key_exists($key, static::$config))
919    {
920      throw new \Exception(sprintf('Unrecognized option: "%s". Valid options are: %s', $key, implode(', ', array_keys(static::$config))));
921    }
922
923    if ($value === null)
924    {
925      return static::$config[$key];
926    }
927
928    if (is_array(static::$config[$key]))
929    {
930      return static::$config[$key] = (array)$value;
931    }
932
933    return static::$config[$key] = $value;
934  }
935
936
937  /**
938   * Total CPU time used by the class
939   *
940   * @param   int precision
941   * @return  double
942   */
943  public static function getTime($precision = 4)
944  {
945    return round(static::$time, $precision);
946  }
947
948
949  /**
950   * Get relevant backtrace info for last ref call
951   *
952   * @return  array|false
953   */
954  public static function getBacktrace()
955  {
956
957    if (ref::config('showBacktrace'))
958    {
959      // pull only basic info with php 5.3.6+ to save some memory
960      if (NULL !== ref::config('Backtrace'))
961      {
962        // Observium hack for get original backtrace
963        $trace = ref::config('Backtrace');
964      } else {
965        $trace = defined('DEBUG_BACKTRACE_IGNORE_ARGS') ? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) : debug_backtrace();
966      }
967    }
968
969    while ($callee = array_pop($trace))
970    {
971
972      // extract only the information we need
973      $callee = array_intersect_key($callee, array_fill_keys(array('file', 'function', 'line'), false));
974      extract($callee, EXTR_OVERWRITE);
975
976      // skip, if the called function doesn't match the shortcut function name
977      if (!$function || !in_array(strtolower((string)$function), static::$config['shortcutFunc']))
978      {
979        continue;
980      }
981
982      return compact('file', 'function', 'line');
983    }
984
985    return false;
986  }
987
988
989  /**
990   * Determines the input expression(s) passed to the shortcut function
991   *
992   * @param   array &$options   Optional, options to gather (from operators)
993   * @return  array             Array of string expressions
994   */
995  public static function getInputExpressions(array &$options = null)
996  {
997
998    // used to determine the position of the current call,
999    // if more queries calls were made on the same line
1000    static $lineInst = array();
1001
1002    $trace = static::getBacktrace();
1003
1004    if (!$trace)
1005    {
1006      return array();
1007    }
1008
1009    extract($trace);
1010
1011    $code     = file($file);
1012    $code     = $code[$line - 1]; // multiline expressions not supported!
1013    $instIndx = 0;
1014    $tokens   = token_get_all("<?php {$code}");
1015
1016    // locate the caller position in the line, and isolate argument tokens
1017    foreach ($tokens as $i => $token)
1018    {
1019
1020      // match token with our shortcut function name
1021      if (is_string($token) || ($token[0] !== T_STRING) || (strcasecmp($token[1], $function) !== 0))
1022      {
1023        continue;
1024      }
1025
1026      // is this some method that happens to have the same name as the shortcut function?
1027      if (isset($tokens[$i - 1]) && is_array($tokens[$i - 1]) && in_array($tokens[$i - 1][0], array(T_DOUBLE_COLON, T_OBJECT_OPERATOR), true))
1028      {
1029        continue;
1030      }
1031
1032      // find argument definition start, just after '('
1033      if (isset($tokens[$i + 1]) && ($tokens[$i + 1][0] === '('))
1034      {
1035        $instIndx++;
1036
1037        if (!isset($lineInst[$line]))
1038        {
1039          $lineInst[$line] = 0;
1040        }
1041
1042        if ($instIndx <= $lineInst[$line])
1043        {
1044          continue;
1045        }
1046
1047        $lineInst[$line]++;
1048
1049        // gather options
1050        if ($options !== null)
1051        {
1052          $j = $i - 1;
1053          while (isset($tokens[$j]) && is_string($tokens[$j]) && in_array($tokens[$j], array('@', '+', '-', '!', '~')))
1054          {
1055            $options[] = $tokens[$j--];
1056          }
1057        }
1058
1059        $lvl = $index = $curlies = 0;
1060        $expressions = array();
1061
1062        // get the expressions
1063        foreach (array_slice($tokens, $i + 2) as $token)
1064        {
1065
1066          if (is_array($token))
1067          {
1068            if ($token[0] !== T_COMMENT)
1069            {
1070              $expressions[$index][] = ($token[0] !== T_WHITESPACE) ? $token[1] : ' ';
1071            }
1072
1073            continue;
1074          }
1075
1076          if ($token === '{')
1077          {
1078            $curlies++;
1079          }
1080
1081          if ($token === '}')
1082          {
1083            $curlies--;
1084          }
1085
1086          if ($token === '(')
1087          {
1088            $lvl++;
1089          }
1090
1091          if ($token === ')')
1092          {
1093            $lvl--;
1094          }
1095
1096          // assume next argument if a comma was encountered,
1097          // and we're not insde a curly bracket or inner parentheses
1098          if (($curlies < 1) && ($lvl === 0) && ($token === ','))
1099          {
1100            $index++;
1101            continue;
1102          }
1103
1104          // negative parentheses count means we reached the end of argument definitions
1105          if ($lvl < 0)
1106          {
1107            foreach($expressions as &$expression)
1108            {
1109              $expression = trim(implode('', $expression));
1110            }
1111
1112            return $expressions;
1113          }
1114
1115          $expressions[$index][] = $token;
1116        }
1117
1118        break;
1119      }
1120    }
1121
1122  }
1123
1124
1125  /**
1126   * Get all parent classes of a class
1127   *
1128   * @param   Reflector $class   Reflection object
1129   * @return  array              Array of ReflectionClass objects (starts with the ancestor, ends with the given class)
1130   */
1131  protected static function getParentClasses(\Reflector $class)
1132  {
1133
1134    $parents = array($class);
1135    while (($class = $class->getParentClass()) !== false)
1136    {
1137      $parents[] = $class;
1138    }
1139
1140    return array_reverse($parents);
1141  }
1142
1143
1144
1145  /**
1146   * Generate class / function info
1147   *
1148   * @param   Reflector $reflector      Class name or reflection object
1149   * @param   string $single            Skip parent classes
1150   * @param   Reflector|null $context   Object context (for methods)
1151   * @return  string
1152   */
1153  protected function fromReflector(\Reflector $reflector, $single = '', \Reflector $context = null)
1154  {
1155
1156    // @todo: test this
1157    $hash = var_export(func_get_args(), true);
1158    //$hash = $reflector->getName() . ';' . $single . ';' . ($context ? $context->getName() : '');
1159
1160    if ($this->fmt->didCache($hash))
1161    {
1162      static::$debug['cacheHits']++;
1163      return;
1164    }
1165
1166    $items = array($reflector);
1167
1168    if (($single === '') && ($reflector instanceof \ReflectionClass))
1169    {
1170      $items = static::getParentClasses($reflector);
1171    }
1172
1173    $first = true;
1174    foreach ($items as $item)
1175    {
1176
1177      if (!$first)
1178      {
1179        $this->fmt->sep(' :: ');
1180      }
1181
1182      $first    = false;
1183      $name     = ($single !== '') ? $single : $item->getName();
1184      $comments = $item->isInternal() ? array() : static::parseComment($item->getDocComment());
1185      $meta     = array('sub' => array());
1186      $bubbles  = array();
1187
1188      if ($item->isInternal())
1189      {
1190        $extension = $item->getExtension();
1191        $meta['title'] = ($extension instanceof \ReflectionExtension) ? sprintf('Internal - part of %s (%s)', $extension->getName(), $extension->getVersion()) : 'Internal';
1192
1193      } else {
1194        $comments = static::parseComment($item->getDocComment());
1195
1196        if ($comments)
1197        {
1198          $meta += $comments;
1199        }
1200
1201        $meta['sub'][] = array('Defined in', basename($item->getFileName()) . ':' . $item->getStartLine());
1202      }
1203
1204      if (($item instanceof \ReflectionFunction) || ($item instanceof \ReflectionMethod))
1205      {
1206        if (($context !== null) && ($context->getShortName() !== $item->getDeclaringClass()->getShortName()))
1207        {
1208          $meta['sub'][] = array('Inherited from', $item->getDeclaringClass()->getShortName());
1209        }
1210
1211        // @note: PHP 7 seems to crash when calling getPrototype on Closure::__invoke()
1212        if (($item instanceof \ReflectionMethod) && !$item->isInternal())
1213        {
1214          try
1215          {
1216            $proto = $item->getPrototype();
1217            $meta['sub'][] = array('Prototype defined by', $proto->class);
1218          } catch(\Exception $e) {}
1219        }
1220
1221        $this->fmt->text('name', $name, $meta, $this->linkify($item));
1222        continue;
1223      }
1224
1225      // @todo: maybe - list interface methods
1226      if (!($item->isInterface() || (static::$env['is54'] && $item->isTrait())))
1227      {
1228
1229        if ($item->isAbstract())
1230        {
1231          $bubbles[] = array('A', 'Abstract');
1232        }
1233
1234        if (static::$env['is7'] && $item->isAnonymous())
1235        {
1236          $bubbles[] = array('?', 'Anonymous');
1237        }
1238
1239        if ($item->isFinal())
1240        {
1241          $bubbles[] = array('F', 'Final');
1242        }
1243
1244        // php 5.4+ only
1245        if (static::$env['is54'] && $item->isCloneable())
1246        {
1247          $bubbles[] = array('C', 'Cloneable');
1248        }
1249
1250        if ($item->isIterateable())
1251        {
1252          $bubbles[] = array('X', 'Iterateable');
1253        }
1254
1255      }
1256
1257      if ($item->isInterface() && $single !== '')
1258      {
1259        $bubbles[] = array('I', 'Interface');
1260      }
1261
1262      if ($bubbles)
1263      {
1264        $this->fmt->bubbles($bubbles);
1265      }
1266
1267      if ($item->isInterface() && $single === '')
1268      {
1269        $name .= sprintf(' (%d)', count($item->getMethods()));
1270      }
1271
1272      $this->fmt->text('name', $name, $meta, $this->linkify($item));
1273    }
1274
1275    $this->fmt->cacheLock($hash);
1276  }
1277
1278
1279  /**
1280   * Generates an URL that points to the documentation page relevant for the requested context
1281   *
1282   * For internal functions and classes, the URI will point to the local PHP manual
1283   * if installed and configured, otherwise to php.net/manual (the english one)
1284   *
1285   * @param   Reflector $reflector    Reflector object (used to determine the URL scheme for internal stuff)
1286   * @param   string|null $constant   Constant name, if this is a request to linkify a constant
1287   * @return  string|null             URL
1288   */
1289  protected function linkify(\Reflector $reflector, $constant = null)
1290  {
1291
1292    static $docRefRoot = null, $docRefExt = null;
1293
1294    // most people don't have this set
1295    if (!$docRefRoot)
1296    {
1297      $docRefRoot = ($docRefRoot = rtrim(ini_get('docref_root'), '/')) ? $docRefRoot : 'http://php.net/manual/en';
1298    }
1299
1300    if (!$docRefExt)
1301    {
1302      $docRefExt = ($docRefExt = ini_get('docref_ext')) ? $docRefExt : '.php';
1303    }
1304
1305    $phpNetSchemes = array(
1306      'class'     => $docRefRoot . '/class.%s'    . $docRefExt,
1307      'function'  => $docRefRoot . '/function.%s' . $docRefExt,
1308      'method'    => $docRefRoot . '/%2$s.%1$s'   . $docRefExt,
1309      'property'  => $docRefRoot . '/class.%2$s'  . $docRefExt . '#%2$s.props.%1$s',
1310      'constant'  => $docRefRoot . '/class.%2$s'  . $docRefExt . '#%2$s.constants.%1$s',
1311    );
1312
1313    $url  = null;
1314    $args = array();
1315
1316    // determine scheme
1317    if ($constant !== null)
1318    {
1319      $type = 'constant';
1320      $args[] = $constant;
1321
1322    } else {
1323      $type = explode('\\', get_class($reflector));
1324      $type = strtolower(ltrim(end($type), 'Reflection'));
1325
1326      if ($type === 'object')
1327      {
1328        $type = 'class';
1329      }
1330    }
1331
1332    // properties don't have the internal flag;
1333    // also note that many internal classes use some kind of magic as properties (eg. DateTime);
1334    // these will only get linkifed if the declared class is internal one, and not an extension :(
1335    $parent = ($type !== 'property') ? $reflector : $reflector->getDeclaringClass();
1336
1337    // internal function/method/class/property/constant
1338    if ($parent->isInternal())
1339    {
1340      $args[] = $reflector->name;
1341
1342      if (in_array($type, array('method', 'property'), true))
1343      {
1344        $args[] = $reflector->getDeclaringClass()->getName();
1345      }
1346
1347      $args = array_map(
1348        function ($text)
1349        {
1350          return str_replace('_', '-', ltrim(strtolower($text), '\\_'));
1351        }, $args);
1352
1353      // check for some special cases that have no links
1354      $valid = (($type === 'method') || (strcasecmp($parent->name, 'stdClass') !== 0))
1355            && (($type !== 'method') || (($reflector->name === '__construct') || strpos($reflector->name, '__') !== 0));
1356
1357      if ($valid)
1358      {
1359        $url = vsprintf($phpNetSchemes[$type], $args);
1360      }
1361
1362    // custom
1363    } else {
1364      switch (true)
1365      {
1366
1367        // WordPress function;
1368        // like pretty much everything else in WordPress, API links are inconsistent as well;
1369        // so we're using queryposts.com as doc source for API
1370        case ($type === 'function') && class_exists('WP', false) && defined('ABSPATH') && defined('WPINC'):
1371          if (strpos($reflector->getFileName(), realpath(ABSPATH . WPINC)) === 0)
1372          {
1373            $url = sprintf('http://queryposts.com/function/%s', urlencode(strtolower($reflector->getName())));
1374            break;
1375          }
1376
1377        // @todo: handle more apps
1378      }
1379
1380    }
1381
1382    return $url;
1383  }
1384
1385
1386  public static function getTimeoutPoint()
1387  {
1388    return static::$timeout;
1389  }
1390
1391
1392  public static function getDebugInfo()
1393  {
1394    return static::$debug;
1395  }
1396
1397
1398
1399  protected function hasInstanceTimedOut()
1400  {
1401
1402    if (static::$timeout > 0)
1403    {
1404      return true;
1405    }
1406
1407    $timeout = static::$config['timeout'];
1408
1409    if (($timeout > 0) && ((microtime(true) - $this->startTime) > $timeout))
1410    {
1411      return (static::$timeout = (microtime(true) - $this->startTime));
1412    }
1413
1414    return false;
1415  }
1416
1417
1418  /**
1419   * Evaluates the given variable
1420   *
1421   * @param   mixed &$subject   Variable to query
1422   * @param   bool $specialStr  Should this be interpreted as a special string?
1423   * @return  mixed             Result (both HTML and text modes generate strings)
1424   */
1425  protected function evaluate(&$subject, $specialStr = false)
1426  {
1427
1428    switch ($type = gettype($subject))
1429    {
1430
1431      // https://github.com/digitalnature/php-ref/issues/13
1432      case 'unknown type':
1433        return $this->fmt->text('unknown');
1434
1435      // null value
1436      case 'NULL':
1437        return $this->fmt->text('null');
1438
1439      // integer/double/float
1440      case 'integer':
1441      case 'double':
1442        return $this->fmt->text($type, $subject, $type);
1443
1444      // boolean
1445      case 'boolean':
1446        $text = $subject ? 'true' : 'false';
1447        return $this->fmt->text($text, $text, $type);
1448
1449      // arrays
1450      case 'array':
1451
1452        // empty array?
1453        if (empty($subject))
1454        {
1455          $this->fmt->text('array');
1456          return $this->fmt->emptyGroup();
1457        }
1458
1459        if (isset($subject[static::MARKER_KEY]))
1460        {
1461          unset($subject[static::MARKER_KEY]);
1462          $this->fmt->text('array');
1463          $this->fmt->emptyGroup('recursion');
1464          return;
1465        }
1466
1467        // first recursion level detection;
1468        // this is optional (used to print consistent recursion info)
1469        foreach ($subject as $key => &$value)
1470        {
1471
1472          if (!is_array($value))
1473          {
1474            continue;
1475          }
1476
1477          // save current value in a temporary variable
1478          $buffer = $value;
1479
1480          // assign new value
1481          $value = ($value !== 1) ? 1 : 2;
1482
1483          // if they're still equal, then we have a reference
1484          if ($value === $subject)
1485          {
1486            $value = $buffer;
1487            $value[static::MARKER_KEY] = true;
1488            $this->evaluate($value);
1489            return;
1490          }
1491
1492          // restoring original value
1493          $value = $buffer;
1494        }
1495
1496        $this->fmt->text('array');
1497        $count = count($subject);
1498        if (!$this->fmt->startGroup($count))
1499        {
1500          return;
1501        }
1502
1503        $max = max(array_map('static::strLen', array_keys($subject)));
1504        $subject[static::MARKER_KEY] = true;
1505
1506        foreach ($subject as $key => &$value)
1507        {
1508
1509          // ignore our temporary marker
1510          if ($key === static::MARKER_KEY)
1511          {
1512            continue;
1513          }
1514
1515          if ($this->hasInstanceTimedOut())
1516          {
1517            break;
1518          }
1519
1520          $keyInfo = gettype($key);
1521
1522          if ($keyInfo === 'string')
1523          {
1524            $encoding = static::$env['mbStr'] ? mb_detect_encoding($key) : '';
1525            $keyLen   = $encoding && ($encoding !== 'ASCII') ? static::strLen($key) . '; ' . $encoding : static::strLen($key);
1526            $keyInfo  = "{$keyInfo}({$keyLen})";
1527          } else {
1528            $keyLen   = strlen($key);
1529          }
1530
1531          $this->fmt->startRow();
1532          $this->fmt->text('key', $key, "Key: {$keyInfo}");
1533          $this->fmt->colDiv($max - $keyLen);
1534          $this->fmt->sep('=>');
1535          $this->fmt->colDiv();
1536          $this->evaluate($value, $specialStr);
1537          $this->fmt->endRow();
1538        }
1539
1540        unset($subject[static::MARKER_KEY]);
1541
1542        $this->fmt->endGroup();
1543        return;
1544
1545      // resource
1546      case 'resource':
1547        $meta    = array();
1548        $resType = get_resource_type($subject);
1549
1550        $this->fmt->text('resource', strval($subject));
1551
1552        if (!static::$config['showResourceInfo'])
1553        {
1554          return $this->fmt->emptyGroup($resType);
1555        }
1556
1557        // @see: http://php.net/manual/en/resource.php
1558        // need to add more...
1559        switch ($resType)
1560        {
1561
1562          // curl extension resource
1563          case 'curl':
1564            $meta = curl_getinfo($subject);
1565            break;
1566
1567          case 'FTP Buffer':
1568            $meta = array(
1569              'time_out'  => ftp_get_option($subject, FTP_TIMEOUT_SEC),
1570              'auto_seek' => ftp_get_option($subject, FTP_AUTOSEEK),
1571            );
1572
1573            break;
1574
1575          // gd image extension resource
1576          case 'gd':
1577            $meta = array(
1578               'size'       => sprintf('%d x %d', imagesx($subject), imagesy($subject)),
1579               'true_color' => imageistruecolor($subject),
1580            );
1581
1582            break;
1583
1584          case 'ldap link':
1585            $constants = get_defined_constants();
1586
1587            array_walk($constants,  function($value, $key) use(&$constants)
1588                                    {
1589                                      if (strpos($key, 'LDAP_OPT_') !== 0)
1590                                      {
1591                                        unset($constants[$key]);
1592                                      }
1593                                    });
1594
1595            // this seems to fail on my setup :(
1596            unset($constants['LDAP_OPT_NETWORK_TIMEOUT']);
1597
1598            foreach (array_slice($constants, 3) as $key => $value)
1599            {
1600              if (ldap_get_option($subject, (int)$value, $ret))
1601              {
1602                $meta[strtolower(substr($key, 9))] = $ret;
1603              }
1604            }
1605
1606            break;
1607
1608          /* mysql connection (mysql extension is deprecated from php 5.4/5.5)
1609          case 'mysql link':
1610          case 'mysql link persistent':
1611            $dbs = array();
1612            $query = @mysql_list_dbs($subject);
1613            while ($row = @mysql_fetch_array($query))
1614            {
1615              $dbs[] = $row['Database'];
1616            }
1617
1618            $meta = array(
1619              'host'             => ltrim(@mysql_get_host_info ($subject), 'MySQL host info: '),
1620              'server_version'   => @mysql_get_server_info($subject),
1621              'protocol_version' => @mysql_get_proto_info($subject),
1622              'databases'        => $dbs,
1623            );
1624
1625            break;
1626
1627          // mysql result
1628          case 'mysql result':
1629            while ($row = @mysql_fetch_object($subject))
1630            {
1631              $meta[] = (array)$row;
1632
1633              if ($this->hasInstanceTimedOut())
1634              {
1635                break;
1636              }
1637            }
1638
1639            break;
1640          */
1641
1642          // stream resource (fopen, fsockopen, popen, opendir etc)
1643          case 'stream':
1644            $meta = stream_get_meta_data($subject);
1645            break;
1646
1647        }
1648
1649        if  (!$meta)
1650        {
1651          return $this->fmt->emptyGroup($resType);
1652        }
1653
1654
1655        if (!$this->fmt->startGroup($resType))
1656        {
1657          return;
1658        }
1659
1660        $max = max(array_map('static::strLen', array_keys($meta)));
1661        foreach ($meta as $key => $value)
1662        {
1663          $this->fmt->startRow();
1664          $this->fmt->text('resourceProp', ucwords(str_replace('_', ' ', $key)));
1665          $this->fmt->colDiv($max - static::strLen($key));
1666          $this->fmt->sep(':');
1667          $this->fmt->colDiv();
1668          $this->evaluate($value);
1669          $this->fmt->endRow();
1670        }
1671        $this->fmt->endGroup();
1672        return;
1673
1674      // string
1675      case 'string':
1676        $length   = static::strLen($subject);
1677        $encoding = static::$env['mbStr'] ? mb_detect_encoding($subject) : false;
1678        $info     = $encoding && ($encoding !== 'ASCII') ? $length . '; ' . $encoding : $length;
1679
1680        if ($specialStr)
1681        {
1682          $this->fmt->sep('"');
1683          $this->fmt->text(array('string', 'special'), $subject, "string({$info})");
1684          $this->fmt->sep('"');
1685          return;
1686        }
1687
1688        $this->fmt->text('string', $subject, "string({$info})");
1689
1690        // advanced checks only if there are 3 characteres or more
1691        if (static::$config['showStringMatches'] && ($length > 2) && (trim($subject) !== ''))
1692        {
1693
1694          $isNumeric = is_numeric($subject);
1695
1696          // very simple check to determine if the string could match a file path
1697          // @note: this part of the code is very expensive
1698          $isFile = ($length < 2048)
1699            && (max(array_map('strlen', explode('/', str_replace('\\', '/', $subject)))) < 128)
1700            && !preg_match('/[^\w\.\-\/\\\\:]|\..*\.|\.$|:(?!(?<=^[a-zA-Z]:)[\/\\\\])/', $subject);
1701
1702          if ($isFile)
1703          {
1704            try
1705            {
1706              $file  = new \SplFileInfo($subject);
1707              $flags = array();
1708              $perms = $file->getPerms();
1709
1710              if (($perms & 0xC000) === 0xC000)      // socket
1711              {
1712                $flags[] = 's';
1713              }
1714              else if (($perms & 0xA000) === 0xA000) // symlink
1715              {
1716                $flags[] = 'l';
1717              }
1718              else if (($perms & 0x8000) === 0x8000) // regular
1719              {
1720                $flags[] = '-';
1721              }
1722              else if (($perms & 0x6000) === 0x6000) // block special
1723              {
1724                $flags[] = 'b';
1725              }
1726              else if (($perms & 0x4000) === 0x4000) // directory
1727              {
1728                $flags[] = 'd';
1729              }
1730              else if (($perms & 0x2000) === 0x2000) // character special
1731              {
1732                $flags[] = 'c';
1733              }
1734              else if (($perms & 0x1000) === 0x1000) // FIFO pipe
1735              {
1736                $flags[] = 'p';
1737              }
1738              else                                   // unknown
1739              {
1740                $flags[] = 'u';
1741              }
1742
1743              // owner
1744              $flags[] = (($perms & 0x0100) ? 'r' : '-');
1745              $flags[] = (($perms & 0x0080) ? 'w' : '-');
1746              $flags[] = (($perms & 0x0040) ? (($perms & 0x0800) ? 's' : 'x' ) : (($perms & 0x0800) ? 'S' : '-'));
1747
1748              // group
1749              $flags[] = (($perms & 0x0020) ? 'r' : '-');
1750              $flags[] = (($perms & 0x0010) ? 'w' : '-');
1751              $flags[] = (($perms & 0x0008) ? (($perms & 0x0400) ? 's' : 'x' ) : (($perms & 0x0400) ? 'S' : '-'));
1752
1753              // world
1754              $flags[] = (($perms & 0x0004) ? 'r' : '-');
1755              $flags[] = (($perms & 0x0002) ? 'w' : '-');
1756              $flags[] = (($perms & 0x0001) ? (($perms & 0x0200) ? 't' : 'x' ) : (($perms & 0x0200) ? 'T' : '-'));
1757
1758              $size = is_dir($subject) ? '' : sprintf(' %.2fK', $file->getSize() / 1024);
1759
1760              $this->fmt->startContain('file', true);
1761              $this->fmt->text('file', implode('', $flags) . $size);
1762              $this->fmt->endContain();
1763
1764            } catch(\Exception $e) {
1765              $isFile = false;
1766            }
1767          }
1768
1769          // class/interface/function
1770          if (!preg_match('/[^\w+\\\\]/', $subject) && ($length < 96))
1771          {
1772            $isClass = class_exists($subject, false);
1773            if ($isClass)
1774            {
1775              $this->fmt->startContain('class', true);
1776              $this->fromReflector(new \ReflectionClass($subject));
1777              $this->fmt->endContain();
1778            }
1779
1780            if (!$isClass && interface_exists($subject, false))
1781            {
1782              $this->fmt->startContain('interface', true);
1783              $this->fromReflector(new \ReflectionClass($subject));
1784              $this->fmt->endContain('interface');
1785            }
1786
1787            if (function_exists($subject))
1788            {
1789              $this->fmt->startContain('function', true);
1790              $this->fromReflector(new \ReflectionFunction($subject));
1791              $this->fmt->endContain('function');
1792            }
1793          }
1794
1795
1796          // skip serialization/json/date checks if the string appears to be numeric,
1797          // or if it's shorter than 5 characters
1798          if (!$isNumeric && ($length > 4))
1799          {
1800
1801            // url
1802            if (static::$config['showUrls'] && static::$env['curlActive'] && filter_var($subject, FILTER_VALIDATE_URL))
1803            {
1804              $ch = curl_init($subject);
1805              curl_setopt($ch, CURLOPT_NOBODY, true);
1806              curl_exec($ch);
1807              $nfo = curl_getinfo($ch);
1808              curl_close($ch);
1809
1810              if ($nfo['http_code'])
1811              {
1812                $this->fmt->startContain('url', true);
1813                $contentType = explode(';', $nfo['content_type']);
1814                $this->fmt->text('url', sprintf('%s:%d %s %.2fms (%d)', !empty($nfo['primary_ip']) ? $nfo['primary_ip'] : null, !empty($nfo['primary_port']) ? $nfo['primary_port'] : null, $contentType[0], $nfo['total_time'], $nfo['http_code']));
1815                $this->fmt->endContain();
1816              }
1817
1818            }
1819
1820            // date
1821            if (($length < 128) && static::$env['supportsDate'] && !preg_match('/[^A-Za-z0-9.:+\s\-\/]/', $subject))
1822            {
1823              try
1824              {
1825                $date   = new \DateTime($subject);
1826                $errors = \DateTime::getLastErrors();
1827
1828                if (($errors['warning_count'] < 1) && ($errors['error_count'] < 1))
1829                {
1830                  $now    = new \Datetime('now');
1831                  $nowUtc = new \Datetime('now', new \DateTimeZone('UTC'));
1832                  $diff   = $now->diff($date);
1833
1834                  $map = array(
1835                    'y' => 'yr',
1836                    'm' => 'mo',
1837                    'd' => 'da',
1838                    'h' => 'hr',
1839                    'i' => 'min',
1840                    's' => 'sec',
1841                  );
1842
1843                  $timeAgo = 'now';
1844                  foreach ($map as $k => $label)
1845                  {
1846                    if ($diff->{$k} > 0)
1847                    {
1848                      $timeAgo = $diff->format("%R%{$k}{$label}");
1849                      break;
1850                    }
1851                  }
1852
1853                  $tz   = $date->getTimezone();
1854                  $offs = round($tz->getOffset($nowUtc) / 3600);
1855
1856                  if ($offs > 0)
1857                  {
1858                    $offs = "+{$offs}";
1859                  }
1860
1861                  $timeAgo .= ((int)$offs !== 0) ? ' ' . sprintf('%s (UTC%s)', $tz->getName(), $offs) : ' UTC';
1862                  $this->fmt->startContain('date', true);
1863                  $this->fmt->text('date', $timeAgo);
1864                  $this->fmt->endContain();
1865
1866                }
1867              } catch(\Exception $e) {
1868                // not a date
1869              }
1870
1871            }
1872
1873            // attempt to detect if this is a serialized string
1874            static $unserializing = 0;
1875            $isSerialized = ($unserializing < 3)
1876              && (($subject[$length - 1] === ';') || ($subject[$length - 1] === '}'))
1877              && in_array($subject[0], array('s', 'a', 'O'), true)
1878              && ((($subject[0] === 's') && ($subject[$length - 2] !== '"')) || preg_match("/^{$subject[0]}:[0-9]+:/s", $subject))
1879              && (($unserialized = @unserialize($subject)) !== false);
1880
1881            if ($isSerialized)
1882            {
1883              $unserializing++;
1884              $this->fmt->startContain('serialized', true);
1885              $this->evaluate($unserialized);
1886              $this->fmt->endContain();
1887              $unserializing--;
1888            }
1889
1890            // try to find out if it's a json-encoded string;
1891            // only do this for json-encoded arrays or objects, because other types have too generic formats
1892            static $decodingJson = 0;
1893            $isJson = !$isSerialized && ($decodingJson < 3) && in_array($subject[0], array('{', '['), true);
1894
1895            if ($isJson)
1896            {
1897              $decodingJson++;
1898              $data = json_decode($subject);
1899
1900              // ensure created objects live enough for PHP to provide a unique hash
1901              if (is_object($data))
1902              {
1903                $this->intObjects->attach($data);
1904              }
1905
1906              if ($isJson = (json_last_error() === JSON_ERROR_NONE))
1907              {
1908                $this->fmt->startContain('json', true);
1909                $this->evaluate($data);
1910                $this->fmt->endContain();
1911              }
1912
1913              $decodingJson--;
1914            }
1915
1916            // attempt to match a regex
1917            if (!$isSerialized && !$isJson && $length < 768)
1918            {
1919              try
1920              {
1921                $components = $this->splitRegex($subject);
1922                if ($components)
1923                {
1924                  $regex = '';
1925
1926                  $this->fmt->startContain('regex', true);
1927                  foreach ($components as $component)
1928                  {
1929                    $this->fmt->text('regex-' . key($component), reset($component));
1930                  }
1931                  $this->fmt->endContain();
1932                }
1933
1934              } catch(\Exception $e) {
1935                // not a regex
1936              }
1937
1938            }
1939          }
1940        }
1941
1942        return;
1943    }
1944
1945    // if we reached this point, $subject must be an object
1946
1947    // track objects to detect recursion
1948    static $hashes = array();
1949
1950    // hash ID of this object
1951    $hash = spl_object_hash($subject);
1952    $recursion = isset($hashes[$hash]);
1953
1954    // sometimes incomplete objects may be created from string unserialization,
1955    // if the class to which the object belongs wasn't included until the unserialization stage...
1956    if ($subject instanceof \__PHP_Incomplete_Class)
1957    {
1958      $this->fmt->text('object');
1959      $this->fmt->emptyGroup('incomplete');
1960      return;
1961    }
1962
1963    // check cache at this point
1964    if (!$recursion && $this->fmt->didCache($hash))
1965    {
1966      static::$debug['cacheHits']++;
1967      return;
1968    }
1969
1970    $reflector = new \ReflectionObject($subject);
1971    $this->fmt->startContain('class');
1972    $this->fromReflector($reflector);
1973    $this->fmt->text('object', ' object');
1974    $this->fmt->endContain();
1975
1976    // already been here?
1977    if ($recursion)
1978    {
1979      return $this->fmt->emptyGroup('recursion');
1980    }
1981
1982    $hashes[$hash] = 1;
1983
1984    $flags = \ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED;
1985
1986    if (static::$config['showPrivateMembers'])
1987    {
1988      $flags |= \ReflectionProperty::IS_PRIVATE;
1989    }
1990
1991    $props   = $reflector->getProperties($flags);
1992    $methods = array();
1993
1994    if (static::$config['showMethods'])
1995    {
1996      $flags = \ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED;
1997
1998      if (static::$config['showPrivateMembers'])
1999      {
2000        $flags |= \ReflectionMethod::IS_PRIVATE;
2001      }
2002
2003      $methods = $reflector->getMethods($flags);
2004    }
2005
2006    $constants  = $reflector->getConstants();
2007    $interfaces = $reflector->getInterfaces();
2008    $traits     = static::$env['is54'] ? $reflector->getTraits() : array();
2009    $parents    = static::getParentClasses($reflector);
2010
2011    // work-around for https://bugs.php.net/bug.php?id=49154
2012    // @see http://stackoverflow.com/questions/15672287/strange-behavior-of-reflectiongetproperties-with-numeric-keys
2013    if (!static::$env['is54'])
2014    {
2015      $props = array_values(array_filter($props, function($prop) use($subject)
2016                                                 {
2017                                                   return !$prop->isPublic() || property_exists($subject, $prop->name);
2018                                                 }));
2019    }
2020
2021    // no data to display?
2022    if (!$props && !$methods && !$constants && !$interfaces && !$traits)
2023    {
2024      unset($hashes[$hash]);
2025      return $this->fmt->emptyGroup();
2026    }
2027
2028    if (!$this->fmt->startGroup())
2029    {
2030      return;
2031    }
2032
2033    // show contents for iterators
2034    if (static::$config['showIteratorContents'] && $reflector->isIterateable())
2035    {
2036
2037      $itContents = iterator_to_array($subject);
2038      $this->fmt->sectionTitle(sprintf('Contents (%d)', count($itContents)));
2039
2040      foreach ($itContents as $key => $value)
2041      {
2042        $keyInfo = gettype($key);
2043        if ($keyInfo === 'string')
2044        {
2045          $encoding = static::$env['mbStr'] ? mb_detect_encoding($key) : '';
2046          $length   = $encoding && ($encoding !== 'ASCII') ? static::strLen($key) . '; ' . $encoding : static::strLen($key);
2047          $keyInfo  = sprintf('%s(%s)', $keyInfo, $length);
2048        }
2049
2050        $this->fmt->startRow();
2051        $this->fmt->text(array('key', 'iterator'), $key, sprintf('Iterator key: %s', $keyInfo));
2052        $this->fmt->colDiv();
2053        $this->fmt->sep('=>');
2054        $this->fmt->colDiv();
2055        $this->evaluate($value);
2056        //$this->evaluate($value instanceof \Traversable ? ((count($value) > 0) ? $value : (string)$value) : $value);
2057        $this->fmt->endRow();
2058      }
2059    }
2060
2061    // display the interfaces this objects' class implements
2062    if ($interfaces)
2063    {
2064      $items = array();
2065      $this->fmt->sectionTitle('Implements');
2066      $this->fmt->startRow();
2067      $this->fmt->startContain('interfaces');
2068
2069      $i     = 0;
2070      $count = count($interfaces);
2071
2072      foreach ($interfaces as $name => $interface)
2073      {
2074        $this->fromReflector($interface);
2075
2076        if (++$i < $count)
2077        {
2078          $this->fmt->sep(', ');
2079        }
2080      }
2081
2082      $this->fmt->endContain();
2083      $this->fmt->endRow();
2084    }
2085
2086    // traits this objects' class uses
2087    if ($traits)
2088    {
2089      $items = array();
2090      $this->fmt->sectionTitle('Uses');
2091      $this->fmt->startRow();
2092      $this->fmt->startContain('traits');
2093
2094      $i     = 0;
2095      $count = count($traits);
2096
2097      foreach ($traits as $name => $trait)
2098      {
2099        $this->fromReflector($trait);
2100
2101        if (++$i < $count)
2102        {
2103          $this->fmt->sep(', ');
2104        }
2105      }
2106
2107      $this->fmt->endContain();
2108      $this->fmt->endRow();
2109    }
2110
2111    // class constants
2112    if ($constants)
2113    {
2114      $this->fmt->sectionTitle('Constants');
2115      $max = max(array_map('static::strLen', array_keys($constants)));
2116      foreach ($constants as $name => $value)
2117      {
2118        $meta = null;
2119        $type = array('const');
2120        foreach ($parents as $parent)
2121        {
2122          if ($parent->hasConstant($name))
2123          {
2124            if ($parent !== $reflector)
2125            {
2126              $type[] = 'inherited';
2127              $meta = array('sub' => array(array('Prototype defined by', $parent->name)));
2128            }
2129            break;
2130          }
2131        }
2132
2133        $this->fmt->startRow();
2134        $this->fmt->sep('::');
2135        $this->fmt->colDiv();
2136        $this->fmt->startContain($type);
2137        $this->fmt->text('name', $name, $meta, $this->linkify($parent, $name));
2138        $this->fmt->endContain();
2139        $this->fmt->colDiv($max - static::strLen($name));
2140        $this->fmt->sep('=');
2141        $this->fmt->colDiv();
2142        $this->evaluate($value);
2143        $this->fmt->endRow();
2144      }
2145    }
2146
2147    // object/class properties
2148    if ($props)
2149    {
2150      $this->fmt->sectionTitle('Properties');
2151
2152      $max = 0;
2153      foreach ($props as $idx => $prop)
2154      {
2155        if (($propNameLen = static::strLen($prop->name)) > $max)
2156        {
2157          $max = $propNameLen;
2158        }
2159      }
2160
2161      foreach($props as $idx => $prop)
2162      {
2163
2164        if ($this->hasInstanceTimedOut())
2165        {
2166          break;
2167        }
2168
2169        $bubbles     = array();
2170        $sourceClass = $prop->getDeclaringClass();
2171        $inherited   = $reflector->getShortName() !== $sourceClass->getShortName();
2172        $meta        = $sourceClass->isInternal() ? null : static::parseComment($prop->getDocComment());
2173
2174        if ($meta)
2175        {
2176          if ($inherited)
2177          {
2178            $meta['sub'] = array(array('Declared in', $sourceClass->getShortName()));
2179          }
2180
2181          if (isset($meta['tags']['var'][0]))
2182          {
2183            $meta['left'] = $meta['tags']['var'][0][0];
2184          }
2185
2186          unset($meta['tags']);
2187        }
2188
2189        if ($prop->isProtected() || $prop->isPrivate())
2190        {
2191          $prop->setAccessible(true);
2192        }
2193
2194        $value = $prop->getValue($subject);
2195
2196        $this->fmt->startRow();
2197        $this->fmt->sep($prop->isStatic() ? '::' : '->');
2198        $this->fmt->colDiv();
2199
2200        $bubbles  = array();
2201        if ($prop->isProtected())
2202        {
2203          $bubbles[] = array('P', 'Protected');
2204        }
2205
2206        if ($prop->isPrivate())
2207        {
2208          $bubbles[] = array('!', 'Private');
2209        }
2210
2211        $this->fmt->bubbles($bubbles);
2212
2213        $type = array('prop');
2214
2215        if ($inherited)
2216        {
2217          $type[] = 'inherited';
2218        }
2219
2220        if ($prop->isPrivate())
2221        {
2222          $type[] = 'private';
2223        }
2224
2225        $this->fmt->colDiv(2 - count($bubbles));
2226        $this->fmt->startContain($type);
2227        $this->fmt->text('name', $prop->name, $meta, $this->linkify($prop));
2228        $this->fmt->endContain();
2229        $this->fmt->colDiv($max - static::strLen($prop->name));
2230        $this->fmt->sep('=');
2231        $this->fmt->colDiv();
2232        $this->evaluate($value);
2233        $this->fmt->endRow();
2234      }
2235    }
2236
2237    // class methods
2238    if ($methods && !$this->hasInstanceTimedOut())
2239    {
2240
2241      $this->fmt->sectionTitle('Methods');
2242      foreach ($methods as $idx => $method)
2243      {
2244
2245        $this->fmt->startRow();
2246        $this->fmt->sep($method->isStatic() ? '::' : '->');
2247        $this->fmt->colDiv();
2248
2249        $bubbles = array();
2250        if ($method->isAbstract())
2251        {
2252          $bubbles[] = array('A', 'Abstract');
2253        }
2254
2255        if ($method->isFinal())
2256        {
2257          $bubbles[] = array('F', 'Final');
2258        }
2259
2260        if ($method->isProtected())
2261        {
2262          $bubbles[] = array('P', 'Protected');
2263        }
2264
2265        if ($method->isPrivate())
2266        {
2267          $bubbles[] = array('!', 'Private');
2268        }
2269
2270        $this->fmt->bubbles($bubbles);
2271
2272        $this->fmt->colDiv(4 - count($bubbles));
2273
2274        // is this method inherited?
2275        $inherited = $reflector->getShortName() !== $method->getDeclaringClass()->getShortName();
2276
2277        $type = array('method');
2278
2279        if ($inherited)
2280        {
2281          $type[] = 'inherited';
2282        }
2283
2284        if ($method->isPrivate())
2285        {
2286          $type[] = 'private';
2287        }
2288
2289        $this->fmt->startContain($type);
2290
2291        $name = $method->name;
2292        if ($method->returnsReference())
2293        {
2294          $name = "&{$name}";
2295        }
2296
2297        $this->fromReflector($method, $name, $reflector);
2298
2299        $paramCom   = $method->isInternal() ? array() : static::parseComment($method->getDocComment(), 'tags');
2300        $paramCom   = empty($paramCom['param']) ? array() : $paramCom['param'];
2301        $paramCount = $method->getNumberOfParameters();
2302
2303        $this->fmt->sep('(');
2304
2305        // process arguments
2306        foreach ($method->getParameters() as $idx => $parameter)
2307        {
2308          $meta      = null;
2309          $paramName = "\${$parameter->name}";
2310          $optional  = $parameter->isOptional();
2311          $variadic  = static::$env['is56'] && $parameter->isVariadic();
2312
2313          if ($parameter->isPassedByReference())
2314          {
2315            $paramName = "&{$paramName}";
2316          }
2317
2318          if ($variadic)
2319          {
2320            $paramName = "...{$paramName}";
2321          }
2322
2323          $type = array('param');
2324
2325          if ($optional)
2326          {
2327            $type[] = 'optional';
2328          }
2329
2330          $this->fmt->startContain($type);
2331
2332          // attempt to build meta
2333          foreach ($paramCom as $tag)
2334          {
2335            list($pcTypes, $pcName, $pcDescription) = $tag;
2336            if ($pcName !== $paramName)
2337            {
2338              continue;
2339            }
2340
2341            $meta = array('title' => $pcDescription);
2342
2343            if ($pcTypes)
2344            {
2345              $meta['left'] = $pcTypes;
2346            }
2347
2348            break;
2349          }
2350
2351          try
2352          {
2353            $paramClass = $parameter->getClass();
2354          } catch(\Exception $e) {
2355            // @see https://bugs.php.net/bug.php?id=32177&edit=1
2356          }
2357
2358          if (!empty($paramClass))
2359          {
2360            $this->fmt->startContain('hint');
2361            $this->fromReflector($paramClass, $paramClass->name);
2362            $this->fmt->endContain();
2363            $this->fmt->sep(' ');
2364
2365          }
2366          else if ($parameter->isArray())
2367          {
2368            $this->fmt->text('hint', 'array');
2369            $this->fmt->sep(' ');
2370
2371          } else {
2372            $hasType = static::$env['is7'] && $parameter->hasType();
2373            if ($hasType)
2374            {
2375              $type = $parameter->getType();
2376              $this->fmt->text('hint', (string)$type);
2377              $this->fmt->sep(' ');
2378            }
2379          }
2380
2381          $this->fmt->text('name', $paramName, $meta);
2382
2383          if ($optional)
2384          {
2385            $paramValue = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
2386            $this->fmt->sep(' = ');
2387
2388            if (static::$env['is546'] && !$parameter->getDeclaringFunction()->isInternal() && $parameter->isDefaultValueConstant())
2389            {
2390              $this->fmt->text('constant', $parameter->getDefaultValueConstantName(), 'Constant');
2391
2392            } else {
2393              $this->evaluate($paramValue, true);
2394            }
2395          }
2396
2397          $this->fmt->endContain();
2398
2399          if ($idx < $paramCount - 1)
2400          {
2401            $this->fmt->sep(', ');
2402          }
2403        }
2404        $this->fmt->sep(')');
2405        $this->fmt->endContain();
2406
2407        $hasReturnType = static::$env['is7'] && $method->hasReturnType();
2408        if ($hasReturnType)
2409        {
2410          $type = $method->getReturnType();
2411          $this->fmt->startContain('ret');
2412          $this->fmt->sep(':');
2413          $this->fmt->text('hint', (string)$type);
2414          $this->fmt->endContain();
2415        }
2416
2417        $this->fmt->endRow();
2418      }
2419    }
2420
2421    unset($hashes[$hash]);
2422    $this->fmt->endGroup();
2423
2424    $this->fmt->cacheLock($hash);
2425  }
2426
2427
2428  /**
2429   * Scans for known classes and functions inside the provided expression,
2430   * and linkifies them when possible
2431   *
2432   * @param   string $expression   Expression to format
2433   * @return  string               Formatted output
2434   */
2435  protected function evaluateExp($expression = null)
2436  {
2437
2438    if ($expression === null)
2439    {
2440      return;
2441    }
2442
2443    if (static::strLen($expression) > 120)
2444    {
2445      $expression = substr($expression, 0, 120) . '...';
2446    }
2447
2448    $this->fmt->sep('> ');
2449
2450    if (strpos($expression, '(') === false)
2451    {
2452      return $this->fmt->text('expTxt', $expression);
2453    }
2454
2455    $keywords = array_map('trim', explode('(', $expression, 2));
2456    $parts = array();
2457
2458    // try to find out if this is a function
2459    try
2460    {
2461      $reflector = new \ReflectionFunction($keywords[0]);
2462      $parts[] = array($keywords[0], $reflector, '');
2463
2464    } catch(\Exception $e) {
2465
2466      if (stripos($keywords[0], 'new ') === 0)
2467      {
2468        $cn = explode(' ' , $keywords[0], 2);
2469
2470        // linkify 'new keyword' (as constructor)
2471        try{
2472          $reflector = new \ReflectionMethod($cn[1], '__construct');
2473          $parts[] = array($cn[0], $reflector, '');
2474
2475        } catch(\Exception $e) {
2476          $reflector = null;
2477          $parts[] = $cn[0];
2478        }
2479
2480        // class name...
2481        try
2482        {
2483          $reflector = new \ReflectionClass($cn[1]);
2484          $parts[] = array($cn[1], $reflector, ' ');
2485
2486        } catch(\Exception $e) {
2487          $reflector = null;
2488          $parts[] = $cn[1];
2489        }
2490
2491      } else {
2492
2493        // we can only linkify methods called statically
2494        if (strpos($keywords[0], '::') === false)
2495        {
2496          return $this->fmt->text('expTxt', $expression);
2497        }
2498
2499        $cn = explode('::', $keywords[0], 2);
2500
2501        // attempt to linkify class name
2502        try
2503        {
2504          $reflector = new \ReflectionClass($cn[0]);
2505          $parts[] = array($cn[0], $reflector, '');
2506
2507        } catch(\Exception $e) {
2508          $reflector = null;
2509          $parts[] = $cn[0];
2510        }
2511
2512        // perhaps it's a static class method; try to linkify method
2513        try
2514        {
2515          $reflector = new \ReflectionMethod($cn[0], $cn[1]);
2516          $parts[] = array($cn[1], $reflector, '::');
2517
2518        } catch(\Exception $e)
2519        {
2520          $reflector = null;
2521          $parts[] = $cn[1];
2522        }
2523      }
2524    }
2525
2526    $parts[] = "({$keywords[1]}";
2527
2528    foreach ($parts as $element)
2529    {
2530      if (!is_array($element))
2531      {
2532        $this->fmt->text('expTxt', $element);
2533        continue;
2534      }
2535
2536      list($text, $reflector, $prefix) = $element;
2537
2538      if ($prefix !== '')
2539      {
2540        $this->fmt->text('expTxt', $prefix);
2541      }
2542
2543      $this->fromReflector($reflector, $text);
2544    }
2545
2546  }
2547
2548
2549  /**
2550   * Calculates real string length
2551   *
2552   * @param   string $string
2553   * @return  int
2554   */
2555  protected static function strLen($string)
2556  {
2557    $encoding = function_exists('mb_detect_encoding') ? mb_detect_encoding($string) : false;
2558    return $encoding ? mb_strlen($string, $encoding) : strlen($string);
2559  }
2560
2561
2562  /**
2563   * Safe str_pad alternative
2564   *
2565   * @param   string $string
2566   * @param   int $padLen
2567   * @param   string $padStr
2568   * @param   int $padType
2569   * @return  string
2570   */
2571  protected static function strPad($input, $padLen, $padStr = ' ', $padType = STR_PAD_RIGHT)
2572  {
2573    $diff = strlen($input) - static::strLen($input);
2574    return str_pad($input, $padLen + $diff, $padStr, $padType);
2575  }
2576
2577}
2578
2579
2580
2581/**
2582 * Formatter abstraction
2583 */
2584abstract class RFormatter
2585{
2586
2587  /**
2588   * Flush output and send contents to the output device
2589   */
2590  abstract public function flush();
2591
2592  /**
2593   * Generate a base entity
2594   *
2595   * @param  string|array $type
2596   * @param  string|null $text
2597   * @param  string|array|null $meta
2598   * @param  string|null $uri
2599   */
2600  abstract public function text($type, $text = null, $meta = null, $uri = null);
2601
2602  /**
2603   * Generate container start token
2604   *
2605   * @param  string|array $type
2606   * @param  string|bool $label
2607   */
2608  public function startContain($type, $label = false){}
2609
2610  /**
2611   * Generate container ending token
2612   */
2613  public function endContain(){}
2614
2615  /**
2616   * Generate empty group token
2617   *
2618   * @param  string $prefix
2619   */
2620  public function emptyGroup($prefix = ''){}
2621
2622  /**
2623   * Generate group start token
2624   *
2625   * This method must return boolean TRUE on success, false otherwise (eg. max depth reached).
2626   * The evaluator will skip this group on FALSE
2627   *
2628   * @param   string $prefix
2629   * @return  bool
2630   */
2631  public function startGroup($prefix = ''){}
2632
2633  /**
2634   * Generate group ending token
2635   */
2636  public function endGroup(){}
2637
2638  /**
2639   * Generate section title
2640   *
2641   * @param  string $title
2642   */
2643  public function sectionTitle($title){}
2644
2645  /**
2646   * Generate row start token
2647   */
2648  public function startRow(){}
2649
2650  /**
2651   * Generate row ending token
2652   */
2653  public function endRow(){}
2654
2655  /**
2656   * Column divider (cell delimiter)
2657   *
2658   * @param  int $padLen
2659   */
2660  public function colDiv($padLen = null){}
2661
2662  /**
2663   * Generate modifier tokens
2664   *
2665   * @param  array $items
2666   */
2667  public function bubbles(array $items){}
2668
2669  /**
2670   * Input expression start
2671   */
2672  public function startExp(){}
2673
2674  /**
2675   * Input expression end
2676   */
2677  public function endExp(){}
2678
2679  /**
2680   * Root starting token
2681   */
2682  public function startRoot(){}
2683
2684  /**
2685   * Root ending token
2686   */
2687  public function endRoot(){}
2688
2689  /**
2690   * Separator token
2691   *
2692   * @param  string $label
2693   */
2694  public function sep($label = ' '){}
2695
2696  /**
2697   * Resolve cache request
2698   *
2699   * If the ID is not present in the cache, then a new cache entry is created
2700   * for the given ID, and string offsets are captured until cacheLock is called
2701   *
2702   * This method must return TRUE if the ID exists in the cache, and append the cached item
2703   * to the output, FALSE otherwise.
2704   *
2705   * @param   string $id
2706   * @return  bool
2707   */
2708  public function didCache($id)
2709  {
2710    return false;
2711  }
2712
2713  /**
2714   * Ends cache capturing for the given ID
2715   *
2716   * @param  string $id
2717   */
2718  public function cacheLock($id){}
2719
2720}
2721
2722
2723/**
2724 * Generates the output in HTML5 format
2725 *
2726 */
2727class RHtmlFormatter extends RFormatter{
2728
2729  protected
2730
2731    /**
2732     * Actual output
2733     *
2734     * @var  string
2735     */
2736    $out = '',
2737
2738    /**
2739     * Tracks current nesting level
2740     *
2741     * @var  int
2742     */
2743    $level = 0,
2744
2745    /**
2746     * Stores tooltip content for all entries
2747     *
2748     * To avoid having duplicate tooltip data in the HTML, we generate them once,
2749     * and use references (the Q index) to pull data when required;
2750     * this improves performance significantly
2751     *
2752     * @var  array
2753     */
2754    $tips = array(),
2755
2756    /**
2757     * Used to cache output to speed up processing.
2758     *
2759     * Contains hashes as keys and string offsets as values.
2760     * Cached objects will not be processed again in the same query
2761     *
2762     * @var  array
2763     */
2764    $cache = array(),
2765
2766    /**
2767     * Map of used HTML tag and attributes
2768     *
2769     * @var string
2770     */
2771    $def = array();
2772
2773
2774
2775  protected static
2776
2777    /**
2778     * Instance counter
2779     *
2780     * @var  int
2781     */
2782    $counter = 0,
2783
2784    /**
2785     * Tracks style/jscript inclusion state
2786     *
2787     * @var  bool
2788     */
2789    $didAssets = false;
2790
2791
2792  public function __construct()
2793  {
2794
2795    if (ref::config('validHtml'))
2796    {
2797
2798      $this->def = array(
2799        'base'   => 'span',
2800        'tip'    => 'div',
2801        'cell'   => 'data-cell',
2802        'table'  => 'data-table',
2803        'row'    => 'data-row',
2804        'group'  => 'data-group',
2805        'gLabel' => 'data-gLabel',
2806        'match'  => 'data-match',
2807        'tipRef' => 'data-tip',
2808      );
2809
2810
2811    } else {
2812
2813      $this->def = array(
2814        'base'   => 'r',
2815        'tip'    => 't',
2816        'cell'   => 'c',
2817        'table'  => 't',
2818        'row'    => 'r',
2819        'group'  => 'g',
2820        'gLabel' => 'gl',
2821        'match'  => 'm',
2822        'tipRef' => 'h',
2823      );
2824
2825    }
2826
2827  }
2828
2829
2830
2831  public function flush()
2832  {
2833    print $this->out;
2834    $this->out   = '';
2835    $this->cache = array();
2836    $this->tips  = array();
2837  }
2838
2839
2840  public function didCache($id)
2841  {
2842
2843    if (!isset($this->cache[$id]))
2844    {
2845      $this->cache[$id] = array();
2846      $this->cache[$id][] = strlen($this->out);
2847      return false;
2848    }
2849
2850    if (!isset($this->cache[$id][1]))
2851    {
2852      $this->cache[$id][0] = strlen($this->out);
2853      return false;
2854    }
2855
2856    $this->out .= substr($this->out, $this->cache[$id][0], $this->cache[$id][1]);
2857    return true;
2858  }
2859
2860  public function cacheLock($id)
2861  {
2862    $this->cache[$id][] = strlen($this->out) - $this->cache[$id][0];
2863  }
2864
2865  public function sep($label = ' ')
2866  {
2867    $this->out .= $label !== ' ' ? '<i>' . static::escape($label) . '</i>' : $label;
2868  }
2869
2870  public function text($type, $text = null, $meta = null, $uri = null)
2871  {
2872
2873    if (!is_array($type))
2874    {
2875      $type = (array)$type;
2876    }
2877
2878    $tip  = '';
2879    $text = ($text !== null) ? static::escape($text) : static::escape($type[0]);
2880
2881    if (in_array('special', $type))
2882    {
2883      $text = strtr($text, array(
2884        "\r" => '<i>\r</i>',     // carriage return
2885        "\t" => '<i>\t</i>',     // horizontal tab
2886        "\n" => '<i>\n</i>',     // linefeed (new line)
2887        "\v" => '<i>\v</i>',     // vertical tab
2888        "\e" => '<i>\e</i>',     // escape
2889        "\f" => '<i>\f</i>',     // form feed
2890        "\0" => '<i>\0</i>',
2891      ));
2892    }
2893
2894    // generate tooltip reference (probably the slowest part of the code ;)
2895    if ($meta !== null)
2896    {
2897      $tipIdx = array_search($meta, $this->tips, true);
2898
2899      if ($tipIdx === false)
2900      {
2901        $tipIdx = array_push($this->tips, $meta) - 1;
2902      }
2903
2904      $tip = " {$this->def['tipRef']}=\"{$tipIdx}\"";
2905      //$tip = sprintf('%s="%d"', $this->def['tipRef'], $tipIdx);
2906    }
2907
2908    // wrap text in a link?
2909    if ($uri !== null)
2910    {
2911      $text = '<a href="' . $uri . '" target="_blank">' . $text . '</a>';
2912    }
2913
2914    $typeStr = '';
2915    foreach ($type as $part)
2916    {
2917      $typeStr .= " data-{$part}";
2918    }
2919
2920    $this->out .= "<{$this->def['base']}{$typeStr}{$tip}>{$text}</{$this->def['base']}>";
2921    //$this->out .= sprintf('<%1$s%2$s %3$s>%4$s</%1$s>', $this->def['base'], $typeStr, $tip, $text);
2922  }
2923
2924  public function startContain($type, $label = false)
2925  {
2926
2927    if (!is_array($type))
2928    {
2929      $type = (array)$type;
2930    }
2931
2932    if ($label)
2933    {
2934      $this->out .= '<br>';
2935    }
2936
2937    $typeStr = '';
2938    foreach ($type as $part)
2939    {
2940      $typeStr .= " data-{$part}";
2941    }
2942
2943    $this->out .= "<{$this->def['base']}{$typeStr}>";
2944
2945    if ($label)
2946    {
2947      $this->out .= "<{$this->def['base']} {$this->def['match']}>{$type[0]}</{$this->def['base']}>";
2948    }
2949  }
2950
2951  public function endContain()
2952  {
2953    $this->out .= "</{$this->def['base']}>";
2954  }
2955
2956  public function emptyGroup($prefix = '')
2957  {
2958
2959    if ($prefix !== '')
2960    {
2961      $prefix = "<{$this->def['base']} {$this->def['gLabel']}>" . static::escape($prefix) . "</{$this->def['base']}>";
2962    }
2963
2964    $this->out .= "<i>(</i>{$prefix}<i>)</i>";
2965  }
2966
2967
2968  public function startGroup($prefix = '')
2969  {
2970
2971    $maxDepth = ref::config('maxDepth');
2972
2973    if (($maxDepth > 0) && (($this->level + 1) > $maxDepth))
2974    {
2975      $this->emptyGroup('...');
2976      return false;
2977    }
2978
2979    $this->level++;
2980
2981    $expLvl = ref::config('expLvl');
2982    $exp = ($expLvl < 0) || (($expLvl > 0) && ($this->level <= $expLvl)) ? ' data-exp' : '';
2983
2984    if ($prefix !== '')
2985    {
2986      $prefix = "<{$this->def['base']} {$this->def['gLabel']}>" . static::escape($prefix) . "</{$this->def['base']}>";
2987    }
2988
2989    $this->out .= "<i>(</i>{$prefix}<{$this->def['base']} data-toggle{$exp}></{$this->def['base']}><{$this->def['base']} {$this->def['group']}><{$this->def['base']} {$this->def['table']}>";
2990
2991    return true;
2992  }
2993
2994  public function endGroup()
2995  {
2996    $this->out .= "</{$this->def['base']}></{$this->def['base']}><i>)</i>";
2997    $this->level--;
2998  }
2999
3000  public function sectionTitle($title)
3001  {
3002    $this->out .= "</{$this->def['base']}><{$this->def['base']} data-tHead>{$title}</{$this->def['base']}><{$this->def['base']} {$this->def['table']}>";
3003  }
3004
3005  public function startRow()
3006  {
3007    $this->out .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>";
3008  }
3009
3010  public function endRow()
3011  {
3012    $this->out .= "</{$this->def['base']}></{$this->def['base']}>";
3013  }
3014
3015  public function colDiv($padLen = null)
3016  {
3017    $this->out .= "</{$this->def['base']}><{$this->def['base']} {$this->def['cell']}>";
3018  }
3019
3020  public function bubbles(array $items)
3021  {
3022
3023    if (!$items)
3024    {
3025      return;
3026    }
3027
3028    $this->out .= "<{$this->def['base']} data-mod>";
3029
3030    foreach ($items as $info)
3031    {
3032      $this->out .= $this->text('mod-' . strtolower($info[1]), $info[0], $info[1]);
3033    }
3034
3035    $this->out .= "</{$this->def['base']}>";
3036  }
3037
3038  public function startExp()
3039  {
3040    $this->out .= "<{$this->def['base']} data-input>";
3041  }
3042
3043  public function endExp()
3044  {
3045    if (ref::config('showBacktrace') && ($trace = ref::getBacktrace()))
3046    {
3047      $docRoot = isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : '';
3048      $path = strpos($trace['file'], $docRoot) !== 0 ? $trace['file'] : ltrim(str_replace($docRoot, '', $trace['file']), '/');
3049      $this->out .= "<{$this->def['base']} data-backtrace>{$path}:{$trace['line']}</{$this->def['base']}>";
3050    }
3051
3052    $this->out .= "</{$this->def['base']}><{$this->def['base']} data-output>";
3053  }
3054
3055  public function startRoot()
3056  {
3057    $this->out .= '<!-- ref#' . ++static::$counter . ' --><div>' . static::getAssets() . '<div class="ref">';
3058  }
3059
3060  public function endRoot()
3061  {
3062    $this->out .= "</{$this->def['base']}>";
3063
3064    // process tooltips
3065    $tipHtml = '';
3066    foreach ($this->tips as $idx => $meta)
3067    {
3068
3069      $tip = '';
3070      if (!is_array($meta))
3071      {
3072        $meta = array('title' => $meta);
3073      }
3074
3075      $meta += array(
3076        'title'       => '',
3077        'left'        => '',
3078        'description' => '',
3079        'tags'        => array(),
3080        'sub'         => array(),
3081      );
3082
3083      $meta = static::escape($meta);
3084      $cols = array();
3085
3086      if ($meta['left'])
3087      {
3088        $cols[] = "<{$this->def['base']} {$this->def['cell']} data-varType>{$meta['left']}</{$this->def['base']}>";
3089      }
3090
3091      $title = $meta['title'] ?       "<{$this->def['base']} data-title>{$meta['title']}</{$this->def['base']}>"       : '';
3092      $desc  = $meta['description'] ? "<{$this->def['base']} data-desc>{$meta['description']}</{$this->def['base']}>"  : '';
3093      $tags  = '';
3094
3095      foreach ($meta['tags'] as $tag => $values)
3096      {
3097        foreach ($values as $value)
3098        {
3099          if ($tag === 'param')
3100          {
3101            $value[0] = "{$value[0]} {$value[1]}";
3102            unset($value[1]);
3103          }
3104
3105          $value  = is_array($value) ? implode("</{$this->def['base']}><{$this->def['base']} {$this->def['cell']}>", $value) : $value;
3106          $tags  .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>@{$tag}</{$this->def['base']}><{$this->def['base']} {$this->def['cell']}>{$value}</{$this->def['base']}></{$this->def['base']}>";
3107        }
3108      }
3109
3110      if ($tags)
3111      {
3112        $tags = "<{$this->def['base']} {$this->def['table']}>{$tags}</{$this->def['base']}>";
3113      }
3114
3115      if ($title || $desc || $tags)
3116      {
3117        $cols[] = "<{$this->def['base']} {$this->def['cell']}>{$title}{$desc}{$tags}</{$this->def['base']}>";
3118      }
3119
3120      if ($cols)
3121      {
3122        $tip = "<{$this->def['base']} {$this->def['row']}>" . implode('', $cols) . "</{$this->def['base']}>";
3123      }
3124
3125      $sub = '';
3126      foreach ($meta['sub'] as $line)
3127      {
3128        $sub .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>" . implode("</{$this->def['base']}><{$this->def['base']} {$this->def['cell']}>", $line) . "</{$this->def['base']}></{$this->def['base']}>";
3129      }
3130
3131      if ($sub)
3132      {
3133        $tip .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']} data-sub><{$this->def['base']} {$this->def['table']}>{$sub}</{$this->def['base']}></{$this->def['base']}></{$this->def['base']}>";
3134      }
3135
3136      if ($tip)
3137      {
3138        $this->out .= "<{$this->def['tip']}>{$tip}</{$this->def['tip']}>";
3139      }
3140    }
3141
3142    if (($timeout = ref::getTimeoutPoint()) > 0)
3143    {
3144      $this->out .= sprintf("<{$this->def['base']} data-error>Listing incomplete. Timed-out after %4.2fs</{$this->def['base']}>", $timeout);
3145    }
3146
3147    $this->out .= '</div></div><!-- /ref#' . static::$counter . ' -->';
3148  }
3149
3150
3151  /**
3152   * Get styles and javascript (only generated for the 1st call)
3153   *
3154   * @return  string
3155   */
3156  public static function getAssets()
3157  {
3158
3159    // first call? include styles and javascript
3160    if (static::$didAssets)
3161    {
3162      return '';
3163    }
3164
3165    ob_start();
3166
3167    if (ref::config('stylePath') !== false)
3168    {
3169      ?>
3170      <style>
3171        <?php readfile(str_replace('{:dir}', __DIR__, ref::config('stylePath'))); ?>
3172      </style>
3173      <?php
3174    }
3175
3176    if (ref::config('scriptPath') !== false)
3177    {
3178      ?>
3179      <script>
3180        <?php readfile(str_replace('{:dir}', __DIR__, ref::config('scriptPath'))); ?>
3181      </script>
3182      <?php
3183    }
3184
3185    // normalize space and remove comments
3186    $output = preg_replace('/\s+/', ' ', trim(ob_get_clean()));
3187    $output = preg_replace('!/\*.*?\*/!s', '', $output);
3188    $output = preg_replace('/\n\s*\n/', "\n", $output);
3189
3190    static::$didAssets = true;
3191    return $output;
3192  }
3193
3194
3195  /**
3196   * Escapes variable for HTML output
3197   *
3198   * @param   string|array $var
3199   * @return  string|array
3200   */
3201  protected static function escape($var)
3202  {
3203    return is_array($var) ? array_map('static::escape', $var) : htmlspecialchars($var, ENT_QUOTES);
3204  }
3205
3206}
3207
3208
3209
3210/**
3211 * Generates the output in plain text format
3212 *
3213 */
3214class RTextFormatter extends RFormatter
3215{
3216
3217  protected
3218
3219    /**
3220     * Actual output
3221     *
3222     * @var  string
3223     */
3224    $out        = '',
3225
3226    /**
3227     * Tracks current nesting level
3228     *
3229     * @var  int
3230     */
3231    $level      = 0,
3232
3233    /**
3234     * Current indenting level
3235     *
3236     * @var  int
3237     */
3238    $indent     = 0,
3239
3240    $lastIdx    = 0,
3241    $lastLineSt = 0,
3242    $levelPad   = array(0);
3243
3244
3245  public function flush()
3246  {
3247    print $this->out;
3248    $this->out   = '';
3249    $this->cache = array();
3250  }
3251
3252  public function sep($label = ' ')
3253  {
3254    $this->out .= $label;
3255  }
3256
3257  public function text($type, $text = null, $meta = null, $uri = null)
3258  {
3259
3260    if (!is_array($type))
3261    {
3262      $type = (array)$type;
3263    }
3264
3265    if ($text === null)
3266    {
3267      $text = $type[0];
3268    }
3269
3270    if (in_array('special', $type, true))
3271    {
3272      $text = strtr($text, array(
3273        "\r" => '\r',     // carriage return
3274        "\t" => '\t',     // horizontal tab
3275        "\n" => '\n',     // linefeed (new line)
3276        "\v" => '\v',     // vertical tab
3277        "\e" => '\e',     // escape
3278        "\f" => '\f',     // form feed
3279        "\0" => '\0',
3280      ));
3281
3282      $this->out .= $text;
3283      return;
3284    }
3285
3286    $formatMap = array(
3287      'string'   => '%3$s "%2$s"',
3288      'integer'  => 'int(%2$s)',
3289      'double'   => 'double(%2$s)',
3290      'true'     => 'bool(%2$s)',
3291      'false'    => 'bool(%2$s)',
3292      'key'      => '[%2$s]',
3293    );
3294
3295    if (!is_string($meta))
3296    {
3297      $meta = '';
3298    }
3299
3300    $this->out .= isset($formatMap[$type[0]]) ? sprintf($formatMap[$type[0]], $type[0], $text, $meta) : $text;
3301  }
3302
3303  public function startContain($type, $label = false)
3304  {
3305
3306    if (!is_array($type))
3307    {
3308      $type = (array)$type;
3309    }
3310
3311    if ($label)
3312    {
3313      $this->out .= "\n" . str_repeat(' ', $this->indent + $this->levelPad[$this->level]) . "┗ {$type[0]} ~ ";
3314    }
3315  }
3316
3317  public function emptyGroup($prefix = '')
3318  {
3319    $this->out .= "({$prefix})";
3320  }
3321
3322  public function startGroup($prefix = '')
3323  {
3324
3325    $maxDepth = ref::config('maxDepth');
3326
3327    if (($maxDepth > 0) && (($this->level + 1) > $maxDepth))
3328    {
3329      $this->emptyGroup('...');
3330      return false;
3331    }
3332
3333    $this->level++;
3334    $this->out .= '(';
3335
3336    $this->indent += $this->levelPad[$this->level - 1];
3337    return true;
3338  }
3339
3340  public function endGroup()
3341  {
3342    $this->out .= "\n" . str_repeat(' ', $this->indent) . ')';
3343    $this->indent -= $this->levelPad[$this->level - 1];
3344    $this->level--;
3345  }
3346
3347  public function sectionTitle($title)
3348  {
3349    $pad = str_repeat(' ', $this->indent + 2);
3350    $this->out .= sprintf("\n\n%s%s\n%s%s", $pad, $title, $pad, str_repeat('-', strlen($title)));
3351  }
3352
3353  public function startRow()
3354  {
3355    $this->out .= "\n  " . str_repeat(' ', $this->indent);
3356    $this->lastLineSt = strlen($this->out);
3357  }
3358
3359  public function endRow(){}
3360
3361  public function colDiv($padLen = null)
3362  {
3363    $padLen = ($padLen !== null) ? $padLen + 1 : 1;
3364    $this->out .= str_repeat(' ', $padLen);
3365
3366    $this->lastIdx = strlen($this->out);
3367    $this->levelPad[$this->level] = $this->lastIdx - $this->lastLineSt + 2;
3368  }
3369
3370  public function bubbles(array $items)
3371  {
3372
3373    if (!$items)
3374    {
3375      $this->out .= '  ';
3376      return;
3377    }
3378
3379    $this->out .= '<';
3380
3381    foreach ($items as $item)
3382    {
3383      $this->out .= $item[0];
3384    }
3385
3386    $this->out .= '>';
3387  }
3388
3389  public function endExp()
3390  {
3391
3392    if (ref::config('showBacktrace') && ($trace = ref::getBacktrace()))
3393    {
3394      $this->out .= ' - ' . $trace['file'] . ':' . $trace['line'];
3395    }
3396
3397    $this->out .= "\n" . str_repeat('=', strlen($this->out)) . "\n";
3398  }
3399
3400  public function startRoot()
3401  {
3402    $this->out .= "\n\n";
3403
3404  }
3405
3406  public function endRoot()
3407  {
3408    $this->out .= "\n";
3409    if (($timeout = ref::getTimeoutPoint()) > 0)
3410    {
3411      $this->out .= sprintf("\n-- Listing incomplete. Timed-out after %4.2fs -- \n", $timeout);
3412    }
3413  }
3414
3415}
3416
3417
3418/**
3419 * Text formatter with color support for CLI -- unfinished
3420 *
3421 */
3422class RCliTextFormatter extends RTextFormatter
3423{
3424
3425  public function sectionTitle($title)
3426  {
3427    $pad = str_repeat(' ', $this->indent + 2);
3428    $this->out .= sprintf("\n\n%s\x1b[4;97m%s\x1b[0m", $pad, $title);
3429  }
3430
3431  public function startExp()
3432  {
3433    $this->out .= "\x1b[1;44;96m ";
3434  }
3435
3436  public function endExp()
3437  {
3438    if (ref::config('showBacktrace') && ($trace = ref::getBacktrace()))
3439    {
3440      $this->out .= "\x1b[0m\x1b[44;36m " . $trace['file'] . ':' . $trace['line'];
3441    }
3442
3443    $this->out .=  " \x1b[0m\n";
3444  }
3445
3446  public function endRoot()
3447  {
3448    $this->out .= "\n";
3449    if (($timeout = ref::getTimeoutPoint()) > 0)
3450    {
3451      $this->out .= sprintf("\n\x1b[3;91m-- Listing incomplete. Timed-out after %4.2fs --\x1b[0m\n", $timeout);
3452    }
3453  }
3454
3455}
3456
3457// EOF