Skip to content

fix(docx-io): preserve nested figures and list paragraphs - #5330

Closed
tarasromil wants to merge 2 commits into
udecode:mainfrom
tarasromil:codex/docx-nested-figures-and-list-paragraphs
Closed

tarasromil wants to merge 2 commits into
udecode:mainfrom
tarasromil:codex/docx-nested-figures-and-list-paragraphs

Conversation

@tarasromil

@tarasromil tarasromil commented Nov 4, 2026 •

Copy link
Copy Markdown
  • Auto release

HTML-to-DOCX export drops images and captions when a figure is nested in a table cell or list item. Multiple block paragraphs in one list item are also merged instead of being preserved as continuations.

For example, <ul><li><p>Before</p><div><figure><img src="data:image/png;base64,..."><figcaption><strong>Caption</strong></figcaption></figure></div><p>After</p></li></ul> loses the figure. A table cell with a wrapped figure has the same problem.

This reuses figure traversal across the document body, table cells and list items. Images retain their paragraph indentation and fit within the available content width, accounting for table cell margins and list indentation, including lists inside cells. Intrinsic and explicit CSS dimensions use the same final width limit while preserving their aspect ratio. Rich captions, block order, nested lists and continuation paragraphs are retained. No new dependencies or public API changes.

Validation on 93275c710a2dc16abd9c4c0d8d504b60625e6a34:

  • Real DOCX ZIP/XML regressions cover PNG bytes, dimensions, bold/italic captions, text order, ordered/unordered/nested lists, continuation paragraphs, oversized cell images, and list image indentation.
  • Regression cases failed before their fixes; the existing document-body control remains green.
  • pnpm test -- ./packages/docx-io: 107 passed across 17 files.
  • pnpm test:slow -- ./packages/docx-io: 80 passed across 8 files.
  • pnpm --filter @platejs/docx-io lint, typecheck, and build: passed.
  • Downstream browser downloads rendered and visually checked in LibreOffice: 288px and 144px columns, bulleted and numbered lists, a list inside a cell, and a body-image control. Images fit the content area; captions and list markers remain intact.
  • The repository-wide pnpm check was not run.

AI-assisted with Codex; the diff was reviewed and the affected package was fully tested locally.

@tarasromil
tarasromil requested a review from a team November 4, 2026 11:53
@codesandbox

codesandbox Bot commented Nov 4, 2026

Copy link
Copy Markdown

Review or Edit in CodeSandbox

Open the branch in Web Editor • VS Code • Insiders

Open Preview

@changeset-bot

changeset-bot Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 93275c7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@platejs/docx-io Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@zbeyens

zbeyens commented Nov 4, 2026

Copy link
Copy Markdown
Member

This PR does not allow maintainer edits, so my fix is in tarasromil#1, a PR into your branch. Merge it there and this PR picks it up.

My commits after yours

On top of the PR

  • Kept
    • One figure builder for figures in the body and in table cells, with captions and their bold and italic.
    • Each block paragraph of a list item is its own paragraph, indented with no bullet after the first.
    • Images fit the width of their table cell or list item, from the author's second commit, with its TABLE_CELL_MARGIN and the maximumWidth that buildList takes.
    • The PR's slow tests and its changeset.
  • Changed
    • Inline content of a list item, such as a line break, kbd or an inline image, stays in the item's paragraph, as on main. A paragraph, a div, a figure or another block tag from the file's SPECIAL_BLOCK_ELEMENTS, such as a blockquote or pre, starts a new one.
    • A list item keeps its number when its figure starts with an image the exporter cannot fetch or with a table, or has no image, table or caption at all. An empty numbered paragraph holds the item's place, as on main, so later items do not renumber.
    • In a list, a figure's image, caption and table are separate blocks of the item, so a figure that opens an item carries the bullet. One buildFigurePart exports a part for both buildFigure and buildList.
    • buildImage keeps the author's single attributes parameter, typed as the exported ParagraphAttributes, but uses its maximumWidth as given. Each caller passes the width left after the paragraph's indent, which is the rule inline images already follow. So every block of a list item gets the item's text width, whether its bullet or an indent sets where it starts.
    • getWrappedTable and the PR's getWrappedFigure become one getWrappedElement(node, tag) in xml-builder.ts, which unwraps single-child divs only. This keeps the PR from adding names to the existing import cycle.
    • That unwrap ignores the hidden data-slate-spacer span Plate's static render puts beside a void's content. A Plate image in a list item sits in such a div, so both the PR and my first repair still dropped it, while hand-written <div><figure> markup passed. Body and cell output do not change.
    • getListTextIndent(level) in utils/list.ts gives the numbering, the continuation paragraphs and the list's image width one indent rule.
    • buildTableCell's two block loops become one importCellBlock, because the inner loop for a div's children repeated the outer loop's branches.
    • buildList types its queue as elements only and drops the checks that could never fail, and its width never drops below 1 twip, like the cell's.
    • Seven slow tests on one row helper: line breaks and inline elements stay in one list paragraph, a blockquote and a pre after a paragraph start their own paragraphs, a list item that opens with a figure gets the bullet while the rest is indented, Plate's static image beside its void spacer stays in its list item, an item keeps its number when its figure starts with an unfetchable image or a table or has nothing to export, a wide image that opens an item fits the item's text width, and a table inside a figure in a list item stays a table.
  • Dropped
    • Nothing.

Demo

  1. Export a document whose list has a logo first in an item, a chart between two paragraphs, and a line with a line break, followed by a table cell holding a captioned image. Open the file with Quick Look on a Mac. On main, the first item is an empty bullet, both images and the text after them are gone, and so is the cell's image. With the PR and my fix, every image and caption shows, the logo sits on its item's bullet, and the line break stays inside one bullet.

Before

before

After

after

  1. Export the same document from the PR's head without my fix. The logo's item has no bullet, and the shortcut line splits into three lines. With my fix, the logo carries the bullet and the shortcut line is one bullet.

Before

before

After

after

Verification

  • On the final code, the package's lint, typecheck and build pass, along with 105 fast tests and 89 slow tests. Each new slow test failed before its fix, either on the PR's head or on my earlier round that lacked it.
  • A script that exports Plate's own image markup shows a list item now keeps its image and caption, and the body and table cell output is the same as on the PR's head. A second script over 10 hand-written edge cases gives the same output as my first round.
  • Quick Look drew the exported file on a Mac. Nobody opened it in Word, and Plate v2 on next is unchanged.

@zbeyens

zbeyens commented Nov 4, 2026

Copy link
Copy Markdown
Member

Closing in favor of #5331, because this PR does not allow maintainer edits. #5331 carries your two commits and my review fixes in one commit, with you as its co-author. Thanks for the fix!

@zbeyens zbeyens closed this Nov 4, 2026
zbeyens added a commit that referenced this pull request Nov 4, 2026
… [skip release]

Word export now keeps figures inside table cells and list items, with
their captions and their bold and italic. Each block paragraph of a
list item is its own paragraph. The first one carries the bullet, and
later ones take the item's indent. Images fit the width of their table
cell or list item.

Inside a list item, line breaks, kbd and inline images stay in the
item's paragraph, an image that starts the item sits on its bullet, and
the item keeps its number when its figure has nothing to export.
Plate's own static image, which sits in its void's div beside a hidden
spacer, stays in its list item too.

This carries #5330, which does not allow maintainer edits,
with review fixes on top.

Co-authored-by: Taras Romilovych <tarasromil@gmail.com>
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.

2 participants