From e2868cd7bf3d8948c4027792637c6608c7736be4 Mon Sep 17 00:00:00 2001 From: Christopher Vagnetoft Date: Tue, 1 Oct 2024 18:46:03 +0200 Subject: [PATCH] Initial commit --- .gitignore | 1 + bin/jedit | 4 + composer.json | 20 ++ composer.lock | 169 +++++++++++++++ src/Editor/Editor.php | 336 ++++++++++++++++++++++++++++++ src/List/Entry.php | 18 ++ src/List/TreeList.php | 179 ++++++++++++++++ src/Terminal/Terminal.php | 135 ++++++++++++ src/Tree/ArrayNode.php | 31 +++ src/Tree/CollapsibleNode.php | 11 + src/Tree/CollapsibleNodeTrait.php | 21 ++ src/Tree/Node.php | 8 + src/Tree/ObjectNode.php | 33 +++ src/Tree/Tree.php | 35 ++++ src/Tree/ValueNode.php | 11 + src/entry.php | 24 +++ 16 files changed, 1036 insertions(+) create mode 100644 .gitignore create mode 100755 bin/jedit create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 src/Editor/Editor.php create mode 100644 src/List/Entry.php create mode 100644 src/List/TreeList.php create mode 100644 src/Terminal/Terminal.php create mode 100644 src/Tree/ArrayNode.php create mode 100644 src/Tree/CollapsibleNode.php create mode 100644 src/Tree/CollapsibleNodeTrait.php create mode 100644 src/Tree/Node.php create mode 100644 src/Tree/ObjectNode.php create mode 100644 src/Tree/Tree.php create mode 100644 src/Tree/ValueNode.php create mode 100644 src/entry.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57872d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/vendor/ diff --git a/bin/jedit b/bin/jedit new file mode 100755 index 0000000..d3db7f2 --- /dev/null +++ b/bin/jedit @@ -0,0 +1,4 @@ +#!/usr/bin/env php +=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "4e561c316e135e053bd758bf3b3eb291d9919de4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/4e561c316e135e053bd758bf3b3eb291d9919de4", + "reference": "4e561c316e135e053bd758bf3b3eb291d9919de4", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-17T12:49:58+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/src/Editor/Editor.php b/src/Editor/Editor.php new file mode 100644 index 0000000..0055c34 --- /dev/null +++ b/src/Editor/Editor.php @@ -0,0 +1,336 @@ +document = new Tree(); + + // $this->document->load((object)[ + // 'foo' => true, + // 'bar' => [ + // 'a', 'b', 'c' + // ], + // 'what' => 42 + // ]); + + $this->list = new TreeList($this->document); + } + + public function loadFile(string $filename): void + { + $this->filename = $filename; + $this->shortfilename = basename($filename); + $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + switch ($ext) { + case 'json': + $doc = json_decode(file_get_contents($filename)); + break; + case 'yml': + case 'yaml': + $doc = Yaml::parseFile($filename); + break; + default: + throw new \RuntimeException("Unable to read file of type {$ext}"); + } + $this->document->load($doc); + $this->list->parseTree(); + } + + public function loadDocument(mixed $document): void + { + $this->document->load($document); + $this->list->parseTree(); + } + + private bool $running = true; + + public function run() + { + $this->list->parseTree(); + $this->redrawEditor(); + + while ($this->running) { + [$w,$h] = $this->term->getSize(); + $read = $this->term->readKey(); + switch ($read) { + case 'k{UP}': + $this->currentRow = max(0, $this->currentRow - 1); + $this->redrawEditor(); + break; + case 'k{DOWN}': + $this->currentRow = min(count($this->list), $this->currentRow + 1); + $this->redrawEditor(); + break; + case 'k{PGUP}': + $this->currentRow = max(0, $this->currentRow - ($h - 3)); + $this->redrawEditor(); + break; + case 'k{PGDN}': + $this->currentRow = min(count($this->list), $this->currentRow + ($h - 3)); + $this->redrawEditor(); + break; + + case 'E': + //$coll = $this->list->findNearestCollection($this->currentRow); + $entry = $this->list->getEntryForIndex($this->currentRow); + $path = $this->list->getPathForIndex($this->currentRow); + $parentIndex = $this->list->getIndexForPath(dirname($path)); + $parent = $this->list->getEntryForIndex($parentIndex); + + if ($parent->node instanceof ObjectNode) { + $newVal = $this->ask("\e[33;1mnew key:\e[0m ", $entry->key); + if ($newVal !== null) { + $entry->key = $newVal; + $this->redrawEditor(); + } + } else { + $this->term->setCursor(1, $h); + echo "\e[97;41mCan only edit keys on objects\e[K\e[0m"; + } + break; + + case 'e': + //$coll = $this->list->findNearestCollection($this->currentRow); + $node = $this->list->getNodeForIndex($this->currentRow); + if ($node instanceof ValueNode) { + $val = json_encode($node->value, JSON_UNESCAPED_SLASHES); + $newVal = $this->ask("\e[33;1mnew value:\e[0m ", $val); + if ($newVal !== null) { + $val = json_decode($newVal); + $node->value = $val; + $this->redrawEditor(); + } else { + $this->redrawInfoBar(); + } + } else { + $this->term->setCursor(1, $h); + echo "\e[97;41mCan not edit array/object\e[K\e[0m"; + } + break; + + case '[': + break; + + case 'D': + $node = $this->list->getNodeForIndex($this->currentRow); + $path = $this->list->getPathForIndex($this->currentRow); + $parentPath = dirname($path); + $deleteKey = basename($path); + $collNode = $this->list->getNodeForPath($parentPath); + if ($collNode instanceof ArrayNode) { + $collNode->removeIndex($deleteKey); + $this->list->parseTree(); + $this->redrawEditor(); + } elseif ($collNode instanceof ObjectNode) { + $collNode->unset($deleteKey); + $this->list->parseTree(); + $this->redrawEditor(); + } else { + $this->term->setCursor(1, $h); + echo "\e[97;41mCan only delete from object, array\e[K\e[0m"; + } + break; + + // FIXME make sure this clones; editing a clone updates original as well + // case 'd': + // $node = $this->list->getNodeForIndex($this->currentRow); + // $coll = $this->list->findNearestCollection($this->currentRow, true); + // if (!$coll) + // break; + // $collNode = $this->list->getNodeForIndex($coll); + // if ($collNode instanceof ArrayNode) { + // $collNode->append(clone $node); + // $this->list->parseTree(); + // $this->redrawEditor(); + // } else { + // $this->term->setCursor(1, $h); + // echo "\e[97;41mCan only duplicate in arrays, node={$this->currentRow}, coll={$coll}\e[K\e[0m"; + // } + // break; + + case 'i': + $coll = $this->list->findNearestCollection($this->currentRow); + $node = $this->list->getNodeForIndex($coll); + if ($node instanceof ObjectNode) { + $key = $this->ask("\e[97mkey:\e[0m "); + if ($key === null) { + $this->redrawInfoBar(); + break; + } + } + + $value = $this->ask("\e[97mvalue:\e[0m "); + if ($value !== null) { + $value = json_decode($value); + $valueNode = match (true) { + is_array($value) => new ArrayNode([]), + is_object($value) => new ObjectNode([]), + default => new ValueNode($value) + }; + if ($node instanceof ArrayNode) { + $node->append($valueNode); + } elseif ($node instanceof ObjectNode) { + $node->set($key, $valueNode); + } + $this->list->parseTree(); + $this->redrawEditor(); + } else { + $this->redrawInfoBar(); + } + break; + + case 'Q': + case "\x03": + $this->running = false; + break; + + case null: + break; + default: + $this->term->setCursor(1, $h, true); + echo $read; + sleep(1); + } + } + + // for ($n = 0; $n < 20; $n++) { + // $this->currentRow = $n; + // $this->redrawEditor(); + // sleep(1); + // } + + } + + public function ask(string $prompt, $value = ''): ?string + { + $plainPrompt = preg_replace('<\e\[.+?m>', '', $prompt); + $promptLen = mb_strlen($plainPrompt); + [$w,$h] = $this->term->getSize(); + + $prompting = true; + $cursorPos = mb_strlen($value); + while ($prompting) { + $this->term->setCursor(1, $h); + echo $prompt."\e[0m\e[K".$value; + $this->term->setCursor($promptLen+$cursorPos+1, $h, true); + while (null === ($ch = $this->term->readKey())) { + usleep(10000); + } + if (mb_strlen($ch) == 1) { + if (ord($ch) < 32) { + if (ord($ch) == 13) { + $this->term->setCursor(1, $h); + echo "\e[0m\e[K"; + return $value; + } + if (ord($ch) == 3) { + $this->term->setCursor(1, $h); + echo "\e[0m\e[K"; + return null; + } + } elseif (ord($ch) == 127) { + if ($cursorPos > 0) { + $value = substr($value, 0, $cursorPos - 1) . substr($value, $cursorPos); + $cursorPos--; + } + } else { + $value = mb_substr($value, 0, $cursorPos) . $ch . mb_substr($value, $cursorPos); + $cursorPos++; + } + } else { + switch ($ch) { + case "k{LEFT}": + if ($cursorPos > 0) + $cursorPos--; + break; + case "k{RIGHT}": + if ($cursorPos < mb_strlen($value)) + $cursorPos++; + break; + } + } + } + + } + + public function redrawEditor() + { + [$w,$h] = $this->term->getSize(); + + while ($this->currentRow < $this->scrollOffset) $this->scrollOffset--; + while ($this->currentRow > $h + $this->scrollOffset - 3) $this->scrollOffset++; + + $path = $this->list->getPathForIndex($this->currentRow); + $node = $this->list->getNodeForIndex($this->currentRow); + + $this->term->setCursor(1, $h-1); + echo "\e[44;37m\e[K\e[1m{$this->shortfilename}\e[22m#\e[3m{$path}\e[37;23m"; + + //$this->term->setCursor(1, $h); + echo " = "; + if ($node instanceof ValueNode) { + echo json_encode($node->value, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE); + } elseif ($node instanceof ArrayNode) { + echo "[…]"; + } elseif ($node instanceof ObjectNode) { + echo "{…}"; + } + echo "\e[0m"; + + $this->redrawInfoBar(); + + // $this->term->setCursor(1, $h); + // echo "\e[0;37m\e[K"; + + for ($n = 0; $n < $h - 2; $n++) { + $this->list->drawEntry($n + 1, $n + $this->scrollOffset, $w, ($n + $this->scrollOffset == $this->currentRow)); + } + // $this->term->setCursor(1, 1); + // echo "\e[90;3m«Empty»\e[0m"; + } + + private function redrawInfoBar() + { + [$w,$h] = $this->term->getSize(); + + $keys = [ + 'e' => 'Edit', + 'E' => 'Edit key', + 'i' => 'Insert', + 'D' => 'Delete', + 'C' => 'Copy', + 'P' => 'Paste', + '^C' => 'Exit', + ]; + + $this->term->setCursor(1, $h); + echo "\e[0m\e[K"; + foreach ($keys as $key=>$info) + echo " \e[1m{$key}\e[2m \e[3m{$info}\e[0m \e[90m\u{2502}\e[0m"; + } + +} diff --git a/src/List/Entry.php b/src/List/Entry.php new file mode 100644 index 0000000..5c493ba --- /dev/null +++ b/src/List/Entry.php @@ -0,0 +1,18 @@ + */ + public array $list = []; + + public function __construct(private Tree $tree) + { + } + + public function count(): int + { + return count($this->list); + } + + public function parseTree(): void + { + $this->list = []; + $this->parseNode($this->tree->root, [], null); + } + + private function parseNode(Node $node, array $path, string|int|null $key): void + { + $level = count($path); + $entryKey = join("/", $path) . (is_null($key) ? "/" : (is_int($key) ? sprintf("/%d", $key) : sprintf("/%s", $key))); + + $entry = new Entry(depth: $level, key: $key, node: $node); + + $this->list[$entryKey] = $entry; + + if ($node instanceof ArrayNode) { + $index = 0; + foreach ($node->items as $item) { + $this->parseNode($item, [ ...$path, $key ], $index++); + } + $this->list[$entryKey.'$'] = new Entry(depth: $level, key: $key, node: $node, closer: true); + } elseif ($node instanceof ObjectNode) { + foreach ($node->properties as $nodekey=>$item) { + $this->parseNode($item, [ ...$path, $key ], $nodekey); + } + $this->list[$entryKey.'$'] = new Entry(depth: $level, key: $key, node: $node, closer: true); + + } + } + + public function getPathForIndex(int $index): ?string + { + return array_keys($this->list)[$index]??null; + } + + public function getIndexForPath(string $path): ?int + { + return array_search($path, array_keys($this->list)); + } + + public function getNodeForPath(string $path): ?Node + { + $entry = $this->list[$path]??null; + return $entry ? $entry->node : null; + } + + public function getNodeForIndex(int $index): ?Node + { + $key = array_keys($this->list)[$index]??null; + $entry = $this->list[$key]??null; + if (!$entry) return null; + return $entry->node; + } + + public function getEntryForIndex(int $index): ?Entry + { + $key = array_keys($this->list)[$index]??null; + $entry = $this->list[$key]??null; + if (!$entry) return null; + return $entry; + } + + public function findNearestCollection(int $index, bool $ignoreSelf = false): ?int + { + $path = $this->getPathForIndex($index); + if ($ignoreSelf) { + $path = dirname($path); + } + while (strlen($path) > 0) { + $entry = $this->list[$path]; + $node = $entry->node; + if ($node instanceof ArrayNode || $node instanceof ObjectNode) { + return $this->getIndexForPath($path); + } + $path = dirname($path); + } + + // $depth = $this->getEntryForIndex($index)->depth; + // if ($ignoreSelf) $depth = max(0, $depth - 1); + // echo "{start={$depth}}"; sleep(1); + // while ($index >= 0) { + // $entry = $this->getEntryForIndex($index); + // if ($entry->depth < $depth) { + // $node = $entry->node; + // if ($node instanceof ArrayNode || $node instanceof ObjectNode) { + // return $index; + // } + // } + // $index--; + // } + return null; + } + + public function drawEntry(int $screenRow, int $entryRow, int $columns, bool $selected): void + { + + $keys = array_keys($this->list); + echo "\e[{$screenRow};1H"; + if ($entryRow >= count($keys)) { + echo ($selected?"\e[100;97m":"\e[0;90m")."\e[K"; + if ($entryRow == count($keys)) echo str_repeat("\u{2574}",$columns); + echo "\e[0m"; + //else echo "\e[90m\u{2805}\e[0m"; + return; + } + $key = $keys[$entryRow]; + if (!isset($this->list[$key])) { + echo "\e[0m\e[K\e[31mE_NO_KEY_IN_LIST\e[0m"; + return; + } + $entry = $this->list[$key]; + if (!$entry) { + echo "\e[0m\e[K\e[31mE_NO_ENTRY_IN_LIST\e[0m"; + return; + } + echo "\e[{$screenRow};1H\e[0m"; + echo ($selected?"\e[100;97m":"\e[0;37m")."\e[K"; + echo "\e[90m".str_repeat("\u{258f} ",$entry->depth)."\e[37m"; + + if ($entry->closer) { + if ($entry->node instanceof ArrayNode) { + echo "]"; + } elseif ($entry->node instanceof ObjectNode) { + echo "}"; + } + return; + } + + if (!is_null($entry->key)) { + echo (is_int($entry->key) + ?"\e[36;2m\u{e0b6}\e[7m#{$entry->key}\e[27m\u{e0b4}\e[22m " + :"\e[36m\"{$entry->key}\":\e[37m "); + } + if ($entry->node instanceof ArrayNode) { + echo "["; + } elseif ($entry->node instanceof ObjectNode) { + echo "{"; + } elseif ($entry->node instanceof ValueNode) { + $value = $entry->node->value; + echo match (gettype($value)) { + 'string' => "\e[33m", + 'integer' => "\e[34m", + 'double' => "\e[32m", + 'boolean' => "\e[35m", + 'NULL' => "\e[31m", + default => "", + }; + echo json_encode($entry->node->value, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE); + } + + } + +} + diff --git a/src/Terminal/Terminal.php b/src/Terminal/Terminal.php new file mode 100644 index 0000000..e049313 --- /dev/null +++ b/src/Terminal/Terminal.php @@ -0,0 +1,135 @@ +setup(); + } + } + + public function __destruct() + { + if (--self::$init == 0) { + $this->shutdown(); + } + } + + private function setup(): void + { + $this->oldStty = exec("stty -g"); + exec("stty raw -echo"); + + //ob_start(); + + echo "\e[s"; // save cursor pos + echo "\e[?1047h"; // alt buffer + echo "\e[H\e[0m\e[2J"; // clear screen + echo "\e[?7l"; // autowrap + + //ob_flush(); + + $this->measureTerminal(); + pcntl_signal(SIGWINCH, $this->measureTerminal(...)); + + $this->active = true; + } + + private function measureTerminal(): void + { + $this->lines = intval(exec("tput lines")); + $this->columns = intval(exec("tput cols")); + } + + public function shutdown(): void + { + //ob_end_clean(); + if (!$this->active) return; + + exec("stty {$this->oldStty}"); + + echo "\e[?1047l"; // main buffer + echo "\e[u\e[?25h"; // restore cursor pos and visibility + echo "\e[?7h"; // autowrap + + $this->active = false; + } + + public function getSize(): array + { + return [ $this->columns, $this->lines ]; + } + + public function setCursor(int $column, int $row, bool $visible = false): void + { + if ($row > 0 && $column > 0) + printf("\e[%d;%dH", $row, $column); + echo "\e[?25".($visible?"h":"l"); + } + + private string $inputBuffer = ''; + + public function readKey(): ?string + { + $r = [ STDIN ]; $w = $e = []; + if (stream_select($r, $w, $e, 0)) { + $read = fread(STDIN, 64); + $this->inputBuffer .= $read; + } + return $this->parseInputBuffer(); + } + + private function parseInputBuffer(): ?string + { + if (str_starts_with($this->inputBuffer, "\e")) { + if (strncmp($this->inputBuffer, "\e[A", 3) === 0) { + $this->inputBuffer = mb_substr($this->inputBuffer, 3); + return "k{UP}"; + } elseif (strncmp($this->inputBuffer, "\e[B", 3) === 0) { + $this->inputBuffer = mb_substr($this->inputBuffer, 3); + return "k{DOWN}"; + } elseif (strncmp($this->inputBuffer, "\e[C", 3) === 0) { + $this->inputBuffer = mb_substr($this->inputBuffer, 3); + return "k{RIGHT}"; + } elseif (strncmp($this->inputBuffer, "\e[D", 3) === 0) { + $this->inputBuffer = mb_substr($this->inputBuffer, 3); + return "k{LEFT}"; + } elseif (strncmp($this->inputBuffer, "\e[5~", 4) === 0) { + $this->inputBuffer = mb_substr($this->inputBuffer, 4); + return "k{PGUP}"; + } elseif (strncmp($this->inputBuffer, "\e[6~", 4) === 0) { + $this->inputBuffer = mb_substr($this->inputBuffer, 4); + return "k{PGDN}"; + } elseif (strncmp($this->inputBuffer, "\e\e", 2) === 0) { + $this->inputBuffer = mb_substr($this->inputBuffer, 2); + return chr(27); + } + $this->inputBuffer = mb_substr($this->inputBuffer, 1); + } + if (mb_strlen($this->inputBuffer) > 0) { + $ch = mb_substr($this->inputBuffer, 0, 1); + // if (ord($ch) == 3) { + // return "k{^C}"; + // } + $this->inputBuffer = mb_substr($this->inputBuffer, 1); + return $ch; + } + return null; + } + +} + diff --git a/src/Tree/ArrayNode.php b/src/Tree/ArrayNode.php new file mode 100644 index 0000000..7b93a90 --- /dev/null +++ b/src/Tree/ArrayNode.php @@ -0,0 +1,31 @@ +items[] = $value; + } + + public function removeIndex(int $index) + { + unset($this->items[$index]); + $this->items = array_values($this->items); + } + + public function __clone() + { + $items = array_map(fn($v) => clone $v, $this->items); + return new ArrayNode($items); + } + +} + diff --git a/src/Tree/CollapsibleNode.php b/src/Tree/CollapsibleNode.php new file mode 100644 index 0000000..84e41a0 --- /dev/null +++ b/src/Tree/CollapsibleNode.php @@ -0,0 +1,11 @@ +collapsed = $collapsed; + return $this; + } + + public function isCollapsed(): bool + { + return $this->collapsed; + } + +} + diff --git a/src/Tree/Node.php b/src/Tree/Node.php new file mode 100644 index 0000000..0935daa --- /dev/null +++ b/src/Tree/Node.php @@ -0,0 +1,8 @@ +properties[$key] = $value; + } + + public function unset(string $key) + { + unset($this->properties[$key]); + } + + public function __clone() + { + $properties = array_combine( + array_keys($this->properties), + array_map(fn($v) => clone $v, $this->properties) + ); + return new ObjectNode($properties); + } + +} + diff --git a/src/Tree/Tree.php b/src/Tree/Tree.php new file mode 100644 index 0000000..542c22f --- /dev/null +++ b/src/Tree/Tree.php @@ -0,0 +1,35 @@ +root = $this->parseNode($document); + return $this; + } + + private function parseNode(mixed $node): Node + { + if (is_array($node) && array_is_list($node)) { + return new ArrayNode( + array_map($this->parseNode(...), $node) + ); + } elseif (is_object($node) || is_array($node)) { + return new ObjectNode( + array_combine( + array_keys((array)$node), + array_map($this->parseNode(...), (array)$node) + ) + ); + } else { + return new ValueNode($node); + } + } + +} + diff --git a/src/Tree/ValueNode.php b/src/Tree/ValueNode.php new file mode 100644 index 0000000..8f80528 --- /dev/null +++ b/src/Tree/ValueNode.php @@ -0,0 +1,11 @@ +loadFile($filename); +} else { + $editor->loadDocument((object)[]); +} + + +set_exception_handler(function (\Throwable $t) use ($terminal) { + register_shutdown_function(function () use ($t, $terminal) { + $terminal->shutdown(); + echo $t."\n"; + }); +}); + +$editor->run();