@stll/folio-core 0.9.0 → 0.11.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 (69) hide show
  1. package/dist/ai-edits/headless.d.ts +52 -12
  2. package/dist/ai-edits/headless.js +181 -55
  3. package/dist/ai-edits/index.d.ts +2 -2
  4. package/dist/ai-edits/index.js +2 -2
  5. package/dist/controller/collaborationModules.d.ts +7 -0
  6. package/dist/controller/collaborationModules.js +15 -0
  7. package/dist/controller/folioEditor.d.ts +1 -1
  8. package/dist/controller/headerFooterEditorManager.d.ts +41 -0
  9. package/dist/controller/headerFooterEditorManager.js +182 -0
  10. package/dist/controller/hiddenEditorManager.d.ts +1 -1
  11. package/dist/controller/layoutPipeline.js +2 -1
  12. package/dist/document-operations.d.ts +11 -2
  13. package/dist/document-operations.js +7 -1
  14. package/dist/document-stories.d.ts +10 -0
  15. package/dist/document-stories.js +27 -0
  16. package/dist/docx/corePropertiesParser.d.ts +8 -0
  17. package/dist/docx/corePropertiesParser.js +53 -0
  18. package/dist/docx/index.d.ts +2 -1
  19. package/dist/docx/index.js +2 -1
  20. package/dist/docx/metadataPrivacy.d.ts +40 -0
  21. package/dist/docx/metadataPrivacy.js +131 -0
  22. package/dist/docx/parser.js +4 -1
  23. package/dist/docx/rezip.d.ts +5 -4
  24. package/dist/docx/rezip.js +6 -8
  25. package/dist/layout-bridge/convert/toFlowBlocks.js +1 -0
  26. package/dist/layout-engine/measure/cache.js +2 -7
  27. package/dist/layout-engine/measure/effectiveLineBreakPolicy.d.ts +29 -0
  28. package/dist/layout-engine/measure/effectiveLineBreakPolicy.js +60 -0
  29. package/dist/layout-engine/measure/measureParagraph.js +32 -36
  30. package/dist/layout-engine/measure/tableCellFloating.js +39 -33
  31. package/dist/layout-engine/types.d.ts +5 -3
  32. package/dist/managers/FindReplaceManager.d.ts +8 -0
  33. package/dist/managers/FindReplaceManager.js +18 -0
  34. package/dist/managers/TableSelectionManager.d.ts +6 -0
  35. package/dist/managers/TableSelectionManager.js +14 -0
  36. package/dist/prosemirror/attrs/index.js +1 -0
  37. package/dist/prosemirror/conversion/effectiveTableCellFormatting.d.ts +62 -0
  38. package/dist/prosemirror/conversion/effectiveTableCellFormatting.js +131 -0
  39. package/dist/prosemirror/conversion/fromProseDoc.js +1 -0
  40. package/dist/prosemirror/conversion/toProseDoc.js +40 -68
  41. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -1
  42. package/dist/prosemirror/extensions/marks/HighlightExtension.js +1 -1
  43. package/dist/prosemirror/extensions/marks/RunShadingExtension.js +1 -1
  44. package/dist/prosemirror/extensions/marks/TextColorExtension.js +1 -1
  45. package/dist/prosemirror/extensions/nodes/ImageExtension.js +1 -0
  46. package/dist/prosemirror/extensions/nodes/TableExtension.js +1 -1
  47. package/dist/prosemirror/findReplaceSelection.d.ts +6 -1
  48. package/dist/prosemirror/findReplaceSelection.js +57 -13
  49. package/dist/prosemirror/schema/nodes.d.ts +2 -1
  50. package/dist/redline.d.ts +26 -11
  51. package/dist/redline.js +95 -62
  52. package/dist/render-dom/BodySelectionOverlay.d.ts +31 -0
  53. package/dist/render-dom/BodySelectionOverlay.js +76 -0
  54. package/dist/render-dom/HeaderFooterSelectionOverlay.d.ts +28 -0
  55. package/dist/render-dom/HeaderFooterSelectionOverlay.js +121 -0
  56. package/dist/render-dom/RemoteSelectionOverlay.d.ts +30 -0
  57. package/dist/render-dom/RemoteSelectionOverlay.js +67 -0
  58. package/dist/render-dom/RenderedDomContext.d.ts +40 -0
  59. package/dist/render-dom/RenderedDomContext.js +126 -0
  60. package/dist/render-dom/resolveSidebarItemPositions.d.ts +33 -0
  61. package/dist/render-dom/resolveSidebarItemPositions.js +45 -0
  62. package/dist/server.d.ts +5 -4
  63. package/dist/server.js +5 -4
  64. package/dist/utils/findReplace.d.ts +32 -0
  65. package/dist/utils/findReplace.js +118 -0
  66. package/dist/utils/headerFooter.js +24 -1
  67. package/dist/version-comparison.d.ts +65 -11
  68. package/dist/version-comparison.js +187 -29
  69. package/package.json +1 -1
