Skip to content

Operationen ​

Eine Operation ist eine Änderung an einem Template: einen Block hinzufügen, verschieben, löschen oder ändern, die Einstellungen ändern oder den ganzen Inhalt ersetzen. Dieselben sechs Operationen sind überall unterwegs, wo sich ein Template außerhalb eines einzelnen Editors ändert:

  • Zusammenarbeit — ein Editor mit init({ collaboration }) schickt jede Änderung als Batch von Operationen an Ihr Backend und wendet die Batches anderer an.
  • Die CLI — template-tools edit wendet sie auf eine Template-Datei an.
  • Templatical Cloud — speichert jeden Batch mit seiner Revision, und mit seinem MCP-Server verbundene KI-Agenten bearbeiten mit demselben Vokabular über edit_template.

applyOperations aus @templatical/types (MIT) wendet sie an. Der Server von Templatical Cloud verwendet eine Engine mit denselben Regeln, und gemeinsame Testfälle halten beide identisch.

Die sechs Operationen ​

json
{"op": "addBlock",       "block": {"id": "b2", "type": "paragraph", "content": "<p>Hi</p>", "styles": {"padding": {"top": 0, "right": 0, "bottom": 0, "left": 0}}}, "parent": null, "after": "b1"}
{"op": "moveBlock",      "blockId": "b2", "parent": {"sectionId": "s1", "column": 0}, "after": null}
{"op": "deleteBlock",    "blockId": "b2"}
{"op": "updateBlock",    "blockId": "b1", "set": {"content": "<p>Hello</p>"}, "unset": ["visibility"]}
{"op": "updateSettings", "set": {"width": 640}, "unset": ["preheaderText"]}
{"op": "setContent",     "content": {"blocks": [], "settings": {"width": 600, "backgroundColor": "#ffffff", "textColor": "#1a1a1a", "linkUnderline": true, "fontFamily": "Arial", "locale": "en"}}}

Die Annahme einer Operation durch die Engine ersetzt keine Schema-Validierung: Führen Sie template-tools validate für das resultierende Template aus.

Ein Batch ist eine Liste von Operationen. Er wird der Reihe nach angewendet, jede Operation sieht die vorherigen, und alles oder nichts: Wird eine abgelehnt, gilt keine Operation des Batches.

Positionen ​

Positionen nennen einen Nachbarblock, nie einen Index, sodass eine Operation dasselbe bedeutet, auch wenn vorher die Änderung eines anderen ankommt.

  • parent ist null für die oberste Ebene oder {"sectionId": "…", "column": 0} für eine Spalte einer Sektion. column ist ein 0-basierter Index in die children der Sektion.
  • after ist die ID des Blocks, hinter dem der Block landet, oder null, damit er als erster landet. Bei moveBlock zählen die Nachbarn im Zielcontainer ohne den verschobenen Block.
  • parent und after sind bei addBlock und moveBlock Pflicht, auch wenn sie null sind.

Sektionen und Spalten ​

  • Eine Sektion steht nur auf der obersten Ebene. Eine Sektion in eine Spalte hinzuzufügen oder zu verschieben wird abgelehnt, ebenso eine neue Sektion mit einer Sektion unter ihren Kindern.
  • deleteBlock auf einer Sektion löscht ihre Blöcke mit.
  • updateBlock ersetzt jeden Schlüssel in set vollständig, ein styles-Objekt eingeschlossen, und entfernt die Schlüssel in unset. id, type und children lassen sich weder setzen noch entfernen, und columns einer Sektion lässt sich nicht entfernen.
  • set: {"columns": …} auf einer Sektion passt ihre children an die Spaltenzahl des Layouts an: "1" hat eine Spalte, "3" hat drei, jedes andere Layout ("2", "2-1", "1-2") hat zwei. Beim Vergrößern kommen leere Spalten hinzu. Beim Verkleinern fallen hintere Spalten nur weg, wenn sie leer sind; verschieben Sie ihre Blöcke also zuerst.
  • Ein neuer Block braucht eine nicht leere String-id und einen String-type, und jede ID darin, die Kinder einer neuen Sektion eingeschlossen, muss im Template neu sein.

Vorbedingungen ​

