*/ private array $byId = []; /** @var array spl_object_id(block) => id */ private array $ids = []; /** @var array spl_object_id(block) => container holding it */ private array $parents = []; /** @var list */ 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->isPlaceholder()) { $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; } /** The parent node of $block; null at the top level (the root or a placeholder holds it). */ public function parentBlock(Block $block): ?Block { $parent = $this->parentOf($block); return $parent instanceof Block && ! $parent->isPlaceholder() ? $parent : null; } /** Admin label of the section around $block (or $block itself), for automatic labels ("" when none). */ public function sectionLabel(?Block $block): string { for ($node = $block; $node !== null; $node = $this->parentBlock($node)) { if ($node->name() === 'divi/section') { return NodeSummary::label($node); } } return ''; } /** @return list In document order. */ public function entries(): array { return $this->entries; } /** @param array $counters */ private function walk(Container $container, string $prefix, int $depth, array &$counters): void { foreach ($container->children() as $block) { if ($prefix === '' && $block->isPlaceholder()) { $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); } } } }