@@ -0,0 +1,29 @@
1
+ import { ParagraphAttrs, TextRun } from "../types.js";
2
+ import { LineBreakPolicy } from "./lineBreakProvider.js";
3
+
4
+ //#region src/layout-engine/measure/effectiveLineBreakPolicy.d.ts
5
+ type EffectiveAutomaticHyphenation = Readonly<{
6
+ type: "disabled";
7
+ }> | Readonly<{
8
+ type: "enabled";
9
+ consecutiveLineLimit: number;
10
+ hyphenationZoneTwips: number;
11
+ }>;
12
+ type EffectiveLineBreakPolicy = Readonly<{
13
+ /** Exact provider inputs; omitted OOXML values stay omitted for custom providers. */provider: Readonly<LineBreakPolicy>; /** `w:overflowPunct` defaults to enabled when omitted. */
14
+ hangingPunctuation: boolean; /** Effective document and paragraph automatic-hyphenation state. */
15
+ automaticHyphenation: EffectiveAutomaticHyphenation;
16
+ }>;
17
+ type ResolveEffectiveLineBreakPolicyOptions = {
18
+ attrs: ParagraphAttrs | undefined;
19
+ run: TextRun;
20
+ };
21
+ /** Resolve all authored line-breaking inputs used while measuring one text run. */
22
+ declare const resolveEffectiveLineBreakPolicy: ({
23
+ attrs,
24
+ run
25
+ }: ResolveEffectiveLineBreakPolicyOptions) => EffectiveLineBreakPolicy;
26
+ /** Cache fragments for every paragraph attribute consumed by the resolver. */
27
+ declare const lineBreakPolicyCacheParts: (attrs: ParagraphAttrs) => readonly string[];
28
+ //#endregion
29
+ export { lineBreakPolicyCacheParts, resolveEffectiveLineBreakPolicy };
@@ -0,0 +1,60 @@
1
+ import { hasCjk } from "../../utils/scriptSegments.js";
2
+ //#region src/layout-engine/measure/effectiveLineBreakPolicy.ts
3
+ const DEFAULT_HYPHENATION_ZONE_TWIPS = 360;
4
+ /** Resolve all authored line-breaking inputs used while measuring one text run. */
5
+ const resolveEffectiveLineBreakPolicy = ({ attrs, run }) => {
6
+ const locale = resolveRunLocale(run);
7
+ const rules = attrs?.lineBreakRules;
8
+ const before = rules?.noLineBreaksBefore;
9
+ const after = rules?.noLineBreaksAfter;
10
+ const automaticHyphenation = attrs?.automaticHyphenation;
11
+ return {
12
+ provider: {
13
+ ...locale ? { locale } : {},
14
+ ...attrs?.kinsoku !== void 0 ? { kinsoku: attrs.kinsoku } : {},
15
+ ...before && languageMatches(before.language, locale) ? { noLineBreaksBefore: before.characters } : {},
16
+ ...after && languageMatches(after.language, locale) ? { noLineBreaksAfter: after.characters } : {},
17
+ ...rules?.useLegacyEthiopicAmharicRules ? { useLegacyEthiopicAmharicRules: true } : {},
18
+ ...automaticHyphenation?.doNotHyphenateCaps !== void 0 ? { doNotHyphenateCaps: automaticHyphenation.doNotHyphenateCaps } : {},
19
+ ...run.allCaps === true ? { renderedAllCaps: true } : {}
20
+ },
21
+ hangingPunctuation: attrs?.overflowPunctuation !== false,
22
+ automaticHyphenation: resolveAutomaticHyphenation(attrs)
23
+ };
24
+ };
25
+ /** Cache fragments for every paragraph attribute consumed by the resolver. */
26
+ const lineBreakPolicyCacheParts = (attrs) => {
27
+ const parts = [];
28
+ if (attrs.kinsoku !== void 0) parts.push(`kinsoku:${attrs.kinsoku}`);
29
+ if (attrs.overflowPunctuation !== void 0) parts.push(`overflow-punct:${attrs.overflowPunctuation}`);
30
+ if (attrs.suppressAutoHyphens !== void 0) parts.push(`suppress-auto-hyphens:${attrs.suppressAutoHyphens}`);
31
+ const automaticHyphenation = attrs.automaticHyphenation;
32
+ if (automaticHyphenation) parts.push(`auto-hyphens:${automaticHyphenation.doNotHyphenateCaps}|${automaticHyphenation.consecutiveLineLimit}|${automaticHyphenation.hyphenationZoneTwips}`);
33
+ const rules = attrs.lineBreakRules;
34
+ if (rules) parts.push(`line-break-rules:${rules.noLineBreaksBefore?.language}|${rules.noLineBreaksBefore?.characters}|${rules.noLineBreaksAfter?.language}|${rules.noLineBreaksAfter?.characters}|${rules.useLegacyEthiopicAmharicRules}`);
35
+ return parts;
36
+ };
37
+ const resolveAutomaticHyphenation = (attrs) => {
38
+ const automaticHyphenation = attrs?.automaticHyphenation;
39
+ if (automaticHyphenation?.enabled !== true || attrs?.suppressAutoHyphens === true) return { type: "disabled" };
40
+ return {
41
+ type: "enabled",
42
+ consecutiveLineLimit: automaticHyphenation.consecutiveLineLimit ?? 0,
43
+ hyphenationZoneTwips: automaticHyphenation.hyphenationZoneTwips ?? DEFAULT_HYPHENATION_ZONE_TWIPS
44
+ };
45
+ };
46
+ const resolveRunLocale = (run) => {
47
+ const language = run.language;
48
+ if (hasCjk(run.text)) return language?.eastAsia ?? language?.val;
49
+ if (run.rtl) return language?.bidi ?? language?.val;
50
+ return language?.val;
51
+ };
52
+ const languageMatches = (ruleLanguage, locale) => {
53
+ if (!ruleLanguage) return true;
54
+ if (!locale) return false;
55
+ const rule = ruleLanguage.toLowerCase();
56
+ const active = locale.toLowerCase();
57
+ return active === rule || active.startsWith(`${rule}-`) || rule.startsWith(`${active}-`);
58
+ };
59
+ //#endregion
60
+ export { lineBreakPolicyCacheParts, resolveEffectiveLineBreakPolicy };
@@ -1,13 +1,14 @@
1
1
  import { CJK_FALLBACK_FONT_FAMILY, isCjkFont } from "../../utils/fontResolver.js";
