@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.
@@ -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 indent = paragraphBlock.attrs?.indent;
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(paragraphBlock, line, localX - indentLeft, availableWidth);
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: getMaxLineWidth(cellMeasure.lines, 100),
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.attrs?.alignment ?? "left";
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 { getTableRowLeadingWidth } from "../../layout-engine/types.js";
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
- for (let c = 0; c < rowMeasure.cells.length; c++) {
221
- const cellMeasure = rowMeasure.cells[c];
222
- if (localX >= colX && localX < colX + cellMeasure.width) {
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
- colX += cellMeasure.width;
227
- }
228
- if (colIndex === -1) {
229
- colIndex = rowMeasure.cells.length - 1;
230
- if (colIndex < 0) continue;
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 cellLocalX = localX - colLeft;
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 { getTableRowLeadingWidth, resolveTableCellPadding } from "../../layout-engine/types.js";
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, contentWidth);
355
- const endX = charOffsetToX(paragraphBlock, line, charOffsetTo, contentWidth);
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 + cellX + contentOffsetX + Math.min(startX, endX),
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 indent = paragraphBlock.attrs?.indent;
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
- let x = paginator.getColumnX(columnIndex);
525
- if (block.justification === "center") x += (paginator.columnWidth - measure.totalWidth) / 2;
526
- else if (block.justification === "right") x = x + paginator.columnWidth - measure.totalWidth;
527
- else if (block.indent !== void 0) x += block.indent;
528
- else {
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
- * Generate a simple hash for a paragraph block
169
- * Used as cache key to identify identical content
170
- */
171
- function hashParagraphBlock(block) {
172
- const parts = [`lbp:${getLineBreakProviderGeneration()}`];
173
- for (const run of block.runs) if (run.kind === "text") parts.push(`t:${run.text}|${run.fontFamily}|${run.eastAsiaFontFamily}|${run.complexScriptFontFamily}|${run.fontSize}|${run.complexScriptFontSize}|${run.bold}|${run.complexScriptBold}|${run.italic}|${run.complexScriptItalic}|${run.forceComplexScript}|${run.allCaps}|${run.smallCaps}|${run.horizontalScale}|${run.letterSpacing}|${run.language?.val}|${run.language?.eastAsia}|${run.language?.bidi}`);
174
- else if (run.kind === "tab") parts.push(`tab:${run.width}`);
175
- else if (run.kind === "image") parts.push(`img:${run.width}x${run.height}:${run.exactLineHeight === true ? "exact" : "text"}`);
176
- else if (run.kind === "lineBreak") parts.push("br");
177
- const attrs = block.attrs;
178
- if (attrs) {
179
- if (attrs.alignment) parts.push(`align:${attrs.alignment}`);
180
- if (attrs.outlineLevel !== void 0) parts.push(`outline:${attrs.outlineLevel}`);
181
- if (attrs.indent) parts.push(`indent:${attrs.indent.left}|${attrs.indent.right}|${attrs.indent.firstLine}|${attrs.indent.hanging}`);
182
- if (attrs.spacing) parts.push(`spacing:${attrs.spacing.before}|${attrs.spacing.after}|${attrs.spacing.line}|${attrs.spacing.lineRule}`);
183
- if (attrs.defaultFontSize != null) parts.push(`dfs:${attrs.defaultFontSize}`);
184
- if (attrs.defaultFontFamily != null) parts.push(`dff:${attrs.defaultFontFamily}`);
185
- if (attrs.suppressEmptyParagraphHeight) parts.push("sup");
186
- if (attrs.reserveEmptyOutlineHeight) parts.push("outline-empty-reserve");
187
- if (attrs.documentGridLinePitch !== void 0) parts.push(`documentGrid:${attrs.documentGridLinePitch}|${attrs.snapToGrid !== false}`);
188
- if (attrs.justificationCompatibility) parts.push(`justify-compat:${attrs.justificationCompatibility.type}`);
189
- if (attrs.listMarker !== void 0) {
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
- return parts.join("||");
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
- let columnIndex = firstAvailableColumn(occupiedColumnsByRow.get(rowIndex), row.gridBefore ?? 0);
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 < rowIndex + rowSpan; spannedRowIndex++) {
21
+ if (rowSpan > 1) for (let spannedRowIndex = rowIndex + 1; spannedRowIndex < rowEnd; spannedRowIndex++) {
21
22
  const occupied = getOrCreateOccupiedRow(occupiedColumnsByRow, spannedRowIndex);
22
- for (let columnOffset = 0; columnOffset < columnSpan; columnOffset++) occupied.add(columnIndex + columnOffset);
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 left margin (in pixels, from w:tblInd) */
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) */