@beyondwork/docx-react-component 1.0.38 → 1.0.40
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 +41 -31
- package/src/api/public-types.ts +305 -6
- package/src/core/commands/table-structure-commands.ts +31 -2
- package/src/core/commands/text-commands.ts +122 -2
- package/src/index.ts +9 -0
- package/src/io/docx-session.ts +1 -0
- package/src/io/export/serialize-numbering.ts +42 -8
- package/src/io/export/serialize-paragraph-formatting.ts +152 -0
- package/src/io/export/serialize-run-formatting.ts +90 -0
- package/src/io/export/serialize-styles.ts +212 -0
- package/src/io/ooxml/parse-fields.ts +10 -3
- package/src/io/ooxml/parse-numbering.ts +41 -1
- package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
- package/src/io/ooxml/parse-run-formatting.ts +129 -0
- package/src/io/ooxml/parse-styles.ts +31 -0
- package/src/io/ooxml/xml-attr-helpers.ts +60 -0
- package/src/io/ooxml/xml-element.ts +19 -0
- package/src/model/canonical-document.ts +83 -3
- package/src/runtime/collab/event-types.ts +165 -0
- package/src/runtime/collab/index.ts +22 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
- package/src/runtime/collab/runtime-collab-sync.ts +273 -0
- package/src/runtime/document-runtime.ts +141 -18
- package/src/runtime/layout/docx-font-loader.ts +30 -11
- package/src/runtime/layout/index.ts +2 -0
- package/src/runtime/layout/inert-layout-facet.ts +3 -0
- package/src/runtime/layout/layout-engine-instance.ts +69 -2
- package/src/runtime/layout/layout-invalidation.ts +14 -5
- package/src/runtime/layout/page-graph.ts +36 -0
- package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
- package/src/runtime/layout/paginated-layout-engine.ts +342 -28
- package/src/runtime/layout/project-block-fragments.ts +154 -20
- package/src/runtime/layout/public-facet.ts +81 -1
- package/src/runtime/layout/resolve-page-fields.ts +70 -0
- package/src/runtime/layout/resolve-page-previews.ts +185 -0
- package/src/runtime/layout/resolved-formatting-state.ts +30 -26
- package/src/runtime/layout/table-render-plan.ts +21 -1
- package/src/runtime/numbering-prefix.ts +5 -0
- package/src/runtime/paragraph-style-resolver.ts +194 -0
- package/src/runtime/render/render-kernel.ts +5 -1
- package/src/runtime/resolved-numbering-geometry.ts +9 -1
- package/src/runtime/surface-projection.ts +129 -9
- package/src/runtime/table-schema.ts +11 -0
- package/src/runtime/workflow-rail-segments.ts +149 -1
- package/src/ui/WordReviewEditor.tsx +302 -5
- package/src/ui/editor-command-bag.ts +4 -0
- package/src/ui/editor-runtime-boundary.ts +16 -0
- package/src/ui/editor-shell-view.tsx +22 -0
- package/src/ui/editor-surface-controller.tsx +9 -1
- package/src/ui/headless/chrome-registry.ts +34 -5
- package/src/ui/headless/scoped-chrome-policy.ts +29 -0
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
- package/src/ui-tailwind/chrome/role-action-sets.ts +14 -8
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +80 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +7 -10
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +101 -21
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +353 -0
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +5 -1
- package/src/ui-tailwind/chrome-overlay/index.ts +2 -6
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +82 -18
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +133 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +386 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +140 -69
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +28 -3
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +7 -2
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +170 -63
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -78
- package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
- package/src/ui-tailwind/index.ts +6 -5
- package/src/ui-tailwind/theme/editor-theme.css +108 -15
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +122 -1
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +36 -3
- package/src/ui-tailwind/tw-review-workspace.tsx +207 -54
- package/src/runtime/collab-review-sync.ts +0 -254
- package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +0 -95
- package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { InsertTableOptions } from "../../api/public-types";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
DocumentRootNode,
|
|
4
|
+
ParagraphNode,
|
|
5
|
+
ParagraphStyleDefinition,
|
|
6
|
+
StylesCatalog,
|
|
7
|
+
} from "../../model/canonical-document.ts";
|
|
3
8
|
import {
|
|
4
9
|
createSelectionSnapshot,
|
|
5
10
|
type CanonicalDocumentEnvelope,
|
|
@@ -18,11 +23,102 @@ import {
|
|
|
18
23
|
resolveParagraphScope,
|
|
19
24
|
type StructuralMutationResult,
|
|
20
25
|
} from "./structural-helpers.ts";
|
|
26
|
+
import { createEditorSurfaceSnapshot } from "../../runtime/surface-projection.ts";
|
|
21
27
|
|
|
22
28
|
export interface TextCommandContext {
|
|
23
29
|
timestamp: string;
|
|
24
30
|
}
|
|
25
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Walk the `basedOn` chain of paragraph styles looking for a `nextStyle`
|
|
34
|
+
* definition. Returns the resolved `nextStyle` id, or undefined if none is
|
|
35
|
+
* found. Caps the walk at 32 steps to guard against circular `basedOn` chains.
|
|
36
|
+
*/
|
|
37
|
+
function resolveNextStyle(
|
|
38
|
+
styleId: string | undefined,
|
|
39
|
+
catalog: StylesCatalog,
|
|
40
|
+
): string | undefined {
|
|
41
|
+
if (!styleId) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
let current: string | undefined = styleId;
|
|
45
|
+
let steps = 0;
|
|
46
|
+
while (current && steps < 32) {
|
|
47
|
+
steps += 1;
|
|
48
|
+
const def: ParagraphStyleDefinition | undefined = catalog.paragraphs[current];
|
|
49
|
+
if (!def) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
if (def.nextStyle && catalog.paragraphs[def.nextStyle]) {
|
|
53
|
+
return def.nextStyle;
|
|
54
|
+
}
|
|
55
|
+
current = def.basedOn;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Given the result document and the head of the new selection (which sits at
|
|
62
|
+
* the start of the newly-created paragraph), locate that paragraph in the
|
|
63
|
+
* top-level `doc` children and return a new document with its `styleId` set to
|
|
64
|
+
* `nextStyleId` and its `numbering` cleared.
|
|
65
|
+
*/
|
|
66
|
+
function applyNextStyleToNewParagraph(
|
|
67
|
+
result: TextTransactionResult,
|
|
68
|
+
nextStyleId: string,
|
|
69
|
+
): TextTransactionResult {
|
|
70
|
+
const root = result.document.content as DocumentRootNode;
|
|
71
|
+
if (!root || root.type !== "doc") {
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const head = result.selection.head;
|
|
76
|
+
const surface = createEditorSurfaceSnapshot(result.document, result.selection);
|
|
77
|
+
|
|
78
|
+
let targetBlockIndex = -1;
|
|
79
|
+
for (let i = 0; i < surface.blocks.length; i += 1) {
|
|
80
|
+
const surfaceBlock = surface.blocks[i];
|
|
81
|
+
if (
|
|
82
|
+
surfaceBlock?.kind === "paragraph" &&
|
|
83
|
+
surfaceBlock.from === head
|
|
84
|
+
) {
|
|
85
|
+
targetBlockIndex = i;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (targetBlockIndex === -1) {
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const targetBlock = root.children[targetBlockIndex];
|
|
95
|
+
if (!targetBlock || targetBlock.type !== "paragraph") {
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const { numbering: _numbering, ...restProps } = targetBlock;
|
|
100
|
+
const updatedParagraph: ParagraphNode = {
|
|
101
|
+
...restProps,
|
|
102
|
+
styleId: nextStyleId,
|
|
103
|
+
children: targetBlock.children,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
...result,
|
|
108
|
+
document: {
|
|
109
|
+
...result.document,
|
|
110
|
+
content: {
|
|
111
|
+
...root,
|
|
112
|
+
children: [
|
|
113
|
+
...root.children.slice(0, targetBlockIndex),
|
|
114
|
+
updatedParagraph,
|
|
115
|
+
...root.children.slice(targetBlockIndex + 1),
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
26
122
|
export function insertText(
|
|
27
123
|
document: CanonicalDocumentEnvelope,
|
|
28
124
|
selection: SelectionSnapshot,
|
|
@@ -122,7 +218,11 @@ export function splitParagraph(
|
|
|
122
218
|
selection: SelectionSnapshot,
|
|
123
219
|
context: TextCommandContext,
|
|
124
220
|
): TextTransactionResult {
|
|
125
|
-
|
|
221
|
+
// Resolve the current paragraph's styleId before the split so we can look up
|
|
222
|
+
// `nextStyle` from the styles catalog.
|
|
223
|
+
const scope = resolveParagraphScope(document, selection);
|
|
224
|
+
|
|
225
|
+
const result = applyTextTransaction(
|
|
126
226
|
document,
|
|
127
227
|
selection,
|
|
128
228
|
{
|
|
@@ -131,6 +231,26 @@ export function splitParagraph(
|
|
|
131
231
|
},
|
|
132
232
|
context,
|
|
133
233
|
);
|
|
234
|
+
|
|
235
|
+
// Only apply nextStyle for top-level paragraphs; table-cell traversal
|
|
236
|
+
// would require walking into nested blocks which the surface snapshot doesn't expose.
|
|
237
|
+
if (scope?.kind !== "top-level") {
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const originalStyleId = scope.paragraph.styleId;
|
|
242
|
+
const nextStyleId =
|
|
243
|
+
originalStyleId !== undefined
|
|
244
|
+
? resolveNextStyle(originalStyleId, document.styles)
|
|
245
|
+
: undefined;
|
|
246
|
+
|
|
247
|
+
// If the original paragraph's style specifies a `nextStyle`, apply it to the
|
|
248
|
+
// newly-created paragraph (the one at result.selection.head).
|
|
249
|
+
if (nextStyleId !== undefined) {
|
|
250
|
+
return applyNextStyleToNewParagraph(result, nextStyleId);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return result;
|
|
134
254
|
}
|
|
135
255
|
|
|
136
256
|
export function insertPageBreak(
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,8 @@ export {
|
|
|
7
7
|
validateEditorSessionState,
|
|
8
8
|
EDITOR_SESSION_STATE_VERSION,
|
|
9
9
|
} from "./api/session-state.ts";
|
|
10
|
+
// R2 — issue metadata id for scope-card-overlay P1.
|
|
11
|
+
export { ISSUE_METADATA_ID } from "./api/public-types.ts";
|
|
10
12
|
export type {
|
|
11
13
|
LoadRequest,
|
|
12
14
|
LoadSourcePolicy,
|
|
@@ -104,6 +106,13 @@ export type {
|
|
|
104
106
|
WorkflowMetadataDefinition,
|
|
105
107
|
WorkflowMetadataEntry,
|
|
106
108
|
WorkflowMetadataSnapshot,
|
|
109
|
+
// R2 — issue metadata (scope-card-overlay P1)
|
|
110
|
+
IssueSeverity,
|
|
111
|
+
IssueMode,
|
|
112
|
+
IssueOwner,
|
|
113
|
+
IssueMetadataValue,
|
|
114
|
+
ScopeIssueAction,
|
|
115
|
+
ScopeCardModel,
|
|
107
116
|
WorkflowBlockedCommandReason,
|
|
108
117
|
WorkflowScopeSnapshot,
|
|
109
118
|
InteractionGuardSnapshot,
|
package/src/io/docx-session.ts
CHANGED
|
@@ -1560,6 +1560,7 @@ function filterValidStyleIds(
|
|
|
1560
1560
|
characters: filterRecord(catalog.characters),
|
|
1561
1561
|
tables: filterRecord(catalog.tables),
|
|
1562
1562
|
...(catalog.latentStyles ? { latentStyles: catalog.latentStyles } : {}),
|
|
1563
|
+
...(catalog.docDefaults ? { docDefaults: catalog.docDefaults } : {}),
|
|
1563
1564
|
...(catalog.fromPackage !== undefined ? { fromPackage: catalog.fromPackage } : {}),
|
|
1564
1565
|
};
|
|
1565
1566
|
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
isSyntheticDocxNullAbstractDefinition,
|
|
4
4
|
isSyntheticDocxNullNumberingInstance,
|
|
5
5
|
} from "../ooxml/numbering-sentinels.ts";
|
|
6
|
+
import { buildRunPropertiesXml } from "./serialize-run-formatting.ts";
|
|
6
7
|
import { twip } from "./twip.ts";
|
|
7
8
|
|
|
8
9
|
export const WORD_NUMBERING_CONTENT_TYPE =
|
|
@@ -62,35 +63,61 @@ function serializeAbstractDefinition(definition: NumberingCatalog["abstractDefin
|
|
|
62
63
|
stripCanonicalPrefix(definition.abstractNumberingId, "abstract-num:"),
|
|
63
64
|
),
|
|
64
65
|
);
|
|
66
|
+
|
|
67
|
+
// ECMA-376 abstractNum child order: nsid, multiLevelType, tmpl, styleLink,
|
|
68
|
+
// numStyleLink — all before the <w:lvl> children.
|
|
69
|
+
const nsid = definition.nsid
|
|
70
|
+
? `<w:nsid w:val="${escapeAttribute(definition.nsid)}"/>`
|
|
71
|
+
: "";
|
|
72
|
+
const multiLevelType = definition.multiLevelType
|
|
73
|
+
? `<w:multiLevelType w:val="${escapeAttribute(definition.multiLevelType)}"/>`
|
|
74
|
+
: "";
|
|
75
|
+
const tmpl = definition.tplc
|
|
76
|
+
? `<w:tmpl w:val="${escapeAttribute(definition.tplc)}"/>`
|
|
77
|
+
: "";
|
|
78
|
+
const styleLink = definition.styleLink
|
|
79
|
+
? `<w:styleLink w:val="${escapeAttribute(definition.styleLink)}"/>`
|
|
80
|
+
: "";
|
|
81
|
+
const numStyleLink = definition.numStyleLink
|
|
82
|
+
? `<w:numStyleLink w:val="${escapeAttribute(definition.numStyleLink)}"/>`
|
|
83
|
+
: "";
|
|
84
|
+
|
|
65
85
|
const levels = [...definition.levels]
|
|
66
86
|
.sort((left, right) => left.level - right.level)
|
|
67
87
|
.map((level) => serializeLevel(level))
|
|
68
88
|
.join("");
|
|
69
89
|
|
|
70
|
-
return `<w:abstractNum w:abstractNumId="${abstractNumId}">${levels}</w:abstractNum>`;
|
|
90
|
+
return `<w:abstractNum w:abstractNumId="${abstractNumId}">${nsid}${multiLevelType}${tmpl}${styleLink}${numStyleLink}${levels}</w:abstractNum>`;
|
|
71
91
|
}
|
|
72
92
|
|
|
73
93
|
function serializeLevel(
|
|
74
94
|
level: NumberingCatalog["abstractDefinitions"][string]["levels"][number],
|
|
75
95
|
serializedLevel = level.level,
|
|
76
96
|
): string {
|
|
97
|
+
// ECMA-376 canonical lvl child order:
|
|
98
|
+
// start, numFmt, lvlRestart, pStyle, isLgl, suff, lvlText, lvlJc, pPr, rPr
|
|
77
99
|
const start =
|
|
78
100
|
level.startAt !== undefined
|
|
79
101
|
? `<w:start w:val="${clampStart(level.startAt)}"/>`
|
|
80
102
|
: "";
|
|
103
|
+
const numFmt = `<w:numFmt w:val="${escapeAttribute(level.format)}"/>`;
|
|
104
|
+
const lvlRestart =
|
|
105
|
+
level.restartAfterLevel !== undefined
|
|
106
|
+
? `<w:lvlRestart w:val="${level.restartAfterLevel}"/>`
|
|
107
|
+
: "";
|
|
81
108
|
const paragraphStyle = level.paragraphStyleId
|
|
82
109
|
? `<w:pStyle w:val="${escapeAttribute(level.paragraphStyleId)}"/>`
|
|
83
110
|
: "";
|
|
84
111
|
const isLegal = level.isLegalNumbering ? "<w:isLgl/>" : "";
|
|
85
112
|
const suffix = level.suffix ? `<w:suff w:val="${escapeAttribute(level.suffix)}"/>` : "";
|
|
113
|
+
const lvlText = `<w:lvlText w:val="${escapeAttribute(level.text)}"/>`;
|
|
86
114
|
const justification = level.paragraphGeometry?.justification
|
|
87
115
|
? `<w:lvlJc w:val="${escapeAttribute(level.paragraphGeometry.justification)}"/>`
|
|
88
116
|
: "";
|
|
89
117
|
const paragraphProperties = serializeLevelParagraphGeometry(level.paragraphGeometry);
|
|
118
|
+
const runProperties = buildRunPropertiesXml(level.runProperties);
|
|
90
119
|
|
|
91
|
-
return `<w:lvl w:ilvl="${clampIlvl(serializedLevel)}">${start}
|
|
92
|
-
level.format,
|
|
93
|
-
)}"/><w:lvlText w:val="${escapeAttribute(level.text)}"/>${paragraphStyle}${isLegal}${suffix}${justification}${paragraphProperties}</w:lvl>`;
|
|
120
|
+
return `<w:lvl w:ilvl="${clampIlvl(serializedLevel)}">${start}${numFmt}${lvlRestart}${paragraphStyle}${isLegal}${suffix}${lvlText}${justification}${paragraphProperties}${runProperties}</w:lvl>`;
|
|
94
121
|
}
|
|
95
122
|
|
|
96
123
|
function serializeLevelOverride(
|
|
@@ -101,14 +128,17 @@ function serializeLevelOverride(
|
|
|
101
128
|
return "";
|
|
102
129
|
}
|
|
103
130
|
|
|
131
|
+
// ECMA-376 canonical lvl child order (override subset):
|
|
132
|
+
// start, numFmt, lvlRestart, pStyle, isLgl, suff, lvlText, lvlJc, pPr, rPr
|
|
104
133
|
const start =
|
|
105
134
|
level.startAt !== undefined
|
|
106
135
|
? `<w:start w:val="${clampStart(level.startAt)}"/>`
|
|
107
136
|
: "";
|
|
108
137
|
const format = level.format ? `<w:numFmt w:val="${escapeAttribute(level.format)}"/>` : "";
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
138
|
+
const lvlRestart =
|
|
139
|
+
level.restartAfterLevel !== undefined
|
|
140
|
+
? `<w:lvlRestart w:val="${level.restartAfterLevel}"/>`
|
|
141
|
+
: "";
|
|
112
142
|
const paragraphStyle = level.paragraphStyleId
|
|
113
143
|
? `<w:pStyle w:val="${escapeAttribute(level.paragraphStyleId)}"/>`
|
|
114
144
|
: "";
|
|
@@ -124,11 +154,15 @@ function serializeLevelOverride(
|
|
|
124
154
|
? `<w:isLgl w:val="false"/>`
|
|
125
155
|
: "";
|
|
126
156
|
const suffix = level.suffix ? `<w:suff w:val="${escapeAttribute(level.suffix)}"/>` : "";
|
|
157
|
+
const text = level.text !== undefined
|
|
158
|
+
? `<w:lvlText w:val="${escapeAttribute(level.text)}"/>`
|
|
159
|
+
: "";
|
|
127
160
|
const justification = level.paragraphGeometry?.justification
|
|
128
161
|
? `<w:lvlJc w:val="${escapeAttribute(level.paragraphGeometry.justification)}"/>`
|
|
129
162
|
: "";
|
|
130
163
|
const paragraphProperties = serializeLevelParagraphGeometry(level.paragraphGeometry);
|
|
131
|
-
const
|
|
164
|
+
const runProperties = buildRunPropertiesXml(level.runProperties);
|
|
165
|
+
const body = `${start}${format}${lvlRestart}${paragraphStyle}${isLegal}${suffix}${text}${justification}${paragraphProperties}${runProperties}`;
|
|
132
166
|
|
|
133
167
|
return body.length > 0
|
|
134
168
|
? `<w:lvl w:ilvl="${clampIlvl(serializedLevel)}">${body}</w:lvl>`
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize a `CanonicalParagraphFormatting` back into an OOXML `<w:pPr>` fragment.
|
|
3
|
+
* Returns empty when the input has no fields. Emits elements in ECMA-376 canonical
|
|
4
|
+
* order so the OpenXML SDK validator accepts the output.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
CanonicalParagraphFormatting,
|
|
9
|
+
ParagraphBorders,
|
|
10
|
+
ParagraphIndentation,
|
|
11
|
+
ParagraphShading,
|
|
12
|
+
ParagraphSpacing,
|
|
13
|
+
TabStop,
|
|
14
|
+
} from "../../model/canonical-document.ts";
|
|
15
|
+
import { buildRunPropertiesXml } from "./serialize-run-formatting.ts";
|
|
16
|
+
|
|
17
|
+
function escXml(value: string): string {
|
|
18
|
+
return value
|
|
19
|
+
.replace(/&/g, "&")
|
|
20
|
+
.replace(/</g, "<")
|
|
21
|
+
.replace(/>/g, ">")
|
|
22
|
+
.replace(/"/g, """);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toggleEl(tag: string, value: boolean | undefined): string {
|
|
26
|
+
if (value === undefined) return "";
|
|
27
|
+
return value ? `<w:${tag}/>` : `<w:${tag} w:val="0"/>`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function borderAttrs(b: {
|
|
31
|
+
value?: string;
|
|
32
|
+
size?: number;
|
|
33
|
+
space?: number;
|
|
34
|
+
color?: string;
|
|
35
|
+
}): string {
|
|
36
|
+
const attrs: string[] = [];
|
|
37
|
+
if (b.value) attrs.push(`w:val="${escXml(b.value)}"`);
|
|
38
|
+
if (b.size !== undefined) attrs.push(`w:sz="${b.size}"`);
|
|
39
|
+
if (b.space !== undefined) attrs.push(`w:space="${b.space}"`);
|
|
40
|
+
if (b.color) attrs.push(`w:color="${escXml(b.color)}"`);
|
|
41
|
+
return attrs.join(" ");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildBordersXml(b: ParagraphBorders | undefined): string {
|
|
45
|
+
if (!b) return "";
|
|
46
|
+
const sides = ["top", "left", "bottom", "right", "between", "bar"] as const;
|
|
47
|
+
const parts: string[] = [];
|
|
48
|
+
for (const side of sides) {
|
|
49
|
+
const spec = b[side];
|
|
50
|
+
if (!spec) continue;
|
|
51
|
+
const attrs = borderAttrs(spec);
|
|
52
|
+
if (attrs) parts.push(`<w:${side} ${attrs}/>`);
|
|
53
|
+
}
|
|
54
|
+
return parts.length > 0 ? `<w:pBdr>${parts.join("")}</w:pBdr>` : "";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function buildShadingXml(s: ParagraphShading | undefined): string {
|
|
58
|
+
if (!s) return "";
|
|
59
|
+
const attrs: string[] = [];
|
|
60
|
+
if (s.val) attrs.push(`w:val="${escXml(s.val)}"`);
|
|
61
|
+
if (s.color) attrs.push(`w:color="${escXml(s.color)}"`);
|
|
62
|
+
if (s.fill) attrs.push(`w:fill="${escXml(s.fill)}"`);
|
|
63
|
+
return attrs.length > 0 ? `<w:shd ${attrs.join(" ")}/>` : "";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildTabsXml(tabs: TabStop[] | undefined): string {
|
|
67
|
+
if (!tabs || tabs.length === 0) return "";
|
|
68
|
+
const parts = tabs.map((t) => {
|
|
69
|
+
// Canonical "middleDot" → OOXML "middledot"
|
|
70
|
+
const leader = t.leader === "middleDot" ? "middledot" : t.leader;
|
|
71
|
+
const attrs: string[] = [`w:val="${escXml(t.align)}"`, `w:pos="${t.position}"`];
|
|
72
|
+
if (leader) attrs.push(`w:leader="${escXml(leader)}"`);
|
|
73
|
+
return `<w:tab ${attrs.join(" ")}/>`;
|
|
74
|
+
});
|
|
75
|
+
return `<w:tabs>${parts.join("")}</w:tabs>`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildSpacingXml(s: ParagraphSpacing | undefined): string {
|
|
79
|
+
if (!s) return "";
|
|
80
|
+
const attrs: string[] = [];
|
|
81
|
+
if (s.before !== undefined) attrs.push(`w:before="${s.before}"`);
|
|
82
|
+
if (s.after !== undefined) attrs.push(`w:after="${s.after}"`);
|
|
83
|
+
if (s.line !== undefined) attrs.push(`w:line="${s.line}"`);
|
|
84
|
+
if (s.lineRule) attrs.push(`w:lineRule="${escXml(s.lineRule)}"`);
|
|
85
|
+
return attrs.length > 0 ? `<w:spacing ${attrs.join(" ")}/>` : "";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildIndentXml(i: ParagraphIndentation | undefined): string {
|
|
89
|
+
if (!i) return "";
|
|
90
|
+
const attrs: string[] = [];
|
|
91
|
+
if (i.left !== undefined) attrs.push(`w:left="${i.left}"`);
|
|
92
|
+
if (i.right !== undefined) attrs.push(`w:right="${i.right}"`);
|
|
93
|
+
if (i.firstLine !== undefined) attrs.push(`w:firstLine="${i.firstLine}"`);
|
|
94
|
+
if (i.hanging !== undefined) attrs.push(`w:hanging="${i.hanging}"`);
|
|
95
|
+
return attrs.length > 0 ? `<w:ind ${attrs.join(" ")}/>` : "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function buildParagraphPropertiesXml(
|
|
99
|
+
pPr: CanonicalParagraphFormatting | undefined,
|
|
100
|
+
): string {
|
|
101
|
+
if (!pPr) return "";
|
|
102
|
+
const parts: string[] = [];
|
|
103
|
+
|
|
104
|
+
// ECMA-376 canonical pPr child order (subset we model):
|
|
105
|
+
// 1. keepNext, keepLines, pageBreakBefore
|
|
106
|
+
parts.push(toggleEl("keepNext", pPr.keepNext));
|
|
107
|
+
parts.push(toggleEl("keepLines", pPr.keepLines));
|
|
108
|
+
parts.push(toggleEl("pageBreakBefore", pPr.pageBreakBefore));
|
|
109
|
+
|
|
110
|
+
// 4. pBdr
|
|
111
|
+
parts.push(buildBordersXml(pPr.borders));
|
|
112
|
+
|
|
113
|
+
// 5. shd
|
|
114
|
+
parts.push(buildShadingXml(pPr.shading));
|
|
115
|
+
|
|
116
|
+
// 6. tabs
|
|
117
|
+
parts.push(buildTabsXml(pPr.tabStops));
|
|
118
|
+
|
|
119
|
+
// 7. spacing
|
|
120
|
+
parts.push(buildSpacingXml(pPr.spacing));
|
|
121
|
+
|
|
122
|
+
// 8. ind
|
|
123
|
+
parts.push(buildIndentXml(pPr.indentation));
|
|
124
|
+
|
|
125
|
+
// 9. contextualSpacing
|
|
126
|
+
parts.push(toggleEl("contextualSpacing", pPr.contextualSpacing));
|
|
127
|
+
|
|
128
|
+
// 10. widowControl
|
|
129
|
+
parts.push(toggleEl("widowControl", pPr.widowControl));
|
|
130
|
+
|
|
131
|
+
// 11. suppressLineNumbers, suppressAutoHyphens
|
|
132
|
+
parts.push(toggleEl("suppressLineNumbers", pPr.suppressLineNumbers));
|
|
133
|
+
parts.push(toggleEl("suppressAutoHyphens", pPr.suppressAutoHyphens));
|
|
134
|
+
|
|
135
|
+
// 12. bidi
|
|
136
|
+
parts.push(toggleEl("bidi", pPr.bidi));
|
|
137
|
+
|
|
138
|
+
// 13. jc
|
|
139
|
+
if (pPr.alignment) parts.push(`<w:jc w:val="${escXml(pPr.alignment)}"/>`);
|
|
140
|
+
|
|
141
|
+
// 14. outlineLvl
|
|
142
|
+
if (pPr.outlineLevel !== undefined) parts.push(`<w:outlineLvl w:val="${pPr.outlineLevel}"/>`);
|
|
143
|
+
|
|
144
|
+
// 15. rPr (paragraph mark)
|
|
145
|
+
if (pPr.paragraphMarkRunProperties) {
|
|
146
|
+
const markXml = buildRunPropertiesXml(pPr.paragraphMarkRunProperties);
|
|
147
|
+
if (markXml) parts.push(markXml);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const body = parts.filter(Boolean).join("");
|
|
151
|
+
return body.length > 0 ? `<w:pPr>${body}</w:pPr>` : "";
|
|
152
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize a `CanonicalRunFormatting` back into an OOXML `<w:rPr>` fragment.
|
|
3
|
+
* Returns an empty string when the input has no fields, so callers can safely
|
|
4
|
+
* concatenate without emitting a `<w:rPr/>` husk.
|
|
5
|
+
*
|
|
6
|
+
* Elements are emitted in ECMA-376 canonical order so the OpenXML SDK
|
|
7
|
+
* validator accepts the output.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CanonicalRunFormatting } from "../../model/canonical-document.ts";
|
|
11
|
+
|
|
12
|
+
function escXml(value: string): string {
|
|
13
|
+
return value
|
|
14
|
+
.replace(/&/g, "&")
|
|
15
|
+
.replace(/</g, "<")
|
|
16
|
+
.replace(/>/g, ">")
|
|
17
|
+
.replace(/"/g, """);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function toggleEl(tag: string, value: boolean | undefined): string {
|
|
21
|
+
if (value === undefined) return "";
|
|
22
|
+
return value ? `<w:${tag}/>` : `<w:${tag} w:val="0"/>`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildRunPropertiesXml(rPr: CanonicalRunFormatting | undefined): string {
|
|
26
|
+
if (!rPr) return "";
|
|
27
|
+
const parts: string[] = [];
|
|
28
|
+
|
|
29
|
+
// 1. rStyle
|
|
30
|
+
if (rPr.characterStyleId) parts.push(`<w:rStyle w:val="${escXml(rPr.characterStyleId)}"/>`);
|
|
31
|
+
|
|
32
|
+
// 2. rFonts
|
|
33
|
+
if (rPr.fontFamilyAscii || rPr.fontFamilyHAnsi || rPr.fontFamilyEastAsia || rPr.fontFamilyCs) {
|
|
34
|
+
const attrs: string[] = [];
|
|
35
|
+
if (rPr.fontFamilyAscii) attrs.push(`w:ascii="${escXml(rPr.fontFamilyAscii)}"`);
|
|
36
|
+
if (rPr.fontFamilyHAnsi) attrs.push(`w:hAnsi="${escXml(rPr.fontFamilyHAnsi)}"`);
|
|
37
|
+
if (rPr.fontFamilyEastAsia) attrs.push(`w:eastAsia="${escXml(rPr.fontFamilyEastAsia)}"`);
|
|
38
|
+
if (rPr.fontFamilyCs) attrs.push(`w:cs="${escXml(rPr.fontFamilyCs)}"`);
|
|
39
|
+
parts.push(`<w:rFonts ${attrs.join(" ")}/>`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 3. b, bCs (bCs not modeled, skip)
|
|
43
|
+
parts.push(toggleEl("b", rPr.bold));
|
|
44
|
+
|
|
45
|
+
// 4. i, iCs (iCs not modeled, skip)
|
|
46
|
+
parts.push(toggleEl("i", rPr.italic));
|
|
47
|
+
|
|
48
|
+
// 5. caps, smallCaps
|
|
49
|
+
parts.push(toggleEl("caps", rPr.allCaps));
|
|
50
|
+
parts.push(toggleEl("smallCaps", rPr.smallCaps));
|
|
51
|
+
|
|
52
|
+
// 6. strike, dstrike
|
|
53
|
+
parts.push(toggleEl("strike", rPr.strikethrough));
|
|
54
|
+
parts.push(toggleEl("dstrike", rPr.doubleStrikethrough));
|
|
55
|
+
|
|
56
|
+
// 7. vanish
|
|
57
|
+
parts.push(toggleEl("vanish", rPr.vanish));
|
|
58
|
+
|
|
59
|
+
// 8. color
|
|
60
|
+
if (rPr.colorHex || rPr.colorThemeSlot) {
|
|
61
|
+
const attrs: string[] = [];
|
|
62
|
+
if (rPr.colorHex) attrs.push(`w:val="${escXml(rPr.colorHex)}"`);
|
|
63
|
+
if (rPr.colorThemeSlot) attrs.push(`w:themeColor="${escXml(rPr.colorThemeSlot)}"`);
|
|
64
|
+
parts.push(`<w:color ${attrs.join(" ")}/>`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 9. spacing (character spacing)
|
|
68
|
+
if (rPr.characterSpacingTwips !== undefined) {
|
|
69
|
+
parts.push(`<w:spacing w:val="${rPr.characterSpacingTwips}"/>`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 10. highlight
|
|
73
|
+
if (rPr.highlight) parts.push(`<w:highlight w:val="${escXml(rPr.highlight)}"/>`);
|
|
74
|
+
|
|
75
|
+
// 11. u (underline)
|
|
76
|
+
if (rPr.underline) parts.push(`<w:u w:val="${escXml(rPr.underline)}"/>`);
|
|
77
|
+
|
|
78
|
+
// 12. vertAlign
|
|
79
|
+
if (rPr.verticalAlign) parts.push(`<w:vertAlign w:val="${escXml(rPr.verticalAlign)}"/>`);
|
|
80
|
+
|
|
81
|
+
// 13. lang
|
|
82
|
+
if (rPr.languageCode) parts.push(`<w:lang w:val="${escXml(rPr.languageCode)}"/>`);
|
|
83
|
+
|
|
84
|
+
// 14. sz, szCs
|
|
85
|
+
if (rPr.fontSizeHalfPoints !== undefined) parts.push(`<w:sz w:val="${rPr.fontSizeHalfPoints}"/>`);
|
|
86
|
+
if (rPr.fontSizeCsHalfPoints !== undefined) parts.push(`<w:szCs w:val="${rPr.fontSizeCsHalfPoints}"/>`);
|
|
87
|
+
|
|
88
|
+
const body = parts.filter(Boolean).join("");
|
|
89
|
+
return body.length > 0 ? `<w:rPr>${body}</w:rPr>` : "";
|
|
90
|
+
}
|