2
2
  import { getFontMetrics, measureRun, measureTextWidth } from "./measureProvider.js";
3
3
  import { countCompressibleSpaces } from "./textMeasurementPolicy.js";
4
- import { buildRunFontStyle, ptToPx } from "./measureHelpers.js";
4
+ import { buildRunFontStyle, ptToPx, twipsToPx } from "./measureHelpers.js";
5
5
  import "./lineBreakProvider.js";
6
6
  import { calculateTabWidth, pixelsToTwips } from "../../prosemirror/utils/tabCalculator.js";
7
7
  import { inlineImageBoundingBox } from "../../utils/rotationBoundingBox.js";
8
8
  import { hasCjk } from "../../utils/scriptSegments.js";
9
9
  import { measuredLineAdvance } from "../lineFlow.js";
10
10
  import { isFloatingImageRun } from "../types.js";
11
+ import { resolveEffectiveLineBreakPolicy } from "./effectiveLineBreakPolicy.js";
11
12
  import { clampFloatingWrapMargins } from "./clampFloatingWrapMargins.js";
12
13
  import { getFloatingAvailableWidth, getFloatingMargins } from "./floatingZones.js";
13
14
  import { getListMarkerInlineWidth } from "./listMarkerWidth.js";
@@ -24,7 +25,7 @@ const DEFAULT_FONT_FAMILY = "Calibri";
24
25
  const DEFAULT_LINE_HEIGHT_MULTIPLIER = 1;
25
26
  const WIDTH_TOLERANCE = .5;
26
27
  const JUSTIFY_SHRINK_TOLERANCE_RATIO = .016;
27
- const JUSTIFY_SPACE_CONTRACTION_RATIO = .1;
28
+ const JUSTIFY_SPACE_CONTRACTION_RATIO = .075;
28
29
  const JUSTIFY_LIST_MARKER_SPACE_CONTRACTION_RATIO = .195;
29
30
  const JUSTIFY_LIST_CONTINUATION_SPACE_CONTRACTION_RATIO = .23;
30
31
  const JUSTIFY_INSET_LIST_SHRINK_TOLERANCE_RATIO = .015;
@@ -51,6 +52,11 @@ function findMaxFittingLength(text, style, maxWidth, forceMin = false, policy) {
51
52
  }
52
53
  return forceMin && best === 0 ? boundaries.at(0) ?? 0 : best;
53
54
  }
