<?php
namespace Engine\Debug\ParserCallTrace;

class ParserCallTraceDevelopment
{
	protected $options = [
		'maxStringLength' => 			150,	// PHP_INT_MAX
		'useBackgroundColors' => 		'call-based', // false, 'depth-based', 'call-based'
		'classHandlers' => 				[],
		'methodHandlers' => 			[],
		'dataHandlers' => 				[],
	];

	protected $trace = [
		'callstack' => [],
	];

	protected $dlog = null;

	public function __construct()
	{
		$this->dlog = new \Engine\Debug\Dline();
		$this->dlog->setReportingOption('memory', 'none');
		$this->dlog->setReportingOption('timestamp', 'none');

		$this->doPrint($this->generateCSS());
	}

	protected function doPrint($str)
	{
		echo $str;
		flush(); ob_flush();
	}

	/**
	 * Destroy the trace object
	 */
	public function destroy()
	{
		$this->doPrint(sprintf("<h1>%s shutdown</h1>", __CLASS__));
		$this->doPrint(sprintf("<pre>\$this->trace: %s\n\n</pre>", var_export($this->trace, true)));
	}

	/**
	 * Set the max string length
	 * @return \Engine\Debug\ParserCallTrace\ParserCallTraceDevelopment
	 */
	public function setMaxStringLength($maxStringLength)
	{
		$this->options['maxStringLength'] = $maxStringLength;

		return $this;
	}

	/**
	 * Get the max string length
	 * @return number
	 */
	public function getMaxStringLength()
	{
		return $this->options['maxStringLength'];
	}

	/**
	 * Add a class callback handler
	 * @return \Engine\Debug\ParserCallTrace\ParserCallTraceDevelopment
	 */
	public function addClassHandler($callback)
	{
		$this->options['classHandlers'][]= $callback;

		return $this;
	}

	/**
	 * Add a method callback handler
	 * @return \Engine\Debug\ParserCallTrace\ParserCallTraceDevelopment
	 */
	public function addMethodHandler($callback)
	{
		$this->options['methodHandlers'][]= $callback;

		return $this;
	}

	/**
	 * Add a data callback handler
	 * @return \Engine\Debug\ParserCallTrace\ParserCallTraceDevelopment
	 */
	public function addDataHandler($callback)
	{
		$this->options['dataHandlers'][]= $callback;

		return $this;
	}

	/**
	 * Reset internal accumulated data, output any needed closing tags
	 */
	public function reset()
	{
		if ($this->trace['callstack']) {
			$stackSize = count($this->trace['callstack']);
			for ($i=0; $i<$stackSize; $i++) {
				$this->logCallEnd();
			}
		}
	}

