diff --git a/src/Integrations/Divi/Compose/AttrWriter.php b/src/Integrations/Divi/Compose/AttrWriter.php index 0f5520c..9f879c6 100644 --- a/src/Integrations/Divi/Compose/AttrWriter.php +++ b/src/Integrations/Divi/Compose/AttrWriter.php @@ -77,8 +77,10 @@ final class AttrWriter $path = (string) $path; $key = $field . '.' . $path; if ($this->isStructure($module, $path)) { - $errors[$key] = 'Struktur des Abschnitts bzw. der Spalte; nur im Divi-Builder änderbar ' - . '(Abschnittsart beim Einfügen über section_type)'; + $errors[$key] = $module === 'divi/section' + ? 'Struktur des Abschnitts; nur im Divi-Builder änderbar (Abschnittsart beim Einfügen über ' + . 'section_type)' + : 'Spezialspalten sind Struktur; nur im Divi-Builder änderbar'; continue; } diff --git a/src/Integrations/Divi/Compose/Composer.php b/src/Integrations/Divi/Compose/Composer.php index 4a478ee..4151a92 100644 --- a/src/Integrations/Divi/Compose/Composer.php +++ b/src/Integrations/Divi/Compose/Composer.php @@ -48,6 +48,12 @@ final class Composer private const DEFAULT_LEVEL = 'h2'; + /** + * The builder's "Column Class" choices (Divi 5.2.1 visual-builder/build/field-library.js): + * n_24, the special fractions and "none" (no width class). Ruling S43. + */ + private const COLUMN_CLASSES = ['1_5', '2_5', '3_5', '1_7', '1_9', '1_10', '1_11', 'none']; + /** Advice of the S40 overlap error for new nodes. */ private const ONE_FORM = 'für diesen Pfad nur Kurzform oder attrs angeben'; @@ -467,6 +473,13 @@ final class Composer } $result = $this->attrWriter->apply($name, $attrs, $spec['attrs'], $path . '.attrs'); + // 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; + if (isset($result['errors'][$key])) { + $this->errors[$key] = $result['errors'][$key]; + } + } $this->errors += $result['errors']; array_push($this->invalidPaths, ...$result['invalid_paths']); @@ -658,6 +671,17 @@ final class Composer return $error; } + /** Ruling S43: whether $width is a column class the Visual Builder offers. */ + public static function isColumnClass(mixed $width): bool + { + if (! is_string($width)) { + return false; + } + + return in_array($width, self::COLUMN_CLASSES, true) + || (preg_match('/^([1-9]|1\d|2[0-4])_24$/D', $width) === 1); + } + /** * Whether the fractions add up to exactly 1: numerators scaled to the least common * denominator (denominators are divisors of 24 or 5, 7, 9, 10, 11 – at most 27720). diff --git a/src/Integrations/Divi/Pages/DiviUpdateAbility.php b/src/Integrations/Divi/Pages/DiviUpdateAbility.php index c562959..90913c0 100644 --- a/src/Integrations/Divi/Pages/DiviUpdateAbility.php +++ b/src/Integrations/Divi/Pages/DiviUpdateAbility.php @@ -24,6 +24,9 @@ final class DiviUpdateAbility implements Ability { private const SIZING = 'module.decoration.sizing'; + /** Divi's column default (column/module-default-render-attributes.json). */ + private const DEFAULT_WIDTH = '24_24'; + public function __construct(private DiviWriter $writer) { } @@ -182,22 +185,34 @@ final class DiviUpdateAbility implements Ability } /** - * Ruling S41: a column whose desktop width changes must leave its row filled exactly – the - * rules of "columns" (Composer::rowWidthsError()), checked over the row after all changes. + * Column widths on update (rulings S41, S43). Every new desktop flexType must be one of the + * builder's column classes (Composer::isColumnClass()) or null (removed: Divi's default + * 24_24). The row sum is only checked for rows that are exact today – every column has a + * class other than "none", the classes fill the row (Composer::rowWidthsError()), the row + * does not wrap or use a grid on desktop – and when no new width is "none". Such a row + * must stay exact after all changes; other rows are laid out by Divi as the builder allows. * * @param list, 2: string}> $updates * @return array */ private function rowWidths(DiviPage $page, Composer $composer, array $updates): array { + $errors = []; $new = []; $rows = []; foreach ($updates as [$block, $attrs, $field]) { - if ($block->name() !== 'divi/column' || self::width($attrs) === self::width($block->attrs())) { + $width = self::width($attrs); + if ($block->name() !== 'divi/column' || $width === self::width($block->attrs())) { continue; } - $new[spl_object_id($block)] = $attrs; + if ($width !== null && ! Composer::isColumnClass($width)) { + $errors[$field . '.attrs.' . self::SIZING] = 'Spaltenbreite (flexType) wie im Divi-Builder erwartet: ' + . '1_24 … 24_24, 1_5, 2_5, 3_5, 1_7, 1_9, 1_10, 1_11 oder none'; + continue; + } + + $new[spl_object_id($block)] = $width ?? self::DEFAULT_WIDTH; $row = $page->index->parentOf($block); if ($row instanceof Block) { $rows[spl_object_id($row)]['row'] = $row; @@ -205,14 +220,19 @@ final class DiviUpdateAbility implements Ability } } - $errors = []; foreach ($rows as ['row' => $row, 'fields' => $fields]) { - $widths = []; + $before = []; + $after = []; foreach ($row->children() as $column) { - $widths[] = self::width($new[spl_object_id($column)] ?? $column->attrs()); + $before[] = self::width($column->attrs()); + $after[] = $new[spl_object_id($column)] ?? self::width($column->attrs()); } - $error = $composer->rowWidthsError($widths); + if (! self::isExact($row, $before, $composer) || in_array('none', $after, true)) { + continue; + } + + $error = $composer->rowWidthsError($after); if ($error !== null) { foreach ($fields as $field) { $errors[$field . '.attrs.' . self::SIZING] = $error @@ -224,6 +244,19 @@ final class DiviUpdateAbility implements Ability return $errors; } + /** @param list $widths Desktop flexTypes of the row's columns today. */ + private static function isExact(Block $row, array $widths, Composer $composer): bool + { + $layout = AttrMap::desktop($row->attrs(), 'module.decoration.layout'); + $wraps = is_array($layout) + && (($layout['flexWrap'] ?? null) === 'wrap' || ($layout['display'] ?? null) === 'grid'); + if ($wraps) { + return false; + } + + return $composer->rowWidthsError($widths) === null; + } + /** @param array $attrs */ private static function width(array $attrs): mixed { diff --git a/tests/Unit/Integrations/Divi/Compose/AttrWriterTest.php b/tests/Unit/Integrations/Divi/Compose/AttrWriterTest.php index cdda92e..32741c8 100644 --- a/tests/Unit/Integrations/Divi/Compose/AttrWriterTest.php +++ b/tests/Unit/Integrations/Divi/Compose/AttrWriterTest.php @@ -527,6 +527,11 @@ final class AttrWriterTest extends TestCase self::assertSame([], $result['attrs'], $path); } + $column = ComposeFixture::attrWriter()->apply('divi/column', [], ['module.advanced.specialtyColumns' => null], 'attrs'); + self::assertStringNotContainsString('section_type', $column['errors']['attrs.module.advanced.specialtyColumns']); + $section = ComposeFixture::attrWriter()->apply('divi/section', [], ['module.advanced.type' => null], 'attrs'); + self::assertStringContainsString('section_type', $section['errors']['attrs.module.advanced.type']); + $heading = ComposeFixture::attrWriter()->apply('divi/heading', [], ['module.meta.adminLabel' => ['desktop' => 'x']], 'attrs'); self::assertSame([], $heading['errors'], 'only section type and specialty columns are structure'); } diff --git a/tests/Unit/Integrations/Divi/Compose/ComposerTest.php b/tests/Unit/Integrations/Divi/Compose/ComposerTest.php index fdfc076..22d6174 100644 --- a/tests/Unit/Integrations/Divi/Compose/ComposerTest.php +++ b/tests/Unit/Integrations/Divi/Compose/ComposerTest.php @@ -261,6 +261,20 @@ final class ComposerTest extends TestCase self::assertStringContainsString('label', $update['errors']['changes[0].attrs.module.meta.adminLabel']); } + /** Ruling S43: an unknown path wins over the overlap message on the same key. */ + public function testUnknownPathWinsOverOverlap(): void + { + $result = ComposeFixture::composer()->compose([[ + 'type' => 'heading', + 'text' => 'x', + 'label' => 'B', + 'attrs' => ['module.meta' => ['desktop' => 'A']], + ]], Block::create('divi/column'), 'nodes'); + + self::assertSame(['module.meta'], $result['invalid_paths']); + self::assertStringContainsString('unbekannter Attributpfad', $result['errors']['nodes[0].attrs.module.meta']); + } + /** Ruling S41: the default heading level survives attrs on the same font value. */ public function testDefaultHeadingLevelIsMergedIntoAttrs(): void { diff --git a/tests/Unit/Integrations/Divi/Pages/DiviWriteAbilitiesTest.php b/tests/Unit/Integrations/Divi/Pages/DiviWriteAbilitiesTest.php index 3a43323..5f329c4 100644 --- a/tests/Unit/Integrations/Divi/Pages/DiviWriteAbilitiesTest.php +++ b/tests/Unit/Integrations/Divi/Pages/DiviWriteAbilitiesTest.php @@ -318,6 +318,71 @@ final class DiviWriteAbilitiesTest extends TestCase self::assertStringContainsString('"flexType":"12_24"}}}}},"builderVersion":"5.2.1"} -->