55
+ function exceedsHyphenationZone(line, zoneTwips) {
56
+ if (line.width <= 0) return true;
57
+ const visibleWidth = Math.max(0, line.width - line.trailingWhitespaceWidth);
58
+ return Math.max(0, line.availableWidth - visibleWidth) > twipsToPx(zoneTwips) + WIDTH_TOLERANCE;
59
+ }
54
60
  /**
55
61
  * Extract FontStyle from a run that carries RunFormatting (text, tab, or
56
62
  * field). All three share the same formatting shape, so they measure the
@@ -60,31 +66,6 @@ function findMaxFittingLength(text, style, maxWidth, forceMin = false, policy) {
60
66
  function runToFontStyle(run) {
61
67
  return buildRunFontStyle(run, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE);
62
68
  }
63
- function languageMatches(ruleLanguage, locale) {
64
- if (!ruleLanguage) return true;
65
- if (!locale) return false;
66
- const rule = ruleLanguage.toLowerCase();
67
- const active = locale.toLowerCase();
68
- return active === rule || active.startsWith(`${rule}-`) || rule.startsWith(`${active}-`);
69
- }
70
- function lineBreakPolicy(block, run) {
71
- const language = run.language;
72
- let locale = language?.val;
73
- if (hasCjk(run.text)) locale = language?.eastAsia ?? language?.val;
74
- else if (run.rtl) locale = language?.bidi ?? language?.val;
75
- const rules = block.attrs?.lineBreakRules;
76
- const before = rules?.noLineBreaksBefore;
77
- const after = rules?.noLineBreaksAfter;
78
- return {
79
- ...locale ? { locale } : {},
80
- ...block.attrs?.kinsoku !== void 0 ? { kinsoku: block.attrs.kinsoku } : {},
81
- ...before && languageMatches(before.language, locale) ? { noLineBreaksBefore: before.characters } : {},
82
- ...after && languageMatches(after.language, locale) ? { noLineBreaksAfter: after.characters } : {},
83
- ...rules?.useLegacyEthiopicAmharicRules ? { useLegacyEthiopicAmharicRules: true } : {},
84
- ...block.attrs?.automaticHyphenation?.doNotHyphenateCaps !== void 0 ? { doNotHyphenateCaps: block.attrs.automaticHyphenation.doNotHyphenateCaps } : {},
85
- ...run.allCaps === true ? { renderedAllCaps: true } : {}
86
- };
87
- }
88
69
  /**
89
70
  * Line-HEIGHT style for a text run. Word derives a CJK line's height from an
90
71
  * East-Asian face, not the run's ascii font: real documents routinely put CJK
@@ -454,7 +435,10 @@ function computeTrailingGlueWidths(block) {
454
435
  }
455
436
  if (isSpaceOrTab(text.at(0))) continue;
456
437
  const style = runToFontStyle(nextRun);
457
- const firstBreak = findWordBreaks(text, lineBreakPolicy(block, nextRun)).at(0);
438
+ const firstBreak = findWordBreaks(text, resolveEffectiveLineBreakPolicy({
439
+ attrs: block.attrs,
440
+ run: nextRun
441
+ }).provider).at(0);
458
442
  widths[index] = measureTextWidth(firstBreak === void 0 ? text : trimTrailingSpacesAndTabs(text.slice(0, firstBreak)), style) + (firstBreak === void 0 ? widths[index + 1] ?? 0 : 0);
459
443
  }
460
444
  return widths;
@@ -466,7 +450,10 @@ function computeProtectedCrossRunGlueWidths(block) {
466
450
  if (!run || !isTextRun(run)) continue;
467
451
  const trailing = /(\S+)(\s*)$/u.exec(run.text ?? "");
468
452
  const token = trailing?.[1];
469
- const policy = lineBreakPolicy(block, run);
453
+ const policy = resolveEffectiveLineBreakPolicy({
454
+ attrs: block.attrs,
455
+ run
456
+ }).provider;
470
457
  if (!token || token.length !== 1 || !policy.locale?.toLocaleLowerCase().startsWith("cs")) continue;
471
458
  let separator = trailing[2] ?? "";
472
459
  let glueWidth = separator.length > 0 ? measureTextWidth(separator, runToFontStyle(run)) : 0;
@@ -512,7 +499,10 @@ function collectCrossRunWord({ block, startRunIndex, startChar }) {
512
499
  if (runIndex !== startRunIndex && isSpaceOrTab(remainder[0])) break;
513
500
  const remainingBudget = 256 - text.length;
514
501
  const boundedRemainder = remainder.slice(0, remainingBudget + 1);
515
- const firstBreak = findWordBreaks(boundedRemainder, lineBreakPolicy(block, run)).at(0);
502
+ const firstBreak = findWordBreaks(boundedRemainder, resolveEffectiveLineBreakPolicy({
503
+ attrs: block.attrs,
504
+ run
505
+ }).provider).at(0);
516
506
  const segmentText = trimTrailingSpacesAndTabs(firstBreak === void 0 ? boundedRemainder : boundedRemainder.slice(0, firstBreak));
517
507
  if (segmentText.length === 0) break;
518
508
  if (segmentText.length > remainingBudget) return;
@@ -534,7 +524,10 @@ function collectCrossRunWord({ block, startRunIndex, startChar }) {
534
524
  return {
535
525
  text,
536
526
  width,
537
- breaks: findHyphenationBreaks(text, lineBreakPolicy(block, firstRun)),
527
+ breaks: findHyphenationBreaks(text, resolveEffectiveLineBreakPolicy({
528
+ attrs: block.attrs,
529
+ run: firstRun
530
+ }).provider),
538
531
  segments
539
532
  };
540
533
  }
@@ -944,7 +937,11 @@ function measureParagraph(block, maxWidth, options) {
944
937
  const textRun = run;
945
938
  const text = textRun.text;
946
939
  const style = runToFontStyle(textRun);
947
- const breakPolicy = lineBreakPolicy(block, textRun);
940
+ const effectiveLineBreakPolicy = resolveEffectiveLineBreakPolicy({
941
+ attrs: block.attrs,
942
+ run: textRun
943
+ });
944
+ const breakPolicy = effectiveLineBreakPolicy.provider;
948
945
  const lineHeightStyle = cjkLineHeightStyle(textRun, style);
949
946
  updateMaxFont(lineHeightStyle);
950
947
  if (!text || text.length === 0) {
@@ -966,13 +963,12 @@ function measureParagraph(block, maxWidth, options) {
966
963
  const measuredWord = trimTrailingSpacesAndTabs(word);
967
964
  const wordWidth = measureTextWidth(measuredWord, style);
968
965
  const fullWordWidth = measureTextWidth(word, style);
969
- const hangingPunctuationWidth = trailingHangingPunctuationWidth(measuredWord, style, breakPolicy, block.attrs?.overflowPunctuation);
966
+ const hangingPunctuationWidth = trailingHangingPunctuationWidth(measuredWord, style, breakPolicy, effectiveLineBreakPolicy.hangingPunctuation);
970
967
  const isFirstLine = lines.length === 0;
971
968
  const regularSpaceWidth = compressibleSpaceWidth(measuredWord, style);
972
969
  const widthTolerance = isJustifiedParagraph ? justifyFitTolerance(currentLine, isFirstLine ? firstLineJustifyFitStrategy : continuationJustifyFitStrategy, regularSpaceWidth) : WIDTH_TOLERANCE;
973
- const automaticHyphenation = block.attrs?.automaticHyphenation;
974
- const consecutiveLineLimit = automaticHyphenation?.consecutiveLineLimit ?? 0;
975
- const mayHyphenateLine = automaticHyphenation?.enabled === true && block.attrs?.suppressAutoHyphens !== true && (consecutiveLineLimit === 0 || consecutiveHyphenatedLines < consecutiveLineLimit);
970
+ const automaticHyphenation = effectiveLineBreakPolicy.automaticHyphenation;
971
+ const mayHyphenateLine = automaticHyphenation.type === "enabled" && (automaticHyphenation.consecutiveLineLimit === 0 || consecutiveHyphenatedLines < automaticHyphenation.consecutiveLineLimit) && exceedsHyphenationZone(currentLine, automaticHyphenation.hyphenationZoneTwips);
976
972
  const isRunTail = nextBreak === text.length;
977
973
  const crossRunWord = mayHyphenateLine && isRunTail && word.length > 0 && !isBreakChar(word.at(-1)) ? collectCrossRunWord({
978
974
  block,
@@ -3,6 +3,36 @@ import { isFloatingImageRun } from "../types.js";
3
3
  import { clampFloatingWrapMargins } from "./clampFloatingWrapMargins.js";
4
4
  import { createTableCellFlowState, placeTableCellBlock } from "./tableCellFlow.js";
5
5
  //#region src/layout-engine/measure/tableCellFloating.ts
6
+ const resolveCellScopedPosition = ({ run, paragraphY, contentWidth }) => {
7
+ const position = run.position;
8
+ let side = "left";
9
+ let x = 0;
10
+ if (position?.horizontal) {
11
+ const horizontal = position.horizontal;
12
+ if (horizontal.align === "right") {
13
+ side = "right";
14
+ x = contentWidth - run.width;
15
+ } else if (horizontal.align === "center") x = (contentWidth - run.width) / 2;
16
+ else if (horizontal.posOffset !== void 0) {
17
+ x = emuToPixels(horizontal.posOffset);
18
+ side = x > contentWidth / 2 ? "right" : "left";
19
+ }
20
+ } else if (run.cssFloat === "right") {
21
+ side = "right";
22
+ x = contentWidth - run.width;
23
+ }
24
+ let y = paragraphY;
25
+ if (position?.vertical) {
26
+ const vertical = position.vertical;
27
+ if (vertical.posOffset !== void 0) y = paragraphY + emuToPixels(vertical.posOffset);
28
+ else if (vertical.align === "top") y = 0;
29
+ }
30
+ return {
31
+ side,
32
+ x,
33
+ y
34
+ };
35
+ };
6
36
  function getTableCellContentWidth(cell, cellMeasure) {
7
37
  const padLeft = cell?.padding?.left ?? 7;
8
38
  const padRight = cell?.padding?.right ?? 7;
@@ -19,40 +49,16 @@ function getTableCellFloatingImages(cell, cellMeasure, contentWidth, resolvePosi
19
49
  if (block.kind !== "paragraph") continue;
20
50
  for (const run of block.runs) {
21
51
  if (run.kind !== "image" || !isFloatingImageRun(run)) continue;
22
- const position = run.position;
23
52
  const distTop = run.distTop ?? 0;
24
53
  const distBottom = run.distBottom ?? 0;
25
54
  const distLeft = run.distLeft ?? 12;
26
55
  const distRight = run.distRight ?? 12;
27
- let side;
28
- let x;
29
- let y;
30
- if (resolvePosition) ({side, x, y} = resolvePosition(run, placement.contentTop));
31
- else {
32
- side = "left";
33
- x = 0;
34
- if (position?.horizontal) {
35
- const horizontal = position.horizontal;
36
- if (horizontal.align === "right") {
37
- side = "right";
38
- x = contentWidth - run.width;
39
- } else if (horizontal.align === "center") x = (contentWidth - run.width) / 2;
40
- else if (horizontal.posOffset !== void 0) {
41
- x = emuToPixels(horizontal.posOffset);
42
- side = x > contentWidth / 2 ? "right" : "left";
43
- }
44
- } else if (run.cssFloat === "right") {
45
- side = "right";
46
- x = contentWidth - run.width;
47
- }
48
- y = placement.contentTop;
49
- if (position?.vertical) {
50
- const vertical = position.vertical;
51
- if (vertical.posOffset !== void 0) y = placement.contentTop + emuToPixels(vertical.posOffset);
52
- else if (vertical.align === "top") y = 0;
53
- }
54
- x = Math.max(0, Math.min(x, contentWidth - run.width));
55
- }
56
+ const verticalOriginY = run.position?.vertical?.relativeTo === "paragraph" ? placement.top : placement.contentTop;
57
+ const resolved = run.layoutInCell === false && resolvePosition ? resolvePosition(run, verticalOriginY) : resolveCellScopedPosition({
58
+ run,
59
+ paragraphY: verticalOriginY,
60
+ contentWidth
61
+ });
56
62
  let wrapText = "bothSides";
57
63
  if (run.cssFloat === "left") wrapText = "right";
58
64
  else if (run.cssFloat === "right") wrapText = "left";
@@ -67,9 +73,9 @@ function getTableCellFloatingImages(cell, cellMeasure, contentWidth, resolvePosi
67
73
  ...run.cropRight != null ? { cropRight: run.cropRight } : {},
68
74
  ...run.cropBottom != null ? { cropBottom: run.cropBottom } : {},
69
75
  ...run.cropLeft != null ? { cropLeft: run.cropLeft } : {},
70
- x,
71
- y,
72
- side,
76
+ x: resolved.x,
77
+ y: resolved.y,
78
+ side: resolved.side,
73
79
  distTop,
74
80
  distBottom,
75
81
  distLeft,
@@ -162,7 +162,8 @@ type ImageRun = {
162
162
  * eigenpal #424 (opacity render pipeline).
163
163
  */
