2024-10-01 18:46:03 +02:00
|
|
|
<?php
|
|
|
|
|
2024-10-02 01:15:22 +02:00
|
|
|
namespace NoccyLabs\JsonEdit\List;
|
2024-10-01 18:46:03 +02:00
|
|
|
|
|
|
|
use Countable;
|
2024-10-02 01:15:22 +02:00
|
|
|
use NoccyLabs\JsonEdit\Settings;
|
|
|
|
use NoccyLabs\JsonEdit\Tree\ArrayNode;
|
2024-10-03 15:29:56 +02:00
|
|
|
use NoccyLabs\JsonEdit\Tree\CollapsibleNode;
|
2024-10-02 01:15:22 +02:00
|
|
|
use NoccyLabs\JsonEdit\Tree\Tree;
|
|
|
|
use NoccyLabs\JsonEdit\Tree\Node;
|
|
|
|
use NoccyLabs\JsonEdit\Tree\ObjectNode;
|
|
|
|
use NoccyLabs\JsonEdit\Tree\ValueNode;
|
2024-10-01 18:46:03 +02:00
|
|
|
|
|
|
|
class TreeList implements Countable
|
|
|
|
{
|
|
|
|
/** @var array<string,Entry> */
|
|
|
|
public array $list = [];
|
|
|
|
|
2024-10-03 15:29:56 +02:00
|
|
|
private const TREE_FOLD_OPEN = "\u{f0fe}";
|
|
|
|
private const TREE_FOLD_CLOSE = "\u{f146}";
|
|
|
|
private const TREE_INDENT_GUIDE = "\u{258f} ";
|
|
|
|
private const TREE_INDENT = " ";
|
|
|
|
|
2024-10-03 16:36:35 +02:00
|
|
|
private const ICON_NUMERIC = "\u{f89f}";
|
|
|
|
private const ICON_STRING = "\u{f475}";
|
|
|
|
private const ICON_BOOL = "\u{f657}";
|
|
|
|
private const ICON_NULL = "\u{2400}";
|
|
|
|
|
2024-10-01 18:46:03 +02:00
|
|
|
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
|
|
|
|
{
|
2024-10-01 22:31:51 +02:00
|
|
|
if (str_contains($key, '/')) {
|
|
|
|
$ekey = urlencode($key);
|
|
|
|
} else {
|
|
|
|
$ekey = $key;
|
|
|
|
}
|
|
|
|
|
2024-10-01 18:46:03 +02:00
|
|
|
$level = count($path);
|
2024-10-01 22:31:51 +02:00
|
|
|
$entryKey = join("/", $path) . (is_null($key) ? "/" : (is_int($key) ? sprintf("/%d", $key) : sprintf("/%s", $ekey)));
|
2024-10-01 18:46:03 +02:00
|
|
|
|
|
|
|
$entry = new Entry(depth: $level, key: $key, node: $node);
|
|
|
|
|
|
|
|
$this->list[$entryKey] = $entry;
|
|
|
|
|
|
|
|
if ($node instanceof ArrayNode) {
|
|
|
|
$index = 0;
|
2024-10-03 01:44:10 +02:00
|
|
|
if ($node->isCollapsed()) return;
|
2024-10-01 18:46:03 +02:00
|
|
|
foreach ($node->items as $item) {
|
|
|
|
$this->parseNode($item, [ ...$path, $key ], $index++);
|
|
|
|
}
|
2024-10-02 02:14:36 +02:00
|
|
|
if (!Settings::$compactGroups)
|
|
|
|
$this->list[$entryKey.'$'] = new Entry(depth: $level, key: $key, node: $node, closer: true);
|
2024-10-01 18:46:03 +02:00
|
|
|
} elseif ($node instanceof ObjectNode) {
|
2024-10-03 01:44:10 +02:00
|
|
|
if ($node->isCollapsed()) return;
|
2024-10-01 18:46:03 +02:00
|
|
|
foreach ($node->properties as $nodekey=>$item) {
|
|
|
|
$this->parseNode($item, [ ...$path, $key ], $nodekey);
|
|
|
|
}
|
2024-10-02 02:14:36 +02:00
|
|
|
if (!Settings::$compactGroups)
|
|
|
|
$this->list[$entryKey.'$'] = new Entry(depth: $level, key: $key, node: $node, closer: true);
|
2024-10-01 18:46:03 +02:00
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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)) {
|
2024-10-01 23:01:26 +02:00
|
|
|
echo ($selected?"\e[44;97m":"\e[0;90m")."\e[K";
|
2024-10-01 18:46:03 +02:00
|
|
|
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";
|
2024-10-01 23:01:26 +02:00
|
|
|
echo ($selected?"\e[44;97m":"\e[0;37m")."\e[K";
|
2024-10-03 15:29:56 +02:00
|
|
|
echo "\e[90m".str_repeat(
|
|
|
|
Settings::$indentationGuides ? self::TREE_INDENT_GUIDE : self::TREE_INDENT,
|
|
|
|
$entry->depth
|
|
|
|
)."\e[37m";
|
2024-10-01 18:46:03 +02:00
|
|
|
|
|
|
|
if ($entry->closer) {
|
2024-10-02 02:14:36 +02:00
|
|
|
if (!Settings::$compactGroups) {
|
|
|
|
if ($entry->node instanceof ArrayNode) {
|
|
|
|
echo "]";
|
|
|
|
} elseif ($entry->node instanceof ObjectNode) {
|
|
|
|
echo "}";
|
|
|
|
}
|
|
|
|
}
|
2024-10-01 18:46:03 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-10-03 15:29:56 +02:00
|
|
|
if (Settings::$collapseBefore) {
|
|
|
|
if ($entry->node instanceof CollapsibleNode) {
|
|
|
|
if ($entry->node->isCollapsed()) {
|
|
|
|
//echo "\e[90m\u{25ba} \e[37m";
|
|
|
|
echo "\e[90m".self::TREE_FOLD_OPEN." \e[37m";
|
|
|
|
} else {
|
|
|
|
//echo "\e[90m\u{25bc} \e[37m";
|
|
|
|
echo "\e[90m".self::TREE_FOLD_CLOSE." \e[37m";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-01 18:46:03 +02:00
|
|
|
if (!is_null($entry->key)) {
|
2024-10-03 16:36:35 +02:00
|
|
|
if (Settings::$collapseBefore && $entry->node instanceof ValueNode) {
|
|
|
|
echo match (gettype($entry->node->value)) {
|
|
|
|
'string' => self::ICON_STRING,
|
|
|
|
'integer' => self::ICON_NUMERIC,
|
|
|
|
'double' => self::ICON_NUMERIC,
|
|
|
|
'boolean' => self::ICON_BOOL,
|
|
|
|
'NULL' => self::ICON_NULL,
|
|
|
|
default => self::ICON_STRING,
|
|
|
|
}." ";
|
|
|
|
}
|
2024-10-01 18:46:03 +02:00
|
|
|
echo (is_int($entry->key)
|
2024-10-02 00:53:11 +02:00
|
|
|
?"\e[36;2m\u{e0b6}\e[7m#{$entry->key}\e[27m\u{e0b4}\e[22;37m "
|
|
|
|
:(Settings::$editorQuotedKeys
|
|
|
|
? "\e[36m\"{$entry->key}\":\e[37m "
|
|
|
|
: "\e[36m{$entry->key}:\e[37m "
|
|
|
|
));
|
2024-10-01 18:46:03 +02:00
|
|
|
}
|
|
|
|
if ($entry->node instanceof ArrayNode) {
|
2024-10-02 02:14:36 +02:00
|
|
|
echo "[" . (Settings::$compactGroups ? "…]":"");
|
2024-10-03 01:44:10 +02:00
|
|
|
if ($entry->node->isCollapsed()) {
|
2024-10-03 15:29:56 +02:00
|
|
|
if (!Settings::$collapseBefore) echo " \e[90m\u{25ba}";
|
|
|
|
echo " \e[90;2m[".count($entry->node->items)."]\e[22m";
|
|
|
|
if (!Settings::$compactGroups) echo "\e[37m ]";
|
2024-10-03 01:44:10 +02:00
|
|
|
} else {
|
2024-10-03 15:29:56 +02:00
|
|
|
if (!Settings::$collapseBefore) echo " \e[90m\u{25bc}";
|
2024-10-03 01:44:10 +02:00
|
|
|
}
|
2024-10-01 18:46:03 +02:00
|
|
|
} elseif ($entry->node instanceof ObjectNode) {
|
2024-10-02 02:14:36 +02:00
|
|
|
echo "{" . (Settings::$compactGroups ? "…}":"");
|
2024-10-03 01:44:10 +02:00
|
|
|
if ($entry->node->isCollapsed()) {
|
2024-10-03 15:29:56 +02:00
|
|
|
if (!Settings::$collapseBefore) echo " \e[90m\u{25ba}";
|
|
|
|
echo " \e[90;2;3m".join(", ",array_keys($entry->node->properties))."\e[22;23m";
|
|
|
|
if (!Settings::$compactGroups) echo "\e[37m }";
|
2024-10-03 01:44:10 +02:00
|
|
|
} else {
|
2024-10-03 15:29:56 +02:00
|
|
|
if (!Settings::$collapseBefore) echo " \e[90m\u{25bc}";
|
2024-10-03 01:44:10 +02:00
|
|
|
}
|
2024-10-01 18:46:03 +02:00
|
|
|
} 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);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|