@stll/folio-core 0.35.1 → 0.36.0

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 (80) hide show
  1. package/dist/ai-edits/apply.js +370 -118
  2. package/dist/ai-edits/headless.js +16 -10
  3. package/dist/ai-edits/index.d.ts +2 -2
  4. package/dist/ai-edits/index.js +2 -2
  5. package/dist/ai-edits/snapshot.js +6 -1
  6. package/dist/ai-edits/types.d.ts +19 -6
  7. package/dist/ai-edits/word-diff.d.ts +18 -3
  8. package/dist/ai-edits/word-diff.js +556 -67
  9. package/dist/compare/compare.js +7 -16
  10. package/dist/compare/plan.js +8 -5
  11. package/dist/compare/verification.d.ts +8 -9
  12. package/dist/compare/verification.js +51 -46
  13. package/dist/compat/eigenpal.d.ts +2 -2
  14. package/dist/compat/eigenpal.js +2 -2
  15. package/dist/controller/headerFooterEditorManager.js +5 -5
  16. package/dist/controller/hiddenEditorApi.js +1 -1
  17. package/dist/controller/noteEditorManager.js +2 -2
  18. package/dist/document-operations.d.ts +5 -3
  19. package/dist/document-operations.js +32 -5
  20. package/dist/docx/commentParser.js +2 -4
  21. package/dist/docx/commentRangeIntegrity.js +2 -4
  22. package/dist/docx/documentClone.d.ts +2 -0
  23. package/dist/docx/documentClone.js +2 -0
  24. package/dist/docx/headerFooterParser.js +2 -4
  25. package/dist/docx/normalizeBaseDirection.js +5 -7
  26. package/dist/docx/paragraphParser.js +27 -29
  27. package/dist/docx/paragraphPropertySource.d.ts +49 -0
  28. package/dist/docx/paragraphPropertySource.js +113 -0
  29. package/dist/docx/runConsolidator.js +2 -4
  30. package/dist/docx/runParser.js +2 -15
  31. package/dist/docx/sectionParser.js +1 -15
  32. package/dist/docx/serializer/paragraphSerializer.js +287 -30
  33. package/dist/docx/serializer/runSerializer.js +2 -10
  34. package/dist/docx/serializer/sectionPropertiesSerializer.js +2 -10
  35. package/dist/docx/serializer/tableSerializer.js +4 -23
  36. package/dist/docx/serializer/trackedChangeAttributes.d.ts +9 -0
  37. package/dist/docx/serializer/trackedChangeAttributes.js +21 -0
  38. package/dist/docx/server/createBilingualDocument.js +8 -10
  39. package/dist/docx/server/evaluateDocxXmlPatchProposal.js +22 -13
  40. package/dist/docx/server/validateDocxConformance.js +17 -13
  41. package/dist/docx/tableParser.js +1 -37
  42. package/dist/docx/trackedChangeInfo.d.ts +11 -0
  43. package/dist/docx/trackedChangeInfo.js +38 -0
  44. package/dist/docx/verbatimCapture.d.ts +20 -9
  45. package/dist/docx/verbatimCapture.js +208 -8
  46. package/dist/docx/xmlParser.d.ts +3 -1
  47. package/dist/docx/xmlParser.js +6 -1
  48. package/dist/docx/xmlSafety.d.ts +1 -1
  49. package/dist/docx/xmlSafety.js +157 -0
  50. package/dist/index.d.ts +2 -2
  51. package/dist/index.js +2 -2
  52. package/dist/layout-bridge/convert/headerFooterLayout.js +4 -4
  53. package/dist/layout-bridge/convert/toFlowBlocks.js +12 -7
  54. package/dist/markdown/renderBlock.js +7 -10
  55. package/dist/prosemirror/attrs/index.js +15 -0
  56. package/dist/prosemirror/commands/comments.js +365 -32
  57. package/dist/prosemirror/commands/pageBreak.js +1 -1
  58. package/dist/prosemirror/commands/propertyChangeScope.d.ts +26 -4
  59. package/dist/prosemirror/commands/propertyChangeScope.js +65 -3
  60. package/dist/prosemirror/conversion/fromProseDoc.d.ts +2 -4
  61. package/dist/prosemirror/conversion/fromProseDoc.js +130 -18
  62. package/dist/prosemirror/conversion/toProseDoc.js +27 -12
  63. package/dist/prosemirror/extensions/core/ParagraphExtension.js +48 -4
  64. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +7 -1
  65. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +20 -7
  66. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +2 -0
  67. package/dist/prosemirror/paragraphAlignment.d.ts +12 -0
  68. package/dist/prosemirror/paragraphAlignment.js +13 -0
  69. package/dist/prosemirror/plugins/suggestionMode.js +1 -1
  70. package/dist/prosemirror/schema/marks.d.ts +2 -0
  71. package/dist/prosemirror/schema/nodes.d.ts +12 -1
  72. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  73. package/dist/redline.js +4 -1
  74. package/dist/server.d.ts +2 -2
  75. package/dist/server.js +2 -2
  76. package/dist/utils/mergeDocumentContent.js +3 -3
  77. package/dist/utils/replaceText.js +2 -1
  78. package/dist/version-comparison.js +6 -4
  79. package/dist/watermark/index.js +2 -4
  80. package/package.json +6 -1
