@stll/folio-core 0.35.0 → 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 +3 -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 +7 -2
@@ -1,11 +1,19 @@
1
+ import { canonicalJson } from "../../utils/canonicalJson.js";
1
2
  import { isValidHexColor } from "../../utils/colorResolver.js";
2
3
  import { numPrEqual } from "../numberingParser.js";
4
+ import { getParagraphPropertySource } from "../paragraphPropertySource.js";
3
5
  import { reconcileRawSdtPr } from "../sdtPropertiesPatch.js";
6
+ import { DATE_UTC_ATTRIBUTE } from "../trackedChangeInfo.js";
7
+ import { toTransitionalNamespaceUri } from "../transitionalSpelling.js";
8
+ import { captureVerbatimXml, sanitizeCapturedXmlElement } from "../verbatimCapture.js";
9
+ import { NAMESPACES, OOXML_NAMESPACE_SCOPE, cloneElement, getChildElements, getLocalName, parseXml } from "../xmlParser.js";
4
10
  import { serializeBorder } from "./borderSerializer.js";
5
11
  import { serializeRun, serializeTextFormatting } from "./runSerializer.js";
6
12
  import { serializeSectionProperties } from "./sectionPropertiesSerializer.js";
13
+ import { serializeTrackedChangeAttributes, trackedChangeAttributeRecord } from "./trackedChangeAttributes.js";
7
14
  import { escapeXml, intAttr, isSingleWellFormedElement } from "./xmlUtils.js";
8
- import { normalizeRevisionId } from "@stll/docx-core/model";
15
+ import { panic } from "better-result";
16
+ import { PARAGRAPH_MARK_CHANGE_KINDS } from "@stll/docx-core/model";
9
17
  //#region src/docx/serializer/paragraphSerializer.ts
10
18
  /**
11
19
  * Serialize paragraph borders (w:pBdr)
@@ -131,16 +139,278 @@ function serializeFrameProperties(frame) {
131
139
  /**
132
140
  * Serialize paragraph formatting properties to w:pPr XML
133
141
  */
134
- function serializeTrackedChangeAttrs(info) {
135
- const parts = [`w:id="${normalizeRevisionId(info.id)}"`, `w:author="${escapeXml(info.author)}"`];
136
- if (info.date !== void 0) parts.push(`w:date="${escapeXml(info.date)}"`);
137
- return parts.join(" ");
138
- }
139
142
  function serializeParagraphMarkChange(mark) {
140
- const attrs = serializeTrackedChangeAttrs(mark.info);
143
+ const attrs = serializeTrackedChangeAttributes(mark.info);
141
144
  return `<w:${mark.kind} ${attrs}/>`;
142
145
  }
