Makros: Engine- und Store-Fundament (Zellen/Zeilen/Spalten/Tabellen/Blätter per Skript)

Erster von zwei Commits (UI folgt separat): neue, explizit ausgelöste
Makro-Sprache — dieselbe block-mode-Grammatik wie bei "Mit Schleife"-
Funktionen (FOR/IF/RETURN), plus ein neues Schreib-Vokabular (SETVALUE,
INSERTROW/DELETEROW/INSERTCOL/DELETECOL, ADDTABLE/RENAMETABLE/DELETETABLE,
ADDSHEET/RENAMESHEET/DELETESHEET), adressiert mit der bestehenden
Blatt!Tabelle::Zelle-Syntax.

Bewusst außerhalb der normalen Neuberechnung: ein Makro läuft nie aus einer
Zellformel heraus, sondern nur explizit getriggert (store.runMacro), und
mutiert ausschließlich über die bereits vorhandenen Store-Primitiven
(setCellInput, insertRow, addTable, addSheet, …) — Workbook/EngineHost/
DependencyGraph bleiben unverändert rein lesend. Ein ganzer synchroner
Makro-Lauf wird automatisch zu einem einzigen Undo-Schritt.

50 neue Tests (Engine + Store), inkl. eines gezielten Regressionstests für
die zentrale Korrektheits-Fallgrube: eine strukturelle Aktion mitten im
Lauf ersetzt die Workbook-Instanz komplett, jeder Host-Zugriff muss daher
frisch aufgelöst werden statt eine Referenz zu cachen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
wkh
2026-09-09 09:35:02 +02:00
co-authored by Claude Sonnet 5
parent c7b316de1b
commit e670f13077
13 changed files with 1188 additions and 36 deletions
+29
View File
@@ -15,6 +15,7 @@ import type {
CustomFunction,
DocumentState,
HAlign,
Macro,
NamedRange,
Sheet,
SheetObject,
@@ -100,6 +101,32 @@ function normaliseNamedRanges(x: unknown, tableIds: ReadonlySet<string>): NamedR
return out;
}
/** Defaults to `[]` -- absent in every document saved before this feature
* existed. Unlike a named range, a macro carries no `tableId`/other
* document-internal reference to validate against -- its body is a plain
* string, parsed and validated lazily (store/document.ts's `validateMacro`)
* only when actually edited/run, same lenient convention as a custom
* function's `body` (an import doesn't re-run every save-time check, only
* shape validation). */
function normaliseMacros(x: unknown): Macro[] {
if (!Array.isArray(x)) return [];
const out: Macro[] = [];
for (const m of x) {
if (
!m ||
typeof m !== 'object' ||
typeof (m as { id?: unknown }).id !== 'string' ||
typeof (m as { name?: unknown }).name !== 'string' ||
typeof (m as { body?: unknown }).body !== 'string'
) {
continue;
}
const rec = m as Macro;
out.push({ id: rec.id, name: rec.name, body: rec.body });
}
return out;
}
const HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
function colorOrUndef(x: unknown): string | undefined {
return typeof x === 'string' && HEX_COLOR.test(x) ? x : undefined;
@@ -375,6 +402,7 @@ export function validateDocument(x: unknown): DocumentState {
csvDelimiter: doc.csvDelimiter === ';' ? ';' : ',',
customFunctions: normaliseCustomFunctions(doc.customFunctions),
namedRanges: normaliseNamedRanges(doc.namedRanges, tableIds),
macros: normaliseMacros(doc.macros),
};
}
@@ -439,6 +467,7 @@ export function migrateToV2(x: unknown): DocumentState {
csvDelimiter: ',',
customFunctions: [],
namedRanges: [],
macros: [],
};
}