ReactJS in PHP: Writing Compilers Is Easy and Fun!

Creating Compilers
Generating Tokens
function tokens($code) {
$tokens = [];
$length = strlen($code);
$cursor = 0;
while ($cursor < $length) {
if ($code[$cursor] === "{") {
print "ATTRIBUTE STARTED ({$cursor})" . PHP_EOL;
}
if ($code[$cursor] === "}") {
print "ATTRIBUTE ENDED ({$cursor})" . PHP_EOL;
}
if ($code[$cursor] === "<") {
print "ELEMENT STARTED ({$cursor})" . PHP_EOL;
}
if ($code[$cursor] === ">") {
print "ELEMENT ENDED ({$cursor})" . PHP_EOL;
}
$cursor++;
}
}
$code = '
<?php
$classNames = "foo bar";
$message = "hello world";
$thing = (
<div
className={() => { return "outer-div"; }}
nested={<span className={"nested-span"}>with text</span>}
>
a bit of text before
<span>
{$message} with a bit of extra text
</span>
a bit of text after
</div>
);
';
tokens($code);
// ELEMENT STARTED (5)
// ELEMENT STARTED (95)
// ATTRIBUTE STARTED (122)
// ELEMENT ENDED (127)
// ATTRIBUTE STARTED (129)
// ATTRIBUTE ENDED (151)
// ATTRIBUTE ENDED (152)
// ATTRIBUTE STARTED (173)
// ELEMENT STARTED (174)
// ATTRIBUTE STARTED (190)
// ATTRIBUTE ENDED (204)
// ELEMENT ENDED (205)
// ELEMENT STARTED (215)
// ELEMENT ENDED (221)
// ATTRIBUTE ENDED (222)
// ELEMENT ENDED (232)
// ELEMENT STARTED (279)
// ELEMENT ENDED (284)
// ATTRIBUTE STARTED (302)
// ATTRIBUTE ENDED (311)
// ELEMENT STARTED (350)
// ELEMENT ENDED (356)
// ELEMENT STARTED (398)
// ELEMENT ENDED (403)
preg_match("#^</?[a-zA-Z]#", substr($code, $cursor, 3), $matchesStart);
if (count($matchesStart)) {
print "ELEMENT STARTED ({$cursor})" . PHP_EOL;
}
// ...
// ELEMENT STARTED (95)
// ATTRIBUTE STARTED (122)
// ELEMENT ENDED (127)
// ATTRIBUTE STARTED (129)
// ATTRIBUTE ENDED (151)
// ATTRIBUTE ENDED (152)
// ATTRIBUTE STARTED (173)
// ELEMENT STARTED (174)
// ...
preg_match("#^=>#", substr($code, $cursor - 1, 2), $matchesEqualBefore);
preg_match("#^>=#", substr($code, $cursor, 2), $matchesEqualAfter);
if ($code[$cursor] === ">" && !$matchesEqualBefore && !$matchesEqualAfter) {
print "ELEMENT ENDED ({$cursor})" . PHP_EOL;
}
// ...
// ELEMENT STARTED (95)
// ATTRIBUTE STARTED (122)
// ATTRIBUTE STARTED (129)
// ATTRIBUTE ENDED (151)
// ATTRIBUTE ENDED (152)
// ATTRIBUTE STARTED (173)
// ELEMENT STARTED (174)
// ...
function tokens($code) {
$tokens = [];
$length = strlen($code);
$cursor = 0;
$elementLevel = 0;
$elementStarted = null;
$elementEnded = null;
$attributes = [];
$attributeLevel = 0;
$attributeStarted = null;
$attributeEnded = null;
while ($cursor < $length) {
$extract = trim(substr($code, $cursor, 5)) . "...";
if ($code[$cursor] === "{" && $elementStarted !== null) {
if ($attributeLevel === 0) {
print "ATTRIBUTE STARTED ({$cursor}, {$extract})" . PHP_EOL;
$attributeStarted = $cursor;
}
$attributeLevel++;
}
if ($code[$cursor] === "}" && $elementStarted !== null) {
$attributeLevel--;
if ($attributeLevel === 0) {
print "ATTRIBUTE ENDED ({$cursor})" . PHP_EOL;
$attributeEnded = $cursor;
}
}
preg_match("#^</?[a-zA-Z]#", substr($code, $cursor, 3), $matchesStart);
if (count($matchesStart) && $attributeLevel < 1) {
print "ELEMENT STARTED ({$cursor}, {$extract})" . PHP_EOL;
$elementLevel++;
$elementStarted = $cursor;
}
preg_match("#^=>#", substr($code, $cursor - 1, 2), $matchesEqualBefore);
preg_match("#^>=#", substr($code, $cursor, 2), $matchesEqualAfter);
if (
$code[$cursor] === ">"
&& !$matchesEqualBefore && !$matchesEqualAfter
&& $attributeLevel < 1
) {
print "ELEMENT ENDED ({$cursor})" . PHP_EOL;
$elementLevel--;
$elementEnded = $cursor;
}
if ($elementStarted && $elementEnded) {
// TODO
$elementStarted = null;
$elementEnded = null;
}
$cursor++;
}
}
// ...
// ELEMENT STARTED (95, <div...)
// ATTRIBUTE STARTED (122, {() =...)
// ATTRIBUTE ENDED (152)
// ATTRIBUTE STARTED (173, {<spa...)
// ATTRIBUTE ENDED (222)
// ELEMENT ENDED (232)
// ELEMENT STARTED (279, <span...)
// ELEMENT ENDED (284)
// ELEMENT STARTED (350, </spa...)
// ELEMENT ENDED (356)
// ELEMENT STARTED (398, </div...)
// ELEMENT ENDED (403)
function tokens($code) {
$tokens = [];
$length = strlen($code);
$cursor = 0;
$elementLevel = 0;
$elementStarted = null;
$elementEnded = null;
$attributes = [];
$attributeLevel = 0;
$attributeStarted = null;
$attributeEnded = null;
$carry = 0;
while ($cursor < $length) {
if ($code[$cursor] === "{" && $elementStarted !== null) {
if ($attributeLevel === 0) {
$attributeStarted = $cursor;
}
$attributeLevel++;
}
if ($code[$cursor] === "}" && $elementStarted !== null) {
$attributeLevel--;
if ($attributeLevel === 0) {
$attributeEnded = $cursor;
}
}
if ($attributeStarted && $attributeEnded) {
$position = (string) count($attributes);
$positionLength = strlen($position);
$attribute = substr(
$code, $attributeStarted + 1, $attributeEnded - $attributeStarted - 1
);
$attributes[$position] = $attribute;
$before = substr($code, 0, $attributeStarted + 1);
$after = substr($code, $attributeEnded);
$code = $before . $position . $after;
$cursor = $attributeStarted + $positionLength + 2 /* curlies */;
$length = strlen($code);
$attributeStarted = null;
$attributeEnded = null;
continue;
}
preg_match("#^</?[a-zA-Z]#", substr($code, $cursor, 3), $matchesStart);
if (count($matchesStart) && $attributeLevel < 1) {
$elementLevel++;
$elementStarted = $cursor;
}
preg_match("#^=>#", substr($code, $cursor - 1, 2), $matchesEqualBefore);
preg_match("#^>=#", substr($code, $cursor, 2), $matchesEqualAfter);
if (
$code[$cursor] === ">"
&& !$matchesEqualBefore && !$matchesEqualAfter
&& $attributeLevel < 1
) {
$elementLevel--;
$elementEnded = $cursor;
}
if ($elementStarted !== null && $elementEnded !== null) {
$distance = $elementEnded - $elementStarted;
$carry += $cursor;
$before = trim(substr($code, 0, $elementStarted));
$tag = trim(substr($code, $elementStarted, $distance + 1));
$after = trim(substr($code, $elementEnded + 1));
$token = ["tag" => $tag, "started" => $carry];
if (count($attributes)) {
$token["attributes"] = $attributes;
}
$tokens[] = $before;
$tokens[] = $token;
$attributes = [];
$code = $after;
$length = strlen($code);
$cursor = 0;
$elementStarted = null;
$elementEnded = null;
continue;
}
$cursor++;
}
return $tokens;
}
$code = '
<?php
$classNames = "foo bar";
$message = "hello world";
$thing = (
<div
className={() => { return "outer-div"; }}
nested={<span className={"nested-span"}>with text</span>}
>
a bit of text before
<span>
{$message} with a bit of extra text
</span>
a bit of text after
</div>
);
';
tokens($code);
// Array
// (
// [0] => <?php
//
// $classNames = "foo bar";
// $message = "hello world";
//
// $thing = (
// [1] => Array
// (
// [tag] => <div className={0} nested={1}>
// [started] => 157
// [attributes] => Array
// (
// [0] => () => { return "outer-div"; }
// [1] => <span className={"nested-span"}>with text</span>
// )
//
// )
//
// [2] => a bit of text before
// [3] => Array
// (
// [tag] => <span>
// [started] => 195
// )
//
// [4] => {$message} with a bit of extra text
// [5] => Array
// (
// [tag] => </span>
// [started] => 249
// )
//
// [6] => a bit of text after
// [7] => Array
// (
// [tag] => </div>
// [started] => 282
// )
//
// )
function tokens($code) {
// ...
while ($cursor < $length) {
// ...
if ($elementStarted !== null && $elementEnded !== null) {
// ...
foreach ($attributes as $key => $value) {
$attributes[$key] = tokens($value);
}
if (count($attributes)) {
$token["attributes"] = $attributes;
}
// ...
}
$cursor++;
}
$tokens[] = trim($code);
return $tokens;
}
// ...
// Array
// (
// [0] => <?php
//
// $classNames = "foo bar";
// $message = "hello world";
//
// $thing = (
// [1] => Array
// (
// [tag] => <div className={0} nested={1}>
// [started] => 157
// [attributes] => Array
// (
// [0] => Array
// (
// [0] => () => { return "outer-div"; }
// )
//
// [1] => Array
// (
// [1] => Array
// (
// [tag] => <span className={0}>
// [started] => 19
// [attributes] => Array
// (
// [0] => Array
// (
// [0] => "nested-span"
// )
//
// )
//
// )
//
// [2] => with text
// [3] => Array
// (
// [tag] => </span>
// [started] => 34
// )
// )
//
// )
//
// )
//
// ...
Organizing Tokens
function nodes($tokens) {
$cursor = 0;
$length = count($tokens);
while ($cursor < $length) {
$token = $tokens[$cursor];
if (is_array($token)) {
print $token["tag"] . PHP_EOL;
}
$cursor++;
}
}
$tokens = [
0 => '<?php
$classNames = "foo bar";
$message = "hello world";
$thing = (',
1 => [
'tag' => '<div className={0} nested={1}>',
'started' => 157,
'attributes' => [
0 => [
0 => '() => { return "outer-div"; }',
],
1 => [
1 => [
'tag' => '<span className={0}>',
'started' => 19,
'attributes' => [
0 => [
0 => '"nested-span"',
],
],
],
2 => 'with text</span>',
],
],
],
2 => 'a bit of text before',
3 => [
'tag' => '<span>',
'started' => 195,
],
4 => '{$message} with a bit of extra text',
5 => [
'tag' => '</span>',
'started' => 249,
],
6 => 'a bit of text after',
7 => [
'tag' => '</div>',
'started' => 282,
],
8 => ');',
];
nodes($tokens);
// <div className={0} nested={1}>
// <span>
// </span>
// </div>
function nodes($tokens) {
$cursor = 0;
$length = count($tokens);
while ($cursor < $length) {
$token = $tokens[$cursor];
if (is_array($token) && $token["tag"][1] !== "/") {
preg_match("#^<([a-zA-Z]+)#", $token["tag"], $matches);
print "OPENING {$matches[1]}" . PHP_EOL;
}
if (is_array($token) && $token["tag"][1] === "/") {
preg_match("#^</([a-zA-Z]+)#", $token["tag"], $matches);
print "CLOSING {$matches[1]}" . PHP_EOL;
}
$cursor++;
}
return $tokens;
}
// ...
// OPENING div
// OPENING span
// CLOSING span
// CLOSING div
function nodes($tokens) {
$nodes = [];
$current = null;
$cursor = 0;
$length = count($tokens);
while ($cursor < $length) {
$token =& $tokens[$cursor];
if (is_array($token) && $token["tag"][1] !== "/") {
preg_match("#^<([a-zA-Z]+)#", $token["tag"], $matches);
if ($current !== null) {
$token["parent"] =& $current;
$current["children"][] =& $token;
} else {
$token["parent"] = null;
$nodes[] =& $token;
}
$current =& $token;
$current["name"] = $matches[1];
$current["children"] = [];
if (isset($current["attributes"])) {
foreach ($current["attributes"] as $key => $value) {
$current["attributes"][$key] = nodes($value);
}
$current["attributes"] = array_map(function($item) {
foreach ($item as $value) {
if (isset($value["tag"])) {
return $value;
}
}
foreach ($item as $value) {
if (!empty($value["token"])) {
return $value;
}
}
return null;
}, $current["attributes"]);
}
}
else if (is_array($token) && $token["tag"][1] === "/") {
preg_match("#^</([a-zA-Z]+)#", $token["tag"], $matches);
if ($current === null) {
throw new Exception("no open tag");
}
if ($matches[1] !== $current["name"]) {
throw new Exception("no matching open tag");
}
if ($current !== null) {
$current =& $current["parent"];
}
}
else if ($current !== null) {
array_push($current["children"], [
"parent" => &$current,
"token" => &$token,
]);
}
else {
array_push($nodes, [
"token" => $token,
]);
}
$cursor++;
}
return $nodes;
}
// ...
// Array
// (
// [0] => Array
// (
// [token] => <?php
//
// $classNames = "foo bar";
// $message = "hello world";
//
// $thing = (
// )
//
// [1] => Array
// (
// [tag] => <div className={0} nested={1}>
// [started] => 157
// [attributes] => Array
// (
// [0] => Array
// (
// [token] => () => { return "outer-div"; }
// )
//
// [1] => Array
// (
// [tag] => <span className={0}>
// [started] => 19
// [attributes] => Array
// (
// [0] => Array
// (
// [token] => "nested-span"
// )
//
// )
//
// [parent] =>
// [name] => span
// [children] => Array
// (
// [0] => Array
// (
// [parent] => *RECURSION*
// [token] => with text
// )
//
// )
//
// )
//
// )
//
// [parent] =>
// [name] => div
// [children] => Array
// (
// [0] => Array
// (
// [parent] => *RECURSION*
// [token] => a bit of text before
// )
//
// [1] => Array
// (
// [tag] => <span>
// [started] => 195
// [parent] => *RECURSION*
// [name] => span
// [children] => Array
// (
// [0] => Array
// (
// [parent] => *RECURSION*
// [token] => {$message} with ...
// )
//
// )
//
// )
//
// [2] => Array
// (
// [parent] => *RECURSION*
// [token] => a bit of text after
// )
//
// )
//
// )
//
// [2] => Array
// (
// [token] => );
// )
//
// )
Rewriting Code
function parse($nodes) {
$code = "";
foreach ($nodes as $node) {
if (isset($node["token"])) {
$code .= $node["token"] . PHP_EOL;
}
}
return $code;
}
$nodes = [
0 => [
'token' => '<?php
$classNames = "foo bar";
$message = "hello world";
$thing = (',
],
1 => [
'tag' => '<div className={0} nested={1}>',
'started' => 157,
'attributes' => [
0 => [
'token' => '() => { return "outer-div"; }',
],
1 => [
'tag' => '<span className={0}>',
'started' => 19,
'attributes' => [
0 => [
'token' => '"nested-span"',
],
],
'name' => 'span',
'children' => [
0 => [
'token' => 'with text',
],
],
],
],
'name' => 'div',
'children' => [
0 => [
'token' => 'a bit of text before',
],
1 => [
'tag' => '<span>',
'started' => 195,
'name' => 'span',
'children' => [
0 => [
'token' => '{$message} with a bit of extra text',
],
],
],
2 => [
'token' => 'a bit of text after',
],
],
],
2 => [
'token' => ');',
],
];
parse($nodes);
// <?php
//
// $classNames = "foo bar";
// $message = "hello world";
//
// $thing = (
// );
require __DIR__ . "/vendor/autoload.php";
function parse($nodes) {
$code = "";
foreach ($nodes as $node) {
if (isset($node["token"])) {
$code .= $node["token"] . PHP_EOL;
}
if (isset($node["tag"])) {
$props = [];
$attributes = [];
$elements = [];
if (isset($node["attributes"])) {
foreach ($node["attributes"] as $key => $value) {
if (isset($value["token"])) {
$attributes["attr_{$key}"] = $value["token"];
}
if (isset($value["tag"])) {
$elements[$key] = true;
$attributes["attr_{$key}"] = parse([$value]);
}
}
}
preg_match_all("#([a-zA-Z]+)={([^}]+)}#", $node["tag"], $dynamic);
preg_match_all("#([a-zA-Z]+)=[']([^']+)[']#", $node["tag"], $static);
if (count($dynamic[0])) {
foreach($dynamic[1] as $key => $value) {
$props["{$value}"] = $attributes["attr_{$key}"];
}
}
if (count($static[1])) {
foreach($static[1] as $key => $value) {
$props["{$value}"] = $static[2][$key];
}
}
$code .= "pre_" . $node["name"] . "([" . PHP_EOL;
foreach ($props as $key => $value) {
$code .= "'{$key}' => {$value}," . PHP_EOL;
}
$code .= "])" . PHP_EOL;
}
}
$code = Pre\Plugin\expand($code);
$code = Pre\Plugin\formatCode($code);
return $code;
}
// ...
// <?php
//
// $classNames = "foo bar";
// $message = "hello world";
//
// $thing = (
// pre_div([
// 'className' => function () {
// return "outer-div";
// },
// 'nested' => pre_span([
// 'className' => "nested-span",
// ]),
// ])
// );
composer require pre/short-closures
require __DIR__ . "/vendor/autoload.php";
function parse($nodes) {
$code = "";
foreach ($nodes as $node) {
if (isset($node["token"])) {
$code .= $node["token"] . PHP_EOL;
}
if (isset($node["tag"])) {
// ...
$children = [];
foreach ($node["children"] as $child) {
if (isset($child["tag"])) {
$children[] = parse([$child]);
}
else {
$children[] = "\"" . addslashes($child["token"]) . "\"";
}
}
$props["children"] = $children;
$code .= "pre_" . $node["name"] . "([" . PHP_EOL;
foreach ($props as $key => $value) {
if ($key === "children") {
$code .= "\"children\" => [" . PHP_EOL;
foreach ($children as $child) {
$code .= "{$child}," . PHP_EOL;
}
$code .= "]," . PHP_EOL;
}
else {
$code .= "\"{$key}\" => {$value}," . PHP_EOL;
}
}
$code .= "])" . PHP_EOL;
}
}
$code = Pre\Plugin\expand($code);
$code = Pre\Plugin\formatCode($code);
return $code;
}
// ...
// <?php
//
// $classNames = "foo bar";
// $message = "hello world";
//
// $thing = (
// pre_div([
// "className" => function () {
// return "outer-div";
// },
// "nested" => pre_span([
// "className" => "nested-span",
// "children" => [
// "with text",
// ],
// ]),
// "children" => [
// "a bit of text before",
// pre_span([
// "children" => [
// "{$message} with a bit of extra text",
// ],
// ]),
// "a bit of text after",
// ],
// ])
// );
require __DIR__ . "/vendor/autoload.php";
function pre_div($props) {
$code = "<div";
if (isset($props["className"])) {
if (is_callable($props["className"])) {
$class = $props["className"]();
}
else {
$class = $props["className"];
}
$code .= " class='{$class}'";
}
$code .= ">";
foreach ($props["children"] as $child) {
$code .= $child;
}
$code .= "</div>";
return trim($code);
}
function pre_span($props) {
$code = pre_div($props);
$code = preg_replace("#^<div#", "<span", $code);
$code = preg_replace("#div>$#", "span>", $code);
return $code;
}
function parse($nodes) {
// ...
}
$nodes = [
0 => [
'token' => '<?php
$classNames = "foo bar";
$message = "hello world";
$thing = (',
],
1 => [
'tag' => '<div className={0} nested={1}>',
'started' => 157,
'attributes' => [
0 => [
'token' => '() => { return $classNames; }',
],
1 => [
'tag' => '<span className={0}>',
'started' => 19,
'attributes' => [
0 => [
'token' => '"nested-span"',
],
],
'name' => 'span',
'children' => [
0 => [
'token' => 'with text',
],
],
],
],
'name' => 'div',
'children' => [
0 => [
'token' => 'a bit of text before',
],
1 => [
'tag' => '<span>',
'started' => 195,
'name' => 'span',
'children' => [
0 => [
'token' => '{$message} with a bit of extra text',
],
],
],
2 => [
'token' => 'a bit of text after',
],
],
],
2 => [
'token' => ');',
],
3 => [
'token' => 'print $thing;',
],
];
eval(substr(parse($nodes), 5));
// <div class='foo bar'>
// a bit of text before
// <span>
// hello world with a bit of extra text
// </span>
// a bit of text after
// </div>
Integrating with Pre
use Silex\Application;
use Silex\Provider\SessionServiceProvider;
use Symfony\Component\HttpFoundation\Request;
use App\Component\AddTask;
use App\Component\Page;
use App\Component\TaskList;
$app = new Application();
$app->register(new SessionServiceProvider());
$app->get("/", (Request $request) => {
$session = $request->getSession();
$tasks = $session->get("tasks", []);
return (
<Page>
<TaskList>{$tasks}</TaskList>
<AddTask></AddTask>
</Page>
);
});
$app->post("/add", (Request $request) => {
$session = $request->getSession();
$id = $session->get("id", 0);
$tasks = $session->get("tasks", []);
$tasks[] = [
"id" => $id++,
"text" => $request->get("text"),
];
$session->set("id", $id);
$session->set("tasks", $tasks);
return $app->redirect("/");
});
$app->get("/remove/{id}", (Request $request, $id) => {
$session = $request->getSession();
$tasks = $session->get("tasks", []);
$tasks = array_filter($tasks, ($task) => {
return $task["id"] !== (int) $id;
});
$session->set("tasks", $tasks);
return $app->redirect("/");
});
$app->run();
require __DIR__ . "/../vendor/autoload.php";
Pre\Plugin\process(__DIR__ . "/../server.pre");
php -S localhost:8080 -t public public/index.php
namespace App\Component;
use InvalidArgumentException;
class Page
{
public function render($props)
{
assert($this->hasValid($props));
{ $children } = $props;
return (
"<!doctype html>".
<html lang="en">
<body>
{$children}
</body>
</html>
);
}
private function hasValid($props)
{
if (empty($props["children"])) {
throw new InvalidArgumentException("page needs content (children)");
}
return true;
}
}
namespace App\Component;
class TaskList
{
public function render($props)
{
{ $children } = $props;
return (
<ul className={"task-list"}>
{$this->children($children)}
</ul>
);
}
private function children($children)
{
if (count($children)) {
return {$children}->map(($task) => {
return (
<Task id={$task["id"]}>{$task["text"]}</Task>
);
});
}
return (
<span>No tasks</span>
);
}
}
- A literal value expression, like
"task-list"
- An array (or key-less
pre/collection
object), like["first", "second"]
- An associative array (or keyed
pre/collection
object), like["first" => true, "second" => false]
namespace App\Component;
use InvalidArgumentException;
class Task
{
public function render($props)
{
assert($this->hasValid($props));
{ $children, $id } = $props;
return (
<li className={"task"}>
{$children}
<a href={"/remove/{$id}"}>remove</a>
</li>
);
}
private function hasValid($props)
{
if (!isset($props["id"])) {
throw new InvalidArgumentException("task needs id (attribute)");
}
if (empty($props["children"])) {
throw new InvalidArgumentException("task needs text (children)");
}
return true;
}
}
namespace App\Component;
class AddTask
{
public function render($props)
{
return (
<form method={"post"} action={"/add"} className={"add-task"}>
<input name={"text"} type={"text"} />
<button type={"submit"}>add</button>
</form>
);
}
}
$app->post("/add", (Request $request) => {
$session = $request->getSession();
$id = $session->get("id", 1);
$tasks = $session->get("tasks", []);
$tasks[] = [
"id" => $id++,
"text" => $request->get("text"),
];
$session->set("id", $id);
$session->set("tasks", $tasks);
return $app->redirect("/");
});
$app->get("/remove/{id}", (Request $request, $id) => {
$session = $request->getSession();
$tasks = $session->get("tasks", []);
$tasks = array_filter($tasks, ($task) => {
return $task["id"] !== (int) $id;
});
$session->set("tasks", $tasks);
return $app->redirect("/");
});
Comments
Post a Comment