fix: Task 6 review round 1 - admin label overlap, heading default level, structure deny-list, row widths (S41)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -42,6 +42,17 @@ final class AttrWriter
|
||||
/** Text-family kinds from laxest to strictest (ruling S33a); url stands apart. */
|
||||
private const STRICTNESS = ['code' => 0, 'html' => 1, 'text' => 2];
|
||||
|
||||
/**
|
||||
* Structure attributes (ruling S41), refused for everyone and whatever the catalog says:
|
||||
* the section type (regular/fullwidth/specialty) and a column's specialty columns decide
|
||||
* which children are valid – StructureRules would be bypassed. Sections get their type
|
||||
* through "section_type" when they are inserted.
|
||||
*/
|
||||
private const STRUCTURE = [
|
||||
'divi/section' => ['module.advanced.type'],
|
||||
'divi/column' => ['module.advanced.specialtyColumns'],
|
||||
];
|
||||
|
||||
private const BUILDER_ONLY = '/(^|\.)(decoration\.attributes|advanced\.html|advanced\.customCss|css)(\.|$)/';
|
||||
|
||||
public function __construct(
|
||||
@@ -65,6 +76,12 @@ final class AttrWriter
|
||||
foreach ($changes as $path => $map) {
|
||||
$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)';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $this->catalog->isKnownPath($module, $path)) {
|
||||
$suggestions = $this->catalog->suggestions($module, $path);
|
||||
$errors[$key] = 'unbekannter Attributpfad'
|
||||
@@ -147,6 +164,17 @@ final class AttrWriter
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
private function isStructure(string $module, string $path): bool
|
||||
{
|
||||
foreach (self::STRUCTURE[$module] ?? [] as $structure) {
|
||||
if (AttrMap::overlaps($path, $structure)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rejects Divi's HTML/attribute/CSS fields for users without unfiltered_html (S7/S26); also
|
||||
* for removals, which the short form does without cleanValue().
|
||||
|
||||
@@ -46,6 +46,8 @@ final class Composer
|
||||
|
||||
private const BACKGROUND = 'module.decoration.background';
|
||||
|
||||
private const DEFAULT_LEVEL = 'h2';
|
||||
|
||||
/** Advice of the S40 overlap error for new nodes. */
|
||||
private const ONE_FORM = 'für diesen Pfad nur Kurzform oder attrs angeben';
|
||||
|
||||
@@ -122,8 +124,13 @@ final class Composer
|
||||
}
|
||||
|
||||
$set = $change['set'] ?? null;
|
||||
$short = is_array($set) ? $this->shortPaths($block->shortName(), $set, 'set.') : [];
|
||||
if (array_key_exists('label', $change)) {
|
||||
// Ruling S41: the admin label is a short-form value too.
|
||||
$short['label'] = self::LABEL_PATH;
|
||||
}
|
||||
$this->overlaps(
|
||||
is_array($set) ? $this->shortPaths($block->shortName(), $set, 'set.') : [],
|
||||
$short,
|
||||
$change['attrs'] ?? null,
|
||||
$field,
|
||||
'bitte als zwei Änderungen senden'
|
||||
@@ -156,10 +163,7 @@ final class Composer
|
||||
foreach (array_keys($full) as $path) {
|
||||
$path = (string) $path;
|
||||
foreach ($short as $name => $shortPath) {
|
||||
if (
|
||||
$path === $shortPath || str_starts_with($path, $shortPath . '.')
|
||||
|| str_starts_with($shortPath, $path . '.')
|
||||
) {
|
||||
if (AttrMap::overlaps($path, $shortPath)) {
|
||||
$this->errors[$field . '.attrs.' . $path] = sprintf(
|
||||
'derselbe Attributpfad wird auch über %s geändert; %s',
|
||||
$name,
|
||||
@@ -171,6 +175,47 @@ final class Composer
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<array-key, mixed> $full "attrs" input. */
|
||||
private function touches(array $full, string $path): bool
|
||||
{
|
||||
foreach (array_keys($full) as $given) {
|
||||
if (AttrMap::overlaps((string) $given, $path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruling S41: the default level of a new heading stays when attrs replaced the desktop font
|
||||
* value it lives in (attrs replace a breakpoint value whole) – unless attrs name a level.
|
||||
*
|
||||
* @param array<string, mixed> $attrs
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function defaultLevel(string $short, array $attrs): array
|
||||
{
|
||||
$field = ShortformMap::fields($short)['level'] ?? null;
|
||||
if ($field === null || $field['sub'] === null) {
|
||||
return $attrs;
|
||||
}
|
||||
|
||||
$value = AttrMap::desktop($attrs, (string) $field['path']);
|
||||
if ($value !== null && ! is_array($value)) {
|
||||
return $attrs;
|
||||
}
|
||||
|
||||
if (is_array($value) && array_key_exists((string) $field['sub'], $value)) {
|
||||
return $attrs;
|
||||
}
|
||||
|
||||
$current = AttrMap::get($attrs, (string) $field['path'] . '.desktop.value');
|
||||
$merged = array_merge([(string) $field['sub'] => self::DEFAULT_LEVEL], is_array($current) ? $current : []);
|
||||
|
||||
return AttrMap::set($attrs, (string) $field['path'] . '.desktop.value', $merged);
|
||||
}
|
||||
|
||||
/**
|
||||
* Paths of the short-form fields a client gave, keyed by their input name ($prefix . field).
|
||||
*
|
||||
@@ -346,8 +391,9 @@ final class Composer
|
||||
// of the same name (contact-field type, social network label) go through lit-divi-update "set".
|
||||
$values = array_intersect_key($spec, array_flip(array_diff($fields, self::COMMON)));
|
||||
$this->overlaps($this->shortPaths($short, $values, ''), $spec['attrs'] ?? null, $path, self::ONE_FORM);
|
||||
if ($name === 'divi/heading' && ! array_key_exists('level', $values)) {
|
||||
$values['level'] = 'h2';
|
||||
$defaultLevel = $name === 'divi/heading' && ! array_key_exists('level', $values);
|
||||
if ($defaultLevel) {
|
||||
$values['level'] = self::DEFAULT_LEVEL;
|
||||
}
|
||||
|
||||
$attrs = ['builderVersion' => $this->builderVersion];
|
||||
@@ -357,7 +403,12 @@ final class Composer
|
||||
$this->errors += $result['errors'];
|
||||
}
|
||||
|
||||
$block = Block::create($name, $this->common($name, $attrs, $spec, $path, $this->autoLabel($name, $context)));
|
||||
$attrs = $this->common($name, $attrs, $spec, $path, $this->autoLabel($name, $context));
|
||||
if ($defaultLevel) {
|
||||
$attrs = $this->defaultLevel($short, $attrs);
|
||||
}
|
||||
|
||||
$block = Block::create($name, $attrs);
|
||||
if ($hasItems) {
|
||||
$this->children($block, $spec['items'] ?? [], $path . '.items', $context);
|
||||
} elseif ($name === 'divi/group') {
|
||||
@@ -377,6 +428,14 @@ final class Composer
|
||||
*/
|
||||
private function common(string $name, array $attrs, array $spec, string $path, ?string $autoLabel): array
|
||||
{
|
||||
// Ruling S41: "label" and attrs on the admin label is an error; attrs alone replace the auto label.
|
||||
$full = $spec['attrs'] ?? null;
|
||||
if (array_key_exists('label', $spec) && $spec['label'] !== null) {
|
||||
$this->overlaps(['label' => self::LABEL_PATH], $full, $path, self::ONE_FORM);
|
||||
} elseif (is_array($full) && $this->touches($full, self::LABEL_PATH)) {
|
||||
$autoLabel = null;
|
||||
}
|
||||
|
||||
$attrs = $this->presetAndAttrs($name, $attrs, $spec, $path);
|
||||
$label = array_key_exists('label', $spec) && $spec['label'] !== null ? $spec['label'] : $autoLabel;
|
||||
if ($label === null || $label === '') {
|
||||
@@ -574,6 +633,31 @@ final class Composer
|
||||
return $widths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruling S41: the desktop flexType values of all columns of a row (in order), checked with
|
||||
* the rules of "columns" – valid Divi widths that fill the row exactly. Null when valid,
|
||||
* else the German problem.
|
||||
*
|
||||
* @param list<mixed> $flexTypes
|
||||
*/
|
||||
public function rowWidthsError(array $flexTypes): ?string
|
||||
{
|
||||
$saved = $this->errors;
|
||||
$this->errors = [];
|
||||
$valid = $flexTypes !== [] && array_filter(
|
||||
$flexTypes,
|
||||
static fn (mixed $width): bool => is_string($width) && preg_match('/^\d{1,2}_\d{1,2}$/D', $width) === 1
|
||||
) === $flexTypes;
|
||||
if ($valid) {
|
||||
$this->widths(implode(',', $flexTypes), 'w');
|
||||
}
|
||||
|
||||
$error = $valid ? ($this->errors['w'] ?? null) : 'Spaltenbreiten wie "8_24" erwartet';
|
||||
$this->errors = $saved;
|
||||
|
||||
return $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
|
||||
Reference in New Issue
Block a user