Dashed/dotted/dashDot shape outlines in HTML preview rendered ~25%
thinner than the solid border path and shrunken relative to their
content box. The SVG overlay emitted stroke-width as unitless user
units (rendered as CSS px, not pt) and positioned the rect
edge-centered while CSS borders sit outside the box. Plus dashDot
relied on stroke-linecap=round painting a zero-length dot which
disappeared at thin strokes.
- HtmlPreview.Shapes.cs: plain-rect / ellipse / polygon / rounded-rect
SVG overlays emit stroke-width in pt; plain-rect + ellipse size in
real pt via calc(100% - {bw}pt), inset by bw/2 so the stroke sits
flush inside the content box. stroke-linecap round -> butt.
- HtmlPreview.Css.cs: DashTypeToSvgDasharray replaces 0.1 zero-length
dot trick with explicit {w} dot segments across dot, sysDot,
dashDot, lgDashDot, sysDashDot, sysDashDotDot, lgDashDotDot.
SH6. Adds a distinct gradientFill= prop for shape/textbox that accepts
C1-C2[-C3][:angle] syntax. fill= stays strictly single-color to avoid
overloading it with gradient semantics (FF0000-0000FF would otherwise
collide with flexible ARGB literal inputs). Emits <a:gradFill> with
two-or-three <a:gs> stops and a <a:lin ang='0' scaled='1'/> default.
CL15. Accepts top-level showLeaderLines=true as an alias of
datalabels.showleaderlines so pie/doughnut callers don't have to
reach for the qualified form. Writes <c:showLeaderLines val='1'/>
under every <c:dLbls> in the chart.
CL23. Extends the trendline setter to accept dotted sub-props at the
chart level that fan out across every series: trendline.label,
trendline.forecastForward, trendline.forecastBackward, trendline.order
(polynomial), trendline.period (moving average), trendline.intercept,
trendline.displayEquation, trendline.displayRSquared. Label also emits
<c:trendlineLbl> so Excel actually paints the label.
HTML preview emitted radial-gradient(circle, ...) with no size keyword,
so CSS defaulted to farthest-corner — the final stop landed at the
bounding-box corner instead of the shape edge. For a square shape with
gradient=radial:4ECDC4-FFFFFF, the right/bottom edge rendered tinted
teal (~#CAF0EE) instead of white.
OOXML <a:path path="circle"> with no explicit fillToRect/tileRect fills
to shape bounds; the CSS equivalent for a rectangular shape is
closest-side.
HTML preview hard-coded polygons for parametric shapes, ignoring
<a:avLst> adjustment values. rightArrow rendered a stubby head (30%
wide instead of default 50%) and star5's bottom points stopped at
y=91% with a pinched waist (inner ratio 0 instead of default 0.382).
Add RightArrowPolygon and Star5Polygon helpers that read adj1/adj2
from prstGeom and compute polygon vertices from OOXML defaults
(rightArrow: adj1=50000, adj2=50000; star5: adj=19098 -> ratio=0.382).
Also remove unused System.Text using directive.
add picture --prop hyperlink=URL (and alias --prop link=URL) previously
accepted the value but wrote no <a:hlinkClick> under <xdr:cNvPr> and no
drawing-level relationship; the picture was not clickable in Excel. The
picture Add branch now appends HyperlinkOnClick to the cNvPr:
external URLs get a new HyperlinkRelationship on the DrawingsPart and
reference its rId; targets starting with '#' use the Location attribute
with no rel, mirroring the cell-link fix in commit 60e1455.
add picture --prop crop.l/r/t/b=N (and compound --prop srcRect=l=N,r=N,...)
previously accepted the parameters but wrote no <a:srcRect> under
<a:blipFill>; real Excel rendered the image uncropped. BuildPictureBlipFill
now parses percent values (10 or 10%) and emits a SourceRectangle before
the <a:stretch> element with each side scaled to the 1/1000-pct units
Excel expects (10% → 10000).
add picture --prop opacity=N previously accepted the parameter but wrote
no <a:alphaModFix> under <a:blip>; real Excel rendered the image fully
opaque. BuildPictureBlipFill now parses opacity (percent 0..100, fraction
0..1, or '50%' string) and emits the alphaModFix element on the blip with
amt scaled to 0..100000. Values at 100%/1.0 (fully opaque) remain no-op.
Setting both dataLabel{N}.text and dataLabel{N}.delete on the same
point previously wrote two separate <c:dLbl idx="N"> elements and
left their children in non-schema order, which Excel rejects with
0x800A03EC.
HandleDataLabelDottedProperty now:
- Finds-or-creates a single dLbl per idx across successive Set calls
so text+delete merge into one element.
- Enforces "delete wins" semantics: when delete=true, strips tx,
numFmt, dLblPos, and show* siblings; a later text/pos/numFmt on an
already-deleted dLbl is a no-op.
- Reorders dLbl children into CT_DLbl schema order after every
mutation (idx, delete, layout, tx, numFmt, spPr, txPr, dLblPos,
show*, separator, extLst) so the chart validates regardless of
which property was set first.
Cells written with link= now reference the built-in Hyperlink cellStyle
(builtinId=8) with a blue underlined font, so they render as proper
hyperlinks in real Excel instead of plain black text.
ExcelStyleManager.EnsureHyperlinkCellStyle() idempotently creates the
Font (color 0563C1 + underline), the cellStyleXfs + cellXfs entries,
and the CellStyles Name="Hyperlink" BuiltinId=8 record. Both Add and
Set hyperlink paths invoke it when the cell has no user-provided
styling, preserving explicit font=/color= overrides.
Without mc:Ignorable="x14" on the worksheet root, Excel silently
drops the entire extLst block and no sparklines render. Add the
mc and x14 namespace declarations and merge x14 into the root
worksheet's MCAttributes.Ignorable when a sparkline is created.
set /SheetN/table[i] --prop ref=... now grows or shrinks the
<x:tableColumns> list so its Count matches the new column span in
the ref. Expanded columns get default ColumnN names (deduped).
Previously Excel rejected the file because tableColumns.count
mismatched the new ref width. See audit T5.
Adding a table whose ref intersects an existing table on the same
sheet now throws at Add time with a clear message. Previously both
adds succeeded silently and Excel rejected the file on open
(0x800A03EC). See audit T4.
TryStartResidentProcess only set RedirectStandardError=true, so posix_spawn
let the __resident-serve__ child inherit the caller's stdout. Any downstream
pipe (officecli create x.xlsx | tail, $(officecli open ...), CI redirects,
SDK stdout capture) then never saw EOF until the resident itself exited
(60s-12min idle), blocking the caller for the resident's full lifetime.
The Windows path was already guarded by SetHandleInformation. Add
RedirectStandardOutput=true so the child gets a fresh .NET-managed pipe on
Unix too, matching the commented intent. Resident writes no startup stdout
(Console.SetOut is sandboxed during ExecuteCommand) so there is no SIGPIPE
or backpressure concern.
Follow-up to #64. Move the colgroup/thead/table-width change detection
from the browser to WatchServer: when TableChromeSignature differs
between old and new HTML, skip the row-level excel-patch path and fall
through to the existing full-action body refresh.
This replaces the client-side _excelPatchChangesStructure heuristic
(cell-count comparison per patch, followed by fetch('/') re-download)
with a single regex compare at the true source of the diff. Drops the
extra network round-trip and catches column-width-only or thead-style
changes that cell counting misses.
- add TableChromeSignature next to ChartOverlaySignature
- guard the excel-patch branch in HandleWatchMessage with the signature
- remove _excelPatchChangesStructure function and its call site
- _replaceDocumentBody helper kept (still used by the full-action path)
- SH3: accept scheme color names (accent1-6, lt1/dk1, bg1/bg2, tx1/tx2,
hlink, folHlink) for sheet tabColor; maps to TabColor@theme index.
Query readback echoes the symbolic name rather than the raw index.
- RC1: row `height=` and col `width=` now accept unit-qualified
strings (40pt, 40px, 1cm, 0.5in). Row height stores points; col
width stores char units via 7-px-per-char approximation. Bare
numbers still accepted for backward compat.
- AF3: autofilter Add rejects garbage `range=` that doesn't parse
as a cell reference, preventing silently-broken OOXML files.
- C10: comment Add rejects a duplicate comment on a cell that already
has one, mirroring the table overlap-reject pattern. Users must
remove the existing comment first.
- P13: picture Add accepts `name=` to override the auto-generated
"Picture {id}" label stamped into xdr:cNvPr @name.
- P9: picture Add accepts `altText=` as alias for `alt=`.
- P11: picture Add accepts `title=` and stamps it into the
xdr:cNvPr @title attribute (distinct from @descr).
- H2: hyperlink Add + Set accept `tooltip=` / `screenTip=`
and write it to the Hyperlink @tooltip attribute. Set on tooltip
alone updates an existing hyperlink without touching its URL.
Previously `sort=desc` and `repeatLabels=true` only affected
write-time rendering / wrote a workbook-wide x14 default; neither
survived Excel reopen. Now:
- sort=asc|desc|locale|locale-desc stamps sortType="ascending" or
"descending" on each row pivotField.
- repeatLabels=true emits a per-field x14 repeatItemLabels="1" ext
(2946ED86-A175-432a-8AC1-64E0C546D7DE) on every outer row field
(all row fields except the innermost). Replaces the prior
workbook-wide fillDownLabelsDefault attribute, which was a
default-for-future-pivots rather than a knob for the current one.
CF2 — stopIfTrue=true now honored on all CF types (dataBar, colorScale,
iconSet, formulacf, cellIs/topN/aboveAverage/etc). Was silently dropped.
CF5 — 3-color colorScale accepts midpoint=N (percentile) instead of
hard-coded "50". Default 50 preserves existing behavior.
CF6 — dataBar showValue=false hides the numeric value under the bar.
OOXML default is true; now emits ShowValue=false when the user opts out.
V6 — validation errorStyle={stop,warning,information} wired onto
DataValidation.ErrorStyle. Was silently dropped on Add.
V7 — validation inCellDropdown / showDropDown: maps user-friendly
`inCellDropdown=false` to OOXML-inverted `showDropDown=true`
("hide the in-cell arrow"). Raw OOXML name also accepted.
T1 — table showHeader=false alias; sets HeaderRowCount=0.
T2 — table showBandedRows/showBandedColumns/showFirstColumn/showLastColumn
now land on TableStyleInfo. Were hard-coded to defaults.
T6 — table style name validated against built-in whitelist +
workbook customStyles; unknown names throw ArgumentException
instead of silently producing an unstyled table.
V1 — `validation type=list formula1="=$Z$1:$Z$5"` (leading `=`) was
being auto-wrapped in quotes, producing a literal-list validation
instead of a cell-range reference. Now strip the `=` and pass through
unquoted for both cell refs and named ranges.
T6 — table `style=BogusStyle` silently accepted, leading to Excel
ignoring the style at load. Now validate against the built-in
TableStyleLight/Medium/Dark1-28 + PivotStyle* names, plus any
workbook-level customStyles. Unknown styles throw ArgumentException.
CF2 — centralized `stopIfTrue=true` handling via ApplyStopIfTrue
helper (wired into every CF Add branch in a follow-up commit).
CL20 — Setting series trendline twice with different types should yield
two trendlines (Excel allows multiple per series). Previously each
Set cleared all trendlines. Now Set appends a new trendline; if a
trendline of the same type exists it is replaced in place
(idempotent). Pass `trendline=none` to clear.
Addresses 14 Excel-corruption and silent-drop bugs from the round-14
property audit. Each fix is self-contained; grouping keeps the helper
method additions next to their callers so a bisect lands on the full
change.
V2/V3/V4 (validation formula normalization):
- type=time formula1=HH:MM now converts to the time serial fraction
(0.375 for 09:00) instead of writing a colon-containing string that
Excel rejects with 0x800A03EC.
- type=date formula1=YYYY-MM-DD now converts to the date serial.
- type=custom formula1='=ISNUMBER(A1)' strips the leading '=' (OOXML
<x:formula1> expects the body without one).
N2/N3 (named range):
- refersTo= is now mapped as an alias for ref= (previously wrote empty
definedName content and corrupted the file).
- name= is validated against the OOXML §18.2.5 identifier rule:
starts with a letter/underscore, no spaces, must not parse as a cell
reference. Rejected at Add.
P2/O3 (picture/OLE raw-integer sizing):
- ParseAnchorDimensionEmu now treats bare integers that exceed the
sheet's column/row max (16384/1048576) as EMU instead of multiplying
them by the approximate cell size, which produced out-of-range
ToMarker coordinates that Excel refused to open.
P4/P5/SH6 (picture and shape rotation/flip):
- rotation=<deg> and flip=h|v|both are now applied to the Transform2D
on both Add paths. Accepts ±angles with wraparound and stores as
OOXML 60000ths-of-a-degree.
CL1 (chart legend): (already committed separately)
SP1 (sparkline winloss): winloss and win-loss accepted as aliases for
stacked, which is the OOXML enum for the Win/Loss sparkline style.
T3 (table totalsRowFunction tokens): per-column function tokens
('none,sum,average,count,max,min,stdDev,var,countNums,custom') now
route to the right TotalsRowFunctionValues enum AND the matching
SUBTOTAL function code. Previously only 'sum' was honored; the rest
silently fell through to sum.
CE18 (array formula literal braces): formula='{=SUM(...)}' now throws
at Add with a hint to use arrayformula=... instead. Previously produced
a file Excel rejected.
CF1 (topn rank): is now accepted as an alias for , so
writes rank='3' instead of the hardcoded default 10.
CF3 (CF type fall-through): wired belowAverage, containsBlanks,
notContainsBlanks, containsErrors, notContainsErrors, contains,
notContains, beginsWith, endsWith to their proper
ConditionalFormatValues enums. Previously all 9 silently became
type='dataBar'.
CF4 (CF timePeriod): dateoccurring accepts both (docs) and
(OOXML attribute spelling). Previously only
was read, so invoking with defaulted to 'today'.
C1 (comment \n): comment text with literal '\n' sequence is now
converted to LF before serialization, matching the shape text
behavior.
Round-14 audit bug H1: hyperlinks to any '#'-prefixed target (sheet cell
reference or named range) were serialized as TargetMode=External
relationships pointing at a URI starting with '#'. Real Excel rendered
them as broken external links. Internal targets now use the inline
<x:hyperlink location='...'/> form with no relationship, matching Excel's
canonical output.
Round-14 audit bug CL1: legend=topRight and aliases (tr, top-right) fell
through to the default 'bottom' switch case. Now mapped to
C.LegendPositionValues.TopRight so the legend renders in the requested
corner.
Data-validation formulas now accept user-friendly inputs: HH:MM[:SS] for
type=time converts to the Excel time-serial fraction, YYYY-MM-DD for
type=date converts to the Excel date serial, and a leading '=' on
type=custom is stripped (OOXML <x:formula1> expects the equals-less
form). Previously these caused Excel to reject the file with 0x800A03EC.
add table ref=A1:C4 totalRow=true used endRow (4) as the totals row,
overwriting the last data row with Total/SUM labels. Now when
totalRow=true is set, the ref is expanded by one row (A1:C5) and the
totals row is appended at row 5, so all data rows survive.
Previously, Add(cell, value=X) left any prior CellFormula on the same
cell intact, so the formula kept re-evaluating in html preview and on
open in Excel, overriding the literal the caller just set. The inverse
direction (formula after literal) already nulled CellValue; this
completes the symmetry so the last write wins.
Cells with font.underline=double wrote correct OOXML (Excel shows two
lines) but the html preview only emitted text-decoration:underline, so
browsers drew a single line. Now also emits text-decoration-style:double
for UnderlineValues.Double / DoubleAccounting.
Excel add shape ignored the preset= property and always wrote
prstGeom prst="rect", so preset=roundRect / ellipse / triangle / etc.
rendered as plain rectangles in Excel. Now parses preset via a token
table mirroring PowerPointHandler.ParsePresetShape so PPT and XLSX
accept the same preset vocabulary.
Two related watch bugs:
1. PatchSlideInHtml matched `data-slide="N"` which also hits the sidebar's
`<div class="thumb" data-slide="N">`. IndexOf found the thumb first, so
every "replace" patch rewrote a sidebar thumb and left the main
slide-container stale — user saw a white main view after each add/set.
Pin marker to `class="slide-container" data-slide="N"`.
2. IsWatching / GetExistingWatchPort used a pipe ping with Connect(100),
costing ~100ms per no-op and producing false negatives when the pipe
server was momentarily busy. Replace with a {pid,port} marker file
written on RunAsync, deleted in DoStopAsync, validated via
Process.GetProcessById. Probe cost drops from ~100ms to ~10µs, and the
pipe "ping" handler is removed (no remaining callers).
Three paths caused Excel to refuse opening officecli-generated tables:
1. Table name/displayName parsing as a cell reference (e.g. 'tbl1' →
column TBL=13584 row 1). Auto-suffix '_' rather than throwing —
officecli-derived defaults shouldn't surprise AI callers.
2. tableColumn name mirroring a numeric header cell ('30'). Excel only
accepts numeric column names when the header cell is typed as string,
so during add convert numeric header cells to inlineStr; tableColumn
name then matches the cell's visible value exactly.
3. Duplicate tableColumn names — auto-dedupe with numeric suffix.
Previous commit threw on (1) but silently let (2) through; replace with a
single SanitizeTableIdentifier helper used consistently for name,
displayName, and column names, plus the header-cell type fix.
Drop the standalone stderr hint on auto-started resident commands and
soften the `create` suffix. UX testing showed the verbose "background
process held — call officecli close when done" wording fired on the
wrong command (random mid-batch get) and created low-grade anxiety
without giving the caller a concrete trigger. Auto-close in 60s
already covers cleanup; other officecli commands work normally through
the resident regardless.
Excel refuses to open files whose table name or displayName looks like
a cell reference (e.g. 'tbl1' → column TBL=13584 row 1 within XFD1048576).
Validate on add and throw ArgumentException with guidance so callers pick
a safe name (Table1, tbl_1, MyTable) instead of producing a corrupt file.
Default td gridline was `border: 1px solid #e0e0e0`, which competes with
neighbours' explicit inline black borders through border-collapse. With
equal 1px solid on both sides, CSS resolves the tie by position (top-left
cell wins), so a style-0 or omitted cell anywhere in a bordered table
erases its bottom-right neighbours' black borders — visible gaps in the
grid (e.g. test-samples/报价单(1).xlsx G10, which is omitted from the
row XML entirely).
Paint gridlines with `box-shadow: inset` instead. box-shadow is outside
the border-collapse mechanism, so explicit cell borders always render at
boundaries regardless of whether the neighbour is styled. First-row top
and first-column left edges get extra insets via :first-of-type rules.
Covers all related edge cases in one pass: omitted <c> cells, rows with
StyleIndex=0 only, entirely omitted <row> elements, and column-default
`<col style>` cells — none of them can now suppress neighbours' borders.
SVG preview embeds HTML inside <foreignObject>, so the span's
font-family value ends up inside a live inline CSS string. HtmlEncode
encoded quotes only at the HTML attribute layer — the browser
unescapes them before the CSS parser sees them, letting a crafted
a:latin typeface like X';background:url(//evil)// inject arbitrary
CSS rules. Route the value through the same CssSanitize allowlist
that PowerPointHandler.HtmlPreview.Css uses.
- Tester R11-1: <sheetPr><tabColor rgb=...> flowed into inline
style="--tab-color:#{rgb}" unvalidated. A crafted sheet with a
non-hex rgb escapes the style attribute. Hex-gate before emission.
- Tester R11-2: Excel styles.xml font[0] name was interpolated into
the generated <style> block as font-family: '{defFontName}' with
no sanitization, letting theme authors break out of the CSS rule.
Route through CssSanitize like per-cell fonts already do.
- Tester R11-3: Word theme.xml supplemental CJK typeface flowed into
the font-family chain unsanitized while the sibling docFont went
through CssSanitize — an inconsistency a malicious theme could
exploit. CssSanitize it before interpolation.