├── LICENCE ├── README.md ├── Sage.php ├── composer.json ├── decorators ├── SageDecoratorsInterface.php ├── SageDecoratorsPlain.php └── SageDecoratorsRich.php ├── inc ├── SageHelper.php ├── SageParser.php ├── SageTraceStep.php ├── SageVariableData.php ├── eloquentListener.inc.php └── shorthands.inc.php ├── parsers ├── SageParserInterface.php ├── SageParsersBlacklist.php ├── SageParsersClassName.php ├── SageParsersClassStatics.php ├── SageParsersClosure.php ├── SageParsersColor.php ├── SageParsersDateTime.php ├── SageParsersEloquent.php ├── SageParsersFilePath.php ├── SageParsersJson.php ├── SageParsersMicrotime.php ├── SageParsersObjectIterateable.php ├── SageParsersSmarty.php ├── SageParsersSplFileInfo.php ├── SageParsersSplObjectStorage.php ├── SageParsersTimestamp.php └── SageParsersXml.php └── resources ├── compiled ├── aante-light.css ├── original-light.css ├── original.css ├── sage.js ├── solarized-dark.css └── solarized.css ├── css ├── base.scss └── themes │ ├── aante-light.scss │ ├── original-light.scss │ ├── original.scss │ ├── solarized-dark.scss │ └── solarized.scss └── js └── base.js /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Rokas Šleinius (raveren@gmail.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sage - Insightful PHP debugging assistant ☯ 2 | 3 | At first glance **Sage** is just a drop-in replacement 4 | for **[var_dump()](http://php.net/manual/en/function.var-dump.php)** 5 | and **[debug_backtrace()](http://php.net/manual/en/function.debug-backtrace.php)**. 6 | 7 | However, it's much, *much* more. 8 | 9 | --- 10 | 11 | ![](.github/img/main-screenshot.png) 12 | 13 | Sage is designed to exceed expectations, it intelligently handles everything that you throw at it and displays in the best possible way. 14 | 15 | If you can think of how it can be better, please create an issue! 16 | 17 | For an overview of Sage's outstanding features jump to the [F.A.Q.](#faq) 18 | 19 | ## Installation 20 | 21 | ```bash 22 | composer require php-sage/sage --dev 23 | ``` 24 | 25 | #### Or if you prefer, [download the phar](https://github.com/php-sage/sage/raw/main/sage.phar) and simply 26 | 27 | ```php 28 | 10 years. It is used by a lot of real people in a lot of 80 | real-world scenarios, it is battle tested and has a lot of advanced features no other tool has. 81 | 82 | ## More cool stuff 83 | 84 | Sage displays the passed variable name and supports prefixes to the dump function call. Use it for some common 85 | on-the-fly adjustments to the dump output. 86 | 87 | ```php 88 | ~ss($var); // outputs plain text 89 | $output = @ss($var); // returns output instead of displaying it 90 | ! sage($var); // ignores depth limit for large objects 91 | + sage($var); // auto-expands the view 92 | print sd($var); // saves output into "sage.html" in the current directory 93 | print ! sd($var); // saves output into "sage.html" while also ignoring the output depth limit! 94 | ``` 95 | 96 | Sage tokenizes & introspects the calling code to get all this information. All that can be done with prefixes can also 97 | be achieved with standard, verbose syntax: 98 | 99 | ```php 100 | ~ss($var); 101 | // is equivalent to: 102 | Sage::enabled(Sage::MODE_TEXT_ONLY); 103 | Sage::dump($var); 104 | ``` 105 | 106 | See [Advanced section](#-advanced-tips--tricks) below if you want more tips & tricks. 107 | 108 | ### Verbose versions 109 | 110 | If you want to use Sage as part of permanent code (e.g. part of a test helper/logger/exception reporter etc), you can 111 | use the verbose way to invoke Sage: 112 | 113 | ```php 114 | // instead of sage() 115 | Sage::dump('this looks way less hacky (yeah right:)'); 116 | 117 | // equivalent to sage(1); 118 | Sage::trace(); 119 | 120 | // a real-life test helper: 121 | class TestHelper{ 122 | public function getVarDump(mixed $providedContext): string 123 | { 124 | if (! $providedContext) { 125 | return ''; 126 | } 127 | 128 | $state = Sage::saveState(); 129 | Sage::enabled(Sage::MODE_TEXT_ONLY); 130 | Sage::$aliases[] = __CLASS__ . '::' . __FUNCTION__; 131 | Sage::$returnOutput = true; 132 | Sage::$displayCalledFrom = false; 133 | $debugOutput = Sage::dump($providedContext); 134 | 135 | Sage::saveState($state); // now reset settings to presumed defaults 136 | 137 | return PHP_EOL . $debugOutput; 138 | } 139 | } 140 | ``` 141 | 142 | ---- 143 | 144 | # Customization 145 | 146 | The main goal of Sage is to be **zero-setup**. There are also several customization options for advanced uses. 147 | 148 | ### Where to store the configuration? 149 | 150 | 1. Add this entry to the `autoload.files` configuration in `composer.json`: 151 | 152 | ```js 153 | "autoload": { 154 | "files": [ 155 | "config/sage.php" /* <--------------- create this file with your settings! */ 156 | ] 157 | }, ... 158 | ``` 159 | 160 | 2. Put settings inside of `php.ini`: 161 | 162 | ```ini 163 | ; change sage theme: 164 | sage.theme = solarized-dark 165 | ; always display all dump levels, almost always crashes the browser: 166 | sage.maxLevels = 0 167 | ; set your IDE links 168 | sage.editor = vscode 169 | ; disable Sage unless explicitly enabled 170 | sage.enabled = 0 171 | ``` 172 | 173 | 3. Include the desired settings in your bootstrap process anywhere™. 174 | 175 | ```php 176 | require 'sage.phar'; 177 | Sage::$theme = Sage::THEME_LIGHT; 178 | ``` 179 | 180 | 181 | # All available options 182 | 183 | ```php 184 | Sage::$theme = Sage::THEME_ORIGINAL; 185 | ``` 186 | 187 | Currently available themes are: 188 | 189 | * `Sage::THEME_ORIGINAL` 190 | * `Sage::THEME_LIGHT` 191 | * `Sage::THEME_SOLARIZED` 192 | * `Sage::THEME_SOLARIZED_DARK` 193 | 194 | --- 195 | 196 | ```php 197 | Sage::$editor = ini_get('xdebug.file_link_format'); 198 | ``` 199 | 200 | Make visible source file paths clickable to open your editor. Available options are: 201 | 202 | > `'phpstorm-remote'` - default (requires [IDE Remote Control](https://plugins.jetbrains.com/plugin/19991-ide-remote-control) plugin), `'sublime'`, `'textmate'`, `'emacs'`, `'macvim'`, `'phpstorm'`, `'idea'`, `'vscode'`, `'vscode-insiders'`, `'vscode-remote'`, `'vscode-insiders-remote'`, `'vscodium'`, `'atom'`, `'nova'`, `'netbeans'`, `'xdebug'` 203 | 204 | Or pass a custom string where %file should be replaced with full file path, %line with line number to create a custom 205 | link. Set to null to disable linking. 206 | 207 | --- 208 | 209 | ```php 210 | Sage::$displayCalledFrom = true; 211 | ``` 212 | 213 | Whether to display where Sage was called from 214 | 215 | --- 216 | 217 | ```php 218 | Sage::$maxLevels = 7; 219 | ``` 220 | 221 | Max array/object levels to go deep, set to zero/false to disable 222 | 223 | --- 224 | 225 | ```php 226 | Sage::$expandedByDefault = false; 227 | ``` 228 | 229 | Draw rich output already expanded without having to click 230 | 231 | --- 232 | 233 | ```php 234 | Sage::$cliDetection = true; 235 | ``` 236 | 237 | Enable detection when running in command line and adjust output format accordingly. 238 | 239 | --- 240 | 241 | ```php 242 | Sage::$cliColors = true; 243 | ``` 244 | 245 | In addition to above setting, enable detection when Sage is run in *UNIX* command line. Attempts to add coloring, but if 246 | opened as plain text, the color information is visible as gibberish. 247 | 248 | --- 249 | 250 | ```php 251 | Sage::$charEncodings = [ 'UTF-8', 'Windows-1252', 'euc-jp' ] 252 | ``` 253 | 254 | Possible alternative char encodings in order of probability. 255 | 256 | --- 257 | 258 | ```php 259 | Sage::$returnOutput = false; 260 | ``` 261 | 262 | Sage returns output instead of printing it. 263 | 264 | --- 265 | 266 | ```php 267 | Sage::$aliases; 268 | ``` 269 | 270 | Add new custom Sage wrapper names. Optional, but needed for backtraces, variable name detection and modifiers to work 271 | properly. Accepts array or comma separated string. Use notation `Class::method` for methods. 272 | 273 | --- 274 | 275 | # 🧙 Advanced Tips & Tricks 276 | 277 | > this section is under construction :) 278 | 279 | ```php 280 | // we already saw: 281 | sage($GLOBALS, $_SERVER); 282 | // you can also go shorter for the same result: 283 | s($GLOBALS, $_SERVER); 284 | // or you can go the verbose way, it's all equivalent: 285 | Sage::dump($GLOBALS, $_SERVER); 286 | 287 | 288 | // ss() will display a more basic, javascript-free display (but with colors) 289 | ss($GLOBALS, $_SERVER); 290 | // to recap: s() or sage() - dumps. Add "d" to die afterwards: sd(), saged() 291 | // preppend "s" to simplify output: ss(), ssage(). 292 | // works in combination, too: ssd() and ssagedd() will dump in "simple mode" and die! 293 | 294 | // prepending a tilde will make the output *even more basic* (rich->basic and basic->plain text) 295 | ~d($GLOBALS, $_SERVER); // more on modifiers below 296 | 297 | // show a trace 298 | Sage::trace(); 299 | s(1); // shorthand works too! 300 | s(2); // trace - but just the paths 301 | Sage::dump( debug_backtrace() ); // you can even pass a custom result from debug_trace and it will be recognized 302 | 303 | // dump and die debugging 304 | sd($GLOBALS, $_SERVER); // dd() might be taken by your framework 305 | saged($GLOBALS, $_SERVER); // so this is an equivalent alternative 306 | ssd($GLOBALS, $_SERVER); // available for plain display too! 307 | 308 | // this will disable Sage completely 309 | Sage::enabled(false); 310 | sd('Get off my lawn!'); // no effect 311 | ``` 312 | 313 | * Sage supports keyboard shortcuts! Just press d when viewing output and the rest is self-explanatory, try it 314 | out! (p.s. 315 | vim-style `hjkl` works as well); 316 | * Call `Sage::enabled(Sage::MODE_PLAIN);` to switch to a simpler, js-free output. 317 | * Call `Sage::enabled(Sage::MODE_TEXT_ONLY);` for pure-plain text output which you can save or pass around by first 318 | setting `Sage::$returnOutput = true;` 319 | * Sage can provide a plain-text version of its output and does so automatically when invoked via PHP running in command 320 | line mode. 321 | 322 | ![](.github/img/cli-output.png) 323 | 324 | * Double clicking the `[+]` sign in the output will expand/collapse ALL nodes; **triple clicking** a big block of text 325 | will select it all. 326 | * Clicking the tiny arrow on the **right** of the output will open it in a separate window where you can keep it for 327 | comparison. 328 | * Sage supports themes: 329 | 330 | ![](.github/img/theme-preview.png) 331 | 332 | For customization instructions read the section below. 333 | * If a variable is an object, its classname can be clicked to open the class in your IDE. 334 | * There are several real-time prefix modifiers you can use (combinations possible): 335 | 336 | | Prefix | | Example | 337 | |--------|----------------------------------------------|--------------| 338 | | print | Puts output into current DIR as sage.html | print sage() | 339 | | ! | Dump ignoring depth limits for large objects | ! sage() | 340 | | ~ | Simplifies sage output (rich->html->plain) | ~ sage() | 341 | | - | Clean up any output before dumping | - sage() | 342 | | + | Expand all nodes (in rich view) | + sage() | 343 | | @ | Return output instead of displaying it | @ sage() | 344 | 345 | * Sage also includes a naïve profiler you may find handy. It's for determining relatively which code blocks take longer 346 | than others: 347 | * Sage runs perfectly fine on PHP 5.1 (couldn't find a way to test it on 5.0). 348 | 349 | ```php 350 | Sage::dump( microtime() ); // just pass microtime() - also works if you pass NOTHING: s(); 351 | sleep( 1 ); 352 | Sage::dump( microtime(), 'after sleep(1)' ); 353 | sleep( 2 ); 354 | sd( microtime(), 'final call, after sleep(2)' ); 355 | ``` 356 | 357 | ![](.github/img/profiling.png) 358 | 359 | --- 360 | 361 | ## F.A.Q. 362 | 363 | ### 💬 How is it different or better than [symfony/var-dumper](https://symfony.com/doc/current/components/var_dumper.html)? 364 | 365 | * Visible **Variable name** 366 | * Keyboard shortcuts. Type d and the rest is just self-explanatory (use arrows, space, tab, enter, you'll get 367 | it!). 368 | * **Debug backtraces** with full insight of arguments, callee objects and more. 369 | * Custom display for a lot of recognized types: 370 | ![custom types](.github/img/alternative-view.png) 371 | * Has text-only, plain and rich views, has several visual themes - created by a professional designer. 372 | * A huge number of usability enhancements - like the (clickable) **call trace** in the footer of each output. 373 | * Supports convenience modifiers, for example `@sage($var);` will return instead of outputting, `-sage($var);` 374 | will `ob_clean` all output to be the only thing on page (see advanced section above for more). 375 | * Compatibility! Fully works on **PHP 5.1 - 8.1+**! 376 | * Code is way less complex - to read and contribute to. 377 | * Sage came **first** - developed 378 | since [pre-2012](https://github.com/php-sage/sage/commit/3c49968cb912fb627c6650c4bfd4673bb1b44277). It inspired the 379 | now 380 | ubiquitous [dd](https://github.com/php-sage/sage/commit/fa6c8074ea1870bb5c6a080e94f7130e9a0f2fda#diff-2cdf3c423d47e373c75638c910674ec68c5aa434e11d4074037c91a543d9cb58R549) 381 | shorthand, pioneered a lot of the concepts in the tool niche. 382 | 383 | ### 💬 What are the other dumpers out there 384 | 385 | * [Symfony/var-dumper](https://symfony.com/doc/current/components/var_dumper.html) 386 | * [yii\helpers\VarDumper](https://www.yiiframework.com/doc/api/2.0/yii-helpers-vardumper) 387 | * [Tracy](https://tracy.nette.org/) 388 | * [PHP Debug Bar](https://github.com/maximebf/php-debugbar) 389 | * [Kint](https://kint-php.github.io/kint/) - sage supersedes Kint. 390 | 391 | ### 💬 Why does Sage look so much like Kint? A.K.A. Why does this repo have so few stars? 392 | 393 | Because it is Kint, and I am its author, however the project was 394 | [**forcibly taken over**](https://github.com/kint-php/kint/commit/1ea81f3add81b586756515673f8364f60feb86a3) by a 395 | malicious contributor! 396 | 397 | Instead of fighting windmills I chose to fork and rename the last good version and continue under a new name! 398 | 399 | You can use Sage as a drop-in replacement for Kint. Simple. 400 | 401 | ### 💬 How is `var_dump` - style debugging still relevant when we have Xdebug? 402 | 403 | 1. In practice, Xdebug is quite often very difficult and time-consuming to install and configure. 404 | 2. There's many use-cases where dump&die is just faster to bring up. 405 | 3. There is no way you can visualise a timeline of changed data with XDebug. For example, all values dumped from within 406 | a loop. 407 | 4. And there's more subtle use-cases, eg. if you stepped over something there's no way to go back, but with var-dumping 408 | the values are still there... 409 | 410 | I use xdebug almost daily, by the way. Side by side with Sage. 411 | 412 | ### Known issues 413 | 414 | 1. Since PHP 8.2 variable name detection for *multiline* Sage calls is broken. Simple matter of Backtrace format 415 | changing the reported line, fix is comming. 416 | 417 | --- 418 | 419 | ### Contributing 420 | 421 | #### 🎲 Prerequisites 422 | 423 | * Install [Docker Compose](https://docs.docker.com/compose/install/#install-compose'). 424 | * If you're on **Windows** 10+ you need to use WSL2: 425 | 1. Setup: `wsl --install` 426 | 2. Set Ubuntu as your default wsl shell: `wsl --set-version Ubuntu 2`. 427 | 3. All commands listed below must be run from inside wsl shell: `wsl` 428 | 429 | Do your changes but before committing run 430 | 431 | ```bash 432 | docker compose run php composer build 433 | # OR (see Makefile): 434 | make build 435 | ``` 436 | 437 | To compile resources and build the phar file. 438 | 439 | ## Author 440 | 441 | **Rokas Šleinius** ([Raveren](https://github.com/raveren)) 442 | 443 | Contributors: https://github.com/php-sage/sage/graphs/contributors 444 | 445 | ### License 446 | 447 | Licensed under the MIT License 448 | 449 | --- 450 | 451 | Hope you'll love using Sage as much as I love creating it! 452 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-sage/sage", 3 | "description": "☯ Insightful PHP debugging assistant.", 4 | "keywords": [ 5 | "sage", 6 | "php", 7 | "dumper", 8 | "debug", 9 | "dump", 10 | "var_dump", 11 | "debug_backtrace", 12 | "json_decode" 13 | ], 14 | "type": "library", 15 | "homepage": "https://github.com/php-sage/sage", 16 | "license": "MIT", 17 | "authors": [ 18 | { 19 | "name": "Rokas Šleinius", 20 | "homepage": "https://github.com/raveren" 21 | }, 22 | { 23 | "name": "Contributors", 24 | "homepage": "https://github.com/php-sage/sage/contributors" 25 | } 26 | ], 27 | "require": { 28 | "php": ">=5.1.0" 29 | }, 30 | "require-dev": { 31 | "pestphp/pest": "^1.0", 32 | "seld/phar-utils": "^1.0", 33 | "spatie/pest-plugin-snapshots": "^1.0", 34 | "symfony/finder": "^3.0 || ^4.0 || ^5.0", 35 | "symfony/var-dumper": "^6.1" 36 | }, 37 | "autoload": { 38 | "files": [ 39 | "Sage.php" 40 | ] 41 | }, 42 | "config": { 43 | "sort-packages": true, 44 | "allow-plugins": { 45 | "pestphp/pest-plugin": true 46 | } 47 | }, 48 | "scripts": { 49 | "post-update-cmd": "npm ci", 50 | "post-install-cmd": "@post-update-cmd", 51 | "build": [ 52 | "@post-update-cmd", 53 | "@build:resources", 54 | "@build:php" 55 | ], 56 | "build:php": "php .github/build/build.php", 57 | "build:resources": "npm run build" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /decorators/SageDecoratorsInterface.php: -------------------------------------------------------------------------------- 1 | name ? $varData->name : ''; 29 | $varData->name = null; 30 | 31 | $output .= $this->title($name); 32 | } 33 | 34 | // make each level different-color 35 | self::$levelColors = array_slice(self::$levelColors, 0, $level); 36 | $s = ' '; 37 | $space = ''; 38 | if (Sage::enabled() === Sage::MODE_CLI) { 39 | for ($i = 0; $i < $level; $i++) { 40 | if (! array_key_exists($i, self::$levelColors)) { 41 | self::$levelColors[$i] = rand(1, 231); 42 | } 43 | $color = self::$levelColors[$i]; 44 | $space .= "\x1b[38;5;{$color}m┆\x1b[0m "; 45 | } 46 | } else { 47 | $space = str_repeat($s, $level); 48 | } 49 | 50 | $output .= $space . $this->drawHeader($varData); 51 | 52 | if (isset($varData->extendedValue)) { 53 | $output .= ' ' . ($varData->type === 'array' ? '[' : '(') . PHP_EOL; 54 | 55 | if (is_array($varData->extendedValue)) { 56 | foreach ($varData->extendedValue as $k => $v) { 57 | if (is_string($v)) { 58 | $output .= $space . $s 59 | . $this->colorize($k, 'key', false) . ': ' 60 | . $this->colorize($v, 'value'); 61 | } else { 62 | $output .= $this->decorate($v, $level + 1); 63 | } 64 | } 65 | } elseif (is_string($varData->extendedValue)) { 66 | $output .= $space . $s . $this->colorize($varData->extendedValue, 'value'); 67 | } else { 68 | // throw new RuntimeException(); 69 | // $output .= self::decorate($varData->extendedValue, $level + 1); // it's SageVariableData 70 | } 71 | $output .= $space . ($varData->type === 'array' ? ']' : ')'); 72 | } 73 | 74 | $output .= PHP_EOL; 75 | 76 | return $output; 77 | } 78 | 79 | /** @param SageTraceStep[] $traceData */ 80 | public function decorateTrace(array $traceData, $pathsOnly = false) 81 | { 82 | $lastStepNumber = count($traceData); 83 | $output = $this->title($pathsOnly ? 'QUICK TRACE' : 'TRACE'); 84 | 85 | $blacklistedStepsInARow = 0; 86 | foreach ($traceData as $stepNumber => $step) { 87 | if ( 88 | $stepNumber >= Sage::$minimumTraceStepsToShowFull 89 | && $step->isBlackListed 90 | ) { 91 | $blacklistedStepsInARow++; 92 | continue; 93 | } 94 | 95 | if ($blacklistedStepsInARow) { 96 | if ($blacklistedStepsInARow <= 5) { 97 | for ($j = $blacklistedStepsInARow; $j > 0; $j--) { 98 | $output .= $this->drawTraceStep( 99 | $stepNumber - $j, 100 | $traceData[$stepNumber - $j], 101 | $pathsOnly, 102 | $lastStepNumber 103 | ); 104 | } 105 | } else { 106 | $output .= "...\n[{$blacklistedStepsInARow} steps skipped]\n...\n"; 107 | } 108 | 109 | $blacklistedStepsInARow = 0; 110 | } 111 | $output .= $this->drawTraceStep($stepNumber, $step, $pathsOnly, $lastStepNumber); 112 | } 113 | 114 | if ($blacklistedStepsInARow > 1) { 115 | $output .= "...\n[{$blacklistedStepsInARow} steps skipped]\n"; 116 | } 117 | 118 | return $output; 119 | } 120 | 121 | private function colorize($text, $type, $nlAfter = true) 122 | { 123 | $nl = $nlAfter ? PHP_EOL : ''; 124 | 125 | switch (Sage::enabled()) { 126 | case Sage::MODE_PLAIN: 127 | if (! self::$_enableColors) { 128 | return $text . $nl; 129 | } 130 | 131 | switch ($type) { 132 | case 'key': 133 | $text = "{$text}"; 134 | break; 135 | case 'access': 136 | $text = "{$text}"; 137 | break; 138 | case 'value': 139 | $text = "{$text}"; 140 | break; 141 | case 'type': 142 | $text = "{$text}"; 143 | break; 144 | case 'header': 145 | $text = "

{$text}

"; 146 | break; 147 | } 148 | 149 | return $text . $nl; 150 | case Sage::MODE_CLI: 151 | if (! self::$_enableColors) { 152 | return $text . $nl; 153 | } 154 | 155 | /* 156 | * Black 0;30 Dark Gray 1;30 157 | * Red 0;31 Light Red 1;31 158 | * Green 0;32 Light Green 1;32 159 | * Brown 0;33 Yellow 1;33 160 | * Blue 0;34 Light Blue 1;34 161 | * Purple 0;35 Light Purple 1;35 162 | * Cyan 0;36 Light Cyan 1;36 163 | * Light Gray 0;37 White 1;37 164 | * 165 | * Format: 166 | * \x1b[[light];[color];[font]m 167 | * light: 1/0 168 | * color: 30-37 169 | * font: 1 - bold, 3 - italic, 4 - underline, 7 - invert, 9 - strikethrough 170 | * 171 | * https://misc.flogisoft.com/bash/tip_colors_and_formatting 172 | */ 173 | 174 | $optionsMap = array( 175 | 'key' => "\x1b[32m", 176 | 'access' => "\x1b[3m", 177 | 'header' => "\x1b[38;5;75m", 178 | 'type' => "\x1b[1m", 179 | 'value' => "\x1b[31m", 180 | ); 181 | 182 | return $optionsMap[$type] . $text . "\x1b[0m" . $nl; 183 | case Sage::MODE_TEXT_ONLY: 184 | default: 185 | return $text . $nl; 186 | } 187 | } 188 | 189 | private function title($text) 190 | { 191 | $escaped = SageHelper::esc($text); 192 | $lengthDifference = strlen($escaped) - strlen($text); 193 | 194 | $ret = '┌──────────────────────────────────────────────────────────────────────────────┐' . PHP_EOL; 195 | if ($text) { 196 | $ret .= '│' . str_pad($escaped, 78 + $lengthDifference, ' ', STR_PAD_BOTH) . '│' . PHP_EOL; 197 | } 198 | $ret .= '└──────────────────────────────────────────────────────────────────────────────┘'; 199 | 200 | return $this->colorize($ret, 'header'); 201 | } 202 | 203 | public function wrapStart() 204 | { 205 | if (Sage::enabled() === Sage::MODE_PLAIN) { 206 | return '
';
207 |         }
208 | 
209 |         return '';
210 |     }
211 | 
212 |     public function wrapEnd($callee, $miniTrace, $prevCaller)
213 |     {
214 |         $lastLine     = '════════════════════════════════════════════════════════════════════════════════';
215 |         $lastChar     = Sage::enabled() === Sage::MODE_PLAIN ? '
' : ''; 216 | $traceDisplay = ''; 217 | 218 | if (! Sage::$displayCalledFrom) { 219 | return $this->colorize($lastLine . $lastChar, 'header'); 220 | } 221 | 222 | if (! empty($miniTrace)) { 223 | $traceDisplay = PHP_EOL; 224 | $i = 0; 225 | foreach ($miniTrace as $step) { 226 | $traceDisplay .= ' ' . ($i + 2) . '. '; 227 | $traceDisplay .= SageHelper::ideLink($step['file'], $step['line']); 228 | $traceDisplay .= PHP_EOL; 229 | if ($i++ > 2) { 230 | break; 231 | } 232 | } 233 | $traceDisplay .= ''; 234 | } 235 | 236 | return $this->colorize( 237 | $lastLine . PHP_EOL 238 | . 'Call stack ' . SageHelper::ideLink($callee['file'], $callee['line']) 239 | . $traceDisplay, 240 | 'header' 241 | ) 242 | . $lastChar; 243 | } 244 | 245 | private function drawHeader(SageVariableData $varData) 246 | { 247 | $output = ''; 248 | 249 | if ($varData->access) { 250 | $output .= ' ' . $this->colorize(SageHelper::esc($varData->access), 'access', false); 251 | } 252 | 253 | if ($varData->name !== null && $varData->name !== '') { 254 | $output .= ' ' . $this->colorize(SageHelper::esc($varData->name), 'key', false); 255 | } 256 | 257 | if ($varData->operator) { 258 | $output .= ' ' . $varData->operator; 259 | } 260 | 261 | $type = $varData->type; 262 | if ($varData->size !== null) { 263 | $type .= ' (' . $varData->size . ')'; 264 | } 265 | 266 | $output .= ' ' . $this->colorize($type, 'type', false); 267 | 268 | if ($varData->value !== null && $varData->value !== '') { 269 | $output .= ' ' . $this->colorize($varData->value, 'value', false); 270 | } 271 | 272 | return ltrim($output); 273 | } 274 | 275 | public function init() 276 | { 277 | if (! Sage::$cliColors) { 278 | self::$_enableColors = false; 279 | } elseif (isset($_SERVER['NO_COLOR']) || getenv('NO_COLOR') !== false) { 280 | self::$_enableColors = false; 281 | } elseif (getenv('TERM_PROGRAM') === 'Hyper') { 282 | self::$_enableColors = true; 283 | } elseif (DIRECTORY_SEPARATOR === '\\') { 284 | self::$_enableColors = 285 | function_exists('sapi_windows_vt100_support') 286 | || getenv('ANSICON') !== false 287 | || getenv('ConEmuANSI') === 'ON' 288 | || getenv('TERM') === 'xterm'; 289 | } else { 290 | self::$_enableColors = true; 291 | } 292 | 293 | if (Sage::enabled() !== Sage::MODE_PLAIN) { 294 | return ''; 295 | } 296 | 297 | return '' 298 | . ''; 299 | } 300 | 301 | private function drawTraceStep($stepNumber, $step, $pathsOnly, $lastStepNumber) 302 | { 303 | $output = ''; 304 | 305 | // ASCII art 🎨 306 | $_________________ = '────────────────────────────────────────────────────────────────────────────────'; 307 | $____Arguments____ = ' ┌────────────────────────── Arguments ─────────────────────────────────┐'; 308 | $__Callee_Object__ = ' ┌───────────────────────── Callee Object ──────────────────────────────┐'; 309 | $L________________ = ' └──────────────────────────────────────────────────────────────────────┘'; 310 | $_________________ = $this->colorize($_________________, 'header'); 311 | $____Arguments____ = $this->colorize($____Arguments____, 'header'); 312 | $__Callee_Object__ = $this->colorize($__Callee_Object__, 'header'); 313 | $L________________ = $this->colorize($L________________, 'header'); 314 | 315 | $output .= str_pad($stepNumber++ . ': ', 4, ' '); 316 | $output .= $this->colorize($step->fileLine, 'header'); 317 | 318 | if ($step->functionName) { 319 | $output .= ' ' . $step->functionName; 320 | $output .= PHP_EOL; 321 | } 322 | 323 | if (! $pathsOnly && $step->arguments) { 324 | $output .= $____Arguments____; 325 | 326 | foreach ($step->arguments as $argument) { 327 | $output .= $this->decorate($argument, 2); 328 | } 329 | 330 | $output .= $L________________; 331 | } 332 | 333 | if (! $pathsOnly && $step->object) { 334 | $output .= $__Callee_Object__; 335 | 336 | $output .= $this->decorate($step->object, 2); 337 | 338 | $output .= $L________________; 339 | } 340 | 341 | if ($stepNumber !== $lastStepNumber) { 342 | $output .= $_________________; 343 | } 344 | 345 | return $output; 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /decorators/SageDecoratorsRich.php: -------------------------------------------------------------------------------- 1 | '; 23 | 24 | $allRepresentations = $varData->getAllRepresentations(); 25 | $extendedPresent = ! empty($allRepresentations); 26 | 27 | if ($extendedPresent) { 28 | $class = '_sage-parent'; 29 | if (Sage::$expandedByDefault) { 30 | $class .= ' _sage-show'; 31 | } 32 | $output .= '
'; 33 | } else { 34 | $output .= '
'; 35 | } 36 | 37 | if ($extendedPresent) { 38 | $output .= ''; 39 | } 40 | 41 | $output .= $this->_drawHeader($varData) . $varData->value . "
"; 42 | 43 | if ($extendedPresent) { 44 | $output .= '
'; 45 | } 46 | 47 | if (count($allRepresentations) === 1 && ! empty($varData->extendedValue)) { 48 | $extendedValue = reset($allRepresentations); 49 | $output .= $this->decorateAlternativeView($extendedValue); 50 | } elseif ($extendedPresent) { 51 | $output .= "'; 69 | } 70 | if ($extendedPresent) { 71 | $output .= '
'; 72 | } 73 | 74 | $output .= "\n"; 75 | 76 | return $output; 77 | } 78 | 79 | /** @param SageTraceStep[] $traceData */ 80 | public function decorateTrace(array $traceData, $pathsOnly = false) 81 | { 82 | $output = '
'; 83 | 84 | $blacklistedStepsInARow = 0; 85 | foreach ($traceData as $stepNumber => $step) { 86 | if ( 87 | $stepNumber >= Sage::$minimumTraceStepsToShowFull 88 | && $step->isBlackListed 89 | ) { 90 | $blacklistedStepsInARow++; 91 | continue; 92 | } 93 | 94 | if ($blacklistedStepsInARow) { 95 | if ($blacklistedStepsInARow <= 5) { 96 | for ($j = $blacklistedStepsInARow; $j > 0; $j--) { 97 | $output .= $this->drawTraceStep($stepNumber - $j, $traceData[$stepNumber - $j], $pathsOnly); 98 | } 99 | } else { 100 | $output .= "
[{$blacklistedStepsInARow} steps skipped]
"; 101 | } 102 | 103 | $blacklistedStepsInARow = 0; 104 | } 105 | 106 | $output .= $this->drawTraceStep($stepNumber, $step, $pathsOnly); 107 | } 108 | 109 | if ($blacklistedStepsInARow > 1) { 110 | $output .= "
[{$blacklistedStepsInARow} steps skipped]
"; 111 | } 112 | 113 | $output .= '
'; 114 | 115 | return $output; 116 | } 117 | 118 | private function drawTraceStep($i, $step, $pathsOnly) 119 | { 120 | $isChildless = ! $step->sourceSnippet && ! $step->arguments && ! $step->object; 121 | 122 | $class = ''; 123 | 124 | if ($step->isBlackListed) { 125 | $class .= ' _sage-blacklisted'; 126 | } elseif ($isChildless) { 127 | $class .= ' _sage-childless'; 128 | } else { 129 | $class .= '_sage-parent'; 130 | 131 | if (Sage::$expandedByDefault) { 132 | $class .= ' _sage-show'; 133 | } 134 | } 135 | 136 | $output = $class ? '
' : '
'; 137 | $output .= '' . ($i + 1) . ''; 138 | if (! $isChildless) { 139 | $output .= ''; 140 | } 141 | $output .= '' . $step->fileLine . ' '; 142 | $output .= $step->functionName; 143 | $output .= '
'; 144 | 145 | if ($isChildless) { 146 | return $output; 147 | } 148 | 149 | $output .= '
'; 185 | 186 | return $output; 187 | } 188 | 189 | /** 190 | * called for each dump, opens the html tag 191 | * 192 | * @return string 193 | */ 194 | public function wrapStart() 195 | { 196 | return "
"; 197 | } 198 | 199 | /** 200 | * closes Sage::_wrapStart() started html tags and displays callee information 201 | * 202 | * @param array $callee caller information taken from debug backtrace 203 | * @param array $miniTrace full path to Sage call 204 | * @param array $prevCaller previous caller information taken from debug backtrace 205 | * 206 | * @return string 207 | */ 208 | public function wrapEnd($callee, $miniTrace, $prevCaller) 209 | { 210 | if (! Sage::$displayCalledFrom) { 211 | return '
'; 212 | } 213 | 214 | $callingFunction = ''; 215 | $calleeInfo = ''; 216 | $traceDisplay = ''; 217 | if (isset($prevCaller['class'])) { 218 | $callingFunction = $prevCaller['class']; 219 | } 220 | if (isset($prevCaller['type'])) { 221 | $callingFunction .= $prevCaller['type']; 222 | } 223 | if (isset($prevCaller['function']) 224 | && ! in_array($prevCaller['function'], array('include', 'include_once', 'require', 'require_once')) 225 | ) { 226 | $callingFunction .= $prevCaller['function'] . '()'; 227 | } 228 | $callingFunction and $callingFunction = " [{$callingFunction}]"; 229 | 230 | if (isset($callee['file'])) { 231 | $calleeInfo .= 'Called from ' . SageHelper::ideLink($callee['file'], $callee['line']); 232 | } 233 | 234 | if (! empty($miniTrace)) { 235 | $traceDisplay = '
    '; 236 | foreach ($miniTrace as $step) { 237 | $traceDisplay .= '
  1. ' . SageHelper::ideLink($step['file'], $step['line']); // closing tag not required 238 | if (isset($step['function']) 239 | && ! in_array($step['function'], array('include', 'include_once', 'require', 'require_once')) 240 | ) { 241 | $classString = ' ['; 242 | if (isset($step['class'])) { 243 | $classString .= $step['class']; 244 | } 245 | if (isset($step['type'])) { 246 | $classString .= $step['type']; 247 | } 248 | $classString .= $step['function'] . '()]'; 249 | $traceDisplay .= $classString; 250 | } 251 | } 252 | $traceDisplay .= '
'; 253 | 254 | $calleeInfo = '' . $calleeInfo; 255 | } 256 | 257 | $callingFunction .= ' @ ' . date('Y-m-d H:i:s'); 258 | 259 | return ''; 263 | } 264 | 265 | private function _drawHeader(SageVariableData $varData) 266 | { 267 | $output = ''; 268 | if ($varData->access !== null) { 269 | $output .= "{$varData->access} "; 270 | } 271 | 272 | if ($varData->name !== null && $varData->name !== '') { 273 | $output .= '' . SageHelper::esc($varData->name) . ' '; 274 | } 275 | 276 | if ($varData->operator !== null) { 277 | $output .= $varData->operator . ' '; 278 | } 279 | 280 | if ($varData->type !== null) { 281 | // tyoe output is unescaped as it is set internally and contains links to user class 282 | $output .= "{$varData->type} "; 283 | } 284 | 285 | if ($varData->size !== null) { 286 | $output .= '(' . $varData->size . ') '; 287 | } 288 | 289 | return $output; 290 | } 291 | 292 | /** 293 | * produces css and js required for display. May be called multiple times, will only produce output once per 294 | * pageload or until `-` or `@` modifier is used 295 | * 296 | * @return string 297 | */ 298 | public function init() 299 | { 300 | $baseDir = SAGE_DIR . 'resources/compiled/'; 301 | 302 | if (! is_readable($cssFile = $baseDir . Sage::$theme . '.css')) { 303 | $cssFile = $baseDir . 'original.css'; 304 | } 305 | 306 | return 307 | '' 308 | . '\n"; 309 | } 310 | 311 | private function decorateAlternativeView($alternative) 312 | { 313 | if (empty($alternative)) { 314 | return ''; 315 | } 316 | 317 | $output = ''; 318 | if (is_array($alternative)) { 319 | // we either get a prepared array of SageVariableData or a raw array of anything 320 | $parse = reset($alternative) instanceof SageVariableData 321 | ? $alternative 322 | : SageParser::process($alternative)->extendedValue; // convert into SageVariableData[] 323 | 324 | foreach ($parse as $v) { 325 | $output .= $this->decorate($v); 326 | } 327 | } elseif (is_string($alternative)) { 328 | // the browser does not render leading new line in
329 |             if ($alternative[0] === "\n" || $alternative[0] === "\r") {
330 |                 $alternative = "\n" . $alternative;
331 |             }
332 |             $output .= "
{$alternative}
"; 333 | } 334 | 335 | return $output; 336 | } 337 | 338 | } 339 | -------------------------------------------------------------------------------- /inc/SageHelper.php: -------------------------------------------------------------------------------- 1 | 'subl://open?url=file://%file&line=%line', 14 | 'textmate' => 'txmt://open?url=file://%file&line=%line', 15 | 'emacs' => 'emacs://open?url=file://%file&line=%line', 16 | 'macvim' => 'mvim://open/?url=file://%file&line=%line', 17 | 'phpstorm' => 'phpstorm://open?file=%file&line=%line', 18 | 'phpstorm-remote' => 'http://localhost:63342/api/file/%file:%line', 19 | 'idea' => 'idea://open?file=%file&line=%line', 20 | 'vscode' => 'vscode://file/%file:%line', 21 | 'vscode-insiders' => 'vscode-insiders://file/%file:%line', 22 | 'vscode-remote' => 'vscode://vscode-remote/%file:%line', 23 | 'vscode-insiders-remote' => 'vscode-insiders://vscode-remote/%file:%line', 24 | 'vscodium' => 'vscodium://file/%file:%line', 25 | 'atom' => 'atom://core/open/file?filename=%file&line=%line', 26 | 'nova' => 'nova://core/open/file?filename=%file&line=%line', 27 | 'netbeans' => 'netbeans://open/?f=%file:%line', 28 | 'xdebug' => 'xdebug://%file@%line' 29 | ); 30 | 31 | private static $aliasesRaw; 32 | private static $projectRootDir; 33 | 34 | public static function php53orLater() 35 | { 36 | if (! isset(self::$_php53)) { 37 | self::$_php53 = version_compare(PHP_VERSION, '5.3.0') > 0; 38 | } 39 | 40 | return self::$_php53; 41 | } 42 | 43 | public static function isRichMode() 44 | { 45 | return Sage::enabled() === Sage::MODE_RICH; 46 | } 47 | 48 | public static function isHtmlMode() 49 | { 50 | $enabledMode = Sage::enabled(); 51 | 52 | return $enabledMode === Sage::MODE_RICH || $enabledMode === Sage::MODE_PLAIN; 53 | } 54 | 55 | /** 56 | * generic path display callback, can be configured in the settings; purpose is to show relevant path info and hide 57 | * as much of the path as possible. 58 | * 59 | * @param string $file 60 | * 61 | * @return string 62 | */ 63 | public static function shortenPath($file) 64 | { 65 | $file = str_replace('\\', '/', $file); 66 | 67 | if (self::$projectRootDir && strpos($file, self::$projectRootDir) === 0) { 68 | return substr($file, strlen(self::$projectRootDir)); 69 | } 70 | 71 | return $file; 72 | } 73 | 74 | public static function buildAliases() 75 | { 76 | self::$aliasesRaw = array( 77 | 'methods' => array( 78 | array('sage', 'dump'), 79 | array('sage', 'doDump'), 80 | array('sage', 'trace') 81 | ), 82 | 'functions' => array() 83 | ); 84 | 85 | foreach (Sage::$aliases as $alias) { 86 | $alias = strtolower($alias); 87 | 88 | if (strpos($alias, '::') !== false) { 89 | self::$aliasesRaw['methods'][] = explode('::', $alias); 90 | } else { 91 | self::$aliasesRaw['functions'][] = $alias; 92 | } 93 | } 94 | } 95 | 96 | public static function detectProjectRoot($calledFromFile) 97 | { 98 | // Find common path with Sage dir 99 | self::$projectRootDir = ''; 100 | 101 | $sagePathParts = explode('/', str_replace('\\', '/', SAGE_DIR)); 102 | $filePathParts = explode('/', $calledFromFile); 103 | foreach ($filePathParts as $i => $filePart) { 104 | if (! isset($sagePathParts[$i]) || $sagePathParts[$i] !== $filePart) { 105 | break; 106 | } 107 | 108 | self::$projectRootDir .= $filePart . '/'; 109 | } 110 | } 111 | 112 | /** 113 | * returns whether current trace step belongs to Sage or its wrappers 114 | * 115 | * @param $step 116 | * 117 | * @return bool 118 | */ 119 | public static function stepIsInternal($step) 120 | { 121 | if (isset($step['class'])) { 122 | foreach (self::$aliasesRaw['methods'] as $alias) { 123 | if ($alias[0] === strtolower($step['class']) && $alias[1] === strtolower($step['function'])) { 124 | return true; 125 | } 126 | } 127 | 128 | return false; 129 | } 130 | 131 | return in_array(strtolower($step['function']), self::$aliasesRaw['functions'], true); 132 | } 133 | 134 | public static function isKeyBlacklisted($key) 135 | { 136 | return in_array(preg_replace('/\W/', '', $key), Sage::$keysBlacklist, true); 137 | } 138 | 139 | public static function substr($string, $start, $end, $encoding = null) 140 | { 141 | if (! isset($string)) { 142 | return ''; 143 | } 144 | 145 | if (function_exists('mb_substr')) { 146 | $encoding or $encoding = self::detectEncoding($string); 147 | 148 | return mb_substr($string, $start, $end, $encoding); 149 | } 150 | 151 | return substr($string, $start, $end); 152 | } 153 | 154 | /** 155 | * returns whether the array: 156 | * 1) is numeric and 157 | * 2) in sequence starting from zero 158 | * 159 | * @param array $array 160 | * 161 | * @return bool 162 | */ 163 | public static function isArraySequential(array $array) 164 | { 165 | $keys = array_keys($array); 166 | 167 | return array_keys($keys) === $keys; 168 | } 169 | 170 | public static function detectEncoding($value) 171 | { 172 | if (function_exists('mb_detect_encoding')) { 173 | $mbDetected = mb_detect_encoding($value); 174 | if ($mbDetected === 'ASCII') { 175 | return 'UTF-8'; 176 | } 177 | } 178 | 179 | if (! function_exists('iconv')) { 180 | return ! empty($mbDetected) ? $mbDetected : 'UTF-8'; 181 | } 182 | 183 | $md5 = md5($value); 184 | foreach (Sage::$charEncodings as $encoding) { 185 | // f*#! knows why, //IGNORE and //TRANSLIT still throw notice 186 | if (md5(@iconv($encoding, $encoding, $value)) === $md5) { 187 | return $encoding; 188 | } 189 | } 190 | 191 | return 'UTF-8'; 192 | } 193 | 194 | public static function strlen($string, $encoding = null) 195 | { 196 | if (function_exists('mb_strlen')) { 197 | $encoding or $encoding = self::detectEncoding($string); 198 | 199 | return mb_strlen($string, $encoding); 200 | } 201 | 202 | return strlen($string); 203 | } 204 | 205 | public static function ideLink($file, $line, $linkText = null) 206 | { 207 | $enabledMode = Sage::enabled(); 208 | $file = self::shortenPath($file); 209 | 210 | $fileLine = $file; 211 | // in some cases (like called from inside template) we don't know the $line 212 | // it's then passed here as null, in that case don't display it in the link text, but keep :0 in the 213 | // url so that the IDE protocols don't break. 214 | if ($line) { 215 | $fileLine .= ':' . $line; 216 | } else { 217 | $line = 0; 218 | } 219 | 220 | if (! self::isHtmlMode()) { 221 | return $fileLine; 222 | } 223 | 224 | $linkText = $linkText 225 | ? $linkText 226 | : $fileLine; 227 | $linkText = self::esc($linkText); 228 | 229 | if (! Sage::$editor) { 230 | return $linkText; 231 | } 232 | 233 | $ideLink = str_replace( 234 | array('%file', '%line', Sage::$fileLinkServerPath), 235 | array($file, $line, Sage::$fileLinkLocalPath), 236 | isset(self::$editors[Sage::$editor]) ? self::$editors[Sage::$editor] : Sage::$editor 237 | ); 238 | 239 | if ($enabledMode === Sage::MODE_RICH) { 240 | $class = (strpos($ideLink, 'http://') === 0) ? ' class="_sage-ide-link" ' : ' '; 241 | 242 | return "{$linkText}"; 243 | } 244 | 245 | // MODE_PLAIN 246 | if (strpos($ideLink, 'http://') === 0) { 247 | return <<{$linkText} 249 | HTML; 250 | } 251 | 252 | return "{$linkText}"; 253 | } 254 | 255 | public static function esc($value, $decode = true) 256 | { 257 | $value = self::isHtmlMode() 258 | ? htmlspecialchars($value, ENT_NOQUOTES, 'UTF-8') 259 | : $value; 260 | 261 | if ($decode) { 262 | $value = self::decodeStr($value); 263 | } 264 | 265 | return $value; 266 | } 267 | 268 | /** 269 | * Make all invisible characters visible. HTML-escape if needed. 270 | */ 271 | private static function decodeStr($value) 272 | { 273 | if (is_int($value)) { 274 | return (string)$value; 275 | } 276 | 277 | if ($value === '') { 278 | return ''; 279 | } 280 | 281 | if (self::isHtmlMode()) { 282 | if (htmlspecialchars($value, ENT_NOQUOTES, 'UTF-8') === '') { 283 | return '‹binary data›'; 284 | } 285 | 286 | $controlCharsMap = array( 287 | "\v" => '\v', 288 | "\f" => '\f', 289 | "\033" => '\e', 290 | "\t" => "\t\\t", 291 | "\r\n" => "\\r\\n\n", 292 | "\n" => "\\n\n", 293 | "\r" => "\\r" 294 | ); 295 | $replaceTemplate = '‹0x%d›'; 296 | } else { 297 | $controlCharsMap = array( 298 | "\v" => '\v', 299 | "\f" => '\f', 300 | "\033" => '\e', 301 | ); 302 | $replaceTemplate = '\x%02X'; 303 | } 304 | 305 | $out = ''; 306 | $i = 0; 307 | do { 308 | $character = $value[$i]; 309 | $ord = ord($character); 310 | // escape all invisible characters except \t, \n and \r - ORD 9, 10 and 13 respectively 311 | if ($ord < 32 && $ord !== 9 && $ord !== 10 && $ord !== 13) { 312 | if (isset($controlCharsMap[$character])) { 313 | $out .= $controlCharsMap[$character]; 314 | } else { 315 | $out .= sprintf($replaceTemplate, $ord); 316 | } 317 | } else { 318 | $out .= $character; 319 | } 320 | } while (isset($value[++$i])); 321 | 322 | return $out; 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /inc/SageParser.php: -------------------------------------------------------------------------------- 1 | */ 10 | private static $_objects; 11 | /** @var string */ 12 | private static $_marker; 13 | 14 | private static $parsingAlternative = array(); 15 | 16 | private static $_placeFullStringInValue = false; 17 | 18 | public static function reset() 19 | { 20 | self::$_level = 0; 21 | self::$_objects = self::$_marker = null; 22 | } 23 | 24 | final public static function process(&$variable, $name = null) 25 | { 26 | // save internal data to revert after dumping to properly handle recursions etc 27 | $revert = array( 28 | 'level' => self::$_level, 29 | 'objects' => self::$_objects, 30 | ); 31 | 32 | self::$_level++; 33 | 34 | // set name 35 | $varData = new SageVariableData(); 36 | if (isset($name)) { 37 | $varData->name = $name; 38 | 39 | if (strlen($varData->name) > 60) { 40 | $varData->name = 41 | SageHelper::substr($varData->name, 0, 27) 42 | . '...' 43 | . SageHelper::substr($varData->name, -28, null); 44 | } 45 | } 46 | 47 | // first go through alternative parsers (eg.: json detection) 48 | foreach (Sage::$enabledParsers as $parserClass => $enabled) { 49 | if (! $enabled || array_key_exists($parserClass, self::$parsingAlternative)) { 50 | continue; 51 | } 52 | self::$parsingAlternative[$parserClass] = true; 53 | $parser = new $parserClass(); 54 | $parseResult = $parser->parse($variable, $varData); 55 | 56 | // if var was parsed by "can only be one"-parser - return here 57 | if ($parseResult !== false && $parser->replacesAllOtherParsers()) { 58 | unset(self::$parsingAlternative[$parserClass]); 59 | self::$_level = $revert['level']; 60 | self::$_objects = $revert['objects']; 61 | 62 | return $varData; 63 | } 64 | unset(self::$parsingAlternative[$parserClass]); 65 | } 66 | 67 | // parse the variable based on its type 68 | $varType = gettype($variable); 69 | $varType === 'unknown type' and $varType = 'unknown'; // PHP 5.4 inconsistency 70 | $methodName = '_parse_' . $varType; 71 | if (! method_exists(__CLASS__, $methodName)) { 72 | $varData->type = $varType; // resource (closed) for example 73 | 74 | return $varData; 75 | } 76 | // base type parser returning false means "stop processing further": e.g. recursion 77 | if (self::$methodName($variable, $varData) === false) { 78 | self::$_level--; 79 | 80 | return $varData; 81 | } 82 | 83 | self::$_level = $revert['level']; 84 | self::$_objects = $revert['objects']; 85 | 86 | return $varData; 87 | } 88 | 89 | private static function isDepthLimit() 90 | { 91 | return Sage::$maxLevels && self::$_level >= Sage::$maxLevels; 92 | } 93 | 94 | /** 95 | * @return array|false return ALL keys from all rows if array is tabular, false otherwise 96 | */ 97 | private static function _isArrayTabular(array $variable) 98 | { 99 | if (Sage::enabled() !== Sage::MODE_RICH) { 100 | return false; 101 | } 102 | 103 | $arrayKeys = array(); 104 | $keys = null; 105 | $closeEnough = false; 106 | foreach ($variable as $k => $row) { 107 | if (isset(self::$_marker) && $k === self::$_marker) { 108 | continue; 109 | } 110 | 111 | if (! is_array($row) || empty($row)) { 112 | return false; 113 | } 114 | 115 | foreach ($row as $col) { 116 | if (! empty($col) && ! is_scalar($col)) { 117 | return false; 118 | } // todo add tabular "tolerance" 119 | } 120 | 121 | if (isset($keys) && ! $closeEnough) { 122 | // let's just see if the first two rows have same keys, that's faster and has the 123 | // positive side effect of easily spotting missing keys in later rows 124 | if ($keys !== array_keys($row)) { 125 | return false; 126 | } 127 | 128 | $closeEnough = true; 129 | } else { 130 | $keys = array_keys($row); 131 | } 132 | 133 | $arrayKeys = array_unique(array_merge($arrayKeys, $keys)); 134 | } 135 | 136 | return $arrayKeys; 137 | } 138 | 139 | private static function _decorateCell(SageVariableData $varData) 140 | { 141 | if ($varData->extendedValue !== null) { 142 | return '' . SageDecoratorsRich::decorate($varData) . ''; 143 | } 144 | 145 | $output = 'value !== null) { 148 | $output .= ' title="' . $varData->type; 149 | 150 | if ($varData->size !== null) { 151 | $output .= ' (' . $varData->size . ')'; 152 | } 153 | 154 | $output .= '">' . $varData->value; 155 | } else { 156 | $output .= '>'; 157 | 158 | if ($varData->type !== 'NULL') { 159 | $output .= '' . $varData->type; 160 | 161 | if ($varData->size !== null) { 162 | $output .= '(' . $varData->size . ')'; 163 | } 164 | 165 | $output .= ''; 166 | } else { 167 | $output .= 'NULL'; 168 | } 169 | } 170 | 171 | return $output . ''; 172 | } 173 | 174 | private static $_dealingWithGlobals = false; 175 | 176 | private static function _parse_array(&$variable, SageVariableData $variableData) 177 | { 178 | isset(self::$_marker) or self::$_marker = "\x00" . uniqid(); 179 | 180 | // naturally, $GLOBALS variable is an intertwined recursion nightmare, use black magic 181 | $globalsDetector = false; 182 | if (array_key_exists('GLOBALS', $variable) && is_array($variable['GLOBALS'])) { 183 | $globalsDetector = "\x01" . uniqid(); 184 | 185 | $variable['GLOBALS'][$globalsDetector] = true; 186 | if (isset($variable[$globalsDetector])) { 187 | unset($variable[$globalsDetector]); 188 | self::$_dealingWithGlobals = true; 189 | } else { 190 | unset($variable['GLOBALS'][$globalsDetector]); 191 | $globalsDetector = false; 192 | } 193 | } 194 | 195 | $variableData->type = 'array'; 196 | $variableData->size = count($variable); 197 | 198 | if ($variableData->size === 0) { 199 | return; 200 | } 201 | if (isset($variable[self::$_marker])) { // recursion; todo mayhaps show from where 202 | if (self::$_dealingWithGlobals) { 203 | $variableData->value = '*RECURSION*'; 204 | } else { 205 | unset($variable[self::$_marker]); 206 | $variableData->value = self::$_marker; 207 | } 208 | 209 | return false; 210 | } 211 | if (self::isDepthLimit()) { 212 | $variableData->extendedValue = '*DEPTH TOO GREAT*'; 213 | 214 | return false; 215 | } 216 | 217 | $isSequential = SageHelper::isArraySequential($variable); 218 | $variable[self::$_marker] = true; 219 | 220 | if ($variableData->size > 1 && ($arrayKeys = self::_isArrayTabular($variable)) !== false) { 221 | // tabular array parse 222 | $firstRow = true; 223 | $extendedValue = ''; 224 | 225 | foreach ($variable as $rowIndex => & $row) { 226 | // display strings in their full length 227 | self::$_placeFullStringInValue = true; 228 | 229 | if ($rowIndex === self::$_marker) { 230 | continue; 231 | } 232 | 233 | if (isset($row[self::$_marker])) { 234 | $variableData->value = '*RECURSION*'; 235 | 236 | return false; 237 | } 238 | 239 | $extendedValue .= ''; 240 | if ($isSequential) { 241 | $output = ''; 242 | } else { 243 | $output = self::_decorateCell(self::process($rowIndex)); 244 | } 245 | if ($firstRow) { 246 | $extendedValue .= ''; 247 | } 248 | 249 | // we iterate the known full set of keys from all rows in case some appeared at later rows, 250 | // as we only check the first two to assume 251 | foreach ($arrayKeys as $key) { 252 | if ($firstRow) { 253 | $extendedValue .= ''; 254 | } 255 | 256 | if (SageHelper::isKeyBlacklisted($key)) { 257 | $output .= ''; 258 | continue; 259 | } 260 | 261 | if (! array_key_exists($key, $row)) { 262 | $output .= ''; 263 | continue; 264 | } 265 | 266 | $var = self::process($row[$key]); 267 | 268 | if ($var->value === self::$_marker) { 269 | $variableData->value = '*RECURSION*'; 270 | 271 | return false; 272 | } elseif ($var->value === '*RECURSION*') { 273 | $output .= ''; 274 | } else { 275 | $output .= self::_decorateCell($var); 276 | } 277 | unset($var); 278 | } 279 | 280 | if ($firstRow) { 281 | $extendedValue .= ''; 282 | $firstRow = false; 283 | } 284 | 285 | $extendedValue .= $output . ''; 286 | } 287 | self::$_placeFullStringInValue = false; 288 | 289 | $variableData->extendedValue = $extendedValue . '
' . (((int)$rowIndex) + 1) . ' ' . SageHelper::esc($key) . '*REDACTED**RECURSION*
'; 290 | } else { 291 | $extendedValue = array(); 292 | 293 | foreach ($variable as $key => & $val) { 294 | if ($key === self::$_marker) { 295 | continue; 296 | } 297 | 298 | if (in_array($key, Sage::$keysBlacklist, true)) { 299 | $val = '*REDACTED*'; 300 | } 301 | 302 | $output = self::process($val); 303 | if ($output->value === self::$_marker) { 304 | // recursion occurred on a higher level, thus $variableData is recursion 305 | $variableData->value = '*RECURSION*'; 306 | 307 | return false; 308 | } 309 | if ($isSequential) { 310 | $output->name = null; 311 | } else { 312 | $output->operator = '=>'; 313 | $output->name = is_int($key) 314 | ? $key 315 | : "'" . $key . "'"; 316 | } 317 | $extendedValue[] = $output; 318 | } 319 | $variableData->extendedValue = $extendedValue; 320 | } 321 | 322 | if ($globalsDetector) { 323 | self::$_dealingWithGlobals = false; 324 | } 325 | 326 | unset($variable[self::$_marker]); 327 | } 328 | 329 | private static function _parse_object(&$variable, SageVariableData $variableData) 330 | { 331 | $hash = self::getObjectHash($variable); 332 | 333 | $castedArray = (array)$variable; 334 | $className = get_class($variable); 335 | $variableData->type = $className; 336 | $variableData->size = count($castedArray); 337 | 338 | if (isset(self::$_objects[$hash])) { 339 | $variableData->value = '*RECURSION*'; 340 | 341 | return false; 342 | } 343 | if (self::isDepthLimit()) { 344 | $variableData->extendedValue = '*DEPTH TOO GREAT*'; 345 | 346 | return false; 347 | } 348 | 349 | // ArrayObject (and maybe ArrayIterator, did not try yet) unsurprisingly consist of mainly dark magic. 350 | // What bothers me most, var_dump sees no problem with it, and ArrayObject also uses a custom, 351 | // undocumented serialize function, so you can see the properties in internal functions, but 352 | // can never iterate some of them if the flags are not STD_PROP_LIST. Fun stuff. 353 | if ($variableData->type === 'ArrayObject' || is_subclass_of($variable, 'ArrayObject')) { 354 | $arrayObjectFlags = $variable->getFlags(); 355 | $variable->setFlags(ArrayObject::STD_PROP_LIST); 356 | } 357 | 358 | if (strpos($variableData->type, "@anonymous\0") !== false) { 359 | $variableData->type = 'Anonymous class'; 360 | } 361 | 362 | self::$_objects[$hash] = true; 363 | $variableReflection = new ReflectionObject($variable); 364 | 365 | // add link to definition of userland objects 366 | if (SageHelper::isHtmlMode() && $variableReflection->isUserDefined()) { 367 | $variableData->type = SageHelper::ideLink( 368 | $variableReflection->getFileName(), 369 | $variableReflection->getStartLine(), 370 | $variableData->type 371 | ); 372 | } 373 | $variableData->size = 0; 374 | 375 | $extendedValue = array(); 376 | static $publicProperties = array(); 377 | if (! isset($publicProperties[$className])) { 378 | $reflectionClass = new ReflectionClass($className); 379 | foreach ($reflectionClass->getProperties(ReflectionProperty::IS_PUBLIC) as $prop) { 380 | $publicProperties[$className][$prop->name] = true; 381 | } 382 | } 383 | 384 | // copy the object as an array as it provides more info than Reflection (depends) 385 | foreach ($castedArray as $key => $value) { 386 | /* casting object to array: 387 | * integer properties are inaccessible; 388 | * private variables have the class name prepended to the variable name; 389 | * protected variables have a '*' prepended to the variable name. 390 | * These prepended values have null bytes on either side. 391 | * http://www.php.net/manual/en/language.types.array.php#language.types.array.casting 392 | */ 393 | if (is_string($key) && $key[0] === "\x00") { 394 | $access = $key[1] === '*' ? 'protected' : 'private'; 395 | 396 | // Remove the access level from the variable name 397 | $key = substr($key, strrpos($key, "\x00") + 1); 398 | } else { 399 | $access = 'public'; 400 | 401 | if ($variableData->type !== 'stdClass' && ! isset($publicProperties[$className][$key])) { 402 | $access .= ' (dynamically added)'; 403 | } 404 | } 405 | 406 | if (SageHelper::isKeyBlacklisted($key)) { 407 | $value = '*REDACTED*'; 408 | } 409 | 410 | $output = self::process($value); 411 | $output->name = SageHelper::esc($key); 412 | $output->access = $access; 413 | $output->operator = '->'; 414 | 415 | $extendedValue[$key] = $output; 416 | 417 | $variableData->size++; 418 | } 419 | 420 | if ($variable instanceof __PHP_Incomplete_Class) { 421 | $variableData->extendedValue = $extendedValue; 422 | 423 | return $castedArray; 424 | } 425 | 426 | foreach ($variableReflection->getProperties() as $property) { 427 | if ($property->isStatic()) { 428 | continue; 429 | } 430 | 431 | $name = $property->getName(); 432 | if (isset($extendedValue[$name])) { 433 | if (method_exists($property, 'isReadOnly') && $property->isReadOnly()) { 434 | $extendedValue[$name]->access .= ' readonly'; 435 | } 436 | 437 | continue; 438 | } 439 | 440 | if ($property->isProtected()) { 441 | $property->setAccessible(true); 442 | $access = 'protected'; 443 | } elseif ($property->isPrivate()) { 444 | $property->setAccessible(true); 445 | $access = 'private'; 446 | } else { 447 | $access = 'public'; 448 | } 449 | 450 | if (method_exists($property, 'isInitialized') 451 | && ! $property->isInitialized($variable)) { 452 | $value = null; 453 | $access .= ' [uninitialized]'; 454 | } else { 455 | $value = $property->getValue($variable); 456 | } 457 | 458 | $output = self::process($value, SageHelper::esc($name)); 459 | 460 | $output->access = $access; 461 | $output->operator = '->'; 462 | $extendedValue[] = $output; 463 | $variableData->size++; 464 | } 465 | 466 | if (isset($arrayObjectFlags)) { 467 | $variable->setFlags($arrayObjectFlags); 468 | } 469 | 470 | if (method_exists($variableReflection, 'isEnum') && $variableReflection->isEnum()) { 471 | $variableData->size = 'enum'; 472 | $variableData->value = '"' . $variable->name . '"'; 473 | } 474 | 475 | if ($variableData->size) { 476 | $variableData->extendedValue = $extendedValue; 477 | } 478 | } 479 | 480 | private static function _parse_boolean(&$variable, SageVariableData $variableData) 481 | { 482 | $variableData->type = 'bool'; 483 | $variableData->value = $variable ? 'TRUE' : 'FALSE'; 484 | } 485 | 486 | private static function _parse_double(&$variable, SageVariableData $variableData) 487 | { 488 | $variableData->type = 'float'; 489 | $variableData->value = $variable; 490 | } 491 | 492 | private static function _parse_integer(&$variable, SageVariableData $variableData) 493 | { 494 | $variableData->type = 'integer'; 495 | $variableData->value = $variable; 496 | } 497 | 498 | private static function _parse_null(&$variable, SageVariableData $variableData) 499 | { 500 | $variableData->type = 'NULL'; 501 | } 502 | 503 | private static function _parse_resource(&$variable, SageVariableData $variableData) 504 | { 505 | $resourceType = get_resource_type($variable); 506 | $variableData->type = "resource ({$resourceType})"; 507 | 508 | if ($resourceType === 'stream' && $meta = stream_get_meta_data($variable)) { 509 | if (isset($meta['uri'])) { 510 | $file = $meta['uri']; 511 | 512 | if (function_exists('stream_is_local')) { 513 | // Only exists on PHP >= 5.2.4 514 | if (stream_is_local($file)) { 515 | $file = SageHelper::shortenPath($file); 516 | } 517 | } 518 | 519 | $variableData->value = $file; 520 | } 521 | } 522 | } 523 | 524 | private static function _parse_string(&$variable, SageVariableData $variableData) 525 | { 526 | if (preg_match('//u', $variable)) { 527 | $variableData->type = 'string'; 528 | } else { 529 | $variableData->type .= 'binary string'; 530 | } 531 | 532 | $encoding = SageHelper::detectEncoding($variable); 533 | if ($encoding !== 'UTF-8') { 534 | $variableData->type .= ' ' . $encoding; 535 | } 536 | 537 | $variableData->size = SageHelper::strlen($variable, $encoding); 538 | 539 | if (self::$_placeFullStringInValue) { // in tabular view 540 | $variableData->value = SageHelper::esc($variable); 541 | } elseif (! SageHelper::isRichMode()) { 542 | $variableData->value = '"' . SageHelper::esc($variable) . '"'; 543 | } else { 544 | $decoded = SageHelper::esc($variable); 545 | 546 | // trim inline value if too long 547 | if ($variableData->size > (SageHelper::MAX_STR_LENGTH + 8)) { 548 | $variableData->value = 549 | '"' 550 | . SageHelper::esc( 551 | SageHelper::substr($variable, 0, SageHelper::MAX_STR_LENGTH, $encoding), 552 | false 553 | ) 554 | . '…"'; 555 | } else { 556 | $variableData->value = '"' . SageHelper::esc($variable, false) . '"'; 557 | } 558 | 559 | // detect invisible characters 560 | if ($variable !== preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\x9F]/u', '', $variable)) { 561 | $variableData->extendedValue = SageHelper::esc($variable, false); 562 | $variableData->addTabToView($variable, 'Hidden characters escaped', $decoded); 563 | } elseif ($variableData->size > (SageHelper::MAX_STR_LENGTH + 8) 564 | || $variable !== preg_replace( 565 | '/\s+/', 566 | ' ', 567 | $variable 568 | )) { 569 | $variableData->extendedValue = $decoded; 570 | } 571 | } 572 | } 573 | 574 | private static function _parse_unknown(&$variable, SageVariableData $variableData) 575 | { 576 | $type = gettype($variable); 577 | $variableData->type = 'UNKNOWN' . (! empty($type) ? " ({$type})" : ''); 578 | $variableData->value = var_export($variable, true); 579 | } 580 | 581 | private static function getObjectHash($variable) 582 | { 583 | if (function_exists('spl_object_hash')) { // since PHP 5.2 584 | return spl_object_hash($variable); 585 | } 586 | 587 | ob_start(); 588 | var_dump($variable); 589 | preg_match('[#(\d+)]', ob_get_clean(), $match); 590 | 591 | return $match[1]; 592 | } 593 | 594 | public static function alternativesParse($originalVar, $alternativesArray) 595 | { 596 | $varData = new SageVariableData(); 597 | 598 | // if the alternatives contain references to the object itself it's an ∞ loop 599 | if (is_object($originalVar)) { 600 | self::$_objects[self::getObjectHash($originalVar)] = true; 601 | } elseif (is_array($originalVar)) { 602 | isset(self::$_marker) or self::$_marker = "\x00" . uniqid(); 603 | 604 | $originalVar[self::$_marker] = true; 605 | } 606 | 607 | if (is_array($alternativesArray)) { 608 | self::_parse_array($alternativesArray, $varData); 609 | } else { 610 | self::_parse_object($alternativesArray, $varData); 611 | } 612 | 613 | return $varData->extendedValue; 614 | } 615 | } 616 | -------------------------------------------------------------------------------- /inc/SageTraceStep.php: -------------------------------------------------------------------------------- 1 | fileLine = $this->getFileAndLine($step); 20 | $this->argumentNames = $this->getStepArgumentNames($step); 21 | $this->functionName = $this->getStepFunctionName($step, $this->argumentNames); 22 | 23 | if ($this->isStepBlacklisted($step, $stepNumber)) { 24 | $this->isBlackListed = true; 25 | 26 | return; 27 | } 28 | 29 | // todo it's possible to parse the object name out from the source!!! 30 | $this->object = $this->getObject($step); 31 | $this->sourceSnippet = $this->getSourceSnippet($step); 32 | $this->arguments = $this->getArguments($step, $this->argumentNames); 33 | } 34 | 35 | private function isStepBlacklisted($step, $stepNumber) 36 | { 37 | if (! Sage::$maxLevels) { 38 | return false; 39 | } 40 | 41 | if (! isset($step['file'])) { 42 | return false; 43 | } 44 | 45 | foreach (Sage::$traceBlacklist as $blacklistedPath) { 46 | if (preg_match($blacklistedPath, $step['file'])) { 47 | return true; 48 | } 49 | } 50 | 51 | return false; 52 | } 53 | 54 | private function getFileAndLine($step) 55 | { 56 | if (! isset($step['file'])) { 57 | return 'PHP internal call'; 58 | } 59 | 60 | return SageHelper::ideLink($step['file'], $step['line']); 61 | } 62 | 63 | private function getStepArgumentNames($step) 64 | { 65 | if (empty($step['args']) || empty($step['function'])) { 66 | return array(); 67 | } 68 | 69 | $function = $step['function']; 70 | if (in_array($function, array('include', 'include_once', 'require', 'require_once'))) { 71 | return array(''); 72 | } 73 | 74 | $reflection = null; 75 | 76 | if (isset($step['class'])) { 77 | if (method_exists($step['class'], $function)) { 78 | $reflection = new ReflectionMethod($step['class'], $function); 79 | } 80 | } elseif (function_exists($function)) { 81 | $reflection = new ReflectionFunction($function); 82 | } 83 | 84 | $params = $reflection ? $reflection->getParameters() : null; 85 | 86 | $names = array(); 87 | foreach ($step['args'] as $i => $arg) { 88 | if (isset($params[$i])) { 89 | $names[] = '$' . $params[$i]->name; 90 | } else { 91 | $names[] = '#' . ($i + 1); 92 | } 93 | } 94 | 95 | return $names; 96 | } 97 | 98 | private function getStepFunctionName($step, $functionNames) 99 | { 100 | if (empty($step['function'])) { 101 | return ''; 102 | } 103 | 104 | $function = $step['function']; 105 | if ($function && isset($step['class'])) { 106 | $function = $step['class'] . $step['type'] . $function; 107 | } 108 | 109 | return $function . '(' . implode(', ', $functionNames) . ')'; 110 | } 111 | 112 | private function getObject($step) 113 | { 114 | if (! isset($step['object'])) { 115 | return null; 116 | } 117 | 118 | return SageParser::process($step['object']); 119 | } 120 | 121 | private function getSourceSnippet($step) 122 | { 123 | if ( 124 | empty($step['file']) 125 | || ! isset($step['line']) 126 | || Sage::enabled() !== Sage::MODE_RICH 127 | || ! is_readable($step['file']) 128 | ) { 129 | return null; 130 | } 131 | 132 | // open the file and set the line position 133 | $file = fopen($step['file'], 'r'); 134 | $line = $step['line']; 135 | $readingLine = 0; 136 | 137 | // Set the reading range 138 | $range = array( 139 | 'start' => $line - 7, 140 | 'end' => $line + 7, 141 | ); 142 | 143 | // set the zero-padding amount for line numbers 144 | $format = '% ' . strlen($range['end']) . 'd'; 145 | 146 | $source = ''; 147 | while (($row = fgets($file)) !== false) { 148 | // increment the line number 149 | if (++$readingLine > $range['end']) { 150 | break; 151 | } 152 | 153 | if ($readingLine >= $range['start']) { 154 | $row = SageHelper::esc($row); 155 | 156 | $row = '' . sprintf($format, $readingLine) . ' ' . $row; 157 | 158 | if ($readingLine === (int)$line) { 159 | // apply highlighting to this row 160 | $row = '
' . $row . '
'; 161 | } else { 162 | $row = '
' . $row . '
'; 163 | } 164 | 165 | $source .= $row; 166 | } 167 | } 168 | 169 | fclose($file); 170 | 171 | return $source; 172 | } 173 | 174 | private function getArguments($step, $argumentNames) 175 | { 176 | $result = array(); 177 | foreach ($this->getRawArguments($step) as $k => $variable) { 178 | $name = isset($argumentNames[$k]) ? $argumentNames[$k] : ''; 179 | if (SageHelper::isKeyBlacklisted($name)) { 180 | $variable = '*REDACTED*'; 181 | } 182 | 183 | $parsed = SageParser::process($variable, $argumentNames[$k]); 184 | $parsed->operator = substr($name, 0, 1) === '$' ? '=' : ':'; 185 | $result[] = $parsed; 186 | } 187 | 188 | return $result; 189 | } 190 | 191 | private function getRawArguments($step) 192 | { 193 | if ( 194 | ! empty($step['args']) 195 | && in_array($step['function'], array('include', 'include_once', 'require', 'require_once'), true) 196 | ) { 197 | // sanitize the included file path 198 | return array(SageHelper::shortenPath($step['args'][0])); 199 | } 200 | 201 | return isset($step['args']) ? $step['args'] : array(); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /inc/SageVariableData.php: -------------------------------------------------------------------------------- 1 | alternativeRepresentations[$tabName] = $value; 48 | } 49 | 50 | public function getAllRepresentations() 51 | { 52 | # if alternative displays exist, push extendedValue to their front and display it as one of alternatives 53 | $prepared = array(); 54 | 55 | if (! empty($this->extendedValue)) { 56 | $prepared['Contents'] = $this->extendedValue; 57 | } 58 | if (! empty($this->alternativeRepresentations)) { 59 | $prepared = array_merge($prepared, $this->alternativeRepresentations); 60 | } 61 | 62 | return $prepared; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /inc/eloquentListener.inc.php: -------------------------------------------------------------------------------- 1 | $queryNumber++, 'sql' => $query->sql, 'bindings' => $query->bindings); 10 | Sage::dump($EloquentQuery); 11 | 12 | Sage::saveState($state); 13 | }); 14 | -------------------------------------------------------------------------------- /inc/shorthands.inc.php: -------------------------------------------------------------------------------- 1 | html->plain) | 35 | * | - | Clean up any output before dumping | 36 | * | + | Expand all nodes (in rich view) | 37 | * | @ | Return output instead of displaying it | 38 | * 39 | */ 40 | 41 | if (! function_exists('sage')) { 42 | /** 43 | * Alias of Sage::dump() 44 | * 45 | * @return string|int 46 | * 47 | * @see Sage::dump() 48 | */ 49 | function sage() 50 | { 51 | if (! Sage::enabled()) { 52 | return 5463; 53 | } 54 | 55 | Sage::$aliases[] = __FUNCTION__; 56 | 57 | $params = func_get_args(); 58 | 59 | return call_user_func_array(array('Sage', 'dump'), $params); 60 | } 61 | } 62 | 63 | if (! function_exists('s')) { 64 | /** 65 | * Alias of Sage::dump() 66 | * 67 | * @return string|int 68 | * 69 | * @see Sage::dump() 70 | */ 71 | function s() 72 | { 73 | if (! Sage::enabled()) { 74 | return 5463; 75 | } 76 | 77 | Sage::$aliases[] = __FUNCTION__; 78 | 79 | $params = func_get_args(); 80 | 81 | return call_user_func_array(array('Sage', 'dump'), $params); 82 | } 83 | } 84 | 85 | if (! function_exists('saged')) { 86 | /** 87 | * Alias of Sage::dump(); die; 88 | * 89 | * @return never [!!!] IMPORTANT: execution will halt after call to this function 90 | */ 91 | function saged() 92 | { 93 | if (! Sage::enabled()) { 94 | return 5463; 95 | } 96 | 97 | Sage::$aliases[] = __FUNCTION__; 98 | 99 | $params = func_get_args(); 100 | call_user_func_array(array('Sage', 'dump'), $params); 101 | die; 102 | } 103 | } 104 | 105 | if (! function_exists('sd')) { 106 | /** 107 | * Alias of Sage::dump(); die; 108 | * 109 | * [!!!] IMPORTANT: execution will halt after call to this function 110 | * 111 | * @return string|int @see Sage::dump() 112 | */ 113 | function sd() 114 | { 115 | if (! Sage::enabled()) { 116 | return 5463; 117 | } 118 | 119 | Sage::$aliases[] = __FUNCTION__; 120 | 121 | $params = func_get_args(); 122 | call_user_func_array(array('Sage', 'dump'), $params); 123 | die; 124 | } 125 | } 126 | 127 | if (! function_exists('ssage')) { 128 | /** 129 | * Alias of Sage::dump(), however the output is in plain htmlescaped text and some minor visibility enhancements 130 | * added. If run in CLI mode, output is pure whitespace. 131 | * 132 | * To force rendering mode without autodetecting anything: 133 | * 134 | * Sage::enabled( Sage::MODE_PLAIN ); 135 | * Sage::dump( $variable ); 136 | * 137 | * @return string|int @see Sage::dump() 138 | */ 139 | function ssage() 140 | { 141 | if (! Sage::enabled()) { 142 | return 5463; 143 | } 144 | 145 | $simplify = Sage::$simplifyDisplay; 146 | Sage::$simplifyDisplay = true; 147 | Sage::$aliases[] = __FUNCTION__; 148 | 149 | $params = func_get_args(); 150 | $dump = call_user_func_array(array('Sage', 'dump'), $params); 151 | Sage::$simplifyDisplay = $simplify; 152 | 153 | return $dump; 154 | } 155 | } 156 | 157 | if (! function_exists('ss')) { 158 | /** 159 | * Alias of Sage::dump(), however the output is in plain htmlescaped text and some minor visibility enhancements 160 | * added. If run in CLI mode, output is pure whitespace. 161 | * 162 | * To force rendering mode without autodetecting anything: 163 | * 164 | * Sage::enabled( Sage::MODE_PLAIN ); 165 | * Sage::dump( $variable ); 166 | * 167 | * @return string|int @see Sage::dump() 168 | */ 169 | function ss() 170 | { 171 | if (! Sage::enabled()) { 172 | return 5463; 173 | } 174 | 175 | $simplify = Sage::$simplifyDisplay; 176 | Sage::$simplifyDisplay = true; 177 | Sage::$aliases[] = __FUNCTION__; 178 | 179 | $params = func_get_args(); 180 | $dump = call_user_func_array(array('Sage', 'dump'), $params); 181 | Sage::$simplifyDisplay = $simplify; 182 | 183 | return $dump; 184 | } 185 | } 186 | 187 | if (! function_exists('ssaged')) { 188 | /** 189 | * @return string|int @see Sage::dump 190 | * @return never [!!!] IMPORTANT: execution will halt after call to this function 191 | * @see s() 192 | */ 193 | function ssaged() 194 | { 195 | if (! Sage::enabled()) { 196 | return 5463; 197 | } 198 | 199 | Sage::$simplifyDisplay = true; 200 | Sage::$aliases[] = __FUNCTION__; 201 | $params = func_get_args(); 202 | call_user_func_array(array('Sage', 'dump'), $params); 203 | die; 204 | } 205 | } 206 | 207 | if (! function_exists('ssd')) { 208 | /** 209 | * @return string|int @see Sage::dump 210 | * @return never [!!!] IMPORTANT: execution will halt after call to this function 211 | * @see s() 212 | */ 213 | function ssd() 214 | { 215 | if (! Sage::enabled()) { 216 | return 5463; 217 | } 218 | 219 | Sage::$simplifyDisplay = true; 220 | Sage::$aliases[] = __FUNCTION__; 221 | $params = func_get_args(); 222 | call_user_func_array(array('Sage', 'dump'), $params); 223 | die; 224 | } 225 | } 226 | 227 | if (! function_exists('d')) { 228 | /** 229 | * Alias of Sage::dump() 230 | * 231 | * Same as sage(), here just to allow drop-in replacement for Kint. 232 | * 233 | * @return string|int @see Sage::dump() 234 | */ 235 | function d() 236 | { 237 | if (! Sage::enabled()) { 238 | return 5463; 239 | } 240 | 241 | Sage::$aliases[] = __FUNCTION__; 242 | 243 | $params = func_get_args(); 244 | 245 | return call_user_func_array(array('Sage', 'dump'), $params); 246 | } 247 | } 248 | 249 | if (! function_exists('sagetrace')) { 250 | /** 251 | * Alias of Sage::dump() 252 | * 253 | * Same as sage(), here just to allow drop-in replacement for Kint. 254 | * 255 | * @return string|int @see Sage::dump() 256 | */ 257 | function sagetrace() 258 | { 259 | if (! Sage::enabled()) { 260 | return 5463; 261 | } 262 | 263 | Sage::$aliases[] = __FUNCTION__; 264 | 265 | return Sage::trace(); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /parsers/SageParserInterface.php: -------------------------------------------------------------------------------- 1 | type = get_class($variable) . ' [skipped, dump object in top level to see contents]'; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /parsers/SageParsersClassName.php: -------------------------------------------------------------------------------- 1 | isUserDefined()) { 37 | return false; 38 | } 39 | 40 | if (SageHelper::isRichMode()) { 41 | $varData->addTabToView( 42 | $variable, 43 | 'Existing class', 44 | SageHelper::ideLink( 45 | $reflector->getFileName(), 46 | $reflector->getStartLine(), 47 | $reflector->getShortName() 48 | ) 49 | ); 50 | } else { 51 | if (SageHelper::isHtmlMode()) { 52 | $varData->extendedValue = 53 | array( 54 | 'Existing class' => SageHelper::ideLink( 55 | $reflector->getFileName(), 56 | $reflector->getStartLine(), 57 | $reflector->getShortName() 58 | ) 59 | ); 60 | } else { 61 | $varData->extendedValue = 62 | array('Existing class' => $reflector->getFileName() . ':' . $reflector->getStartLine()); 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /parsers/SageParsersClassStatics.php: -------------------------------------------------------------------------------- 1 | getProperties(ReflectionProperty::IS_STATIC) as $property) { 25 | if ($property->isProtected()) { 26 | $property->setAccessible(true); 27 | $access = 'protected'; 28 | } elseif ($property->isPrivate()) { 29 | $property->setAccessible(true); 30 | $access = 'private'; 31 | } else { 32 | $access = 'public'; 33 | } 34 | 35 | if (method_exists($property, 'isInitialized') && ! $property->isInitialized($variable)) { 36 | $value = null; 37 | $access .= ' [uninitialized]'; 38 | } else { 39 | $value = $property->getValue($variable); 40 | } 41 | 42 | $name = '$' . $property->getName(); 43 | $output = SageParser::process($value, SageHelper::esc($name)); 44 | 45 | $output->access = $access; 46 | $output->operator = '::'; 47 | $statics[] = $output; 48 | } 49 | 50 | if (empty($statics)) { 51 | return false; 52 | } 53 | 54 | $varData->addTabToView( 55 | $variable, 56 | 'Static class properties (' . count($statics) . ')', 57 | $statics 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /parsers/SageParsersClosure.php: -------------------------------------------------------------------------------- 1 | type = 'Closure'; 21 | $reflection = new ReflectionFunction($variable); 22 | 23 | $parameters = array(); 24 | foreach ($reflection->getParameters() as $parameter) { 25 | $parameters = $parameter->name; 26 | } 27 | if (! empty($parameters)) { 28 | $varData->addTabToView($variable, 'Closure Parameters', $parameters); 29 | } 30 | 31 | $uses = array(); 32 | if ($val = $reflection->getStaticVariables()) { 33 | $uses = $val; 34 | } 35 | if (method_exists($reflection, 'getClousureThis') && $val = $reflection->getClosureThis()) { 36 | $uses[] = SageParser::process($val, 'Closure $this'); 37 | } 38 | if (! empty($uses)) { 39 | $varData->addTabToView($variable, 'Closure Parameters', $uses); 40 | } 41 | 42 | if ($reflection->getFileName()) { 43 | $varData->value = SageHelper::ideLink($reflection->getFileName(), $reflection->getStartLine()); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /parsers/SageParsersColor.php: -------------------------------------------------------------------------------- 1 | '#f0f8ff', 10 | 'antiquewhite' => '#faebd7', 11 | 'aqua' => '#00ffff', 12 | 'aquamarine' => '#7fffd4', 13 | 'azure' => '#f0ffff', 14 | 'beige' => '#f5f5dc', 15 | 'bisque' => '#ffe4c4', 16 | 'black' => '#000000', 17 | 'blanchedalmond' => '#ffebcd', 18 | 'blue' => '#0000ff', 19 | 'blueviolet' => '#8a2be2', 20 | 'brown' => '#a52a2a', 21 | 'burlywood' => '#deb887', 22 | 'cadetblue' => '#5f9ea0', 23 | 'chartreuse' => '#7fff00', 24 | 'chocolate' => '#d2691e', 25 | 'coral' => '#ff7f50', 26 | 'cornflowerblue' => '#6495ed', 27 | 'cornsilk' => '#fff8dc', 28 | 'crimson' => '#dc143c', 29 | 'cyan' => '#00ffff', 30 | 'darkblue' => '#00008b', 31 | 'darkcyan' => '#008b8b', 32 | 'darkgoldenrod' => '#b8860b', 33 | 'darkgray' => '#a9a9a9', 34 | 'darkgrey' => '#a9a9a9', 35 | 'darkgreen' => '#006400', 36 | 'darkkhaki' => '#bdb76b', 37 | 'darkmagenta' => '#8b008b', 38 | 'darkolivegreen' => '#556b2f', 39 | 'darkorange' => '#ff8c00', 40 | 'darkorchid' => '#9932cc', 41 | 'darkred' => '#8b0000', 42 | 'darksalmon' => '#e9967a', 43 | 'darkseagreen' => '#8fbc8f', 44 | 'darkslateblue' => '#483d8b', 45 | 'darkslategray' => '#2f4f4f', 46 | 'darkslategrey' => '#2f4f4f', 47 | 'darkturquoise' => '#00ced1', 48 | 'darkviolet' => '#9400d3', 49 | 'deeppink' => '#ff1493', 50 | 'deepskyblue' => '#00bfff', 51 | 'dimgray' => '#696969', 52 | 'dimgrey' => '#696969', 53 | 'dodgerblue' => '#1e90ff', 54 | 'firebrick' => '#b22222', 55 | 'floralwhite' => '#fffaf0', 56 | 'forestgreen' => '#228b22', 57 | 'fuchsia' => '#ff00ff', 58 | 'gainsboro' => '#dcdcdc', 59 | 'ghostwhite' => '#f8f8ff', 60 | 'gold' => '#ffd700', 61 | 'goldenrod' => '#daa520', 62 | 'gray' => '#808080', 63 | 'grey' => '#808080', 64 | 'green' => '#008000', 65 | 'greenyellow' => '#adff2f', 66 | 'honeydew' => '#f0fff0', 67 | 'hotpink' => '#ff69b4', 68 | 'indianred' => '#cd5c5c', 69 | 'indigo' => '#4b0082', 70 | 'ivory' => '#fffff0', 71 | 'khaki' => '#f0e68c', 72 | 'lavender' => '#e6e6fa', 73 | 'lavenderblush' => '#fff0f5', 74 | 'lawngreen' => '#7cfc00', 75 | 'lemonchiffon' => '#fffacd', 76 | 'lightblue' => '#add8e6', 77 | 'lightcoral' => '#f08080', 78 | 'lightcyan' => '#e0ffff', 79 | 'lightgoldenrodyellow' => '#fafad2', 80 | 'lightgray' => '#d3d3d3', 81 | 'lightgrey' => '#d3d3d3', 82 | 'lightgreen' => '#90ee90', 83 | 'lightpink' => '#ffb6c1', 84 | 'lightsalmon' => '#ffa07a', 85 | 'lightseagreen' => '#20b2aa', 86 | 'lightskyblue' => '#87cefa', 87 | 'lightslategray' => '#778899', 88 | 'lightslategrey' => '#778899', 89 | 'lightsteelblue' => '#b0c4de', 90 | 'lightyellow' => '#ffffe0', 91 | 'lime' => '#00ff00', 92 | 'limegreen' => '#32cd32', 93 | 'linen' => '#faf0e6', 94 | 'magenta' => '#ff00ff', 95 | 'maroon' => '#800000', 96 | 'mediumaquamarine' => '#66cdaa', 97 | 'mediumblue' => '#0000cd', 98 | 'mediumorchid' => '#ba55d3', 99 | 'mediumpurple' => '#9370d8', 100 | 'mediumseagreen' => '#3cb371', 101 | 'mediumslateblue' => '#7b68ee', 102 | 'mediumspringgreen' => '#00fa9a', 103 | 'mediumturquoise' => '#48d1cc', 104 | 'mediumvioletred' => '#c71585', 105 | 'midnightblue' => '#191970', 106 | 'mintcream' => '#f5fffa', 107 | 'mistyrose' => '#ffe4e1', 108 | 'moccasin' => '#ffe4b5', 109 | 'navajowhite' => '#ffdead', 110 | 'navy' => '#000080', 111 | 'oldlace' => '#fdf5e6', 112 | 'olive' => '#808000', 113 | 'olivedrab' => '#6b8e23', 114 | 'orange' => '#ffa500', 115 | 'orangered' => '#ff4500', 116 | 'orchid' => '#da70d6', 117 | 'palegoldenrod' => '#eee8aa', 118 | 'palegreen' => '#98fb98', 119 | 'paleturquoise' => '#afeeee', 120 | 'palevioletred' => '#d87093', 121 | 'papayawhip' => '#ffefd5', 122 | 'peachpuff' => '#ffdab9', 123 | 'peru' => '#cd853f', 124 | 'pink' => '#ffc0cb', 125 | 'plum' => '#dda0dd', 126 | 'powderblue' => '#b0e0e6', 127 | 'purple' => '#800080', 128 | 'red' => '#ff0000', 129 | 'rosybrown' => '#bc8f8f', 130 | 'royalblue' => '#4169e1', 131 | 'saddlebrown' => '#8b4513', 132 | 'salmon' => '#fa8072', 133 | 'sandybrown' => '#f4a460', 134 | 'seagreen' => '#2e8b57', 135 | 'seashell' => '#fff5ee', 136 | 'sienna' => '#a0522d', 137 | 'silver' => '#c0c0c0', 138 | 'skyblue' => '#87ceeb', 139 | 'slateblue' => '#6a5acd', 140 | 'slategray' => '#708090', 141 | 'slategrey' => '#708090', 142 | 'snow' => '#fffafa', 143 | 'springgreen' => '#00ff7f', 144 | 'steelblue' => '#4682b4', 145 | 'tan' => '#d2b48c', 146 | 'teal' => '#008080', 147 | 'thistle' => '#d8bfd8', 148 | 'tomato' => '#ff6347', 149 | 'turquoise' => '#40e0d0', 150 | 'violet' => '#ee82ee', 151 | 'wheat' => '#f5deb3', 152 | 'white' => '#ffffff', 153 | 'whitesmoke' => '#f5f5f5', 154 | 'yellow' => '#ffff00', 155 | 'yellowgreen' => '#9acd32' 156 | ); 157 | 158 | public function replacesAllOtherParsers() 159 | { 160 | return false; 161 | } 162 | 163 | public function parse(&$variable, $varData) 164 | { 165 | if (! $this->_fits($variable)) { 166 | return false; 167 | } 168 | 169 | // todo after we migrate from less: 170 | // $originalVarData->name .= "
{$variable}
"; 171 | 172 | $variants = $this->_convert($variable); 173 | $value = <<{$variable}hex: {$variants['hex']} 175 | rgb: {$variants['rgb']} 176 | hsl: {$variants['hsl']} 177 | HTML; 178 | if (array_key_exists('name', $variants)) { 179 | $value .= PHP_EOL . "css: {$variants['name']}"; 180 | } 181 | 182 | $varData->addTabToView($variable, 'CSS color', $value); 183 | } 184 | 185 | private function _fits($variable) 186 | { 187 | if (! SageHelper::isRichMode()) { 188 | return false; 189 | } 190 | 191 | if (! is_string($variable)) { 192 | return false; 193 | } 194 | 195 | if (strlen($variable) > 32) { 196 | return false; 197 | } 198 | 199 | $var = strtolower(trim($variable)); 200 | 201 | return isset(self::$colorNames[$var]) 202 | || preg_match( 203 | '/^(?:#[0-9A-Fa-f]{3}|#[0-9A-Fa-f]{6}|(?:rgb|hsl)a?\s*\((?:\s*[0-9.%]+\s*,?){3,4}\))$/', 204 | $var 205 | ); 206 | } 207 | 208 | private function _convert($color) 209 | { 210 | $color = strtolower($color); 211 | $decimalColors = array(); 212 | $variants = array( 213 | 'hex' => null, 214 | 'rgb' => null, 215 | 'name' => null, 216 | 'hsl' => null, 217 | ); 218 | 219 | if (isset(self::$colorNames[$color])) { 220 | $variants['name'] = $color; 221 | $color = self::$colorNames[$color]; 222 | } 223 | 224 | if ($color[0] === '#') { 225 | $variants['hex'] = $color; 226 | $color = substr($color, 1); 227 | if (strlen($color) === 6) { 228 | $colors = str_split($color, 2); 229 | } else { 230 | $colors = array( 231 | $color[0] . $color[0], 232 | $color[1] . $color[1], 233 | $color[2] . $color[2], 234 | ); 235 | } 236 | 237 | $decimalColors = array_map('hexdec', $colors); 238 | } elseif (substr($color, 0, 3) === 'rgb') { 239 | $variants['rgb'] = $color; 240 | preg_match_all('#([0-9.%]+)#', $color, $matches); 241 | $decimalColors = $matches[1]; 242 | foreach ($decimalColors as &$color) { 243 | if (strpos($color, '%') !== false) { 244 | $color = str_replace('%', '', $color) * 2.55; 245 | } 246 | } 247 | } elseif (substr($color, 0, 3) === 'hsl') { 248 | $variants['hsl'] = $color; 249 | preg_match_all('#([0-9.%]+)#', $color, $matches); 250 | 251 | $colors = $matches[1]; 252 | $colors[0] /= 360; 253 | $colors[1] = str_replace('%', '', $colors[1]) / 100; 254 | $colors[2] = str_replace('%', '', $colors[2]) / 100; 255 | 256 | $decimalColors = $this->_HSLtoRGB($colors); 257 | if (isset($colors[3])) { 258 | $decimalColors[] = $colors[3]; 259 | } 260 | } 261 | 262 | if (isset($decimalColors[3])) { 263 | $alpha = $decimalColors[3]; 264 | unset($decimalColors[3]); 265 | } else { 266 | $alpha = null; 267 | } 268 | foreach ($variants as $type => &$variant) { 269 | if (isset($variant)) { 270 | continue; 271 | } 272 | 273 | switch ($type) { 274 | case 'hex': 275 | $variant = '#'; 276 | foreach ($decimalColors as &$color) { 277 | $variant .= str_pad(dechex($color), 2, '0', STR_PAD_LEFT); 278 | } 279 | $variant .= isset($alpha) ? ' (alpha omitted)' : ''; 280 | break; 281 | case 'rgb': 282 | $rgb = $decimalColors; 283 | if (isset($alpha)) { 284 | $rgb[] = $alpha; 285 | $a = 'a'; 286 | } else { 287 | $a = ''; 288 | } 289 | $variant = "rgb{$a}( " . implode(', ', $rgb) . ' )'; 290 | break; 291 | case 'hsl': 292 | $rgb = $this->_RGBtoHSL($decimalColors); 293 | if ($rgb === null) { 294 | unset($variants[$type]); 295 | break; 296 | } 297 | if (isset($alpha)) { 298 | $rgb[] = $alpha; 299 | $a = 'a'; 300 | } else { 301 | $a = ''; 302 | } 303 | 304 | $variant = "hsl{$a}( " . implode(', ', $rgb) . ' )'; 305 | break; 306 | case 'name': 307 | // [!] name in initial variants array must go after hex 308 | if (($key = array_search($variants['hex'], self::$colorNames, true)) !== false) { 309 | $variant = $key; 310 | } else { 311 | unset($variants[$type]); 312 | } 313 | break; 314 | } 315 | } 316 | 317 | return $variants; 318 | } 319 | 320 | private function _HSLtoRGB(array $hsl) 321 | { 322 | list($h, $s, $l) = $hsl; 323 | $m2 = ($l <= 0.5) ? $l * ($s + 1) : $l + $s - $l * $s; 324 | $m1 = $l * 2 - $m2; 325 | 326 | return array( 327 | round($this->_hue2rgb($m1, $m2, $h + 0.33333) * 255), 328 | round($this->_hue2rgb($m1, $m2, $h) * 255), 329 | round($this->_hue2rgb($m1, $m2, $h - 0.33333) * 255), 330 | ); 331 | } 332 | 333 | /** 334 | * Helper function for _color_hsl2rgb(). 335 | */ 336 | private function _hue2rgb($m1, $m2, $h) 337 | { 338 | $h = ($h < 0) ? $h + 1 : (($h > 1) ? $h - 1 : $h); 339 | if ($h * 6 < 1) { 340 | return $m1 + ($m2 - $m1) * $h * 6; 341 | } 342 | if ($h * 2 < 1) { 343 | return $m2; 344 | } 345 | if ($h * 3 < 2) { 346 | return $m1 + ($m2 - $m1) * (0.66666 - $h) * 6; 347 | } 348 | 349 | return $m1; 350 | } 351 | 352 | private function _RGBtoHSL(array $rgb) 353 | { 354 | list($clrR, $clrG, $clrB) = $rgb; 355 | 356 | $clrMin = min($clrR, $clrG, $clrB); 357 | $clrMax = max($clrR, $clrG, $clrB); 358 | $deltaMax = $clrMax - $clrMin; 359 | 360 | $L = ($clrMax + $clrMin) / 510; 361 | 362 | if (0 == $deltaMax) { 363 | $H = 0; 364 | $S = 0; 365 | } else { 366 | if (0.5 > $L) { 367 | $S = $deltaMax / ($clrMax + $clrMin); 368 | } else { 369 | $S = $deltaMax / (510 - $clrMax - $clrMin); 370 | } 371 | 372 | if ($clrMax == $clrR) { 373 | $H = ($clrG - $clrB) / (6.0 * $deltaMax); 374 | } elseif ($clrMax == $clrG) { 375 | $H = 1 / 3 + ($clrB - $clrR) / (6.0 * $deltaMax); 376 | } else { 377 | $H = 2 / 3 + ($clrR - $clrG) / (6.0 * $deltaMax); 378 | } 379 | 380 | if (0 > $H) { 381 | $H += 1; 382 | } 383 | if (1 < $H) { 384 | $H -= 1; 385 | } 386 | } 387 | 388 | return array( 389 | round($H * 360), 390 | round($S * 100) . '%', 391 | round($L * 100) . '%' 392 | ); 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /parsers/SageParsersDateTime.php: -------------------------------------------------------------------------------- 1 | format('u'); 22 | if (rtrim($ms, '0')) { 23 | $format .= '.' . $ms; 24 | } else { 25 | $format .= '.0'; 26 | } 27 | 28 | if ($variable->getTimezone()->getLocation()) { 29 | $format .= ' e'; 30 | } 31 | $format .= ' (P)'; 32 | 33 | $varData->value = $variable->format($format); 34 | $varData->type = get_class($variable); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /parsers/SageParsersEloquent.php: -------------------------------------------------------------------------------- 1 | getProperty('attributes'); 22 | $attrReflecion->setAccessible(true); 23 | $attributes = $attrReflecion->getValue($variable); 24 | 25 | $reference = '`' . $variable->getConnection()->getDatabaseName() . '`.`' . $variable->getTable() . '`'; 26 | 27 | $varData->size = count($attributes); 28 | if (SageHelper::isRichMode()) { 29 | $varData->type = $reflection->getName(); 30 | $varData->addTabToView($variable, 'data from ' . $reference, $attributes); 31 | } else { 32 | $varData->type = $reflection->getName() . '; ' . $reference . ' row data:'; 33 | $varData->extendedValue = SageParser::alternativesParse($variable, $attributes); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /parsers/SageParsersFilePath.php: -------------------------------------------------------------------------------- 1 | 2048 19 | || $strlen < 3 20 | || ! preg_match('#[\\\\/]#', $variable) 21 | || preg_match('/[?<>"*|]/', $variable) 22 | || ! @is_readable($variable) // PHP and its random warnings 23 | ) { 24 | return false; 25 | } 26 | 27 | return $this->run($variable, $varData, new SplFileInfo($variable)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /parsers/SageParsersJson.php: -------------------------------------------------------------------------------- 1 | addTabToView($variable, 'Json', $val); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /parsers/SageParsersMicrotime.php: -------------------------------------------------------------------------------- 1 | 0) { 34 | $lap = $time - end(self::$times); 35 | self::$laps[] = $lap; 36 | 37 | $sinceLast = round($lap, 4) . 's.'; 38 | if ($numberOfCalls > 1) { 39 | $sinceStart = round($time - self::$times[0], 4) . 's.'; 40 | $averageDuration = round(array_sum(self::$laps) / $numberOfCalls, 4) . 's.'; 41 | } else { 42 | $sinceStart = null; 43 | $averageDuration = null; 44 | } 45 | 46 | if (SageHelper::isRichMode()) { 47 | $tabContents = "SINCE LAST SUCH CALL: " . round($lap, 4) . 's.'; 48 | if ($numberOfCalls > 1) { 49 | $tabContents .= "\nSINCE START: {$sinceStart}"; 50 | $tabContents .= "\nAVERAGE DURATION: {$averageDuration}"; 51 | } 52 | $tabContents .= "\nPHP MEMORY USAGE: {$memoryUsage}"; 53 | 54 | $varData->addTabToView($variable, 'Benchmark', $tabContents); 55 | } else { 56 | $varData->extendedValue = array( 57 | 'Since last such call' => $sinceLast 58 | ); 59 | 60 | if ($sinceStart !== null) { 61 | $varData->extendedValue['Since start'] = $sinceStart; 62 | $varData->extendedValue['Average duration'] = $averageDuration; 63 | } 64 | 65 | $varData->extendedValue['Memory usage'] = $memoryUsage; 66 | } 67 | } else { 68 | $varData->extendedValue = array( 69 | 'Time (from microtime)' => @date('Y-m-d H:i:s', (int)$sec) . substr($usec, 1), 70 | 'PHP MEMORY USAGE' => $memoryUsage 71 | ); 72 | } 73 | 74 | self::$times[] = $time; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /parsers/SageParsersObjectIterateable.php: -------------------------------------------------------------------------------- 1 | addTabToView($variable, "Iterator contents ({$size})", $arrayCopy); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /parsers/SageParsersSmarty.php: -------------------------------------------------------------------------------- 1 | name = 'Smarty v' . Smarty::SMARTY_VERSION; 22 | 23 | $assigned = $globalAssigns = array(); 24 | foreach ($variable->tpl_vars as $name => $var) { 25 | $assigned[$name] = $var->value; 26 | } 27 | foreach (Smarty::$global_tpl_vars as $name => $var) { 28 | if ($name === 'SCRIPT_NAME') { 29 | continue; 30 | } 31 | 32 | $globalAssigns[$name] = $var->value; 33 | } 34 | 35 | $varData->addTabToView($variable, 'Assigned to view', $assigned); 36 | $varData->addTabToView($variable, 'Assigned globally', $globalAssigns); 37 | $varData->addTabToView($variable, 'Configuration', array( 38 | 'Compiled files stored in' => isset($variable->compile_dir) 39 | ? $variable->compile_dir 40 | : $variable->getCompileDir(), 41 | ) 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /parsers/SageParsersSplFileInfo.php: -------------------------------------------------------------------------------- 1 | run($variable, $varData, $variable); 23 | } 24 | 25 | /** 26 | * @param mixed $variable 27 | * @param SageVariableData $varData 28 | * @param SplFileInfo $fileInfo 29 | * 30 | * @return bool 31 | */ 32 | protected function run(&$variable, $varData, $fileInfo) 33 | { 34 | $varData->value = '"' . SageHelper::esc($fileInfo->getPathname()) . '"'; 35 | $varData->type = get_class($fileInfo); 36 | 37 | if (! $fileInfo->getPathname() || ! $fileInfo->getRealPath()) { 38 | $varData->size = 'invalid path'; 39 | 40 | return true; 41 | } 42 | 43 | try { 44 | $flags = array(); 45 | $perms = $fileInfo->getPerms(); 46 | 47 | if (($perms & 0xC000) === 0xC000) { 48 | $type = 'File socket'; 49 | $flags[] = 's'; 50 | } elseif (($perms & 0xA000) === 0xA000) { 51 | $type = 'File symlink'; 52 | $flags[] = 'l'; 53 | } elseif (($perms & 0x8000) === 0x8000) { 54 | $type = 'File'; 55 | $flags[] = '-'; 56 | } elseif (($perms & 0x6000) === 0x6000) { 57 | $type = 'Block special file'; 58 | $flags[] = 'b'; 59 | } elseif (($perms & 0x4000) === 0x4000) { 60 | $type = 'Directory'; 61 | $flags[] = 'd'; 62 | } elseif (($perms & 0x2000) === 0x2000) { 63 | $type = 'Character special file'; 64 | $flags[] = 'c'; 65 | } elseif (($perms & 0x1000) === 0x1000) { 66 | $type = 'FIFO pipe file'; 67 | $flags[] = 'p'; 68 | } else { 69 | $type = 'Unknown file'; 70 | $flags[] = 'u'; 71 | } 72 | 73 | // owner 74 | $flags[] = (($perms & 0x0100) ? 'r' : '-'); 75 | $flags[] = (($perms & 0x0080) ? 'w' : '-'); 76 | $flags[] = (($perms & 0x0040) ? (($perms & 0x0800) ? 's' : 'x') : (($perms & 0x0800) ? 'S' : '-')); 77 | 78 | // group 79 | $flags[] = (($perms & 0x0020) ? 'r' : '-'); 80 | $flags[] = (($perms & 0x0010) ? 'w' : '-'); 81 | $flags[] = (($perms & 0x0008) ? (($perms & 0x0400) ? 's' : 'x') : (($perms & 0x0400) ? 'S' : '-')); 82 | 83 | // world 84 | $flags[] = (($perms & 0x0004) ? 'r' : '-'); 85 | $flags[] = (($perms & 0x0002) ? 'w' : '-'); 86 | $flags[] = (($perms & 0x0001) ? (($perms & 0x0200) ? 't' : 'x') : (($perms & 0x0200) ? 'T' : '-')); 87 | 88 | $varData->type = get_class($fileInfo); 89 | 90 | if ($type === 'Directory') { 91 | $name = 'Existing Directory'; 92 | $size = iterator_count( 93 | new FilesystemIterator($fileInfo->getRealPath(), FilesystemIterator::SKIP_DOTS) 94 | ) . ' item(s)'; 95 | } else { 96 | $name = "Existing {$type}"; 97 | $size = $this->humanFilesize($fileInfo->getSize()); 98 | } 99 | 100 | $extra = array(); 101 | 102 | if ($fileInfo->getRealPath() !== $fileInfo->getPathname()) { 103 | $extra['realPath'] = $fileInfo->getRealPath(); 104 | } 105 | 106 | if (SageHelper::isRichMode()) { 107 | $extra['flags'] = implode($flags); 108 | 109 | if ($fileInfo->getGroup() || $fileInfo->getOwner()) { 110 | $extra['group&owner'] = $fileInfo->getGroup() . ':' . $fileInfo->getOwner(); 111 | } 112 | 113 | $extra['created'] = date('Y-m-d H:i:s', $fileInfo->getCTime()); 114 | $extra['modified'] = date('Y-m-d H:i:s', $fileInfo->getMTime()); 115 | $extra['accessed'] = date('Y-m-d H:i:s', $fileInfo->getATime()); 116 | 117 | if ($fileInfo->isLink()) { 118 | $extra['link'] = 'true'; 119 | $extra['linkTarget'] = $fileInfo->getLinkTarget(); 120 | } 121 | 122 | $varData->addTabToView($variable, $name . " [{$size}]", $extra); 123 | } else { 124 | if ($type === 'Directory') { 125 | $extended = array('Existing Directory' => $fileInfo->getFilename()); 126 | } else { 127 | $extended = array("Existing {$type}" => $this->humanFilesize($fileInfo->getSize())); 128 | } 129 | 130 | $varData->extendedValue = array($name => $size) + $extra; 131 | } 132 | } catch (Exception $e) { 133 | return false; 134 | } 135 | 136 | return true; 137 | } 138 | 139 | private function humanFilesize($bytes) 140 | { 141 | $sizeInBytes = $bytes; 142 | if ($bytes < 10240) { 143 | return "{$bytes} bytes"; 144 | } 145 | 146 | $units = array('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'); 147 | $precisionByUnit = array(0, 1, 1, 2, 2, 3, 3, 4, 4); 148 | for ($order = 0; ($bytes / 1024) >= 0.9 && $order < count($units); $order++) { 149 | $bytes /= 1024; 150 | } 151 | 152 | return $sizeInBytes . ' bytes (' . round($bytes, $precisionByUnit[$order]) . $units[$order] . ')'; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /parsers/SageParsersSplObjectStorage.php: -------------------------------------------------------------------------------- 1 | count(); 20 | if ($count === 0) { 21 | return false; 22 | } 23 | 24 | $variable->rewind(); 25 | $arrayCopy = array(); 26 | while ($variable->valid()) { 27 | $arrayCopy[] = $variable->current(); 28 | $variable->next(); 29 | } 30 | 31 | $varData->addTabToView($variable, "Storage contents ({$count})", $arrayCopy); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /parsers/SageParsersTimestamp.php: -------------------------------------------------------------------------------- 1 | _fits($variable)) { 16 | return false; 17 | } 18 | 19 | $var = strlen($variable) === 13 ? substr($variable, 0, -3) : $variable; 20 | 21 | // avoid dreaded "Timezone must be set" error 22 | $varData->addTabToView($variable, 'Timestamp', @date('Y-m-d H:i:s', $var)); 23 | } 24 | 25 | private function _fits($variable) 26 | { 27 | if (! SageHelper::isRichMode()) { 28 | return false; 29 | } 30 | 31 | if (! is_string($variable) && ! is_int($variable)) { 32 | return false; 33 | } 34 | 35 | $len = strlen((int)$variable); 36 | 37 | return 38 | ( 39 | $len === 9 || $len === 10 // a little naive 40 | || ($len === 13 && substr($variable, -3) === '000') // also handles javascript micro timestamps 41 | ) 42 | && ((string)(int)$variable == $variable); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /parsers/SageParsersXml.php: -------------------------------------------------------------------------------- 1 | addTabToView($variable, 'XML', SageParser::alternativesParse($variable, $xml)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /resources/compiled/aante-light.css: -------------------------------------------------------------------------------- 1 | ._sage::selection{background:#aaa;color:#1d1e1e}._sage,._sage::before,._sage::after,._sage *,._sage *::before,._sage *::after{box-sizing:border-box;border-radius:0;color:#1d1e1e;float:none !important;font-family:ui-monospace,"Cascadia Code","Source Code Pro",Menlo,Consolas,"DejaVu Sans Mono",monospace;line-height:15px;margin:0;padding:0;text-align:left}._sage{font-size:13px;margin:8px 0;overflow-x:auto;white-space:nowrap}._sage u{color:#ff8c00}._sage dt{background:#f8f8f8;border:1px solid #d7d7d7;color:#1d1e1e;display:block;font-weight:bold;list-style:none outside none;overflow:auto;padding:4px}._sage dt:hover{border-color:#aaa}._sage>dl dl{padding:0 0 0 12px}._sage nav{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAA8CAYAAAC5OOBJAAAAvklEQVR42mNgGMFAS0uLLSQk5HxoaOh/XDg4OLgdpwEBAQEGQAN+YtMIFH/i4ODAg9cFQNMbsWkOCgrKJMv5ID5Qipko/6M7PzAw0ImkAIQ5H0hvISv0gZpP+fn5qZAVfcBAkmEYBaNZcjRLjmbJUUCvqAIlDlAiASUWkjWDkiU0eTaSpBGUEZBy1E9QRiFWLzO2LEmU80GZHkcRhN/5oGIGVNzgKIbwOx9UwOErAIl2/igYzZKjWXI0S9IHAAASJijo0Ypj8AAAAABJRU5ErkJggg==") no-repeat scroll 0 0 rgba(0,0,0,0);cursor:pointer;display:inline-block;height:15px;width:15px;margin-right:3px;vertical-align:middle}._sage dt._sage-parent{cursor:pointer}._sage dt._sage-parent:hover nav{background-position:0 -15px}._sage dt._sage-parent._sage-show:hover>nav{background-position:0 -45px}._sage dt._sage-show>nav{background-position:0 -30px}._sage dt._sage-parent+dd{display:none;border-left:1px dashed #d7d7d7}._sage dt._sage-parent._sage-show+dd{display:block}._sage var,._sage var a{color:#06f;font-style:normal}._sage dt:hover var,._sage dt:hover var a{color:red}._sage dfn{font-style:normal;font-weight:normal;color:#1d1e1e}._sage pre{color:#1d1e1e;margin:0 0 0 12px;padding:5px;overflow-y:hidden;border-top:0;border:1px solid #d7d7d7;background:#f8f8f8;display:block;word-break:normal}._sage ._sage-popup-trigger{float:right !important;cursor:pointer;color:#aaa}._sage ._sage-popup-trigger:hover{color:#d7d7d7}._sage dt._sage-parent>._sage-popup-trigger{font-size:13px}._sage footer{padding:0 3px 3px;font-size:9px;color:#999;text-shadow:0 0 1px #ccc}._sage footer>._sage-popup-trigger{font-size:12px}._sage footer nav{background-size:10px;height:10px;width:10px}._sage footer nav:hover{background-position:0 -10px}._sage footer>ol{display:none;margin-left:32px}._sage footer li{color:#999}._sage footer._sage-show>ol{display:block}._sage footer._sage-show nav{background-position:0 -20px}._sage footer._sage-show nav:hover{background-position:0 -30px}._sage a{color:#1d1e1e;text-shadow:none}._sage a:hover{color:#1d1e1e;border-bottom:1px dotted #1d1e1e}._sage ul{list-style:none;padding-left:12px}._sage ul:not(._sage-tabs) li{border-left:1px dashed #d7d7d7}._sage ul:not(._sage-tabs) li>dl{border-left:none}._sage ul._sage-tabs{margin:0 0 0 12px;padding-left:0;background:#f8f8f8;border:1px solid #d7d7d7;border-top:0}._sage ul._sage-tabs li{background:#f8f8f8;border:1px solid #d7d7d7;cursor:pointer;display:inline-block;height:24px;margin:2px;padding:0 12px;vertical-align:top}._sage ul._sage-tabs li:hover,._sage ul._sage-tabs li._sage-active-tab:hover{border-color:#aaa;color:red}._sage ul._sage-tabs li._sage-active-tab{background:#f8f8f8;border-top:0;margin-top:-2px;height:27px;line-height:24px}._sage ul._sage-tabs li:not(._sage-active-tab){line-height:20px}._sage ul._sage-tabs li+li{margin-left:0}._sage ul:not(._sage-tabs)>li:not(:first-child){display:none}._sage dt:hover+dd>ul>li._sage-active-tab{border-color:#aaa;color:red}._sage-report{border-collapse:collapse;empty-cells:show;border-spacing:0}._sage-report *{font-size:12px}._sage-report dt{background:none;padding:2px}._sage-report dt ._sage-parent{min-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}._sage-report td,._sage-report th{border:1px solid #d7d7d7;padding:2px;vertical-align:center}._sage-report th{cursor:alias}._sage-report td:first-child,._sage-report th{font-weight:bold;background:#f8f8f8;color:#1d1e1e}._sage-report td{background:#f8f8f8;white-space:pre}._sage-report td>dl{padding:0}._sage-report pre{border-top:0;border-right:0}._sage-report th:first-child{background:none;border:0}._sage-report td._sage-empty{background:#d33682 !important}._sage-report tr:hover>td{box-shadow:0 0 1px 0 #aaa inset}._sage-report tr:hover var{color:red}._sage-report ul._sage-tabs li._sage-active-tab{height:20px;line-height:17px}._sage-trace ._sage-source{line-height:14px}._sage-trace ._sage-source span{padding-right:1px;border-right:3px inset #06f}._sage-trace ._sage-source ._sage-highlight{background:#f8f8f8}._sage-trace b{min-width:18px;display:inline-block;text-align:right;margin-right:6px;color:#1d1e1e}._sage-trace ._sage-blacklisted>b,._sage-trace ._sage-childless>b{margin-right:22px}._sage-trace ._sage-blacklisted{filter:brightness(120%)}._sage-trace ._sage-parent>var>a{color:#06f}._sage-focused{box-shadow:0 0 3px 2px red}._sage-microtime,._sage-color-preview{box-shadow:0 0 2px 0 #b6cedb;height:16px;text-align:center;text-shadow:-1px 0 #839496,0 1px #839496,1px 0 #839496,0 -1px #839496;width:230px;color:#fdf6e3}._sage dt{font-weight:normal;margin-top:4px}._sage>dl{background:linear-gradient(90deg, rgba(255, 255, 255, 0) 0, #fff 15px)}._sage dl dl{margin-top:4px;padding-left:25px;border-left:none}._sage>dl>dt{background:#f8f8f8}._sage ul{margin:0;padding-left:0}._sage ul:not(._sage-tabs)>li{border-left:0}._sage ul._sage-tabs{background:#f8f8f8;border:1px solid #d7d7d7;border-width:0 1px 1px 1px;padding:4px 0 0 12px;margin-left:-1px;margin-top:-1px}._sage ul._sage-tabs li,._sage ul._sage-tabs li+li{margin:0 0 0 4px}._sage ul._sage-tabs li{border-bottom-width:0;height:25px}._sage ul._sage-tabs li:first-child{margin-left:0}._sage ul._sage-tabs li._sage-active-tab{border-top:1px solid #d7d7d7;background:#fff;font-weight:bold;padding-top:0;border-bottom:1px solid #fff !important;margin-bottom:-1px}._sage ul._sage-tabs li._sage-active-tab:hover{border-bottom:1px solid #fff}._sage ul>li>pre{border:1px solid #d7d7d7}._sage dt:hover+dd>ul{border-color:#aaa}._sage pre{background:#fff;margin-top:4px;margin-left:25px}._sage ._sage-popup-trigger:hover{color:red}._sage ._sage-source ._sage-highlight{background:#cfc}._sage ._sage-source span{border-right:3px inset #268bd2}._sage-report td{background:#fff}._sage-report td>dl{padding:0;margin:0}._sage-report td>dl>dt._sage-parent{margin:0}._sage-report td:first-child,._sage-report td,._sage-report th{padding:2px 4px}._sage-report td._sage-empty{background:#d7d7d7 !important}._sage-report dd,._sage-report dt{background:#fff}._sage-report tr:hover>td{box-shadow:none;background:#cfc} 2 | -------------------------------------------------------------------------------- /resources/compiled/original-light.css: -------------------------------------------------------------------------------- 1 | ._sage::selection{background:#0092db;color:#1d1e1e}._sage,._sage::before,._sage::after,._sage *,._sage *::before,._sage *::after{box-sizing:border-box;border-radius:0;color:#1d1e1e;float:none !important;font-family:ui-monospace,"Cascadia Code","Source Code Pro",Menlo,Consolas,"DejaVu Sans Mono",monospace;line-height:15px;margin:0;padding:0;text-align:left}._sage{font-size:13px;margin:8px 0;overflow-x:auto;white-space:nowrap}._sage u{color:#ff8c00}._sage dt{background:#f1f5f7;border:1px solid #b6cedb;color:#1d1e1e;display:block;font-weight:bold;list-style:none outside none;overflow:auto;padding:4px}._sage dt:hover{border-color:#0092db}._sage>dl dl{padding:0 0 0 12px}._sage nav{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAA8CAMAAACO5hB7AAAACVBMVEX///82b4tctzBls0NMAAAAQklEQVR42rWRIQ4AAAgCwf8/2qgER2BeuwJsgAqIzekMfMryPL9XQSkobE6vwKcsz/N7FfPvh09lnPf/b+7673+f0uHuAZ/EdkNQAAAAAElFTkSuQmCC") no-repeat scroll 0 0 rgba(0,0,0,0);cursor:pointer;display:inline-block;height:15px;width:15px;margin-right:3px;vertical-align:middle}._sage dt._sage-parent{cursor:pointer}._sage dt._sage-parent:hover nav{background-position:0 -15px}._sage dt._sage-parent._sage-show:hover>nav{background-position:0 -45px}._sage dt._sage-show>nav{background-position:0 -30px}._sage dt._sage-parent+dd{display:none;border-left:1px dashed #b6cedb}._sage dt._sage-parent._sage-show+dd{display:block}._sage var,._sage var a{color:#0092db;font-style:normal}._sage dt:hover var,._sage dt:hover var a{color:#5cb730}._sage dfn{font-style:normal;font-weight:normal;color:#1d1e1e}._sage pre{color:#1d1e1e;margin:0 0 0 12px;padding:5px;overflow-y:hidden;border-top:0;border:1px solid #b6cedb;background:#f1f5f7;display:block;word-break:normal}._sage ._sage-popup-trigger{float:right !important;cursor:pointer;color:#0092db}._sage ._sage-popup-trigger:hover{color:#b6cedb}._sage dt._sage-parent>._sage-popup-trigger{font-size:13px}._sage footer{padding:0 3px 3px;font-size:9px;color:#999;text-shadow:0 0 1px #ccc}._sage footer>._sage-popup-trigger{font-size:12px}._sage footer nav{background-size:10px;height:10px;width:10px}._sage footer nav:hover{background-position:0 -10px}._sage footer>ol{display:none;margin-left:32px}._sage footer li{color:#999}._sage footer._sage-show>ol{display:block}._sage footer._sage-show nav{background-position:0 -20px}._sage footer._sage-show nav:hover{background-position:0 -30px}._sage a{color:#1d1e1e;text-shadow:none}._sage a:hover{color:#1d1e1e;border-bottom:1px dotted #1d1e1e}._sage ul{list-style:none;padding-left:12px}._sage ul:not(._sage-tabs) li{border-left:1px dashed #b6cedb}._sage ul:not(._sage-tabs) li>dl{border-left:none}._sage ul._sage-tabs{margin:0 0 0 12px;padding-left:0;background:#f1f5f7;border:1px solid #b6cedb;border-top:0}._sage ul._sage-tabs li{background:#e3ecf0;border:1px solid #b6cedb;cursor:pointer;display:inline-block;height:24px;margin:2px;padding:0 12px;vertical-align:top}._sage ul._sage-tabs li:hover,._sage ul._sage-tabs li._sage-active-tab:hover{border-color:#0092db;color:#5cb730}._sage ul._sage-tabs li._sage-active-tab{background:#f1f5f7;border-top:0;margin-top:-2px;height:27px;line-height:24px}._sage ul._sage-tabs li:not(._sage-active-tab){line-height:20px}._sage ul._sage-tabs li+li{margin-left:0}._sage ul:not(._sage-tabs)>li:not(:first-child){display:none}._sage dt:hover+dd>ul>li._sage-active-tab{border-color:#0092db;color:#5cb730}._sage-report{border-collapse:collapse;empty-cells:show;border-spacing:0}._sage-report *{font-size:12px}._sage-report dt{background:none;padding:2px}._sage-report dt ._sage-parent{min-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}._sage-report td,._sage-report th{border:1px solid #b6cedb;padding:2px;vertical-align:center}._sage-report th{cursor:alias}._sage-report td:first-child,._sage-report th{font-weight:bold;background:#e3ecf0;color:#1d1e1e}._sage-report td{background:#f1f5f7;white-space:pre}._sage-report td>dl{padding:0}._sage-report pre{border-top:0;border-right:0}._sage-report th:first-child{background:none;border:0}._sage-report td._sage-empty{background:#d33682 !important}._sage-report tr:hover>td{box-shadow:0 0 1px 0 #0092db inset}._sage-report tr:hover var{color:#5cb730}._sage-report ul._sage-tabs li._sage-active-tab{height:20px;line-height:17px}._sage-trace ._sage-source{line-height:14px}._sage-trace ._sage-source span{padding-right:1px;border-right:3px inset #0092db}._sage-trace ._sage-source ._sage-highlight{background:#e3ecf0}._sage-trace b{min-width:18px;display:inline-block;text-align:right;margin-right:6px;color:#1d1e1e}._sage-trace ._sage-blacklisted>b,._sage-trace ._sage-childless>b{margin-right:22px}._sage-trace ._sage-blacklisted{filter:brightness(120%)}._sage-trace ._sage-parent>var>a{color:#0092db}._sage-focused{box-shadow:0 0 3px 2px #5cb730}._sage-microtime,._sage-color-preview{box-shadow:0 0 2px 0 #b6cedb;height:16px;text-align:center;text-shadow:-1px 0 #839496,0 1px #839496,1px 0 #839496,0 -1px #839496;width:230px;color:#fdf6e3}._sage>dl>dt,._sage ul._sage-tabs{background:linear-gradient(to bottom, #fff 0, #f1f5f7 100%)}._sage dl:not(:last-of-type) dt:not(._sage-show){border-bottom:0}._sage ul._sage-tabs li{background:#e3ecf0}._sage>dl._sage-trace>dt{background:linear-gradient(to bottom, #f1f5f7 0, #fff 100%)}._sage>dl._sage-trace dt:not(:last-of-type):not(._sage-show){border-bottom:0}._sage>dl._sage-trace>dd>ul._sage-tabs>li._sage-active-tab{background:#fff}._sage ._sage-source ._sage-highlight{background:#f0eb96}._sage ._sage-source>div{margin-top:-1px} 2 | -------------------------------------------------------------------------------- /resources/compiled/original.css: -------------------------------------------------------------------------------- 1 | ._sage::selection{background:#0092db;color:#1d1e1e}._sage,._sage::before,._sage::after,._sage *,._sage *::before,._sage *::after{box-sizing:border-box;border-radius:0;color:#1d1e1e;float:none !important;font-family:ui-monospace,"Cascadia Code","Source Code Pro",Menlo,Consolas,"DejaVu Sans Mono",monospace;line-height:15px;margin:0;padding:0;text-align:left}._sage{font-size:13px;margin:8px 0;overflow-x:auto;white-space:nowrap}._sage u{color:#ff8c00}._sage dt{background:#e0eaef;border:1px solid #b6cedb;color:#1d1e1e;display:block;font-weight:bold;list-style:none outside none;overflow:auto;padding:4px}._sage dt:hover{border-color:#0092db}._sage>dl dl{padding:0 0 0 12px}._sage nav{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAA8CAMAAACO5hB7AAAACVBMVEX///82b4tctzBls0NMAAAAQklEQVR42rWRIQ4AAAgCwf8/2qgER2BeuwJsgAqIzekMfMryPL9XQSkobE6vwKcsz/N7FfPvh09lnPf/b+7673+f0uHuAZ/EdkNQAAAAAElFTkSuQmCC") no-repeat scroll 0 0 rgba(0,0,0,0);cursor:pointer;display:inline-block;height:15px;width:15px;margin-right:3px;vertical-align:middle}._sage dt._sage-parent{cursor:pointer}._sage dt._sage-parent:hover nav{background-position:0 -15px}._sage dt._sage-parent._sage-show:hover>nav{background-position:0 -45px}._sage dt._sage-show>nav{background-position:0 -30px}._sage dt._sage-parent+dd{display:none;border-left:1px dashed #b6cedb}._sage dt._sage-parent._sage-show+dd{display:block}._sage var,._sage var a{color:#0092db;font-style:normal}._sage dt:hover var,._sage dt:hover var a{color:#5cb730}._sage dfn{font-style:normal;font-weight:normal;color:#1d1e1e}._sage pre{color:#1d1e1e;margin:0 0 0 12px;padding:5px;overflow-y:hidden;border-top:0;border:1px solid #b6cedb;background:#e0eaef;display:block;word-break:normal}._sage ._sage-popup-trigger{float:right !important;cursor:pointer;color:#0092db}._sage ._sage-popup-trigger:hover{color:#b6cedb}._sage dt._sage-parent>._sage-popup-trigger{font-size:13px}._sage footer{padding:0 3px 3px;font-size:9px;color:#999;text-shadow:0 0 1px #ccc}._sage footer>._sage-popup-trigger{font-size:12px}._sage footer nav{background-size:10px;height:10px;width:10px}._sage footer nav:hover{background-position:0 -10px}._sage footer>ol{display:none;margin-left:32px}._sage footer li{color:#999}._sage footer._sage-show>ol{display:block}._sage footer._sage-show nav{background-position:0 -20px}._sage footer._sage-show nav:hover{background-position:0 -30px}._sage a{color:#1d1e1e;text-shadow:none}._sage a:hover{color:#1d1e1e;border-bottom:1px dotted #1d1e1e}._sage ul{list-style:none;padding-left:12px}._sage ul:not(._sage-tabs) li{border-left:1px dashed #b6cedb}._sage ul:not(._sage-tabs) li>dl{border-left:none}._sage ul._sage-tabs{margin:0 0 0 12px;padding-left:0;background:#e0eaef;border:1px solid #b6cedb;border-top:0}._sage ul._sage-tabs li{background:#c1d4df;border:1px solid #b6cedb;cursor:pointer;display:inline-block;height:24px;margin:2px;padding:0 12px;vertical-align:top}._sage ul._sage-tabs li:hover,._sage ul._sage-tabs li._sage-active-tab:hover{border-color:#0092db;color:#5cb730}._sage ul._sage-tabs li._sage-active-tab{background:#e0eaef;border-top:0;margin-top:-2px;height:27px;line-height:24px}._sage ul._sage-tabs li:not(._sage-active-tab){line-height:20px}._sage ul._sage-tabs li+li{margin-left:0}._sage ul:not(._sage-tabs)>li:not(:first-child){display:none}._sage dt:hover+dd>ul>li._sage-active-tab{border-color:#0092db;color:#5cb730}._sage-report{border-collapse:collapse;empty-cells:show;border-spacing:0}._sage-report *{font-size:12px}._sage-report dt{background:none;padding:2px}._sage-report dt ._sage-parent{min-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}._sage-report td,._sage-report th{border:1px solid #b6cedb;padding:2px;vertical-align:center}._sage-report th{cursor:alias}._sage-report td:first-child,._sage-report th{font-weight:bold;background:#c1d4df;color:#1d1e1e}._sage-report td{background:#e0eaef;white-space:pre}._sage-report td>dl{padding:0}._sage-report pre{border-top:0;border-right:0}._sage-report th:first-child{background:none;border:0}._sage-report td._sage-empty{background:#d33682 !important}._sage-report tr:hover>td{box-shadow:0 0 1px 0 #0092db inset}._sage-report tr:hover var{color:#5cb730}._sage-report ul._sage-tabs li._sage-active-tab{height:20px;line-height:17px}._sage-trace ._sage-source{line-height:14px}._sage-trace ._sage-source span{padding-right:1px;border-right:3px inset #0092db}._sage-trace ._sage-source ._sage-highlight{background:#c1d4df}._sage-trace b{min-width:18px;display:inline-block;text-align:right;margin-right:6px;color:#1d1e1e}._sage-trace ._sage-blacklisted>b,._sage-trace ._sage-childless>b{margin-right:22px}._sage-trace ._sage-blacklisted{filter:brightness(120%)}._sage-trace ._sage-parent>var>a{color:#0092db}._sage-focused{box-shadow:0 0 3px 2px #5cb730}._sage-microtime,._sage-color-preview{box-shadow:0 0 2px 0 #b6cedb;height:16px;text-align:center;text-shadow:-1px 0 #839496,0 1px #839496,1px 0 #839496,0 -1px #839496;width:230px;color:#fdf6e3}._sage>dl>dt{background:linear-gradient(to bottom, #e3ecf0 0, #c0d4df 100%)}._sage ul._sage-tabs{background:linear-gradient(to bottom, #9dbed0 0px, #b2ccda 100%)}._sage>dl:not(._sage-trace)>dd>ul._sage-tabs li{background:#e0eaef}._sage>dl:not(._sage-trace)>dd>ul._sage-tabs li._sage-active-tab{background:#c1d4df}._sage>dl._sage-trace>dt{background:linear-gradient(to bottom, #c0d4df 0px, #e3ecf0 100%)}._sage ._sage-source ._sage-highlight{background:#f0eb96} 2 | -------------------------------------------------------------------------------- /resources/compiled/sage.js: -------------------------------------------------------------------------------- 1 | if("undefined"==typeof _sageInitialized){const a={t:[],i:-(_sageInitialized=1),o:[],u:(n,t=100)=>{let i;return(...e)=>{clearTimeout(i),i=setTimeout(()=>n(...e),t)}},l:function(){let t=a.o.length;for(;t--;){let e=a.o[t],n=e.g;(window.scrollY<=n._||window.scrollY>=n.v)&&(a.o.splice(t,1),n.remove(),e.g=null)}var e=document.querySelectorAll("._sage");for(t=e.length;t--;){const n=e[t];if(!a.p(n))return;let o=0;n.querySelectorAll("._sage-clone").forEach(function(e){o+=e.offsetHeight}),n.querySelectorAll("dt._sage-parent._sage-show:not(._sage-clone)").forEach(function(e){if(!a.o.includes(e)){const t=e.nextElementSibling;if(a.p(t)){const i=e.cloneNode(!0);var n=t.getBoundingClientRect();i._=n.top+window.scrollY,i.v=n.bottom+window.scrollY,i.style.width=n.width+"px",i.style.top=o+"px",i.style.position="fixed",i.style.opacity=.9,i.classList.add("_sage-clone"),o+=e.offsetHeight,e.g=i,a.o.push(e),e.after(i)}}});break}},p:function(e){e=e.getBoundingClientRect();return 0!==e.height&&(!(e.top>=window.innerHeight)&&(!(0dd>._sage-tabs>._sage-active-tab").forEach(function(e){e=e.nextSibling;e&&a.C(e)})},C:function(e){let n,t=e,i=0;for(a.A(e.parentNode.getElementsByClassName("_sage-active-tab")[0],"_sage-active-tab"),a.k(e,"_sage-active-tab");t=t.previousSibling;)1===t.nodeType&&i++;n=e.parentNode.nextSibling.childNodes;for(let e=0;eli:not(._sage-active-tab)",function(e){0===e.offsetWidth&&0===e.offsetHeight||a.t.push(e)})},tag:function(e){return"<"+e+">"},O:function(e){let n;(n=window.open())&&(n.document.open(),n.document.write(a.tag("html")+a.tag("head")+"Sage ☯ ("+(new Date).toISOString()+")"+a.tag('meta charset="utf-8"')+document.getElementsByClassName("_sage-js")[0].outerHTML+document.getElementsByClassName("_sage-css")[0].outerHTML+a.tag("/head")+a.tag("body")+'
'+e.parentNode.outerHTML+"
"+a.tag("/body")),n.document.close())},R:function(e,t,n){const i=e.tBodies[0],o=new Intl.Collator(void 0,{numeric:!0,sensitivity:"base"}),a=void 0===n.V?1:n.V;n.V=-1*a,[].slice.call(e.tBodies[0].rows).sort(function(e,n){return a*o.compare(e.cells[t].textContent,n.cells[t].textContent)}).forEach(function(e){i.appendChild(e)})},S:{L:function(e){var n="_sage-focused",t=document.querySelector("."+n);if(t&&a.A(t,n),-1!==e){t=a.t[e];a.k(t,n);const i=function(e){return e.offsetTop+(e.offsetParent?i(e.offsetParent):0)};n=i(t)-window.innerHeight/2;window.scrollTo(0,n)}a.i=e},j:function(e,n){return e?--n<0&&(n=a.t.length-1):++n>=a.t.length&&(n=0),a.S.L(n),!1}}};window.addEventListener("click",function(e){let n=e.target,t=n.tagName;if(a.D(n)){if("DFN"===t)a.m(n),n=n.parentNode;else if("VAR"===t)n=n.parentNode,t=n.tagName;else if("TH"===t)return e.ctrlKey||a.R(n.parentNode.parentNode.parentNode,n.cellIndex,n),!1;if("LI"===t&&a.T(n.parentNode,"_sage-tabs"))return a.T(n,"_sage-active-tab")||(a.C(n),-1!==a.i&&a.F()),!1;if("NAV"===t)return"FOOTER"===n.parentNode.tagName?(n=n.parentNode,a.toggle(n)):setTimeout(function(){0e&&(t=e),idl dl{padding:0 0 0 12px}._sage nav{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAA8CAMAAACO5hB7AAAA7VBMVEVYbnWToaH///9YbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnWToaGToaGToaGToaGToaGToaGToaGToaGToaFYbnWToaFYbnVYbnWToaGToaFYbnWToaFYbnWToaFYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnWToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaFYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnWToaGToaGToaGToaGToaGToaGToaGToaGToaFYbnWToaEeRtj1AAAATXRSTlMAAACLab0B7iP2R7CL9iPuaUewAb36F0/QnPoX0JxPYQaurepi6QV8FhV7MzFhM6586RYFMa3qYnsVBpNQ+ZGOGJ3PkM9QkZAY+Y6TnV7gnYsAAAEcSURBVHhebdHVbgMxEEBRu8sUZmqapBBoSuGUmWb+/3NayxtrLOe+Hck745XZgd6/eb3CVcKt7HmR+gKxlCGOETG5vFbOoahR6NxI5zHt6nuq+2dqnqfzzH3mfcz70vb8H6MJt5u6a96hS30E4PjEFgAEp2fKNojKUfVEOoS044ex7u3YPE/nmfvM+5j3pe17T9oe/77O41w+n4vnrbrwZxbTshVhvtx5Kb8vliRLRWmeSQSTjJq/El5x5fUXYmNN9hcQC5y4g/hGvVksNtT8451rns2IScb7mn567ll2GNpWr9YWfvQgzWsKs8HOA/m960g6rjTzA8HAV/NHwiOmPLwDKA/J/gggYsRVgFvqbr/fpWYv90zzZKKs9Qfx00Jx65oxLAAAAABJRU5ErkJggg==") no-repeat scroll 0 0 rgba(0,0,0,0);cursor:pointer;display:inline-block;height:15px;width:15px;margin-right:3px;vertical-align:middle}._sage dt._sage-parent{cursor:pointer}._sage dt._sage-parent:hover nav{background-position:0 -15px}._sage dt._sage-parent._sage-show:hover>nav{background-position:0 -45px}._sage dt._sage-show>nav{background-position:0 -30px}._sage dt._sage-parent+dd{display:none;border-left:1px dashed #586e75}._sage dt._sage-parent._sage-show+dd{display:block}._sage var,._sage var a{color:#268bd2;font-style:normal}._sage dt:hover var,._sage dt:hover var a{color:#2aa198}._sage dfn{font-style:normal;font-weight:normal;color:#93a1a1}._sage pre{color:#839496;margin:0 0 0 12px;padding:5px;overflow-y:hidden;border-top:0;border:1px solid #586e75;background:#002b36;display:block;word-break:normal}._sage ._sage-popup-trigger{float:right !important;cursor:pointer;color:#268bd2}._sage ._sage-popup-trigger:hover{color:#586e75}._sage dt._sage-parent>._sage-popup-trigger{font-size:13px}._sage footer{padding:0 3px 3px;font-size:9px;color:#999;text-shadow:0 0 1px #ccc}._sage footer>._sage-popup-trigger{font-size:12px}._sage footer nav{background-size:10px;height:10px;width:10px}._sage footer nav:hover{background-position:0 -10px}._sage footer>ol{display:none;margin-left:32px}._sage footer li{color:#999}._sage footer._sage-show>ol{display:block}._sage footer._sage-show nav{background-position:0 -20px}._sage footer._sage-show nav:hover{background-position:0 -30px}._sage a{color:#839496;text-shadow:none}._sage a:hover{color:#93a1a1;border-bottom:1px dotted #93a1a1}._sage ul{list-style:none;padding-left:12px}._sage ul:not(._sage-tabs) li{border-left:1px dashed #586e75}._sage ul:not(._sage-tabs) li>dl{border-left:none}._sage ul._sage-tabs{margin:0 0 0 12px;padding-left:0;background:#002b36;border:1px solid #586e75;border-top:0}._sage ul._sage-tabs li{background:#073642;border:1px solid #586e75;cursor:pointer;display:inline-block;height:24px;margin:2px;padding:0 12px;vertical-align:top}._sage ul._sage-tabs li:hover,._sage ul._sage-tabs li._sage-active-tab:hover{border-color:#268bd2;color:#2aa198}._sage ul._sage-tabs li._sage-active-tab{background:#002b36;border-top:0;margin-top:-2px;height:27px;line-height:24px}._sage ul._sage-tabs li:not(._sage-active-tab){line-height:20px}._sage ul._sage-tabs li+li{margin-left:0}._sage ul:not(._sage-tabs)>li:not(:first-child){display:none}._sage dt:hover+dd>ul>li._sage-active-tab{border-color:#268bd2;color:#2aa198}._sage-report{border-collapse:collapse;empty-cells:show;border-spacing:0}._sage-report *{font-size:12px}._sage-report dt{background:none;padding:2px}._sage-report dt ._sage-parent{min-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}._sage-report td,._sage-report th{border:1px solid #586e75;padding:2px;vertical-align:center}._sage-report th{cursor:alias}._sage-report td:first-child,._sage-report th{font-weight:bold;background:#073642;color:#93a1a1}._sage-report td{background:#002b36;white-space:pre}._sage-report td>dl{padding:0}._sage-report pre{border-top:0;border-right:0}._sage-report th:first-child{background:none;border:0}._sage-report td._sage-empty{background:#d33682 !important}._sage-report tr:hover>td{box-shadow:0 0 1px 0 #268bd2 inset}._sage-report tr:hover var{color:#2aa198}._sage-report ul._sage-tabs li._sage-active-tab{height:20px;line-height:17px}._sage-trace ._sage-source{line-height:14px}._sage-trace ._sage-source span{padding-right:1px;border-right:3px inset #268bd2}._sage-trace ._sage-source ._sage-highlight{background:#073642}._sage-trace b{min-width:18px;display:inline-block;text-align:right;margin-right:6px;color:#93a1a1}._sage-trace ._sage-blacklisted>b,._sage-trace ._sage-childless>b{margin-right:22px}._sage-trace ._sage-blacklisted{filter:brightness(120%)}._sage-trace ._sage-parent>var>a{color:#268bd2}._sage-focused{box-shadow:0 0 3px 2px #859900 inset;border-radius:7px}._sage-microtime,._sage-color-preview{box-shadow:0 0 2px 0 #b6cedb;height:16px;text-align:center;text-shadow:-1px 0 #839496,0 1px #839496,1px 0 #839496,0 -1px #839496;width:230px;color:#fdf6e3}body{background:#073642;color:#fff}._sage{background:#073642;box-shadow:0 0 5px 3px #073642}._sage>dl>dt,._sage ul._sage-tabs{box-shadow:4px 0 2px -3px #268bd2 inset}._sage ul._sage-tabs li._sage-active-tab{padding-top:7px;height:34px} 2 | -------------------------------------------------------------------------------- /resources/compiled/solarized.css: -------------------------------------------------------------------------------- 1 | ._sage::selection{background:#268bd2;color:#657b83}._sage,._sage::before,._sage::after,._sage *,._sage *::before,._sage *::after{box-sizing:border-box;border-radius:0;color:#657b83;float:none !important;font-family:ui-monospace,"Cascadia Code","Source Code Pro",Menlo,Consolas,"DejaVu Sans Mono",monospace;line-height:15px;margin:0;padding:0;text-align:left}._sage{font-size:13px;margin:8px 0;overflow-x:auto;white-space:nowrap}._sage u{color:#ff8c00}._sage dt{background:#fdf6e3;border:1px solid #93a1a1;color:#657b83;display:block;font-weight:bold;list-style:none outside none;overflow:auto;padding:4px}._sage dt:hover{border-color:#268bd2}._sage>dl dl{padding:0 0 0 12px}._sage nav{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAA8CAMAAACO5hB7AAAA7VBMVEVYbnWToaH///9YbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnWToaGToaGToaGToaGToaGToaGToaGToaGToaFYbnWToaFYbnVYbnWToaGToaFYbnWToaFYbnWToaFYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnWToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaFYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnWToaGToaGToaGToaGToaGToaGToaGToaGToaFYbnWToaEeRtj1AAAATXRSTlMAAACLab0B7iP2R7CL9iPuaUewAb36F0/QnPoX0JxPYQaurepi6QV8FhV7MzFhM6586RYFMa3qYnsVBpNQ+ZGOGJ3PkM9QkZAY+Y6TnV7gnYsAAAEcSURBVHhebdHVbgMxEEBRu8sUZmqapBBoSuGUmWb+/3NayxtrLOe+Hck745XZgd6/eb3CVcKt7HmR+gKxlCGOETG5vFbOoahR6NxI5zHt6nuq+2dqnqfzzH3mfcz70vb8H6MJt5u6a96hS30E4PjEFgAEp2fKNojKUfVEOoS044ex7u3YPE/nmfvM+5j3pe17T9oe/77O41w+n4vnrbrwZxbTshVhvtx5Kb8vliRLRWmeSQSTjJq/El5x5fUXYmNN9hcQC5y4g/hGvVksNtT8451rns2IScb7mn567ll2GNpWr9YWfvQgzWsKs8HOA/m960g6rjTzA8HAV/NHwiOmPLwDKA/J/gggYsRVgFvqbr/fpWYv90zzZKKs9Qfx00Jx65oxLAAAAABJRU5ErkJggg==") no-repeat scroll 0 0 rgba(0,0,0,0);cursor:pointer;display:inline-block;height:15px;width:15px;margin-right:3px;vertical-align:middle}._sage dt._sage-parent{cursor:pointer}._sage dt._sage-parent:hover nav{background-position:0 -15px}._sage dt._sage-parent._sage-show:hover>nav{background-position:0 -45px}._sage dt._sage-show>nav{background-position:0 -30px}._sage dt._sage-parent+dd{display:none;border-left:1px dashed #93a1a1}._sage dt._sage-parent._sage-show+dd{display:block}._sage var,._sage var a{color:#268bd2;font-style:normal}._sage dt:hover var,._sage dt:hover var a{color:#2aa198}._sage dfn{font-style:normal;font-weight:normal;color:#586e75}._sage pre{color:#657b83;margin:0 0 0 12px;padding:5px;overflow-y:hidden;border-top:0;border:1px solid #93a1a1;background:#fdf6e3;display:block;word-break:normal}._sage ._sage-popup-trigger{float:right !important;cursor:pointer;color:#268bd2}._sage ._sage-popup-trigger:hover{color:#93a1a1}._sage dt._sage-parent>._sage-popup-trigger{font-size:13px}._sage footer{padding:0 3px 3px;font-size:9px;color:#999;text-shadow:0 0 1px #ccc}._sage footer>._sage-popup-trigger{font-size:12px}._sage footer nav{background-size:10px;height:10px;width:10px}._sage footer nav:hover{background-position:0 -10px}._sage footer>ol{display:none;margin-left:32px}._sage footer li{color:#999}._sage footer._sage-show>ol{display:block}._sage footer._sage-show nav{background-position:0 -20px}._sage footer._sage-show nav:hover{background-position:0 -30px}._sage a{color:#657b83;text-shadow:none}._sage a:hover{color:#586e75;border-bottom:1px dotted #586e75}._sage ul{list-style:none;padding-left:12px}._sage ul:not(._sage-tabs) li{border-left:1px dashed #93a1a1}._sage ul:not(._sage-tabs) li>dl{border-left:none}._sage ul._sage-tabs{margin:0 0 0 12px;padding-left:0;background:#fdf6e3;border:1px solid #93a1a1;border-top:0}._sage ul._sage-tabs li{background:#eee8d5;border:1px solid #93a1a1;cursor:pointer;display:inline-block;height:24px;margin:2px;padding:0 12px;vertical-align:top}._sage ul._sage-tabs li:hover,._sage ul._sage-tabs li._sage-active-tab:hover{border-color:#268bd2;color:#2aa198}._sage ul._sage-tabs li._sage-active-tab{background:#fdf6e3;border-top:0;margin-top:-2px;height:27px;line-height:24px}._sage ul._sage-tabs li:not(._sage-active-tab){line-height:20px}._sage ul._sage-tabs li+li{margin-left:0}._sage ul:not(._sage-tabs)>li:not(:first-child){display:none}._sage dt:hover+dd>ul>li._sage-active-tab{border-color:#268bd2;color:#2aa198}._sage-report{border-collapse:collapse;empty-cells:show;border-spacing:0}._sage-report *{font-size:12px}._sage-report dt{background:none;padding:2px}._sage-report dt ._sage-parent{min-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}._sage-report td,._sage-report th{border:1px solid #93a1a1;padding:2px;vertical-align:center}._sage-report th{cursor:alias}._sage-report td:first-child,._sage-report th{font-weight:bold;background:#eee8d5;color:#586e75}._sage-report td{background:#fdf6e3;white-space:pre}._sage-report td>dl{padding:0}._sage-report pre{border-top:0;border-right:0}._sage-report th:first-child{background:none;border:0}._sage-report td._sage-empty{background:#d33682 !important}._sage-report tr:hover>td{box-shadow:0 0 1px 0 #268bd2 inset}._sage-report tr:hover var{color:#2aa198}._sage-report ul._sage-tabs li._sage-active-tab{height:20px;line-height:17px}._sage-trace ._sage-source{line-height:14px}._sage-trace ._sage-source span{padding-right:1px;border-right:3px inset #268bd2}._sage-trace ._sage-source ._sage-highlight{background:#eee8d5}._sage-trace b{min-width:18px;display:inline-block;text-align:right;margin-right:6px;color:#586e75}._sage-trace ._sage-blacklisted>b,._sage-trace ._sage-childless>b{margin-right:22px}._sage-trace ._sage-blacklisted{filter:brightness(120%)}._sage-trace ._sage-parent>var>a{color:#268bd2}._sage-focused{box-shadow:0 0 3px 2px #859900 inset;border-radius:7px}._sage-microtime,._sage-color-preview{box-shadow:0 0 2px 0 #b6cedb;height:16px;text-align:center;text-shadow:-1px 0 #839496,0 1px #839496,1px 0 #839496,0 -1px #839496;width:230px;color:#fdf6e3}._sage>dl>dt,._sage ul._sage-tabs{box-shadow:4px 0 2px -3px #268bd2 inset}._sage ul._sage-tabs li._sage-active-tab{padding-top:7px;height:34px} 2 | -------------------------------------------------------------------------------- /resources/css/base.scss: -------------------------------------------------------------------------------- 1 | // 2 | // VARIABLES FOR THEMES TO OVERRIDE 3 | // -------------------------------------------------- 4 | 5 | $spacing: 4; 6 | 7 | // 8 | // SET UP HELPER VARIABLES 9 | // -------------------------------------------------- 10 | 11 | $border: 1px solid $border-color; 12 | 13 | @mixin selection() { 14 | background: $border-color-hover; 15 | color: $text-color; 16 | } 17 | 18 | // 19 | // BASE STYLES 20 | // -------------------------------------------------- 21 | 22 | ._sage::selection { 23 | @include selection; 24 | } 25 | 26 | 27 | ._sage, 28 | ._sage::before, 29 | ._sage::after, 30 | ._sage *, 31 | ._sage *::before, 32 | ._sage *::after { 33 | box-sizing: border-box; 34 | border-radius: 0; 35 | color: $text-color; 36 | float: none !important; 37 | font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace; 38 | line-height: 15px; 39 | margin: 0; 40 | padding: 0; 41 | text-align: left; 42 | } 43 | 44 | ._sage { 45 | font-size: 13px; 46 | margin: $spacing * 2px 0; 47 | overflow-x: auto; 48 | white-space: nowrap; 49 | 50 | // invisible symbols 51 | u { 52 | color: #ff8c00; 53 | } 54 | 55 | dt { 56 | background: $main-background; 57 | border: $border; 58 | color: $text-color; 59 | display: block; 60 | font-weight: bold; 61 | list-style: none outside none; 62 | overflow: auto; 63 | padding: $spacing * 1px; 64 | 65 | &:hover { 66 | border-color: $border-color-hover; 67 | } 68 | 69 | 70 | } 71 | 72 | > dl dl { 73 | padding: 0 0 0 $spacing * 3px; 74 | } 75 | 76 | // 77 | // DROPDOWN CARET 78 | // -------------------------------------------------- 79 | 80 | nav { 81 | background: $caret-image no-repeat scroll 0 0 transparent; 82 | cursor: pointer; 83 | display: inline-block; 84 | height: 15px; 85 | width: 15px; 86 | margin-right: 3px; 87 | vertical-align: middle; 88 | } 89 | 90 | dt._sage-parent { 91 | cursor: pointer; 92 | } 93 | 94 | dt._sage-parent:hover nav { 95 | background-position: 0 -15px; 96 | } 97 | 98 | dt._sage-parent._sage-show:hover > nav { 99 | background-position: 0 -45px; 100 | } 101 | 102 | dt._sage-show > nav { 103 | background-position: 0 -30px; 104 | } 105 | 106 | dt._sage-parent + dd { 107 | display: none; 108 | border-left: 1px dashed $border-color; 109 | } 110 | 111 | dt._sage-parent._sage-show + dd { 112 | display: block; 113 | } 114 | 115 | // 116 | // INDIVIDUAL ITEMS 117 | // -------------------------------------------------- 118 | 119 | var, 120 | var a { 121 | color: $variable-type-color; 122 | font-style: normal; 123 | } 124 | 125 | dt:hover var, 126 | dt:hover var a { 127 | color: $variable-type-color-hover; 128 | } 129 | 130 | dfn { 131 | font-style: normal; 132 | font-weight: normal; 133 | color: $variable-name-color; 134 | } 135 | 136 | pre { 137 | color: $text-color; 138 | margin: 0 0 0 $spacing * 3px; 139 | padding: 5px; 140 | overflow-y: hidden; 141 | border-top: 0; 142 | border: $border; 143 | background: $main-background; 144 | display: block; 145 | word-break: normal; 146 | } 147 | 148 | ._sage-popup-trigger { 149 | float: right !important; 150 | cursor: pointer; 151 | color: $border-color-hover; 152 | 153 | &:hover { 154 | color: $border-color; 155 | } 156 | } 157 | 158 | dt._sage-parent > ._sage-popup-trigger { 159 | font-size: 13px; 160 | } 161 | 162 | footer { 163 | padding: 0 3px 3px; 164 | font-size: 9px; 165 | color: #999; 166 | text-shadow: 0 0 1px #ccc; 167 | 168 | > ._sage-popup-trigger { 169 | font-size: 12px; 170 | } 171 | 172 | nav { 173 | background-size: 10px; 174 | height: 10px; 175 | width: 10px; 176 | 177 | &:hover { 178 | background-position: 0 -10px; 179 | } 180 | } 181 | 182 | > ol { 183 | display: none; 184 | margin-left: 32px; 185 | } 186 | 187 | li { 188 | color: #999; 189 | } 190 | 191 | &._sage-show { 192 | > ol { 193 | display: block; 194 | } 195 | 196 | nav { 197 | background-position: 0 -20px; 198 | 199 | &:hover { 200 | background-position: 0 -30px; 201 | } 202 | } 203 | } 204 | } 205 | 206 | a { 207 | color: $text-color; 208 | text-shadow: none; 209 | 210 | &:hover { 211 | color: $variable-name-color; 212 | border-bottom: 1px dotted $variable-name-color; 213 | } 214 | } 215 | 216 | // 217 | // TABS 218 | // -------------------------------------------------- 219 | 220 | ul { 221 | list-style: none; 222 | padding-left: $spacing * 3px; 223 | 224 | &:not(._sage-tabs) { 225 | li { 226 | border-left: 1px dashed $border-color; 227 | 228 | > dl { 229 | border-left: none; 230 | } 231 | } 232 | } 233 | 234 | &._sage-tabs { 235 | margin: 0 0 0 $spacing * 3px; 236 | padding-left: 0; 237 | background: $main-background; 238 | border: $border; 239 | border-top: 0; 240 | 241 | li { 242 | background: $secondary-background; 243 | border: $border; 244 | cursor: pointer; 245 | display: inline-block; 246 | height: $spacing * 6px; 247 | margin: calc($spacing / 2) * 1px; 248 | padding: 0 2px + round($spacing * 2.5px); 249 | vertical-align: top; 250 | 251 | &:hover, 252 | &._sage-active-tab:hover { 253 | border-color: $border-color-hover; 254 | color: $variable-type-color-hover; 255 | } 256 | 257 | &._sage-active-tab { 258 | background: $main-background; 259 | border-top: 0; 260 | margin-top: -2px; 261 | height: 27px; 262 | line-height: 24px; 263 | } 264 | 265 | &:not(._sage-active-tab) { 266 | line-height: $spacing * 5px; 267 | } 268 | } 269 | 270 | li + li { 271 | margin-left: 0 272 | } 273 | } 274 | 275 | &:not(._sage-tabs) > li:not(:first-child) { 276 | display: none; 277 | } 278 | } 279 | 280 | dt:hover + dd > ul > li._sage-active-tab { 281 | border-color: $border-color-hover; 282 | color: $variable-type-color-hover; 283 | } 284 | } 285 | 286 | // 287 | // REPORT 288 | // -------------------------------------------------- 289 | 290 | ._sage-report { 291 | border-collapse: collapse; 292 | empty-cells: show; 293 | border-spacing: 0; 294 | 295 | * { 296 | font-size: 12px; 297 | } 298 | 299 | dt { 300 | background: none; 301 | padding: calc($spacing/2) * 1px; 302 | 303 | ._sage-parent { 304 | min-width: 100%; 305 | overflow: hidden; 306 | text-overflow: ellipsis; 307 | white-space: nowrap; 308 | } 309 | } 310 | 311 | td, 312 | th { 313 | border: $border; 314 | padding: calc($spacing/2) * 1px; 315 | vertical-align: center; 316 | } 317 | 318 | th { 319 | cursor: alias; 320 | } 321 | 322 | td:first-child, 323 | th { 324 | font-weight: bold; 325 | background: $secondary-background; 326 | color: $variable-name-color; 327 | } 328 | 329 | td { 330 | background: $main-background; 331 | white-space: pre; 332 | 333 | > dl { 334 | padding: 0; 335 | } 336 | } 337 | 338 | pre { 339 | border-top: 0; 340 | border-right: 0; 341 | } 342 | 343 | th:first-child { 344 | background: none; 345 | border: 0; 346 | } 347 | 348 | td._sage-empty { 349 | background: #d33682 !important; 350 | } 351 | 352 | tr:hover { 353 | > td { 354 | box-shadow: 0 0 1px 0 $border-color-hover inset; 355 | } 356 | 357 | var { 358 | color: $variable-type-color-hover; 359 | } 360 | } 361 | 362 | ul._sage-tabs li._sage-active-tab { 363 | height: 20px; 364 | line-height: 17px; 365 | } 366 | } 367 | 368 | // 369 | // TRACE 370 | // -------------------------------------------------- 371 | ._sage-trace { 372 | ._sage-source { 373 | line-height: round($spacing * 3.5) * 1px; 374 | 375 | span { 376 | padding-right: 1px; 377 | border-right: 3px inset $variable-type-color; 378 | } 379 | 380 | ._sage-highlight { 381 | background: $secondary-background; 382 | } 383 | } 384 | 385 | b { // line number 386 | min-width: 18px; 387 | display: inline-block; 388 | text-align: right; 389 | margin-right: 6px; 390 | color: $variable-name-color; 391 | } 392 | 393 | ._sage-blacklisted, 394 | ._sage-childless { 395 | > b { // line number 396 | margin-right: 22px; 397 | } 398 | } 399 | 400 | ._sage-blacklisted { 401 | filter: brightness(120%); 402 | } 403 | 404 | ._sage-parent { 405 | > var { 406 | > a { 407 | color: $variable-type-color; 408 | } 409 | } 410 | } 411 | } 412 | 413 | // 414 | // MISC 415 | // -------------------------------------------------- 416 | 417 | // keyboard navigation caret 418 | ._sage-focused { 419 | @include keyboard-caret; 420 | } 421 | 422 | ._sage-microtime, 423 | ._sage-color-preview { 424 | box-shadow: 0 0 2px 0 #b6cedb; 425 | height: 16px; 426 | text-align: center; 427 | text-shadow: -1px 0 #839496, 0 1px #839496, 1px 0 #839496, 0 -1px #839496; 428 | width: 230px; 429 | color: #fdf6e3; 430 | } 431 | -------------------------------------------------------------------------------- /resources/css/themes/aante-light.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ante Aljinovic https://github.com/aljinovic 3 | */ 4 | 5 | $main-background: #f8f8f8; 6 | $secondary-background: #f8f8f8; 7 | 8 | $text-color: #1d1e1e; 9 | $variable-name-color: #1d1e1e; 10 | $variable-type-color: #06f; 11 | $variable-type-color-hover: #f00; 12 | 13 | $border-color: #d7d7d7; 14 | $border-color-hover: #aaa; 15 | 16 | $caret-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAA8CAYAAAC5OOBJAAAAvklEQVR42mNgGMFAS0uLLSQk5HxoaOh/XDg4OLgdpwEBAQEGQAN+YtMIFH/i4ODAg9cFQNMbsWkOCgrKJMv5ID5Qipko/6M7PzAw0ImkAIQ5H0hvISv0gZpP+fn5qZAVfcBAkmEYBaNZcjRLjmbJUUCvqAIlDlAiASUWkjWDkiU0eTaSpBGUEZBy1E9QRiFWLzO2LEmU80GZHkcRhN/5oGIGVNzgKIbwOx9UwOErAIl2/igYzZKjWXI0S9IHAAASJijo0Ypj8AAAAABJRU5ErkJggg=="); 17 | 18 | @mixin keyboard-caret() { 19 | box-shadow: 0 0 3px 2px $variable-type-color-hover; 20 | } 21 | 22 | @import "../base"; 23 | 24 | ._sage { 25 | dt { 26 | font-weight: normal; 27 | margin-top: 4px; 28 | } 29 | 30 | > dl { 31 | background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0, #fff 15px); 32 | } 33 | 34 | dl dl { 35 | margin-top: 4px; 36 | padding-left: 25px; 37 | border-left: none; 38 | } 39 | 40 | > dl > dt { 41 | background: $secondary-background; 42 | } 43 | 44 | // 45 | // TABS 46 | // -------------------------------------------------- 47 | 48 | ul { 49 | margin: 0; 50 | padding-left: 0; 51 | 52 | &:not(._sage-tabs) > li { 53 | border-left: 0; 54 | } 55 | 56 | &._sage-tabs { 57 | background: $secondary-background; 58 | border: $border; 59 | border-width: 0 1px 1px 1px; 60 | padding: 4px 0 0 12px; 61 | margin-left: -1px; 62 | margin-top: -1px; 63 | 64 | li, 65 | li + li { 66 | margin: 0 0 0 4px; 67 | } 68 | 69 | li { 70 | border-bottom-width: 0; 71 | height: $spacing * 6px + 1px; 72 | 73 | 74 | &:first-child { 75 | margin-left: 0; 76 | } 77 | 78 | &._sage-active-tab { 79 | border-top: $border; 80 | background: #fff; 81 | font-weight: bold; 82 | padding-top: 0; 83 | border-bottom: 1px solid #fff !important; 84 | margin-bottom: -1px; 85 | } 86 | 87 | &._sage-active-tab:hover { 88 | border-bottom: 1px solid #fff; 89 | } 90 | } 91 | } 92 | 93 | > li > pre { 94 | border: $border; 95 | } 96 | } 97 | 98 | dt:hover + dd > ul { 99 | border-color: $border-color-hover; 100 | } 101 | 102 | pre { 103 | background: #fff; 104 | margin-top: 4px; 105 | margin-left: 25px; 106 | } 107 | 108 | ._sage-popup-trigger:hover { 109 | color: $variable-type-color-hover; 110 | } 111 | 112 | ._sage-source ._sage-highlight { 113 | background: #cfc; 114 | } 115 | 116 | ._sage-source span { 117 | border-right: 3px inset #268bd2; 118 | } 119 | } 120 | 121 | // 122 | // REPORT 123 | // -------------------------------------------------- 124 | 125 | ._sage-report { 126 | td { 127 | background: #fff; 128 | 129 | > dl { 130 | padding: 0; 131 | margin: 0; 132 | 133 | > dt._sage-parent { 134 | margin: 0; 135 | } 136 | } 137 | } 138 | 139 | td:first-child, 140 | td, 141 | th { 142 | padding: 2px 4px; 143 | } 144 | 145 | td._sage-empty { 146 | background: $border-color !important; 147 | } 148 | 149 | dd, dt { 150 | background: #fff; 151 | } 152 | 153 | tr:hover > td { 154 | box-shadow: none; 155 | background: #cfc; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /resources/css/themes/original-light.scss: -------------------------------------------------------------------------------- 1 | $main-background: #f1f5f7; 2 | $secondary-background: #e3ecf0; 3 | 4 | $text-color: #1d1e1e; 5 | $variable-name-color: #1d1e1e; 6 | $variable-type-color: #0092db; 7 | $variable-type-color-hover: #5cb730; 8 | 9 | $border-color: #b6cedb; 10 | $border-color-hover: #0092db; 11 | 12 | $caret-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAA8CAMAAACO5hB7AAAACVBMVEX///82b4tctzBls0NMAAAAQklEQVR42rWRIQ4AAAgCwf8/2qgER2BeuwJsgAqIzekMfMryPL9XQSkobE6vwKcsz/N7FfPvh09lnPf/b+7673+f0uHuAZ/EdkNQAAAAAElFTkSuQmCC"); 13 | 14 | 15 | @mixin keyboard-caret() { 16 | box-shadow: 0 0 3px 2px #5cb730; 17 | } 18 | 19 | @import "../base"; 20 | 21 | ._sage { 22 | > dl > dt, 23 | ul._sage-tabs { 24 | background: linear-gradient(to bottom, #fff 0, $main-background 100%); 25 | } 26 | 27 | dl:not(:last-of-type) dt:not(._sage-show) { 28 | border-bottom: 0; 29 | } 30 | 31 | //._sage dl._sage-trace dd ul._sage-tabs li._sage-active-tab 32 | & ul._sage-tabs li { 33 | background: $secondary-background; 34 | } 35 | 36 | & > dl._sage-trace { 37 | > dt { 38 | background: linear-gradient(to bottom, $main-background 0, #fff 100%); 39 | } 40 | 41 | dt:not(:last-of-type):not(._sage-show) { 42 | border-bottom: 0; 43 | } 44 | 45 | > dd > ul._sage-tabs > li._sage-active-tab { 46 | background: #fff; 47 | } 48 | } 49 | 50 | ._sage-source ._sage-highlight { 51 | background: #f0eb96; 52 | } 53 | 54 | ._sage-source > div { 55 | margin-top: -1px; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /resources/css/themes/original.scss: -------------------------------------------------------------------------------- 1 | $main-background: #e0eaef; 2 | $secondary-background: #c1d4df; 3 | 4 | $text-color: #1d1e1e; 5 | $variable-name-color: #1d1e1e; 6 | $variable-type-color: #0092db; 7 | $variable-type-color-hover: #5cb730; 8 | 9 | $border-color: #b6cedb; 10 | $border-color-hover: #0092db; 11 | 12 | $caret-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAA8CAMAAACO5hB7AAAACVBMVEX///82b4tctzBls0NMAAAAQklEQVR42rWRIQ4AAAgCwf8/2qgER2BeuwJsgAqIzekMfMryPL9XQSkobE6vwKcsz/N7FfPvh09lnPf/b+7673+f0uHuAZ/EdkNQAAAAAElFTkSuQmCC"); 13 | 14 | 15 | @mixin keyboard-caret() { 16 | box-shadow: 0 0 3px 2px #5cb730; 17 | } 18 | 19 | @import "../base"; 20 | 21 | ._sage { 22 | > dl > dt { 23 | background: linear-gradient(to bottom, #e3ecf0 0, #c0d4df 100%); 24 | } 25 | 26 | ul._sage-tabs { 27 | background: linear-gradient(to bottom, #9dbed0 0px, #b2ccda 100%); 28 | } 29 | 30 | & > dl:not(._sage-trace) > dd > ul._sage-tabs li { 31 | background: $main-background; 32 | 33 | &._sage-active-tab { 34 | background: $secondary-background; 35 | } 36 | } 37 | 38 | & > dl._sage-trace > dt { 39 | background: linear-gradient(to bottom, #c0d4df 0px, #e3ecf0 100%); 40 | } 41 | 42 | ._sage-source ._sage-highlight { 43 | background: #f0eb96; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /resources/css/themes/solarized-dark.scss: -------------------------------------------------------------------------------- 1 | $spacing: 5; 2 | 3 | $main-background: #002b36; // base 03 4 | $secondary-background: #073642; // base 02 5 | 6 | $text-color: #839496; // base 0 7 | $variable-name-color: #93a1a1; // base 1 8 | $variable-type-color: #268bd2; // blue 9 | $variable-type-color-hover: #2aa198; // cyan 10 | 11 | $border-color: #586e75; // base 01 12 | $border-color-hover: #268bd2; // blue 13 | 14 | 15 | $caret-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAA8CAMAAACO5hB7AAAA7VBMVEVYbnWToaH///9YbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnWToaGToaGToaGToaGToaGToaGToaGToaGToaFYbnWToaFYbnVYbnWToaGToaFYbnWToaFYbnWToaFYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnWToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaFYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnWToaGToaGToaGToaGToaGToaGToaGToaGToaFYbnWToaEeRtj1AAAATXRSTlMAAACLab0B7iP2R7CL9iPuaUewAb36F0/QnPoX0JxPYQaurepi6QV8FhV7MzFhM6586RYFMa3qYnsVBpNQ+ZGOGJ3PkM9QkZAY+Y6TnV7gnYsAAAEcSURBVHhebdHVbgMxEEBRu8sUZmqapBBoSuGUmWb+/3NayxtrLOe+Hck745XZgd6/eb3CVcKt7HmR+gKxlCGOETG5vFbOoahR6NxI5zHt6nuq+2dqnqfzzH3mfcz70vb8H6MJt5u6a96hS30E4PjEFgAEp2fKNojKUfVEOoS044ex7u3YPE/nmfvM+5j3pe17T9oe/77O41w+n4vnrbrwZxbTshVhvtx5Kb8vliRLRWmeSQSTjJq/El5x5fUXYmNN9hcQC5y4g/hGvVksNtT8451rns2IScb7mn567ll2GNpWr9YWfvQgzWsKs8HOA/m960g6rjTzA8HAV/NHwiOmPLwDKA/J/gggYsRVgFvqbr/fpWYv90zzZKKs9Qfx00Jx65oxLAAAAABJRU5ErkJggg=="); 16 | 17 | @mixin keyboard-caret() { 18 | box-shadow: 0 0 3px 2px #859900 inset; // green 19 | border-radius: 7px; 20 | } 21 | 22 | @import "../base"; 23 | 24 | body { 25 | background: $secondary-background; 26 | color: #fff; // for non-_sage elements to remain at least semi-readable 27 | } 28 | 29 | ._sage { 30 | background: $secondary-background; 31 | box-shadow: 0 0 5px 3px $secondary-background; 32 | 33 | > dl > dt, 34 | ul._sage-tabs { 35 | box-shadow: 4px 0 2px -3px $variable-type-color inset; 36 | } 37 | } 38 | 39 | ._sage ul._sage-tabs li._sage-active-tab { 40 | padding-top: 7px; 41 | height: 34px; 42 | } 43 | -------------------------------------------------------------------------------- /resources/css/themes/solarized.scss: -------------------------------------------------------------------------------- 1 | $spacing: 5; 2 | 3 | $main-background: #fdf6e3; // base 3 4 | $secondary-background: #eee8d5; // base 2 5 | 6 | $text-color: #657b83; // base 00 7 | $variable-name-color: #586e75; // base 01 8 | $variable-type-color: #268bd2; // blue 9 | $variable-type-color-hover: #2aa198; // cyan 10 | 11 | $border-color: #93a1a1; // base 1 12 | $border-color-hover: #268bd2; // blue 13 | 14 | 15 | $caret-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAA8CAMAAACO5hB7AAAA7VBMVEVYbnWToaH///9YbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnWToaGToaGToaGToaGToaGToaGToaGToaGToaFYbnWToaFYbnVYbnWToaGToaFYbnWToaFYbnWToaFYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnWToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaFYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnWToaGToaGToaGToaGToaGToaGToaGToaGToaFYbnWToaEeRtj1AAAATXRSTlMAAACLab0B7iP2R7CL9iPuaUewAb36F0/QnPoX0JxPYQaurepi6QV8FhV7MzFhM6586RYFMa3qYnsVBpNQ+ZGOGJ3PkM9QkZAY+Y6TnV7gnYsAAAEcSURBVHhebdHVbgMxEEBRu8sUZmqapBBoSuGUmWb+/3NayxtrLOe+Hck745XZgd6/eb3CVcKt7HmR+gKxlCGOETG5vFbOoahR6NxI5zHt6nuq+2dqnqfzzH3mfcz70vb8H6MJt5u6a96hS30E4PjEFgAEp2fKNojKUfVEOoS044ex7u3YPE/nmfvM+5j3pe17T9oe/77O41w+n4vnrbrwZxbTshVhvtx5Kb8vliRLRWmeSQSTjJq/El5x5fUXYmNN9hcQC5y4g/hGvVksNtT8451rns2IScb7mn567ll2GNpWr9YWfvQgzWsKs8HOA/m960g6rjTzA8HAV/NHwiOmPLwDKA/J/gggYsRVgFvqbr/fpWYv90zzZKKs9Qfx00Jx65oxLAAAAABJRU5ErkJggg=="); 16 | 17 | @mixin keyboard-caret() { 18 | box-shadow: 0 0 3px 2px #859900 inset; // green 19 | border-radius: 7px; 20 | } 21 | 22 | @import "../base"; 23 | 24 | ._sage > dl > dt, 25 | ._sage ul._sage-tabs { 26 | box-shadow: 4px 0 2px -3px $variable-type-color inset; 27 | } 28 | 29 | ._sage ul._sage-tabs li._sage-active-tab { 30 | padding-top: 7px; 31 | height: 34px; 32 | } 33 | -------------------------------------------------------------------------------- /resources/js/base.js: -------------------------------------------------------------------------------- 1 | if (typeof _sageInitialized === 'undefined') { 2 | _sageInitialized = 1; 3 | const _sage = { 4 | visiblePluses: [], // all visible toggle carets 5 | currentPlus: -1, // currently selected caret 6 | pinnedParents: [], // fixed to top parents 7 | 8 | debounce: (callback, wait = 100) => { 9 | let timeoutId; 10 | return (...args) => { 11 | clearTimeout(timeoutId); 12 | timeoutId = setTimeout(() => callback(...args), wait) 13 | }; 14 | }, 15 | 16 | processScroll: function () { 17 | let i = _sage.pinnedParents.length; 18 | while (i--) { 19 | let el = _sage.pinnedParents[i] 20 | let pinnedClone = el.sageClone 21 | if (window.scrollY <= pinnedClone.sageMinY || window.scrollY >= pinnedClone.sageMaxY) { 22 | _sage.pinnedParents.splice(i, 1) 23 | pinnedClone.remove() 24 | el.sageClone = null 25 | } 26 | } 27 | 28 | let all = document.querySelectorAll('._sage') 29 | i = all.length; 30 | while (i--) { 31 | const sage = all[i] 32 | 33 | if (!_sage.isPinningNeeded(sage)) { 34 | return; 35 | } 36 | 37 | let totalPinnedHeight = 0 38 | sage.querySelectorAll('._sage-clone').forEach(function (clone) { 39 | totalPinnedHeight += clone.offsetHeight 40 | }) 41 | sage.querySelectorAll('dt._sage-parent._sage-show:not(._sage-clone)').forEach(function (parent) { 42 | if (_sage.pinnedParents.includes(parent)) { 43 | return; 44 | } 45 | 46 | const children = parent.nextElementSibling; //
47 | 48 | if (!_sage.isPinningNeeded(children)) { 49 | return; 50 | } 51 | 52 | // we create a pinned clone to float - for many technical reasons :) 53 | const clone = parent.cloneNode(true); 54 | const rect = children.getBoundingClientRect(); 55 | 56 | clone.sageMinY = rect.top + window.scrollY; 57 | clone.sageMaxY = rect.bottom + window.scrollY; 58 | clone.style.width = rect.width + 'px' 59 | clone.style.top = totalPinnedHeight + 'px' 60 | clone.style.position = 'fixed' 61 | clone.style.opacity = 0.9 62 | clone.classList.add('_sage-clone') 63 | 64 | totalPinnedHeight += parent.offsetHeight 65 | parent.sageClone = clone 66 | _sage.pinnedParents.push(parent) 67 | parent.after(clone) 68 | }) 69 | 70 | // my brain & time management restrictions do not support side by side sage outputs 71 | break; 72 | } 73 | }, 74 | 75 | isPinningNeeded: function (el) { 76 | const rect = el.getBoundingClientRect() 77 | 78 | if (rect.height === 0) { 79 | return false; 80 | } 81 | 82 | 83 | // if top is lower than the viewport 84 | if (rect.top >= window.innerHeight) { 85 | return false 86 | } 87 | 88 | // if top is visible we don't need to scroll it to view 89 | if (rect.top > 0) { 90 | return false 91 | } 92 | 93 | // if bottom is above view port, we scrolled passed it and it's invisible now 94 | if (rect.bottom < 25) { 95 | return false 96 | } 97 | 98 | return true 99 | }, 100 | 101 | selectText: function (element) { 102 | const selection = window.getSelection(), 103 | range = document.createRange(); 104 | 105 | range.selectNodeContents(element); 106 | selection.removeAllRanges(); 107 | selection.addRange(range); 108 | }, 109 | 110 | each: function (selector, callback) { 111 | Array.prototype.slice.call(document.querySelectorAll(selector), 0).forEach(callback) 112 | }, 113 | 114 | hasClass: function (target, className = '_sage-show') { 115 | if (!target.classList) { 116 | return false; 117 | } 118 | 119 | return target.classList.contains(className); 120 | }, 121 | 122 | addClass: function (target, className = '_sage-show') { 123 | target.classList.add(className); 124 | }, 125 | 126 | removeClass: function (target, className = '_sage-show') { 127 | target.classList.remove(className); 128 | return target; 129 | }, 130 | 131 | next: function (element) { 132 | do { 133 | element = element.nextElementSibling; 134 | } while (element && element.tagName !== 'DD'); 135 | 136 | return element; 137 | }, 138 | 139 | toggle: function (element, hide) { 140 | if (typeof hide === 'undefined') { 141 | hide = _sage.hasClass(element); 142 | } 143 | 144 | if (hide) { 145 | _sage.removeClass(element); 146 | } else { 147 | _sage.addClass(element); 148 | } 149 | 150 | // also open up child element if there's only one 151 | let parent = _sage.next(element); 152 | if (parent && parent.childElementCount === 1) { 153 | parent = parent.children[0]; // reuse variable cause I can 154 | 155 | // parent is checked in case of empty
 when array("\n") is dumped
156 |                 if (parent && _sage.hasClass(parent, '_sage-parent')) {
157 |                     _sage.toggle(parent, hide)
158 |                 }
159 |             }
160 |         },
161 | 
162 |         toggleChildren: function (element, hide) {
163 |             const parent = _sage.next(element)
164 |                 , nodes = parent.getElementsByClassName('_sage-parent');
165 |             let i = nodes.length;
166 | 
167 |             if (typeof hide === 'undefined') {
168 |                 hide = _sage.hasClass(element);
169 |             }
170 | 
171 |             while (i--) {
172 |                 _sage.toggle(nodes[i], hide);
173 |             }
174 |             _sage.toggle(element, hide);
175 |         },
176 | 
177 |         toggleAll: function (show) {
178 |             const elements = document.getElementsByClassName('_sage-parent')
179 |             let i = elements.length
180 | 
181 |             while (i--) {
182 |                 if (show) {
183 |                     _sage.addClass(elements[i]);
184 |                 } else {
185 |                     _sage.removeClass(elements[i]);
186 |                 }
187 |             }
188 |         },
189 | 
190 |         switchAllToNextTraceTab: function () {
191 |             document.querySelectorAll('._sage-trace>dd>._sage-tabs>._sage-active-tab').forEach(function (element) {
192 |                 const nextTab = element.nextSibling;
193 |                 if (nextTab) {
194 |                     _sage.switchTab(nextTab);
195 |                 }
196 |             })
197 |         },
198 | 
199 |         switchTab: function (target) {
200 |             let lis, el = target, index = 0;
201 | 
202 |             _sage.removeClass(
203 |                 target.parentNode.getElementsByClassName('_sage-active-tab')[0],
204 |                 '_sage-active-tab'
205 |             );
206 |             _sage.addClass(target, '_sage-active-tab');
207 | 
208 |             // take the index of clicked title tab and make the same n-th content tab visible
209 |             while (el = el.previousSibling) {
210 |                 el.nodeType === 1 && index++;
211 |             }
212 | 
213 |             lis = target.parentNode.nextSibling.childNodes;
214 |             for (let i = 0; i < lis.length; i++) {
215 |                 lis[i].style.display = i === index ? 'block' : 'none';
216 |             }
217 |         },
218 | 
219 |         isInsideSage: function (el) {
220 |             for (; ;) {
221 |                 // if it's a pinned clone scroll to original element on click
222 |                 if (_sage.hasClass(el, '_sage-clone')) {
223 |                     let scrollTo = el.offsetHeight;
224 |                     if (el.style.top !== '0px') {
225 |                         scrollTo *= 2;
226 |                     }
227 |                     window.scroll(0, el.sageMinY - scrollTo);
228 |                     return false;
229 |                 }
230 | 
231 |                 el = el.parentNode;
232 |                 if (!el || _sage.hasClass(el, '_sage')) {
233 |                     break;
234 |                 }
235 |             }
236 | 
237 |             return !!el;
238 |         },
239 | 
240 |         fetchVisiblePluses: function () {
241 |             _sage.visiblePluses = [];
242 |             _sage.each('._sage nav, ._sage-tabs>li:not(._sage-active-tab)', function (el) {
243 |                 if (el.offsetWidth !== 0 || el.offsetHeight !== 0) {
244 |                     _sage.visiblePluses.push(el)
245 |                 }
246 |             });
247 |         },
248 | 
249 |         // some custom implementations screw up the JS when they see  or 
250 |         // this method survives minification
251 |         tag: function (contents) {
252 |             return '<' + contents + '>';
253 |         },
254 | 
255 |         openInNewWindow: function (_sageContainer) {
256 |             let newWindow;
257 | 
258 |             if (newWindow = window.open()) {
259 |                 newWindow.document.open();
260 |                 newWindow.document.write(
261 |                     _sage.tag('html')
262 |                     + _sage.tag('head')
263 |                     + 'Sage ☯ (' + new Date().toISOString() + ')'
264 |                     + _sage.tag('meta charset="utf-8"')
265 |                     + document.getElementsByClassName('_sage-js')[0].outerHTML
266 |                     + document.getElementsByClassName('_sage-css')[0].outerHTML
267 |                     + _sage.tag('/head')
268 |                     + _sage.tag('body')
269 |                     + ''
270 |                     + '
' 271 | + _sageContainer.parentNode.outerHTML 272 | + '
' 273 | + _sage.tag('/body') 274 | ); 275 | newWindow.document.close(); 276 | } 277 | }, 278 | 279 | sortTable: function (table, column, header) { 280 | const tbody = table.tBodies[0]; 281 | 282 | const collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'}); 283 | 284 | const direction = (typeof header.sage_direction === 'undefined') ? 1 : header.sage_direction 285 | header.sage_direction = -1 * direction; 286 | 287 | [].slice.call(table.tBodies[0].rows) 288 | .sort(function (a, b) { 289 | return direction * collator.compare(a.cells[column].textContent, b.cells[column].textContent) 290 | }) 291 | .forEach(function (el) { 292 | tbody.appendChild(el); 293 | }); 294 | }, 295 | 296 | keyCallBacks: { 297 | cleanup: function (i) { 298 | const focusedClass = '_sage-focused'; 299 | const prevElement = document.querySelector('.' + focusedClass); 300 | prevElement && _sage.removeClass(prevElement, focusedClass); 301 | 302 | if (i !== -1) { 303 | const el = _sage.visiblePluses[i]; 304 | _sage.addClass(el, focusedClass); 305 | 306 | const offsetTop = function (el) { 307 | return el.offsetTop + (el.offsetParent ? offsetTop(el.offsetParent) : 0); 308 | }; 309 | 310 | const top = offsetTop(el) - (window.innerHeight / 2); 311 | window.scrollTo(0, top); 312 | } 313 | 314 | _sage.currentPlus = i; 315 | }, 316 | 317 | moveCursor: function (up, i) { 318 | if (up) { 319 | if (--i < 0) { 320 | i = _sage.visiblePluses.length - 1; 321 | } 322 | } else { 323 | if (++i >= _sage.visiblePluses.length) { 324 | i = 0; 325 | } 326 | } 327 | 328 | _sage.keyCallBacks.cleanup(i); 329 | return false; 330 | } 331 | } 332 | }; 333 | 334 | window.addEventListener('click', function (e) { 335 | let target = e.target 336 | , tagName = target.tagName; 337 | 338 | if (!_sage.isInsideSage(target)) { 339 | return; 340 | } 341 | 342 | // auto-select name of variable 343 | if (tagName === 'DFN') { 344 | _sage.selectText(target); 345 | target = target.parentNode; 346 | } else if (tagName === 'VAR') { // stupid workaround for misc elements 347 | target = target.parentNode; // to not stop event from further propagating 348 | tagName = target.tagName; 349 | } else if (tagName === 'TH') { 350 | if (!e.ctrlKey) { 351 | _sage.sortTable(target.parentNode.parentNode.parentNode, target.cellIndex, target) 352 | } 353 | return false; 354 | } 355 | 356 | // switch tabs 357 | if (tagName === 'LI' && _sage.hasClass(target.parentNode, '_sage-tabs')) { 358 | if (!_sage.hasClass(target, '_sage-active-tab')) { 359 | _sage.switchTab(target); 360 | if (_sage.currentPlus !== -1) { 361 | _sage.fetchVisiblePluses(); 362 | } 363 | } 364 | return false; 365 | } 366 | 367 | // handle clicks on the navigation caret 368 | if (tagName === 'NAV') { 369 | // special case for nav in footer 370 | if (target.parentNode.tagName === 'FOOTER') { 371 | target = target.parentNode; 372 | _sage.toggle(target) 373 | } else { 374 | // ensure doubleclick has different behaviour, see below 375 | setTimeout(function () { 376 | const timer = parseInt(target._sageTimer, 10); 377 | if (timer > 0) { 378 | target._sageTimer--; 379 | } else { 380 | _sage.toggleChildren(target.parentNode); //
381 | if (_sage.currentPlus !== -1) { 382 | _sage.fetchVisiblePluses(); 383 | } 384 | } 385 | }, 300); 386 | } 387 | 388 | e.stopPropagation(); 389 | return false; 390 | } else if (_sage.hasClass(target, '_sage-parent')) { 391 | _sage.toggle(target); 392 | if (_sage.currentPlus !== -1) { 393 | _sage.fetchVisiblePluses(); 394 | } 395 | return false; 396 | } else if (_sage.hasClass(target, '_sage-ide-link')) { 397 | e.preventDefault() 398 | fetch(target.href); 399 | return false; 400 | } else if (_sage.hasClass(target, '_sage-popup-trigger')) { 401 | let _sageContainer = target.parentNode; 402 | if (_sageContainer.tagName === 'FOOTER') { 403 | _sageContainer = _sageContainer.previousSibling; 404 | } else { 405 | while (_sageContainer && !_sage.hasClass(_sageContainer, '_sage-parent')) { 406 | _sageContainer = _sageContainer.parentNode; 407 | } 408 | } 409 | 410 | _sage.openInNewWindow(_sageContainer); 411 | } else if (tagName === 'PRE' && e.detail === 3) { // triple click pre to select it all 412 | _sage.selectText(target); 413 | } 414 | }, false); 415 | 416 | window.addEventListener('dblclick', function (e) { 417 | const target = e.target; 418 | if (!_sage.isInsideSage(target)) { 419 | return; 420 | } 421 | 422 | if (target.tagName === 'NAV') { 423 | target._sageTimer = 2; 424 | _sage.toggleAll(_sage.hasClass(target)); 425 | if (_sage.currentPlus !== -1) { 426 | _sage.fetchVisiblePluses(); 427 | } 428 | e.stopPropagation(); 429 | } 430 | }, false); 431 | 432 | // keyboard navigation 433 | window.onkeydown = function (e) { // direct assignment is used to have priority over ex FAYT 434 | // todo use e.key https://www.toptal.com/developers/keycode 435 | const keyCode = e.keyCode; 436 | let currentPlus = _sage.currentPlus; 437 | 438 | // user pressed ctrl+f 439 | if (keyCode === 70 && e.ctrlKey) { 440 | _sage.toggleAll(true); 441 | // we are probably more interested in the Arguments, or Callee object tab in traces, whichever exists 442 | _sage.switchAllToNextTraceTab(true); 443 | return; 444 | } 445 | 446 | // do nothing if alt/ctrl key is pressed or if we're actually typing somewhere 447 | if (['INPUT', 'TEXTAREA'].includes(e.target.tagName) || e.altKey || e.ctrlKey) { 448 | return; 449 | } 450 | 451 | if (keyCode === 9) { // TAB jumps out of navigation 452 | _sage.keyCallBacks.cleanup(-1); 453 | 454 | return; 455 | // todo 's' too 456 | } else if (keyCode === 68) { // 'd' : toggles navigation on/off 457 | if (currentPlus === -1) { 458 | _sage.fetchVisiblePluses(); 459 | return _sage.keyCallBacks.moveCursor(false, currentPlus); 460 | } else { 461 | _sage.keyCallBacks.cleanup(-1); 462 | return false; 463 | } 464 | } else { 465 | if (currentPlus === -1) { 466 | return; 467 | } 468 | 469 | if (keyCode === 38) { // ARROW UP : moves up 470 | return _sage.keyCallBacks.moveCursor(true, currentPlus); 471 | } else if (keyCode === 40) { // ARROW DOWN : down 472 | return _sage.keyCallBacks.moveCursor(false, currentPlus); 473 | } 474 | } 475 | 476 | 477 | let currentNav = _sage.visiblePluses[currentPlus]; 478 | if (currentNav.tagName === 'LI') { // we're on a trace tab 479 | if (keyCode === 32 || keyCode === 13) { // SPACE/ENTER 480 | _sage.switchTab(currentNav); 481 | _sage.fetchVisiblePluses(); 482 | return _sage.keyCallBacks.moveCursor(true, currentPlus); 483 | } else if (keyCode === 39) { // arrows 484 | return _sage.keyCallBacks.moveCursor(false, currentPlus); 485 | } else if (keyCode === 37) { 486 | return _sage.keyCallBacks.moveCursor(true, currentPlus); 487 | } 488 | } 489 | 490 | // we are on a regular/footer [+] 491 | 492 | currentNav = currentNav.parentNode; // simple dump 493 | 494 | if (currentNav.tagName === 'FOOTER') { 495 | if (keyCode === 32 || keyCode === 13) { // SPACE/ENTER : toggles 496 | _sage.toggle(currentNav); 497 | 498 | return false; 499 | } 500 | } 501 | 502 | if (keyCode === 32 || keyCode === 13) { // SPACE/ENTER : toggles 503 | _sage.toggle(currentNav); 504 | _sage.fetchVisiblePluses(); 505 | return false; 506 | } else if (keyCode === 39 || keyCode === 37) { // ARROW LEFT/RIGHT : respectively hides/shows and traverses 507 | const visible = _sage.hasClass(currentNav); 508 | const hide = keyCode === 37; 509 | 510 | if (visible) { 511 | _sage.toggleChildren(currentNav, hide); // expand/collapse all children if immediate ones are showing 512 | } else { 513 | if (hide) { // LEFT 514 | // traverse to parent and THEN hide 515 | do { 516 | currentNav = currentNav.parentNode 517 | } while (currentNav && currentNav.tagName !== 'DD'); 518 | 519 | if (currentNav) { 520 | currentNav = currentNav.previousElementSibling; 521 | 522 | currentPlus = -1; 523 | const parentPlus = currentNav.querySelector('nav'); 524 | while (parentPlus !== _sage.visiblePluses[++currentPlus]) { 525 | } 526 | _sage.keyCallBacks.cleanup(currentPlus) 527 | } else { // we are at root 528 | currentNav = _sage.visiblePluses[currentPlus].parentNode; 529 | } 530 | } 531 | _sage.toggle(currentNav, hide); 532 | } 533 | _sage.fetchVisiblePluses(); 534 | return false; 535 | } 536 | }; 537 | 538 | window.addEventListener('load', function () { // colorize microtime results relative to others 539 | const elements = Array.prototype.slice.call(document.querySelectorAll('._sage-microtime'), 0); 540 | let min = Infinity 541 | , max = -Infinity; 542 | 543 | elements.forEach(function (el) { 544 | const val = parseFloat(el.innerHTML); 545 | 546 | if (min > val) { 547 | min = val; 548 | } 549 | if (max < val) { 550 | max = val; 551 | } 552 | }); 553 | 554 | elements.forEach(function (el) { 555 | const val = parseFloat(el.innerHTML); 556 | const ratio = 1 - (val - min) / (max - min); 557 | 558 | el.style.background = 'hsl(' + Math.round(ratio * 120) + ',60%,70%)'; 559 | }); 560 | }); 561 | 562 | window.addEventListener('scroll', _sage.debounce(_sage.processScroll)); 563 | } 564 | 565 | // debug purposes only, removed in minified source 566 | function clg(i) { 567 | if (!window.console) { 568 | return; 569 | } 570 | const l = arguments.length; 571 | let o = 0; 572 | while (o < l) console.log(arguments[o++]) 573 | } 574 | --------------------------------------------------------------------------------