164
164
  opacity?: number; /** Position for floating/anchored images */
165
- position?: ImageRunPosition; /** Wrap type from DOCX (inline, square, tight, through, topAndBottom, etc.) */
165
+ position?: ImageRunPosition; /** Whether a table-cell anchor uses the cell as its positioning scope. Undefined defaults true. */
166
+ layoutInCell?: boolean; /** Wrap type from DOCX (inline, square, tight, through, topAndBottom, etc.) */
166
167
  wrapType?: ImageWrap["type"]; /** Display mode for CSS rendering */
167
168
  displayMode?: "inline" | "block" | "float"; /** CSS float direction */
168
169
  cssFloat?: "left" | "right" | "none"; /** Wrap distances in pixels */
@@ -293,13 +294,14 @@ type ListNumPr = {
293
294
  */
294
295
  type ParagraphAttrs = {
295
296
  alignment?: "left" | "center" | "right" | "justify"; /** East Asian first/last-character restrictions (`w:kinsoku`). */
296
- kinsoku?: boolean; /** Hanging punctuation (`w:overflowPunct`), retained for layout policy. */
297
+ kinsoku?: boolean; /** Hanging punctuation (`w:overflowPunct`); defaults to enabled when omitted. */
297
298
  overflowPunctuation?: boolean; /** Exempt this paragraph from document automatic hyphenation. */
298
299
  suppressAutoHyphens?: boolean; /** Document-wide automatic hyphenation controls retained for line layout. */
299
300
  automaticHyphenation?: {
300
301
  enabled: true;
301
302
  doNotHyphenateCaps?: boolean;
302
- consecutiveLineLimit?: number;
303
+ consecutiveLineLimit?: number; /** Maximum tolerated line-end whitespace before hyphenation, in twips. */
304
+ hyphenationZoneTwips?: number;
303
305
  }; /** Document-wide custom line-edge characters and compatibility behavior. */
304
306
  lineBreakRules?: {
305
307
  noLineBreaksBefore?: {
@@ -37,6 +37,14 @@ declare class FindReplaceManager<TMatch extends FindMatchPosition> {
37
37
  match: TMatch;
38
38
  index: number;
39
39
  } | null;
40
+ /**
41
+ * Move the cursor to an explicit match index. Returns the selected match, or
42
+ * null when there is no active result or the index is out of bounds.
43
+ */
44
+ goTo(index: number): {
45
+ match: TMatch;
46
+ index: number;
47
+ } | null;
40
48
  /**
41
49
  * Replace the current match's text. Returns the new document, or null when
42
50
  * there is no current match or the replace fails.
@@ -61,6 +61,24 @@ var FindReplaceManager = class {
61
61
  } : null;
62
62
  }
63
63
  /**
64
+ * Move the cursor to an explicit match index. Returns the selected match, or
65
+ * null when there is no active result or the index is out of bounds.
66
+ */
67
+ goTo(index) {
68
+ const result = this.result;
69
+ if (!result || index < 0 || index >= result.matches.length) return null;
70
+ const match = result.matches.at(index);
71
+ if (!match) return null;
72
+ this.result = {
73
+ ...result,
74
+ currentIndex: index
75
+ };
76
+ return {
77
+ match,
78
+ index
79
+ };
80
+ }
81
+ /**
64
82
  * Replace the current match's text. Returns the new document, or null when
65
83
  * there is no current match or the replace fails.
66
84
  */
@@ -52,6 +52,12 @@ type TableActionResult = {
52
52
  };
53
53
  declare class TableSelectionManager extends Subscribable<TableSelectionState> {
54
54
  constructor();
55
+ /**
56
+ * Track cell coordinates before a document model is available. This supports
57
+ * adapter pointer state without fabricating a table context; callers that
58
+ * need structural operations should use {@link selectCell} with a document.
59
+ */
60
+ selectCellCoordinates(coords: CellCoordinates): void;
55
61
  /**
56
62
  * Select a cell within `doc`. Resolves the table at `coords.tableIndex` and
57
63
  * derives the full {@link TableContext}. Returns the context, or `null` (and
@@ -107,6 +107,20 @@ var TableSelectionManager = class extends Subscribable {
107
107
  super(EMPTY_SELECTION);
108
108
  }
109
109
  /**
110
+ * Track cell coordinates before a document model is available. This supports
111
+ * adapter pointer state without fabricating a table context; callers that
112
+ * need structural operations should use {@link selectCell} with a document.
113
+ */
114
+ selectCellCoordinates(coords) {
115
+ this.setSnapshot({
116
+ context: null,
117
+ table: null,
118
+ tableIndex: coords.tableIndex,
119
+ rowIndex: coords.rowIndex,
120
+ columnIndex: coords.columnIndex
121
+ });
122
+ }
123
+ /**
110
124
  * Select a cell within `doc`. Resolves the table at `coords.tableIndex` and
111
125
  * derives the full {@link TableContext}. Returns the context, or `null` (and
112
126
  * clears the selection) when that table no longer exists.
@@ -306,6 +306,7 @@ const readImageAttrs = (node) => {
306
306
  optionalNumber(attrs, "distLeft", "image.attrs.distLeft", issues);
307
307
  optionalNumber(attrs, "distRight", "image.attrs.distRight", issues);
308
308
  optionalImagePosition(attrs, "position", "image.attrs.position", issues);
309
+ optionalBoolean(attrs, "layoutInCell", "image.attrs.layoutInCell", issues);
309
310
  optionalNumber(attrs, "borderWidth", "image.attrs.borderWidth", issues);
310
311
  optionalString(attrs, "borderColor", "image.attrs.borderColor", issues);
311
312
  optionalString(attrs, "borderStyle", "image.attrs.borderStyle", issues);
@@ -0,0 +1,62 @@
1
+ import { document_d_exports } from "../../types/document.js";
2
+
3
+ //#region src/prosemirror/conversion/effectiveTableCellFormatting.d.ts
4
+ type TableCellMarginsAttrs = {
5
+ top?: number;
6
+ bottom?: number;
7
+ left?: number;
8
+ right?: number;
9
+ };
10
+ type EffectiveTableCellWidth = {
11
+ type: "none";
12
+ } | {
13
+ type: "value";
14
+ source: "direct" | "grid";
15
+ value: number;
16
+ widthType: document_d_exports.TableWidthType;
17
+ };
18
+ type EffectiveTableCellBackground = {
19
+ type: "none";
20
+ source: "none" | "direct" | "style";
21
+ } | {
22
+ type: "color";
23
+ source: "direct" | "style";
24
+ rgb: string;
25
+ };
26
+ type EffectiveTableCellFormatting = Readonly<{
27
+ width: EffectiveTableCellWidth;
28
+ background: EffectiveTableCellBackground;
29
+ borders: document_d_exports.TableCellBorders | undefined;
30
+ margins: TableCellMarginsAttrs | undefined;
31
+ }>;
32
+ type TableCellPosition = {
33
+ isFirstRow?: boolean;
34
+ isLastRow?: boolean;
35
+ isFirstColumn?: boolean;
36
+ isLastColumn?: boolean;
37
+ };
38
+ type ResolveEffectiveTableCellFormattingOptions = {
39
+ directFormatting: document_d_exports.TableCellFormatting | undefined;
40
+ styleFormatting: document_d_exports.TableCellFormatting | undefined;
41
+ tableBorders: document_d_exports.TableBorders | undefined;
42
+ position: TableCellPosition;
43
+ gridWidthPercent: number | undefined;
44
+ defaultMargins: TableCellMarginsAttrs | undefined;
45
+ theme: document_d_exports.Theme | null | undefined;
46
+ };
47
+ /**
48
+ * Resolve the display-facing table-cell cascade without mutating authored
49
+ * OOXML formatting. Source discriminators keep inherited values distinct from
50
+ * direct values that the editor may later override.
51
+ */
52
+ declare const resolveEffectiveTableCellFormatting: ({
53
+ directFormatting,
54
+ styleFormatting,
55
+ tableBorders,
56
+ position,
57
+ gridWidthPercent,
58
+ defaultMargins,
59
+ theme
60
+ }: ResolveEffectiveTableCellFormattingOptions) => EffectiveTableCellFormatting;
61
+ //#endregion
62
+ export { TableCellMarginsAttrs, TableCellPosition, resolveEffectiveTableCellFormatting };
@@ -0,0 +1,131 @@
1
+ import { resolveColor } from "../../utils/colorResolver.js";
2
+ import { resolveShadingFill } from "../../utils/formatToStyle.js";
3
+ //#region src/prosemirror/conversion/effectiveTableCellFormatting.ts
4
+ const TABLE_BORDER_SIDES = [
5
+ "top",
6
+ "bottom",
7
+ "left",
8
+ "right",
9
+ "insideH",
10
+ "insideV",
11
+ "topLeftToBottomRight",
12
+ "topRightToBottomLeft"
13
+ ];
14
+ /**
15
+ * Resolve the display-facing table-cell cascade without mutating authored
16
+ * OOXML formatting. Source discriminators keep inherited values distinct from
17
+ * direct values that the editor may later override.
18
+ */
19
+ const resolveEffectiveTableCellFormatting = ({ directFormatting, styleFormatting, tableBorders, position, gridWidthPercent, defaultMargins, theme }) => {
20
+ return {
21
+ width: resolveEffectiveWidth(directFormatting, gridWidthPercent),
22
+ background: resolveEffectiveBackground({
23
+ directFormatting,
24
+ styleFormatting,
25
+ theme
26
+ }),
27
+ borders: resolveEffectiveBorders({
28
+ directFormatting,
29
+ styleFormatting,
30
+ tableBorders,
31
+ position,
32
+ theme
33
+ }),
34
+ margins: resolveEffectiveMargins({
35
+ directFormatting,
36
+ styleFormatting,
37
+ defaultMargins
38
+ })
39
+ };
40
+ };
41
+ const resolveEffectiveWidth = (directFormatting, gridWidthPercent) => {
42
+ const directWidth = directFormatting?.width;
43
+ if (directWidth) return {
44
+ type: "value",
45
+ source: "direct",
46
+ value: directWidth.value,
47
+ widthType: directWidth.type
48
+ };
49
+ if (gridWidthPercent !== void 0) return {
50
+ type: "value",
51
+ source: "grid",
52
+ value: gridWidthPercent,
53
+ widthType: "pct"
54
+ };
55
+ return { type: "none" };
56
+ };
57
+ const resolveEffectiveBackground = ({ directFormatting, styleFormatting, theme }) => {
58
+ const directShading = directFormatting?.shading;
59
+ if (directShading !== void 0) return resolveBackgroundColor({
60
+ shading: directShading,
61
+ source: "direct",
62
+ theme
63
+ });
64
+ const styleShading = styleFormatting?.shading;
65
+ if (styleShading !== void 0) return resolveBackgroundColor({
66
+ shading: styleShading,
67
+ source: "style",
68
+ theme
69
+ });
70
+ return {
71
+ type: "none",
72
+ source: "none"
73
+ };
74
+ };
75
+ const resolveBackgroundColor = ({ shading, source, theme }) => {
76
+ const rgb = resolveShadingFill(shading, theme).replace(/^#/u, "");
77
+ if (!rgb) return {
78
+ type: "none",
79
+ source
80
+ };
81
+ return {
82
+ type: "color",
83
+ source,
84
+ rgb
85
+ };
86
+ };
87
+ const resolveEffectiveBorders = ({ directFormatting, styleFormatting, tableBorders, position, theme }) => {
88
+ const baseBorders = tableBorders ? {
89
+ top: position.isFirstRow ? tableBorders.top : tableBorders.insideH,
90
+ bottom: position.isLastRow ? tableBorders.bottom : tableBorders.insideH,
91
+ left: position.isFirstColumn ? tableBorders.left : tableBorders.insideV,
92
+ right: position.isLastColumn ? tableBorders.right : tableBorders.insideV
93
+ } : void 0;
94
+ const styleBorders = styleFormatting?.borders;
95
+ const directBorders = directFormatting?.borders;
96
+ const borders = baseBorders || styleBorders || directBorders ? {
97
+ ...baseBorders,
98
+ ...styleBorders,
99
+ ...directBorders
100
+ } : void 0;
101
+ return resolveThemedBorderColors(borders, theme);
102
+ };
103
+ const resolveEffectiveMargins = ({ directFormatting, styleFormatting, defaultMargins }) => {
104
+ if (directFormatting?.margins) return cellMarginsToAttrs(directFormatting.margins);
105
+ if (styleFormatting?.margins) return cellMarginsToAttrs(styleFormatting.margins);
106
+ return defaultMargins;
107
+ };
108
+ const cellMarginsToAttrs = (margins) => {
109
+ const result = {};
110
+ if (margins.top?.value !== void 0) result.top = margins.top.value;
111
+ if (margins.bottom?.value !== void 0) result.bottom = margins.bottom.value;
112
+ if (margins.left?.value !== void 0) result.left = margins.left.value;
113
+ if (margins.right?.value !== void 0) result.right = margins.right.value;
114
+ return result;
115
+ };
116
+ const resolveThemedBorderColors = (borders, theme) => {
117
+ if (!borders || !theme?.colorScheme) return borders;
118
+ let resolved;
119
+ for (const side of TABLE_BORDER_SIDES) {
120
+ const border = borders[side];
121
+ if (!border?.color?.themeColor || border.color.auto) continue;
122
+ resolved ??= { ...borders };
123
+ resolved[side] = {
124
+ ...border,
125
+ color: { rgb: resolveColor(border.color, theme).replace(/^#/u, "") }
126
+ };
127
+ }
128
+ return resolved ?? borders;
129
+ };
130
+ //#endregion
131
+ export { resolveEffectiveTableCellFormatting };
@@ -911,6 +911,7 @@ function createImageRun(node) {
911
911
  if (attrs.opacity != null) image.opacity = attrs.opacity;
912
912
  const imagePosition = imagePositionFromAttrs(attrs.position);
913
913
  if (imagePosition) image.position = imagePosition;
914
+ if (attrs.layoutInCell !== void 0) image.layoutInCell = attrs.layoutInCell;
914
915
  if (attrs.borderWidth && attrs.borderWidth > 0) {
915
916
  const outline = {
916
917
  width: pixelsToEmu(attrs.borderWidth),