143
- const serializeParagraphFormattingWithOptions = (formatting, { propertyChanges, paragraphMarkChange, sectionProperties } = {}) => {
146
+ const RESERVED_PARAGRAPH_PROPERTY_CHILDREN = /* @__PURE__ */ new Set(["pPrChange", "sectPr"]);
147
+ const RESERVED_PARAGRAPH_CAPTURE_CHILDREN = /* @__PURE__ */ new Set([
148
+ ...RESERVED_PARAGRAPH_PROPERTY_CHILDREN,
149
+ ...PARAGRAPH_MARK_CHANGE_KINDS,
150
+ "cellDel",
151
+ "cellIns",
152
+ "cellMerge",
153
+ "numberingChange",
154
+ "rPrChange",
155
+ "tblGridChange",
156
+ "tblPrChange",
157
+ "tcPrChange",
158
+ "trPrChange"
159
+ ]);
160
+ const PARAGRAPH_PROPERTY_ROOT_NAME = /* @__PURE__ */ new Set(["pPr"]);
161
+ const WORDPROCESSINGML_NAMESPACE = /* @__PURE__ */ new Set([NAMESPACES.w]);
162
+ const PARAGRAPH_APPEND_PREFIXES = /* @__PURE__ */ new Map([
163
+ ["w", NAMESPACES.w],
164
+ ["r", NAMESPACES.r],
165
+ ["w15", NAMESPACES.w15]
166
+ ]);
167
+ const PARAGRAPH_PROPERTY_CHILD_ORDER = new Map([
168
+ "pStyle",
169
+ "keepNext",
170
+ "keepLines",
171
+ "pageBreakBefore",
172
+ "framePr",
173
+ "widowControl",
174
+ "numPr",
175
+ "suppressLineNumbers",
176
+ "pBdr",
177
+ "shd",
178
+ "tabs",
179
+ "suppressAutoHyphens",
180
+ "kinsoku",
181
+ "wordWrap",
182
+ "overflowPunct",
183
+ "topLinePunct",
184
+ "autoSpaceDE",
185
+ "autoSpaceDN",
186
+ "bidi",
187
+ "adjustRightInd",
188
+ "snapToGrid",
189
+ "spacing",
190
+ "ind",
191
+ "contextualSpacing",
192
+ "mirrorIndents",
193
+ "suppressOverlap",
194
+ "jc",
195
+ "textDirection",
196
+ "textAlignment",
197
+ "textboxTightWrap",
198
+ "outlineLvl",
199
+ "divId",
200
+ "cnfStyle",
201
+ "rPr"
202
+ ].map((name, index) => [name, index]));
203
+ const PARAGRAPH_MARK_BASE_CHILDREN = /* @__PURE__ */ new Set([
204
+ "rStyle",
205
+ "rFonts",
206
+ "b",
207
+ "bCs",
208
+ "i",
209
+ "iCs",
210
+ "caps",
211
+ "smallCaps",
212
+ "strike",
213
+ "dstrike",
214
+ "outline",
215
+ "shadow",
216
+ "emboss",
217
+ "imprint",
218
+ "snapToGrid",
219
+ "vanish",
220
+ "webHidden",
221
+ "color",
222
+ "spacing",
223
+ "w",
224
+ "kern",
225
+ "position",
226
+ "sz",
227
+ "szCs",
228
+ "noProof",
229
+ "highlight",
230
+ "u",
231
+ "effect",
232
+ "bdr",
233
+ "shd",
234
+ "fitText",
235
+ "vertAlign",
236
+ "rtl",
237
+ "cs",
238
+ "em",
239
+ "lang",
240
+ "eastAsianLayout",
241
+ "specVanish",
242
+ "oMath"
243
+ ]);
244
+ const PARAGRAPH_NESTED_PROPERTY_CHILDREN = /* @__PURE__ */ new Map([
245
+ ["numPr", /* @__PURE__ */ new Set(["ilvl", "numId"])],
246
+ ["pBdr", /* @__PURE__ */ new Set([
247
+ "top",
248
+ "left",
249
+ "bottom",
250
+ "right",
251
+ "between",
252
+ "bar"
253
+ ])],
254
+ ["tabs", /* @__PURE__ */ new Set(["tab"])],
255
+ ["rPr", PARAGRAPH_MARK_BASE_CHILDREN]
256
+ ]);
257
+ const resolveNamespaceBinding = (scope, prefix) => {
258
+ for (let current = scope; current; current = current.parent) {
259
+ const value = current.bindings.get(prefix);
260
+ if (value !== void 0) return value;
261
+ }
262
+ };
263
+ const hasDescendantNamed = (element, namespaceUri, localName) => {
264
+ const pending = [...getChildElements(element)];
265
+ while (pending.length > 0) {
266
+ const child = pending.pop();
267
+ if (!child) continue;
268
+ if (getLocalName(child.name) === localName && toTransitionalNamespaceUri(child.namespaceUri ?? "") === namespaceUri) return true;
269
+ for (const descendant of getChildElements(child)) pending.push(descendant);
270
+ }
271
+ return false;
272
+ };
273
+ const hasInvalidParagraphPropertyRevision = (root) => {
274
+ const pending = [...getChildElements(root)];
275
+ while (pending.length > 0) {
276
+ const element = pending.pop();
277
+ if (!element) continue;
278
+ if (toTransitionalNamespaceUri(element.namespaceUri ?? "") === NAMESPACES.w) {
279
+ const localName = getLocalName(element.name);
280
+ if (RESERVED_PARAGRAPH_CAPTURE_CHILDREN.has(localName)) return true;
281
+ }
282
+ for (const child of getChildElements(element)) pending.push(child);
283
+ }
284
+ return false;
285
+ };
286
+ const hasInvalidParagraphPropertyShape = (root) => {
287
+ const pending = getChildElements(root).map((element) => ({
288
+ element,
289
+ parent: root
290
+ }));
291
+ if ((root.elements ?? []).some((child) => child.type === "text" && String(child.text ?? "").trim() !== "")) return true;
292
+ while (pending.length > 0) {
293
+ const entry = pending.pop();
294
+ if (!entry) continue;
295
+ const { element, parent } = entry;
296
+ if ((element.elements ?? []).some((child) => child.type === "text" && String(child.text ?? "").trim() !== "")) return true;
297
+ if (toTransitionalNamespaceUri(element.namespaceUri ?? "") === NAMESPACES.w && parent !== root) {
298
+ if (toTransitionalNamespaceUri(parent.namespaceUri ?? "") !== NAMESPACES.w) return true;
299
+ if (!PARAGRAPH_NESTED_PROPERTY_CHILDREN.get(getLocalName(parent.name))?.has(getLocalName(element.name))) return true;
300
+ }
301
+ for (const child of getChildElements(element)) pending.push({
302
+ element: child,
303
+ parent: element
304
+ });
305
+ }
306
+ return false;
307
+ };
308
+ const sourceShadowsDateUtcPrefix = (sourceXml) => {
309
+ const root = parseXml(sourceXml, OOXML_NAMESPACE_SCOPE).elements?.at(0);
310
+ if (!root || root.type !== "element") return true;
311
+ const pending = [root];
312
+ while (pending.length > 0) {
313
+ const element = pending.pop();
314
+ if (!element) continue;
315
+ const resolved = resolveNamespaceBinding(element.namespaceScope, "w16du");
316
+ if (resolved !== void 0 && resolved !== "http://schemas.microsoft.com/office/word/2023/wordml/word16du") return true;
317
+ for (const child of getChildElements(element)) pending.push(child);
318
+ }
319
+ return false;
320
+ };
321
+ const hasInvalidParagraphMarkProperties = (root) => {
322
+ for (const child of getChildElements(root)) {
323
+ if (toTransitionalNamespaceUri(child.namespaceUri ?? "") !== NAMESPACES.w) continue;
324
+ const localName = getLocalName(child.name);
325
+ if (!PARAGRAPH_MARK_BASE_CHILDREN.has(localName)) return true;
326
+ }
327
+ return false;
328
+ };
329
+ const replayableParagraphPropertySourceXml = (sourceXml) => {
330
+ return sanitizeCapturedXmlElement(sourceXml, {
331
+ allowedLocalNames: PARAGRAPH_PROPERTY_ROOT_NAME,
332
+ allowedNamespaceUris: WORDPROCESSINGML_NAMESPACE,
333
+ inheritedNamespaceScope: OOXML_NAMESPACE_SCOPE,
334
+ requiredNamespaceBindings: PARAGRAPH_APPEND_PREFIXES,
335
+ validate: (root) => {
336
+ let paragraphMarkProperties = null;
337
+ const seenChildren = /* @__PURE__ */ new Set();
338
+ let previousChildOrder = -1;
339
+ for (const child of getChildElements(root)) {
340
+ const localName = getLocalName(child.name);
341
+ const isWordprocessingChild = toTransitionalNamespaceUri(child.namespaceUri ?? "") === NAMESPACES.w;
342
+ if (isWordprocessingChild) {
343
+ if (RESERVED_PARAGRAPH_PROPERTY_CHILDREN.has(localName)) return false;
344
+ const childOrder = PARAGRAPH_PROPERTY_CHILD_ORDER.get(localName);
345
+ if (childOrder === void 0 || seenChildren.has(localName) || childOrder < previousChildOrder) return false;
346
+ seenChildren.add(localName);
347
+ previousChildOrder = childOrder;
348
+ }
349
+ if (localName !== "rPr" || !isWordprocessingChild) {
350
+ if (hasDescendantNamed(child, NAMESPACES.w, "rPr")) return false;
351
+ continue;
352
+ }
353
+ if (paragraphMarkProperties !== null || toTransitionalNamespaceUri(resolveNamespaceBinding(child.namespaceScope, "w") ?? "") !== NAMESPACES.w) return false;
354
+ paragraphMarkProperties = child;
355
+ }
356
+ return !(paragraphMarkProperties && hasInvalidParagraphMarkProperties(paragraphMarkProperties) || hasInvalidParagraphPropertyShape(root) || hasInvalidParagraphPropertyRevision(root));
357
+ }
358
+ });
359
+ };
360
+ const verifiedParagraphPropertySource = (formatting, source) => {
361
+ if (!source) return null;
362
+ const replayableSource = replayableParagraphPropertySourceXml(source.xml);
363
+ if (replayableSource === null) return null;
364
+ return canonicalJson(formatting ?? {}) === source.formattingJson ? replayableSource : null;
365
+ };
366
+ const withTrailingParagraphPropertyChildren = (sourceXml, children) => {
367
+ const written = children.join("");
368
+ if (written.length === 0) return sourceXml;
369
+ const selfClosing = /^<((?:[A-Za-z_][\w.-]*:)?pPr)\b([^>]*)\/>$/u.exec(sourceXml);
370
+ if (selfClosing) {
371
+ const [, name, attrs] = selfClosing;
372
+ return `<${name}${attrs}>${written}</${name}>`;
373
+ }
374
+ const close = sourceXml.lastIndexOf("</");
375
+ return close === -1 ? sourceXml : `${sourceXml.slice(0, close)}${written}${sourceXml.slice(close)}`;
376
+ };
377
+ const withParagraphMarkChange = (sourceXml, mark) => {
378
+ const root = parseXml(sourceXml, OOXML_NAMESPACE_SCOPE).elements?.at(0);
379
+ if (!root || root.type !== "element") panic("A validated paragraph-property capture could not be parsed for composition");
380
+ const attributes = trackedChangeAttributeRecord(mark.info);
381
+ const markElement = cloneElement(root, {
382
+ name: `w:${mark.kind}`,
383
+ attributes,
384
+ elements: []
385
+ });
386
+ const elements = [...root.elements ?? []];
387
+ const paragraphMarkIndex = elements.findIndex((child) => child.type === "element" && getLocalName(child.name) === "rPr" && toTransitionalNamespaceUri(child.namespaceUri ?? "") === NAMESPACES.w);
388
+ if (paragraphMarkIndex === -1) elements.push(cloneElement(root, {
389
+ name: "w:rPr",
390
+ attributes: {},
391
+ elements: [markElement]
392
+ }));
393
+ else {
394
+ const paragraphMarkProperties = elements[paragraphMarkIndex];
395
+ if (!paragraphMarkProperties || paragraphMarkProperties.type !== "element") panic("A validated paragraph-mark property node disappeared during composition");
396
+ elements[paragraphMarkIndex] = cloneElement(paragraphMarkProperties, { elements: [markElement, ...paragraphMarkProperties.elements ?? []] });
397
+ }
398
+ return captureVerbatimXml(cloneElement(root, { elements }));
399
+ };
400
+ const serializeParagraphFormattingWithOptions = (formatting, { propertyChanges, paragraphMarkChange, propertySource, sectionProperties } = {}) => {
401
+ const paragraphMarkXml = paragraphMarkChange ? serializeParagraphMarkChange(paragraphMarkChange) : "";
402
+ const sectionPropertiesXml = serializeSectionProperties(sectionProperties);
403
+ const propertyChangesXml = (propertyChanges ?? []).map((change) => serializeParagraphPropertyChange(change));
404
+ const composedChildrenUseDateUtc = [
405
+ paragraphMarkXml,
406
+ sectionPropertiesXml,
407
+ ...propertyChangesXml
408
+ ].some((xml) => xml.includes(`${DATE_UTC_ATTRIBUTE}=`));
409
+ const verifiedSource = verifiedParagraphPropertySource(formatting, propertySource);
410
+ if (verifiedSource !== null && (!composedChildrenUseDateUtc || !sourceShadowsDateUtcPrefix(verifiedSource))) {
411
+ const sourceWithMark = paragraphMarkChange ? withParagraphMarkChange(verifiedSource, paragraphMarkChange) : verifiedSource;
412
+ return withTrailingParagraphPropertyChildren(sourceWithMark, [sectionPropertiesXml, ...propertyChangesXml]);
413
+ }
144
414
  const parts = [];
145
415
  const pushToggle = (name, value) => {
146
416
  if (value === true) parts.push(`<w:${name}/>`);
@@ -156,13 +426,13 @@ const serializeParagraphFormattingWithOptions = (formatting, { propertyChanges,
156
426
  pushToggle("widowControl", formatting.widowControl);
157
427
  const numPrXml = formatting.numPrFromStyle != null && numPrEqual(formatting.numPr, formatting.numPrFromStyle) ? "" : serializeNumbering(formatting.numPr);
158
428
  if (numPrXml) parts.push(numPrXml);
429
+ pushToggle("suppressLineNumbers", formatting.suppressLineNumbers);
159
430
  const bordersXml = serializeParagraphBorders(formatting.borders);
160
431
  if (bordersXml) parts.push(bordersXml);
161
432
  const shadingXml = serializeShading(formatting.shading);
162
433
  if (shadingXml) parts.push(shadingXml);
163
434
  const tabsXml = serializeTabStops(formatting.tabs);
164
435
  if (tabsXml) parts.push(tabsXml);
165
- pushToggle("suppressLineNumbers", formatting.suppressLineNumbers);
166
436
  pushToggle("suppressAutoHyphens", formatting.suppressAutoHyphens);
167
437
  pushToggle("kinsoku", formatting.kinsoku);
168
438
  pushToggle("overflowPunct", formatting.overflowPunctuation);
@@ -176,12 +446,12 @@ const serializeParagraphFormattingWithOptions = (formatting, { propertyChanges,
176
446
  if (formatting.alignment) parts.push(`<w:jc w:val="${formatting.alignment}"/>`);
177
447
  if (formatting.outlineLevel !== void 0) parts.push(`<w:outlineLvl w:val="${formatting.outlineLevel}"/>`);
178
448
  if (paragraphMarkChange || formatting.runProperties || formatting.runInWithNext) {
179
- const fullInner = `${paragraphMarkChange ? serializeParagraphMarkChange(paragraphMarkChange) : ""}${formatting.runProperties ? extractRPrInner(serializeTextFormatting(formatting.runProperties)) : ""}${formatting.runInWithNext ? "<w:specVanish/>" : ""}`;
449
+ const fullInner = `${paragraphMarkChange ? paragraphMarkXml : ""}${formatting.runProperties ? extractRPrInner(serializeTextFormatting(formatting.runProperties)) : ""}${formatting.runInWithNext ? "<w:specVanish/>" : ""}`;
180
450
  if (fullInner.length > 0) parts.push(`<w:rPr>${fullInner}</w:rPr>`);
181
451
  }
182
- } else if (paragraphMarkChange) parts.push(`<w:rPr>${serializeParagraphMarkChange(paragraphMarkChange)}</w:rPr>`);
183
- parts.push(serializeSectionProperties(sectionProperties));
184
- if (propertyChanges && propertyChanges.length > 0) parts.push(...propertyChanges.map((change) => serializeParagraphPropertyChange(change)));
452
+ } else if (paragraphMarkChange) parts.push(`<w:rPr>${paragraphMarkXml}</w:rPr>`);
453
+ parts.push(sectionPropertiesXml);
454
+ if (propertyChangesXml.length > 0) parts.push(...propertyChangesXml);
185
455
  const inner = parts.join("");
186
456
  if (inner.length === 0) return "";
187
457
  return `<w:pPr>${inner}</w:pPr>`;
@@ -206,17 +476,9 @@ function extractRPrInner(rPrXml) {
206
476
  return rPrXml.slice(7, -8);
207
477
  }
208
478
  function serializeParagraphPropertyChange(change) {
209
- const normalizedId = normalizeRevisionId(change.info.id);
210
- const authorCandidate = typeof change.info.author === "string" ? change.info.author.trim() : "";
211
- const normalizedAuthor = authorCandidate.length > 0 ? authorCandidate : "Unknown";
212
- const normalizedDate = typeof change.info.date === "string" ? change.info.date.trim() : void 0;
213
- const normalizedRsid = typeof change.info.rsid === "string" ? change.info.rsid.trim() : void 0;
214
- const attrs = [`w:id="${normalizedId}"`, `w:author="${escapeXml(normalizedAuthor)}"`];
215
- if (normalizedDate) attrs.push(`w:date="${escapeXml(normalizedDate)}"`);
216
- if (normalizedRsid) attrs.push(`w:rsid="${escapeXml(normalizedRsid)}"`);
217
479
  const previousPPrInner = extractPPrInner(serializeParagraphFormatting(change.previousFormatting) || "<w:pPr/>");
218
480
  const normalizedPreviousPPr = previousPPrInner.length > 0 ? `<w:pPr>${previousPPrInner}</w:pPr>` : "<w:pPr/>";
219
- return `<w:pPrChange ${attrs.join(" ")}>${normalizedPreviousPPr}</w:pPrChange>`;
481
+ return `<w:pPrChange ${serializeTrackedChangeAttributes(change.info)}>${normalizedPreviousPPr}</w:pPrChange>`;
220
482
  }
221
483
  /** The attribute list of a `w:hyperlink`, without its children. */
222
484
  function hyperlinkAttributes(hyperlink) {
@@ -390,13 +652,7 @@ function trackedChangeTag(change) {
390
652
  }
391
653
  }
392
654
  function serializeTrackedChange(tag, change) {
393
- const info = change.info;
394
- const normalizedId = normalizeRevisionId(info.id);
395
- const authorCandidate = typeof info.author === "string" ? info.author.trim() : "";
396
- const normalizedAuthor = authorCandidate.length > 0 ? authorCandidate : "Unknown";
397
- const normalizedDate = typeof info.date === "string" ? info.date.trim() : void 0;
398
- const attrs = [`w:id="${normalizedId}"`, `w:author="${escapeXml(normalizedAuthor)}"`];
399
- if (normalizedDate) attrs.push(`w:date="${escapeXml(normalizedDate)}"`);
655
+ const attrs = serializeTrackedChangeAttributes(change.info);
400
656
  const serializeDeletedRun = (run) => {
401
657
  const xml = serializeRun(run);
402
658
  if (!run.content.some((c) => c.type === "drawing" || c.type === "shape")) return rewriteRunTextAsDeleted(xml);
@@ -420,7 +676,7 @@ function serializeTrackedChange(tag, change) {
420
676
  if (item.type === "insertion" || item.type === "deletion" || item.type === "moveFrom" || item.type === "moveTo") return serializeTrackedChange(trackedChangeTag(item), item);
421
677
  return item.type === "bookmarkStart" ? serializeBookmarkStart(item) : serializeBookmarkEnd(item);
422
678
  };
423
- const open = `<w:${tag} ${attrs.join(" ")}>`;
679
+ const open = `<w:${tag} ${attrs}>`;
424
680
  const close = `</w:${tag}>`;
425
681
  const wrap = (inner) => inner.length === 0 ? "" : `${open}${inner}${close}`;
426
682
  if (change.content.length === 0) return `${open}${close}`;
@@ -497,6 +753,7 @@ function serializeParagraph(paragraph) {
497
753
  parts.push(serializeParagraphFormattingWithOptions(paragraph.formatting, {
498
754
  propertyChanges: paragraph.propertyChanges,
499
755
  paragraphMarkChange: paragraph.pPrMark,
756
+ propertySource: getParagraphPropertySource(paragraph),
500
757
  sectionProperties: paragraph.sectionProperties
501
758
  }));
502
759
  const explicitCommentReferenceIds = /* @__PURE__ */ new Set();
@@ -5,9 +5,9 @@ import { THEME_COLOR_TO_DRAWING_SCHEME } from "../drawingUtils.js";
5
5
  import { requiresXmlSpacePreserve } from "../textWhitespace.js";
6
6
  import { serializeParagraph } from "./paragraphSerializer.js";
7
7
  import { serializeTable } from "./tableSerializer.js";
8
+ import { serializeTrackedChangeAttributes } from "./trackedChangeAttributes.js";
8
9
  import { escapeXml, intAttr } from "./xmlUtils.js";
9
10
  import { panic } from "better-result";
10
- import { normalizeRevisionId } from "@stll/docx-core/model";
11
11
  //#region src/docx/serializer/runSerializer.ts
12
12
  /**
13
13
  * Run Serializer - Serialize runs to OOXML XML
@@ -169,16 +169,8 @@ function extractRPrInner(rPrXml) {
169
169
  return rPrXml.slice(7, -8);
170
170
  }
171
171
  function serializeRunPropertyChange(change) {
172
- const normalizedId = normalizeRevisionId(change.info.id);
173
- const authorCandidate = typeof change.info.author === "string" ? change.info.author.trim() : "";
174
- const normalizedAuthor = authorCandidate.length > 0 ? authorCandidate : "Unknown";
175
- const normalizedDate = typeof change.info.date === "string" ? change.info.date.trim() : void 0;
176
- const normalizedRsid = typeof change.info.rsid === "string" ? change.info.rsid.trim() : void 0;
177
- const attrs = [`w:id="${normalizedId}"`, `w:author="${escapeXml(normalizedAuthor)}"`];
178
- if (normalizedDate) attrs.push(`w:date="${escapeXml(normalizedDate)}"`);
179
- if (normalizedRsid) attrs.push(`w:rsid="${escapeXml(normalizedRsid)}"`);
180
172
  const previousRPrXml = serializeTextFormatting(change.previousFormatting) || "<w:rPr/>";
181
- return `<w:rPrChange ${attrs.join(" ")}>${previousRPrXml}</w:rPrChange>`;
173
+ return `<w:rPrChange ${serializeTrackedChangeAttributes(change.info)}>${previousRPrXml}</w:rPrChange>`;
182
174
  }
183
175
  function serializeRunProperties(formatting, propertyChanges) {
184
176
  const currentRPrXml = serializeTextFormatting(formatting);
@@ -1,7 +1,7 @@
1
1
  import { getUnserializedSectionPropertyChildNames } from "../sectionParser.js";
2
2
  import { serializeBorder } from "./borderSerializer.js";
3
+ import { serializeTrackedChangeAttributes } from "./trackedChangeAttributes.js";
3
4
  import { escapeXml, intAttr } from "./xmlUtils.js";
4
- import { normalizeRevisionId } from "@stll/docx-core/model";
5
5
  //#region src/docx/serializer/sectionPropertiesSerializer.ts
6
6
  const serializeHeaderReference = (ref) => `<w:headerReference w:type="${ref.type}" r:id="${escapeXml(ref.rId)}"/>`;
7
7
  const serializeFooterReference = (ref) => `<w:footerReference w:type="${ref.type}" r:id="${escapeXml(ref.rId)}"/>`;
@@ -133,16 +133,8 @@ function serializeOnOffElement(value, name) {
133
133
  return value ? `<w:${name}/>` : `<w:${name} w:val="0"/>`;
134
134
  }
135
135
  function serializeSectionPropertyChange(change) {
136
- const normalizedId = normalizeRevisionId(change.info.id);
137
- const authorCandidate = typeof change.info.author === "string" ? change.info.author.trim() : "";
138
- const normalizedAuthor = authorCandidate.length > 0 ? authorCandidate : "Unknown";
139
- const normalizedDate = typeof change.info.date === "string" ? change.info.date.trim() : void 0;
140
- const normalizedRsid = typeof change.info.rsid === "string" ? change.info.rsid.trim() : void 0;
141
- const attrs = [`w:id="${normalizedId}"`, `w:author="${escapeXml(normalizedAuthor)}"`];
142
- if (normalizedDate) attrs.push(`w:date="${escapeXml(normalizedDate)}"`);
143
- if (normalizedRsid) attrs.push(`w:rsid="${escapeXml(normalizedRsid)}"`);
144
136
  const previousSectPrXml = serializeSectionProperties(change.previousProperties) || "<w:sectPr/>";
145
- return `<w:sectPrChange ${attrs.join(" ")}>${previousSectPrXml}</w:sectPrChange>`;
137
+ return `<w:sectPrChange ${serializeTrackedChangeAttributes(change.info)}>${previousSectPrXml}</w:sectPrChange>`;
146
138
  }
147
139
  function serializeSectionProperties(props) {
148
140
  if (!props) return "";
@@ -3,8 +3,8 @@ import { isValidHexColor } from "../../utils/colorResolver.js";
3
3
  import { parseTableCellProperties, parseTableGrid, parseTableProperties, parseTableRowProperties } from "../tableParser.js";
4
4
  import { OOXML_NAMESPACE_SCOPE, parseXml } from "../xmlParser.js";
5
5
  import { serializeBorder } from "./borderSerializer.js";
6
+ import { serializeTrackedChangeAttributes } from "./trackedChangeAttributes.js";
6
7
  import { escapeXml, intAttr } from "./xmlUtils.js";
7
- import { normalizeRevisionId } from "@stll/docx-core/model";
8
8
  //#region src/docx/serializer/tableSerializer.ts
9
9
  /**
10
10
  * The captured element with the revision children written back into it.
@@ -69,25 +69,6 @@ const verifiedSourceXml = (formatting, parse) => {
69
69
  const reparsed = parse(parseXml(sourceXml, OOXML_NAMESPACE_SCOPE).elements?.[0] ?? null);
70
70
  return canonicalJson(withoutCaptures(reparsed)) === canonicalJson(withoutCaptures(formatting)) ? sourceXml : null;
71
71
  };
72
- function normalizeTrackedChangeInfo(info) {
73
- const normalizedId = normalizeRevisionId(info.id);
74
- const authorCandidate = typeof info.author === "string" ? info.author.trim() : "";
75
- const normalizedAuthor = authorCandidate.length > 0 ? authorCandidate : "Unknown";
76
- const normalizedDate = typeof info.date === "string" ? info.date.trim() : void 0;
77
- return {
78
- id: normalizedId,
79
- author: normalizedAuthor,
80
- ...normalizedDate !== void 0 ? { date: normalizedDate } : {}
81
- };
82
- }
83
- function serializeTrackedChangeAttributes(info, rsid) {
84
- const normalized = normalizeTrackedChangeInfo(info);
85
- const attrs = [`w:id="${normalized.id}"`, `w:author="${escapeXml(normalized.author)}"`];
86
- if (normalized.date) attrs.push(`w:date="${escapeXml(normalized.date)}"`);
87
- if (info.utcDate) attrs.push(`${info.utcDate.attribute}="${escapeXml(info.utcDate.value)}"`);
88
- if (rsid && rsid.trim().length > 0) attrs.push(`w:rsid="${escapeXml(rsid.trim())}"`);
89
- return attrs.join(" ");
90
- }
91
72
  /**
92
73
  * Serialize a table measurement (width, height)
93
74
  */
@@ -231,7 +212,7 @@ function extractTblPrInner(tblPrXml) {
231
212
  return tblPrXml.slice(9, -10);
232
213
  }
233
214
  function serializeTablePropertyChange(change) {
234
- const attrs = serializeTrackedChangeAttributes(change.info, change.info.rsid);
215
+ const attrs = serializeTrackedChangeAttributes(change.info);
235
216
  const previousTblPrInner = extractTblPrInner(serializeTableFormatting(change.previousFormatting) || "<w:tblPr/>");
236
217
  return `<w:tblPrChange ${attrs}>${previousTblPrInner.length > 0 ? `<w:tblPr>${previousTblPrInner}</w:tblPr>` : "<w:tblPr/>"}</w:tblPrChange>`;
237
218
  }
@@ -269,7 +250,7 @@ function extractTrPrInner(trPrXml) {
269
250
  return trPrXml.slice(8, -9);
270
251
  }
271
252
  function serializeTableRowPropertyChange(change) {
272
- const attrs = serializeTrackedChangeAttributes(change.info, change.info.rsid);
253
+ const attrs = serializeTrackedChangeAttributes(change.info);
273
254
  const previousTrPrInner = extractTrPrInner(serializeTableRowFormatting(change.previousFormatting) || "<w:trPr/>");
274
255
  return `<w:trPrChange ${attrs}>${previousTrPrInner.length > 0 ? `<w:trPr>${previousTrPrInner}</w:trPr>` : "<w:trPr/>"}</w:trPrChange>`;
275
256
  }
@@ -333,7 +314,7 @@ function extractTcPrInner(tcPrXml) {
333
314
  return tcPrXml.slice(8, -9);
334
315
  }
335
316
  function serializeTableCellPropertyChange(change) {
336
- const attrs = serializeTrackedChangeAttributes(change.info, change.info.rsid);
317
+ const attrs = serializeTrackedChangeAttributes(change.info);
337
318
  const previousTcPrInner = extractTcPrInner(serializeTableCellFormatting(change.previousFormatting) || "<w:tcPr/>");
338
319
  return `<w:tcPrChange ${attrs}>${previousTcPrInner.length > 0 ? `<w:tcPr>${previousTcPrInner}</w:tcPr>` : "<w:tcPr/>"}</w:tcPrChange>`;
339
320
  }
@@ -0,0 +1,9 @@
1
+ import { document_d_exports } from "../../types/document.js";
2
+ //#region src/docx/serializer/trackedChangeAttributes.d.ts
3
+ type SerializableTrackedChangeInfo = document_d_exports.TrackedChangeInfo | document_d_exports.PropertyChangeInfo;
4
+ /** Normalized, unescaped attributes in schema order for an XML element or string serializer. */
5
+ declare const trackedChangeAttributeEntries: (info: SerializableTrackedChangeInfo) => readonly (readonly [string, string])[];
6
+ declare const trackedChangeAttributeRecord: (info: SerializableTrackedChangeInfo) => Record<string, string>;
7
+ declare const serializeTrackedChangeAttributes: (info: SerializableTrackedChangeInfo) => string;
8
+ //#endregion
9
+ export { serializeTrackedChangeAttributes, trackedChangeAttributeEntries, trackedChangeAttributeRecord };
@@ -0,0 +1,21 @@
1
+ import { DATE_UTC_ATTRIBUTE } from "../trackedChangeInfo.js";
2
+ import { escapeXml } from "./xmlUtils.js";
3
+ import { normalizeRevisionId } from "@stll/docx-core/model";
4
+ //#region src/docx/serializer/trackedChangeAttributes.ts
5
+ /** Normalized, unescaped attributes in schema order for an XML element or string serializer. */
6
+ const trackedChangeAttributeEntries = (info) => {
7
+ const author = typeof info.author === "string" ? info.author.trim() : "";
8
+ const date = typeof info.date === "string" ? info.date.trim() : "";
9
+ const rawUtcDate = info.utcDate;
10
+ const utcDate = typeof rawUtcDate === "object" && rawUtcDate !== null && "value" in rawUtcDate && typeof rawUtcDate.value === "string" ? rawUtcDate.value.trim() : "";
11
+ const rsid = "rsid" in info && typeof info.rsid === "string" ? info.rsid.trim() : "";
12
+ const entries = [["w:id", String(normalizeRevisionId(info.id))], ["w:author", author.length > 0 ? author : "Unknown"]];
13
+ if (date.length > 0) entries.push(["w:date", date]);
14
+ if (utcDate.length > 0) entries.push([DATE_UTC_ATTRIBUTE, utcDate]);
15
+ if (rsid.length > 0) entries.push(["w:rsid", rsid]);
16
+ return entries;
17
+ };
18
+ const trackedChangeAttributeRecord = (info) => Object.fromEntries(trackedChangeAttributeEntries(info));
19
+ const serializeTrackedChangeAttributes = (info) => trackedChangeAttributeEntries(info).map(([name, value]) => `${name}="${escapeXml(value)}"`).join(" ");
20
+ //#endregion
21
+ export { serializeTrackedChangeAttributes, trackedChangeAttributeEntries, trackedChangeAttributeRecord };
@@ -1,5 +1,6 @@
1
1
  import { deterministicHexId } from "../../utils/hexId.js";
2
2
  import { getParagraphText } from "../paragraphParser.js";
3
+ import { cloneParagraphWithoutPropertySource } from "../paragraphPropertySource.js";
3
4
  import { TaggedError } from "better-result";
4
5
  //#region src/docx/server/createBilingualDocument.ts
5
6
  /**
@@ -365,7 +366,6 @@ const createStyleCloner = ({ styleById, suffix, cloner }) => {
365
366
  };
366
367
  };
367
368
  const cloneParagraphForTarget = (paragraph, targetParaId, styleCloner, cloner, bookmarkIds) => {
368
- const { textId: _textId, sectionProperties: _sectionProperties, ...rest } = paragraph;
369
369
  const formatting = paragraph.formatting;
370
370
  const nextFormatting = formatting && {
371
371
  ...formatting,
@@ -379,13 +379,15 @@ const cloneParagraphForTarget = (paragraph, targetParaId, styleCloner, cloner, b
379
379
  numId: cloner.cloneNumId(formatting.numPrFromStyle.numId)
380
380
  } }
381
381
  };
382
- return {
383
- ...rest,
382
+ const cloned = cloneParagraphWithoutPropertySource(paragraph, {
384
383
  content: remapClonedBookmarkIds(structuredClone(paragraph.content), bookmarkIds),
385
384
  paraId: targetParaId,
386
385
  ...nextFormatting && { formatting: nextFormatting },
387
386
  ...paragraph.listRendering && { listRendering: remapListRendering(paragraph.listRendering, cloner) }
388
- };
387
+ });
388
+ Reflect.deleteProperty(cloned, "textId");
389
+ Reflect.deleteProperty(cloned, "sectionProperties");
390
+ return cloned;
389
391
  };
390
392
  const createBookmarkIdMinter = (source) => {
391
393
  let nextId = 0;
@@ -526,7 +528,7 @@ const projectParagraphIntoColumn = (paragraph, geometry, sourceWidth, columnWidt
526
528
  indentLeft = minimumLeftIndent + Math.round(flexibleLeft / flexibleTotal * flexibleMaximum);
527
529
  indentRight = maxSideIndent - indentLeft;
528
530
  }
529
- const formatting = {
531
+ return cloneParagraphWithoutPropertySource(paragraph, { formatting: {
530
532
  ...paragraph.formatting,
531
533
  ...(geometry.indentLeft !== void 0 || minimumLeftIndent > 0) && { indentLeft },
532
534
  ...geometry.indentRight !== void 0 && { indentRight },
@@ -536,11 +538,7 @@ const projectParagraphIntoColumn = (paragraph, geometry, sourceWidth, columnWidt
536
538
  ...tab,
537
539
  position: Math.min(Math.max(0, Math.round(tab.position * scale)), Math.max(0, columnWidth - MIN_TAB_TRAILING_WIDTH_TWIPS))
538
540
  })) }
539
- };
540
- return {
541
- ...paragraph,
542
- formatting
543
- };
541
+ } });
544
542
  };
545
543
  const projectSideIndent = (value, scale, maximum) => Math.min(Math.max(0, Math.round((value ?? 0) * scale)), maximum);
546
544
  const buildCell = (paragraph) => ({
@@ -29,6 +29,20 @@ const FOLIO_DOCX_XML_PATCH_PROPOSAL_ISSUE_CODES = Object.freeze([
29
29
  "base-hash-mismatch",
30
30
  "package-inspection-failed"
31
31
  ]);
32
+ const XML_SAFETY_ISSUE = {
33
+ "doctype-forbidden": {
34
+ code: "xml-doctype-forbidden",
35
+ message: "Replacement XML must not declare a document type."
36
+ },
37
+ "entity-forbidden": {
38
+ code: "xml-not-well-formed",
39
+ message: "Replacement XML contains an invalid entity reference."
40
+ },
41
+ "not-well-formed": {
42
+ code: "xml-not-well-formed",
43
+ message: "Replacement XML must be well formed."
44
+ }
45
+ };
32
46
  var InvalidFolioDocxXmlPatchProposalError = class extends TaggedError("InvalidFolioDocxXmlPatchProposalError") {};
33
47
  var InvalidFolioDocxXmlPatchProposalOptionsError = class extends TaggedError("InvalidFolioDocxXmlPatchProposalOptionsError") {};
34
48
  const SHA256_PATTERN = /^[\da-f]{64}$/u;
@@ -271,19 +285,14 @@ const evaluateDocxXmlPatchProposal = async ({ bytes, proposal: rawProposal, allo
271
285
  if (exceedsEncodedPart || totalLimitReported) continue;
272
286
  encodedByPath.set(replacement.path, replacementBytes);
273
287
  const safetyIssue = getDocxXmlSafetyIssue(replacement.replacementXml);
274
- if (safetyIssue === "doctype-forbidden") issues.push({
275
- code: "xml-doctype-forbidden",
276
- message: "Replacement XML must not declare a document type.",
277
- proposalPath: `${proposalPath}.replacementXml`,
278
- part: replacement.path
279
- });
280
- else if (safetyIssue === "not-well-formed") issues.push({
281
- code: "xml-not-well-formed",
282
- message: "Replacement XML must be well formed.",
283
- proposalPath: `${proposalPath}.replacementXml`,
284
- part: replacement.path
285
- });
286
- else {
288
+ if (safetyIssue !== null) {
289
+ const issue = XML_SAFETY_ISSUE[safetyIssue];
290
+ issues.push({
291
+ ...issue,
292
+ proposalPath: `${proposalPath}.replacementXml`,
293
+ part: replacement.path
294
+ });
295
+ } else {
287
296
  const declaredEncoding = replacement.replacementXml.match(XML_DECLARED_ENCODING_PATTERN)?.at(1);
288
297
  if (declaredEncoding !== void 0 && declaredEncoding.toLowerCase() !== "utf-8" && declaredEncoding.toLowerCase() !== "utf8") issues.push({
289
298
  code: "xml-encoding-mismatch",