feat: Divi BlockTree – byte-exact parser/serializer with round-trip fixtures
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,137 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\BlockTree;
|
||||
|
||||
/**
|
||||
* One block of the tree. A parsed block keeps its original opener/closer bytes; they are
|
||||
* dropped only when the block itself changes (setAttrs(), a void block getting children),
|
||||
* so the Serializer regenerates exactly the changed openers and nothing else.
|
||||
*
|
||||
* Opaque blocks (attributes that are no JSON object, divi/global-layout, anything carrying
|
||||
* globalModule) are never regenerated and never descended into by the tools – Divi's own
|
||||
* code warns that parse_blocks() mishandles global modules; Etappe 3 edits them.
|
||||
*/
|
||||
final class Block extends Container
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $attrs
|
||||
*/
|
||||
private function __construct(
|
||||
private string $name,
|
||||
private array $attrs,
|
||||
private ?string $rawOpen,
|
||||
private ?string $rawClose,
|
||||
private bool $void,
|
||||
private bool $opaque
|
||||
) {
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $attrs */
|
||||
public static function parsed(string $name, array $attrs, string $rawOpen, bool $void, bool $opaque): self
|
||||
{
|
||||
return new self($name, $attrs, $rawOpen, null, $void, $opaque);
|
||||
}
|
||||
|
||||
/**
|
||||
* A new block (opener and closer are generated; written as a pair like Divi does).
|
||||
*
|
||||
* @param array<string, mixed> $attrs
|
||||
* @param list<Block> $children
|
||||
*/
|
||||
public static function create(string $name, array $attrs = [], array $children = []): self
|
||||
{
|
||||
$block = new self($name, $attrs, null, null, false, false);
|
||||
foreach ($children as $child) {
|
||||
$block->parts[] = $child;
|
||||
}
|
||||
|
||||
return $block;
|
||||
}
|
||||
|
||||
/** Used by the Parser for the closing delimiter. */
|
||||
public function closeWith(string $rawClose): void
|
||||
{
|
||||
$this->rawClose = $rawClose;
|
||||
}
|
||||
|
||||
/** Full block name, e.g. "divi/section"; blocks without namespace are "core/…". */
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/** Name without the "divi/" namespace, e.g. "button"; other namespaces stay. */
|
||||
public function shortName(): string
|
||||
{
|
||||
return str_starts_with($this->name, 'divi/') ? substr($this->name, 5) : $this->name;
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function attrs(): array
|
||||
{
|
||||
return $this->attrs;
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $attrs */
|
||||
public function setAttrs(array $attrs): void
|
||||
{
|
||||
$this->attrs = $attrs;
|
||||
$this->rawOpen = null;
|
||||
}
|
||||
|
||||
public function rawOpen(): ?string
|
||||
{
|
||||
return $this->rawOpen;
|
||||
}
|
||||
|
||||
public function rawClose(): ?string
|
||||
{
|
||||
return $this->rawClose;
|
||||
}
|
||||
|
||||
public function isVoid(): bool
|
||||
{
|
||||
return $this->void;
|
||||
}
|
||||
|
||||
public function isOpaque(): bool
|
||||
{
|
||||
return $this->opaque;
|
||||
}
|
||||
|
||||
public function isDivi(): bool
|
||||
{
|
||||
return str_starts_with($this->name, 'divi/');
|
||||
}
|
||||
|
||||
/** A global module reference or global layout (edited only in Etappe 3). */
|
||||
public function isGlobal(): bool
|
||||
{
|
||||
return $this->name === 'divi/global-layout' || array_key_exists('globalModule', $this->attrs);
|
||||
}
|
||||
|
||||
/** Deep copy – unchanged copies serialize to exactly the same bytes. */
|
||||
public function copy(): self
|
||||
{
|
||||
return clone $this;
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
foreach ($this->parts as $position => $part) {
|
||||
if ($part instanceof Block) {
|
||||
$this->parts[$position] = clone $part;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function touched(): void
|
||||
{
|
||||
if ($this->void) {
|
||||
$this->void = false;
|
||||
$this->rawOpen = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user