Offline Office Editor
Overview
The C:/office project is a single-page, zero-backend Office editor. Three editors
(Excel grid, Word page, PPT stage) share one index.html + per-format engine
(excel.js, word.js, ppt.js). A build step inlines every library and script
into office-standalone.html (≈5.7 MB) so it runs by double-clicking — no
server, no CDN, no network.
The hard part is not the UI; it is making Excel/Word/PPT files round-trip
correctly. SheetJS (libs/xlsx.full.min.js) reads/writes the zip/XML but silently
drops many features (conditional formatting, comments, data validation, charts,
styles, freeze panes, hyperlinks). The project's solution is a OOXML post-processing
layer: SheetJS produces a baseline file, then small injectXxx functions rewrite
specific XML parts (worksheet, rels, [Content_Types].xml) to add what SheetJS
missed; reverse readXxx functions parse those parts back on open. Regression is
guaranteed by a jsdom harness that loads the real engine and asserts behavior.
When to use this skill
- The user asks to add/fix any feature in the C:/office editors.
- Any task to generate or edit
.xlsx/.docx/.pptxoffline, especially:
conditional formatting (color scale, data bar, formula/cellIs rules), formula
engine (absolute/cross-sheet refs, VLOOKUP/HLOOKUP/MATCH/INDEX/IFS), cell
comments, data validation, charts, hyperlinks, freeze panes, row/column
insert/delete, merged cells, custom styles. - When writing or debugging the jsdom regression tests for these features.
Core architecture (read this before editing)
Keep one mental model — it is the same for every feature:
- Single source of truth = plain JS structures, NOT the OOXML. For Excel:
datais a 2D array (ROWS × COLS); everything else is a parallel Map or
array keyed byr + ',' + c(per-cell:styleMap,fmtMap,commentMap,
borderMap,hyperlinks) or by a range/{r1,c1,r2,c2}(conditional
formattingcfRules, data validationdvRules, merges). Word/PPT use analogous
sheets[]/elements[]structures. - The editor renders from these structures (
renderCell,renderAll). The
file format is a serialization of them, never the source. - Two chain functions own the file boundary:
- Save:
buildAndSaveBuffer()→ produces aBlob. Feature logic lives in
injectXxx(zip, sheetsArr)helpers (see references/ooxml_paradigm.md). - Open:
openWorkbook(buf)→ reads via SheetJS for cell values, then calls
readXxx(buf)helpers to recover dropped features into the JS structures. - Test/debug hooks:
window.__excelDeepexposes internals
(setCellValue,getCfRules,addCfRule,cfMatchCell,renderAll,
saveToBuffer,openWorkbook,evaluate, …). The jsdom harness drives these
to test real engine behavior without a browser.
Workflow: adding a new Excel feature
- Model it. Add the field to the per-cell Map or to
cfRules/dvRules/
sheets[]. Decide the in-memory shape first (e.g. a CF rule is
{r1,c1,r2,c2,type, ...}). - Render it in
renderCell/renderAll(and add CSS instyles.css).
Text must always stay above overlay elements — give.cell-input
position:relative; z-index:1and overlays (cf-bar, markers) a lower
z-indexwithpointer-events:none. - Export it via an
injectXxxfunction that rewrites the OOXML part, and
wire it intobuildAndSaveBufferat the right CT_Worksheet child order. - Import it via a
readXxxfunction parsing the same part; wire into
openWorkbook(wrap intry/catch— a missing part must not break open). - Expose hooks on
window.__excelDeepso the harness can set state and
read state. - Write a regression test
_excel_<feature>_test.js(see
references/regression_harness.md and scripts/regression_harness.js). - Rebuild + smoke:
node build-standalone.js, then run
_standalone_smoke.jsto confirm 0 load-time errors.
OOXML inject/read paradigm (the non-obvious core)
This is the most important reusable knowledge. Full code patterns, buffer-type
rules, [Content_Types].xml override mechanics, rels wiring, and a worked
conditional-formatting example live in references/ooxml_paradigm.md. Read it
before writing any injectXxx/readXxx function. Key rules (do not skip):
- Injectors take a
JSZipinstance and return anarraybuffer(NOT a Blob) so
the next injector canJSZip.loadAsyncit. Only the finalbuildAndSaveBuffer
wraps the last buffer innew Blob([out]). - Always pair
escapeXmlwithunescapeXmlwhen round-tripping formulas or any
text containing<,>,&,"(e.g.>A1>10<in a CF formula). - Keep the model type names distinct from the OOXML type names. Example: the
in-memory CF rule usestype:'scale'|'bar'|'formula', while the OOXML
<cfRule type="colorScale"|"dataBar"|"expression">. Map both directions; a
mismatch silently drops the rule on save. XLSX.write(..., {cellStyles:true})thenJSZip.loadAsyncthe array result.
jsdom regression harness
Every feature ships with a test that loads the real engine in jsdom and asserts
behavior (including full save→reopen round-trips). The template, the exact
NODE_PATH, the jsdom ^24 pin, and the two classic harness bugs (un-awaited
async in setTimeout; || -1 turning a legitimate 0 into -1) are detailed in
references/regression_harness.md and the ready-to-copy
scripts/regression_harness.js.
Resources
references/ooxml_paradigm.md— save/read chain, buffer types, Content_Types
overrides, rels, worked conditional-formatting + comments examples.references/regression_harness.md— jsdom harness template, env setup, gotchas.scripts/regression_harness.js— copy-paste harness skeleton for a new test.