Add static pictures inside cells with XLSX roundtrip
Rebased onto main (conflicts with #83/#98 and main resolved by maintainer); the contributor's branch does not allow maintainer edits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Progress docs per craftrules standards/progress-docs.md: target-app parity, gaps, roadmap with alpha gate, readiness by audience (full / mainstream / essentials) with Opus 5.5 hours, and feature, format, hardware, UI and localization parity docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#101/#100 added tests that name temp dirs with SystemTime::now(); #125's
disallowed-methods lint (wasm has no clock) landed alongside them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-anchor loop zips objects with their measured corners instead of indexing; a sort moves
only objects anchored inside the sorted block on the other axis too (a picture at Z3 stays
put when A1:B4 is sorted). insert.picture defaults to Move but don't size (Excel's
editAs="oneCell", which the writer emits for it), and object.setAnchorMode errors on an
unknown or empty kind. Tests for each.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
edit.copy and edit.cut build and return the HTML table only with
{"html": true}; the UI asks for it when the host can publish HTML, so
MCP and control-channel responses are not bloated by up to 4 MB. Tests
cover the default, html:false and html:true.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every legacy .xls/.doc is a CFB/OLE2 file, so the signature alone isn't encryption. Walk the
compound file's directory (bounds-checked, bounded loops; raw UTF-16 scan when the structure
is unreadable) and report Encrypted only for an EncryptedPackage/EncryptionInfo stream;
other compound files are LegacyBinary with a "save as .xlsx" message. Tests build CFB bytes
in code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shape.setText refuses edits on a protected sheet (drawing objects are
locked; there is no edit-objects allowance yet) and text longer than
32,767 characters, like a cell. The editor doesn't open on a protected
sheet and its TextEdit carries the same character limit. Tests for both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blank cell and row repeats that run past 16384 columns / 1048576 rows
(padding from writers with larger sheets) are clipped instead of
rejecting the file; any data or merge past the edge is still an error.
convert() builds its Cell with ..Cell::value(value) so new Cell fields
take their defaults.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
smooth_run gives every segment the same number of sub-segments (cap / n, 1..100) so long
series are smoothed end to end within the point cap, and clamps the Catmull-Rom curve to
the plot box. List "smooth" in chart.addElement's params doc, drop the smooth_demo
example, and use the test sheet's own name in series refs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves the shared integration points for both formats: combined sniff,
file.open spec, file filters, MCP description, docs. open_bytes now lets
sniffed content (ODS or XLSB) win over the extension.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clip a file chart's series block to the sheet's used range and refuse blocks over the
insert.chart cell cap, so whole-column references can't build ~1M series.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add, Subtract, Multiply and Divide replaced every cell that didn't hold
a plain number with the pasted value: multiplying a column by 1 turned
numbers stored as text, headers and #N/A all into 1, and a formula into
a constant. As in Excel, numbers stored as text now take part (the
usual way to turn them into numbers), other text and errors stay, and
a formula is kept and combined with the pasted number: =(B3*2)*1.
current_region (Ctrl+A, and the range that sort, filter, AutoSum,
tables and Get Data use when one cell is selected) grew the region one
ring of cells per step and stopped after 10,000 steps, rescanning every
cell of the ring each time. With 20,000 rows of data and one cell
selected, data.sortDescending sorted only the first 10,001 rows and
left the rest as they were; a 100,000-row region took 30 s and was cut
at row 10,001.
It now grows one side at a time while the line next to that side has
data (diagonals included), checking only the cells of that line, with
no step limit.
jump() searched for the next filled cell by stepping (dr, dc); with
neither set, the step never moved, so selection.move {"jump":true}
from a blank cell of a sheet with content looped forever (found with
an agent call that left out dr and dc). It now returns the cell it
started from.
Every edit goes through cmd::commit, which read std::time::Instant::now()
up front for GRIDCRAFT_PROFILE timing, whether profiling was on or not.
wasm32-unknown-unknown has no clock: Instant::now panics there, and with
panic=abort the browser reports `RuntimeError: unreachable`. So in the
web app, typing 1 into A1 and pressing Enter trapped (#118). The native
apps and the CLI have a clock and were unaffected.
commit now reads the clock only when GRIDCRAFT_PROFILE is set, and
never on wasm. The other clock reads (NOW(), the current year for typed
dates, the UI's frame timer) already skip wasm.
Native tests can't see this trap, so clippy.toml now disallows
Instant::now and SystemTime::now. A clock read has to be gated off wasm
and allowed at the call, and `cargo clippy -D warnings` fails on the old
commit code. The existing, already-gated reads and the native-only perf
example allow it.
Checked by building the engine for wasm32-unknown-unknown into a small
cdylib that runs cell.set A1=1 on a new workbook, and running it in
Node: before the fix it traps with `RuntimeError: unreachable`, after
it returns normally.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chart.switchRowColumn did nothing on a chart read from a file: only
charts made by insert.chart keep their source range. Without one, the
command now takes the block the series' names, categories and values
span (when they're all on one sheet), and keeps it.
formula::parse("Sheet1!$D$1#") failed with "unknown error literal".
A # right after a cell reference is now the spill range operator: it
evaluates to the anchor's current spill range, or #REF! when the cell
doesn't spill (or its spill is blocked). Its users depend on the
anchor, so they recalculate when the spill grows or shrinks. Names can
use it.
Files have no # operator: like Excel, the writer stores A1# as
_xlfn.ANCHORARRAY(A1), and the parser reads that back as A1#.
The parser upper-cases function names, so =Double(4) showed as
=DOUBLE(4). Calls of LAMBDA names are now spelled like the name when
a formula is entered and when a file is read.
formulas.createFromSelection ignored bottom and right, and replaced any
workbook name with the same label without asking. Bottom-row and
right-column labels now name the cells above and to their left. An
existing name is kept and listed in the result's skipped, unless
replace is set (Excel asks; a command doesn't open dialogs).