@stll/folio-core 0.1.3 → 0.3.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.
- package/dist/ai-edits/clean-text.d.ts +21 -1
- package/dist/ai-edits/clean-text.js +69 -1
- package/dist/ai-edits/headless.d.ts +49 -22
- package/dist/ai-edits/headless.js +101 -77
- package/dist/ai-edits/index.d.ts +3 -1
- package/dist/ai-edits/index.js +3 -1
- package/dist/ai-edits/read.d.ts +43 -0
- package/dist/ai-edits/read.js +91 -0
- package/dist/docx/index.d.ts +2 -0
- package/dist/docx/index.js +2 -0
- package/dist/docx/parser.js +1 -1
- package/dist/docx/wrapTypes.d.ts +8 -1
- package/dist/docx/wrapTypes.js +0 -8
- package/dist/i18n/messages/catalogs.gen.d.ts +9899 -0
- package/dist/i18n/messages/catalogs.gen.js +9831 -0
- package/dist/i18n/messages/messages.gen.d.ts +585 -0
- package/dist/i18n/messages/messages.gen.js +0 -0
- package/dist/i18n/messages.d.ts +13 -0
- package/dist/i18n/messages.js +39 -0
- package/dist/layout-bridge/cellSelectionHighlight.d.ts +18 -0
- package/dist/layout-bridge/cellSelectionHighlight.js +36 -0
- package/dist/layout-bridge/headerFooterLayout.d.ts +50 -0
- package/dist/layout-bridge/headerFooterLayout.js +216 -0
- package/dist/layout-bridge/measuring/measureBlocksPipeline.d.ts +33 -0
- package/dist/layout-bridge/measuring/measureBlocksPipeline.js +0 -0
- package/dist/layout-bridge/sectionColumns.d.ts +11 -0
- package/dist/layout-bridge/sectionColumns.js +21 -0
- package/dist/layout-bridge/tableInsertHover.d.ts +42 -0
- package/dist/layout-bridge/tableInsertHover.js +102 -0
- package/dist/layout-engine/index.d.ts +2 -1
- package/dist/layout-engine/index.js +2 -1
- package/dist/layout-engine/measure/measureParagraph.js +40 -5
- package/dist/layout-engine/pmPageIndex.d.ts +24 -0
- package/dist/layout-engine/pmPageIndex.js +37 -0
- package/dist/layout-painter/imageLayout.d.ts +101 -0
- package/dist/layout-painter/imageLayout.js +161 -0
- package/dist/layout-painter/renderPage.d.ts +16 -1
- package/dist/layout-painter/renderPage.js +28 -1
- package/dist/layout-painter/renderParagraph.js +1 -1
- package/dist/layout-painter/sdtBoundary.d.ts +24 -1
- package/dist/layout-painter/sdtBoundary.js +40 -1
- package/dist/managers/AutoSaveManager.d.ts +89 -0
- package/dist/managers/AutoSaveManager.js +279 -0
- package/dist/managers/ClipboardManager.d.ts +31 -0
- package/dist/managers/ClipboardManager.js +147 -0
- package/dist/paged-layout/rangeProjection.d.ts +23 -1
- package/dist/paged-layout/rangeProjection.js +34 -1
- package/dist/prosemirror/cellDragSelection.d.ts +34 -0
- package/dist/prosemirror/cellDragSelection.js +69 -0
- package/dist/prosemirror/commands/sectionBreak.d.ts +15 -0
- package/dist/prosemirror/commands/sectionBreak.js +54 -0
- package/dist/prosemirror/commentIdAllocator.d.ts +32 -0
- package/dist/prosemirror/commentIdAllocator.js +35 -0
- package/dist/prosemirror/commentOps.d.ts +35 -0
- package/dist/prosemirror/commentOps.js +104 -0
- package/dist/prosemirror/conversion/toProseDoc.js +29 -6
- package/dist/prosemirror/extensions/index.d.ts +3 -0
- package/dist/prosemirror/extensions/index.js +3 -0
- package/dist/prosemirror/imageCommit.d.ts +42 -0
- package/dist/prosemirror/imageCommit.js +118 -0
- package/dist/prosemirror/paraText.d.ts +26 -0
- package/dist/prosemirror/paraText.js +73 -0
- package/dist/prosemirror/plugins/templateDirectives.d.ts +21 -1
- package/dist/prosemirror/plugins/templateDirectives.js +40 -1
- package/dist/prosemirror/queries.d.ts +46 -0
- package/dist/prosemirror/queries.js +74 -0
- package/dist/prosemirror/styles/styleResolver.d.ts +43 -1
- package/dist/prosemirror/styles/styleResolver.js +34 -0
- package/dist/prosemirror/tableResize.d.ts +49 -0
- package/dist/prosemirror/tableResize.js +162 -0
- package/dist/prosemirror/utils/extractTrackedChanges.d.ts +106 -0
- package/dist/prosemirror/utils/extractTrackedChanges.js +379 -0
- package/dist/prosemirror/utils/visualLineNavigation.d.ts +27 -0
- package/dist/prosemirror/utils/visualLineNavigation.js +217 -0
- package/dist/server.d.ts +2 -1
- package/dist/style-engine/index.d.ts +2 -2
- package/dist/style-engine/styleEngine.d.ts +14 -1
- package/dist/style-engine/styleEngine.js +15 -0
- package/dist/utils/colorResolver.d.ts +8 -1
- package/dist/utils/colorResolver.js +12 -1
- package/dist/utils/findVerticalScrollParent.d.ts +20 -0
- package/dist/utils/findVerticalScrollParent.js +30 -0
- package/dist/utils/fontResolver.d.ts +22 -3
- package/dist/utils/fontResolver.js +216 -31
- package/package.json +10 -1
|
@@ -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 };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
//#region src/prosemirror/tableResize.ts
|
|
2
|
+
/** 1px ≈ 15 twips at 96dpi (20 twips/pt × 72pt/in ÷ 96px/in). */
|
|
3
|
+
const TWIPS_PER_PIXEL = 15;
|
|
4
|
+
/** Minimum column width (~0.2"). */
|
|
5
|
+
const MIN_CELL_WIDTH_TWIPS = 300;
|
|
6
|
+
/** Minimum row height (~0.14"). */
|
|
7
|
+
const MIN_ROW_HEIGHT_TWIPS = 200;
|
|
8
|
+
/** Walk up from `pmStart + 1` to find the enclosing table node. */
|
|
9
|
+
function findTableAt(view, pmStart) {
|
|
10
|
+
try {
|
|
11
|
+
const $pos = view.state.doc.resolve(pmStart + 1);
|
|
12
|
+
for (let d = $pos.depth; d >= 0; d--) {
|
|
13
|
+
const node = $pos.node(d);
|
|
14
|
+
if (node.type.name === "table") return {
|
|
15
|
+
node,
|
|
16
|
+
tablePos: $pos.before(d)
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
} catch {}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
/** Read the [left, right] column widths at `colIndex` in the table starting at `pmStart`. */
|
|
23
|
+
function readColumnWidths(view, pmStart, colIndex) {
|
|
24
|
+
const found = findTableAt(view, pmStart);
|
|
25
|
+
if (!found) return null;
|
|
26
|
+
const widths = found.node.attrs["columnWidths"];
|
|
27
|
+
if (!widths) return null;
|
|
28
|
+
const left = widths[colIndex];
|
|
29
|
+
const right = widths[colIndex + 1];
|
|
30
|
+
if (left === void 0 || right === void 0) return null;
|
|
31
|
+
return {
|
|
32
|
+
left,
|
|
33
|
+
right
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Read the row height (in twips) for `rowIndex` in the table starting at
|
|
38
|
+
* `pmStart`. Returns null if the row has no explicit height — the caller
|
|
39
|
+
* can fall back to measuring the rendered DOM cell.
|
|
40
|
+
*/
|
|
41
|
+
function readRowHeight(view, pmStart, rowIndex) {
|
|
42
|
+
const found = findTableAt(view, pmStart);
|
|
43
|
+
if (!found) return null;
|
|
44
|
+
let rowHeight = null;
|
|
45
|
+
let idx = 0;
|
|
46
|
+
found.node.forEach((child) => {
|
|
47
|
+
if (idx === rowIndex) {
|
|
48
|
+
const h = child.attrs["height"];
|
|
49
|
+
if (h) rowHeight = h;
|
|
50
|
+
}
|
|
51
|
+
idx++;
|
|
52
|
+
});
|
|
53
|
+
return rowHeight;
|
|
54
|
+
}
|
|
55
|
+
/** Read the last-column width (the one being resized from the table's right edge). */
|
|
56
|
+
function readColumnWidthAt(view, pmStart, colIndex) {
|
|
57
|
+
const found = findTableAt(view, pmStart);
|
|
58
|
+
if (!found) return null;
|
|
59
|
+
const widths = found.node.attrs["columnWidths"];
|
|
60
|
+
if (!widths) return null;
|
|
61
|
+
const width = widths[colIndex];
|
|
62
|
+
if (width === void 0) return null;
|
|
63
|
+
return width;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Bake a column-resize into the PM doc: update the table's `columnWidths`
|
|
67
|
+
* attr and every cell at `colIdx` / `colIdx + 1` in every row.
|
|
68
|
+
*/
|
|
69
|
+
function commitColumnResize(view, opts) {
|
|
70
|
+
const { pmStart, colIdx, newLeft, newRight } = opts;
|
|
71
|
+
const found = findTableAt(view, pmStart);
|
|
72
|
+
if (!found) return;
|
|
73
|
+
const { node, tablePos } = found;
|
|
74
|
+
const tr = view.state.tr;
|
|
75
|
+
const widths = [...node.attrs["columnWidths"] ?? []];
|
|
76
|
+
widths[colIdx] = newLeft;
|
|
77
|
+
widths[colIdx + 1] = newRight;
|
|
78
|
+
tr.setNodeMarkup(tablePos, void 0, {
|
|
79
|
+
...node.attrs,
|
|
80
|
+
columnWidths: widths,
|
|
81
|
+
tableLayout: "fixed"
|
|
82
|
+
});
|
|
83
|
+
let rowOffset = tablePos + 1;
|
|
84
|
+
node.forEach((row) => {
|
|
85
|
+
let cellOffset = rowOffset + 1;
|
|
86
|
+
let cellColIdx = 0;
|
|
87
|
+
row.forEach((cell) => {
|
|
88
|
+
const colspan = cell.attrs["colspan"] || 1;
|
|
89
|
+
if (cellColIdx === colIdx || cellColIdx === colIdx + 1) {
|
|
90
|
+
const newWidth = cellColIdx === colIdx ? newLeft : newRight;
|
|
91
|
+
tr.setNodeMarkup(tr.mapping.map(cellOffset), void 0, {
|
|
92
|
+
...cell.attrs,
|
|
93
|
+
width: newWidth,
|
|
94
|
+
widthType: "dxa",
|
|
95
|
+
colwidth: null
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
cellOffset += cell.nodeSize;
|
|
99
|
+
cellColIdx += colspan;
|
|
100
|
+
});
|
|
101
|
+
rowOffset += row.nodeSize;
|
|
102
|
+
});
|
|
103
|
+
view.dispatch(tr);
|
|
104
|
+
}
|
|
105
|
+
/** Bake a row-resize into the PM doc: update the target row's `height` + `heightRule`. */
|
|
106
|
+
function commitRowResize(view, opts) {
|
|
107
|
+
const { pmStart, rowIdx, newHeight } = opts;
|
|
108
|
+
const found = findTableAt(view, pmStart);
|
|
109
|
+
if (!found) return;
|
|
110
|
+
const { node, tablePos } = found;
|
|
111
|
+
const tr = view.state.tr;
|
|
112
|
+
let rowOffset = tablePos + 1;
|
|
113
|
+
let idx = 0;
|
|
114
|
+
node.forEach((row) => {
|
|
115
|
+
if (idx === rowIdx) tr.setNodeMarkup(tr.mapping.map(rowOffset), void 0, {
|
|
116
|
+
...row.attrs,
|
|
117
|
+
height: newHeight,
|
|
118
|
+
heightRule: "atLeast"
|
|
119
|
+
});
|
|
120
|
+
rowOffset += row.nodeSize;
|
|
121
|
+
idx++;
|
|
122
|
+
});
|
|
123
|
+
view.dispatch(tr);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Bake a right-edge resize into the PM doc: grow only the last column.
|
|
127
|
+
* Updates `columnWidths[colIdx]` and the cell at `colIdx` in every row.
|
|
128
|
+
*/
|
|
129
|
+
function commitRightEdgeResize(view, opts) {
|
|
130
|
+
const { pmStart, colIdx, newWidth } = opts;
|
|
131
|
+
const found = findTableAt(view, pmStart);
|
|
132
|
+
if (!found) return;
|
|
133
|
+
const { node, tablePos } = found;
|
|
134
|
+
const tr = view.state.tr;
|
|
135
|
+
const widths = [...node.attrs["columnWidths"] ?? []];
|
|
136
|
+
widths[colIdx] = newWidth;
|
|
137
|
+
tr.setNodeMarkup(tablePos, void 0, {
|
|
138
|
+
...node.attrs,
|
|
139
|
+
columnWidths: widths,
|
|
140
|
+
tableLayout: "fixed"
|
|
141
|
+
});
|
|
142
|
+
let rowOffset = tablePos + 1;
|
|
143
|
+
node.forEach((row) => {
|
|
144
|
+
let cellOffset = rowOffset + 1;
|
|
145
|
+
let cellColIdx = 0;
|
|
146
|
+
row.forEach((cell) => {
|
|
147
|
+
const colspan = cell.attrs["colspan"] || 1;
|
|
148
|
+
if (cellColIdx === colIdx) tr.setNodeMarkup(tr.mapping.map(cellOffset), void 0, {
|
|
149
|
+
...cell.attrs,
|
|
150
|
+
width: newWidth,
|
|
151
|
+
widthType: "dxa",
|
|
152
|
+
colwidth: null
|
|
153
|
+
});
|
|
154
|
+
cellOffset += cell.nodeSize;
|
|
155
|
+
cellColIdx += colspan;
|
|
156
|
+
});
|
|
157
|
+
rowOffset += row.nodeSize;
|
|
158
|
+
});
|
|
159
|
+
view.dispatch(tr);
|
|
160
|
+
}
|
|
161
|
+
//#endregion
|
|
162
|
+
export { MIN_CELL_WIDTH_TWIPS, MIN_ROW_HEIGHT_TWIPS, TWIPS_PER_PIXEL, commitColumnResize, commitRightEdgeResize, commitRowResize, readColumnWidthAt, readColumnWidths, readRowHeight };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { EditorState } from "prosemirror-state";
|
|
2
|
+
|
|
3
|
+
//#region src/prosemirror/utils/extractTrackedChanges.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* One tracked change surfaced by {@link extractTrackedChanges}. Each entry
|
|
6
|
+
* groups all sites of one revision into a single row that the sidebar
|
|
7
|
+
* renders as one card.
|
|
8
|
+
*
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
type TrackedChangeEntry = {
|
|
12
|
+
/**
|
|
13
|
+
* Revision shape. Inline shapes (`insertion`, `deletion`, `replacement`)
|
|
14
|
+
* wrap text runs; the rest are structural revisions on node attrs.
|
|
15
|
+
*
|
|
16
|
+
* - `insertion` — text was added (`<w:ins>`).
|
|
17
|
+
* - `deletion` — text was struck through but not removed (`<w:del>`).
|
|
18
|
+
* - `replacement` — a deletion + insertion by the same author at the
|
|
19
|
+
* same position+time; sidebar shows one combined card. `deletedText`
|
|
20
|
+
* and `insertionRevisionId` are set on this variant.
|
|
21
|
+
* - `paragraphMarkInsertion` / `paragraphMarkDeletion` — Enter /
|
|
22
|
+
* Backspace produced a tracked paragraph break (`<w:pPr><w:rPr><w:ins/>` /
|
|
23
|
+
* `<w:del/>`).
|
|
24
|
+
* - `paragraphPropertiesChanged` — formatting (alignment, spacing,
|
|
25
|
+
* etc.) on the paragraph was changed (`<w:pPrChange>`).
|
|
26
|
+
* - `rowInserted` / `rowDeleted` / `rowPropertiesChanged` — table
|
|
27
|
+
* row authored / removed / formatted (`<w:trPr><w:ins/>` / `<w:del/>`
|
|
28
|
+
* / `<w:trPrChange>`).
|
|
29
|
+
* - `cellInserted` / `cellDeleted` / `cellMerged` /
|
|
30
|
+
* `cellPropertiesChanged` — per-cell revisions
|
|
31
|
+
* (`<w:cellIns>` / `<w:cellDel>` / `<w:cellMerge>` / `<w:tcPrChange>`).
|
|
32
|
+
* - `tablePropertiesChanged` — table-level formatting
|
|
33
|
+
* (`<w:tblPrChange>`).
|
|
34
|
+
*/
|
|
35
|
+
type: "insertion" | "deletion" | "replacement" | "paragraphMarkInsertion" | "paragraphMarkDeletion" | "paragraphPropertiesChanged" | "rowInserted" | "rowDeleted" | "rowPropertiesChanged" | "cellInserted" | "cellDeleted" | "cellMerged" | "cellPropertiesChanged" | "tableInserted" | "tableDeleted" | "tablePropertiesChanged";
|
|
36
|
+
/**
|
|
37
|
+
* Affected text. For inline types this is the run's text; for
|
|
38
|
+
* structural types it's the surrounding paragraph / cell content
|
|
39
|
+
* (truncated by the sidebar before display).
|
|
40
|
+
*/
|
|
41
|
+
text: string;
|
|
42
|
+
/**
|
|
43
|
+
* Only set when `type === 'replacement'` — the text the user removed.
|
|
44
|
+
* The inserted text lives in {@link TrackedChangeEntry.text}.
|
|
45
|
+
*/
|
|
46
|
+
deletedText?: string | undefined; /** Author that minted the revision (`w:author`). */
|
|
47
|
+
author: string; /** ISO timestamp the revision was minted (`w:date`). May be undefined for legacy imports. */
|
|
48
|
+
date?: string | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Document position where the revision starts. For inline types this
|
|
51
|
+
* is the start of the marked text run; for structural types it's the
|
|
52
|
+
* containing paragraph / row / cell / table node's start position.
|
|
53
|
+
*/
|
|
54
|
+
from: number;
|
|
55
|
+
/**
|
|
56
|
+
* Document position where the revision ends. For inline coalesced
|
|
57
|
+
* runs that span multiple paragraphs, this is the END position of the
|
|
58
|
+
* LAST run in the group; the intervening structural positions are not
|
|
59
|
+
* preserved.
|
|
60
|
+
*/
|
|
61
|
+
to: number; /** The `w:id` of the revision. */
|
|
62
|
+
revisionId: number;
|
|
63
|
+
/**
|
|
64
|
+
* Only set when `type === 'replacement'` — the insertion half carries
|
|
65
|
+
* a DIFFERENT `w:id` from the deletion (sharing would trip the OOXML
|
|
66
|
+
* move-pair serializer).
|
|
67
|
+
*/
|
|
68
|
+
insertionRevisionId?: number | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* Extra `w:id`s that map to the same logical revision as this card.
|
|
71
|
+
* Populated when the extractor coalesces a burst of distinct ids by
|
|
72
|
+
* (author, date) — e.g. a foreign document where the source editor
|
|
73
|
+
* minted a fresh id per atomic edit.
|
|
74
|
+
*/
|
|
75
|
+
coalescedRevisionIds?: number[] | undefined;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Output of {@link extractTrackedChanges}.
|
|
79
|
+
*
|
|
80
|
+
* @public
|
|
81
|
+
*/
|
|
82
|
+
type TrackedChangesResult = {
|
|
83
|
+
/** Tracked-change entries, sorted by document position, with adjacent same-revision entries merged. */entries: TrackedChangeEntry[];
|
|
84
|
+
/**
|
|
85
|
+
* Map of `commentId -> revisionId` for comments whose range overlaps a tracked-change mark.
|
|
86
|
+
* Consumers (DocxEditor's threading effect) use this to thread comments under their tracked change.
|
|
87
|
+
*/
|
|
88
|
+
commentToRevision: Map<number, number>;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Walk the PM doc and extract every tracked change as a flat list of
|
|
92
|
+
* `TrackedChangeEntry` plus a comment→revision overlap map. Adjacent
|
|
93
|
+
* inline marks coalesce by `(type, revisionId, author, date)`; a
|
|
94
|
+
* deletion immediately followed by an insertion (same author + same
|
|
95
|
+
* date) collapses into a single `replacement` entry; paragraph-mark
|
|
96
|
+
* cards (`paragraphMarkInsertion` / `paragraphMarkDeletion`) are
|
|
97
|
+
* hidden when an inline entry already covers their revision triple
|
|
98
|
+
* (one Accept clears every site of one conceptual change).
|
|
99
|
+
*
|
|
100
|
+
* Pure and deterministic. Returns `EMPTY_RESULT` on null state.
|
|
101
|
+
*
|
|
102
|
+
* @public
|
|
103
|
+
*/
|
|
104
|
+
declare function extractTrackedChanges(state: EditorState | null): TrackedChangesResult;
|
|
105
|
+
//#endregion
|
|
106
|
+
export { TrackedChangeEntry, TrackedChangesResult, extractTrackedChanges };
|