	/**
	 * Log a function call enter
	 * @param string $mainMethod
	 * @param array $args
	 * @return \Engine\Debug\ParserCallTrace\ParserCallTraceDevelopment
	 */
	public function logCallStart($calledObj, $staticMethod, $args)
	{
		/*
		 * called like this:
		 * 		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallStart($this, __METHOD__, func_get_args());
		 * $calledObj = $this
		 * $staticMethodName = __METHOD__
		 */


		$calledObjClass = get_class($calledObj);
		$matches = [];
		preg_match("/^(?P<class>[^:]+)::(?P<method>.+)$/", $staticMethod, $matches);
		$staticMethodName =  $matches['method'];
		$staticMethodClass = $matches['class'];


		$classSettings = [
			'display' => 		true,
			'label' => 			null,
		];
		foreach ($this->options['classHandlers'] as $handler) {
			$classSettings = call_user_func($handler, $classSettings, [
				'calledObjClass' => 	$calledObjClass,
				'staticMethodClass' => 	$staticMethodClass,
				'callstack' => 			$this->trace['callstack'],
			]);
		}

		$thisClassLabel = ($classSettings['label']?:$calledObjClass);

		$argsStr = "";
		$argNames = [];
		try {
			$reflectionMethod =  new \ReflectionMethod($calledObjClass, $staticMethodName);
			$reflArgs = $reflectionMethod->getParameters();
			$argNames = array_map(function( $item ){
				return $item->getName();
			}, $reflArgs);
		}
		catch(\ReflectionException $ex) {
			$argsStr = sprintf("<span class='args unknown'>unknown args</span>");
		}


		$methodSettings = [
			'display' => 		true,
			'display:args' => 	true,	// TODO: selectively hide some args
			'label' => 			null,
		];
		foreach ($this->options['methodHandlers'] as $handler) {
			$methodSettings = call_user_func($handler, $methodSettings, [
				'calledObjClass' => 	$calledObjClass,
				'staticMethodClass' => 	$staticMethodClass,
				'staticMethodName' => 	$staticMethodName,
				'argNames' => 			$argNames,
				'args' => 				$args,
			]);
		}

		if ($args) {
			$argsArr = [];
			foreach ($argNames as $argk=>$argn) {
				if ($methodSettings['display:args']===false) {
					$argsArr[]= sprintf("<span class='arg arg-%d'><span class='argn'>$%s</span>:<span class='argv var'>%s</span></span>", $argk, $argn, '*');
				}
				else {
					$argsArr[]= sprintf("<span class='arg arg-%d'><span class='argn'>$%s</span>:<span class='argv var'>%s</span></span>", $argk, $argn, $this->renderVariable_value(null, $args[$argk], 'default'));
				}
			}
			$argsStr = sprintf("<span class='args'>%s</span>", implode(", ", $argsArr));
		}

		$this->trace['callstack'][] = [
			'time' => 				microtime(true),
			'staticMethodName' => 	$staticMethodName,
			'staticMethodClass' => 	$staticMethodClass,
			'calledObjClass' => 	$calledObjClass,
			'doDisplay' => 			!($methodSettings['display']===false || $classSettings['display']===false),
		];

		$lastMethod = end($this->trace['callstack']);
		if ($lastMethod['doDisplay']) {
			$this->trace['displays']++;
			$bg = '';
			$logId = '';
			if ($this->options['useBackgroundColors']=='depth-based') {
				$bg = sprintf("background-color: %s;", \App\Smarty\Functions\Theme::pickUniqueColor(['cycles'=>10, 'key'=>count($this->trace['callstack']), ], null));
				$logId = count($this->trace['callstack']);
			}
			elseif ($this->options['useBackgroundColors']=='call-based') {
				$bg = sprintf("background-color: %s;", \App\Smarty\Functions\Theme::pickUniqueColor(['cycles'=>10, 'key'=>$this->trace['displays'], ], null));
				$logId = sprintf("%s/%05d", substr(md5($this->trace['displays']), 0, 4), $this->trace['displays']);
			}

			$this->doPrint(sprintf("<div class='logCallStart depth-%d' style='%s'><span class='logId'>%s</span>", count($this->trace['callstack']), $bg, $logId));
			$this->doPrint(sprintf("<div class='call'><span class='class'>%s</span>-><span class='method'>%s</span>(%s)</div>", $thisClassLabel, $staticMethodName, $argsStr));
		}

		return $this;
	}

	/**
	 * Log a call end. Will automatically pop the last entry in the stack
	 * @return \Engine\Debug\ParserCallTrace\ParserCallTraceDevelopment
	 */
	public function logCallEnd()
	{
		$lastMethod = array_pop($this->trace['callstack']);
		#if (!$lastMethod) {
		#	printf("%s : %s<br/>\n", __FILE__, __LINE__); die();
		#}

		if ($lastMethod['doDisplay']) {
			$this->doPrint($this->_logDuration($lastMethod));

			$this->doPrint(sprintf("</div>"));	// should close logCallStart
		}

		return $this;
	}

	public function _logDuration(&$lastMethod)
	{
		$t = microtime(true);

		if ($lastMethod['last_callee']) {
			$duration = $t - $lastMethod['last_callee'];
		}
		else {
			$duration = $t - $lastMethod['time'];
		}

		if ($lastMethod['doDisplay'] && $duration>0.1) {
			return sprintf("<span class='duration'>(took %.6fs)</span>", $duration);
		}

		return '';
	}

	protected function _callee()
	{
		end($this->trace['callstack']);
		$lastMethod = &$this->trace['callstack'][key($this->trace['callstack'])];
		return $lastMethod;
	}

	protected function _callee_mark_call($t)
	{
		end($this->trace['callstack']);
		$lastMethod = &$this->trace['callstack'][key($this->trace['callstack'])];
		$lastMethod['last_callee'] = $t;
		return $lastMethod;
	}



