feat: Divi path IDs, attribute paths and outline
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\BlockTree;
|
||||
|
||||
/**
|
||||
* Path IDs of a Document (spec §5.3): s0, s0.r1, s0.r1.c0.m2. One letter per role –
|
||||
* s section, r row / row-inner, c column / column-inner, m module (incl. group and child
|
||||
* items), g global module or global layout, x non-Divi block – numbered per parent and letter.
|
||||
* divi/placeholder wrappers are transparent (their children count as top level). Opaque and
|
||||
* non-Divi blocks get an ID but are not descended into. IDs are only valid for the tree_rev
|
||||
* they were read with; build a new index after every change.
|
||||
*/
|
||||
final class NodeIndex
|
||||
{
|
||||
public const ID_PATTERN = '/^[srcmgx]\d+(\.[srcmgx]\d+)*$/';
|
||||
|
||||
/** @var array<string, Block> */
|
||||
private array $byId = [];
|
||||
|
||||
/** @var array<int, string> spl_object_id(block) => id */
|
||||
private array $ids = [];
|
||||
|
||||
/** @var array<int, Container> spl_object_id(block) => container holding it */
|
||||
private array $parents = [];
|
||||
|
||||
/** @var list<array{id: string, block: Block, depth: int}> */
|
||||
private array $entries = [];
|
||||
|
||||
private function __construct(private Container $root)
|
||||
{
|
||||
}
|
||||
|
||||
public static function build(Document $document): self
|
||||
{
|
||||
$root = $document;
|
||||
foreach ($document->children() as $block) {
|
||||
if ($block->name() === 'divi/placeholder') {
|
||||
$root = $block;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$index = new self($root);
|
||||
$counters = [];
|
||||
$index->walk($document, '', 0, $counters);
|
||||
|
||||
return $index;
|
||||
}
|
||||
|
||||
public static function letter(Block $block): string
|
||||
{
|
||||
if ($block->isGlobal()) {
|
||||
return 'g';
|
||||
}
|
||||
|
||||
return match ($block->name()) {
|
||||
'divi/section' => 's',
|
||||
'divi/row', 'divi/row-inner' => 'r',
|
||||
'divi/column', 'divi/column-inner' => 'c',
|
||||
default => $block->isDivi() ? 'm' : 'x',
|
||||
};
|
||||
}
|
||||
|
||||
/** Container that receives new top-level sections: the first divi/placeholder, else the document. */
|
||||
public function root(): Container
|
||||
{
|
||||
return $this->root;
|
||||
}
|
||||
|
||||
public function find(string $id): ?Block
|
||||
{
|
||||
return $this->byId[$id] ?? null;
|
||||
}
|
||||
|
||||
public function idOf(Block $block): ?string
|
||||
{
|
||||
return $this->ids[spl_object_id($block)] ?? null;
|
||||
}
|
||||
|
||||
public function parentOf(Block $block): ?Container
|
||||
{
|
||||
return $this->parents[spl_object_id($block)] ?? null;
|
||||
}
|
||||
|
||||
/** ID of the parent node, null for top-level nodes (their container is the root or a placeholder). */
|
||||
public function parentId(Block $block): ?string
|
||||
{
|
||||
$parent = $this->parentOf($block);
|
||||
|
||||
return $parent instanceof Block ? $this->idOf($parent) : null;
|
||||
}
|
||||
|
||||
/** @return list<array{id: string, block: Block, depth: int}> In document order. */
|
||||
public function entries(): array
|
||||
{
|
||||
return $this->entries;
|
||||
}
|
||||
|
||||
/** @param array<string, int> $counters */
|
||||
private function walk(Container $container, string $prefix, int $depth, array &$counters): void
|
||||
{
|
||||
foreach ($container->children() as $block) {
|
||||
if ($prefix === '' && $block->name() === 'divi/placeholder') {
|
||||
$this->walk($block, '', $depth, $counters);
|
||||
continue;
|
||||
}
|
||||
|
||||
$letter = self::letter($block);
|
||||
$number = $counters[$letter] ?? 0;
|
||||
$counters[$letter] = $number + 1;
|
||||
$id = ($prefix === '' ? '' : $prefix . '.') . $letter . $number;
|
||||
|
||||
$this->byId[$id] = $block;
|
||||
$this->ids[spl_object_id($block)] = $id;
|
||||
$this->parents[spl_object_id($block)] = $container;
|
||||
$this->entries[] = ['id' => $id, 'block' => $block, 'depth' => $depth];
|
||||
|
||||
if ($block->isDivi() && ! $block->isOpaque()) {
|
||||
$childCounters = [];
|
||||
$this->walk($block, $id, $depth + 1, $childCounters);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user