@stll/folio-core 0.25.2 → 0.25.4
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/layout-bridge/convert/toFlowBlocks.js +3 -4
- package/dist/layout-bridge/engine/clickToPosition.js +13 -28
- package/dist/layout-bridge/engine/hitTest.d.ts +2 -0
- package/dist/layout-bridge/engine/hitTest.js +28 -13
- package/dist/layout-bridge/engine/selectionRects.js +26 -15
- package/dist/layout-engine/index.js +6 -9
- package/dist/layout-engine/measure/cache.d.ts +1 -4
- package/dist/layout-engine/measure/cache.js +31 -34
- package/dist/layout-engine/measure/hyperlinkInstance.d.ts +6 -0
- package/dist/layout-engine/measure/hyperlinkInstance.js +8 -0
- package/dist/layout-engine/measure/tableCellGrid.d.ts +15 -1
- package/dist/layout-engine/measure/tableCellGrid.js +29 -6
- package/dist/layout-engine/measure/tableInlinePlacement.d.ts +12 -0
- package/dist/layout-engine/measure/tableInlinePlacement.js +22 -0
- package/dist/layout-engine/types.d.ts +1 -1
- package/dist/layout-painter/renderParagraph.d.ts +3 -0
- package/dist/layout-painter/renderParagraph.js +39 -37
- package/dist/layout-painter/renderTable.js +36 -74
- package/dist/prosemirror/conversion/toProseDoc.js +35 -39
- package/dist/utils/paragraphInlineLayout.d.ts +14 -0
- package/dist/utils/paragraphInlineLayout.js +25 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { convertBulletToUnicode } from "../../docx/bulletMarkers.js";
|
|
2
2
|
import { resolveDocumentGridLinePitch } from "../../docx/documentGrid.js";
|
|
3
3
|
import { formatOoxmlCounter } from "../../docx/ooxmlCounterFormatter.js";
|
|
4
|
+
import { setHyperlinkInstanceIndex } from "../../layout-engine/measure/hyperlinkInstance.js";
|
|
4
5
|
import { setParagraphFrame } from "../../layout-engine/paragraphFrame.js";
|
|
5
6
|
import { setTextBoxGroupId } from "../../layout-engine/textBoxGroup.js";
|
|
6
7
|
import { DEFAULT_TEXTBOX_MARGINS } from "../../layout-engine/types.js";
|
|
@@ -267,6 +268,7 @@ function extractRunFormatting(marks, theme) {
|
|
|
267
268
|
const attrs = expectHyperlinkMarkAttrs(mark);
|
|
268
269
|
const link = { href: attrs.href };
|
|
269
270
|
if (attrs.tooltip !== void 0) link.tooltip = attrs.tooltip;
|
|
271
|
+
if (attrs._docxHyperlinkIndex !== void 0) setHyperlinkInstanceIndex(link, attrs._docxHyperlinkIndex);
|
|
270
272
|
formatting.hyperlink = link;
|
|
271
273
|
break;
|
|
272
274
|
}
|
|
@@ -468,10 +470,7 @@ const TOC_STYLE_ID = /^TOC\d*$/iu;
|
|
|
468
470
|
*/
|
|
469
471
|
function stripTocHyperlinkStyle(formatting) {
|
|
470
472
|
if (!formatting.hyperlink) return;
|
|
471
|
-
formatting.hyperlink =
|
|
472
|
-
...formatting.hyperlink,
|
|
473
|
-
noDefaultStyle: true
|
|
474
|
-
};
|
|
473
|
+
formatting.hyperlink.noDefaultStyle = true;
|
|
475
474
|
delete formatting.color;
|
|
476
475
|
delete formatting.underline;
|
|
477
476
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { measuredLineAdvance, measuredLineContentOffset } from "../../layout-engine/lineFlow.js";
|
|
2
2
|
import { buildRunFontStyle, findCharacterAtX } from "../../layout-engine/measure/measureHelpers.js";
|
|
3
3
|
import { measureRun } from "../../layout-engine/measure/measureProvider.js";
|
|
4
|
+
import { resolvePhysicalParagraphInlineLayout } from "../../utils/paragraphInlineLayout.js";
|
|
4
5
|
import { inlineImageBoundingBox } from "../../utils/rotationBoundingBox.js";
|
|
5
6
|
//#region src/layout-bridge/engine/clickToPosition.ts
|
|
6
7
|
/**
|
|
@@ -120,24 +121,12 @@ function findLineAtY(measure, localY, fromLine, toLine) {
|
|
|
120
121
|
if (toLine > fromLine) return Math.min(toLine - 1, measure.lines.length - 1);
|
|
121
122
|
return null;
|
|
122
123
|
}
|
|
123
|
-
|
|
124
|
-
* Find the character position at an X coordinate within a line.
|
|
125
|
-
*
|
|
126
|
-
* Uses canvas text measurement for pixel-perfect accuracy.
|
|
127
|
-
*
|
|
128
|
-
* @param block - The paragraph block.
|
|
129
|
-
* @param line - The measured line.
|
|
130
|
-
* @param x - X coordinate relative to the line's start position.
|
|
131
|
-
* @param availableWidth - Available width for alignment calculations.
|
|
132
|
-
* @returns Character offset and PM position.
|
|
133
|
-
*/
|
|
134
|
-
function findCharacterInLine(block, line, x, availableWidth) {
|
|
124
|
+
function findCharacterInLine({ block, line, x, availableWidth, alignment }) {
|
|
135
125
|
const { pmStart, pmEnd } = computeLinePmRange(block, line);
|
|
136
126
|
if (pmStart === void 0 || pmEnd === void 0) return {
|
|
137
127
|
charOffset: 0,
|
|
138
128
|
pmPosition: block.pmStart ?? 0
|
|
139
129
|
};
|
|
140
|
-
const alignment = block.attrs?.alignment ?? "left";
|
|
141
130
|
let alignmentOffset = 0;
|
|
142
131
|
if (alignment === "center") alignmentOffset = Math.max(0, (availableWidth - line.width) / 2);
|
|
143
132
|
else if (alignment === "right") alignmentOffset = Math.max(0, availableWidth - line.width);
|
|
@@ -236,11 +225,15 @@ function clickToPositionInParagraph(fragmentHit) {
|
|
|
236
225
|
if (lineIndex === null) return null;
|
|
237
226
|
const line = paragraphMeasure.lines[lineIndex];
|
|
238
227
|
if (!line) return null;
|
|
239
|
-
const
|
|
240
|
-
const indentLeft = indent?.left ?? 0;
|
|
241
|
-
const indentRight = indent?.right ?? 0;
|
|
228
|
+
const { alignment, indentLeft, indentRight } = resolvePhysicalParagraphInlineLayout(paragraphBlock);
|
|
242
229
|
const availableWidth = Math.max(0, fragment.width - indentLeft - indentRight);
|
|
243
|
-
const { charOffset, pmPosition } = findCharacterInLine(
|
|
230
|
+
const { charOffset, pmPosition } = findCharacterInLine({
|
|
231
|
+
block: paragraphBlock,
|
|
232
|
+
line,
|
|
233
|
+
x: localX - indentLeft,
|
|
234
|
+
availableWidth,
|
|
235
|
+
alignment
|
|
236
|
+
});
|
|
244
237
|
return {
|
|
245
238
|
pmPosition,
|
|
246
239
|
charOffset,
|
|
@@ -254,7 +247,7 @@ function clickToPositionInParagraph(fragmentHit) {
|
|
|
254
247
|
* @returns PM position, or null if mapping fails.
|
|
255
248
|
*/
|
|
256
249
|
function clickToPositionInTableCell(tableCellHit) {
|
|
257
|
-
const { cellBlock, cellMeasure, cellLocalX, cellLocalY } = tableCellHit;
|
|
250
|
+
const { cellBlock, cellMeasure, cellLocalX, cellLocalY, cellContentWidth } = tableCellHit;
|
|
258
251
|
if (!cellBlock || !cellMeasure) return null;
|
|
259
252
|
return clickToPositionInParagraph({
|
|
260
253
|
fragment: {
|
|
@@ -262,7 +255,7 @@ function clickToPositionInTableCell(tableCellHit) {
|
|
|
262
255
|
blockId: cellBlock.id,
|
|
263
256
|
x: 0,
|
|
264
257
|
y: 0,
|
|
265
|
-
width:
|
|
258
|
+
width: cellContentWidth,
|
|
266
259
|
fromLine: 0,
|
|
267
260
|
toLine: cellMeasure.lines.length,
|
|
268
261
|
height: cellMeasure.totalHeight
|
|
@@ -371,21 +364,13 @@ function positionToX(block, measure, pmPosition, _fragmentWidth) {
|
|
|
371
364
|
}
|
|
372
365
|
return null;
|
|
373
366
|
}
|
|
374
|
-
const getMaxLineWidth = (lines, fallback) => {
|
|
375
|
-
let maxWidth = fallback;
|
|
376
|
-
for (const line of lines) maxWidth = Math.max(maxWidth, line.width);
|
|
377
|
-
return maxWidth;
|
|
378
|
-
};
|
|
379
367
|
/**
|
|
380
368
|
* Get the bounding rect for a PM position (for caret rendering).
|
|
381
369
|
*/
|
|
382
370
|
function getPositionRect(block, measure, pmPosition, fragmentX, fragmentY, fragmentWidth, fromLine) {
|
|
383
371
|
const result = positionToX(block, measure, pmPosition, fragmentWidth);
|
|
384
372
|
if (!result) return null;
|
|
385
|
-
const alignment = block
|
|
386
|
-
const indent = block.attrs?.indent;
|
|
387
|
-
const indentLeft = indent?.left ?? 0;
|
|
388
|
-
const indentRight = indent?.right ?? 0;
|
|
373
|
+
const { alignment, indentLeft, indentRight } = resolvePhysicalParagraphInlineLayout(block);
|
|
389
374
|
const availableWidth = Math.max(0, fragmentWidth - indentLeft - indentRight);
|
|
390
375
|
const line = measure.lines[result.lineIndex];
|
|
391
376
|
if (!line) return null;
|
|
@@ -57,6 +57,8 @@ type TableCellHit = {
|
|
|
57
57
|
cellMeasure?: ParagraphMeasure;
|
|
58
58
|
/** X position relative to cell content area. */
|
|
59
59
|
cellLocalX: number;
|
|
60
|
+
/** Width of the cell content area after physical padding. */
|
|
61
|
+
cellContentWidth: number;
|
|
60
62
|
/** Y position relative to cell content area. */
|
|
61
63
|
cellLocalY: number;
|
|
62
64
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { getHeaderRowsHeight } from "../../layout-engine/index.js";
|
|
2
2
|
import { measuredLineRangeHeight } from "../../layout-engine/lineFlow.js";
|
|
3
|
-
import {
|
|
3
|
+
import { getTableCellContentWidth } from "../../layout-engine/measure/tableCellFloating.js";
|
|
4
|
+
import { buildTableCellGrid, buildTableCellPlacements } from "../../layout-engine/measure/tableCellGrid.js";
|
|
5
|
+
import { resolveTableCellPadding } from "../../layout-engine/types.js";
|
|
4
6
|
//#region src/layout-bridge/engine/hitTest.ts
|
|
5
7
|
/**
|
|
6
8
|
* Hit Testing Utilities
|
|
@@ -214,21 +216,33 @@ function hitTestTableCell(pageHit, blocks, measures, pagePoint) {
|
|
|
214
216
|
const rowMeasure = tableMeasure.rows[rowIndex];
|
|
215
217
|
const row = tableBlock.rows[rowIndex];
|
|
216
218
|
if (!rowMeasure || !row) continue;
|
|
217
|
-
let colX = getTableRowLeadingWidth(row, tableMeasure.columnWidths);
|
|
218
219
|
let colIndex = -1;
|
|
220
|
+
let cellLeft = 0;
|
|
219
221
|
if (rowMeasure.cells.length === 0 || row.cells.length === 0) continue;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
222
|
+
const cellPlacements = buildTableCellPlacements({
|
|
223
|
+
grid: buildTableCellGrid(tableBlock.rows, tableMeasure.columnWidths.length),
|
|
224
|
+
columnWidths: tableMeasure.columnWidths,
|
|
225
|
+
bidi: tableBlock.bidi === true
|
|
226
|
+
});
|
|
227
|
+
let nearestDistance = Infinity;
|
|
228
|
+
for (let c = 0; c < row.cells.length; c++) {
|
|
229
|
+
const cell = row.cells[c];
|
|
230
|
+
if (!cell) continue;
|
|
231
|
+
const placement = cellPlacements.get(cell);
|
|
232
|
+
if (!placement) continue;
|
|
233
|
+
if (localX >= placement.left && localX < placement.left + placement.width) {
|
|
223
234
|
colIndex = c;
|
|
235
|
+
cellLeft = placement.left;
|
|
224
236
|
break;
|
|
225
237
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
238
|
+
const distance = Math.min(Math.abs(localX - placement.left), Math.abs(localX - placement.left - placement.width));
|
|
239
|
+
if (distance < nearestDistance) {
|
|
240
|
+
nearestDistance = distance;
|
|
241
|
+
colIndex = c;
|
|
242
|
+
cellLeft = placement.left;
|
|
243
|
+
}
|
|
231
244
|
}
|
|
245
|
+
if (colIndex === -1) continue;
|
|
232
246
|
const cellMeasure = rowMeasure.cells[colIndex];
|
|
233
247
|
const cell = row.cells[colIndex];
|
|
234
248
|
if (!cellMeasure || !cell) continue;
|
|
@@ -239,8 +253,6 @@ function hitTestTableCell(pageHit, blocks, measures, pagePoint) {
|
|
|
239
253
|
rowTop = headerHeight;
|
|
240
254
|
for (let r = tableFragment.fromRow; r < rowIndex; r++) rowTop += tableMeasure.rows[r]?.height ?? 0;
|
|
241
255
|
}
|
|
242
|
-
let colLeft = 0;
|
|
243
|
-
for (let c = 0; c < colIndex; c++) colLeft += rowMeasure.cells[c]?.width ?? 0;
|
|
244
256
|
let cellBlock;
|
|
245
257
|
let cellBlockMeasure;
|
|
246
258
|
if (cell.blocks.length > 0) {
|
|
@@ -251,7 +263,9 @@ function hitTestTableCell(pageHit, blocks, measures, pagePoint) {
|
|
|
251
263
|
cellBlockMeasure = firstMeasure;
|
|
252
264
|
}
|
|
253
265
|
}
|
|
254
|
-
const
|
|
266
|
+
const { left: padLeft } = resolveTableCellPadding(cell);
|
|
267
|
+
const cellLocalX = localX - cellLeft - padLeft;
|
|
268
|
+
const cellContentWidth = getTableCellContentWidth(cell, cellMeasure);
|
|
255
269
|
const clipOffset = isClickOnHeader ? 0 : tableFragment.topClip ?? 0;
|
|
256
270
|
const cellLocalY = localY - rowTop + clipOffset;
|
|
257
271
|
return {
|
|
@@ -264,6 +278,7 @@ function hitTestTableCell(pageHit, blocks, measures, pagePoint) {
|
|
|
264
278
|
...cellBlock !== void 0 ? { cellBlock } : {},
|
|
265
279
|
...cellBlockMeasure !== void 0 ? { cellMeasure: cellBlockMeasure } : {},
|
|
266
280
|
cellLocalX: Math.max(0, cellLocalX),
|
|
281
|
+
cellContentWidth,
|
|
267
282
|
cellLocalY: Math.max(0, cellLocalY)
|
|
268
283
|
};
|
|
269
284
|
}
|
|
@@ -4,7 +4,9 @@ import { buildRunFontStyle } from "../../layout-engine/measure/measureHelpers.js
|
|
|
4
4
|
import { measureParagraph } from "../../layout-engine/measure/measureParagraph.js";
|
|
5
5
|
import { measureRun } from "../../layout-engine/measure/measureProvider.js";
|
|
6
6
|
import { buildTableCellFloatingZones, getTableCellContentWidth, getTableCellFloatingImages } from "../../layout-engine/measure/tableCellFloating.js";
|
|
7
|
-
import {
|
|
7
|
+
import { buildTableCellGrid, buildTableCellPlacements } from "../../layout-engine/measure/tableCellGrid.js";
|
|
8
|
+
import { resolveTableCellPadding } from "../../layout-engine/types.js";
|
|
9
|
+
import { resolvePhysicalParagraphInlineLayout } from "../../utils/paragraphInlineLayout.js";
|
|
8
10
|
import { inlineImageBoundingBox } from "../../utils/rotationBoundingBox.js";
|
|
9
11
|
import { getPageTop } from "./hitTest.js";
|
|
10
12
|
//#region src/layout-bridge/engine/selectionRects.ts
|
|
@@ -246,6 +248,7 @@ function selectionToRects(layout, blocks, measures, from, to) {
|
|
|
246
248
|
const selFrom = Math.min(from, to);
|
|
247
249
|
const selTo = Math.max(from, to);
|
|
248
250
|
const rects = [];
|
|
251
|
+
const tableCellPlacements = /* @__PURE__ */ new WeakMap();
|
|
249
252
|
for (let pageIndex = 0; pageIndex < layout.pages.length; pageIndex++) {
|
|
250
253
|
const page = layout.pages[pageIndex];
|
|
251
254
|
const pageTopY = getPageTop(layout, pageIndex);
|
|
@@ -260,6 +263,8 @@ function selectionToRects(layout, blocks, measures, from, to) {
|
|
|
260
263
|
const paragraphBlock = block;
|
|
261
264
|
const paragraphMeasure = measure;
|
|
262
265
|
const paragraphFragment = fragment;
|
|
266
|
+
const { alignment, indentLeft, indentRight } = resolvePhysicalParagraphInlineLayout(paragraphBlock);
|
|
267
|
+
const availableWidth = Math.max(0, fragment.width - indentLeft - indentRight);
|
|
263
268
|
const intersectingLines = findLinesInRange(paragraphBlock, paragraphMeasure, selFrom, selTo);
|
|
264
269
|
for (const { line, index } of intersectingLines) {
|
|
265
270
|
if (index < paragraphFragment.fromLine || index >= paragraphFragment.toLine) continue;
|
|
@@ -272,13 +277,8 @@ function selectionToRects(layout, blocks, measures, from, to) {
|
|
|
272
277
|
if (!isEmptyLine && sliceFrom >= sliceTo) continue;
|
|
273
278
|
const charOffsetFrom = pmPosToCharOffset(paragraphBlock, line, sliceFrom);
|
|
274
279
|
const charOffsetTo = pmPosToCharOffset(paragraphBlock, line, sliceTo);
|
|
275
|
-
const indent = paragraphBlock.attrs?.indent;
|
|
276
|
-
const indentLeft = indent?.left ?? 0;
|
|
277
|
-
const indentRight = indent?.right ?? 0;
|
|
278
|
-
const availableWidth = Math.max(0, fragment.width - indentLeft - indentRight);
|
|
279
280
|
const startX = charOffsetToX(paragraphBlock, line, charOffsetFrom, availableWidth);
|
|
280
281
|
const endX = charOffsetToX(paragraphBlock, line, charOffsetTo, availableWidth);
|
|
281
|
-
const alignment = paragraphBlock.attrs?.alignment ?? "left";
|
|
282
282
|
let alignmentOffset = 0;
|
|
283
283
|
if (alignment === "center") alignmentOffset = Math.max(0, (availableWidth - line.width) / 2);
|
|
284
284
|
else if (alignment === "right") alignmentOffset = Math.max(0, availableWidth - line.width);
|
|
@@ -305,6 +305,15 @@ function selectionToRects(layout, blocks, measures, from, to) {
|
|
|
305
305
|
const tableBlock = block;
|
|
306
306
|
const tableMeasure = measure;
|
|
307
307
|
const tableFragment = fragment;
|
|
308
|
+
let cellPlacements = tableCellPlacements.get(tableBlock);
|
|
309
|
+
if (!cellPlacements) {
|
|
310
|
+
cellPlacements = buildTableCellPlacements({
|
|
311
|
+
grid: buildTableCellGrid(tableBlock.rows, tableMeasure.columnWidths.length),
|
|
312
|
+
columnWidths: tableMeasure.columnWidths,
|
|
313
|
+
bidi: tableBlock.bidi === true
|
|
314
|
+
});
|
|
315
|
+
tableCellPlacements.set(tableBlock, cellPlacements);
|
|
316
|
+
}
|
|
308
317
|
const hdrCount = tableFragment.headerRowCount ?? 0;
|
|
309
318
|
let rowY = hdrCount > 0 && tableFragment.continuesFromPrev ? getHeaderRowsHeight(tableMeasure, hdrCount) : 0;
|
|
310
319
|
for (let rowIndex = tableFragment.fromRow; rowIndex < tableFragment.toRow && rowIndex < tableBlock.rows.length; rowIndex++) {
|
|
@@ -313,11 +322,12 @@ function selectionToRects(layout, blocks, measures, from, to) {
|
|
|
313
322
|
if (!row || !rowMeasure) continue;
|
|
314
323
|
const clipTop = tableFragment.topClip ?? 0;
|
|
315
324
|
const clipBottom = tableFragment.bottomClip ?? rowMeasure.height;
|
|
316
|
-
let cellX = getTableRowLeadingWidth(row, tableMeasure.columnWidths);
|
|
317
325
|
for (let cellIndex = 0; cellIndex < row.cells.length; cellIndex++) {
|
|
318
326
|
const cell = row.cells[cellIndex];
|
|
319
327
|
const cellMeasure = rowMeasure.cells[cellIndex];
|
|
320
328
|
if (!cell || !cellMeasure) continue;
|
|
329
|
+
const placement = cellPlacements.get(cell);
|
|
330
|
+
if (!placement) continue;
|
|
321
331
|
const contentWidth = getTableCellContentWidth(cell, cellMeasure);
|
|
322
332
|
const floatingZones = buildTableCellFloatingZones(getTableCellFloatingImages(cell, cellMeasure, contentWidth), contentWidth);
|
|
323
333
|
const contentOffsetX = getCellContentOffsetX(cell);
|
|
@@ -340,6 +350,8 @@ function selectionToRects(layout, blocks, measures, from, to) {
|
|
|
340
350
|
floatingZones,
|
|
341
351
|
paragraphYOffset: blockY
|
|
342
352
|
});
|
|
353
|
+
const { alignment, indentLeft, indentRight } = resolvePhysicalParagraphInlineLayout(paragraphBlock);
|
|
354
|
+
const availableWidth = Math.max(0, contentWidth - indentLeft - indentRight);
|
|
343
355
|
const intersectingLines = findLinesInRange(paragraphBlock, paragraphMeasure, selFrom, selTo);
|
|
344
356
|
for (const { line, index } of intersectingLines) {
|
|
345
357
|
const range = computeLinePmRange(paragraphBlock, line);
|
|
@@ -351,13 +363,16 @@ function selectionToRects(layout, blocks, measures, from, to) {
|
|
|
351
363
|
if (!isEmptyLine && sliceFrom >= sliceTo) continue;
|
|
352
364
|
const charOffsetFrom = pmPosToCharOffset(paragraphBlock, line, sliceFrom);
|
|
353
365
|
const charOffsetTo = pmPosToCharOffset(paragraphBlock, line, sliceTo);
|
|
354
|
-
const startX = charOffsetToX(paragraphBlock, line, charOffsetFrom,
|
|
355
|
-
const endX = charOffsetToX(paragraphBlock, line, charOffsetTo,
|
|
366
|
+
const startX = charOffsetToX(paragraphBlock, line, charOffsetFrom, availableWidth);
|
|
367
|
+
const endX = charOffsetToX(paragraphBlock, line, charOffsetTo, availableWidth);
|
|
368
|
+
let alignmentOffset = 0;
|
|
369
|
+
if (alignment === "center") alignmentOffset = Math.max(0, (availableWidth - line.width) / 2);
|
|
370
|
+
else if (alignment === "right") alignmentOffset = Math.max(0, availableWidth - line.width);
|
|
356
371
|
const lineY = measuredLineContentOffset(paragraphMeasure.lines, 0, index);
|
|
357
372
|
const clippedLineY = contentOffsetY + blockY + lineY;
|
|
358
373
|
if (clippedLineY + line.lineHeight <= clipTop || clippedLineY >= clipBottom) continue;
|
|
359
374
|
rects.push({
|
|
360
|
-
x: tableFragment.x +
|
|
375
|
+
x: tableFragment.x + placement.left + contentOffsetX + indentLeft + alignmentOffset + Math.min(startX, endX),
|
|
361
376
|
y: tableFragment.y + rowY + clippedLineY - clipTop + pageTopY,
|
|
362
377
|
width: isEmptyLine ? EMPTY_PARAGRAPH_SLIVER_WIDTH : Math.max(1, Math.abs(endX - startX)),
|
|
363
378
|
height: line.lineHeight,
|
|
@@ -366,7 +381,6 @@ function selectionToRects(layout, blocks, measures, from, to) {
|
|
|
366
381
|
}
|
|
367
382
|
blockY += paragraphMeasure.totalHeight;
|
|
368
383
|
}
|
|
369
|
-
cellX += cellMeasure.width;
|
|
370
384
|
}
|
|
371
385
|
rowY += rowMeasure.height;
|
|
372
386
|
}
|
|
@@ -419,12 +433,9 @@ function getCaretPosition(layout, blocks, measures, pmPosition) {
|
|
|
419
433
|
if (range.pmStart === void 0 || range.pmEnd === void 0) continue;
|
|
420
434
|
if (pmPosition >= range.pmStart && pmPosition <= range.pmEnd) {
|
|
421
435
|
const charOffset = pmPosToCharOffset(paragraphBlock, line, pmPosition);
|
|
422
|
-
const
|
|
423
|
-
const indentLeft = indent?.left ?? 0;
|
|
424
|
-
const indentRight = indent?.right ?? 0;
|
|
436
|
+
const { alignment, indentLeft, indentRight } = resolvePhysicalParagraphInlineLayout(paragraphBlock);
|
|
425
437
|
const availableWidth = Math.max(0, fragment.width - indentLeft - indentRight);
|
|
426
438
|
const x = charOffsetToX(paragraphBlock, line, charOffset, availableWidth);
|
|
427
|
-
const alignment = paragraphBlock.attrs?.alignment ?? "left";
|
|
428
439
|
let alignmentOffset = 0;
|
|
429
440
|
if (alignment === "center") alignmentOffset = Math.max(0, (availableWidth - line.width) / 2);
|
|
430
441
|
else if (alignment === "right") alignmentOffset = Math.max(0, availableWidth - line.width);
|
|
@@ -3,6 +3,7 @@ import { resolveSectionHeaderFooterRefs } from "./headerFooterRefs.js";
|
|
|
3
3
|
import { calculateChainHeight, computeKeepNextChains, getMidChainIndices, hasKeepLines, hasPageBreakBefore } from "./keep-together.js";
|
|
4
4
|
import { measuredLineAdvance } from "./lineFlow.js";
|
|
5
5
|
import { resolveFloatingTableX } from "./measure/floatingTablePosition.js";
|
|
6
|
+
import { resolveTableInlinePlacement } from "./measure/tableInlinePlacement.js";
|
|
6
7
|
import { createPaginator } from "./paginator.js";
|
|
7
8
|
import { getParagraphFragmentPmRange } from "./paragraphFragmentRange.js";
|
|
8
9
|
import { collapseParagraphSpacing, getParagraphSpacingAfter, getParagraphSpacingBefore, isEmptyParagraph, paragraphsShareStyle, resolveEffectiveParagraphSpacingTree } from "./paragraphSpacing.js";
|
|
@@ -521,15 +522,11 @@ function layoutTable(block, measure, paginator, footnoteHeightById) {
|
|
|
521
522
|
const breakInfo = buildTableRowBreakInfo(block, measure);
|
|
522
523
|
const verticallyMergedRows = getVerticallyMergedRows(block);
|
|
523
524
|
const computeTableX = (columnIndex) => {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const leadingCellMargin = block.rows.at(0)?.cells.at(0)?.padding?.left ?? 0;
|
|
530
|
-
x -= leadingCellMargin;
|
|
531
|
-
}
|
|
532
|
-
return x;
|
|
525
|
+
const x = paginator.getColumnX(columnIndex);
|
|
526
|
+
const placement = resolveTableInlinePlacement(block);
|
|
527
|
+
if (placement.alignment === "center") return x + (paginator.columnWidth - measure.totalWidth) / 2;
|
|
528
|
+
if (placement.alignment === "right") return x + paginator.columnWidth - measure.totalWidth - placement.offset;
|
|
529
|
+
return x + placement.offset;
|
|
533
530
|
};
|
|
534
531
|
const getCurrentRowCapacity = (state = paginator.getCurrentState()) => state.rawContentBottom - state.topMargin;
|
|
535
532
|
const hasAdjacentPriorTableRows = (rowIndex, state = paginator.getCurrentState()) => {
|
|
@@ -54,10 +54,7 @@ declare function setFontCacheSize(size: number): void;
|
|
|
54
54
|
* Get current font metrics cache size
|
|
55
55
|
*/
|
|
56
56
|
declare function getFontCacheSize(): number;
|
|
57
|
-
/**
|
|
58
|
-
* Generate a simple hash for a paragraph block
|
|
59
|
-
* Used as cache key to identify identical content
|
|
60
|
-
*/
|
|
57
|
+
/** Serialize the complete paragraph measurement contract into a cache key. */
|
|
61
58
|
declare function hashParagraphBlock(block: ParagraphBlock): string;
|
|
62
59
|
/**
|
|
63
60
|
* Get cached paragraph measurement or return undefined
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { lineBreakPolicyCacheParts } from "./effectiveLineBreakPolicy.js";
|
|
2
1
|
import { getLineBreakProviderGeneration } from "./lineBreakProvider.js";
|
|
3
2
|
import { clearFontResolvedCache } from "./measureHelpers.js";
|
|
4
3
|
//#region src/layout-engine/measure/cache.ts
|
|
@@ -164,40 +163,38 @@ let paragraphCacheMaxSize = 5e3;
|
|
|
164
163
|
* Key format: block content hash
|
|
165
164
|
*/
|
|
166
165
|
const paragraphMeasureCache = /* @__PURE__ */ new Map();
|
|
167
|
-
/**
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const marker = attrs.listMarkerFormatting;
|
|
191
|
-
parts.push(`marker:${attrs.listMarker}|${attrs.listMarkerHidden}|${attrs.listMarkerAlignment}|${attrs.listMarkerSuffix}|${marker?.fontFamily}|${marker?.eastAsiaFontFamily}|${marker?.complexScriptFontFamily}|${marker?.fontSize}|${marker?.complexScriptFontSize}|${marker?.bold}|${marker?.complexScriptBold}|${marker?.italic}|${marker?.complexScriptItalic}|${marker?.rtl}|${marker?.forceComplexScript}`);
|
|
192
|
-
}
|
|
193
|
-
parts.push(...lineBreakPolicyCacheParts(attrs));
|
|
194
|
-
const borders = attrs.borders;
|
|
195
|
-
if (borders) {
|
|
196
|
-
const signature = (border) => border ? `${border.width ?? ""},${border.style ?? ""},${border.color ?? ""}` : "";
|
|
197
|
-
parts.push(`bdr:${signature(borders.top)}|${signature(borders.bottom)}|${signature(borders.left)}|${signature(borders.right)}`);
|
|
198
|
-
}
|
|
166
|
+
/** Keep image payloads bounded while classifying every field as measured or intentionally ignored. */
|
|
167
|
+
const imageMeasureCacheInput = (run) => ({
|
|
168
|
+
kind: run.kind,
|
|
169
|
+
width: run.width,
|
|
170
|
+
height: run.height,
|
|
171
|
+
transform: run.transform,
|
|
172
|
+
wrapType: run.wrapType,
|
|
173
|
+
displayMode: run.displayMode,
|
|
174
|
+
distTop: run.distTop,
|
|
175
|
+
distBottom: run.distBottom,
|
|
176
|
+
exactLineHeight: run.exactLineHeight,
|
|
177
|
+
positioned: run.position !== void 0
|
|
178
|
+
});
|
|
179
|
+
const runMeasureCacheInput = (run) => {
|
|
180
|
+
switch (run.kind) {
|
|
181
|
+
case "text":
|
|
182
|
+
case "tab":
|
|
183
|
+
case "lineBreak":
|
|
184
|
+
case "renderedPageBreak":
|
|
185
|
+
case "field":
|
|
186
|
+
case "math": return run;
|
|
187
|
+
case "image": return imageMeasureCacheInput(run);
|
|
188
|
+
default: return run;
|
|
199
189
|
}
|
|
200
|
-
|
|
190
|
+
};
|
|
191
|
+
/** Serialize the complete paragraph measurement contract into a cache key. */
|
|
192
|
+
function hashParagraphBlock(block) {
|
|
193
|
+
return JSON.stringify({
|
|
194
|
+
lineBreakProviderGeneration: getLineBreakProviderGeneration(),
|
|
195
|
+
attrs: block.attrs,
|
|
196
|
+
runs: block.runs.map(runMeasureCacheInput)
|
|
197
|
+
});
|
|
201
198
|
}
|
|
202
199
|
/**
|
|
203
200
|
* Evict oldest entries if paragraph cache exceeds max size
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { HyperlinkInfo } from "../types.js";
|
|
2
|
+
//#region src/layout-engine/measure/hyperlinkInstance.d.ts
|
|
3
|
+
declare const setHyperlinkInstanceIndex: (hyperlink: HyperlinkInfo, instanceIndex: number) => void;
|
|
4
|
+
declare const getHyperlinkInstanceIndex: (hyperlink: HyperlinkInfo) => number | undefined;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { getHyperlinkInstanceIndex, setHyperlinkInstanceIndex };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/layout-engine/measure/hyperlinkInstance.ts
|
|
2
|
+
const hyperlinkInstanceIndexes = /* @__PURE__ */ new WeakMap();
|
|
3
|
+
const setHyperlinkInstanceIndex = (hyperlink, instanceIndex) => {
|
|
4
|
+
hyperlinkInstanceIndexes.set(hyperlink, instanceIndex);
|
|
5
|
+
};
|
|
6
|
+
const getHyperlinkInstanceIndex = (hyperlink) => hyperlinkInstanceIndexes.get(hyperlink);
|
|
7
|
+
//#endregion
|
|
8
|
+
export { getHyperlinkInstanceIndex, setHyperlinkInstanceIndex };
|
|
@@ -5,10 +5,24 @@ type TableCellGrid = {
|
|
|
5
5
|
sourceCellsByRow: ReadonlyMap<number, ReadonlyMap<number, TableCell>>;
|
|
6
6
|
sourceColumnsByCell: ReadonlyMap<TableCell, number>;
|
|
7
7
|
};
|
|
8
|
+
type TableCellPlacement = {
|
|
9
|
+
sourceColumn: number;
|
|
10
|
+
columnSpan: number;
|
|
11
|
+
left: number;
|
|
12
|
+
width: number;
|
|
13
|
+
};
|
|
14
|
+
type TableCellPlacements = ReadonlyMap<TableCell, TableCellPlacement>;
|
|
15
|
+
type BuildTableCellPlacementsOptions = {
|
|
16
|
+
grid: TableCellGrid;
|
|
17
|
+
columnWidths: readonly number[];
|
|
18
|
+
bidi: boolean;
|
|
19
|
+
};
|
|
8
20
|
declare const buildTableCellGrid: (rows: readonly TableRow[], columnCount: number) => TableCellGrid;
|
|
9
21
|
declare const getFirstAvailableColumn: (grid: TableCellGrid, rowIndex: number, startingColumn: number) => number;
|
|
10
22
|
declare const getSourceCellAt: (grid: TableCellGrid, rowIndex: number, columnIndex: number) => TableCell | undefined;
|
|
11
23
|
declare const getSourceCellColumn: (grid: TableCellGrid, cell: TableCell) => number | undefined;
|
|
24
|
+
/** Resolve all source cells to canonical logical columns and physical boxes. */
|
|
25
|
+
declare const buildTableCellPlacements: ({ grid, columnWidths, bidi }: BuildTableCellPlacementsOptions) => TableCellPlacements;
|
|
12
26
|
declare const getTableCellVerticalBorderHeight: (grid: TableCellGrid, cell: TableCell | undefined, rowIndex: number) => number;
|
|
13
27
|
//#endregion
|
|
14
|
-
export { TableCellGrid, buildTableCellGrid, getFirstAvailableColumn, getSourceCellAt, getSourceCellColumn, getTableCellVerticalBorderHeight };
|
|
28
|
+
export { TableCellGrid, TableCellPlacement, TableCellPlacements, buildTableCellGrid, buildTableCellPlacements, getFirstAvailableColumn, getSourceCellAt, getSourceCellColumn, getTableCellVerticalBorderHeight };
|
|
@@ -6,10 +6,11 @@ const buildTableCellGrid = (rows, columnCount) => {
|
|
|
6
6
|
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
|
|
7
7
|
const row = rows[rowIndex];
|
|
8
8
|
if (!row) continue;
|
|
9
|
-
|
|
9
|
+
const gridBefore = Math.max(0, Math.min(columnCount, Math.trunc(row.gridBefore ?? 0)));
|
|
10
|
+
let columnIndex = firstAvailableColumn(occupiedColumnsByRow.get(rowIndex), gridBefore);
|
|
10
11
|
for (const cell of row.cells) {
|
|
11
|
-
const columnSpan = cell.colSpan ?? 1;
|
|
12
|
-
const rowSpan = cell.rowSpan ?? 1;
|
|
12
|
+
const columnSpan = Math.max(1, Math.trunc(cell.colSpan ?? 1));
|
|
13
|
+
const rowSpan = Math.max(1, Math.trunc(cell.rowSpan ?? 1));
|
|
13
14
|
sourceColumnsByCell.set(cell, columnIndex);
|
|
14
15
|
const rowEnd = Math.min(rows.length, rowIndex + rowSpan);
|
|
15
16
|
const columnEnd = Math.min(columnCount, columnIndex + columnSpan);
|
|
@@ -17,9 +18,9 @@ const buildTableCellGrid = (rows, columnCount) => {
|
|
|
17
18
|
const cellsByColumn = getOrCreateCellRow(sourceCellsByRow, gridRowIndex);
|
|
18
19
|
for (let gridColumnIndex = columnIndex; gridColumnIndex < columnEnd; gridColumnIndex++) cellsByColumn.set(gridColumnIndex, cell);
|
|
19
20
|
}
|
|
20
|
-
if (rowSpan > 1) for (let spannedRowIndex = rowIndex + 1; spannedRowIndex <
|
|
21
|
+
if (rowSpan > 1) for (let spannedRowIndex = rowIndex + 1; spannedRowIndex < rowEnd; spannedRowIndex++) {
|
|
21
22
|
const occupied = getOrCreateOccupiedRow(occupiedColumnsByRow, spannedRowIndex);
|
|
22
|
-
for (let
|
|
23
|
+
for (let gridColumnIndex = columnIndex; gridColumnIndex < columnEnd; gridColumnIndex++) occupied.add(gridColumnIndex);
|
|
23
24
|
}
|
|
24
25
|
columnIndex = firstAvailableColumn(occupiedColumnsByRow.get(rowIndex), columnIndex + columnSpan);
|
|
25
26
|
}
|
|
@@ -33,6 +34,28 @@ const buildTableCellGrid = (rows, columnCount) => {
|
|
|
33
34
|
const getFirstAvailableColumn = (grid, rowIndex, startingColumn) => firstAvailableColumn(grid.occupiedColumnsByRow.get(rowIndex), startingColumn);
|
|
34
35
|
const getSourceCellAt = (grid, rowIndex, columnIndex) => grid.sourceCellsByRow.get(rowIndex)?.get(columnIndex);
|
|
35
36
|
const getSourceCellColumn = (grid, cell) => grid.sourceColumnsByCell.get(cell);
|
|
37
|
+
/** Resolve all source cells to canonical logical columns and physical boxes. */
|
|
38
|
+
const buildTableCellPlacements = ({ grid, columnWidths, bidi }) => {
|
|
39
|
+
const columnOffsets = [0];
|
|
40
|
+
for (const columnWidth of columnWidths) columnOffsets.push((columnOffsets.at(-1) ?? 0) + columnWidth);
|
|
41
|
+
const tableWidth = columnOffsets.at(-1) ?? 0;
|
|
42
|
+
const placements = /* @__PURE__ */ new Map();
|
|
43
|
+
for (const [cell, sourceColumn] of grid.sourceColumnsByCell) {
|
|
44
|
+
if (sourceColumn < 0 || sourceColumn >= columnWidths.length) continue;
|
|
45
|
+
const declaredColumnSpan = Math.max(1, Math.trunc(cell.colSpan ?? 1));
|
|
46
|
+
const columnSpan = Math.min(declaredColumnSpan, columnWidths.length - sourceColumn);
|
|
47
|
+
const logicalLeft = columnOffsets.at(sourceColumn) ?? 0;
|
|
48
|
+
const logicalRight = columnOffsets.at(sourceColumn + columnSpan) ?? logicalLeft;
|
|
49
|
+
const width = logicalRight - logicalLeft;
|
|
50
|
+
placements.set(cell, {
|
|
51
|
+
sourceColumn,
|
|
52
|
+
columnSpan,
|
|
53
|
+
left: bidi ? tableWidth - logicalRight : logicalLeft,
|
|
54
|
+
width
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return placements;
|
|
58
|
+
};
|
|
36
59
|
const getTableCellVerticalBorderHeight = (grid, cell, rowIndex) => {
|
|
37
60
|
const sourceColumn = cell ? getSourceCellColumn(grid, cell) : void 0;
|
|
38
61
|
const aboveBottom = (sourceColumn === void 0 ? void 0 : getSourceCellAt(grid, rowIndex - 1, sourceColumn))?.borders?.bottom;
|
|
@@ -59,4 +82,4 @@ const getOrCreateOccupiedRow = (occupiedColumnsByRow, rowIndex) => {
|
|
|
59
82
|
return occupiedColumns;
|
|
60
83
|
};
|
|
61
84
|
//#endregion
|
|
62
|
-
export { buildTableCellGrid, getFirstAvailableColumn, getSourceCellAt, getSourceCellColumn, getTableCellVerticalBorderHeight };
|
|
85
|
+
export { buildTableCellGrid, buildTableCellPlacements, getFirstAvailableColumn, getSourceCellAt, getSourceCellColumn, getTableCellVerticalBorderHeight };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { TableBlock } from "../types.js";
|
|
2
|
+
//#region src/layout-engine/measure/tableInlinePlacement.d.ts
|
|
3
|
+
type TableInlinePlacement = {
|
|
4
|
+
alignment: "center";
|
|
5
|
+
} | {
|
|
6
|
+
alignment: "left" | "right";
|
|
7
|
+
offset: number;
|
|
8
|
+
};
|
|
9
|
+
/** Resolve an inline table's horizontal anchor without losing RTL leading-edge semantics. */
|
|
10
|
+
declare const resolveTableInlinePlacement: (table: Pick<TableBlock, "bidi" | "indent" | "justification" | "rows">) => TableInlinePlacement;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { resolveTableInlinePlacement };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { resolveTableCellPadding } from "../types.js";
|
|
2
|
+
//#region src/layout-engine/measure/tableInlinePlacement.ts
|
|
3
|
+
/** Resolve an inline table's horizontal anchor without losing RTL leading-edge semantics. */
|
|
4
|
+
const resolveTableInlinePlacement = (table) => {
|
|
5
|
+
if (table.justification === "center") return { alignment: "center" };
|
|
6
|
+
if (table.justification === "right") return {
|
|
7
|
+
alignment: "right",
|
|
8
|
+
offset: 0
|
|
9
|
+
};
|
|
10
|
+
const firstCell = table.rows.at(0)?.cells.at(0);
|
|
11
|
+
const firstCellPadding = firstCell ? resolveTableCellPadding(firstCell) : void 0;
|
|
12
|
+
if (table.justification === "left" || table.bidi !== true) return {
|
|
13
|
+
alignment: "left",
|
|
14
|
+
offset: table.indent ?? -(firstCellPadding?.left ?? 0)
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
alignment: "right",
|
|
18
|
+
offset: table.indent ?? -(firstCellPadding?.right ?? 0)
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
export { resolveTableInlinePlacement };
|
|
@@ -632,7 +632,7 @@ type TableBlock = {
|
|
|
632
632
|
layout?: "fixed" | "autofit";
|
|
633
633
|
/** Table horizontal alignment */
|
|
634
634
|
justification?: "left" | "center" | "right";
|
|
635
|
-
/** Table indent from
|
|
635
|
+
/** Table indent from the leading margin (in pixels, from w:tblInd). */
|
|
636
636
|
indent?: number;
|
|
637
637
|
/** Right-to-left column order (w:bidiVisual): logical column 0 paints on the right. */
|
|
638
638
|
bidi?: boolean;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HyperlinkInfo, MeasuredLine, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphMeasure, Run, TabStop } from "../layout-engine/types.js";
|
|
2
2
|
import { RenderContext } from "./renderUtils.js";
|
|
3
|
+
import { PhysicalParagraphInlineLayout } from "../utils/paragraphInlineLayout.js";
|
|
3
4
|
//#region src/layout-painter/renderParagraph.d.ts
|
|
4
5
|
/**
|
|
5
6
|
* CSS class names for paragraph rendering
|
|
@@ -70,6 +71,8 @@ type RenderLineOptions = {
|
|
|
70
71
|
firstLineIndentPx?: number;
|
|
71
72
|
/** Paragraph base direction for logical first-line indentation. */
|
|
72
73
|
isRtl?: boolean;
|
|
74
|
+
/** Explicit alignment after resolving authored bidi physical sides. */
|
|
75
|
+
explicitAlignment?: PhysicalParagraphInlineLayout["explicitAlignment"];
|
|
73
76
|
/** Full paragraph content-box width before physical indents. */
|
|
74
77
|
contentWidthPx?: number;
|
|
75
78
|
/** Line-specific floating image margins (calculated per-line based on Y overlap) */
|