	/**
	 * Log some sort of input data. Skips null values
	 * @param string $style
	 * @return \Engine\Debug\ParserCallTrace\ParserCallTraceDevelopment
	 */
	public function logInput($args, $style='default')
	{
		$lastMethod = $this->_callee();
		if ($lastMethod['doDisplay']) {
			$this->doPrint($this->logData($args, 'logInput', $style, true));
		}
		return $this;
	}

	/**
	 * Log some sort of input data
	 * @param string $style
	 * @return \Engine\Debug\ParserCallTrace\ParserCallTraceDevelopment
	 */
	public function logInputWithNull($args, $style='default')
	{
		$lastMethod = $this->_callee();
		if ($lastMethod['doDisplay']) {
			$this->doPrint($this->logData($args, 'logInput', $style, false));
		}
		return $this;
	}

	/**
	 * Log some sort of output data. Skips null values
	 * @param string $style
	 * @return \Engine\Debug\ParserCallTrace\ParserCallTraceDevelopment
	 */
	public function logOutput($args, $style='default')
	{
		$lastMethod = $this->_callee();
		if ($lastMethod['doDisplay']) {
			$this->doPrint($this->logData($args, 'logOutput', $style, true));
		}
		return $this;
	}

	/**
	 * Log some sort of output data
	 * @param string $style
	 * @return \Engine\Debug\ParserCallTrace\ParserCallTraceDevelopment
	 */
	public function logOutputWithNull($args, $style='default')
	{
		$lastMethod = $this->_callee();
		if ($lastMethod['doDisplay']) {
			$this->doPrint($this->logData($args, 'logOutput', $style, false));
		}
		return $this;
	}

	/**
	 * Log some sort of output data
	 * @param string $style
	 * @return \Engine\Debug\ParserCallTrace\ParserCallTraceDevelopment
	 */
	public function logStep($msg, $style='default')
	{
		$lastMethod = $this->_callee();
		if ($lastMethod['doDisplay']) {
			$this->doPrint("<div class='logStep {$style} var'> &#x22D9; <span class='value string'>{$msg}{$this->_logDuration($lastMethod)}</span></div>");
			$lastMethod = $this->_callee_mark_call(microtime(true));
		}
		return $this;
	}

	/**
	 * Log a separator
	 * @param string $style
	 * @return \Engine\Debug\ParserCallTrace\ParserCallTraceDevelopment
	 */
	public function logSeparator($style='default')
	{
		$lastMethod = $this->_callee();
		if ($lastMethod['doDisplay']) {
			$this->doPrint("<hr class='{$style}'/>");
		}
		return $this;
	}

	/**
	 * Display a log line
	 * @param string $text
	 * @param array $params
	 * @return \Engine\Debug\ParserCallTrace\ParserCallTraceDevelopment
	 */
	public function dlog($text, $params = [])
	{
		$lastMethod = $this->_callee();
		if ($lastMethod['doDisplay']) {
			$this->doPrint($this->dlog->vsprintf($text, $params));
		}
		return $this;
	}

	/**
	 * Log general data
	 */
	protected function logData($args, $mainStyle, $style, $skipNulls)
	{
		$output = "";
		$lastMethod = end($this->trace['callstack']);
		if ($lastMethod['doDisplay']) {
			$dataSettings = [
				'display' => 	true,
				'args' => 		array_fill_keys(array_keys($args), true),
			];
			foreach ($this->options['dataHandlers'] as $handler) {
				$dataSettings = call_user_func($handler, $dataSettings, [
					'calledObjClass' => 	$lastMethod['calledObjClass'],
					'staticMethodClass' => 	$lastMethod['staticMethodClass'],
					'staticMethodName' => 	$lastMethod['staticMethodName'],
				]);
			}

			if ($dataSettings['display']) {
				$output = "<div class='{$mainStyle} {$style}'>";
				$displayedKeys = array_intersect(array_keys(array_filter($dataSettings['args'])), array_keys($args));
				foreach ($displayedKeys as $k) {
					$v = $args[$k];
					$render = true;

					if ($skipNulls) {
						if ($v===null) {
							$render = false;
						}
					}

					if ($render) {
						$output.= $this->renderVariable($k, $v, $style);
					}
				}
				$output.= "</div>";
			}
		}

		$this->doPrint($output);
	}

