@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,403 @@
1
+ import type {
2
+ BlockNode,
3
+ FootnoteCollection,
4
+ FootnoteDefinition,
5
+ InlineNode,
6
+ ParagraphNode,
7
+ TextMark,
8
+ } from "../../model/canonical-document.ts";
9
+
10
+ // ---- XML node types (inline, no external dep) ----
11
+
12
+ interface XmlElementNode {
13
+ type: "element";
14
+ name: string;
15
+ attributes: Record<string, string>;
16
+ children: XmlNode[];
17
+ }
18
+
19
+ interface XmlTextNode {
20
+ type: "text";
21
+ text: string;
22
+ }
23
+
24
+ type XmlNode = XmlElementNode | XmlTextNode;
25
+
26
+ // ---- Special footnote/endnote IDs to skip ----
27
+
28
+ const SPECIAL_NOTE_IDS = new Set(["-1", "0"]);
29
+ const SPECIAL_NOTE_TYPES = new Set(["separator", "continuationSeparator"]);
30
+
31
+ // ---- Public API ----
32
+
33
+ /**
34
+ * Parse footnotes.xml (<w:footnotes> root) into a FootnoteCollection.
35
+ * Also accepts endnotes.xml (<w:endnotes> root).
36
+ */
37
+ export function parseFootnotesXml(xml: string): FootnoteCollection {
38
+ const root = parseXml(xml);
39
+
40
+ const footnotesElement = findChildElementOptional(root, "footnotes");
41
+ const endnotesElement = findChildElementOptional(root, "endnotes");
42
+
43
+ const footnotes: Record<string, FootnoteDefinition> = {};
44
+ const endnotes: Record<string, FootnoteDefinition> = {};
45
+
46
+ if (footnotesElement) {
47
+ for (const child of footnotesElement.children) {
48
+ if (child.type !== "element") {
49
+ continue;
50
+ }
51
+ if (localName(child.name) !== "footnote") {
52
+ continue;
53
+ }
54
+ const definition = parseNoteElement(child, "footnote");
55
+ if (definition) {
56
+ footnotes[definition.noteId] = definition;
57
+ }
58
+ }
59
+ }
60
+
61
+ if (endnotesElement) {
62
+ for (const child of endnotesElement.children) {
63
+ if (child.type !== "element") {
64
+ continue;
65
+ }
66
+ if (localName(child.name) !== "endnote") {
67
+ continue;
68
+ }
69
+ const definition = parseNoteElement(child, "endnote");
70
+ if (definition) {
71
+ endnotes[definition.noteId] = definition;
72
+ }
73
+ }
74
+ }
75
+
76
+ return { footnotes, endnotes };
77
+ }
78
+
79
+ /**
80
+ * Parse a standalone endnotes.xml (<w:endnotes> root).
81
+ * Merges into the provided collection or creates a new one.
82
+ */
83
+ export function parseEndnotesXml(
84
+ xml: string,
85
+ existing?: FootnoteCollection,
86
+ ): FootnoteCollection {
87
+ const root = parseXml(xml);
88
+ const endnotesElement = findChildElementOptional(root, "endnotes");
89
+ const endnotes: Record<string, FootnoteDefinition> = {
90
+ ...(existing?.endnotes ?? {}),
91
+ };
92
+
93
+ if (endnotesElement) {
94
+ for (const child of endnotesElement.children) {
95
+ if (child.type !== "element") {
96
+ continue;
97
+ }
98
+ if (localName(child.name) !== "endnote") {
99
+ continue;
100
+ }
101
+ const definition = parseNoteElement(child, "endnote");
102
+ if (definition) {
103
+ endnotes[definition.noteId] = definition;
104
+ }
105
+ }
106
+ }
107
+
108
+ return {
109
+ footnotes: existing?.footnotes ?? {},
110
+ endnotes,
111
+ };
112
+ }
113
+
114
+ // ---- Internal helpers ----
115
+
116
+ function parseNoteElement(
117
+ element: XmlElementNode,
118
+ kind: "footnote" | "endnote",
119
+ ): FootnoteDefinition | undefined {
120
+ const rawId =
121
+ element.attributes["w:id"] ?? element.attributes.id ?? "";
122
+ const rawType =
123
+ element.attributes["w:type"] ?? element.attributes.type ?? "";
124
+
125
+ if (!rawId || SPECIAL_NOTE_IDS.has(rawId) || SPECIAL_NOTE_TYPES.has(rawType)) {
126
+ return undefined;
127
+ }
128
+
129
+ const noteId = rawId;
130
+ const blocks: BlockNode[] = [];
131
+
132
+ for (const child of element.children) {
133
+ if (child.type !== "element") {
134
+ continue;
135
+ }
136
+ const name = localName(child.name);
137
+ if (name === "p") {
138
+ blocks.push(parseParagraphElement(child));
139
+ } else if (name === "tbl") {
140
+ blocks.push({
141
+ type: "opaque_block",
142
+ fragmentId: `fragment:note-tbl-${noteId}`,
143
+ warningId: `warning:note-opaque-table`,
144
+ });
145
+ } else {
146
+ blocks.push({
147
+ type: "opaque_block",
148
+ fragmentId: `fragment:note-opaque-${noteId}`,
149
+ warningId: `warning:note-opaque-block`,
150
+ });
151
+ }
152
+ }
153
+
154
+ return { noteId, kind, blocks };
155
+ }
156
+
157
+ function parseParagraphElement(pElement: XmlElementNode): ParagraphNode {
158
+ let styleId: string | undefined;
159
+ const children: InlineNode[] = [];
160
+
161
+ for (const child of pElement.children) {
162
+ if (child.type !== "element") {
163
+ continue;
164
+ }
165
+
166
+ const name = localName(child.name);
167
+
168
+ if (name === "pPr") {
169
+ const pStyle = findChildElementOptional(child, "pStyle");
170
+ styleId = pStyle?.attributes["w:val"] ?? pStyle?.attributes.val;
171
+ } else if (name === "r") {
172
+ children.push(...parseRunElement(child));
173
+ } else if (name === "hyperlink") {
174
+ for (const hChild of child.children) {
175
+ if (hChild.type === "element" && localName(hChild.name) === "r") {
176
+ children.push(...parseRunElement(hChild));
177
+ }
178
+ }
179
+ }
180
+ }
181
+
182
+ return {
183
+ type: "paragraph",
184
+ ...(styleId ? { styleId } : {}),
185
+ children,
186
+ };
187
+ }
188
+
189
+ function parseRunElement(rElement: XmlElementNode): InlineNode[] {
190
+ const nodes: InlineNode[] = [];
191
+ const marks: TextMark[] = parseRunProperties(rElement);
192
+
193
+ for (const child of rElement.children) {
194
+ if (child.type !== "element") {
195
+ continue;
196
+ }
197
+
198
+ const name = localName(child.name);
199
+
200
+ if (name === "t") {
201
+ const text = extractTextContent(child);
202
+ if (text.length > 0) {
203
+ nodes.push({
204
+ type: "text",
205
+ text,
206
+ ...(marks.length > 0 ? { marks } : {}),
207
+ });
208
+ }
209
+ } else if (name === "br") {
210
+ nodes.push({ type: "hard_break" });
211
+ } else if (name === "tab") {
212
+ nodes.push({ type: "tab" });
213
+ } else if (name === "footnoteRef" || name === "endnoteRef") {
214
+ // The in-note reference marker (superscript) - skip, rendered by the host
215
+ } else if (name === "footnoteReference") {
216
+ const noteId =
217
+ child.attributes["w:id"] ?? child.attributes.id ?? "";
218
+ if (noteId) {
219
+ nodes.push({ type: "footnote_ref", noteId, noteKind: "footnote" });
220
+ }
221
+ } else if (name === "endnoteReference") {
222
+ const noteId =
223
+ child.attributes["w:id"] ?? child.attributes.id ?? "";
224
+ if (noteId) {
225
+ nodes.push({ type: "footnote_ref", noteId, noteKind: "endnote" });
226
+ }
227
+ }
228
+ }
229
+
230
+ return nodes;
231
+ }
232
+
233
+ function parseRunProperties(rElement: XmlElementNode): TextMark[] {
234
+ const rPr = findChildElementOptional(rElement, "rPr");
235
+ if (!rPr) {
236
+ return [];
237
+ }
238
+
239
+ const marks: TextMark[] = [];
240
+
241
+ for (const child of rPr.children) {
242
+ if (child.type !== "element") {
243
+ continue;
244
+ }
245
+
246
+ const name = localName(child.name);
247
+ const val = child.attributes["w:val"] ?? child.attributes.val ?? "true";
248
+
249
+ switch (name) {
250
+ case "b":
251
+ if (val !== "0" && val !== "false") marks.push({ type: "bold" });
252
+ break;
253
+ case "i":
254
+ if (val !== "0" && val !== "false") marks.push({ type: "italic" });
255
+ break;
256
+ case "u":
257
+ if (val !== "none" && val !== "0") marks.push({ type: "underline" });
258
+ break;
259
+ case "strike":
260
+ if (val !== "0" && val !== "false") marks.push({ type: "strikethrough" });
261
+ break;
262
+ }
263
+ }
264
+
265
+ return marks;
266
+ }
267
+
268
+ function extractTextContent(tElement: XmlElementNode): string {
269
+ let text = "";
270
+ for (const child of tElement.children) {
271
+ if (child.type === "text") {
272
+ text += child.text;
273
+ }
274
+ }
275
+ return text;
276
+ }
277
+
278
+ function findChildElementOptional(
279
+ node: XmlElementNode,
280
+ childLocalName: string,
281
+ ): XmlElementNode | undefined {
282
+ return node.children.find(
283
+ (entry): entry is XmlElementNode =>
284
+ entry.type === "element" && localName(entry.name) === childLocalName,
285
+ );
286
+ }
287
+
288
+ function localName(name: string): string {
289
+ const idx = name.indexOf(":");
290
+ return idx >= 0 ? name.slice(idx + 1) : name;
291
+ }
292
+
293
+ // ---- Minimal XML parser (same pattern as parse-numbering.ts) ----
294
+
295
+ function parseXml(xml: string): XmlElementNode {
296
+ const root: XmlElementNode = {
297
+ type: "element",
298
+ name: "__root__",
299
+ attributes: {},
300
+ children: [],
301
+ };
302
+ const stack: XmlElementNode[] = [root];
303
+ let cursor = 0;
304
+
305
+ while (cursor < xml.length) {
306
+ if (xml.startsWith("<!--", cursor)) {
307
+ const end = xml.indexOf("-->", cursor);
308
+ cursor = end >= 0 ? end + 3 : xml.length;
309
+ continue;
310
+ }
311
+
312
+ if (xml.startsWith("<?", cursor)) {
313
+ const end = xml.indexOf("?>", cursor);
314
+ cursor = end >= 0 ? end + 2 : xml.length;
315
+ continue;
316
+ }
317
+
318
+ if (xml.startsWith("<![CDATA[", cursor)) {
319
+ const end = xml.indexOf("]]>", cursor);
320
+ const textEnd = end >= 0 ? end : xml.length;
321
+ stack[stack.length - 1]?.children.push({
322
+ type: "text",
323
+ text: xml.slice(cursor + 9, textEnd),
324
+ });
325
+ cursor = end >= 0 ? end + 3 : xml.length;
326
+ continue;
327
+ }
328
+
329
+ if (xml[cursor] !== "<") {
330
+ const nextTag = xml.indexOf("<", cursor);
331
+ const end = nextTag >= 0 ? nextTag : xml.length;
332
+ const text = decodeXmlEntities(xml.slice(cursor, end));
333
+ if (text.trim().length > 0 || (text.length > 0 && stack.length > 1)) {
334
+ stack[stack.length - 1]?.children.push({ type: "text", text });
335
+ }
336
+ cursor = end;
337
+ continue;
338
+ }
339
+
340
+ if (xml[cursor + 1] === "/") {
341
+ const end = xml.indexOf(">", cursor);
342
+ if (end < 0) break;
343
+ stack.pop();
344
+ cursor = end + 1;
345
+ continue;
346
+ }
347
+
348
+ const tagEnd = xml.indexOf(">", cursor);
349
+ if (tagEnd < 0) break;
350
+
351
+ const tagContent = xml.slice(cursor + 1, tagEnd);
352
+ const selfClosing = tagContent.endsWith("/");
353
+ const normalized = selfClosing ? tagContent.slice(0, -1).trimEnd() : tagContent;
354
+
355
+ const spaceIndex = normalized.search(/\s/);
356
+ const tagName = spaceIndex >= 0 ? normalized.slice(0, spaceIndex) : normalized;
357
+ const attrString = spaceIndex >= 0 ? normalized.slice(spaceIndex + 1) : "";
358
+ const attributes = parseAttributes(attrString);
359
+
360
+ const element: XmlElementNode = {
361
+ type: "element",
362
+ name: tagName,
363
+ attributes,
364
+ children: [],
365
+ };
366
+
367
+ stack[stack.length - 1]?.children.push(element);
368
+
369
+ if (!selfClosing) {
370
+ stack.push(element);
371
+ }
372
+
373
+ cursor = tagEnd + 1;
374
+ }
375
+
376
+ return root;
377
+ }
378
+
379
+ function parseAttributes(attrString: string): Record<string, string> {
380
+ const attrs: Record<string, string> = {};
381
+ const pattern = /([A-Za-z_:][A-Za-z0-9:._-]*)\s*=\s*("([^"]*)"|'([^']*)')/gu;
382
+ for (const match of attrString.matchAll(pattern)) {
383
+ const name = match[1];
384
+ const value = match[3] ?? match[4] ?? "";
385
+ if (name) {
386
+ attrs[name] = decodeXmlEntities(value);
387
+ }
388
+ }
389
+ return attrs;
390
+ }
391
+
392
+ function decodeXmlEntities(text: string): string {
393
+ return text
394
+ .replace(/&amp;/g, "&")
395
+ .replace(/&lt;/g, "<")
396
+ .replace(/&gt;/g, ">")
397
+ .replace(/&quot;/g, '"')
398
+ .replace(/&apos;/g, "'")
399
+ .replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number.parseInt(dec, 10)))
400
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, hex) =>
401
+ String.fromCodePoint(Number.parseInt(hex, 16)),
402
+ );
403
+ }