Skip to content

feat: support rendering of column line separators#2088

Merged
caio-pizzol merged 11 commits intosuperdoc-dev:mainfrom
JoaaoVerona:feat/support-column-line-separator
Apr 18, 2026
Merged

feat: support rendering of column line separators#2088
caio-pizzol merged 11 commits intosuperdoc-dev:mainfrom
JoaaoVerona:feat/support-column-line-separator

Conversation

@JoaaoVerona
Copy link
Copy Markdown
Contributor

@JoaaoVerona JoaaoVerona commented Feb 18, 2026

Summary

Adds support for rendering "line between" separators between page columns in DOM Painter, extracting their presence by looking up the w:sep tag in the layout engine and ProseMirror adapter. Closes #2067.


Changes

  • layout-engine, pm-adapter: Extracts w:sep tag according to OOXML spec [1] [2]
  • painters: Renders one separator for each page column (w:num)
  • contracts, layout-bridge, super-editor: pass down withSeparator property
  • New tests

Questions & Additional details

  • There were multiple occurrences (within the existing code) of copying the columns data (i.e. y.columns = { count: x.count, gap: x.gap }) that needed to be updated also to copy withSeparator; I could have used spread operator to shallow clone the data, but I went with a more conservative approach to avoid, potentially, copying unnecessary/unintended properties that could be declared in these objects at runtime;
  • withSeparator will be true when w:sep="true", w:sep="1" or w:sep="on", according to the spec;
  • Separator color hard-coded as #b3b3b3, as I'm not sure if we could infer this somehow;
  • I'm not sure about the retrocompatibility of this change; I marked withSeparator as optional to make sure we don't break userland, but a second look from someone more experienced with the packaging/release of superdoc would be great.

Showcase

GDocs - Two-column layout (docx here) SuperDoc - Imported from GDocs
GDocs - Three-column layout (docx here) SuperDoc - Imported from GDocs
MSWord - Two-column layout (docx here) SuperDoc - Imported from MSWord

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@JoaaoVerona
Copy link
Copy Markdown
Contributor Author

@caio-pizzol I could not find, in ECMA-376, a definition for w:sectPr > lineBetween as specified on #2067. Should we add support for that attribute too? Are programs out there exporting docx files with it?

Copy link
Copy Markdown
Contributor

@caio-pizzol caio-pizzol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @JoaaoVerona, this is a great start! The withSeparator flag is threaded cleanly through the whole pipeline - extraction, layout, rendering — nice work.

Tests: would be great to add a visual rendering test with a sample .docx that has w:sep="1" on <w:cols>. The test infra auto-discovers .docx files so it's mostly just dropping one in tests/visual/. This is the best way to catch regressions in separator positioning and styling down the road.

On your lineBetween question: lineBetween isn't part of the ECMA-376 spec - the actual XML attribute for column separators is w:sep on <w:cols> (§17.6.4), which is exactly what this PR implements. The name lineBetween probably comes from the Open XML SDK's property naming, not from the XML itself. Word and LibreOffice both use w:sep, so no need to support lineBetween.

left some inline comments on a few things worth tweaking before merging.

Comment thread packages/layout-engine/painters/dom/src/renderer.ts Outdated
Comment thread packages/layout-engine/layout-engine/src/index.ts
Comment thread packages/layout-engine/painters/dom/src/renderer.ts Outdated
Comment thread packages/layout-engine/painters/dom/src/renderer.ts
Comment thread packages/layout-engine/layout-engine/src/paginator.ts Outdated
Comment thread packages/layout-engine/layout-bridge/src/incrementalLayout.ts Outdated
@caio-pizzol
Copy link
Copy Markdown
Contributor

@JoaaoVerona btw we've created our own OOXML MCP server (https://ooxml.dev/mcp) :)

@caio-pizzol
Copy link
Copy Markdown
Contributor

@JoaaoVerona, let us know if you have any questions on this one

@JoaaoVerona JoaaoVerona force-pushed the feat/support-column-line-separator branch from fefdd96 to 9a732ac Compare February 24, 2026 02:44
@JoaaoVerona
Copy link
Copy Markdown
Contributor Author

JoaaoVerona commented Feb 24, 2026

hey @caio-pizzol, thanks for the info!

also, great work with the mcp -- been using it already with CC!

additionally to the inline comments:

  • I simply added a test .docx directly to tests/visual/ directory as instructed; I'm guessing there's no way right now for external contributors to run the visual tests (given that the docs:download script requires auth)?
  • I'm not sure yet why this doc test.docx has "misleading" w:cols entries and the actual columns in each page. it is a two-page doc; each page has a different column layout. SuperDoc itself seems to render the content correctly, but it seems the w:cols I'm extracting is misleading (perhaps I should not be looking at them and should infer columns another way? or maybe it could be related to the continuous section break you mentioned?).
Google Docs SuperDoc

by looking at document.xml, I can see there are 3 w:cols entries (although there are only 2 pages). will need to investigate that further whenever I have additional time to work on this again, but please let me know if you have any insights!

@caio-pizzol
Copy link
Copy Markdown
Contributor

I simply added a test .docx directly to tests/visual/ directory as instructed; I'm guessing there's no way right now for external contributors to run the visual tests (given that the docs:download script requires auth)?

correct — the visual test infra isn't set up for external contributors to run locally yet. we'll upload the baseline on our end. we're looking into better ways to support this for the community going forward.

I'm not sure yet why this doc test.docx has "misleading" w:cols entries [...] by looking at document.xml, I can see there are 3 w:cols entries (although there are only 2 pages).

i looked at your document.xml — OOXML sections are end-tagged, meaning the sectPr on a paragraph defines the section that ends at that paragraph, not the one that starts there. your doc has 3 sections:

Section w:cols w:type What it controls
1 (paragraph sectPr, line 7) num="3" (default = nextPage) just "Start line." — a single-paragraph section
2 (paragraph sectPr, line 895) num="2" continuous "Start first column." through "End." — page 1's 2-column layout
3 (body sectPr, line 2195) num="3" continuous "Start line page 2." through "End." — page 2's 3-column layout

the confusing part is that the first w:cols says num="3", but it only applies to a throwaway single-paragraph section that Google Docs emits before the actual column content begins. the num="2" in section 2 is what drives page 1's layout, and the num="3" in the body sectPr drives page 2.

so the extraction is correct — the values just read counterintuitively because of end-tagged semantics. nothing to fix here.

Copy link
Copy Markdown
Contributor

@caio-pizzol caio-pizzol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JoaaoVerona the round 1 feedback is all addressed — the separator color, the columnWidth <= 1 guard, the ColumnLayout type usage, and the SINGLE_COLUMN_DEFAULT constant are all looking good. left a few inline comments on some remaining rough edges before this is ready to merge.

Comment thread packages/layout-engine/pm-adapter/src/sections/extraction.ts Outdated
Comment thread packages/layout-engine/pm-adapter/src/sections/types.ts Outdated
Comment thread packages/layout-engine/layout-engine/src/section-breaks.d.ts Outdated
Comment thread packages/layout-engine/layout-engine/src/index.ts Outdated
@JoaaoVerona JoaaoVerona force-pushed the feat/support-column-line-separator branch from 9a732ac to 32753c9 Compare March 15, 2026 03:53
@JoaaoVerona
Copy link
Copy Markdown
Contributor Author

hey @caio-pizzol, thanks for the context! inline comments should be solved now. rebased it with main too.

Copy link
Copy Markdown
Contributor

@caio-pizzol caio-pizzol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JoaaoVerona all the feedback from previous rounds is addressed — looking good.

two small things:

  1. the column snapshot in section-props.ts copies fields by hand and misses equalWidth and widths. there's already a helper (getColumnConfig) that copies everything — reusing it would avoid this.

  2. normalizeColumnsForFootnotes in incrementalLayout.ts is a copy of normalizeColumns in index.ts. worth consolidating so the next person doesn't update one and forget the other.

on tests: renderColumnSeparators doesn't have a unit test for its positioning math and guard clauses. also, tests/visual/columns-with-line-separator.docx won't get picked up by the layout regression suite — it needs to go through pnpm corpus:upload to land in the R2 corpus where rendering.spec.ts auto-discovers test files.

left a couple inline comments.

if (block.columns) {
hasProps = true;
props.columns = { count: block.columns.count, gap: block.columns.gap };
props.columns = { count: block.columns.count, gap: block.columns.gap, withSeparator: block.columns.withSeparator };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the manual { count, gap, withSeparator } copy here and at line 138 drops equalWidth and widths — so documents with different-sized columns lose that info in the snapshot. getColumnConfig() in section-breaks.ts already copies everything correctly. worth reusing it here?

const gap = Math.max(0, input?.gap ?? 0);
const totalGap = gap * (count - 1);
const width = (contentWidth - totalGap) / count;
const withSeparator = input?.withSeparator ?? false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this does the same thing as normalizeColumns in layout-engine/src/index.ts:2633 — both are one-liners wrapping normalizeColumnLayout. not blocking, but if one changes and the other doesn't, things will break quietly.

@caio-pizzol
Copy link
Copy Markdown
Contributor

Hey @JoaaoVerona, this PR has had no activity for 7 days.

Two related fixes so the new column-line-separator feature works correctly
on pages where a continuous section break changes column layout mid-page.

1. isColumnConfigChanging now compares withSeparator. Before, a sep-only
   toggle (count+gap unchanged) returned false, so no mid-page region was
   created and the toggle was silently dropped. Applied in both
   section-breaks.ts and the inline fallback in index.ts.

2. constraintBoundaries captured during layout are serialized onto a new
   page.columnRegions contract field. renderColumnSeparators now iterates
   regions and draws each separator bounded by its yStart/yEnd instead of
   painting a single full-page overlay. When no mid-page change occurs,
   columnRegions is omitted and the renderer falls back to page.columns
   (unchanged behavior).

Verified by loading a fixture with 7 scenarios (2-col, 3-col, unequal
widths, separator on/off, continuous breaks toggling the separator).
Pages now show per-region separators tiled correctly; a 3-col region
followed by a 2-col region no longer paints a shared full-page line.

Out of scope here, tracked for follow-up: widths/equalWidth are still
dropped at pm-adapter extractColumns, so unequal-width separators render
at the equal-width midpoint; body-level w:sep is dropped at v2
docxImporter; there is no w:sep export.
13 unit tests over the separator renderer. Splits coverage into the
fallback path (page.columns only) and the region-aware path
(page.columnRegions).

Fallback path: pins the 2-col and 3-col geometry, and each early-return
guard (withSeparator false/undefined, single column, missing margins,
no columns at all, pathologically-small columnWidth).

Region path: verifies per-region yStart/yEnd bounding, mixed regions
(some draw, some skip for withSeparator=false or count<=1), zero-height
regions, and that columnRegions wins when both it and page.columns are
present.

Previously renderColumnSeparators had zero DOM-level coverage — the
region-aware refactor in the prior commit relied entirely on
layout-engine tests that never exercised the DOM output.
Main adopted the refactor we deferred to SD-2627 while this PR was open:
`cloneColumnLayout` and `normalizeColumnLayout` are now the single source of
truth for column copies, and `ColumnLayout` carries `widths` and `equalWidth`
for unequal-width columns. The merge threads this PR's `withSeparator`
through the new helpers:

- `cloneColumnLayout` and `normalizeColumnLayout` (contracts/column-layout.ts)
  now preserve `withSeparator`. Without this, every site using the helpers
  would silently drop the separator on clone/normalize.
- `isColumnConfigChanging` combines both intents: detects a change in
  `count`, `gap`, `withSeparator`, `equalWidth`, or `widths`. A sep-only
  toggle still triggers a new column region.
- All manual `{count, gap, withSeparator}` copies replaced by
  `cloneColumnLayout`/`ooXmlSectionColumns` calls picked up from main.
- Two pm-adapter test expectations updated to include `withSeparator: false`
  in the extracted columns shape.

Full test suite passes: contracts 177, layout-engine 602, painter-dom 997,
layout-bridge 1166, pm-adapter 1737.
@caio-pizzol caio-pizzol enabled auto-merge April 18, 2026 18:37
@caio-pizzol caio-pizzol added this pull request to the merge queue Apr 18, 2026
Merged via the queue into superdoc-dev:main with commit 68dcff2 Apr 18, 2026
52 checks passed
@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot bot commented Apr 18, 2026

🎉 This PR is included in vscode-ext v2.3.0-next.25

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot bot commented Apr 18, 2026

🎉 This PR is included in @superdoc-dev/react v1.2.0-next.22

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot bot commented Apr 18, 2026

🎉 This PR is included in template-builder v1.5.0-next.25

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot bot commented Apr 18, 2026

🎉 This PR is included in esign v2.3.0-next.25

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot bot commented Apr 18, 2026

🎉 This PR is included in superdoc-cli v0.7.0-next.26

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot bot commented Apr 18, 2026

🎉 This PR is included in superdoc v1.26.0-next.25

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot bot commented Apr 18, 2026

🎉 This PR is included in superdoc-sdk v1.6.0-next.23

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: support rendering of line between columns

3 participants