	/**
	 * Intetrnal: render a variable
	 * @param string $k
	 * @param mixed $v
	 * @return string
	 */
	protected function renderVariable($k, $v, $style)
	{
		return sprintf("<div class='var'><span class='key'>%s</span> : %s</div>", $k, $this->renderVariable_value($k, $v, $style));
	}

	/**
	 * Intetrnal: render a variable
	 */
	protected function renderVariable_value($k, $v, $style)
	{
		$vstr = null;
		$vcls = [];
		if (is_bool($v)) {
			$vs = ($v?'true':'false');
			$vcls[] = "bool";
			$vcls[] = "value-{$vs}";
			$vstr = sprintf("<span class='value %s'>%s</span>", implode(" ", $vcls), $vs);
		}
		elseif (is_null($v)) {
			$vcls[] = "null";
			$vstr = sprintf("<span class='value %s'>%s</span>", implode(" ", $vcls), 'null');
		}
		elseif (is_int($v)) {
			$vcls[] = "numeric";
			$vstr = sprintf("<span class='value %s'>%s</span>", implode(" ", $vcls), $v);
		}
		elseif (is_float($v)) {
			$vcls[] = "numeric";
			$vstr = sprintf("<span class='value %s'>%.4f</span>", implode(" ", $vcls), $v);
		}
		elseif (is_array($v)) {
			$vcls[] = "array";
			$vstr = sprintf("<span class='value %s'>%s</span>", implode(" ", $vcls), var_export($v, true));
		}
		elseif (is_object($v)) {
			$vcls[] = "array";
			$vstr = sprintf("<span class='value %s'>%s</span>", implode(" ", $vcls), get_class($v));
		}
		else {		// is_string
			if ($style=='regexp') {
				$vstr = $this->renderVariable_regexp($k, $v, $style);
			}
			else {
				$lines = explode("\n", $v);
				if (count($lines)>1) {
					$vstr = $this->renderVariable_string_multiline($k, $v, $style, $lines);
				}
				else {
					$vstr = $this->renderVariable_string_singleline($k, $v, $style);
				}
			}
		}

		return $vstr;
	}

	/**
	 * Render variable handling special characters
	 * @param string $v
	 * @return mixed
	 */
	protected function renderVariable_string_replace_special_chars($v)
	{
		$nonAsciiChars = [];
		for ($i=0; $i<strlen($v); $i++) {
			if ($v[$i]<' ' || $v[$i]>'~') {
				$nonAsciiChars[$v[$i]]++;
			}
		}

		$inputTruncated = false;
		if (strlen($v)>$this->options['maxStringLength']) {
			$inputTruncated = true;
			$v = substr($v, 0, $this->options['maxStringLength']);
		}

		$vstr = htmlentities($v);

		if (strlen($v)>$this->options['maxStringLength']) {
			$v = substr($v, 0, $this->options['maxStringLength'])."<span class=''>...</span>";
		}

		foreach ($nonAsciiChars as $chr=>$x) {
			$replacements[$chr] = sprintf("<span class='specialChar hex'>x%02x</span>", ord($chr));
		}

		$replacements["\n"] = "<span class='specialChar'>\\n</span>";
		$replacements["\r"] = "<span class='specialChar'>\\r</span>";
		$replacements["\t"] = "<span class='specialChar'>\\t</span>";

		if ($inputTruncated) {
			$vstr.= "<span class='specialChar sign-truncated'>...</span>";
		}

		return str_replace(array_keys($replacements), array_values($replacements), $vstr);
	}

	/**
	 * Intetrnal: render a variable
	 * @param string $k
	 * @param mixed $v
	 * @return string
	 */
	protected function renderVariable_string_multiline($k, $v, $style, $lines)
	{
		$vcls[] = "string";

		$vstr = sprintf("'<span class='value %s'>%s</span>'", implode(" ", $vcls), $this->renderVariable_string_replace_special_chars($v));
		return $vstr;
	}

	/**
	 * Intetrnal: render a variable
	 * @param string $k
	 * @param mixed $v
	 * @return string
	 */
	protected function renderVariable_string_singleline($k, $v, $style)
	{
		if ($style=='regexp') {
			$vstr = $this->renderVariable_regexp($k, $v, $style);
		}
		else {
			$vcls[] = "string";
			$vstr = sprintf("'<span class='value %s'>%s</span>'", implode(" ", $vcls), $this->renderVariable_string_replace_special_chars($v));
		}
		return $vstr;
	}

