Skip to content

Commit b615274

Browse files
gpardhivvarmacaio-pizzol
authored andcommitted
feat: support paragraph between borders (w:pBdr/w:between) (#2074)
Flow `w:between` borders through the rendering pipeline so consecutive paragraphs sharing the same border definition display a horizontal separator line between them. - Add `between` to `ParagraphBorders` contract type - Include `between` in `normalizeParagraphBorders` side iteration - Add `bw:` segment to `hashParagraphBorders` in both hash util files - Add `computeBetweenBorderFlags` to pre-compute which fragments need the between border, following the `computeSdtBoundaries` pattern - Render between border as CSS `border-bottom` on decoration overlay - Handle header/footer sections, page splits, and patch invalidation - Add 55+ tests covering edge cases
1 parent b6511ca commit b615274

9 files changed

Lines changed: 826 additions & 26 deletions

File tree

packages/layout-engine/contracts/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,6 +1097,7 @@ export type ParagraphBorders = {
10971097
right?: ParagraphBorder;
10981098
bottom?: ParagraphBorder;
10991099
left?: ParagraphBorder;
1100+
between?: ParagraphBorder;
11001101
};
11011102

11021103
export type ParagraphShading = {

packages/layout-engine/layout-bridge/src/paragraph-hash-utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const hashParagraphBorders = (borders: ParagraphBorders): string => {
3838
if (borders.right) parts.push(`r:[${hashParagraphBorder(borders.right)}]`);
3939
if (borders.bottom) parts.push(`b:[${hashParagraphBorder(borders.bottom)}]`);
4040
if (borders.left) parts.push(`l:[${hashParagraphBorder(borders.left)}]`);
41+
if (borders.between) parts.push(`bw:[${hashParagraphBorder(borders.between)}]`);
4142
return parts.join(';');
4243
};
4344

packages/layout-engine/layout-bridge/test/paragraph-hash-utils.test.ts

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,15 @@
55
*/
66

77
import { describe, it, expect } from 'vitest';
8-
import { hashBorderSpec, hashTableBorderValue, hashTableBorders, hashCellBorders } from '../src/paragraph-hash-utils';
9-
import type { BorderSpec, TableBorders, CellBorders } from '@superdoc/contracts';
8+
import {
9+
hashBorderSpec,
10+
hashTableBorderValue,
11+
hashTableBorders,
12+
hashCellBorders,
13+
hashParagraphBorders,
14+
hashParagraphAttrs,
15+
} from '../src/paragraph-hash-utils';
16+
import type { BorderSpec, TableBorders, CellBorders, ParagraphBorders, ParagraphAttrs } from '@superdoc/contracts';
1017

1118
describe('hashBorderSpec', () => {
1219
it('produces deterministic hash for same border properties', () => {
@@ -391,3 +398,87 @@ describe('hashCellBorders', () => {
391398
expect(hash).toContain('sp:2');
392399
});
393400
});
401+
402+
describe('hashParagraphBorders', () => {
403+
it('includes between border in hash with bw: prefix', () => {
404+
const borders: ParagraphBorders = {
405+
top: { style: 'solid', width: 1, color: '#000' },
406+
between: { style: 'solid', width: 2, color: '#FF0000' },
407+
};
408+
const hash = hashParagraphBorders(borders);
409+
expect(hash).toContain('t:[');
410+
expect(hash).toContain('bw:[');
411+
expect(hash).toContain('w:2');
412+
});
413+
414+
it('produces different hashes with and without between', () => {
415+
const with_: ParagraphBorders = {
416+
top: { style: 'solid', width: 1 },
417+
between: { style: 'solid', width: 1 },
418+
};
419+
const without_: ParagraphBorders = {
420+
top: { style: 'solid', width: 1 },
421+
};
422+
expect(hashParagraphBorders(with_)).not.toBe(hashParagraphBorders(without_));
423+
});
424+
425+
it('does not include bw: when between is undefined', () => {
426+
const borders: ParagraphBorders = {
427+
top: { style: 'solid', width: 1 },
428+
bottom: { style: 'solid', width: 1 },
429+
};
430+
expect(hashParagraphBorders(borders)).not.toContain('bw:');
431+
});
432+
433+
it('places bw: after l: in hash output', () => {
434+
const borders: ParagraphBorders = {
435+
left: { style: 'solid', width: 1 },
436+
between: { style: 'solid', width: 1 },
437+
};
438+
const hash = hashParagraphBorders(borders);
439+
expect(hash.indexOf('l:[')).toBeLessThan(hash.indexOf('bw:['));
440+
});
441+
});
442+
443+
describe('hashParagraphAttrs', () => {
444+
it('includes between border in attrs hash via borders', () => {
445+
const attrs: ParagraphAttrs = {
446+
borders: {
447+
top: { style: 'solid', width: 1 },
448+
between: { style: 'solid', width: 2, color: '#F00' },
449+
},
450+
};
451+
const hash = hashParagraphAttrs(attrs);
452+
expect(hash).toContain('br:');
453+
expect(hash).toContain('bw:[');
454+
});
455+
456+
it('produces different hashes when between border changes', () => {
457+
const attrs1: ParagraphAttrs = {
458+
borders: {
459+
top: { style: 'solid', width: 1 },
460+
between: { style: 'solid', width: 1 },
461+
},
462+
};
463+
const attrs2: ParagraphAttrs = {
464+
borders: {
465+
top: { style: 'solid', width: 1 },
466+
between: { style: 'dashed', width: 2 },
467+
},
468+
};
469+
expect(hashParagraphAttrs(attrs1)).not.toBe(hashParagraphAttrs(attrs2));
470+
});
471+
472+
it('produces different hashes when between border is added', () => {
473+
const withoutBetween: ParagraphAttrs = {
474+
borders: { top: { style: 'solid', width: 1 } },
475+
};
476+
const withBetween: ParagraphAttrs = {
477+
borders: {
478+
top: { style: 'solid', width: 1 },
479+
between: { style: 'solid', width: 1 },
480+
},
481+
};
482+
expect(hashParagraphAttrs(withoutBetween)).not.toBe(hashParagraphAttrs(withBetween));
483+
});
484+
});

0 commit comments

Comments
 (0)