1: <?php
2:
3: /*
4: * The MIT License (MIT)
5: *
6: * Copyright (c) 2013 Jonathan Vollebregt (jnvsor@gmail.com), Rokas Šleinius (raveren@gmail.com)
7: *
8: * Permission is hereby granted, free of charge, to any person obtaining a copy of
9: * this software and associated documentation files (the "Software"), to deal in
10: * the Software without restriction, including without limitation the rights to
11: * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
12: * the Software, and to permit persons to whom the Software is furnished to do so,
13: * subject to the following conditions:
14: *
15: * The above copyright notice and this permission notice shall be included in all
16: * copies or substantial portions of the Software.
17: *
18: * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19: * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
20: * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
21: * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
22: * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23: * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24: */
25:
26: namespace Kint\Renderer;
27:
28: use Kint\Kint;
29: use Kint\Object\BasicObject;
30: use Kint\Object\BlobObject;
31: use Kint\Object\InstanceObject;
32: use Kint\Object\Representation\Representation;
33: use Kint\Utils;
34:
35: class RichRenderer extends Renderer
36: {
37: /**
38: * RichRenderer object plugins should implement Kint\Renderer\Rich\ObjectPluginInterface.
39: */
40: public static $object_plugins = array(
41: 'blacklist' => 'Kint\\Renderer\\Rich\\BlacklistPlugin',
42: 'callable' => 'Kint\\Renderer\\Rich\\CallablePlugin',
43: 'closure' => 'Kint\\Renderer\\Rich\\ClosurePlugin',
44: 'color' => 'Kint\\Renderer\\Rich\\ColorPlugin',
45: 'depth_limit' => 'Kint\\Renderer\\Rich\\DepthLimitPlugin',
46: 'recursion' => 'Kint\\Renderer\\Rich\\RecursionPlugin',
47: 'simplexml_element' => 'Kint\\Renderer\\Rich\\SimpleXMLElementPlugin',
48: 'trace_frame' => 'Kint\\Renderer\\Rich\\TraceFramePlugin',
49: );
50:
51: /**
52: * RichRenderer tab plugins should implement Kint\Renderer\Rich\TabPluginInterface.
53: */
54: public static $tab_plugins = array(
55: 'binary' => 'Kint\\Renderer\\Rich\\BinaryPlugin',
56: 'color' => 'Kint\\Renderer\\Rich\\ColorPlugin',
57: 'docstring' => 'Kint\\Renderer\\Rich\\DocstringPlugin',
58: 'microtime' => 'Kint\\Renderer\\Rich\\MicrotimePlugin',
59: 'source' => 'Kint\\Renderer\\Rich\\SourcePlugin',
60: 'table' => 'Kint\\Renderer\\Rich\\TablePlugin',
61: 'timestamp' => 'Kint\\Renderer\\Rich\\TimestampPlugin',
62: );
63:
64: public static $pre_render_sources = array(
65: 'script' => array(
66: array('Kint\\Renderer\\RichRenderer', 'renderJs'),
67: array('Kint\\Renderer\\Rich\\MicrotimePlugin', 'renderJs'),
68: ),
69: 'style' => array(
70: array('Kint\\Renderer\\RichRenderer', 'renderCss'),
71: ),
72: 'raw' => array(),
73: );
74:
75: /**
76: * Whether or not to render access paths.
77: *
78: * Access paths can become incredibly heavy with very deep and wide
79: * structures. Given mostly public variables it will typically make
80: * up one quarter of the output HTML size.
81: *
82: * If this is an unacceptably large amount and your browser is groaning
83: * under the weight of the access paths - your first order of buisiness
84: * should be to get a new browser. Failing that, use this to turn them off.
85: *
86: * @var bool
87: */
88: public static $access_paths = true;
89:
90: /**
91: * The maximum length of a string before it is truncated.
92: *
93: * Falsey to disable
94: *
95: * @var int
96: */
97: public static $strlen_max = 80;
98:
99: /**
100: * Path to the CSS file to load by default.
101: *
102: * @var string
103: */
104: public static $theme = 'original.css';
105:
106: /**
107: * Assume types and sizes don't need to be escaped.
108: *
109: * Turn this off if you use anything but ascii in your class names,
110: * but it'll cause a slowdown of around 10%
111: *
112: * @var bool
113: */
114: public static $escape_types = false;
115:
116: /**
117: * Move all dumps to a folder at the bottom of the body.
118: *
119: * @var bool
120: */
121: public static $folder = true;
122:
123: /**
124: * Sort mode for object properties.
125: *
126: * @var int
127: */
128: public static $sort = self::SORT_NONE;
129:
130: public static $needs_pre_render = true;
131: public static $needs_folder_render = true;
132:
133: public static $always_pre_render = false;
134:
135: protected $plugin_objs = array();
136: protected $expand = false;
137: protected $force_pre_render = false;
138: protected $pre_render;
139: protected $use_folder;
140:
141: public function __construct()
142: {
143: $this->pre_render = self::$needs_pre_render;
144: $this->use_folder = self::$folder;
145:
146: if (self::$always_pre_render) {
147: $this->setForcePreRender();
148: }
149: }
150:
151: public function setCallInfo(array $info)
152: {
153: parent::setCallInfo($info);
154:
155: if (\in_array('!', $this->call_info['modifiers'], true)) {
156: $this->setExpand(true);
157: $this->use_folder = false;
158: }
159:
160: if (\in_array('@', $this->call_info['modifiers'], true)) {
161: $this->setForcePreRender();
162: }
163: }
164:
165: public function setStatics(array $statics)
166: {
167: parent::setStatics($statics);
168:
169: if (!empty($statics['expanded'])) {
170: $this->setExpand(true);
171: }
172:
173: if (!empty($statics['return'])) {
174: $this->setForcePreRender();
175: }
176: }
177:
178: public function setExpand($expand)
179: {
180: $this->expand = $expand;
181: }
182:
183: public function getExpand()
184: {
185: return $this->expand;
186: }
187:
188: public function setForcePreRender()
189: {
190: $this->force_pre_render = true;
191: $this->pre_render = true;
192: }
193:
194: public function setPreRender($pre_render)
195: {
196: $this->setForcePreRender(); // TODO: Remove line in next major version
197: $this->pre_render = $pre_render;
198: }
199:
200: public function getPreRender()
201: {
202: return $this->pre_render;
203: }
204:
205: public function setUseFolder($use_folder)
206: {
207: $this->use_folder = $use_folder;
208: }
209:
210: public function getUseFolder()
211: {
212: return $this->use_folder;
213: }
214:
215: public function render(BasicObject $o)
216: {
217: if ($plugin = $this->getPlugin(self::$object_plugins, $o->hints)) {
218: if (\strlen($output = $plugin->renderObject($o))) {
219: return $output;
220: }
221: }
222:
223: $children = $this->renderChildren($o);
224: $header = $this->renderHeaderWrapper($o, (bool) \strlen($children), $this->renderHeader($o));
225:
226: return '<dl>'.$header.$children.'</dl>';
227: }
228:
229: public function renderNothing()
230: {
231: return '<dl><dt><var>No argument</var></dt></dl>';
232: }
233:
234: public function renderHeaderWrapper(BasicObject $o, $has_children, $contents)
235: {
236: $out = '<dt';
237:
238: if ($has_children) {
239: $out .= ' class="kint-parent';
240:
241: if ($this->expand) {
242: $out .= ' kint-show';
243: }
244:
245: $out .= '"';
246: }
247:
248: $out .= '>';
249:
250: if (self::$access_paths && $o->depth > 0 && $ap = $o->getAccessPath()) {
251: $out .= '<span class="kint-access-path-trigger" title="Show access path">&rlarr;</span>';
252: }
253:
254: if ($has_children) {
255: $out .= '<span class="kint-popup-trigger" title="Open in new window">&boxbox;</span>';
256:
257: if (0 === $o->depth) {
258: $out .= '<span class="kint-search-trigger" title="Show search box">&telrec;</span>';
259: $out .= '<input type="text" class="kint-search" value="">';
260: }
261:
262: $out .= '<nav></nav>';
263: }
264:
265: $out .= $contents;
266:
267: if (!empty($ap)) {
268: $out .= '<div class="access-path">'.$this->escape($ap).'</div>';
269: }
270:
271: return $out.'</dt>';
272: }
273:
274: public function renderHeader(BasicObject $o)
275: {
276: $output = '';
277:
278: if (null !== ($s = $o->getModifiers())) {
279: $output .= '<var>'.$s.'</var> ';
280: }
281:
282: if (null !== ($s = $o->getName())) {
283: $output .= '<dfn>'.$this->escape($s).'</dfn> ';
284:
285: if ($s = $o->getOperator()) {
286: $output .= $this->escape($s, 'ASCII').' ';
287: }
288: }
289:
290: if (null !== ($s = $o->getType())) {
291: if (self::$escape_types) {
292: $s = $this->escape($s);
293: }
294:
295: if ($o->reference) {
296: $s = '&amp;'.$s;
297: }
298:
299: $output .= '<var>'.$s.'</var> ';
300: }
301:
302: if (null !== ($s = $o->getSize())) {
303: if (self::$escape_types) {
304: $s = $this->escape($s);
305: }
306: $output .= '('.$s.') ';
307: }
308:
309: if (null !== ($s = $o->getValueShort())) {
310: $s = \preg_replace('/\\s+/', ' ', $s);
311:
312: if (self::$strlen_max) {
313: $s = Utils::truncateString($s, self::$strlen_max);
314: }
315:
316: $output .= $this->escape($s);
317: }
318:
319: return \trim($output);
320: }
321:
322: public function renderChildren(BasicObject $o)
323: {
324: $contents = array();
325: $tabs = array();
326:
327: foreach ($o->getRepresentations() as $rep) {
328: $result = $this->renderTab($o, $rep);
329: if (\strlen($result)) {
330: $contents[] = $result;
331: $tabs[] = $rep;
332: }
333: }
334:
335: if (empty($tabs)) {
336: return '';
337: }
338:
339: $output = '<dd>';
340:
341: if (1 === \count($tabs) && $tabs[0]->labelIsImplicit()) {
342: $output .= \reset($contents);
343: } else {
344: $output .= '<ul class="kint-tabs">';
345:
346: foreach ($tabs as $i => $tab) {
347: if (0 === $i) {
348: $output .= '<li class="kint-active-tab">';
349: } else {
350: $output .= '<li>';
351: }
352:
353: $output .= $this->escape($tab->getLabel()).'</li>';
354: }
355:
356: $output .= '</ul><ul>';
357:
358: foreach ($contents as $tab) {
359: $output .= '<li>'.$tab.'</li>';
360: }
361:
362: $output .= '</ul>';
363: }
364:
365: return $output.'</dd>';
366: }
367:
368: public function preRender()
369: {
370: $output = '';
371:
372: if ($this->pre_render) {
373: foreach (self::$pre_render_sources as $type => $values) {
374: $contents = '';
375: foreach ($values as $v) {
376: $contents .= \call_user_func($v, $this);
377: }
378:
379: if (!\strlen($contents)) {
380: continue;
381: }
382:
383: switch ($type) {
384: case 'script':
385: $output .= '<script class="kint-rich-script">'.$contents.'</script>';
386: break;
387: case 'style':
388: $output .= '<style class="kint-rich-style">'.$contents.'</style>';
389: break;
390: default:
391: $output .= $contents;
392: }
393: }
394:
395: // Don't pre-render on every dump
396: if (!$this->force_pre_render) {
397: self::$needs_pre_render = false;
398: }
399: }
400:
401: $output .= '<div class="kint-rich';
402:
403: if ($this->use_folder) {
404: $output .= ' kint-file';
405:
406: if (self::$needs_folder_render || $this->force_pre_render) {
407: $output = $this->renderFolder().$output;
408:
409: if (!$this->force_pre_render) {
410: self::$needs_folder_render = false;
411: }
412: }
413: }
414:
415: $output .= '">';
416:
417: return $output;
418: }
419:
420: public function postRender()
421: {
422: if (!$this->show_trace) {
423: return '</div>';
424: }
425:
426: $output = '<footer>';
427: $output .= '<span class="kint-popup-trigger" title="Open in new window">&boxbox;</span> ';
428:
429: if (!empty($this->call_info['trace']) && \count($this->call_info['trace']) > 1) {
430: $output .= '<nav></nav>';
431: }
432:
433: if (isset($this->call_info['callee']['file'])) {
434: $output .= 'Called from '.$this->ideLink(
435: $this->call_info['callee']['file'],
436: $this->call_info['callee']['line']
437: );
438: }
439:
440: if (isset($this->call_info['callee']['function']) && (
441: !empty($this->call_info['callee']['class']) ||
442: !\in_array(
443: $this->call_info['callee']['function'],
444: array('include', 'include_once', 'require', 'require_once'),
445: true
446: )
447: )
448: ) {
449: $output .= ' [';
450: if (isset($this->call_info['callee']['class'])) {
451: $output .= $this->call_info['callee']['class'];
452: }
453: if (isset($this->call_info['callee']['type'])) {
454: $output .= $this->call_info['callee']['type'];
455: }
456: $output .= $this->call_info['callee']['function'].'()]';
457: }
458:
459: if (!empty($this->call_info['trace']) && \count($this->call_info['trace']) > 1) {
460: $output .= '<ol>';
461: foreach ($this->call_info['trace'] as $index => $step) {
462: if (!$index) {
463: continue;
464: }
465:
466: $output .= '<li>'.$this->ideLink($step['file'], $step['line']); // closing tag not required
467: if (isset($step['function'])
468: && !\in_array($step['function'], array('include', 'include_once', 'require', 'require_once'), true)
469: ) {
470: $output .= ' [';
471: if (isset($step['class'])) {
472: $output .= $step['class'];
473: }
474: if (isset($step['type'])) {
475: $output .= $step['type'];
476: }
477: $output .= $step['function'].'()]';
478: }
479: }
480: $output .= '</ol>';
481: }
482:
483: $output .= '</footer></div>';
484:
485: return $output;
486: }
487:
488: public function escape($string, $encoding = false)
489: {
490: if (false === $encoding) {
491: $encoding = BlobObject::detectEncoding($string);
492: }
493:
494: $original_encoding = $encoding;
495:
496: if (false === $encoding || 'ASCII' === $encoding) {
497: $encoding = 'UTF-8';
498: }
499:
500: $string = \htmlspecialchars($string, ENT_NOQUOTES, $encoding);
501:
502: // this call converts all non-ASCII characters into numeirc htmlentities
503: if (\function_exists('mb_encode_numericentity') && 'ASCII' !== $original_encoding) {
504: $string = \mb_encode_numericentity($string, array(0x80, 0xffff, 0, 0xffff), $encoding);
505: }
506:
507: return $string;
508: }
509:
510: public function ideLink($file, $line)
511: {
512: $path = $this->escape(Kint::shortenPath($file)).':'.$line;
513: $ideLink = Kint::getIdeLink($file, $line);
514:
515: if (!$ideLink) {
516: return $path;
517: }
518:
519: $class = '';
520:
521: if (\preg_match('/https?:\\/\\//i', $ideLink)) {
522: $class = 'class="kint-ide-link" ';
523: }
524:
525: return '<a '.$class.'href="'.$this->escape($ideLink).'">'.$path.'</a>';
526: }
527:
528: protected function renderTab(BasicObject $o, Representation $rep)
529: {
530: if ($plugin = $this->getPlugin(self::$tab_plugins, $rep->hints)) {
531: if (\strlen($output = $plugin->renderTab($rep))) {
532: return $output;
533: }
534: }
535:
536: if (\is_array($rep->contents)) {
537: $output = '';
538:
539: if ($o instanceof InstanceObject && 'properties' === $rep->getName()) {
540: foreach (self::sortProperties($rep->contents, self::$sort) as $obj) {
541: $output .= $this->render($obj);
542: }
543: } else {
544: foreach ($rep->contents as $obj) {
545: $output .= $this->render($obj);
546: }
547: }
548:
549: return $output;
550: }
551:
552: if (\is_string($rep->contents)) {
553: $show_contents = false;
554:
555: // If it is the value representation of a string and its whitespace
556: // was truncated in the header, always display the full string
557: if ('string' !== $o->type || $o->value !== $rep) {
558: $show_contents = true;
559: } else {
560: if (\preg_match('/(:?[\\r\\n\\t\\f\\v]| {2})/', $rep->contents)) {
561: $show_contents = true;
562: } elseif (self::$strlen_max && BlobObject::strlen($o->getValueShort()) > self::$strlen_max) {
563: $show_contents = true;
564: }
565:
566: if (empty($o->encoding)) {
567: $show_contents = false;
568: }
569: }
570:
571: if ($show_contents) {
572: return '<pre>'.$this->escape($rep->contents)."\n</pre>";
573: }
574: }
575:
576: if ($rep->contents instanceof BasicObject) {
577: return $this->render($rep->contents);
578: }
579: }
580:
581: protected function getPlugin(array $plugins, array $hints)
582: {
583: if ($plugins = $this->matchPlugins($plugins, $hints)) {
584: $plugin = \end($plugins);
585:
586: if (!isset($this->plugin_objs[$plugin])) {
587: $this->plugin_objs[$plugin] = new $plugin($this);
588: }
589:
590: return $this->plugin_objs[$plugin];
591: }
592: }
593:
594: protected static function renderJs()
595: {
596: return \file_get_contents(KINT_DIR.'/resources/compiled/shared.js').\file_get_contents(KINT_DIR.'/resources/compiled/rich.js');
597: }
598:
599: protected static function renderCss()
600: {
601: if (\file_exists(KINT_DIR.'/resources/compiled/'.self::$theme)) {
602: return \file_get_contents(KINT_DIR.'/resources/compiled/'.self::$theme);
603: }
604:
605: return \file_get_contents(self::$theme);
606: }
607:
608: protected static function renderFolder()
609: {
610: return '<div class="kint-rich kint-folder"><dl><dt class="kint-parent"><nav></nav>Kint</dt><dd class="kint-folder"></dd></dl></div>';
611: }
612: }
613: