Makros: GET/POST-Anfragen mit JSON-Ergebnis und FOR-Iteration

Erweitert die Makro-Sprache um Netzwerkzugriff: "x = GET(url)"/
"x = POST(url; body)" binden die geparste JSON-Antwort an eine Variable;
FOR item IN x DO ... END iteriert, wenn x ein JSON-Array ist, über dessen
Elemente; JSONGET(wert; pfad) liest ein Feld heraus (punktgetrennt,
Zahl = Array-Index) und funktioniert – anders als GET/POST – überall in
einem Ausdruck verschachtelt, nicht nur als ganze Zuweisung.

Da ein echter Netzwerkaufruf nicht synchron laufen kann, ohne den Tab
einzufrieren, bekommt der Makro-Interpreter einen separaten asynchronen
Zweig (execBlockAsync/execStatementAsync); der bestehende synchrone
Interpreter, den auch gewöhnliche "Mit Schleife"-Funktionen mitten in der
synchronen Neuberechnung nutzen, bleibt unangetastet. Netzwerkaufrufe
werden pro Makro einmalig mit Zielname bestätigt (dauerhaft gemerkt); die
bestehende "ein Lauf = ein Undo-Schritt"-Garantie, die bislang auf
Synchronität beruhte, wird jetzt explizit wiederhergestellt statt sich
weiter auf die Zeit-Koaleszenz zu verlassen.

Zwei echte Lücken erst beim eigenen Live-Test gefunden und behoben: (1)
JSONGET direkt in eine Aktion verschachtelt (statt erst einer Variable
zugewiesen) scheiterte zunächst an #NAME?; (2) JSONGET(...) direkt als
FOR-Quelle (ohne Zwischenvariable) scheiterte sowohl an der
Speicher-Zeit-Prüfung als auch zur Laufzeit, da substitute() einen
Funktionsaufruf nie auswertet.

Live gegen eine neue same-origin-Fixture (public/test-fixtures/sample.json)
verifiziert: echter GET, echter Bestätigungsdialog, FOR/JSONGET über die
echte Antwort, kein erneuter Dialog beim zweiten Lauf.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
wkh
2026-09-09 11:10:19 +02:00
co-authored by Claude Sonnet 5
parent a59fda25b0
commit 3ae3d81066
15 changed files with 1043 additions and 101 deletions
+14 -1
View File
@@ -115,6 +115,7 @@ function expand(
case 'namedref':
case 'param':
case 'omitted':
case 'json':
return node; // leaves -- nothing to expand
case 'unary':
return { ...node, operand: expand(node.operand, index, bodyCache, blockCache, depth) };
@@ -203,6 +204,7 @@ export function substitute(node: Node, bound: ReadonlyMap<string, Node>): Node {
case 'range':
case 'namedref':
case 'omitted':
case 'json':
return node;
case 'unary':
return { ...node, operand: substitute(node.operand, bound) };
@@ -328,7 +330,7 @@ export function declaredBlockNames(block: BlockNode, out: string[] = []): string
export function findInvalidForSource(block: BlockNode): ForNode | null {
for (const stmt of block.statements) {
if (stmt.kind === 'for') {
if (!isReferenceShaped(stmt.source)) return stmt;
if (!isReferenceShaped(stmt.source) && !isJsonGetCall(stmt.source)) return stmt;
const nested = findInvalidForSource(stmt.body);
if (nested) return nested;
} else if (stmt.kind === 'ifstmt') {
@@ -348,6 +350,17 @@ function isReferenceShaped(node: Node): boolean {
return node.kind === 'param' || node.kind === 'cell' || node.kind === 'range' || node.kind === 'namedref';
}
/** A `FOR` source may also be a `JSONGET(...)` call -- once substituted and
* evaluated it may resolve to a JSON array (see evaluator.ts's
* `execStatementAsync` `'for'` case), a possibility `isReferenceShaped`
* above doesn't cover since it's a genuinely new FOR-source *kind*, not a
* reference. Not shared with `findInvalidActionTarget`'s SETVALUE-target
* check -- a JSONGET result is a VALUE, never a place to write to, so a
* write target must stay reference-shaped only. */
function isJsonGetCall(node: Node): boolean {
return node.kind === 'func' && node.name.toUpperCase() === 'JSONGET';
}
/** {@link findInvalidForSource}'s counterpart for a macro's `SETVALUE`
* target (its first argument) instead of a FOR source -- same reasoning,
* checked at save time by store/document.ts#validateMacro. */