@beyondwork/docx-react-component 1.0.41 → 1.0.43
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/package.json +38 -37
- package/src/api/awareness-identity-types.ts +35 -0
- package/src/api/comment-negotiation-types.ts +130 -0
- package/src/api/comment-presentation-types.ts +106 -0
- package/src/api/editor-state-types.ts +110 -0
- package/src/api/external-custody-types.ts +74 -0
- package/src/api/participants-types.ts +18 -0
- package/src/api/public-types.ts +541 -5
- package/src/api/scope-metadata-resolver-types.ts +88 -0
- package/src/core/commands/formatting-commands.ts +1 -1
- package/src/core/commands/index.ts +601 -9
- package/src/core/search/search-text.ts +15 -2
- package/src/index.ts +131 -1
- package/src/io/docx-session.ts +672 -2
- package/src/io/export/escape-xml-attribute.ts +26 -0
- package/src/io/export/external-send.ts +188 -0
- package/src/io/export/serialize-comments.ts +13 -16
- package/src/io/export/serialize-footnotes.ts +17 -24
- package/src/io/export/serialize-headers-footers.ts +17 -24
- package/src/io/export/serialize-main-document.ts +59 -62
- package/src/io/export/serialize-numbering.ts +20 -27
- package/src/io/export/serialize-runtime-revisions.ts +2 -9
- package/src/io/export/serialize-tables.ts +8 -15
- package/src/io/export/table-properties-xml.ts +25 -32
- package/src/io/import/external-reimport.ts +40 -0
- package/src/io/load-scheduler.ts +230 -0
- package/src/io/normalize/normalize-text.ts +83 -0
- package/src/io/ooxml/bw-xml.ts +244 -0
- package/src/io/ooxml/canonicalize-payload.ts +301 -0
- package/src/io/ooxml/comment-negotiation-payload.ts +288 -0
- package/src/io/ooxml/comment-presentation-payload.ts +311 -0
- package/src/io/ooxml/external-custody-payload.ts +102 -0
- package/src/io/ooxml/participants-payload.ts +97 -0
- package/src/io/ooxml/payload-signature.ts +112 -0
- package/src/io/ooxml/workflow-payload-validator.ts +367 -0
- package/src/io/ooxml/workflow-payload.ts +317 -7
- package/src/runtime/awareness-identity.ts +173 -0
- package/src/runtime/collab/event-types.ts +27 -0
- package/src/runtime/collab-session-bridge.ts +157 -0
- package/src/runtime/collab-session-facet.ts +193 -0
- package/src/runtime/collab-session.ts +273 -0
- package/src/runtime/comment-negotiation-sync.ts +91 -0
- package/src/runtime/comment-negotiation.ts +158 -0
- package/src/runtime/comment-presentation.ts +223 -0
- package/src/runtime/document-runtime.ts +639 -124
- package/src/runtime/editor-state-channel.ts +544 -0
- package/src/runtime/editor-state-integration.ts +217 -0
- package/src/runtime/external-send-runtime.ts +117 -0
- package/src/runtime/layout/docx-font-loader.ts +11 -30
- package/src/runtime/layout/index.ts +2 -0
- package/src/runtime/layout/inert-layout-facet.ts +4 -0
- package/src/runtime/layout/layout-engine-instance.ts +139 -14
- package/src/runtime/layout/page-graph.ts +79 -7
- package/src/runtime/layout/paginated-layout-engine.ts +441 -48
- package/src/runtime/layout/public-facet.ts +585 -14
- package/src/runtime/layout/table-row-split.ts +316 -0
- package/src/runtime/markdown-sanitizer.ts +132 -0
- package/src/runtime/participants.ts +134 -0
- package/src/runtime/perf-counters.ts +28 -0
- package/src/runtime/render/render-frame-types.ts +17 -0
- package/src/runtime/render/render-kernel.ts +172 -29
- package/src/runtime/resign-payload.ts +120 -0
- package/src/runtime/surface-projection.ts +10 -5
- package/src/runtime/tamper-gate.ts +157 -0
- package/src/runtime/workflow-markup.ts +80 -16
- package/src/runtime/workflow-rail-segments.ts +244 -5
- package/src/ui/WordReviewEditor.tsx +654 -45
- package/src/ui/editor-command-bag.ts +14 -0
- package/src/ui/editor-runtime-boundary.ts +111 -11
- package/src/ui/editor-shell-view.tsx +21 -0
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-helpers.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +28 -0
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
- package/src/ui-tailwind/chrome/collab-audience-chip.tsx +73 -0
- package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +244 -0
- package/src/ui-tailwind/chrome/collab-presence-strip.tsx +150 -0
- package/src/ui-tailwind/chrome/collab-role-chip.tsx +62 -0
- package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +68 -0
- package/src/ui-tailwind/chrome/collab-send-to-supplier-modal.tsx +149 -0
- package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +68 -0
- package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
- package/src/ui-tailwind/chrome/forward-non-drag-click.ts +104 -0
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +1 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +7 -1
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -38
- package/src/ui-tailwind/chrome-overlay/index.ts +6 -0
- package/src/ui-tailwind/chrome-overlay/scope-card-role-model.ts +78 -0
- package/src/ui-tailwind/chrome-overlay/scope-keyboard-cycle.ts +49 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +106 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +527 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +120 -22
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +310 -32
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +93 -14
- package/src/ui-tailwind/editor-surface/paste-plain-text.ts +72 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +118 -8
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -1
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +86 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +167 -17
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +35 -7
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +20 -3
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +265 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +10 -256
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +66 -0
- package/src/ui-tailwind/index.ts +37 -1
- package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +57 -0
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +71 -0
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +73 -0
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +74 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +477 -0
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +374 -0
- package/src/ui-tailwind/review/comment-markdown-renderer.tsx +155 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +29 -3
- package/src/ui-tailwind/status/tw-status-bar.tsx +52 -1
- package/src/ui-tailwind/theme/editor-theme.css +25 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +455 -118
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
SurfaceBlockSnapshot,
|
|
5
|
+
SurfaceTextMark,
|
|
6
|
+
} from "../../api/public-types.ts";
|
|
7
|
+
import type { CanonicalRunFormatting } from "../../model/canonical-document.ts";
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Pure helpers shared by `tw-page-block-view` (body) and `tw-region-block-
|
|
11
|
+
// renderer` (header / footer / footnote / endnote bands). Extracted in P8.4
|
|
12
|
+
// so per-page regions reuse body typography verbatim — indent/margin/line-
|
|
13
|
+
// height/marker geometry stay identical across all regions.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export const HEX_COLOR_RE = /^[0-9A-Fa-f]{3,8}$/;
|
|
17
|
+
export const SAFE_FONT_RE = /^[A-Za-z0-9 ,\-'"]+$/;
|
|
18
|
+
export const SAFE_ALIGNMENT = new Set([
|
|
19
|
+
"left",
|
|
20
|
+
"center",
|
|
21
|
+
"right",
|
|
22
|
+
"justify",
|
|
23
|
+
"start",
|
|
24
|
+
"end",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
export function safeHexColor(raw: string | null | undefined): string | null {
|
|
28
|
+
if (!raw || raw === "auto") return null;
|
|
29
|
+
return HEX_COLOR_RE.test(raw) ? `#${raw}` : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Resolve heading level from styleId or outlineLevel (outlineLevel 0 = Heading 1). */
|
|
33
|
+
export function resolveHeadingLevel(
|
|
34
|
+
styleId?: string,
|
|
35
|
+
outlineLevel?: number,
|
|
36
|
+
): number | null {
|
|
37
|
+
if (styleId) {
|
|
38
|
+
const normalized = styleId.toLowerCase();
|
|
39
|
+
const compact = normalized.replace(/[\s_-]+/g, "");
|
|
40
|
+
const headingMatch = /^heading([1-6])$/.exec(compact);
|
|
41
|
+
if (headingMatch) {
|
|
42
|
+
return Number.parseInt(headingMatch[1], 10);
|
|
43
|
+
}
|
|
44
|
+
if (compact === "title") return 1;
|
|
45
|
+
if (compact === "subtitle") return 2;
|
|
46
|
+
if (compact === "tocheading") return 1;
|
|
47
|
+
if (/^(appendix|schedule|annex|exhibit|attachment)(heading|title)$/.test(compact)) return 2;
|
|
48
|
+
}
|
|
49
|
+
if (
|
|
50
|
+
typeof outlineLevel === "number" &&
|
|
51
|
+
Number.isInteger(outlineLevel) &&
|
|
52
|
+
outlineLevel >= 0 &&
|
|
53
|
+
outlineLevel <= 5
|
|
54
|
+
) {
|
|
55
|
+
return outlineLevel + 1;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resolveMarkerJustificationCss(raw: string | undefined): string {
|
|
61
|
+
switch (raw) {
|
|
62
|
+
case "left":
|
|
63
|
+
return "flex-start";
|
|
64
|
+
case "center":
|
|
65
|
+
return "center";
|
|
66
|
+
case "right":
|
|
67
|
+
case "both":
|
|
68
|
+
case "distribute":
|
|
69
|
+
default:
|
|
70
|
+
return "flex-end";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Build CSSProperties for a paragraph block from spacing/indent/alignment. */
|
|
75
|
+
export function buildParagraphStyle(
|
|
76
|
+
block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
|
|
77
|
+
): React.CSSProperties {
|
|
78
|
+
const style: React.CSSProperties = {};
|
|
79
|
+
|
|
80
|
+
// Alignment — direct takes precedence over resolvedParagraphFormatting
|
|
81
|
+
const rawAlignment = block.alignment ?? block.resolvedParagraphFormatting?.alignment;
|
|
82
|
+
const safeAlign = rawAlignment === "both" ? "justify" : rawAlignment;
|
|
83
|
+
if (safeAlign && SAFE_ALIGNMENT.has(safeAlign)) {
|
|
84
|
+
style.textAlign = safeAlign as React.CSSProperties["textAlign"];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Spacing
|
|
88
|
+
const spacingBefore =
|
|
89
|
+
block.spacing?.before ?? block.resolvedParagraphFormatting?.spacing?.before;
|
|
90
|
+
const spacingAfter =
|
|
91
|
+
block.spacing?.after ?? block.resolvedParagraphFormatting?.spacing?.after;
|
|
92
|
+
const lineSpacing =
|
|
93
|
+
block.spacing?.line ?? block.resolvedParagraphFormatting?.spacing?.line;
|
|
94
|
+
const lineRule =
|
|
95
|
+
block.spacing?.lineRule ?? block.resolvedParagraphFormatting?.spacing?.lineRule;
|
|
96
|
+
|
|
97
|
+
if (spacingBefore != null) {
|
|
98
|
+
style.marginTop = `${spacingBefore / 20}pt`;
|
|
99
|
+
}
|
|
100
|
+
if (spacingAfter != null) {
|
|
101
|
+
style.marginBottom = `${spacingAfter / 20}pt`;
|
|
102
|
+
}
|
|
103
|
+
if (lineSpacing && lineRule === "auto") {
|
|
104
|
+
style.lineHeight = String(lineSpacing / 240);
|
|
105
|
+
} else if (lineSpacing && lineRule === "exact") {
|
|
106
|
+
style.lineHeight = `${lineSpacing / 20}pt`;
|
|
107
|
+
} else if (lineSpacing && lineRule === "atLeast") {
|
|
108
|
+
style.minHeight = `${lineSpacing / 20}pt`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Indentation
|
|
112
|
+
const indentLeft =
|
|
113
|
+
block.indentation?.left ?? block.resolvedParagraphFormatting?.indentation?.left;
|
|
114
|
+
const indentRight =
|
|
115
|
+
block.indentation?.right ?? block.resolvedParagraphFormatting?.indentation?.right;
|
|
116
|
+
const indentFirstLine =
|
|
117
|
+
block.indentation?.firstLine ?? block.resolvedParagraphFormatting?.indentation?.firstLine;
|
|
118
|
+
const indentHanging =
|
|
119
|
+
block.indentation?.hanging ?? block.resolvedParagraphFormatting?.indentation?.hanging;
|
|
120
|
+
|
|
121
|
+
if (indentLeft) style.paddingLeft = `${indentLeft / 20}pt`;
|
|
122
|
+
if (indentRight) style.paddingRight = `${indentRight / 20}pt`;
|
|
123
|
+
if (indentHanging) style.textIndent = `-${indentHanging / 20}pt`;
|
|
124
|
+
else if (indentFirstLine) style.textIndent = `${indentFirstLine / 20}pt`;
|
|
125
|
+
|
|
126
|
+
// Shading
|
|
127
|
+
const shadingFill = block.shading?.fill;
|
|
128
|
+
const shadingColor = safeHexColor(shadingFill);
|
|
129
|
+
if (shadingColor) style.backgroundColor = shadingColor;
|
|
130
|
+
|
|
131
|
+
// Page break visual indicator
|
|
132
|
+
if (block.pageBreakBefore) {
|
|
133
|
+
style.borderTop = "2px dashed rgba(0,0,0,0.1)";
|
|
134
|
+
style.paddingTop = "8px";
|
|
135
|
+
style.marginTop = "16px";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return style;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Build CSSProperties for the numbering marker span. */
|
|
142
|
+
export function buildMarkerStyle(
|
|
143
|
+
prefix: string,
|
|
144
|
+
suffix: "tab" | "space" | "nothing" | undefined,
|
|
145
|
+
markerRunProperties: CanonicalRunFormatting | undefined,
|
|
146
|
+
markerWidth: number | undefined,
|
|
147
|
+
markerJustification: string | undefined,
|
|
148
|
+
): React.CSSProperties {
|
|
149
|
+
const style: React.CSSProperties = {
|
|
150
|
+
fontVariantNumeric: "tabular-nums",
|
|
151
|
+
justifyContent: resolveMarkerJustificationCss(markerJustification),
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
if (markerRunProperties) {
|
|
155
|
+
if (markerRunProperties.bold) style.fontWeight = "bold";
|
|
156
|
+
if (markerRunProperties.italic) style.fontStyle = "italic";
|
|
157
|
+
if (markerRunProperties.underline && markerRunProperties.underline !== "none") {
|
|
158
|
+
style.textDecoration = "underline";
|
|
159
|
+
}
|
|
160
|
+
if (typeof markerRunProperties.fontSizeHalfPoints === "number") {
|
|
161
|
+
style.fontSize = `${markerRunProperties.fontSizeHalfPoints / 2}pt`;
|
|
162
|
+
}
|
|
163
|
+
const colorHex = markerRunProperties.colorHex;
|
|
164
|
+
if (colorHex && colorHex !== "auto") {
|
|
165
|
+
style.color = `#${colorHex.toLowerCase()}`;
|
|
166
|
+
}
|
|
167
|
+
const family = markerRunProperties.fontFamilyAscii ?? markerRunProperties.fontFamily;
|
|
168
|
+
if (family && SAFE_FONT_RE.test(family)) {
|
|
169
|
+
style.fontFamily = family;
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
style.color = "var(--color-text-tertiary)";
|
|
173
|
+
style.fontFamily = "var(--font-legal-sans)";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const hasResolvedMarkerWidth = typeof markerWidth === "number" && markerWidth > 0;
|
|
177
|
+
if (hasResolvedMarkerWidth) {
|
|
178
|
+
// P13.a: emit marker geometry in pt so it self-scales under CSS `zoom`.
|
|
179
|
+
const markerWidthPt = Math.max(1, markerWidth! / 20);
|
|
180
|
+
style.width = `${markerWidthPt}pt`;
|
|
181
|
+
style.minWidth = `${markerWidthPt}pt`;
|
|
182
|
+
style.flexBasis = `${markerWidthPt}pt`;
|
|
183
|
+
style.marginRight = 0;
|
|
184
|
+
style.overflow = "visible";
|
|
185
|
+
} else {
|
|
186
|
+
const fallbackMinWidth = Math.min(Math.max(prefix.length + 1, 4), 14);
|
|
187
|
+
const fallbackMarginRight =
|
|
188
|
+
suffix === "nothing" ? "0.25rem" : suffix === "space" ? "0.5rem" : "0.75rem";
|
|
189
|
+
style.minWidth = `${fallbackMinWidth}ch`;
|
|
190
|
+
style.marginRight = fallbackMarginRight;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return style;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Build CSSProperties for a text segment from marks and markAttrs. */
|
|
197
|
+
export function buildSegmentStyle(
|
|
198
|
+
marks: SurfaceTextMark[] | undefined,
|
|
199
|
+
markAttrs?: {
|
|
200
|
+
fontSize?: number;
|
|
201
|
+
textColor?: string;
|
|
202
|
+
fontFamily?: string;
|
|
203
|
+
backgroundColor?: string;
|
|
204
|
+
charSpacing?: number;
|
|
205
|
+
},
|
|
206
|
+
): React.CSSProperties {
|
|
207
|
+
const style: React.CSSProperties = {};
|
|
208
|
+
|
|
209
|
+
if (marks) {
|
|
210
|
+
if (marks.includes("bold")) style.fontWeight = "bold";
|
|
211
|
+
if (marks.includes("italic")) style.fontStyle = "italic";
|
|
212
|
+
if (marks.includes("underline")) style.textDecoration = "underline";
|
|
213
|
+
if (marks.includes("strikethrough") || marks.includes("doubleStrikethrough")) {
|
|
214
|
+
style.textDecoration = marks.includes("underline")
|
|
215
|
+
? "underline line-through"
|
|
216
|
+
: "line-through";
|
|
217
|
+
}
|
|
218
|
+
if (marks.includes("superscript")) {
|
|
219
|
+
style.verticalAlign = "super";
|
|
220
|
+
style.fontSize = "smaller";
|
|
221
|
+
}
|
|
222
|
+
if (marks.includes("subscript")) {
|
|
223
|
+
style.verticalAlign = "sub";
|
|
224
|
+
style.fontSize = "smaller";
|
|
225
|
+
}
|
|
226
|
+
if (marks.includes("allCaps")) style.textTransform = "uppercase";
|
|
227
|
+
if (marks.includes("smallCaps")) style.fontVariant = "small-caps";
|
|
228
|
+
if (marks.includes("vanish")) style.display = "none";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (markAttrs) {
|
|
232
|
+
if (markAttrs.fontSize) style.fontSize = `${markAttrs.fontSize}pt`;
|
|
233
|
+
if (markAttrs.textColor) style.color = markAttrs.textColor;
|
|
234
|
+
if (markAttrs.fontFamily && SAFE_FONT_RE.test(markAttrs.fontFamily)) {
|
|
235
|
+
style.fontFamily = markAttrs.fontFamily;
|
|
236
|
+
}
|
|
237
|
+
if (markAttrs.backgroundColor) style.backgroundColor = markAttrs.backgroundColor;
|
|
238
|
+
if (markAttrs.charSpacing) style.letterSpacing = `${markAttrs.charSpacing}px`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return style;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function hasStyleEntries(style: React.CSSProperties): boolean {
|
|
245
|
+
return Object.keys(style).length > 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function headingClassList(level: number): string[] {
|
|
249
|
+
switch (level) {
|
|
250
|
+
case 1:
|
|
251
|
+
return ["text-3xl", "font-semibold", "tracking-tight", "leading-tight"];
|
|
252
|
+
case 2:
|
|
253
|
+
return ["text-2xl", "font-semibold", "tracking-tight"];
|
|
254
|
+
case 3:
|
|
255
|
+
return ["text-xl", "font-medium"];
|
|
256
|
+
case 4:
|
|
257
|
+
return ["text-lg", "font-medium"];
|
|
258
|
+
case 5:
|
|
259
|
+
return ["text-base", "font-semibold", "uppercase", "tracking-[0.12em]"];
|
|
260
|
+
case 6:
|
|
261
|
+
return ["text-sm", "font-semibold", "uppercase", "tracking-[0.16em]"];
|
|
262
|
+
default:
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -3,242 +3,15 @@ import React from "react";
|
|
|
3
3
|
import type {
|
|
4
4
|
SurfaceBlockSnapshot,
|
|
5
5
|
SurfaceInlineSegment,
|
|
6
|
-
SurfaceTextMark,
|
|
7
6
|
} from "../../api/public-types.ts";
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const SAFE_ALIGNMENT = new Set(["left", "center", "right", "justify", "start", "end"]);
|
|
17
|
-
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
// Helper utilities (mirror pm-schema.ts helpers)
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
|
|
22
|
-
function safeHexColor(raw: string | null | undefined): string | null {
|
|
23
|
-
if (!raw || raw === "auto") return null;
|
|
24
|
-
return HEX_COLOR_RE.test(raw) ? `#${raw}` : null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Resolve heading level from styleId or outlineLevel (outlineLevel 0 = Heading 1). */
|
|
28
|
-
function resolveHeadingLevel(styleId?: string, outlineLevel?: number): number | null {
|
|
29
|
-
if (styleId) {
|
|
30
|
-
const normalized = styleId.toLowerCase();
|
|
31
|
-
const compact = normalized.replace(/[\s_-]+/g, "");
|
|
32
|
-
const headingMatch = /^heading([1-6])$/.exec(compact);
|
|
33
|
-
if (headingMatch) {
|
|
34
|
-
return Number.parseInt(headingMatch[1], 10);
|
|
35
|
-
}
|
|
36
|
-
if (compact === "title") return 1;
|
|
37
|
-
if (compact === "subtitle") return 2;
|
|
38
|
-
if (compact === "tocheading") return 1;
|
|
39
|
-
if (/^(appendix|schedule|annex|exhibit|attachment)(heading|title)$/.test(compact)) return 2;
|
|
40
|
-
}
|
|
41
|
-
if (
|
|
42
|
-
typeof outlineLevel === "number" &&
|
|
43
|
-
Number.isInteger(outlineLevel) &&
|
|
44
|
-
outlineLevel >= 0 &&
|
|
45
|
-
outlineLevel <= 5
|
|
46
|
-
) {
|
|
47
|
-
return outlineLevel + 1;
|
|
48
|
-
}
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function resolveMarkerJustificationCss(raw: string | undefined): string {
|
|
53
|
-
switch (raw) {
|
|
54
|
-
case "left":
|
|
55
|
-
return "flex-start";
|
|
56
|
-
case "center":
|
|
57
|
-
return "center";
|
|
58
|
-
case "right":
|
|
59
|
-
case "both":
|
|
60
|
-
case "distribute":
|
|
61
|
-
default:
|
|
62
|
-
return "flex-end";
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
// Style builders
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
|
|
70
|
-
/** Build CSSProperties for a paragraph block from spacing/indent/alignment. */
|
|
71
|
-
function buildParagraphStyle(
|
|
72
|
-
block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
|
|
73
|
-
): React.CSSProperties {
|
|
74
|
-
const style: React.CSSProperties = {};
|
|
75
|
-
|
|
76
|
-
// Alignment — direct takes precedence over resolvedParagraphFormatting
|
|
77
|
-
const rawAlignment = block.alignment ?? block.resolvedParagraphFormatting?.alignment;
|
|
78
|
-
const safeAlign = rawAlignment === "both" ? "justify" : rawAlignment;
|
|
79
|
-
if (safeAlign && SAFE_ALIGNMENT.has(safeAlign)) {
|
|
80
|
-
style.textAlign = safeAlign as React.CSSProperties["textAlign"];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Spacing
|
|
84
|
-
const spacingBefore =
|
|
85
|
-
block.spacing?.before ?? block.resolvedParagraphFormatting?.spacing?.before;
|
|
86
|
-
const spacingAfter =
|
|
87
|
-
block.spacing?.after ?? block.resolvedParagraphFormatting?.spacing?.after;
|
|
88
|
-
const lineSpacing =
|
|
89
|
-
block.spacing?.line ?? block.resolvedParagraphFormatting?.spacing?.line;
|
|
90
|
-
const lineRule =
|
|
91
|
-
block.spacing?.lineRule ?? block.resolvedParagraphFormatting?.spacing?.lineRule;
|
|
92
|
-
|
|
93
|
-
if (spacingBefore != null) {
|
|
94
|
-
style.marginTop = `${spacingBefore / 20}px`;
|
|
95
|
-
}
|
|
96
|
-
if (spacingAfter != null) {
|
|
97
|
-
style.marginBottom = `${spacingAfter / 20}px`;
|
|
98
|
-
}
|
|
99
|
-
if (lineSpacing && lineRule === "auto") {
|
|
100
|
-
style.lineHeight = String(lineSpacing / 240);
|
|
101
|
-
} else if (lineSpacing && lineRule === "exact") {
|
|
102
|
-
style.lineHeight = `${lineSpacing / 20}px`;
|
|
103
|
-
} else if (lineSpacing && lineRule === "atLeast") {
|
|
104
|
-
style.minHeight = `${lineSpacing / 20}px`;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Indentation
|
|
108
|
-
const indentLeft =
|
|
109
|
-
block.indentation?.left ?? block.resolvedParagraphFormatting?.indentation?.left;
|
|
110
|
-
const indentRight =
|
|
111
|
-
block.indentation?.right ?? block.resolvedParagraphFormatting?.indentation?.right;
|
|
112
|
-
const indentFirstLine =
|
|
113
|
-
block.indentation?.firstLine ?? block.resolvedParagraphFormatting?.indentation?.firstLine;
|
|
114
|
-
const indentHanging =
|
|
115
|
-
block.indentation?.hanging ?? block.resolvedParagraphFormatting?.indentation?.hanging;
|
|
116
|
-
|
|
117
|
-
if (indentLeft) style.paddingLeft = `${indentLeft / 20}px`;
|
|
118
|
-
if (indentRight) style.paddingRight = `${indentRight / 20}px`;
|
|
119
|
-
if (indentHanging) style.textIndent = `-${indentHanging / 20}px`;
|
|
120
|
-
else if (indentFirstLine) style.textIndent = `${indentFirstLine / 20}px`;
|
|
121
|
-
|
|
122
|
-
// Shading
|
|
123
|
-
const shadingFill = block.shading?.fill;
|
|
124
|
-
const shadingColor = safeHexColor(shadingFill);
|
|
125
|
-
if (shadingColor) style.backgroundColor = shadingColor;
|
|
126
|
-
|
|
127
|
-
// Page break visual indicator
|
|
128
|
-
if (block.pageBreakBefore) {
|
|
129
|
-
style.borderTop = "2px dashed rgba(0,0,0,0.1)";
|
|
130
|
-
style.paddingTop = "8px";
|
|
131
|
-
style.marginTop = "16px";
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return style;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Build CSSProperties for the numbering marker span. */
|
|
138
|
-
function buildMarkerStyle(
|
|
139
|
-
prefix: string,
|
|
140
|
-
suffix: "tab" | "space" | "nothing" | undefined,
|
|
141
|
-
markerRunProperties: CanonicalRunFormatting | undefined,
|
|
142
|
-
markerWidth: number | undefined,
|
|
143
|
-
markerJustification: string | undefined,
|
|
144
|
-
): React.CSSProperties {
|
|
145
|
-
const style: React.CSSProperties = {
|
|
146
|
-
fontVariantNumeric: "tabular-nums",
|
|
147
|
-
justifyContent: resolveMarkerJustificationCss(markerJustification),
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
if (markerRunProperties) {
|
|
151
|
-
if (markerRunProperties.bold) style.fontWeight = "bold";
|
|
152
|
-
if (markerRunProperties.italic) style.fontStyle = "italic";
|
|
153
|
-
if (markerRunProperties.underline && markerRunProperties.underline !== "none") {
|
|
154
|
-
style.textDecoration = "underline";
|
|
155
|
-
}
|
|
156
|
-
if (typeof markerRunProperties.fontSizeHalfPoints === "number") {
|
|
157
|
-
style.fontSize = `${markerRunProperties.fontSizeHalfPoints / 2}pt`;
|
|
158
|
-
}
|
|
159
|
-
const colorHex = markerRunProperties.colorHex;
|
|
160
|
-
if (colorHex && colorHex !== "auto") {
|
|
161
|
-
style.color = `#${colorHex.toLowerCase()}`;
|
|
162
|
-
}
|
|
163
|
-
const family = markerRunProperties.fontFamilyAscii ?? markerRunProperties.fontFamily;
|
|
164
|
-
if (family && SAFE_FONT_RE.test(family)) {
|
|
165
|
-
style.fontFamily = family;
|
|
166
|
-
}
|
|
167
|
-
} else {
|
|
168
|
-
style.color = "var(--color-text-tertiary)";
|
|
169
|
-
style.fontFamily = "var(--font-legal-sans)";
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const hasResolvedMarkerWidth = typeof markerWidth === "number" && markerWidth > 0;
|
|
173
|
-
if (hasResolvedMarkerWidth) {
|
|
174
|
-
const markerWidthPx = Math.max(1, Math.round(markerWidth! / 20));
|
|
175
|
-
style.width = `${markerWidthPx}px`;
|
|
176
|
-
style.minWidth = `${markerWidthPx}px`;
|
|
177
|
-
style.flexBasis = `${markerWidthPx}px`;
|
|
178
|
-
style.marginRight = 0;
|
|
179
|
-
style.overflow = "visible";
|
|
180
|
-
} else {
|
|
181
|
-
const fallbackMinWidth = Math.min(Math.max(prefix.length + 1, 4), 14);
|
|
182
|
-
const fallbackMarginRight =
|
|
183
|
-
suffix === "nothing" ? "0.25rem" : suffix === "space" ? "0.5rem" : "0.75rem";
|
|
184
|
-
style.minWidth = `${fallbackMinWidth}ch`;
|
|
185
|
-
style.marginRight = fallbackMarginRight;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return style;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/** Build CSSProperties for a text segment from marks and markAttrs. */
|
|
192
|
-
function buildSegmentStyle(
|
|
193
|
-
marks: SurfaceTextMark[] | undefined,
|
|
194
|
-
markAttrs?: {
|
|
195
|
-
fontSize?: number;
|
|
196
|
-
textColor?: string;
|
|
197
|
-
fontFamily?: string;
|
|
198
|
-
backgroundColor?: string;
|
|
199
|
-
charSpacing?: number;
|
|
200
|
-
},
|
|
201
|
-
): React.CSSProperties {
|
|
202
|
-
const style: React.CSSProperties = {};
|
|
203
|
-
|
|
204
|
-
if (marks) {
|
|
205
|
-
if (marks.includes("bold")) style.fontWeight = "bold";
|
|
206
|
-
if (marks.includes("italic")) style.fontStyle = "italic";
|
|
207
|
-
if (marks.includes("underline")) style.textDecoration = "underline";
|
|
208
|
-
if (marks.includes("strikethrough") || marks.includes("doubleStrikethrough")) {
|
|
209
|
-
style.textDecoration = marks.includes("underline")
|
|
210
|
-
? "underline line-through"
|
|
211
|
-
: "line-through";
|
|
212
|
-
}
|
|
213
|
-
if (marks.includes("superscript")) {
|
|
214
|
-
style.verticalAlign = "super";
|
|
215
|
-
style.fontSize = "smaller";
|
|
216
|
-
}
|
|
217
|
-
if (marks.includes("subscript")) {
|
|
218
|
-
style.verticalAlign = "sub";
|
|
219
|
-
style.fontSize = "smaller";
|
|
220
|
-
}
|
|
221
|
-
if (marks.includes("allCaps")) style.textTransform = "uppercase";
|
|
222
|
-
if (marks.includes("smallCaps")) style.fontVariant = "small-caps";
|
|
223
|
-
if (marks.includes("vanish")) style.display = "none";
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (markAttrs) {
|
|
227
|
-
if (markAttrs.fontSize) style.fontSize = `${markAttrs.fontSize}pt`;
|
|
228
|
-
if (markAttrs.textColor) style.color = markAttrs.textColor;
|
|
229
|
-
if (markAttrs.fontFamily && SAFE_FONT_RE.test(markAttrs.fontFamily)) {
|
|
230
|
-
style.fontFamily = markAttrs.fontFamily;
|
|
231
|
-
}
|
|
232
|
-
if (markAttrs.backgroundColor) style.backgroundColor = markAttrs.backgroundColor;
|
|
233
|
-
if (markAttrs.charSpacing) style.letterSpacing = `${markAttrs.charSpacing}px`;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return style;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function hasStyleEntries(style: React.CSSProperties): boolean {
|
|
240
|
-
return Object.keys(style).length > 0;
|
|
241
|
-
}
|
|
7
|
+
import {
|
|
8
|
+
buildMarkerStyle,
|
|
9
|
+
buildParagraphStyle,
|
|
10
|
+
buildSegmentStyle,
|
|
11
|
+
hasStyleEntries,
|
|
12
|
+
headingClassList,
|
|
13
|
+
resolveHeadingLevel,
|
|
14
|
+
} from "./tw-page-block-view.helpers.ts";
|
|
242
15
|
|
|
243
16
|
// ---------------------------------------------------------------------------
|
|
244
17
|
// Segment renderer
|
|
@@ -408,25 +181,6 @@ function ParagraphBlock({
|
|
|
408
181
|
);
|
|
409
182
|
}
|
|
410
183
|
|
|
411
|
-
function headingClassList(level: number): string[] {
|
|
412
|
-
switch (level) {
|
|
413
|
-
case 1:
|
|
414
|
-
return ["text-3xl", "font-semibold", "tracking-tight", "leading-tight"];
|
|
415
|
-
case 2:
|
|
416
|
-
return ["text-2xl", "font-semibold", "tracking-tight"];
|
|
417
|
-
case 3:
|
|
418
|
-
return ["text-xl", "font-medium"];
|
|
419
|
-
case 4:
|
|
420
|
-
return ["text-lg", "font-medium"];
|
|
421
|
-
case 5:
|
|
422
|
-
return ["text-base", "font-semibold", "uppercase", "tracking-[0.12em]"];
|
|
423
|
-
case 6:
|
|
424
|
-
return ["text-sm", "font-semibold", "uppercase", "tracking-[0.16em]"];
|
|
425
|
-
default:
|
|
426
|
-
return [];
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
184
|
/** Render a table block as <table>. */
|
|
431
185
|
function TableBlock({
|
|
432
186
|
block,
|
|
@@ -450,9 +204,9 @@ function TableBlock({
|
|
|
450
204
|
key={rowIdx}
|
|
451
205
|
style={
|
|
452
206
|
row.height != null && row.heightRule === "exact"
|
|
453
|
-
? { height: `${row.height / 20}
|
|
207
|
+
? { height: `${row.height / 20}pt` }
|
|
454
208
|
: row.height != null && row.heightRule === "atLeast"
|
|
455
|
-
? { minHeight: `${row.height / 20}
|
|
209
|
+
? { minHeight: `${row.height / 20}pt` }
|
|
456
210
|
: undefined
|
|
457
211
|
}
|
|
458
212
|
>
|
|
@@ -188,6 +188,11 @@ export interface TwProseMirrorSurfaceProps {
|
|
|
188
188
|
onUndo?: () => void;
|
|
189
189
|
onRedo?: () => void;
|
|
190
190
|
onBlockedInput?: (command: "paste" | "drop", message: string) => void;
|
|
191
|
+
onPasteApplied?: (meta: {
|
|
192
|
+
segmentCount: number;
|
|
193
|
+
charCount: number;
|
|
194
|
+
source: "paste" | "drop";
|
|
195
|
+
}) => void;
|
|
191
196
|
onCommentActivated?: (commentId: string) => void;
|
|
192
197
|
onRevisionActivated?: (revisionId: string) => void;
|
|
193
198
|
onSelectionToolbarAnchorChange?: (anchor: SelectionToolbarAnchor | null) => void;
|
|
@@ -321,6 +326,9 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
321
326
|
onBlockedInput: (command, message) => {
|
|
322
327
|
props.onBlockedInput?.(command, message);
|
|
323
328
|
},
|
|
329
|
+
onPasteApplied: (meta) => {
|
|
330
|
+
props.onPasteApplied?.(meta);
|
|
331
|
+
},
|
|
324
332
|
onSelectionChange: (sel) => {
|
|
325
333
|
pendingSelectionProbeRef.current = startPerfProbe("selection");
|
|
326
334
|
props.onSelectionChange?.(
|
|
@@ -448,6 +456,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
448
456
|
onUndo: () => callbacksRef.current?.onUndo(),
|
|
449
457
|
onRedo: () => callbacksRef.current?.onRedo(),
|
|
450
458
|
onBlockedInput: (command, message) => callbacksRef.current?.onBlockedInput?.(command, message),
|
|
459
|
+
onPasteApplied: (meta) => callbacksRef.current?.onPasteApplied?.(meta),
|
|
451
460
|
onCompositionChange: (composing) => {
|
|
452
461
|
sessionRef.current?.setComposing(composing);
|
|
453
462
|
},
|
|
@@ -294,6 +294,72 @@ function applyTableAttrs(table: HTMLTableElement, node: PMNode): void {
|
|
|
294
294
|
} else if (alignment === "right") {
|
|
295
295
|
table.style.marginLeft = "auto";
|
|
296
296
|
}
|
|
297
|
+
syncColgroup(table, node);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* P6.a: real `<colgroup>` driven by the table's canonical `gridColumns`.
|
|
302
|
+
*
|
|
303
|
+
* Pre-P6 tables relied on per-row padding cells (`data-row-padding`) to
|
|
304
|
+
* approximate column widths. That produced correct-looking tables on
|
|
305
|
+
* simple CCEP fixtures but broke as soon as a row used `w:gridBefore` /
|
|
306
|
+
* `w:gridAfter` or a cell spanned multiple logical columns with
|
|
307
|
+
* non-uniform widths. HTML's `<colgroup>` is the canonical place to
|
|
308
|
+
* declare per-column widths; every browser honors it exactly and we
|
|
309
|
+
* avoid the padding-cell trick entirely for width purposes.
|
|
310
|
+
*
|
|
311
|
+
* Each `<col>` uses its twips width converted to `pt` via `twips / 20`
|
|
312
|
+
* (the same unit the row-padding helper already uses for its
|
|
313
|
+
* `style.width`). Keeping `pt` units avoids tying the NodeView to a
|
|
314
|
+
* pxPerTwip value — the CSS `zoom` applied at the workspace root
|
|
315
|
+
* rescales `pt` alongside every other length, so the columns follow
|
|
316
|
+
* zoom automatically.
|
|
317
|
+
*
|
|
318
|
+
* When `gridColumns` is empty (malformed input, or a table built
|
|
319
|
+
* programmatically without a declared grid), the colgroup is removed
|
|
320
|
+
* so the browser falls back to auto-sizing.
|
|
321
|
+
*/
|
|
322
|
+
function syncColgroup(table: HTMLTableElement, node: PMNode): void {
|
|
323
|
+
const gridColumns = Array.isArray(node.attrs.gridColumns)
|
|
324
|
+
? (node.attrs.gridColumns as number[])
|
|
325
|
+
: [];
|
|
326
|
+
const existing = Array.from(table.children).find(
|
|
327
|
+
(child): child is HTMLTableColElement =>
|
|
328
|
+
child instanceof (table.ownerDocument?.defaultView?.HTMLTableColElement ??
|
|
329
|
+
HTMLTableColElement) && child.tagName === "COLGROUP",
|
|
330
|
+
) as HTMLElement | undefined;
|
|
331
|
+
|
|
332
|
+
if (gridColumns.length === 0) {
|
|
333
|
+
existing?.remove();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const owner = table.ownerDocument ?? document;
|
|
338
|
+
const colgroup = existing ?? owner.createElement("colgroup");
|
|
339
|
+
colgroup.setAttribute("data-pm-table-colgroup", "true");
|
|
340
|
+
// Reuse existing <col> children where possible; add/remove tail cols.
|
|
341
|
+
const desired = gridColumns.length;
|
|
342
|
+
const actual = colgroup.childElementCount;
|
|
343
|
+
for (let i = actual; i < desired; i += 1) {
|
|
344
|
+
colgroup.appendChild(owner.createElement("col"));
|
|
345
|
+
}
|
|
346
|
+
while (colgroup.childElementCount > desired) {
|
|
347
|
+
colgroup.lastElementChild?.remove();
|
|
348
|
+
}
|
|
349
|
+
for (let i = 0; i < desired; i += 1) {
|
|
350
|
+
const col = colgroup.children[i] as HTMLTableColElement;
|
|
351
|
+
const twips = gridColumns[i] ?? 0;
|
|
352
|
+
col.setAttribute("data-col-index", String(i));
|
|
353
|
+
col.setAttribute("data-col-twips", String(twips));
|
|
354
|
+
col.style.width = twips > 0 ? `${twips / 20}pt` : "";
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (!existing) {
|
|
358
|
+
// Colgroup must be the first child of <table> per HTML spec so the
|
|
359
|
+
// column widths propagate to every row. `insertBefore(first)`
|
|
360
|
+
// handles both empty and populated tables uniformly.
|
|
361
|
+
table.insertBefore(colgroup, table.firstChild);
|
|
362
|
+
}
|
|
297
363
|
}
|
|
298
364
|
|
|
299
365
|
function applyRowAttrs(row: HTMLTableRowElement, node: PMNode): void {
|