@stll/folio-core 0.25.3 → 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.
@@ -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
@@ -1454,9 +1454,10 @@ function renderParagraphFragment(fragment, block, measure, context, options = {}
1454
1454
  });
1455
1455
  const isFlexLine = lineEl.dataset["flexLine"] === "true";
1456
1456
  const lineMarginLeft = Math.min(indentLeft, 0) + lineLeftOffset;
1457
+ const lineMarginRight = Math.min(indentRight, 0) + lineRightOffset;
1457
1458
  if (lineMarginLeft !== 0 || lineRightOffset > 0 || indentLeft < 0 || indentRight < 0) {
1458
1459
  lineEl.style.marginLeft = `${lineMarginLeft}px`;
1459
- if (lineRightOffset > 0) lineEl.style.marginRight = `${lineRightOffset}px`;
1460
+ if (lineMarginRight !== 0) lineEl.style.marginRight = `${lineMarginRight}px`;
1460
1461
  const constrainedWidth = lineAvailableWidth - lineLeftOffset - lineRightOffset;
1461
1462
  if (constrainedWidth > 0) lineEl.style.width = `${constrainedWidth}px`;
1462
1463
  }
@@ -1476,27 +1477,33 @@ function renderParagraphFragment(fragment, block, measure, context, options = {}
1476
1477
  if (isFirstLine && block.attrs?.listMarker && !block.attrs.listMarkerHidden) {
1477
1478
  const hanging = indent?.hanging ?? 0;
1478
1479
  const firstLine = indent?.firstLine ?? 0;
1479
- const markerStart = hanging > 0 ? indentLeft - hanging : indentLeft + firstLine;
1480
- lineEl.style.paddingLeft = `${Math.max(0, markerStart)}px`;
1480
+ const markerPhysicalStart = isRtl ? "right" : "left";
1481
+ const markerIndent = markerPhysicalStart === "right" ? indentRight : indentLeft;
1482
+ const markerStart = hanging > 0 ? markerIndent - hanging : markerIndent + firstLine;
1483
+ const logicalMarkerVisualOffset = getListMarkerVisualOffset(block);
1484
+ if (markerPhysicalStart === "right") lineEl.style.paddingRight = `${Math.max(0, markerStart)}px`;
1485
+ else lineEl.style.paddingLeft = `${Math.max(0, markerStart)}px`;
1481
1486
  lineEl.style.textIndent = "0";
1482
- const marker = renderListMarker(block.attrs.listMarker, getListMarkerInlineWidth(block), getListMarkerVisualOffset(block), doc, resolveListMarkerFont(block), block.attrs.listMarkerRevision, block.attrs.listMarkerSecondSlotOffsetTwips);
1483
- const markerMarginLeft = markerStart - Math.min(indentLeft, 0);
1484
- if (markerMarginLeft < 0) marker.style.marginLeft = `${markerMarginLeft}px`;
1487
+ const marker = renderListMarker({
1488
+ marker: block.attrs.listMarker,
1489
+ inlineWidth: getListMarkerInlineWidth(block),
1490
+ visualOffset: markerPhysicalStart === "right" ? -logicalMarkerVisualOffset : logicalMarkerVisualOffset,
1491
+ doc,
1492
+ formatting: resolveListMarkerFont(block),
1493
+ ...block.attrs.listMarkerRevision === void 0 ? {} : { revision: block.attrs.listMarkerRevision },
1494
+ ...block.attrs.listMarkerSecondSlotOffsetTwips === void 0 ? {} : { secondSlotOffsetTwips: block.attrs.listMarkerSecondSlotOffsetTwips },
1495
+ physicalStart: markerPhysicalStart
1496
+ });
1497
+ const markerMarginLeft = markerStart - (markerPhysicalStart === "right" ? Math.min(indentRight, 0) : Math.min(indentLeft, 0));
1498
+ if (markerMarginLeft < 0) if (markerPhysicalStart === "right") marker.style.marginRight = `${markerMarginLeft}px`;
1499
+ else marker.style.marginLeft = `${markerMarginLeft}px`;
1485
1500
  lineEl.prepend(marker);
1486
1501
  }
1487
1502
  fragmentEl.append(lineEl);
1488
1503
  }
1489
1504
  return fragmentEl;
1490
1505
  }
1491
- /**
1492
- * Render a list marker element as an inline-block at the start of the first
1493
- * body line. `inlineWidth` (from `getListMarkerInlineWidth`) sizes the marker
1494
- * so the body text aligns at the next tab stop per ECMA-376 §17.9.25 —
1495
- * this honours `w:suff` (`tab` / `space` / `nothing`) and the document's
1496
- * tab grid. Long markers like "1.1.1." therefore grow to the next stop
1497
- * instead of butting against the body text.
1498
- */
1499
- function renderListMarker(marker, inlineWidth, visualOffset, doc, formatting, revision, secondSlotOffsetTwips) {
1506
+ function renderListMarker({ marker, inlineWidth, visualOffset, doc, formatting, revision, secondSlotOffsetTwips, physicalStart }) {
1500
1507
  const span = doc.createElement("span");
1501
1508
  span.className = "layout-list-marker";
1502
1509
  span.style.display = "inline-block";
@@ -1505,8 +1512,8 @@ function renderListMarker(marker, inlineWidth, visualOffset, doc, formatting, re
1505
1512
  if (formatting.bold !== void 0) span.style.fontWeight = formatting.bold ? "700" : "normal";
1506
1513
  if (formatting.italic !== void 0) span.style.fontStyle = formatting.italic ? "italic" : "normal";
1507
1514
  if (formatting.rtl !== void 0) span.dir = formatting.rtl ? "rtl" : "ltr";
1508
- span.style.textAlign = "left";
1509
- span.style.textAlignLast = "left";
1515
+ span.style.textAlign = physicalStart;
1516
+ span.style.textAlignLast = physicalStart;
1510
1517
  span.style.boxSizing = "border-box";
1511
1518
  span.style.width = `${inlineWidth}px`;
1512
1519
  if (visualOffset !== 0) span.style.transform = `translateX(${visualOffset}px)`;
@@ -1540,7 +1547,8 @@ function renderListMarker(marker, inlineWidth, visualOffset, doc, formatting, re
1540
1547
  secondSlot.style.display = "inline-block";
1541
1548
  span.style.position = "relative";
1542
1549
  secondSlot.style.position = "absolute";
1543
- secondSlot.style.left = `${secondSlotOffsetTwips / 15}px`;
1550
+ if (physicalStart === "right") secondSlot.style.right = `${secondSlotOffsetTwips / 15}px`;
1551
+ else secondSlot.style.left = `${secondSlotOffsetTwips / 15}px`;
1544
1552
  secondSlot.style.top = "0";
1545
1553
  span.append(firstSlot, secondSlot);
1546
1554
  return span;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/folio-core",
3
- "version": "0.25.3",
3
+ "version": "0.25.4",
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",