Last active
November 25, 2020 00:47
-
-
Save LionsAd/77b3bf4e606b37e1bfab492efa288e64 to your computer and use it in GitHub Desktop.
Preact and template literals in PHP
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
require_once "tl.php"; | |
function html($string) { | |
return tl('html', $string); | |
} | |
$tabs = [ | |
(object) ['tray' => TRUE, 'url' => '/', 'title' => 'A tray item'], | |
(object) ['tray' => NULL, 'url' => '/admin', 'title' => 'Admin'], | |
]; | |
$Tab = function($props) { | |
$href = $props->href; | |
return htm(eval(html('<div><a href="${$href}">Some Text</a></div>'))); | |
}; | |
$tabItems = array_map(function($tab, $i) use ($Tab) { | |
$onMouseDown = ''; | |
$isActive = 0; | |
$href = $tab->tray == null ? $tab->url : null; | |
return htm(eval(html(' | |
<${$Tab} index=${$i} key=${$tab->url} href=${$href} onMouseDown=${$onMouseDown} isActive=${$isActive}> | |
${$tab->title} | |
<//> | |
'))); | |
}, $tabs, array_keys($tabs)); | |
$items = []; | |
$items[] = htm(eval(html('<span class="item">An item</span>'))); | |
$SubNav = 'x-subnav'; | |
$PositionToggle = 'x-toggle'; | |
$orientation = 'left'; | |
function cn($x) { | |
return $x; | |
} | |
$navBar = htm(eval(html('<${$SubNav} orientation=${$orientation}> | |
${$items} | |
<div className=${cn( | |
($orientation === "left" ? "left" : "top") . | |
" hidden lg:flex" | |
)}> | |
<${$PositionToggle} | |
orientation=${$orientation} | |
/> | |
</div> | |
<//>'))); | |
$x = htm(eval(html(' <div> | |
<div | |
className=${cn( | |
"overflow-x-auto scrolling-touch relative" | |
)} | |
> | |
${$tabItems} | |
</div> | |
<div> | |
${$navBar} | |
</div> | |
</div>'))); | |
print_r($x); | |
return $x; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
class Component { | |
protected $name = null; | |
protected $callback = null; | |
function __construct($name, $callback) { | |
$this->name = $name; | |
$this->callback = $callback; | |
} | |
function getName() { | |
return $this->name; | |
} | |
function getCallback() { | |
return $this->callback; | |
} | |
}; | |
define('MODE_SLASH', 0); | |
define('MODE_TEXT', 1); | |
define('MODE_WHITESPACE', 2); | |
define('MODE_TAGNAME', 3); | |
define('MODE_COMMENT', 4); | |
define('MODE_PROP_SET', 5); | |
define('MODE_PROP_APPEND', 6); | |
define('CHILD_APPEND', 0); | |
define('CHILD_RECURSE', 2); | |
define('TAG_SET', 3); | |
define('PROPS_ASSIGN', 4); | |
define('PROP_SET', MODE_PROP_SET); | |
define('PROP_APPEND', MODE_PROP_APPEND); | |
/* | |
define('CHILD_APPEND', 'CHILD_APPEND'); | |
define('CHILD_RECURSE', 'CHILD_RECURSE'); | |
define('TAG_SET', 'TAG_SET'); | |
define('PROPS_ASSIGN', 'PROPS_ASSIGN'); | |
define('PROP_SET', 'PROP_SET'); | |
define('PROP_APPEND', 'PROP_APPEND'); | |
*/ | |
define('MINI', FALSE); | |
function htm($args) { | |
$statics = $args[1]; | |
$fields = $args[2]; | |
array_unshift($fields, NULL); | |
if (MINI) { | |
return htm_html($statics, $fields); | |
} | |
$built = htm_html($statics, $fields); | |
//print_r([$built, $fields]); | |
//$tree = htm_treeify($built, $fields); | |
//print_r([$tree]); | |
$vdom = htm_evaluate($built, $fields); | |
//print_r([$vdom]); | |
return $vdom; | |
} | |
function component($name, $type, $args = [], $children = '') { | |
/* if (is_callable($type)) { | |
return <<<EOF | |
<-- <$name> --> | |
{$type((object)$args[0])}; | |
<!-- </$name> --> | |
EOF; | |
}*/ | |
$props_string = ''; | |
foreach ($args as $key => $value) { | |
$props_string .= " $key=\"$value\""; | |
} | |
return "<{$name}{$props_string}>" . $children . "</$name>"; | |
} | |
function preact_render_children($children) { | |
$child_string = ''; | |
foreach ($children as $child) { | |
if (is_array($child)) { | |
$child_string .= preact_render_children($child); | |
} | |
else { | |
$child_string .= $child; | |
} | |
$child_string .= PHP_EOL; | |
} | |
return $child_string; | |
} | |
function preact_h($type, $props, ...$children) { | |
if (is_array($type) && is_callable($type[1])) { | |
$cb = $type[1]; | |
return call_user_func($cb, (object) $props); | |
} | |
if (is_array($type) && $type[1] instanceof Component) { | |
$cb = $type[1]; | |
$name = $cb->getName(); | |
$cb = $cb->getCallback(); | |
$args = []; | |
$props_string = ''; | |
foreach ($props as $key => $value) { | |
$value = $value[1]; | |
$args[$key] = $value; | |
if (!is_string($value)) { | |
$value = gettype($value); | |
} | |
$props_string .= " $key=\"$value\""; | |
} | |
$args['children'] = $children; | |
$result = call_user_func($cb, (object) $args); | |
$result = ["<!-- <{$name}{$props_string}> -->", $result, "<!-- </$name> -->"]; | |
return $result; | |
} | |
return (object) [ | |
'type' => $type, | |
'props' => $props, | |
'children' => $children | |
]; | |
if (!is_array($props)) { | |
$props = []; | |
} | |
if (is_callable($type)) { | |
$x = '<!-- <Foo> -->'; | |
$props['children'] = $children; | |
$x .= $type((object) $props); | |
$x .= '<!-- </Foo> -->'; | |
return $x; | |
} | |
//$child_string = count($children) == 1 ? $children[0] : var_export($children, TRUE); | |
$child_string = preact_render_children($children); | |
return component($type, NULL, $props, $child_string); | |
return (object) [ | |
'tag' => $type, | |
'props' => $props, | |
'children' => $children | |
]; | |
} | |
function htm_evaluate($built, $fields, $args = []) { | |
$built[0] = 0; | |
for ($i = 1; $i < count($built); $i++) { | |
$type = $built[$i++]; | |
// $value = $built[$i] ? [(($built[0] |= $type) ? 1 : 2), $fields[$built[$i++]]] : $built[++$i]; | |
if ($built[$i]) { | |
$value = [(($built[0] |= $type) ? 1 : 2), $fields[$built[$i++]]]; | |
} | |
else { | |
$i++; | |
$value = $built[$i]; | |
} | |
if ($type === TAG_SET) { | |
$args[0] = $value; | |
} | |
else if ($type === PROPS_ASSIGN) { | |
if (empty($args[1])) { | |
$args[1] = []; | |
} | |
$args[1] = $args[1] + $value; | |
} | |
else if ($type === PROP_SET) { | |
$i++; | |
$args[1][$built[$i]] = $value; | |
} | |
else if ($type === PROP_APPEND) { | |
$i++; | |
$args[1][$built[$i]] .= ($value . ''); | |
} | |
else if ($type) { // type === CHILD_RECURSE | |
$tmp = call_user_func_array('preact_h', htm_evaluate($value, $fields, ['', null])); | |
array_push($args, $tmp); | |
if ($value[0]) { | |
// Set the 2nd lowest bit it the child element is dynamic. | |
$built[0] |= 2; | |
} else { | |
// Rewrite the operation list in-place if the child element is static. | |
// The currently evaluated piece `CHILD_RECURSE, 0, [...]` becomes | |
// `CHILD_APPEND, 0, tmp`. | |
// Essentially the operation list gets optimized for potential future | |
// re-evaluations. | |
$built[$i-2] = CHILD_APPEND; | |
$built[$i] = $tmp; | |
} | |
} | |
else { // CHILD_APPEND | |
array_push($args, $value); | |
} | |
} | |
return $args; | |
} | |
function htm_treeify($built, $fields) { | |
$_treeify = function($built) use (&$fields, &$_treeify) { | |
$tag = ""; | |
$currentProps = []; | |
$props = []; | |
$children = []; | |
for ($i = 1; $i < count($built); $i++) { | |
$type = $built[$i++]; | |
$value = $built[$i] ? $fields[$built[$i++] - 1] : $built[++$i]; | |
if ($type === TAG_SET) { | |
$tag = $value; | |
} else if ($type === PROPS_ASSIGN) { | |
array_push($props, $value); | |
$currentProps = FALSE; | |
} else if ($type === PROP_SET) { | |
if (!$currentProps) { | |
$currentProps = []; | |
array_push($props, $currentProps); | |
} | |
$currentProps[$built[++$i]] = [$value]; | |
} else if ($type === PROP_APPEND) { | |
array_push($currentProps[$built[++$i]], $value); | |
} else if ($type === CHILD_RECURSE) { | |
array_push($children, $_treeify($value)); | |
} else if ($type === CHILD_APPEND) { | |
array_push($children, $value); | |
} | |
} | |
return [$tag, $props, $children]; | |
}; | |
$children = $_treeify($built)[2]; | |
return count($children) > 1 ? $children : $children[0]; | |
} | |
function htm_html($statics, $fields) { | |
$mode = MODE_TEXT; | |
$buffer = ""; | |
$quote = ""; | |
$current = [0]; | |
$char = NULL; | |
$propName = NULL; | |
$commit = function($field = 0) use (&$mode, &$buffer, &$current, &$fields, &$propName) { | |
if ($mode === MODE_TEXT && ($field || ($buffer = trim(preg_replace("/^\\s*\\n\\s*|\\s*\\n\\s*\$/", '', $buffer))))) { | |
if (MINI) { | |
array_push($current, $field ? $fields[$field] : $buffer); | |
} else { | |
array_push($current, CHILD_APPEND, $field, $buffer); | |
} | |
} else if ($mode === MODE_TAGNAME && ($field || $buffer)) { | |
if (MINI) { | |
$current[1] = $field ? $fields[$field] : $buffer; | |
} else { | |
array_push($current, TAG_SET, $field, $buffer); | |
} | |
$mode = MODE_WHITESPACE; | |
} else if ($mode === MODE_WHITESPACE && $buffer === "..." && $field) { | |
if (MINI) { | |
$tmp_array = []; | |
if (!empty($current[2])) { | |
$tmp_array = $current[2]; | |
} | |
$current[2] = $fields[$field] + $tmp_array; | |
} else { | |
array_push($current, PROPS_ASSIGN, $field, 0); | |
} | |
} else if ($mode === MODE_WHITESPACE && $buffer && !$field) { | |
if (MINI) { | |
$current[2][$buffer] = TRUE; | |
} else { | |
array_push($current, PROP_SET, 0, TRUE, $buffer); | |
} | |
} else if ($mode >= MODE_PROP_SET) { | |
if (MINI) { | |
if ($mode === MODE_PROP_SET) { | |
$current[2][$propName] = $field ? ($buffer ? ($buffer . $fields[$field]) : $fields[$field]) : $buffer; | |
$mode = MODE_PROP_APPEND; | |
} else if ($field || $buffer) { | |
$current[2][$propName] .= $field ? ($buffer . $fields[$field]) : $buffer; | |
} | |
} else { | |
if ($buffer || (!isset($field) && $mode === MODE_PROP_SET)) { | |
array_push($current, $mode, 0, $buffer, $propName); | |
$mode = MODE_PROP_APPEND; | |
} | |
if ($field) { | |
array_push($current, $mode, $field, 0, $propName); | |
$mode = MODE_PROP_APPEND; | |
} | |
} | |
} | |
$buffer = ""; | |
}; | |
for ($i = 0; $i < count($statics); $i++) { | |
if ($i) { | |
if ($mode === MODE_TEXT) { | |
$commit(); | |
} | |
$commit($i); | |
} | |
for ($j = 0; $j < strlen($statics[$i]); $j++) { | |
$char = $statics[$i][$j]; | |
if ($mode === MODE_TEXT) { | |
if ($char === "<") { | |
$commit(); | |
if (MINI) { | |
$current = [$current, '', NULL]; | |
} else { | |
$current = [$current]; | |
} | |
$mode = MODE_TAGNAME; | |
} else { | |
$buffer .= $char; | |
} | |
} else if ($mode === MODE_COMMENT) { | |
if ($buffer === "--" && $char === ">") { | |
$mode = MODE_TEXT; | |
$buffer = ''; | |
} else { | |
$buffer = $char . $buffer[0]; | |
} | |
} else if ($quote) { | |
if ($char === $quote) { | |
$quote = ''; | |
} else { | |
$buffer .= $char; | |
} | |
} else if ($char === '"' || $char === "'") { | |
$quote = $char; | |
} else if ($char === ">") { | |
$commit(); | |
$mode = MODE_TEXT; | |
} else if ($mode == 0) { | |
// Ignore everything until the tag ends | |
} else if ($char === "=") { | |
$mode = MODE_PROP_SET; | |
$propName = $buffer; | |
$buffer = ""; | |
} else if ($char === "/" && ($mode < MODE_PROP_SET || $statics[$i][$j+1] === ">")) { | |
$commit(); | |
if ($mode === MODE_TAGNAME) { | |
$current = $current[0]; | |
} | |
// Mode scope is overwritten here. | |
$_mode = $current; | |
if (MINI) { | |
$current = $current[0]; | |
$vdom_node = call_user_func_array('preact_h', array_slice($_mode, 1)); | |
array_push($current, $vdom_node); | |
} else { | |
$current = $current[0]; | |
$_mode[0] = []; | |
array_push($current, CHILD_RECURSE, 0, $_mode); | |
} | |
$mode = MODE_SLASH; | |
} else if ($char === ' ' || $char === "\t" || $char === "\n" || $char === "\r") { | |
// <a disabled> | |
$commit(); | |
$mode = MODE_WHITESPACE; | |
} else { | |
$buffer .= $char; | |
} | |
if ($mode === MODE_TAGNAME && $buffer === "!--") { | |
$mode = MODE_COMMENT; | |
$current = $current[0]; | |
} | |
} | |
} | |
$commit(); | |
if (MINI) { | |
return count($current) > 2 ? array_slice($current, 1) : $current[1]; | |
} | |
return $current; | |
} | |
function process_tokens(&$tokens) { | |
$strings = []; | |
$placeholders = []; | |
$buffer = ''; | |
$in_tag = FALSE; | |
while (!empty($tokens)) { | |
$token_var = array_shift($tokens); | |
$token = T_INLINE_HTML; | |
$token_string = $token_var; | |
if (is_array($token_var)) { | |
$token = $token_var[0]; | |
$token_string = $token_var[1]; | |
} | |
if ($token == T_OPEN_TAG_WITH_ECHO) { | |
$in_tag = TRUE; | |
$strings[] = $buffer; | |
$buffer = ''; | |
continue; | |
} | |
elseif ($token == T_CLOSE_TAG) { | |
$in_tag = FALSE; | |
$placeholders[] = "$buffer"; | |
$buffer = ''; | |
continue; | |
} | |
$buffer .= $token_string; | |
} | |
if (!empty($buffer)) { | |
$strings[] = $buffer; | |
} | |
return [$strings, $placeholders]; | |
} | |
function tl($func, $str) { | |
$str = str_replace('${', '<?=', $str); | |
$str = str_replace('}', '?>', $str); | |
$tokens = token_get_all($str); | |
$processed = process_tokens($tokens); | |
$statics = var_export($processed[0], TRUE); | |
$fields = []; | |
foreach ($processed[1] as $field) { | |
$fields[] = $field; | |
} | |
$fields_string = '[' . implode(',' . PHP_EOL, $fields) . ']'; | |
$code =<<<EOF | |
return [ | |
'$func', | |
$statics, | |
$fields_string, | |
]; | |
EOF; | |
return $code; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment