Skip to content

Implement DOCX export pipeline (Tier 3 fidelity)#12

Merged
kevincarlson merged 12 commits into
mainfrom
claude/fix-editing-bugs-aPRKf
May 11, 2026
Merged

Implement DOCX export pipeline (Tier 3 fidelity)#12
kevincarlson merged 12 commits into
mainfrom
claude/fix-editing-bugs-aPRKf

Conversation

@kevincarlson

Copy link
Copy Markdown
Member

Summary

This PR implements the DOCX export functionality for loki-ooxml, converting a loki-doc-model::Document into a conformant .docx file. The implementation targets Tier 3 fidelity (basic block types, primary character formatting, document structure) as specified in ADR-0007.

Key Changes

Core Export Infrastructure

  • loki-ooxml/src/docx/write/ — New module implementing the complete DOCX serialization pipeline:

    • assembly.rs: Top-level OPC package assembly, wires up relationships and content types
    • document.rs: Serializes sections and blocks to word/document.xml; handles headers/footers
    • styles.rs: Generates word/styles.xml with built-in headings and catalog styles
    • numbering.rs: Tracks and serializes list definitions to word/numbering.xml
    • footnotes.rs: Serializes footnotes/endnotes to their respective parts
    • collector.rs: Accumulates export state (media, hyperlinks, notes) during serialization
    • xml.rs: Low-level XML writing helpers and namespace constants
    • media.rs: Image data extraction from data URIs
    • mod.rs: Module organization
  • loki-ooxml/src/docx/export.rs — Replaced stub with functional implementation calling write::assembly

Document Model Enhancements

  • loki-doc-model/src/settings.rs (new) — DocumentSettings for global document-wide configuration
  • loki-doc-model/src/content/table/col.rs — Added TableWidth enum for table-level width specifications
  • loki-doc-model/src/content/table/row.rs — Added CellTextDirection enum for cell text direction
  • loki-doc-model/src/style/para_style.rs — Added next_style_id field for style chaining
  • loki-doc-model/src/document.rs — Integrated DocumentSettings into Document

Import Improvements

  • loki-ooxml/src/docx/mapper/table.rs — Enhanced table mapping:

    • Two-pass algorithm for resolving vertical merge (w:vMerge) spans
    • Proper row_span computation and continuation cell removal
    • Column width and cell property extraction
  • loki-ooxml/src/docx/reader/document.rs — Extended table parsing to capture:

    • Table width (w:tblW)
    • Cell margins (w:tcMar)
    • Text direction and vertical alignment
    • Cell borders and shading
  • loki-odf/src/odt/mapper/document.rs & props.rs — ODF import enhancements:

    • Paragraph borders, tab stops, and background color mapping
    • Cell properties extraction and mapping
    • Master page support for section layout transitions

Testing & Documentation

  • loki-ooxml/tests/export_p0.rs (new) — Integration tests for P0 features (hyperlinks, images, footnotes)
  • loki-ooxml/tests/export_p1.rs (new) — Integration tests for P1 features (paragraph formatting, lists, tables)
  • docs/adr/0007-export-pipeline.md (new) — Architecture Decision Record documenting design choices
  • docs/export-gap-audit-2026-05-10.md (new) — Comprehensive gap audit of export fidelity vs. import
  • Test helpers — Extended with reference DOCX building and ODF test fixtures

Minor Improvements

  • loki-text/src/editing/navigation.rs — Added find_prev_para_data helper for backward paragraph navigation
  • loki-layout/src/para.rs — Added line_end_offset method for line

https://claude.ai/code/session_01WFkcgZx556Fn94tbqnM3nh

claude and others added 12 commits May 10, 2026 02:30
…argin cursor, End key, split heading level, and add tests

- Bug 1: Cmd+key (macOS) now triggers shortcuts instead of inserting the
  character. Detects Modifiers::SUPER (winit's mapping for the macOS
  Command key) in addition to CTRL/META. Implements Cmd+A select-all.
- Bug 2+3: Arrow-up/down now crosses paragraph boundaries when the target
  y-coordinate falls outside the current page's content area. Adds
  find_prev_para_data / find_next_para_data helpers and guards
  hit_test_page calls with a target_y >= margins.top check to avoid
  matching Fragment B's negative origin.
- Bug 4: End key now uses line_end_offset (new ParagraphLayout method)
  which returns text_range().end trimmed of trailing '\n', rather than
  hit_test_point(100_000.0) which could land at the visual start of the
  next line.
- Bug 5: split_block now copies KEY_HEADING_LEVEL from the source block
  to the new block, preserving heading level (and para/char props) on
  Enter in headings.
- Tests: line_end_offset unit tests in loki-layout; cross-paragraph nav
  tests in loki-text; split_block style-preservation integration tests
  (heading level, para props, char props, independence) in loki-doc-model.

https://claude.ai/code/session_01WFkcgZx556Fn94tbqnM3nh
build_chain_layouts produced a zero-height stub (parley_layout=None) for
any non-StyledPara block (Heading, Para, Plain) that appeared in a
keep-with-next chain. That stub was pushed into editing_data.paragraphs,
so resolve_cursor_paint found the block but cursor_rect always returned
None — causing the repeated WARN log and an invisible cursor.

Fix: convert Heading/Para/Plain blocks to StyledParagraph via the same
synthesize_heading_para/synthesize_plain_para helpers used in flow_block,
so they receive a full layout_paragraph call and a valid parley_layout.
Non-text blocks (HR, table, etc.) still use the zero-height stub since
the cursor cannot land in them.

https://claude.ai/code/session_01WFkcgZx556Fn94tbqnM3nh
layout_paragraph returned early with parley_layout=None whenever
text_content was empty. Every empty paragraph in a document (used as
blank-line spacers, section dividers, etc.) therefore had no Parley
layout, causing resolve_cursor_paint to warn on every render frame
while the cursor was in one.

Fix: when preserve_for_editing=true and text is empty, build a phantom
Parley layout from a single space character.  The space forces Parley to
produce one line with the paragraph's resolved font metrics, so
cursor_rect(0) can return a properly-sized, visible caret.  height and
line_boundaries remain zero so the empty paragraph still takes no
vertical space in the flow (behaviour unchanged from read-only mode).
The phantom layout is only built in editing mode; read-only rendering
keeps the existing fast path.

https://claude.ai/code/session_01WFkcgZx556Fn94tbqnM3nh
ODF fixes:
- ODF-1: Map fo:border/fo:padding shorthand → ParaProps.border_*/padding_*
- ODF-2: Map OdfTabStop list → ParaProps.tab_stops (TabLeader::None always)
- ODF-3: Map fo:background-color → ParaProps.background_color
- ODF-4: Map text:text-shadow → CharProps.shadow (non-"none" = true)
- ODF-5: Parse style:table-column-properties/style:column-width; pre-build
  col_style_widths map in OdfMappingContext; resolve per-column widths in
  map_table with proportional(1.0) fallback
- ODF-6: Filter is_covered=true cells in map_table with TODO comment for
  row_span propagation

OOXML fixes:
- OOXML-1: Add TableWidth enum (Fixed/Percent/Auto) to doc-model; add
  Table.width field; map w:tblW in table mapper; parse w:tblPr/w:tblW
  in document reader (was silently skipped)
- OOXML-2: Add DocumentSettings struct with default_tab_stop_pt; add
  Document.settings field; forward w:defaultTabStop from DocxSettings;
  add TODO in layout para.rs to use it once threaded through
- OOXML-3: Add ParagraphStyle.next_style_id; forward DocxStyle.next in
  styles mapper

Round-trip tests:
- loki-ooxml: table width maps to TableWidth::Fixed(250pt); settings maps
  default_tab_stop_pt ≈ 36pt; fixture extended with w:tblW and settings.xml
- loki-odf: border_top, tab_stops (≥2), background_color all set after
  import; fixture extended with BorderPara/TabPara/BgPara styles

https://claude.ai/code/session_01WFkcgZx556Fn94tbqnM3nh
Documents all five architecture decisions for implementing OOXML and ODF
export: crate structure (Option A, add write modules to existing crates),
intermediate model (Option B, direct serialization from loki-doc-model),
fidelity tier (Tier 3, content-only for v0.1), export source (Option A,
Document snapshot), and OPC/ZIP infrastructure (loki-opc for OOXML,
zip crate directly for ODF, consistent with ADR-0001).

Includes: stub state audit, LoroDoc bridge completeness table, crate search
results for docx/odf write libraries, minimum part lists for .docx and .odt,
8-step implementation order, v0.1 scope table, and 5 open questions.

https://claude.ai/code/session_01WFkcgZx556Fn94tbqnM3nh
Parse all six header/footer variants (default, first-page, even-page)
from `style:master-page` in styles.xml and map them into the
`PageLayout` header/footer fields via the full inline paragraph parser.
Deleted the simplified `collect_para_text` helper in favour of the
existing `read_paragraph` function so field codes, spans, and hyperlinks
in headers are correctly preserved. Adds five round-trip tests and a
layout integration test confirming `page.header_items` / `footer_items`
are populated by `loki-layout`.

https://claude.ai/code/session_01WFkcgZx556Fn94tbqnM3nh
Add `compute_v_merge_spans` to the OOXML table mapper. Pass 1 expands
each row into a flat grid accounting for `w:gridSpan`, recording vMerge
state per grid column. Pass 2 scans each column for `Restart` cells and
counts consecutive `Continue` cells below, setting `row_span` on the
restart cell and removing continuation cells from the output row.

The reader already parsed `DocxVMerge::Restart` / `Continue` correctly,
so no reader changes were required (OOXML §17.4.84).

Also adds a TODO comment in `loki-layout` noting that `row_span` is not
yet applied in the visual layout engine.

Covered by 5 unit tests in the mapper and 1 round-trip integration test
that asserts row_span=2 and continuation-cell removal on a 2×3 fixture.

https://claude.ai/code/session_01WFkcgZx556Fn94tbqnM3nh
Tracks style:master-page-name transitions during ODF body mapping and
emits a new document section whenever the active master page changes.
Each section carries the correct PageLayout (page dimensions, margins,
and headers/footers) from its master page.

Changes:
- Add `master_page_name: Option<String>` to `OdfStyle` (model + all
  construction sites), parsed from `style:master-page-name` in the
  styles reader for both styles.xml and content.xml auto-styles.
- Add `resolve_master_page_name` helper that walks the
  `style:parent-style-name` inheritance chain to find a master page
  transition, with depth-limited cycle protection.
- Restructure `map_document` to iterate body children inline, detecting
  transitions on paragraphs/headings and flushing accumulated blocks
  into a completed `Section` when the master page changes.
- Rename `resolve_page_layout` to `resolve_page_layout_by_name` so it
  accepts an explicit master page name rather than always selecting the
  first/Standard master page.
- Cover with 5 unit tests for `resolve_master_page_name` and 2
  round-trip tests (portrait→landscape section split; no spurious
  breaks for single-master documents).

https://claude.ai/code/session_01WFkcgZx556Fn94tbqnM3nh
…rection)

Add parsing and mapping of w:tcMar (cell margins), w:vAlign (vertical
alignment), and w:textDirection to the doc-model CellProps. Cell padding
is applied in the layout engine to shrink the content area accordingly.

https://claude.ai/code/session_01WFkcgZx556Fn94tbqnM3nh
… borders)

Parse style:table-cell-properties from ODF table-cell styles and map all
properties (padding, vertical-align, writing-mode, background-color,
borders) to the format-neutral CellProps. ODF documents now produce the
same padded, aligned, coloured cell layout as OOXML.

https://claude.ai/code/session_01WFkcgZx556Fn94tbqnM3nh
Implements `DocxExport::export` by assembling a spec-conformant `.docx`
ZIP via `loki-opc`. All Tier-3 content is serialized: paragraphs, headings
(H1–H6), bullet and ordered lists, tables (with cell padding/valign),
horizontal rules, code blocks, line blocks, and div/blockquote containers.

Inline formatting covers bold, italic, underline, strikethrough,
super/subscript, small caps, code, quoted text, span, styled runs, and
bookmarks. Section properties (page size, margins, orientation) are
emitted as `w:sectPr` for every section.

New modules:
- `write/xml.rs`        – low-level XML writer helpers and unit converters
- `write/document.rs`   – `word/document.xml` serializer
- `write/styles.rs`     – `word/styles.xml` serializer (docDefaults + H1–H6)
- `write/numbering.rs`  – numbering state + `word/numbering.xml` serializer
- `write/assembly.rs`   – OPC package assembly and ZIP output

Tier-4 content (images, footnotes, hyperlinks, fields) is silently omitted
per ADR-0007 Decision 3. All 134 loki-ooxml tests pass.

https://claude.ai/code/session_01WFkcgZx556Fn94tbqnM3nh
@kevincarlson kevincarlson merged commit 2fc6b60 into main May 11, 2026
1 check passed
@kevincarlson kevincarlson deleted the claude/fix-editing-bugs-aPRKf branch May 11, 2026 03:57
@AppThere AppThere locked as resolved and limited conversation to collaborators May 11, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants