feat: Divi hints point to the read tools; guides divi-structure/attrs/design-system; test: Divi read end-to-end
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -23,8 +23,8 @@ Werkzeugnamen: `lit-…` sind direkt aufrufbare Tools; `lit/…` sind Abilities,
|
|||||||
Trefferliste. Geliefert werden nur Einträge, die der verbundene Nutzer selbst bearbeiten darf (fremde
|
Trefferliste. Geliefert werden nur Einträge, die der verbundene Nutzer selbst bearbeiten darf (fremde
|
||||||
Entwürfe/private Inhalte ohne die passende Berechtigung erscheinen nicht).
|
Entwürfe/private Inhalte ohne die passende Berechtigung erscheinen nicht).
|
||||||
- `lit-content-get`: alle Daten eines Eintrags inkl. Feldern, Begriffen, Beitragsbild und `rev`.
|
- `lit-content-get`: alle Daten eines Eintrags inkl. Feldern, Begriffen, Beitragsbild und `rev`.
|
||||||
Bei Divi-Seiten fehlt `content`; Divi-Werkzeuge folgen in einer späteren Version, bis dahin werden
|
Bei Divi-Seiten fehlt `content`; den Aufbau liest `lit-divi-outline` (siehe `divi-structure`), geändert
|
||||||
Divi-Inhalte im Divi-Builder bearbeitet (Titel, Auszug, Felder und Begriffe gehen weiter über
|
wird er vorerst im Divi-Builder (Titel, Auszug, Felder und Begriffe gehen weiter über
|
||||||
`lit-content-update`).
|
`lit-content-update`).
|
||||||
|
|
||||||
## Anlegen und Ändern
|
## Anlegen und Ändern
|
||||||
@@ -59,7 +59,8 @@ Werkzeugnamen: `lit-…` sind direkt aufrufbare Tools; `lit/…` sind Abilities,
|
|||||||
## Sicher überarbeiten
|
## Sicher überarbeiten
|
||||||
|
|
||||||
1. `lit/post-duplicate` → Entwurfskopie; die Antwort enthält die `rev` der Kopie für `lit-content-update`.
|
1. `lit/post-duplicate` → Entwurfskopie; die Antwort enthält die `rev` der Kopie für `lit-content-update`.
|
||||||
2. Kopie mit `lit-content-update` (Divi-Inhalte bis zu den Divi-Werkzeugen im Divi-Builder) bearbeiten und dem Nutzer die Vorschau zeigen.
|
2. Kopie mit `lit-content-update` (Divi-Inhalte vorerst im Divi-Builder) bearbeiten und dem Nutzer die
|
||||||
|
Vorschau zeigen; `lit-page-render-summary` fasst den gerenderten Inhalt zusammen.
|
||||||
3. `lit/post-replace-with-copy` übernimmt die Kopie ins Original (ID, URL, Status und Menüeinträge bleiben).
|
3. `lit/post-replace-with-copy` übernimmt die Kopie ins Original (ID, URL, Status und Menüeinträge bleiben).
|
||||||
Voraussetzungen: die Kopie muss noch Entwurf oder ausstehend sein, `rev` des Originals aus
|
Voraussetzungen: die Kopie muss noch Entwurf oder ausstehend sein, `rev` des Originals aus
|
||||||
`lit-content-get` ist Pflicht (sie deckt Titel, Inhalt, Auszug und Status ab – Felder/Meta, die direkt am
|
`lit-content-get` ist Pflicht (sie deckt Titel, Inhalt, Auszug und Status ab – Felder/Meta, die direkt am
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Divi-Einstellungen (Vollform)
|
||||||
|
|
||||||
|
Jedes Divi-5-Modul speichert seine Einstellungen unter **Attributpfaden**, z. B.
|
||||||
|
|
||||||
|
- `title.innerContent` – Text der Überschrift,
|
||||||
|
- `title.decoration.font.font` – Schrift der Überschrift (Farbe, Größe, Ebene `headingLevel`),
|
||||||
|
- `button.innerContent` – Beschriftung und Link eines Buttons (`text`, `linkUrl`, `linkTarget`),
|
||||||
|
- `module.decoration.background` – Hintergrund, `module.decoration.spacing` – Innen-/Außenabstand,
|
||||||
|
- `module.meta.adminLabel` – Admin-Label im Divi-Builder.
|
||||||
|
|
||||||
|
Der Wert eines Pfads ist eine Zuordnung **Breakpoint → Zustand → Wert**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "desktop": { "value": { "color": "#1a1a1a", "headingLevel": "h2" }, "hover": { "color": "#000000" } },
|
||||||
|
"phone": { "value": { "size": "28px" } } }
|
||||||
|
```
|
||||||
|
|
||||||
|
Breakpoints: `desktop`, `tablet`, `phone` (je nach Website auch `tabletWide`, `phoneWide`, `widescreen`,
|
||||||
|
`ultraWide`); Zustände: `value`, `hover`, `sticky`.
|
||||||
|
|
||||||
|
## Pfade finden
|
||||||
|
|
||||||
|
- `lit-divi-catalog` mit `module` (z. B. `"heading"`) listet alle gültigen Pfade dieses Moduls auf dieser
|
||||||
|
Website (live aus Divi) und die Kurzform-Felder mit ihrem Pfad. `path_kinds` nennt die Feldart eines Pfads
|
||||||
|
(`text`, `richtext`, `code`, `url`, `image`, `color`, `enum`, `onoff`, `number`, `list`, `icon`), soweit aus
|
||||||
|
den Divi-Metadaten ableitbar; zusammengesetzte Pfade (mehrere mögliche Feldarten je nach Unterschlüssel)
|
||||||
|
gelten als unbekannt, statt eine Feldart zu raten.
|
||||||
|
- `lit-divi-node-get` zeigt die gesetzten Werte eines vorhandenen Knotens in genau dieser Form (`attrs`).
|
||||||
|
- Schrift-Gruppen liegen eine Ebene tiefer: `…decoration.font.font`, `…decoration.bodyFont.body.font`,
|
||||||
|
`…decoration.headingFont.h2.font`, `module.advanced.text.text`.
|
||||||
|
- Werte wie `$variable({…})$` sind Verweise auf globale Farben, Variablen oder dynamische Inhalte – nicht
|
||||||
|
durch feste Werte ersetzen (siehe `divi-design-system`).
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Divi-Designsystem
|
||||||
|
|
||||||
|
`lit-divi-design-system` liest das Designsystem der Website live aus Divi:
|
||||||
|
|
||||||
|
- **Globale Farben** (`colors`): ID (`gcid-…`), Name, Wert und der Platzhalter für neue Inhalte, z. B.
|
||||||
|
`{{color:primary}}` für die Primärfarbe oder `{{color:Nachtblau}}` für eine eigene globale Farbe. Die fünf
|
||||||
|
Theme-Farben heißen `primary`, `secondary`, `heading`, `body` und `link`.
|
||||||
|
- **Globale Variablen** (`variables`): Zahlen, Texte, Bilder, Links (`gvid-…`), Platzhalter `{{var:<Name>}}`.
|
||||||
|
- **Theme-Schriften** (`fonts`): Überschrift und Fließtext, Platzhalter `{{font:heading}}` / `{{font:body}}`.
|
||||||
|
- **Größen** (`sizes`): Schriftgrößen, Inhaltsbreite und Spaltenabstand aus den Theme-Optionen.
|
||||||
|
- **Presets** (`presets`): Modul-Presets je Modul (`kind: module`, z. B. alle Button-Presets) und
|
||||||
|
Gruppen-Presets (`kind: group`); `default: true` markiert das Standard-Preset eines Moduls.
|
||||||
|
|
||||||
|
Regeln:
|
||||||
|
|
||||||
|
1. Vor dem Gestalten einer Seite das Designsystem lesen.
|
||||||
|
2. Globale Farben, Variablen und Presets vor festen Werten verwenden; feste Farben oder Schriften nur, wenn
|
||||||
|
der Nutzer es ausdrücklich möchte.
|
||||||
|
3. Gibt es für einen Zweck noch keine Farbe, Variable oder kein Preset, dem Nutzer vorschlagen, es im
|
||||||
|
Divi-Builder anzulegen, statt feste Werte zu verteilen.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Divi-Seiten: Aufbau lesen
|
||||||
|
|
||||||
|
Divi 5 speichert eine Seite als Block-Markup: Abschnitte (`section`) enthalten Zeilen (`row`), Zeilen
|
||||||
|
Spalten (`column`), Spalten Module (`text`, `heading`, `button`, …). Manche Module haben Kind-Module
|
||||||
|
(`accordion` → `accordion-item`). Die Divi-Werkzeuge gibt es nur, wenn der Divi-5-Builder aktiv ist.
|
||||||
|
|
||||||
|
## Seitenbaum
|
||||||
|
|
||||||
|
- `lit-divi-outline` mit `post_id` liefert jeden Knoten mit **Pfad-ID**, Modultyp, Admin-Label, Textauszug,
|
||||||
|
Layout-Info (z. B. `Spalten 8_24+16_24`) und Preset-Namen – als kompakten Text (`outline`) und als Liste
|
||||||
|
(`nodes`). Mit `id` nur ein Teilbaum, mit `depth` begrenzte Tiefe.
|
||||||
|
- Pfad-IDs: `s0` = erster Abschnitt, `s0.r1` = zweite Zeile darin, `s0.r1.c0` = erste Spalte,
|
||||||
|
`s0.r1.c0.m2` = drittes Modul, `…m2.m0` = erstes Kind-Modul. `g` = globales Modul, `x` = fremder Block
|
||||||
|
(z. B. aus dem Block-Editor). Gezählt wird je Buchstabe innerhalb des Elternknotens.
|
||||||
|
- `lit-divi-node-get` mit `post_id` und `id` liefert alle Einstellungen eines Knotens (siehe `divi-attrs`),
|
||||||
|
die aktuellen Kurzform-Werte (`shortform`) und die IDs der Kinder.
|
||||||
|
|
||||||
|
## Stand und Sperre
|
||||||
|
|
||||||
|
- `tree_rev` ist der Stand des Seiteninhalts. Pfad-IDs gelten nur für diesen Stand – ändert jemand die Seite
|
||||||
|
(auch im Divi-Builder), verschieben sich die IDs. Divi-Änderungen verlangen deshalb immer die `tree_rev`
|
||||||
|
des letzten Lesevorgangs.
|
||||||
|
- `rev` ist davon getrennt: der Stand des ganzen Eintrags (Titel, Auszug, Status …) für `lit-content-update`.
|
||||||
|
|
||||||
|
## Was nicht bearbeitet wird
|
||||||
|
|
||||||
|
- Knoten mit „nur lesbar“ (globale Module `g…`, fremde Blöcke `x…`) ändern die Divi-Werkzeuge nicht.
|
||||||
|
- Divi-4-Inhalt (Shortcodes `[et_pb_…]`) wird nicht gelesen oder geändert: Seite einmal im Divi-Builder
|
||||||
|
öffnen und speichern, Divi 5 wandelt sie dabei um.
|
||||||
|
- Ist das Markup beschädigt, lehnen die Werkzeuge ab (`unsupported`); die Seite im Divi-Builder öffnen und
|
||||||
|
speichern.
|
||||||
|
|
||||||
|
## Ergebnis prüfen
|
||||||
|
|
||||||
|
`lit-page-render-summary` rendert den Inhaltsbereich wie im Frontend und listet Überschriften, Texte, Links
|
||||||
|
und Bilder samt Warnungen (Sprünge in der Überschriftenhierarchie, fehlende Alt-Texte, Links ohne Ziel).
|
||||||
|
Header, Footer und Theme-Builder-Vorlagen sind nicht enthalten.
|
||||||
@@ -33,7 +33,10 @@ Status (`lit/post-unpublish`, `lit/post-schedule`, `lit/post-trash`), Kopien (`l
|
|||||||
Einstellungen (`lit/wp-settings-get`, `lit/wp-settings-update`), Medien (`lit/media-get`, `lit/media-upload`,
|
Einstellungen (`lit/wp-settings-get`, `lit/wp-settings-update`), Medien (`lit/media-get`, `lit/media-upload`,
|
||||||
`lit/media-update`, `lit/media-delete`).
|
`lit/media-update`, `lit/media-delete`).
|
||||||
|
|
||||||
Weitere Themen: `lit-guide` mit `topic` = `content-types`, `workflows` oder `media`.
|
Die `lit-divi-…`-Werkzeuge gibt es nur, wenn der Divi-5-Builder aktiv ist.
|
||||||
|
|
||||||
|
Weitere Themen: `lit-guide` mit `topic` = `content-types`, `workflows`, `media`, `divi-structure`,
|
||||||
|
`divi-attrs` oder `divi-design-system`.
|
||||||
|
|
||||||
## Rückmeldungen
|
## Rückmeldungen
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Etappe 2a – Manuelle Abnahme (Divi lesen)
|
||||||
|
|
||||||
|
Vorbereitet von Task 8 (Hinweise/Guides/Serveranweisungen/Ende-zu-Ende-Test). Automatisiert geprüft ist
|
||||||
|
bereits: alle Unit-Tests (`composer test`, 692 Tests), Lint (`composer lint`) und alle vier
|
||||||
|
Integrationsskripte (`tests/Integration/divi-read-smoke.sh`, `tests/Integration/mcp-smoke.sh`,
|
||||||
|
`tests/Integration/wp-basis-smoke.sh`, `tests/Integration/media-admin-smoke.sh`) – Details im Report von
|
||||||
|
Task 8 (`.superpowers/sdd/2026-09-26-etappe-2a-divi-lesen/task-8-report.md`).
|
||||||
|
|
||||||
|
Die folgenden Punkte lassen sich nicht automatisiert prüfen (echter Divi Visual Builder,
|
||||||
|
claude.ai-Connector); das führt Jan-Niclas selbst durch.
|
||||||
|
|
||||||
|
## 1. Builder-Fixtures exportieren (Roundtrip mit echten Seiten)
|
||||||
|
|
||||||
|
Alle bisherigen Divi-Fixtures sind entweder handgeschrieben (`synthetic-*.html`), von Divis eigener
|
||||||
|
Shortcode→Block-Konvertierung erzeugt (`converted-*.html`) oder reine Modul-Metadaten
|
||||||
|
(`metadata-sample.json`). Es fehlt noch mindestens eine echte, im Visual Builder gebaute Seite als
|
||||||
|
Gegenprobe für Parser/Serializer.
|
||||||
|
|
||||||
|
- [ ] 2–3 Seiten im Divi Visual Builder bauen. Zusammen sollten sie enthalten:
|
||||||
|
- ein **globales Modul** (aus der Divi-Bibliothek eingefügt),
|
||||||
|
- ein **Modul-Preset**,
|
||||||
|
- eine **globale Farbe** und eine **globale Variable**,
|
||||||
|
- **Dynamic Content** (z. B. Beitragstitel, ein Meta-Feld),
|
||||||
|
- einen **Spezialabschnitt** (Specialty Section) und eine **verschachtelte Zeile**.
|
||||||
|
- Keine personenbezogenen Daten verwenden – die Fixtures werden eingecheckt.
|
||||||
|
- [ ] Je Seite exportieren und den Roundtrip prüfen:
|
||||||
|
```bash
|
||||||
|
cd /home/janniclas/Projekte/WordPress
|
||||||
|
timeout 300 ddev exec wp eval-file wp-content/plugins/WP-LIT-MCP/tests/Integration/divi-export-page.php <post_id> <name>
|
||||||
|
cd wp-content/plugins/WP-LIT-MCP && composer test
|
||||||
|
```
|
||||||
|
Erwartet: `Success: builder-<name>.html: <N> bytes, <N> blocks, globalModule: ja/nein, tree_rev <hash>.`,
|
||||||
|
danach `composer test` weiterhin grün (der Test läuft automatisch über alle `builder-*.html`-Dateien mit).
|
||||||
|
- [ ] Schlägt der Roundtrip fehl (Parser/Serializer erzeugen nicht mehr das byte-identische Markup): das
|
||||||
|
ist ein **Fehler im Plugin** (`Parser`/`Serializer`), nie ein Grund, die Fixture anzupassen. Als
|
||||||
|
Folgeaufgabe melden statt selbst zu reparieren.
|
||||||
|
- [ ] Fixtures ohne personenbezogene Daten einchecken:
|
||||||
|
```bash
|
||||||
|
git add tests/Fixtures/divi/builder-*.html
|
||||||
|
git -c user.name="Jan-Niclas Loosen" -c user.email="janniclas@loosen-it.de" commit -m "test: Divi builder fixtures"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. claude.ai: Divi-Lese-Werkzeuge im Gespräch
|
||||||
|
|
||||||
|
Mit dem claude.ai-Connector gegen eine Website mit aktivem Divi-5-Theme (z. B. dieses DDEV-Setup oder
|
||||||
|
Staging) verbinden und prüfen, dass die Antworten mit dem tatsächlichen Aufbau im Divi-Builder
|
||||||
|
übereinstimmen:
|
||||||
|
|
||||||
|
- [ ] „Zeig mir den Aufbau der Startseite“ → sollte `lit-divi-outline` aufrufen und einen Seitenbaum mit
|
||||||
|
Pfad-IDs, Modultypen und Textauszügen liefern, der zum Divi-Builder passt.
|
||||||
|
- [ ] „Welche Farben und Presets hat die Website?“ → sollte `lit-divi-design-system` aufrufen und die
|
||||||
|
globalen Farben/Presets nennen, die auch im Divi-Builder (Designsystem-Bereich) zu sehen sind.
|
||||||
|
- [ ] „Was sieht ein Besucher auf der Seite?“ → sollte `lit-page-render-summary` aufrufen und
|
||||||
|
Überschriften/Texte/Links/Bilder liefern, die dem Frontend entsprechen.
|
||||||
|
- [ ] Bei einer der zuvor im Visual Builder gebauten Seiten (Schritt 1): stichprobenartig prüfen, dass
|
||||||
|
globales Modul, Preset, Dynamic Content und Spezialabschnitt im Outline/Node-Get sinnvoll
|
||||||
|
wiedergegeben werden (globales Modul und Spezialabschnitt ggf. als „nur lesbar“ markiert – das ist
|
||||||
|
in dieser Etappe so vorgesehen, siehe `divi-structure`-Guide).
|
||||||
|
|
||||||
|
## Ergebnis
|
||||||
|
|
||||||
|
Kurze Rückmeldung an den Ausführenden (Task 8 / Controller): welche Punkte grün sind, welche
|
||||||
|
Builder-Fixtures committet wurden, und was ggf. als Folgeaufgabe (Bugfix im Parser/Serializer oder
|
||||||
|
Doku-Korrektur) aufgenommen werden muss.
|
||||||
@@ -52,6 +52,11 @@ final class InstructionsBuilder
|
|||||||
. ' (Felder und Taxonomien: lit/content-types, direkt: lit-content-types).';
|
. ' (Felder und Taxonomien: lit/content-types, direkt: lit-content-types).';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$divi = str_contains(', ' . $active . ',', ', divi,')
|
||||||
|
? ['- Divi-Seiten: vor Änderungen lit-divi-outline und lit-divi-design-system lesen; Aufbau: lit-guide '
|
||||||
|
. 'topic "divi-structure".']
|
||||||
|
: [];
|
||||||
|
|
||||||
return implode("\n", array_merge($lines, [
|
return implode("\n", array_merge($lines, [
|
||||||
'Regeln:',
|
'Regeln:',
|
||||||
'- Vor dem ersten Arbeitsschritt lit-guide (ohne Topic) aufrufen; Details je Thema über lit-guide '
|
'- Vor dem ersten Arbeitsschritt lit-guide (ohne Topic) aufrufen; Details je Thema über lit-guide '
|
||||||
@@ -63,6 +68,6 @@ final class InstructionsBuilder
|
|||||||
'- Beim Bearbeiten immer den rev-Wert aus dem letzten Lesevorgang mitgeben (Pflicht).',
|
'- Beim Bearbeiten immer den rev-Wert aus dem letzten Lesevorgang mitgeben (Pflicht).',
|
||||||
'- Löschen und Ersetzen erst nach Rückfrage beim Nutzer (confirm: true).',
|
'- Löschen und Ersetzen erst nach Rückfrage beim Nutzer (confirm: true).',
|
||||||
'- Fehler enthalten einen hint – ihm folgen, statt blind zu wiederholen.',
|
'- Fehler enthalten einen hint – ihm folgen, statt blind zu wiederholen.',
|
||||||
]));
|
], $divi));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ final class ContentUpdateAbility implements Ability
|
|||||||
if (array_key_exists('content', $input) && PostPresenter::usesDivi($post)) {
|
if (array_key_exists('content', $input) && PostPresenter::usesDivi($post)) {
|
||||||
return Errors::unsupported(
|
return Errors::unsupported(
|
||||||
'Divi-Inhalte können nicht als Rohtext überschrieben werden; nichts wurde gespeichert.',
|
'Divi-Inhalte können nicht als Rohtext überschrieben werden; nichts wurde gespeichert.',
|
||||||
PostPresenter::DIVI_NOTE . ' Titel, Auszug, Felder und Begriffe hier ohne content ändern.'
|
PostPresenter::diviNote() . ' Titel, Auszug, Felder und Begriffe hier ohne content ändern.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace LIT\MCP\Integrations\WordPress\Content;
|
|||||||
|
|
||||||
use LIT\MCP\Abilities\JsonMap;
|
use LIT\MCP\Abilities\JsonMap;
|
||||||
use LIT\MCP\Abilities\Rev;
|
use LIT\MCP\Abilities\Rev;
|
||||||
|
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
|
||||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||||
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
|
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
|
||||||
use WP_Post;
|
use WP_Post;
|
||||||
@@ -13,14 +14,36 @@ use WP_Post;
|
|||||||
/**
|
/**
|
||||||
* Output format of an entry for the content tools. Not final so abilities can be
|
* Output format of an entry for the content tools. Not final so abilities can be
|
||||||
* unit-tested with a mock.
|
* unit-tested with a mock.
|
||||||
|
*
|
||||||
|
* Layering exception: diviNote()/diviHint() call DiviModules::isAvailable() (Divi
|
||||||
|
* integration) to decide whether the Divi tools exist – the WordPress integration
|
||||||
|
* otherwise never depends on Divi. Kept because the alternative (injecting
|
||||||
|
* availability through every constructor that reaches PostPresenter) would spread the
|
||||||
|
* same one-line check across content-get, content-update and revisions-list for no
|
||||||
|
* behavioral gain; $toolsAvailable can still be passed in explicitly for tests.
|
||||||
*/
|
*/
|
||||||
class PostPresenter
|
class PostPresenter
|
||||||
{
|
{
|
||||||
public const DIVI_NOTE = 'Divi-Werkzeuge folgen in einer späteren Version; bis dahin im Divi-Builder bearbeiten.';
|
/** How Divi content is read and changed with the Divi tools. */
|
||||||
|
public const DIVI_NOTE = 'Den Aufbau mit lit/divi-outline (direkt: lit-divi-outline) lesen, einzelne Knoten mit '
|
||||||
|
. 'lit/divi-node-get (direkt: lit-divi-node-get); ändern vorerst im Divi-Builder.';
|
||||||
|
|
||||||
public const DIVI_HINT = 'Dieser Inhalt ist mit Divi gebaut; content wird deshalb nicht ausgegeben. '
|
/** Divi content on a site where the Divi 5 builder is not loaded (no Divi tools registered). */
|
||||||
. self::DIVI_NOTE . ' Titel, Auszug, Felder und Begriffe sind über lit/content-update '
|
public const DIVI_INACTIVE_NOTE = 'Der Divi-5-Builder ist auf dieser Website nicht aktiv, daher gibt es keine '
|
||||||
. '(direkt: lit-content-update) änderbar.';
|
. 'Divi-Werkzeuge; Divi-Inhalte nur im Divi-Builder bearbeiten.';
|
||||||
|
|
||||||
|
/** $toolsAvailable defaults to whether the Divi integration is available (Divi 5 loaded). */
|
||||||
|
public static function diviNote(?bool $toolsAvailable = null): string
|
||||||
|
{
|
||||||
|
return ($toolsAvailable ?? DiviModules::isAvailable()) ? self::DIVI_NOTE : self::DIVI_INACTIVE_NOTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function diviHint(?bool $toolsAvailable = null): string
|
||||||
|
{
|
||||||
|
return 'Dieser Inhalt ist mit Divi gebaut; content wird deshalb nicht ausgegeben. '
|
||||||
|
. self::diviNote($toolsAvailable) . ' Titel, Auszug, Felder und Begriffe sind über lit/content-update '
|
||||||
|
. '(direkt: lit-content-update) änderbar.';
|
||||||
|
}
|
||||||
|
|
||||||
public function __construct(private ContentSchema $schema)
|
public function __construct(private ContentSchema $schema)
|
||||||
{
|
{
|
||||||
@@ -63,7 +86,7 @@ class PostPresenter
|
|||||||
'fields' => JsonMap::of($this->fields($post)),
|
'fields' => JsonMap::of($this->fields($post)),
|
||||||
'featured_image' => $this->featuredImage($post),
|
'featured_image' => $this->featuredImage($post),
|
||||||
'rev' => Rev::ofPost($post),
|
'rev' => Rev::ofPost($post),
|
||||||
'hints' => $divi ? [self::DIVI_HINT] : [],
|
'hints' => $divi ? [self::diviHint()] : [],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use WP_Post;
|
|||||||
* the same detection lit/content-get uses) because a Divi/block-built revision's
|
* the same detection lit/content-get uses) because a Divi/block-built revision's
|
||||||
* post_content is shortcode or block-comment markup, not prose – excerpt is then a
|
* post_content is shortcode or block-comment markup, not prose – excerpt is then a
|
||||||
* stripped fragment of that markup rather than a real preview; hints repeats
|
* stripped fragment of that markup rather than a real preview; hints repeats
|
||||||
* PostPresenter::DIVI_HINT once when any item is flagged.
|
* PostPresenter::diviHint() once when any item is flagged.
|
||||||
*/
|
*/
|
||||||
final class RevisionsListAbility implements Ability
|
final class RevisionsListAbility implements Ability
|
||||||
{
|
{
|
||||||
@@ -124,7 +124,7 @@ final class RevisionsListAbility implements Ability
|
|||||||
'post_id' => (int) $post->ID,
|
'post_id' => (int) $post->ID,
|
||||||
'items' => $items,
|
'items' => $items,
|
||||||
'next_cursor' => $page['next_cursor'],
|
'next_cursor' => $page['next_cursor'],
|
||||||
'hints' => in_array(true, array_column($items, 'divi'), true) ? [PostPresenter::DIVI_HINT] : [],
|
'hints' => in_array(true, array_column($items, 'divi'), true) ? [PostPresenter::diviHint()] : [],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Executable
+100
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# End-to-end checks of the Etappe-2a Divi read tools through the OAuth-protected endpoint.
|
||||||
|
# Runs INSIDE the DDEV web container (Divi 5 must be the active theme):
|
||||||
|
# cd /home/janniclas/Projekte/WordPress && timeout 600 ddev exec bash wp-content/plugins/WP-LIT-MCP/tests/Integration/divi-read-smoke.sh
|
||||||
|
# Creates draft pages from tests/Fixtures/divi and deletes them again.
|
||||||
|
set -euo pipefail
|
||||||
|
cd /var/www/html
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source wp-content/plugins/WP-LIT-MCP/tests/Integration/lib.sh
|
||||||
|
|
||||||
|
ADMIN_LOGIN=$(wp user list --role=administrator --field=user_login | head -1)
|
||||||
|
FIXTURES=$PLUGIN/tests/Fixtures/divi
|
||||||
|
CREATED_POSTS=()
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
for id in "${CREATED_POSTS[@]}"; do
|
||||||
|
wp post delete "$id" --force >/dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
cleanup_app_passwords_for "$ADMIN_LOGIN"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Creates a draft page from a fixture file as the admin (no kses re-serialization) and prints its ID.
|
||||||
|
create_page() { # $1 = fixture file, $2 = title
|
||||||
|
LIT_FILE="$FIXTURES/$1" LIT_TITLE="$2" wp --user="$ADMIN_LOGIN" eval '
|
||||||
|
$id = wp_insert_post(["post_type" => "page", "post_status" => "draft", "post_title" => getenv("LIT_TITLE"),
|
||||||
|
"post_content" => wp_slash((string) file_get_contents(getenv("LIT_FILE")))]);
|
||||||
|
update_post_meta($id, "_et_pb_use_builder", "on");
|
||||||
|
update_post_meta($id, "_et_pb_use_divi_5", "on");
|
||||||
|
echo $id;'
|
||||||
|
}
|
||||||
|
|
||||||
|
content_hash() { # $1 = post ID -> tree_rev of the stored content
|
||||||
|
wp eval "echo substr(hash('sha256', (string) get_post($1)->post_content), 0, 12);"
|
||||||
|
}
|
||||||
|
|
||||||
|
[ "$(wp eval 'echo get_template();')" = "Divi" ] || fail "Divi must be the active theme"
|
||||||
|
wp eval-file "$PLUGIN/tests/Integration/divi-shortform-check.php" >/dev/null || fail "short form does not match the Divi catalog"
|
||||||
|
pass "short form matches the live Divi catalog"
|
||||||
|
|
||||||
|
PAGE_ID=$(create_page converted-landing.html "LIT Divi Lesen")
|
||||||
|
CREATED_POSTS+=("$PAGE_ID")
|
||||||
|
D4_ID=$(create_page divi4-shortcodes.html "LIT Divi 4")
|
||||||
|
CREATED_POSTS+=("$D4_ID")
|
||||||
|
BEFORE=$(content_hash "$PAGE_ID")
|
||||||
|
connect "$ADMIN_LOGIN"
|
||||||
|
|
||||||
|
# 1. Direct read tools with readOnlyHint
|
||||||
|
LIST=$(rpc tools/list '{}')
|
||||||
|
for t in lit-divi-outline lit-divi-node-get lit-divi-catalog lit-divi-design-system lit-page-render-summary; do
|
||||||
|
echo "$LIST" | jq -e --arg t "$t" '.result.tools[] | select(.name == $t) | .annotations.readOnlyHint == true' >/dev/null \
|
||||||
|
|| fail "tools/list lacks $t with readOnlyHint"
|
||||||
|
done
|
||||||
|
pass "tools/list: Divi read tools are direct and read-only"
|
||||||
|
|
||||||
|
# 2. Outline with tree_rev = hash of post_content (ruling P16) and path IDs
|
||||||
|
OUTLINE=$(tool lit-divi-outline "{\"post_id\":$PAGE_ID}")
|
||||||
|
[ "$(echo "$OUTLINE" | jq -r '.tree_rev')" = "$BEFORE" ] || fail "tree_rev is not the hash of post_content: $OUTLINE"
|
||||||
|
echo "$OUTLINE" | jq -e '.rev != .tree_rev' >/dev/null || fail "rev and tree_rev must differ: $OUTLINE"
|
||||||
|
echo "$OUTLINE" | jq -e '.nodes[] | select(.id == "s1.r1.c0.m2.m0" and .type == "accordion-item")' >/dev/null \
|
||||||
|
|| fail "outline lacks the accordion item: $OUTLINE"
|
||||||
|
echo "$OUTLINE" | jq -r '.outline' | head -1 | grep -q '^s0 section "Hero"' || fail "outline text: $OUTLINE"
|
||||||
|
pass "lit-divi-outline: path IDs, tree_rev, rev"
|
||||||
|
|
||||||
|
# 3. Node in full form plus short-form values
|
||||||
|
NODE=$(tool lit-divi-node-get "{\"post_id\":$PAGE_ID,\"id\":\"s0.r0.c0.m0\"}")
|
||||||
|
echo "$NODE" | jq -e '.type == "divi/heading" and .shortform.text == "Handwerk mit Herz"' >/dev/null || fail "node-get: $NODE"
|
||||||
|
echo "$NODE" | jq -e '.attrs["title.innerContent"].desktop.value == "Handwerk mit Herz"' >/dev/null || fail "node-get attrs: $NODE"
|
||||||
|
MISSING=$(tool lit-divi-node-get "{\"post_id\":$PAGE_ID,\"id\":\"s9\"}")
|
||||||
|
[ "$(error_code "$MISSING")" = "not_found" ] || fail "unknown node: $MISSING"
|
||||||
|
pass "lit-divi-node-get: full form, short form, unknown node"
|
||||||
|
|
||||||
|
# 4. Catalog and design system live from Divi
|
||||||
|
CATALOG=$(tool lit-divi-catalog '{}')
|
||||||
|
echo "$CATALOG" | jq -e '(.modules | length) >= 90' >/dev/null || fail "catalog too small: $CATALOG"
|
||||||
|
BUTTON=$(tool lit-divi-catalog '{"module":"button"}')
|
||||||
|
echo "$BUTTON" | jq -e '.module.paths | index("button.innerContent") != null' >/dev/null || fail "catalog button: $BUTTON"
|
||||||
|
DESIGN=$(tool lit-divi-design-system '{}')
|
||||||
|
echo "$DESIGN" | jq -e '.colors[] | select(.id == "gcid-primary-color" and .placeholder == "{{color:primary}}")' >/dev/null \
|
||||||
|
|| fail "design system: $DESIGN"
|
||||||
|
pass "lit-divi-catalog and lit-divi-design-system read live data"
|
||||||
|
|
||||||
|
# 5. Rendered summary
|
||||||
|
SUMMARY=$(tool lit-page-render-summary "{\"post_id\":$PAGE_ID}")
|
||||||
|
echo "$SUMMARY" | jq -e '.headings[0].text == "Handwerk mit Herz"' >/dev/null || fail "render summary: $SUMMARY"
|
||||||
|
echo "$SUMMARY" | jq -e '[.texts[] | select(contains("Seit 1987"))] | length == 1' >/dev/null || fail "render texts: $SUMMARY"
|
||||||
|
pass "lit-page-render-summary renders Divi modules"
|
||||||
|
|
||||||
|
# 6. Hints and refusals
|
||||||
|
GET=$(tool lit-content-get "{\"post_id\":$PAGE_ID}")
|
||||||
|
echo "$GET" | jq -e '.content == null and (.hints[0] | contains("lit-divi-outline"))' >/dev/null || fail "content-get hint: $GET"
|
||||||
|
D4=$(tool lit-divi-outline "{\"post_id\":$D4_ID}")
|
||||||
|
[ "$(error_code "$D4")" = "unsupported" ] || fail "Divi 4 page must be unsupported: $D4"
|
||||||
|
pass "content-get points to the Divi tools; Divi 4 content is refused"
|
||||||
|
|
||||||
|
# 7. Reading changed nothing
|
||||||
|
[ "$(content_hash "$PAGE_ID")" = "$BEFORE" ] || fail "reading changed post_content"
|
||||||
|
pass "post_content unchanged after reading"
|
||||||
|
|
||||||
|
echo "ALL CHECKS PASSED"
|
||||||
@@ -45,6 +45,22 @@ final class InstructionsBuilderTest extends TestCase
|
|||||||
self::assertLessThan(2000, strlen($text), 'Instructions must stay short – they are loaded in every conversation.');
|
self::assertLessThan(2000, strlen($text), 'Instructions must stay short – they are loaded in every conversation.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testAddsTheDiviRuleOnlyWhenDiviIsActive(): void
|
||||||
|
{
|
||||||
|
$divi = Mockery::mock(Integration::class);
|
||||||
|
$divi->allows('id')->andReturn('divi');
|
||||||
|
$divi->allows('isAvailable')->andReturn(true);
|
||||||
|
$divi->allows('abilities')->andReturn([$this->ability('lit/divi-outline', true)]);
|
||||||
|
$core = $this->coreWith($this->ability('lit/guide', true));
|
||||||
|
|
||||||
|
$with = (new InstructionsBuilder(new IntegrationRegistry($core, $divi)))->build();
|
||||||
|
$without = (new InstructionsBuilder(new IntegrationRegistry($core)))->build();
|
||||||
|
|
||||||
|
self::assertStringContainsString('lit-divi-outline und lit-divi-design-system lesen', $with);
|
||||||
|
self::assertStringNotContainsString('lit-divi-outline und', $without);
|
||||||
|
self::assertLessThan(2000, strlen($with));
|
||||||
|
}
|
||||||
|
|
||||||
public function testListsPostTypesWhenAProviderIsGiven(): void
|
public function testListsPostTypesWhenAProviderIsGiven(): void
|
||||||
{
|
{
|
||||||
$core = $this->coreWith($this->ability('lit/guide', true));
|
$core = $this->coreWith($this->ability('lit/guide', true));
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use LIT\MCP\Abilities\Rev;
|
|||||||
use LIT\MCP\Abilities\WriteFailed;
|
use LIT\MCP\Abilities\WriteFailed;
|
||||||
use LIT\MCP\ChangeLog\ChangeLog;
|
use LIT\MCP\ChangeLog\ChangeLog;
|
||||||
use LIT\MCP\Integrations\WordPress\Content\ContentUpdateAbility;
|
use LIT\MCP\Integrations\WordPress\Content\ContentUpdateAbility;
|
||||||
|
use LIT\MCP\Integrations\WordPress\Content\PostPresenter;
|
||||||
use LIT\MCP\Integrations\WordPress\Content\PostWriter;
|
use LIT\MCP\Integrations\WordPress\Content\PostWriter;
|
||||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
@@ -116,9 +117,7 @@ final class ContentUpdateAbilityTest extends TestCase
|
|||||||
->execute(['post_id' => 5, 'rev' => Rev::ofPost($this->post), 'content' => '<p>Neu</p>']);
|
->execute(['post_id' => 5, 'rev' => Rev::ofPost($this->post), 'content' => '<p>Neu</p>']);
|
||||||
|
|
||||||
self::assertSame('lit_unsupported', $result->get_error_code());
|
self::assertSame('lit_unsupported', $result->get_error_code());
|
||||||
self::assertStringNotContainsString('lit-divi', $result->get_error_message());
|
self::assertStringContainsString(PostPresenter::DIVI_INACTIVE_NOTE, $result->get_error_data()['hint']);
|
||||||
self::assertStringContainsString('Divi-Werkzeuge folgen in einer späteren Version; bis dahin im Divi-Builder '
|
|
||||||
. 'bearbeiten.', $result->get_error_data()['hint']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testNothingToChangeIsAValidationError(): void
|
public function testNothingToChangeIsAValidationError(): void
|
||||||
|
|||||||
@@ -87,13 +87,20 @@ final class PostPresenterTest extends TestCase
|
|||||||
self::assertTrue($full['divi']);
|
self::assertTrue($full['divi']);
|
||||||
self::assertNull($full['content']);
|
self::assertNull($full['content']);
|
||||||
self::assertNull($full['featured_image']);
|
self::assertNull($full['featured_image']);
|
||||||
self::assertStringNotContainsString('lit-divi', $full['hints'][0]);
|
self::assertSame([PostPresenter::diviHint(false)], $full['hints']);
|
||||||
self::assertStringContainsString('Divi-Werkzeuge folgen in einer späteren Version', $full['hints'][0]);
|
|
||||||
self::assertInstanceOf(stdClass::class, $full['terms']);
|
self::assertInstanceOf(stdClass::class, $full['terms']);
|
||||||
self::assertInstanceOf(stdClass::class, $full['fields']);
|
self::assertInstanceOf(stdClass::class, $full['fields']);
|
||||||
self::assertSame(Rev::ofPost($post), $full['rev']);
|
self::assertSame(Rev::ofPost($post), $full['rev']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testDiviHintNamesTheDiviToolsOnlyWhenDiviIsActive(): void
|
||||||
|
{
|
||||||
|
self::assertStringContainsString('lit-divi-outline', PostPresenter::diviHint(true));
|
||||||
|
self::assertStringContainsString('lit-content-update', PostPresenter::diviHint(true));
|
||||||
|
self::assertStringNotContainsString('lit-divi', PostPresenter::diviHint(false));
|
||||||
|
self::assertStringContainsString(PostPresenter::DIVI_INACTIVE_NOTE, PostPresenter::diviHint());
|
||||||
|
}
|
||||||
|
|
||||||
public function testPasswordAndProtectedReadonlyMetaFieldsAreOmitted(): void
|
public function testPasswordAndProtectedReadonlyMetaFieldsAreOmitted(): void
|
||||||
{
|
{
|
||||||
$this->stubLinks();
|
$this->stubLinks();
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ final class RevisionsListAbilityTest extends TestCase
|
|||||||
|
|
||||||
self::assertTrue($result['items'][0]['divi']);
|
self::assertTrue($result['items'][0]['divi']);
|
||||||
self::assertFalse($result['items'][1]['divi']);
|
self::assertFalse($result['items'][1]['divi']);
|
||||||
self::assertSame([PostPresenter::DIVI_HINT], $result['hints']);
|
self::assertSame([PostPresenter::diviHint()], $result['hints']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRevisionsOfATrashedPostCanStillBeListed(): void
|
public function testRevisionsOfATrashedPostCanStillBeListed(): void
|
||||||
|
|||||||
Reference in New Issue
Block a user