Jede Operation außer addBlock und setContent kann expect mitführen, das gelten muss, bevor sie angewendet wird. Bei einer Abweichung wird die Operation mit precondition_failed abgelehnt. Rückgängig nutzt das: Ein Rückgängig-Schritt, den die Änderung eines anderen überholt hat, wird abgelehnt, statt sie zu überschreiben.

  • updateBlock und updateSettings: expect ordnet Schlüsseln ihre erwarteten aktuellen Werte zu. null passt zu einem null-Wert oder einem fehlenden Schlüssel.
  • moveBlock: expect.parent und expect.after sind die aktuelle Position des Blocks. Ein fehlender Schlüssel zählt als null.
  • deleteBlock: expect.block ist der ganze aktuelle Block, Kinder eingeschlossen, exakt verglichen.
  • Werte werden wie JSON verglichen: Die Reihenfolge der Objektschlüssel spielt keine Rolle, Arrays werden der Reihe nach verglichen und Zahlen nach Wert, sodass 1 gleich 1.0 ist. set, unset und expect mit dem Wert null zählen als nicht angegeben.

Sperren ​

Ein Backend übergibt die Sperren, die es hält, und die handelnde Sitzung: applyOperations(content, ops, { locks, session }), mit locks als [{ blockId, sessionId }].

  • updateBlock, moveBlock und deleteBlock auf einem Block, den eine andere Sitzung hält, werden mit lock_held abgelehnt. Beim Verschieben oder Löschen einer Sektion zählt auch jeder Block darin.
  • setContent wird mit locked_by_others abgelehnt, solange eine andere Sitzung eine Sperre hält.
  • Mit session gleich null (eine serverseitige Änderung) gehört jede Sperre jemand anderem.

Locale-Operationen ​

addLocale, removeLocale, setDefaultLocale, updateBlockLocale und updateSettingsLocale verwalten Sprachversionen im gemeinsamen Baum des SDK. Aktualisieren Sie Renderer, Validator und Collaboration-Backend, bevor Sie lokalisierte Inhalte speichern.

Der folgende Batch beginnt mit Absatz b1 mit <p>Original</p>, Standardsprache en, Richtung ltr und ohne Sprachversionen. Er fügt Deutsch hinzu, schreibt einen deutschen Wert, blendet den Block auf Deutsch aus, überschreibt die Richtung und entfernt dann Deutsch samt seinen Ebenen. sourceHash ist sourceHash("<p>Original</p>").

json
{"op":"addLocale","locale":"de"}
{"op":"updateBlockLocale","blockId":"b1","locale":"de","text":{"content":{"value":"<p>Hallo</p>","sourceHash":"f898b751"}},"hidden":true}
{"op":"updateSettingsLocale","locale":"de","set":{"direction":"rtl"}}
{"op":"removeLocale","locale":"de","expect":{"blocks":{"b1":{"text":{"content":{"value":"<p>Hallo</p>","sourceHash":"f898b751"}},"hidden":true}},"settings":{"set":{"direction":"rtl"}}}}
OperationVertrag
addLocalelocale: string; position?: number fügt in settings.locales ein, standardmäßig am Ende. Die Position muss eine Ganzzahl von 0 bis zur aktuellen Länge sein. Standard (settings.locale ?? "en") und vorhandene Versionen werden ohne Beachtung der Groß-/Kleinschreibung abgelehnt.
removeLocalelocale: string; expect?: { blocks: Record<string, BlockLocaleLayer>; settings?: SettingsLocaleLayer } vergleicht alle Ebenen exakt. blocks enthält Ebenen nach Block-ID; eine fehlende ID bedeutet keine Ebene. Das separate Feld settings enthält die Einstellungsebene; fehlt es, gibt es keine. expect: { blocks: {} } verlangt keine Block- oder Einstellungsebenen, auch bei einem Block namens "settings". Entfernt die Sprache und alle Block- und Einstellungsebenen, auch in Spalten.
setDefaultLocalelocale: string; ein exakter Tag aus settings.locales. Übernimmt Text und Eigenschaften als Basis; die bisherige Standardsprache erhält die Listenposition und eine Ebene für abweichende Werte sowie nicht leeren übersetzbaren Text, einschließlich Lücken. Fehlende Felder behalten ihre Basiswerte und eine aktuelle Textkopie für die bisherige Standardsprache. Kollidiert mit jeder Sperre einer anderen Sitzung (locked_by_others).
updateBlockLocaleblockId: string; locale: string; text?: Record<string, LocalizedText>; set?: Record<string, unknown>; unset?: string[]; hidden?: boolean; expect?: LocaleLayerExpect. Mindestens ein Änderungsfeld muss vorhanden sein.
updateSettingsLocalelocale: string; text?: Record<string, LocalizedText>; set?: Record<string, unknown>; unset?: string[]; expect?: Omit<LocaleLayerExpect, "hidden">. Mindestens ein Änderungsfeld muss vorhanden sein. Kein Ausblendestatus und keine Sperren.

locale muss isLocaleTag erfüllen: /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/. Tags außerhalb dieses Musters, darunter x-acme und i-klingon, werden abgelehnt. Jede Operation außer dem Hinzufügen verlangt den exakten Tag in settings.locales; ungültige Tags und unbekannte Versionen ergeben invalid_operation.

Basisupdates entfernen verwaiste Locale-Einträge; ihre Inverse stellt zuerst die Basis und dann die entfernten Ebenenpfade wieder her. Locale-set-Overrides lehnen columns bei Sektionen, items bei Menüs, rows und rows.<id>.cells bei Tabellen sowie icons bei Social-Blöcken mit invalid_operation ab. Eigenschaften einzelner Elemente und vollständige benutzerdefinierte fieldValues.<key>-Overrides bleiben erlaubt.

LocalizedText ist { value: string; sourceHash: string }. Textpfade müssen TRANSLATABLE_FIELDS des Blocktyps oder SETTINGS_TRANSLATABLE_FIELDS (preheaderText) für Einstellungen entsprechen und in der Basis auflösbar sein. Pfade verwenden Element-IDs, etwa items.i1.text; IDs von Menüeinträgen, Social-Icons, Tabellenzeilen und Tabellenzellen dürfen weder . noch / enthalten. Ein set-Pfad muss auflösbar sein oder einen vorhandenen Objekt-Elternpfad haben. Prototypsegmente (__proto__, constructor, prototype) und leere Segmente werden abgelehnt. Block-Overrides dürfen nicht mit id, type, children oder l10n beginnen; Einstellungs-Overrides nicht mit locale, locales oder l10n.

Die Engine wendet zuerst unset an (auf beide Maps), dann text, dann set, dann hidden. Ein Pfad in einer Map wird aus der anderen gelöscht. hidden: false entfernt das Flag. hidden: true auf dem slot-Block eines Layouts oder einem Block, der ihn enthält, wird mit invalid_operation, der blockId des Blocks und A layout's slot block, and any block that contains it, can't be hidden. abgelehnt, weil das Layout diese Sprache sonst nicht zusammensetzen könnte. Aus demselben Grund lehnen moveBlock und addBlock ab, den Slot oder einen Block, der ihn enthält, unter eine Sektion zu setzen, die eine Sprache ausblendet, und addBlock lehnt einen neuen Block ab, dessen eigene Ebenen den enthaltenen Slot ausblenden: invalid_operation, die blockId des gesetzten Blocks und A layout's slot block can't be placed inside a block that a language hides. Leere Maps, Ebenen und Lokalisierungscontainer werden weggelassen; das Entfernen der letzten Version lässt settings.locales weg.

set: { path: null } speichert einen löschenden Override: Die Eigenschaft fehlt in der resolveLocale-Projektion dieser Sprache. unset: [path] entfernt den Override, sodass wieder der Wert der Standardsprache gilt. Ebenen speichern niemals undefined; undefinierte text- oder set-Einträge und Texteinträge ohne die Strings value und sourceHash werden mit invalid_operation abgelehnt. Jeder unset-Pfad muss nicht leer sein und isSafePath erfüllen, auch wenn der Basispfad nicht mehr existiert; sichere verwaiste Pfade bleiben entfernbar.

Standardsprache ​

createSetDefaultLocaleOperation(content: TemplateContent, locale: string, definitions?: readonly CustomBlockDefinition[]): SetDefaultLocaleOperation erstellt die Operation; definitions ist standardmäßig []. Für benutzerdefinierte text- und textarea-Felder übergeben Sie ihre Definitionen. Die optionale Eigenschaft customTextFields?: Record<string, string[]> ordnet IDs benutzerdefinierter Blöcke vorhandene sichere fieldValues.<key>-Stringpfade zu. Sie reist mit der Operation, damit jede Replik ohne lokale Definitionen dieselben Kopien erhält. Fehlende oder nicht benutzerdefinierte Blöcke sowie unsichere, fehlende oder nicht als String auflösbare Pfade werden mit invalid_operation abgelehnt. Die Map-Schlüssel __proto__, constructor und prototype werden ebenfalls abgelehnt, auch wenn ein benutzerdefinierter Block diese ID hat. Der Builder lehnt eine solche ID mit demselben Fehler und opIndex: 0 ab, wenn er Textpfade dafür erfassen würde; benennen Sie die Block-ID vor dem Erfassen dieser Pfade um. Ohne Metadaten erkennt die Engine benutzerdefinierte Textpfade nur an vorhandenen text-Einträgen; andere benutzerdefinierte Lücken werden nicht erkannt. Der Kern stellt editor.createSetDefaultLocaleOperation(locale) mit den registrierten Definitionen bereit; die visuelle Bestätigung verwendet ihn. Im CLI übergeben Sie edit --definitions definitions.json.

{ "op": "setDefaultLocale", "locale": "de" } macht eine vorhandene deutsche Version zur Standardsprache. Für Blöcke jeder Tiefe, einschließlich Layout-Wrappern, und Einstellungen werden text-Werte und set-Überschreibungen zur Basis (null entfernt eine Eigenschaft). Abweichender Text der bisherigen Standardsprache wird als text mit sourceHash der neuen Basis gespeichert; andere abweichende Werte als set. Gleicher nicht leerer übersetzbarer Text, einschließlich Lücken ohne Text in der übernommenen Version, bleibt als aktueller text-Eintrag in der Ebene der bisherigen Standardsprache, gestempelt mit dem sourceHash der neuen Basis. Eine spätere Bearbeitung der Basis markiert diese Kopie als veraltet und bewahrt die E-Mail der bisherigen Standardsprache. Unveränderte leere Standardwerte sind keine Lücken und erhalten keinen Eintrag. Lücken behalten die alte Basis; nach der Übernahme weist nichts mehr auf deren unübersetzten Text hin. Übersetzen Sie diese daher zuerst mit Übersetzen oder strings export / strings import, HTML-Blöcke jedoch von Hand, weil diese Werkzeuge sie ausschließen. Nach der Übernahme von Deutsch statt Englisch listet validate --locale en diese kopierten Felder nicht mehr als localization.untranslated. settings.locale ändert sich; die bisherige Standardsprache übernimmt den Index der gewählten Version in settings.locales.

Aktueller Text anderer Sprachen erhält den Hash der neuen Basis. Veralteter Text behält seinen gespeicherten Hash; entspricht dieser dem übernommenen Wert, wird der Text aktuell. Fehlt der neuen Basis ein Textpfad, werden der bisherige Standardwert und Texte anderer Sprachen zu set-Einträgen ohne Veraltungsprüfung. Eigenschaftsüberschreibungen, deren übergeordnetes Objekt entfällt, werden am nächsten beschreibbaren Vorfahren zusammengefasst. Der Wert ist der zuvor projizierte Teilbaum dieser Sprache einschließlich geerbter Eigenschaften und Überschreibungen. Ein benutzerdefinierter Block verliert renderedHtml nur, wenn sich seine fieldValues ändern.

localeHiddenBlocks(content, locale): string[] listet in Tiefenreihenfolge die IDs der Blöcke, deren eigenes l10n[locale].hidden true ist; es sucht auch in ausgeblendeten Blöcken, sodass ein ausgeblendetes Kind eines ausgeblendeten Abschnitts separat erscheint und ein eigenes hidden: false braucht. Für Standard- und unbekannte Sprachen ist das Ergebnis []. defaultLocaleGaps(content, locale, definitions?): string[] listet übersetzbare Feld-IDs mit nicht leerem Standardwert, die mangels Text oder anwendbarer Eigenschaftsüberschreibung die alte Basis behalten. Veralteter Text wird übernommen und zählt nicht als Lücke, ebenso wenig wie ein leerer Standardwert, wie bei localeCoverage. Beide Helfer sind rein; übergeben Sie Definitionen, um Textfelder benutzerdefinierter Blöcke mitzuzählen.

Eine unbekannte Version oder die aktuelle Standardsprache erzeugt invalid_operation: "<locale>" is not a language version of this template. Ausgeblendete Blöcke erzeugen invalid_operation, die erste ausgeblendete blockId und <count> blocks are hidden in "<locale>". Show them before making it the default. Jede Sperre einer anderen Sitzung lehnt sie wie bei setContent mit locked_by_others ab; onRemoteChange.locale ist für diese Operation null. Die Prüfungen laufen in dieser Reihenfolge: Tag, Version, ausgeblendete Blöcke, dann Sperren; ausgeblendete Blöcke gewinnen also vor einer Sperre. KI-Batches in einer Sprachansicht lehnen sie ab.

invertOperations stellt mit setContent den exakten vorherigen Inhalt einschließlich Hashes, übernommener Ebene und HTML-Cache wieder her. Für Rückgängig gilt daher dieselbe templateweite Sperrregel. Ein Snapshot kann nicht erwarten, was er überschreibt; mit Zusammenarbeit merkt sich der Editor deshalb auch den Inhalt, auf den ein solcher Rückgängig- oder Wiederholen-Schritt angewendet wird, und überspringt den Schritt (undoSkipped, precondition_failed), sobald dieser Editor danach einen Batch einer anderen Sitzung empfangen hat, gesperrt oder nicht. Einen Batch, den das Backend schon angenommen, aber noch nicht zugestellt hat, fängt das Backend ab: Es lehnt einen Batch mit einem setContent ab, dessen baseRevision nicht die aktuelle Revision ist (Zusammenarbeit). Das Rückgängigmachen von setContent funktioniert genauso. Ohne Batches anderer Sitzungen stellen Rückgängig und Wiederholen den Inhalt exakt wieder her. Senden Sie die explizite Operation über Collaboration; ein Snapshot-Diff erzeugt für eine Änderung der Standardsprache setContent.

Overrides löschen ​

Dieser separate Batch beginnt mit Deutsch bereits in settings.locales, Block b1 mit styles.backgroundColor: "#ffffff" und Einstellungen mit linkColor: "#1d4ed8". Die ersten beiden Operationen lassen diese Eigenschaften auf Deutsch fehlen. Die letzten beiden entfernen die löschenden Overrides und stellen die Standardwerte wieder her. Die Basiswerte bleiben unverändert.

json
{"op":"updateBlockLocale","blockId":"b1","locale":"de","set":{"styles.backgroundColor":null}}
{"op":"updateSettingsLocale","locale":"de","set":{"linkColor":null}}
{"op":"updateBlockLocale","blockId":"b1","locale":"de","unset":["styles.backgroundColor"]}
{"op":"updateSettingsLocale","locale":"de","unset":["linkColor"]}

LocaleLayerExpect ist { text?: Record<string, LocalizedText | null>; set?: Record<string, unknown>; hidden?: boolean }. Text- und Override-Prüfungen vergleichen den aktuellen Ebenenwert, fehlende Werte zählen als null. Eine Ausblendeprüfung vergleicht layer.hidden === true. Eine Abweichung ergibt precondition_failed. Block-Locale-Updates prüfen Form, benannten Block (block_missing), Pfade, Sperren, Vorbedingungen und dann die Änderung, zu der die Slot-Regel gehört.

Eine Sperre kann locale?: string | null tragen. Eine fehlende oder null-Locale sperrt Standard und Struktur. Ein Block-Locale-Update kollidiert mit der Sperre einer anderen Sitzung auf demselben Block und derselben Sprache oder auf der Struktur (lock_held, mit blockId). Verschiedene Sprachen können gleichzeitig bearbeitet werden. Strukturelle Block-Updates, Verschieben und Löschen kollidieren mit jeder Locale-Sperre auf den betroffenen Blöcken. Hinzufügen und Entfernen von Sprachen sowie Einstellungs-Locale-Updates ignorieren Sperren.

Nur Locale-Operationen ändern bestehende Lokalisierungscontainer: updateBlock lehnt l10n in set oder unset ab; updateSettings lehnt locales und l10n ab und verbietet das Setzen oder Entfernen von locale, solange Versionen existieren (invalid_operation). addBlock darf l10n mitbringen, setContent ersetzt alles.

