From 2e4309cb42fa6d0198d791f0eb41b76c90646e2c Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Mon, 28 Sep 2026 00:26:33 +0200 Subject: [PATCH] test: Divi 5 design system fixtures and format notes for Etappe 4 Seed script writes four global colors (one relative), one variable per type, four module presets (one default) and two option group presets plus the draft page "[Fixture] Designsystem" through Divi's own builder REST routes; export script saves them to tests/Fixtures/divi/stage4/. Notes document storage, write functions, side effects and permissions. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7 --- .../notes/2026-09-28-etappe-4-divi-formate.md | 198 ++++ tests/Fixtures/divi/stage4/README.md | 73 ++ .../Fixtures/divi/stage4/et-divi-subset.json | 58 ++ tests/Fixtures/divi/stage4/global-colors.json | 116 +++ .../divi/stage4/global-variables.json | 210 ++++ tests/Fixtures/divi/stage4/manifest.json | 16 + .../divi/stage4/page--designsystem.html | 14 + .../divi/stage4/page--designsystem.json | 36 + .../Fixtures/divi/stage4/presets-history.json | 17 + tests/Fixtures/divi/stage4/presets.json | 909 ++++++++++++++++++ .../divi-export-stage4-fixtures.php | 191 ++++ tests/Integration/divi-seed-stage4-design.php | 558 +++++++++++ 12 files changed, 2396 insertions(+) create mode 100644 docs/superpowers/notes/2026-09-28-etappe-4-divi-formate.md create mode 100644 tests/Fixtures/divi/stage4/README.md create mode 100644 tests/Fixtures/divi/stage4/et-divi-subset.json create mode 100644 tests/Fixtures/divi/stage4/global-colors.json create mode 100644 tests/Fixtures/divi/stage4/global-variables.json create mode 100644 tests/Fixtures/divi/stage4/manifest.json create mode 100644 tests/Fixtures/divi/stage4/page--designsystem.html create mode 100644 tests/Fixtures/divi/stage4/page--designsystem.json create mode 100644 tests/Fixtures/divi/stage4/presets-history.json create mode 100644 tests/Fixtures/divi/stage4/presets.json create mode 100644 tests/Integration/divi-export-stage4-fixtures.php create mode 100644 tests/Integration/divi-seed-stage4-design.php diff --git a/docs/superpowers/notes/2026-09-28-etappe-4-divi-formate.md b/docs/superpowers/notes/2026-09-28-etappe-4-divi-formate.md new file mode 100644 index 0000000..ddf9ee5 --- /dev/null +++ b/docs/superpowers/notes/2026-09-28-etappe-4-divi-formate.md @@ -0,0 +1,198 @@ +# Etappe 4 – Datenformate des Divi-Designsystems (Divi 5.2.1) + +Stand 2026-09-28. Grundlage: Quelltext von Divi 5.2.1 und echte Daten, die über Divis eigene Speicherwege angelegt wurden (Seed-Skript `tests/Integration/divi-seed-stage4-design.php`, Fixtures `tests/Fixtures/divi/stage4/`). Erfüllt Spec §14 („Datenformate Designsystem nur aus Code abgeleitet“) und §15 („Format der Presets/Variablen an echten Daten verifizieren“). + +Pfadangaben relativ zu `wp-content/themes/Divi/`. Abkürzung **B5** = `includes/builder-5/server/`. Minifizierte Builder-Dateien liegen in `includes/builder-5/visual-builder/build/`; dort steht statt einer Zeile ein Suchbegriff. + +## 0. Grundlagen: wo Divi speichert + +`et_get_option()`/`et_update_option()` (`epanel/custom_functions.php:211`, `:272`) haben drei Ziele: + +- normale Theme-Option → Schlüssel im Array der Option **`et_divi`** (autoload); +- „Product Setting“ (letzter Parameter `true`) → **eigene Option `et_divi_`**, autoload aus (`:224`, `:288-292`); +- `et_delete_option()` (`:316`) kennt **keine** Product Settings und löscht immer nur den Schlüssel in `et_divi`. + +Folge: `GlobalPreset::delete_data()` (B5 `Packages/GlobalData/GlobalPreset.php:149-158`) ruft `et_delete_option(option_name())` auf und entfernt die Option `et_divi_builder_global_presets_d5` **nicht** – nur den statischen Cache. Für Etappe 4 nicht verwenden. + +`et_update_option()` aktualisiert zusätzlich das globale `$et_theme_options`; direktes `update_option('et_divi', …)` lässt diesen Cache im selben Request veralten. + +## 1. Globale Farben + +**Speicherort:** `et_divi['et_global_data']['global_colors']` (normale Theme-Option, kein Product Setting). Die fünf Customizer-Farben liegen **nicht** dort, sondern als `et_divi['accent_color']`, `secondary_accent_color`, `header_color`, `font_color`, `link_color` (Zuordnung `GlobalData::$customizer_colors`, B5 `Packages/GlobalData/GlobalData.php:58-83`). + +**Format eines Eintrags** (Schlüssel = ID): + +```json +"gcid-q9swzy97df": { + "color": "#5b3a29", + "folder": "", + "label": "[Fixture] Holz dunkel", + "lastUpdated": "2026-09-27T22:21:39.000Z", + "status": "active", + "usedInPosts": [] +} +``` + +- ID: `gcid-` + 10 Zeichen `[0-9a-z]` (Builder: `generateVariableId`/`generateNanoUuid`, `global-data.js`, Suche ``S=(e,t)=>e||("colors"===t?`gcid-${_()}``). Ältere Daten können UUIDs haben (`gcid-79d0acb1-…`, Beispiele `GlobalData.php:700-770`). Prüfung serverseitig nur `substr($id, 0, 5) === 'gcid-'` (`:597-603`). +- `status`: `active` | `inactive` | `temporary`. Der Builder **löscht nie**: „Löschen“ setzt `status: inactive` (`global-data.js`, Suche `case"DELETE_GLOBAL_COLOR"`). +- `label`/`folder` werden gespeichert, obwohl der Konverter-Kommentar „not until D6“ sagt (`:159-160`); der Builder liest `label` (Suche `label:t.label||""`). +- `lastUpdated`: Format `Y-m-d\TH:i:s.v\Z`. +- **Relative Farbe** (Farbe aus anderer Farbe): `color` enthält eine Referenz + `$variable({"type":"color","value":{"name":"gcid-q9swzy97df","settings":{"lightness":30}}})$` + mit `settings` aus `hue`, `saturation`, `lightness`, `opacity` (`GlobalData::transform_state_into_global_color_value`, `:181-206`). CSS-Ausgabe: `--gcid-989stx15zc: hsl(from var(--gcid-q9swzy97df) calc(h + 0) calc(s + 0) calc(l + 30));` (`FrontEnd/Module/Style.php:910-947` über `StyleLibrary/Utils/Utils.php:86-130`). Abhängigkeiten werden rekursiv aufgelöst (`GlobalData.php:220-310`, Tiefe max. 10 in `:1336ff`). + +**Lesen:** `GlobalData::get_global_colors()` (`:333-354`) liefert Customizer-Farben (neu erzeugt, `folder: customizer`, übersetztes Label, `lastUpdated` = jetzt, `:370-398`) **vor** den gespeicherten Farben; gleichnamige gespeicherte Customizer-IDs werden verworfen. Ergebnis wird statisch gecacht (`$_cached_data`), `set_global_colors()` setzt den Cache zurück, `get_customizer_colors()` hat einen eigenen `static`-Cache ohne Reset. + +**Schreiben:** `GlobalData::set_global_colors(array $data, $already_sanitized = false)` (`:650-691`): + +1. ohne `$already_sanitized`: `sanitize_global_colors_data()` (`:585-616`) – verwirft Einträge ohne `color` oder ohne `gcid-`, wendet `sanitize_text_field` auf jeden skalaren Wert an; `usedInPosts` wird mit `array_map` behandelt und **muss ein Array sein** (sonst `TypeError`). +2. Customizer-IDs im Payload → `et_update_option(, color)` und aus dem Payload entfernt (`:664-674`). +3. **`$already_sanitized = false` ersetzt alle gespeicherten Farben, `true` mischt** (`array_merge`) mit den vorhandenen (`:678-684`). +4. `et_update_option('et_global_data', …)`, dann `ET_Core_PageResource::remove_static_resources('all', 'all', true)` (`:687-690`). + +**Builder-Speicherweg:** `POST divi/v1/global-data/global-colors` (`VisualBuilder/REST/RESTRegistration.php:1001-1008`) → Schema-Sanitize `sanitize_global_colors_data` (`GlobalDataController.php:100-108`) → `set_global_colors($colors, true)` (`:71-78`), also **Mischen**. Der Builder schickt dabei die komplette Farbliste **einschließlich der fünf Customizer-Farben** (`global-data.js`, Suche `restRoute:"/divi/v1/global-data/global-colors"`); Entfernen einer Farbe ist über diesen Weg nicht möglich. + +**Seiteneffekte:** +- Customizer-Sync ist direkt eingebaut (Schritt 2); ein separater Hook existiert nur für Divi 4 (`includes/builder/functions.php:13205-13237`, `et_global_colors_saved`, mit veralteten IDs `gcid-header-color`/`gcid-font-color`). +- `usedInPosts` pflegt nur der Builder im Browser (`UPDATE_GLOBAL_COLORS_USED_IN_POSTS`); serverseitig wird es nie berechnet. Über PHP angelegte Farben behalten `[]`. +- Einmalige Migration Divi 4 → 5 (`maybe_convert_global_colors_data`, `:526-551`) läuft beim Laden der Builder-Einstellungen (`Framework/Settings/Settings.php:159-162`). + +**Rechte:** REST-Route nur `current_user_can('edit_posts')` (`GlobalDataController.php:124-130`); `set_global_colors()` selbst prüft nichts. Zusätzlich verlangt jede Divi-REST-Route den Nonce `X-ET-Nonce` für die Aktion `/divi/v1/--POST` (`Framework/Route/RESTRoute.php:98`, `Framework/Controllers/RESTController.php:158-178`). + +## 2. Globale Variablen + +**Speicherort:** Product Setting **`et_divi_global_variables`** (eigene Option, autoload aus), gespeichert mit `et_update_option('global_variables', $data, false, '', '', true)` (`GlobalData.php:1016`). + +**Gespeichertes Format** (PHP-Arrays): + +```json +{ + "numbers": { "gvid-f6yq6mayfy": { "id": "gvid-f6yq6mayfy", "label": "[Fixture] Eckenradius", "value": "8px", + "order": "1", "status": "active", "lastUpdated": "…", "variableType": "numbers" } }, + "strings": { … }, "images": { … }, "links": { … }, "fonts": { … } +} +``` + +- Typen: nur `numbers`, `strings`, `images`, `links`, `fonts` (`sanitize_global_variables_data`, `:804-850`). **`colors` wird beim Speichern verworfen** – „Farb-Variablen“ im Variablen-Manager sind die globalen Farben aus Abschnitt 1. +- ID: `gvid-` + 10 Zeichen `[0-9a-z]`; Pflicht: `gvid-`-Präfix und nicht-leeres `label`. +- **Das Feld `id` im Eintrag ist nötig:** die CSS-Ausgabe benutzt `$value['id']`, nicht den Schlüssel (`FrontEnd/Module/Style.php:1002`). +- Sanitize: `value` bei `strings` zeichengetreu (`wp_check_invalid_utf8` + `wp_kses_no_null`), bei `links` `esc_url_raw`, sonst `sanitize_text_field` – dadurch wird `order` zum String (`"1"`). `allowedActions` wird verworfen. Der Builder speichert außerdem `lastUpdated` und `variableType` mit (`global-data.js`, Suche `case"ADD_GLOBAL_VARIABLE"`). +- `status`: `active` | `archived`. Builder-„Löschen“ setzt `archived` und `order: null` (Suche `case"DELETE_GLOBAL_VARIABLE"`), echtes Entfernen nur ohne Label/Wert. +- `numbers` erlaubt beliebige CSS-Ausdrücke (`clamp(1.5rem, 4vw, 2.5rem)` bleibt erhalten). + +**Lesen:** `GlobalData::get_global_variables()` (`:896-963`) liefert ein Array, **jede Gruppe als `stdClass`** (`(object) $items`, `:959-962`), die Einträge darin als Arrays; ergänzt `allowedActions` und stellt die Customizer-Schriften `--et_global_heading_font` / `--et_global_body_font` (aus `et_divi['heading_font']`/`['body_font']`, Standard „Open Sans“, `:93-104`, `:415-437`) an den Anfang von `fonts`. Leere Gruppen sind leere `stdClass` (JSON `{}`). Eine beschädigte `fonts`-Gruppe wird still auf `[]` gesetzt (`:925`). + +**Schreiben:** `GlobalData::set_global_variables(array $data)` (`:1002-1020`) **ersetzt die komplette Option**. Vorher also vollständig lesen, `stdClass` → Array wandeln, `allowedActions` und die beiden Customizer-Schriften herausnehmen (sonst landen sie in der Option), dann schreiben. `import_global_variables()` (`:1087-1137`) mischt, arbeitet aber mit `stdClass`-Gruppen und schreibt nur `id/label/value/status`. +Danach `remove_static_resources('all', 'all', true)`. + +**Rechte:** `set_global_variables()` prüft `current_user_can('edit_theme_options') && et_pb_is_allowed('variables_manager')` und **kehrt sonst ohne Fehler zurück** (`:1003-1005`). Die REST-Route prüft nur `edit_posts` (`GlobalDataController.php:350-356`) und meldet danach Erfolg mit den unveränderten Daten – ein stiller Fehlschlag. `variables_manager` ist ein Recht des Divi-Rollen-Editors (`includes/builder/functions.php:9219-9222`, Auswertung `includes/builder/core.php:1128ff`, Option `et_pb_role_settings`; ohne Eintrag gilt „an“). + +**Referenzen in Modulen und Presets:** `$variable({"type":"content","value":{"name":"gvid-…","settings":{}}})$` – für Zahlen/Schriften wird daraus `var(--gvid-…)` (`StyleLibrary/Utils/Utils.php:86-130`), für Strings/Links/Bilder der Wert beim Rendern (Dynamic Content, `Packages/Module/Layout/Components/DynamicContent/DynamicContentGlobalVariableOptions.php`). `:root` enthält nur `numbers` und `fonts` (`Style.php:991-1030`), Schriftwerte in Anführungszeichen. In `post_content` ist `"` innerhalb der Referenz als `"` kodiert (WordPress-Block-Serialisierung). + +**Schriften der Customizer-Seite** schreibt der Builder separat: `POST divi/v1/global-data/global-fonts` → `et_update_option('heading_font'|'body_font')` + Cache leeren (`GlobalDataController.php:166-190`, Recht `edit_posts`). + +## 3. Presets (Modul- und Optionsgruppen-Presets) + +**Speicherort:** Product Setting **`et_divi_builder_global_presets_d5`** (`GlobalPreset::option_name()` = `builder_global_presets_d5`, `GlobalPreset.php:129`), autoload aus. Divi-4-Presets: `et_divi_builder_global_presets_ng` (wird bei Bedarf importiert, Flag `builder_is_legacy_presets_imported_to_d5`). + +**Gespeichertes Format:** + +```json +{ + "module": { + "divi/button": { "default": "", "items": { "3gbmxlurey": { …Modul-Preset… } } }, + "divi/divider": { "default": "g7kvfq5uh8", "items": { "g7kvfq5uh8": { … } } } + }, + "group": { + "divi/font-body": { "default": "", "items": { "ja6o54jtlc": { …Gruppen-Preset… } } } + } +} +``` + +Modul-Preset-Eintrag: `type: "module"`, `id`, `name`, `moduleName`, `version` (Builder-Version, hier `"5.2.1"`), `created`/`updated` (**Millisekunden**, Builder `Date.now()`; PHP-Hilfen `_generate_new_preset` nutzen `time()` in Sekunden, `:1695-1707`), `attrs`, optional `styleAttrs`, `renderAttrs`, `groupPresets`, `priority`, `order`. +Gruppen-Preset-Eintrag zusätzlich `groupName` (z. B. `divi/font-body`), `groupId` (Attributpfad im Quellmodul, z. B. `content.decoration.bodyFont`), `moduleName` (Quellmodul), optional `primaryAttrName` (nur über JS-Filter gesetzt, `module-utils.js`, Suche `divi.optionGroupPresetPrimaryAttrNameResolver`). + +- **IDs** 10 Zeichen `[0-9a-z]`, ohne Präfix (Builder `generateDefaultPresetName`, `global-data.js`). Namen sind **nicht eindeutig** – der Builder zählt „Button 1, Button 2 …“. +- **`attrs`** = normale Modulattribute (Pfad wie im Block, z. B. `button.decoration.background.desktop.value.color`); Gruppen-Presets tragen die **vollen Pfade des Quellmoduls** und werden beim Anwenden auf andere Module umgerechnet (`GlobalPresetItemGroup::_attrs_resolver`, `GlobalPresetItemGroup.php:245ff`, `GlobalPresetItemGroupAttrNameResolver`). +- **`styleAttrs`/`renderAttrs`**: Teilmengen von `attrs` für CSS bzw. HTML/Skript. Serverseitig berechenbar mit `Conversion::get_preset_attrs($attrs, ['style'] | ['html','script'], Conversion::get_preset_attrs_mapping($moduleName))` (B5 `Packages/Conversion/Conversion.php:2242`, `:2479`; genutzt in `convert_preset`, `:2144-2145`). Das Frontend benutzt `renderAttrs`/`styleAttrs` der Gruppen-Presets direkt (`Packages/ModuleLibrary/ModuleRegistration.php:326-340`). +- **`groupPresets`** im Modul-Preset (verschachtelte Gruppen-Presets): `{ "": { "presetId": [""], "groupName": "divi/…" } }`. +- **`default`** je Modul/Gruppe: ID des Standard-Presets oder `""`. Der Builder legt beim Öffnen des Preset-Managers leere Standard-Presets an (`ADD_DEFAULT_MODULE_PRESET`); ein Standard-Preset **ohne `attrs`** erzeugt keine Klasse (`get_module_preset_class_names`, `GlobalPreset.php:730-800`). +- **`priority`** fehlt meist und gilt dann als `10` (`:676`, `:2078`, `:2761`); das REST-Schema-Default `10` (`GlobalPresetController.php`, `preset_type_properties`) wird für verschachtelte Einträge nicht eingesetzt. + +**Verweise in Blöcken** (`post_content`): +- Modul-Preset: `"modulePreset": ["3gbmxlurey"]` – **Array** (Stapel); ein String ist Altformat und wird noch akzeptiert (`normalize_preset_stack`, `:2627-2680`). `""`, `"default"`, `"_initial"` bedeuten „Standard-Preset“ (`is_preset_id_as_default`, `:2691`). Fehlt `modulePreset`, gilt das Standard-Preset des Moduls. +- Gruppen-Preset: `"groupPreset": { "module.decoration.spacing": { "presetId": ["apzj7e17ok"], "groupName": "divi/spacing" } }` (`get_selected_group_presets`, `:2021-2320`; Überlagerung `:2148`). +- Stapel: letztes Element gewinnt bei `get_selected_preset` (`:522-600`); `renderAttrs` werden nach `priority` aufsteigend gemischt (`:626-712`). Verschachtelte und explizite Gruppen-Presets werden gestapelt, nicht ersetzt (`:2148-2163`). +- Welche Gruppen ein Modul hat und wie sie heißen: `GlobalPreset::get_group_preset_default_attr(ModuleRegistration::get_module_settings($name))` (`:2332-2465`), Namensliste `get_default_group_name` (`:2475-2511`). Beispiel `divi/text`: `content.decoration.bodyFont` → `divi/font-body`, `module.decoration.spacing` → `divi/spacing`; `divi/button`: `button.decoration.font` → `divi/font`, `button.decoration.button` → `divi/button`. + +**CSS-Klassen im Frontend** (`GlobalPresetItemUtils::generate_preset_class_name`, `GlobalPresetItemUtils.php:43-78`): `preset--module--divi-button--3gbmxlurey`, Standard-Preset `preset--module--divi-divider--default`, Gruppe `preset--group--divi-blurb--divi-spacing--apzj7e17ok`, verschachtelt `preset--group--divi-text--divi-font-body--nested--ja6o54jtlc`. Keine Klassen für `divi/id-classes`, `divi/animation`. Am Fixture geprüft (README in `tests/Fixtures/divi/stage4/`). + +**Schreiben:** `GlobalPreset::save_data(array $data)` (`:419-431`) schreibt die **komplette Option ungeprüft**, leert Caches (`remove_static_resources('all','all',true,'all',true)` – Builder-CSS bleibt erhalten) und setzt `self::$_data` zurück. Sanitisierung gibt es nur in `GlobalPreset::prepare_data()` (`:213-268`): wandelt das REST-Format (`{module: [{default, items: [ … ]}], group: …}`, Listen statt ID-Schlüssel) ins Speicherformat, entfernt leere Attribut-Arrays und sanitisiert `attrs`/`renderAttrs`/`styleAttrs` mit `SavingUtility::sanitize_block_attrs` bzw. `sanitize_group_attrs` (`VisualBuilder/Saving/SavingUtility.php:2153`, `:2296`; ohne `unfiltered_html` wird gefiltert). + +**Builder-Speicherweg:** `POST divi/v1/global-data/global-preset/sync` (`RESTRegistration.php:1035-1045`) → `GlobalPresetController::sync` (`GlobalPresetController.php:45-187`): optional in Blöcken (Transient 5 min), `prepare_data`, **`validate_preset_count`** (`GlobalPreset.php:335-410`: weniger Presets als gespeichert → Fehler 400 `preset_count_decreased`, außer `actionType` `DELETE_MODULE_PRESET`/`DELETE_OPTION_GROUP_PRESET`), bei Löschaktionen Bereinigung der Divi-4-Option (`remove_preset_from_legacy_option`), dann `save_data`. Der Builder schickt immer den **vollständigen** Bestand. + +**Rechte:** `sync_permission()` = `UserRole::can_current_user_use_visual_builder()` (`GlobalPresetController.php:460`, `Framework/UserRole/UserRole.php:38-70`) – nur das Rollen-Recht „Visual Builder verwenden“ (Standard „an“), **kein** `edit_theme_options`. Dazu der Routen-Nonce wie in Abschnitt 1. + +**Preset-Historie:** `et_divi_builder_presets_history_meta` (`{index, size}`) und `et_divi_builder_presets_history_item_` gehören zu Divi 4 (`includes/builder/feature/global-presets/History.php:4-10`, Schreiben nur per AJAX `et_builder_save_global_presets_history` mit `switch_themes`, `:75-139`). Der Divi-5-Sync schreibt dort **nichts**; Rückgängig im Builder ist rein clientseitig. Lokal: `{index: -1, size: 0}`. Für Etappe 4 heißt das: eine Wiederherstellung gibt es nur über eigene Snapshots (Spec §7). + +## 4. Theme-Optionen (`et_divi`) + +Lokal ist **keiner** der Whitelist-Schlüssel gesetzt (Fixture `et-divi-subset.json`) – überall gelten Divi-Standards. + +| Gruppe | Schlüssel in `et_divi` | Schreibweg in Divi | Sanitize / Standard (Customizer, `functions.php`) | +|---|---|---|---| +| Schriften | `heading_font`, `body_font`, `primary_nav_font`, `secondary_nav_font`, `all_buttons_font` | Customizer, `global-fonts`-REST (nur heading/body) | `et_sanitize_font_choices`, Standard `none` (`:2484`, `:2557`, `:3398`, `:3522`, `:4996`) | +| | `heading_font_weight`, `body_font_weight` | Customizer | `et_sanitize_global_font_weight`, `500` (`:2509`, `:2582`) | +| Größen | `body_font_size` (14), `tablet_body_font_size`, `phone_body_font_size`, `body_header_size` (30), `tablet_header_font_size`, `phone_header_font_size` | Customizer | `absint` (`:2208-2456`); Tablet/Phone-Standard = nächstgrößerer Wert | +| | `body_font_height` (1.7), `body_header_height` (1) | Customizer | `et_sanitize_float_number` (`:2236`, `:2376`) | +| | `body_header_spacing` (0), `body_header_style` (`''`) | Customizer | `et_sanitize_int_number`, `et_divi_global_font_style` (`:2348`, `:2404`) | +| Layout | `content_width` (1080), `gutter_width` (3), `section_padding` (4), `row_padding` (2) | Customizer | `absint` (`:1824`, `:1852`, `:1927`, `:2011`) | +| Farben | `accent_color`, `secondary_accent_color`, `header_color`, `font_color`, `link_color` | Customizer **und** `set_global_colors` (Abschnitt 1) | `et_sanitize_alpha_color` (`:2534-2665`) | +| Buttons | `all_buttons_*` (19 Schlüssel inkl. `*_hover`, z. B. `all_buttons_bg_color`, `all_buttons_border_radius_hover`) | Customizer | je Feld (`:4843ff`) | +| Logo | `divi_logo` | ePanel „Theme-Optionen“ | Typ `upload`, URL-String (`options_divi.php:81`) | +| SEO | `divi_seo_home_*`, `divi_seo_single_*`, `divi_seo_index_*` | ePanel | Checkboxen `'on'`/`'false'`, Texte, Selects (`options_divi.php:721-894`; Speichern `epanel/core_functions.php:868ff`, Checkbox `:969-970`/`:1049`) | + +- Customizer-Einstellungen sind `type => 'option'` mit Schlüssel `et_divi[]` und `capability => 'edit_theme_options'`; Divi prüft zusätzlich das Rollen-Recht `theme_customizer` (`functions.php:1278`). ePanel: `edit_theme_options` (`epanel/core_functions.php:873`) und Rollen-Recht `theme_options` (`functions.php:8834-8846`). +- **Standardwerte weichen voneinander ab:** `secondary_accent_color` hat im Customizer `#8800FF` (`functions.php:2665`), in `GlobalData::$customizer_colors` und im Builder `#2ea3f2` (`GlobalData.php:64-68`, `includes/builder/core.php:7293`). Wer die Builder-Sicht (`get_global_colors()`) zurückschreibt, setzt die Customizer-Farbe damit fest auf `#2ea3f2`. Ebenso Schriften: Customizer-Standard `none`, Builder-Sicht „Open Sans“. +- Caches: nach Customizer-Speichern `remove_static_resources('all','all')` (`core/components/PageResource.php:944`, `:1139-1141`) und `et_pb_force_regenerate_templates` (`includes/builder/core.php:3771`). Für eigene Schreibzugriffe gilt dasselbe. + +## 5. Caches + +`ET_Core_PageResource::remove_static_resources($post_id, $owner, $force, $slug, $preserve_vb_files, $delete_files)` (`core/components/PageResource.php:1213-1232`) läuft **nur**, wenn `wp_doing_cron()` oder `et_core_security_check_passed('edit_posts')` – ohne angemeldeten Benutzer (z. B. WP-CLI ohne `--user`) ist der Aufruf wirkungslos. Farben und Variablen rufen `('all','all',true)`, Presets `('all','all',true,'all',true)` bereits selbst auf; der in Spec §7 vorgesehene Zusatzaufruf ist harmlos, aber redundant. + +In-Request-Caches: `GlobalData::$_cached_data` (Reset bei `set_*`), `GlobalPreset::$_data` und `$_normalized_preset_stack_cache` (Reset in `save_data`/`delete_data`), `static` in `get_customizer_colors()`/`get_customizer_fonts()` und `get_group_preset_default_attr()` (kein Reset). + +## 6. Rechte im Überblick + +| Weg | Prüfung in Divi | +|---|---| +| REST `global-colors`, `global-fonts`, `global-variables` | `edit_posts` + Routen-Nonce | +| `GlobalData::set_global_variables()` | `edit_theme_options` **und** `et_pb_is_allowed('variables_manager')`, sonst stilles `return` | +| `GlobalData::set_global_colors()`, `GlobalPreset::save_data()` | keine | +| REST `global-preset/sync` | Rollen-Recht `use_visual_builder` + Routen-Nonce | +| Customizer | `edit_theme_options` + Rollen-Recht `theme_customizer` | +| ePanel | `edit_theme_options` + Rollen-Recht `theme_options` | +| `remove_static_resources` | `edit_posts` (oder Cron), sonst wirkungslos | + +## 7. Abweichungen und Ergänzungen zu Spec §7 + +1. **„Variablen … über `GlobalData::set_global_variables`“** – stimmt, aber die Funktion ersetzt die ganze Option, liefert keinen Fehler bei fehlenden Rechten und kennt keinen Typ `colors`. Das Tool muss vorher lesen, `stdClass` → Array wandeln, `allowedActions` und die zwei Customizer-Schriften entfernen und nach dem Schreiben per Lesen prüfen. +2. **„globale Farben … inkl. der fünf Hauptfarben (über `set_global_colors`)“** – die Hauptfarben landen in `et_divi['accent_color']` usw.; der zweite Parameter entscheidet zwischen Ersetzen (`false`) und Mischen (`true`). Für Änderungen einzelner Farben: Mischen; Löschen wie der Builder als `status: inactive`. Hauptfarben nur schicken, wenn sie wirklich geändert werden sollen (Standardwert-Abweichung, Abschnitt 4). +3. **„Presets … per Name“** – Namen sind nicht eindeutig; IDs sind 10-stellige Zeichenketten. Schreiben immer als vollständiger Bestand; `save_data()` sanitisiert nicht → vorher `prepare_data()` (REST-Format) und `validate_preset_count()` wie der Controller verwenden. `styleAttrs`/`renderAttrs` müssen mitgeschrieben werden (Frontend liest sie). +4. **„Voraussetzung: `edit_theme_options`; für Variablen zusätzlich `variables_manager`“** – strenger als Divi selbst (Farben: `edit_posts`; Presets: nur Visual-Builder-Recht). Als bewusste Plugin-Entscheidung beibehalten; für Variablen ist die Doppelprüfung zwingend, weil Divi sonst still nichts tut. +5. **„danach `remove_static_resources('all','all',true)`“** – Divis Setter tun das schon; wirkungslos ohne angemeldeten Benutzer. Für Presets nutzt Divi `preserve_vb_files = true`. +6. **Snapshots** sind die einzige Wiederherstellung: Divi 5 führt für Farben, Variablen und Presets keine serverseitige Historie; die Option `et_divi_builder_presets_history_meta` gehört zu Divi 4. +7. **Theme-Optionen:** `divi_logo` und `divi_seo_*` sind ePanel-Felder (andere Sanitize-Regeln, Checkbox-Werte `'on'`/`'false'`), keine Customizer-Einstellungen. Die übrigen Whitelist-Felder haben die Customizer-Sanitize-Callbacks aus Abschnitt 4 – diese sollten wiederverwendet werden. +8. **Lesepfad des Plugins** (`src/Integrations/Divi/Design/DesignSystem.php`): Farb-„Variablen“ (`variables['colors']`) sind in 5.2.1 immer leer – globale Farben tragen Label und ID selbst. `fonts()` liefert die Builder-Sicht („Open Sans“), nicht den Customizer-Standard `none`. + +## 8. Stolpersteine (Kurzliste) + +- `get_global_variables()` liefert `stdClass` je Gruppe, gespeichert sind Arrays; leere Gruppe = `{}` im JSON. +- Variableneinträge brauchen das Feld `id`; `order` wird zum String. +- `usedInPosts` muss ein Array sein; wird nur vom Builder gepflegt. +- `created`/`updated` in Millisekunden (Builder) vs. Sekunden (PHP-Hilfen). +- `modulePreset` ist ein Array; Standard-Preset wirkt nur mit `attrs`. +- Sync-Endpunkt verweigert kleinere Preset-Anzahl ohne `DELETE_*`-actionType. +- `GlobalPreset::delete_data()` löscht die Product-Setting-Option nicht. +- Divi-REST-Routen verlangen `X-ET-Nonce` pro Route; direkte PHP-Aufrufe umgehen Nonce, Schema und Rechteprüfung. +- `secondary_accent_color`: Customizer-Standard `#8800FF`, Builder-Standard `#2ea3f2`. diff --git a/tests/Fixtures/divi/stage4/README.md b/tests/Fixtures/divi/stage4/README.md new file mode 100644 index 0000000..135f16b --- /dev/null +++ b/tests/Fixtures/divi/stage4/README.md @@ -0,0 +1,73 @@ +# Divi-5-Fixtures für Etappe 4 (Designsystem: Farben, Variablen, Presets, Theme-Optionen) + +Echte Daten aus Divi 5.2.1, erzeugt am 2026-09-28 auf der lokalen DDEV-Site (`https://wordpress.ddev.site:8443`). Vorher hatte die Site keine eigenen Designdaten: nur die fünf Customizer-Farben (alle auf Divi-Standard, in `et_divi` nicht gespeichert), keine Variablen, keine Presets. + +Formate, Quellen mit Zeilenangaben und Stolpersteine: `docs/superpowers/notes/2026-09-28-etappe-4-divi-formate.md`. + +## Wie die Daten entstanden sind + +Skript `tests/Integration/divi-seed-stage4-design.php` (idempotent, findet Einträge über ihr „[Fixture] …“-Label wieder): + +``` +cd /home/janniclas/Projekte/WordPress +ddev exec wp eval-file wp-content/plugins/WP-LIT-MCP/tests/Integration/divi-seed-stage4-design.php +ddev exec wp eval-file wp-content/plugins/WP-LIT-MCP/tests/Integration/divi-export-stage4-fixtures.php +``` + +Das Seed-Skript schreibt **ausschließlich über die REST-Routen, die der Visual Builder selbst aufruft** – prozessintern per `rest_do_request()` als Administrator, mit Divis Routen-Nonce im Header `X-ET-Nonce`. Damit laufen Divis Argument-Schema, Sanitize-Callbacks, Berechtigungsprüfung und Speicherfunktionen genau wie beim Speichern im Builder: + +| Bereich | Route | Divi-Funktion | +|---|---|---| +| Farben | `POST divi/v1/global-data/global-colors` | `GlobalDataController::update_global_colors` → `GlobalData::set_global_colors($data, true)` | +| Variablen | `POST divi/v1/global-data/global-variables` | `GlobalDataController::update_global_variables` → `GlobalData::set_global_variables()` | +| Presets | `POST divi/v1/global-data/global-preset/sync` | `GlobalPresetController::sync` → `GlobalPreset::prepare_data()` → `validate_preset_count()` → `save_data()` | +| Seite | `POST divi/v1/sync-to-server` | `SyncToServerController::update` (wie „Entwurf speichern“) | + +Die Seite wurde vorher mit `wp_insert_post()` als leerer Entwurf angelegt und mit `et_builder_enable_for_post()` für den Builder freigeschaltet (wie Etappe 3). Die Aufteilung der Preset-Attribute in `styleAttrs`/`renderAttrs` stammt aus Divis eigener Attribut-Karte (`Conversion::get_preset_attrs_mapping()` / `Conversion::get_preset_attrs()`, derselbe Weg wie beim Import von Divi-4-Presets). IDs sind wie im Builder 10 Zeichen aus `[0-9a-z]` (`generateNanoUuid`, `global-data.js`). + +Nicht angefasst: die fünf Customizer-Farben (nicht im Payload, `accent_color` & Co. bleiben ungesetzt), Theme-Optionen, Divi-4-Preset-Historie, alle übrigen Inhalte der Site. + +## Angelegte Einträge + +| Art | ID | Label/Name | Inhalt | +|---|---|---|---| +| Farbe | `gcid-q9swzy97df` | [Fixture] Holz dunkel | `#5b3a29` | +| Farbe | `gcid-989stx15zc` | [Fixture] Holz hell | relativ: Holz dunkel, `lightness +30` | +| Farbe | `gcid-pgtgy2j1i7` | [Fixture] Salbeigrün | `#7d9b76` | +| Farbe | `gcid-gg4lef6xns` | [Fixture] Sandbeige | `#efe6d8` | +| Variable `numbers` | `gvid-f6yq6mayfy` | [Fixture] Eckenradius | `8px` | +| Variable `numbers` | `gvid-5dec72fwsc` | [Fixture] Abstand Karte | `clamp(1.5rem, 4vw, 2.5rem)` | +| Variable `strings` | `gvid-7uckqqhmrh` | [Fixture] Slogan | `Handwerk aus Meisterhand – seit 1987` | +| Variable `images` | `gvid-qmc3cy1alq` | [Fixture] Werkstattbild | URL eines vorhandenen Bildes der Mediathek | +| Variable `links` | `gvid-nt8f6iz2qh` | [Fixture] Kontaktseite | `https://wordpress.ddev.site:8443/kontakt/` | +| Variable `fonts` | `gvid-d4a5bsz2gt` | [Fixture] Akzentschrift | `Merriweather` | +| Modul-Preset `divi/button` | `3gbmxlurey` | [Fixture] Button Primär | Hintergrund Holz dunkel, Schrift Sandbeige/600, Radius = Eckenradius | +| Modul-Preset `divi/text` | `hsiy1sjxt7` | [Fixture] Text Hinweis | Hintergrund Sandbeige, Radius, Innenabstand = Abstand Karte, **verschachteltes** Gruppen-Preset Fließtext | +| Modul-Preset `divi/blurb` | `vdcl6msnuo` | [Fixture] Blurb Leistung | Titel Akzentschrift/Holz dunkel/700, Rahmen Salbeigrün, Radius | +| Modul-Preset `divi/divider` | `g7kvfq5uh8` | [Fixture] Trenner Standard | **Standard-Preset** des Moduls; Linie Holz hell, 3px | +| Gruppen-Preset `divi/font-body` | `ja6o54jtlc` | [Fixture] Fließtext | aus `divi/text`, `content.decoration.bodyFont`; Akzentschrift, Holz dunkel, 17px/1.8em | +| Gruppen-Preset `divi/spacing` | `apzj7e17ok` | [Fixture] Abstand Karte | aus `divi/blurb`, `module.decoration.spacing`; Innenabstand = Abstand Karte, Außenabstand unten 24px | +| Seite | 3182 | [Fixture] Designsystem | Entwurf; nutzt alle Presets und die Variablen string/link/image | + +`divi/divider` wird auf keiner anderen Seite der Site verwendet – das Standard-Preset ändert dort nichts. + +## Dateien + +| Datei | Inhalt | +|---|---| +| `global-colors.json` | `et_divi['et_global_data']` wie gespeichert und `GlobalData::get_global_colors()` (Customizer-Farben vorn; deren `lastUpdated` wird bei jedem Lesen neu erzeugt) | +| `global-variables.json` | Option `et_divi_global_variables` (dekodiert und als serialisierte Zeile), `GlobalData::get_global_variables()` und die PHP-Typen je Gruppe (`stdClass` – eine leere Gruppe erscheint im JSON als `{}`) | +| `presets.json` | Option `et_divi_builder_global_presets_d5`, `GlobalPreset::get_data()`, die Divi-4-Option `et_divi_builder_global_presets_ng`, Importkennzeichen | +| `presets-history.json` | Divi-4-Preset-Historie (`et_divi_builder_presets_history_meta`, keine Einträge) und die Migrationsflags aus `et_divi` | +| `et-divi-subset.json` | Theme-Optionen der Whitelist aus Spec §7: vorhandene Schlüssel mit Wert, fehlende Schlüssel, wirksame Akzentfarben und Customizer-Schriften | +| `page--designsystem.json` / `.html` | Post-Zeile, alle Meta-Einträge roh, `post_content` byte-genau | +| `manifest.json` | Divi-Version, Dateiliste, Post-ID | + +## Prüfung im Frontend + +Die Vorschau des Entwurfs (angemeldet, `?page_id=3182&preview=true`) enthielt am 2026-09-28: + +- Klassen `preset--module--divi-button--3gbmxlurey`, `preset--module--divi-text--hsiy1sjxt7` mit `preset--group--divi-text--divi-font-body--nested--ja6o54jtlc`, `preset--group--divi-text--divi-font-body--ja6o54jtlc` (explizites Gruppen-Preset), `preset--module--divi-blurb--vdcl6msnuo` mit `preset--group--divi-blurb--divi-spacing--apzj7e17ok` und `preset--module--divi-divider--default` (Trenner ohne `modulePreset` → Standard-Preset); +- CSS-Regeln zu diesen Klassen mit `var(--gcid-…)`/`var(--gvid-…)`; +- `:root` mit `--gvid-f6yq6mayfy: 8px`, `--gvid-5dec72fwsc: clamp(…)`, `--gvid-d4a5bsz2gt: 'Merriweather'` und `--gcid-989stx15zc: hsl(from var(--gcid-q9swzy97df) calc(h + 0) calc(s + 0) calc(l + 30))`; +- aufgelöste Inhalte: Slogan-Text, `href="https://wordpress.ddev.site:8443/kontakt/"`, Bild-URL aus der Bild-Variable. diff --git a/tests/Fixtures/divi/stage4/et-divi-subset.json b/tests/Fixtures/divi/stage4/et-divi-subset.json new file mode 100644 index 0000000..dc80be1 --- /dev/null +++ b/tests/Fixtures/divi/stage4/et-divi-subset.json @@ -0,0 +1,58 @@ +{ + "option_name": "et_divi", + "present": {}, + "absent": [ + "heading_font", + "body_font", + "heading_font_weight", + "body_font_weight", + "primary_nav_font", + "secondary_nav_font", + "all_buttons_font", + "body_font_size", + "tablet_body_font_size", + "phone_body_font_size", + "body_header_size", + "tablet_header_font_size", + "phone_header_font_size", + "body_font_height", + "body_header_height", + "body_header_spacing", + "body_header_style", + "content_width", + "gutter_width", + "section_padding", + "row_padding", + "accent_color", + "secondary_accent_color", + "header_color", + "font_color", + "link_color", + "divi_logo" + ], + "effective": { + "accent_colors": { + "primary": "#2ea3f2", + "secondary": "#2ea3f2", + "heading": "#666666", + "body": "#666666", + "link": "#2ea3f2" + }, + "customizer_fonts": { + "--et_global_heading_font": { + "id": "--et_global_heading_font", + "label": "Überschrift", + "value": "Open Sans", + "status": "active", + "order": 1 + }, + "--et_global_body_font": { + "id": "--et_global_body_font", + "label": "Fließtext", + "value": "Open Sans", + "status": "active", + "order": 2 + } + } + } +} diff --git a/tests/Fixtures/divi/stage4/global-colors.json b/tests/Fixtures/divi/stage4/global-colors.json new file mode 100644 index 0000000..f2b2ed0 --- /dev/null +++ b/tests/Fixtures/divi/stage4/global-colors.json @@ -0,0 +1,116 @@ +{ + "stored": { + "option_name": "et_divi", + "key": "et_global_data", + "value": { + "global_colors": { + "gcid-q9swzy97df": { + "color": "#5b3a29", + "folder": "", + "label": "[Fixture] Holz dunkel", + "lastUpdated": "2026-09-27T22:21:39.000Z", + "status": "active", + "usedInPosts": [] + }, + "gcid-989stx15zc": { + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{\"lightness\":30}}})$", + "folder": "", + "label": "[Fixture] Holz hell", + "lastUpdated": "2026-09-27T22:21:39.000Z", + "status": "active", + "usedInPosts": [] + }, + "gcid-pgtgy2j1i7": { + "color": "#7d9b76", + "folder": "", + "label": "[Fixture] Salbeigrün", + "lastUpdated": "2026-09-27T22:21:39.000Z", + "status": "active", + "usedInPosts": [] + }, + "gcid-gg4lef6xns": { + "color": "#efe6d8", + "folder": "", + "label": "[Fixture] Sandbeige", + "lastUpdated": "2026-09-27T22:21:39.000Z", + "status": "active", + "usedInPosts": [] + } + } + } + }, + "get_global_colors": { + "gcid-primary-color": { + "color": "#2ea3f2", + "folder": "customizer", + "label": "Primärfarbe", + "lastUpdated": "2026-09-28T00:22:52.000Z", + "status": "active", + "usedInPosts": [] + }, + "gcid-secondary-color": { + "color": "#2ea3f2", + "folder": "customizer", + "label": "Sekundärfarbe", + "lastUpdated": "2026-09-28T00:22:52.000Z", + "status": "active", + "usedInPosts": [] + }, + "gcid-heading-color": { + "color": "#666666", + "folder": "customizer", + "label": "Überschriftentextfarbe", + "lastUpdated": "2026-09-28T00:22:52.000Z", + "status": "active", + "usedInPosts": [] + }, + "gcid-body-color": { + "color": "#666666", + "folder": "customizer", + "label": "Fließtextfarbe", + "lastUpdated": "2026-09-28T00:22:52.000Z", + "status": "active", + "usedInPosts": [] + }, + "gcid-link-color": { + "color": "#2ea3f2", + "folder": "customizer", + "label": "Linkfarbe", + "lastUpdated": "2026-09-28T00:22:52.000Z", + "status": "active", + "usedInPosts": [] + }, + "gcid-q9swzy97df": { + "color": "#5b3a29", + "folder": "", + "label": "[Fixture] Holz dunkel", + "lastUpdated": "2026-09-27T22:21:39.000Z", + "status": "active", + "usedInPosts": [] + }, + "gcid-989stx15zc": { + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{\"lightness\":30}}})$", + "folder": "", + "label": "[Fixture] Holz hell", + "lastUpdated": "2026-09-27T22:21:39.000Z", + "status": "active", + "usedInPosts": [] + }, + "gcid-pgtgy2j1i7": { + "color": "#7d9b76", + "folder": "", + "label": "[Fixture] Salbeigrün", + "lastUpdated": "2026-09-27T22:21:39.000Z", + "status": "active", + "usedInPosts": [] + }, + "gcid-gg4lef6xns": { + "color": "#efe6d8", + "folder": "", + "label": "[Fixture] Sandbeige", + "lastUpdated": "2026-09-27T22:21:39.000Z", + "status": "active", + "usedInPosts": [] + } + } +} diff --git a/tests/Fixtures/divi/stage4/global-variables.json b/tests/Fixtures/divi/stage4/global-variables.json new file mode 100644 index 0000000..4735c7e --- /dev/null +++ b/tests/Fixtures/divi/stage4/global-variables.json @@ -0,0 +1,210 @@ +{ + "stored": { + "numbers": { + "gvid-f6yq6mayfy": { + "id": "gvid-f6yq6mayfy", + "label": "[Fixture] Eckenradius", + "value": "8px", + "order": "1", + "status": "active", + "lastUpdated": "2026-09-27T22:21:39.000Z", + "variableType": "numbers" + }, + "gvid-5dec72fwsc": { + "id": "gvid-5dec72fwsc", + "label": "[Fixture] Abstand Karte", + "value": "clamp(1.5rem, 4vw, 2.5rem)", + "order": "2", + "status": "active", + "lastUpdated": "2026-09-27T22:21:39.000Z", + "variableType": "numbers" + } + }, + "strings": { + "gvid-7uckqqhmrh": { + "id": "gvid-7uckqqhmrh", + "label": "[Fixture] Slogan", + "value": "Handwerk aus Meisterhand – seit 1987", + "order": "1", + "status": "active", + "lastUpdated": "2026-09-27T22:21:39.000Z", + "variableType": "strings" + } + }, + "images": { + "gvid-qmc3cy1alq": { + "id": "gvid-qmc3cy1alq", + "label": "[Fixture] Werkstattbild", + "value": "https://wordpress.ddev.site:8443/wp-content/uploads/2026/09/600.jpg", + "order": "1", + "status": "active", + "lastUpdated": "2026-09-27T22:21:39.000Z", + "variableType": "images" + } + }, + "links": { + "gvid-nt8f6iz2qh": { + "id": "gvid-nt8f6iz2qh", + "label": "[Fixture] Kontaktseite", + "value": "https://wordpress.ddev.site:8443/kontakt/", + "order": "1", + "status": "active", + "lastUpdated": "2026-09-27T22:21:39.000Z", + "variableType": "links" + } + }, + "fonts": { + "gvid-d4a5bsz2gt": { + "id": "gvid-d4a5bsz2gt", + "label": "[Fixture] Akzentschrift", + "value": "Merriweather", + "order": "1", + "status": "active", + "lastUpdated": "2026-09-27T22:21:39.000Z", + "variableType": "fonts" + } + } + }, + "stored_raw": { + "option_name": "et_divi_global_variables", + "autoload": "off", + "option_value": "a:5:{s:7:\"numbers\";a:2:{s:15:\"gvid-f6yq6mayfy\";a:7:{s:2:\"id\";s:15:\"gvid-f6yq6mayfy\";s:5:\"label\";s:21:\"[Fixture] Eckenradius\";s:5:\"value\";s:3:\"8px\";s:5:\"order\";s:1:\"1\";s:6:\"status\";s:6:\"active\";s:11:\"lastUpdated\";s:24:\"2026-09-27T22:21:39.000Z\";s:12:\"variableType\";s:7:\"numbers\";}s:15:\"gvid-5dec72fwsc\";a:7:{s:2:\"id\";s:15:\"gvid-5dec72fwsc\";s:5:\"label\";s:23:\"[Fixture] Abstand Karte\";s:5:\"value\";s:26:\"clamp(1.5rem, 4vw, 2.5rem)\";s:5:\"order\";s:1:\"2\";s:6:\"status\";s:6:\"active\";s:11:\"lastUpdated\";s:24:\"2026-09-27T22:21:39.000Z\";s:12:\"variableType\";s:7:\"numbers\";}}s:7:\"strings\";a:1:{s:15:\"gvid-7uckqqhmrh\";a:7:{s:2:\"id\";s:15:\"gvid-7uckqqhmrh\";s:5:\"label\";s:16:\"[Fixture] Slogan\";s:5:\"value\";s:38:\"Handwerk aus Meisterhand – seit 1987\";s:5:\"order\";s:1:\"1\";s:6:\"status\";s:6:\"active\";s:11:\"lastUpdated\";s:24:\"2026-09-27T22:21:39.000Z\";s:12:\"variableType\";s:7:\"strings\";}}s:6:\"images\";a:1:{s:15:\"gvid-qmc3cy1alq\";a:7:{s:2:\"id\";s:15:\"gvid-qmc3cy1alq\";s:5:\"label\";s:23:\"[Fixture] Werkstattbild\";s:5:\"value\";s:67:\"https://wordpress.ddev.site:8443/wp-content/uploads/2026/09/600.jpg\";s:5:\"order\";s:1:\"1\";s:6:\"status\";s:6:\"active\";s:11:\"lastUpdated\";s:24:\"2026-09-27T22:21:39.000Z\";s:12:\"variableType\";s:6:\"images\";}}s:5:\"links\";a:1:{s:15:\"gvid-nt8f6iz2qh\";a:7:{s:2:\"id\";s:15:\"gvid-nt8f6iz2qh\";s:5:\"label\";s:22:\"[Fixture] Kontaktseite\";s:5:\"value\";s:41:\"https://wordpress.ddev.site:8443/kontakt/\";s:5:\"order\";s:1:\"1\";s:6:\"status\";s:6:\"active\";s:11:\"lastUpdated\";s:24:\"2026-09-27T22:21:39.000Z\";s:12:\"variableType\";s:5:\"links\";}}s:5:\"fonts\";a:1:{s:15:\"gvid-d4a5bsz2gt\";a:7:{s:2:\"id\";s:15:\"gvid-d4a5bsz2gt\";s:5:\"label\";s:23:\"[Fixture] Akzentschrift\";s:5:\"value\";s:12:\"Merriweather\";s:5:\"order\";s:1:\"1\";s:6:\"status\";s:6:\"active\";s:11:\"lastUpdated\";s:24:\"2026-09-27T22:21:39.000Z\";s:12:\"variableType\";s:5:\"fonts\";}}}" + }, + "get_global_variables": { + "numbers": { + "gvid-f6yq6mayfy": { + "id": "gvid-f6yq6mayfy", + "label": "[Fixture] Eckenradius", + "value": "8px", + "order": "1", + "status": "active", + "lastUpdated": "2026-09-27T22:21:39.000Z", + "variableType": "numbers", + "allowedActions": { + "editLabel": true, + "editValue": true, + "reorder": true, + "remove": true + } + }, + "gvid-5dec72fwsc": { + "id": "gvid-5dec72fwsc", + "label": "[Fixture] Abstand Karte", + "value": "clamp(1.5rem, 4vw, 2.5rem)", + "order": "2", + "status": "active", + "lastUpdated": "2026-09-27T22:21:39.000Z", + "variableType": "numbers", + "allowedActions": { + "editLabel": true, + "editValue": true, + "reorder": true, + "remove": true + } + } + }, + "strings": { + "gvid-7uckqqhmrh": { + "id": "gvid-7uckqqhmrh", + "label": "[Fixture] Slogan", + "value": "Handwerk aus Meisterhand – seit 1987", + "order": "1", + "status": "active", + "lastUpdated": "2026-09-27T22:21:39.000Z", + "variableType": "strings", + "allowedActions": { + "editLabel": true, + "editValue": true, + "reorder": true, + "remove": true + } + } + }, + "images": { + "gvid-qmc3cy1alq": { + "id": "gvid-qmc3cy1alq", + "label": "[Fixture] Werkstattbild", + "value": "https://wordpress.ddev.site:8443/wp-content/uploads/2026/09/600.jpg", + "order": "1", + "status": "active", + "lastUpdated": "2026-09-27T22:21:39.000Z", + "variableType": "images", + "allowedActions": { + "editLabel": true, + "editValue": true, + "reorder": true, + "remove": true + } + } + }, + "links": { + "gvid-nt8f6iz2qh": { + "id": "gvid-nt8f6iz2qh", + "label": "[Fixture] Kontaktseite", + "value": "https://wordpress.ddev.site:8443/kontakt/", + "order": "1", + "status": "active", + "lastUpdated": "2026-09-27T22:21:39.000Z", + "variableType": "links", + "allowedActions": { + "editLabel": true, + "editValue": true, + "reorder": true, + "remove": true + } + } + }, + "colors": {}, + "fonts": { + "--et_global_heading_font": { + "id": "--et_global_heading_font", + "label": "Überschrift", + "value": "Open Sans", + "status": "active", + "order": 1, + "allowedActions": { + "editLabel": true, + "editValue": true, + "reorder": true, + "remove": true + } + }, + "--et_global_body_font": { + "id": "--et_global_body_font", + "label": "Fließtext", + "value": "Open Sans", + "status": "active", + "order": 2, + "allowedActions": { + "editLabel": true, + "editValue": true, + "reorder": true, + "remove": true + } + }, + "gvid-d4a5bsz2gt": { + "id": "gvid-d4a5bsz2gt", + "label": "[Fixture] Akzentschrift", + "value": "Merriweather", + "order": "1", + "status": "active", + "lastUpdated": "2026-09-27T22:21:39.000Z", + "variableType": "fonts", + "allowedActions": { + "editLabel": true, + "editValue": true, + "reorder": true, + "remove": true + } + } + } + }, + "get_global_variables_php_types": { + "numbers": "stdClass", + "strings": "stdClass", + "images": "stdClass", + "links": "stdClass", + "colors": "stdClass", + "fonts": "stdClass" + } +} diff --git a/tests/Fixtures/divi/stage4/manifest.json b/tests/Fixtures/divi/stage4/manifest.json new file mode 100644 index 0000000..1447915 --- /dev/null +++ b/tests/Fixtures/divi/stage4/manifest.json @@ -0,0 +1,16 @@ +{ + "divi_version": "5.2.1", + "builder_version": "5.2.1", + "files": [ + "global-colors.json", + "global-variables.json", + "presets.json", + "presets-history.json", + "et-divi-subset.json", + "page--designsystem.json", + "page--designsystem.html" + ], + "posts": { + "page--designsystem": 3182 + } +} diff --git a/tests/Fixtures/divi/stage4/page--designsystem.html b/tests/Fixtures/divi/stage4/page--designsystem.html new file mode 100644 index 0000000..0a6c3ef --- /dev/null +++ b/tests/Fixtures/divi/stage4/page--designsystem.html @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Fixtures/divi/stage4/page--designsystem.json b/tests/Fixtures/divi/stage4/page--designsystem.json new file mode 100644 index 0000000..099265a --- /dev/null +++ b/tests/Fixtures/divi/stage4/page--designsystem.json @@ -0,0 +1,36 @@ +{ + "post": { + "ID": 3182, + "post_type": "page", + "post_status": "draft", + "post_title": "[Fixture] Designsystem", + "post_name": "", + "post_date_gmt": "0000-00-00 00:00:00", + "post_modified_gmt": "2026-09-27 22:21:39", + "content_file": "page--designsystem.html", + "content_sha256": "758485d1c95447ecae444e823a62eee1ce4d5e8525d13f607c0cdf4ab1ac207f" + }, + "meta": { + "_et_pb_use_builder": [ + "on" + ], + "_et_pb_show_page_creation": [ + "off" + ], + "_et_pb_first_image": [ + "" + ], + "_et_pb_truncate_post": [ + "" + ], + "_et_pb_truncate_post_date": [ + "" + ], + "_et_pb_use_divi_5": [ + "on" + ], + "et_enqueued_post_fonts": [ + "a:3:{s:6:\"family\";a:2:{i:0;s:111:\"Merriweather:300,regular,500,600,700,800,900,300italic,italic,500italic,600italic,700italic,800italic,900italic\";i:1;s:94:\"Open+Sans:300,regular,500,600,700,800,300italic,italic,500italic,600italic,700italic,800italic\";}s:6:\"subset\";a:2:{i:0;s:5:\"latin\";i:1;s:9:\"latin-ext\";}s:9:\"cache_key\";s:74:\"{\"gph\":-1,\"divi\":\"5.2.1\",\"wp\":\"7.1.2\",\"enable_all_character_sets\":\"false\"}\";}" + ] + } +} diff --git a/tests/Fixtures/divi/stage4/presets-history.json b/tests/Fixtures/divi/stage4/presets-history.json new file mode 100644 index 0000000..e5e997d --- /dev/null +++ b/tests/Fixtures/divi/stage4/presets-history.json @@ -0,0 +1,17 @@ +{ + "rows": [ + { + "option_name": "et_divi_builder_presets_history_meta", + "autoload": "off", + "value": { + "index": -1, + "size": 0 + } + } + ], + "et_divi_flags": { + "presets_storage_migrated_flag": true, + "builder_global_presets_history_migrated": true, + "custom_defaults_migrated_flag": true + } +} diff --git a/tests/Fixtures/divi/stage4/presets.json b/tests/Fixtures/divi/stage4/presets.json new file mode 100644 index 0000000..d81ac57 --- /dev/null +++ b/tests/Fixtures/divi/stage4/presets.json @@ -0,0 +1,909 @@ +{ + "option_name": "et_divi_builder_global_presets_d5", + "stored": { + "module": { + "divi/button": { + "default": "", + "items": { + "3gbmxlurey": { + "type": "module", + "id": "3gbmxlurey", + "name": "[Fixture] Button Primär", + "moduleName": "divi/button", + "version": "5.2.1", + "created": 1790547699462, + "updated": 1790547699462, + "attrs": { + "button": { + "decoration": { + "background": { + "desktop": { + "value": { + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$" + } + } + }, + "border": { + "desktop": { + "value": { + "radius": { + "sync": "on", + "topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$" + } + } + } + }, + "font": { + "font": { + "desktop": { + "value": { + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-gg4lef6xns\",\"settings\":{}}})$", + "weight": "600" + } + } + } + } + } + } + }, + "styleAttrs": { + "button": { + "decoration": { + "background": { + "desktop": { + "value": { + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$" + } + } + }, + "border": { + "desktop": { + "value": { + "radius": { + "sync": "on", + "topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$" + } + } + } + }, + "font": { + "font": { + "desktop": { + "value": { + "weight": "600", + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-gg4lef6xns\",\"settings\":{}}})$" + } + } + } + } + } + } + } + } + } + }, + "divi/text": { + "default": "", + "items": { + "hsiy1sjxt7": { + "type": "module", + "id": "hsiy1sjxt7", + "name": "[Fixture] Text Hinweis", + "moduleName": "divi/text", + "version": "5.2.1", + "created": 1790547699462, + "updated": 1790547699462, + "attrs": { + "module": { + "decoration": { + "background": { + "desktop": { + "value": { + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-gg4lef6xns\",\"settings\":{}}})$" + } + } + }, + "border": { + "desktop": { + "value": { + "radius": { + "sync": "on", + "topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$" + } + } + } + }, + "spacing": { + "desktop": { + "value": { + "padding": { + "top": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "right": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "bottom": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "left": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "syncVertical": "on", + "syncHorizontal": "on" + } + } + } + } + } + } + }, + "styleAttrs": { + "module": { + "decoration": { + "background": { + "desktop": { + "value": { + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-gg4lef6xns\",\"settings\":{}}})$" + } + } + }, + "border": { + "desktop": { + "value": { + "radius": { + "sync": "on", + "topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$" + } + } + } + }, + "spacing": { + "desktop": { + "value": { + "padding": { + "top": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "right": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "bottom": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "left": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "syncVertical": "on", + "syncHorizontal": "on" + } + } + } + } + } + } + }, + "groupPresets": { + "content.decoration.bodyFont": { + "presetId": [ + "ja6o54jtlc" + ], + "groupName": "divi/font-body" + } + } + } + } + }, + "divi/blurb": { + "default": "", + "items": { + "vdcl6msnuo": { + "type": "module", + "id": "vdcl6msnuo", + "name": "[Fixture] Blurb Leistung", + "moduleName": "divi/blurb", + "version": "5.2.1", + "created": 1790547699462, + "updated": 1790547699462, + "attrs": { + "title": { + "decoration": { + "font": { + "font": { + "desktop": { + "value": { + "family": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-d4a5bsz2gt\",\"settings\":{}}})$", + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$", + "weight": "700" + } + } + } + } + } + }, + "module": { + "decoration": { + "border": { + "desktop": { + "value": { + "radius": { + "sync": "on", + "topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$" + }, + "styles": { + "all": { + "width": "2px", + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-pgtgy2j1i7\",\"settings\":{}}})$", + "style": "solid" + } + } + } + } + } + } + } + }, + "styleAttrs": { + "module": { + "decoration": { + "border": { + "desktop": { + "value": { + "radius": { + "sync": "on", + "topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$" + }, + "styles": { + "all": { + "width": "2px", + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-pgtgy2j1i7\",\"settings\":{}}})$", + "style": "solid" + } + } + } + } + } + } + }, + "title": { + "decoration": { + "font": { + "font": { + "desktop": { + "value": { + "family": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-d4a5bsz2gt\",\"settings\":{}}})$", + "weight": "700", + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$" + } + } + } + } + } + } + } + } + } + }, + "divi/divider": { + "default": "g7kvfq5uh8", + "items": { + "g7kvfq5uh8": { + "type": "module", + "id": "g7kvfq5uh8", + "name": "[Fixture] Trenner Standard", + "moduleName": "divi/divider", + "version": "5.2.1", + "created": 1790547699462, + "updated": 1790547699462, + "attrs": { + "divider": { + "advanced": { + "line": { + "desktop": { + "value": { + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-989stx15zc\",\"settings\":{}}})$", + "weight": "3px" + } + } + } + } + } + }, + "styleAttrs": { + "divider": { + "advanced": { + "line": { + "desktop": { + "value": { + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-989stx15zc\",\"settings\":{}}})$", + "weight": "3px" + } + } + } + } + } + } + } + } + } + }, + "group": { + "divi/font-body": { + "default": "", + "items": { + "ja6o54jtlc": { + "type": "group", + "id": "ja6o54jtlc", + "name": "[Fixture] Fließtext", + "groupName": "divi/font-body", + "groupId": "content.decoration.bodyFont", + "moduleName": "divi/text", + "version": "5.2.1", + "created": 1790547699462, + "updated": 1790547699462, + "attrs": { + "content": { + "decoration": { + "bodyFont": { + "body": { + "font": { + "desktop": { + "value": { + "family": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-d4a5bsz2gt\",\"settings\":{}}})$", + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$", + "size": "17px", + "lineHeight": "1.8em" + } + } + } + } + } + } + } + }, + "styleAttrs": { + "content": { + "decoration": { + "bodyFont": { + "body": { + "font": { + "desktop": { + "value": { + "family": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-d4a5bsz2gt\",\"settings\":{}}})$", + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$", + "size": "17px", + "lineHeight": "1.8em" + } + } + } + } + } + } + } + } + } + } + }, + "divi/spacing": { + "default": "", + "items": { + "apzj7e17ok": { + "type": "group", + "id": "apzj7e17ok", + "name": "[Fixture] Abstand Karte", + "groupName": "divi/spacing", + "groupId": "module.decoration.spacing", + "moduleName": "divi/blurb", + "version": "5.2.1", + "created": 1790547699462, + "updated": 1790547699462, + "attrs": { + "module": { + "decoration": { + "spacing": { + "desktop": { + "value": { + "padding": { + "top": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "right": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "bottom": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "left": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "syncVertical": "on", + "syncHorizontal": "on" + }, + "margin": { + "bottom": "24px" + } + } + } + } + } + } + }, + "styleAttrs": { + "module": { + "decoration": { + "spacing": { + "desktop": { + "value": { + "margin": { + "bottom": "24px" + }, + "padding": { + "top": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "right": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "bottom": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "left": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "syncVertical": "on", + "syncHorizontal": "on" + } + } + } + } + } + } + } + } + } + } + } + }, + "get_data": { + "module": { + "divi/button": { + "default": "", + "items": { + "3gbmxlurey": { + "type": "module", + "id": "3gbmxlurey", + "name": "[Fixture] Button Primär", + "moduleName": "divi/button", + "version": "5.2.1", + "created": 1790547699462, + "updated": 1790547699462, + "attrs": { + "button": { + "decoration": { + "background": { + "desktop": { + "value": { + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$" + } + } + }, + "border": { + "desktop": { + "value": { + "radius": { + "sync": "on", + "topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$" + } + } + } + }, + "font": { + "font": { + "desktop": { + "value": { + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-gg4lef6xns\",\"settings\":{}}})$", + "weight": "600" + } + } + } + } + } + } + }, + "styleAttrs": { + "button": { + "decoration": { + "background": { + "desktop": { + "value": { + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$" + } + } + }, + "border": { + "desktop": { + "value": { + "radius": { + "sync": "on", + "topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$" + } + } + } + }, + "font": { + "font": { + "desktop": { + "value": { + "weight": "600", + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-gg4lef6xns\",\"settings\":{}}})$" + } + } + } + } + } + } + } + } + } + }, + "divi/text": { + "default": "", + "items": { + "hsiy1sjxt7": { + "type": "module", + "id": "hsiy1sjxt7", + "name": "[Fixture] Text Hinweis", + "moduleName": "divi/text", + "version": "5.2.1", + "created": 1790547699462, + "updated": 1790547699462, + "attrs": { + "module": { + "decoration": { + "background": { + "desktop": { + "value": { + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-gg4lef6xns\",\"settings\":{}}})$" + } + } + }, + "border": { + "desktop": { + "value": { + "radius": { + "sync": "on", + "topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$" + } + } + } + }, + "spacing": { + "desktop": { + "value": { + "padding": { + "top": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "right": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "bottom": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "left": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "syncVertical": "on", + "syncHorizontal": "on" + } + } + } + } + } + } + }, + "styleAttrs": { + "module": { + "decoration": { + "background": { + "desktop": { + "value": { + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-gg4lef6xns\",\"settings\":{}}})$" + } + } + }, + "border": { + "desktop": { + "value": { + "radius": { + "sync": "on", + "topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$" + } + } + } + }, + "spacing": { + "desktop": { + "value": { + "padding": { + "top": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "right": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "bottom": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "left": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "syncVertical": "on", + "syncHorizontal": "on" + } + } + } + } + } + } + }, + "groupPresets": { + "content.decoration.bodyFont": { + "presetId": [ + "ja6o54jtlc" + ], + "groupName": "divi/font-body" + } + } + } + } + }, + "divi/blurb": { + "default": "", + "items": { + "vdcl6msnuo": { + "type": "module", + "id": "vdcl6msnuo", + "name": "[Fixture] Blurb Leistung", + "moduleName": "divi/blurb", + "version": "5.2.1", + "created": 1790547699462, + "updated": 1790547699462, + "attrs": { + "title": { + "decoration": { + "font": { + "font": { + "desktop": { + "value": { + "family": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-d4a5bsz2gt\",\"settings\":{}}})$", + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$", + "weight": "700" + } + } + } + } + } + }, + "module": { + "decoration": { + "border": { + "desktop": { + "value": { + "radius": { + "sync": "on", + "topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$" + }, + "styles": { + "all": { + "width": "2px", + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-pgtgy2j1i7\",\"settings\":{}}})$", + "style": "solid" + } + } + } + } + } + } + } + }, + "styleAttrs": { + "module": { + "decoration": { + "border": { + "desktop": { + "value": { + "radius": { + "sync": "on", + "topLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "topRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomLeft": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$", + "bottomRight": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-f6yq6mayfy\",\"settings\":{}}})$" + }, + "styles": { + "all": { + "width": "2px", + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-pgtgy2j1i7\",\"settings\":{}}})$", + "style": "solid" + } + } + } + } + } + } + }, + "title": { + "decoration": { + "font": { + "font": { + "desktop": { + "value": { + "family": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-d4a5bsz2gt\",\"settings\":{}}})$", + "weight": "700", + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$" + } + } + } + } + } + } + } + } + } + }, + "divi/divider": { + "default": "g7kvfq5uh8", + "items": { + "g7kvfq5uh8": { + "type": "module", + "id": "g7kvfq5uh8", + "name": "[Fixture] Trenner Standard", + "moduleName": "divi/divider", + "version": "5.2.1", + "created": 1790547699462, + "updated": 1790547699462, + "attrs": { + "divider": { + "advanced": { + "line": { + "desktop": { + "value": { + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-989stx15zc\",\"settings\":{}}})$", + "weight": "3px" + } + } + } + } + } + }, + "styleAttrs": { + "divider": { + "advanced": { + "line": { + "desktop": { + "value": { + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-989stx15zc\",\"settings\":{}}})$", + "weight": "3px" + } + } + } + } + } + } + } + } + } + }, + "group": { + "divi/font-body": { + "default": "", + "items": { + "ja6o54jtlc": { + "type": "group", + "id": "ja6o54jtlc", + "name": "[Fixture] Fließtext", + "groupName": "divi/font-body", + "groupId": "content.decoration.bodyFont", + "moduleName": "divi/text", + "version": "5.2.1", + "created": 1790547699462, + "updated": 1790547699462, + "attrs": { + "content": { + "decoration": { + "bodyFont": { + "body": { + "font": { + "desktop": { + "value": { + "family": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-d4a5bsz2gt\",\"settings\":{}}})$", + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$", + "size": "17px", + "lineHeight": "1.8em" + } + } + } + } + } + } + } + }, + "styleAttrs": { + "content": { + "decoration": { + "bodyFont": { + "body": { + "font": { + "desktop": { + "value": { + "family": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-d4a5bsz2gt\",\"settings\":{}}})$", + "color": "$variable({\"type\":\"color\",\"value\":{\"name\":\"gcid-q9swzy97df\",\"settings\":{}}})$", + "size": "17px", + "lineHeight": "1.8em" + } + } + } + } + } + } + } + } + } + } + }, + "divi/spacing": { + "default": "", + "items": { + "apzj7e17ok": { + "type": "group", + "id": "apzj7e17ok", + "name": "[Fixture] Abstand Karte", + "groupName": "divi/spacing", + "groupId": "module.decoration.spacing", + "moduleName": "divi/blurb", + "version": "5.2.1", + "created": 1790547699462, + "updated": 1790547699462, + "attrs": { + "module": { + "decoration": { + "spacing": { + "desktop": { + "value": { + "padding": { + "top": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "right": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "bottom": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "left": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "syncVertical": "on", + "syncHorizontal": "on" + }, + "margin": { + "bottom": "24px" + } + } + } + } + } + } + }, + "styleAttrs": { + "module": { + "decoration": { + "spacing": { + "desktop": { + "value": { + "margin": { + "bottom": "24px" + }, + "padding": { + "top": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "right": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "bottom": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "left": "$variable({\"type\":\"content\",\"value\":{\"name\":\"gvid-5dec72fwsc\",\"settings\":{}}})$", + "syncVertical": "on", + "syncHorizontal": "on" + } + } + } + } + } + } + } + } + } + } + } + }, + "legacy_d4_option": { + "option_name": "et_divi_builder_global_presets_ng", + "autoload": "off", + "option_value": "O:8:\"stdClass\":0:{}" + }, + "is_legacy_presets_imported": "" +} diff --git a/tests/Integration/divi-export-stage4-fixtures.php b/tests/Integration/divi-export-stage4-fixtures.php new file mode 100644 index 0000000..03085be --- /dev/null +++ b/tests/Integration/divi-export-stage4-fixtures.php @@ -0,0 +1,191 @@ + wp_json_encode( + $data, + JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE +) . "\n"; + +/** The option row exactly as stored (serialized string, autoload flag) – shows the PHP types Divi writes. */ +$lit_raw_option = static function (string $name): ?array { + global $wpdb; + $row = $wpdb->get_row($wpdb->prepare( + "SELECT option_value, autoload FROM {$wpdb->options} WHERE option_name = %s", + $name + )); + + return $row === null ? null : ['option_name' => $name, 'autoload' => $row->autoload, 'option_value' => $row->option_value]; +}; + +$lit_pages = get_posts([ + 'post_type' => 'page', + 'post_status' => ['draft', 'publish', 'private', 'pending'], + 'title' => $lit_page_title, + 'numberposts' => 1, + 'orderby' => 'ID', + 'order' => 'ASC', +]); +if ($lit_pages === []) { + WP_CLI::error('Page "' . $lit_page_title . '" not found – run tests/Integration/divi-seed-stage4-design.php first.'); +} +$lit_page = $lit_pages[0]; + +if (! is_dir($lit_dir) && ! wp_mkdir_p($lit_dir)) { + WP_CLI::error('Cannot create ' . $lit_dir); +} + +// Global colors: et_divi['et_global_data'] as stored, plus GlobalData::get_global_colors() (customizer colors merged +// in front; their lastUpdated is generated on every read). +$lit_et_divi = get_option('et_divi', []); +$lit_write($lit_dir . '/global-colors.json', $lit_json([ + 'stored' => [ + 'option_name' => 'et_divi', + 'key' => 'et_global_data', + 'value' => maybe_unserialize($lit_et_divi['et_global_data'] ?? null), + ], + 'get_global_colors' => GlobalData::get_global_colors(), +])); + +// Global variables: product option et_divi_global_variables (arrays), getter result (every type is a stdClass – +// an empty type therefore encodes as {} here, not []). +$lit_write($lit_dir . '/global-variables.json', $lit_json([ + 'stored' => get_option('et_divi_global_variables', null), + 'stored_raw' => $lit_raw_option('et_divi_global_variables'), + 'get_global_variables' => GlobalData::get_global_variables(), + 'get_global_variables_php_types' => array_map( + static fn ($items) => get_debug_type($items), + GlobalData::get_global_variables() + ), +])); + +// Presets: product option et_divi_builder_global_presets_d5, plus the D4 legacy option and import flag. +$lit_write($lit_dir . '/presets.json', $lit_json([ + 'option_name' => 'et_divi_' . GlobalPreset::option_name(), + 'stored' => get_option('et_divi_' . GlobalPreset::option_name(), null), + 'get_data' => GlobalPreset::get_data(), + 'legacy_d4_option' => $lit_raw_option('et_divi_builder_global_presets_ng'), + 'is_legacy_presets_imported' => GlobalPreset::is_legacy_presets_imported(), +])); + +// Preset history (Divi 4 storage; Divi 5 preset saves do not write it). +global $wpdb; +$lit_history_rows = $wpdb->get_results( + "SELECT option_name, option_value, autoload FROM {$wpdb->options} + WHERE option_name LIKE 'et\\_divi\\_builder\\_presets\\_history%' + OR option_name LIKE 'et\\_divi\\_builder\\_global\\_presets\\_history%' + OR option_name LIKE 'et\\_divi\\_builder\\_custom\\_defaults\\_history%' + ORDER BY option_name" +); +$lit_write($lit_dir . '/presets-history.json', $lit_json([ + 'rows' => array_map(static fn ($row) => [ + 'option_name' => $row->option_name, + 'autoload' => $row->autoload, + 'value' => maybe_unserialize($row->option_value), + ], $lit_history_rows), + 'et_divi_flags' => array_intersect_key((array) $lit_et_divi, array_flip([ + 'builder_global_presets_history_migrated', + 'presets_storage_migrated_flag', + 'custom_defaults_migrated_flag', + ])), +])); + +// Theme options relevant to spec §7 (whitelist): present keys with their stored value, absent keys listed. +$lit_keys = [ + 'heading_font', 'body_font', 'heading_font_weight', 'body_font_weight', 'primary_nav_font', 'secondary_nav_font', + 'all_buttons_font', 'body_font_size', 'tablet_body_font_size', 'phone_body_font_size', 'body_header_size', + 'tablet_header_font_size', 'phone_header_font_size', 'body_font_height', 'body_header_height', + 'body_header_spacing', 'body_header_style', 'content_width', 'gutter_width', 'section_padding', 'row_padding', + 'accent_color', 'secondary_accent_color', 'header_color', 'font_color', 'link_color', 'divi_logo', +]; +foreach (array_keys((array) $lit_et_divi) as $key) { + if (str_starts_with($key, 'all_buttons_') || str_starts_with($key, 'divi_seo_')) { + $lit_keys[] = $key; + } +} +$lit_keys = array_values(array_unique($lit_keys)); +$lit_present = []; +$lit_absent = []; +foreach ($lit_keys as $key) { + if (array_key_exists($key, (array) $lit_et_divi)) { + $lit_present[$key] = $lit_et_divi[$key]; + } else { + $lit_absent[] = $key; + } +} +$lit_write($lit_dir . '/et-divi-subset.json', $lit_json([ + 'option_name' => 'et_divi', + 'present' => (object) $lit_present, + 'absent' => $lit_absent, + 'effective' => [ + 'accent_colors' => GlobalData::get_accent_color_values(), + 'customizer_fonts' => GlobalData::get_customizer_fonts(), + ], +])); + +// The page: post row, all meta (raw), post_content byte-exact. +$lit_meta = []; +foreach ($wpdb->get_results($wpdb->prepare( + "SELECT meta_key, meta_value FROM {$wpdb->postmeta} WHERE post_id = %d ORDER BY meta_id", + $lit_page->ID +)) as $row) { + if (! in_array($row->meta_key, ['_edit_lock', '_edit_last'], true)) { + $lit_meta[$row->meta_key][] = $row->meta_value; + } +} +$lit_write($lit_dir . '/page--designsystem.json', $lit_json([ + 'post' => [ + 'ID' => $lit_page->ID, + 'post_type' => $lit_page->post_type, + 'post_status' => $lit_page->post_status, + 'post_title' => $lit_page->post_title, + 'post_name' => $lit_page->post_name, + 'post_date_gmt' => $lit_page->post_date_gmt, + 'post_modified_gmt' => $lit_page->post_modified_gmt, + 'content_file' => 'page--designsystem.html', + 'content_sha256' => hash('sha256', $lit_page->post_content), + ], + 'meta' => (object) $lit_meta, +])); +$lit_write($lit_dir . '/page--designsystem.html', $lit_page->post_content); + +$lit_write($lit_dir . '/manifest.json', $lit_json([ + 'divi_version' => wp_get_theme('Divi')->get('Version'), + 'builder_version' => ET_BUILDER_VERSION, + 'files' => [ + 'global-colors.json', + 'global-variables.json', + 'presets.json', + 'presets-history.json', + 'et-divi-subset.json', + 'page--designsystem.json', + 'page--designsystem.html', + ], + 'posts' => ['page--designsystem' => $lit_page->ID], +])); + +WP_CLI::success('Stage 4 fixtures exported to ' . $lit_dir); diff --git a/tests/Integration/divi-seed-stage4-design.php b/tests/Integration/divi-seed-stage4-design.php new file mode 100644 index 0000000..4ffff58 --- /dev/null +++ b/tests/Integration/divi-seed-stage4-design.php @@ -0,0 +1,558 @@ + 'administrator', 'number' => 1, 'orderby' => 'ID']); + if ($lit_admins === []) { + WP_CLI::error('No administrator found.'); + } + wp_set_current_user($lit_admins[0]->ID); +} +if (! current_user_can('edit_theme_options') || ! et_pb_is_allowed('variables_manager')) { + WP_CLI::error('The current user needs edit_theme_options and Divi\'s role right "variables_manager".'); +} + +/** Random ID like the Visual Builder's generateNanoUuid(): 10 characters of [0-9a-z]. */ +$lit_nano = static function (): string { + $alphabet = '0123456789abcdefghijklmnopqrstuvwxyz'; + $id = ''; + for ($i = 0; $i < 10; $i++) { + $id .= $alphabet[random_int(0, 35)]; + } + + return $id; +}; + +/** + * Dispatches a request to one of Divi's REST routes as the current user; stops on any error. Divi checks a per-route + * nonce in the X-ET-Nonce header (RESTRoute, action "divi/v1--POST"), created here like the builder gets it. + */ +$lit_rest = static function (string $route, array $body): array { + $request = new WP_REST_Request('POST', '/divi/v1' . $route); + $request->set_header('Content-Type', 'application/json'); + $request->set_header('X-ET-Nonce', RESTController::create_nonce('divi/v1', $route, 'POST')); + $request->set_body(wp_json_encode($body)); + $response = rest_do_request($request); + $data = rest_get_server()->response_to_data($response, false); + if ($response->is_error() || $response->get_status() >= 300) { + WP_CLI::error(sprintf('%s → HTTP %d: %s', $route, $response->get_status(), wp_json_encode($data))); + } + + return is_array($data) ? $data : (array) $data; +}; + +/** Dynamic reference to a global color, optionally with HSL/opacity adjustments (relative color). */ +$lit_color_ref = static fn (string $id, array $settings = []): string => '$variable(' . wp_json_encode( + ['type' => 'color', 'value' => ['name' => $id, 'settings' => (object) $settings]] +) . ')$'; + +/** Dynamic reference to a global variable (numbers, strings, images, links, fonts). */ +$lit_var_ref = static fn (string $id): string => '$variable(' . wp_json_encode( + ['type' => 'content', 'value' => ['name' => $id, 'settings' => (object) []]] +) . ')$'; + +$lit_now_iso = gmdate('Y-m-d\TH:i:s.v\Z'); +$lit_now_ms = (int) round(microtime(true) * 1000); + +// --------------------------------------------------------------------------------------------------------------- +// 1. Global colors +// --------------------------------------------------------------------------------------------------------------- +$lit_colors_before = GlobalData::get_global_colors(); +$lit_color_id = static function (string $label) use ($lit_colors_before, $lit_nano): string { + foreach ($lit_colors_before as $id => $color) { + if (is_array($color) && ($color['label'] ?? '') === $label && ! isset(GlobalData::$customizer_colors[$id])) { + return (string) $id; + } + } + + return 'gcid-' . $lit_nano(); +}; + +$lit_c = [ + 'holz_dunkel' => $lit_color_id('[Fixture] Holz dunkel'), + 'holz_hell' => $lit_color_id('[Fixture] Holz hell'), + 'salbei' => $lit_color_id('[Fixture] Salbeigrün'), + 'sand' => $lit_color_id('[Fixture] Sandbeige'), +]; +$lit_color_specs = [ + $lit_c['holz_dunkel'] => ['[Fixture] Holz dunkel', '#5b3a29'], + // Relative color: Holz dunkel, 30 % lighter – stored as a $variable() reference with HSL settings. + $lit_c['holz_hell'] => ['[Fixture] Holz hell', $lit_color_ref($lit_c['holz_dunkel'], ['lightness' => 30])], + $lit_c['salbei'] => ['[Fixture] Salbeigrün', '#7d9b76'], + $lit_c['sand'] => ['[Fixture] Sandbeige', '#efe6d8'], +]; + +$lit_colors_payload = []; +foreach ($lit_color_specs as $id => [$label, $value]) { + $old = $lit_colors_before[$id] ?? null; + $unchanged = is_array($old) && ($old['color'] ?? null) === $value && ($old['status'] ?? '') === 'active'; + $lit_colors_payload[$id] = [ + 'color' => $value, + 'folder' => '', + 'label' => $label, + 'lastUpdated' => $unchanged ? (string) $old['lastUpdated'] : $lit_now_iso, + 'status' => 'active', + 'usedInPosts' => is_array($old['usedInPosts'] ?? null) ? $old['usedInPosts'] : [], + ]; +} +// Only the fixture colors are sent: the endpoint merges (set_global_colors(…, true)), and leaving out the customizer +// colors keeps accent_color & co. in et_divi untouched. +$lit_rest('/global-data/global-colors', ['global_colors' => $lit_colors_payload]); +WP_CLI::log('Colors: ' . implode(', ', array_map( + static fn ($id, $spec) => $spec[0] . ' = ' . $id, + array_keys($lit_color_specs), + $lit_color_specs +))); + +// --------------------------------------------------------------------------------------------------------------- +// 2. Global variables – the endpoint REPLACES the whole option, so the full current set is sent. +// --------------------------------------------------------------------------------------------------------------- +$lit_vars = []; +foreach (GlobalData::get_global_variables() as $type => $items) { + if (! in_array($type, ['numbers', 'strings', 'images', 'links', 'fonts'], true)) { + continue; // "colors" is never stored in global_variables (sanitize_global_variables_data drops it). + } + foreach ((array) $items as $id => $item) { + if ($type === 'fonts' && isset(GlobalData::$customizer_fonts[$id])) { + continue; // Customizer fonts come from et_divi heading_font/body_font, not from this option. + } + unset($item['allowedActions']); + $lit_vars[$type][$id] = $item; + } +} + +$lit_var_id = static function (string $type, string $label) use (&$lit_vars, $lit_nano): string { + foreach ($lit_vars[$type] ?? [] as $id => $item) { + if (($item['label'] ?? '') === $label) { + return (string) $id; + } + } + + return 'gvid-' . $lit_nano(); +}; + +$lit_image = get_posts(['post_type' => 'attachment', 'post_mime_type' => 'image', 'numberposts' => 1, 'orderby' => 'ID', 'order' => 'ASC']); +$lit_image_url = $lit_image !== [] ? (string) wp_get_attachment_url($lit_image[0]->ID) : get_template_directory_uri() . '/images/logo.png'; + +$lit_var_specs = [ + 'numbers' => [ + 'eckenradius' => ['[Fixture] Eckenradius', '8px'], + 'abstand' => ['[Fixture] Abstand Karte', 'clamp(1.5rem, 4vw, 2.5rem)'], + ], + 'strings' => ['slogan' => ['[Fixture] Slogan', 'Handwerk aus Meisterhand – seit 1987']], + 'images' => ['werkstatt' => ['[Fixture] Werkstattbild', $lit_image_url]], + 'links' => ['kontakt' => ['[Fixture] Kontaktseite', home_url('/kontakt/')]], + 'fonts' => ['akzent' => ['[Fixture] Akzentschrift', 'Merriweather']], +]; + +$lit_v = []; +foreach ($lit_var_specs as $type => $specs) { + foreach ($specs as $key => [$label, $value]) { + $id = $lit_var_id($type, $label); + $lit_v[$key] = $id; + $old = $lit_vars[$type][$id] ?? null; + $unchanged = is_array($old) && (string) ($old['value'] ?? '') === $value && ($old['status'] ?? '') === 'active'; + $order = is_array($old) && isset($old['order']) + ? (int) $old['order'] + : 1 + count(array_filter($lit_vars[$type] ?? [], static fn ($i) => ($i['status'] ?? '') === 'active')); + $lit_vars[$type][$id] = [ + 'id' => $id, + 'label' => $label, + 'value' => $value, + 'order' => $order, + 'status' => 'active', + 'lastUpdated' => $unchanged && isset($old['lastUpdated']) ? (string) $old['lastUpdated'] : $lit_now_iso, + 'variableType' => $type, + ]; + } +} +$lit_rest('/global-data/global-variables', ['global_variables' => $lit_vars]); +WP_CLI::log('Variables: ' . wp_json_encode($lit_v)); + +// --------------------------------------------------------------------------------------------------------------- +// 3. Presets – the sync endpoint REPLACES the whole option; it refuses a payload with fewer presets than stored. +// --------------------------------------------------------------------------------------------------------------- +$lit_presets = GlobalPreset::get_data(); +$lit_find_preset = static function (string $kind, string $sub, string $name) use ($lit_presets): ?array { + foreach ($lit_presets[$kind][$sub]['items'] ?? [] as $item) { + if (is_array($item) && ($item['name'] ?? '') === $name) { + return $item; + } + } + + return null; +}; + +/** Splits preset attrs into styleAttrs / renderAttrs with Divi's own attribute map (Conversion, D4→D5 preset path). */ +$lit_split = static function (string $moduleName, array $attrs): array { + $map = Conversion::get_preset_attrs_mapping($moduleName); + + return [ + Conversion::get_preset_attrs($attrs, ['style'], $map), + Conversion::get_preset_attrs($attrs, ['html', 'script'], $map), + ]; +}; + +/** Builds one preset item like the Visual Builder's generateNewPreset / generateNewOptionGroupPreset. */ +$lit_preset = static function (string $kind, string $sub, array $spec) use ($lit_find_preset, $lit_split, $lit_nano, $lit_now_ms): array { + $old = $lit_find_preset($kind, $sub, $spec['name']); + [$styleAttrs, $renderAttrs] = $lit_split($spec['moduleName'], $spec['attrs']); + $item = ['type' => $kind, 'id' => $old['id'] ?? $lit_nano(), 'name' => $spec['name']]; + if ($kind === 'group') { + $item += ['groupName' => $sub, 'groupId' => $spec['groupId']]; + } + $item += [ + 'moduleName' => $spec['moduleName'], + 'version' => ET_BUILDER_VERSION, + 'created' => (int) ($old['created'] ?? $lit_now_ms), + 'updated' => (int) ($old['updated'] ?? $lit_now_ms), + 'attrs' => $spec['attrs'], + ]; + if ($styleAttrs !== []) { + $item['styleAttrs'] = $styleAttrs; + } + if ($renderAttrs !== []) { + $item['renderAttrs'] = $renderAttrs; + } + if (! empty($spec['groupPresets'])) { + $item['groupPresets'] = $spec['groupPresets']; + } + if ($old !== null) { + $compare = static function (array $preset): string { + unset($preset['updated'], $preset['priority'], $preset['order']); + ksort($preset); + + return wp_json_encode($preset); + }; + if ($compare($item) !== $compare($old)) { + $item['updated'] = $lit_now_ms; + } + foreach (['priority', 'order'] as $key) { + if (isset($old[$key])) { + $item[$key] = $old[$key]; + } + } + } + + return $item; +}; + +$lit_radius = static fn (string $value): array => [ + 'sync' => 'on', + 'topLeft' => $value, + 'topRight' => $value, + 'bottomLeft' => $value, + 'bottomRight' => $value, +]; +$lit_padding = static fn (string $value): array => [ + 'top' => $value, + 'right' => $value, + 'bottom' => $value, + 'left' => $value, + 'syncVertical' => 'on', + 'syncHorizontal' => 'on', +]; + +// Option group presets first – the text module preset nests the body-font preset. +$lit_group_font = $lit_preset('group', 'divi/font-body', [ + 'name' => '[Fixture] Fließtext', + 'moduleName' => 'divi/text', + 'groupId' => 'content.decoration.bodyFont', + 'attrs' => ['content' => ['decoration' => ['bodyFont' => ['body' => ['font' => ['desktop' => ['value' => [ + 'family' => $lit_var_ref($lit_v['akzent']), + 'color' => $lit_color_ref($lit_c['holz_dunkel']), + 'size' => '17px', + 'lineHeight' => '1.8em', + ]]]]]]]], +]); +$lit_group_spacing = $lit_preset('group', 'divi/spacing', [ + 'name' => '[Fixture] Abstand Karte', + 'moduleName' => 'divi/blurb', + 'groupId' => 'module.decoration.spacing', + 'attrs' => ['module' => ['decoration' => ['spacing' => ['desktop' => ['value' => [ + 'padding' => $lit_padding($lit_var_ref($lit_v['abstand'])), + 'margin' => ['bottom' => '24px'], + ]]]]]], +]); + +$lit_module_button = $lit_preset('module', 'divi/button', [ + 'name' => '[Fixture] Button Primär', + 'moduleName' => 'divi/button', + 'attrs' => ['button' => ['decoration' => [ + 'background' => ['desktop' => ['value' => ['color' => $lit_color_ref($lit_c['holz_dunkel'])]]], + 'border' => ['desktop' => ['value' => ['radius' => $lit_radius($lit_var_ref($lit_v['eckenradius']))]]], + 'font' => ['font' => ['desktop' => ['value' => [ + 'color' => $lit_color_ref($lit_c['sand']), + 'weight' => '600', + ]]]], + ]]], +]); +$lit_module_text = $lit_preset('module', 'divi/text', [ + 'name' => '[Fixture] Text Hinweis', + 'moduleName' => 'divi/text', + 'attrs' => ['module' => ['decoration' => [ + 'background' => ['desktop' => ['value' => ['color' => $lit_color_ref($lit_c['sand'])]]], + 'border' => ['desktop' => ['value' => ['radius' => $lit_radius($lit_var_ref($lit_v['eckenradius']))]]], + 'spacing' => ['desktop' => ['value' => ['padding' => $lit_padding($lit_var_ref($lit_v['abstand']))]]], + ]]], + 'groupPresets' => [ + 'content.decoration.bodyFont' => ['presetId' => [$lit_group_font['id']], 'groupName' => 'divi/font-body'], + ], +]); +$lit_module_blurb = $lit_preset('module', 'divi/blurb', [ + 'name' => '[Fixture] Blurb Leistung', + 'moduleName' => 'divi/blurb', + 'attrs' => [ + 'title' => ['decoration' => ['font' => ['font' => ['desktop' => ['value' => [ + 'family' => $lit_var_ref($lit_v['akzent']), + 'color' => $lit_color_ref($lit_c['holz_dunkel']), + 'weight' => '700', + ]]]]]], + 'module' => ['decoration' => ['border' => ['desktop' => ['value' => [ + 'radius' => $lit_radius($lit_var_ref($lit_v['eckenradius'])), + 'styles' => ['all' => ['width' => '2px', 'color' => $lit_color_ref($lit_c['salbei']), 'style' => 'solid']], + ]]]]], + ], +]); +// Default preset of divi/divider – a module no other page of this site uses, so the default changes nothing else. +$lit_module_divider = $lit_preset('module', 'divi/divider', [ + 'name' => '[Fixture] Trenner Standard', + 'moduleName' => 'divi/divider', + 'attrs' => ['divider' => ['advanced' => ['line' => ['desktop' => ['value' => [ + 'color' => $lit_color_ref($lit_c['holz_hell']), + 'weight' => '3px', + ]]]]]], +]); + +$lit_mine = [ + 'module' => [ + 'divi/button' => [$lit_module_button], + 'divi/text' => [$lit_module_text], + 'divi/blurb' => [$lit_module_blurb], + 'divi/divider' => [$lit_module_divider], + ], + 'group' => [ + 'divi/font-body' => [$lit_group_font], + 'divi/spacing' => [$lit_group_spacing], + ], +]; +$lit_defaults = ['module' => ['divi/divider' => $lit_module_divider['id']], 'group' => []]; + +// Stored format (option) → REST schema format (list of {default, items: list}), merged with our items. +$lit_payload = ['module' => [], 'group' => []]; +foreach (['module', 'group'] as $kind) { + $subs = array_unique(array_merge(array_keys($lit_presets[$kind] ?? []), array_keys($lit_mine[$kind]))); + foreach ($subs as $sub) { + $items = $lit_presets[$kind][$sub]['items'] ?? []; + foreach ($lit_mine[$kind][$sub] ?? [] as $item) { + $items[$item['id']] = $item; + } + $lit_payload[$kind][] = [ + 'default' => (string) ($lit_defaults[$kind][$sub] ?? ($lit_presets[$kind][$sub]['default'] ?? '')), + 'items' => array_values($items), + ]; + } +} +$lit_rest('/global-data/global-preset/sync', ['presets' => $lit_payload, 'actionType' => 'ADD_NEW_MODULE_PRESET']); +WP_CLI::log('Presets: ' . wp_json_encode([ + 'button' => $lit_module_button['id'], + 'text' => $lit_module_text['id'], + 'blurb' => $lit_module_blurb['id'], + 'divider (default)' => $lit_module_divider['id'], + 'font-body' => $lit_group_font['id'], + 'spacing' => $lit_group_spacing['id'], +])); + +// --------------------------------------------------------------------------------------------------------------- +// 4. Page "[Fixture] Designsystem" (draft) with preset and variable references, saved like the builder's draft save. +// --------------------------------------------------------------------------------------------------------------- +$lit_block = static function (string $name, array $attrs, array $inner = []): string { + $attrs['builderVersion'] = ET_BUILDER_VERSION; + $json = serialize_block_attributes($attrs); + if ($inner === []) { + return "\n"; + } + + return "\n" . implode('', $inner) . "\n"; +}; +$lit_label = static fn (string $label): array => ['meta' => ['adminLabel' => ['desktop' => ['value' => $label]]]]; +$lit_text = static fn (string $html): array => ['innerContent' => ['desktop' => ['value' => $html]]]; + +$lit_modules = [ + $lit_block('divi/heading', [ + 'module' => $lit_label('Überschrift'), + 'title' => ['innerContent' => ['desktop' => ['value' => 'Designsystem-Fixture']]], + ]), + $lit_block('divi/text', [ + 'module' => $lit_label('Text mit Modul-Preset'), + 'modulePreset' => [$lit_module_text['id']], + 'content' => $lit_text('

Hinweis-Text mit dem Modul-Preset „[Fixture] Text Hinweis“ (enthält das Gruppen-Preset „[Fixture] Fließtext“).

'), + ]), + $lit_block('divi/text', [ + 'module' => $lit_label('Text mit Gruppen-Preset'), + 'groupPreset' => ['content.decoration.bodyFont' => ['presetId' => [$lit_group_font['id']], 'groupName' => 'divi/font-body']], + 'content' => $lit_text('

Fließtext mit dem Gruppen-Preset „[Fixture] Fließtext“ direkt am Modul.

'), + ]), + $lit_block('divi/text', [ + 'module' => $lit_label('Text mit String-Variable'), + 'content' => $lit_text($lit_var_ref($lit_v['slogan'])), + ]), + $lit_block('divi/button', [ + 'module' => $lit_label('Button mit Modul-Preset'), + 'modulePreset' => [$lit_module_button['id']], + 'button' => ['innerContent' => ['desktop' => ['value' => [ + 'text' => 'Angebot anfragen', + 'linkUrl' => $lit_var_ref($lit_v['kontakt']), + ]]]], + ]), + $lit_block('divi/blurb', [ + 'module' => $lit_label('Blurb mit Modul- und Gruppen-Preset'), + 'modulePreset' => [$lit_module_blurb['id']], + 'groupPreset' => ['module.decoration.spacing' => ['presetId' => [$lit_group_spacing['id']], 'groupName' => 'divi/spacing']], + 'title' => $lit_text('Möbel nach Maß'), + 'content' => $lit_text('