	/**
	 * Intetrnal: render a variable
	 * @param string $k
	 * @param mixed $v
	 * @return string
	 */
	protected function renderVariable_regexp($k, $v, $style)
	{
		$classes = ['g1', 'g2', 'g3', 'g4', 'g5', 'g6',];
		$vcls[] = "regexp";

		// split the regexp into groups
		$extractedGroups = [];
		$extracted = true;
		$tv = $v;
		while ($extracted) {
			$m = [];
			$extracted = preg_match("/\([^\)\(]+?\)/", $tv, $m, PREG_OFFSET_CAPTURE);

			if ($extracted) {
				$k = sprintf("|_:_group-G%d-group_:_|", count($extractedGroups)+1);
				$extractedGroups[$k] = $m[0];
				$tv = substr_replace($tv, $k, $m[0][1], strlen($m[0][0]));
			}
		}

		// rebuild the regexp, html-formatted
		$gidx = 0;
		foreach (array_reverse($extractedGroups, true) as $k=>$exg) {
			$gidx++;
			// extract the group header
			preg_match('/(\?P\<[^>]*\>)/', $exg[0], $m, PREG_OFFSET_CAPTURE);
			$exgfull = substr_replace(
				(string)$exg[0],
				sprintf("<span class='header'>%s</span>", htmlentities((string)$m[0][0])),
				($m[0][1]?:0),
				strlen((string)$m[0][0])
			);

			$tv = str_replace($k, sprintf("<span class='group %s'>%s</span>", $classes[$gidx % count($classes)], $exgfull), $tv);
		}

		$replacements = [
			"\n" => "<span class='specialChar'>\\n</span>",
			"\r" => "<span class='specialChar'>\\r</span>",
			"\t" => "<span class='specialChar'>\\t</span>",
		];

		$vstr = sprintf("'<span class='value %s'>%s</span>'", implode(" ", $vcls), str_replace(array_keys($replacements), array_values($replacements), $tv));

		return $vstr;
	}