@@ -98,6 +98,20 @@ const validateRequiredParts = (archive, report) => {
98
98
  });
99
99
  return false;
100
100
  };
101
+ const XML_SAFETY_ISSUE = {
102
+ "doctype-forbidden": {
103
+ code: "xml-doctype-forbidden",
104
+ message: "XML package parts must not declare a document type."
105
+ },
106
+ "entity-forbidden": {
107
+ code: "xml-not-well-formed",
108
+ message: "An XML package part contains an invalid entity reference."
109
+ },
110
+ "not-well-formed": {
111
+ code: "xml-not-well-formed",
112
+ message: "An XML package part is not well formed."
113
+ }
114
+ };
101
115
  const validateXmlParts = async (archive, report) => {
102
116
  const xmlByPath = /* @__PURE__ */ new Map();
103
117
  let hasInvalidXml = false;
@@ -106,27 +120,17 @@ const validateXmlParts = async (archive, report) => {
106
120
  if (xml === null) continue;
107
121
  xmlByPath.set(part, xml);
108
122
  const safetyIssue = getDocxXmlSafetyIssue(xml);
109
- if (safetyIssue === "doctype-forbidden") {
123
+ if (safetyIssue !== null) {
110
124
  hasInvalidXml = true;
125
+ const issue = XML_SAFETY_ISSUE[safetyIssue];
111
126
  addIssue(report, {
112
127
  check: "xml-well-formedness",
113
- code: "xml-doctype-forbidden",
114
- message: "XML package parts must not declare a document type.",
128
+ ...issue,
115
129
  severity: "error",
116
130
  part
117
131
  });
118
132
  continue;
119
133
  }
120
- if (safetyIssue === "not-well-formed") {
121
- hasInvalidXml = true;
122
- addIssue(report, {
123
- check: "xml-well-formedness",
124
- code: "xml-not-well-formed",
125
- message: "An XML package part is not well formed.",
126
- severity: "error",
127
- part
128
- });
129
- }
130
134
  }
131
135
  if (hasInvalidXml) {
132
136
  report.checks.set("xml-well-formedness", "failed");
@@ -3,9 +3,9 @@ import { appendBookmarkMarkerToLastParagraphInBlocks, appendBookmarkMarkerToLast
3
3
  import { parseParagraph } from "./paragraphParser.js";
4
4
  import { enrichParagraphTextBoxes } from "./paragraphTextBoxEnrichment.js";
5
5
  import { BorderStyleSchema, FloatingTableXSpecSchema, FloatingTableYSpecSchema, ShadingPatternSchema, TableCellTextDirectionSchema, ThemeColorSlotSchema, narrowEnum } from "./parserEnums.js";
6
+ import { parsePropertyChangeInfo, parseTrackedChangeInfo } from "./trackedChangeInfo.js";
6
7
  import { captureVerbatimXml } from "./verbatimCapture.js";
7
8
  import { cloneElement, findChild, findChildByLocalName, findChildren, getAttribute, getChildElements, getLocalName, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute, parseTableMeasurementValue } from "./xmlParser.js";
8
- import { normalizeRevisionId } from "@stll/docx-core/model";
9
9
  //#region src/docx/tableParser.ts
10
10
  /**
11
11
  * Sanity cap on `w:gridSpan` (and the derived table column count). Word's
@@ -36,42 +36,6 @@ function parseTableMeasurement(element) {
36
36
  function parseWidth(element) {
37
37
  return parseTableMeasurement(element);
38
38
  }
39
- function parseTrackedChangeInfo(node) {
40
- const rawId = getAttribute(node, "w", "id");
41
- const parsedId = rawId ? Number.parseInt(rawId, 10) : 0;
42
- const author = (getAttribute(node, "w", "author") ?? "").trim();
43
- const date = (getAttribute(node, "w", "date") ?? "").trim();
44
- const initials = (getAttribute(node, "w", "initials") ?? "").trim();
45
- const info = {
46
- id: normalizeRevisionId(parsedId),
47
- author: author.length > 0 ? author : "Unknown"
48
- };
49
- if (date.length > 0) info.date = date;
50
- if (initials.length > 0) info.initials = initials;
51
- const utcDate = utcDateAttribute(node);
52
- if (utcDate) info.utcDate = utcDate;
53
- return info;
54
- }
55
- /** `w16du:dateUtc` under whatever prefix the document bound it to. */
56
- const utcDateAttribute = (node) => {
57
- for (const [attribute, value] of Object.entries(node.attributes ?? {})) {
58
- if (typeof value !== "string" || getLocalName(attribute) !== "dateUtc") continue;
59
- const trimmed = value.trim();
60
- if (trimmed.length > 0) return {
61
- attribute,
62
- value: trimmed
63
- };
64
- }
65
- return null;
66
- };
67
- function parsePropertyChangeInfo(node) {
68
- const base = parseTrackedChangeInfo(node);
69
- const rsid = (getAttribute(node, "w", "rsid") ?? "").trim();
70
- return rsid.length > 0 ? {
71
- ...base,
72
- rsid
73
- } : base;
74
- }
75
39
  /**
76
40
  * Parse a single border specification
77
41
  *
@@ -0,0 +1,11 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+ import { XmlElement } from "./xmlParser.js";
3
+ //#region src/docx/trackedChangeInfo.d.ts
4
+ declare const DATE_UTC_NAMESPACE_URI = "http://schemas.microsoft.com/office/word/2023/wordml/word16du";
5
+ declare const DATE_UTC_ATTRIBUTE = "w16du:dateUtc";
6
+ /** Parse the metadata shared by every WordprocessingML tracked-change element. */
7
+ declare const parseTrackedChangeInfo: (node: XmlElement) => document_d_exports.TrackedChangeInfo;
8
+ /** Parse the shared metadata plus the optional property-change revision session. */
9
+ declare const parsePropertyChangeInfo: (node: XmlElement) => document_d_exports.PropertyChangeInfo;
10
+ //#endregion
11
+ export { DATE_UTC_ATTRIBUTE, DATE_UTC_NAMESPACE_URI, parsePropertyChangeInfo, parseTrackedChangeInfo };
@@ -0,0 +1,38 @@
1
+ import { WORDPROCESSINGML_NAMESPACE_URIS, findAttributeByNamespaceUri, getAttributeByNamespaceUri } from "./xmlParser.js";
2
+ import { normalizeRevisionId } from "@stll/docx-core/model";
3
+ //#region src/docx/trackedChangeInfo.ts
4
+ const DATE_UTC_NAMESPACE_URI = "http://schemas.microsoft.com/office/word/2023/wordml/word16du";
5
+ const DATE_UTC_ATTRIBUTE = "w16du:dateUtc";
6
+ const DATE_UTC_NAMESPACE_URIS = /* @__PURE__ */ new Set([DATE_UTC_NAMESPACE_URI]);
7
+ /** Parse the metadata shared by every WordprocessingML tracked-change element. */
8
+ const parseTrackedChangeInfo = (node) => {
9
+ const rawId = getAttributeByNamespaceUri(node, WORDPROCESSINGML_NAMESPACE_URIS, "id");
10
+ const parsedId = rawId ? Number.parseInt(rawId, 10) : 0;
11
+ const author = (getAttributeByNamespaceUri(node, WORDPROCESSINGML_NAMESPACE_URIS, "author") ?? "").trim();
12
+ const date = (getAttributeByNamespaceUri(node, WORDPROCESSINGML_NAMESPACE_URIS, "date") ?? "").trim();
13
+ const initials = (getAttributeByNamespaceUri(node, WORDPROCESSINGML_NAMESPACE_URIS, "initials") ?? "").trim();
14
+ const info = {
15
+ id: normalizeRevisionId(parsedId),
16
+ author: author.length > 0 ? author : "Unknown"
17
+ };
18
+ if (date.length > 0) info.date = date;
19
+ if (initials.length > 0) info.initials = initials;
20
+ const utcDate = findAttributeByNamespaceUri(node, DATE_UTC_NAMESPACE_URIS, "dateUtc");
21
+ const utcDateValue = utcDate?.value.trim() ?? "";
22
+ if (utcDate && utcDateValue.length > 0) info.utcDate = {
23
+ attribute: DATE_UTC_ATTRIBUTE,
24
+ value: utcDateValue
25
+ };
26
+ return info;
27
+ };
28
+ /** Parse the shared metadata plus the optional property-change revision session. */
29
+ const parsePropertyChangeInfo = (node) => {
30
+ const info = parseTrackedChangeInfo(node);
31
+ const rsid = (getAttributeByNamespaceUri(node, WORDPROCESSINGML_NAMESPACE_URIS, "rsid") ?? "").trim();
32
+ return rsid.length > 0 ? {
33
+ ...info,
34
+ rsid
35
+ } : info;
36
+ };
37
+ //#endregion
38
+ export { DATE_UTC_ATTRIBUTE, DATE_UTC_NAMESPACE_URI, parsePropertyChangeInfo, parseTrackedChangeInfo };
@@ -1,4 +1,4 @@
1
- import { XmlElement } from "./xmlParser.js";
1
+ import { XmlElement, XmlNamespaceScope } from "./xmlParser.js";
2
2
  //#region src/docx/verbatimCapture.d.ts
3
3
  declare const UntranslatableStrictNamespaceError_base: import("better-result").TaggedErrorClass<"UntranslatableStrictNamespaceError">;
4
4
  /** A Strict fragment uses a namespace that has no Transitional counterpart. */
@@ -6,15 +6,26 @@ declare class UntranslatableStrictNamespaceError extends UntranslatableStrictNam
6
6
  message: string;
7
7
  uri: string;
8
8
  }> {}
9
+ declare const captureVerbatimXml: (element: XmlElement) => string;
10
+ type SanitizeCapturedXmlOptions = {
11
+ allowedLocalNames: ReadonlySet<string>;
12
+ allowedNamespaceUris: ReadonlySet<string>;
13
+ inheritedNamespaceScope?: XmlNamespaceScope;
14
+ requiredNamespaceBindings?: ReadonlyMap<string, string>;
15
+ transform?: (element: XmlElement) => XmlElement;
16
+ validate?: (element: XmlElement) => boolean;
17
+ };
18
+ /** Validate a complete XML part immediately before package output. */
19
+ declare const isSafeCapturedXmlDocument: (xml: string) => boolean;
9
20
  /**
10
- * Serialize parsed markup for replay inside a rebuilt Transitional part.
21
+ * Re-parse and serialize one captured element before replaying it.
11
22
  *
12
- * Every verbatim capture goes through here — a lint rule keeps
13
- * {@link elementToXml} out of reach of the parsers so a replay path added
14
- * later cannot skip the conversion. Markup that binds no Strict namespace,
15
- * anywhere in scope or in the fragment itself, is serialized once and returned
16
- * unchanged.
23
+ * The parser deliberately ignores declarations, processing instructions,
24
+ * comments, and document types. Returning the caller's source string after a
25
+ * successful parse would therefore replay tokens that were never validated.
26
+ * This function returns only the parsed root serialized through the capture
27
+ * owner, and rejects siblings, unexpected roots, and namespace shadowing.
17
28
  */
18
- declare const captureVerbatimXml: (element: XmlElement) => string;
29
+ declare const sanitizeCapturedXmlElement: (xml: string | undefined, options: SanitizeCapturedXmlOptions) => string | null;
19
30
  //#endregion
20
- export { UntranslatableStrictNamespaceError, captureVerbatimXml };
31
+ export { UntranslatableStrictNamespaceError, captureVerbatimXml, isSafeCapturedXmlDocument, sanitizeCapturedXmlElement };
@@ -1,7 +1,9 @@
1
1
  import { TRANSITIONAL_NAMESPACE_BY_STRICT_URI } from "./strictValueEncodings.gen.js";
2
2
  import { isStrictNamespaceUri, toTransitionalNamespaceUri, transitionalSlotEncoding } from "./transitionalSpelling.js";
3
3
  import { roundHalfAwayFromZero, universalMeasureAs } from "./universalMeasure.js";
4
- import { elementToXml, getLocalName } from "./xmlParser.js";
4
+ import { NAMESPACES, OOXML_NAMESPACE_SCOPE, cloneElement, elementToXml, getChildElements, getLocalName, getNamespacePrefix, parseXml } from "./xmlParser.js";
5
+ import { assertXmlResourceLimits } from "./xmlResourceLimits.js";
6
+ import { getDocxXmlSafetyIssue } from "./xmlSafety.js";
5
7
  import { TaggedError } from "better-result";
6
8
  //#region src/docx/verbatimCapture.ts
7
9
  /**
@@ -24,6 +26,15 @@ import { TaggedError } from "better-result";
24
26
  /** A Strict fragment uses a namespace that has no Transitional counterpart. */
25
27
  var UntranslatableStrictNamespaceError = class extends TaggedError("UntranslatableStrictNamespaceError") {};
26
28
  const TRANSITIONAL_URIS = new Set(TRANSITIONAL_NAMESPACE_BY_STRICT_URI.values());
29
+ const XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
30
+ const XMLNS_NAMESPACE_URI = "http://www.w3.org/2000/xmlns/";
31
+ const XSI_NAMESPACE_URI = "http://www.w3.org/2001/XMLSchema-instance";
32
+ const NCNAME = /^[\p{L}_][\p{L}\p{N}._\-\u00b7\p{M}]*$/u;
33
+ const MC_QNAME_LIST_ATTRIBUTES = /* @__PURE__ */ new Set([
34
+ "PreserveAttributes",
35
+ "PreserveElements",
36
+ "ProcessContent"
37
+ ]);
27
38
  /**
28
39
  * Drawing extensions the repository has no schema for.
29
40
  *
@@ -71,12 +82,14 @@ const originOf = (element, inherited) => {
71
82
  });
72
83
  return TRANSITIONAL_URIS.has(uri) ? "transitional" : inherited;
73
84
  };
74
- const convertAttributes = (element, namespaceUri, localName) => {
85
+ const convertAttributes = (element, namespaceUri, localName, convertValues) => {
75
86
  const attributes = element.attributes ?? {};
76
87
  let converted;
77
88
  for (const [name, raw] of Object.entries(attributes)) {
78
89
  if (typeof raw !== "string") continue;
79
- const next = name === "xmlns" || name.startsWith("xmlns:") ? toTransitionalNamespaceUri(raw) : transitionalValue(raw, namespaceUri, transitionalSlotEncoding(element.namespaceUri, localName, getLocalName(name)));
90
+ let next = raw;
91
+ if (name === "xmlns" || name.startsWith("xmlns:")) next = toTransitionalNamespaceUri(raw);
92
+ else if (convertValues) next = transitionalValue(raw, namespaceUri, transitionalSlotEncoding(element.namespaceUri, localName, getLocalName(name)));
80
93
  if (next === raw) continue;
81
94
  converted ??= { ...attributes };
82
95
  converted[name] = next;
@@ -93,7 +106,7 @@ const toTransitional = (element, inherited) => {
93
106
  const namespaceUri = element.namespaceUri === void 0 ? "" : toTransitionalNamespaceUri(element.namespaceUri);
94
107
  const localName = getLocalName(element.name);
95
108
  const textEncoding = transitionalSlotEncoding(element.namespaceUri, localName);
96
- const attributes = origin === "strict" && element.attributes ? convertAttributes(element, namespaceUri, localName) : void 0;
109
+ const attributes = element.attributes ? convertAttributes(element, namespaceUri, localName, origin === "strict") : void 0;
97
110
  let elements;
98
111
  for (const [index, child] of (element.elements ?? []).entries()) {
99
112
  const next = origin === "strict" && child.type === "text" && typeof child.text === "string" ? childWithValue(child, transitionalValue(child.text, namespaceUri, textEncoding)) : toTransitional(child, origin);
@@ -113,6 +126,45 @@ const scopeDeclaresStrict = (scope) => {
113
126
  for (let current = scope; current; current = current.parent) for (const uri of current.bindings.values()) if (uri.startsWith("http://purl.oclc.org/ooxml/")) return true;
114
127
  return false;
115
128
  };
129
+ const resolveNamespaceBinding = (scope, prefix) => {
130
+ if (prefix === "xml") return XML_NAMESPACE_URI;
131
+ for (let current = scope; current; current = current.parent) {
132
+ const value = current.bindings.get(prefix);
133
+ if (value !== void 0) return value;
134
+ }
135
+ };
136
+ const namespaceValueKind = (element, attributeName) => {
137
+ const prefix = getNamespacePrefix(attributeName);
138
+ const namespaceUri = prefix === null ? "" : toTransitionalNamespaceUri(resolveNamespaceBinding(element.namespaceScope, prefix) ?? "");
139
+ const localName = getLocalName(attributeName);
140
+ if (namespaceUri === XSI_NAMESPACE_URI && localName === "type") return "qname";
141
+ if (namespaceUri === NAMESPACES.mc) {
142
+ if (localName === "Ignorable" || localName === "MustUnderstand") return "prefix-list";
143
+ if (MC_QNAME_LIST_ATTRIBUTES.has(localName)) return "qname-list";
144
+ }
145
+ if (prefix === null && localName === "Requires" && getLocalName(element.name ?? "") === "Choice" && toTransitionalNamespaceUri(element.namespaceUri ?? "") === NAMESPACES.mc) return "prefix-list";
146
+ return null;
147
+ };
148
+ const referencedNamespacePrefixes = (element, attributeName, value) => {
149
+ const kind = namespaceValueKind(element, attributeName);
150
+ if (kind === null) return [];
151
+ const tokens = value.trim() === "" ? [] : value.trim().split(/\s+/u);
152
+ if (kind === "qname" && tokens.length !== 1) return null;
153
+ const prefixes = [];
154
+ for (const token of tokens) {
155
+ if (kind === "prefix-list") {
156
+ if (!NCNAME.test(token) || resolveNamespaceBinding(element.namespaceScope, token) === void 0) return null;
157
+ prefixes.push(token);
158
+ continue;
159
+ }
160
+ const parts = token.split(":");
161
+ if (parts.length > 2 || parts.some((part) => !NCNAME.test(part))) return null;
162
+ const prefix = parts.length === 2 ? parts[0] : "";
163
+ if (prefix !== "" && resolveNamespaceBinding(element.namespaceScope, prefix) === void 0) return null;
164
+ prefixes.push(prefix);
165
+ }
166
+ return prefixes;
167
+ };
116
168
  /**
117
169
  * Serialize parsed markup for replay inside a rebuilt Transitional part.
118
170
  *
@@ -122,10 +174,158 @@ const scopeDeclaresStrict = (scope) => {
122
174
  * anywhere in scope or in the fragment itself, is serialized once and returned
123
175
  * unchanged.
124
176
  */
177
+ const materializeInheritedNamespaceBindings = (element) => {
178
+ const additions = {};
179
+ const rootAttributes = element.attributes ?? {};
180
+ const addInheritedBinding = (node, prefix) => {
181
+ if (prefix === "xml" || prefix === "xmlns") return;
182
+ const attributeName = prefix === "" ? "xmlns" : `xmlns:${prefix}`;
183
+ if (rootAttributes[attributeName] !== void 0 || additions[attributeName] !== void 0) return;
184
+ const namespaceUri = resolveNamespaceBinding(node.namespaceScope, prefix);
185
+ if (namespaceUri === void 0) return;
186
+ const canonicalNamespace = OOXML_NAMESPACE_SCOPE.bindings.get(prefix);
187
+ if (canonicalNamespace !== void 0 && toTransitionalNamespaceUri(namespaceUri) === canonicalNamespace) return;
188
+ additions[attributeName] = namespaceUri;
189
+ };
190
+ const visit = (node, inheritedDeclarations) => {
191
+ const declarations = new Set(inheritedDeclarations);
192
+ for (const name of Object.keys(node.attributes ?? {})) if (name === "xmlns") declarations.add("");
193
+ else if (name.startsWith("xmlns:")) declarations.add(name.slice(6));
194
+ const elementPrefix = node.name ? getNamespacePrefix(node.name) : null;
195
+ if (elementPrefix !== null && !declarations.has(elementPrefix)) addInheritedBinding(node, elementPrefix);
196
+ else if (elementPrefix === null && !declarations.has("")) addInheritedBinding(node, "");
197
+ for (const [name, value] of Object.entries(node.attributes ?? {})) {
198
+ if (name === "xmlns" || name.startsWith("xmlns:")) continue;
199
+ const attributePrefix = getNamespacePrefix(name);
200
+ if (attributePrefix !== null && !declarations.has(attributePrefix)) addInheritedBinding(node, attributePrefix);
201
+ const prefixes = referencedNamespacePrefixes(node, name, String(value));
202
+ if (prefixes === null) continue;
203
+ for (const prefix of prefixes) if (!declarations.has(prefix)) addInheritedBinding(node, prefix);
204
+ }
205
+ for (const child of node.elements ?? []) if (child.type === "element") visit(child, declarations);
206
+ };
207
+ visit(element, /* @__PURE__ */ new Set());
208
+ return Object.keys(additions).length === 0 ? element : cloneElement(element, { attributes: {
209
+ ...rootAttributes,
210
+ ...additions
211
+ } });
212
+ };
125
213
  const captureVerbatimXml = (element) => {
126
- const xml = elementToXml(element);
127
- if (!xml.includes("http://purl.oclc.org/ooxml/") && !scopeDeclaresStrict(element.namespaceScope)) return xml;
128
- return elementToXml(toTransitional(element, "transitional"));
214
+ const replayableElement = materializeInheritedNamespaceBindings(element);
215
+ const xml = elementToXml(replayableElement);
216
+ if (!xml.includes("http://purl.oclc.org/ooxml/") && !scopeDeclaresStrict(replayableElement.namespaceScope)) return xml;
217
+ return elementToXml(toTransitional(replayableElement, "transitional"));
218
+ };
219
+ const hasRequiredNamespaceBindings = (element, required) => {
220
+ if (!required) return true;
221
+ for (const [prefix, namespaceUri] of required) {
222
+ const resolved = resolveNamespaceBinding(element.namespaceScope, prefix);
223
+ if (resolved === void 0 || toTransitionalNamespaceUri(resolved) !== namespaceUri) return false;
224
+ }
225
+ return true;
226
+ };
227
+ const validNamespaceDeclaration = (name, value) => {
228
+ if (name === "xmlns") return value !== XML_NAMESPACE_URI && value !== XMLNS_NAMESPACE_URI;
229
+ const prefix = name.slice(6);
230
+ if (prefix.toLowerCase().startsWith("xml") && prefix !== "xml") return false;
231
+ if (prefix === "xml") return value === XML_NAMESPACE_URI;
232
+ return value !== "" && value !== XML_NAMESPACE_URI && value !== XMLNS_NAMESPACE_URI;
233
+ };
234
+ const hasBoundNamespaces = (root) => {
235
+ const pending = [root];
236
+ while (pending.length > 0) {
237
+ const element = pending.pop();
238
+ if (!element || element.type !== "element" || !element.name) return false;
239
+ const elementPrefix = getNamespacePrefix(element.name);
240
+ if (elementPrefix === "xmlns" || elementPrefix !== null && elementPrefix !== "xml" && resolveNamespaceBinding(element.namespaceScope, elementPrefix) === void 0) return false;
241
+ const expandedAttributeNames = /* @__PURE__ */ new Set();
242
+ for (const [name, rawValue] of Object.entries(element.attributes ?? {})) {
243
+ const value = String(rawValue ?? "");
244
+ if (name === "xmlns" || name.startsWith("xmlns:")) {
245
+ if (!validNamespaceDeclaration(name, value)) return false;
246
+ continue;
247
+ }
248
+ const prefix = getNamespacePrefix(name);
249
+ if (prefix === "xmlns" || prefix !== null && prefix !== "xml" && resolveNamespaceBinding(element.namespaceScope, prefix) === void 0) return false;
250
+ const expandedName = `${toTransitionalNamespaceUri((prefix === null ? "" : resolveNamespaceBinding(element.namespaceScope, prefix)) ?? "")}\u0000${getLocalName(name)}`;
251
+ if (expandedAttributeNames.has(expandedName)) return false;
252
+ expandedAttributeNames.add(expandedName);
253
+ if (referencedNamespacePrefixes(element, name, value) === null) return false;
254
+ }
255
+ for (const child of getChildElements(element)) pending.push(child);
256
+ }
257
+ return true;
258
+ };
259
+ const parsedReplayEnvelope = (xml, inheritedNamespaceScope) => {
260
+ let wrapper;
261
+ try {
262
+ wrapper = parseXml(`<folio-replay-envelope>${xml}</folio-replay-envelope>`, inheritedNamespaceScope).elements?.at(0);
263
+ } catch {
264
+ return null;
265
+ }
266
+ if (!wrapper || wrapper.type !== "element") return null;
267
+ let root = null;
268
+ for (const child of wrapper.elements ?? []) {
269
+ if (child.type === "text") {
270
+ if (String(child.text ?? "").trim() !== "") return null;
271
+ continue;
272
+ }
273
+ if (child.type !== "element" || root !== null) return null;
274
+ root = child;
275
+ }
276
+ return root;
277
+ };
278
+ const withinXmlResourceLimits = (xml) => {
279
+ try {
280
+ assertXmlResourceLimits(xml);
281
+ return true;
282
+ } catch {
283
+ return false;
284
+ }
285
+ };
286
+ const EMPTY_NAMESPACE_SCOPE = { bindings: /* @__PURE__ */ new Map() };
287
+ /** Validate a complete XML part immediately before package output. */
288
+ const isSafeCapturedXmlDocument = (xml) => {
289
+ const trimmed = xml.trim();
290
+ if (trimmed === "" || !withinXmlResourceLimits(xml) || getDocxXmlSafetyIssue(xml) !== null) return false;
291
+ const root = parsedReplayEnvelope(trimmed, EMPTY_NAMESPACE_SCOPE);
292
+ return root !== null && hasBoundNamespaces(root);
293
+ };
294
+ const captureAllowedElement = (element, { allowedLocalNames, allowedNamespaceUris, inheritedNamespaceScope, requiredNamespaceBindings, transform, validate }) => {
295
+ let passesDomainValidation = true;
296
+ try {
297
+ passesDomainValidation = validate?.(element) ?? true;
298
+ } catch {
299
+ return null;
300
+ }
301
+ if (element.type !== "element" || !element.name || !allowedLocalNames.has(getLocalName(element.name)) || !allowedNamespaceUris.has(toTransitionalNamespaceUri(element.namespaceUri ?? "")) || !hasBoundNamespaces(element) || !hasRequiredNamespaceBindings(element, requiredNamespaceBindings) || !passesDomainValidation) return null;
302
+ try {
303
+ const replayElement = transform?.(element) ?? element;
304
+ if (!(transform === void 0 ? true : validate?.(replayElement) ?? true) || !hasBoundNamespaces(replayElement) || !hasRequiredNamespaceBindings(replayElement, requiredNamespaceBindings)) return null;
305
+ const captured = captureVerbatimXml(replayElement);
306
+ const capturedRoot = parsedReplayEnvelope(captured, inheritedNamespaceScope ?? OOXML_NAMESPACE_SCOPE);
307
+ if (capturedRoot === null || !hasBoundNamespaces(capturedRoot) || !hasRequiredNamespaceBindings(capturedRoot, requiredNamespaceBindings) || !(validate?.(capturedRoot) ?? true)) return null;
308
+ return captured;
309
+ } catch {
310
+ return null;
311
+ }
312
+ };
313
+ /**
314
+ * Re-parse and serialize one captured element before replaying it.
315
+ *
316
+ * The parser deliberately ignores declarations, processing instructions,
317
+ * comments, and document types. Returning the caller's source string after a
318
+ * successful parse would therefore replay tokens that were never validated.
319
+ * This function returns only the parsed root serialized through the capture
320
+ * owner, and rejects siblings, unexpected roots, and namespace shadowing.
321
+ */
322
+ const sanitizeCapturedXmlElement = (xml, options) => {
323
+ if (xml === void 0) return null;
324
+ const trimmed = xml.trim();
325
+ if (!trimmed) return null;
326
+ if (!withinXmlResourceLimits(xml) || getDocxXmlSafetyIssue(xml) !== null) return null;
327
+ const root = parsedReplayEnvelope(trimmed, options.inheritedNamespaceScope ?? OOXML_NAMESPACE_SCOPE);
328
+ return root ? captureAllowedElement(root, options) : null;
129
329
  };
130
330
  //#endregion
131
- export { UntranslatableStrictNamespaceError, captureVerbatimXml };
331
+ export { UntranslatableStrictNamespaceError, captureVerbatimXml, isSafeCapturedXmlDocument, sanitizeCapturedXmlElement };
@@ -117,6 +117,8 @@ declare const WORDPROCESSINGML_NAMESPACE_URIS: ReadonlySet<string>;
117
117
  * from a foreign namespace is not accepted.
118
118
  */
119
119
  declare function findChildByNamespaceUri(parent: XmlElement | null | undefined, namespaceUris: ReadonlySet<string>, localName: string): XmlElement | null;
120
+ /** All direct children whose local name and resolved namespace both match. */
121
+ declare function findChildrenByNamespaceUri(parent: XmlElement | null | undefined, namespaceUris: ReadonlySet<string>, localName: string): XmlElement[];
120
122
  type XmlAttributeMatch = {
121
123
  name: string;
122
124
  value: string;
@@ -367,4 +369,4 @@ declare function cloneWithXmlnsDeclarations(element: XmlElement, xmlnsDecls: Rec
367
369
  */
368
370
  declare function cloneElement(element: XmlElement, overrides: Partial<XmlElement>): XmlElement;
369
371
  //#endregion
370
- export { NAMESPACES, OOXML_NAMESPACE_SCOPE, WORDPROCESSINGML_NAMESPACE_URIS, XmlAttributeMatch, XmlElement, XmlNamespaceScope, attachXmlNamespaceContext, cloneElement, cloneWithXmlnsDeclarations, collectXmlnsDeclarations, elementToXml, findAllDeep, findAttributeByNamespaceUri, findByFullName, findChild, findChildByLocalName, findChildByNamespaceUri, findChildren, findChildrenByLocalName, findDeep, getAttribute, getAttributeAny, getAttributeAnyPrefix, getAttributeByNamespaceUri, getAttributes, getChildElements, getLocalName, getNamespacePrefix, getNamespaceUri, getTextContent, hasChild, hasFlag, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseColorElement, parseNumberingLevelAttribute, parseNumericAttribute, parseOnOffValue, parseTableMeasurementValue, parseXml, parseXmlDocument };
372
+ export { NAMESPACES, OOXML_NAMESPACE_SCOPE, WORDPROCESSINGML_NAMESPACE_URIS, XmlAttributeMatch, XmlElement, XmlNamespaceScope, attachXmlNamespaceContext, cloneElement, cloneWithXmlnsDeclarations, collectXmlnsDeclarations, elementToXml, findAllDeep, findAttributeByNamespaceUri, findByFullName, findChild, findChildByLocalName, findChildByNamespaceUri, findChildren, findChildrenByLocalName, findChildrenByNamespaceUri, findDeep, getAttribute, getAttributeAny, getAttributeAnyPrefix, getAttributeByNamespaceUri, getAttributes, getChildElements, getLocalName, getNamespacePrefix, getNamespaceUri, getTextContent, hasChild, hasFlag, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseColorElement, parseNumberingLevelAttribute, parseNumericAttribute, parseOnOffValue, parseTableMeasurementValue, parseXml, parseXmlDocument };
@@ -227,6 +227,11 @@ function findChildByNamespaceUri(parent, namespaceUris, localName) {
227
227
  for (const child of parent.elements) if (child.type === "element" && hasLocalName(child.name, localName) && namespaceUris.has(child.namespaceUri ?? "")) return child;
228
228
  return null;
229
229
  }
230
+ /** All direct children whose local name and resolved namespace both match. */
231
+ function findChildrenByNamespaceUri(parent, namespaceUris, localName) {
232
+ if (!parent?.elements) return [];
233
+ return parent.elements.filter((child) => child.type === "element" && hasLocalName(child.name, localName) && namespaceUris.has(child.namespaceUri ?? ""));
234
+ }
230
235
  /** Find an attribute whose prefix resolves to one of the accepted namespace URIs. */
231
236
  function findAttributeByNamespaceUri(element, namespaceUris, localName) {
232
237
  if (!element?.attributes) return null;
@@ -783,4 +788,4 @@ function cloneElement(element, overrides) {
783
788
  return clone;
784
789
  }
785
790
  //#endregion
786
- export { NAMESPACES, OOXML_NAMESPACE_SCOPE, WORDPROCESSINGML_NAMESPACE_URIS, attachXmlNamespaceContext, cloneElement, cloneWithXmlnsDeclarations, collectXmlnsDeclarations, elementToXml, findAllDeep, findAttributeByNamespaceUri, findByFullName, findChild, findChildByLocalName, findChildByNamespaceUri, findChildren, findChildrenByLocalName, findDeep, getAttribute, getAttributeAny, getAttributeAnyPrefix, getAttributeByNamespaceUri, getAttributes, getChildElements, getLocalName, getNamespacePrefix, getNamespaceUri, getTextContent, hasChild, hasFlag, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseColorElement, parseNumberingLevelAttribute, parseNumericAttribute, parseOnOffValue, parseTableMeasurementValue, parseXml, parseXmlDocument };
791
+ export { NAMESPACES, OOXML_NAMESPACE_SCOPE, WORDPROCESSINGML_NAMESPACE_URIS, attachXmlNamespaceContext, cloneElement, cloneWithXmlnsDeclarations, collectXmlnsDeclarations, elementToXml, findAllDeep, findAttributeByNamespaceUri, findByFullName, findChild, findChildByLocalName, findChildByNamespaceUri, findChildren, findChildrenByLocalName, findChildrenByNamespaceUri, findDeep, getAttribute, getAttributeAny, getAttributeAnyPrefix, getAttributeByNamespaceUri, getAttributes, getChildElements, getLocalName, getNamespacePrefix, getNamespaceUri, getTextContent, hasChild, hasFlag, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseColorElement, parseNumberingLevelAttribute, parseNumericAttribute, parseOnOffValue, parseTableMeasurementValue, parseXml, parseXmlDocument };
@@ -1,5 +1,5 @@
1
1
  //#region src/docx/xmlSafety.d.ts
2
- type DocxXmlSafetyIssue = "doctype-forbidden" | "not-well-formed";
2
+ type DocxXmlSafetyIssue = "doctype-forbidden" | "entity-forbidden" | "not-well-formed";
3
3
  declare const getDocxXmlSafetyIssue: (xml: string) => DocxXmlSafetyIssue | null;
4
4
  //#endregion
5
5
  export { DocxXmlSafetyIssue, getDocxXmlSafetyIssue };