@stll/folio-core 0.29.0 → 0.30.0

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.
Files changed (99) hide show
  1. package/dist/controller/folioEditor.d.ts +28 -1
  2. package/dist/controller/folioEditor.js +37 -26
  3. package/dist/controller/fontReadiness.js +28 -24
  4. package/dist/controller/hiddenEditorManager.d.ts +2 -7
  5. package/dist/controller/layoutPipeline.js +33 -3
  6. package/dist/controller/noteEditorManager.d.ts +3 -6
  7. package/dist/docx/metafileSvg.d.ts +12 -0
  8. package/dist/docx/metafileSvg.js +462 -0
  9. package/dist/docx/paragraphTextBoxEnrichment.js +55 -4
  10. package/dist/docx/parser.d.ts +2 -2
  11. package/dist/docx/parser.js +26 -0
  12. package/dist/docx/runParser.js +2 -1
  13. package/dist/docx/serializer/runSerializer.js +14 -2
  14. package/dist/docx/styleParser.js +2 -1
  15. package/dist/docx/textBoxParser.js +14 -0
  16. package/dist/docx/vmlImageParser.js +72 -152
  17. package/dist/docx/vmlPreview.d.ts +31 -0
  18. package/dist/docx/vmlPreview.js +535 -0
  19. package/dist/docx/wrapTypes.d.ts +2 -36
  20. package/dist/docx/wrapTypes.js +1 -39
  21. package/dist/fonts/fontAlternates.d.ts +8 -0
  22. package/dist/fonts/fontAlternates.js +16 -0
  23. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  24. package/dist/layout-bridge/convert/footnoteLayout.js +34 -3
  25. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  26. package/dist/layout-bridge/convert/headerFooterLayout.js +5 -0
  27. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +3 -0
  28. package/dist/layout-bridge/convert/toFlowBlocks.js +78 -28
  29. package/dist/layout-bridge/dom/noteStoryDom.d.ts +1 -1
  30. package/dist/layout-bridge/engine/selectionRects.js +3 -1
  31. package/dist/layout-engine/index.js +65 -32
  32. package/dist/layout-engine/justifiedLineFit.d.ts +7 -0
  33. package/dist/layout-engine/justifiedLineFit.js +6 -0
  34. package/dist/layout-engine/keep-together.js +15 -10
  35. package/dist/layout-engine/measure/complexScriptFormatting.d.ts +6 -1
  36. package/dist/layout-engine/measure/complexScriptFormatting.js +11 -2
  37. package/dist/layout-engine/measure/floatingTablePosition.d.ts +16 -1
  38. package/dist/layout-engine/measure/floatingTablePosition.js +29 -6
  39. package/dist/layout-engine/measure/lineBreakProvider.js +67 -8
  40. package/dist/layout-engine/measure/listMarkerWidth.d.ts +1 -0
  41. package/dist/layout-engine/measure/listMarkerWidth.js +23 -10
  42. package/dist/layout-engine/measure/measureBlocks.d.ts +15 -0
  43. package/dist/layout-engine/measure/measureBlocks.js +197 -30
  44. package/dist/layout-engine/measure/measureContainer.js +20 -24
  45. package/dist/layout-engine/measure/measureHelpers.d.ts +1 -1
  46. package/dist/layout-engine/measure/measureHelpers.js +14 -8
  47. package/dist/layout-engine/measure/measureParagraph.js +111 -37
  48. package/dist/layout-engine/measure/measureTypes.d.ts +3 -0
  49. package/dist/layout-engine/measure/tabCalculator.d.ts +123 -0
  50. package/dist/layout-engine/measure/tabCalculator.js +144 -0
  51. package/dist/layout-engine/measure/tableCellFlow.d.ts +21 -1
  52. package/dist/layout-engine/measure/tableCellFlow.js +45 -8
  53. package/dist/layout-engine/measure/tableInlinePlacement.d.ts +10 -2
  54. package/dist/layout-engine/measure/tableInlinePlacement.js +17 -15
  55. package/dist/layout-engine/paginator.d.ts +26 -2
  56. package/dist/layout-engine/paginator.js +76 -14
  57. package/dist/layout-engine/paragraphSpacing.d.ts +3 -1
  58. package/dist/layout-engine/paragraphSpacing.js +4 -3
  59. package/dist/layout-engine/renderedBreakReconciliation.d.ts +14 -3
  60. package/dist/layout-engine/renderedBreakReconciliation.js +45 -14
  61. package/dist/layout-engine/section-breaks.d.ts +3 -1
  62. package/dist/layout-engine/section-breaks.js +6 -2
  63. package/dist/layout-engine/types.d.ts +18 -1
  64. package/dist/layout-engine/types.js +1 -1
  65. package/dist/layout-painter/imageLayout.d.ts +1 -1
  66. package/dist/layout-painter/renderPage.js +6 -1
  67. package/dist/layout-painter/renderParagraph.js +169 -82
  68. package/dist/layout-painter/renderTable.d.ts +1 -1
  69. package/dist/layout-painter/renderTable.js +18 -7
  70. package/dist/layout-painter/renderTextBox.d.ts +1 -1
  71. package/dist/layout-painter/renderTextBox.js +6 -1
  72. package/dist/model.d.ts +3 -3
  73. package/dist/model.js +2 -2
  74. package/dist/paged-layout/sectionBlockWidths.d.ts +14 -0
  75. package/dist/paged-layout/sectionBlockWidths.js +103 -14
  76. package/dist/prosemirror/attrs/index.js +12 -1
  77. package/dist/prosemirror/conversion/fromProseDoc.js +6 -2
  78. package/dist/prosemirror/conversion/toProseDoc.js +10 -3
  79. package/dist/prosemirror/extensions/marks/CharacterSpacingExtension.js +9 -5
  80. package/dist/prosemirror/extensions/nodes/TableExtension.js +2 -0
  81. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +6 -2
  82. package/dist/prosemirror/schema/nodes.d.ts +4 -0
  83. package/dist/prosemirror/utils/tabCalculator.d.ts +2 -123
  84. package/dist/prosemirror/utils/tabCalculator.js +1 -140
  85. package/dist/render-dom/RemoteSelectionOverlay.d.ts +4 -4
  86. package/dist/types/documentEnumValues.d.ts +4 -1
  87. package/dist/types/documentEnumValues.js +12 -1
  88. package/dist/types/editor-story.d.ts +8 -0
  89. package/dist/types/editor-story.js +0 -0
  90. package/dist/types/remote-selection.d.ts +11 -0
  91. package/dist/types/remote-selection.js +0 -0
  92. package/dist/types/wrap.d.ts +36 -0
  93. package/dist/types/wrap.js +40 -0
  94. package/dist/utils/fontResolver.d.ts +1 -1
  95. package/dist/utils/fontResolver.js +42 -2
  96. package/dist/utils/formatToStyle.js +4 -2
  97. package/dist/utils/horizontalScale.d.ts +28 -0
  98. package/dist/utils/horizontalScale.js +42 -0
  99. package/package.json +2 -2
@@ -29,6 +29,8 @@ type RunFormatting = {
29
29
  */
30
30
  shading?: string;
31
31
  fontFamily?: string;
32
+ /** OOXML `w:altName` fallback for `fontFamily`, used only when the primary face is absent. */
33
+ alternateFontFamily?: string;
32
34
  /**
33
35
  * Resolved East-Asian font (`w:eastAsia` / `eastAsiaTheme`). CJK code points
34
36
  * in this run measure and paint with this font; non-CJK keeps `fontFamily`
@@ -37,6 +39,8 @@ type RunFormatting = {
37
39
  * to `fontFamily`.
38
40
  */
39
41
  eastAsiaFontFamily?: string;
42
+ /** OOXML alternate-name fallback for `eastAsiaFontFamily`. */
43
+ eastAsiaAlternateFontFamily?: string;
40
44
  /**
41
45
  * Resolved complex-script font (`w:cs` / `cstheme`). Arabic, Hebrew, Indic
42
46
  * and South-East Asian code points in this run measure and paint with this
@@ -46,6 +50,8 @@ type RunFormatting = {
46
50
  * does NOT do and is why this slot has to reach layout at all.
47
51
  */
48
52
  complexScriptFontFamily?: string;
53
+ /** OOXML alternate-name fallback for `complexScriptFontFamily`. */
54
+ complexScriptAlternateFontFamily?: string;
49
55
  /** Independent complex-script size (`w:szCs`), in points. */
50
56
  complexScriptFontSize?: number;
51
57
  /** Independent complex-script weight (`w:bCs`). */
@@ -127,7 +133,7 @@ type RunFormatting = {
127
133
  /** Groups a suggestion's runs for accept/reject and scroll-to. */
128
134
  suggestionId?: string;
129
135
  };
130
- type ListMarkerFormatting = Pick<RunFormatting, "fontFamily" | "eastAsiaFontFamily" | "complexScriptFontFamily" | "fontSize" | "complexScriptFontSize" | "bold" | "complexScriptBold" | "italic" | "complexScriptItalic" | "rtl" | "forceComplexScript">;
136
+ type ListMarkerFormatting = Pick<RunFormatting, "fontFamily" | "alternateFontFamily" | "eastAsiaFontFamily" | "eastAsiaAlternateFontFamily" | "complexScriptFontFamily" | "complexScriptAlternateFontFamily" | "fontSize" | "complexScriptFontSize" | "bold" | "complexScriptBold" | "italic" | "complexScriptItalic" | "rtl" | "forceComplexScript">;
131
137
  /**
132
138
  * Hyperlink information for a run.
133
139
  */
@@ -500,6 +506,8 @@ type ParagraphAttrs = {
500
506
  listMarkerSecondSlotOffsetTwips?: number;
501
507
  defaultFontSize?: number;
502
508
  defaultFontFamily?: string;
509
+ /** OOXML alternate-name fallback for `defaultFontFamily`. */
510
+ defaultAlternateFontFamily?: string;
503
511
  /**
504
512
  * Document-wide `w:defaultTabStop` (§17.6.13) in twips. Read by the list
505
513
  * marker tab-stop math so long markers align body text at the document's
@@ -591,6 +599,8 @@ type TableRow = {
591
599
  gridAfter?: number;
592
600
  height?: number;
593
601
  heightRule?: "auto" | "atLeast" | "exact";
602
+ /** Effective row-level table justification after OOXML precedence. */
603
+ justification?: "left" | "center" | "right";
594
604
  isHeader?: boolean;
595
605
  /** `w:cantSplit`: keep this row in one flow region. */
596
606
  cantSplit?: boolean;
@@ -765,6 +775,8 @@ type TextBoxBlock = {
765
775
  autoFit?: ShapeTextBody["autoFit"];
766
776
  /** Horizontal text wrapping inside the box */
767
777
  textWrap?: ShapeTextBody["textWrap"];
778
+ /** Vertical text alignment inside the box */
779
+ verticalAlign?: ShapeTextBody["anchor"];
768
780
  /** Fill/background color */
769
781
  fillColor?: string;
770
782
  /** Border width in pixels */
@@ -868,6 +880,11 @@ type MeasuredLine = {
868
880
  descent: number;
869
881
  /** Total line height in pixels. */
870
882
  lineHeight: number;
883
+ /** Paint adjustment admitted by measurement for this line. */
884
+ justificationPaint?: {
885
+ type: "space-contraction";
886
+ contractionPx: number;
887
+ };
871
888
  /** Left offset from floating images (pixels from content left edge). */
872
889
  leftOffset?: number;
873
890
  /** Right offset from floating images (pixels from content right edge). */
@@ -1,5 +1,5 @@
1
1
  import { __exportAll } from "../_virtual/_rolldown/runtime.js";
2
- import { isFloatingWrapType, isWrapNone } from "../docx/wrapTypes.js";
2
+ import { isFloatingWrapType, isWrapNone } from "../types/wrap.js";
3
3
  //#region src/layout-engine/types.ts
4
4
  var types_exports = /* @__PURE__ */ __exportAll({
5
5
  DEFAULT_TEXTBOX_MARGINS: () => DEFAULT_TEXTBOX_MARGINS,
@@ -1,7 +1,7 @@
1
1
  import { FlowBlock, Measure } from "../layout-engine/types.js";
2
2
  import { ImageAttrs } from "../prosemirror/schema/nodes.js";
3
3
  import { BlockLookup } from "./index.js";
4
- import { WrapType } from "../docx/wrapTypes.js";
4
+ import { WrapType } from "../types/wrap.js";
5
5
  //#region src/layout-painter/imageLayout.d.ts
6
6
  /**
7
7
  * Anchored wrap-type targets — the OOXML wrap types except `inline`.
@@ -4,6 +4,7 @@ import { measureParagraph } from "../layout-engine/measure/measureParagraph.js";
4
4
  import { floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun } from "../layout-engine/types.js";
5
5
  import { resolveFontFamily } from "../utils/fontResolver.js";
6
6
  import { borderToStyle } from "../utils/formatToStyle.js";
7
+ import { normalizeHorizontalScalePercent } from "../utils/horizontalScale.js";
7
8
  import { applySanitizedImageSrc } from "../utils/sanitizeImageSrc.js";
8
9
  import { eighthsToPixels, pointsToPixels } from "../utils/units.js";
9
10
  import { resolveAnchoredImagePosition as resolveAnchoredImagePosition$1 } from "./anchoredImagePosition.js";
@@ -1335,12 +1336,15 @@ function runContentKey(run) {
1335
1336
  if (run.color) parts.push(`c:${run.color}`);
1336
1337
  if (run.highlight) parts.push(`hi:${run.highlight}`);
1337
1338
  if (run.fontFamily) parts.push(`ff:${run.fontFamily}`);
1339
+ if (run.alternateFontFamily) parts.push(`ff-alt:${run.alternateFontFamily}`);
1338
1340
  if (run.complexScriptFontFamily) parts.push(`cs:${run.complexScriptFontFamily}`);
1341
+ if (run.complexScriptAlternateFontFamily) parts.push(`cs-alt:${run.complexScriptAlternateFontFamily}`);
1339
1342
  if (run.complexScriptFontSize !== void 0) parts.push(`cs-size:${run.complexScriptFontSize}`);
1340
1343
  if (run.complexScriptBold !== void 0) parts.push(`cs-bold:${run.complexScriptBold}`);
1341
1344
  if (run.complexScriptItalic !== void 0) parts.push(`cs-italic:${run.complexScriptItalic}`);
1342
1345
  if (run.forceComplexScript !== void 0) parts.push(`cs-force:${run.forceComplexScript}`);
1343
1346
  if (run.eastAsiaFontFamily) parts.push(`ea:${run.eastAsiaFontFamily}`);
1347
+ if (run.eastAsiaAlternateFontFamily) parts.push(`ea-alt:${run.eastAsiaAlternateFontFamily}`);
1344
1348
  if (run.fontSize !== void 0) parts.push(`fs:${run.fontSize}`);
1345
1349
  if (run.letterSpacing !== void 0) parts.push(`ls:${run.letterSpacing}`);
1346
1350
  if (run.superscript) parts.push("sup");
@@ -1348,7 +1352,8 @@ function runContentKey(run) {
1348
1352
  if (run.allCaps) parts.push("ac");
1349
1353
  if (run.smallCaps) parts.push("sc");
1350
1354
  if (run.positionPx !== void 0) parts.push(`pp:${run.positionPx}`);
1351
- if (run.horizontalScale !== void 0 && run.horizontalScale !== 100) parts.push(`hs:${run.horizontalScale}`);
1355
+ const horizontalScale = normalizeHorizontalScalePercent(run.horizontalScale);
1356
+ if (horizontalScale !== void 0 && horizontalScale !== 100) parts.push(`hs:${horizontalScale}`);
1352
1357
  if (run.imprint) parts.push("imp");
1353
1358
  if (run.emboss) parts.push("emb");
1354
1359
  if (run.textShadow) parts.push("sh");
@@ -1,16 +1,17 @@
1
1
  import { ommlToMathml } from "../docx/mathToMathml.js";
2
2
  import { parseXmlDocument } from "../docx/xmlParser.js";
3
3
  import { evaluateFieldInstruction } from "../fields/evaluateField.js";
4
- import { hasComplexScriptFormatting, resolveComplexScriptFormatting } from "../layout-engine/measure/complexScriptFormatting.js";
4
+ import { applyComplexScriptFormatting, hasComplexScriptFormatting, resolveComplexScriptFormatting } from "../layout-engine/measure/complexScriptFormatting.js";
5
5
  import { getHyperlinkInstanceIndex } from "../layout-engine/measure/hyperlinkInstance.js";
6
6
  import { getListMarkerInlineWidth, getListMarkerVisualOffset, resolveListMarkerFont } from "../layout-engine/measure/listMarkerWidth.js";
7
7
  import { DOCX_SCRIPT_FONT_SCALE } from "../layout-engine/measure/measureHelpers.js";
8
+ import { calculateTabWidth } from "../layout-engine/measure/tabCalculator.js";
8
9
  import { FONT_KERNING_MODE, countCompressibleSpaces, getFontKerningMode, getRunFontKerningMode, toPaintedText } from "../layout-engine/measure/textMeasurementPolicy.js";
9
10
  import { isFloatingImageRun } from "../layout-engine/types.js";
10
- import { calculateTabWidth } from "../prosemirror/utils/tabCalculator.js";
11
11
  import { AUTHOR_COLORS, getAuthorColorIdx } from "../utils/authorColors.js";
12
12
  import { resolveFontFamily } from "../utils/fontResolver.js";
13
13
  import "../utils/fontWeights.js";
14
+ import { getHorizontalScaleFactor } from "../utils/horizontalScale.js";
14
15
  import { resolvePhysicalParagraphInlineLayout } from "../utils/paragraphInlineLayout.js";
15
16
  import { inlineImageBoundingBox, parseRotationDegrees, rotatedBoundingBox } from "../utils/rotationBoundingBox.js";
16
17
  import { applySanitizedImageSrc } from "../utils/sanitizeImageSrc.js";
@@ -153,11 +154,18 @@ function getRaisedRunFontSize(run) {
153
154
  if (run.fontSize) return `${fontSizePtToPx(run.fontSize) * DOCX_SCRIPT_FONT_SCALE}px`;
154
155
  return `${DOCX_SCRIPT_FONT_SCALE}em`;
155
156
  }
157
+ function applyHorizontalScaleTransform(element, horizontalScale) {
158
+ const horizontalScaleFactor = getHorizontalScaleFactor(horizontalScale);
159
+ if (horizontalScaleFactor === 1) return;
160
+ element.style.display = "inline-block";
161
+ element.style.transform = `scaleX(${horizontalScaleFactor})`;
162
+ element.style.transformOrigin = "left center";
163
+ }
156
164
  /**
157
165
  * Apply text run styles to an element
158
166
  */
159
167
  function applyRunStyles(element, run) {
160
- if (run.fontFamily) element.style.fontFamily = resolveFontFamily(run.fontFamily).cssFallback;
168
+ if (run.fontFamily) element.style.fontFamily = resolveFontFamily(run.fontFamily, run.alternateFontFamily).cssFallback;
161
169
  if (run.fontSize) element.style.fontSize = `${fontSizePtToPx(run.fontSize)}px`;
162
170
  if (run.bold) element.style.fontWeight = "700";
163
171
  if (run.italic) element.style.fontStyle = "italic";
@@ -172,11 +180,7 @@ function applyRunStyles(element, run) {
172
180
  if (run.allCaps) element.style.textTransform = "uppercase";
173
181
  if (run.smallCaps) element.style.fontVariant = "small-caps";
174
182
  if (run.positionPx) element.style.verticalAlign = `${run.positionPx}px`;
175
- if (run.horizontalScale && run.horizontalScale !== 100) {
176
- element.style.display = "inline-block";
177
- element.style.transform = `scaleX(${run.horizontalScale / 100})`;
178
- element.style.transformOrigin = "left center";
179
- }
183
+ applyHorizontalScaleTransform(element, run.horizontalScale);
180
184
  element.style.fontKerning = getRunFontKerningMode(run, 11);
181
185
  if (run.emboss) element.style.textShadow = "1px 1px 1px rgba(255,255,255,0.5), -1px -1px 1px rgba(0,0,0,0.3)";
182
186
  if (run.imprint) element.style.textShadow = "-1px -1px 1px rgba(255,255,255,0.5), 1px 1px 1px rgba(0,0,0,0.3)";
@@ -293,9 +297,11 @@ function applyRunStyles(element, run) {
293
297
  element.style.fontSize = getRaisedRunFontSize(run);
294
298
  }
295
299
  }
296
- function reserveScaledAdvance(element, unscaledWidth, horizontalScale) {
297
- if (horizontalScale === void 0 || horizontalScale === 100) return;
298
- element.style.width = `${unscaledWidth * (horizontalScale / 100)}px`;
300
+ function reserveScaledAdvance({ element, unscaledWidth, horizontalScale, visualSpaceContractionPx }) {
301
+ const horizontalScaleFactor = getHorizontalScaleFactor(horizontalScale);
302
+ const visualAdvance = Math.max(0, unscaledWidth * horizontalScaleFactor - (visualSpaceContractionPx ?? 0));
303
+ if (horizontalScaleFactor !== 1) element.style.width = `${visualAdvance}px`;
304
+ return visualAdvance;
299
305
  }
300
306
  /**
301
307
  * Apply PM position data attributes
@@ -304,10 +310,28 @@ function applyPmPositions(element, pmStart, pmEnd) {
304
310
  if (pmStart !== void 0) element.dataset["pmStart"] = String(pmStart);
305
311
  if (pmEnd !== void 0) element.dataset["pmEnd"] = String(pmEnd);
306
312
  }
307
- /**
308
- * Render a text run
309
- */
310
- function renderTextRun(run, doc, hyperlinkDirection) {
313
+ const NON_COMPRESSIBLE_WHITESPACE_PATTERN = /([^\S ]+)/gu;
314
+ const HAS_NON_COMPRESSIBLE_WHITESPACE_PATTERN = /[^\S ]/u;
315
+ function appendPaintedText({ host, text, doc, contractedWordSpacing }) {
316
+ if (!contractedWordSpacing) {
317
+ host.textContent = text;
318
+ return;
319
+ }
320
+ if (!HAS_NON_COMPRESSIBLE_WHITESPACE_PATTERN.test(text)) {
321
+ host.textContent = text;
322
+ host.style.wordSpacing = countCompressibleSpaces(text) > 0 ? contractedWordSpacing : "0";
323
+ return;
324
+ }
325
+ host.style.wordSpacing = "0";
326
+ for (const segment of text.split(NON_COMPRESSIBLE_WHITESPACE_PATTERN)) {
327
+ if (segment.length === 0) continue;
328
+ const segmentEl = doc.createElement("span");
329
+ segmentEl.textContent = segment;
330
+ segmentEl.style.wordSpacing = countCompressibleSpaces(segment) > 0 ? contractedWordSpacing : "0";
331
+ host.append(segmentEl);
332
+ }
333
+ }
334
+ function renderTextRun(run, doc, options) {
311
335
  const span = doc.createElement("span");
312
336
  span.className = `${PARAGRAPH_CLASS_NAMES.run} ${PARAGRAPH_CLASS_NAMES.text}`;
313
337
  if (run.footnoteRefId !== void 0) {
@@ -324,18 +348,26 @@ function renderTextRun(run, doc, hyperlinkDirection) {
324
348
  applyRunStyles(span, run);
325
349
  applyPmPositions(span, run.pmStart, run.pmEnd);
326
350
  const paintedText = toPaintedText(run.text);
351
+ const visualSpaceContractionPx = options?.visualSpaceContractionPx;
352
+ const effectiveHorizontalScaleFactor = getHorizontalScaleFactor(run.horizontalScale) * (options?.ancestorHorizontalScaleFactor ?? 1);
353
+ const contractedWordSpacing = visualSpaceContractionPx === void 0 || effectiveHorizontalScaleFactor === 0 ? void 0 : `${-visualSpaceContractionPx / effectiveHorizontalScaleFactor}px`;
327
354
  const isBookmarkTarget = run.hyperlink?.href.startsWith("#") === true;
328
355
  const hyperlinkHref = resolveHyperlinkHref(run.hyperlink?.href, isBookmarkTarget);
329
356
  if (run.hyperlink && hyperlinkHref !== void 0) {
330
357
  const anchor = doc.createElement("a");
331
358
  anchor.href = hyperlinkHref;
332
- if (hyperlinkDirection || DISPLAYED_URL_PATTERN.test(paintedText.trim())) anchor.dir = LEFT_TO_RIGHT_DIRECTION;
359
+ if (options?.hyperlinkDirection || DISPLAYED_URL_PATTERN.test(paintedText.trim())) anchor.dir = LEFT_TO_RIGHT_DIRECTION;
333
360
  if (!isBookmarkTarget) {
334
361
  anchor.target = "_blank";
335
362
  anchor.rel = "noopener noreferrer";
336
363
  }
337
364
  if (run.hyperlink.tooltip) anchor.title = run.hyperlink.tooltip;
338
- anchor.textContent = paintedText;
365
+ appendPaintedText({
366
+ host: anchor,
367
+ text: paintedText,
368
+ doc,
369
+ ...contractedWordSpacing !== void 0 ? { contractedWordSpacing } : {}
370
+ });
339
371
  if (!run.hyperlink.noDefaultStyle) {
340
372
  const hyperlinkColor = getHyperlinkTextColor(run, span.style.color);
341
373
  anchor.style.color = hyperlinkColor;
@@ -349,7 +381,12 @@ function renderTextRun(run, doc, hyperlinkDirection) {
349
381
  }
350
382
  }
351
383
  span.append(anchor);
352
- } else span.textContent = paintedText;
384
+ } else appendPaintedText({
385
+ host: span,
386
+ text: paintedText,
387
+ doc,
388
+ ...contractedWordSpacing !== void 0 ? { contractedWordSpacing } : {}
389
+ });
353
390
  applyWhitespaceUnderline(span, run);
354
391
  return span;
355
392
  }
@@ -648,40 +685,36 @@ function resolveFieldText(run, context) {
648
685
  ...run.fldLock ? { locked: true } : {}
649
686
  });
650
687
  }
651
- function renderFieldRun(run, doc, context) {
688
+ function renderFieldRun(run, doc, { context, visualSpaceContractionPx }) {
652
689
  const text = resolveFieldText(run, context);
653
690
  const resolvedRun = {
654
691
  ...run,
655
692
  kind: "text",
656
693
  text
657
694
  };
658
- if (resolvedRun.forceComplexScript && hasComplexScriptFormatting(resolvedRun)) return renderTextRun({
659
- ...resolvedRun,
660
- ...resolveComplexScriptFormatting(resolvedRun)
661
- }, doc);
695
+ if (resolvedRun.forceComplexScript && hasComplexScriptFormatting(resolvedRun)) return renderTextRun(applyComplexScriptFormatting(resolvedRun, resolvedRun), doc, { ...visualSpaceContractionPx !== void 0 ? { visualSpaceContractionPx } : {} });
662
696
  if (needsPerScriptSpans({
663
697
  ...resolvedRun,
664
698
  text
665
699
  })) {
666
700
  const wrapper = doc.createElement("span");
667
701
  applyPmPositions(wrapper, resolvedRun.pmStart, resolvedRun.pmEnd);
668
- if (resolvedRun.horizontalScale && resolvedRun.horizontalScale !== 100) {
669
- wrapper.style.display = "inline-block";
670
- wrapper.style.transform = `scaleX(${resolvedRun.horizontalScale / 100})`;
671
- wrapper.style.transformOrigin = "left center";
672
- }
702
+ const horizontalScaleFactor = getHorizontalScaleFactor(resolvedRun.horizontalScale);
703
+ applyHorizontalScaleTransform(wrapper, resolvedRun.horizontalScale);
673
704
  const { pmStart: _pmStart, pmEnd: _pmEnd, horizontalScale: _horizontalScale, ...segmentBase } = resolvedRun;
674
705
  for (const segment of segmentByScript(text)) {
675
- const segmentRun = {
706
+ const segmentRun = formatTextRunForScript({
676
707
  ...segmentBase,
677
- text: segment.text,
678
- ...scriptFormattingOverride(resolvedRun, segment.script)
679
- };
680
- wrapper.append(renderTextRun(segmentRun, doc));
708
+ text: segment.text
709
+ }, segment.script);
710
+ wrapper.append(renderTextRun(segmentRun, doc, {
711
+ ...visualSpaceContractionPx !== void 0 ? { visualSpaceContractionPx } : {},
712
+ ancestorHorizontalScaleFactor: horizontalScaleFactor
713
+ }));
681
714
  }
682
715
  return wrapper;
683
716
  }
684
- return renderTextRun(resolvedRun, doc);
717
+ return renderTextRun(resolvedRun, doc, { ...visualSpaceContractionPx !== void 0 ? { visualSpaceContractionPx } : {} });
685
718
  }
686
719
  /**
687
720
  * Render an OMML math run by converting it to MathML and injecting a
@@ -710,6 +743,7 @@ function renderMathRun(run, doc) {
710
743
  host.style.verticalAlign = "middle";
711
744
  }
712
745
  host.style.fontFamily = "\"Cambria Math\", \"Latin Modern Math\", \"STIX Two Math\", serif";
746
+ applyHorizontalScaleTransform(host, run.horizontalScale);
713
747
  try {
714
748
  host.innerHTML = mathml;
715
749
  } catch {
@@ -731,6 +765,7 @@ function renderMathFallback(run, doc, fallbackText, errorFlag) {
731
765
  }
732
766
  span.style.fontStyle = "italic";
733
767
  span.style.fontFamily = "\"Cambria Math\", \"Latin Modern Math\", \"STIX Two Math\", serif";
768
+ applyHorizontalScaleTransform(span, run.horizontalScale);
734
769
  span.textContent = fallbackText;
735
770
  applyPmPositions(span, run.pmStart, run.pmEnd);
736
771
  return span;
@@ -743,7 +778,7 @@ function renderRun(run, doc, context) {
743
778
  if (isTabRun(run)) return renderTabRun(run, doc, 48);
744
779
  if (isImageRun(run)) return renderImageRun(run, doc);
745
780
  if (isLineBreakRun(run)) return renderLineBreakRun(run, doc);
746
- if (isFieldRun(run) && context) return renderFieldRun(run, doc, context);
781
+ if (isFieldRun(run) && context) return renderFieldRun(run, doc, { context });
747
782
  if (isMathRun(run)) return renderMathRun(run, doc);
748
783
  if (run.kind === "renderedPageBreak") {
749
784
  const span = doc.createElement("span");
@@ -793,10 +828,18 @@ function sliceRunsForLine(block, line) {
793
828
  * The font a script segment paints with, mirroring the measurer's
794
829
  * `scriptFontFamily`. Returning undefined leaves the run's own `fontFamily`.
795
830
  */
796
- const scriptFormattingOverride = (run, script) => {
797
- if (run.forceComplexScript || script === SCRIPT_CLASS.complex) return resolveComplexScriptFormatting(run);
798
- if (script === SCRIPT_CLASS.eastAsia && run.eastAsiaFontFamily !== void 0) return { fontFamily: run.eastAsiaFontFamily };
799
- return {};
831
+ const formatTextRunForScript = (run, script) => {
832
+ if (run.forceComplexScript || script === SCRIPT_CLASS.complex) return applyComplexScriptFormatting(run, run);
833
+ if (script === SCRIPT_CLASS.eastAsia && run.eastAsiaFontFamily !== void 0) {
834
+ const result = {
835
+ ...run,
836
+ fontFamily: run.eastAsiaFontFamily
837
+ };
838
+ delete result.alternateFontFamily;
839
+ if (run.eastAsiaAlternateFontFamily !== void 0) result.alternateFontFamily = run.eastAsiaAlternateFontFamily;
840
+ return result;
841
+ }
842
+ return run;
800
843
  };
801
844
  /**
802
845
  * Whether a run needs per-script sibling spans: it carries a script-specific
@@ -822,10 +865,7 @@ function splitTextRunsByEastAsia(runs) {
822
865
  const result = [];
823
866
  for (const run of runs) {
824
867
  if (isTextRun(run) && run.forceComplexScript && hasComplexScriptFormatting(run)) {
825
- result.push({
826
- ...run,
827
- ...resolveComplexScriptFormatting(run)
828
- });
868
+ result.push(applyComplexScriptFormatting(run, run));
829
869
  continue;
830
870
  }
831
871
  if (!isTextRun(run) || !needsPerScriptSpans(run)) {
@@ -834,15 +874,14 @@ function splitTextRunsByEastAsia(runs) {
834
874
  }
835
875
  let offset = 0;
836
876
  for (const segment of segmentByScript(run.text)) {
837
- result.push({
877
+ result.push(formatTextRunForScript({
838
878
  ...run,
839
879
  text: segment.text,
840
- ...scriptFormattingOverride(run, segment.script),
841
880
  ...run.pmStart !== void 0 ? {
842
881
  pmStart: run.pmStart + offset,
843
882
  pmEnd: run.pmStart + offset + segment.text.length
844
883
  } : {}
845
- });
884
+ }, segment.script));
846
885
  offset += segment.text.length;
847
886
  }
848
887
  }
@@ -926,10 +965,19 @@ const startsAfterSoftWrap = (block, line) => {
926
965
  const RIGHT_EDGE_EPSILON_PX = .5;
927
966
  function countShrinkableSpaces(runs, context) {
928
967
  let count = 0;
929
- for (const run of runs) if (isTextRun(run)) count += countCompressibleSpaces(run.text ?? "");
930
- else if (isFieldRun(run)) count += countCompressibleSpaces(resolveFieldText(run, context));
968
+ for (const run of runs) if (isTextRun(run)) {
969
+ if (getHorizontalScaleFactor(run.horizontalScale) === 0) continue;
970
+ count += countCompressibleSpaces(run.text ?? "");
971
+ } else if (isFieldRun(run)) {
972
+ if (getHorizontalScaleFactor(run.horizontalScale) === 0) continue;
973
+ count += countCompressibleSpaces(resolveFieldText(run, context));
974
+ }
931
975
  return count;
932
976
  }
977
+ function runVisualSpaceContraction({ text, horizontalScale, visualSpaceContractionPx }) {
978
+ if (visualSpaceContractionPx === void 0 || getHorizontalScaleFactor(horizontalScale) === 0) return 0;
979
+ return countCompressibleSpaces(text) * visualSpaceContractionPx;
980
+ }
933
981
  /**
934
982
  * Build the shared measurement style from a paintable text run.
935
983
  */
@@ -939,8 +987,11 @@ function runMeasureStyle(run) {
939
987
  ...run.italic !== void 0 ? { italic: run.italic } : {},
940
988
  ...run.letterSpacing !== void 0 ? { letterSpacing: run.letterSpacing } : {},
941
989
  ...run.smallCaps !== void 0 ? { smallCaps: run.smallCaps } : {},
990
+ ...run.alternateFontFamily !== void 0 ? { alternateFontFamily: run.alternateFontFamily } : {},
942
991
  ...run.eastAsiaFontFamily !== void 0 ? { eastAsiaFontFamily: run.eastAsiaFontFamily } : {},
992
+ ...run.eastAsiaAlternateFontFamily !== void 0 ? { eastAsiaAlternateFontFamily: run.eastAsiaAlternateFontFamily } : {},
943
993
  ...run.complexScriptFontFamily !== void 0 ? { complexScriptFontFamily: run.complexScriptFontFamily } : {},
994
+ ...run.complexScriptAlternateFontFamily !== void 0 ? { complexScriptAlternateFontFamily: run.complexScriptAlternateFontFamily } : {},
944
995
  ...run.complexScriptFontSize !== void 0 ? { complexScriptFontSize: run.complexScriptFontSize } : {},
945
996
  ...run.complexScriptBold !== void 0 ? { complexScriptBold: run.complexScriptBold } : {},
946
997
  ...run.complexScriptItalic !== void 0 ? { complexScriptItalic: run.complexScriptItalic } : {},
@@ -960,15 +1011,14 @@ function measureFollowingContentWidth(runs, tabRunIndex, measureText, context) {
960
1011
  for (let i = tabRunIndex + 1; i < runs.length; i++) {
961
1012
  const run = runs[i];
962
1013
  if (isTabRun(run) || isLineBreakRun(run)) break;
963
- const scale = run.horizontalScale ?? 100;
964
1014
  if (isTextRun(run)) {
965
1015
  const text = run.allCaps ? run.text.toLocaleUpperCase() : run.text;
966
- width += measureText(text, run.fontSize ?? 11, run.fontFamily ?? "Calibri", runMeasureStyle(run)) * (scale / 100);
1016
+ width += measureText(text, run.fontSize ?? 11, run.fontFamily ?? "Calibri", runMeasureStyle(run)) * getHorizontalScaleFactor(run.horizontalScale);
967
1017
  } else if (isFieldRun(run)) {
968
1018
  const fieldText = resolveFieldText(run, context);
969
- width += measureText(run.allCaps ? fieldText.toLocaleUpperCase() : fieldText, run.fontSize ?? 11, run.fontFamily ?? "Calibri", runMeasureStyle(run)) * (scale / 100);
1019
+ width += measureText(run.allCaps ? fieldText.toLocaleUpperCase() : fieldText, run.fontSize ?? 11, run.fontFamily ?? "Calibri", runMeasureStyle(run)) * getHorizontalScaleFactor(run.horizontalScale);
970
1020
  } else if (isImageRun(run) && !isFloatingImageRun(run)) width += inlineImageBoundingBox(run).width || 0;
971
- else if (isMathRun(run)) width += measureText(run.plainText, run.fontSize ?? 11, run.fontFamily ?? "Cambria Math", runMeasureStyle(run)) * (scale / 100);
1021
+ else if (isMathRun(run)) width += measureText(run.plainText, run.fontSize ?? 11, run.fontFamily ?? "Cambria Math", runMeasureStyle(run)) * getHorizontalScaleFactor(run.horizontalScale);
972
1022
  }
973
1023
  return width;
974
1024
  }
@@ -995,8 +1045,8 @@ function measureDecimalPrefixWidth(runs, tabRunIndex, decimalIndex, measureText,
995
1045
  else runText = run.allCaps ? run.text.toLocaleUpperCase() : run.text;
996
1046
  const take = Math.min(runText.length, decimalIndex - consumed);
997
1047
  if (take > 0) {
998
- const scale = run.horizontalScale ?? 100;
999
- width += measureText(runText.slice(0, take), run.fontSize ?? 11, run.fontFamily ?? "Calibri", runMeasureStyle(run)) * (scale / 100);
1048
+ const horizontalScaleFactor = getHorizontalScaleFactor(run.horizontalScale);
1049
+ width += measureText(runText.slice(0, take), run.fontSize ?? 11, run.fontFamily ?? "Calibri", runMeasureStyle(run)) * horizontalScaleFactor;
1000
1050
  }
1001
1051
  consumed += runText.length;
1002
1052
  }
@@ -1051,17 +1101,17 @@ function createTextMeasurer(doc) {
1051
1101
  return (text, fontSize = 11, fontFamily = "Calibri", style = {}) => {
1052
1102
  if (!ctx) return applyLetterSpacingToMeasuredWidth(text.length * 7, text, style.letterSpacing);
1053
1103
  ctx.fontKerning = getFontKerningMode(style);
1054
- const fontString = (family, size, bold, italic) => {
1104
+ const fontString = (family, size, bold, italic, alternateFamily) => {
1055
1105
  const fontPrefixParts = [];
1056
1106
  if (italic) fontPrefixParts.push("italic");
1057
1107
  if (style.smallCaps) fontPrefixParts.push("small-caps");
1058
1108
  if (bold) fontPrefixParts.push("700");
1059
1109
  fontPrefixParts.push(`${size * 96 / 72}px`);
1060
- return `${fontPrefixParts.join(" ")} ${resolveFontFamily(family).cssFallback}`;
1110
+ return `${fontPrefixParts.join(" ")} ${resolveFontFamily(family, alternateFamily).cssFallback}`;
1061
1111
  };
1062
- const baseFont = fontString(fontFamily, fontSize, style.bold, style.italic);
1112
+ const baseFont = fontString(fontFamily, fontSize, style.bold, style.italic, style.alternateFontFamily);
1063
1113
  const complex = resolveComplexScriptFormatting(style);
1064
- const complexFont = fontString(complex.fontFamily ?? fontFamily, complex.fontSize ?? fontSize, complex.bold ?? style.bold, complex.italic ?? style.italic);
1114
+ const complexFont = fontString(complex.fontFamily ?? fontFamily, complex.fontSize ?? fontSize, complex.bold ?? style.bold, complex.italic ?? style.italic, complex.alternateFontFamily ?? style.alternateFontFamily);
1065
1115
  if (style.forceComplexScript) {
1066
1116
  ctx.font = complexFont;
1067
1117
  return applyLetterSpacingToMeasuredWidth(ctx.measureText(text).width, text, style.letterSpacing);
@@ -1069,7 +1119,7 @@ function createTextMeasurer(doc) {
1069
1119
  if (!style.letterSpacing && (style.eastAsiaFontFamily && hasCjk(text) || hasComplexScriptFormatting(style) && hasComplexScript(text))) {
1070
1120
  let segmentedWidth = 0;
1071
1121
  for (const segment of segmentByScript(text)) {
1072
- if (segment.script === SCRIPT_CLASS.eastAsia && style.eastAsiaFontFamily) ctx.font = fontString(style.eastAsiaFontFamily, fontSize, style.bold, style.italic);
1122
+ if (segment.script === SCRIPT_CLASS.eastAsia && style.eastAsiaFontFamily) ctx.font = fontString(style.eastAsiaFontFamily, fontSize, style.bold, style.italic, style.eastAsiaAlternateFontFamily);
1073
1123
  else if (segment.script === SCRIPT_CLASS.complex) ctx.font = complexFont;
1074
1124
  else ctx.font = baseFont;
1075
1125
  segmentedWidth += ctx.measureText(segment.text).width;
@@ -1114,12 +1164,17 @@ function renderLine(block, line, alignment, doc, options) {
1114
1164
  if (lineEl.style.display === "flex") return [runEl];
1115
1165
  return withCursiveJoiners(runEl, run, cursiveJoinerPlan, applyJoinerRunStyles, doc);
1116
1166
  };
1167
+ let visualSpaceContractionPx;
1117
1168
  const renderLineTextRun = (run) => {
1118
- const runEl = renderTextRun(run, doc, run.hyperlink && options?.leftToRightDisplayedUrlHyperlinks?.has(run.hyperlink) ? LEFT_TO_RIGHT_DIRECTION : void 0);
1169
+ const hyperlinkDirection = run.hyperlink && options?.leftToRightDisplayedUrlHyperlinks?.has(run.hyperlink) ? LEFT_TO_RIGHT_DIRECTION : void 0;
1170
+ const runEl = renderTextRun(run, doc, {
1171
+ ...hyperlinkDirection !== void 0 ? { hyperlinkDirection } : {},
1172
+ ...visualSpaceContractionPx !== void 0 ? { visualSpaceContractionPx } : {}
1173
+ });
1119
1174
  if (collapsedLeadingSpaceRuns.has(run)) runEl.dataset["collapsedLeadingSpaces"] = "true";
1120
1175
  if (collapsedTrailingSpaceRuns.has(run)) runEl.dataset["collapsedTrailingSpaces"] = "true";
1121
1176
  if (isCollapsedLineEdgeSpaceRun(run)) {
1122
- const spaceAdvance = (collapsedSpaceMeasureText?.(" ", run.fontSize || 11, run.fontFamily || "Calibri", runMeasureStyle(run)) ?? 0) * ((run.horizontalScale ?? 100) / 100);
1177
+ const spaceAdvance = (collapsedSpaceMeasureText?.(" ", run.fontSize || 11, run.fontFamily || "Calibri", runMeasureStyle(run)) ?? 0) * getHorizontalScaleFactor(run.horizontalScale);
1123
1178
  runEl.dataset["collapsedSpaceAdvance"] = String(spaceAdvance);
1124
1179
  runEl.style.fontSize = "0";
1125
1180
  runEl.style.letterSpacing = "0";
@@ -1155,19 +1210,24 @@ function renderLine(block, line, alignment, doc, options) {
1155
1210
  }
1156
1211
  const hasTabRuns = runsForLine.some(isTabRun);
1157
1212
  if (alignment === "justify" && options) {
1158
- if (!options.isLastLine || options.paragraphEndsWithLineBreak) {
1159
- const firstLineIndentPx = options.isFirstLine ? options.firstLineIndentPx ?? 0 : 0;
1160
- const firstLinePositiveIndentPx = Math.max(0, firstLineIndentPx);
1161
- const firstLineHangingPx = Math.max(0, -firstLineIndentPx);
1162
- const hasVisibleListMarker = options.isFirstLine && block.attrs?.listMarker && !block.attrs.listMarkerHidden;
1163
- const firstLineHangingExpansionPx = hasVisibleListMarker ? Math.min(firstLineHangingPx, Math.max(0, options.leftIndentPx ?? 0)) : firstLineHangingPx;
1164
- const justifyCapacityPx = options.availableWidth - firstLinePositiveIndentPx + firstLineHangingExpansionPx;
1165
- const overfullPx = line.width - justifyCapacityPx;
1166
- const shrinkableSpaces = countShrinkableSpaces(runsForLine.filter((run) => !isTextRun(run) || !isCollapsedLineEdgeSpaceRun(run)), options.context);
1167
- if (overfullPx > RIGHT_EDGE_EPSILON_PX && shrinkableSpaces > 0) {
1213
+ const firstLineIndentPx = options.isFirstLine ? options.firstLineIndentPx ?? 0 : 0;
1214
+ const firstLinePositiveIndentPx = Math.max(0, firstLineIndentPx);
1215
+ const firstLineHangingPx = Math.max(0, -firstLineIndentPx);
1216
+ const hasVisibleListMarker = options.isFirstLine && block.attrs?.listMarker && !block.attrs.listMarkerHidden;
1217
+ const firstLineHangingExpansionPx = hasVisibleListMarker ? Math.min(firstLineHangingPx, Math.max(0, options.leftIndentPx ?? 0)) : firstLineHangingPx;
1218
+ const justifyCapacityPx = options.availableWidth - firstLinePositiveIndentPx + firstLineHangingExpansionPx;
1219
+ const overfullPx = line.width - justifyCapacityPx;
1220
+ const shrinkableSpaces = countShrinkableSpaces(runsForLine.filter((run) => !isTextRun(run) || !isCollapsedLineEdgeSpaceRun(run)), options.context);
1221
+ const shouldJustify = !options.isLastLine || options.paragraphEndsWithLineBreak;
1222
+ const finalContractionPx = line.justificationPaint?.type === "space-contraction" ? line.justificationPaint.contractionPx : void 0;
1223
+ const shouldCompressFinalLine = options.isLastLine && finalContractionPx !== void 0 && finalContractionPx > RIGHT_EDGE_EPSILON_PX && overfullPx > RIGHT_EDGE_EPSILON_PX && shrinkableSpaces > 0;
1224
+ if (shouldJustify || shouldCompressFinalLine) {
1225
+ const contractionPx = shouldCompressFinalLine ? finalContractionPx : overfullPx;
1226
+ if (contractionPx > RIGHT_EDGE_EPSILON_PX && shrinkableSpaces > 0) {
1168
1227
  lineEl.style.textAlign = "left";
1169
1228
  lineEl.style.textAlignLast = "auto";
1170
- lineEl.style.wordSpacing = `${-overfullPx / shrinkableSpaces}px`;
1229
+ visualSpaceContractionPx = contractionPx / shrinkableSpaces;
1230
+ lineEl.style.wordSpacing = "0";
1171
1231
  } else if (hasTabRuns && shrinkableSpaces > 0 && -overfullPx > RIGHT_EDGE_EPSILON_PX) {
1172
1232
  lineEl.style.textAlign = "left";
1173
1233
  lineEl.style.textAlignLast = "auto";
@@ -1183,7 +1243,7 @@ function renderLine(block, line, alignment, doc, options) {
1183
1243
  lineEl.style.whiteSpace = "pre";
1184
1244
  lineEl.style.overflow = "visible";
1185
1245
  let tabContext;
1186
- const hasScaledTextRun = runsForLine.some((run) => (isTextRun(run) || isFieldRun(run) || isMathRun(run)) && run.horizontalScale !== void 0 && run.horizontalScale !== 100);
1246
+ const hasScaledTextRun = runsForLine.some((run) => (isTextRun(run) || isFieldRun(run) || isMathRun(run)) && getHorizontalScaleFactor(run.horizontalScale) !== 1);
1187
1247
  const measureText = collapsedSpaceMeasureText ?? (hasTabRuns || hasScaledTextRun ? createTextMeasurer(doc) : void 0);
1188
1248
  if (hasTabRuns) {
1189
1249
  const explicitStops = options?.tabStops?.map(convertTabStopToCalc);
@@ -1244,7 +1304,10 @@ function renderLine(block, line, alignment, doc, options) {
1244
1304
  const next = runsForLine[j];
1245
1305
  if (isTabRun(next) || isLineBreakRun(next)) break;
1246
1306
  if (isTextRun(next)) lineEl.append(...withJoiners(renderLineTextRun(next), next));
1247
- else if (isFieldRun(next) && options?.context) lineEl.append(renderFieldRun(next, doc, options.context));
1307
+ else if (isFieldRun(next) && options?.context) lineEl.append(renderFieldRun(next, doc, {
1308
+ context: options.context,
1309
+ ...visualSpaceContractionPx !== void 0 ? { visualSpaceContractionPx } : {}
1310
+ }));
1248
1311
  else if (isImageRun(next)) {
1249
1312
  if (isFloatingImageRun(next)) continue;
1250
1313
  const imageKey = getInlineImageRunKey(next);
@@ -1272,8 +1335,17 @@ function renderLine(block, line, alignment, doc, options) {
1272
1335
  const fontSize = run.fontSize || 11;
1273
1336
  const fontFamily = run.fontFamily || "Calibri";
1274
1337
  const measuredWidth = measureText(run.allCaps ? run.text.toLocaleUpperCase() : run.text, fontSize, fontFamily, runMeasureStyle(run));
1275
- reserveScaledAdvance(runEl, measuredWidth, run.horizontalScale);
1276
- currentX += measuredWidth * ((run.horizontalScale ?? 100) / 100);
1338
+ const runContractionPx = runVisualSpaceContraction({
1339
+ text: run.text,
1340
+ ...run.horizontalScale !== void 0 ? { horizontalScale: run.horizontalScale } : {},
1341
+ ...visualSpaceContractionPx !== void 0 ? { visualSpaceContractionPx } : {}
1342
+ });
1343
+ currentX += reserveScaledAdvance({
1344
+ element: runEl,
1345
+ unscaledWidth: measuredWidth,
1346
+ ...run.horizontalScale !== void 0 ? { horizontalScale: run.horizontalScale } : {},
1347
+ ...runContractionPx > 0 ? { visualSpaceContractionPx: runContractionPx } : {}
1348
+ });
1277
1349
  } else if (isImageRun(run)) {
1278
1350
  if (isFloatingImageRun(run)) continue;
1279
1351
  const imageKey = getInlineImageRunKey(run);
@@ -1286,22 +1358,37 @@ function renderLine(block, line, alignment, doc, options) {
1286
1358
  const runEl = renderLineBreakRun(run, doc);
1287
1359
  lineEl.append(runEl);
1288
1360
  } else if (isFieldRun(run) && options?.context) {
1289
- const runEl = renderFieldRun(run, doc, options.context);
1361
+ const runEl = renderFieldRun(run, doc, {
1362
+ context: options.context,
1363
+ ...visualSpaceContractionPx !== void 0 ? { visualSpaceContractionPx } : {}
1364
+ });
1290
1365
  lineEl.append(runEl);
1291
1366
  if (!measureText) continue;
1292
1367
  const fieldText = resolveFieldText(run, options.context);
1293
1368
  const fontSize = run.fontSize || 11;
1294
1369
  const fontFamily = run.fontFamily || "Calibri";
1295
1370
  const measuredWidth = measureText(run.allCaps ? fieldText.toLocaleUpperCase() : fieldText, fontSize, fontFamily, runMeasureStyle(run));
1296
- reserveScaledAdvance(runEl, measuredWidth, run.horizontalScale);
1297
- currentX += measuredWidth * ((run.horizontalScale ?? 100) / 100);
1371
+ const runContractionPx = runVisualSpaceContraction({
1372
+ text: fieldText,
1373
+ ...run.horizontalScale !== void 0 ? { horizontalScale: run.horizontalScale } : {},
1374
+ ...visualSpaceContractionPx !== void 0 ? { visualSpaceContractionPx } : {}
1375
+ });
1376
+ currentX += reserveScaledAdvance({
1377
+ element: runEl,
1378
+ unscaledWidth: measuredWidth,
1379
+ ...run.horizontalScale !== void 0 ? { horizontalScale: run.horizontalScale } : {},
1380
+ ...runContractionPx > 0 ? { visualSpaceContractionPx: runContractionPx } : {}
1381
+ });
1298
1382
  } else if (isMathRun(run)) {
1299
1383
  const runEl = renderMathRun(run, doc);
1300
1384
  lineEl.append(runEl);
1301
1385
  if (!measureText) continue;
1302
1386
  const measuredWidth = measureText(run.plainText, run.fontSize ?? 11, run.fontFamily ?? "Cambria Math", runMeasureStyle(run));
1303
- reserveScaledAdvance(runEl, measuredWidth, run.horizontalScale);
1304
- currentX += measuredWidth * ((run.horizontalScale ?? 100) / 100);
1387
+ currentX += reserveScaledAdvance({
1388
+ element: runEl,
1389
+ unscaledWidth: measuredWidth,
1390
+ ...run.horizontalScale !== void 0 ? { horizontalScale: run.horizontalScale } : {}
1391
+ });
1305
1392
  } else {
1306
1393
  const runEl = renderRun(run, doc, options?.context);
1307
1394
  lineEl.append(runEl);
@@ -1534,7 +1621,7 @@ function renderListMarker({ marker, inlineWidth, visualOffset, doc, formatting,
1534
1621
  const span = doc.createElement("span");
1535
1622
  span.className = "layout-list-marker";
1536
1623
  span.style.display = "inline-block";
1537
- span.style.fontFamily = resolveFontFamily(formatting.fontFamily).cssFallback;
1624
+ span.style.fontFamily = resolveFontFamily(formatting.fontFamily, formatting.alternateFontFamily).cssFallback;
1538
1625
  if (formatting.fontSize) span.style.fontSize = `${formatting.fontSize * 96 / 72}px`;
1539
1626
  if (formatting.bold !== void 0) span.style.fontWeight = formatting.bold ? "700" : "normal";
1540
1627
  if (formatting.italic !== void 0) span.style.fontStyle = formatting.italic ? "italic" : "normal";
@@ -25,7 +25,7 @@ type RenderTableFragmentOptions = {
25
25
  /**
26
26
  * Render a nested table (within a cell)
27
27
  */
28
- declare function renderNestedTable(block: TableBlock, measure: TableMeasure, context: RenderContext, doc: Document): HTMLElement;
28
+ declare function renderNestedTable(block: TableBlock, measure: TableMeasure, context: RenderContext, doc: Document, frameWidth?: number): HTMLElement;
29
29
  /**
30
30
  * Render a table fragment to DOM
31
31
  *