// Copyright 2026 OfficeCLI (https://OfficeCLI.AI) // SPDX-License-Identifier: Apache-2.0 using System.Text.Json; namespace OfficeCli.Core; /// /// Public entry point for running a batch against an already-open /// without going through a CLI process or /// resident pipe — for embedders that link officecli directly (in-process /// hosts). Reuses the same batch-item dispatch and JSON /// envelope shape as the CLI `batch` command and the Node/Python SDKs, so a /// caller sees one consistent protocol regardless of transport. /// public static class BatchExecutor { /// /// Runs a batch and returns EXACTLY what the CLI `batch` command writes to /// stdout, so an in-process host is byte-identical to the CLI (and to /// what the watch server relays): with the /// `{success, data:{results, summary}}` envelope, without it the /// `[i] ...\n\nBatch complete: ...` text. This is the same output path the /// CLI command uses (RunNonResidentBatch → PrintBatchResults → WrapEnvelope, /// see CommandBuilder.Batch.cs); the earlier shortcut here emitted a bare /// results array with `json` hardcoded, diverging from every other transport. /// /// A JSON array of batch items — the same shape documented /// for the CLI `batch` command and the `BatchItem` SDK type. /// Mirrors the CLI `--json` toggle: structured envelope vs plain text. public static string ExecuteBatch(IDocumentHandler handler, string itemsJson, bool json, bool stopOnError = false) { try { var items = JsonSerializer.Deserialize(itemsJson, BatchJsonContext.Default.ListBatchItem) ?? new List(); var unrecognizedLatex = new List(); var results = CommandBuilder.RunNonResidentBatch(handler, items, stopOnError, json, unrecognizedLatex); // Reuse the CLI's own body formatter so both modes are byte-identical. using var sw = new System.IO.StringWriter(); CommandBuilder.PrintBatchResults(results, json, items.Count, sw); var inner = sw.ToString().TrimEnd('\n', '\r'); if (!json) return inner; // Same outer envelope the CLI batch command applies: unrecognized-LaTeX // warnings + outer success true only when every step succeeded // (CommandBuilder.Batch.cs). Judgment command — a single failed step // flips outer success to false. var warnings = new List(); foreach (var tok in unrecognizedLatex) warnings.Add(new CliWarning { Message = $"unrecognized_latex_command: {tok}", Code = "unrecognized_latex_command", Suggestion = "Check the command spelling; see https://katex.org/docs/supported.html for supported syntax.", }); var success = results.Count == 0 || !results.Any(r => !r.Success); return OutputFormatter.WrapEnvelope(inner, warnings, success); } catch (Exception ex) { return RenderTopLevelError(ex, json); } } /// /// Mirrors the SDKs' send(item) (as distinct from batch(items)): /// runs ONE batch-shaped item and returns EXACTLY what the CLI single command /// writes to stdout — with the standalone command's /// envelope (WrapEnvelopeText's `{success,data,message}` for text commands /// like set/add/remove, WrapEnvelope's `{success,data}` for data commands like /// get/query), without it the plain text. A failure throws instead of being /// captured per-item, matching the standalone command's exit contract. /// /// A single batch item object — the same shape as one /// entry in 's array, or the SDKs' `send(item)` argument. /// Mirrors the CLI `--json` toggle: structured envelope vs plain text. public static string ExecuteSend(IDocumentHandler handler, string itemJson, bool json) { try { var item = JsonSerializer.Deserialize(itemJson, BatchJsonContext.Default.BatchItem) ?? throw new ArgumentException("send: empty item"); var inner = CommandBuilder.ExecuteBatchItem(handler, item, json); if (!json) return inner; // Match the standalone command's envelope by inner shape — a JSON payload // (get/query/view/validate/…) wraps with WrapEnvelope; a plain-text // message (set/add/remove/…) wraps with WrapEnvelopeText (which adds the // `message` field the CLI emits). Content-based so a new command inherits // the right envelope without a hand-kept command→envelope map. var trimmed = inner.TrimStart(); return trimmed.StartsWith('{') || trimmed.StartsWith('[') ? OutputFormatter.WrapEnvelope(inner) : OutputFormatter.WrapEnvelopeText(inner); } catch (Exception ex) { return RenderTopLevelError(ex, json); } } /// /// Mirror of the CLI's shared WriteError(ex, json) (CommandBuilder): /// in JSON mode a failed command puts the {success:false, error:{…}} /// envelope on stdout — return it byte-identically. In text mode the CLI /// writes to stderr and leaves stdout empty; with no stderr channel in-process /// we surface the error by propagating it (the embedder host's failure /// signal), matching the prior throw-on-failure contract. /// private static string RenderTopLevelError(Exception ex, bool json) { // CONSISTENCY(error-wrap): same friendlier rendering the CLI applies to a // bare XmlException from an externally-corrupted OOXML part. var rendered = ex is System.Xml.XmlException xe ? new System.IO.InvalidDataException( $"Malformed XML in document part: {xe.Message} " + $"(the file appears to have a corrupted OOXML part).", xe) : ex; if (json) return OutputFormatter.WrapErrorEnvelope(rendered); throw rendered; } }