feat: Presets als Stapel, Gruppen-Presets und clear_overrides im Composer
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
fafa140546
commit
be6ca5bee0
@@ -7,7 +7,7 @@ namespace LIT\MCP\Integrations\Divi\Compose;
|
||||
use InvalidArgumentException;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\AttrMap;
|
||||
use LIT\MCP\Integrations\Divi\BlockTree\Block;
|
||||
use LIT\MCP\Integrations\Divi\Design\DesignSystem;
|
||||
use LIT\MCP\Integrations\Divi\Design\PresetAssignment;
|
||||
use LIT\MCP\Integrations\Divi\Schema\ModuleCatalog;
|
||||
use LIT\MCP\Integrations\Divi\Schema\ShortformMap;
|
||||
|
||||
@@ -16,7 +16,8 @@ defined('ABSPATH') || exit;
|
||||
/**
|
||||
* Builds new Divi blocks from node descriptions (spec §5.4): short form, full form ("attrs")
|
||||
* or both; sections with rows ("columns": "1_3,2_3") and modules; parent modules with
|
||||
* "items"; presets by name; automatic admin labels "<Abschnitt> – <Modul>" (spec §5.5).
|
||||
* "items"; preset stacks and group presets by name, clear_overrides; automatic admin labels
|
||||
* "<Abschnitt> – <Modul>" (spec §5.5).
|
||||
* Every error is collected with its input path; blocks are only usable when there are none.
|
||||
* Pure PHP.
|
||||
*
|
||||
@@ -28,16 +29,19 @@ defined('ABSPATH') || exit;
|
||||
* names (catalog + StructureRules + Block::create), "fullwidth" and column flexTypes built
|
||||
* from digits (ColumnWidths). Design-system names that match several entries are errors
|
||||
* listing the ids (ruling S13a: colors/variables in Placeholders, presets in
|
||||
* DesignSystem::modulePresetId()).
|
||||
* PresetAssignment).
|
||||
*/
|
||||
final class Composer
|
||||
{
|
||||
private const COMMON = ['type', 'label', 'preset', 'attrs'];
|
||||
private const COMMON = ['type', 'label', 'preset', 'group_presets', 'clear_overrides', 'attrs'];
|
||||
private const SECTION = ['section_type', 'background_color', 'rows', 'modules'];
|
||||
private const ROW = ['columns', 'modules'];
|
||||
|
||||
/** Keys of one lit-divi-update change ("id" names the node, read by the ability). */
|
||||
private const UPDATE = ['id', 'set', 'attrs', 'preset', 'label'];
|
||||
private const UPDATE = ['id', 'set', 'attrs', 'preset', 'group_presets', 'clear_overrides', 'label'];
|
||||
|
||||
/** The keys of a node that PresetAssignment::apply() reads (rulings R2, R3). */
|
||||
private const PRESET_KEYS = ['preset', 'group_presets'];
|
||||
|
||||
/** ET_BUILDER_PRODUCT_VERSION, e.g. "5.2.1" – written into every new block. */
|
||||
private const VERSION = '/^[0-9][0-9A-Za-z.+-]*$/D';
|
||||
@@ -64,7 +68,7 @@ final class Composer
|
||||
private ShortformWriter $shortform,
|
||||
private AttrWriter $attrWriter,
|
||||
private ValueCleaner $cleaner,
|
||||
private DesignSystem $design,
|
||||
private PresetAssignment $presets,
|
||||
private string $builderVersion
|
||||
) {
|
||||
if (preg_match(self::VERSION, $builderVersion) !== 1) {
|
||||
@@ -97,9 +101,11 @@ final class Composer
|
||||
/**
|
||||
* Applies short-form fields ("set"), full-form attrs, preset and label to an existing block
|
||||
* (lit-divi-update); errors like compose(). The block itself is not changed – the caller
|
||||
* sets the returned attrs. "preset": null and "label": null remove them.
|
||||
* sets the returned attrs. "preset": null and "label": null remove them; "preset" may be a stack (list),
|
||||
* "group_presets" assigns group presets (null removes one), "clear_overrides": true removes the local values
|
||||
* the assigned presets also set – after set and attrs (ruling R7).
|
||||
*
|
||||
* @param array<array-key, mixed> $change id, set, attrs, preset, label
|
||||
* @param array<array-key, mixed> $change id, set, attrs, preset, group_presets, clear_overrides, label
|
||||
* @return array{attrs: array<string, mixed>, errors: array<string, string>, invalid_paths: list<string>}
|
||||
*/
|
||||
public function update(Block $block, array $change, string $field): array
|
||||
@@ -449,7 +455,8 @@ final class Composer
|
||||
|
||||
$attrs = $this->common($name, $attrs, $spec, $path, $this->autoLabel($name, $context));
|
||||
if ($defaultLevel) {
|
||||
$attrs = $this->defaultLevel($short, $attrs);
|
||||
// The automatic level is no client value: a preset's headingLevel wins when overrides are cleared.
|
||||
$attrs = $this->clearedOverrides($this->defaultLevel($short, $attrs), $spec, $path);
|
||||
}
|
||||
|
||||
$block = Block::create($name, $attrs);
|
||||
@@ -496,24 +503,30 @@ final class Composer
|
||||
*/
|
||||
private function presetAndAttrs(string $name, array $attrs, array $spec, string $path): array
|
||||
{
|
||||
if (array_key_exists('preset', $spec)) {
|
||||
$attrs = $this->preset($name, $attrs, $spec['preset'], $path . '.preset');
|
||||
$attrs = $this->presets($name, $attrs, $spec, $path);
|
||||
if (array_key_exists('attrs', $spec)) {
|
||||
$attrs = $this->fullForm($name, $attrs, $spec['attrs'], $path . '.attrs');
|
||||
}
|
||||
|
||||
if (! array_key_exists('attrs', $spec)) {
|
||||
return $attrs;
|
||||
}
|
||||
return $this->clearedOverrides($attrs, $spec, $path);
|
||||
}
|
||||
|
||||
if (! is_array($spec['attrs']) || array_is_list($spec['attrs'])) {
|
||||
$this->errors[$path . '.attrs'] = 'Objekt "Attributpfad": {Breakpoint: Wert} erwartet';
|
||||
/**
|
||||
* @param array<string, mixed> $attrs
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function fullForm(string $name, array $attrs, mixed $full, string $path): array
|
||||
{
|
||||
if (! is_array($full) || array_is_list($full)) {
|
||||
$this->errors[$path] = 'Objekt "Attributpfad": {Breakpoint: Wert} erwartet';
|
||||
|
||||
return $attrs;
|
||||
}
|
||||
|
||||
$result = $this->attrWriter->apply($name, $attrs, $spec['attrs'], $path . '.attrs');
|
||||
$result = $this->attrWriter->apply($name, $attrs, $full, $path);
|
||||
// An unknown path is the more useful message on its key (it decides invalid_attr_path).
|
||||
foreach ($result['invalid_paths'] as $invalid) {
|
||||
$key = $path . '.attrs.' . $invalid;
|
||||
$key = $path . '.' . $invalid;
|
||||
if (isset($result['errors'][$key])) {
|
||||
$this->errors[$key] = $result['errors'][$key];
|
||||
}
|
||||
@@ -557,53 +570,56 @@ final class Composer
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attrs
|
||||
* "preset" (name or id, a stack of them, null/"" = remove) and "group_presets" – through PresetAssignment, the
|
||||
* one place presets reach a block (D7, rulings R2, R3). Its error keys already carry $path. PresetAssignment
|
||||
* keeps the valid group keys when another one fails, so with errors the attrs stay as they were.
|
||||
*
|
||||
* @param array<string, mixed> $attrs
|
||||
* @param array<array-key, mixed> $spec
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function preset(string $name, array $attrs, mixed $preset, string $path): array
|
||||
private function presets(string $name, array $attrs, array $spec, string $path): array
|
||||
{
|
||||
if ($preset === null || $preset === '') {
|
||||
unset($attrs['modulePreset']);
|
||||
$given = array_intersect_key($spec, array_flip(self::PRESET_KEYS));
|
||||
if ($given === []) {
|
||||
return $attrs;
|
||||
}
|
||||
|
||||
if (($given['preset'] ?? null) === '') {
|
||||
$given['preset'] = null;
|
||||
}
|
||||
|
||||
$result = $this->presets->apply($name, $attrs, $given, $path);
|
||||
if ($result['errors'] !== []) {
|
||||
$this->errors += $result['errors'];
|
||||
|
||||
return $attrs;
|
||||
}
|
||||
|
||||
try {
|
||||
$id = is_string($preset) ? $this->design->modulePresetId($name, $preset) : null;
|
||||
} catch (InvalidArgumentException $e) {
|
||||
// Ruling S13a: a name several presets share (DesignSystem lists their ids).
|
||||
$this->errors[$path] = $e->getMessage();
|
||||
return $result['attrs'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruling R7: "clear_overrides": true removes the local values that sit on a leaf an explicitly assigned preset
|
||||
* also sets; it runs after set and attrs, so it also takes what this very call wrote. Not a boolean is an error.
|
||||
*
|
||||
* @param array<string, mixed> $attrs
|
||||
* @param array<array-key, mixed> $spec
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function clearedOverrides(array $attrs, array $spec, string $path): array
|
||||
{
|
||||
if (! array_key_exists('clear_overrides', $spec)) {
|
||||
return $attrs;
|
||||
}
|
||||
|
||||
if (! is_bool($spec['clear_overrides'])) {
|
||||
$this->errors[$path . '.clear_overrides'] = 'true oder false erwartet';
|
||||
|
||||
return $attrs;
|
||||
}
|
||||
|
||||
if ($id === null) {
|
||||
$names = $this->design->modulePresetNames($name);
|
||||
$this->errors[$path] = sprintf(
|
||||
'unbekanntes Preset; vorhanden für %s: %s',
|
||||
$name,
|
||||
$names === [] ? '(keine)' : implode(', ', $names)
|
||||
);
|
||||
|
||||
return $attrs;
|
||||
}
|
||||
|
||||
// Design-system data, but still through the choke point: an id text() would change is refused.
|
||||
try {
|
||||
$clean = $this->cleaner->clean($name, 'modulePreset', $id, null, ValueKind::Text);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$clean = null;
|
||||
}
|
||||
|
||||
if ($clean !== $id) {
|
||||
$this->errors[$path] = sprintf('Preset-ID "%s" ist ungültig', $id);
|
||||
|
||||
return $attrs;
|
||||
}
|
||||
|
||||
$attrs['modulePreset'] = [$id];
|
||||
|
||||
return $attrs;
|
||||
return $spec['clear_overrides'] ? $this->presets->clearOverrides($attrs) : $attrs;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -63,6 +63,9 @@ final class ValueCleaner
|
||||
private const BUILDER_ONLY = '/(^|\.)(decoration\.attributes|decoration\.interactions|advanced\.html'
|
||||
. '|advanced\.customCss|css|lottie\.innerContent)(\.|$)/';
|
||||
|
||||
/** Module the catalog routes design-system ids through; the kind is fixed to text, so any module will do. */
|
||||
private const ID_MODULE = 'divi/text';
|
||||
|
||||
public function __construct(
|
||||
private ModuleCatalog $catalog,
|
||||
private VariableRef $references,
|
||||
@@ -92,6 +95,24 @@ final class ValueCleaner
|
||||
return $this->cleanNode($module, $path, $value, $subKey, $subKey, $kind);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether $id passes clean() unchanged as plain text – the check for ids that come from the design system
|
||||
* (preset ids, ruling S13): an id the sanitizers would change or refuse is never stored, and an id is never
|
||||
* a Divi reference.
|
||||
*/
|
||||
public function keepsId(string $id): bool
|
||||
{
|
||||
if (VariableRef::contains($id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->clean(self::ID_MODULE, 'modulePreset', $id, null, ValueKind::Text) === $id;
|
||||
} catch (InvalidArgumentException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The kind clean() routes a string of $path (sub-field $subKey) to. lit/divi-replace-text
|
||||
* uses it to leave URL and code fields alone (D10).
|
||||
|
||||
Reference in New Issue
Block a user