Skip to content

feat: the raw layer — lossless, addressable, writable OOXML access (v0.4.0)#19

Merged
CocoRoF merged 14 commits into
mainfrom
feat/raw-layer
Jul 7, 2026
Merged

feat: the raw layer — lossless, addressable, writable OOXML access (v0.4.0)#19
CocoRoF merged 14 commits into
mainfrom
feat/raw-layer

Conversation

@CocoRoF

@CocoRoF CocoRoF commented Jul 7, 2026

Copy link
Copy Markdown
Owner

What

Contextifier gains its second view: alongside the AI-friendly extraction pipeline, open_raw() returns a lossless, addressable, writable model of OOXML documents.

from contextifier import open_raw

raw = open_raw("report.xlsx")
raw.sheets["Sales"].set_cell("B3", 142)          # surgical — style preserved
raw.charts[0].set_data(categories=["Q1","Q2","Q3"], series=[("Sales",[120,142,150])])
raw.save("edited.xlsx")                          # untouched parts byte-identical
  • Byte-preservation contract: untouched parts round-trip byte-identically — sparklines, custom XML, chart styles, pivots all survive (contrast test proves openpyxl's round-trip destroys them).
  • XlsxRawDocument / DocxRawDocument / PptxRawDocument with format-true semantics: python-docx-compatible addressing + run-preserving edits (inline images survive paragraph/cell replaces); PPTX native-preserving replace_content + orphan-sweeping remove_slide; XLSX surgical cells + calcPr fullCalcOnLoad.
  • Shared ChartModel: read + write (title, full data with series grow/shrink, embedded-workbook regeneration) for classic charts across all three formats; chartEx read.
  • Public surface: top-level open_raw + DocumentProcessor.open_raw(); every model exposes .package for part-level OPC work.
  • Fixes found along the way: writestr ZipInfo mutation corrupting repeated saves.

Verification

598 passed (499 baseline + 99 new): byte-preservation contract tests (synthetic + real files + BOM/whitespace quirks), openpyxl-destruction contrast test, Office-library reopen assertions after every mutation (openpyxl / python-docx / python-pptx), cross-format integration of the README flows. ruff clean.

🤖 Generated with Claude Code

CocoRoF and others added 13 commits July 7, 2026 16:36
…XML parts

The foundation of the raw layer (the lossless twin of the extraction
pipeline): contextifier.raw with OpcPackage (lazy parts, rels graphs,
content-types, and a save that emits untouched parts byte-identical —
contract-tested incl. BOM/whitespace quirks and real xlsx/docx files),
XmlPart (lazy lxml facade with explicit dirty tracking), RawDocumentBase
(flush-on-save plumbing for the format models), the frozen ChartModel
interface contract (implemented in C3), and the public open_raw() entry
that dispatches xlsx/docx/pptx models (landing in C2/C4/C5).

lxml promoted to an explicit dependency. 499 tests pass (485 + 14 new).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… and README

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DocxRawDocument over word/document.xml with python-docx-compatible
addressing (body-level w:p / w:tbl indices, grid-resolved cells with
gridSpan/vMerge semantics matching row.cells — interchangeable with
edit2docs addresses).

- set_paragraph_text: run-preserving replace (P0-3 fix) — text lands in
  the first pure-text run keeping its rPr; drawings/pictures/OLE,
  bookmarks, math etc. stay untouched in place; hyperlink elements
  survive (emptied), removable via strip_empty_hyperlinks incl. orphan
  rel cleanup.
- RawCell.set_text: same rules per paragraph — image/nested-table
  paragraphs untouched, other pure-text paragraphs emptied, never
  removed (fixes cell-image destruction).
- insert_paragraph_after (-1 = body start, sectPr-safe), delete_paragraph.
- RawTable: n_rows/n_cols, cell(r,c), nested_tables(r,c),
  insert_row/delete_row (template deep-copy, text cleared, props kept).
- chart_part_names + lazy charts (C3 ChartModel contract), read-only
  headers/footers text, body_order() for outline builders.

Tests: addressing parity vs python-docx incl. merges, run/image
preservation proofs, hyperlink policy, row & paragraph editing, nested
tables, byte-preservation (only document.xml differs after one edit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PptxRawDocument (format="pptx") on top of the C1 OPC container:

- slides: p:sldIdLst order via presentation rels; RawSlide exposes a
  shape inventory (text/picture/table/chart/diagram/group kinds, ids
  from p:cNvPr, group recursion), format-preserving get_text/set_text
  (first a:r keeps its a:rPr; a:fld/a:br survive), native a:tbl tables
  (cell read/write, insert_row/delete_row via row-template deep copy),
  chart_part_names + lazy ChartModel list, and notes_text.
- RawSlide.replace_content(new_slide_xml, preserve_native=True): the
  edit2docs enabler — swaps the slide XML for generated markup while
  lifting the original chart/table/diagram graphicFrames (and
  optionally pictures) into the new spTree with renumbered cNvPr ids;
  rels are untouched so carried-over r:id/r:embed keep resolving.
- remove_slide(index): drops the sldId + pres rel, deletes the slide,
  its rels and notes slide, then reference-counts every transitively
  reachable part against all remaining rels and sweeps true orphans
  (chart, embedded workbook, images) plus their content-type
  overrides — no more orphan-part bloat.

16 tests round-trip every edit through python-pptx and pin the
byte-preservation contract (single set_text touches exactly one part).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the C3 stub with the shared chart model used by all three
OOXML format models:

- kind: classic plot-type mapping (barDir-aware bar/column, 3D
  variants fold to base kinds) + "chartex:<layoutId>" for cx: charts
- title: a:t runs joined, autoTitleDeleted-aware; chartEx cx:title
- series: str/num cache parsing (ptCount-sized, gaps → None),
  scatter/bubble xVal/yVal, chartEx strDim/numDim via dataId
- set_title: replaces runs in the first paragraph keeping the first
  run's a:rPr, creates c:title when absent, drops autoTitleDeleted=1
- set_data: rewrites c:cat/c:val (or xVal/yVal) refs + caches against
  the learned sheet name, clones/removes c:ser on series-count change,
  and REGENERATES the embedded workbook with openpyxl (data-only)
- embedded_workbook_part + load_chart convenience helper
- chartEx writes raise RawUnsupportedError (documented limitation)

Tests build real files (openpyxl / python-pptx / hand-built packages)
and prove Office-readability by reopening saved bytes with python-pptx,
plus the byte-preservation guarantee for untouched parts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
XlsxRawDocument(RawDocumentBase) over the OPC byte-preservation
container:

- sheets accessor (by name or index) -> RawSheet with get_cell /
  get_formula / set_cell / append_rows / iter_rows / dimensions /
  merged_ranges; sheet_names, defined_names, chart_part_names (charts
  defers to ChartModel, landing in C3)
- set_cell is surgical: rows/cells created sorted by @r, style attr
  preserved, strings written as inlineStr (sharedStrings never touched
  or created), formula overwrite drops <f> and ensures
  <calcPr fullCalcOnLoad="1"/> so Excel recalculates stale caches;
  dimension ref extended when the edit grows the sheet
- only the touched worksheet (+ workbook.xml for calcPr) is ever
  re-serialized; chart styles, custom XML, sparkline extLst, media and
  everything else round-trip byte-identical — the exact parts an
  openpyxl round-trip destroys (proven by the contrast test)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…er corrupts the source reader

Found during C2 verification: zipfile.writestr mutates the passed
ZipInfo (header_offset/sizes), so reusing source infos broke any second
save on the same package with BadZipFile. Regression-tested.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@CocoRoF CocoRoF closed this Jul 7, 2026
@CocoRoF CocoRoF deleted the feat/raw-layer branch July 7, 2026 07:56
CI installs latest ruff; 0.15.x flags ~200 pre-existing violations
(unused imports/vars, ambiguous 'l' names, semicolons, formatting)
across handlers/tests untouched by the raw layer. Auto-fixed + renamed
lambda/comprehension 'l' -> 'ln' in pdf_plus. No behavior changes;
598 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@CocoRoF CocoRoF reopened this Jul 7, 2026
@CocoRoF CocoRoF merged commit 29f0c3d into main Jul 7, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant