@stll/folio-core 0.1.3 → 0.2.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 (75) hide show
  1. package/dist/docx/index.d.ts +2 -0
  2. package/dist/docx/index.js +2 -0
  3. package/dist/docx/wrapTypes.d.ts +8 -1
  4. package/dist/docx/wrapTypes.js +0 -8
  5. package/dist/i18n/messages/catalogs.gen.d.ts +2776 -0
  6. package/dist/i18n/messages/catalogs.gen.js +2742 -0
  7. package/dist/i18n/messages/messages.gen.d.ts +166 -0
  8. package/dist/i18n/messages/messages.gen.js +0 -0
  9. package/dist/i18n/messages.d.ts +13 -0
  10. package/dist/i18n/messages.js +39 -0
  11. package/dist/layout-bridge/cellSelectionHighlight.d.ts +18 -0
  12. package/dist/layout-bridge/cellSelectionHighlight.js +36 -0
  13. package/dist/layout-bridge/headerFooterLayout.d.ts +50 -0
  14. package/dist/layout-bridge/headerFooterLayout.js +216 -0
  15. package/dist/layout-bridge/measuring/measureBlocksPipeline.d.ts +33 -0
  16. package/dist/layout-bridge/measuring/measureBlocksPipeline.js +0 -0
  17. package/dist/layout-bridge/sectionColumns.d.ts +11 -0
  18. package/dist/layout-bridge/sectionColumns.js +21 -0
  19. package/dist/layout-bridge/tableInsertHover.d.ts +42 -0
  20. package/dist/layout-bridge/tableInsertHover.js +102 -0
  21. package/dist/layout-engine/index.d.ts +2 -1
  22. package/dist/layout-engine/index.js +2 -1
  23. package/dist/layout-engine/measure/measureParagraph.js +40 -5
  24. package/dist/layout-engine/pmPageIndex.d.ts +24 -0
  25. package/dist/layout-engine/pmPageIndex.js +37 -0
  26. package/dist/layout-painter/imageLayout.d.ts +101 -0
  27. package/dist/layout-painter/imageLayout.js +161 -0
  28. package/dist/layout-painter/renderPage.d.ts +16 -1
  29. package/dist/layout-painter/renderPage.js +28 -1
  30. package/dist/layout-painter/renderParagraph.js +1 -1
  31. package/dist/layout-painter/sdtBoundary.d.ts +24 -1
  32. package/dist/layout-painter/sdtBoundary.js +40 -1
  33. package/dist/managers/AutoSaveManager.d.ts +89 -0
  34. package/dist/managers/AutoSaveManager.js +279 -0
  35. package/dist/managers/ClipboardManager.d.ts +31 -0
  36. package/dist/managers/ClipboardManager.js +147 -0
  37. package/dist/paged-layout/rangeProjection.d.ts +23 -1
  38. package/dist/paged-layout/rangeProjection.js +34 -1
  39. package/dist/prosemirror/cellDragSelection.d.ts +34 -0
  40. package/dist/prosemirror/cellDragSelection.js +69 -0
  41. package/dist/prosemirror/commands/sectionBreak.d.ts +15 -0
  42. package/dist/prosemirror/commands/sectionBreak.js +54 -0
  43. package/dist/prosemirror/commentIdAllocator.d.ts +32 -0
  44. package/dist/prosemirror/commentIdAllocator.js +35 -0
  45. package/dist/prosemirror/commentOps.d.ts +35 -0
  46. package/dist/prosemirror/commentOps.js +104 -0
  47. package/dist/prosemirror/conversion/toProseDoc.js +29 -6
  48. package/dist/prosemirror/extensions/index.d.ts +3 -0
  49. package/dist/prosemirror/extensions/index.js +3 -0
  50. package/dist/prosemirror/imageCommit.d.ts +42 -0
  51. package/dist/prosemirror/imageCommit.js +118 -0
  52. package/dist/prosemirror/paraText.d.ts +26 -0
  53. package/dist/prosemirror/paraText.js +73 -0
  54. package/dist/prosemirror/plugins/templateDirectives.d.ts +21 -1
  55. package/dist/prosemirror/plugins/templateDirectives.js +40 -1
  56. package/dist/prosemirror/queries.d.ts +46 -0
  57. package/dist/prosemirror/queries.js +74 -0
  58. package/dist/prosemirror/styles/styleResolver.d.ts +43 -1
  59. package/dist/prosemirror/styles/styleResolver.js +34 -0
  60. package/dist/prosemirror/tableResize.d.ts +49 -0
  61. package/dist/prosemirror/tableResize.js +162 -0
  62. package/dist/prosemirror/utils/extractTrackedChanges.d.ts +106 -0
  63. package/dist/prosemirror/utils/extractTrackedChanges.js +379 -0
  64. package/dist/prosemirror/utils/visualLineNavigation.d.ts +27 -0
  65. package/dist/prosemirror/utils/visualLineNavigation.js +217 -0
  66. package/dist/style-engine/index.d.ts +2 -2
  67. package/dist/style-engine/styleEngine.d.ts +14 -1
  68. package/dist/style-engine/styleEngine.js +15 -0
  69. package/dist/utils/colorResolver.d.ts +8 -1
  70. package/dist/utils/colorResolver.js +12 -1
  71. package/dist/utils/findVerticalScrollParent.d.ts +20 -0
  72. package/dist/utils/findVerticalScrollParent.js +30 -0
  73. package/dist/utils/fontResolver.d.ts +22 -3
  74. package/dist/utils/fontResolver.js +216 -31
  75. package/package.json +9 -1
