@stll/folio-core 0.54.1 → 0.55.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 (251) hide show
  1. package/dist/ai-edits/apply.d.ts +1 -1
  2. package/dist/ai-edits/apply.js +40 -32
  3. package/dist/ai-edits/headless.d.ts +1 -2
  4. package/dist/ai-edits/headless.js +13 -27
  5. package/dist/ai-edits/read.d.ts +1 -0
  6. package/dist/ai-edits/read.js +7 -2
  7. package/dist/ai-edits/table-mutation-plan.d.ts +2 -1
  8. package/dist/ai-edits/table-mutation-plan.js +39 -1
  9. package/dist/ai-suggestions/apply.d.ts +1 -1
  10. package/dist/ai-suggestions/apply.js +3 -2
  11. package/dist/compare/compare.js +2 -1
  12. package/dist/compare/content-types.d.ts +3 -15
  13. package/dist/controller/canonicalComposition.d.ts +36 -0
  14. package/dist/controller/canonicalComposition.js +168 -0
  15. package/dist/controller/canonicalInput.d.ts +45 -0
  16. package/dist/controller/canonicalInput.js +338 -0
  17. package/dist/controller/canonicalOperations.d.ts +22 -0
  18. package/dist/controller/canonicalOperations.js +85 -0
  19. package/dist/controller/canonicalReview.d.ts +12 -0
  20. package/dist/controller/canonicalReview.js +19 -0
  21. package/dist/controller/canonicalSession.d.ts +149 -0
  22. package/dist/controller/canonicalSession.js +1038 -0
  23. package/dist/controller/canonicalStoryEditor.d.ts +34 -0
  24. package/dist/controller/canonicalStoryEditor.js +82 -0
  25. package/dist/controller/canonicalStructure.d.ts +16 -0
  26. package/dist/controller/canonicalStructure.js +269 -0
  27. package/dist/controller/folioEditor.d.ts +1 -1
  28. package/dist/controller/folioEditor.js +9 -0
  29. package/dist/controller/headerFooterEditorManager.d.ts +4 -0
  30. package/dist/controller/headerFooterEditorManager.js +71 -3
  31. package/dist/controller/hiddenEditorApi.d.ts +35 -0
  32. package/dist/controller/hiddenEditorApi.js +25 -8
  33. package/dist/controller/hiddenEditorManager.d.ts +11 -2
  34. package/dist/controller/hiddenEditorManager.js +456 -30
  35. package/dist/controller/layoutPipeline.d.ts +1 -1
  36. package/dist/controller/layoutPipeline.js +2 -0
  37. package/dist/controller/noteEditorManager.d.ts +4 -0
  38. package/dist/controller/noteEditorManager.js +113 -15
  39. package/dist/docx/attributeRemainder.js +1 -0
  40. package/dist/docx/bookmarkIds.d.ts +11 -0
  41. package/dist/docx/bookmarkIds.js +33 -0
  42. package/dist/docx/canonicalSessionInput.d.ts +11 -0
  43. package/dist/docx/canonicalSessionInput.js +27 -0
  44. package/dist/docx/canonicalStoryRepack.d.ts +10 -0
  45. package/dist/docx/canonicalStoryRepack.js +41 -0
  46. package/dist/docx/contentTypeRegistry.d.ts +8 -0
  47. package/dist/docx/contentTypeRegistry.js +64 -0
  48. package/dist/docx/documentParser.d.ts +14 -1
  49. package/dist/docx/documentParser.js +17 -4
  50. package/dist/docx/drawingIdNormalization.js +23 -8
  51. package/dist/docx/ensureParaIds.d.ts +12 -1
  52. package/dist/docx/ensureParaIds.js +173 -213
  53. package/dist/docx/foldedListNumberFields.d.ts +85 -0
  54. package/dist/docx/foldedListNumberFields.js +267 -0
  55. package/dist/docx/headerFooterVerbatim.d.ts +14 -1
  56. package/dist/docx/headerFooterVerbatim.js +95 -2
  57. package/dist/docx/listNumberingInstances.js +11 -9
  58. package/dist/docx/noteIds.d.ts +9 -0
  59. package/dist/docx/noteIds.js +17 -0
  60. package/dist/docx/noteReferenceMark.d.ts +3 -1
  61. package/dist/docx/noteReferenceMark.js +15 -14
  62. package/dist/docx/numberingIds.d.ts +14 -0
  63. package/dist/docx/numberingIds.js +16 -0
  64. package/dist/docx/numberingParser.d.ts +3 -1
  65. package/dist/docx/numberingParser.js +17 -3
  66. package/dist/docx/numericIdAllocator.d.ts +12 -0
  67. package/dist/docx/numericIdAllocator.js +33 -0
  68. package/dist/docx/numericIdNormalization.d.ts +16 -0
  69. package/dist/docx/numericIdNormalization.js +174 -0
  70. package/dist/docx/packageParts.d.ts +3 -1
  71. package/dist/docx/packageParts.js +65 -16
  72. package/dist/docx/paragraphMarkPropertyPresence.d.ts +9 -0
  73. package/dist/docx/paragraphMarkPropertyPresence.js +21 -0
  74. package/dist/docx/paragraphParser.js +101 -53
  75. package/dist/docx/paragraphPropertySource.d.ts +15 -1
  76. package/dist/docx/paragraphPropertySource.js +76 -22
  77. package/dist/docx/paragraphTextBoxEnrichment.js +2 -1
  78. package/dist/docx/parser.js +19 -6
  79. package/dist/docx/rasterMime.d.ts +64 -0
  80. package/dist/docx/rasterMime.js +116 -0
  81. package/dist/docx/replyToComment.js +13 -6
  82. package/dist/docx/reviewHistoryNamespace.d.ts +4 -0
  83. package/dist/docx/reviewHistoryNamespace.js +4 -0
  84. package/dist/docx/reviewResolutionProvenance.d.ts +25 -0
  85. package/dist/docx/reviewResolutionProvenance.js +153 -0
  86. package/dist/docx/rezip.d.ts +7 -3
  87. package/dist/docx/rezip.js +121 -52
  88. package/dist/docx/runConsolidator.d.ts +2 -58
  89. package/dist/docx/runConsolidator.js +3 -175
  90. package/dist/docx/runParser.d.ts +2 -1
  91. package/dist/docx/runParser.js +156 -116
  92. package/dist/docx/saveDiagnostics.d.ts +11 -0
  93. package/dist/docx/saveDiagnostics.js +0 -0
  94. package/dist/docx/selectiveSave.d.ts +2 -1
  95. package/dist/docx/selectiveSave.js +14 -1
  96. package/dist/docx/selectiveXmlPatch.d.ts +16 -8
  97. package/dist/docx/selectiveXmlPatch.js +74 -2
  98. package/dist/docx/serializer/headerFooterSerializer.d.ts +5 -1
  99. package/dist/docx/serializer/headerFooterSerializer.js +19 -11
  100. package/dist/docx/serializer/noteSerializer.d.ts +2 -2
  101. package/dist/docx/serializer/noteSerializer.js +24 -8
  102. package/dist/docx/serializer/paragraphSerializer.js +41 -4
  103. package/dist/docx/serializer/partNamespaces.js +6 -3
  104. package/dist/docx/serializer/runSerializer.js +8 -5
  105. package/dist/docx/serializer/trackedChangeAttributes.js +7 -1
  106. package/dist/docx/server/build.js +14 -3
  107. package/dist/docx/server/createBilingualDocument.js +10 -9
  108. package/dist/docx/settingsHeaderFooterUpdate.d.ts +5 -0
  109. package/dist/docx/settingsHeaderFooterUpdate.js +90 -0
  110. package/dist/docx/settingsParser.d.ts +1 -2
  111. package/dist/docx/settingsParser.js +5 -6
  112. package/dist/docx/storyBlockReplay.d.ts +11 -0
  113. package/dist/docx/storyBlockReplay.js +367 -0
  114. package/dist/docx/streamingXmlParser.d.ts +31 -1
  115. package/dist/docx/streamingXmlParser.js +136 -21
  116. package/dist/docx/structuralXmlPatch.js +8 -1
  117. package/dist/docx/unzip.d.ts +3 -1
  118. package/dist/docx/unzip.js +36 -16
  119. package/dist/docx/verbatimCapture.d.ts +3 -1
  120. package/dist/docx/verbatimCapture.js +49 -6
  121. package/dist/i18n/messages/ar.gen.d.ts +5 -0
  122. package/dist/i18n/messages/ar.gen.js +656 -0
  123. package/dist/i18n/messages/cs.gen.d.ts +5 -0
  124. package/dist/i18n/messages/cs.gen.js +656 -0
  125. package/dist/i18n/messages/de.gen.d.ts +5 -0
  126. package/dist/i18n/messages/de.gen.js +656 -0
  127. package/dist/i18n/messages/en.gen.d.ts +5 -0
  128. package/dist/i18n/messages/en.gen.js +656 -0
  129. package/dist/i18n/messages/es.gen.d.ts +5 -0
  130. package/dist/i18n/messages/es.gen.js +656 -0
  131. package/dist/i18n/messages/et.gen.d.ts +5 -0
  132. package/dist/i18n/messages/et.gen.js +656 -0
  133. package/dist/i18n/messages/fr.gen.d.ts +5 -0
  134. package/dist/i18n/messages/fr.gen.js +656 -0
  135. package/dist/i18n/messages/he.gen.d.ts +5 -0
  136. package/dist/i18n/messages/he.gen.js +656 -0
  137. package/dist/i18n/messages/hi.gen.d.ts +5 -0
  138. package/dist/i18n/messages/hi.gen.js +656 -0
  139. package/dist/i18n/messages/hu.gen.d.ts +5 -0
  140. package/dist/i18n/messages/hu.gen.js +656 -0
  141. package/dist/i18n/messages/locales.d.ts +5 -0
  142. package/dist/i18n/messages/locales.js +22 -0
  143. package/dist/i18n/messages/lt.gen.d.ts +5 -0
  144. package/dist/i18n/messages/lt.gen.js +656 -0
  145. package/dist/i18n/messages/lv.gen.d.ts +5 -0
  146. package/dist/i18n/messages/lv.gen.js +656 -0
  147. package/dist/i18n/messages/pl.gen.d.ts +5 -0
  148. package/dist/i18n/messages/pl.gen.js +656 -0
  149. package/dist/i18n/messages/pt-BR.gen.d.ts +5 -0
  150. package/dist/i18n/messages/pt-BR.gen.js +656 -0
  151. package/dist/i18n/messages/sk.gen.d.ts +5 -0
  152. package/dist/i18n/messages/sk.gen.js +656 -0
  153. package/dist/i18n/messages/tr.gen.d.ts +5 -0
  154. package/dist/i18n/messages/tr.gen.js +656 -0
  155. package/dist/i18n/messages/zh-CN.gen.d.ts +5 -0
  156. package/dist/i18n/messages/zh-CN.gen.js +656 -0
  157. package/dist/i18n/messages.d.ts +2 -3
  158. package/dist/i18n/messages.js +1 -19
  159. package/dist/internal/noteMarkerAttrs.d.ts +8 -0
  160. package/dist/internal/noteMarkerAttrs.js +16 -0
  161. package/dist/internal/pageBreakRunSourceDescendantIndex.js +1 -0
  162. package/dist/internal/prosemirrorAttrBoundary.d.ts +21 -0
  163. package/dist/internal/prosemirrorAttrBoundary.js +50 -0
  164. package/dist/layout-bridge/convert/fixedTableColumnWidths.d.ts +6 -0
  165. package/dist/layout-bridge/convert/fixedTableColumnWidths.js +23 -0
  166. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  167. package/dist/layout-bridge/convert/footnoteLayout.js +3 -3
  168. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  169. package/dist/layout-bridge/convert/headerFooterLayout.js +1 -0
  170. package/dist/layout-bridge/convert/noteReferences.d.ts +1 -3
  171. package/dist/layout-bridge/convert/noteReferences.js +1 -6
  172. package/dist/layout-bridge/convert/paragraphRuns.js +2 -4
  173. package/dist/layout-bridge/convert/tableConversion.js +4 -1
  174. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +2 -2
  175. package/dist/layout-bridge/convert/toFlowBlocks.js +2 -2
  176. package/dist/layout-painter/index.js +2 -5
  177. package/dist/managers/DocumentLoaderManager.d.ts +1 -0
  178. package/dist/managers/DocumentLoaderManager.js +8 -1
  179. package/dist/markdown/renderBlock.js +15 -1
  180. package/dist/markdown/renderParagraph.js +9 -13
  181. package/dist/markdown/renderRuns.js +3 -2
  182. package/dist/markdown/renderTable.js +2 -0
  183. package/dist/markdown/types.d.ts +3 -9
  184. package/dist/paged-layout/editorScrollRoot.d.ts +15 -0
  185. package/dist/paged-layout/editorScrollRoot.js +72 -0
  186. package/dist/paged-layout/scrollToPmPosition.js +8 -22
  187. package/dist/prosemirror/attrs/index.d.ts +2 -12
  188. package/dist/prosemirror/attrs/index.js +22 -48
  189. package/dist/prosemirror/bookmarkBoundaryAttrs.d.ts +2 -1
  190. package/dist/prosemirror/canonicalCommands.d.ts +97 -0
  191. package/dist/prosemirror/canonicalCommands.js +28 -0
  192. package/dist/prosemirror/commands/clearParagraphIndent.d.ts +6 -0
  193. package/dist/prosemirror/commands/clearParagraphIndent.js +25 -0
  194. package/dist/prosemirror/commands/pageBreak.js +15 -1
  195. package/dist/prosemirror/commentIdAllocator.d.ts +15 -22
  196. package/dist/prosemirror/commentIdAllocator.js +50 -25
  197. package/dist/prosemirror/commentReferenceAttrs.d.ts +2 -1
  198. package/dist/prosemirror/conversion/fromProseDoc.js +91 -19
  199. package/dist/prosemirror/conversion/listRenderingDefinition.d.ts +6 -0
  200. package/dist/prosemirror/conversion/listRenderingDefinition.js +97 -0
  201. package/dist/prosemirror/conversion/toProseDoc.d.ts +4 -3
  202. package/dist/prosemirror/conversion/toProseDoc.js +51 -11
  203. package/dist/prosemirror/executeEditorCommand.d.ts +11 -0
  204. package/dist/prosemirror/executeEditorCommand.js +26 -0
  205. package/dist/prosemirror/extensions/StarterKit.js +2 -0
  206. package/dist/prosemirror/extensions/core/HistoryExtension.js +97 -5
  207. package/dist/prosemirror/extensions/core/ParagraphExtension.js +53 -20
  208. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +22 -28
  209. package/dist/prosemirror/extensions/features/ListExtension.js +18 -5
  210. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +5 -2
  211. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +13 -1
  212. package/dist/prosemirror/extensions/marks/TextColorExtension.js +3 -2
  213. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +2 -0
  214. package/dist/prosemirror/extensions/marks/markUtils.js +70 -8
  215. package/dist/prosemirror/extensions/nodes/NoteMarkerExtension.d.ts +6 -0
  216. package/dist/prosemirror/extensions/nodes/NoteMarkerExtension.js +36 -0
  217. package/dist/prosemirror/extensions/nodes/PreservedXmlExtension.js +4 -2
  218. package/dist/prosemirror/extensions/nodes/ShapeExtension.js +8 -6
  219. package/dist/prosemirror/listMarker.d.ts +1 -0
  220. package/dist/prosemirror/listMarker.js +16 -7
  221. package/dist/prosemirror/listNumbering.js +26 -5
  222. package/dist/prosemirror/listRenderingAttrs.d.ts +1 -0
  223. package/dist/prosemirror/listRenderingAttrs.js +3 -0
  224. package/dist/prosemirror/markupViewNotes.js +3 -2
  225. package/dist/prosemirror/moveRangeBoundaryAttrs.d.ts +2 -1
  226. package/dist/prosemirror/noteReferenceReview.js +5 -3
  227. package/dist/prosemirror/plugins/revisionIds.d.ts +23 -19
  228. package/dist/prosemirror/plugins/revisionIds.js +110 -29
  229. package/dist/prosemirror/plugins/suggestionMode.d.ts +2 -2
  230. package/dist/prosemirror/plugins/suggestionMode.js +246 -42
  231. package/dist/prosemirror/rangeAnchorAttrs.d.ts +2 -1
  232. package/dist/prosemirror/runFormattingFromMarks.d.ts +4 -1
  233. package/dist/prosemirror/runFormattingFromMarks.js +15 -1
  234. package/dist/prosemirror/runFormattingInlineCarriers.d.ts +2 -1
  235. package/dist/prosemirror/runFormattingInlineCarriers.js +4 -1
  236. package/dist/prosemirror/schema/marks.d.ts +2 -0
  237. package/dist/prosemirror/schema/nodes.d.ts +10 -2
  238. package/dist/prosemirror/storyListNumbering.js +11 -9
  239. package/dist/prosemirror/textBoxAnchorAttrs.d.ts +2 -1
  240. package/dist/prosemirror/textInput.d.ts +6 -1
  241. package/dist/prosemirror/textInput.js +14 -3
  242. package/dist/prosemirror/trackedRevisionPath.js +2 -1
  243. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +1 -0
  244. package/dist/prosemirror/trackedRunInlineAtoms.js +1 -0
  245. package/dist/prosemirror/utils/visualLineNavigation.js +6 -8
  246. package/dist/prosemirror/validation.js +4 -0
  247. package/dist/types/content.d.ts +2 -2
  248. package/dist/utils/createDocument.js +2 -2
  249. package/dist/utils/mergeDocumentContent.d.ts +2 -2
  250. package/dist/utils/mergeDocumentContent.js +11 -8
  251. package/package.json +14 -2
