Initial commit

This commit is contained in:
Chris 2024-10-01 18:46:03 +02:00
commit e2868cd7bf
16 changed files with 1036 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/vendor/

4
bin/jedit Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env php
<?php
require_once __DIR__."/../src/entry.php";

20
composer.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "noccylabs/jedit",
"description": "JSON/YAML editor for the terminal",
"type": "application",
"license": "GPL-3.0-or-later",
"autoload": {
"psr-4": {
"NoccyLabs\\JEdit\\": "src/"
}
},
"authors": [
{
"name": "Christopher Vagnetoft",
"email": "labs@noccy.com"
}
],
"require": {
"symfony/yaml": "^7.1"
}
}

169
composer.lock generated Normal file
View File

@ -0,0 +1,169 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f4286ca23e3bc801977031988fb6cfe0",
"packages": [
{
"name": "symfony/polyfill-ctype",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
"shasum": ""
},
"require": {
"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"
}

336
src/Editor/Editor.php Normal file
View File

@ -0,0 +1,336 @@
<?php
namespace NoccyLabs\JEdit\Editor;
use NoccyLabs\JEdit\List\TreeList;
use NoccyLabs\JEdit\Terminal\Terminal;
use NoccyLabs\JEdit\Tree\ArrayNode;
use NoccyLabs\JEdit\Tree\ObjectNode;
use NoccyLabs\JEdit\Tree\Tree;
use NoccyLabs\JEdit\Tree\ValueNode;
use Symfony\Component\Yaml\Yaml;
class Editor
{
private Tree $document;
private TreeList $list;
private int $currentRow = 0;
private int $scrollOffset = 0;
private ?string $filename = "untitled.json";
private ?string $shortfilename = "untitled.json";
public function __construct(private Terminal $term)
{
$this->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";
}
}

18
src/List/Entry.php Normal file
View File

@ -0,0 +1,18 @@
<?php
namespace NoccyLabs\JEdit\List;
use NoccyLabs\JEdit\Tree\Tree;
use NoccyLabs\JEdit\Tree\Node;
class Entry
{
public function __construct(
public int $depth,
public string|int|null $key,
public ?Node $node,
public bool $closer = false,
)
{ }
}

179
src/List/TreeList.php Normal file
View File

@ -0,0 +1,179 @@
<?php
namespace NoccyLabs\JEdit\List;
use Countable;
use NoccyLabs\JEdit\Tree\ArrayNode;
use NoccyLabs\JEdit\Tree\Tree;
use NoccyLabs\JEdit\Tree\Node;
use NoccyLabs\JEdit\Tree\ObjectNode;
use NoccyLabs\JEdit\Tree\ValueNode;
class TreeList implements Countable
{
/** @var array<string,Entry> */
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);
}
}
}

135
src/Terminal/Terminal.php Normal file
View File

@ -0,0 +1,135 @@
<?php
namespace NoccyLabs\JEdit\Terminal;
class Terminal
{
private static int $init = 0;
private ?string $oldStty = null;
private int $lines = 0;
private int $columns = 0;
private bool $active = false;
public function __construct()
{
if (self::$init++ == 0) {
$this->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;
}
}

31
src/Tree/ArrayNode.php Normal file
View File

@ -0,0 +1,31 @@
<?php
namespace NoccyLabs\JEdit\Tree;
class ArrayNode extends Node implements CollapsibleNode
{
use CollapsibleNodeTrait;
public function __construct(public array $items)
{
}
public function append(Node $value)
{
$this->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);
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace NoccyLabs\JEdit\Tree;
interface CollapsibleNode
{
public function collapse(bool $collapsed): self;
public function isCollapsed(): bool;
}

View File

@ -0,0 +1,21 @@
<?php
namespace NoccyLabs\JEdit\Tree;
trait CollapsibleNodeTrait
{
private bool $collapsed = false;
public function collapse(bool $collapsed): self
{
$this->collapsed = $collapsed;
return $this;
}
public function isCollapsed(): bool
{
return $this->collapsed;
}
}

8
src/Tree/Node.php Normal file
View File

@ -0,0 +1,8 @@
<?php
namespace NoccyLabs\JEdit\Tree;
abstract class Node
{
}

33
src/Tree/ObjectNode.php Normal file
View File

@ -0,0 +1,33 @@
<?php
namespace NoccyLabs\JEdit\Tree;
class ObjectNode extends Node implements CollapsibleNode
{
use CollapsibleNodeTrait;
public function __construct(public array $properties)
{
}
public function set(string $key, Node $value)
{
$this->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);
}
}

35
src/Tree/Tree.php Normal file
View File

@ -0,0 +1,35 @@
<?php
namespace NoccyLabs\JEdit\Tree;
class Tree
{
public ?Node $root = null;
public function load(mixed $document): self
{
$this->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);
}
}
}

11
src/Tree/ValueNode.php Normal file
View File

@ -0,0 +1,11 @@
<?php
namespace NoccyLabs\JEdit\Tree;
class ValueNode extends Node
{
public function __construct(public mixed $value)
{
}
}

24
src/entry.php Normal file
View File

@ -0,0 +1,24 @@
<?php
require_once __DIR__."/../vendor/autoload.php";
$filename = $argv[1]??null; // ?(__DIR__."/../composer.json");
$terminal = new NoccyLabs\JEdit\Terminal\Terminal();
$editor = new NoccyLabs\JEdit\Editor\Editor($terminal);
if ($filename) {
$editor->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();