@beyondwork/docx-react-component 1.0.58 → 1.0.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +2 -1
- package/src/api/awareness-identity-types.ts +4 -2
- package/src/api/comment-negotiation-types.ts +4 -1
- package/src/api/external-custody-types.ts +16 -0
- package/src/api/internal/build-ref-projections.ts +108 -0
- package/src/api/package-version.ts +1 -1
- package/src/api/participants-types.ts +11 -1
- package/src/api/public-types.ts +980 -10
- package/src/api/scope-metadata-resolver-types.ts +6 -0
- package/src/compare/diff-engine.ts +3 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/index.ts +225 -16
- package/src/core/commands/legacy-form-field-commands.ts +181 -0
- package/src/core/commands/table-structure-commands.ts +149 -31
- package/src/core/selection/mapping.ts +20 -0
- package/src/core/state/editor-state.ts +4 -1
- package/src/index.ts +28 -0
- package/src/io/docx-session.ts +22 -3
- package/src/io/export/export-session.ts +11 -7
- package/src/io/export/ooxml-namespaces.ts +47 -0
- package/src/io/export/reattach-preserved-parts.ts +4 -16
- package/src/io/export/serialize-comments.ts +3 -131
- package/src/io/export/serialize-ffdata.ts +89 -0
- package/src/io/export/serialize-headers-footers.ts +5 -0
- package/src/io/export/serialize-main-document.ts +224 -34
- package/src/io/export/serialize-numbering.ts +22 -2
- package/src/io/export/serialize-revisions.ts +99 -0
- package/src/io/export/serialize-tables.ts +9 -0
- package/src/io/export/split-review-boundaries.ts +1 -0
- package/src/io/export/table-properties-xml.ts +14 -0
- package/src/io/load-scheduler.ts +70 -28
- package/src/io/normalize/normalize-text.ts +13 -0
- package/src/io/ooxml/_mini-xml.ts +198 -0
- package/src/io/ooxml/canonicalize-payload.ts +1 -4
- package/src/io/ooxml/chart/chart-style-table.ts +4 -3
- package/src/io/ooxml/chart/parse-chart-space.ts +2 -4
- package/src/io/ooxml/chart/parse-series.ts +2 -1
- package/src/io/ooxml/chart/resolve-color.ts +2 -2
- package/src/io/ooxml/chart/types.ts +6 -434
- package/src/io/ooxml/comment-presentation-payload.ts +6 -5
- package/src/io/ooxml/highlight-colors.ts +8 -5
- package/src/io/ooxml/parse-anchor.ts +68 -53
- package/src/io/ooxml/parse-comments.ts +14 -142
- package/src/io/ooxml/parse-complex-content.ts +3 -106
- package/src/io/ooxml/parse-drawing.ts +100 -195
- package/src/io/ooxml/parse-ffdata.ts +93 -0
- package/src/io/ooxml/parse-fields.ts +7 -146
- package/src/io/ooxml/parse-fill.ts +88 -8
- package/src/io/ooxml/parse-font-table.ts +5 -105
- package/src/io/ooxml/parse-footnotes.ts +28 -152
- package/src/io/ooxml/parse-headers-footers.ts +106 -212
- package/src/io/ooxml/parse-inline-media.ts +3 -200
- package/src/io/ooxml/parse-main-document.ts +180 -217
- package/src/io/ooxml/parse-numbering.ts +154 -335
- package/src/io/ooxml/parse-object.ts +147 -0
- package/src/io/ooxml/parse-ole-relationship.ts +82 -0
- package/src/io/ooxml/parse-paragraph-formatting.ts +7 -10
- package/src/io/ooxml/parse-picture-sdt.ts +85 -0
- package/src/io/ooxml/parse-picture.ts +72 -42
- package/src/io/ooxml/parse-revisions.ts +285 -51
- package/src/io/ooxml/parse-settings.ts +6 -99
- package/src/io/ooxml/parse-shapes.ts +25 -140
- package/src/io/ooxml/parse-styles.ts +3 -218
- package/src/io/ooxml/parse-tables.ts +76 -256
- package/src/io/ooxml/parse-theme.ts +1 -4
- package/src/io/ooxml/property-grab-bag.ts +5 -47
- package/src/io/ooxml/workflow-payload.ts +6 -1
- package/src/io/ooxml/xml-element-serialize.ts +32 -0
- package/src/io/ooxml/xml-parser.ts +183 -0
- package/src/legal/bookmarks.ts +1 -1
- package/src/legal/cross-references.ts +1 -1
- package/src/legal/defined-terms.ts +1 -1
- package/src/legal/{_document-root.ts → document-root.ts} +8 -0
- package/src/legal/signature-blocks.ts +1 -1
- package/src/model/canonical-document.ts +159 -6
- package/src/model/chart-types.ts +439 -0
- package/src/model/snapshot.ts +5 -1
- package/src/review/store/comment-remapping.ts +24 -11
- package/src/review/store/revision-actions.ts +482 -2
- package/src/review/store/revision-store.ts +15 -0
- package/src/review/store/revision-types.ts +76 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +24 -0
- package/src/runtime/collab/runtime-collab-sync.ts +33 -0
- package/src/runtime/diagnostics/build-diagnostic.ts +153 -0
- package/src/runtime/diagnostics/code-metadata-table.ts +230 -0
- package/src/runtime/document-runtime.ts +821 -54
- package/src/runtime/document-search.ts +115 -0
- package/src/runtime/edit-ops/index.ts +18 -2
- package/src/runtime/footnote-resolver.ts +130 -0
- package/src/runtime/layout/layout-engine-instance.ts +31 -4
- package/src/runtime/layout/layout-engine-version.ts +37 -1
- package/src/runtime/layout/page-graph.ts +14 -1
- package/src/runtime/layout/resolved-formatting-state.ts +21 -0
- package/src/runtime/numbering-prefix.ts +17 -0
- package/src/runtime/query-scopes.ts +108 -10
- package/src/runtime/resolved-numbering-geometry.ts +37 -6
- package/src/runtime/revision-runtime.ts +27 -1
- package/src/runtime/selection/post-edit-validator.ts +60 -6
- package/src/runtime/structure-ops/index.ts +20 -4
- package/src/runtime/surface-projection.ts +290 -21
- package/src/runtime/table-schema.ts +6 -0
- package/src/runtime/theme-color-resolver.ts +2 -2
- package/src/runtime/units.ts +9 -0
- package/src/runtime/workflow-rail-segments.ts +4 -0
- package/src/ui/WordReviewEditor.tsx +187 -43
- package/src/ui/editor-runtime-boundary.ts +10 -0
- package/src/ui/editor-shell-view.tsx +4 -1
- package/src/ui/headless/chrome-registry.ts +53 -0
- package/src/ui/headless/selection-tool-resolver.ts +11 -1
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +13 -0
- package/src/ui-tailwind/chrome/tw-command-palette-mount.tsx +96 -0
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +2 -1
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +5 -4
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +6 -2
- package/src/ui-tailwind/chrome/use-container-breakpoint.ts +111 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +0 -9
- package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +1 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +6 -7
- package/src/ui-tailwind/editor-surface/pm-schema.ts +87 -25
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +9 -0
- package/src/ui-tailwind/editor-surface/shape-renderer.ts +76 -14
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +18 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +2 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +18 -2
- package/src/ui-tailwind/index.ts +9 -0
- package/src/ui-tailwind/page-chrome-model.ts +77 -5
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +56 -1
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +2 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +116 -113
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +2 -2
- package/src/ui-tailwind/theme/tokens.ts +14 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +5 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +29 -87
- package/src/validation/diagnostics.ts +1 -0
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
* four fill families.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import { GRADIENT_STOP_UNITS } from "../../runtime/units.ts";
|
|
21
|
+
import { localName } from "./xml-attr-helpers.ts";
|
|
22
|
+
|
|
20
23
|
export interface XmlElementNode {
|
|
21
24
|
type: "element";
|
|
22
25
|
name: string;
|
|
@@ -57,6 +60,11 @@ export interface GradientFillResult {
|
|
|
57
60
|
direction: GradientDirection;
|
|
58
61
|
/** Rotate with shape (a:gradFill rotWithShape). Defaults true per OOXML. */
|
|
59
62
|
rotWithShape?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Phase 4.5 G5 — `a:gradFill flip` attribute ("x" | "y" | "xy") mirrors the
|
|
65
|
+
* gradient along the given axis/axes. Absent when no flip is specified.
|
|
66
|
+
*/
|
|
67
|
+
flip?: "x" | "y" | "xy";
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
export interface PatternFillResult {
|
|
@@ -127,28 +135,104 @@ function parseSolidFillInner(solid: XmlElementNode): SolidFillResult | undefined
|
|
|
127
135
|
function parseGradientFill(grad: XmlElementNode): GradientFillResult | undefined {
|
|
128
136
|
const gsLst = findFirstChild(grad, "gsLst");
|
|
129
137
|
if (!gsLst) return undefined;
|
|
130
|
-
|
|
138
|
+
|
|
139
|
+
// Phase 4.5 G6 — collect stops with optional `pos` (null = unspecified);
|
|
140
|
+
// post-pass interpolates the nulls across neighbors rather than collapsing
|
|
141
|
+
// them all to 0. Matches real-world Word behavior where middle stops may
|
|
142
|
+
// omit pos and are laid out evenly between specified neighbors.
|
|
143
|
+
interface RawStop {
|
|
144
|
+
pos: number | null;
|
|
145
|
+
color: string;
|
|
146
|
+
colorType: "srgbClr" | "schemeClr";
|
|
147
|
+
}
|
|
148
|
+
const raw: RawStop[] = [];
|
|
131
149
|
for (const child of gsLst.children) {
|
|
132
150
|
if (child.type !== "element") continue;
|
|
133
151
|
if (localName(child.name) !== "gs") continue;
|
|
134
152
|
const posRaw = child.attributes.pos;
|
|
135
|
-
|
|
153
|
+
let pos: number | null;
|
|
154
|
+
if (posRaw === undefined || posRaw === "") {
|
|
155
|
+
pos = null;
|
|
156
|
+
} else {
|
|
157
|
+
const n = parseInt(posRaw, 10);
|
|
158
|
+
pos = Number.isFinite(n) ? n : null;
|
|
159
|
+
}
|
|
136
160
|
const token = extractColorToken(child);
|
|
137
161
|
if (!token) continue;
|
|
138
|
-
|
|
162
|
+
raw.push({ pos, ...token });
|
|
139
163
|
}
|
|
140
|
-
if (
|
|
164
|
+
if (raw.length === 0) return undefined;
|
|
165
|
+
|
|
166
|
+
const stops = interpolateStopPositions(raw);
|
|
141
167
|
|
|
142
168
|
const direction = readGradientDirection(grad);
|
|
143
169
|
const rotWithShapeRaw = grad.attributes.rotWithShape;
|
|
144
170
|
const rotWithShape =
|
|
145
171
|
rotWithShapeRaw === undefined ? undefined : rotWithShapeRaw !== "0" && rotWithShapeRaw !== "false";
|
|
172
|
+
const flipRaw = grad.attributes.flip;
|
|
173
|
+
const flip = flipRaw === "x" || flipRaw === "y" || flipRaw === "xy" ? flipRaw : undefined;
|
|
146
174
|
|
|
147
175
|
const result: GradientFillResult = { kind: "gradient", stops, direction };
|
|
148
176
|
if (rotWithShape !== undefined) result.rotWithShape = rotWithShape;
|
|
177
|
+
if (flip !== undefined) result.flip = flip;
|
|
149
178
|
return result;
|
|
150
179
|
}
|
|
151
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Phase 4.5 G6 — fill `null` positions by linear interpolation across
|
|
183
|
+
* defined neighbors. Leading nulls default to 0; trailing nulls default to
|
|
184
|
+
* 100000 (OOXML gsLst pos unit). Runs of nulls between known positions get
|
|
185
|
+
* evenly spaced.
|
|
186
|
+
*/
|
|
187
|
+
function interpolateStopPositions(raw: ReadonlyArray<{
|
|
188
|
+
pos: number | null;
|
|
189
|
+
color: string;
|
|
190
|
+
colorType: "srgbClr" | "schemeClr";
|
|
191
|
+
}>): GradientStop[] {
|
|
192
|
+
const resolved: number[] = new Array(raw.length);
|
|
193
|
+
// Forward pass with leading-null default of 0
|
|
194
|
+
let lastKnownIndex = -1;
|
|
195
|
+
for (let i = 0; i < raw.length; i++) {
|
|
196
|
+
if (raw[i]?.pos !== null) {
|
|
197
|
+
resolved[i] = raw[i]!.pos as number;
|
|
198
|
+
// Fill intervening nulls (lastKnownIndex..i exclusive) by interpolation
|
|
199
|
+
if (lastKnownIndex >= 0 && i - lastKnownIndex > 1) {
|
|
200
|
+
const start = resolved[lastKnownIndex]!;
|
|
201
|
+
const end = resolved[i]!;
|
|
202
|
+
for (let j = lastKnownIndex + 1; j < i; j++) {
|
|
203
|
+
resolved[j] = start + ((end - start) * (j - lastKnownIndex)) / (i - lastKnownIndex);
|
|
204
|
+
}
|
|
205
|
+
} else if (lastKnownIndex < 0 && i > 0) {
|
|
206
|
+
// Leading nulls before first known pos — default to 0, then interp
|
|
207
|
+
for (let j = 0; j < i; j++) {
|
|
208
|
+
resolved[j] = (resolved[i]! * j) / i;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
lastKnownIndex = i;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Trailing nulls after the last known pos — default toward GRADIENT_STOP_UNITS
|
|
215
|
+
if (lastKnownIndex < raw.length - 1) {
|
|
216
|
+
const start = lastKnownIndex >= 0 ? resolved[lastKnownIndex]! : 0;
|
|
217
|
+
const end = GRADIENT_STOP_UNITS;
|
|
218
|
+
const count = raw.length - 1 - Math.max(lastKnownIndex, 0);
|
|
219
|
+
for (let j = Math.max(lastKnownIndex, 0) + 1; j < raw.length; j++) {
|
|
220
|
+
resolved[j] = start + ((end - start) * (j - Math.max(lastKnownIndex, 0))) / (count);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// All-null case: evenly distribute 0..GRADIENT_STOP_UNITS
|
|
224
|
+
if (lastKnownIndex < 0) {
|
|
225
|
+
for (let i = 0; i < raw.length; i++) {
|
|
226
|
+
resolved[i] = raw.length === 1 ? 0 : (GRADIENT_STOP_UNITS * i) / (raw.length - 1);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return raw.map((r, i) => ({
|
|
230
|
+
pos: Math.round(resolved[i]!),
|
|
231
|
+
color: r.color,
|
|
232
|
+
colorType: r.colorType,
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
|
|
152
236
|
function readGradientDirection(grad: XmlElementNode): GradientDirection {
|
|
153
237
|
const lin = findFirstChild(grad, "lin");
|
|
154
238
|
if (lin) {
|
|
@@ -209,7 +293,3 @@ function findFirstChild(
|
|
|
209
293
|
return undefined;
|
|
210
294
|
}
|
|
211
295
|
|
|
212
|
-
function localName(name: string): string {
|
|
213
|
-
const i = name.indexOf(":");
|
|
214
|
-
return i >= 0 ? name.slice(i + 1) : name;
|
|
215
|
-
}
|
|
@@ -14,22 +14,11 @@ import type {
|
|
|
14
14
|
CanonicalFontEntry,
|
|
15
15
|
CanonicalFontTable,
|
|
16
16
|
} from "../../model/canonical-document.ts";
|
|
17
|
+
import type { XmlElementNode } from "./xml-element.ts";
|
|
18
|
+
import { parseXml } from "./xml-parser.ts";
|
|
19
|
+
import { localName } from "./xml-attr-helpers.ts";
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
type: "element";
|
|
20
|
-
name: string;
|
|
21
|
-
attributes: Record<string, string>;
|
|
22
|
-
children: XmlNode[];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface XmlTextNode {
|
|
26
|
-
type: "text";
|
|
27
|
-
text: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
type XmlNode = XmlElementNode | XmlTextNode;
|
|
31
|
-
|
|
32
|
-
const KNOWN_FAMILIES = new Set(["roman", "swiss", "modern", "script", "decorative"]);
|
|
21
|
+
const KNOWN_FAMILIES = new Set<CanonicalFontEntry["family"]>(["roman", "swiss", "modern", "script", "decorative"]);
|
|
33
22
|
|
|
34
23
|
export function parseFontTable(xml: string): CanonicalFontTable {
|
|
35
24
|
const root = parseXml(xml);
|
|
@@ -50,7 +39,7 @@ export function parseFontTable(xml: string): CanonicalFontTable {
|
|
|
50
39
|
switch (localName(sub.name)) {
|
|
51
40
|
case "family": {
|
|
52
41
|
const raw = sub.attributes["w:val"] ?? sub.attributes["val"];
|
|
53
|
-
if (raw && KNOWN_FAMILIES.has(raw)) {
|
|
42
|
+
if (raw && KNOWN_FAMILIES.has(raw as CanonicalFontEntry["family"])) {
|
|
54
43
|
entry.family = raw as CanonicalFontEntry["family"];
|
|
55
44
|
}
|
|
56
45
|
break;
|
|
@@ -88,10 +77,6 @@ export function parseFontTable(xml: string): CanonicalFontTable {
|
|
|
88
77
|
// XML helpers — same shape as parse-numbering.ts internals
|
|
89
78
|
// ---------------------------------------------------------------------------
|
|
90
79
|
|
|
91
|
-
function localName(name: string): string {
|
|
92
|
-
const separator = name.indexOf(":");
|
|
93
|
-
return separator >= 0 ? name.slice(separator + 1) : name;
|
|
94
|
-
}
|
|
95
80
|
|
|
96
81
|
function findChildElementOptional(
|
|
97
82
|
node: XmlElementNode,
|
|
@@ -103,88 +88,3 @@ function findChildElementOptional(
|
|
|
103
88
|
);
|
|
104
89
|
}
|
|
105
90
|
|
|
106
|
-
function parseXml(xml: string): XmlElementNode {
|
|
107
|
-
const root: XmlElementNode = {
|
|
108
|
-
type: "element",
|
|
109
|
-
name: "__root__",
|
|
110
|
-
attributes: {},
|
|
111
|
-
children: [],
|
|
112
|
-
};
|
|
113
|
-
const stack: XmlElementNode[] = [root];
|
|
114
|
-
let cursor = 0;
|
|
115
|
-
|
|
116
|
-
while (cursor < xml.length) {
|
|
117
|
-
if (xml.startsWith("<!--", cursor)) {
|
|
118
|
-
const end = xml.indexOf("-->", cursor);
|
|
119
|
-
cursor = end >= 0 ? end + 3 : xml.length;
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
if (xml.startsWith("<?", cursor)) {
|
|
123
|
-
const end = xml.indexOf("?>", cursor);
|
|
124
|
-
cursor = end >= 0 ? end + 2 : xml.length;
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
if (xml[cursor] !== "<") {
|
|
128
|
-
const nextTag = xml.indexOf("<", cursor);
|
|
129
|
-
cursor = nextTag >= 0 ? nextTag : xml.length;
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
if (xml[cursor + 1] === "/") {
|
|
133
|
-
const end = xml.indexOf(">", cursor);
|
|
134
|
-
if (end < 0) throw new Error("Malformed XML: missing closing >.");
|
|
135
|
-
stack.pop();
|
|
136
|
-
cursor = end + 1;
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const tagEnd = findTagEnd(xml, cursor);
|
|
141
|
-
const tagBody = xml.slice(cursor + 1, tagEnd);
|
|
142
|
-
const selfClosing = /\/\s*$/.test(tagBody);
|
|
143
|
-
const { name, attributes } = parseTag(tagBody.replace(/\/\s*$/, "").trim());
|
|
144
|
-
|
|
145
|
-
const element: XmlElementNode = {
|
|
146
|
-
type: "element",
|
|
147
|
-
name,
|
|
148
|
-
attributes,
|
|
149
|
-
children: [],
|
|
150
|
-
};
|
|
151
|
-
stack[stack.length - 1]?.children.push(element);
|
|
152
|
-
if (!selfClosing) stack.push(element);
|
|
153
|
-
cursor = tagEnd + 1;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return root;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function findTagEnd(xml: string, start: number): number {
|
|
160
|
-
let cursor = start + 1;
|
|
161
|
-
let inQuote: '"' | "'" | null = null;
|
|
162
|
-
while (cursor < xml.length) {
|
|
163
|
-
const ch = xml[cursor];
|
|
164
|
-
if (inQuote) {
|
|
165
|
-
if (ch === inQuote) inQuote = null;
|
|
166
|
-
} else {
|
|
167
|
-
if (ch === '"' || ch === "'") inQuote = ch;
|
|
168
|
-
else if (ch === ">") return cursor;
|
|
169
|
-
}
|
|
170
|
-
cursor++;
|
|
171
|
-
}
|
|
172
|
-
throw new Error("Malformed XML: missing closing >.");
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function parseTag(body: string): { name: string; attributes: Record<string, string> } {
|
|
176
|
-
const attributes: Record<string, string> = {};
|
|
177
|
-
let cursor = 0;
|
|
178
|
-
while (cursor < body.length && !/\s/.test(body[cursor]!)) cursor++;
|
|
179
|
-
const name = body.slice(0, cursor);
|
|
180
|
-
|
|
181
|
-
const attrRegex = /([\w:\-.]+)\s*=\s*"([^"]*)"|([\w:\-.]+)\s*=\s*'([^']*)'/g;
|
|
182
|
-
let match: RegExpExecArray | null;
|
|
183
|
-
while ((match = attrRegex.exec(body)) !== null) {
|
|
184
|
-
const key = match[1] ?? match[3]!;
|
|
185
|
-
const value = match[2] ?? match[4] ?? "";
|
|
186
|
-
attributes[key] = value;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return { name, attributes };
|
|
190
|
-
}
|
|
@@ -44,6 +44,8 @@ import {
|
|
|
44
44
|
readTableStyleId,
|
|
45
45
|
readTableWidth,
|
|
46
46
|
} from "./parse-tables.ts";
|
|
47
|
+
import { parseXmlWithOffsets as parseXml } from "./xml-parser.ts";
|
|
48
|
+
import { localName, readStringAttr } from "./xml-attr-helpers.ts";
|
|
47
49
|
|
|
48
50
|
// ---- XML node types (inline, no external dep) ----
|
|
49
51
|
|
|
@@ -77,7 +79,7 @@ const SPECIAL_NOTE_TYPES = new Set(["separator", "continuationSeparator"]);
|
|
|
77
79
|
* Also accepts endnotes.xml (<w:endnotes> root).
|
|
78
80
|
*/
|
|
79
81
|
export function parseFootnotesXml(xml: string): FootnoteCollection {
|
|
80
|
-
const root = parseXml(xml);
|
|
82
|
+
const root = parseXml(xml) as XmlElementNode;
|
|
81
83
|
|
|
82
84
|
const footnotesElement = findChildElementOptional(root, "footnotes");
|
|
83
85
|
const endnotesElement = findChildElementOptional(root, "endnotes");
|
|
@@ -131,7 +133,7 @@ export function parseEndnotesXml(
|
|
|
131
133
|
xml: string,
|
|
132
134
|
existing?: FootnoteCollection,
|
|
133
135
|
): FootnoteCollection {
|
|
134
|
-
const root = parseXml(xml);
|
|
136
|
+
const root = parseXml(xml) as XmlElementNode;
|
|
135
137
|
const endnotesElement = findChildElementOptional(root, "endnotes");
|
|
136
138
|
const endnotes: Record<string, FootnoteDefinition> = {
|
|
137
139
|
...(existing?.endnotes ?? {}),
|
|
@@ -165,7 +167,7 @@ export function parseEndnotesXml(
|
|
|
165
167
|
* by the page-chrome renderer at the footnote divider line.
|
|
166
168
|
*/
|
|
167
169
|
export function parseFootnoteSeparators(xml: string): FootnoteSeparators {
|
|
168
|
-
const root = parseXml(xml);
|
|
170
|
+
const root = parseXml(xml) as XmlElementNode;
|
|
169
171
|
const footnotesEl = findChildElementOptional(root, "footnotes");
|
|
170
172
|
const endnotesEl = findChildElementOptional(root, "endnotes");
|
|
171
173
|
const containerEl = footnotesEl ?? endnotesEl;
|
|
@@ -179,7 +181,7 @@ export function parseFootnoteSeparators(xml: string): FootnoteSeparators {
|
|
|
179
181
|
const name = localName(child.name);
|
|
180
182
|
if (name !== "footnote" && name !== "endnote") continue;
|
|
181
183
|
|
|
182
|
-
const rawType = child
|
|
184
|
+
const rawType = readStringAttr(child, "w:type") ?? "";
|
|
183
185
|
if (rawType !== "separator" && rawType !== "continuationSeparator") continue;
|
|
184
186
|
|
|
185
187
|
const paraEl = findChildElementOptional(child, "p");
|
|
@@ -210,9 +212,9 @@ function parseNoteElement(
|
|
|
210
212
|
kind: "footnote" | "endnote",
|
|
211
213
|
): FootnoteDefinition | undefined {
|
|
212
214
|
const rawId =
|
|
213
|
-
element
|
|
215
|
+
readStringAttr(element, "w:id") ?? "";
|
|
214
216
|
const rawType =
|
|
215
|
-
element
|
|
217
|
+
readStringAttr(element, "w:type") ?? "";
|
|
216
218
|
|
|
217
219
|
if (!rawId || SPECIAL_NOTE_IDS.has(rawId) || SPECIAL_NOTE_TYPES.has(rawType)) {
|
|
218
220
|
return undefined;
|
|
@@ -347,7 +349,7 @@ function appendRunNodes(
|
|
|
347
349
|
|
|
348
350
|
const name = localName(child.name);
|
|
349
351
|
if (name === "fldChar") {
|
|
350
|
-
const fldType = child
|
|
352
|
+
const fldType = readStringAttr(child, "w:fldCharType");
|
|
351
353
|
if (fldType === "begin") {
|
|
352
354
|
activeComplexField = { instruction: "", children: [], mode: "instruction" };
|
|
353
355
|
} else if (fldType === "separate" && activeComplexField) {
|
|
@@ -414,13 +416,13 @@ function parseRunElement(rElement: XmlElementNode): InlineNode[] {
|
|
|
414
416
|
// The in-note reference marker (superscript) - skip, rendered by the host
|
|
415
417
|
} else if (name === "footnoteReference") {
|
|
416
418
|
const noteId =
|
|
417
|
-
child
|
|
419
|
+
readStringAttr(child, "w:id") ?? "";
|
|
418
420
|
if (noteId) {
|
|
419
421
|
nodes.push({ type: "footnote_ref", noteId, noteKind: "footnote" });
|
|
420
422
|
}
|
|
421
423
|
} else if (name === "endnoteReference") {
|
|
422
424
|
const noteId =
|
|
423
|
-
child
|
|
425
|
+
readStringAttr(child, "w:id") ?? "";
|
|
424
426
|
if (noteId) {
|
|
425
427
|
nodes.push({ type: "footnote_ref", noteId, noteKind: "endnote" });
|
|
426
428
|
}
|
|
@@ -459,7 +461,7 @@ function parseRunChildNode(
|
|
|
459
461
|
}
|
|
460
462
|
if (name === "footnoteReference") {
|
|
461
463
|
const noteId =
|
|
462
|
-
child
|
|
464
|
+
readStringAttr(child, "w:id") ?? "";
|
|
463
465
|
if (noteId) {
|
|
464
466
|
return { type: "footnote_ref", noteId, noteKind: "footnote" };
|
|
465
467
|
}
|
|
@@ -467,7 +469,7 @@ function parseRunChildNode(
|
|
|
467
469
|
}
|
|
468
470
|
if (name === "endnoteReference") {
|
|
469
471
|
const noteId =
|
|
470
|
-
child
|
|
472
|
+
readStringAttr(child, "w:id") ?? "";
|
|
471
473
|
if (noteId) {
|
|
472
474
|
return { type: "footnote_ref", noteId, noteKind: "endnote" };
|
|
473
475
|
}
|
|
@@ -506,12 +508,12 @@ function parseHyperlinkElement(element: XmlElementNode): Extract<InlineNode, { t
|
|
|
506
508
|
function parseBookmarkElement(
|
|
507
509
|
element: XmlElementNode,
|
|
508
510
|
): Extract<InlineNode, { type: "bookmark_start" | "bookmark_end" }> {
|
|
509
|
-
const bookmarkId = element
|
|
511
|
+
const bookmarkId = readStringAttr(element, "w:id") ?? "0";
|
|
510
512
|
if (localName(element.name) === "bookmarkStart") {
|
|
511
513
|
return {
|
|
512
514
|
type: "bookmark_start",
|
|
513
515
|
bookmarkId,
|
|
514
|
-
name: element
|
|
516
|
+
name: readStringAttr(element, "w:name") ?? "",
|
|
515
517
|
};
|
|
516
518
|
}
|
|
517
519
|
|
|
@@ -555,8 +557,7 @@ function createFieldNode(
|
|
|
555
557
|
|
|
556
558
|
function readFieldInstruction(element: XmlElementNode): string | undefined {
|
|
557
559
|
const instruction =
|
|
558
|
-
element
|
|
559
|
-
element.attributes.instr ??
|
|
560
|
+
readStringAttr(element, "w:instr") ??
|
|
560
561
|
extractTextContent(element);
|
|
561
562
|
return instruction.trim().length > 0 ? instruction : undefined;
|
|
562
563
|
}
|
|
@@ -575,7 +576,7 @@ function parseRunProperties(rElement: XmlElementNode): TextMark[] {
|
|
|
575
576
|
}
|
|
576
577
|
|
|
577
578
|
const name = localName(child.name);
|
|
578
|
-
const val = child
|
|
579
|
+
const val = readStringAttr(child, "w:val") ?? "true";
|
|
579
580
|
|
|
580
581
|
switch (name) {
|
|
581
582
|
case "b":
|
|
@@ -603,7 +604,7 @@ function parseRunProperties(rElement: XmlElementNode): TextMark[] {
|
|
|
603
604
|
break;
|
|
604
605
|
}
|
|
605
606
|
case "sz": {
|
|
606
|
-
const szVal = child
|
|
607
|
+
const szVal = readStringAttr(child, "w:val");
|
|
607
608
|
if (szVal) {
|
|
608
609
|
const size = Number.parseInt(szVal, 10);
|
|
609
610
|
if (Number.isFinite(size) && size > 0) marks.push({ type: "fontSize", val: size });
|
|
@@ -611,7 +612,7 @@ function parseRunProperties(rElement: XmlElementNode): TextMark[] {
|
|
|
611
612
|
break;
|
|
612
613
|
}
|
|
613
614
|
case "color": {
|
|
614
|
-
const colorVal = child
|
|
615
|
+
const colorVal = readStringAttr(child, "w:val");
|
|
615
616
|
// A.9: preserve "auto" verbatim for round-trip.
|
|
616
617
|
if (colorVal) marks.push({ type: "textColor", color: colorVal });
|
|
617
618
|
break;
|
|
@@ -632,13 +633,13 @@ function readParagraphSpacing(pPr: XmlElementNode): ParagraphSpacing | undefined
|
|
|
632
633
|
const spacingNode = findChildElementOptional(pPr, "spacing");
|
|
633
634
|
if (!spacingNode) return undefined;
|
|
634
635
|
const result: ParagraphSpacing = {};
|
|
635
|
-
const before = spacingNode
|
|
636
|
+
const before = readStringAttr(spacingNode, "w:before");
|
|
636
637
|
if (before) result.before = Number.parseInt(before, 10);
|
|
637
|
-
const after = spacingNode
|
|
638
|
+
const after = readStringAttr(spacingNode, "w:after");
|
|
638
639
|
if (after) result.after = Number.parseInt(after, 10);
|
|
639
|
-
const line = spacingNode
|
|
640
|
+
const line = readStringAttr(spacingNode, "w:line");
|
|
640
641
|
if (line) result.line = Number.parseInt(line, 10);
|
|
641
|
-
const lineRule = spacingNode
|
|
642
|
+
const lineRule = readStringAttr(spacingNode, "w:lineRule");
|
|
642
643
|
if (lineRule === "auto" || lineRule === "exact" || lineRule === "atLeast") {
|
|
643
644
|
result.lineRule = lineRule;
|
|
644
645
|
}
|
|
@@ -649,13 +650,13 @@ function readParagraphIndentation(pPr: XmlElementNode): ParagraphIndentation | u
|
|
|
649
650
|
const indNode = findChildElementOptional(pPr, "ind");
|
|
650
651
|
if (!indNode) return undefined;
|
|
651
652
|
const result: ParagraphIndentation = {};
|
|
652
|
-
const left = indNode
|
|
653
|
+
const left = readStringAttr(indNode, "w:left");
|
|
653
654
|
if (left) result.left = Number.parseInt(left, 10);
|
|
654
|
-
const right = indNode
|
|
655
|
+
const right = readStringAttr(indNode, "w:right");
|
|
655
656
|
if (right) result.right = Number.parseInt(right, 10);
|
|
656
|
-
const firstLine = indNode
|
|
657
|
+
const firstLine = readStringAttr(indNode, "w:firstLine");
|
|
657
658
|
if (firstLine) result.firstLine = Number.parseInt(firstLine, 10);
|
|
658
|
-
const hanging = indNode
|
|
659
|
+
const hanging = readStringAttr(indNode, "w:hanging");
|
|
659
660
|
if (hanging) result.hanging = Number.parseInt(hanging, 10);
|
|
660
661
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
661
662
|
}
|
|
@@ -680,10 +681,6 @@ function findChildElementOptional(
|
|
|
680
681
|
);
|
|
681
682
|
}
|
|
682
683
|
|
|
683
|
-
function localName(name: string): string {
|
|
684
|
-
const idx = name.indexOf(":");
|
|
685
|
-
return idx >= 0 ? name.slice(idx + 1) : name;
|
|
686
|
-
}
|
|
687
684
|
|
|
688
685
|
// ---- Simple secondary-story table support ----
|
|
689
686
|
|
|
@@ -864,7 +861,7 @@ function parseSimpleTableCell(tcElement: XmlElementNode): TableCellNode {
|
|
|
864
861
|
|
|
865
862
|
const vmEl = findChildElementOptional(child, "vMerge");
|
|
866
863
|
if (vmEl) {
|
|
867
|
-
const vmVal = vmEl
|
|
864
|
+
const vmVal = readStringAttr(vmEl, "w:val") ?? "continue";
|
|
868
865
|
verticalMerge = vmVal === "restart" ? "restart" : "continue";
|
|
869
866
|
}
|
|
870
867
|
width = readCellWidth(child);
|
|
@@ -931,124 +928,3 @@ function escapeXmlText(text: string): string {
|
|
|
931
928
|
.replace(/</g, "<")
|
|
932
929
|
.replace(/>/g, ">");
|
|
933
930
|
}
|
|
934
|
-
|
|
935
|
-
// ---- Minimal XML parser (same pattern as parse-numbering.ts) ----
|
|
936
|
-
|
|
937
|
-
function parseXml(xml: string): XmlElementNode {
|
|
938
|
-
const root: XmlElementNode = {
|
|
939
|
-
type: "element",
|
|
940
|
-
name: "__root__",
|
|
941
|
-
attributes: {},
|
|
942
|
-
children: [],
|
|
943
|
-
start: 0,
|
|
944
|
-
end: xml.length,
|
|
945
|
-
};
|
|
946
|
-
const stack: XmlElementNode[] = [root];
|
|
947
|
-
let cursor = 0;
|
|
948
|
-
|
|
949
|
-
while (cursor < xml.length) {
|
|
950
|
-
if (xml.startsWith("<!--", cursor)) {
|
|
951
|
-
const end = xml.indexOf("-->", cursor);
|
|
952
|
-
cursor = end >= 0 ? end + 3 : xml.length;
|
|
953
|
-
continue;
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
if (xml.startsWith("<?", cursor)) {
|
|
957
|
-
const end = xml.indexOf("?>", cursor);
|
|
958
|
-
cursor = end >= 0 ? end + 2 : xml.length;
|
|
959
|
-
continue;
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
if (xml.startsWith("<![CDATA[", cursor)) {
|
|
963
|
-
const end = xml.indexOf("]]>", cursor);
|
|
964
|
-
const textEnd = end >= 0 ? end : xml.length;
|
|
965
|
-
stack[stack.length - 1]?.children.push({
|
|
966
|
-
type: "text",
|
|
967
|
-
text: xml.slice(cursor + 9, textEnd),
|
|
968
|
-
start: cursor,
|
|
969
|
-
end: end >= 0 ? end + 3 : xml.length,
|
|
970
|
-
});
|
|
971
|
-
cursor = end >= 0 ? end + 3 : xml.length;
|
|
972
|
-
continue;
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
if (xml[cursor] !== "<") {
|
|
976
|
-
const nextTag = xml.indexOf("<", cursor);
|
|
977
|
-
const end = nextTag >= 0 ? nextTag : xml.length;
|
|
978
|
-
const text = decodeXmlEntities(xml.slice(cursor, end));
|
|
979
|
-
if (text.trim().length > 0 || (text.length > 0 && stack.length > 1)) {
|
|
980
|
-
stack[stack.length - 1]?.children.push({ type: "text", text, start: cursor, end });
|
|
981
|
-
}
|
|
982
|
-
cursor = end;
|
|
983
|
-
continue;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
if (xml[cursor + 1] === "/") {
|
|
987
|
-
const end = xml.indexOf(">", cursor);
|
|
988
|
-
if (end < 0) break;
|
|
989
|
-
const current = stack.pop();
|
|
990
|
-
if (current) {
|
|
991
|
-
current.end = end + 1;
|
|
992
|
-
}
|
|
993
|
-
cursor = end + 1;
|
|
994
|
-
continue;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
const tagEnd = xml.indexOf(">", cursor);
|
|
998
|
-
if (tagEnd < 0) break;
|
|
999
|
-
|
|
1000
|
-
const tagContent = xml.slice(cursor + 1, tagEnd);
|
|
1001
|
-
const selfClosing = tagContent.endsWith("/");
|
|
1002
|
-
const normalized = selfClosing ? tagContent.slice(0, -1).trimEnd() : tagContent;
|
|
1003
|
-
|
|
1004
|
-
const spaceIndex = normalized.search(/\s/);
|
|
1005
|
-
const tagName = spaceIndex >= 0 ? normalized.slice(0, spaceIndex) : normalized;
|
|
1006
|
-
const attrString = spaceIndex >= 0 ? normalized.slice(spaceIndex + 1) : "";
|
|
1007
|
-
const attributes = parseAttributes(attrString);
|
|
1008
|
-
|
|
1009
|
-
const element: XmlElementNode = {
|
|
1010
|
-
type: "element",
|
|
1011
|
-
name: tagName,
|
|
1012
|
-
attributes,
|
|
1013
|
-
children: [],
|
|
1014
|
-
start: cursor,
|
|
1015
|
-
end: tagEnd + 1,
|
|
1016
|
-
};
|
|
1017
|
-
|
|
1018
|
-
stack[stack.length - 1]?.children.push(element);
|
|
1019
|
-
|
|
1020
|
-
if (!selfClosing) {
|
|
1021
|
-
stack.push(element);
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
cursor = tagEnd + 1;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
return root;
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
function parseAttributes(attrString: string): Record<string, string> {
|
|
1031
|
-
const attrs: Record<string, string> = {};
|
|
1032
|
-
const pattern = /([A-Za-z_:][A-Za-z0-9:._-]*)\s*=\s*("([^"]*)"|'([^']*)')/gu;
|
|
1033
|
-
for (const match of attrString.matchAll(pattern)) {
|
|
1034
|
-
const name = match[1];
|
|
1035
|
-
const value = match[3] ?? match[4] ?? "";
|
|
1036
|
-
if (name) {
|
|
1037
|
-
attrs[name] = decodeXmlEntities(value);
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
return attrs;
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
function decodeXmlEntities(text: string): string {
|
|
1044
|
-
return text
|
|
1045
|
-
.replace(/&/g, "&")
|
|
1046
|
-
.replace(/</g, "<")
|
|
1047
|
-
.replace(/>/g, ">")
|
|
1048
|
-
.replace(/"/g, '"')
|
|
1049
|
-
.replace(/'/g, "'")
|
|
1050
|
-
.replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number.parseInt(dec, 10)))
|
|
1051
|
-
.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) =>
|
|
1052
|
-
String.fromCodePoint(Number.parseInt(hex, 16)),
|
|
1053
|
-
);
|
|
1054
|
-
}
|