@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.
Files changed (172) hide show
  1. package/README.md +44 -104
  2. package/package.json +50 -30
  3. package/src/README.md +85 -0
  4. package/src/api/README.md +22 -0
  5. package/src/api/public-types.ts +525 -0
  6. package/src/compare/diff-engine.ts +530 -0
  7. package/src/compare/export-redlines.ts +162 -0
  8. package/src/compare/snapshot.ts +37 -0
  9. package/src/component-inventory.md +99 -0
  10. package/src/core/README.md +10 -0
  11. package/src/core/commands/README.md +3 -0
  12. package/src/core/commands/formatting-commands.ts +161 -0
  13. package/src/core/commands/image-commands.ts +144 -0
  14. package/src/core/commands/index.ts +1013 -0
  15. package/src/core/commands/list-commands.ts +370 -0
  16. package/src/core/commands/review-commands.ts +108 -0
  17. package/src/core/commands/text-commands.ts +119 -0
  18. package/src/core/schema/README.md +3 -0
  19. package/src/core/schema/text-schema.ts +512 -0
  20. package/src/core/selection/README.md +3 -0
  21. package/src/core/selection/mapping.ts +238 -0
  22. package/src/core/selection/review-anchors.ts +94 -0
  23. package/src/core/state/README.md +3 -0
  24. package/src/core/state/editor-state.ts +580 -0
  25. package/src/core/state/text-transaction.ts +276 -0
  26. package/src/formats/xlsx/io/parse-shared-strings.ts +41 -0
  27. package/src/formats/xlsx/io/parse-sheet.ts +289 -0
  28. package/src/formats/xlsx/io/parse-styles.ts +57 -0
  29. package/src/formats/xlsx/io/parse-workbook.ts +75 -0
  30. package/src/formats/xlsx/io/xlsx-session.ts +306 -0
  31. package/src/formats/xlsx/model/cell.ts +189 -0
  32. package/src/formats/xlsx/model/sheet.ts +244 -0
  33. package/src/formats/xlsx/model/styles.ts +118 -0
  34. package/src/formats/xlsx/model/workbook.ts +449 -0
  35. package/src/index.ts +45 -0
  36. package/src/io/README.md +10 -0
  37. package/src/io/docx-session.ts +1763 -0
  38. package/src/io/export/README.md +3 -0
  39. package/src/io/export/export-session.ts +165 -0
  40. package/src/io/export/minimal-docx.ts +115 -0
  41. package/src/io/export/reattach-preserved-parts.ts +54 -0
  42. package/src/io/export/serialize-comments.ts +876 -0
  43. package/src/io/export/serialize-footnotes.ts +217 -0
  44. package/src/io/export/serialize-headers-footers.ts +200 -0
  45. package/src/io/export/serialize-main-document.ts +982 -0
  46. package/src/io/export/serialize-numbering.ts +97 -0
  47. package/src/io/export/serialize-revisions.ts +389 -0
  48. package/src/io/export/serialize-runtime-revisions.ts +265 -0
  49. package/src/io/export/serialize-tables.ts +147 -0
  50. package/src/io/export/split-review-boundaries.ts +194 -0
  51. package/src/io/normalize/README.md +3 -0
  52. package/src/io/normalize/normalize-text.ts +437 -0
  53. package/src/io/ooxml/README.md +3 -0
  54. package/src/io/ooxml/parse-comments.ts +779 -0
  55. package/src/io/ooxml/parse-complex-content.ts +287 -0
  56. package/src/io/ooxml/parse-fields.ts +438 -0
  57. package/src/io/ooxml/parse-footnotes.ts +403 -0
  58. package/src/io/ooxml/parse-headers-footers.ts +483 -0
  59. package/src/io/ooxml/parse-inline-media.ts +431 -0
  60. package/src/io/ooxml/parse-main-document.ts +1846 -0
  61. package/src/io/ooxml/parse-numbering.ts +425 -0
  62. package/src/io/ooxml/parse-revisions.ts +658 -0
  63. package/src/io/ooxml/parse-shapes.ts +271 -0
  64. package/src/io/ooxml/parse-tables.ts +568 -0
  65. package/src/io/ooxml/parse-theme.ts +314 -0
  66. package/src/io/ooxml/part-manifest.ts +136 -0
  67. package/src/io/ooxml/revision-boundaries.ts +351 -0
  68. package/src/io/opc/README.md +3 -0
  69. package/src/io/opc/corrupt-package.ts +166 -0
  70. package/src/io/opc/docx-package.ts +74 -0
  71. package/src/io/opc/package-reader.ts +325 -0
  72. package/src/io/opc/package-writer.ts +273 -0
  73. package/src/legal/bookmarks.ts +196 -0
  74. package/src/legal/cross-references.ts +356 -0
  75. package/src/legal/defined-terms.ts +203 -0
  76. package/src/model/README.md +3 -0
  77. package/src/model/canonical-document.ts +1911 -0
  78. package/src/model/cds-1.0.0.ts +196 -0
  79. package/src/model/snapshot.ts +393 -0
  80. package/src/preservation/README.md +3 -0
  81. package/src/preservation/markup-compatibility.ts +48 -0
  82. package/src/preservation/opaque-fragment-store.ts +89 -0
  83. package/src/preservation/opaque-region.ts +233 -0
  84. package/src/preservation/package-preservation.ts +120 -0
  85. package/src/preservation/preserved-part-manifest.ts +56 -0
  86. package/src/preservation/relationship-retention.ts +57 -0
  87. package/src/preservation/store.ts +185 -0
  88. package/src/review/README.md +16 -0
  89. package/src/review/store/README.md +3 -0
  90. package/src/review/store/comment-anchors.ts +70 -0
  91. package/src/review/store/comment-remapping.ts +154 -0
  92. package/src/review/store/comment-store.ts +331 -0
  93. package/src/review/store/comment-thread.ts +109 -0
  94. package/src/review/store/revision-actions.ts +394 -0
  95. package/src/review/store/revision-store.ts +303 -0
  96. package/src/review/store/revision-types.ts +168 -0
  97. package/src/review/store/runtime-comment-store.ts +43 -0
  98. package/src/runtime/README.md +3 -0
  99. package/src/runtime/ai-action-policy.ts +764 -0
  100. package/src/runtime/document-runtime.ts +967 -0
  101. package/src/runtime/read-only-diagnostics-runtime.ts +232 -0
  102. package/src/runtime/review-runtime.ts +44 -0
  103. package/src/runtime/revision-runtime.ts +107 -0
  104. package/src/runtime/session-capabilities.ts +138 -0
  105. package/src/runtime/surface-projection.ts +570 -0
  106. package/src/runtime/table-commands.ts +87 -0
  107. package/src/runtime/table-schema.ts +140 -0
  108. package/src/runtime/virtualized-rendering.ts +258 -0
  109. package/src/ui/README.md +30 -0
  110. package/src/ui/WordReviewEditor.tsx +1506 -0
  111. package/src/ui/comments/README.md +3 -0
  112. package/src/ui/compatibility/README.md +3 -0
  113. package/src/ui/editor-surface/README.md +3 -0
  114. package/src/ui/headless/comment-decoration-model.ts +124 -0
  115. package/src/ui/headless/revision-decoration-model.ts +128 -0
  116. package/src/ui/headless/selection-helpers.ts +34 -0
  117. package/src/ui/headless/use-editor-keyboard.ts +98 -0
  118. package/src/ui/review/README.md +3 -0
  119. package/src/ui/shared/revision-filters.ts +31 -0
  120. package/src/ui/status/README.md +3 -0
  121. package/src/ui/theme/README.md +3 -0
  122. package/src/ui/toolbar/README.md +3 -0
  123. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +48 -0
  124. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +44 -0
  125. package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +58 -0
  126. package/src/ui-tailwind/chrome/use-before-unload.ts +20 -0
  127. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +139 -0
  128. package/src/ui-tailwind/editor-surface/pm-decorations.ts +98 -0
  129. package/src/ui-tailwind/editor-surface/pm-position-map.ts +123 -0
  130. package/src/ui-tailwind/editor-surface/pm-schema.ts +452 -0
  131. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +327 -0
  132. package/src/ui-tailwind/editor-surface/search-plugin.ts +157 -0
  133. package/src/ui-tailwind/editor-surface/tw-caret.tsx +12 -0
  134. package/src/ui-tailwind/editor-surface/tw-editor-surface.tsx +150 -0
  135. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +118 -0
  136. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +52 -0
  137. package/src/ui-tailwind/editor-surface/tw-paragraph-block.tsx +151 -0
  138. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +215 -0
  139. package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +111 -0
  140. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +122 -0
  141. package/src/ui-tailwind/index.ts +61 -0
  142. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +276 -0
  143. package/src/ui-tailwind/review/tw-health-panel.tsx +120 -0
  144. package/src/ui-tailwind/review/tw-review-rail.tsx +120 -0
  145. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +164 -0
  146. package/src/ui-tailwind/status/tw-status-bar.tsx +58 -0
  147. package/src/ui-tailwind/theme/editor-theme.css +190 -0
  148. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +48 -0
  149. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +231 -0
  150. package/src/ui-tailwind/tw-review-workspace.tsx +140 -0
  151. package/src/validation/README.md +3 -0
  152. package/src/validation/compatibility-engine.ts +317 -0
  153. package/src/validation/compatibility-report.ts +160 -0
  154. package/src/validation/diagnostics.ts +203 -0
  155. package/src/validation/import-diagnostics.ts +128 -0
  156. package/src/validation/low-priority-word-surfaces.ts +373 -0
  157. package/dist/chunk-32W6IVQE.js +0 -7725
  158. package/dist/chunk-32W6IVQE.js.map +0 -1
  159. package/dist/index.cjs +0 -23722
  160. package/dist/index.cjs.map +0 -1
  161. package/dist/index.d.cts +0 -7
  162. package/dist/index.d.ts +0 -7
  163. package/dist/index.js +0 -16011
  164. package/dist/index.js.map +0 -1
  165. package/dist/public-types-DqCURAz8.d.cts +0 -1152
  166. package/dist/public-types-DqCURAz8.d.ts +0 -1152
  167. package/dist/tailwind.cjs +0 -8295
  168. package/dist/tailwind.cjs.map +0 -1
  169. package/dist/tailwind.d.cts +0 -323
  170. package/dist/tailwind.d.ts +0 -323
  171. package/dist/tailwind.js +0 -553
  172. 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
+ }