Blurb mit „[Fixture] Blurb Leistung“ und dem Gruppen-Preset „[Fixture] Abstand Karte“.

'), + ]), + $lit_block('divi/image', [ + 'module' => $lit_label('Bild aus Bild-Variable'), + 'image' => ['innerContent' => ['desktop' => ['value' => [ + 'src' => $lit_var_ref($lit_v['werkstatt']), + 'alt' => 'Werkstatt', + ]]]], + ]), + $lit_block('divi/divider', [ + 'module' => $lit_label('Trenner ohne Preset (Standard-Preset)'), + ]), +]; +$lit_column = $lit_block('divi/column', [ + 'module' => ['decoration' => ['sizing' => ['desktop' => ['value' => ['flexType' => '24_24']]]]], +], $lit_modules); +$lit_row = $lit_block('divi/row', ['module' => $lit_label('Designsystem Zeile')], [$lit_column]); +$lit_section = $lit_block('divi/section', ['module' => $lit_label('Designsystem Abschnitt')], [$lit_row]); +$lit_content = '' . rtrim($lit_section, "\n") . ''; + +$lit_page_title = '[Fixture] Designsystem'; +$lit_existing = get_posts([ + 'post_type' => 'page', + 'post_status' => ['draft', 'publish', 'private', 'pending'], + 'title' => $lit_page_title, + 'numberposts' => 1, + 'orderby' => 'ID', + 'order' => 'ASC', +]); +if ($lit_existing !== []) { + $lit_page_id = $lit_existing[0]->ID; +} else { + $lit_page_id = wp_insert_post([ + 'post_type' => 'page', + 'post_status' => 'draft', + 'post_title' => $lit_page_title, + 'post_content' => '', + ], true); + if (is_wp_error($lit_page_id)) { + WP_CLI::error($lit_page_id->get_error_message()); + } + et_builder_enable_for_post($lit_page_id); +} +if (get_post_field('post_content', $lit_page_id) !== $lit_content) { + $lit_rest('/sync-to-server', [ + 'post_id' => $lit_page_id, + 'post_status' => get_post_status($lit_page_id), + 'syncType' => 'draft', + 'content' => ['post_content' => $lit_content], + ]); +} +WP_CLI::log('Page: ' . $lit_page_id); + +// --------------------------------------------------------------------------------------------------------------- +// 5. Read back through Divi and through the plugin's DesignSystemReader. +// --------------------------------------------------------------------------------------------------------------- +$lit_colors_after = GlobalData::get_global_colors(); +foreach ($lit_color_specs as $id => [$label, $value]) { + if (($lit_colors_after[$id]['color'] ?? null) !== $value || ($lit_colors_after[$id]['label'] ?? null) !== $label) { + WP_CLI::error("Color {$id} did not read back."); + } +} +$lit_vars_after = GlobalData::get_global_variables(); +foreach ($lit_var_specs as $type => $specs) { + foreach ($specs as $key => [$label, $value]) { + $item = ((array) ($lit_vars_after[$type] ?? []))[$lit_v[$key]] ?? null; + if (! is_array($item) || $item['value'] !== $value || $item['label'] !== $label) { + WP_CLI::error("Variable {$lit_v[$key]} ({$type}) did not read back."); + } + } +} +$lit_presets_after = GlobalPreset::get_data(); +foreach ($lit_mine as $kind => $subs) { + foreach ($subs as $sub => $items) { + foreach ($items as $item) { + if (($lit_presets_after[$kind][$sub]['items'][$item['id']]['name'] ?? null) !== $item['name']) { + WP_CLI::error("Preset {$item['id']} ({$kind} {$sub}) did not read back."); + } + } + } +} +if (($lit_presets_after['module']['divi/divider']['default'] ?? '') !== $lit_module_divider['id']) { + WP_CLI::error('The divider default preset did not read back.'); +} + +if (class_exists(\LIT\MCP\Integrations\Divi\Design\DesignSystemReader::class)) { + $lit_ds = (new \LIT\MCP\Integrations\Divi\Design\DesignSystemReader())->read(); + $lit_labels = array_column($lit_ds->colors(), 'label'); + $lit_var_labels = array_column($lit_ds->variables(), 'label'); + $lit_preset_names = array_values($lit_ds->presetNames()); + foreach (array_column($lit_color_specs, 0) as $label) { + in_array($label, $lit_labels, true) || WP_CLI::error("DesignSystemReader misses color {$label}."); + } + foreach ($lit_var_specs as $specs) { + foreach ($specs as [$label]) { + in_array($label, $lit_var_labels, true) || WP_CLI::error("DesignSystemReader misses variable {$label}."); + } + } + foreach ($lit_mine as $subs) { + foreach ($subs as $items) { + foreach ($items as $item) { + in_array($item['name'], $lit_preset_names, true) || WP_CLI::error("DesignSystemReader misses preset {$item['name']}."); + } + } + } + WP_CLI::log(sprintf( + 'DesignSystemReader: %d colors, %d variables, %d presets, fonts %s', + count($lit_ds->colors()), + count($lit_ds->variables()), + count($lit_ds->presets()), + wp_json_encode($lit_ds->fonts()) + )); +} else { + WP_CLI::warning('DesignSystemReader not loaded (plugin inactive?) – skipped.'); +} + +WP_CLI::success('Stage 4 design data seeded; page ' . $lit_page_id . '.');