Two issues (#68):
1. MatchesRunSelector fell through to the generic XML attribute path for
`color`, which returns the OOXML-stored value without `#`. Comparing
against user input `#FF0000` always failed. Normalize both sides (strip
`#`, upper-case) to align with the project Color Rules (Add/Set accept
`#FF0000`, `FF0000`, named colors).
2. The table branch in Query only descended into cells for OLE and equation
selectors; run selectors were skipped, so red runs inside tables were
invisible to `query "run[color=#FF0000]"`. Added a run-selector branch
that mirrors the existing OLE traversal, emitting
/body/tbl[i]/tr[j]/tc[k]/p[m]/r[n] paths.
Text-overflow scanning now emits as DocumentIssue (Format+Warning, Id
prefix 'O') from ExcelHandler/PowerPointHandler ViewAsIssues, reusing
the existing CheckAllCellOverflow and CheckTextOverflow logic. The
standalone 'officecli check' command is removed — users migrate to
'officecli view <file> issues' (optionally '--type format').
Underlying CheckShapeTextOverflow/CheckCellOverflow/CheckAllCellOverflow
handler APIs are retained; they still back the inline overflow warning
emitted on add/set through the resident server.
The chart Set path accepted logBase=N and logScale=true (commit b5caba1)
but Add silently dropped yAxisScale/logScale/logBase because they were
handled only via the deferred-key path and yAxisScale wasn't registered.
Register yaxisscale as a deferred Add key and teach the logbase/logscale
setter case to accept 'log' / 'linear' as yAxisScale-style verbs. Add
now emits <c:logBase val="10"/> for yAxisScale=log or logScale=true,
and <c:logBase val="N"/> for logBase=N, matching Set.
When building a pivot from a source range, resolve each source column's
StyleIndex to its numFmtId and stamp it onto the cacheField. Without
this, a date-formatted column (numFmtId 164, yyyy-mm-dd) rendered in
the pivot as raw OADate serials (45306, 45337, ...) instead of the
intended date format. Reuses ResolveColumnNumFmtIds already used for
DataField.NumberFormatId.
freeze=A1 has colSplit=0 and rowSplit=0, which produced an invalid
<pane state="frozen" activePane="topRight"/> with no xSplit/ySplit.
Treat A1 as a no-op — any existing pane is already cleared earlier in
the branch, so simply break before emitting a new one.
When title (or axisTitle/catTitle) is passed a single-cell reference like
Sheet1!A1, emit <c:tx><c:strRef><c:f>Sheet1!$A$1</c:f></c:strRef></c:tx>
instead of a literal <a:t>Sheet1!A1</a:t>. Same fix family as the series
name strRef path — BuildChartTitle is shared by chart title and both axis
titles, so one call site fans out to all three.
The formulacf dxf builder only threaded font.color and font.bold into
the dxf Font; font.underline, font.italic, font.strike, font.size,
font.name were silently dropped. Extract a BuildFormulaCfFont helper
that appends every sub-prop (b, i, u, strike, sz, name/FontName, color)
and share it from the formulacf Add path. Users who pass the full set
now get a dxf Font that Excel renders correctly.
Excel expects <x:tableColumn> to carry <x:calculatedColumnFormula> so
newly appended rows auto-fill. When Add builds a table over a range
where every data row in a column carries a formula with the same
relative shape (e.g. =J2*K2, =J3*K3 …), stamp the first formula onto
the tableColumn. Detection uses a naive strip-digits heuristic — it
matches how users actually author calc'd columns without trying to
reimplement Excel's relative-reference normalizer.
When users pass series{N}.name=Sheet1!A1 the legend/tooltip text was
being written as literal <c:v>Sheet1!A1</c:v>, so Excel displayed the
string 'Sheet1!A1' instead of resolving it to the cell's value. Detect
cell-reference patterns in both Add (ApplySeriesReferences) and Set
(series.name case) and emit <c:tx><c:strRef><c:f>…</c:f></c:strRef></c:tx>
so Excel resolves the cell on open. Reader now also surfaces nameRef
alongside the existing valuesRef/categoriesRef format keys.
Shape TextBody stores one <a:p> per paragraph; the Get readback
flattened all <a:r> descendants into a single string, so multi-line
text like 'Line1\nLine2\nLine3' was returned as 'Line1Line2Line3'.
Join runs within each paragraph, then join paragraphs with '\n' so
multi-line shape text round-trips through Set/Get.
Set on slicer paths returned 'Element not found' because the Set
dispatch fell through to the generic XML fallback. Add a handler
that maps caption/style/rowHeight/columnCount/name onto the backing
X14.Slicer element via TryFindSlicerByIndex, mirroring the property
vocabulary exposed on Add.
conditional formatting rule=aboveAverage previously silently dropped
the stdDev= and equalAverage= properties. Both now flow onto the
cfRule attributes: stdDev=N (integer deviations from mean) and
equalAverage=true (include values equal to the mean). Also accepted
the aboveaverage= spelling as an alias for above= to mirror the
OOXML attribute name.
add shape --prop ref=B2 previously mapped to anchor=B2:B2 with
identical from/to markers, producing a zero-width/height invisible
shape in Excel. Expand single-cell refs to a 1-column x 1-row
rectangle (B2 -> B2:C3) so the shape has a visible extent. Range
refs (B2:D6) are unchanged.
labelRotation= was previously accepted silently but dropped. Wire it
onto the target c:catAx / c:valAx via a <c:txPr>/<a:bodyPr rot="N"/>
where N is degrees * 60000 (OOXML 60000ths-of-a-degree encoding).
Accept bare labelRotation= (both axes) plus xAxis./valAxis./yAxis.
labelRotation aliases.
Explicit type=string now forces an inline-string cell even when the
value is numeric-looking (e.g. value=123), instead of silently storing
a number. Explicit type=number with a non-numeric value now throws
ArgumentException instead of silently storing as string.
Excel silently drops effectLst children that violate DrawingML schema
order (blur → fillOverlay → glow → innerShdw → outerShdw → prstShdw →
reflection → softEdge). Previously shape Add appended outerShdw before
glow, so a shape with both shadow and glow rendered without the glow
halo despite being present in the raw XML.
Reorder the shape Add effect build to emit children in schema order,
and rewrite Set TrySetShapeEffect to InsertBefore the next-in-order
sibling when adding effects incrementally. Same fix applied to the
text-level effect block used by fill=none shapes.
When both value= and formula= are passed to Set or Add on the same
cell, formula wins (it is written last, clearing CellValue). Users who
typo'd or duplicated props previously had no indication the literal
value had been discarded.
Emit a stderr warning ("Both value= and formula= supplied — using
formula, value ignored.") from both the Add cell path and the Set
value/text path so the precedence is visible.
Setting a cell to an ISO datetime like "2024-03-15T10:30:00" with
type=date previously stored the literal string, so Excel rendered it
as text instead of a real date. Only date-only ("2024-03-15") and
numeric serials were converted.
Centralize the format list as IsoDateFormats + TryParseIsoDateFlexible
in ExcelHandler.Helpers.cs, accepting yyyy-MM-dd, yyyy-MM-ddTHH:mm,
yyyy-MM-ddTHH:mm:ss, yyyy-MM-dd HH:mm:ss, and the ...Z / .fff[Z]
variants. Add and Set cell-value paths route through the shared helper
so every entry point converts to a fractional OADate.
Excel refuses to open files containing cells whose text length exceeds
2^15-1 (0x800A03EC on save/open). Previously Add, Set, and CSV import
wrote oversized values silently, corrupting the file.
Add EnsureCellValueLength in ExcelHandler.Helpers.cs (internal const
MaxCellTextLength = 32767) and call it from the Add cell value path,
the Set cell value/text path, and the CSV/TSV import path. Rejection
is a clear ArgumentException naming the cell and the observed length.
Three gaps remaining from R14.
Hyperlinks now accept tooltip= alongside link= on both run and shape
Add / Set. ApplyRunHyperlink and ApplyShapeHyperlink set the Tooltip
attribute on HyperlinkOnClick.
link= gains internal-jump support:
- slide[N] creates a slide-to-slide relationship and emits
action="ppaction://hlinksldjump"
- firstslide / lastslide / nextslide / previousslide emit the
corresponding ppaction://hlinkshowjump?jump=... (PowerPoint-native)
ReadRunHyperlinkUrl resolves both forms back to the original string.
HTML preview wraps shape-level hyperlinks in an <a> tag. Shape
hlinkClick now lives on nvSpPr/cNvPr (canonical shape location) in
addition to runs so the HTML renderer can detect it from the shape
tree; runs still carry the inline anchor from R14 so text stays
clickable inline. External URLs get a wrapping anchor with
rel="noopener" target="_blank"; cursor:pointer affordance added.
Internal slide-jump links are intentionally not wrapped (no
navigable href in a static HTML preview).
Two CLI gaps carried from earlier rounds.
AddPlaceholder: `add /slide[N] --type placeholder --prop phType=...`
now creates a <p:sp> with <p:ph type="..."/> for any of the known
placeholder types (body, date, footer, slidenum, header, subtitle,
title, picture, chart, table, diagram, media, obj, clipart). Emits
an empty <p:spPr> so geometry and fonts inherit from the layout,
plus a minimal <p:txBody> that optionally prepopulates via a text=
property. Dispatch wired in Add.cs.
Group resize preserves scaling: when `set /slide[N]/group[M] width=`
or `height=` / `x=` / `y=` is applied and the group's
`TransformGroup.ChildExtents` / `ChildOffset` is null, snapshot the
current Extents / Offset into new ChildExtents / ChildOffset BEFORE
updating Extents / Offset. This establishes the ext≠chExt baseline
that PowerPoint needs to visibly compress the group's children
instead of resizing them 1:1 with the container.
Two CLI-surface gaps carried from earlier rounds.
Slide HeaderFooter toggle (R16 deferred): set /slide[N] now accepts
showFooter, showSlideNumber, showDate, showHeader. Each maps to the
corresponding <p:hf ftr/sldNum/dt/hdr> attribute on the slide element
(PowerPoint writes <p:hf> directly under <p:sld> when the per-slide
"Header & Footer" dialog toggles it, so we match).
Picture fill modes (R10 Cleanup-D): add picture --prop fill=... now
accepts stretch (default, non-regression), contain, cover, and tile.
contain/cover sniff the image's native pixel dimensions via
ImageSource.TryGetDimensions and compute <a:fillRect> insets (in
thousandths of a percent) — positive for letterbox, negative for
crop. tile emits <a:tile sx sy algn flip> with optional tilescale,
tilealign, tileflip sub-keys. Falls back to bare stretch when image
dimensions can't be sniffed.
Three small CLI-parity gaps surfaced across prior rounds.
Word underline= now accepts the full OOXML enum. NormalizeUnderlineValue
centralizes the mapping (wavy -> wave, dashdot -> dotDash, plus
double / thick / dotted / *Heavy variants / words). Previously the
handler rejected wavy despite PPTX accepting it.
Word firstLineIndent= accepts unit strings (2cm, 0.5in, 18pt) via
SpacingConverter.ParseWordSpacing. Bare twips still work (backward
compat). Range check > 31680 twips preserved.
Chart datalabels.* sub-keys (showvalue, showpercent, showcatname,
showsername, showlegendkey) now emit the corresponding c:show*
elements. Registered in DeferredAddKeys so they apply at Add time,
not only via post-build Set. ChartHelper is shared across PPTX and
Excel so the fix lands on both sides.
GetShapeNode now reads <a:xfrm rot/flipH/flipV> on the shape's Transform2D
and surfaces them as format.rotation (canonicalized to 0-360 degrees) and
format.flip (h/v/both), matching how Add accepts these properties.
When a cell has a formula and a cached non-numeric value but no explicit
DataType (t="") attribute, CellToNode previously defaulted to Number.
Excel writes t="str" on such cells, but external tools and our own
writer can leave the attribute off; infer String from the cached value
so Get matches Excel's effective type.
Post-2016 functions (SEQUENCE, FILTER, XLOOKUP, LET, LAMBDA, TEXTJOIN,
SORT, UNIQUE, etc.) must be emitted as `_xlfn.FUNC(...)` in the stored
OOXML formula; real Excel 365 shows `#NAME?` for the bare name. FILTER
additionally needs `_xlfn._xlws.`. Excel strips these prefixes back out
when displaying the formula, so the round-trip is transparent.
New helper `Core.ModernFunctionQualifier`:
- Qualify(formula): scans for function calls (identifier immediately
followed by `(`), skips quoted string literals and already-qualified
names, and prepends `_xlfn.` / `_xlfn._xlws.` for names in the
catalogue.
- Unqualify(formula): strips the prefix for readback / evaluation.
Applied at every CellFormula write site: Add cell (auto-detect `=`,
explicit formula=, arrayformula=), Set cell (formula, arrayformula),
Import. Applied at every read site: CellToNode formula readback,
GetCellDisplayValue formula fallback, and FormulaEvaluator so internal
evaluation keeps working against the qualified stored form.
Previously `add /Sheet1 --type chart --prop name=MyChart` silently dropped
the name — cNvPr.name always defaulted to chartTitle ?? "Chart". This made
chart drawings inconsistent with sheet / namedrange / picture / shape /
OLE, which all honor `name=`.
Fix: read `name=` from properties first, fall back to title for back-compat,
then "Chart". Applies to both regular charts and extended (chartEx) charts.
Databar conditional format previously emitted only the Excel 2007
<x:dataBar> element, which causes Excel to render negative values as
positive (rightward bars, no zero-axis, no red for negatives).
Now emit the paired x14 extension under the worksheet extLst:
- x14:dataBar with axisPosition, fillColor, negativeFillColor, axisColor
- x14:cfRule linked to the 2007 cfRule via {B025F937-...} extension GUID
- mc:Ignorable="x14" on the worksheet root (via new helper
EnsureWorksheetX14Ignorable, factored out of the sparkline path)
New user props:
- negativeColor=<hex> (default FF0000)
- axisColor=<hex> (default 000000)
- axisPosition=automatic|middle|none
Also: omit cfvo val="" attribute when type is min/max (Excel rejects
the empty attribute).
When a sheet is removed, formulas in other sheets that still reference
the deleted sheet by name kept their stale <x:v> (cached value). Real
Excel recomputes to #REF! on open, so officecli's Get was inconsistent.
- After sheet remove, walk remaining worksheets and clear CellValue on
any formula whose body contains the removed sheet name (bare or
single-quote wrapped).
- In CellToNode, suppress the evaluator-based cachedValue fallback when
the formula references a sheet that no longer exists in the workbook;
otherwise FormulaEvaluator.ResolveSheetCellResult silently returns 0
and we report a fake cached value where Excel would show #REF!.
Shape and textbox Add silently ignored ref=<cell>, so repeated calls
stacked all shapes at A1. cell, comment, and table already accept
ref= as the placement address; shape/textbox used anchor= instead,
which is undiscoverable without reading the source.
Map ref=B2 to anchor=B2:B2 (single-cell anchor) before the existing
anchor parser runs, unless an explicit anchor= is already provided.
This brings shape/textbox in line with the rest of Excel Add.
CloneNode preserved the source row's RowIndex and every cell's
CellReference (e.g. "A1","B1"), so the "cloned" row collided with
the source at the same index — Excel showed only one row and A2
rendered empty despite a "Copied to /Sheet1/row[2]" success message.
Recompute a fresh RowIndex from the target sheet (append to end, or
ShiftRowsDown when inserting at a position) and rewrite every cell's
CellReference to point at the new row. Styles (StyleIndex) and values
ride along with the clone, so the copy now preserves fill/bold/value
as expected.
Adds the LooksLikeFormulaBody helper used by the definedName Add path:
when the body is a function call or arithmetic expression (SUM(...),
A1+B1, IF(...), etc.) the CLI now flips <x:calcPr fullCalcOnLoad="1"/>
in workbook.xml so Excel computes the name on first open — previously
Excel displayed 0 until the user triggered a manual recalc.
Pure range refs (Sheet1!$A$1:$A$5) still do not force a recalc; they
evaluate lazily as references.
Per ECMA-376 §18.2.5, the content of <x:definedName> must not carry a
leading '=' — that form is specific to Excel's formula bar, not the
serialized OOXML. When users passed `--prop ref==SUM(Sheet1!A1:A5)`
(copied verbatim from the formula bar), Excel rejected the file on
open with 0x800A03EC.
Strip exactly one leading '=' before writing the definedName body.
When Add is given a parent path like /Sheet1/Z99 with --type cell and
no ref=/address= prop, the handler silently dropped the Z99 tail and
auto-assigned the next free cell in row 1 (so /Sheet1/A1 was written
instead). This mirrors the fix R4-1 landed for --prop ref=: now the
path tail is accepted as the target address too, with parity to the
`comment` case which already did this.
If both a cell-ref path tail AND --prop ref=/address= are supplied and
they disagree, the explicit prop still wins, but a warning is printed
to stderr so the caller notices the dropped path component.
The secondary valAx was emitted with axPos="r" but inherited the
default Crosses=autoZero from BuildValueAxis. Excel treats axPos as
advisory and renders the axis wherever it crosses the (hidden)
secondary catAx, so autoZero stacked both Y axes on the left edge.
Strip the inherited Crosses/CrossesAt and append Crosses=Maximum on
the secondary valAx so it binds to the right edge as intended. No
effect on primary axis or on charts without a secondary axis.
`border.all.color` was silently dropped because GetOrCreateBorder only
recognized the plain `color` shorthand, not the `all.color` subkey that
falls out of the `border.` prefix strip. Fan it out to every side just
like `color` does so users get a uniform border color without naming
each side individually.
Per-side `border.{top,left,right,bottom}.color` already worked and is
unchanged. Get readback already surfaces per-side colors canonically.
PickHeaderFooter branched on <w:titlePg/> but only consumed the
first-type bundle when it existed. A section with titlePg + a default
footer but no <w:footerReference w:type="first"/> fell through to the
default, leaking the default footer onto page 1. Word's actual
behavior is to emit no footer in that case, which matched the fixture
with a first-page header but no first-page footer definition.
On the first page of a section with titlePg, return bundle.First if
present, else the empty string — never the default. The continuation
pages still pick up the default header/footer, which matches Word's
pagination model.
Cell Add/Set rejected type=error with ArgumentException (CE16).
Standard Excel error tokens (#N/A, #DIV/0!, #REF!, #NAME?, #NULL!,
#NUM!, #VALUE!) could not be authored directly; users had to write
a formula that evaluated to the error.
Accept type=error|err in both Add (ExcelHandler.Add.cs) and Set
(ExcelHandler.Set.cs), emitting <x:c t="e"><x:v>{token}</x:v></x:c>
so Excel renders the cell as a native error value.
gradientFill=C1-C2[:angle] on chart Add (and per-series
seriesN.gradient/gradientFill=) was silently dropped because the
chart Add path only forwarded keys passing IsDeferredKey to
SetChartProperties. gradientFill was not in DeferredAddKeys and
series{N}.* dotted keys were never deferred at all.
- Add gradientFill alias for the existing gradient setter case and
per-series dotted handler (matches shape/textbox vocabulary used
by BuildShapeGradientFill).
- Route visual-effect series{N}.{gradient,marker,trendline,...}
keys through HandleSeriesDottedProperty by adding a deferred
subkey allowlist to IsDeferredKey. Build-time subkeys (.name,
.values, .ref, .color) are intentionally excluded so
ParseSeriesData / ParseSeriesColors keep owning them.
Word HTML preview already read <w:rFonts w:ascii> and emitted
font-family stacks for each run. The stacks ended on CJK fonts
(SimSun / 宋体 / Songti SC / STSong) with no generic family keyword,
so in any environment where those CJK fonts are not installed (e.g.
Playwright Chromium on Linux, most headless CI) every run fell back
to the browser's default sans-serif and the per-run font distinction
was erased — a Times-New-Roman run rendered identically to a plain
run.
Append a generic family ("serif" or "sans-serif") at the end of the
run's font-family stack. Pick it via a lightweight IsLikelySerif
heuristic that matches common serif names in Latin and CJK
(Times / Song / Ming / Mincho / Cambria / Georgia / Garamond /
宋 / 仿宋 / 明朝 / ...).
Also skip theme-font shorthand values starting with "+" (e.g.
+mn-lt, +mj-ea) matching the PPTX convention in
PowerPointHandler.HtmlPreview.Text.cs.
SanitizeTableIdentifier treated 'T1' (valid Excel table name that
happens to look like a cell reference) the same as the auto-default
'Table{id}' — it silently suffixed _ to yield 'T1_'. User-provided
names are now honored verbatim; only auto-derived defaults still
get the _ suffix as a safety net. displayName inherits the
user-provided flag from name so both land on 'T1', not 'T1_'.
Users naturally write `add /namedrange[MyRange] ...` to mirror the
selector notation used elsewhere, but the handler only looked at
--prop name= and failed with 'name property is required'. Extract
the identifier from the path bracket as the default name (integers
are preserved as index semantics for future use).
_knownPivotKeys was missing calculatedField / calculatedFields, so
even though ApplyCalculatedFields wired up the cacheField + dataField
+ calculatedFields block, the Add pipeline still warned 'UNSUPPORTED
props: calculatedField' to stderr. Add both keys to the known set and
strip trailing digits in CollectUnknownPivotKeys so numbered variants
(calculatedField1, calculatedField2, ...) also match. Expose the
check as a public IsKnownPivotProperty helper for tests.
Rows-only pivots with multiple data fields built a synthetic
single-bucket col axis labeled '__total__' and then rendered that
sentinel as the col-label row text, plus emitted 'Total <name>'
placeholder cells that bled into the columns to the right of the
pivot's data area. Skip the col-label row entirely for rows-only
layouts: the K data cells under the (empty) col field are already
the grand totals, so no extra labels or sentinels are needed.