invertOperations schützt das Rückgängigmachen. Die Umkehrung von addLocale verlangt leere Ebenen. Die Umkehrung von removeLocale stellt die alte Listenposition, dann Block- und Einstellungsebenen mit Prüfungen auf leere Pfade wieder her. Umkehrungen von Locale-Updates stellen beide Maps und den Ausblendestatus wieder her und erwarten die Werte der Vorwärtsoperation.

Prüfung ​

Führen Sie pnpm --filter @templatical/types exec vitest run tests/operations.test.ts tests/invert-operations.test.ts tests/set-default-locale.test.ts tests/locale-hidden-blocks.test.ts für gemeinsame Fixtures und geschützte Umkehrungen aus. Führen Sie template-tools validate für das resultierende Template aus; die Engine-Annahme validiert das Schema nicht.

Fehler ​

Ein abgelehnter Batch wirft einen OperationError mit code und opIndex, dem 0-basierten Index der abgelehnten Operation.

CodeWannTemplatical Cloud antwortet
block_existsDie ID eines neuen Blocks oder eine ID darin gibt es im Template schon409
block_missingDer Block oder die Eltern-Sektion fehlt409
anchor_missingDer after-Block ist nicht im Zielcontainer409
precondition_failedexpect gilt nicht409
lock_heldEine andere Sitzung hält den Block409
locked_by_othersEine andere Sitzung hält eine Sperre, bei setContent oder einem Wechsel der Standardsprache409
invalid_operationDie Operation ist fehlerhaft oder kann nie gelten422

Welcher Fehler gewinnt ​

Verletzt eine Operation mehrere Regeln, entscheidet die erste fehlschlagende Prüfung:

  1. Ihre Form (invalid_operation): eine fehlerhafte Operation, ein fehlender Pflichtschlüssel, eine neue Sektion mit Eltern, id/type/children in set oder unset, columns einer Sektion in unset.
  2. Die Blöcke, die sie nennt (block_missing, block_exists).
  3. Sperren (lock_held, locked_by_others).
  4. Vorbedingungen (precondition_failed).
  5. Platzierung (invalid_operation): eine Sektion in eine Spalte, ein Block hinter sich selbst.
  6. Der Zielcontainer: block_missing für eine fehlende Eltern-Sektion; invalid_operation für Eltern, die keine Sektion sind, oder eine Spalte außerhalb des Bereichs.
  7. Der Anker (anchor_missing).
  8. Die Änderung selbst (invalid_operation): ein Layout, das Spalten mit Blöcken wegfallen ließe.

Element-IDs mit . oder / werden nur abgelehnt, wo eine Operation eine hinzufügt, und die Prüfung steht je Operation an einer anderen Stelle: addBlock prüft sie mit seiner Form (Schritt 1) und prüft gar keine Sperren; updateBlock prüft sie als die Änderung selbst (Schritt 8), nach Sperren und Vorbedingungen; setContent prüft sie nach seiner Form und vor den Sperren. Die gemeinsamen Fixtures halten jeden Fall fest.

Operationen anwenden ​

ts
import {
  applyOperations,
  OperationError,
  type Operation,
} from '@templatical/types';

const ops: Operation[] = [
  { op: 'updateBlock', blockId: 'b1', set: { content: '<p>Hello</p>' } },
];

try {
  const next = applyOperations(template.content, ops, {
    locks: [{ blockId: 'b4', sessionId: 'f7c1…' }],
    session: requestSessionId,
  });
  // `next` speichern
} catch (error) {
  if (error instanceof OperationError) {
    // error.code, error.opIndex
  }
  throw error;
}

applyOperations liefert ein neues Dokument und verändert das übergebene nie. applyOperationsInPlace ändert es direkt, für eine Kopie, die Sie verwerfen können. getColumnCount(layout) ist die Layout-Regel von oben.

In der CLI ​

bash
npx -y @templatical/template-tools edit emails/welcome.json \
  --op '{"op":"updateBlock","blockId":"button_1","set":{"backgroundColor":"#1d4ed8"}}'

--ops <file> nimmt ein JSON-Array für einen Batch. Siehe Template Tools.