@beyondwork/docx-react-component 1.0.58 → 1.0.59

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.
Files changed (134) hide show
  1. package/README.md +2 -2
  2. package/package.json +2 -1
  3. package/src/api/awareness-identity-types.ts +4 -2
  4. package/src/api/comment-negotiation-types.ts +4 -1
  5. package/src/api/external-custody-types.ts +16 -0
  6. package/src/api/internal/build-ref-projections.ts +108 -0
  7. package/src/api/package-version.ts +1 -1
  8. package/src/api/participants-types.ts +11 -1
  9. package/src/api/public-types.ts +978 -10
  10. package/src/api/scope-metadata-resolver-types.ts +6 -0
  11. package/src/compare/diff-engine.ts +3 -0
  12. package/src/core/commands/formatting-commands.ts +1 -0
  13. package/src/core/commands/index.ts +225 -16
  14. package/src/core/commands/legacy-form-field-commands.ts +181 -0
  15. package/src/core/commands/table-structure-commands.ts +149 -31
  16. package/src/core/selection/mapping.ts +20 -0
  17. package/src/core/state/editor-state.ts +2 -1
  18. package/src/index.ts +28 -0
  19. package/src/io/docx-session.ts +22 -3
  20. package/src/io/export/export-session.ts +11 -7
  21. package/src/io/export/ooxml-namespaces.ts +47 -0
  22. package/src/io/export/reattach-preserved-parts.ts +4 -16
  23. package/src/io/export/serialize-comments.ts +3 -131
  24. package/src/io/export/serialize-ffdata.ts +89 -0
  25. package/src/io/export/serialize-headers-footers.ts +5 -0
  26. package/src/io/export/serialize-main-document.ts +224 -34
  27. package/src/io/export/serialize-numbering.ts +22 -2
  28. package/src/io/export/serialize-revisions.ts +99 -0
  29. package/src/io/export/serialize-tables.ts +9 -0
  30. package/src/io/export/split-review-boundaries.ts +1 -0
  31. package/src/io/export/table-properties-xml.ts +14 -0
  32. package/src/io/load-scheduler.ts +70 -28
  33. package/src/io/normalize/normalize-text.ts +13 -0
  34. package/src/io/ooxml/_mini-xml.ts +198 -0
  35. package/src/io/ooxml/canonicalize-payload.ts +1 -4
  36. package/src/io/ooxml/chart/chart-style-table.ts +4 -3
  37. package/src/io/ooxml/chart/parse-chart-space.ts +2 -4
  38. package/src/io/ooxml/chart/parse-series.ts +2 -1
  39. package/src/io/ooxml/chart/resolve-color.ts +2 -2
  40. package/src/io/ooxml/chart/types.ts +6 -434
  41. package/src/io/ooxml/comment-presentation-payload.ts +6 -5
  42. package/src/io/ooxml/highlight-colors.ts +8 -5
  43. package/src/io/ooxml/parse-anchor.ts +68 -53
  44. package/src/io/ooxml/parse-comments.ts +14 -142
  45. package/src/io/ooxml/parse-complex-content.ts +3 -106
  46. package/src/io/ooxml/parse-drawing.ts +100 -195
  47. package/src/io/ooxml/parse-ffdata.ts +93 -0
  48. package/src/io/ooxml/parse-fields.ts +7 -146
  49. package/src/io/ooxml/parse-fill.ts +88 -8
  50. package/src/io/ooxml/parse-font-table.ts +5 -105
  51. package/src/io/ooxml/parse-footnotes.ts +28 -152
  52. package/src/io/ooxml/parse-headers-footers.ts +106 -212
  53. package/src/io/ooxml/parse-inline-media.ts +3 -200
  54. package/src/io/ooxml/parse-main-document.ts +180 -217
  55. package/src/io/ooxml/parse-numbering.ts +154 -335
  56. package/src/io/ooxml/parse-object.ts +147 -0
  57. package/src/io/ooxml/parse-ole-relationship.ts +82 -0
  58. package/src/io/ooxml/parse-paragraph-formatting.ts +7 -10
  59. package/src/io/ooxml/parse-picture-sdt.ts +85 -0
  60. package/src/io/ooxml/parse-picture.ts +72 -42
  61. package/src/io/ooxml/parse-revisions.ts +285 -51
  62. package/src/io/ooxml/parse-settings.ts +6 -99
  63. package/src/io/ooxml/parse-shapes.ts +25 -140
  64. package/src/io/ooxml/parse-styles.ts +3 -218
  65. package/src/io/ooxml/parse-tables.ts +76 -256
  66. package/src/io/ooxml/parse-theme.ts +1 -4
  67. package/src/io/ooxml/property-grab-bag.ts +5 -47
  68. package/src/io/ooxml/xml-element-serialize.ts +32 -0
  69. package/src/io/ooxml/xml-parser.ts +183 -0
  70. package/src/legal/bookmarks.ts +1 -1
  71. package/src/legal/cross-references.ts +1 -1
  72. package/src/legal/defined-terms.ts +1 -1
  73. package/src/legal/{_document-root.ts → document-root.ts} +8 -0
  74. package/src/legal/signature-blocks.ts +1 -1
  75. package/src/model/canonical-document.ts +159 -6
  76. package/src/model/chart-types.ts +439 -0
  77. package/src/model/snapshot.ts +3 -1
  78. package/src/review/store/comment-remapping.ts +24 -11
  79. package/src/review/store/revision-actions.ts +482 -2
  80. package/src/review/store/revision-store.ts +15 -0
  81. package/src/review/store/revision-types.ts +76 -0
  82. package/src/runtime/collab/remote-cursor-awareness.ts +24 -0
  83. package/src/runtime/collab/runtime-collab-sync.ts +33 -0
  84. package/src/runtime/diagnostics/build-diagnostic.ts +151 -0
  85. package/src/runtime/diagnostics/code-metadata-table.ts +221 -0
  86. package/src/runtime/document-runtime.ts +476 -34
  87. package/src/runtime/document-search.ts +115 -0
  88. package/src/runtime/edit-ops/index.ts +18 -2
  89. package/src/runtime/footnote-resolver.ts +130 -0
  90. package/src/runtime/layout/layout-engine-instance.ts +31 -4
  91. package/src/runtime/layout/layout-engine-version.ts +37 -1
  92. package/src/runtime/layout/page-graph.ts +14 -1
  93. package/src/runtime/layout/resolved-formatting-state.ts +21 -0
  94. package/src/runtime/numbering-prefix.ts +17 -0
  95. package/src/runtime/query-scopes.ts +5 -8
  96. package/src/runtime/resolved-numbering-geometry.ts +37 -6
  97. package/src/runtime/revision-runtime.ts +27 -1
  98. package/src/runtime/selection/post-edit-validator.ts +60 -6
  99. package/src/runtime/structure-ops/index.ts +20 -4
  100. package/src/runtime/surface-projection.ts +290 -21
  101. package/src/runtime/table-schema.ts +6 -0
  102. package/src/runtime/theme-color-resolver.ts +2 -2
  103. package/src/runtime/units.ts +9 -0
  104. package/src/runtime/workflow-rail-segments.ts +4 -0
  105. package/src/ui/WordReviewEditor.tsx +187 -43
  106. package/src/ui/editor-runtime-boundary.ts +10 -0
  107. package/src/ui/editor-shell-view.tsx +4 -1
  108. package/src/ui/headless/chrome-registry.ts +53 -0
  109. package/src/ui/headless/selection-tool-resolver.ts +11 -1
  110. package/src/ui-tailwind/chrome/chrome-preset-model.ts +13 -0
  111. package/src/ui-tailwind/chrome/tw-command-palette-mount.tsx +96 -0
  112. package/src/ui-tailwind/chrome/tw-context-menu.tsx +2 -1
  113. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +5 -4
  114. package/src/ui-tailwind/chrome/tw-mode-dock.tsx +6 -2
  115. package/src/ui-tailwind/chrome/use-container-breakpoint.ts +111 -0
  116. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +0 -9
  117. package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +1 -0
  118. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +6 -7
  119. package/src/ui-tailwind/editor-surface/pm-schema.ts +87 -25
  120. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +9 -0
  121. package/src/ui-tailwind/editor-surface/shape-renderer.ts +76 -14
  122. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +18 -1
  123. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +2 -0
  124. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +18 -2
  125. package/src/ui-tailwind/index.ts +9 -0
  126. package/src/ui-tailwind/page-chrome-model.ts +77 -5
  127. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +56 -1
  128. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +2 -0
  129. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +116 -113
  130. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +2 -2
  131. package/src/ui-tailwind/theme/tokens.ts +14 -0
  132. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +5 -0
  133. package/src/ui-tailwind/tw-review-workspace.tsx +29 -87
  134. 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
- // Text box detection: rect or no geometry with text content
92
- const isTextBox = Boolean(txbxContent && (!prst || prst === "rect"));
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, drawingXml) : undefined;
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, sourceXml: string): string | undefined {
152
- // Find the txbxContent element boundaries in the source XML by tag name
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
- // ---- Minimal XML parser (local, self-contained) ----
178
-
179
- interface XmlElementNode {
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, drawingRawXml) : undefined;
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
- // WordArt heuristic: geometry starting with "text" is a WordArt preset
344
- // those flow through parseShapeXml/WordArtNode. parseShapeContent covers
345
- // rectangular shapes + text boxes.
346
- const isTextBox = Boolean(txbxContent && (!geometry || geometry === "rect"));
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 = parseXml(xml);
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
- }