@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
|
@@ -16,6 +16,9 @@ export interface XmlTextNode {
|
|
|
16
16
|
|
|
17
17
|
export type XmlNode = XmlElementNode | XmlTextNode;
|
|
18
18
|
|
|
19
|
+
import { parseXmlWithOffsets } from "./xml-parser.ts";
|
|
20
|
+
import { localName, readStringAttr } from "./xml-attr-helpers.ts";
|
|
21
|
+
|
|
19
22
|
export interface ParsedBorderSpec {
|
|
20
23
|
value?: string;
|
|
21
24
|
size?: number;
|
|
@@ -50,6 +53,13 @@ export interface ParsedCellShading {
|
|
|
50
53
|
fill?: string;
|
|
51
54
|
color?: string;
|
|
52
55
|
val?: string;
|
|
56
|
+
/** SOW gap G3 — theme-fill references kept verbatim for runtime resolve + round-trip. */
|
|
57
|
+
themeFill?: string;
|
|
58
|
+
themeFillTint?: string;
|
|
59
|
+
themeFillShade?: string;
|
|
60
|
+
themeColor?: string;
|
|
61
|
+
themeColorTint?: string;
|
|
62
|
+
themeColorShade?: string;
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
export interface ParsedCellMargins {
|
|
@@ -143,7 +153,7 @@ export interface ParsedTableCell {
|
|
|
143
153
|
}
|
|
144
154
|
|
|
145
155
|
export function parseTablesFromDocumentXml(xml: string): ParsedTableDocument {
|
|
146
|
-
const root =
|
|
156
|
+
const root = parseXmlWithOffsets(xml) as XmlElementNode;
|
|
147
157
|
const documentElement = findChildElement(root, "document");
|
|
148
158
|
const bodyElement = findChildElement(documentElement, "body");
|
|
149
159
|
|
|
@@ -241,7 +251,7 @@ function parseCell(node: XmlElementNode, sourceXml: string): ParsedTableCell {
|
|
|
241
251
|
|
|
242
252
|
return {
|
|
243
253
|
...(propertiesNode ? { propertiesXml: sourceXml.slice(propertiesNode.start, propertiesNode.end) } : {}),
|
|
244
|
-
...(gridSpanNode ? { gridSpan: parsePositiveInteger(gridSpanNode
|
|
254
|
+
...(gridSpanNode ? { gridSpan: parsePositiveInteger(readStringAttr(gridSpanNode, "w:val")) } : {}),
|
|
245
255
|
...(verticalMergeNode ? { verticalMerge: readVerticalMerge(verticalMergeNode) } : {}),
|
|
246
256
|
blocksXml,
|
|
247
257
|
rawXml: sourceXml.slice(node.start, node.end),
|
|
@@ -260,11 +270,11 @@ function parseCell(node: XmlElementNode, sourceXml: string): ParsedTableCell {
|
|
|
260
270
|
export function readGridColumns(node: XmlElementNode): number[] {
|
|
261
271
|
return node.children
|
|
262
272
|
.filter((child): child is XmlElementNode => child.type === "element" && localName(child.name) === "gridCol")
|
|
263
|
-
.map((child) => parsePositiveInteger(child
|
|
273
|
+
.map((child) => parsePositiveInteger(readStringAttr(child, "w:w") ?? "0"));
|
|
264
274
|
}
|
|
265
275
|
|
|
266
276
|
function readVerticalMerge(node: XmlElementNode): "restart" | "continue" {
|
|
267
|
-
const value = (node
|
|
277
|
+
const value = (readStringAttr(node, "w:val") ?? "continue").toLowerCase();
|
|
268
278
|
return value === "restart" ? "restart" : "continue";
|
|
269
279
|
}
|
|
270
280
|
|
|
@@ -289,229 +299,14 @@ function findFirstChild(node: XmlElementNode, childLocalName: string): XmlElemen
|
|
|
289
299
|
);
|
|
290
300
|
}
|
|
291
301
|
|
|
292
|
-
function localName(name: string): string {
|
|
293
|
-
const separatorIndex = name.indexOf(":");
|
|
294
|
-
return separatorIndex >= 0 ? name.slice(separatorIndex + 1) : name;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function parseXml(xml: string): XmlElementNode {
|
|
298
|
-
const root: XmlElementNode = {
|
|
299
|
-
type: "element",
|
|
300
|
-
name: "__root__",
|
|
301
|
-
attributes: {},
|
|
302
|
-
children: [],
|
|
303
|
-
start: 0,
|
|
304
|
-
end: xml.length,
|
|
305
|
-
};
|
|
306
|
-
const stack: XmlElementNode[] = [root];
|
|
307
|
-
let cursor = 0;
|
|
308
|
-
|
|
309
|
-
while (cursor < xml.length) {
|
|
310
|
-
if (xml.startsWith("<!--", cursor)) {
|
|
311
|
-
const end = xml.indexOf("-->", cursor);
|
|
312
|
-
cursor = end >= 0 ? end + 3 : xml.length;
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (xml.startsWith("<?", cursor)) {
|
|
317
|
-
const end = xml.indexOf("?>", cursor);
|
|
318
|
-
cursor = end >= 0 ? end + 2 : xml.length;
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (xml.startsWith("<![CDATA[", cursor)) {
|
|
323
|
-
const end = xml.indexOf("]]>", cursor);
|
|
324
|
-
const textEnd = end >= 0 ? end : xml.length;
|
|
325
|
-
stack[stack.length - 1]?.children.push({
|
|
326
|
-
type: "text",
|
|
327
|
-
text: xml.slice(cursor + 9, textEnd),
|
|
328
|
-
start: cursor,
|
|
329
|
-
end: end >= 0 ? end + 3 : xml.length,
|
|
330
|
-
});
|
|
331
|
-
cursor = end >= 0 ? end + 3 : xml.length;
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (xml[cursor] !== "<") {
|
|
336
|
-
const nextTag = xml.indexOf("<", cursor);
|
|
337
|
-
const end = nextTag >= 0 ? nextTag : xml.length;
|
|
338
|
-
const text = decodeXmlEntities(xml.slice(cursor, end));
|
|
339
|
-
if (text.length > 0) {
|
|
340
|
-
stack[stack.length - 1]?.children.push({
|
|
341
|
-
type: "text",
|
|
342
|
-
text,
|
|
343
|
-
start: cursor,
|
|
344
|
-
end,
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
cursor = end;
|
|
348
|
-
continue;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (xml[cursor + 1] === "/") {
|
|
352
|
-
const end = xml.indexOf(">", cursor);
|
|
353
|
-
if (end < 0) {
|
|
354
|
-
throw new Error("Malformed XML: missing closing >.");
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const name = xml.slice(cursor + 2, end).trim();
|
|
358
|
-
const current = stack.pop();
|
|
359
|
-
if (!current || localName(current.name) !== localName(name)) {
|
|
360
|
-
throw new Error(`Malformed XML: unexpected closing tag </${name}>.`);
|
|
361
|
-
}
|
|
362
|
-
current.end = end + 1;
|
|
363
|
-
cursor = end + 1;
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const tagEnd = findTagEnd(xml, cursor);
|
|
368
|
-
const tagBody = xml.slice(cursor + 1, tagEnd);
|
|
369
|
-
const selfClosing = /\/\s*$/.test(tagBody);
|
|
370
|
-
const { name, attributes } = parseTag(tagBody.replace(/\/\s*$/, "").trim());
|
|
371
|
-
const element: XmlElementNode = {
|
|
372
|
-
type: "element",
|
|
373
|
-
name,
|
|
374
|
-
attributes,
|
|
375
|
-
children: [],
|
|
376
|
-
start: cursor,
|
|
377
|
-
end: tagEnd + 1,
|
|
378
|
-
};
|
|
379
|
-
stack[stack.length - 1]?.children.push(element);
|
|
380
|
-
|
|
381
|
-
if (!selfClosing) {
|
|
382
|
-
stack.push(element);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
cursor = tagEnd + 1;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (stack.length !== 1) {
|
|
389
|
-
throw new Error("Malformed XML: unclosed element.");
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
return root;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function parseTag(tagBody: string): { name: string; attributes: Record<string, string> } {
|
|
396
|
-
let cursor = 0;
|
|
397
|
-
while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
|
|
398
|
-
cursor += 1;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const nameStart = cursor;
|
|
402
|
-
while (cursor < tagBody.length && !/\s/.test(tagBody[cursor] ?? "")) {
|
|
403
|
-
cursor += 1;
|
|
404
|
-
}
|
|
405
|
-
const name = tagBody.slice(nameStart, cursor);
|
|
406
|
-
const attributes: Record<string, string> = {};
|
|
407
|
-
|
|
408
|
-
while (cursor < tagBody.length) {
|
|
409
|
-
while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
|
|
410
|
-
cursor += 1;
|
|
411
|
-
}
|
|
412
|
-
if (cursor >= tagBody.length) {
|
|
413
|
-
break;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const keyStart = cursor;
|
|
417
|
-
while (cursor < tagBody.length && !/[\s=]/.test(tagBody[cursor] ?? "")) {
|
|
418
|
-
cursor += 1;
|
|
419
|
-
}
|
|
420
|
-
const key = tagBody.slice(keyStart, cursor);
|
|
421
|
-
|
|
422
|
-
while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
|
|
423
|
-
cursor += 1;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
if (tagBody[cursor] !== "=") {
|
|
427
|
-
attributes[key] = "";
|
|
428
|
-
continue;
|
|
429
|
-
}
|
|
430
|
-
cursor += 1;
|
|
431
|
-
|
|
432
|
-
while (cursor < tagBody.length && /\s/.test(tagBody[cursor] ?? "")) {
|
|
433
|
-
cursor += 1;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const quote = tagBody[cursor];
|
|
437
|
-
if (quote !== `"` && quote !== `'`) {
|
|
438
|
-
throw new Error(`Malformed XML attribute ${key}.`);
|
|
439
|
-
}
|
|
440
|
-
cursor += 1;
|
|
441
|
-
|
|
442
|
-
const valueStart = cursor;
|
|
443
|
-
while (cursor < tagBody.length && tagBody[cursor] !== quote) {
|
|
444
|
-
cursor += 1;
|
|
445
|
-
}
|
|
446
|
-
attributes[key] = decodeXmlEntities(tagBody.slice(valueStart, cursor));
|
|
447
|
-
cursor += 1;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
return { name, attributes };
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function findTagEnd(xml: string, start: number): number {
|
|
454
|
-
let cursor = start + 1;
|
|
455
|
-
let quote: string | null = null;
|
|
456
|
-
|
|
457
|
-
while (cursor < xml.length) {
|
|
458
|
-
const current = xml[cursor];
|
|
459
|
-
if (quote) {
|
|
460
|
-
if (current === quote) {
|
|
461
|
-
quote = null;
|
|
462
|
-
}
|
|
463
|
-
cursor += 1;
|
|
464
|
-
continue;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
if (current === `"` || current === `'`) {
|
|
468
|
-
quote = current;
|
|
469
|
-
cursor += 1;
|
|
470
|
-
continue;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
if (current === ">") {
|
|
474
|
-
return cursor;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
cursor += 1;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
throw new Error("Malformed XML: missing >.");
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function decodeXmlEntities(value: string): string {
|
|
484
|
-
return value.replace(/&(#x[0-9a-fA-F]+|#\d+|amp|lt|gt|quot|apos);/g, (match, entity) => {
|
|
485
|
-
switch (entity) {
|
|
486
|
-
case "amp":
|
|
487
|
-
return "&";
|
|
488
|
-
case "lt":
|
|
489
|
-
return "<";
|
|
490
|
-
case "gt":
|
|
491
|
-
return ">";
|
|
492
|
-
case "quot":
|
|
493
|
-
return `"`;
|
|
494
|
-
case "apos":
|
|
495
|
-
return "'";
|
|
496
|
-
default:
|
|
497
|
-
if (entity.startsWith("#x")) {
|
|
498
|
-
return String.fromCodePoint(Number.parseInt(entity.slice(2), 16));
|
|
499
|
-
}
|
|
500
|
-
if (entity.startsWith("#")) {
|
|
501
|
-
return String.fromCodePoint(Number.parseInt(entity.slice(1), 10));
|
|
502
|
-
}
|
|
503
|
-
return match;
|
|
504
|
-
}
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
302
|
|
|
508
303
|
// Cell property readers
|
|
509
304
|
|
|
510
305
|
export function readCellWidth(propertiesNode: XmlElementNode): ParsedTableWidth | undefined {
|
|
511
306
|
const widthNode = findFirstChild(propertiesNode, "tcW");
|
|
512
307
|
if (!widthNode) return undefined;
|
|
513
|
-
const value = parsePositiveInteger(widthNode
|
|
514
|
-
const rawType = (widthNode
|
|
308
|
+
const value = parsePositiveInteger(readStringAttr(widthNode, "w:w"));
|
|
309
|
+
const rawType = (readStringAttr(widthNode, "w:type") ?? "dxa").toLowerCase();
|
|
515
310
|
const type: ParsedTableWidth["type"] =
|
|
516
311
|
rawType === "auto" ? "auto" : rawType === "pct" ? "pct" : rawType === "nil" ? "nil" : "dxa";
|
|
517
312
|
return { value, type };
|
|
@@ -534,21 +329,46 @@ export function readCellBorders(propertiesNode: XmlElementNode): ParsedTableCell
|
|
|
534
329
|
export function readCellShading(propertiesNode: XmlElementNode): ParsedCellShading | undefined {
|
|
535
330
|
const shdNode = findFirstChild(propertiesNode, "shd");
|
|
536
331
|
if (!shdNode) return undefined;
|
|
537
|
-
const fill = shdNode
|
|
538
|
-
const color = shdNode
|
|
539
|
-
const val = shdNode
|
|
540
|
-
|
|
332
|
+
const fill = readStringAttr(shdNode, "w:fill");
|
|
333
|
+
const color = readStringAttr(shdNode, "w:color");
|
|
334
|
+
const val = readStringAttr(shdNode, "w:val");
|
|
335
|
+
// SOW gap G3 — theme-shading refs. The CCEP SOW uses
|
|
336
|
+
// `<w:shd w:themeFill="accent5" w:themeFillTint="33"/>` on the Appendix-1
|
|
337
|
+
// mid-table sub-rows; when `w:fill` is absent or "auto", the runtime
|
|
338
|
+
// resolves the paint via ThemeColorResolver.resolveWordThemeColor so the
|
|
339
|
+
// cell picks up the theme cascade rather than rendering unshaded.
|
|
340
|
+
const themeFill = readStringAttr(shdNode, "w:themeFill");
|
|
341
|
+
const themeFillTint = readStringAttr(shdNode, "w:themeFillTint");
|
|
342
|
+
const themeFillShade = readStringAttr(shdNode, "w:themeFillShade");
|
|
343
|
+
const themeColor = readStringAttr(shdNode, "w:themeColor");
|
|
344
|
+
const themeColorTint = readStringAttr(shdNode, "w:themeColorTint");
|
|
345
|
+
const themeColorShade = readStringAttr(shdNode, "w:themeColorShade");
|
|
346
|
+
if (
|
|
347
|
+
!fill &&
|
|
348
|
+
!color &&
|
|
349
|
+
!val &&
|
|
350
|
+
!themeFill &&
|
|
351
|
+
!themeColor
|
|
352
|
+
) {
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
541
355
|
const result: ParsedCellShading = {};
|
|
542
356
|
if (fill) result.fill = fill;
|
|
543
357
|
if (color) result.color = color;
|
|
544
358
|
if (val) result.val = val;
|
|
359
|
+
if (themeFill) result.themeFill = themeFill;
|
|
360
|
+
if (themeFillTint) result.themeFillTint = themeFillTint;
|
|
361
|
+
if (themeFillShade) result.themeFillShade = themeFillShade;
|
|
362
|
+
if (themeColor) result.themeColor = themeColor;
|
|
363
|
+
if (themeColorTint) result.themeColorTint = themeColorTint;
|
|
364
|
+
if (themeColorShade) result.themeColorShade = themeColorShade;
|
|
545
365
|
return result;
|
|
546
366
|
}
|
|
547
367
|
|
|
548
368
|
export function readCellVerticalAlign(propertiesNode: XmlElementNode): "top" | "center" | "bottom" | undefined {
|
|
549
369
|
const vAlignNode = findFirstChild(propertiesNode, "vAlign");
|
|
550
370
|
if (!vAlignNode) return undefined;
|
|
551
|
-
const val = vAlignNode
|
|
371
|
+
const val = readStringAttr(vAlignNode, "w:val");
|
|
552
372
|
if (val === "center" || val === "top" || val === "bottom") return val;
|
|
553
373
|
return undefined;
|
|
554
374
|
}
|
|
@@ -558,8 +378,8 @@ export function readCellVerticalAlign(propertiesNode: XmlElementNode): "top" | "
|
|
|
558
378
|
export function readTableWidth(propertiesNode: XmlElementNode): ParsedTableWidth | undefined {
|
|
559
379
|
const widthNode = findFirstChild(propertiesNode, "tblW");
|
|
560
380
|
if (!widthNode) return undefined;
|
|
561
|
-
const value = parsePositiveInteger(widthNode
|
|
562
|
-
const rawType = (widthNode
|
|
381
|
+
const value = parsePositiveInteger(readStringAttr(widthNode, "w:w"));
|
|
382
|
+
const rawType = (readStringAttr(widthNode, "w:type") ?? "dxa").toLowerCase();
|
|
563
383
|
const type: ParsedTableWidth["type"] =
|
|
564
384
|
rawType === "auto" ? "auto" : rawType === "pct" ? "pct" : rawType === "nil" ? "nil" : "dxa";
|
|
565
385
|
return { value, type };
|
|
@@ -568,7 +388,7 @@ export function readTableWidth(propertiesNode: XmlElementNode): ParsedTableWidth
|
|
|
568
388
|
export function readTableAlignment(propertiesNode: XmlElementNode): "left" | "center" | "right" | undefined {
|
|
569
389
|
const jcNode = findFirstChild(propertiesNode, "jc");
|
|
570
390
|
if (!jcNode) return undefined;
|
|
571
|
-
const val = jcNode
|
|
391
|
+
const val = readStringAttr(jcNode, "w:val");
|
|
572
392
|
if (val === "left" || val === "center" || val === "right") return val;
|
|
573
393
|
return undefined;
|
|
574
394
|
}
|
|
@@ -576,7 +396,7 @@ export function readTableAlignment(propertiesNode: XmlElementNode): "left" | "ce
|
|
|
576
396
|
export function readTableStyleId(propertiesNode: XmlElementNode): string | undefined {
|
|
577
397
|
const styleNode = findFirstChild(propertiesNode, "tblStyle");
|
|
578
398
|
if (!styleNode) return undefined;
|
|
579
|
-
return styleNode
|
|
399
|
+
return readStringAttr(styleNode, "w:val");
|
|
580
400
|
}
|
|
581
401
|
|
|
582
402
|
export function readTableLook(propertiesNode: XmlElementNode): ParsedTableLook | undefined {
|
|
@@ -584,7 +404,7 @@ export function readTableLook(propertiesNode: XmlElementNode): ParsedTableLook |
|
|
|
584
404
|
if (!tblLookNode) return undefined;
|
|
585
405
|
|
|
586
406
|
const tableLook: ParsedTableLook = {};
|
|
587
|
-
const val = tblLookNode
|
|
407
|
+
const val = readStringAttr(tblLookNode, "w:val");
|
|
588
408
|
if (val) {
|
|
589
409
|
tableLook.val = val;
|
|
590
410
|
}
|
|
@@ -627,7 +447,7 @@ export function readTableCellMargins(propertiesNode: XmlElementNode): ParsedCell
|
|
|
627
447
|
const readSide = (name: string): number | undefined => {
|
|
628
448
|
const node = findFirstChild(marginsNode, name);
|
|
629
449
|
if (!node) return undefined;
|
|
630
|
-
return parsePositiveInteger(node
|
|
450
|
+
return parsePositiveInteger(readStringAttr(node, "w:w"));
|
|
631
451
|
};
|
|
632
452
|
const top = readSide("top");
|
|
633
453
|
const bottom = readSide("bottom");
|
|
@@ -650,13 +470,13 @@ export function readTableCellMargins(propertiesNode: XmlElementNode): ParsedCell
|
|
|
650
470
|
export function readRowHeight(propertiesNode: XmlElementNode): number | undefined {
|
|
651
471
|
const heightNode = findFirstChild(propertiesNode, "trHeight");
|
|
652
472
|
if (!heightNode) return undefined;
|
|
653
|
-
return parsePositiveInteger(heightNode
|
|
473
|
+
return parsePositiveInteger(readStringAttr(heightNode, "w:val"));
|
|
654
474
|
}
|
|
655
475
|
|
|
656
476
|
export function readRowHeightRule(propertiesNode: XmlElementNode): "auto" | "atLeast" | "exact" | undefined {
|
|
657
477
|
const heightNode = findFirstChild(propertiesNode, "trHeight");
|
|
658
478
|
if (!heightNode) return undefined;
|
|
659
|
-
const raw = (heightNode
|
|
479
|
+
const raw = (readStringAttr(heightNode, "w:hRule") ?? "").toLowerCase();
|
|
660
480
|
if (raw === "atleast") return "atLeast";
|
|
661
481
|
if (raw === "exact") return "exact";
|
|
662
482
|
if (raw === "auto") return "auto";
|
|
@@ -666,17 +486,17 @@ export function readRowHeightRule(propertiesNode: XmlElementNode): "auto" | "atL
|
|
|
666
486
|
export function readRowIsHeader(propertiesNode: XmlElementNode): boolean | undefined {
|
|
667
487
|
const headerNode = findFirstChild(propertiesNode, "tblHeader");
|
|
668
488
|
if (!headerNode) return undefined;
|
|
669
|
-
const val = headerNode
|
|
489
|
+
const val = readStringAttr(headerNode, "w:val");
|
|
670
490
|
return val !== "false" && val !== "0";
|
|
671
491
|
}
|
|
672
492
|
|
|
673
493
|
export function readTableIndent(propertiesNode: XmlElementNode): ParsedTableIndent | undefined {
|
|
674
494
|
const indentNode = findFirstChild(propertiesNode, "tblInd");
|
|
675
495
|
if (!indentNode) return undefined;
|
|
676
|
-
const valueRaw = indentNode
|
|
496
|
+
const valueRaw = readStringAttr(indentNode, "w:w");
|
|
677
497
|
const value = valueRaw !== undefined ? Number.parseInt(valueRaw, 10) : 0;
|
|
678
498
|
if (!Number.isFinite(value)) return undefined;
|
|
679
|
-
const rawType = (indentNode
|
|
499
|
+
const rawType = (readStringAttr(indentNode, "w:type") ?? "dxa").toLowerCase();
|
|
680
500
|
const type: ParsedTableIndent["type"] =
|
|
681
501
|
rawType === "auto" ? "auto" : rawType === "pct" ? "pct" : rawType === "nil" ? "nil" : "dxa";
|
|
682
502
|
return { value, type };
|
|
@@ -685,7 +505,7 @@ export function readTableIndent(propertiesNode: XmlElementNode): ParsedTableInde
|
|
|
685
505
|
export function readTableLayoutMode(propertiesNode: XmlElementNode): "fixed" | "autofit" | undefined {
|
|
686
506
|
const layoutNode = findFirstChild(propertiesNode, "tblLayout");
|
|
687
507
|
if (!layoutNode) return undefined;
|
|
688
|
-
const raw = (layoutNode
|
|
508
|
+
const raw = (readStringAttr(layoutNode, "w:type") ?? "").toLowerCase();
|
|
689
509
|
if (raw === "fixed") return "fixed";
|
|
690
510
|
if (raw === "autofit") return "autofit";
|
|
691
511
|
return undefined;
|
|
@@ -694,10 +514,10 @@ export function readTableLayoutMode(propertiesNode: XmlElementNode): "fixed" | "
|
|
|
694
514
|
export function readTableCellSpacing(propertiesNode: XmlElementNode): ParsedTableWidth | undefined {
|
|
695
515
|
const spacingNode = findFirstChild(propertiesNode, "tblCellSpacing");
|
|
696
516
|
if (!spacingNode) return undefined;
|
|
697
|
-
const valueRaw = spacingNode
|
|
517
|
+
const valueRaw = readStringAttr(spacingNode, "w:w");
|
|
698
518
|
const value = valueRaw !== undefined ? Number.parseInt(valueRaw, 10) : 0;
|
|
699
519
|
if (!Number.isFinite(value)) return undefined;
|
|
700
|
-
const rawType = (spacingNode
|
|
520
|
+
const rawType = (readStringAttr(spacingNode, "w:type") ?? "dxa").toLowerCase();
|
|
701
521
|
const type: ParsedTableWidth["type"] =
|
|
702
522
|
rawType === "auto" ? "auto" : rawType === "pct" ? "pct" : rawType === "nil" ? "nil" : "dxa";
|
|
703
523
|
return { value, type };
|
|
@@ -706,19 +526,19 @@ export function readTableCellSpacing(propertiesNode: XmlElementNode): ParsedTabl
|
|
|
706
526
|
export function readTableCaption(propertiesNode: XmlElementNode): string | undefined {
|
|
707
527
|
const captionNode = findFirstChild(propertiesNode, "tblCaption");
|
|
708
528
|
if (!captionNode) return undefined;
|
|
709
|
-
return captionNode
|
|
529
|
+
return readStringAttr(captionNode, "w:val");
|
|
710
530
|
}
|
|
711
531
|
|
|
712
532
|
export function readTableDescription(propertiesNode: XmlElementNode): string | undefined {
|
|
713
533
|
const descriptionNode = findFirstChild(propertiesNode, "tblDescription");
|
|
714
534
|
if (!descriptionNode) return undefined;
|
|
715
|
-
return descriptionNode
|
|
535
|
+
return readStringAttr(descriptionNode, "w:val");
|
|
716
536
|
}
|
|
717
537
|
|
|
718
538
|
export function readTableBidiVisual(propertiesNode: XmlElementNode): boolean | undefined {
|
|
719
539
|
const bidiNode = findFirstChild(propertiesNode, "bidiVisual");
|
|
720
540
|
if (!bidiNode) return undefined;
|
|
721
|
-
const val = bidiNode
|
|
541
|
+
const val = readStringAttr(bidiNode, "w:val");
|
|
722
542
|
return val !== "false" && val !== "0" && val !== "off";
|
|
723
543
|
}
|
|
724
544
|
|
|
@@ -773,7 +593,7 @@ export function readTableFloating(propertiesNode: XmlElementNode): ParsedTableFl
|
|
|
773
593
|
}
|
|
774
594
|
|
|
775
595
|
if (overlapNode) {
|
|
776
|
-
const val = (overlapNode
|
|
596
|
+
const val = (readStringAttr(overlapNode, "w:val") ?? "overlap").toLowerCase();
|
|
777
597
|
floating.overlap = val === "overlap";
|
|
778
598
|
}
|
|
779
599
|
|
|
@@ -783,14 +603,14 @@ export function readTableFloating(propertiesNode: XmlElementNode): ParsedTableFl
|
|
|
783
603
|
export function readRowCantSplit(propertiesNode: XmlElementNode): boolean | undefined {
|
|
784
604
|
const cantSplitNode = findFirstChild(propertiesNode, "cantSplit");
|
|
785
605
|
if (!cantSplitNode) return undefined;
|
|
786
|
-
const val = cantSplitNode
|
|
606
|
+
const val = readStringAttr(cantSplitNode, "w:val");
|
|
787
607
|
return val !== "false" && val !== "0" && val !== "off";
|
|
788
608
|
}
|
|
789
609
|
|
|
790
610
|
export function readRowHorizontalAlignment(propertiesNode: XmlElementNode): "left" | "center" | "right" | undefined {
|
|
791
611
|
const jcNode = findFirstChild(propertiesNode, "jc");
|
|
792
612
|
if (!jcNode) return undefined;
|
|
793
|
-
const val = jcNode
|
|
613
|
+
const val = readStringAttr(jcNode, "w:val");
|
|
794
614
|
if (val === "left" || val === "center" || val === "right") return val;
|
|
795
615
|
return undefined;
|
|
796
616
|
}
|
|
@@ -798,13 +618,13 @@ export function readRowHorizontalAlignment(propertiesNode: XmlElementNode): "lef
|
|
|
798
618
|
export function readRowCnfStyle(propertiesNode: XmlElementNode): string | undefined {
|
|
799
619
|
const cnfNode = findFirstChild(propertiesNode, "cnfStyle");
|
|
800
620
|
if (!cnfNode) return undefined;
|
|
801
|
-
return cnfNode
|
|
621
|
+
return readStringAttr(cnfNode, "w:val");
|
|
802
622
|
}
|
|
803
623
|
|
|
804
624
|
export function readCellTextDirection(propertiesNode: XmlElementNode): "lrTb" | "tbRl" | "btLr" | undefined {
|
|
805
625
|
const dirNode = findFirstChild(propertiesNode, "textDirection");
|
|
806
626
|
if (!dirNode) return undefined;
|
|
807
|
-
const val = dirNode
|
|
627
|
+
const val = readStringAttr(dirNode, "w:val");
|
|
808
628
|
if (val === "lrTb" || val === "tbRl" || val === "btLr") return val;
|
|
809
629
|
return undefined;
|
|
810
630
|
}
|
|
@@ -812,14 +632,14 @@ export function readCellTextDirection(propertiesNode: XmlElementNode): "lrTb" |
|
|
|
812
632
|
export function readCellNoWrap(propertiesNode: XmlElementNode): boolean | undefined {
|
|
813
633
|
const noWrapNode = findFirstChild(propertiesNode, "noWrap");
|
|
814
634
|
if (!noWrapNode) return undefined;
|
|
815
|
-
const val = noWrapNode
|
|
635
|
+
const val = readStringAttr(noWrapNode, "w:val");
|
|
816
636
|
return val !== "false" && val !== "0" && val !== "off";
|
|
817
637
|
}
|
|
818
638
|
|
|
819
639
|
export function readCellFitText(propertiesNode: XmlElementNode): boolean | undefined {
|
|
820
640
|
const fitNode = findFirstChild(propertiesNode, "tcFitText");
|
|
821
641
|
if (!fitNode) return undefined;
|
|
822
|
-
const val = fitNode
|
|
642
|
+
const val = readStringAttr(fitNode, "w:val");
|
|
823
643
|
return val !== "false" && val !== "0" && val !== "off";
|
|
824
644
|
}
|
|
825
645
|
|
|
@@ -829,7 +649,7 @@ export function readCellMargins(propertiesNode: XmlElementNode): ParsedCellMargi
|
|
|
829
649
|
const readSide = (name: string): number | undefined => {
|
|
830
650
|
const sideNode = findFirstChild(marginsNode, name);
|
|
831
651
|
if (!sideNode) return undefined;
|
|
832
|
-
const raw = sideNode
|
|
652
|
+
const raw = readStringAttr(sideNode, "w:w");
|
|
833
653
|
if (raw === undefined) return undefined;
|
|
834
654
|
const parsed = Number.parseInt(raw, 10);
|
|
835
655
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
@@ -852,14 +672,14 @@ export function readCellMargins(propertiesNode: XmlElementNode): ParsedCellMargi
|
|
|
852
672
|
export function readCellCnfStyle(propertiesNode: XmlElementNode): string | undefined {
|
|
853
673
|
const cnfNode = findFirstChild(propertiesNode, "cnfStyle");
|
|
854
674
|
if (!cnfNode) return undefined;
|
|
855
|
-
return cnfNode
|
|
675
|
+
return readStringAttr(cnfNode, "w:val");
|
|
856
676
|
}
|
|
857
677
|
|
|
858
678
|
function parseBorderSpec(child: XmlElementNode): ParsedBorderSpec | undefined {
|
|
859
|
-
const value = child
|
|
860
|
-
const sizeRaw = child
|
|
861
|
-
const spaceRaw = child
|
|
862
|
-
const color = child
|
|
679
|
+
const value = readStringAttr(child, "w:val");
|
|
680
|
+
const sizeRaw = readStringAttr(child, "w:sz");
|
|
681
|
+
const spaceRaw = readStringAttr(child, "w:space");
|
|
682
|
+
const color = readStringAttr(child, "w:color");
|
|
863
683
|
if (!value && !sizeRaw && !spaceRaw && !color) return undefined;
|
|
864
684
|
const spec: ParsedBorderSpec = {};
|
|
865
685
|
if (value) spec.value = value;
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
} from "../../model/canonical-document.ts";
|
|
9
9
|
import type { XmlElementNode } from "./xml-element.ts";
|
|
10
10
|
import { parseXml } from "./xml-parser.ts";
|
|
11
|
+
import { localName } from "./xml-attr-helpers.ts";
|
|
11
12
|
|
|
12
13
|
// ---- Well-known DrawingML color slot names ----
|
|
13
14
|
|
|
@@ -274,8 +275,4 @@ function findChildElementOptional(
|
|
|
274
275
|
);
|
|
275
276
|
}
|
|
276
277
|
|
|
277
|
-
function localName(name: string): string {
|
|
278
|
-
const idx = name.indexOf(":");
|
|
279
|
-
return idx >= 0 ? name.slice(idx + 1) : name;
|
|
280
|
-
}
|
|
281
278
|
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
* `modelledChildNames`.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
+
import type { SerializableXmlElement } from "./xml-element-serialize.ts";
|
|
27
|
+
import { serializeXmlElementToString } from "./xml-element-serialize.ts";
|
|
28
|
+
|
|
26
29
|
/**
|
|
27
30
|
* Input node shape accepted by `capturePropertyGrabBag`. Intentionally
|
|
28
31
|
* minimal so every caller can adapt their own scanner output — per-file
|
|
@@ -56,14 +59,10 @@ export interface GrabBagSourceChild {
|
|
|
56
59
|
* the correct trade-off — unmodelled children's semantic content
|
|
57
60
|
* survives, which closes the silent-drop gap.
|
|
58
61
|
*/
|
|
59
|
-
export function buildGrabBagSourceChildFromParsed(node: {
|
|
60
|
-
name: string;
|
|
61
|
-
attributes: Record<string, string>;
|
|
62
|
-
children: Array<{ type: "element"; name: string; attributes: Record<string, string>; children: unknown[] } | { type: "text"; text: string }>;
|
|
63
|
-
}): GrabBagSourceChild {
|
|
62
|
+
export function buildGrabBagSourceChildFromParsed(node: SerializableXmlElement): GrabBagSourceChild {
|
|
64
63
|
return {
|
|
65
64
|
localName: localNameOf(node.name),
|
|
66
|
-
rawXml:
|
|
65
|
+
rawXml: serializeXmlElementToString(node),
|
|
67
66
|
};
|
|
68
67
|
}
|
|
69
68
|
|
|
@@ -72,47 +71,6 @@ function localNameOf(qualified: string): string {
|
|
|
72
71
|
return colon < 0 ? qualified : qualified.slice(colon + 1);
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
function escapeAttr(value: string): string {
|
|
76
|
-
return value
|
|
77
|
-
.replace(/&/gu, "&")
|
|
78
|
-
.replace(/</gu, "<")
|
|
79
|
-
.replace(/>/gu, ">")
|
|
80
|
-
.replace(/"/gu, """);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function escapeText(value: string): string {
|
|
84
|
-
return value
|
|
85
|
-
.replace(/&/gu, "&")
|
|
86
|
-
.replace(/</gu, "<")
|
|
87
|
-
.replace(/>/gu, ">");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function serializeElementToString(node: {
|
|
91
|
-
name: string;
|
|
92
|
-
attributes: Record<string, string>;
|
|
93
|
-
children: Array<{ type: "element"; name: string; attributes: Record<string, string>; children: unknown[] } | { type: "text"; text: string }>;
|
|
94
|
-
}): string {
|
|
95
|
-
const attrs = Object.entries(node.attributes)
|
|
96
|
-
.map(([name, value]) => ` ${name}="${escapeAttr(value)}"`)
|
|
97
|
-
.join("");
|
|
98
|
-
if (node.children.length === 0) {
|
|
99
|
-
return `<${node.name}${attrs}/>`;
|
|
100
|
-
}
|
|
101
|
-
const body = node.children
|
|
102
|
-
.map((child) => {
|
|
103
|
-
if (child.type === "text") return escapeText(child.text);
|
|
104
|
-
return serializeElementToString(
|
|
105
|
-
child as {
|
|
106
|
-
name: string;
|
|
107
|
-
attributes: Record<string, string>;
|
|
108
|
-
children: Array<{ type: "element"; name: string; attributes: Record<string, string>; children: unknown[] } | { type: "text"; text: string }>;
|
|
109
|
-
},
|
|
110
|
-
);
|
|
111
|
-
})
|
|
112
|
-
.join("");
|
|
113
|
-
return `<${node.name}${attrs}>${body}</${node.name}>`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
74
|
/**
|
|
117
75
|
* Descriptor a per-container parser supplies to the helper to declare
|
|
118
76
|
* which child element names it dispatches into its modelled fields.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface SerializableXmlElement {
|
|
2
|
+
name: string;
|
|
3
|
+
attributes: Record<string, string>;
|
|
4
|
+
children: Array<SerializableXmlElement | { type: "text"; text: string }>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function serializeXmlElementToString(node: SerializableXmlElement): string {
|
|
8
|
+
const attrs = Object.entries(node.attributes)
|
|
9
|
+
.map(([name, value]) => ` ${name}="${escapeXmlAttr(value)}"`)
|
|
10
|
+
.join("");
|
|
11
|
+
if (node.children.length === 0) {
|
|
12
|
+
return `<${node.name}${attrs}/>`;
|
|
13
|
+
}
|
|
14
|
+
const body = node.children
|
|
15
|
+
.map((child) =>
|
|
16
|
+
"text" in child ? escapeXmlText(child.text) : serializeXmlElementToString(child),
|
|
17
|
+
)
|
|
18
|
+
.join("");
|
|
19
|
+
return `<${node.name}${attrs}>${body}</${node.name}>`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function escapeXmlAttr(value: string): string {
|
|
23
|
+
return value
|
|
24
|
+
.replace(/&/gu, "&")
|
|
25
|
+
.replace(/</gu, "<")
|
|
26
|
+
.replace(/>/gu, ">")
|
|
27
|
+
.replace(/"/gu, """);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function escapeXmlText(value: string): string {
|
|
31
|
+
return value.replace(/&/gu, "&").replace(/</gu, "<").replace(/>/gu, ">");
|
|
32
|
+
}
|