Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
100 lines
3.8 KiB
PHP
100 lines
3.8 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Integrations\Divi\Pages;
|
||
|
||
use Closure;
|
||
use LIT\MCP\Abilities\Errors;
|
||
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||
use LIT\MCP\Integrations\Divi\BlockTree\Outline;
|
||
use LIT\MCP\Integrations\Divi\BlockTree\Serializer;
|
||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||
use LIT\MCP\Integrations\WordPress\Content\ContentComposer;
|
||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||
use WP_Error;
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
/**
|
||
* Divi short form for lit/content-create (spec §6.2): the sections become a Divi 5 layout in
|
||
* exactly one divi/placeholder – inserted by TreeEditor like the first section of an empty page
|
||
* (ruling S9) – stored through DiviSaver's kses window, with the builder flags (DiviFlags, S11).
|
||
*
|
||
* Allow map (ruling S22) for an entry that does not exist yet: check() runs before the insert
|
||
* with the composition bound to post ID 0 – Divi's own registry for "no entry" (get_options(0)
|
||
* treats it like a post; live the same options as for a page, only the taxonomy choices of
|
||
* post_categories/post_tags differ). write() composes again, bound to the new entry – the
|
||
* layout that is stored always passed that entry's live map, like every other Divi write
|
||
* (DiviWriter::composition). Should the entry's map refuse something (an option Divi offers
|
||
* only for other types), write() returns the errors and lit/content-create deletes the draft.
|
||
*
|
||
* Every client string goes through Composer (AttrWriter / ShortformWriter → ValueCleaner) – the
|
||
* single choke point that makes the kses bypass safe (spec D7).
|
||
*/
|
||
final class DiviContentComposer implements ContentComposer
|
||
{
|
||
/** @var Closure(): bool */
|
||
private Closure $writable;
|
||
|
||
/** @param (Closure(): bool)|null $writable Tools-tab gate (Plugin: lit/divi-insert enabled); null = always. */
|
||
public function __construct(
|
||
private ContentSchema $schema,
|
||
private DiviComposition $composition,
|
||
private DiviSaver $saver,
|
||
private DesignSystemReader $design,
|
||
?Closure $writable = null
|
||
) {
|
||
$this->writable = $writable ?? static fn (): bool => true;
|
||
}
|
||
|
||
public function available(): bool
|
||
{
|
||
return DiviModules::isAvailable();
|
||
}
|
||
|
||
public function writable(): bool
|
||
{
|
||
return ($this->writable)();
|
||
}
|
||
|
||
public function check(string $postType, array $sections): ?WP_Error
|
||
{
|
||
if (! $this->schema->diviEnabled($postType)) {
|
||
return Errors::invalidInput(
|
||
'Für diesen Inhaltstyp ist der Divi-Builder nicht aktiviert; nichts wurde angelegt.',
|
||
['divi' => sprintf('Divi-Builder für "%s" nicht aktiv', $postType)]
|
||
);
|
||
}
|
||
|
||
$layout = LayoutBuilder::build($this->composition->forPost(0), $sections, 'divi');
|
||
|
||
return $layout instanceof WP_Error ? $layout : null;
|
||
}
|
||
|
||
public function write(int $postId, string $postType, array $sections): array|WP_Error
|
||
{
|
||
$composition = $this->composition->forPost($postId);
|
||
$layout = LayoutBuilder::build($composition, $sections, 'divi');
|
||
if ($layout instanceof WP_Error) {
|
||
return $layout;
|
||
}
|
||
|
||
$index = NodeIndex::build($layout['document']);
|
||
$warnings = $composition->warnings()->collect($index, $layout['blocks']);
|
||
$outline = (new Outline($this->design->read()->presetNames()))->summary($index);
|
||
if ($outline['truncated']) {
|
||
$warnings[] = DiviWriter::OUTLINE_TRUNCATED;
|
||
}
|
||
|
||
$saved = $this->saver->saveNew($postId, Serializer::document($layout['document']));
|
||
|
||
return [
|
||
'warnings' => array_merge($warnings, $saved),
|
||
'outline' => (string) $outline['text'],
|
||
'edit_url' => DiviPage::builderUrl($postId),
|
||
];
|
||
}
|
||
}
|