Skip to content

feat: support paragraph bar borders#2736

Open
iguit0 wants to merge 1 commit intosuperdoc-dev:mainfrom
iguit0:feat/support-paragraph-bar-borders
Open

feat: support paragraph bar borders#2736
iguit0 wants to merge 1 commit intosuperdoc-dev:mainfrom
iguit0:feat/support-paragraph-bar-borders

Conversation

@iguit0
Copy link
Copy Markdown
Contributor

@iguit0 iguit0 commented Apr 7, 2026

Summary

Adds support for Word paragraph bar borders (w:pBdr/w:bar) in the layout/rendering pipeline.

w:bar is the vertical decorative line rendered on the left edge of a paragraph. It was already parsed upstream, but it never made it through the layout-engine path, so it could not render in DomPainter.

Root cause

w:bar was being dropped in normalizeParagraphBorders() inside the pm-adapter layer.

Because of that:

  • ParagraphBorders did not expose bar
  • normalized paragraph attrs never carried bar
  • paragraph border hashes/equality checks ignored bar
  • DomPainter never had any bar data to render

What changed

Contracts

  • added bar?: ParagraphBorder to packages/layout-engine/contracts/src/index.ts

PM adapter

  • updated normalizeParagraphBorders() to preserve bar
  • kept the existing between special-case behavior scoped to between only
  • bar now normalizes like the other paragraph border sides:
    • style mapping
    • width conversion
    • color normalization
    • space support

Hashing / invalidation / grouping

Updated the paragraph-border identity paths so bar-only changes are treated as real visual changes:

  • packages/layout-engine/layout-bridge/src/paragraph-hash-utils.ts
  • packages/layout-engine/painters/dom/src/paragraph-hash-utils.ts
  • packages/layout-engine/layout-bridge/src/diff.ts
  • packages/layout-engine/layout-engine/src/layout-paragraph.ts

This ensures:

  • cache invalidation happens when only bar changes
  • paragraphs that differ only by bar do not incorrectly group for between rendering

DomPainter rendering

  • implemented bar rendering in packages/layout-engine/painters/dom/src/features/paragraph-borders/border-layer.ts
  • renders bar as a separate absolutely-positioned vertical rule on the left side of the paragraph border box
  • keeps bar independent from the normal left border so both can coexist
  • does not change existing between behavior

Implementation detail:

  • bar is rendered as a dedicated child decoration element attached to the existing paragraph border layer
  • its horizontal offset is based on the rendered left border width plus normalized bar.space
  • it uses the normalized style, width, and color values
  • pointer events remain disabled

Test plan

  • Import a .docx with a paragraph using only w:bar; verify a left-side vertical bar renders.
  • Import a .docx with both w:left and w:bar; verify both render independently.
  • Verify w:bar + paragraph shading renders correctly and the bar stays visible.
  • Verify consecutive paragraphs with identical borders still group correctly with between.
  • Verify consecutive paragraphs that differ only by bar no longer group.
  • Inspect rendered DOM and confirm bar is a separate decoration element with pointer events disabled.

Closes #2282

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.

@iguit0 nice work — the bar rendering is the best of the three apps we tested. Word ignores bar borders entirely, Google Docs renders them but loses the color, SuperDoc gets it right.

one thing to look at: both Word and Google Docs ignore bar when deciding whether to group paragraphs together. right now SuperDoc breaks the group when bars differ, but the other two don't. left an inline comment with more detail.

couple of small cleanup suggestions in the other comments. tests look good.

if (borders.right) parts.push(`r:[${hashParagraphBorder(borders.right)}]`);
if (borders.bottom) parts.push(`b:[${hashParagraphBorder(borders.bottom)}]`);
if (borders.left) parts.push(`l:[${hashParagraphBorder(borders.left)}]`);
if (borders.bar) parts.push(`bar:[${hashParagraphBorder(borders.bar)}]`);
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.

we tested this file in Word and Google Docs — both still group paragraphs together even when their bar borders are different. including bar here breaks that grouping in SuperDoc. the bar should still trigger a re-render when it changes, but it shouldn't affect whether paragraphs are grouped for between borders. worth splitting those two?

@@ -689,6 +722,25 @@ describe('computeBetweenBorderFlags', () => {
expect(computeBetweenBorderFlags(fragments, lookup).size).toBe(0);
});
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.

heads up — this test expects bar-only differences to break grouping, but Word and Google Docs both keep them grouped. if the grouping change above goes in, this test would need to flip.

Comment on lines +263 to +265
barElement.style.borderLeftStyle = resolvedBar.style;
barElement.style.borderLeftWidth = `${resolvedBar.width}px`;
barElement.style.borderLeftColor = resolvedBar.color;
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.

these three lines do the same thing as setBorderSideStyle(barElement, 'left', barBorder) — worth reusing it?

Suggested change
barElement.style.borderLeftStyle = resolvedBar.style;
barElement.style.borderLeftWidth = `${resolvedBar.width}px`;
barElement.style.borderLeftColor = resolvedBar.color;
setBorderSideStyle(barElement, 'left', barBorder);

Comment on lines +219 to +222
const getParagraphBarElement = (element: HTMLElement): HTMLElement | undefined => {
return Array.from(element.children).find(
(child): child is HTMLElement => child instanceof HTMLElement && child.classList.contains(PARAGRAPH_BAR_CLASS),
);
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.

element.querySelector('.superdoc-paragraph-bar') does the same thing in one line — worth switching?

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.

Support paragraph bar borders (w:pBdr/w:bar)

2 participants