@ 2013-08-02T12:53:17Z <?php
interface ILexer {
/**
* return associative array containing tokens for parser
* @param string $input code to tokenize
* @return array
*/
public function tokenize($input);
}
interface INameValidator {
/**
* Checks if name is valid. In a case of failure trigger syntax error.
* @param string $name
* @return void
*/
public function validate($name);
}
interface IParser {
/**
* Sets array of keywords.
* @param array $keywords associative array where key is keyword and value respective php instruction.
* @return void
*/
public function setKeywords(array $keywords);
/**
* Parses code. Checks for syntax errors.
* If no error occurs returns data for compiler to assemble php class
* otherwise trigger syntax error.
* @param string $input code to parse.
* @return array IParserResult
*/
public function parse($input);
}
interface IParseResult {
public function getDefinition();
public function getName();
public function getConstants();
}
interface ICompiler {
/**
* Compiles enum into valid php class
* @param string $input code to compile.
* @return string
*/
public function compile($input);
}
interface IErrorManager {
/**
* Triggers fatal error.
* @param string $message error message
* @return void
*/
public function ariseFatal($message);
/**
* Triggers warning.
* @param string $message error message
* @return void
*/
public function ariseWarning($message);
/**
* Triggers notice.
* @param string $message error message
* @return void
*/
public function ariseNotice($message);
}
//I know this lexer sucks.
class Lexer extends CompilerElement implements ILexer {
// Potential hidden dependency
private $specialChars = array(
'{' => 'start_body',
'}' => 'end_body'
);
public function tokenize($input) {
if (!is_string($input)) {
$this->getErrorManager()->ariseFatal(get_class($this).'::parse expects parameter 1 to be string. '.gettype($input).' given.');
}
$tokens = array();
$token = $this->getEmptyToken();
$tokenMetadata = $this->getDefaultTokenMetadata();
$inputLength = strlen($input);
for ($i = 0; $i < $inputLength; $i++) {
$current = $input[$i];
//check if special character
if (isset($this->specialChars[$current])) {
$token[$this->specialChars[$current]] = $current;
if ($i !== $inputLength - 1)
continue;
}
if (isset($token['end_body'])) {
$tokens[] = $token;
$token = $this->getEmptyToken();
$tokenMetadata = $this->getDefaultTokenMetadata();
if ($i === $inputLength - 1)
continue;
}
$isWhitespace = ctype_space($current);
if (isset($token['start_body'])) {
//skip if whitespace
if ($isWhitespace) {
continue;
}
if ($current === ',') {
$tokenMetadata['newItem'] = true;
$tokenMetadata['newItemNameResolved'] = false;
continue;
}
if ($current === '=') {
$tokenMetadata['newItemNameResolved'] = true;
continue;
}
if ($tokenMetadata['newItem']) {
$token['e_'.$current] = null;
$tokenMetadata['newItem'] = false;
} else {
end($token);
$lastKey = key($token);
if (!$tokenMetadata['newItemNameResolved']) {
unset($token[$lastKey]);
$token[$lastKey.$current] = null;
} else {
$token[$lastKey] .= $current;
}
}
continue;
}
if (!$tokenMetadata['typeResolved']) {
if ($isWhitespace) {
if (strlen($token['type']) === 0) {
continue;
} else {
$tokenMetadata['typeResolved'] = true;
}
} else {
$token['type'] .= $current;
}
} else if (!$tokenMetadata['nameResolved']) {
if ($isWhitespace) {
if (strlen($token['name']) === 0) {
continue;
} else {
$tokenMetadata['nameResolved'] = true;
}
} else {
$token['name'] .= $current;
}
}
}
return $tokens;
}
private function getEmptyToken() {
return array(
'type' => '',
'name' => ''
);
}
private function getDefaultTokenMetadata() {
return array(
'typeResolved' => false,
'nameResolved' => false,
'newItem' => true,
'newItemNameResolved' => false
);
}
public function __construct(IErrorManager $errorManager) {
parent::__construct($errorManager);
}
}
class NameValidator extends CompilerElement implements INameValidator {
private $allowedChars;
public function validate($name) {
if (!is_string($name)) {
$this->getErrorManager()->ariseFatal(get_class($this).'::validate expects parameter 1 to be string. '.gettype($name).' given.');
}
$nameLength = strlen($name);
for ($i = 0; $i < $nameLength; $i++) {
$current = $name[$i];
if ($i === 0 && !ctype_alpha($current)) {
$this->getErrorManager()->ariseFatal('Name should start with alphabetic characters. Given name '.$name.' starts with: '.$current);
}
if (!in_array(strtolower($current), $this->allowedChars, true)) {
$this->getErrorManager()->ariseFatal('Unexpected character '.$current.' in name '.$name.'.');
}
}
}
public function __construct(IErrorManager $errorManager) {
parent::__construct($errorManager);
// Yes, I hate that everything is allowed as name in php.
$this->allowedChars = str_split('abcdefghijklmnopqrstuvwxyz0123456789');
}
}
class Parser extends CompilerElement implements IParser {
private $lexer;
private $nameValidator;
private $keywords = array();
public function setKeywords(array $keywords) {
$this->keywords = $keywords;
}
public function parse($input) {
if (!is_string($input)) {
$this->getErrorManager()->ariseFatal(get_class($this).'::parse expects parameter 1 to be string. '.gettype($input).' given.');
}
$input = $this->cleanUp($input);
$errorManager = $this->getErrorManager();
$allTokens = $this->lexer->tokenize($input);
$result = array();
$currentResult = null;
$processingBody = false;
foreach ($allTokens as $tokens) {
foreach ($tokens as $token => $tokenValue) {
switch ($token) {
case 'type':
if ($currentResult !== null || !isset($this->keywords[$tokenValue])) {
$errorManager->ariseFatal('Syntax error. Unexpected '.$tokenValue);
}
$currentResult = new ParseResult($this->keywords[$tokenValue], $errorManager);
break;
case 'name':
if ($currentResult === null) {
$errorManager->ariseFatal('Syntax error. Unexpected '.$tokenValue);
}
$this->nameValidator->validate($tokenValue);
$currentResult->setName($tokenValue);
break;
case 'start_body':
if ($currentResult === null || $currentResult->getName() === null) {
$errorManager->ariseFatal('Syntax error. Unexpected '.$tokenValue);
}
$processingBody = true;
break;
case 'end_body':
if ($currentResult === null || $currentResult->getName() === null || $processingBody === false) {
$errorManager->ariseFatal('Syntax error. Unexpected '.$tokenValue);
}
$result[] = $currentResult;
$currentResult = null;
$processingBody = false;
break;
default:
if ($processingBody === true && $currentResult !== null) {
$name = ltrim($token, 'e_');
$this->nameValidator->validate($name);
if ($tokenValue === null) {
$constants = $currentResult->getConstants();
$last = end($constants);
$value = ($last !== false) ? $last + 1 : 0;
} else {
$value = $tokenValue;
}
$currentResult->setItem($name, $value);
} else {
$errorManager->ariseFatal('Syntax error. Unexpected '.$tokenValue);
}
break;
}
}
}
return $result;
}
private function cleanUp($input) {
return trim($input);
}
public function __construct(ILexer $lexer, INameValidator $nameValidator, IErrorManager $errorManager) {
parent::__construct($errorManager);
$this->lexer = $lexer;
$this->nameValidator = $nameValidator;
}
}
class ParseResult extends CompilerElement implements IParseResult {
private $definition;
private $name;
private $constants = array();
public function getDefinition() {
return $this->definition;
}
public function getName() {
return $this->name;
}
public function getConstants() {
return $this->constants;
}
public function setItem($name, $value) {
if (!is_string($name)) {
$this->getErrorManager()->ariseFatal('Enumeration item name can only be string.');
}
if (!is_numeric($value)) {
$this->getErrorManager()->ariseFatal('Enumeration item can only hold numeric value.');
}
$this->constants[$name] = (ctype_digit($value) === true) ? (int)$value : (double)$value;
}
public function setName($name) {
if (!is_string($name)) {
$this->getErrorManager()->ariseFatal(get_class($this).'::setName expects parameter 1 to be string. '.gettype($name).' given.');
}
$this->name = $name;
}
public function __construct($definition, IErrorManager $errorManager) {
parent::__construct($errorManager);
if (!is_string($definition)) {
$this->getErrorManager()->ariseFatal(get_class($this).'::__construct expects parameter 1 to be string. '.gettype($definition).' given.');
}
$this->definition = $definition;
}
}
abstract class CompilerElement {
private $errorManager;
protected function getErrorManager() {
return $this->errorManager;
}
protected function __construct(IErrorManager $errorManager) {
$this->errorManager = $errorManager;
}
}
class Enum2PhpCompiler implements ICompiler {
const INDENTATION_CHAR = "\t";
private $parser;
public function compile($input) {
$parseResult = $this->parser->parse($input);
$return = '';
foreach ($parseResult as $result) {
$return .= $this->generateClass($result);
}
return $return;
}
private function generateClass(IParseResult $parseResult) {
$return = $parseResult->getDefinition().' '.$parseResult->getName().' {'.PHP_EOL;
foreach ($parseResult->getConstants() as $name => $value) {
$return .= Enum2PhpCompiler::INDENTATION_CHAR.'const '.$name.' = '.$value.';'.PHP_EOL;
}
$return .= Enum2PhpCompiler::INDENTATION_CHAR.'private function __construct() {}'.PHP_EOL;
$return .= '}'.PHP_EOL;
return $return;
}
public function __construct(IParser $parser) {
$this->parser = $parser;
$this->parser->setKeywords(array('enum' => 'final class'));
}
}
class ErrorManager implements IErrorManager {
public function ariseFatal($message) {
$this->triggerError($message, E_USER_ERROR);
}
public function ariseWarning($message) {
$this->triggerError($message, E_USER_WARNING);
}
public function ariseNotice($message) {
$this->triggerError($message, E_USER_NOTICE);
}
private function triggerError($message, $type) {
trigger_error((string)$message, (int)$type);
}
}
$errorManager = new ErrorManager();
$nameValidator = new NameValidator($errorManager);
$lexer = new Lexer($errorManager);
$parser = new Parser($lexer, $nameValidator, $errorManager);
$compiler = new Enum2PhpCompiler($parser);
$enum = <<<PHP
enum MyEnum {
Item1 = 1,
Item2 = 5,
Item3
}
enum AnotherEnum {
Item
}
PHP;
eval($compiler->compile($enum));
var_dump(MyEnum::Item1, MyEnum::Item2, MyEnum::Item3, AnotherEnum::Item);
Enable javascript to submit You have javascript disabled. You will not be able to edit any code.
Here you find the average performance (time & memory) of each version. A grayed out version indicates it didn't complete successfully (based on exit-code).
Version System time (s) User time (s) Memory (MiB) 8.3.6 0.008 0.011 18.40 8.3.5 0.008 0.009 21.97 8.3.4 0.016 0.000 18.84 8.3.3 0.011 0.004 18.85 8.3.2 0.008 0.000 18.98 8.3.1 0.004 0.004 21.12 8.3.0 0.000 0.007 23.48 8.2.18 0.009 0.006 16.63 8.2.17 0.007 0.007 22.96 8.2.16 0.000 0.015 20.52 8.2.15 0.008 0.000 24.18 8.2.14 0.004 0.004 24.66 8.2.13 0.008 0.000 26.16 8.2.12 0.006 0.003 19.77 8.2.11 0.007 0.003 20.66 8.2.10 0.004 0.007 18.09 8.2.9 0.008 0.000 19.35 8.2.8 0.003 0.006 18.16 8.2.7 0.006 0.003 17.75 8.2.6 0.003 0.005 18.05 8.2.5 0.003 0.006 18.07 8.2.4 0.000 0.008 18.34 8.2.3 0.006 0.003 21.09 8.2.2 0.000 0.008 17.76 8.2.1 0.004 0.004 17.77 8.2.0 0.003 0.006 17.95 8.1.28 0.011 0.004 25.92 8.1.27 0.003 0.007 23.99 8.1.26 0.004 0.004 26.35 8.1.25 0.005 0.003 28.09 8.1.24 0.006 0.003 22.52 8.1.23 0.012 0.000 21.01 8.1.22 0.004 0.004 18.77 8.1.21 0.000 0.008 19.03 8.1.20 0.007 0.003 17.60 8.1.19 0.000 0.009 17.66 8.1.18 0.008 0.000 18.10 8.1.17 0.004 0.004 18.59 8.1.16 0.000 0.008 18.90 8.1.15 0.000 0.009 18.66 8.1.14 0.004 0.004 17.52 8.1.13 0.007 0.000 17.98 8.1.12 0.005 0.003 17.61 8.1.11 0.004 0.004 17.58 8.1.10 0.005 0.003 17.59 8.1.9 0.003 0.005 17.53 8.1.8 0.000 0.007 17.64 8.1.7 0.007 0.000 17.57 8.1.6 0.008 0.000 17.64 8.1.5 0.005 0.003 17.71 8.1.4 0.000 0.008 17.71 8.1.3 0.000 0.008 17.69 8.1.2 0.000 0.009 17.74 8.1.1 0.000 0.008 17.75 8.1.0 0.000 0.009 17.70 8.0.30 0.000 0.008 20.11 8.0.29 0.006 0.003 17.30 8.0.28 0.000 0.007 18.59 8.0.27 0.000 0.009 17.41 8.0.26 0.004 0.004 17.00 8.0.25 0.000 0.007 17.17 8.0.24 0.003 0.006 17.17 8.0.23 0.005 0.002 17.17 8.0.22 0.000 0.007 17.15 8.0.21 0.000 0.008 17.11 8.0.20 0.003 0.003 17.18 8.0.19 0.008 0.000 17.06 8.0.18 0.000 0.008 17.16 8.0.17 0.000 0.008 17.16 8.0.16 0.000 0.008 17.11 8.0.15 0.003 0.005 17.02 8.0.14 0.005 0.002 16.96 8.0.13 0.003 0.003 13.64 8.0.12 0.005 0.003 17.00 8.0.11 0.000 0.008 17.14 8.0.10 0.004 0.004 16.97 8.0.9 0.003 0.005 17.22 8.0.8 0.000 0.016 17.03 8.0.7 0.008 0.000 17.15 8.0.6 0.004 0.004 16.98 8.0.5 0.003 0.005 16.98 8.0.3 0.014 0.010 17.24 8.0.2 0.008 0.011 17.48 8.0.1 0.004 0.004 17.30 8.0.0 0.009 0.009 16.87 7.4.33 0.005 0.000 14.70 7.4.32 0.000 0.010 16.71 7.4.30 0.003 0.003 16.64 7.4.29 0.004 0.004 16.69 7.4.28 0.004 0.004 16.57 7.4.27 0.004 0.004 16.59 7.4.26 0.007 0.000 13.39 7.4.25 0.000 0.009 16.61 7.4.24 0.004 0.004 16.55 7.4.23 0.005 0.002 16.66 7.4.22 0.014 0.003 16.65 7.4.21 0.011 0.007 16.71 7.4.20 0.008 0.000 16.71 7.4.19 0.008 0.000 16.84 7.4.16 0.008 0.008 16.52 7.4.15 0.011 0.007 17.40 7.4.14 0.012 0.006 17.86 7.4.13 0.009 0.009 16.63 7.4.12 0.010 0.008 16.70 7.4.11 0.000 0.017 16.69 7.4.10 0.014 0.004 16.70 7.4.9 0.010 0.016 16.77 7.4.8 0.006 0.012 19.39 7.4.7 0.009 0.009 16.78 7.4.6 0.009 0.009 16.48 7.4.5 0.003 0.006 16.62 7.4.4 0.016 0.003 22.77 7.4.3 0.009 0.012 16.75 7.4.0 0.006 0.011 15.33 7.3.33 0.000 0.006 13.54 7.3.32 0.000 0.007 13.63 7.3.31 0.007 0.000 16.69 7.3.30 0.000 0.007 16.64 7.3.29 0.009 0.006 16.59 7.3.28 0.010 0.009 16.61 7.3.27 0.006 0.012 17.40 7.3.26 0.013 0.006 18.24 7.3.25 0.008 0.010 16.79 7.3.24 0.012 0.012 16.63 7.3.23 0.012 0.006 16.48 7.3.21 0.007 0.010 16.65 7.3.20 0.006 0.013 19.39 7.3.19 0.004 0.020 16.70 7.3.18 0.011 0.006 16.69 7.3.17 0.007 0.010 16.61 7.3.16 0.008 0.008 16.60 7.3.12 0.003 0.015 14.85 7.3.11 0.000 0.014 15.12 7.3.10 0.004 0.011 15.00 7.3.9 0.011 0.003 15.02 7.3.8 0.007 0.010 14.86 7.3.7 0.009 0.003 14.92 7.3.6 0.007 0.007 14.81 7.3.5 0.000 0.013 15.02 7.3.4 0.008 0.004 15.11 7.3.3 0.004 0.011 15.05 7.3.2 0.008 0.000 16.71 7.3.1 0.006 0.006 16.47 7.3.0 0.004 0.004 16.82 7.2.33 0.012 0.009 16.98 7.2.32 0.010 0.010 16.64 7.2.31 0.011 0.007 17.05 7.2.30 0.009 0.009 16.65 7.2.29 0.009 0.013 16.69 7.2.25 0.010 0.010 15.29 7.2.24 0.009 0.003 15.20 7.2.23 0.009 0.006 15.38 7.2.22 0.003 0.009 15.35 7.2.21 0.003 0.007 14.85 7.2.20 0.009 0.006 14.85 7.2.19 0.009 0.003 15.33 7.2.18 0.000 0.011 15.41 7.2.17 0.003 0.006 15.37 7.2.6 0.007 0.007 16.86 7.2.0 0.000 0.013 19.61 7.1.33 0.006 0.008 15.77 7.1.32 0.009 0.006 15.76 7.1.31 0.003 0.013 15.88 7.1.30 0.003 0.006 15.78 7.1.29 0.004 0.007 15.66 7.1.28 0.009 0.000 15.78 7.1.27 0.006 0.003 15.52 7.1.26 0.009 0.004 15.55 7.1.20 0.007 0.010 15.62 7.1.10 0.008 0.004 17.86 7.1.7 0.000 0.008 17.19 7.1.6 0.010 0.016 19.40 7.1.5 0.015 0.006 17.13 7.1.0 0.000 0.037 22.36 7.0.20 0.005 0.002 16.77 7.0.14 0.000 0.077 21.94 7.0.10 0.003 0.083 20.06 7.0.9 0.010 0.080 20.06 7.0.8 0.010 0.037 20.06 7.0.7 0.023 0.067 19.98 7.0.6 0.003 0.080 19.94 7.0.5 0.023 0.037 20.46 7.0.4 0.000 0.047 20.02 7.0.3 0.010 0.083 20.15 7.0.2 0.000 0.053 20.13 7.0.1 0.010 0.070 20.10 7.0.0 0.013 0.077 20.05 5.6.28 0.007 0.073 21.05 5.6.25 0.017 0.080 20.71 5.6.24 0.013 0.087 20.74 5.6.23 0.010 0.083 20.70 5.6.22 0.003 0.077 20.74 5.6.21 0.000 0.087 20.58 5.6.20 0.010 0.047 21.21 5.6.19 0.003 0.080 21.15 5.6.18 0.010 0.080 21.21 5.6.17 0.013 0.060 21.29 5.6.16 0.007 0.050 21.10 5.6.15 0.010 0.077 21.12 5.6.14 0.007 0.083 21.14 5.6.13 0.010 0.050 21.13 5.6.12 0.017 0.070 21.13 5.6.11 0.000 0.060 21.22 5.6.10 0.003 0.083 21.18 5.6.9 0.003 0.063 21.20 5.6.8 0.013 0.077 20.63 5.6.7 0.013 0.047 20.54 5.6.6 0.000 0.083 20.50 5.6.5 0.010 0.073 20.46 5.6.4 0.003 0.083 20.58 5.6.3 0.017 0.077 20.52 5.6.2 0.000 0.040 20.64 5.6.1 0.003 0.077 20.63 5.6.0 0.000 0.083 20.55 5.5.38 0.007 0.050 20.51 5.5.37 0.010 0.060 20.50 5.5.36 0.010 0.057 20.52 5.5.35 0.003 0.080 20.59 5.5.34 0.010 0.067 20.95 5.5.33 0.003 0.067 21.04 5.5.32 0.003 0.087 20.99 5.5.31 0.017 0.063 20.91 5.5.30 0.007 0.083 20.84 5.5.29 0.007 0.087 21.02 5.5.28 0.000 0.077 20.98 5.5.27 0.010 0.083 20.98 5.5.26 0.010 0.080 21.00 5.5.25 0.007 0.073 20.56 5.5.24 0.010 0.070 20.33 5.5.23 0.007 0.047 20.10 5.5.22 0.007 0.070 20.27 5.5.21 0.000 0.040 20.37 5.5.20 0.007 0.080 20.34 5.5.19 0.007 0.077 20.29 5.5.18 0.010 0.067 20.36 5.5.16 0.007 0.077 20.29 5.5.15 0.013 0.033 20.27 5.5.14 0.023 0.060 20.37 5.5.13 0.007 0.080 20.23 5.5.12 0.003 0.070 20.34 5.5.11 0.007 0.083 20.38 5.5.10 0.007 0.080 20.29 5.5.9 0.000 0.050 20.22 5.5.8 0.007 0.073 20.24 5.5.7 0.007 0.047 20.16 5.5.6 0.013 0.073 20.10 5.5.5 0.007 0.073 20.22 5.5.4 0.017 0.053 20.16 5.5.3 0.003 0.077 20.15 5.5.2 0.010 0.047 20.16 5.5.1 0.010 0.073 20.15 5.5.0 0.007 0.070 20.22 5.4.45 0.013 0.077 19.39 5.4.44 0.013 0.073 19.23 5.4.43 0.013 0.080 19.41 5.4.42 0.000 0.060 19.43 5.4.41 0.007 0.077 19.48 5.4.40 0.003 0.057 18.93 5.4.39 0.010 0.077 19.10 5.4.38 0.010 0.070 19.18 5.4.37 0.000 0.080 18.94 5.4.36 0.003 0.080 19.16 5.4.35 0.010 0.033 19.07 5.4.34 0.007 0.080 19.06 5.4.32 0.010 0.063 19.14 5.4.31 0.010 0.070 19.26 5.4.30 0.007 0.073 19.25 5.4.29 0.010 0.077 19.07 5.4.28 0.013 0.043 19.13 5.4.27 0.007 0.067 18.94 5.4.26 0.013 0.060 18.99 5.4.25 0.013 0.073 19.23 5.4.24 0.010 0.070 19.07 5.4.23 0.003 0.040 19.03 5.4.22 0.003 0.073 19.25 5.4.21 0.010 0.073 19.10 5.4.20 0.027 0.050 18.98 5.4.19 0.007 0.080 18.91 5.4.18 0.007 0.080 19.15 5.4.17 0.020 0.073 19.08 5.4.16 0.003 0.080 19.20 5.4.15 0.010 0.070 19.20 5.4.14 0.010 0.070 16.33 5.4.13 0.017 0.060 16.49 5.4.12 0.010 0.063 16.48 5.4.11 0.017 0.070 16.42 5.4.10 0.000 0.050 16.45 5.4.9 0.007 0.067 16.57 5.4.8 0.010 0.067 16.46 5.4.7 0.003 0.070 16.57 5.4.6 0.010 0.070 16.54 5.4.5 0.003 0.063 16.49 5.4.4 0.010 0.067 16.42 5.4.3 0.000 0.080 16.42 5.4.2 0.007 0.070 16.52 5.4.1 0.013 0.033 16.44 5.4.0 0.003 0.043 15.93 5.3.29 0.003 0.067 14.79 5.3.28 0.013 0.067 14.77 5.3.27 0.013 0.037 14.84 5.3.26 0.003 0.070 14.72 5.3.25 0.003 0.050 14.83 5.3.24 0.010 0.077 14.76 5.3.23 0.007 0.077 14.71 5.3.22 0.007 0.073 14.82 5.3.21 0.000 0.083 14.82 5.3.20 0.003 0.057 14.74 5.3.19 0.000 0.087 14.80 5.3.18 0.020 0.030 14.75 5.3.17 0.010 0.070 14.85 5.3.16 0.003 0.080 14.68 5.3.15 0.000 0.083 14.75 5.3.14 0.010 0.040 14.76 5.3.13 0.003 0.067 14.73 5.3.12 0.003 0.077 14.75 5.3.11 0.007 0.077 14.84 5.3.10 0.007 0.067 14.30 5.3.9 0.000 0.067 14.24 5.3.8 0.007 0.043 14.16 5.3.7 0.003 0.067 14.21 5.3.6 0.003 0.043 14.15 5.3.5 0.010 0.060 14.00 5.3.4 0.000 0.057 14.02 5.3.3 0.000 0.077 14.19 5.3.2 0.000 0.040 14.00 5.3.1 0.010 0.070 13.95 5.3.0 0.007 0.063 13.69
preferences:dark mode live preview
53.57 ms | 401 KiB | 5 Q