@beyondwork/docx-react-component 1.0.37 → 1.0.39
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 +496 -1
- package/src/core/commands/section-layout-commands.ts +58 -0
- package/src/core/commands/table-grid.ts +431 -0
- package/src/core/commands/table-structure-commands.ts +845 -56
- package/src/core/commands/text-commands.ts +122 -2
- package/src/io/docx-session.ts +1 -0
- package/src/io/export/serialize-main-document.ts +2 -11
- package/src/io/export/serialize-numbering.ts +43 -10
- 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/export/serialize-tables.ts +74 -0
- package/src/io/export/table-properties-xml.ts +139 -4
- package/src/io/normalize/normalize-text.ts +15 -0
- package/src/io/ooxml/parse-fields.ts +10 -3
- package/src/io/ooxml/parse-footnotes.ts +60 -0
- package/src/io/ooxml/parse-headers-footers.ts +60 -0
- package/src/io/ooxml/parse-main-document.ts +137 -0
- 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/parse-tables.ts +249 -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 +117 -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-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +1 -1
- package/src/runtime/document-runtime.ts +248 -18
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/index.ts +47 -0
- package/src/runtime/layout/inert-layout-facet.ts +16 -0
- package/src/runtime/layout/layout-engine-instance.ts +100 -23
- package/src/runtime/layout/layout-invalidation.ts +14 -5
- package/src/runtime/layout/margin-preset-catalog.ts +178 -0
- package/src/runtime/layout/page-format-catalog.ts +233 -0
- package/src/runtime/layout/page-graph.ts +55 -0
- package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
- package/src/runtime/layout/paginated-layout-engine.ts +484 -37
- package/src/runtime/layout/project-block-fragments.ts +225 -0
- package/src/runtime/layout/public-facet.ts +748 -16
- 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 +249 -0
- package/src/runtime/numbering-prefix.ts +5 -0
- package/src/runtime/paragraph-style-resolver.ts +194 -0
- package/src/runtime/render/block-fragment-projection.ts +35 -0
- package/src/runtime/render/decoration-resolver.ts +189 -0
- package/src/runtime/render/index.ts +57 -0
- package/src/runtime/render/pending-op-delta-reader.ts +129 -0
- package/src/runtime/render/render-frame-types.ts +317 -0
- package/src/runtime/render/render-kernel.ts +759 -0
- 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/view-state.ts +67 -0
- package/src/runtime/workflow-markup.ts +1 -5
- package/src/runtime/workflow-rail-segments.ts +280 -0
- package/src/ui/WordReviewEditor.tsx +368 -19
- 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 +10 -0
- package/src/ui/editor-surface-controller.tsx +9 -1
- package/src/ui/headless/chrome-registry.ts +310 -15
- package/src/ui/headless/scoped-chrome-policy.ts +49 -1
- package/src/ui/headless/selection-tool-types.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
- package/src/ui-tailwind/chrome/role-action-sets.ts +80 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +160 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +68 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +356 -140
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +94 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +16 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +96 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +40 -4
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
- 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 +144 -62
- 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 -75
- 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 +29 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
- package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
- package/src/ui-tailwind/theme/editor-theme.css +498 -163
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +680 -0
- package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +104 -2
- package/src/ui-tailwind/tw-review-workspace.tsx +234 -21
- package/src/runtime/collab-review-sync.ts +0 -254
- 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/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
|
}
|
|
@@ -1263,20 +1263,11 @@ function offsetParagraphBoundary(
|
|
|
1263
1263
|
};
|
|
1264
1264
|
}
|
|
1265
1265
|
|
|
1266
|
-
|
|
1267
|
-
* Detect a production build without referencing the Node-only `process`
|
|
1268
|
-
* global directly (the production tsconfig excludes @types/node). Returns
|
|
1269
|
-
* true when NODE_ENV === "production"; otherwise false (browser or dev).
|
|
1270
|
-
*/
|
|
1271
|
-
function isProductionEnvironment(): boolean {
|
|
1266
|
+
function assertUniqueBookmarkIdsOrWarn(documentXml: string): void {
|
|
1272
1267
|
const proc = (globalThis as unknown as {
|
|
1273
1268
|
process?: { env?: Record<string, string | undefined> };
|
|
1274
1269
|
}).process;
|
|
1275
|
-
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
function assertUniqueBookmarkIdsOrWarn(documentXml: string): void {
|
|
1279
|
-
if (isProductionEnvironment()) {
|
|
1270
|
+
if (proc?.env?.NODE_ENV === "production") {
|
|
1280
1271
|
return;
|
|
1281
1272
|
}
|
|
1282
1273
|
const seen = new Set<string>();
|
|
@@ -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>`
|
|
@@ -276,8 +310,7 @@ function warnClamp(attr: string, from: number, to: number): void {
|
|
|
276
310
|
warnedClamps += 1;
|
|
277
311
|
// Only warn outside of production builds. The test runner shows these
|
|
278
312
|
// via console.warn automatically; in production this is a silent no-op
|
|
279
|
-
// so the Buffer cost is zero.
|
|
280
|
-
// production tsconfig (which excludes @types/node) type-checks cleanly.
|
|
313
|
+
// so the Buffer cost is zero.
|
|
281
314
|
const proc = (globalThis as unknown as {
|
|
282
315
|
process?: { env?: Record<string, string | undefined> };
|
|
283
316
|
}).process;
|
|
@@ -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
|
+
}
|