@beyondwork/docx-react-component 1.0.1 → 1.0.3
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 +44 -104
- package/package.json +50 -30
- package/src/README.md +85 -0
- package/src/api/README.md +22 -0
- package/src/api/public-types.ts +525 -0
- package/src/compare/diff-engine.ts +530 -0
- package/src/compare/export-redlines.ts +162 -0
- package/src/compare/snapshot.ts +37 -0
- package/src/component-inventory.md +99 -0
- package/src/core/README.md +10 -0
- package/src/core/commands/README.md +3 -0
- package/src/core/commands/formatting-commands.ts +161 -0
- package/src/core/commands/image-commands.ts +144 -0
- package/src/core/commands/index.ts +1013 -0
- package/src/core/commands/list-commands.ts +370 -0
- package/src/core/commands/review-commands.ts +108 -0
- package/src/core/commands/text-commands.ts +119 -0
- package/src/core/schema/README.md +3 -0
- package/src/core/schema/text-schema.ts +512 -0
- package/src/core/selection/README.md +3 -0
- package/src/core/selection/mapping.ts +238 -0
- package/src/core/selection/review-anchors.ts +94 -0
- package/src/core/state/README.md +3 -0
- package/src/core/state/editor-state.ts +580 -0
- package/src/core/state/text-transaction.ts +276 -0
- package/src/formats/xlsx/io/parse-shared-strings.ts +41 -0
- package/src/formats/xlsx/io/parse-sheet.ts +289 -0
- package/src/formats/xlsx/io/parse-styles.ts +57 -0
- package/src/formats/xlsx/io/parse-workbook.ts +75 -0
- package/src/formats/xlsx/io/xlsx-session.ts +306 -0
- package/src/formats/xlsx/model/cell.ts +189 -0
- package/src/formats/xlsx/model/sheet.ts +244 -0
- package/src/formats/xlsx/model/styles.ts +118 -0
- package/src/formats/xlsx/model/workbook.ts +449 -0
- package/src/index.ts +45 -0
- package/src/io/README.md +10 -0
- package/src/io/docx-session.ts +1763 -0
- package/src/io/export/README.md +3 -0
- package/src/io/export/export-session.ts +165 -0
- package/src/io/export/minimal-docx.ts +115 -0
- package/src/io/export/reattach-preserved-parts.ts +54 -0
- package/src/io/export/serialize-comments.ts +876 -0
- package/src/io/export/serialize-footnotes.ts +217 -0
- package/src/io/export/serialize-headers-footers.ts +200 -0
- package/src/io/export/serialize-main-document.ts +982 -0
- package/src/io/export/serialize-numbering.ts +97 -0
- package/src/io/export/serialize-revisions.ts +389 -0
- package/src/io/export/serialize-runtime-revisions.ts +265 -0
- package/src/io/export/serialize-tables.ts +147 -0
- package/src/io/export/split-review-boundaries.ts +194 -0
- package/src/io/normalize/README.md +3 -0
- package/src/io/normalize/normalize-text.ts +437 -0
- package/src/io/ooxml/README.md +3 -0
- package/src/io/ooxml/parse-comments.ts +779 -0
- package/src/io/ooxml/parse-complex-content.ts +287 -0
- package/src/io/ooxml/parse-fields.ts +438 -0
- package/src/io/ooxml/parse-footnotes.ts +403 -0
- package/src/io/ooxml/parse-headers-footers.ts +483 -0
- package/src/io/ooxml/parse-inline-media.ts +431 -0
- package/src/io/ooxml/parse-main-document.ts +1846 -0
- package/src/io/ooxml/parse-numbering.ts +425 -0
- package/src/io/ooxml/parse-revisions.ts +658 -0
- package/src/io/ooxml/parse-shapes.ts +271 -0
- package/src/io/ooxml/parse-tables.ts +568 -0
- package/src/io/ooxml/parse-theme.ts +314 -0
- package/src/io/ooxml/part-manifest.ts +136 -0
- package/src/io/ooxml/revision-boundaries.ts +351 -0
- package/src/io/opc/README.md +3 -0
- package/src/io/opc/corrupt-package.ts +166 -0
- package/src/io/opc/docx-package.ts +74 -0
- package/src/io/opc/package-reader.ts +325 -0
- package/src/io/opc/package-writer.ts +273 -0
- package/src/legal/bookmarks.ts +196 -0
- package/src/legal/cross-references.ts +356 -0
- package/src/legal/defined-terms.ts +203 -0
- package/src/model/README.md +3 -0
- package/src/model/canonical-document.ts +1911 -0
- package/src/model/cds-1.0.0.ts +196 -0
- package/src/model/snapshot.ts +393 -0
- package/src/preservation/README.md +3 -0
- package/src/preservation/markup-compatibility.ts +48 -0
- package/src/preservation/opaque-fragment-store.ts +89 -0
- package/src/preservation/opaque-region.ts +233 -0
- package/src/preservation/package-preservation.ts +120 -0
- package/src/preservation/preserved-part-manifest.ts +56 -0
- package/src/preservation/relationship-retention.ts +57 -0
- package/src/preservation/store.ts +185 -0
- package/src/review/README.md +16 -0
- package/src/review/store/README.md +3 -0
- package/src/review/store/comment-anchors.ts +70 -0
- package/src/review/store/comment-remapping.ts +154 -0
- package/src/review/store/comment-store.ts +331 -0
- package/src/review/store/comment-thread.ts +109 -0
- package/src/review/store/revision-actions.ts +394 -0
- package/src/review/store/revision-store.ts +303 -0
- package/src/review/store/revision-types.ts +168 -0
- package/src/review/store/runtime-comment-store.ts +43 -0
- package/src/runtime/README.md +3 -0
- package/src/runtime/ai-action-policy.ts +764 -0
- package/src/runtime/document-runtime.ts +967 -0
- package/src/runtime/read-only-diagnostics-runtime.ts +232 -0
- package/src/runtime/review-runtime.ts +44 -0
- package/src/runtime/revision-runtime.ts +107 -0
- package/src/runtime/session-capabilities.ts +138 -0
- package/src/runtime/surface-projection.ts +570 -0
- package/src/runtime/table-commands.ts +87 -0
- package/src/runtime/table-schema.ts +140 -0
- package/src/runtime/virtualized-rendering.ts +258 -0
- package/src/ui/README.md +30 -0
- package/src/ui/WordReviewEditor.tsx +1506 -0
- package/src/ui/comments/README.md +3 -0
- package/src/ui/compatibility/README.md +3 -0
- package/src/ui/editor-surface/README.md +3 -0
- package/src/ui/headless/comment-decoration-model.ts +124 -0
- package/src/ui/headless/revision-decoration-model.ts +128 -0
- package/src/ui/headless/selection-helpers.ts +34 -0
- package/src/ui/headless/use-editor-keyboard.ts +98 -0
- package/src/ui/review/README.md +3 -0
- package/src/ui/shared/revision-filters.ts +31 -0
- package/src/ui/status/README.md +3 -0
- package/src/ui/theme/README.md +3 -0
- package/src/ui/toolbar/README.md +3 -0
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +48 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +44 -0
- package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +58 -0
- package/src/ui-tailwind/chrome/use-before-unload.ts +20 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +139 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +98 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +123 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +452 -0
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +327 -0
- package/src/ui-tailwind/editor-surface/search-plugin.ts +157 -0
- package/src/ui-tailwind/editor-surface/tw-caret.tsx +12 -0
- package/src/ui-tailwind/editor-surface/tw-editor-surface.tsx +150 -0
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +118 -0
- package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +52 -0
- package/src/ui-tailwind/editor-surface/tw-paragraph-block.tsx +151 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +215 -0
- package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +111 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +122 -0
- package/src/ui-tailwind/index.ts +61 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +276 -0
- package/src/ui-tailwind/review/tw-health-panel.tsx +120 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +120 -0
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +164 -0
- package/src/ui-tailwind/status/tw-status-bar.tsx +58 -0
- package/src/ui-tailwind/theme/editor-theme.css +190 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +48 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +231 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +140 -0
- package/src/validation/README.md +3 -0
- package/src/validation/compatibility-engine.ts +317 -0
- package/src/validation/compatibility-report.ts +160 -0
- package/src/validation/diagnostics.ts +203 -0
- package/src/validation/import-diagnostics.ts +128 -0
- package/src/validation/low-priority-word-surfaces.ts +373 -0
- package/dist/chunk-32W6IVQE.js +0 -7725
- package/dist/chunk-32W6IVQE.js.map +0 -1
- package/dist/index.cjs +0 -23722
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -7
- package/dist/index.d.ts +0 -7
- package/dist/index.js +0 -16011
- package/dist/index.js.map +0 -1
- package/dist/public-types-DqCURAz8.d.cts +0 -1152
- package/dist/public-types-DqCURAz8.d.ts +0 -1152
- package/dist/tailwind.cjs +0 -8295
- package/dist/tailwind.cjs.map +0 -1
- package/dist/tailwind.d.cts +0 -323
- package/dist/tailwind.d.ts +0 -323
- package/dist/tailwind.js +0 -553
- package/dist/tailwind.js.map +0 -1
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import type { NumberingCatalog, NumberingLevelDefinition, NumberingLevelOverride } from "../../model/canonical-document.ts";
|
|
2
|
+
|
|
3
|
+
export interface ParsedParagraphNumberingReference {
|
|
4
|
+
paragraphIndex: number;
|
|
5
|
+
numberingInstanceId: string;
|
|
6
|
+
level: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface XmlElementNode {
|
|
10
|
+
type: "element";
|
|
11
|
+
name: string;
|
|
12
|
+
attributes: Record<string, string>;
|
|
13
|
+
children: XmlNode[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface XmlTextNode {
|
|
17
|
+
type: "text";
|
|
18
|
+
text: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type XmlNode = XmlElementNode | XmlTextNode;
|
|
22
|
+
|
|
23
|
+
export function parseNumberingXml(xml: string): NumberingCatalog {
|
|
24
|
+
const root = parseXml(xml);
|
|
25
|
+
const numberingElement = findChildElement(root, "numbering");
|
|
26
|
+
const abstractDefinitions: NumberingCatalog["abstractDefinitions"] = {};
|
|
27
|
+
const instances: NumberingCatalog["instances"] = {};
|
|
28
|
+
|
|
29
|
+
for (const child of numberingElement.children) {
|
|
30
|
+
if (child.type !== "element") {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
switch (localName(child.name)) {
|
|
35
|
+
case "abstractNum": {
|
|
36
|
+
const rawId = child.attributes["w:abstractNumId"] ?? child.attributes.abstractNumId;
|
|
37
|
+
if (!rawId) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const abstractNumberingId = toCanonicalAbstractNumberingId(rawId);
|
|
42
|
+
abstractDefinitions[abstractNumberingId] = {
|
|
43
|
+
abstractNumberingId,
|
|
44
|
+
levels: readLevels(child),
|
|
45
|
+
};
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case "num": {
|
|
49
|
+
const rawId = child.attributes["w:numId"] ?? child.attributes.numId;
|
|
50
|
+
const abstractReference = findChildElementOptional(child, "abstractNumId");
|
|
51
|
+
const rawAbstractId =
|
|
52
|
+
abstractReference?.attributes["w:val"] ?? abstractReference?.attributes.val;
|
|
53
|
+
|
|
54
|
+
if (!rawId || !rawAbstractId) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const numberingInstanceId = toCanonicalNumberingInstanceId(rawId);
|
|
59
|
+
instances[numberingInstanceId] = {
|
|
60
|
+
numberingInstanceId,
|
|
61
|
+
abstractNumberingId: toCanonicalAbstractNumberingId(rawAbstractId),
|
|
62
|
+
overrides: readOverrides(child),
|
|
63
|
+
};
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
abstractDefinitions,
|
|
71
|
+
instances,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function parseParagraphNumberingReferences(
|
|
76
|
+
documentXml: string,
|
|
77
|
+
): ParsedParagraphNumberingReference[] {
|
|
78
|
+
const root = parseXml(documentXml);
|
|
79
|
+
const documentElement = findChildElement(root, "document");
|
|
80
|
+
const bodyElement = findChildElement(documentElement, "body");
|
|
81
|
+
const references: ParsedParagraphNumberingReference[] = [];
|
|
82
|
+
let paragraphIndex = 0;
|
|
83
|
+
|
|
84
|
+
for (const child of bodyElement.children) {
|
|
85
|
+
if (child.type !== "element" || localName(child.name) !== "p") {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const paragraphProperties = findChildElementOptional(child, "pPr");
|
|
90
|
+
const numberingProperties = paragraphProperties
|
|
91
|
+
? findChildElementOptional(paragraphProperties, "numPr")
|
|
92
|
+
: undefined;
|
|
93
|
+
|
|
94
|
+
if (numberingProperties) {
|
|
95
|
+
const levelNode = findChildElementOptional(numberingProperties, "ilvl");
|
|
96
|
+
const instanceNode = findChildElementOptional(numberingProperties, "numId");
|
|
97
|
+
const rawLevel = levelNode?.attributes["w:val"] ?? levelNode?.attributes.val;
|
|
98
|
+
const rawInstanceId = instanceNode?.attributes["w:val"] ?? instanceNode?.attributes.val;
|
|
99
|
+
|
|
100
|
+
if (rawLevel !== undefined && rawInstanceId) {
|
|
101
|
+
const level = parseInteger(rawLevel);
|
|
102
|
+
if (level !== undefined) {
|
|
103
|
+
references.push({
|
|
104
|
+
paragraphIndex,
|
|
105
|
+
numberingInstanceId: toCanonicalNumberingInstanceId(rawInstanceId),
|
|
106
|
+
level,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
paragraphIndex += 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return references;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function toCanonicalAbstractNumberingId(value: string): string {
|
|
119
|
+
return value.startsWith("abstract-num:") ? value : `abstract-num:${value}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function toCanonicalNumberingInstanceId(value: string): string {
|
|
123
|
+
return value.startsWith("num:") ? value : `num:${value}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function readLevels(abstractNode: XmlElementNode): NumberingLevelDefinition[] {
|
|
127
|
+
const levels: NumberingLevelDefinition[] = [];
|
|
128
|
+
|
|
129
|
+
for (const child of abstractNode.children) {
|
|
130
|
+
if (child.type !== "element" || localName(child.name) !== "lvl") {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const rawLevel = child.attributes["w:ilvl"] ?? child.attributes.ilvl;
|
|
135
|
+
const level = rawLevel === undefined ? undefined : parseInteger(rawLevel);
|
|
136
|
+
if (level === undefined) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const startNode = findChildElementOptional(child, "start");
|
|
141
|
+
const formatNode = findChildElementOptional(child, "numFmt");
|
|
142
|
+
const textNode = findChildElementOptional(child, "lvlText");
|
|
143
|
+
const paragraphStyleNode = findChildElementOptional(child, "pStyle");
|
|
144
|
+
const rawStart = startNode?.attributes["w:val"] ?? startNode?.attributes.val;
|
|
145
|
+
const startAt = rawStart === undefined ? undefined : parseInteger(rawStart);
|
|
146
|
+
const format = formatNode?.attributes["w:val"] ?? formatNode?.attributes.val ?? "decimal";
|
|
147
|
+
const text = textNode?.attributes["w:val"] ?? textNode?.attributes.val ?? `%${level + 1}.`;
|
|
148
|
+
const paragraphStyleId =
|
|
149
|
+
paragraphStyleNode?.attributes["w:val"] ?? paragraphStyleNode?.attributes.val;
|
|
150
|
+
|
|
151
|
+
levels.push({
|
|
152
|
+
level,
|
|
153
|
+
format,
|
|
154
|
+
text,
|
|
155
|
+
...(startAt !== undefined ? { startAt } : {}),
|
|
156
|
+
...(paragraphStyleId ? { paragraphStyleId } : {}),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return levels.sort((left, right) => left.level - right.level);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function readOverrides(numNode: XmlElementNode): NumberingLevelOverride[] {
|
|
164
|
+
const overrides: NumberingLevelOverride[] = [];
|
|
165
|
+
|
|
166
|
+
for (const child of numNode.children) {
|
|
167
|
+
if (child.type !== "element" || localName(child.name) !== "lvlOverride") {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const rawLevel = child.attributes["w:ilvl"] ?? child.attributes.ilvl;
|
|
172
|
+
const level = rawLevel === undefined ? undefined : parseInteger(rawLevel);
|
|
173
|
+
if (level === undefined) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const startOverrideNode = findChildElementOptional(child, "startOverride");
|
|
178
|
+
const rawStart = startOverrideNode?.attributes["w:val"] ?? startOverrideNode?.attributes.val;
|
|
179
|
+
const startAt = rawStart === undefined ? undefined : parseInteger(rawStart);
|
|
180
|
+
|
|
181
|
+
overrides.push({
|
|
182
|
+
level,
|
|
183
|
+
...(startAt !== undefined ? { startAt } : {}),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return overrides.sort((left, right) => left.level - right.level);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function findChildElement(node: XmlElementNode, childLocalName: string): XmlElementNode {
|
|
191
|
+
const child = findChildElementOptional(node, childLocalName);
|
|
192
|
+
if (!child) {
|
|
193
|
+
throw new Error(`Expected <${childLocalName}> element in numbering XML.`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return child;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function findChildElementOptional(
|
|
200
|
+
node: XmlElementNode,
|
|
201
|
+
childLocalName: string,
|
|
202
|
+
): XmlElementNode | undefined {
|
|
203
|
+
return node.children.find(
|
|
204
|
+
(entry): entry is XmlElementNode =>
|
|
205
|
+
entry.type === "element" && localName(entry.name) === childLocalName,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function localName(name: string): string {
|
|
210
|
+
const separatorIndex = name.indexOf(":");
|
|
211
|
+
return separatorIndex >= 0 ? name.slice(separatorIndex + 1) : name;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function parseInteger(value: string): number | undefined {
|
|
215
|
+
if (!/^-?\d+$/.test(value)) {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return Number.parseInt(value, 10);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function parseXml(xml: string): XmlElementNode {
|
|
223
|
+
const root: XmlElementNode = {
|
|
224
|
+
type: "element",
|
|
225
|
+
name: "__root__",
|
|
226
|
+
attributes: {},
|
|
227
|
+
children: [],
|
|
228
|
+
};
|
|
229
|
+
const stack: XmlElementNode[] = [root];
|
|
230
|
+
let cursor = 0;
|
|
231
|
+
|
|
232
|
+
while (cursor < xml.length) {
|
|
233
|
+
if (xml.startsWith("<!--", cursor)) {
|
|
234
|
+
const end = xml.indexOf("-->", cursor);
|
|
235
|
+
cursor = end >= 0 ? end + 3 : xml.length;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (xml.startsWith("<?", cursor)) {
|
|
240
|
+
const end = xml.indexOf("?>", cursor);
|
|
241
|
+
cursor = end >= 0 ? end + 2 : xml.length;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (xml.startsWith("<![CDATA[", cursor)) {
|
|
246
|
+
const end = xml.indexOf("]]>", cursor);
|
|
247
|
+
const textEnd = end >= 0 ? end : xml.length;
|
|
248
|
+
stack[stack.length - 1]?.children.push({
|
|
249
|
+
type: "text",
|
|
250
|
+
text: xml.slice(cursor + 9, textEnd),
|
|
251
|
+
});
|
|
252
|
+
cursor = end >= 0 ? end + 3 : xml.length;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (xml[cursor] !== "<") {
|
|
257
|
+
const nextTag = xml.indexOf("<", cursor);
|
|
258
|
+
const end = nextTag >= 0 ? nextTag : xml.length;
|
|
259
|
+
const text = decodeXmlEntities(xml.slice(cursor, end));
|
|
260
|
+
if (text.length > 0) {
|
|
261
|
+
stack[stack.length - 1]?.children.push({
|
|
262
|
+
type: "text",
|
|
263
|
+
text,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
cursor = end;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (xml[cursor + 1] === "/") {
|
|
271
|
+
const end = xml.indexOf(">", cursor);
|
|
272
|
+
if (end < 0) {
|
|
273
|
+
throw new Error("Malformed XML: missing closing >.");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const name = xml.slice(cursor + 2, end).trim();
|
|
277
|
+
const current = stack.pop();
|
|
278
|
+
if (!current || localName(current.name) !== localName(name)) {
|
|
279
|
+
throw new Error(`Malformed XML: unexpected closing tag </${name}>.`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
cursor = end + 1;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const tagEnd = findTagEnd(xml, cursor);
|
|
287
|
+
const tagBody = xml.slice(cursor + 1, tagEnd);
|
|
288
|
+
const selfClosing = /\/\s*$/.test(tagBody);
|
|
289
|
+
const { name, attributes } = parseTag(tagBody.replace(/\/\s*$/, "").trim());
|
|
290
|
+
const element: XmlElementNode = {
|
|
291
|
+
type: "element",
|
|
292
|
+
name,
|
|
293
|
+
attributes,
|
|
294
|
+
children: [],
|
|
295
|
+
};
|
|
296
|
+
stack[stack.length - 1]?.children.push(element);
|
|
297
|
+
|
|
298
|
+
if (!selfClosing) {
|
|
299
|
+
stack.push(element);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
cursor = tagEnd + 1;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (stack.length !== 1) {
|
|
306
|
+
throw new Error("Malformed XML: unclosed element.");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return root;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function parseTag(tagBody: string): { name: string; attributes: Record<string, string> } {
|
|
313
|
+
let cursor = 0;
|
|
314
|
+
while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
|
|
315
|
+
cursor += 1;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const nameStart = cursor;
|
|
319
|
+
while (cursor < tagBody.length && !/\s/.test(tagBody[cursor] ?? "")) {
|
|
320
|
+
cursor += 1;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const name = tagBody.slice(nameStart, cursor);
|
|
324
|
+
const attributes: Record<string, string> = {};
|
|
325
|
+
|
|
326
|
+
while (cursor < tagBody.length) {
|
|
327
|
+
while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
|
|
328
|
+
cursor += 1;
|
|
329
|
+
}
|
|
330
|
+
if (cursor >= tagBody.length) {
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const keyStart = cursor;
|
|
335
|
+
while (cursor < tagBody.length && !/[\s=]/.test(tagBody[cursor] ?? "")) {
|
|
336
|
+
cursor += 1;
|
|
337
|
+
}
|
|
338
|
+
const key = tagBody.slice(keyStart, cursor);
|
|
339
|
+
|
|
340
|
+
while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
|
|
341
|
+
cursor += 1;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (tagBody[cursor] !== "=") {
|
|
345
|
+
attributes[key] = "";
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
cursor += 1;
|
|
349
|
+
|
|
350
|
+
while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
|
|
351
|
+
cursor += 1;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const quote = tagBody[cursor];
|
|
355
|
+
if (quote !== `"` && quote !== `'`) {
|
|
356
|
+
throw new Error(`Malformed XML attribute ${key}.`);
|
|
357
|
+
}
|
|
358
|
+
cursor += 1;
|
|
359
|
+
|
|
360
|
+
const valueStart = cursor;
|
|
361
|
+
while (cursor < tagBody.length && tagBody[cursor] !== quote) {
|
|
362
|
+
cursor += 1;
|
|
363
|
+
}
|
|
364
|
+
const rawValue = tagBody.slice(valueStart, cursor);
|
|
365
|
+
attributes[key] = decodeXmlEntities(rawValue);
|
|
366
|
+
cursor += 1;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return { name, attributes };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function findTagEnd(xml: string, start: number): number {
|
|
373
|
+
let cursor = start + 1;
|
|
374
|
+
let quote: string | null = null;
|
|
375
|
+
|
|
376
|
+
while (cursor < xml.length) {
|
|
377
|
+
const current = xml[cursor];
|
|
378
|
+
if (quote) {
|
|
379
|
+
if (current === quote) {
|
|
380
|
+
quote = null;
|
|
381
|
+
}
|
|
382
|
+
cursor += 1;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (current === `"` || current === `'`) {
|
|
387
|
+
quote = current;
|
|
388
|
+
cursor += 1;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (current === ">") {
|
|
393
|
+
return cursor;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
cursor += 1;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
throw new Error("Malformed XML: missing >.");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function decodeXmlEntities(value: string): string {
|
|
403
|
+
return value.replace(/&(#x[0-9a-fA-F]+|#\d+|amp|lt|gt|quot|apos);/g, (match, entity) => {
|
|
404
|
+
switch (entity) {
|
|
405
|
+
case "amp":
|
|
406
|
+
return "&";
|
|
407
|
+
case "lt":
|
|
408
|
+
return "<";
|
|
409
|
+
case "gt":
|
|
410
|
+
return ">";
|
|
411
|
+
case "quot":
|
|
412
|
+
return `"`;
|
|
413
|
+
case "apos":
|
|
414
|
+
return "'";
|
|
415
|
+
default:
|
|
416
|
+
if (entity.startsWith("#x")) {
|
|
417
|
+
return String.fromCodePoint(Number.parseInt(entity.slice(2), 16));
|
|
418
|
+
}
|
|
419
|
+
if (entity.startsWith("#")) {
|
|
420
|
+
return String.fromCodePoint(Number.parseInt(entity.slice(1), 10));
|
|
421
|
+
}
|
|
422
|
+
return match;
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|