Files
WP-LIT-MCP/src/Integrations/Divi/BlockTree/Block.php
T

207 lines
6.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\BlockTree;
use InvalidArgumentException;
use LogicException;
use stdClass;
defined('ABSPATH') || exit;
/**
* 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
{
/** A valid block name: "namespace/block-name" (WordPress' own block name convention). */
private const NAME_PATTERN = '/^[a-z][a-z0-9_-]*\/[a-z][a-z0-9_-]*$/D';
/**
* @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
{
if (preg_match(self::NAME_PATTERN, $name) !== 1) {
throw new InvalidArgumentException(sprintf('"%s" is not a valid block name.', $name));
}
$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
{
if ($this->opaque) {
throw new LogicException('Cannot set attributes of an opaque block.');
}
$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/');
}
/** The divi/placeholder wrapper the builder puts around a page's sections (transparent for IDs). */
public function isPlaceholder(): bool
{
return $this->name === 'divi/placeholder';
}
/** 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;
}
/**
* A deep copy of this block under another name and attributes: opener and closer are
* regenerated, children and the text between them are kept, the void flag too. Opaque exactly
* when the Parser would make it so. GlobalInstance uses it to turn a library root into an
* instance and back.
*
* @param array<string, mixed> $attrs
*/
public function copyAs(string $name, array $attrs): self
{
if (preg_match(self::NAME_PATTERN, $name) !== 1) {
throw new InvalidArgumentException(sprintf('"%s" is not a valid block name.', $name));
}
$copy = clone $this;
$copy->name = $name;
$copy->attrs = self::deepCloneAttrs($attrs);
$copy->rawOpen = null;
$copy->rawClose = null;
$copy->opaque = $name === 'divi/global-layout' || array_key_exists('globalModule', $attrs);
return $copy;
}
public function __clone()
{
foreach ($this->parts as $position => $part) {
if ($part instanceof Block) {
$this->parts[$position] = clone $part;
}
}
$this->attrs = self::deepCloneAttrs($this->attrs);
}
/**
* PHP's own array-copy-on-write already deep-copies nested arrays, but stdClass values
* (Json.php keeps empty/list-like JSON objects as stdClass) are objects – clone() alone
* would leave the copy's attrs() sharing them with the original.
*
* @param array<string, mixed> $attrs
* @return array<string, mixed>
*/
private static function deepCloneAttrs(array $attrs): array
{
foreach ($attrs as $key => $value) {
if ($value instanceof stdClass) {
$attrs[$key] = (object) self::deepCloneAttrs((array) $value);
} elseif (is_array($value)) {
$attrs[$key] = self::deepCloneAttrs($value);
}
}
return $attrs;
}
protected function touched(): void
{
if ($this->void) {
$this->void = false;
$this->rawOpen = null;
}
}
}