PageRenderTime 64ms CodeModel.GetById 3ms app.highlight 46ms RepoModel.GetById 1ms app.codeStats 1ms

/ref.php

https://github.com/firejdl/php-ref
PHP | 2178 lines | 1285 code | 443 blank | 450 comment | 206 complexity | b2587fa2f2b1df4da959d1af127fce48 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1<?php
   2
   3
   4
   5/**
   6 * Shortcut to ref, HTML mode
   7 *
   8 * @version  1.0
   9 * @param    mixed $args
  10 */
  11function r(){
  12
  13  // arguments passed to this function
  14  $args = func_get_args();
  15
  16  // options (operators) gathered by the expression parser;
  17  // this variable gets passed as reference to getInputExpressions(), which will store the operators in it
  18  $options = array();
  19
  20  // doh
  21  $output = '';
  22
  23  $ref = new ref('html');
  24
  25  // names of the arguments that were passed to this function
  26  $expressions = ref::getInputExpressions($options);
  27
  28  // something went wrong while trying to parse the source expressions?
  29  // if so, silently ignore this part and leave out the expression info
  30  if(func_num_args() !== count($expressions))
  31    $expressions = array_fill(0, func_num_args(), null);
  32
  33  foreach($args as $index => $arg)
  34    $output .= $ref->query($arg, $expressions[$index]);
  35
  36  // return the results if this function was called with the error suppression operator 
  37  if(in_array('@', $options, true))
  38    return $output;
  39
  40  // IE goes funky if there's no doctype
  41  if(!headers_sent() && !ob_get_length())
  42    print '<!DOCTYPE HTML><html><head><title>REF</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /></head><body>';
  43
  44  print $output;
  45
  46  // stop the script if this function was called with the bitwise not operator
  47  if(in_array('~', $options, true)){
  48    print '</body></html>';
  49    exit(0);
  50  }  
  51}
  52
  53
  54
  55/**
  56 * Shortcut to ref, plain text mode
  57 *
  58 * @version  1.0
  59 * @param    mixed $args
  60 */
  61function rt(){
  62  $args        = func_get_args();
  63  $options     = array();  
  64  $output      = '';
  65  $expressions = ref::getInputExpressions($options);
  66  $ref         = new ref('text');
  67
  68  if(func_num_args() !== count($expressions))
  69    $expressions = array_fill(0, func_num_args(), null);
  70
  71  foreach($args as $index => $arg)
  72    $output .= $ref->query($arg, $expressions[$index]);
  73
  74  if(in_array('@', $options, true))
  75    return $output;  
  76
  77  if(!headers_sent())    
  78    header('Content-Type: text/plain; charset=utf-8');
  79
  80  print $output;
  81
  82  if(in_array('~', $options, true))
  83    exit(0);  
  84}
  85
  86
  87
  88/**
  89 * REF is a nicer alternative to PHP's print_r() / var_dump().
  90 *
  91 * @version  1.0
  92 * @author   digitalnature - http://digitalnature.eu
  93 */
  94class ref{
  95
  96  const
  97
  98    MARKER_KEY = '_phpRefArrayMarker_';
  99
 100
 101
 102  protected static
 103  
 104    /**
 105     * CPU time used for processing
 106     *
 107     * @var  array
 108     */   
 109    $time   = 0,
 110
 111    /**
 112     * Configuration (+ default values)
 113     *
 114     * @var  array
 115     */     
 116    $config = array(
 117
 118                // initial expand depth (for HTML mode only)
 119                'expandDepth'  => 1,
 120
 121                // shortcut functions used to access the query method below;
 122                // if they are namespaced, the namespace must be present as well (methods are not supported)
 123                'shortcutFunc' => array('r', 'rt'),
 124
 125                // callbacks for custom/external formatters (as associative array: format => callback)
 126                'formatter'    => array(),
 127
 128                // when this option is set to TRUE, additional information is
 129                // returned (note that this seriously affects performance):
 130                // - string matches (date, file, functions, classes, json, serialized data, regex etc.)
 131                // - extra information for some resource types                
 132                // - contents of iterator objects
 133                'extendedInfo' => true,
 134
 135                // stylesheet path (for HTML only);
 136                // 'false' means no styles
 137                'stylePath'    => '{:dir}/ref.css',
 138
 139                // javascript path (for HTML only);
 140                // 'false' means no js                      
 141                'scriptPath'   => '{:dir}/ref.js',
 142
 143              );
 144
 145
 146  protected
 147
 148    /**
 149     * Tracks current nesting level
 150     *
 151     * @var  int
 152     */  
 153    $level    = 0,
 154
 155    /**
 156     * Max. expand depth of this instance
 157     *
 158     * @var  int
 159     */     
 160    $expDepth = 1,
 161
 162    /**
 163     * Output format of this instance
 164     *
 165     * @var  string
 166     */     
 167    $format   = null,
 168
 169    /**
 170     * Some environment variables
 171     * used to determine feature support
 172     *
 173     * @var  string
 174     */ 
 175    $env      = array(),
 176
 177    /**
 178     * Used to cache output to speed up processing.
 179     * Cached objects will not be processed again in the same query
 180     *
 181     * @var  array
 182     */ 
 183    $cache    = array();
 184
 185
 186
 187  /**
 188   * Constructor
 189   *
 190   * @param   string $format        Output format, defaults to 'html'
 191   * @param   int|null $expDepth    Maximum expand depth (relevant to the HTML format)
 192   */
 193  public function __construct($format = 'html', $expDepth = null){
 194
 195    $this->format   = $format;
 196    $this->expDepth = ($expDepth !== null) ? $expDepth : static::$config['expandDepth'];
 197
 198    $this->env = array(
 199
 200      // php 5.4+ ?
 201      'is54'         => version_compare(PHP_VERSION, '5.4') >= 0,
 202
 203      // is the 'mbstring' extension active?
 204      'mbStr'        => function_exists('mb_detect_encoding'),
 205
 206      // @see: https://bugs.php.net/bug.php?id=52469     
 207      'supportsDate' => (strncasecmp(PHP_OS, 'WIN', 3) !== 0) || (version_compare(PHP_VERSION, '5.3.10') >= 0),
 208    );
 209  }
 210
 211
 212
 213  /**
 214   * Enforce proper use of this class
 215   *
 216   * @param   string $name
 217   */
 218  public function __get($name){
 219    throw new \Exception('No such property');
 220  }
 221
 222
 223
 224  /**
 225   * Enforce proper use of this class
 226   *
 227   * @param   string $name
 228   * @param   mixed $value
 229   */
 230  public function __set($name, $value){
 231    throw new \Exception('Not allowed');
 232  }  
 233
 234
 235
 236  /**
 237   * Used to dispatch output to a custom formatter
 238   *   
 239   * @param   string $name
 240   * @param   array $args
 241   * @return  string
 242   */
 243  public function __call($name, array $args = array()){
 244
 245    if(isset(static::$config['formatters'][$name]))
 246      return call_user_func_array(static::$config['formatters'][$name], $args);
 247
 248    throw new \Exception('Method not defined');
 249  }
 250
 251
 252
 253  /**
 254   * Generate structured information about a variable/value/expression (subject)
 255   *   
 256   * @param   mixed $subject
 257   * @param   string $expression
 258   * @return  string
 259   */
 260  public function query($subject, $expression = null){
 261
 262    $startTime     = microtime(true);
 263    $output        = $this->{"to{$this->format}"}('root', $this->evaluate($subject), $this->evaluateExp($expression));
 264    $this->cache   = array();
 265    static::$time += microtime(true) - $startTime; 
 266
 267    return $output;
 268  }
 269
 270
 271
 272  /**
 273   * Executes a function the given number of times and returns the elapsed time.
 274   *
 275   * Keep in mind that the returned time includes function call overhead (including
 276   * microtime calls) x iteration count. This is why this is better suited for
 277   * determining which of two or more functions is the fastest, rather than
 278   * finding out how fast is a single function.
 279   *
 280   * @param   int $iterations      Number of times the function will be executed
 281   * @param   callable $function   Function to execute
 282   * @param   mixed &$output       If given, last return value will be available in this variable
 283   * @return  double               Elapsed time
 284   */
 285  public static function timeFunc($iterations, $function, &$output = null){
 286    
 287    $time = 0;
 288
 289    for($i = 0; $i < $iterations; $i++){
 290      $start  = microtime(true);
 291      $output = call_user_func($function);
 292      $time  += microtime(true) - $start;
 293    }
 294    
 295    return round($time, 4);
 296  }
 297
 298
 299
 300  /**
 301   * Timery utility
 302   *
 303   * First call of this function will start the timer.
 304   * The second call will stop the timer and return the elapsed time
 305   * since the timer started.
 306   *
 307   * Multiple timers can be controlled simultaneously by specifying a timer ID.
 308   *
 309   * @since   1.0   
 310   * @param   int $id          Timer ID, optional
 311   * @param   int $precision   Precision of the result, optional
 312   * @return  void|double      Elapsed time, or void if the timer was just started
 313   */
 314  public static function timer($id = 1, $precision = 4){
 315
 316    static
 317      $timers = array();
 318
 319    // check if this timer was started, and display the elapsed time if so
 320    if(isset($timers[$id])){
 321      $elapsed = round(microtime(true) - $timers[$id], $precision);
 322      unset($timers[$id]);
 323      return $elapsed;
 324    }
 325
 326    // ID doesn't exist, start new timer
 327    $timers[$id] = microtime(true);
 328  }
 329
 330
 331
 332  /**
 333   * Parses a DocBlock comment into a data structure.
 334   *
 335   * @link    http://pear.php.net/manual/en/standards.sample.php
 336   * @param   string $comment    DocBlock comment (must start with /**)
 337   * @param   string|null $key   Field to return (optional)
 338   * @return  array|string|null  Array containing all fields, array/string with the contents of
 339   *                             the requested field, or null if the comment is empty/invalid
 340   */
 341  public static function parseComment($comment, $key = null){
 342
 343    $description = '';
 344    $tags        = array();
 345    $tag         = null;
 346    $pointer     = '';
 347    $padding     = 0;
 348    $comment     = preg_split('/\r\n|\r|\n/', '* ' . trim($comment, "/* \t\n\r\0\x0B"));
 349
 350    // analyze each line
 351    foreach($comment as $line){
 352
 353      // drop any wrapping spaces
 354      $line = trim($line);
 355
 356      // drop "* "
 357      if($line !== '')
 358        $line = substr($line, 2);      
 359
 360      if(strpos($line, '@') !== 0){
 361
 362        // preserve formatting of tag descriptions,
 363        // because they may span across multiple lines
 364        if($tag !== null){
 365          $trimmed = trim($line);
 366
 367          if($padding !== 0)
 368            $trimmed = static::strPad($trimmed, static::strLen($line) - $padding, ' ', STR_PAD_LEFT);
 369          else
 370            $padding = static::strLen($line) - static::strLen($trimmed);
 371
 372          $pointer .= "\n{$trimmed}";
 373          continue;
 374        }
 375        
 376        // tag definitions have not started yet; assume this is part of the description text
 377        $description .= "\n{$line}";        
 378        continue;
 379      }  
 380
 381      $padding = 0;
 382      $parts = explode(' ', $line, 2);
 383
 384      // invalid tag? (should we include it as an empty array?)
 385      if(!isset($parts[1]))
 386        continue;
 387
 388      $tag = substr($parts[0], 1);
 389      $line = ltrim($parts[1]);
 390
 391      // tags that have a single component (eg. link, license, author, throws...);
 392      // note that @throws may have 2 components, however most people use it like "@throws ExceptionClass if whatever...",
 393      // which, if broken into two values, leads to an inconsistent description sentence
 394      if(!in_array($tag, array('global', 'param', 'return', 'var'))){
 395        $tags[$tag][] = $line;
 396        end($tags[$tag]);
 397        $pointer = &$tags[$tag][key($tags[$tag])];
 398        continue;
 399      }
 400
 401      // tags with 2 or 3 components (var, param, return);
 402      $parts    = explode(' ', $line, 2);
 403      $parts[1] = isset($parts[1]) ? ltrim($parts[1]) : null;
 404      $lastIdx  = 1;
 405
 406      // expecting 3 components on the 'param' tag: type varName varDescription
 407      if($tag === 'param'){
 408        $lastIdx = 2;
 409        if(in_array($parts[1][0], array('&', '$'), true)){
 410          $line     = ltrim(array_pop($parts));
 411          $parts    = array_merge($parts, explode(' ', $line, 2));        
 412          $parts[2] = isset($parts[2]) ? ltrim($parts[2]) : null;
 413        }else{
 414          $parts[2] = $parts[1];
 415          $parts[1] = null;
 416        }
 417      }  
 418
 419      $tags[$tag][] = $parts;
 420      end($tags[$tag]);
 421      $pointer = &$tags[$tag][key($tags[$tag])][$lastIdx];
 422    }
 423
 424    // split title from the description texts at the nearest 2x new-line combination
 425    // (note: loose check because 0 isn't valid as well)
 426    if(strpos($description, "\n\n")){
 427      list($title, $description) = explode("\n\n", $description, 2);
 428
 429    // if we don't have 2 new lines, try to extract first sentence
 430    }else{  
 431      // in order for a sentence to be considered valid,
 432      // the next one must start with an uppercase letter    
 433      $sentences = preg_split('/(?<=[.?!])\s+(?=[A-Z])/', $description, 2, PREG_SPLIT_NO_EMPTY);
 434
 435      // failed to detect a second sentence? then assume there's only title and no description text
 436      $title = isset($sentences[0]) ? $sentences[0] : $description;
 437      $description = isset($sentences[1]) ? $sentences[1] : '';
 438    }
 439
 440    $title = ltrim($title);
 441    $description = ltrim($description);
 442
 443    $data = compact('title', 'description', 'tags');
 444
 445    if(!array_filter($data))
 446      return null;
 447
 448    if($key !== null)
 449      return isset($data[$key]) ? $data[$key] : null;
 450
 451    return $data;
 452  }
 453
 454
 455
 456  /**
 457   * Split a regex into its components
 458   * 
 459   * Based on "Regex Colorizer" by Steven Levithan (this is a translation from javascript)
 460   *
 461   * @link     https://github.com/slevithan/regex-colorizer
 462   * @link     https://github.com/symfony/Finder/blob/master/Expression/Regex.php#L64-74
 463   * @param    string $pattern
 464   * @return   array
 465   */
 466  public static function splitRegex($pattern){
 467
 468    // detection attempt code from the Symfony Finder component
 469    $maybeValid = false;
 470    if(preg_match('/^(.{3,}?)([imsxuADU]*)$/', $pattern, $m)) {
 471      $start = substr($m[1], 0, 1);
 472      $end   = substr($m[1], -1);
 473
 474      if(($start === $end && !preg_match('/[*?[:alnum:] \\\\]/', $start)) || ($start === '{' && $end === '}'))
 475        $maybeValid = true;
 476    }
 477
 478    if(!$maybeValid)
 479      throw new \Exception('Pattern does not appear to be a valid PHP regex');
 480
 481    $output              = array();
 482    $capturingGroupCount = 0;
 483    $groupStyleDepth     = 0;
 484    $openGroups          = array();
 485    $lastIsQuant         = false;
 486    $lastType            = 1;      // 1 = none; 2 = alternator
 487    $lastStyle           = null;
 488
 489    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);
 490
 491    $matches = $matches[0];
 492
 493    $getTokenCharCode = function($token){
 494      if(strlen($token) > 1 && $token[0] === '\\'){
 495        $t1 = substr($token, 1);
 496
 497        if(preg_match('/^c[A-Za-z]$/', $t1))
 498          return strpos("ABCDEFGHIJKLMNOPQRSTUVWXYZ", strtoupper($t1[1])) + 1;
 499
 500        if(preg_match('/^(?:x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4})$/', $t1))
 501          return intval(substr($t1, 1), 16);
 502
 503        if(preg_match('/^(?:[0-3][0-7]{0,2}|[4-7][0-7]?)$/', $t1))
 504          return intval($t1, 8);
 505
 506        $len = strlen($t1);
 507
 508        if($len === 1 && strpos('cuxDdSsWw', $t1) !== false)
 509          return null;
 510
 511        if($len === 1){
 512          switch ($t1) {
 513            case 'b': return 8;  
 514            case 'f': return 12; 
 515            case 'n': return 10; 
 516            case 'r': return 13; 
 517            case 't': return 9;  
 518            case 'v': return 11; 
 519            default: return $t1[0]; 
 520          }
 521        }
 522      }
 523
 524      return ($token !== '\\') ? $token[0] : null;  
 525    };   
 526
 527    foreach($matches as $m){
 528
 529      if($m[0] === '['){
 530        $lastCC         = null;  
 531        $cLastRangeable = false;
 532        $cLastType      = 0;  // 0 = none; 1 = range hyphen; 2 = short class
 533
 534        preg_match('/^(\[\^?)(]?(?:[^\\\\\]]+|\\\\[\S\s]?)*)(]?)$/', $m, $parts);
 535
 536        array_shift($parts);
 537        list($opening, $content, $closing) = $parts;
 538
 539        if(!$closing)
 540          throw new \Exception('Unclosed character class');
 541
 542        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);
 543        $ccTokens     = $ccTokens[0];
 544        $ccTokenCount = count($ccTokens);
 545        $output[]     = array('chr' => $opening);
 546
 547        foreach($ccTokens as $i => $cm) {
 548
 549          if($cm[0] === '\\'){
 550            if(preg_match('/^\\\\[cux]$/', $cm))
 551              throw new \Exception('Incomplete regex token');
 552
 553            if(preg_match('/^\\\\[dsw]$/i', $cm)) {
 554              $output[]     = array('chr-meta' => $cm);
 555              $cLastRangeable  = ($cLastType !== 1);
 556              $cLastType       = 2;
 557
 558            }elseif($cm === '\\'){
 559              throw new \Exception('Incomplete regex token');
 560              
 561            }else{
 562              $output[]       = array('chr-meta' => $cm);
 563              $cLastRangeable = $cLastType !== 1;
 564              $lastCC         = $getTokenCharCode($cm);
 565            }
 566
 567          }elseif($cm === '-'){
 568            if($cLastRangeable){
 569              $nextToken = ($i + 1 < $ccTokenCount) ? $ccTokens[$i + 1] : false;
 570
 571              if($nextToken){
 572                $nextTokenCharCode = $getTokenCharCode($nextToken[0]);
 573
 574                if((!is_null($nextTokenCharCode) && $lastCC > $nextTokenCharCode) || $cLastType === 2 || preg_match('/^\\\\[dsw]$/i', $nextToken[0]))
 575                  throw new \Exception('Reversed or invalid range');
 576
 577                $output[]       = array('chr-range' => '-');
 578                $cLastRangeable = false;
 579                $cLastType      = 1;
 580               
 581              }else{
 582                $output[] = $closing ? array('chr' => '-') : array('chr-range' => '-');
 583              }
 584
 585            }else{
 586              $output[]        = array('chr' => '-');
 587              $cLastRangeable  = ($cLastType !== 1);
 588            }
 589
 590          }else{
 591            $output[]       = array('chr' => $cm);
 592            $cLastRangeable = strlen($cm) > 1 || ($cLastType !== 1);
 593            $lastCC         = $cm[strlen($cm) - 1];
 594          }
 595        }
 596
 597        $output[] = array('chr' => $closing);
 598        $lastIsQuant  = true;
 599
 600      }elseif($m[0] === '('){
 601        if(strlen($m) === 2)
 602          throw new \Exception('Invalid or unsupported group type');
 603   
 604        if(strlen($m) === 1)
 605          $capturingGroupCount++;
 606
 607        $groupStyleDepth = ($groupStyleDepth !== 5) ? $groupStyleDepth + 1 : 1;
 608        $openGroups[]    = $m; // opening
 609        $lastIsQuant     = false;
 610        $output[]        = array("g{$groupStyleDepth}" => $m);
 611
 612      }elseif($m[0] === ')'){
 613        if(!count($openGroups)) 
 614          throw new \Exception('No matching opening parenthesis');
 615
 616        $output[]        = array('g' . $groupStyleDepth => ')');
 617        $prevGroup       = $openGroups[count($openGroups) - 1];
 618        $prevGroup       = isset($prevGroup[2]) ? $prevGroup[2] : '';
 619        $lastIsQuant     = !preg_match('/^[=!]/', $prevGroup);
 620        $lastStyle       = "g{$groupStyleDepth}";
 621        $lastType        = 0;
 622        $groupStyleDepth = ($groupStyleDepth !== 1) ? $groupStyleDepth - 1 : 5;
 623
 624        array_pop($openGroups);
 625        continue;
 626      
 627      }elseif($m[0] === '\\'){
 628        if(isset($m[1]) && preg_match('/^[1-9]/', $m[1])){
 629          $nonBackrefDigits = '';
 630          $num = substr(+$m, 1);
 631
 632          while($num > $capturingGroupCount){
 633            preg_match('/[0-9]$/', $num, $digits);
 634            $nonBackrefDigits = $digits[0] . $nonBackrefDigits;
 635            $num = floor($num / 10); 
 636          }
 637
 638          if($num > 0){
 639            $output[] = array('meta' =>  "\\{$num}", 'text' => $nonBackrefDigits);
 640
 641          }else{
 642            preg_match('/^\\\\([0-3][0-7]{0,2}|[4-7][0-7]?|[89])([0-9]*)/', $m, $pts);
 643            $output[] = array('meta' => '\\' . $pts[1], 'text' => $pts[2]);
 644          }
 645
 646          $lastIsQuant = true;
 647
 648        }elseif(isset($m[1]) && preg_match('/^[0bBcdDfnrsStuvwWx]/', $m[1])){
 649   
 650          if(preg_match('/^\\\\[cux]$/', $m))
 651            throw new \Exception('Incomplete regex token');
 652
 653          $output[]    = array('meta' => $m);
 654          $lastIsQuant = (strpos('bB', $m[1]) === false);
 655
 656        }elseif($m === '\\'){
 657          throw new \Exception('Incomplete regex token');
 658            
 659        }else{
 660          $output[]    = array('text' => $m);
 661          $lastIsQuant = true;
 662        }
 663
 664      }elseif(preg_match('/^(?:[?*+]|\{[0-9]+(?:,[0-9]*)?\})\??$/', $m)){
 665        if(!$lastIsQuant)
 666          throw new \Exception('Quantifiers must be preceded by a token that can be repeated');
 667
 668        preg_match('/^\{([0-9]+)(?:,([0-9]*))?/', $m, $interval);
 669
 670        if($interval && (+$interval[1] > 65535 || (isset($interval[2]) && (+$interval[2] > 65535))))
 671          throw new \Exception('Interval quantifier cannot use value over 65,535');
 672        
 673        if($interval && isset($interval[2]) && (+$interval[1] > +$interval[2]))
 674          throw new \Exception('Interval quantifier range is reversed');
 675        
 676        $output[]     = array($lastStyle ? $lastStyle : 'meta' => $m);
 677        $lastIsQuant  = false;
 678
 679      }elseif($m === '|'){
 680        if($lastType === 1 || ($lastType === 2 && !count($openGroups)))
 681          throw new \Exception('Empty alternative effectively truncates the regex here');
 682
 683        $output[]    = count($openGroups) ? array("g{$groupStyleDepth}" => '|') : array('meta' => '|');
 684        $lastIsQuant = false;
 685        $lastType    = 2;
 686        $lastStyle   = '';
 687        continue;
 688
 689      }elseif($m === '^' || $m === '$'){
 690        $output[]    = array('meta' => $m);
 691        $lastIsQuant = false;
 692
 693      }elseif($m === '.'){
 694        $output[]    = array('meta' => '.');
 695        $lastIsQuant = true;
 696   
 697      }else{
 698        $output[]    = array('text' => $m);
 699        $lastIsQuant = true;
 700      }
 701
 702      $lastType  = 0;
 703      $lastStyle = '';    
 704    }
 705
 706    if($openGroups)
 707      throw new \Exception('Unclosed grouping');
 708
 709    return $output;
 710  }
 711
 712
 713
 714  /**
 715   * Set or get configuration options
 716   *
 717   * @param   string $key
 718   * @param   mixed|null $value
 719   * @return  mixed
 720   */
 721  public static function config($key, $value = null){
 722
 723    if(!array_key_exists($key, static::$config))
 724      throw new \Exception(sprintf('Unrecognized option: "%s". Valid options are: %s', $key, implode(', ', array_keys(static::$config))));
 725
 726    if($value === null)
 727      return static::$config[$key];
 728
 729    if(is_array(static::$config[$key]))
 730      return static::$config[$key] = (array)$value;
 731
 732    return static::$config[$key] = $value;
 733  }
 734
 735
 736
 737  /**
 738   * Get styles and javascript (only generated for the 1st call)
 739   *
 740   * @return  string
 741   */
 742  public static function getAssets(){
 743
 744    // tracks style/jscript inclusion state (html only)
 745    static $didAssets = false;
 746
 747    // first call? include styles and javascript
 748    if($didAssets)
 749      return '';   
 750
 751    ob_start();
 752
 753    if(static::$config['stylePath'] !== false){
 754      ?>
 755      <style scoped>
 756        <?php readfile(str_replace('{:dir}', __DIR__, static::$config['stylePath'])); ?>
 757      </style>
 758      <?php
 759    }
 760
 761    if(static::$config['scriptPath'] !== false){
 762      ?>
 763      <script>
 764        <?php readfile(str_replace('{:dir}', __DIR__, static::$config['scriptPath'])); ?>
 765      </script>
 766      <?php
 767    }  
 768
 769    // normalize space and remove comments
 770    $output = preg_replace('/\s+/', ' ', trim(ob_get_clean()));
 771    $output = preg_replace('!/\*.*?\*/!s', '', $output);
 772    $output = preg_replace('/\n\s*\n/', "\n", $output);
 773
 774    $didAssets = true;
 775
 776    return $output;
 777  }
 778
 779
 780
 781  /**
 782   * Total CPU time used by the class
 783   *   
 784   * @param   int precision
 785   * @return  double
 786   */
 787  public static function getTime($precision = 4){
 788    return round(static::$time, $precision);
 789  }
 790
 791
 792
 793  /**
 794   * Determines the input expression(s) passed to the shortcut function
 795   *
 796   * @param   array &$options   Optional, options to gather (from operators)
 797   * @return  array             Array of string expressions
 798   */
 799  public static function getInputExpressions(array &$options = null){    
 800
 801    // used to determine the position of the current call,
 802    // if more queries calls were made on the same line
 803    static $lineInst = array();
 804
 805    // pull only basic info with php 5.3.6+ to save some memory
 806    $trace = defined('DEBUG_BACKTRACE_IGNORE_ARGS') ? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) : debug_backtrace();
 807    
 808    while($callee = array_pop($trace)){
 809
 810      // extract only the information we neeed
 811      $callee = array_intersect_key($callee, array_fill_keys(array('file', 'function', 'line'), false));
 812      extract($callee);
 813
 814      // skip, if the called function doesn't match the shortcut function name
 815      if(!$function || !preg_grep("/{$function}/i" , static::$config['shortcutFunc']))
 816        continue;
 817
 818      if(!$line || !$file)
 819        return array();
 820    
 821      $code     = file($file);
 822      $code     = $code[$line - 1]; // multiline expressions not supported!
 823      $instIndx = 0;
 824      $tokens   = token_get_all("<?php {$code}");
 825
 826      // locate the caller position in the line, and isolate argument tokens
 827      foreach($tokens as $i => $token){
 828
 829        // match token with our shortcut function name
 830        if(is_string($token) || ($token[0] !== T_STRING) || (strcasecmp($token[1], $function) !== 0))
 831          continue;
 832
 833        // is this some method that happens to have the same name as the shortcut function?
 834        if(isset($tokens[$i - 1]) && is_array($tokens[$i - 1]) && in_array($tokens[$i - 1][0], array(T_DOUBLE_COLON, T_OBJECT_OPERATOR), true))
 835          continue;
 836
 837        // find argument definition start, just after '('
 838        if(isset($tokens[$i + 1]) && ($tokens[$i + 1][0] === '(')){
 839          $instIndx++;
 840
 841          if(!isset($lineInst[$line]))
 842            $lineInst[$line] = 0;
 843
 844          if($instIndx <= $lineInst[$line])
 845            continue;
 846
 847          $lineInst[$line]++;
 848
 849          // gather options
 850          if($options !== null){
 851            $j = $i - 1;
 852            while(isset($tokens[$j]) && is_string($tokens[$j]) && in_array($tokens[$j], array('@', '+', '-', '!', '~')))
 853              $options[] = $tokens[$j--];
 854          }  
 855         
 856          $lvl = $index = $curlies = 0;
 857          $expressions = array();
 858
 859          // get the expressions
 860          foreach(array_slice($tokens, $i + 2) as $token){
 861
 862            if(is_array($token)){
 863              if($token[0] !== T_COMMENT)
 864                $expressions[$index][] = ($token[0] !== T_WHITESPACE) ? $token[1] : ' ';
 865
 866              continue;
 867            }
 868
 869            if($token === '{')
 870              $curlies++;
 871
 872            if($token === '}')
 873              $curlies--;        
 874
 875            if($token === '(')
 876              $lvl++;
 877
 878            if($token === ')')
 879              $lvl--;
 880
 881            // assume next argument if a comma was encountered,
 882            // and we're not insde a curly bracket or inner parentheses
 883            if(($curlies < 1) && ($lvl === 0) && ($token === ',')){
 884              $index++;
 885              continue;
 886            }  
 887
 888            // negative parentheses count means we reached the end of argument definitions
 889            if($lvl < 0){         
 890              foreach($expressions as &$expression)
 891                $expression = trim(implode('', $expression));
 892
 893              return $expressions;
 894            }
 895
 896            $expressions[$index][] = $token;      
 897          }
 898
 899          break;
 900        }    
 901      }     
 902    }
 903  }
 904
 905
 906
 907  /**
 908   * Generates the output in plain text format
 909   *   
 910   * @param   string $element
 911   * @param   string|null $arg1
 912   * @param   string|null $arg2
 913   * @param   string|array|null $meta
 914   * @return  string
 915   */
 916  protected function toText($element, $arg1 = null, $arg2 = null, $meta = null){
 917
 918    switch($element){
 919      case 'sep':
 920        return $arg1;
 921
 922      case 'text':
 923        if($arg2 === null)
 924          $arg2 = $arg1;
 925
 926        if($arg1 === 'specialString'){
 927          $arg2 = strtr($arg2, array(
 928            "\r" => '\r',     // carriage return
 929            "\t" => '\t',     // horizontal tab
 930            "\n" => '\n',     // linefeed (new line)
 931            "\v" => '\v',     // vertical tab
 932            "\e" => '\e',     // escape
 933            "\f" => '\f',     // form feed
 934            "\0" => '\0',
 935          ));
 936        }        
 937
 938        $formatMap = array(
 939          'string'   => '%3$s "%2$s"',
 940          'integer'  => 'int(%2$s)',
 941          'double'   => 'double(%2$s)',
 942          'true'     => 'bool(%2$s)',
 943          'false'    => 'bool(%2$s)',
 944          'key'      => '[%2$s]',
 945        );
 946
 947        if(!is_string($meta))
 948          $meta = '';
 949
 950        return isset($formatMap[$arg1]) ? sprintf($formatMap[$arg1], $arg1, $arg2, $meta) : $arg2;
 951
 952      case 'match':
 953        return ($arg1 !== 'regex') ? "\n~~ {$arg1}: {$arg2}" : '';
 954
 955      case 'contain':
 956        return ($arg1 !== 'regex') ? $arg2 : '';
 957
 958      case 'link':
 959        return $arg1;
 960
 961      case 'group':
 962        $prefix = ($arg2 !== null) ? $arg2 : '';
 963        if($arg1){
 964          $arg1 =  $arg1 . "\n";
 965          $prefix .= " \xe2\x96\xbc";
 966        }
 967
 968        return "({$prefix}{$arg1})";
 969
 970      case 'section':        
 971        $output = '';
 972
 973        if($arg2 !== null)
 974          $output .= "\n\n " . $arg2 . "\n " . str_repeat('-', static::strLen($arg2));
 975
 976        $lengths = array();
 977
 978        // determine maximum column width
 979        foreach($arg1 as $item)
 980          foreach($item as $colIdx => $c)
 981            if(!isset($lengths[$colIdx]) || $lengths[$colIdx] < static::strLen($c))
 982              $lengths[$colIdx] = static::strLen($c);
 983
 984        foreach($arg1 as $item){
 985          $lastColIdx = count($item) - 1;
 986          $padLen     = 0;
 987          $output    .= "\n  ";
 988
 989          foreach($item as $colIdx => $c){
 990
 991            // skip empty columns
 992            if($lengths[$colIdx] < 1)
 993              continue;
 994
 995            if($colIdx < $lastColIdx){
 996              $output .= static::strPad($c, $lengths[$colIdx]) . ' ';
 997              $padLen += $lengths[$colIdx] + 1;
 998              continue;
 999            }
1000       
1001            // indent the entire block
1002            $output .= str_replace("\n", "\n" . str_repeat(' ', $padLen + 2), $c);
1003          }         
1004        }
1005
1006        return $output;
1007
1008      case 'bubbles':
1009        return $arg1 ? '[' . implode('', $arg1) . '] ' : '';
1010
1011      case 'root':
1012        return sprintf("\n%s\n%s\n%s\n", $arg2, str_repeat('=', static::strLen($arg2)), $arg1);        
1013        
1014      default:
1015        return '';
1016
1017    }
1018  }
1019
1020
1021
1022  /**
1023   * Generates the output in HTML5 format
1024   *   
1025   * @param   string $element
1026   * @param   string|null $arg1
1027   * @param   string|null $arg2
1028   * @param   string|array|null $meta
1029   * @return  string
1030   */
1031  protected function toHtml($element, $arg1 = null, $arg2 = null, $meta = null){
1032
1033    // stores tooltip content for all entries;
1034    // to avoid having duplicate tooltip data in the HTML, we generate them once,
1035    // and use references (the Q index) to pull data when required; 
1036    // this improves performance significantly    
1037    static $tips = array();
1038
1039    switch($element){
1040      case 'sep':
1041        return '<i>' . static::escape($arg1) . '</i>';
1042
1043      case 'text':
1044        $tip  = '';
1045        $arg2 = ($arg2 !== null) ? static::escape($arg2) : static::escape($arg1);
1046
1047        if($arg1 === 'specialString'){
1048          $arg2 = strtr($arg2, array(
1049            "\r" => '<ins>\r</ins>',     // carriage return
1050            "\t" => '<ins>\t</ins>',     // horizontal tab
1051            "\n" => '<ins>\n</ins>',     // linefeed (new line)
1052            "\v" => '<ins>\v</ins>',     // vertical tab
1053            "\e" => '<ins>\e</ins>',     // escape
1054            "\f" => '<ins>\f</ins>',     // form feed
1055            "\0" => '<ins>\0</ins>',
1056          ));
1057        }
1058
1059        // generate tooltip reference (probably the slowest part of the code ;)
1060        if($meta !== null){       
1061          $tipIdx = array_search($meta, $tips, true);
1062
1063          if($tipIdx === false)
1064            $tipIdx = array_push($tips, $meta) - 1;
1065
1066          $tip = ' data-tip="' . $tipIdx . '"';
1067        }  
1068
1069        return ($arg1 !== 'name') ? "<b data-{$arg1}{$tip}>{$arg2}</b>" : "<b{$tip}>{$arg2}</b>";
1070
1071      case 'match':
1072        return "<br><s>{$arg1}</s>{$arg2}";
1073
1074      case 'contain':
1075        return "<b data-{$arg1}>{$arg2}</b>";
1076
1077      case 'link':
1078        return "<a href=\"{$arg2}\" target=\"_blank\">{$arg1}</a>";
1079
1080      case 'group':
1081
1082        if($arg1){
1083          $exp = ($this->expDepth < 0) || (($this->expDepth > 0) && ($this->level <= $this->expDepth)) ? ' data-exp' : '';
1084          $arg1 = "<kbd{$exp}></kbd><div>{$arg1}</div>";          
1085        }
1086
1087        $prefix = ($arg2 !== null) ? '<ins>' . static::escape($arg2) . '</ins>' : '';
1088        return "<i>(</i>{$prefix}{$arg1}<i>)</i>";
1089
1090      case 'section':
1091        $title  = ($arg2 !== null) ? "<h4>{$arg2}</h4>" : '';
1092        $output = '';
1093
1094        foreach($arg1 as $row)
1095          $output .= '<dl><dd>' . implode('</dd><dd>', $row) . '</dd></dl>';
1096
1097        return "{$title}<div>{$output}</div>";
1098
1099      case 'bubbles':
1100        return '<b data-mod>' . implode('', $arg1) . '</b>';
1101
1102      case 'root':
1103        static $counter = 0;
1104
1105        if($arg2 !== null)
1106          $arg2 = "<kbd>{$arg2}</kbd>";
1107
1108        $assets = static::getAssets();
1109        $counter++;
1110
1111        // process tooltips
1112        $tipHtml = '';
1113        foreach($tips as $idx => $meta){
1114
1115          $tip = '';
1116          if(!is_array($meta))
1117            $meta = array('title' => $meta);
1118
1119          $meta += array(
1120            'title'       => '',
1121            'left'        => '',
1122            'description' => '',
1123            'tags'        => array(),
1124            'sub'         => array(),            
1125          );
1126
1127          $meta = static::escape($meta);
1128
1129          if($meta['title'])
1130            $tip = "<i>{$meta['title']}</i>";
1131
1132          if($meta['description'])
1133            $tip .= "<i>{$meta['description']}</i>";
1134
1135          $tip = "<i>{$tip}</i>";
1136
1137          if($meta['left'])
1138            $tip = "<b>{$meta['left']}</b>{$tip}";
1139
1140          $tags = '';
1141          foreach($meta['tags'] as $tag => $values){
1142            foreach($values as $value){
1143              if($tag === 'param'){
1144                $value[0] = "{$value[0]} {$value[1]}";
1145                unset($value[1]);
1146              }
1147
1148              $value  = is_array($value) ? implode('</b><b>', $value) : $value;
1149              $tags  .= "<i><b>@{$tag}</b><b>{$value}</b></i>";
1150            }
1151          }
1152
1153          if($tags)
1154            $tip .= "<u>{$tags}</u>";
1155
1156          $sub = '';
1157          foreach($meta['sub'] as $line)
1158            $sub .= '<i><b>' . implode('</b> <b>', $line) . '</b></i>';
1159
1160          if($sub !== '')
1161            $tip .= "<u>{$sub}</u>";
1162
1163          $tipHtml .= "<q>{$tip}</q>";
1164        }
1165
1166        // empty tip array
1167        $tips = array();
1168
1169        return "<!-- ref#{$counter} --><div>{$assets}<div class=\"ref\">{$arg2}<div>{$arg1}</div>{$tipHtml}</div></div><!-- /ref#{$counter} -->";
1170
1171      default:
1172        return '';
1173
1174    }
1175  }
1176
1177
1178
1179  /**
1180   * Get all parent classes of a class
1181   *
1182   * @param   Reflector $class   Reflection object
1183   * @return  array              Array of ReflectionClass objects (starts with the ancestor, ends with the given class)
1184   */
1185  protected static function getParentClasses(\Reflector $class){
1186
1187    $parents = array($class);
1188    while(($class = $class->getParentClass()) !== false)
1189      $parents[] = $class;
1190   
1191    return array_reverse($parents);
1192  }
1193
1194
1195
1196  /**
1197   * Generate class / function info
1198   *
1199   * @param   Reflector $reflector      Class name or reflection object
1200   * @param   string $single            Skip parent classes
1201   * @param   Reflector|null $context   Object context (for methods)
1202   * @return  string
1203   */
1204  protected function fromReflector(\Reflector $reflector, $single = '', \Reflector $context = null){
1205
1206    // @todo: test this
1207    $hash = var_export(func_get_args(), true);
1208    
1209    // check if we already have this in the cache
1210    if(isset($this->cache[__FUNCTION__][$hash]))
1211      return $this->cache[__FUNCTION__][$hash];
1212
1213    $f     = "to{$this->format}";
1214    $items = array($reflector);
1215
1216    if(($single === '') && ($reflector instanceof \ReflectionClass))
1217      $items = static::getParentClasses($reflector);
1218
1219    foreach($items as &$item){
1220
1221      $name     = ($single !== '') ? $single : $item->getName();
1222      $comments = $item->isInternal() ? array() : static::parseComment($item->getDocComment());
1223      $meta     = array('sub' => array());
1224
1225      if($item->isInternal()){
1226        $extension = $item->getExtension();
1227        $meta['title'] = ($extension instanceof \ReflectionExtension) ? sprintf('Internal - part of %s (%s)', $extension->getName(), $extension->getVersion()) : 'Internal';
1228      
1229      }else{
1230        $comments = static::parseComment($item->getDocComment()); 
1231
1232        if($comments)
1233          $meta += $comments;
1234
1235        $meta['sub'][] = array('Defined in', basename($item->getFileName()) . ':' . $item->getStartLine());        
1236      }
1237
1238      if(($item instanceof \ReflectionFunction) || ($item instanceof \ReflectionMethod)){
1239        if(($context !== null) && ($context->getShortName() !== $item->getDeclaringClass()->getShortName()))
1240          $meta['sub'][] = array('Inherited from', $item->getDeclaringClass()->getShortName());
1241
1242        if($item instanceof \ReflectionMethod){
1243          try{
1244            $proto = $item->getPrototype();
1245            $meta['sub'][] = array('Prototype defined by', $proto->class);
1246          }catch(\Exception $e){}
1247        }  
1248
1249        $item = $this->linkify($this->{$f}('text', 'name', $name, $meta), $item);
1250        continue;
1251      }
1252
1253      $bubbles = array();
1254
1255      // @todo: maybe - list interface methods
1256      if(!($item->isInterface() || ($this->env['is54'] && $item->isTrait()))){
1257
1258        if($item->isAbstract())
1259          $bubbles[] = $this->{$f}('text', 'mod-abstract', 'A', 'Abstract');
1260
1261        if($item->isFinal())
1262          $bubbles[] = $this->{$f}('text', 'mod-final', 'F', 'Final');
1263
1264        // php 5.4+ only
1265        if($this->env['is54'] && $item->isCloneable())
1266          $bubbles[] = $this->{$f}('text', 'mod-cloneable', 'C', 'Cloneable');
1267
1268        if($item->isIterateable())
1269          $bubbles[] = $this->{$f}('text', 'mod-iterateable', 'X', 'Iterateable');            
1270      
1271      }
1272
1273      if($item->isInterface() && $single !== '')
1274       $bubbles[] = $this->{$f}('text', 'mod-interface', 'I', 'Interface');
1275
1276      $bubbles = $bubbles ? $this->{$f}('bubbles', $bubbles) : '';
1277      $name = $this->{$f}('text', 'name', $name, $meta);
1278
1279      if($item->isInterface() && $single === '')
1280        $name .= sprintf(' (%d)', count($item->getMethods()));
1281
1282      $item = $bubbles . $this->linkify($name, $item);
1283    }
1284
1285    // store in cache and return
1286    return $this->cache[__FUNCTION__][$hash] = count($items) > 1 ? implode($this->{$f}('sep', ' :: '), $items) : $items[0];
1287  }
1288
1289
1290
1291  /**
1292   * Generates an URL that points to the documentation page relevant for the requested context
1293   *
1294   * For internal functions and classes, the URI will point to the local PHP manual
1295   * if installed and configured, otherwise to php.net/manual (the english one)
1296   *
1297   * @param   string $node            Text to linkify
1298   * @param   Reflector $reflector    Reflector object (used to determine the URL scheme for internal stuff)
1299   * @param   string|null $constant   Constant name, if this is a request to linkify a constant
1300   * @return  string                  Updated text
1301   */
1302  protected function linkify($node, \Reflector $reflector, $constant = null){
1303
1304    static $docRefRoot = null, $docRefExt = null;
1305
1306    // most people don't have this set
1307    if(!$docRefRoot)
1308      $docRefRoot = ($docRefRoot = rtrim(ini_get('docref_root'), '/')) ? $docRefRoot : 'http://php.net/manual/en';
1309
1310    if(!$docRefExt)
1311      $docRefExt = ($docRefExt = ini_get('docref_ext')) ? $docRefExt : '.php';
1312
1313    $phpNetSchemes = array(
1314      'class'     => $docRefRoot . '/class.%s'    . $docRefExt,
1315      'function'  => $docRefRoot . '/function.%s' . $docRefExt,
1316      'method'    => $docRefRoot . '/%2$s.%1$s'   . $docRefExt,
1317      'property'  => $docRefRoot . '/class.%2$s'  . $docRefExt . '#%2$s.props.%1$s',
1318      'constant'  => $docRefRoot . '/class.%2$s'  . $docRefExt . '#%2$s.constants.%1$s',      
1319    );
1320
1321    $url = '';    
1322    $args = array();
1323
1324    // determine scheme
1325    if($constant !== null){
1326      $type = 'constant';
1327      $args[] = $constant;
1328    
1329    }else{
1330      $type = explode('\\', get_class($reflector)); 
1331      $type = strtolower(ltrim(end($type), 'Reflection'));
1332
1333      if($type === 'object')
1334        $type = 'class';
1335    }
1336
1337    // properties don't have the internal flag;
1338    // also note that many internal classes use some kind of magic as properties (eg. DateTime);
1339    // these will only get linkifed if the declared class is internal one, and not an extension :(
1340    $parent = ($type !== 'property') ? $reflector : $reflector->getDeclaringClass();
1341
1342    // internal function/method/class/property/constant
1343    if($parent->isInternal()){
1344      $args[] = $reflector->name;
1345
1346      if(in_array($type, array('method', 'property'), true))
1347        $args[] = $reflector->getDeclaringClass()->getName();
1348
1349      $args = array_map(function($text){
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        $url = vsprintf($phpNetSchemes[$type], $args);
1359
1360    // custom
1361    }else{
1362      switch(true){      
1363
1364        // WordPress function;
1365        // like pretty much everything else in WordPress, API links are inconsistent as well;
1366        // so we're using queryposts.com as doc source for API
1367        case ($type === 'function') && class_exists('WP') && defined('ABSPATH') && defined('WPINC'):
1368          if(strpos($reflector->getFileName(), realpath(ABSPATH . WPINC)) === 0){
1369            $url = sprintf('http://queryposts.com/function/%s', urlencode(strtolower($reflector->getName())));
1370            break;
1371          }
1372
1373        // @todo: handle more apps
1374      }      
1375
1376    }
1377
1378    if($url !== '')
1379      return $this->{"to{$this->format}"}('link', $node, $url);
1380
1381    return $node;
1382  }
1383
1384
1385
1386  /**
1387   * Evaluates the given variable
1388   *
1389   * @param   mixed $subject    Variable to query
1390   * @param   bool $specialStr  Should this be interpreted as a special string?
1391   * @return  mixed             Result (both HTML and text modes generate strings)
1392   */
1393  protected function evaluate(&$subject, $specialStr = false){
1394
1395    // internal shortcut for to(Format) methods
1396    $f = "to{$this->format}";
1397
1398    switch($type = gettype($subject)){
1399    
1400      // null value
1401      case 'NULL':
1402        return $this->{$f}('text', 'null');
1403
1404      // integer/double/float
1405      case 'integer':
1406      case 'double':
1407        return $this->{$f}('text', $type, $subject, $type);
1408
1409      // boolean
1410      case 'boolean':
1411        $text = $subject ? 'true' : 'false';
1412        return $this->{$f}('text', $text, $text, $type);
1413
1414      // arrays
1415      case 'array':
1416
1417        // empty array?
1418        if(empty($subject))
1419          return $this->{$f}('text', 'array') . $this->{$f}('group');
1420
1421        if(isset($subject[static::MARKER_KEY])){
1422          unset($subject[static::MARKER_KEY]);
1423          return $this->{$f}('text', 'array') . $this->{$f}('group', null, 'recursion');          
1424        }  
1425
1426        $count = count($subject);
1427        $subject[static::MARKER_KEY] = true;
1428
1429        // use splFixedArray() on PHP 5.4+ to save up some memory, because the subject array
1430        // might contain a huge amount of entries.
1431        // (note: lower versions of PHP 5.3 throw a heap corruption error after 10K entries)
1432        // A more efficient way is to build the items as we go as strings,
1433        // by concatenating the info foreach entry, but then we loose the flexibility that the
1434        // entity/group/section methods provide us (exporting data in different formats
1435        // and indenting in text mode would become harder)
1436        $section = $this->env['is54'] ? new \SplFixedArray($count) : array();
1437        $idx     = 0;
1438
1439        $this->level++;
1440        foreach($subject as $key => &$value){
1441          
1442          // ignore our temporary marker
1443          if($key === static::MARKER_KEY)
1444            continue;      
1445
1446          // first recursion level detection;
1447          // this is optional (used to print consistent recursion info)
1448          if(is_array($value)){
1449
1450            // save current value in a temporary variable
1451            $buffer = $value;
1452
1453            // assign new value
1454            $value = ($value !== 1) ? 1 : 2;
1455            
1456            // if they're still equal, then we have a reference            
1457            if($value === $subject){
1458              $value = $buffer;                      
1459              $value[static::MARKER_KEY] = true;
1460              $output = $this->evaluate($value);
1461              $this->level--;
1462              return $output;
1463            }
1464
1465            // restoring original value
1466            $value = $buffer;          
1467          }
1468
1469          $keyInfo = gettype($key);
1470
1471          if($keyInfo === 'string'){
1472            $encoding = $this->env['mbStr'] ? mb_detect_encoding($key) : '';
1473            $keyLen = $encoding && ($encoding !== 'ASCII') ? static::strLen($key) . '; ' . $encoding : static::strLen($key);
1474            $keyInfo = "{$keyInfo}({$keyLen})";
1475          }
1476
1477          $section[$idx++] = array(
1478            $this->{$f}('text', 'key', $key, "Key: {$keyInfo}"),
1479            $this->{$f}('sep', '=>'),
1480            $this->evaluate($value, $specialStr),
1481          );
1482        }
1483
1484        unset($subject[static::MARKER_KEY]);
1485
1486        $output = $this->{$f}('text', 'array') . $this->{$f}('group', $this->{$f}('section', $section), $count);
1487        $this->level--;
1488
1489        return $output;
1490
1491      // resource
1492      case 'resource':
1493        $meta    = array();
1494        $resType = get_resource_type($subject);
1495
1496        // @see: http://php.net/manual/en/resource.php
1497        // need to add more...
1498        switch($resType){
1499
1500          // curl extension resource
1501          case 'curl':
1502            $meta = curl_getinfo($subject);
1503          break;
1504
1505          case 'FTP Buffer':
1506            $meta = array(
1507              'time_out'  => ftp_get_option($subject, FTP_TIMEOUT_SEC),
1508              'auto_seek' => ftp_get_option($subject, FTP_AUTOSEEK),
1509            );
1510
1511          break;
1512
1513          // gd image extension resource
1514          case 'gd':
1515            if(!static::$config['extendedInfo'])
1516              break;
1517
1518            $meta = array(
1519               'size'       => sprintf('%d x %d', imagesx($subject), imagesy($subject)),
1520               'true_color' => imageistruecolor($subject),
1521            );
1522
1523          break;
1524
1525          case 'ldap link':
1526            if(!static::$config['extendedInfo'])
1527              break;
1528
1529            $constants = get_defined_constants();
1530
1531            array_walk($constants, function($value, $key) use(&$constants){
1532              if(strpos($key, 'LDAP_OPT_') !== 0)
1533                unset($constants[$key]);
1534            });
1535
1536            // this seems to fail on my setup :(
1537            unset($constants['LDAP_OPT_NETWORK_TIMEOUT']);
1538
1539            foreach(array_slice($constants, 3) as $key => $value)
1540              if(ldap_get_option($subject, (int)$value, $ret))
1541                $meta[strtolower(substr($key, 9))] = $ret;
1542
1543          break;
1544
1545          // mysql connection (mysql extension is deprecated from php 5.4/5.5)
1546          case 'mysql link':
1547          case 'mysql link persistent':
1548
1549            if(!static::$config['extendedInfo'])
1550              break;
1551
1552            $dbs = array();
1553            $query = @mysql_list_dbs($subject);
1554            while($row = @mysql_fetch_array($query))
1555              $dbs[] = $row['Database'];
1556
1557            $meta = array(
1558              'host'             => ltrim(@mysql_get_host_info ($subject), 'MySQL host info: '),
1559              'server_version'   => @mysql_get_server_info($subject),
1560              'protocol_version' => @mysql_get_proto_info($subject),
1561              'databases'        => $dbs,
1562            );
1563
1564          break;
1565
1566          // mysql result
1567          case 'mysql result':
1568            if(!static::$config['extendedInfo'])
1569              break;
1570
1571            while($row = @mysql_fetch_object($subject))
1572              $meta[] = (array)$row;
1573
1574          break;
1575
1576          // stream resource (fopen, fsockopen, popen, opendir etc)
1577          case 'stream':
1578            $meta = stream_get_meta_data($subject);
1579          break;
1580
1581        }
1582
1583        $section = array();
1584        $this->level++;
1585        foreach($meta as $key => $value){
1586          $section[] = array(
1587            $this->{$f}('text', 'resourceInfo', ucwords(str_replace('_', ' ', $key))),
1588            $this->{$f}('sep', ':'),
1589            $this->evaluate($value),
1590          );
1591        }
1592
1593        $output = $this->{$f}('text', 'resource', strval($subject)) . $this->{$f}('group', $this->{$f}('section', $section), $resType);
1594        $this->level--;
1595        return $output;
1596
1597      // string      
1598      case 'string':
1599     
1600        $length   = static::strLen($subject);       
1601        $encoding = $this->env['mbStr'] ? mb_detect_encoding($subject) : false;      
1602        $info     = $encoding && ($encoding !== 'ASCII') ? $length . '; ' . $encoding : $length;
1603        $add      = '';
1604
1605        if($specialStr)
1606          return $this->{$f}('sep', '"') . $this->{$f}('text', 'specialString', $subject, "string({$info})") . $this->{$f}('sep', '"');
1607
1608        // advanced checks only if there are 3 characteres or more
1609        if(static::$config['extendedInfo'] && $length > 2){
1610
1611          $isNumeric = is_numeric($subject);
1612
1613          // very simple check to determine if the string could match a file path
1614          // @note: this part of the code is very expensive
1615          $isFile = ($length < 2048)
1616            && (max(array_map('strlen', explode('/', str_replace('\\', '/', $subject)))) < 128)
1617            && !preg_match('/[^\w\.\-\/\\\\:]|\..*\.|\.$|:(?!(?<=^[a-zA-Z]:)[\/\\\\])/', $subject);            
1618
1619          if($isFile){
1620            try{
1621              $file  = new \SplFileInfo($subject);
1622              $flags = array();
1623              $perms = $file->getPerms();
1624
1625              if(($perms & 0xC000) === 0xC000)      …

Large files files are truncated, but you can click here to view the full file