@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.
- 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 +978 -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 +2 -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/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 +3 -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 +151 -0
- package/src/runtime/diagnostics/code-metadata-table.ts +221 -0
- package/src/runtime/document-runtime.ts +476 -34
- 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 +5 -8
- 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
|
@@ -10,6 +10,14 @@ import type {
|
|
|
10
10
|
TabStop,
|
|
11
11
|
} from "../../model/canonical-document.ts";
|
|
12
12
|
import { readRunProperties } from "./parse-run-formatting.ts";
|
|
13
|
+
import type { OpcRelationship } from "./part-manifest.ts";
|
|
14
|
+
import { normalizePartPath, resolveRelationshipTarget } from "./part-manifest.ts";
|
|
15
|
+
import { serializeXmlElementToString } from "./xml-element-serialize.ts";
|
|
16
|
+
import { parseXml } from "./xml-parser.ts";
|
|
17
|
+
import { localName, readStringAttr } from "./xml-attr-helpers.ts";
|
|
18
|
+
|
|
19
|
+
const TAB_ALIGN_VOCAB = new Set<TabStop["align"]>(["left", "center", "right", "decimal", "num", "bar", "clear"]);
|
|
20
|
+
const TAB_LEADER_VOCAB = new Set<TabStop["leader"]>(["none", "dot", "hyphen", "underscore", "heavy", "middleDot"]);
|
|
13
21
|
|
|
14
22
|
export interface ParsedParagraphNumberingReference {
|
|
15
23
|
paragraphIndex: number;
|
|
@@ -31,7 +39,12 @@ interface XmlTextNode {
|
|
|
31
39
|
|
|
32
40
|
type XmlNode = XmlElementNode | XmlTextNode;
|
|
33
41
|
|
|
34
|
-
export
|
|
42
|
+
export interface ParseNumberingContext {
|
|
43
|
+
relationships?: readonly OpcRelationship[];
|
|
44
|
+
partPath?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function parseNumberingXml(xml: string, context?: ParseNumberingContext): NumberingCatalog {
|
|
35
48
|
const root = parseXml(xml);
|
|
36
49
|
const numberingElement = findChildElement(root, "numbering");
|
|
37
50
|
const abstractDefinitions: NumberingCatalog["abstractDefinitions"] = {};
|
|
@@ -48,40 +61,35 @@ export function parseNumberingXml(xml: string): NumberingCatalog {
|
|
|
48
61
|
// as a catalog entry keyed by numPicBulletId; raw XML is preserved for
|
|
49
62
|
// byte-identical round-trip.
|
|
50
63
|
if (localName(child.name) === "numPicBullet") {
|
|
51
|
-
const rawId =
|
|
52
|
-
child.attributes["w:numPicBulletId"] ?? child.attributes.numPicBulletId;
|
|
64
|
+
const rawId = readStringAttr(child, "w:numPicBulletId");
|
|
53
65
|
if (rawId) {
|
|
54
|
-
numPicBullets[rawId] = readNumPicBullet(child, rawId);
|
|
66
|
+
numPicBullets[rawId] = readNumPicBullet(child, rawId, context);
|
|
55
67
|
}
|
|
56
68
|
continue;
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
switch (localName(child.name)) {
|
|
60
72
|
case "abstractNum": {
|
|
61
|
-
const rawId = child
|
|
73
|
+
const rawId = readStringAttr(child, "w:abstractNumId");
|
|
62
74
|
if (!rawId) {
|
|
63
75
|
continue;
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
const abstractNumberingId = toCanonicalAbstractNumberingId(rawId);
|
|
67
79
|
const nsidEl = findChildElementOptional(child, "nsid");
|
|
68
|
-
const nsid = nsidEl ? (nsidEl
|
|
80
|
+
const nsid = nsidEl ? readStringAttr(nsidEl, "w:val") : undefined;
|
|
69
81
|
const mltEl = findChildElementOptional(child, "multiLevelType");
|
|
70
|
-
const mltRaw = mltEl ? (mltEl
|
|
82
|
+
const mltRaw = mltEl ? readStringAttr(mltEl, "w:val") : undefined;
|
|
71
83
|
const multiLevelType =
|
|
72
84
|
mltRaw === "singleLevel" || mltRaw === "multilevel" || mltRaw === "hybridMultilevel"
|
|
73
85
|
? mltRaw
|
|
74
86
|
: undefined;
|
|
75
87
|
const tmplEl = findChildElementOptional(child, "tmpl");
|
|
76
|
-
const tplc = tmplEl ? (tmplEl
|
|
88
|
+
const tplc = tmplEl ? readStringAttr(tmplEl, "w:val") : undefined;
|
|
77
89
|
const styleLinkEl = findChildElementOptional(child, "styleLink");
|
|
78
|
-
const styleLink = styleLinkEl
|
|
79
|
-
? (styleLinkEl.attributes["w:val"] ?? styleLinkEl.attributes.val)
|
|
80
|
-
: undefined;
|
|
90
|
+
const styleLink = styleLinkEl ? readStringAttr(styleLinkEl, "w:val") : undefined;
|
|
81
91
|
const numStyleLinkEl = findChildElementOptional(child, "numStyleLink");
|
|
82
|
-
const numStyleLink = numStyleLinkEl
|
|
83
|
-
? (numStyleLinkEl.attributes["w:val"] ?? numStyleLinkEl.attributes.val)
|
|
84
|
-
: undefined;
|
|
92
|
+
const numStyleLink = numStyleLinkEl ? readStringAttr(numStyleLinkEl, "w:val") : undefined;
|
|
85
93
|
abstractDefinitions[abstractNumberingId] = {
|
|
86
94
|
abstractNumberingId,
|
|
87
95
|
levels: readLevels(child),
|
|
@@ -94,10 +102,9 @@ export function parseNumberingXml(xml: string): NumberingCatalog {
|
|
|
94
102
|
break;
|
|
95
103
|
}
|
|
96
104
|
case "num": {
|
|
97
|
-
const rawId = child
|
|
105
|
+
const rawId = readStringAttr(child, "w:numId");
|
|
98
106
|
const abstractReference = findChildElementOptional(child, "abstractNumId");
|
|
99
|
-
const rawAbstractId =
|
|
100
|
-
abstractReference?.attributes["w:val"] ?? abstractReference?.attributes.val;
|
|
107
|
+
const rawAbstractId = abstractReference ? readStringAttr(abstractReference, "w:val") : undefined;
|
|
101
108
|
|
|
102
109
|
if (!rawId || !rawAbstractId) {
|
|
103
110
|
continue;
|
|
@@ -126,17 +133,24 @@ export function parseNumberingXml(xml: string): NumberingCatalog {
|
|
|
126
133
|
* inner `wp:extent` when present (drawingML path); the raw XML is
|
|
127
134
|
* preserved verbatim so the export round-trips byte-for-byte regardless
|
|
128
135
|
* of whether the bullet uses drawing or VML markup.
|
|
136
|
+
*
|
|
137
|
+
* Word 2016+ wraps the bullet content in mc:AlternateContent; we prefer
|
|
138
|
+
* the mc:Choice branch (DrawingML) and fall back to mc:Fallback (VML).
|
|
129
139
|
*/
|
|
130
140
|
function readNumPicBullet(
|
|
131
141
|
node: XmlElementNode,
|
|
132
142
|
numPicBulletId: string,
|
|
143
|
+
context?: ParseNumberingContext,
|
|
133
144
|
): NumPicBullet {
|
|
134
145
|
let widthEmu: number | undefined;
|
|
135
146
|
let heightEmu: number | undefined;
|
|
147
|
+
let mediaId: string | undefined;
|
|
148
|
+
|
|
149
|
+
// Unwrap mc:AlternateContent if present (Word 2016+).
|
|
150
|
+
const contentNode = unwrapNumPicBulletContent(node);
|
|
136
151
|
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
const drawing = findChildElementOptional(node, "drawing");
|
|
152
|
+
// DrawingML path: walk drawing → inline/anchor → extent for EMU dims.
|
|
153
|
+
const drawing = findChildElementOptional(contentNode, "drawing");
|
|
140
154
|
if (drawing) {
|
|
141
155
|
const inline = findChildElementOptional(drawing, "inline");
|
|
142
156
|
const anchor = findChildElementOptional(drawing, "anchor");
|
|
@@ -149,54 +163,65 @@ function readNumPicBullet(
|
|
|
149
163
|
if (cx !== undefined) widthEmu = cx;
|
|
150
164
|
if (cy !== undefined) heightEmu = cy;
|
|
151
165
|
}
|
|
166
|
+
|
|
167
|
+
// Resolve mediaId via blip r:embed → relationships map.
|
|
168
|
+
// Walk: inline/anchor → a:graphic → a:graphicData → pic:pic → pic:blipFill → a:blip
|
|
169
|
+
if (context?.relationships && context.relationships.length > 0) {
|
|
170
|
+
const graphic = findChildElementOptional(envelope, "graphic");
|
|
171
|
+
const graphicData = graphic ? findChildElementOptional(graphic, "graphicData") : undefined;
|
|
172
|
+
const pic = graphicData ? findChildElementOptional(graphicData, "pic") : undefined;
|
|
173
|
+
const blipFill = pic ? findChildElementOptional(pic, "blipFill") : undefined;
|
|
174
|
+
const blip = blipFill ? findChildElementOptional(blipFill, "blip") : undefined;
|
|
175
|
+
const rEmbed = blip?.attributes["r:embed"] ?? blip?.attributes.embed;
|
|
176
|
+
if (rEmbed) {
|
|
177
|
+
const rel = context.relationships.find((r) => r.id === rEmbed);
|
|
178
|
+
if (rel) {
|
|
179
|
+
const packagePartName = normalizePartPath(
|
|
180
|
+
resolveRelationshipTarget(context.partPath ?? "/word/numbering.xml", rel),
|
|
181
|
+
);
|
|
182
|
+
mediaId = `media:${packagePartName.slice(1)}`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// VML path: <w:pict> → <v:shape> → <v:imagedata r:id="rIdN">.
|
|
190
|
+
// Also parses width/height from the VML style attribute (1 pt = 12700 EMU).
|
|
191
|
+
if (mediaId === undefined) {
|
|
192
|
+
const pict = findChildElementOptional(contentNode, "pict");
|
|
193
|
+
if (pict) {
|
|
194
|
+
const shape = findChildElementOptional(pict, "shape");
|
|
195
|
+
const imagedata = shape ? findChildElementOptional(shape, "imagedata") : undefined;
|
|
196
|
+
const rId = imagedata?.attributes["r:id"] ?? imagedata?.attributes.id;
|
|
197
|
+
if (rId && context?.relationships && context.relationships.length > 0) {
|
|
198
|
+
const rel = context.relationships.find((r) => r.id === rId);
|
|
199
|
+
if (rel) {
|
|
200
|
+
const packagePartName = normalizePartPath(
|
|
201
|
+
resolveRelationshipTarget(context.partPath ?? "/word/numbering.xml", rel),
|
|
202
|
+
);
|
|
203
|
+
mediaId = `media:${packagePartName.slice(1)}`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (widthEmu === undefined && shape?.attributes.style) {
|
|
207
|
+
const style = shape.attributes.style;
|
|
208
|
+
const wMatch = /width:\s*([\d.]+)pt/.exec(style);
|
|
209
|
+
const hMatch = /height:\s*([\d.]+)pt/.exec(style);
|
|
210
|
+
if (wMatch) widthEmu = Math.round(parseFloat(wMatch[1]) * 12700);
|
|
211
|
+
if (hMatch) heightEmu = Math.round(parseFloat(hMatch[1]) * 12700);
|
|
212
|
+
}
|
|
152
213
|
}
|
|
153
214
|
}
|
|
154
215
|
|
|
155
216
|
return {
|
|
156
217
|
numPicBulletId,
|
|
157
|
-
rawXml:
|
|
218
|
+
rawXml: serializeXmlElementToString(node),
|
|
158
219
|
...(widthEmu !== undefined ? { widthEmu } : {}),
|
|
159
220
|
...(heightEmu !== undefined ? { heightEmu } : {}),
|
|
221
|
+
...(mediaId !== undefined ? { mediaId } : {}),
|
|
160
222
|
};
|
|
161
223
|
}
|
|
162
224
|
|
|
163
|
-
/**
|
|
164
|
-
* Best-effort reconstruction of the source XML for a `<w:numPicBullet>`.
|
|
165
|
-
* Mirrors `buildGrabBagSourceChildFromParsed` in property-grab-bag.ts —
|
|
166
|
-
* attribute/element semantic content round-trips, whitespace between
|
|
167
|
-
* elements + quote styles normalize. For the Lane 3b scope this is the
|
|
168
|
-
* right trade-off: picture-bullet catalog entries usually round-trip
|
|
169
|
-
* once-per-document and their mediaId lookup is what matters.
|
|
170
|
-
*/
|
|
171
|
-
function reconstructElementXml(node: XmlElementNode): string {
|
|
172
|
-
const attrs = Object.entries(node.attributes)
|
|
173
|
-
.map(([name, value]) => ` ${name}="${escapeAttr(value)}"`)
|
|
174
|
-
.join("");
|
|
175
|
-
if (node.children.length === 0) {
|
|
176
|
-
return `<${node.name}${attrs}/>`;
|
|
177
|
-
}
|
|
178
|
-
const body = node.children
|
|
179
|
-
.map((child) => {
|
|
180
|
-
if (child.type === "text") return escapeText(child.text);
|
|
181
|
-
if (child.type === "element") return reconstructElementXml(child);
|
|
182
|
-
return "";
|
|
183
|
-
})
|
|
184
|
-
.join("");
|
|
185
|
-
return `<${node.name}${attrs}>${body}</${node.name}>`;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function escapeAttr(value: string): string {
|
|
189
|
-
return value
|
|
190
|
-
.replace(/&/gu, "&")
|
|
191
|
-
.replace(/</gu, "<")
|
|
192
|
-
.replace(/>/gu, ">")
|
|
193
|
-
.replace(/"/gu, """);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function escapeText(value: string): string {
|
|
197
|
-
return value.replace(/&/gu, "&").replace(/</gu, "<").replace(/>/gu, ">");
|
|
198
|
-
}
|
|
199
|
-
|
|
200
225
|
export function parseParagraphNumberingReferences(
|
|
201
226
|
documentXml: string,
|
|
202
227
|
): ParsedParagraphNumberingReference[] {
|
|
@@ -219,8 +244,8 @@ export function parseParagraphNumberingReferences(
|
|
|
219
244
|
if (numberingProperties) {
|
|
220
245
|
const levelNode = findChildElementOptional(numberingProperties, "ilvl");
|
|
221
246
|
const instanceNode = findChildElementOptional(numberingProperties, "numId");
|
|
222
|
-
const rawLevel = levelNode
|
|
223
|
-
const rawInstanceId = instanceNode
|
|
247
|
+
const rawLevel = levelNode ? readStringAttr(levelNode, "w:val") : undefined;
|
|
248
|
+
const rawInstanceId = instanceNode ? readStringAttr(instanceNode, "w:val") : undefined;
|
|
224
249
|
|
|
225
250
|
if (rawLevel !== undefined && rawInstanceId) {
|
|
226
251
|
const level = parseInteger(rawLevel);
|
|
@@ -256,7 +281,7 @@ function readLevels(abstractNode: XmlElementNode): NumberingLevelDefinition[] {
|
|
|
256
281
|
continue;
|
|
257
282
|
}
|
|
258
283
|
|
|
259
|
-
const rawLevel = child
|
|
284
|
+
const rawLevel = readStringAttr(child, "w:ilvl");
|
|
260
285
|
const level = rawLevel === undefined ? undefined : parseInteger(rawLevel);
|
|
261
286
|
if (level === undefined) {
|
|
262
287
|
continue;
|
|
@@ -280,14 +305,14 @@ function readOverrides(numNode: XmlElementNode): NumberingLevelOverride[] {
|
|
|
280
305
|
continue;
|
|
281
306
|
}
|
|
282
307
|
|
|
283
|
-
const rawLevel = child
|
|
308
|
+
const rawLevel = readStringAttr(child, "w:ilvl");
|
|
284
309
|
const level = rawLevel === undefined ? undefined : parseInteger(rawLevel);
|
|
285
310
|
if (level === undefined) {
|
|
286
311
|
continue;
|
|
287
312
|
}
|
|
288
313
|
|
|
289
314
|
const startOverrideNode = findChildElementOptional(child, "startOverride");
|
|
290
|
-
const rawStart = startOverrideNode
|
|
315
|
+
const rawStart = startOverrideNode ? readStringAttr(startOverrideNode, "w:val") : undefined;
|
|
291
316
|
const startAt = rawStart === undefined ? undefined : parseInteger(rawStart);
|
|
292
317
|
const levelDefinitionNode = findChildElementOptional(child, "lvl");
|
|
293
318
|
const levelDefinition = levelDefinitionNode
|
|
@@ -308,7 +333,7 @@ function readLevelDefinition(
|
|
|
308
333
|
levelNode: XmlElementNode,
|
|
309
334
|
fallbackLevel?: number,
|
|
310
335
|
): NumberingLevelDefinition | undefined {
|
|
311
|
-
const rawLevel = levelNode
|
|
336
|
+
const rawLevel = readStringAttr(levelNode, "w:ilvl");
|
|
312
337
|
const level = rawLevel === undefined ? fallbackLevel : parseInteger(rawLevel);
|
|
313
338
|
if (level === undefined) {
|
|
314
339
|
return undefined;
|
|
@@ -318,16 +343,21 @@ function readLevelDefinition(
|
|
|
318
343
|
const formatNode = findChildElementOptional(levelNode, "numFmt");
|
|
319
344
|
const textNode = findChildElementOptional(levelNode, "lvlText");
|
|
320
345
|
const paragraphStyleNode = findChildElementOptional(levelNode, "pStyle");
|
|
321
|
-
const rawStart = startNode
|
|
346
|
+
const rawStart = startNode ? readStringAttr(startNode, "w:val") : undefined;
|
|
322
347
|
const startAt = rawStart === undefined ? undefined : parseInteger(rawStart);
|
|
323
|
-
const format = formatNode
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
348
|
+
const format = (formatNode ? readStringAttr(formatNode, "w:val") : undefined) ?? "decimal";
|
|
349
|
+
// ECMA-376 §17.9.11: w:null="1" means no text displayed; treat as empty string.
|
|
350
|
+
const lvlTextIsNull =
|
|
351
|
+
(textNode?.attributes["w:null"] ?? textNode?.attributes["null"]) === "1";
|
|
352
|
+
const text = lvlTextIsNull
|
|
353
|
+
? ""
|
|
354
|
+
: ((textNode ? readStringAttr(textNode, "w:val") : undefined) ??
|
|
355
|
+
(format === "bullet" ? "•" : `%${level + 1}.`));
|
|
356
|
+
const paragraphStyleId = paragraphStyleNode ? readStringAttr(paragraphStyleNode, "w:val") : undefined;
|
|
327
357
|
const isLegalNode = findChildElementOptional(levelNode, "isLgl");
|
|
328
358
|
const isLegalNumbering = readIsLegalNumberingValue(isLegalNode);
|
|
329
359
|
const suffixNode = findChildElementOptional(levelNode, "suff");
|
|
330
|
-
const suffixVal = suffixNode
|
|
360
|
+
const suffixVal = suffixNode ? readStringAttr(suffixNode, "w:val") : undefined;
|
|
331
361
|
const suffix =
|
|
332
362
|
suffixVal === "space" || suffixVal === "nothing"
|
|
333
363
|
? suffixVal
|
|
@@ -336,13 +366,12 @@ function readLevelDefinition(
|
|
|
336
366
|
: undefined;
|
|
337
367
|
const paragraphGeometry = readLevelParagraphGeometry(levelNode);
|
|
338
368
|
const lvlRestartNode = findChildElementOptional(levelNode, "lvlRestart");
|
|
339
|
-
const rawRestart = lvlRestartNode
|
|
369
|
+
const rawRestart = lvlRestartNode ? readStringAttr(lvlRestartNode, "w:val") : undefined;
|
|
340
370
|
const restartAfterLevel = rawRestart !== undefined ? parseInteger(rawRestart) : undefined;
|
|
341
371
|
const rPrNode = findChildElementOptional(levelNode, "rPr");
|
|
342
372
|
const runProperties = readRunProperties(rPrNode);
|
|
343
373
|
const lvlPicBulletNode = findChildElementOptional(levelNode, "lvlPicBulletId");
|
|
344
|
-
const picBulletId =
|
|
345
|
-
lvlPicBulletNode?.attributes["w:val"] ?? lvlPicBulletNode?.attributes.val;
|
|
374
|
+
const picBulletId = lvlPicBulletNode ? readStringAttr(lvlPicBulletNode, "w:val") : undefined;
|
|
346
375
|
|
|
347
376
|
return {
|
|
348
377
|
level,
|
|
@@ -355,7 +384,7 @@ function readLevelDefinition(
|
|
|
355
384
|
...(paragraphGeometry ? { paragraphGeometry } : {}),
|
|
356
385
|
...(runProperties ? { runProperties } : {}),
|
|
357
386
|
...(restartAfterLevel !== undefined ? { restartAfterLevel } : {}),
|
|
358
|
-
...(picBulletId ? { picBulletId } : {}),
|
|
387
|
+
...(picBulletId !== undefined ? { picBulletId } : {}),
|
|
359
388
|
};
|
|
360
389
|
}
|
|
361
390
|
|
|
@@ -363,7 +392,7 @@ function readLevelOverrideDefinition(
|
|
|
363
392
|
levelNode: XmlElementNode,
|
|
364
393
|
fallbackLevel?: number,
|
|
365
394
|
): NumberingLevelOverrideDefinition | undefined {
|
|
366
|
-
const rawLevel = levelNode
|
|
395
|
+
const rawLevel = readStringAttr(levelNode, "w:ilvl");
|
|
367
396
|
const level = rawLevel === undefined ? fallbackLevel : parseInteger(rawLevel);
|
|
368
397
|
if (level === undefined) {
|
|
369
398
|
return undefined;
|
|
@@ -373,16 +402,19 @@ function readLevelOverrideDefinition(
|
|
|
373
402
|
const formatNode = findChildElementOptional(levelNode, "numFmt");
|
|
374
403
|
const textNode = findChildElementOptional(levelNode, "lvlText");
|
|
375
404
|
const paragraphStyleNode = findChildElementOptional(levelNode, "pStyle");
|
|
376
|
-
const rawStart = startNode
|
|
405
|
+
const rawStart = startNode ? readStringAttr(startNode, "w:val") : undefined;
|
|
377
406
|
const startAt = rawStart === undefined ? undefined : parseInteger(rawStart);
|
|
378
|
-
const format = formatNode
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
407
|
+
const format = formatNode ? readStringAttr(formatNode, "w:val") : undefined;
|
|
408
|
+
const lvlTextIsNull =
|
|
409
|
+
(textNode?.attributes["w:null"] ?? textNode?.attributes["null"]) === "1";
|
|
410
|
+
const text = lvlTextIsNull
|
|
411
|
+
? ""
|
|
412
|
+
: (textNode ? readStringAttr(textNode, "w:val") : undefined);
|
|
413
|
+
const paragraphStyleId = paragraphStyleNode ? readStringAttr(paragraphStyleNode, "w:val") : undefined;
|
|
382
414
|
const isLegalNode = findChildElementOptional(levelNode, "isLgl");
|
|
383
415
|
const isLegalNumbering = readIsLegalNumberingValue(isLegalNode);
|
|
384
416
|
const suffixNode = findChildElementOptional(levelNode, "suff");
|
|
385
|
-
const suffixVal = suffixNode
|
|
417
|
+
const suffixVal = suffixNode ? readStringAttr(suffixNode, "w:val") : undefined;
|
|
386
418
|
const suffix =
|
|
387
419
|
suffixVal === "space" || suffixVal === "nothing"
|
|
388
420
|
? suffixVal
|
|
@@ -391,10 +423,12 @@ function readLevelOverrideDefinition(
|
|
|
391
423
|
: undefined;
|
|
392
424
|
const paragraphGeometry = readLevelParagraphGeometry(levelNode);
|
|
393
425
|
const lvlRestartNode = findChildElementOptional(levelNode, "lvlRestart");
|
|
394
|
-
const rawRestart = lvlRestartNode
|
|
426
|
+
const rawRestart = lvlRestartNode ? readStringAttr(lvlRestartNode, "w:val") : undefined;
|
|
395
427
|
const restartAfterLevel = rawRestart !== undefined ? parseInteger(rawRestart) : undefined;
|
|
396
428
|
const rPrNode = findChildElementOptional(levelNode, "rPr");
|
|
397
429
|
const runProperties = readRunProperties(rPrNode);
|
|
430
|
+
const lvlPicBulletNode = findChildElementOptional(levelNode, "lvlPicBulletId");
|
|
431
|
+
const picBulletId = lvlPicBulletNode ? readStringAttr(lvlPicBulletNode, "w:val") : undefined;
|
|
398
432
|
|
|
399
433
|
const hasExplicitFields =
|
|
400
434
|
startAt !== undefined ||
|
|
@@ -405,7 +439,8 @@ function readLevelOverrideDefinition(
|
|
|
405
439
|
suffix !== undefined ||
|
|
406
440
|
paragraphGeometry !== undefined ||
|
|
407
441
|
runProperties !== undefined ||
|
|
408
|
-
restartAfterLevel !== undefined
|
|
442
|
+
restartAfterLevel !== undefined ||
|
|
443
|
+
picBulletId !== undefined;
|
|
409
444
|
|
|
410
445
|
if (!hasExplicitFields) {
|
|
411
446
|
return undefined;
|
|
@@ -422,6 +457,7 @@ function readLevelOverrideDefinition(
|
|
|
422
457
|
...(paragraphGeometry ? { paragraphGeometry } : {}),
|
|
423
458
|
...(runProperties ? { runProperties } : {}),
|
|
424
459
|
...(restartAfterLevel !== undefined ? { restartAfterLevel } : {}),
|
|
460
|
+
...(picBulletId !== undefined ? { picBulletId } : {}),
|
|
425
461
|
};
|
|
426
462
|
}
|
|
427
463
|
|
|
@@ -456,11 +492,7 @@ function readLevelJustification(
|
|
|
456
492
|
return undefined;
|
|
457
493
|
}
|
|
458
494
|
|
|
459
|
-
const rawValue = (
|
|
460
|
-
justificationNode.attributes["w:val"] ??
|
|
461
|
-
justificationNode.attributes.val ??
|
|
462
|
-
""
|
|
463
|
-
).toLowerCase();
|
|
495
|
+
const rawValue = (readStringAttr(justificationNode, "w:val") ?? "").toLowerCase();
|
|
464
496
|
|
|
465
497
|
if (rawValue === "start") {
|
|
466
498
|
return "left";
|
|
@@ -488,18 +520,10 @@ function readParagraphIndentation(node: XmlElementNode): ParagraphIndentation |
|
|
|
488
520
|
}
|
|
489
521
|
|
|
490
522
|
const indentation: ParagraphIndentation = {};
|
|
491
|
-
const left =
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
indNode.attributes.left;
|
|
496
|
-
const right =
|
|
497
|
-
indNode.attributes["w:end"] ??
|
|
498
|
-
indNode.attributes.end ??
|
|
499
|
-
indNode.attributes["w:right"] ??
|
|
500
|
-
indNode.attributes.right;
|
|
501
|
-
const firstLine = indNode.attributes["w:firstLine"] ?? indNode.attributes.firstLine;
|
|
502
|
-
const hanging = indNode.attributes["w:hanging"] ?? indNode.attributes.hanging;
|
|
523
|
+
const left = readStringAttr(indNode, "w:start") ?? readStringAttr(indNode, "w:left");
|
|
524
|
+
const right = readStringAttr(indNode, "w:end") ?? readStringAttr(indNode, "w:right");
|
|
525
|
+
const firstLine = readStringAttr(indNode, "w:firstLine");
|
|
526
|
+
const hanging = readStringAttr(indNode, "w:hanging");
|
|
503
527
|
|
|
504
528
|
if (left !== undefined) {
|
|
505
529
|
const value = Number.parseInt(left, 10);
|
|
@@ -537,10 +561,10 @@ function readParagraphSpacing(node: XmlElementNode): ParagraphSpacing | undefine
|
|
|
537
561
|
}
|
|
538
562
|
|
|
539
563
|
const spacing: ParagraphSpacing = {};
|
|
540
|
-
const before = spacingNode
|
|
541
|
-
const after = spacingNode
|
|
542
|
-
const line = spacingNode
|
|
543
|
-
const lineRule = spacingNode
|
|
564
|
+
const before = readStringAttr(spacingNode, "w:before");
|
|
565
|
+
const after = readStringAttr(spacingNode, "w:after");
|
|
566
|
+
const line = readStringAttr(spacingNode, "w:line");
|
|
567
|
+
const lineRule = readStringAttr(spacingNode, "w:lineRule");
|
|
544
568
|
|
|
545
569
|
if (before !== undefined) {
|
|
546
570
|
const value = Number.parseInt(before, 10);
|
|
@@ -587,9 +611,9 @@ function readParagraphTabStops(node: XmlElementNode): TabStop[] | undefined {
|
|
|
587
611
|
continue;
|
|
588
612
|
}
|
|
589
613
|
|
|
590
|
-
const pos = child
|
|
591
|
-
const val = (child
|
|
592
|
-
const leader = (child
|
|
614
|
+
const pos = readStringAttr(child, "w:pos");
|
|
615
|
+
const val = (readStringAttr(child, "w:val") ?? "left").toLowerCase();
|
|
616
|
+
const leader = (readStringAttr(child, "w:leader") ?? "none").toLowerCase();
|
|
593
617
|
if (pos === undefined) {
|
|
594
618
|
continue;
|
|
595
619
|
}
|
|
@@ -599,21 +623,10 @@ function readParagraphTabStops(node: XmlElementNode): TabStop[] | undefined {
|
|
|
599
623
|
continue;
|
|
600
624
|
}
|
|
601
625
|
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
)
|
|
605
|
-
|
|
606
|
-
: "left";
|
|
607
|
-
const leaderValue =
|
|
608
|
-
leader === "none" ||
|
|
609
|
-
leader === "dot" ||
|
|
610
|
-
leader === "hyphen" ||
|
|
611
|
-
leader === "underscore" ||
|
|
612
|
-
leader === "heavy"
|
|
613
|
-
? (leader as Exclude<TabStop["leader"], "middleDot">)
|
|
614
|
-
: leader === "middledot"
|
|
615
|
-
? "middleDot"
|
|
616
|
-
: undefined;
|
|
626
|
+
const alignCandidate = val as TabStop["align"];
|
|
627
|
+
const align: TabStop["align"] = TAB_ALIGN_VOCAB.has(alignCandidate) ? alignCandidate : "left";
|
|
628
|
+
const leaderNorm = leader === "middledot" ? "middleDot" : (leader as TabStop["leader"]);
|
|
629
|
+
const leaderValue: TabStop["leader"] | undefined = TAB_LEADER_VOCAB.has(leaderNorm) ? leaderNorm : undefined;
|
|
617
630
|
|
|
618
631
|
tabStops.push({
|
|
619
632
|
position,
|
|
@@ -632,11 +645,7 @@ function readIsLegalNumberingValue(
|
|
|
632
645
|
return undefined;
|
|
633
646
|
}
|
|
634
647
|
|
|
635
|
-
const rawValue = (
|
|
636
|
-
isLegalNode.attributes["w:val"] ??
|
|
637
|
-
isLegalNode.attributes.val ??
|
|
638
|
-
"1"
|
|
639
|
-
).toLowerCase();
|
|
648
|
+
const rawValue = (readStringAttr(isLegalNode, "w:val") ?? "1").toLowerCase();
|
|
640
649
|
|
|
641
650
|
if (rawValue === "0" || rawValue === "false" || rawValue === "off") {
|
|
642
651
|
return false;
|
|
@@ -664,11 +673,26 @@ function findChildElementOptional(
|
|
|
664
673
|
);
|
|
665
674
|
}
|
|
666
675
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
676
|
+
/**
|
|
677
|
+
* If the numPicBullet node has an mc:AlternateContent child (Word 2016+),
|
|
678
|
+
* return the mc:Choice branch when it contains recognizable bullet content
|
|
679
|
+
* (w:drawing or w:pict), otherwise fall back to mc:Fallback or the original
|
|
680
|
+
* node. This mirrors parse-drawing.ts's pickAlternateContentBranch strategy.
|
|
681
|
+
*/
|
|
682
|
+
function unwrapNumPicBulletContent(node: XmlElementNode): XmlElementNode {
|
|
683
|
+
const alt = findChildElementOptional(node, "AlternateContent");
|
|
684
|
+
if (!alt) return node;
|
|
685
|
+
const choice = findChildElementOptional(alt, "Choice");
|
|
686
|
+
const fallback = findChildElementOptional(alt, "Fallback");
|
|
687
|
+
if (choice) {
|
|
688
|
+
const hasDrawing = findChildElementOptional(choice, "drawing") !== undefined;
|
|
689
|
+
const hasPict = findChildElementOptional(choice, "pict") !== undefined;
|
|
690
|
+
if (hasDrawing || hasPict) return choice;
|
|
691
|
+
}
|
|
692
|
+
return fallback ?? node;
|
|
670
693
|
}
|
|
671
694
|
|
|
695
|
+
|
|
672
696
|
function parseInteger(value: string): number | undefined {
|
|
673
697
|
if (!/^-?\d+$/.test(value)) {
|
|
674
698
|
return undefined;
|
|
@@ -676,208 +700,3 @@ function parseInteger(value: string): number | undefined {
|
|
|
676
700
|
|
|
677
701
|
return Number.parseInt(value, 10);
|
|
678
702
|
}
|
|
679
|
-
|
|
680
|
-
function parseXml(xml: string): XmlElementNode {
|
|
681
|
-
const root: XmlElementNode = {
|
|
682
|
-
type: "element",
|
|
683
|
-
name: "__root__",
|
|
684
|
-
attributes: {},
|
|
685
|
-
children: [],
|
|
686
|
-
};
|
|
687
|
-
const stack: XmlElementNode[] = [root];
|
|
688
|
-
let cursor = 0;
|
|
689
|
-
|
|
690
|
-
while (cursor < xml.length) {
|
|
691
|
-
if (xml.startsWith("<!--", cursor)) {
|
|
692
|
-
const end = xml.indexOf("-->", cursor);
|
|
693
|
-
cursor = end >= 0 ? end + 3 : xml.length;
|
|
694
|
-
continue;
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
if (xml.startsWith("<?", cursor)) {
|
|
698
|
-
const end = xml.indexOf("?>", cursor);
|
|
699
|
-
cursor = end >= 0 ? end + 2 : xml.length;
|
|
700
|
-
continue;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
if (xml.startsWith("<![CDATA[", cursor)) {
|
|
704
|
-
const end = xml.indexOf("]]>", cursor);
|
|
705
|
-
const textEnd = end >= 0 ? end : xml.length;
|
|
706
|
-
stack[stack.length - 1]?.children.push({
|
|
707
|
-
type: "text",
|
|
708
|
-
text: xml.slice(cursor + 9, textEnd),
|
|
709
|
-
});
|
|
710
|
-
cursor = end >= 0 ? end + 3 : xml.length;
|
|
711
|
-
continue;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
if (xml[cursor] !== "<") {
|
|
715
|
-
const nextTag = xml.indexOf("<", cursor);
|
|
716
|
-
const end = nextTag >= 0 ? nextTag : xml.length;
|
|
717
|
-
const text = decodeXmlEntities(xml.slice(cursor, end));
|
|
718
|
-
if (text.length > 0) {
|
|
719
|
-
stack[stack.length - 1]?.children.push({
|
|
720
|
-
type: "text",
|
|
721
|
-
text,
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
cursor = end;
|
|
725
|
-
continue;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
if (xml[cursor + 1] === "/") {
|
|
729
|
-
const end = xml.indexOf(">", cursor);
|
|
730
|
-
if (end < 0) {
|
|
731
|
-
throw new Error("Malformed XML: missing closing >.");
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
const name = xml.slice(cursor + 2, end).trim();
|
|
735
|
-
const current = stack.pop();
|
|
736
|
-
if (!current || localName(current.name) !== localName(name)) {
|
|
737
|
-
throw new Error(`Malformed XML: unexpected closing tag </${name}>.`);
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
cursor = end + 1;
|
|
741
|
-
continue;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
const tagEnd = findTagEnd(xml, cursor);
|
|
745
|
-
const tagBody = xml.slice(cursor + 1, tagEnd);
|
|
746
|
-
const selfClosing = /\/\s*$/.test(tagBody);
|
|
747
|
-
const { name, attributes } = parseTag(tagBody.replace(/\/\s*$/, "").trim());
|
|
748
|
-
const element: XmlElementNode = {
|
|
749
|
-
type: "element",
|
|
750
|
-
name,
|
|
751
|
-
attributes,
|
|
752
|
-
children: [],
|
|
753
|
-
};
|
|
754
|
-
stack[stack.length - 1]?.children.push(element);
|
|
755
|
-
|
|
756
|
-
if (!selfClosing) {
|
|
757
|
-
stack.push(element);
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
cursor = tagEnd + 1;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
if (stack.length !== 1) {
|
|
764
|
-
throw new Error("Malformed XML: unclosed element.");
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
return root;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
function parseTag(tagBody: string): { name: string; attributes: Record<string, string> } {
|
|
771
|
-
let cursor = 0;
|
|
772
|
-
while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
|
|
773
|
-
cursor += 1;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
const nameStart = cursor;
|
|
777
|
-
while (cursor < tagBody.length && !/\s/.test(tagBody[cursor] ?? "")) {
|
|
778
|
-
cursor += 1;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
const name = tagBody.slice(nameStart, cursor);
|
|
782
|
-
const attributes: Record<string, string> = {};
|
|
783
|
-
|
|
784
|
-
while (cursor < tagBody.length) {
|
|
785
|
-
while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
|
|
786
|
-
cursor += 1;
|
|
787
|
-
}
|
|
788
|
-
if (cursor >= tagBody.length) {
|
|
789
|
-
break;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
const keyStart = cursor;
|
|
793
|
-
while (cursor < tagBody.length && !/[\s=]/.test(tagBody[cursor] ?? "")) {
|
|
794
|
-
cursor += 1;
|
|
795
|
-
}
|
|
796
|
-
const key = tagBody.slice(keyStart, cursor);
|
|
797
|
-
|
|
798
|
-
while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
|
|
799
|
-
cursor += 1;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
if (tagBody[cursor] !== "=") {
|
|
803
|
-
attributes[key] = "";
|
|
804
|
-
continue;
|
|
805
|
-
}
|
|
806
|
-
cursor += 1;
|
|
807
|
-
|
|
808
|
-
while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
|
|
809
|
-
cursor += 1;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
const quote = tagBody[cursor];
|
|
813
|
-
if (quote !== `"` && quote !== `'`) {
|
|
814
|
-
throw new Error(`Malformed XML attribute ${key}.`);
|
|
815
|
-
}
|
|
816
|
-
cursor += 1;
|
|
817
|
-
|
|
818
|
-
const valueStart = cursor;
|
|
819
|
-
while (cursor < tagBody.length && tagBody[cursor] !== quote) {
|
|
820
|
-
cursor += 1;
|
|
821
|
-
}
|
|
822
|
-
const rawValue = tagBody.slice(valueStart, cursor);
|
|
823
|
-
attributes[key] = decodeXmlEntities(rawValue);
|
|
824
|
-
cursor += 1;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
return { name, attributes };
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
function findTagEnd(xml: string, start: number): number {
|
|
831
|
-
let cursor = start + 1;
|
|
832
|
-
let quote: string | null = null;
|
|
833
|
-
|
|
834
|
-
while (cursor < xml.length) {
|
|
835
|
-
const current = xml[cursor];
|
|
836
|
-
if (quote) {
|
|
837
|
-
if (current === quote) {
|
|
838
|
-
quote = null;
|
|
839
|
-
}
|
|
840
|
-
cursor += 1;
|
|
841
|
-
continue;
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
if (current === `"` || current === `'`) {
|
|
845
|
-
quote = current;
|
|
846
|
-
cursor += 1;
|
|
847
|
-
continue;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
if (current === ">") {
|
|
851
|
-
return cursor;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
cursor += 1;
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
throw new Error("Malformed XML: missing >.");
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
function decodeXmlEntities(value: string): string {
|
|
861
|
-
return value.replace(/&(#x[0-9a-fA-F]+|#\d+|amp|lt|gt|quot|apos);/g, (match, entity) => {
|
|
862
|
-
switch (entity) {
|
|
863
|
-
case "amp":
|
|
864
|
-
return "&";
|
|
865
|
-
case "lt":
|
|
866
|
-
return "<";
|
|
867
|
-
case "gt":
|
|
868
|
-
return ">";
|
|
869
|
-
case "quot":
|
|
870
|
-
return `"`;
|
|
871
|
-
case "apos":
|
|
872
|
-
return "'";
|
|
873
|
-
default:
|
|
874
|
-
if (entity.startsWith("#x")) {
|
|
875
|
-
return String.fromCodePoint(Number.parseInt(entity.slice(2), 16));
|
|
876
|
-
}
|
|
877
|
-
if (entity.startsWith("#")) {
|
|
878
|
-
return String.fromCodePoint(Number.parseInt(entity.slice(1), 10));
|
|
879
|
-
}
|
|
880
|
-
return match;
|
|
881
|
-
}
|
|
882
|
-
});
|
|
883
|
-
}
|