@stll/folio-core 0.31.0 → 0.31.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-edits/word-diff.js +17 -1
- package/dist/docx/server/createBilingualDocument.js +52 -6
- package/dist/layout-bridge/convert/paragraphFrames.js +7 -1
- package/dist/layout-engine/types.d.ts +2 -0
- package/dist/layout-painter/renderPage.d.ts +12 -7
- package/dist/layout-painter/renderPage.js +56 -15
- package/dist/layout-painter/renderTable.js +41 -2
- package/dist/layout-painter/tableRowPaintGeometry.d.ts +15 -0
- package/dist/layout-painter/tableRowPaintGeometry.js +22 -0
- package/package.json +1 -1
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
//#region src/ai-edits/word-diff.ts
|
|
2
|
-
const
|
|
2
|
+
const WHITESPACE = /\s/u;
|
|
3
|
+
const tokenize = (s) => {
|
|
4
|
+
const tokens = [];
|
|
5
|
+
let tokenStart = 0;
|
|
6
|
+
let cursor = 0;
|
|
7
|
+
while (cursor < s.length) {
|
|
8
|
+
while (cursor < s.length && WHITESPACE.test(s.charAt(cursor))) cursor++;
|
|
9
|
+
if (cursor === s.length) break;
|
|
10
|
+
while (cursor < s.length && !WHITESPACE.test(s.charAt(cursor))) cursor++;
|
|
11
|
+
tokens.push(s.slice(tokenStart, cursor));
|
|
12
|
+
tokenStart = cursor;
|
|
13
|
+
}
|
|
14
|
+
const last = tokens.at(-1);
|
|
15
|
+
if (last === void 0) return s.length === 0 ? [] : [s];
|
|
16
|
+
if (tokenStart < s.length) tokens[tokens.length - 1] = last + s.slice(tokenStart);
|
|
17
|
+
return tokens;
|
|
18
|
+
};
|
|
3
19
|
/**
|
|
4
20
|
* Cell budget for the O(n*m) word-diff DP table below, mirroring
|
|
5
21
|
* `MAX_LCS_CELLS` in `version-comparison.ts`. `before`/`after` come from
|
|
@@ -86,6 +86,7 @@ function createBilingualDocument(source, options) {
|
|
|
86
86
|
cloner
|
|
87
87
|
});
|
|
88
88
|
const paraIds = createParaIdMinter(collectPackageParaIds(source.package));
|
|
89
|
+
const bookmarkIds = createBookmarkIdMinter(source.package);
|
|
89
90
|
const rows = [];
|
|
90
91
|
const content = [];
|
|
91
92
|
let sectionRows = [];
|
|
@@ -97,7 +98,7 @@ function createBilingualDocument(source, options) {
|
|
|
97
98
|
const copyParagraph = (paragraph) => {
|
|
98
99
|
const targetParaId = paraIds.mint(paragraph.paraId);
|
|
99
100
|
return {
|
|
100
|
-
copy: cloneParagraphForTarget(paragraph, targetParaId, styleCloner, cloner),
|
|
101
|
+
copy: cloneParagraphForTarget(paragraph, targetParaId, styleCloner, cloner, bookmarkIds),
|
|
101
102
|
ref: {
|
|
102
103
|
sourceParaId: paragraph.paraId,
|
|
103
104
|
targetParaId,
|
|
@@ -152,7 +153,8 @@ function createBilingualDocument(source, options) {
|
|
|
152
153
|
editableParagraphIds: options.editableParagraphIds,
|
|
153
154
|
paraIds,
|
|
154
155
|
styleCloner,
|
|
155
|
-
cloner
|
|
156
|
+
cloner,
|
|
157
|
+
bookmarkIds
|
|
156
158
|
});
|
|
157
159
|
rows.push({
|
|
158
160
|
kind: "table",
|
|
@@ -362,7 +364,7 @@ const createStyleCloner = ({ styleById, suffix, cloner }) => {
|
|
|
362
364
|
})
|
|
363
365
|
};
|
|
364
366
|
};
|
|
365
|
-
const cloneParagraphForTarget = (paragraph, targetParaId, styleCloner, cloner) => {
|
|
367
|
+
const cloneParagraphForTarget = (paragraph, targetParaId, styleCloner, cloner, bookmarkIds) => {
|
|
366
368
|
const { textId: _textId, sectionProperties: _sectionProperties, ...rest } = paragraph;
|
|
367
369
|
const formatting = paragraph.formatting;
|
|
368
370
|
const nextFormatting = formatting && {
|
|
@@ -379,12 +381,56 @@ const cloneParagraphForTarget = (paragraph, targetParaId, styleCloner, cloner) =
|
|
|
379
381
|
};
|
|
380
382
|
return {
|
|
381
383
|
...rest,
|
|
382
|
-
content: structuredClone(paragraph.content),
|
|
384
|
+
content: remapClonedBookmarkIds(structuredClone(paragraph.content), bookmarkIds),
|
|
383
385
|
paraId: targetParaId,
|
|
384
386
|
...nextFormatting && { formatting: nextFormatting },
|
|
385
387
|
...paragraph.listRendering && { listRendering: remapListRendering(paragraph.listRendering, cloner) }
|
|
386
388
|
};
|
|
387
389
|
};
|
|
390
|
+
const createBookmarkIdMinter = (source) => {
|
|
391
|
+
let nextId = 0;
|
|
392
|
+
const remapped = /* @__PURE__ */ new Map();
|
|
393
|
+
const visit = (value, seen) => {
|
|
394
|
+
if (typeof value !== "object" || value === null || seen.has(value)) return;
|
|
395
|
+
seen.add(value);
|
|
396
|
+
if (Array.isArray(value)) {
|
|
397
|
+
value.forEach((item) => visit(item, seen));
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const record = value;
|
|
401
|
+
if (record["type"] === "bookmarkStart" || record["type"] === "bookmarkEnd") {
|
|
402
|
+
const id = record["id"];
|
|
403
|
+
if (typeof id === "number") nextId = Math.max(nextId, id + 1);
|
|
404
|
+
}
|
|
405
|
+
Object.values(record).forEach((item) => visit(item, seen));
|
|
406
|
+
};
|
|
407
|
+
visit(source, /* @__PURE__ */ new Set());
|
|
408
|
+
return { mint: (sourceId) => {
|
|
409
|
+
const existing = remapped.get(sourceId);
|
|
410
|
+
if (existing !== void 0) return existing;
|
|
411
|
+
const id = nextId++;
|
|
412
|
+
remapped.set(sourceId, id);
|
|
413
|
+
return id;
|
|
414
|
+
} };
|
|
415
|
+
};
|
|
416
|
+
const remapClonedBookmarkIds = (value, bookmarkIds) => {
|
|
417
|
+
const visit = (item, seen) => {
|
|
418
|
+
if (typeof item !== "object" || item === null || seen.has(item)) return;
|
|
419
|
+
seen.add(item);
|
|
420
|
+
if (Array.isArray(item)) {
|
|
421
|
+
item.forEach((child) => visit(child, seen));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const record = item;
|
|
425
|
+
if (record["type"] === "bookmarkStart" || record["type"] === "bookmarkEnd") {
|
|
426
|
+
const id = record["id"];
|
|
427
|
+
if (typeof id === "number") record["id"] = bookmarkIds.mint(id);
|
|
428
|
+
}
|
|
429
|
+
Object.values(record).forEach((child) => visit(child, seen));
|
|
430
|
+
};
|
|
431
|
+
visit(value, /* @__PURE__ */ new Set());
|
|
432
|
+
return value;
|
|
433
|
+
};
|
|
388
434
|
const remapListRendering = (rendering, cloner) => {
|
|
389
435
|
const clonedAbstract = rendering.abstractNumId === void 0 ? void 0 : cloner.clonedAbstractNumId(rendering.abstractNumId);
|
|
390
436
|
return {
|
|
@@ -399,7 +445,7 @@ const collectTableParagraphs = (table) => {
|
|
|
399
445
|
else out.push(...collectTableParagraphs(item));
|
|
400
446
|
return out;
|
|
401
447
|
};
|
|
402
|
-
const cloneTableForTarget = ({ table, editableParagraphIds, paraIds, styleCloner, cloner }) => {
|
|
448
|
+
const cloneTableForTarget = ({ table, editableParagraphIds, paraIds, styleCloner, cloner, bookmarkIds }) => {
|
|
403
449
|
const paragraphs = [];
|
|
404
450
|
const cloneTable = (source) => ({
|
|
405
451
|
...structuredClone(source),
|
|
@@ -410,7 +456,7 @@ const cloneTableForTarget = ({ table, editableParagraphIds, paraIds, styleCloner
|
|
|
410
456
|
content: cell.content.map((item) => {
|
|
411
457
|
if (item.type === "table") return cloneTable(item);
|
|
412
458
|
const targetParaId = paraIds.mint(item.paraId);
|
|
413
|
-
const copy = cloneParagraphForTarget(item, targetParaId, styleCloner, cloner);
|
|
459
|
+
const copy = cloneParagraphForTarget(item, targetParaId, styleCloner, cloner, bookmarkIds);
|
|
414
460
|
if (item.paraId !== void 0 && editableParagraphIds.has(item.paraId)) paragraphs.push({
|
|
415
461
|
sourceParaId: item.paraId,
|
|
416
462
|
targetParaId,
|
|
@@ -61,10 +61,11 @@ function frameWrapType(frame) {
|
|
|
61
61
|
function toFrameTextBox(content, frame, nextBlockId) {
|
|
62
62
|
const first = content.at(0);
|
|
63
63
|
const last = content.at(-1);
|
|
64
|
+
const hasAuthoredWidth = frame.width !== void 0;
|
|
64
65
|
const textBox = {
|
|
65
66
|
kind: "textBox",
|
|
66
67
|
id: nextBlockId(),
|
|
67
|
-
width: frame.width ??
|
|
68
|
+
width: frame.width ?? 0,
|
|
68
69
|
margins: {
|
|
69
70
|
top: 0,
|
|
70
71
|
right: 0,
|
|
@@ -82,6 +83,11 @@ function toFrameTextBox(content, frame, nextBlockId) {
|
|
|
82
83
|
...first?.pmStart !== void 0 ? { pmStart: first.pmStart } : {},
|
|
83
84
|
...last?.pmEnd !== void 0 ? { pmEnd: last.pmEnd } : {}
|
|
84
85
|
};
|
|
86
|
+
if (!hasAuthoredWidth) {
|
|
87
|
+
textBox.widthMode = "intrinsic";
|
|
88
|
+
textBox.autoFit = "shape";
|
|
89
|
+
textBox.textWrap = "none";
|
|
90
|
+
}
|
|
85
91
|
if (frame.height !== void 0) textBox.height = frame.height;
|
|
86
92
|
const position = framePosition(frame);
|
|
87
93
|
if (position !== void 0) textBox.position = position;
|
|
@@ -769,6 +769,8 @@ type TextBoxBlock = {
|
|
|
769
769
|
id: BlockId;
|
|
770
770
|
/** Width in pixels */
|
|
771
771
|
width: number;
|
|
772
|
+
/** Whether width is authored or derived from the live rendered content. */
|
|
773
|
+
widthMode?: "intrinsic";
|
|
772
774
|
/** Height in pixels (may be auto-calculated) */
|
|
773
775
|
height?: number;
|
|
774
776
|
/** Text fitting behavior */
|
|
@@ -177,6 +177,12 @@ type HeaderFooterLayoutInfo = {
|
|
|
177
177
|
left: number;
|
|
178
178
|
};
|
|
179
179
|
};
|
|
180
|
+
type HeaderFooterHorizontalPosition = {
|
|
181
|
+
relativeTo?: string;
|
|
182
|
+
posOffset?: number;
|
|
183
|
+
align?: string;
|
|
184
|
+
alignment?: string;
|
|
185
|
+
};
|
|
180
186
|
/**
|
|
181
187
|
* Resolve the CSS `left` (px string) for an anchored object (image or text box)
|
|
182
188
|
* in a header/footer, honoring `wp:positionH` (relativeTo page/margin, align
|
|
@@ -184,12 +190,11 @@ type HeaderFooterLayoutInfo = {
|
|
|
184
190
|
* a page-centered banner in the header lands centered like Word, not pinned to
|
|
185
191
|
* the left. Ported from eigenpal/docx-editor#700.
|
|
186
192
|
*/
|
|
187
|
-
declare function resolveHeaderFooterFloatLeft(width: number, h:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
} | undefined, layout: HeaderFooterLayoutInfo): string;
|
|
193
|
+
declare function resolveHeaderFooterFloatLeft(width: number, h: HeaderFooterHorizontalPosition | undefined, layout: HeaderFooterLayoutInfo): string;
|
|
194
|
+
declare function resolveHeaderFooterIntrinsicFrameHorizontalPosition(h: HeaderFooterHorizontalPosition | undefined, layout: HeaderFooterLayoutInfo): {
|
|
195
|
+
left: string;
|
|
196
|
+
transform?: string;
|
|
197
|
+
};
|
|
193
198
|
/**
|
|
194
199
|
* Resolve the on-page coordinates of an anchored floating image.
|
|
195
200
|
*
|
|
@@ -297,4 +302,4 @@ declare function findPageShellForPmPos(container: HTMLElement, pmPos: number): {
|
|
|
297
302
|
isExact: boolean;
|
|
298
303
|
} | null;
|
|
299
304
|
//#endregion
|
|
300
|
-
export { type AnchoredImagePosition, FootnoteRenderItem, type HeaderFooterContent, HeaderFooterLayoutInfo, PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, PageFloatingImage, type PageGeometry, PainterPaintedDetail, type RenderContext, RenderPageOptions, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, floatingTableReservesBand, getDefaultPageFontFamily, isFloatingImageRun, renderAllPagesNow, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft };
|
|
305
|
+
export { type AnchoredImagePosition, FootnoteRenderItem, type HeaderFooterContent, HeaderFooterLayoutInfo, PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, PageFloatingImage, type PageGeometry, PainterPaintedDetail, type RenderContext, RenderPageOptions, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, floatingTableReservesBand, getDefaultPageFontFamily, isFloatingImageRun, renderAllPagesNow, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft, resolveHeaderFooterIntrinsicFrameHorizontalPosition };
|
|
@@ -207,6 +207,37 @@ function applyContentAreaStyles(element, page) {
|
|
|
207
207
|
function getPositionAlignment(position) {
|
|
208
208
|
return position?.align ?? position?.alignment;
|
|
209
209
|
}
|
|
210
|
+
function resolveHeaderFooterHorizontalAnchorPoint(h, layout) {
|
|
211
|
+
if (!h) return {
|
|
212
|
+
left: 0,
|
|
213
|
+
alignment: "left"
|
|
214
|
+
};
|
|
215
|
+
if (h.posOffset !== void 0) {
|
|
216
|
+
const pageOffset = h.relativeTo === "page" ? -layout.flowLeft : 0;
|
|
217
|
+
return {
|
|
218
|
+
left: emuToPixels(h.posOffset) + pageOffset,
|
|
219
|
+
alignment: "left"
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
let align = getPositionAlignment(h);
|
|
223
|
+
if (align === "inside") align = "left";
|
|
224
|
+
else if (align === "outside") align = "right";
|
|
225
|
+
const alignment = align === "center" || align === "right" ? align : "left";
|
|
226
|
+
const frameWidth = h.relativeTo === "page" ? layout.pageWidth : layout.contentWidth;
|
|
227
|
+
const frameLeft = h.relativeTo === "page" ? -layout.flowLeft : 0;
|
|
228
|
+
if (alignment === "center") return {
|
|
229
|
+
left: frameLeft + frameWidth / 2,
|
|
230
|
+
alignment
|
|
231
|
+
};
|
|
232
|
+
if (alignment === "right") return {
|
|
233
|
+
left: frameLeft + frameWidth,
|
|
234
|
+
alignment
|
|
235
|
+
};
|
|
236
|
+
return {
|
|
237
|
+
left: frameLeft,
|
|
238
|
+
alignment
|
|
239
|
+
};
|
|
240
|
+
}
|
|
210
241
|
function resolveHeaderFooterFloatTop(floatImg, layout) {
|
|
211
242
|
const v = floatImg.position.vertical;
|
|
212
243
|
if (!v) return floatImg.paragraphY;
|
|
@@ -261,20 +292,24 @@ function resolveHeaderFooterFloatingTablePosition(floating, measure, layout, sou
|
|
|
261
292
|
* the left. Ported from eigenpal/docx-editor#700.
|
|
262
293
|
*/
|
|
263
294
|
function resolveHeaderFooterFloatLeft(width, h, layout) {
|
|
264
|
-
|
|
265
|
-
let
|
|
266
|
-
if (
|
|
267
|
-
else if (
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
295
|
+
const anchor = resolveHeaderFooterHorizontalAnchorPoint(h, layout);
|
|
296
|
+
let widthFactor = 0;
|
|
297
|
+
if (anchor.alignment === "right") widthFactor = 1;
|
|
298
|
+
else if (anchor.alignment === "center") widthFactor = .5;
|
|
299
|
+
const left = anchor.left - width * widthFactor;
|
|
300
|
+
return left === 0 ? "0" : `${left}px`;
|
|
301
|
+
}
|
|
302
|
+
function resolveHeaderFooterIntrinsicFrameHorizontalPosition(h, layout) {
|
|
303
|
+
const anchor = resolveHeaderFooterHorizontalAnchorPoint(h, layout);
|
|
304
|
+
if (anchor.alignment === "center") return {
|
|
305
|
+
left: `${anchor.left}px`,
|
|
306
|
+
transform: "translateX(-50%)"
|
|
307
|
+
};
|
|
308
|
+
if (anchor.alignment === "right") return {
|
|
309
|
+
left: `${anchor.left}px`,
|
|
310
|
+
transform: "translateX(-100%)"
|
|
311
|
+
};
|
|
312
|
+
return { left: anchor.left === 0 ? "0" : `${anchor.left}px` };
|
|
278
313
|
}
|
|
279
314
|
function applyHeaderFooterFloatHorizontalPosition(img, floatImg, layout) {
|
|
280
315
|
img.style.left = resolveHeaderFooterFloatLeft(floatImg.width, floatImg.position.horizontal, layout);
|
|
@@ -500,6 +535,12 @@ function renderHeaderFooterContent(content, context, options, layout) {
|
|
|
500
535
|
}, layout) : cursorY;
|
|
501
536
|
fragEl.style.top = `${textBoxTop}px`;
|
|
502
537
|
fragEl.style.left = resolveHeaderFooterFloatLeft(measure.width, block.position?.horizontal, layout);
|
|
538
|
+
if (block.widthMode === "intrinsic") {
|
|
539
|
+
const horizontalPosition = resolveHeaderFooterIntrinsicFrameHorizontalPosition(block.position?.horizontal, layout);
|
|
540
|
+
fragEl.style.width = "max-content";
|
|
541
|
+
fragEl.style.left = horizontalPosition.left;
|
|
542
|
+
fragEl.style.transform = horizontalPosition.transform ?? "";
|
|
543
|
+
}
|
|
503
544
|
if (block.wrapType === "behind") fragEl.style.zIndex = "-1";
|
|
504
545
|
containerEl.append(fragEl);
|
|
505
546
|
if (!isPositionedHeaderFooterTextBoxBlock(block)) cursorY += measure.height;
|
|
@@ -1823,4 +1864,4 @@ function depopulatePageShell(shell, pageDataMap) {
|
|
|
1823
1864
|
}
|
|
1824
1865
|
}
|
|
1825
1866
|
//#endregion
|
|
1826
|
-
export { PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, floatingTableReservesBand, getDefaultPageFontFamily, isFloatingImageRun, renderAllPagesNow, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft };
|
|
1867
|
+
export { PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, floatingTableReservesBand, getDefaultPageFontFamily, isFloatingImageRun, renderAllPagesNow, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft, resolveHeaderFooterIntrinsicFrameHorizontalPosition };
|
|
@@ -12,6 +12,7 @@ import { getAutomaticTextColorForBackground } from "./documentColors.js";
|
|
|
12
12
|
import { applyImageVisualAttrs, hasImageCrop, hasImageVisualAttrs } from "./renderImage.js";
|
|
13
13
|
import { renderParagraphFragment } from "./renderParagraph.js";
|
|
14
14
|
import { renderTextBoxFragment } from "./renderTextBox.js";
|
|
15
|
+
import { ownedRowBottomBorderOffsets } from "./tableRowPaintGeometry.js";
|
|
15
16
|
//#region src/layout-painter/renderTable.ts
|
|
16
17
|
/**
|
|
17
18
|
* Table Renderer
|
|
@@ -36,6 +37,7 @@ const TABLE_CLASS_NAMES = {
|
|
|
36
37
|
tableEdgeHandleRight: "layout-table-edge-handle-right"
|
|
37
38
|
};
|
|
38
39
|
const CELL_DIAGONAL_BORDER_CLASS = "layout-table-cell-diagonal-border";
|
|
40
|
+
const CELL_BOTTOM_BORDER_CLASS = "layout-table-cell-bottom-border";
|
|
39
41
|
function renderCellContent({ cell, cellMeasure, context, doc, contentWidthOverride, pageContentPosition }) {
|
|
40
42
|
const contentEl = doc.createElement("div");
|
|
41
43
|
contentEl.className = TABLE_CLASS_NAMES.cellContent;
|
|
@@ -355,7 +357,7 @@ function renderTableCell({ cell, cellMeasure, x, width, rowHeight, borderFlags,
|
|
|
355
357
|
if (cell.borders) {
|
|
356
358
|
if (borderFlags.drawTop) applyBorder(cellEl, "top", cell.borders.top);
|
|
357
359
|
applyBorder(cellEl, "right", cell.borders.right);
|
|
358
|
-
applyBorder(cellEl, "bottom", cell.borders.bottom);
|
|
360
|
+
if (borderFlags.drawBottom) applyBorder(cellEl, "bottom", cell.borders.bottom);
|
|
359
361
|
if (borderFlags.drawLeft) applyBorder(cellEl, "left", cell.borders.left);
|
|
360
362
|
}
|
|
361
363
|
if (cell.background) {
|
|
@@ -444,7 +446,26 @@ function renderTableCell({ cell, cellMeasure, x, width, rowHeight, borderFlags,
|
|
|
444
446
|
return cellEl;
|
|
445
447
|
}
|
|
446
448
|
const hasVisibleBorder = (border) => border !== void 0 && border.style !== "none" && border.style !== "nil";
|
|
447
|
-
|
|
449
|
+
const isMinimumHeightRow = (row) => row?.height !== void 0 && row.heightRule !== "exact";
|
|
450
|
+
const rowHasVerticalMerge = (row) => row?.cells.some((cell) => (cell.rowSpan ?? 1) > 1) === true;
|
|
451
|
+
const continuationRowBottomBorderOffsets = (fragment, block, measure, cellGrid) => {
|
|
452
|
+
const rowHeights = measure.rows.slice(fragment.fromRow, fragment.toRow).map(({ height }) => height);
|
|
453
|
+
const fragmentHasVerticalMerge = block.rows.slice(fragment.fromRow, fragment.toRow).some((row, fragmentRowIndex) => rowHasVerticalMerge(row) || (cellGrid.occupiedColumnsByRow.get(fragment.fromRow + fragmentRowIndex)?.size ?? 0) > 0);
|
|
454
|
+
if (fragment.continuesFromPrev !== true || fragment.headerRowCount || fragment.topClip !== void 0 || fragment.bottomClip !== void 0 || fragmentHasVerticalMerge) return rowHeights.map(() => 0);
|
|
455
|
+
const snapAfterRow = rowHeights.slice(0, -1).map((_, fragmentRowIndex) => {
|
|
456
|
+
const rowIndex = fragment.fromRow + fragmentRowIndex;
|
|
457
|
+
const row = block.rows[rowIndex];
|
|
458
|
+
const nextRow = block.rows[rowIndex + 1];
|
|
459
|
+
return isMinimumHeightRow(row) && isMinimumHeightRow(nextRow) && !rowHasVerticalMerge(row) && !rowHasVerticalMerge(nextRow) && row?.cells.some((cell) => hasVisibleBorder(cell.borders?.bottom)) === true;
|
|
460
|
+
});
|
|
461
|
+
if (!snapAfterRow.some(Boolean)) return rowHeights.map(() => 0);
|
|
462
|
+
return ownedRowBottomBorderOffsets({
|
|
463
|
+
origin: fragment.y,
|
|
464
|
+
rowHeights,
|
|
465
|
+
snapAfterRow
|
|
466
|
+
});
|
|
467
|
+
};
|
|
468
|
+
function renderTableRow({ row, rowMeasure, rowIndex, y, columnWidths, totalRows, context, doc, rowYPositions, isFirstRowInFragment, bidi = false, columnsPinned = false, cellGrid, cellPlacements, contentClip, pageContentPosition, inlineOffset = 0, bottomBorderOffset = 0 }) {
|
|
448
469
|
const rowEl = doc.createElement("div");
|
|
449
470
|
rowEl.className = TABLE_CLASS_NAMES.row;
|
|
450
471
|
rowEl.style.position = "absolute";
|
|
@@ -477,6 +498,7 @@ function renderTableRow({ row, rowMeasure, rowIndex, y, columnWidths, totalRows,
|
|
|
477
498
|
const leftCell = getSourceCellAt(cellGrid, rowIndex, bidi ? columnIndex + colSpan : columnIndex - 1);
|
|
478
499
|
const drawTop = isFirstRow || !hasVisibleBorder(aboveCell?.borders?.bottom);
|
|
479
500
|
const drawLeft = isFirstCol || !hasVisibleBorder(leftCell?.borders?.right);
|
|
501
|
+
const paintBottomBorderSeparately = bottomBorderOffset > 0 && hasVisibleBorder(cell.borders?.bottom);
|
|
480
502
|
const cellEl = renderTableCell({
|
|
481
503
|
cell,
|
|
482
504
|
cellMeasure,
|
|
@@ -485,6 +507,7 @@ function renderTableRow({ row, rowMeasure, rowIndex, y, columnWidths, totalRows,
|
|
|
485
507
|
rowHeight: cellHeight,
|
|
486
508
|
borderFlags: {
|
|
487
509
|
drawTop,
|
|
510
|
+
drawBottom: !paintBottomBorderSeparately,
|
|
488
511
|
isLastRow,
|
|
489
512
|
drawLeft,
|
|
490
513
|
isLastCol
|
|
@@ -503,6 +526,20 @@ function renderTableRow({ row, rowMeasure, rowIndex, y, columnWidths, totalRows,
|
|
|
503
526
|
cellEl.dataset["columnIndex"] = String(columnIndex);
|
|
504
527
|
if (rowSpan > 1) cellEl.dataset["rowSpan"] = String(rowSpan);
|
|
505
528
|
rowEl.append(cellEl);
|
|
529
|
+
if (paintBottomBorderSeparately) {
|
|
530
|
+
const bottomBorderEl = doc.createElement("div");
|
|
531
|
+
bottomBorderEl.className = CELL_BOTTOM_BORDER_CLASS;
|
|
532
|
+
bottomBorderEl.style.position = "absolute";
|
|
533
|
+
bottomBorderEl.style.left = `${cellLeft}px`;
|
|
534
|
+
bottomBorderEl.style.top = "0";
|
|
535
|
+
bottomBorderEl.style.width = `${width}px`;
|
|
536
|
+
bottomBorderEl.style.height = `${cellHeight + bottomBorderOffset}px`;
|
|
537
|
+
bottomBorderEl.style.boxSizing = "border-box";
|
|
538
|
+
bottomBorderEl.style.pointerEvents = "none";
|
|
539
|
+
bottomBorderEl.style.zIndex = "1";
|
|
540
|
+
applyBorder(bottomBorderEl, "bottom", cell.borders?.bottom);
|
|
541
|
+
rowEl.append(bottomBorderEl);
|
|
542
|
+
}
|
|
506
543
|
}
|
|
507
544
|
return rowEl;
|
|
508
545
|
}
|
|
@@ -568,6 +605,7 @@ function renderTableFragment(fragment, block, measure, context, options = {}) {
|
|
|
568
605
|
columnWidths: measure.columnWidths,
|
|
569
606
|
bidi: block.bidi === true
|
|
570
607
|
});
|
|
608
|
+
const contentRowBottomBorderOffsets = continuationRowBottomBorderOffsets(fragment, block, measure, cellGrid);
|
|
571
609
|
const headerRowCount = fragment.headerRowCount ?? 0;
|
|
572
610
|
let y = 0;
|
|
573
611
|
if (headerRowCount > 0 && fragment.continuesFromPrev) for (let hdrIdx = 0; hdrIdx < headerRowCount; hdrIdx++) {
|
|
@@ -646,6 +684,7 @@ function renderTableFragment(fragment, block, measure, context, options = {}) {
|
|
|
646
684
|
columnsPinned,
|
|
647
685
|
cellGrid,
|
|
648
686
|
cellPlacements,
|
|
687
|
+
bottomBorderOffset: contentRowBottomBorderOffsets[rowIndex - fragment.fromRow] ?? 0,
|
|
649
688
|
...contentClip ? { contentClip } : {},
|
|
650
689
|
...tablePageContentPosition ? { pageContentPosition: {
|
|
651
690
|
...tablePageContentPosition,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/layout-painter/tableRowPaintGeometry.d.ts
|
|
2
|
+
type OwnedRowBottomBorderOffsetsOptions = {
|
|
3
|
+
origin: number;
|
|
4
|
+
rowHeights: readonly number[];
|
|
5
|
+
/** Whether the row ending at this index owns a visible shared bottom edge. */
|
|
6
|
+
snapAfterRow: readonly boolean[];
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Offset bottom-owned shared edges onto CSS pixel boundaries. Source row
|
|
10
|
+
* geometry remains unchanged, so content floors and the final band edge stay
|
|
11
|
+
* exact; only the independently painted border moves.
|
|
12
|
+
*/
|
|
13
|
+
declare const ownedRowBottomBorderOffsets: ({ origin, rowHeights, snapAfterRow }: OwnedRowBottomBorderOffsetsOptions) => number[];
|
|
14
|
+
//#endregion
|
|
15
|
+
export { ownedRowBottomBorderOffsets };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/layout-painter/tableRowPaintGeometry.ts
|
|
2
|
+
const CSS_PIXEL_ROUNDING_EPSILON = 1e-6;
|
|
3
|
+
/**
|
|
4
|
+
* Offset bottom-owned shared edges onto CSS pixel boundaries. Source row
|
|
5
|
+
* geometry remains unchanged, so content floors and the final band edge stay
|
|
6
|
+
* exact; only the independently painted border moves.
|
|
7
|
+
*/
|
|
8
|
+
const ownedRowBottomBorderOffsets = ({ origin, rowHeights, snapAfterRow }) => {
|
|
9
|
+
const offsets = [];
|
|
10
|
+
let boundary = origin;
|
|
11
|
+
for (let rowIndex = 0; rowIndex < rowHeights.length; rowIndex++) {
|
|
12
|
+
boundary += rowHeights[rowIndex] ?? 0;
|
|
13
|
+
if (rowIndex === rowHeights.length - 1 || snapAfterRow[rowIndex] !== true) {
|
|
14
|
+
offsets.push(0);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
offsets.push(Math.ceil(boundary - CSS_PIXEL_ROUNDING_EPSILON) - boundary);
|
|
18
|
+
}
|
|
19
|
+
return offsets;
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
export { ownedRowBottomBorderOffsets };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/folio-core",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.2",
|
|
4
4
|
"description": "Headless, framework-neutral core of folio: the OOXML (.docx) parser, document model, ProseMirror integration, and page-layout engine. No React.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"document-model",
|