@@ -0,0 +1,12 @@
1
+ //#region src/docx/numericIdAllocator.d.ts
2
+ type NumericIdAllocatorOptions = {
3
+ space: string;
4
+ firstId: 0 | 1;
5
+ };
6
+ /** Each OOXML id space owns one instance, retaining loaded and minted ids through rollover. */
7
+ declare const createNumericIdAllocator: ({ space, firstId }: NumericIdAllocatorOptions) => {
8
+ reserve: (ids: Iterable<number>) => void;
9
+ next: () => number;
10
+ };
11
+ //#endregion
12
+ export { createNumericIdAllocator };
@@ -0,0 +1,33 @@
1
+ import { TaggedError } from "better-result";
2
+ import { MAX_REVISION_ID } from "@stll/docx-core/model";
3
+ //#region src/docx/numericIdAllocator.ts
4
+ var OoxmlIdSpaceExhaustedError = class extends TaggedError("OoxmlIdSpaceExhaustedError") {};
5
+ /** Each OOXML id space owns one instance, retaining loaded and minted ids through rollover. */
6
+ const createNumericIdAllocator = ({ space, firstId }) => {
7
+ const reserved = /* @__PURE__ */ new Set();
8
+ let nextId = firstId;
9
+ return {
10
+ reserve: (ids) => {
11
+ let max = -1;
12
+ for (const id of ids) {
13
+ if (!Number.isInteger(id) || id < firstId || id > MAX_REVISION_ID) continue;
14
+ reserved.add(id);
15
+ max = Math.max(max, id);
16
+ }
17
+ if (max >= nextId) nextId = max === MAX_REVISION_ID ? firstId : max + 1;
18
+ },
19
+ next: () => {
20
+ if (reserved.size >= MAX_REVISION_ID - firstId + 1) throw new OoxmlIdSpaceExhaustedError({
21
+ message: `OOXML ${space} id space is exhausted`,
22
+ space
23
+ });
24
+ while (reserved.has(nextId)) nextId = nextId === MAX_REVISION_ID ? firstId : nextId + 1;
25
+ const id = nextId;
26
+ reserved.add(id);
27
+ nextId = id === MAX_REVISION_ID ? firstId : id + 1;
28
+ return id;
29
+ }
30
+ };
31
+ };
32
+ //#endregion
33
+ export { createNumericIdAllocator };
@@ -0,0 +1,16 @@
1
+ import { RawDocxContent } from "./unzip.js";
2
+ import { XmlElement } from "./xmlParser.js";
3
+ //#region src/docx/numericIdNormalization.d.ts
4
+ type NumericIdNormalizationOptions = {
5
+ onParsedDocument?: (document: XmlElement) => void;
6
+ };
7
+ /**
8
+ * Repair out-of-range imported integers before any model or opaque XML is captured.
9
+ * Reserve the complete package first, then remap each old value once per space.
10
+ * Source splices preserve unrelated XML bytes; a valid package takes the fast path.
11
+ */
12
+ declare const normalizeImportedNumericIds: (parts: ReadonlyMap<string, string>, options?: NumericIdNormalizationOptions) => ReadonlyMap<string, string>;
13
+ /** Parser boundary: all captures and selective-save bytes see the same identities. */
14
+ declare const normalizeRawDocxNumericIds: (raw: RawDocxContent) => Promise<XmlElement | undefined>;
15
+ //#endregion
16
+ export { normalizeImportedNumericIds, normalizeRawDocxNumericIds };
@@ -0,0 +1,174 @@
1
+ import { REVISION_ELEMENT_NAMES } from "./revisionIdNormalization.js";
2
+ import { parseStreamingXmlWithIdentityVisitor, scanStreamingXmlNumericIdAttributes } from "./streamingXmlParser.js";
3
+ import { replaceRawDocxXmlParts } from "./unzip.js";
4
+ import { getLocalName, getNamespaceUri, resolveAttributeNamespaceUri } from "./xmlParser.js";
5
+ import { XmlResourceLimitError } from "./xmlResourceLimits.js";
6
+ import { panic } from "better-result";
7
+ import { assertValidOoxmlNumericId, isOoxmlNumericIdAttributeName, isValidOoxmlNumericId, mayContainInvalidOoxmlNumericIds, mayContainOoxmlNumericIds, ooxmlNumericIdDomain } from "@stll/docx-core";
8
+ //#region src/docx/numericIdNormalization.ts
9
+ const identityKey = (value) => {
10
+ const spelling = String(value).trim();
11
+ if (!/^[+-]?\d+$/u.test(spelling)) return spelling;
12
+ const digits = spelling.replace(/^[+-]?0*/u, "");
13
+ return digits === "" ? "0" : `${spelling.startsWith("-") ? "-" : ""}${digits}`;
14
+ };
15
+ const identitySpace = ({ elementName, attributeName, domain }) => {
16
+ if (domain === "unsigned32") return "drawing";
17
+ const element = getLocalName(elementName);
18
+ const attribute = getLocalName(attributeName);
19
+ switch (attribute === "val" ? element : attribute) {
20
+ case "numId": return "numbering-instance";
21
+ case "abstractNumId": return "numbering-abstract";
22
+ case "numPicBulletId":
23
+ case "lvlPicBulletId": return "numbering-picture";
24
+ default: break;
25
+ }
26
+ switch (element) {
27
+ case "footnote":
28
+ case "footnoteReference": return "footnote";
29
+ case "endnote":
30
+ case "endnoteReference": return "endnote";
31
+ case "id": return "content-control";
32
+ default: return `annotation:${REVISION_ELEMENT_NAMES.has(element) ? "revision" : element.replace(/(?:Range)?(?:Start|End)$/u, "").replace(/^comment.*$/u, "comment")}`;
33
+ }
34
+ };
35
+ const identifiedAttributes = (element) => {
36
+ const attributes = [];
37
+ for (const [name, value] of Object.entries(element.attributes ?? {})) {
38
+ if (!isOoxmlNumericIdAttributeName({
39
+ elementName: element.name ?? "",
40
+ attributeName: name
41
+ })) continue;
42
+ if (value === void 0 || !/^[+-]?\d+$/u.test(String(value).trim())) continue;
43
+ const domain = ooxmlNumericIdDomain({
44
+ elementName: element.name ?? "",
45
+ elementNamespace: getNamespaceUri(element),
46
+ attributeName: name,
47
+ attributeNamespace: resolveAttributeNamespaceUri(element, name)
48
+ });
49
+ if (domain === void 0) continue;
50
+ attributes.push({
51
+ name,
52
+ value,
53
+ domain,
54
+ space: identitySpace({
55
+ elementName: element.name ?? "",
56
+ attributeName: name,
57
+ domain
58
+ })
59
+ });
60
+ }
61
+ return attributes;
62
+ };
63
+ /**
64
+ * Repair out-of-range imported integers before any model or opaque XML is captured.
65
+ * Reserve the complete package first, then remap each old value once per space.
66
+ * Source splices preserve unrelated XML bytes; a valid package takes the fast path.
67
+ */
68
+ const normalizeImportedNumericIds = (parts, options = {}) => {
69
+ if (![...parts.values()].some((xml) => mayContainInvalidOoxmlNumericIds(xml, "range"))) return parts;
70
+ const normalized = new Map(parts);
71
+ const candidates = [...parts].filter(([, xml]) => mayContainOoxmlNumericIds(xml)).toSorted(([left], [right]) => left.localeCompare(right));
72
+ const spaces = /* @__PURE__ */ new Map();
73
+ const reservedPools = /* @__PURE__ */ new Map();
74
+ const changedSpans = /* @__PURE__ */ new Map();
75
+ let parsedDocument;
76
+ const scan = ({ path, xml, visitor }) => {
77
+ const scanned = path.toLowerCase() === "word/document.xml" && options.onParsedDocument !== void 0 ? parseStreamingXmlWithIdentityVisitor(xml, visitor) : scanStreamingXmlNumericIdAttributes(xml, visitor);
78
+ if (scanned.status === "unsupported") throw new XmlResourceLimitError({
79
+ message: `Numeric-id normalization could not safely scan ${path}`,
80
+ limit: "syntax",
81
+ observed: 0,
82
+ allowed: 0
83
+ });
84
+ if (scanned.status === "parsed") parsedDocument = scanned.value;
85
+ };
86
+ for (const [path, xml] of candidates) scan({
87
+ path,
88
+ xml,
89
+ visitor: (element, attributeValueSpans) => {
90
+ for (const attribute of identifiedAttributes(element)) {
91
+ let space = spaces.get(attribute.space);
92
+ if (space === void 0) {
93
+ const poolName = attribute.space.startsWith("annotation:") ? "annotation" : attribute.space;
94
+ let reserved = reservedPools.get(poolName);
95
+ if (reserved === void 0) {
96
+ reserved = /* @__PURE__ */ new Set();
97
+ reservedPools.set(poolName, reserved);
98
+ }
99
+ space = {
100
+ reserved,
101
+ replacements: /* @__PURE__ */ new Map(),
102
+ next: 1
103
+ };
104
+ spaces.set(attribute.space, space);
105
+ }
106
+ if (isValidOoxmlNumericId(attribute.value, attribute.domain)) {
107
+ space.reserved.add(Number(attribute.value));
108
+ continue;
109
+ }
110
+ const key = identityKey(attribute.value);
111
+ space.replacements.set(key, "");
112
+ const span = attributeValueSpans.get(attribute.name);
113
+ if (span === void 0) panic("Missing imported numeric identity source span");
114
+ let spans = changedSpans.get(path);
115
+ if (spans === void 0) {
116
+ spans = [];
117
+ changedSpans.set(path, spans);
118
+ }
119
+ spans.push({
120
+ start: span.start,
121
+ end: span.end,
122
+ space: attribute.space,
123
+ key,
124
+ element,
125
+ attributeName: attribute.name
126
+ });
127
+ }
128
+ return null;
129
+ }
130
+ });
131
+ for (const space of spaces.values()) for (const key of space.replacements.keys()) {
132
+ while (space.reserved.has(space.next)) space.next += 1;
133
+ assertValidOoxmlNumericId({
134
+ value: space.next,
135
+ partPath: "import",
136
+ elementName: "allocated identity",
137
+ attributeName: "id"
138
+ });
139
+ const replacement = String(space.next);
140
+ space.replacements.set(key, replacement);
141
+ space.reserved.add(space.next);
142
+ space.next += 1;
143
+ }
144
+ for (const [path, spans] of changedSpans) {
145
+ const xml = parts.get(path);
146
+ if (xml === void 0) panic(`Missing imported numeric identity part: ${path}`);
147
+ const chunks = [];
148
+ let cursor = 0;
149
+ for (const span of spans.toSorted((left, right) => left.start - right.start)) {
150
+ const replacement = spaces.get(span.space)?.replacements.get(span.key);
151
+ if (replacement === void 0 || replacement === "") panic("Missing imported numeric identity replacement");
152
+ if (span.element.attributes === void 0) panic("Missing imported numeric identity attributes");
153
+ span.element.attributes[span.attributeName] = replacement;
154
+ chunks.push(xml.slice(cursor, span.start), replacement);
155
+ cursor = span.end;
156
+ }
157
+ chunks.push(xml.slice(cursor));
158
+ normalized.set(path, chunks.join(""));
159
+ }
160
+ if (parsedDocument !== void 0) options.onParsedDocument?.(parsedDocument);
161
+ return normalized;
162
+ };
163
+ /** Parser boundary: all captures and selective-save bytes see the same identities. */
164
+ const normalizeRawDocxNumericIds = async (raw) => {
165
+ let documentTree;
166
+ const normalized = normalizeImportedNumericIds(raw.allXml, { onParsedDocument: (tree) => {
167
+ documentTree = tree;
168
+ } });
169
+ if (normalized === raw.allXml) return documentTree;
170
+ await replaceRawDocxXmlParts(raw, normalized);
171
+ return documentTree;
172
+ };
173
+ //#endregion
174
+ export { normalizeImportedNumericIds, normalizeRawDocxNumericIds };
@@ -1,5 +1,7 @@
1
1
  import JSZip from "jszip";
2
2
  //#region src/docx/packageParts.d.ts
3
+ /** Resolve an internal URI reference once, before comparing it with ZIP entries. */
4
+ declare const resolvePackageRelationshipTarget: (target: string, relsPath: string) => string | undefined;
3
5
  /**
4
6
  * Whether an entry path would escape the package when a host writes the archive
5
7
  * to disk: absolute, backslash-separated, or climbing out through `..`. This is
@@ -76,4 +78,4 @@ type PackageIntegrityReport = {
76
78
  */
77
79
  declare const checkPackageIntegrity: (zip: JSZip) => Promise<PackageIntegrityReport>;
78
80
  //#endregion
79
- export { PackageIntegrityReport, PackageReferenceRepair, checkPackageIntegrity, isUnsafePackagePath, reconcilePackageReferences, removeUnsafeEntries };
81
+ export { PackageIntegrityReport, PackageReferenceRepair, checkPackageIntegrity, isUnsafePackagePath, reconcilePackageReferences, removeUnsafeEntries, resolvePackageRelationshipTarget };
@@ -1,5 +1,30 @@
1
- import { resolveRelativePath } from "./relsParser.js";
1
+ import { Result } from "better-result";
2
2
  //#region src/docx/packageParts.ts
3
+ /**
4
+ * What a repack carries out of the source package, and the guarantee that
5
+ * nothing in the output points at a part the output does not hold.
6
+ *
7
+ * A `.docx` is an OPC package: `[Content_Types].xml` declares every part in it
8
+ * and the `.rels` parts wire them together. Folio models a handful of those
9
+ * parts (the document body, headers, footers, notes, numbering, styles,
10
+ * comments) and rewrites them on save. Everything else — embedded objects,
11
+ * media, custom XML, fonts, charts, macro projects, parts folio has never heard
12
+ * of — belongs to the author, and a repack hands it back byte for byte.
13
+ *
14
+ * Folio never executes document content and never removes it. The document is
15
+ * the user's; the only thing a save refuses is a package path that would let
16
+ * the archive escape its own directory when a host writes it out, which is a
17
+ * property of the package structure rather than of the content. A macro-enabled
18
+ * document therefore stays macro-enabled, main-part content type included:
19
+ * silently handing back a `.docm` stripped of its project is data loss dressed
20
+ * up as safety.
21
+ *
22
+ * A refused path loses its relationships and its content-type override with it.
23
+ * That is the invariant this module exists for: Word and LibreOffice both
24
+ * refuse a package whose `document.xml.rels` points at nothing, so a repack
25
+ * that removes a part without removing its references produces a file that
26
+ * does not open.
27
+ */
3
28
  const CONTENT_TYPE_ELEMENT = /<(?:Default|Override)\b[^>]*?\/?>/giu;
4
29
  const PART_NAME_ATTRIBUTE = /\bPartName\s*=\s*(?<quote>["'])(?<value>[^"']*)\k<quote>/u;
5
30
  const RELATIONSHIP_ELEMENT = /<Relationship\b[^>]*?(?:\/>|>\s*<\/Relationship>)/giu;
@@ -24,12 +49,38 @@ const owningPartPath = (relsPath) => {
24
49
  };
25
50
  /** Normalize a package path for comparison: lower case, no leading slash. */
26
51
  const normalizePartPath = (path) => (path.startsWith("/") ? path.slice(1) : path).toLowerCase();
27
- /**
28
- * Decode `%XX` escapes in a part name. `decodeURI` throws on a malformed
29
- * escape, and a malformed target is exactly the input this has to survive, so
30
- * the replacement is done by hand.
31
- */
32
- const decodePartName = (name) => name.replaceAll(/%[0-9A-Fa-f]{2}/gu, (escape) => String.fromCharCode(Number.parseInt(escape.slice(1), 16)));
52
+ /** Decode URI escapes as UTF-8; malformed references cannot name a part. */
53
+ const decodePartName = (name) => {
54
+ const decoded = Result.try({
55
+ try: () => decodeURIComponent(name),
56
+ catch: () => void 0
57
+ });
58
+ return decoded.isOk() ? decoded.value : void 0;
59
+ };
60
+ /** Resolve an internal URI reference once, before comparing it with ZIP entries. */
61
+ const resolvePackageRelationshipTarget = (target, relsPath) => {
62
+ const fragment = target.indexOf("#");
63
+ if (fragment === 0) {
64
+ const owner = owningPartPath(relsPath);
65
+ return owner === void 0 ? void 0 : normalizePartPath(owner);
66
+ }
67
+ const reference = fragment > 0 ? target.slice(0, fragment) : target;
68
+ if (!reference || reference.startsWith("//") || /[\\?:]/u.test(reference)) return void 0;
69
+ const owner = owningPartPath(relsPath);
70
+ const segments = reference.startsWith("/") || owner === void 0 ? [] : owner.split("/").slice(0, -1);
71
+ for (const encoded of reference.split("/")) {
72
+ const segment = decodePartName(encoded);
73
+ if (segment === void 0 || /[/\\]/u.test(segment) || segment.includes(String.fromCharCode(0))) return void 0;
74
+ if (segment === "" || segment === ".") continue;
75
+ if (segment === "..") {
76
+ if (segments.pop() === void 0) return void 0;
77
+ continue;
78
+ }
79
+ segments.push(segment);
80
+ }
81
+ const path = normalizePartPath(segments.join("/"));
82
+ return isUnsafePackagePath(path) ? void 0 : path;
83
+ };
33
84
  /**
34
85
  * Whether an entry path would escape the package when a host writes the archive
35
86
  * to disk: absolute, backslash-separated, or climbing out through `..`. This is
@@ -53,15 +104,13 @@ const presentPartPaths = (zip) => {
53
104
  for (const [path, file] of Object.entries(zip.files)) if (!file.dir) present.add(normalizePartPath(path));
54
105
  return present;
55
106
  };
56
- const isPresent = (present, partPath) => present.has(normalizePartPath(partPath)) || present.has(normalizePartPath(decodePartName(partPath)));
107
+ const isPresent = (present, partPath) => present.has(normalizePartPath(partPath)) || present.has(normalizePartPath(decodePartName(partPath) ?? ""));
57
108
  /** The part a relationship names, or null when it points outside the package. */
58
109
  const relationshipTarget = (element, relsPath) => {
59
110
  if (TARGET_MODE_ATTRIBUTE.exec(element)?.groups?.["value"] === "External") return null;
60
111
  const target = TARGET_ATTRIBUTE.exec(element)?.groups?.["value"];
61
112
  if (target === void 0) return null;
62
- const fragment = target.indexOf("#");
63
- if (fragment === 0) return owningPartPath(relsPath) ?? "";
64
- return resolveRelativePath(relsPath, fragment > 0 ? target.slice(0, fragment) : target);
113
+ return resolvePackageRelationshipTarget(target, relsPath) ?? "";
65
114
  };
66
115
  /**
67
116
  * Every `.rels` part with its text. Read one at a time rather than through
@@ -107,8 +156,8 @@ const reconcilePackageReferences = async (zip, compressionLevel) => {
107
156
  let dropped = false;
108
157
  const pruned = xml.replaceAll(RELATIONSHIP_ELEMENT, (element) => {
109
158
  const target = relationshipTarget(element, path);
110
- if (target === null || isPresent(present, target)) return element;
111
- repair.danglingRelationships.push(target);
159
+ if (target === null || present.has(target)) return element;
160
+ repair.danglingRelationships.push(target || TARGET_ATTRIBUTE.exec(element)?.groups?.["value"] || "");
112
161
  dropped = true;
113
162
  const id = RELATIONSHIP_ID_ATTRIBUTE.exec(element)?.groups?.["value"];
114
163
  const owningPart = owningPartPath(path);
@@ -154,7 +203,7 @@ const reconcilePackageReferences = async (zip, compressionLevel) => {
154
203
  const prunedContentTypes = (await contentTypes.async("text")).replaceAll(CONTENT_TYPE_ELEMENT, (element) => {
155
204
  const partName = PART_NAME_ATTRIBUTE.exec(element)?.groups?.["value"];
156
205
  if (partName === void 0 || isPresent(present, partName)) return element;
157
- repair.danglingOverrides.push(normalizePartPath(decodePartName(partName)));
206
+ repair.danglingOverrides.push(normalizePartPath(decodePartName(partName) ?? partName));
158
207
  droppedOverride = true;
159
208
  return "";
160
209
  });
@@ -195,7 +244,7 @@ const checkPackageIntegrity = async (zip) => {
195
244
  const id = RELATIONSHIP_ID_ATTRIBUTE.exec(element)?.groups?.["value"];
196
245
  if (id !== void 0) ids.add(id);
197
246
  const target = relationshipTarget(element, path);
198
- if (target !== null && !isPresent(present, target)) report.danglingRelationshipTargets.push(`${path}|${target}`);
247
+ if (target !== null && !present.has(target)) report.danglingRelationshipTargets.push(`${path}|${target || TARGET_ATTRIBUTE.exec(element)?.groups?.["value"] || ""}`);
199
248
  }
200
249
  if (owningPart !== void 0) idsByOwningPart.set(owningPart, ids);
201
250
  }
@@ -211,4 +260,4 @@ const checkPackageIntegrity = async (zip) => {
211
260
  return report;
212
261
  };
213
262
  //#endregion
214
- export { checkPackageIntegrity, isUnsafePackagePath, reconcilePackageReferences, removeUnsafeEntries };
263
+ export { checkPackageIntegrity, isUnsafePackagePath, reconcilePackageReferences, removeUnsafeEntries, resolvePackageRelationshipTarget };
@@ -0,0 +1,9 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+ import { XmlElement } from "./xmlParser.js";
3
+ //#region src/docx/paragraphMarkPropertyPresence.d.ts
4
+ declare const EMPTY_MARK_PROPERTIES_ATTRIBUTE = "emptyMarkProperties";
5
+ /** A paragraph-owned rPr carrier cannot distinguish absent and explicitly empty formatting. */
6
+ declare const serializeEmptyMarkProperties: (paragraph: document_d_exports.Paragraph) => string | undefined;
7
+ declare const restoreEmptyMarkProperties: (paragraph: document_d_exports.Paragraph, element: XmlElement) => void;
8
+ //#endregion
9
+ export { EMPTY_MARK_PROPERTIES_ATTRIBUTE, restoreEmptyMarkProperties, serializeEmptyMarkProperties };
@@ -0,0 +1,21 @@
1
+ import { OOXML_NAMESPACES } from "./serializer/partNamespaces.js";
2
+ import { getAttributeByNamespaceUri } from "./xmlParser.js";
3
+ import { EMPTY_PROPERTY_SETS } from "@stll/docx-core/ops";
4
+ //#region src/docx/paragraphMarkPropertyPresence.ts
5
+ const EMPTY_MARK_PROPERTIES_ATTRIBUTE = "emptyMarkProperties";
6
+ const REVIEW_HISTORY_NAMESPACES = /* @__PURE__ */ new Set([OOXML_NAMESPACES.folio.uri]);
7
+ /** A paragraph-owned rPr carrier cannot distinguish absent and explicitly empty formatting. */
8
+ const serializeEmptyMarkProperties = (paragraph) => {
9
+ const properties = paragraph.formatting?.runProperties;
10
+ if (!(paragraph.pPrMark !== void 0 || paragraph.formatting?.runInWithNext !== void 0) || properties === void 0 || Object.keys(properties).length !== 0) return void 0;
11
+ return `folio:${EMPTY_MARK_PROPERTIES_ATTRIBUTE}="${EMPTY_PROPERTY_SETS.KEEP}"`;
12
+ };
13
+ const restoreEmptyMarkProperties = (paragraph, element) => {
14
+ if (paragraph.pPrMark === void 0 && paragraph.formatting?.runInWithNext === void 0 || getAttributeByNamespaceUri(element, REVIEW_HISTORY_NAMESPACES, "emptyMarkProperties") !== EMPTY_PROPERTY_SETS.KEEP || paragraph.formatting?.runProperties !== void 0) return;
15
+ paragraph.formatting = {
16
+ ...paragraph.formatting,
17
+ runProperties: {}
18
+ };
19
+ };
20
+ //#endregion
21
+ export { EMPTY_MARK_PROPERTIES_ATTRIBUTE, restoreEmptyMarkProperties, serializeEmptyMarkProperties };