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

164 lines
6.0 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;
defined('ABSPATH') || exit;
/**
* The page tree as rows and as compact text (lit-divi-outline, spec §5.3/§5.7; also the
* "outline" of write results). Preset IDs are shown with their names when known.
*
* Ruling S18: a page can have far more nodes at one level than are useful to show (e.g. a
* 200-item slider). rows() therefore caps the number of *direct* children shown per parent
* (breadth, not depth) at $limit; the parent whose children got cut is marked
* 'truncated' => true. wasTruncated() additionally reports whether ANY cap fired, including at
* the top level (the page root has no row of its own to mark).
*/
final class Outline
{
/** Sane default breadth cap per parent when no explicit limit is given. */
public const DEFAULT_LIMIT = 50;
private bool $truncated = false;
/** @param array<string, string> $presetNames Preset ID => name. */
public function __construct(private array $presetNames = [])
{
}
/** Whether the last rows() call cut off any children (at any level, including the top). */
public function wasTruncated(): bool
{
return $this->truncated;
}
/**
* The outline text of a write result: the whole page ($sectionIds null) or the given
* sections one after another (none: text null), and whether any breadth cap fired (S18).
*
* @param list<string>|null $sectionIds
* @return array{text: ?string, truncated: bool}
*/
public function summary(NodeIndex $index, ?array $sectionIds = null): array
{
if ($sectionIds === []) {
return ['text' => null, 'truncated' => false];
}
$texts = [];
$truncated = false;
foreach ($sectionIds ?? [null] as $sectionId) {
$texts[] = $this->text($this->rows($index, $sectionId));
$truncated = $truncated || $this->wasTruncated();
}
return ['text' => implode("\n", $texts), 'truncated' => $truncated];
}
/**
* @return list<array{id: string, type: string, label: string, excerpt: string, info: string,
* presets: list<string>, children: int, editable: bool, depth: int, truncated: bool,
* global_id: ?string}>
*/
public function rows(NodeIndex $index, ?string $rootId = null, ?int $maxDepth = null, ?int $limit = null): array
{
$limit = $limit ?? self::DEFAULT_LIMIT;
$this->truncated = false;
$rows = [];
$rowIndexById = [];
$counts = [];
$skipped = [];
$baseDepth = null;
foreach ($index->entries() as $entry) {
$id = $entry['id'];
if ($rootId !== null && $id !== $rootId && ! str_starts_with($id, $rootId . '.')) {
continue;
}
$parentId = $index->parentId($entry['block']);
if ($parentId !== null && isset($skipped[$parentId])) {
$skipped[$id] = true;
continue;
}
$baseDepth ??= $entry['depth'];
$depth = $entry['depth'] - $baseDepth;
if ($maxDepth !== null && $depth > $maxDepth) {
continue;
}
$parentKey = $parentId ?? '';
$counts[$parentKey] = ($counts[$parentKey] ?? 0) + 1;
if ($counts[$parentKey] > $limit) {
$this->truncated = true;
$skipped[$id] = true;
if ($parentId !== null && isset($rowIndexById[$parentId])) {
$rows[$rowIndexById[$parentId]]['truncated'] = true;
}
continue;
}
$rowIndexById[$id] = count($rows);
$rows[] = $this->row($entry['block'], $id, $depth);
}
return $rows;
}
/** @param list<array<string, mixed>> $rows Output of rows(). */
public function text(array $rows): string
{
$lines = [];
foreach ($rows as $row) {
$line = str_repeat(' ', (int) $row['depth']) . $row['id'] . ' ' . $row['type'];
$line .= $row['label'] !== '' ? ' "' . $row['label'] . '"' : '';
$line .= ($row['global_id'] ?? null) !== null ? ' [global ' . $row['global_id'] . ']' : '';
$line .= $row['info'] !== '' ? ' [' . $row['info'] . ']' : '';
$line .= $row['presets'] !== [] ? ' {Preset: ' . implode(', ', $row['presets']) . '}' : '';
$line .= $row['excerpt'] !== '' ? ' – ' . $row['excerpt'] : '';
$line .= $row['editable'] ? '' : ' (nur lesbar)';
$line .= ($row['truncated'] ?? false) ? ' (weitere Kinder abgeschnitten)' : '';
$lines[] = $line;
}
return implode("\n", $lines);
}
/**
* @return array{id: string, type: string, label: string, excerpt: string, info: string,
* presets: list<string>, children: int, editable: bool, depth: int, truncated: bool,
* global_id: ?string}
*/
private function row(Block $block, string $id, int $depth): array
{
$editable = $block->isDivi() && ! $block->isOpaque();
$global = NodeSummary::globalRef($block);
$type = $block->isDivi() ? $block->shortName() : $block->name();
if ($global !== null && $global['block_name'] !== '') {
$type = str_starts_with($global['block_name'], 'divi/')
? substr($global['block_name'], 5)
: $global['block_name'];
}
return [
'id' => $id,
'type' => $type,
'label' => NodeSummary::label($block),
'excerpt' => NodeSummary::excerpt($block),
'info' => $editable ? NodeSummary::info($block) : '',
'presets' => array_map(
fn (string $presetId): string => $this->presetNames[$presetId] ?? $presetId,
NodeSummary::presetIds($block)
),
'children' => $editable ? count($block->children()) : 0,
'editable' => $editable,
'depth' => $depth,
'truncated' => false,
'global_id' => $global['id'] ?? null,
];
}
}