feat: lit/content-create with Divi short form
New input divi (1-30 sections, short form like lit-divi-insert) builds a Divi 5 layout in one divi/placeholder. The whole description is checked before the insert (Divi's map for "no entry yet"); the draft is inserted with its normal WordPress fields only, then DiviContentComposer composes again bound to the new entry and stores it via DiviSaver::saveNew (same kses window as save(), strict DiviFlags). A refusal or failure deletes the draft. Result adds tree_rev and the outline. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Pages;
|
||||
|
||||
use LIT\MCP\Abilities\Errors;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Document;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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) – the single choke
|
||||
* point that makes the kses bypass safe (spec D7).
|
||||
*/
|
||||
final class DiviContentComposer implements ContentComposer
|
||||
{
|
||||
public function __construct(
|
||||
private ContentSchema $schema,
|
||||
private DiviComposition $composition,
|
||||
private DiviSaver $saver,
|
||||
private DesignSystemReader $design
|
||||
) {
|
||||
}
|
||||
|
||||
public function available(): bool
|
||||
{
|
||||
return DiviModules::isAvailable();
|
||||
}
|
||||
|
||||
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 = $this->layout($this->composition->forPost(0), $sections);
|
||||
|
||||
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 = $this->layout($composition, $sections);
|
||||
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());
|
||||
$text = $outline->text($outline->rows($index));
|
||||
if ($outline->wasTruncated()) {
|
||||
$warnings[] = DiviWriter::OUTLINE_TRUNCATED;
|
||||
}
|
||||
|
||||
$saved = $this->saver->saveNew($postId, Serializer::document($layout['document']));
|
||||
|
||||
return ['warnings' => array_merge($warnings, $saved), 'outline' => $text];
|
||||
}
|
||||
|
||||
/**
|
||||
* The whole description composed and inserted into an empty document – or every error.
|
||||
*
|
||||
* @param list<mixed> $sections
|
||||
* @return array{document: Document, blocks: list<Block>}|WP_Error
|
||||
*/
|
||||
private function layout(DiviComposition $composition, array $sections): array|WP_Error
|
||||
{
|
||||
if ($sections === []) {
|
||||
return Errors::invalidInput('Kein Divi-Inhalt angegeben; nichts wurde angelegt.', [
|
||||
'divi' => 'mindestens ein Abschnitt',
|
||||
]);
|
||||
}
|
||||
|
||||
$composed = $composition->composer()->compose(array_values($sections), null, 'divi');
|
||||
if ($composed['errors'] !== []) {
|
||||
return $composed['invalid_paths'] !== []
|
||||
? Errors::invalidAttrPath($composed['errors'])
|
||||
: Errors::invalidInput('Divi-Inhalt ist ungültig; nichts wurde angelegt.', $composed['errors']);
|
||||
}
|
||||
|
||||
$document = new Document();
|
||||
$violation = $composition->editor()->insert(
|
||||
['container' => $document, 'parent' => null, 'index' => PHP_INT_MAX],
|
||||
$composed['blocks']
|
||||
);
|
||||
if ($violation !== null) {
|
||||
return Errors::invalidInput('Divi-Inhalt passt nicht auf die Seite; nichts wurde angelegt.', [
|
||||
'divi' => $violation,
|
||||
]);
|
||||
}
|
||||
|
||||
return ['document' => $document, 'blocks' => $composed['blocks']];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user