diff --git a/src/officecli/CommandBuilder.View.cs b/src/officecli/CommandBuilder.View.cs index a3594e02c..ea9e0ed70 100644 --- a/src/officecli/CommandBuilder.View.cs +++ b/src/officecli/CommandBuilder.View.cs @@ -304,7 +304,7 @@ private static Command BuildViewCommand(Option jsonOption) } } else if (handler is OfficeCli.Handlers.ExcelHandler excelHandler) - html = RenderViaRegistry(excelHandler, "xlsx", new OfficeCli.Core.Rendering.RenderOptions())!; + html = RenderViaRegistry(excelHandler, "xlsx", new OfficeCli.Core.Rendering.RenderOptions { CellRange = clipArg })!; else if (handler is OfficeCli.Handlers.WordHandler wordHandlerGrid && gridCols != 0) { // Contact-sheet grid: tile every page into an N-column (or auto) diff --git a/src/officecli/Core/Rendering/RenderOptions.cs b/src/officecli/Core/Rendering/RenderOptions.cs index 285dcf32f..1dbb92d45 100644 --- a/src/officecli/Core/Rendering/RenderOptions.cs +++ b/src/officecli/Core/Rendering/RenderOptions.cs @@ -46,4 +46,9 @@ public sealed class RenderOptions /// Target raster height in px for Png/Pdf output. 0 = renderer default. public int RasterHeightPx { get; init; } + + /// Xlsx cell range ("Sheet1!A1:J20" or "/Sheet1/A1:J20") whose cells must + /// exist in the rendered grid, so a screenshot --range crop can resolve corner + /// cells beyond the used range. Null = used range only. Non-Excel renderers ignore it. + public string? CellRange { get; init; } } diff --git a/src/officecli/Handlers/Excel/ExcelHandler.HtmlPreview.cs b/src/officecli/Handlers/Excel/ExcelHandler.HtmlPreview.cs index 9ed645732..9e558cbeb 100644 --- a/src/officecli/Handlers/Excel/ExcelHandler.HtmlPreview.cs +++ b/src/officecli/Handlers/Excel/ExcelHandler.HtmlPreview.cs @@ -148,8 +148,12 @@ private string[] GetResolvedIndexedColors() /// Supports cell formatting (font, fill, borders, alignment), merged cells, /// column widths, row heights, frozen panes, and sheet tab switching. /// - public string ViewAsHtml() + /// Optional cell range ("Sheet1!A1:J20" or "/Sheet1/A1:J20"). + /// The named sheet's grid is extended to cover it, so screenshot --range corner + /// cells beyond the used range still resolve to real grid cells. + public string ViewAsHtml(string? ensureRange = null) { + var ensure = ParseViewRange(ensureRange, ignoreInvalid: true); using var _cul = InvariantCultureScope.Enter(); var sb = new StringBuilder(); var sheets = GetWorksheets(); @@ -242,7 +246,9 @@ public string ViewAsHtml() var pictures = CollectSheetPictures(worksheetPart); if (pictures.Count > 0) charts.AddRange(pictures); - RenderSheetTable(sb, sheetName, renderPart, stylesheet, renderStyles, charts, sheetIdx, showGridLines); + var ensureCell = ensure != null && string.Equals(ensure.Value.Sheet, sheetName, StringComparison.OrdinalIgnoreCase) + ? (ensure.Value.R2, ensure.Value.C2) : ((int, int)?)null; + RenderSheetTable(sb, sheetName, renderPart, stylesheet, renderStyles, charts, sheetIdx, showGridLines, ensureCell); sb.AppendLine(""); } sb.AppendLine(""); @@ -307,11 +313,12 @@ public int GetSheetIndex(string sheetName) return -1; } + // ==================== Sheet Rendering ==================== private void RenderSheetTable(StringBuilder sb, string sheetName, WorksheetPart worksheetPart, Stylesheet? stylesheet, RenderStyleArrays renderStyles, List<(int fromRow, int toRow, int fromCol, int toCol, double colOffsetPt, string html)>? charts = null, int sheetIdx = 0, - bool showGridLines = true) + bool showGridLines = true, (int Row, int Col)? ensureCell = null) { var ws = GetSheet(worksheetPart); var sheetData = ws.GetFirstChild(); @@ -463,6 +470,15 @@ private void RenderSheetTable(StringBuilder sb, string sheetName, WorksheetPart if (toCol > maxCol) maxCol = toCol; if (toRow > maxRow) maxRow = toRow; } + // Extend maxRow/maxCol to cover an explicitly requested screenshot range + // (issue #246): its corner cells must exist in the grid or the clip-crop + // union collapses to whichever corner is inside the used range. Subject + // to the same render caps below. + if (ensureCell != null) + { + if (ensureCell.Value.Row > maxRow) maxRow = ensureCell.Value.Row; + if (ensureCell.Value.Col > maxCol) maxCol = ensureCell.Value.Col; + } // Column cap: >200 cols is unusable in a browser table regardless of rendering mode. // Row cap: default 5000; overridable via OnGetHtmlRowCap when the rendering backend diff --git a/src/officecli/Handlers/Excel/ExcelHandler.View.cs b/src/officecli/Handlers/Excel/ExcelHandler.View.cs index df9b23d25..ec98997dd 100644 --- a/src/officecli/Handlers/Excel/ExcelHandler.View.cs +++ b/src/officecli/Handlers/Excel/ExcelHandler.View.cs @@ -80,13 +80,13 @@ public string ViewAsText(int? startLine = null, int? endLine = null, int? maxLin } /// - /// Parse a `view text --range` target ('Sheet1!A1:C10', '/Sheet1/A1:C10', + /// Parse an xlsx cell-range target ('Sheet1!A1:C10', '/Sheet1/A1:C10', /// or a single cell 'Sheet1!B5') into an inclusive 1-based rectangle. /// Corner order is normalized (C10:A1 works). The sheet must exist — - /// unknown names throw not_found listing the available sheets, mirroring - /// screenshot mode's range_target_not_found actionability. + /// unknown names throw not_found listing the available sheets. + /// Set for callers that also accept non-cell paths. /// - private (string Sheet, int R1, int C1, int R2, int C2)? ParseViewRange(string? range) + private (string Sheet, int R1, int C1, int R2, int C2)? ParseViewRange(string? range, bool ignoreInvalid = false) { if (string.IsNullOrWhiteSpace(range)) return null; var c = range.Trim(); @@ -96,9 +96,12 @@ public string ViewAsText(int? startLine = null, int? endLine = null, int? maxLin c = "/" + c[..bang] + "/" + c[(bang + 1)..]; var m = Regex.Match(c, @"^/([^/]+)/([A-Za-z]{1,3}\d+)(?::([A-Za-z]{1,3}\d+))?$"); if (!m.Success) + { + if (ignoreInvalid) return null; throw new Core.CliException( $"Invalid --range '{range}'. Expected 'Sheet1!A1:C10', '/Sheet1/A1:C10', or a single cell 'Sheet1!B5'.") { Code = "invalid_value" }; + } var sheet = m.Groups[1].Value; var names = GetWorksheets().Select(s => s.Item1).ToList(); diff --git a/src/officecli/Handlers/Rendering/BasicRenderers.cs b/src/officecli/Handlers/Rendering/BasicRenderers.cs index f42737a97..6e605be5f 100644 --- a/src/officecli/Handlers/Rendering/BasicRenderers.cs +++ b/src/officecli/Handlers/Rendering/BasicRenderers.cs @@ -58,7 +58,7 @@ public RenderResult Render(IRenderInput input, RenderOptions options) { var h = (input as HandlerRenderInput)?.Handler as ExcelHandler ?? throw new InvalidOperationException("ExcelHandler render input expected"); - return RenderResult.Html(h.ViewAsHtml()); + return RenderResult.Html(h.ViewAsHtml(options.CellRange)); } } diff --git a/src/officecli/ResidentServer.cs b/src/officecli/ResidentServer.cs index 46052de3a..12f0deef2 100644 --- a/src/officecli/ResidentServer.cs +++ b/src/officecli/ResidentServer.cs @@ -1640,7 +1640,7 @@ private void ExecuteView(ResidentRequest req, OutputFormat format) } } else if (_handler is OfficeCli.Handlers.ExcelHandler excelShotHandler) - html = CommandBuilder.RenderViaRegistry(excelShotHandler, "xlsx", new OfficeCli.Core.Rendering.RenderOptions())!; + html = CommandBuilder.RenderViaRegistry(excelShotHandler, "xlsx", new OfficeCli.Core.Rendering.RenderOptions { CellRange = rangeArg })!; else if (_handler is OfficeCli.Handlers.WordHandler wordShotGrid && gridCols != 0) { // Contact-sheet grid — mirrors CommandBuilder.View.cs's docx grid