@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
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
import type { ShapeContent } from "../../model/canonical-document.ts";
|
|
14
14
|
import { parseFill } from "./parse-fill.ts";
|
|
15
|
+
import {
|
|
16
|
+
type XmlElementNode,
|
|
17
|
+
findFirstChild,
|
|
18
|
+
findFirstDescendant,
|
|
19
|
+
localName,
|
|
20
|
+
parseXml,
|
|
21
|
+
serializeXmlNode,
|
|
22
|
+
} from "./_mini-xml.ts";
|
|
15
23
|
|
|
16
24
|
const WPS_SHAPE_GRAPHIC_URI =
|
|
17
25
|
"http://schemas.microsoft.com/office/word/2010/wordprocessingShape";
|
|
@@ -88,11 +96,13 @@ export function parseShapeXml(drawingXml: string): ParsedWpsShape | ParsedWordAr
|
|
|
88
96
|
};
|
|
89
97
|
}
|
|
90
98
|
|
|
91
|
-
//
|
|
92
|
-
|
|
99
|
+
// Phase 1.2 B6 — text-box detection is defined by presence of txbxContent.
|
|
100
|
+
// Geometry is styling (rect, roundRect, ellipse, callouts, custom flowchart
|
|
101
|
+
// shapes) and any of them can legitimately hold editable text.
|
|
102
|
+
const isTextBox = Boolean(txbxContent);
|
|
93
103
|
|
|
94
104
|
// Extract raw txbxContent XML for structured re-rendering of text boxes
|
|
95
|
-
const txbxContentXml = txbxContent ? extractRawXml(txbxContent
|
|
105
|
+
const txbxContentXml = txbxContent ? extractRawXml(txbxContent) : undefined;
|
|
96
106
|
|
|
97
107
|
return {
|
|
98
108
|
type: "shape",
|
|
@@ -148,15 +158,8 @@ export function parseVmlXml(pictXml: string): ParsedVmlShape | null {
|
|
|
148
158
|
|
|
149
159
|
// ---- Raw XML extraction helpers ----
|
|
150
160
|
|
|
151
|
-
function extractRawXml(node: XmlElementNode
|
|
152
|
-
|
|
153
|
-
const tagName = node.name;
|
|
154
|
-
const openIdx = sourceXml.indexOf(`<${tagName}`);
|
|
155
|
-
if (openIdx < 0) return undefined;
|
|
156
|
-
const closeTag = `</${tagName}>`;
|
|
157
|
-
const closeIdx = sourceXml.indexOf(closeTag, openIdx);
|
|
158
|
-
if (closeIdx < 0) return undefined;
|
|
159
|
-
return sourceXml.slice(openIdx, closeIdx + closeTag.length);
|
|
161
|
+
function extractRawXml(node: XmlElementNode): string {
|
|
162
|
+
return serializeXmlNode(node);
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
// ---- Text extraction helpers ----
|
|
@@ -174,129 +177,9 @@ function extractAllText(node: XmlElementNode): string {
|
|
|
174
177
|
.join("");
|
|
175
178
|
}
|
|
176
179
|
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
type: "element";
|
|
181
|
-
name: string;
|
|
182
|
-
attributes: Record<string, string>;
|
|
183
|
-
children: XmlNode[];
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
interface XmlTextNode {
|
|
187
|
-
type: "text";
|
|
188
|
-
text: string;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
type XmlNode = XmlElementNode | XmlTextNode;
|
|
192
|
-
|
|
193
|
-
function findFirstChild(node: XmlElementNode, local: string): XmlElementNode | undefined {
|
|
194
|
-
for (const child of node.children) {
|
|
195
|
-
if (child.type === "element" && localName(child.name) === local) return child;
|
|
196
|
-
}
|
|
197
|
-
return undefined;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function findFirstDescendant(node: XmlElementNode, local: string): XmlElementNode | undefined {
|
|
201
|
-
for (const child of node.children) {
|
|
202
|
-
if (child.type !== "element") continue;
|
|
203
|
-
if (localName(child.name) === local) return child;
|
|
204
|
-
const nested = findFirstDescendant(child, local);
|
|
205
|
-
if (nested) return nested;
|
|
206
|
-
}
|
|
207
|
-
return undefined;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function localName(name: string): string {
|
|
211
|
-
const i = name.indexOf(":");
|
|
212
|
-
return i >= 0 ? name.slice(i + 1) : name;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function parseXml(xml: string): XmlElementNode {
|
|
216
|
-
const root: XmlElementNode = { type: "element", name: "__root__", attributes: {}, children: [] };
|
|
217
|
-
const stack: XmlElementNode[] = [root];
|
|
218
|
-
let cursor = 0;
|
|
219
|
-
|
|
220
|
-
while (cursor < xml.length) {
|
|
221
|
-
if (xml.startsWith("<!--", cursor)) {
|
|
222
|
-
const end = xml.indexOf("-->", cursor);
|
|
223
|
-
cursor = end >= 0 ? end + 3 : xml.length;
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
if (xml.startsWith("<?", cursor)) {
|
|
227
|
-
const end = xml.indexOf("?>", cursor);
|
|
228
|
-
cursor = end >= 0 ? end + 2 : xml.length;
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
if (xml[cursor] !== "<") {
|
|
232
|
-
const nextTag = xml.indexOf("<", cursor);
|
|
233
|
-
const end = nextTag >= 0 ? nextTag : xml.length;
|
|
234
|
-
const text = xml.slice(cursor, end);
|
|
235
|
-
if (text.length > 0) stack[stack.length - 1]?.children.push({ type: "text", text });
|
|
236
|
-
cursor = end;
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
if (xml[cursor + 1] === "/") {
|
|
240
|
-
const end = xml.indexOf(">", cursor);
|
|
241
|
-
stack.pop();
|
|
242
|
-
cursor = end + 1;
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
const tagEnd = findTagEnd(xml, cursor);
|
|
246
|
-
const tagBody = xml.slice(cursor + 1, tagEnd);
|
|
247
|
-
const selfClosing = /\/\s*$/.test(tagBody);
|
|
248
|
-
const { name, attributes } = parseTag(tagBody.replace(/\/\s*$/, "").trim());
|
|
249
|
-
const element: XmlElementNode = { type: "element", name, attributes, children: [] };
|
|
250
|
-
stack[stack.length - 1]?.children.push(element);
|
|
251
|
-
if (!selfClosing) stack.push(element);
|
|
252
|
-
cursor = tagEnd + 1;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return root;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function parseTag(tagBody: string): { name: string; attributes: Record<string, string> } {
|
|
259
|
-
let cursor = 0;
|
|
260
|
-
while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) cursor++;
|
|
261
|
-
const nameStart = cursor;
|
|
262
|
-
while (cursor < tagBody.length && !/\s/.test(tagBody[cursor] ?? "")) cursor++;
|
|
263
|
-
const name = tagBody.slice(nameStart, cursor);
|
|
264
|
-
const attributes: Record<string, string> = {};
|
|
265
|
-
|
|
266
|
-
while (cursor < tagBody.length) {
|
|
267
|
-
while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) cursor++;
|
|
268
|
-
if (cursor >= tagBody.length) break;
|
|
269
|
-
const keyStart = cursor;
|
|
270
|
-
while (cursor < tagBody.length && !/[\s=]/.test(tagBody[cursor] ?? "")) cursor++;
|
|
271
|
-
const key = tagBody.slice(keyStart, cursor);
|
|
272
|
-
while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) cursor++;
|
|
273
|
-
if (tagBody[cursor] !== "=") { attributes[key] = ""; continue; }
|
|
274
|
-
cursor++;
|
|
275
|
-
while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) cursor++;
|
|
276
|
-
const quote = tagBody[cursor];
|
|
277
|
-
if (quote !== `"` && quote !== `'`) break;
|
|
278
|
-
cursor++;
|
|
279
|
-
const valueStart = cursor;
|
|
280
|
-
while (cursor < tagBody.length && tagBody[cursor] !== quote) cursor++;
|
|
281
|
-
attributes[key] = tagBody.slice(valueStart, cursor);
|
|
282
|
-
cursor++;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return { name, attributes };
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function findTagEnd(xml: string, start: number): number {
|
|
289
|
-
let cursor = start + 1;
|
|
290
|
-
let quote: string | null = null;
|
|
291
|
-
while (cursor < xml.length) {
|
|
292
|
-
const c = xml[cursor];
|
|
293
|
-
if (quote) { if (c === quote) quote = null; cursor++; continue; }
|
|
294
|
-
if (c === `"` || c === `'`) { quote = c; cursor++; continue; }
|
|
295
|
-
if (c === ">") return cursor;
|
|
296
|
-
cursor++;
|
|
297
|
-
}
|
|
298
|
-
return xml.length - 1;
|
|
299
|
-
}
|
|
180
|
+
// Phase 6 — XML helpers imported from ./_mini-xml.ts (consolidates the
|
|
181
|
+
// previously-duplicated parser across parse-anchor/parse-drawing/parse-picture/
|
|
182
|
+
// parse-shapes). See that module for B4 throw-on-unterminated-tag contract.
|
|
300
183
|
|
|
301
184
|
// ───────────────────────────────────────────────────────────────────────────
|
|
302
185
|
// CO4.3 — parseShapeContent: wps:wsp → ShapeContent (geometry, fill, line,
|
|
@@ -327,7 +210,7 @@ export function parseShapeContent(
|
|
|
327
210
|
// optional blockParser callback (CO4 F3.3) to populate txbxBlocks.
|
|
328
211
|
const txbx = findFirstChild(wsp, "txbx");
|
|
329
212
|
const txbxContent = txbx ? findFirstDescendant(txbx, "txbxContent") : undefined;
|
|
330
|
-
const txbxContentXml = txbxContent ? extractRawXml(txbxContent
|
|
213
|
+
const txbxContentXml = txbxContent ? extractRawXml(txbxContent) : undefined;
|
|
331
214
|
|
|
332
215
|
let txbxBlocks: ReadonlyArray<{ type: string; [key: string]: unknown }> | undefined;
|
|
333
216
|
if (txbxContentXml && blockParser) {
|
|
@@ -340,10 +223,12 @@ export function parseShapeContent(
|
|
|
340
223
|
}
|
|
341
224
|
}
|
|
342
225
|
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
|
|
226
|
+
// Phase 1.2 B6 — text-box detection is defined by presence of txbxContent.
|
|
227
|
+
// Geometry is styling (rect, roundRect, ellipse, callouts, custom flowchart
|
|
228
|
+
// shapes) and any of them can legitimately hold editable text. Previously
|
|
229
|
+
// only "rect" or missing geometry qualified, mis-classifying Word's default
|
|
230
|
+
// roundRect text boxes + callout/ellipse shapes as non-text.
|
|
231
|
+
const isTextBox = Boolean(txbxContent);
|
|
347
232
|
|
|
348
233
|
const result: ShapeContent = { type: "shape", rawXml: drawingRawXml };
|
|
349
234
|
if (geometry) result.geometry = geometry;
|
|
@@ -41,6 +41,8 @@ import {
|
|
|
41
41
|
import { toCanonicalNumberingInstanceId } from "./parse-numbering.ts";
|
|
42
42
|
import { readRunProperties } from "./parse-run-formatting.ts";
|
|
43
43
|
import { readParagraphProperties } from "./parse-paragraph-formatting.ts";
|
|
44
|
+
import { parseXmlWithOffsets } from "./xml-parser.ts";
|
|
45
|
+
import { localName } from "./xml-attr-helpers.ts";
|
|
44
46
|
|
|
45
47
|
// ---------------------------------------------------------------------------
|
|
46
48
|
// Inline XML node types (same pattern as parse-numbering.ts)
|
|
@@ -96,7 +98,7 @@ export function parseStylesXml(xml: string): ParseStylesResult {
|
|
|
96
98
|
|
|
97
99
|
let root: XmlElementNode;
|
|
98
100
|
try {
|
|
99
|
-
root =
|
|
101
|
+
root = parseXmlWithOffsets(xml) as XmlElementNode;
|
|
100
102
|
} catch {
|
|
101
103
|
diagnostics.push("styles.xml could not be parsed; synthetic fallback will be used");
|
|
102
104
|
return {
|
|
@@ -588,221 +590,4 @@ function findChildElementOptional(
|
|
|
588
590
|
);
|
|
589
591
|
}
|
|
590
592
|
|
|
591
|
-
function localName(name: string): string {
|
|
592
|
-
const separatorIndex = name.indexOf(":");
|
|
593
|
-
return separatorIndex >= 0 ? name.slice(separatorIndex + 1) : name;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function parseXml(xml: string): XmlElementNode {
|
|
597
|
-
const root: XmlElementNode = {
|
|
598
|
-
type: "element",
|
|
599
|
-
name: "__root__",
|
|
600
|
-
attributes: {},
|
|
601
|
-
children: [],
|
|
602
|
-
start: 0,
|
|
603
|
-
end: xml.length,
|
|
604
|
-
};
|
|
605
|
-
const stack: XmlElementNode[] = [root];
|
|
606
|
-
let cursor = 0;
|
|
607
|
-
|
|
608
|
-
while (cursor < xml.length) {
|
|
609
|
-
if (xml.startsWith("<!--", cursor)) {
|
|
610
|
-
const end = xml.indexOf("-->", cursor);
|
|
611
|
-
cursor = end >= 0 ? end + 3 : xml.length;
|
|
612
|
-
continue;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
if (xml.startsWith("<?", cursor)) {
|
|
616
|
-
const end = xml.indexOf("?>", cursor);
|
|
617
|
-
cursor = end >= 0 ? end + 2 : xml.length;
|
|
618
|
-
continue;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
if (xml.startsWith("<![CDATA[", cursor)) {
|
|
622
|
-
const end = xml.indexOf("]]>", cursor);
|
|
623
|
-
const textEnd = end >= 0 ? end : xml.length;
|
|
624
|
-
stack[stack.length - 1]?.children.push({
|
|
625
|
-
type: "text",
|
|
626
|
-
text: xml.slice(cursor + 9, textEnd),
|
|
627
|
-
start: cursor,
|
|
628
|
-
end: end >= 0 ? end + 3 : xml.length,
|
|
629
|
-
});
|
|
630
|
-
cursor = end >= 0 ? end + 3 : xml.length;
|
|
631
|
-
continue;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
if (xml[cursor] !== "<") {
|
|
635
|
-
const nextTag = xml.indexOf("<", cursor);
|
|
636
|
-
const end = nextTag >= 0 ? nextTag : xml.length;
|
|
637
|
-
const text = decodeXmlEntities(xml.slice(cursor, end));
|
|
638
|
-
if (text.length > 0) {
|
|
639
|
-
stack[stack.length - 1]?.children.push({
|
|
640
|
-
type: "text",
|
|
641
|
-
text,
|
|
642
|
-
start: cursor,
|
|
643
|
-
end,
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
cursor = end;
|
|
647
|
-
continue;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
if (xml[cursor + 1] === "/") {
|
|
651
|
-
const end = xml.indexOf(">", cursor);
|
|
652
|
-
if (end < 0) {
|
|
653
|
-
throw new Error("Malformed XML: missing closing >.");
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
const name = xml.slice(cursor + 2, end).trim();
|
|
657
|
-
const current = stack.pop();
|
|
658
|
-
if (!current || localName(current.name) !== localName(name)) {
|
|
659
|
-
throw new Error(`Malformed XML: unexpected closing tag </${name}>.`);
|
|
660
|
-
}
|
|
661
|
-
current.end = end + 1;
|
|
662
|
-
|
|
663
|
-
cursor = end + 1;
|
|
664
|
-
continue;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
const tagEnd = findTagEnd(xml, cursor);
|
|
668
|
-
const tagBody = xml.slice(cursor + 1, tagEnd);
|
|
669
|
-
const selfClosing = /\/\s*$/.test(tagBody);
|
|
670
|
-
const { name, attributes } = parseTag(tagBody.replace(/\/\s*$/, "").trim());
|
|
671
|
-
const element: XmlElementNode = {
|
|
672
|
-
type: "element",
|
|
673
|
-
name,
|
|
674
|
-
attributes,
|
|
675
|
-
children: [],
|
|
676
|
-
start: cursor,
|
|
677
|
-
end: tagEnd + 1,
|
|
678
|
-
};
|
|
679
|
-
stack[stack.length - 1]?.children.push(element);
|
|
680
|
-
|
|
681
|
-
if (!selfClosing) {
|
|
682
|
-
stack.push(element);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
cursor = tagEnd + 1;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
if (stack.length !== 1) {
|
|
689
|
-
throw new Error("Malformed XML: unclosed element.");
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
return root;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
function parseTag(tagBody: string): { name: string; attributes: Record<string, string> } {
|
|
696
|
-
let cursor = 0;
|
|
697
|
-
while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
|
|
698
|
-
cursor += 1;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
const nameStart = cursor;
|
|
702
|
-
while (cursor < tagBody.length && !/\s/.test(tagBody[cursor] ?? "")) {
|
|
703
|
-
cursor += 1;
|
|
704
|
-
}
|
|
705
593
|
|
|
706
|
-
const name = tagBody.slice(nameStart, cursor);
|
|
707
|
-
const attributes: Record<string, string> = {};
|
|
708
|
-
|
|
709
|
-
while (cursor < tagBody.length) {
|
|
710
|
-
while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
|
|
711
|
-
cursor += 1;
|
|
712
|
-
}
|
|
713
|
-
if (cursor >= tagBody.length) {
|
|
714
|
-
break;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
const keyStart = cursor;
|
|
718
|
-
while (cursor < tagBody.length && !/[\s=]/.test(tagBody[cursor] ?? "")) {
|
|
719
|
-
cursor += 1;
|
|
720
|
-
}
|
|
721
|
-
const key = tagBody.slice(keyStart, cursor);
|
|
722
|
-
|
|
723
|
-
while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
|
|
724
|
-
cursor += 1;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
if (tagBody[cursor] !== "=") {
|
|
728
|
-
attributes[key] = "";
|
|
729
|
-
continue;
|
|
730
|
-
}
|
|
731
|
-
cursor += 1;
|
|
732
|
-
|
|
733
|
-
while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
|
|
734
|
-
cursor += 1;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
const quote = tagBody[cursor];
|
|
738
|
-
if (quote !== `"` && quote !== `'`) {
|
|
739
|
-
throw new Error(`Malformed XML attribute ${key}.`);
|
|
740
|
-
}
|
|
741
|
-
cursor += 1;
|
|
742
|
-
|
|
743
|
-
const valueStart = cursor;
|
|
744
|
-
while (cursor < tagBody.length && tagBody[cursor] !== quote) {
|
|
745
|
-
cursor += 1;
|
|
746
|
-
}
|
|
747
|
-
const rawValue = tagBody.slice(valueStart, cursor);
|
|
748
|
-
attributes[key] = decodeXmlEntities(rawValue);
|
|
749
|
-
cursor += 1;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
return { name, attributes };
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
function findTagEnd(xml: string, start: number): number {
|
|
756
|
-
let cursor = start + 1;
|
|
757
|
-
let quote: string | null = null;
|
|
758
|
-
|
|
759
|
-
while (cursor < xml.length) {
|
|
760
|
-
const current = xml[cursor];
|
|
761
|
-
if (quote) {
|
|
762
|
-
if (current === quote) {
|
|
763
|
-
quote = null;
|
|
764
|
-
}
|
|
765
|
-
cursor += 1;
|
|
766
|
-
continue;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
if (current === `"` || current === `'`) {
|
|
770
|
-
quote = current;
|
|
771
|
-
cursor += 1;
|
|
772
|
-
continue;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
if (current === ">") {
|
|
776
|
-
return cursor;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
cursor += 1;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
throw new Error("Malformed XML: missing >.");
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
function decodeXmlEntities(value: string): string {
|
|
786
|
-
return value.replace(/&(#x[0-9a-fA-F]+|#\d+|amp|lt|gt|quot|apos);/g, (match, entity) => {
|
|
787
|
-
switch (entity) {
|
|
788
|
-
case "amp":
|
|
789
|
-
return "&";
|
|
790
|
-
case "lt":
|
|
791
|
-
return "<";
|
|
792
|
-
case "gt":
|
|
793
|
-
return ">";
|
|
794
|
-
case "quot":
|
|
795
|
-
return `"`;
|
|
796
|
-
case "apos":
|
|
797
|
-
return "'";
|
|
798
|
-
default:
|
|
799
|
-
if (entity.startsWith("#x")) {
|
|
800
|
-
return String.fromCodePoint(Number.parseInt(entity.slice(2), 16));
|
|
801
|
-
}
|
|
802
|
-
if (entity.startsWith("#")) {
|
|
803
|
-
return String.fromCodePoint(Number.parseInt(entity.slice(1), 10));
|
|
804
|
-
}
|
|
805
|
-
return match;
|
|
806
|
-
}
|
|
807
|
-
});
|
|
808
|
-
}
|