@beyondwork/docx-react-component 1.0.18 → 1.0.20
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/README.md +8 -2
- package/package.json +24 -34
- package/src/api/README.md +5 -1
- package/src/api/public-types.ts +710 -4
- package/src/api/session-state.ts +60 -0
- package/src/core/commands/formatting-commands.ts +2 -1
- package/src/core/commands/image-commands.ts +147 -0
- package/src/core/commands/index.ts +19 -3
- package/src/core/commands/list-commands.ts +231 -36
- package/src/core/commands/paragraph-layout-commands.ts +339 -0
- package/src/core/commands/section-layout-commands.ts +680 -0
- package/src/core/commands/style-commands.ts +262 -0
- package/src/core/search/search-text.ts +357 -0
- package/src/core/selection/mapping.ts +41 -0
- package/src/core/state/editor-state.ts +4 -1
- package/src/index.ts +51 -0
- package/src/io/docx-session.ts +623 -56
- package/src/io/export/serialize-comments.ts +104 -34
- package/src/io/export/serialize-footnotes.ts +198 -1
- package/src/io/export/serialize-headers-footers.ts +203 -10
- package/src/io/export/serialize-main-document.ts +285 -8
- package/src/io/export/serialize-numbering.ts +28 -7
- package/src/io/export/split-review-boundaries.ts +181 -19
- package/src/io/normalize/normalize-text.ts +144 -32
- package/src/io/ooxml/highlight-colors.ts +39 -0
- package/src/io/ooxml/numbering-sentinels.ts +44 -0
- package/src/io/ooxml/parse-comments.ts +85 -19
- package/src/io/ooxml/parse-fields.ts +396 -0
- package/src/io/ooxml/parse-footnotes.ts +452 -22
- package/src/io/ooxml/parse-headers-footers.ts +657 -29
- package/src/io/ooxml/parse-inline-media.ts +30 -0
- package/src/io/ooxml/parse-main-document.ts +807 -20
- package/src/io/ooxml/parse-numbering.ts +7 -0
- package/src/io/ooxml/parse-revisions.ts +317 -38
- package/src/io/ooxml/parse-settings.ts +184 -0
- package/src/io/ooxml/parse-shapes.ts +25 -0
- package/src/io/ooxml/parse-styles.ts +463 -0
- package/src/io/ooxml/parse-theme.ts +32 -0
- package/src/legal/bookmarks.ts +44 -0
- package/src/legal/cross-references.ts +59 -1
- package/src/model/canonical-document.ts +250 -4
- package/src/model/cds-1.0.0.ts +13 -0
- package/src/model/snapshot.ts +87 -2
- package/src/review/store/revision-store.ts +6 -0
- package/src/review/store/revision-types.ts +1 -0
- package/src/runtime/document-layout.ts +332 -0
- package/src/runtime/document-navigation.ts +603 -0
- package/src/runtime/document-runtime.ts +1754 -78
- package/src/runtime/document-search.ts +145 -0
- package/src/runtime/numbering-prefix.ts +47 -26
- package/src/runtime/page-layout-estimation.ts +212 -0
- package/src/runtime/read-only-diagnostics-runtime.ts +9 -0
- package/src/runtime/session-capabilities.ts +35 -3
- package/src/runtime/story-context.ts +164 -0
- package/src/runtime/story-targeting.ts +162 -0
- package/src/runtime/surface-projection.ts +324 -36
- package/src/runtime/table-schema.ts +89 -7
- package/src/runtime/view-state.ts +477 -0
- package/src/runtime/workflow-markup.ts +349 -0
- package/src/ui/WordReviewEditor.tsx +2469 -1344
- package/src/ui/browser-export.ts +52 -0
- package/src/ui/editor-command-bag.ts +120 -0
- package/src/ui/editor-runtime-boundary.ts +1422 -0
- package/src/ui/editor-shell-view.tsx +134 -0
- package/src/ui/editor-surface-controller.tsx +51 -0
- package/src/ui/headless/preserve-editor-selection.ts +5 -0
- package/src/ui/headless/revision-decoration-model.ts +4 -4
- package/src/ui/headless/selection-helpers.ts +20 -0
- package/src/ui/headless/selection-toolbar-model.ts +22 -0
- package/src/ui/headless/use-editor-keyboard.ts +6 -1
- package/src/ui/runtime-snapshot-selectors.ts +197 -0
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +18 -2
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +129 -0
- package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +34 -0
- package/src/ui-tailwind/chrome/tw-page-ruler.tsx +386 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +150 -14
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +179 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +46 -7
- package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +3 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +186 -13
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +191 -68
- package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +51 -0
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
- package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +528 -85
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
- package/src/ui-tailwind/index.ts +2 -1
- package/src/ui-tailwind/page-chrome-model.ts +27 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
- package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
- package/src/ui-tailwind/review/tw-review-rail.tsx +8 -8
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
- package/src/ui-tailwind/theme/editor-theme.css +127 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +829 -12
- package/src/ui-tailwind/tw-review-workspace.tsx +1238 -42
- package/src/validation/compatibility-engine.ts +119 -24
- package/src/validation/compatibility-report.ts +1 -0
- package/src/validation/diagnostics.ts +1 -0
- package/src/validation/docx-comment-proof.ts +707 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DocumentNavigationSnapshot,
|
|
3
|
+
EditorStoryTarget,
|
|
4
|
+
SearchOptions,
|
|
5
|
+
SearchResultSnapshot,
|
|
6
|
+
SelectionSnapshot,
|
|
7
|
+
} from "../api/public-types";
|
|
8
|
+
import {
|
|
9
|
+
MAIN_STORY_TARGET,
|
|
10
|
+
storyTargetsEqual,
|
|
11
|
+
} from "../core/selection/mapping.ts";
|
|
12
|
+
import {
|
|
13
|
+
createSelectionSnapshot,
|
|
14
|
+
type CanonicalDocumentEnvelope,
|
|
15
|
+
} from "../core/state/editor-state.ts";
|
|
16
|
+
import {
|
|
17
|
+
searchSecondaryStories,
|
|
18
|
+
searchSurfaceBlocks,
|
|
19
|
+
} from "../core/search/search-text.ts";
|
|
20
|
+
import { findPageForOffset } from "./document-navigation.ts";
|
|
21
|
+
import {
|
|
22
|
+
buildResolvedSections,
|
|
23
|
+
resolveSectionForStoryTarget,
|
|
24
|
+
} from "./document-layout.ts";
|
|
25
|
+
import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
|
|
26
|
+
|
|
27
|
+
export function searchDocument(
|
|
28
|
+
document: CanonicalDocumentEnvelope,
|
|
29
|
+
selection: SelectionSnapshot,
|
|
30
|
+
activeStory: EditorStoryTarget,
|
|
31
|
+
navigation: DocumentNavigationSnapshot,
|
|
32
|
+
query: string,
|
|
33
|
+
options: SearchOptions = {},
|
|
34
|
+
): SearchResultSnapshot[] {
|
|
35
|
+
const normalizedQuery = query.trim();
|
|
36
|
+
if (!normalizedQuery) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const mainSurface = createEditorSurfaceSnapshot(
|
|
41
|
+
document,
|
|
42
|
+
createSelectionSnapshot(selection.anchor, selection.head),
|
|
43
|
+
MAIN_STORY_TARGET,
|
|
44
|
+
);
|
|
45
|
+
const sections = buildResolvedSections(document);
|
|
46
|
+
const combined: SearchResultSnapshot[] = [];
|
|
47
|
+
|
|
48
|
+
for (const match of searchSurfaceBlocks(mainSurface.blocks, normalizedQuery, options)) {
|
|
49
|
+
const pageIndex = findPageForOffset(navigation.pages, match.from);
|
|
50
|
+
combined.push({
|
|
51
|
+
resultId: `search-main-${combined.length}`,
|
|
52
|
+
anchor: {
|
|
53
|
+
kind: "range",
|
|
54
|
+
from: match.from,
|
|
55
|
+
to: match.to,
|
|
56
|
+
assoc: {
|
|
57
|
+
start: -1,
|
|
58
|
+
end: 1,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
excerpt: match.excerpt,
|
|
62
|
+
isActive: false,
|
|
63
|
+
storyTarget: MAIN_STORY_TARGET,
|
|
64
|
+
sectionIndex: navigation.pages[pageIndex]?.sectionIndex ?? 0,
|
|
65
|
+
pageIndex,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const match of searchSecondaryStories(
|
|
70
|
+
mainSurface.secondaryStories,
|
|
71
|
+
normalizedQuery,
|
|
72
|
+
options,
|
|
73
|
+
)) {
|
|
74
|
+
const section = resolveSectionForStoryTarget(
|
|
75
|
+
document,
|
|
76
|
+
sections,
|
|
77
|
+
match.storyTarget,
|
|
78
|
+
);
|
|
79
|
+
const pageIndex =
|
|
80
|
+
section === undefined
|
|
81
|
+
? undefined
|
|
82
|
+
: navigation.pages.find((page) => page.sectionIndex === section.index)
|
|
83
|
+
?.pageIndex ?? 0;
|
|
84
|
+
|
|
85
|
+
combined.push({
|
|
86
|
+
resultId: `search-secondary-${combined.length}`,
|
|
87
|
+
anchor: {
|
|
88
|
+
kind: "range",
|
|
89
|
+
from: match.from,
|
|
90
|
+
to: match.to,
|
|
91
|
+
assoc: {
|
|
92
|
+
start: -1,
|
|
93
|
+
end: 1,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
excerpt: match.excerpt,
|
|
97
|
+
isActive: false,
|
|
98
|
+
storyTarget: match.storyTarget,
|
|
99
|
+
...(section ? { sectionIndex: section.index } : {}),
|
|
100
|
+
...(pageIndex !== undefined ? { pageIndex } : {}),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const limited = combined.slice(0, options.limit ?? Number.POSITIVE_INFINITY);
|
|
105
|
+
const activeIndex = getActiveSearchResultIndex(limited, selection, activeStory);
|
|
106
|
+
|
|
107
|
+
return limited.map((result, index) => ({
|
|
108
|
+
...result,
|
|
109
|
+
isActive: index === activeIndex,
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getActiveSearchResultIndex(
|
|
114
|
+
results: readonly SearchResultSnapshot[],
|
|
115
|
+
selection: SelectionSnapshot,
|
|
116
|
+
activeStory: EditorStoryTarget,
|
|
117
|
+
): number {
|
|
118
|
+
if (results.length === 0) {
|
|
119
|
+
return -1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const selectionFrom = Math.min(selection.anchor, selection.head);
|
|
123
|
+
const selectionTo = Math.max(selection.anchor, selection.head);
|
|
124
|
+
const activeIndex = results.findIndex((result) => {
|
|
125
|
+
if (!result.storyTarget || !storyTargetsEqual(result.storyTarget, activeStory)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (result.anchor.kind !== "range") {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (selectionFrom === selectionTo) {
|
|
134
|
+
return (
|
|
135
|
+
selectionFrom >= result.anchor.from && selectionFrom <= result.anchor.to
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
selectionFrom < result.anchor.to && selectionTo > result.anchor.from
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return activeIndex >= 0 ? activeIndex : 0;
|
|
145
|
+
}
|
|
@@ -9,8 +9,14 @@ interface NumberingSequenceState {
|
|
|
9
9
|
lastLevel: number | null;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export interface NumberingPrefixResult {
|
|
13
|
+
text: string;
|
|
14
|
+
suffix?: "tab" | "space" | "nothing";
|
|
15
|
+
}
|
|
16
|
+
|
|
12
17
|
export interface NumberingPrefixResolver {
|
|
13
18
|
resolve(numbering: ParagraphNode["numbering"] | undefined): string | null;
|
|
19
|
+
resolveDetailed(numbering: ParagraphNode["numbering"] | undefined): NumberingPrefixResult | null;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
const DEFAULT_START_AT = 1;
|
|
@@ -20,34 +26,49 @@ export function createNumberingPrefixResolver(
|
|
|
20
26
|
): NumberingPrefixResolver {
|
|
21
27
|
const sequenceStates = new Map<string, NumberingSequenceState>();
|
|
22
28
|
|
|
29
|
+
function resolveInternal(numbering: ParagraphNode["numbering"] | undefined): NumberingPrefixResult | null {
|
|
30
|
+
if (!numbering) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const instance = catalog.instances[numbering.numberingInstanceId];
|
|
35
|
+
if (!instance) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const definition = catalog.abstractDefinitions[instance.abstractNumberingId];
|
|
40
|
+
if (!definition) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const levelDefinitions = new Map(
|
|
45
|
+
definition.levels.map((level) => [level.level, level] as const),
|
|
46
|
+
);
|
|
47
|
+
const levelDefinition = levelDefinitions.get(numbering.level);
|
|
48
|
+
if (!levelDefinition || levelDefinition.format === "none") {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const sequenceState = getSequenceState(sequenceStates, numbering.numberingInstanceId);
|
|
53
|
+
advanceSequence(sequenceState, numbering.level, levelDefinitions, instance.overrides);
|
|
54
|
+
|
|
55
|
+
// When isLegalNumbering is true, use decimal format for all referenced levels
|
|
56
|
+
const effectiveLevelDefs = levelDefinition.isLegalNumbering
|
|
57
|
+
? new Map(Array.from(levelDefinitions.entries()).map(([k, v]) => [k, { ...v, format: "decimal" }]))
|
|
58
|
+
: levelDefinitions;
|
|
59
|
+
|
|
60
|
+
const text = renderLevelText(levelDefinition.text, sequenceState.counters, effectiveLevelDefs);
|
|
61
|
+
if (text === null) return null;
|
|
62
|
+
return { text, suffix: levelDefinition.suffix };
|
|
63
|
+
}
|
|
64
|
+
|
|
23
65
|
return {
|
|
24
66
|
resolve(numbering) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (!instance) {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const definition = catalog.abstractDefinitions[instance.abstractNumberingId];
|
|
35
|
-
if (!definition) {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const levelDefinitions = new Map(
|
|
40
|
-
definition.levels.map((level) => [level.level, level] as const),
|
|
41
|
-
);
|
|
42
|
-
const levelDefinition = levelDefinitions.get(numbering.level);
|
|
43
|
-
if (!levelDefinition || levelDefinition.format === "none") {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const sequenceState = getSequenceState(sequenceStates, numbering.numberingInstanceId);
|
|
48
|
-
advanceSequence(sequenceState, numbering.level, levelDefinitions, instance.overrides);
|
|
49
|
-
|
|
50
|
-
return renderLevelText(levelDefinition.text, sequenceState.counters, levelDefinitions);
|
|
67
|
+
const result = resolveInternal(numbering);
|
|
68
|
+
return result?.text ?? null;
|
|
69
|
+
},
|
|
70
|
+
resolveDetailed(numbering) {
|
|
71
|
+
return resolveInternal(numbering);
|
|
51
72
|
},
|
|
52
73
|
};
|
|
53
74
|
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PageLayoutSnapshot,
|
|
3
|
+
SurfaceBlockSnapshot,
|
|
4
|
+
SurfaceInlineSegment,
|
|
5
|
+
} from "../api/public-types";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_FONT_SIZE_POINTS = 12;
|
|
8
|
+
const DEFAULT_LINE_HEIGHT_TWIPS = 280;
|
|
9
|
+
const MIN_BLOCK_HEIGHT_TWIPS = 240;
|
|
10
|
+
const TABLE_ROW_PADDING_TWIPS = 120;
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP = 1 / 15;
|
|
13
|
+
|
|
14
|
+
export function estimateBlockHeight(
|
|
15
|
+
block: SurfaceBlockSnapshot | undefined,
|
|
16
|
+
columnWidth: number,
|
|
17
|
+
): number {
|
|
18
|
+
if (!block) {
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
switch (block.kind) {
|
|
23
|
+
case "paragraph":
|
|
24
|
+
return estimateParagraphHeight(block, columnWidth);
|
|
25
|
+
case "table":
|
|
26
|
+
return estimateTableHeight(block, columnWidth);
|
|
27
|
+
case "sdt_block":
|
|
28
|
+
return Math.max(
|
|
29
|
+
MIN_BLOCK_HEIGHT_TWIPS,
|
|
30
|
+
block.children.reduce((total, child) => total + estimateBlockHeight(child, columnWidth), 0),
|
|
31
|
+
);
|
|
32
|
+
case "opaque_block":
|
|
33
|
+
return block.label === "Section break" ? 0 : MIN_BLOCK_HEIGHT_TWIPS;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function estimateParagraphHeight(
|
|
38
|
+
block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
|
|
39
|
+
columnWidth: number,
|
|
40
|
+
): number {
|
|
41
|
+
const lineHeight = estimateParagraphLineHeight(block);
|
|
42
|
+
const lineCount = estimateParagraphLineCount(block, columnWidth);
|
|
43
|
+
const spacingBefore = block.spacing?.before ?? 0;
|
|
44
|
+
const spacingAfter = block.spacing?.after ?? 0;
|
|
45
|
+
return Math.max(
|
|
46
|
+
MIN_BLOCK_HEIGHT_TWIPS,
|
|
47
|
+
lineHeight * lineCount + spacingBefore + spacingAfter,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function estimateParagraphLineHeight(
|
|
52
|
+
block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
|
|
53
|
+
): number {
|
|
54
|
+
const explicitLine = block.spacing?.line;
|
|
55
|
+
if (typeof explicitLine === "number" && explicitLine > 0) {
|
|
56
|
+
return explicitLine;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const fontSizeHalfPoints = block.segments.find(
|
|
60
|
+
(segment): segment is Extract<SurfaceInlineSegment, { kind: "text" }> =>
|
|
61
|
+
segment.kind === "text" && typeof segment.markAttrs?.fontSize === "number",
|
|
62
|
+
)?.markAttrs?.fontSize;
|
|
63
|
+
const fontSizePoints =
|
|
64
|
+
typeof fontSizeHalfPoints === "number"
|
|
65
|
+
? fontSizeHalfPoints / 2
|
|
66
|
+
: DEFAULT_FONT_SIZE_POINTS;
|
|
67
|
+
return Math.max(
|
|
68
|
+
DEFAULT_LINE_HEIGHT_TWIPS,
|
|
69
|
+
Math.round(fontSizePoints * 20 * 1.35),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function estimateParagraphLineCount(
|
|
74
|
+
block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
|
|
75
|
+
columnWidth: number,
|
|
76
|
+
): number {
|
|
77
|
+
const averageCharWidth = estimateAverageCharWidth(block);
|
|
78
|
+
const charsPerLine = Math.max(12, Math.floor(columnWidth / averageCharWidth));
|
|
79
|
+
let lineCount = 1;
|
|
80
|
+
let currentLineChars = estimatedPrefixLength(block);
|
|
81
|
+
|
|
82
|
+
for (const segment of block.segments) {
|
|
83
|
+
switch (segment.kind) {
|
|
84
|
+
case "text":
|
|
85
|
+
currentLineChars += Array.from(segment.text).length;
|
|
86
|
+
while (currentLineChars > charsPerLine) {
|
|
87
|
+
lineCount += 1;
|
|
88
|
+
currentLineChars -= charsPerLine;
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
case "tab":
|
|
92
|
+
currentLineChars += 4;
|
|
93
|
+
break;
|
|
94
|
+
case "hard_break":
|
|
95
|
+
lineCount += 1;
|
|
96
|
+
currentLineChars = 0;
|
|
97
|
+
break;
|
|
98
|
+
case "image":
|
|
99
|
+
lineCount += Math.max(1, Math.round(segment.display === "floating" ? 2 : 1));
|
|
100
|
+
currentLineChars = 0;
|
|
101
|
+
break;
|
|
102
|
+
case "note_ref":
|
|
103
|
+
currentLineChars += 1;
|
|
104
|
+
break;
|
|
105
|
+
case "opaque_inline":
|
|
106
|
+
if (segment.presentation !== "quiet-marker") {
|
|
107
|
+
currentLineChars += segment.label.length > 0 ? 1 : 0;
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return Math.max(1, lineCount);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getUsablePageHeight(layout: PageLayoutSnapshot): number {
|
|
117
|
+
return Math.max(
|
|
118
|
+
1440,
|
|
119
|
+
layout.pageHeight - layout.marginTop - layout.marginBottom,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface PageColumnMetric {
|
|
124
|
+
width: number;
|
|
125
|
+
space: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function getUsableColumnMetrics(
|
|
129
|
+
layout: PageLayoutSnapshot,
|
|
130
|
+
): PageColumnMetric[] {
|
|
131
|
+
const usableWidth = Math.max(
|
|
132
|
+
1440,
|
|
133
|
+
layout.pageWidth - layout.marginLeft - layout.marginRight - layout.gutter,
|
|
134
|
+
);
|
|
135
|
+
const columnCount = Math.max(1, layout.columns);
|
|
136
|
+
if (
|
|
137
|
+
!layout.equalWidthColumns &&
|
|
138
|
+
layout.columnDefinitions.length > 0
|
|
139
|
+
) {
|
|
140
|
+
return layout.columnDefinitions.map((column, index) => ({
|
|
141
|
+
width: Math.max(720, column.width),
|
|
142
|
+
space:
|
|
143
|
+
index < layout.columnDefinitions.length - 1
|
|
144
|
+
? Math.max(0, column.space ?? 0)
|
|
145
|
+
: 0,
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const width = columnCount <= 1
|
|
150
|
+
? usableWidth
|
|
151
|
+
: Math.max(720, Math.floor(usableWidth / columnCount));
|
|
152
|
+
return Array.from({ length: columnCount }, () => ({
|
|
153
|
+
width,
|
|
154
|
+
space: 0,
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function getUsableColumnWidth(layout: PageLayoutSnapshot): number {
|
|
159
|
+
return getUsableColumnMetrics(layout)[0]?.width ?? 1440;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function estimateTableHeight(
|
|
163
|
+
block: Extract<SurfaceBlockSnapshot, { kind: "table" }>,
|
|
164
|
+
columnWidth: number,
|
|
165
|
+
): number {
|
|
166
|
+
let totalHeight = 0;
|
|
167
|
+
for (const row of block.rows) {
|
|
168
|
+
const explicitHeight = row.height ?? 0;
|
|
169
|
+
if (explicitHeight > 0) {
|
|
170
|
+
totalHeight += explicitHeight;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let rowHeight = MIN_BLOCK_HEIGHT_TWIPS;
|
|
175
|
+
for (const cell of row.cells) {
|
|
176
|
+
const cellHeight = cell.content.reduce(
|
|
177
|
+
(total, child) => total + estimateBlockHeight(child, columnWidth),
|
|
178
|
+
0,
|
|
179
|
+
);
|
|
180
|
+
rowHeight = Math.max(rowHeight, cellHeight + TABLE_ROW_PADDING_TWIPS);
|
|
181
|
+
}
|
|
182
|
+
totalHeight += rowHeight;
|
|
183
|
+
}
|
|
184
|
+
return Math.max(MIN_BLOCK_HEIGHT_TWIPS, totalHeight);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function estimateAverageCharWidth(
|
|
188
|
+
block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
|
|
189
|
+
): number {
|
|
190
|
+
const fontSizeHalfPoints = block.segments.find(
|
|
191
|
+
(segment): segment is Extract<SurfaceInlineSegment, { kind: "text" }> =>
|
|
192
|
+
segment.kind === "text" && typeof segment.markAttrs?.fontSize === "number",
|
|
193
|
+
)?.markAttrs?.fontSize;
|
|
194
|
+
const fontSizePoints =
|
|
195
|
+
typeof fontSizeHalfPoints === "number"
|
|
196
|
+
? fontSizeHalfPoints / 2
|
|
197
|
+
: DEFAULT_FONT_SIZE_POINTS;
|
|
198
|
+
return Math.max(96, Math.round(fontSizePoints * 12));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function estimatedPrefixLength(
|
|
202
|
+
block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
|
|
203
|
+
): number {
|
|
204
|
+
const prefix = block.numberingPrefix ?? "";
|
|
205
|
+
const suffix =
|
|
206
|
+
block.numberingSuffix === "space"
|
|
207
|
+
? 1
|
|
208
|
+
: block.numberingSuffix === "tab"
|
|
209
|
+
? 4
|
|
210
|
+
: 0;
|
|
211
|
+
return prefix.length + suffix;
|
|
212
|
+
}
|
|
@@ -155,6 +155,7 @@ function createDiagnosticsRenderSnapshot(
|
|
|
155
155
|
isDirty: false,
|
|
156
156
|
readOnly: true,
|
|
157
157
|
selection: collapsedSelection(),
|
|
158
|
+
activeStory: { kind: "main" },
|
|
158
159
|
documentStats: {
|
|
159
160
|
storyLength: 0,
|
|
160
161
|
commentCount: 0,
|
|
@@ -192,6 +193,14 @@ function createDiagnosticsRenderSnapshot(
|
|
|
192
193
|
canRedo: false,
|
|
193
194
|
readOnly: true,
|
|
194
195
|
},
|
|
196
|
+
documentMode: "viewing",
|
|
197
|
+
protectionSnapshot: {
|
|
198
|
+
hasDocumentProtection: false,
|
|
199
|
+
enforcementActive: false,
|
|
200
|
+
ranges: [],
|
|
201
|
+
enforcedRangeCount: 0,
|
|
202
|
+
preservedRangeCount: 0,
|
|
203
|
+
},
|
|
195
204
|
};
|
|
196
205
|
}
|
|
197
206
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RuntimeRenderSnapshot } from "../api/public-types";
|
|
1
|
+
import type { RuntimeRenderSnapshot, WorkflowScopeSnapshot } from "../api/public-types";
|
|
2
2
|
import {
|
|
3
3
|
createDetachedAnchor,
|
|
4
4
|
createNodeAnchor,
|
|
@@ -21,6 +21,10 @@ export interface SessionCapabilities {
|
|
|
21
21
|
/** Effective editor mode after accounting for runtime state. */
|
|
22
22
|
mode: "editing" | "review" | "read-only-diagnostics";
|
|
23
23
|
|
|
24
|
+
// ── Document mode ──
|
|
25
|
+
/** Runtime document mode — editing authority, distinct from view/workspace mode. */
|
|
26
|
+
documentMode: "editing" | "suggesting" | "viewing";
|
|
27
|
+
|
|
24
28
|
// ── Command capabilities ──
|
|
25
29
|
canUndo: boolean;
|
|
26
30
|
canRedo: boolean;
|
|
@@ -44,10 +48,22 @@ export interface SessionCapabilities {
|
|
|
44
48
|
preserveOnlyCount: number;
|
|
45
49
|
unsupportedFatalCount: number;
|
|
46
50
|
|
|
51
|
+
// ── Protection posture ──
|
|
52
|
+
/** Whether the source package declares document-level protection. */
|
|
53
|
+
hasDocumentProtection: boolean;
|
|
54
|
+
/** Count of permission ranges from the source package. */
|
|
55
|
+
protectedRangeCount: number;
|
|
56
|
+
|
|
47
57
|
// ── Health ──
|
|
48
58
|
/** Total count of health issues (preserve-only + unsupported-fatal + warnings). */
|
|
49
59
|
healthIssueCount: number;
|
|
50
60
|
|
|
61
|
+
// ── Workflow ──
|
|
62
|
+
/** Whether a workflow overlay is currently applied. */
|
|
63
|
+
workflowOverlayPresent: boolean;
|
|
64
|
+
/** Whether the current selection is blocked by workflow scope enforcement. */
|
|
65
|
+
workflowBlocked: boolean;
|
|
66
|
+
|
|
51
67
|
// ── Status ──
|
|
52
68
|
isDirty: boolean;
|
|
53
69
|
isReady: boolean;
|
|
@@ -63,11 +79,14 @@ export interface SessionCapabilities {
|
|
|
63
79
|
export function deriveCapabilities(
|
|
64
80
|
snapshot: RuntimeRenderSnapshot,
|
|
65
81
|
reviewMode: "editing" | "review",
|
|
82
|
+
workflowScope?: WorkflowScopeSnapshot | null,
|
|
66
83
|
): SessionCapabilities {
|
|
67
84
|
const hasFatalError = Boolean(snapshot.fatalError);
|
|
68
85
|
const isReady = snapshot.isReady;
|
|
69
86
|
const isReadOnly = snapshot.readOnly;
|
|
70
87
|
const exportBlocked = snapshot.compatibility.blockExport;
|
|
88
|
+
const activeStory = snapshot.activeStory ?? { kind: "main" as const };
|
|
89
|
+
const documentMode = snapshot.documentMode ?? "editing";
|
|
71
90
|
|
|
72
91
|
// Phase derivation
|
|
73
92
|
const phase: SessionCapabilities["phase"] = !isReady
|
|
@@ -82,12 +101,13 @@ export function deriveCapabilities(
|
|
|
82
101
|
? "read-only-diagnostics"
|
|
83
102
|
: reviewMode;
|
|
84
103
|
|
|
85
|
-
// Command capabilities
|
|
86
|
-
const canEdit = isReady && !isReadOnly && !hasFatalError;
|
|
104
|
+
// Command capabilities — document mode "viewing" disables editing
|
|
105
|
+
const canEdit = isReady && !isReadOnly && !hasFatalError && documentMode !== "viewing";
|
|
87
106
|
const canUndo = snapshot.commandState.canUndo && canEdit;
|
|
88
107
|
const canRedo = snapshot.commandState.canRedo && canEdit;
|
|
89
108
|
const canAddComment =
|
|
90
109
|
canEdit &&
|
|
110
|
+
activeStory.kind === "main" &&
|
|
91
111
|
!snapshot.selection.isCollapsed &&
|
|
92
112
|
Boolean(snapshot.surface) &&
|
|
93
113
|
canCreateDocxCommentAnchor(snapshot.surface, toRuntimeAnchor(snapshot.selection.activeRange));
|
|
@@ -123,9 +143,17 @@ export function deriveCapabilities(
|
|
|
123
143
|
|
|
124
144
|
const healthIssueCount = preserveOnlyCount + unsupportedFatalCount + snapshot.warnings.length;
|
|
125
145
|
|
|
146
|
+
const protection = snapshot.protectionSnapshot;
|
|
147
|
+
const hasDocumentProtection = protection?.hasDocumentProtection ?? false;
|
|
148
|
+
const protectedRangeCount = protection?.ranges?.length ?? 0;
|
|
149
|
+
|
|
150
|
+
const workflowOverlayPresent = workflowScope?.overlayPresent ?? false;
|
|
151
|
+
const workflowBlocked = (workflowScope?.blockedReasons?.length ?? 0) > 0;
|
|
152
|
+
|
|
126
153
|
return {
|
|
127
154
|
phase,
|
|
128
155
|
mode,
|
|
156
|
+
documentMode,
|
|
129
157
|
canUndo,
|
|
130
158
|
canRedo,
|
|
131
159
|
canEdit,
|
|
@@ -140,7 +168,11 @@ export function deriveCapabilities(
|
|
|
140
168
|
exportBlocked,
|
|
141
169
|
preserveOnlyCount,
|
|
142
170
|
unsupportedFatalCount,
|
|
171
|
+
hasDocumentProtection,
|
|
172
|
+
protectedRangeCount,
|
|
143
173
|
healthIssueCount,
|
|
174
|
+
workflowOverlayPresent,
|
|
175
|
+
workflowBlocked,
|
|
144
176
|
isDirty: snapshot.isDirty,
|
|
145
177
|
isReady,
|
|
146
178
|
hasFatalError,
|