	/**
	 * Generate the CSS needed for displaying data
	 * @return string
	 */
	protected function generateCSS()
	{
		return <<<CSS
<style type='text/css'>
	/* @see http://paletton.com/#uid=70h1f0ktgoQ2KuDhpoICHqmXgtE
			-> darker http://paletton.com/#uid=70h1f0ktw5H59fBjM6rK24EU2bK
	*/
div.logCallStart div.logCallStart {
	margin-left: 4em;
	margin-bottom: 0.5em;
}
div.logCallStart >span.logId {
	float: right;
	color: #ef0000;
	border: 1px solid #ccc;
	font-size: 0.75em;
	padding-left: 1em;
	white-space: pre;
	font-family: monospace;
}
div.logCallStart {
	border-left: 1px solid transparent;
}
div.logCallStart.depth-1:hover { border-left-color: #040000; }
div.logCallStart.depth-2:hover { border-left-color: #180000; }
div.logCallStart.depth-3:hover { border-left-color: #2b0000; }
div.logCallStart.depth-4:hover { border-left-color: #3f0000; }
div.logCallStart.depth-5:hover { border-left-color: #520000; }
div.logCallStart.depth-6:hover { border-left-color: #660000; }  /* base color */
div.logCallStart.depth-7:hover { border-left-color: #7a0000; }
div.logCallStart.depth-8:hover { border-left-color: #8d0000; }
div.logCallStart.depth-9:hover { border-left-color: #a10000; }
div.logCallStart.depth-10:hover { border-left-color: #b40000; }
div.logCallStart.depth-11:hover { border-left-color: #c80000; }
div.logCallStart.depth-12:hover { border-left-color: #dc0000; }
div.logCallStart.depth-14:hover { border-left-color: #ef0000; }
div.logCallStart:hover { border-left-color: #ef0000; }  /* others */

div.call {
}
div.call >span.class {
	color: #600;
	font-weight: bold;
}
div.call >span.class >span.base {
	color: #666;
}
div.call >span.method {
	color: #200;
}
div.call >span.args {
	color: #00a;
}

div.logInput {
	padding-left: 1em;
	white-space: pre;
	font-family: monospace;
}
div.logInput >div >span.key::before {
    content: "→";
	color: #800;
	font-size: 1.3em;
	padding-right: 0.25em;
	display: inline-block;
	line-height: 0.8em;
	vertical-align: text-top;
}
div.logInput >div >span.key {
	color: #600;
}

div.logInput.regexp >div >span.key::before {
	color: #fff000;
}
div.logInput.regexp >div >span.key {
	color: #fff000;
}

div.logOutput {
	padding-left: 1em;
	white-space: pre;
	font-family: monospace;
}
div.logOutput >div >span.key::before {
    content: "←";
	color: #c00;
	font-size: 1.3em;
	padding-right: 0.25em;
	display: inline-block;
	line-height: 0.8em;
	vertical-align: text-top;
}
div.logOutput >div >span.key {
	color: #c00;
}

div.logOutput.highlight >div >span.key::before {
	color: #e20;
}
div.logOutput.highlight >div >span.key {
	color: #e20;
	font-weight: bold;
}


div.logStep {
	padding-left: 1em;
	white-space: pre;
	font-family: monospace;
}
div.logStep >div >span.key::before {
    content: "↳";
	color: #c00;
	font-size: 1.3em;
	padding-right: 0.25em;
	display: inline-block;
	line-height: 0.8em;
	vertical-align: text-top;
}
div.logStep >div >span.key {
	color: #c00;
}

div.logStep.highlight >div >span.key::before {
	color: #e20;
}
div.logStep.highlight >div >span.key {
	color: #e20;
	font-weight: bold;
}

/* call args */
span.args >span.arg >span.argn {
	color: #999;
}

/* var types */
div .var >span.value {
	white-space: pre;
	font-family: monospace;
}
div .var >span.value.null {
	color: #c00;
}
div .var >span.value.bool {
	color: #888;
}
div .var >span.value.bool.value-false {
	border-left: 4px solid #c00;
	padding-left: 4px;
}
div .var >span.value.bool.value-true {
	border-left: 4px solid #0c0;
	padding-left: 4px;
}
div .var >span.value.numeric {
	color: #666;
}
div .var >span.value.string {
	color: #006;
}
div .var >span.value.string >span.specialChar {
	color: #08f;
}
div .var >span.value.string >span.specialChar.hex {
	color: #08f;
	border-bottom: 1px dotted #f80;
}
div .var >span.value.string >span.specialChar.sign-truncated {
	color: #48f;
	letter-spacing: -4px;
}
div .var >span.value.string:hover {
	border-bottom: 1px solid #006;
}

div .var >span.value.array {
	color: #444;
}

div .var >span.value.regexp {
	display: inline-block;
	white-space: pre-line;
}
div .var >span.value.regexp .group {
	border-bottom: 1px solid transparent;
	display: inline-block;
}
div .var >span.value.regexp .group:hover {
	border-bottom: 1px solid transparent;
}
div .var >span.value.regexp .group .group {
	border-bottom-width: 2px;
}
div .var >span.value.regexp .group .group .group {
	border-bottom-width: 3px;
}
div .var >span.value.regexp .group .group .group .group {
	border-bottom-width: 4px;
}

div .var >span.value.regexp .group {
	white-space: pre;
}
div .var >span.value.regexp .group >span.header {
	font-weight: bold;
	filter: brightness(0.95);
	border-bottom: 1px dotted #888;
}
div .var >span.value.regexp .group.g1 { color: #62fffa; }
div .var >span.value.regexp .group.g1:hover { border-bottom-color: #62fffa; }
div .var >span.value.regexp .group.g2 { color: #6267ff; }
div .var >span.value.regexp .group.g2:hover { border-bottom-color: #6267ff; }
div .var >span.value.regexp .group.g3 { color: #ff6267; }
div .var >span.value.regexp .group.g3:hover { border-bottom-color: #ff6267; }
div .var >span.value.regexp .group.g4 { color: #fffa62; }
div .var >span.value.regexp .group.g4:hover { border-bottom-color: #fffa62; }
div .var >span.value.regexp .group.g5 { color: #ac62ff; }
div .var >span.value.regexp .group.g5:hover { border-bottom-color: #ac62ff; }
div .var >span.value.regexp .group.g6 { color: #67ff62; }
div .var >span.value.regexp .group.g6:hover { border-bottom-color: #67ff62; }

</style>
CSS;
	}
}