@@ -0,0 +1,118 @@
1
+ //#region src/prosemirror/imageCommit.ts
2
+ const MIN_IMAGE_PX = 20;
3
+ const MAX_IMAGE_PX = 2e3;
4
+ /**
5
+ * New image dimensions for a resize drag, shared by the React and Vue overlays
6
+ * (issue #266). Corner handles drive both axes (aspect-locked unless
7
+ * `lockAspect` is false, e.g. Shift held); edge handles drive one axis and
8
+ * never lock. The non-driven axis is returned unchanged. Driven axes are
9
+ * clamped to a sane pixel range.
10
+ */
11
+ function calculateResizedImageDimensions(handle, deltaX, deltaY, startWidth, startHeight, lockAspect) {
12
+ const drivesWidth = handle.includes("w") || handle.includes("e");
13
+ const drivesHeight = handle.includes("n") || handle.includes("s");
14
+ const isCorner = drivesWidth && drivesHeight;
15
+ const signX = handle.includes("w") ? -1 : 1;
16
+ const signY = handle.includes("n") ? -1 : 1;
17
+ let newWidth = drivesWidth ? startWidth + deltaX * signX : startWidth;
18
+ let newHeight = drivesHeight ? startHeight + deltaY * signY : startHeight;
19
+ if (isCorner && lockAspect) {
20
+ const scale = Math.max(newWidth / startWidth, newHeight / startHeight);
21
+ newWidth = startWidth * scale;
22
+ newHeight = startHeight * scale;
23
+ }
24
+ const clamp = (n) => Math.max(MIN_IMAGE_PX, Math.min(MAX_IMAGE_PX, n));
25
+ return {
26
+ width: drivesWidth ? clamp(newWidth) : startWidth,
27
+ height: drivesHeight ? clamp(newHeight) : startHeight
28
+ };
29
+ }
30
+ /** True when the image is floating (anchored) rather than inline. */
31
+ function isFloatingImage(node) {
32
+ const wrapType = node.attrs["wrapType"];
33
+ return node.attrs["displayMode"] === "float" || (wrapType ? [
34
+ "square",
35
+ "tight",
36
+ "through"
37
+ ].includes(wrapType) : false);
38
+ }
39
+ /** Resolve the image node at `pmPos`, or null if it isn't an image. */
40
+ function imageNodeAt(view, pmPos) {
41
+ const node = view.state.doc.nodeAt(pmPos);
42
+ if (!node || node.type.name !== "image") return null;
43
+ return node;
44
+ }
45
+ /**
46
+ * Resize commit: set the image node's `width`/`height`. Returns `pmPos` to
47
+ * re-select, or null if the position no longer holds an image.
48
+ */
49
+ function commitImageResize(view, pmPos, newWidth, newHeight) {
50
+ try {
51
+ const node = imageNodeAt(view, pmPos);
52
+ if (!node) return null;
53
+ view.dispatch(view.state.tr.setNodeMarkup(pmPos, void 0, {
54
+ ...node.attrs,
55
+ width: newWidth,
56
+ height: newHeight
57
+ }));
58
+ return pmPos;
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+ /**
64
+ * Floating drag commit: rewrite the anchor's margin-relative `position`
65
+ * offsets (in EMU) so the image lands at the drop point while staying
66
+ * floating. Returns `pmPos` to re-select, or null on no-op / failure.
67
+ */
68
+ function commitImageFloatMove(view, pmPos, hOffsetEmu, vOffsetEmu) {
69
+ try {
70
+ const node = imageNodeAt(view, pmPos);
71
+ if (!node) return null;
72
+ const newPosition = {
73
+ horizontal: {
74
+ posOffset: hOffsetEmu,
75
+ relativeTo: "margin"
76
+ },
77
+ vertical: {
78
+ posOffset: vOffsetEmu,
79
+ relativeTo: "margin"
80
+ }
81
+ };
82
+ view.dispatch(view.state.tr.setNodeMarkup(pmPos, void 0, {
83
+ ...node.attrs,
84
+ position: newPosition
85
+ }));
86
+ return pmPos;
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+ /**
92
+ * Inline drag commit: move the image node to `dropPos` via a delete + insert
93
+ * pair. Returns the PM position to re-select, or null when the drop is a
94
+ * no-op (same slot) or fails.
95
+ */
96
+ function commitImageInlineMove(view, pmPos, dropPos) {
97
+ try {
98
+ const node = imageNodeAt(view, pmPos);
99
+ if (!node) return null;
100
+ if (dropPos === pmPos || dropPos === pmPos + 1) return null;
101
+ let tr = view.state.tr;
102
+ if (dropPos <= pmPos) {
103
+ tr = tr.delete(pmPos, pmPos + node.nodeSize);
104
+ tr = tr.insert(dropPos, node);
105
+ view.dispatch(tr);
106
+ return dropPos;
107
+ }
108
+ tr = tr.delete(pmPos, pmPos + node.nodeSize);
109
+ const adjusted = Math.min(dropPos - node.nodeSize, tr.doc.content.size);
110
+ tr = tr.insert(adjusted, node);
111
+ view.dispatch(tr);
112
+ return Math.min(adjusted, view.state.doc.content.size - 1);
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+ //#endregion
118
+ export { calculateResizedImageDimensions, commitImageFloatMove, commitImageInlineMove, commitImageResize, isFloatingImage };
@@ -0,0 +1,26 @@
1
+ import { Node } from "prosemirror-model";
2
+
3
+ //#region src/prosemirror/paraText.d.ts
4
+ /** Text of a single PM node (typically a paragraph), vanilla view. */
5
+ declare function getVanillaNodeText(node: Node): string;
6
+ /** Text between two doc positions, vanilla view. */
7
+ declare function getVanillaTextBetween(doc: Node, from: number, to: number): string;
8
+ /**
9
+ * Find `searchText` within a PM paragraph range and return its position.
10
+ *
11
+ * Returns null if:
12
+ * - searchText is empty
13
+ * - searchText is not found
14
+ * - searchText appears more than once (ambiguous; caller disambiguates)
15
+ *
16
+ * The fullText is built from PM text nodes only and matches the vanilla
17
+ * view the agent reads via `read_document`: tracked insertions are
18
+ * excluded (not in the doc yet), tracked deletions are included (still
19
+ * in the doc until accepted), and comment markers are stripped.
20
+ */
21
+ declare function findTextInPmParagraph(doc: Node, paragraphFrom: number, paragraphTo: number, searchText: string): {
22
+ from: number;
23
+ to: number;
24
+ } | null;
25
+ //#endregion
26
+ export { findTextInPmParagraph, getVanillaNodeText, getVanillaTextBetween };
@@ -0,0 +1,73 @@
1
+ //#region src/prosemirror/paraText.ts
2
+ /** Text of a single PM node (typically a paragraph), vanilla view. */
3
+ function getVanillaNodeText(node) {
4
+ const parts = [];
5
+ node.descendants((child) => {
6
+ if (!child.isText || !child.text) return true;
7
+ if (child.marks.some((m) => m.type.name === "insertion")) return false;
8
+ parts.push(child.text);
9
+ return true;
10
+ });
11
+ return parts.join("");
12
+ }
13
+ /** Text between two doc positions, vanilla view. */
14
+ function getVanillaTextBetween(doc, from, to) {
15
+ if (from >= to) return "";
16
+ const parts = [];
17
+ doc.nodesBetween(from, to, (child, pos) => {
18
+ if (!child.isText || !child.text) return;
19
+ if (child.marks.some((m) => m.type.name === "insertion")) return;
20
+ const start = Math.max(from, pos);
21
+ const end = Math.min(to, pos + child.text.length);
22
+ if (start < end) parts.push(child.text.slice(start - pos, end - pos));
23
+ });
24
+ return parts.join("");
25
+ }
26
+ /**
27
+ * Find `searchText` within a PM paragraph range and return its position.
28
+ *
29
+ * Returns null if:
30
+ * - searchText is empty
31
+ * - searchText is not found
32
+ * - searchText appears more than once (ambiguous; caller disambiguates)
33
+ *
34
+ * The fullText is built from PM text nodes only and matches the vanilla
35
+ * view the agent reads via `read_document`: tracked insertions are
36
+ * excluded (not in the doc yet), tracked deletions are included (still
37
+ * in the doc until accepted), and comment markers are stripped.
38
+ */
39
+ function findTextInPmParagraph(doc, paragraphFrom, paragraphTo, searchText) {
40
+ if (!searchText) return null;
41
+ let fullText = "";
42
+ const textPositions = [];
43
+ doc.nodesBetween(paragraphFrom, paragraphTo, (node, pos) => {
44
+ if (!node.isText || !node.text) return;
45
+ if (node.marks.some((m) => m.type.name === "insertion")) return;
46
+ textPositions.push({
47
+ pos,
48
+ len: node.text.length
49
+ });
50
+ fullText += node.text;
51
+ });
52
+ const firstMatch = fullText.indexOf(searchText);
53
+ if (firstMatch === -1) return null;
54
+ if (fullText.indexOf(searchText, firstMatch + 1) !== -1) return null;
55
+ let charOffset = 0;
56
+ let fromPos = paragraphFrom;
57
+ let toPos = paragraphFrom;
58
+ for (const tp of textPositions) {
59
+ const segEnd = charOffset + tp.len;
60
+ if (charOffset <= firstMatch && firstMatch < segEnd) fromPos = tp.pos + (firstMatch - charOffset);
61
+ if (charOffset <= firstMatch + searchText.length && firstMatch + searchText.length <= segEnd) {
62
+ toPos = tp.pos + (firstMatch + searchText.length - charOffset);
63
+ break;
64
+ }
65
+ charOffset = segEnd;
66
+ }
67
+ return {
68
+ from: fromPos,
69
+ to: toPos
70
+ };
71
+ }
72
+ //#endregion
73
+ export { findTextInPmParagraph, getVanillaNodeText, getVanillaTextBetween };
@@ -12,6 +12,26 @@ type DirectiveRange = {
12
12
  clauseVersion?: string; /** True for block directives that occupy their own paragraph. */
13
13
  block: boolean;
14
14
  };
15
+ /**
16
+ * Nesting depth (0-based) of every block-directive opener, derived purely from
17
+ * the scanned ranges by containment: walk the block openers/closers in document
18
+ * order with a kind-aware stack, and record each opener's depth as the stack size
19
+ * before it is pushed. Only `block:true` if/each pairs participate (inline markers
20
+ * resolve within a paragraph and get no rail).
21
+ *
22
+ * Matching is kind-aware so a mid-edit / unbalanced template stays sane: a closer
23
+ * pops the nearest opener of the *same family* ({{/if}} ⇒ {{#if}}, {{/each}} ⇒
24
+ * {{#each}}), dropping any still-open openers nested above it; a closer with no
25
+ * matching opener is ignored (never decrements a foreign block's depth). A blind
26
+ * open/close counter would mis-count here: e.g. a stray {{/each}} between {{#if}}
27
+ * and a nested {{#each}} would wrongly pull the inner {{#each}} back to depth 0.
28
+ *
29
+ * Keyed by the opener's `from` PM position, which is unique per marker, so the
30
+ * overlay can look a band's depth up from its opener range. This is a pure
31
+ * function of the ranges (no layout), hence unit-testable in isolation; the
32
+ * overlay caps the *visual* indentation separately.
33
+ */
34
+ declare const computeBlockDepths: (ranges: readonly DirectiveRange[]) => Map<number, number>;
15
35
  declare const scanDirectives: (doc: Node) => DirectiveRange[];
16
36
  declare const templateDirectivesKey: PluginKey<DocScanState<undefined, DirectiveRange>>;
17
37
  type TemplateDirectivesPluginOptions = {
@@ -22,4 +42,4 @@ declare const createTemplateDirectivesPlugin: ({
22
42
  }?: TemplateDirectivesPluginOptions) => import("prosemirror-state").Plugin<DocScanState<undefined, DirectiveRange>>;
23
43
  declare const getTemplateDirectives: (state: EditorState) => readonly DirectiveRange[];
24
44
  //#endregion
25
- export { type DirectiveKind, DirectiveRange, TemplateDirectivesPluginOptions, createTemplateDirectivesPlugin, getTemplateDirectives, scanDirectives, templateDirectivesKey };
45
+ export { type DirectiveKind, DirectiveRange, TemplateDirectivesPluginOptions, computeBlockDepths, createTemplateDirectivesPlugin, getTemplateDirectives, scanDirectives, templateDirectivesKey };
@@ -21,6 +21,45 @@ const directiveExpr = (meta) => {
21
21
  default: return assertNever(meta);
22
22
  }
23
23
  };
24
+ /** Block-directive openers ({{#if}}, {{#each}}) that start a gutter-rail band. */
25
+ const BLOCK_OPENER_KINDS = /* @__PURE__ */ new Set(["if", "each"]);
26
+ /** Block-directive closers ({{/if}}, {{/each}}) that end a gutter-rail band. */
27
+ const BLOCK_CLOSER_KINDS = /* @__PURE__ */ new Set(["endif", "endeach"]);
28
+ /**
29
+ * Nesting depth (0-based) of every block-directive opener, derived purely from
30
+ * the scanned ranges by containment: walk the block openers/closers in document
31
+ * order with a kind-aware stack, and record each opener's depth as the stack size
32
+ * before it is pushed. Only `block:true` if/each pairs participate (inline markers
33
+ * resolve within a paragraph and get no rail).
34
+ *
35
+ * Matching is kind-aware so a mid-edit / unbalanced template stays sane: a closer
36
+ * pops the nearest opener of the *same family* ({{/if}} ⇒ {{#if}}, {{/each}} ⇒
37
+ * {{#each}}), dropping any still-open openers nested above it; a closer with no
38
+ * matching opener is ignored (never decrements a foreign block's depth). A blind
39
+ * open/close counter would mis-count here: e.g. a stray {{/each}} between {{#if}}
40
+ * and a nested {{#each}} would wrongly pull the inner {{#each}} back to depth 0.
41
+ *
42
+ * Keyed by the opener's `from` PM position, which is unique per marker, so the
43
+ * overlay can look a band's depth up from its opener range. This is a pure
44
+ * function of the ranges (no layout), hence unit-testable in isolation; the
45
+ * overlay caps the *visual* indentation separately.
46
+ */
47
+ const computeBlockDepths = (ranges) => {
48
+ const depths = /* @__PURE__ */ new Map();
49
+ const stack = [];
50
+ const ordered = ranges.filter((r) => r.block && (BLOCK_OPENER_KINDS.has(r.kind) || BLOCK_CLOSER_KINDS.has(r.kind))).slice().sort((a, b) => a.from - b.from);
51
+ for (const range of ordered) {
52
+ if (BLOCK_OPENER_KINDS.has(range.kind)) {
53
+ depths.set(range.from, stack.length);
54
+ stack.push(range.kind);
55
+ continue;
56
+ }
57
+ const wantOpener = range.kind === "endif" ? "if" : "each";
58
+ const matchIdx = stack.lastIndexOf(wantOpener);
59
+ if (matchIdx !== -1) stack.length = matchIdx;
60
+ }
61
+ return depths;
62
+ };
24
63
  const scanDirectives = (doc) => {
25
64
  const ranges = [];
26
65
  for (const chunks of collectBlockChunks(doc)) {
@@ -64,4 +103,4 @@ const createTemplateDirectivesPlugin = ({ onRangesChange } = {}) => createDocSca
64
103
  });
65
104
  const getTemplateDirectives = (state) => getDocScanRanges(templateDirectivesKey, state);
66
105
  //#endregion
67
- export { createTemplateDirectivesPlugin, getTemplateDirectives, scanDirectives, templateDirectivesKey };
106
+ export { computeBlockDepths, createTemplateDirectivesPlugin, getTemplateDirectives, scanDirectives, templateDirectivesKey };
@@ -0,0 +1,46 @@
1
+ import { Node } from "prosemirror-model";
2
+ import { EditorView } from "prosemirror-view";
3
+
4
+ //#region src/prosemirror/queries.d.ts
5
+ /** A resolved PM position range — half-open `[from, to)` in PM coordinates. */
6
+ type PmRange = {
7
+ from: number;
8
+ to: number;
9
+ };
10
+ /**
11
+ * Clamp a caller-supplied `[from, to]` range to a valid in-document span, or
12
+ * return `null` when it cannot be made valid: non-integer, negative, reversed
13
+ * (`to < from`), or a `from` past the document end. `to` is clamped to the
14
+ * document size so an out-of-range end never makes `doc.resolve()` throw a
15
+ * `RangeError`. Both adapters' `highlightRange` route raw caller positions
16
+ * through this so the no-op contract holds identically.
17
+ */
18
+ declare function clampRangeToDoc(doc: Node, from: number, to: number): PmRange | null;
19
+ /**
20
+ * Resolve a `commentId` to the PM position range its `comment` mark
21
+ * spans. Walks every inline node carrying a `comment` mark with the
22
+ * matching id and returns the union range (earliest start → latest end),
23
+ * so a comment whose range is interrupted by un-marked inline atoms still
24
+ * resolves to a single span. Returns `null` when the id is no longer
25
+ * present (the comment was deleted, or the marked text was removed) — the
26
+ * caller distinguishes "scrolled" from "stale" on that signal.
27
+ *
28
+ * Pure read over `view.state`; no dispatch.
29
+ */
30
+ declare function findCommentRange(view: EditorView | null, commentId: number): PmRange | null;
31
+ /**
32
+ * Resolve a tracked-change `revisionId` to the PM position range of its
33
+ * first site. Delegates to {@link extractTrackedChanges} so a coalesced
34
+ * revision (sites scattered across paragraphs, replace pairs, Enter
35
+ * chains) resolves to the same entry the sidebar shows. Matches on the
36
+ * entry's primary `revisionId`, its `insertionRevisionId`, or any
37
+ * `coalescedRevisionIds` member. Returns `null` when no entry carries the
38
+ * id (the change was accepted/rejected/deleted) — the caller uses this to
39
+ * show a "location no longer exists" affordance instead of a silent
40
+ * no-op.
41
+ *
42
+ * Pure read over `view.state`; no dispatch.
43
+ */
44
+ declare function findChangeRange(view: EditorView | null, revisionId: number): PmRange | null;
45
+ //#endregion
46
+ export { PmRange, clampRangeToDoc, findChangeRange, findCommentRange };
@@ -0,0 +1,74 @@
1
+ import { extractTrackedChanges } from "./utils/extractTrackedChanges.js";
2
+ //#region src/prosemirror/queries.ts
3
+ /**
4
+ * Clamp a caller-supplied `[from, to]` range to a valid in-document span, or
5
+ * return `null` when it cannot be made valid: non-integer, negative, reversed
6
+ * (`to < from`), or a `from` past the document end. `to` is clamped to the
7
+ * document size so an out-of-range end never makes `doc.resolve()` throw a
8
+ * `RangeError`. Both adapters' `highlightRange` route raw caller positions
9
+ * through this so the no-op contract holds identically.
10
+ */
11
+ function clampRangeToDoc(doc, from, to) {
12
+ if (!Number.isInteger(from) || !Number.isInteger(to) || from < 0 || to < from) return null;
13
+ const max = doc.content.size;
14
+ if (from > max) return null;
15
+ return {
16
+ from,
17
+ to: Math.min(to, max)
18
+ };
19
+ }
20
+ /**
21
+ * Resolve a `commentId` to the PM position range its `comment` mark
22
+ * spans. Walks every inline node carrying a `comment` mark with the
23
+ * matching id and returns the union range (earliest start → latest end),
24
+ * so a comment whose range is interrupted by un-marked inline atoms still
25
+ * resolves to a single span. Returns `null` when the id is no longer
26
+ * present (the comment was deleted, or the marked text was removed) — the
27
+ * caller distinguishes "scrolled" from "stale" on that signal.
28
+ *
29
+ * Pure read over `view.state`; no dispatch.
30
+ */
31
+ function findCommentRange(view, commentId) {
32
+ if (!view) return null;
33
+ const commentType = view.state.schema.marks["comment"];
34
+ if (!commentType) return null;
35
+ let from = Infinity;
36
+ let to = -Infinity;
37
+ view.state.doc.descendants((node, pos) => {
38
+ if (!node.isInline) return;
39
+ for (const mark of node.marks) if (mark.type === commentType && mark.attrs["commentId"] === commentId) {
40
+ from = Math.min(from, pos);
41
+ to = Math.max(to, pos + node.nodeSize);
42
+ }
43
+ });
44
+ if (to < 0) return null;
45
+ return {
46
+ from,
47
+ to
48
+ };
49
+ }
50
+ /**
51
+ * Resolve a tracked-change `revisionId` to the PM position range of its
52
+ * first site. Delegates to {@link extractTrackedChanges} so a coalesced
53
+ * revision (sites scattered across paragraphs, replace pairs, Enter
54
+ * chains) resolves to the same entry the sidebar shows. Matches on the
55
+ * entry's primary `revisionId`, its `insertionRevisionId`, or any
56
+ * `coalescedRevisionIds` member. Returns `null` when no entry carries the
57
+ * id (the change was accepted/rejected/deleted) — the caller uses this to
58
+ * show a "location no longer exists" affordance instead of a silent
59
+ * no-op.
60
+ *
61
+ * Pure read over `view.state`; no dispatch.
62
+ */
63
+ function findChangeRange(view, revisionId) {
64
+ if (!view) return null;
65
+ const { entries } = extractTrackedChanges(view.state);
66
+ const entry = entries.find((e) => e.revisionId === revisionId || e.insertionRevisionId === revisionId || e.coalescedRevisionIds?.includes(revisionId));
67
+ if (!entry) return null;
68
+ return {
69
+ from: entry.from,
70
+ to: entry.to
71
+ };
72
+ }
73
+ //#endregion
74
+ export { clampRangeToDoc, findChangeRange, findCommentRange };
@@ -8,6 +8,22 @@ type ResolvedParagraphStyle = {
8
8
  /** Paragraph formatting (alignment, spacing, indentation, etc.) */paragraphFormatting?: document_d_exports.ParagraphFormatting; /** Default run formatting from the style */
9
9
  runFormatting?: document_d_exports.TextFormatting;
10
10
  };
11
+ /**
12
+ * Paragraph-spacing fields sourced from the table style enclosing a cell
13
+ * paragraph: the table style's own `w:pPr`, then the applicable
14
+ * `w:tblStylePr` conditional region's `w:pPr` layered on top (already merged
15
+ * by the caller — see `resolveTableBaseStyle`/`resolveTableStyleConditional`
16
+ * in toProseDoc.ts).
17
+ *
18
+ * Deliberately narrower than the full {@link ParagraphFormatting}: this is
19
+ * the layer-2 overlay described on {@link StyleResolver.resolveParagraphStyleInTable}
20
+ * and is scoped to the spacing fields responsible for folio's table-row
21
+ * height bug (table styles like Word's `TableGrid` zero these out relative
22
+ * to `docDefaults`). Alignment, indentation, borders, etc. are out of scope
23
+ * here — applying the whole `pPr` would risk regressing unrelated cell
24
+ * layout that isn't part of this fix.
25
+ */
26
+ type TableCellParagraphSpacingOverlay = Pick<document_d_exports.ParagraphFormatting, "spaceBefore" | "spaceAfter" | "lineSpacing" | "lineSpacingRule" | "contextualSpacing">;
11
27
  /**
12
28
  * StyleResolver provides efficient access to resolved style properties
13
29
  */
@@ -33,6 +49,32 @@ declare class StyleResolver {
33
49
  * @returns Resolved paragraph and run formatting
34
50
  */
35
51
  resolveParagraphStyle(styleId: string | undefined | null): ResolvedParagraphStyle;
52
+ /**
53
+ * Resolve paragraph style properties for a paragraph inside a table cell,
54
+ * layering the enclosing table style's paragraph-spacing fields into the
55
+ * cascade between docDefaults and the paragraph's own style chain.
56
+ *
57
+ * Per ECMA-376 §17.7.2, a cell paragraph's properties resolve in this
58
+ * order (later wins):
59
+ * 1. docDefaults (`w:pPrDefault`)
60
+ * 2. the enclosing table style's `w:pPr` (`tableParagraphOverlay`, here)
61
+ * 3. the paragraph's own style chain (`w:pStyle` + `basedOn` ancestors)
62
+ * 4. direct formatting on the paragraph (`w:pPr`)
63
+ *
64
+ * Layer 4 is applied by the caller (`paragraphFormattingToAttrs`), which
65
+ * already prefers the paragraph's direct formatting over this method's
66
+ * result. Layer 3 is applied below, after the overlay, so an explicit
67
+ * paragraph style still wins over the table for any field it sets.
68
+ *
69
+ * @param styleId - The paragraph's style ID (e.g., 'Heading1', 'Normal')
70
+ * @param tableParagraphOverlay - Paragraph-spacing fields from the
71
+ * enclosing table style (base `pPr` merged with the applicable
72
+ * conditional region), or `undefined` when the paragraph isn't in a
73
+ * table cell, the table has no style, or the style sets no spacing.
74
+ * @returns Resolved paragraph and run formatting
75
+ */
76
+ resolveParagraphStyleInTable(styleId: string | undefined | null, tableParagraphOverlay: TableCellParagraphSpacingOverlay | undefined): ResolvedParagraphStyle;
77
+ private resolveParagraphStyleCascade;
36
78
  /**
37
79
  * Resolve the style applied to the paragraph that follows one styled with
38
80
  * `styleId` when the user presses Enter (OOXML `w:next`, §17.7.4.10).
@@ -91,4 +133,4 @@ declare class StyleResolver {
91
133
  */
92
134
  declare function createStyleResolver(styleDefinitions: document_d_exports.StyleDefinitions | undefined): StyleResolver;
93
135
  //#endregion
94
- export { ResolvedParagraphStyle, StyleResolver, createStyleResolver };
136
+ export { ResolvedParagraphStyle, StyleResolver, TableCellParagraphSpacingOverlay, createStyleResolver };
@@ -70,9 +70,43 @@ var StyleResolver = class {
70
70
  * @returns Resolved paragraph and run formatting
71
71
  */
72
72
  resolveParagraphStyle(styleId) {
73
+ return this.resolveParagraphStyleCascade(styleId, void 0);
74
+ }
75
+ /**
76
+ * Resolve paragraph style properties for a paragraph inside a table cell,
77
+ * layering the enclosing table style's paragraph-spacing fields into the
78
+ * cascade between docDefaults and the paragraph's own style chain.
79
+ *
80
+ * Per ECMA-376 §17.7.2, a cell paragraph's properties resolve in this
81
+ * order (later wins):
82
+ * 1. docDefaults (`w:pPrDefault`)
83
+ * 2. the enclosing table style's `w:pPr` (`tableParagraphOverlay`, here)
84
+ * 3. the paragraph's own style chain (`w:pStyle` + `basedOn` ancestors)
85
+ * 4. direct formatting on the paragraph (`w:pPr`)
86
+ *
87
+ * Layer 4 is applied by the caller (`paragraphFormattingToAttrs`), which
88
+ * already prefers the paragraph's direct formatting over this method's
89
+ * result. Layer 3 is applied below, after the overlay, so an explicit
90
+ * paragraph style still wins over the table for any field it sets.
91
+ *
92
+ * @param styleId - The paragraph's style ID (e.g., 'Heading1', 'Normal')
93
+ * @param tableParagraphOverlay - Paragraph-spacing fields from the
94
+ * enclosing table style (base `pPr` merged with the applicable
95
+ * conditional region), or `undefined` when the paragraph isn't in a
96
+ * table cell, the table has no style, or the style sets no spacing.
97
+ * @returns Resolved paragraph and run formatting
98
+ */
99
+ resolveParagraphStyleInTable(styleId, tableParagraphOverlay) {
100
+ return this.resolveParagraphStyleCascade(styleId, tableParagraphOverlay);
101
+ }
102
+ resolveParagraphStyleCascade(styleId, tableParagraphOverlay) {
73
103
  const result = {};
74
104
  if (this.docDefaults?.pPr) result.paragraphFormatting = { ...this.docDefaults.pPr };
75
105
  if (this.docDefaults?.rPr) result.runFormatting = { ...this.docDefaults.rPr };
106
+ if (tableParagraphOverlay) {
107
+ const merged = mergeParagraphFormatting(result.paragraphFormatting, tableParagraphOverlay);
108
+ if (merged !== void 0) result.paragraphFormatting = merged;
109
+ }
76
110
  if (!styleId) {
77
111
  if (this.defaultParagraphStyle) this.mergeStyleIntoResult(result, this.defaultParagraphStyle);
78
112
  return result;
@@ -0,0 +1,49 @@
1
+ import { EditorView } from "prosemirror-view";
2
+
3
+ //#region src/prosemirror/tableResize.d.ts
4
+ /** 1px ≈ 15 twips at 96dpi (20 twips/pt × 72pt/in ÷ 96px/in). */
5
+ declare const TWIPS_PER_PIXEL = 15;
6
+ /** Minimum column width (~0.2"). */
7
+ declare const MIN_CELL_WIDTH_TWIPS = 300;
8
+ /** Minimum row height (~0.14"). */
9
+ declare const MIN_ROW_HEIGHT_TWIPS = 200;
10
+ /** Read the [left, right] column widths at `colIndex` in the table starting at `pmStart`. */
11
+ declare function readColumnWidths(view: EditorView, pmStart: number, colIndex: number): {
12
+ left: number;
13
+ right: number;
14
+ } | null;
15
+ /**
16
+ * Read the row height (in twips) for `rowIndex` in the table starting at
17
+ * `pmStart`. Returns null if the row has no explicit height — the caller
18
+ * can fall back to measuring the rendered DOM cell.
19
+ */
20
+ declare function readRowHeight(view: EditorView, pmStart: number, rowIndex: number): number | null;
21
+ /** Read the last-column width (the one being resized from the table's right edge). */
22
+ declare function readColumnWidthAt(view: EditorView, pmStart: number, colIndex: number): number | null;
23
+ /**
24
+ * Bake a column-resize into the PM doc: update the table's `columnWidths`
25
+ * attr and every cell at `colIdx` / `colIdx + 1` in every row.
26
+ */
27
+ declare function commitColumnResize(view: EditorView, opts: {
28
+ pmStart: number;
29
+ colIdx: number;
30
+ newLeft: number;
31
+ newRight: number;
32
+ }): void;
33
+ /** Bake a row-resize into the PM doc: update the target row's `height` + `heightRule`. */
34
+ declare function commitRowResize(view: EditorView, opts: {
35
+ pmStart: number;
36
+ rowIdx: number;
37
+ newHeight: number;
38
+ }): void;
39
+ /**
40
+ * Bake a right-edge resize into the PM doc: grow only the last column.
41
+ * Updates `columnWidths[colIdx]` and the cell at `colIdx` in every row.
42
+ */
43
+ declare function commitRightEdgeResize(view: EditorView, opts: {
44
+ pmStart: number;
45
+ colIdx: number;
46
+ newWidth: number;
47
+ }): void;
48
+ //#endregion
49
+ export { MIN_CELL_WIDTH_TWIPS, MIN_ROW_HEIGHT_TWIPS, TWIPS_PER_PIXEL, commitColumnResize, commitRightEdgeResize, commitRowResize, readColumnWidthAt, readColumnWidths, readRowHeight };