@portone/docx-editor 0.2.1 → 0.3.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 (225) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/CONTRIBUTING.md +5 -1
  3. package/dist/DocxEditor.d.ts +6 -0
  4. package/dist/DocxEditor.js +12 -29
  5. package/dist/core.d.ts +23 -8
  6. package/dist/core.js +9 -4
  7. package/dist/docx/cloning.d.ts +38 -0
  8. package/dist/docx/cloning.js +74 -0
  9. package/dist/docx/commentOnlyChange.d.ts +12 -7
  10. package/dist/docx/commentOnlyChange.js +9 -156
  11. package/dist/docx/comments/constants.d.ts +0 -1
  12. package/dist/docx/comments/constants.js +0 -2
  13. package/dist/docx/comments/grammar.d.ts +10 -4
  14. package/dist/docx/comments/grammar.js +12 -2
  15. package/dist/docx/comments/{verifying.d.ts → parts.d.ts} +26 -18
  16. package/dist/docx/comments/parts.js +279 -0
  17. package/dist/docx/comments/people.d.ts +11 -2
  18. package/dist/docx/comments/people.js +16 -92
  19. package/dist/docx/comments/policy.d.ts +7 -0
  20. package/dist/docx/comments/policy.js +43 -0
  21. package/dist/docx/comments/reading.js +8 -10
  22. package/dist/docx/comments/writing.d.ts +19 -7
  23. package/dist/docx/comments/writing.js +52 -111
  24. package/dist/docx/documentSettings.d.ts +7 -0
  25. package/dist/docx/documentSettings.js +10 -2
  26. package/dist/docx/exportDocx.d.ts +40 -5
  27. package/dist/docx/exportDocx.js +69 -71
  28. package/dist/docx/exportRefs.d.ts +5 -2
  29. package/dist/docx/exportRefs.js +3 -1
  30. package/dist/docx/fidelity.d.ts +43 -0
  31. package/dist/docx/fidelity.js +64 -0
  32. package/dist/docx/formatting/attrs.d.ts +27 -0
  33. package/dist/docx/formatting/attrs.js +31 -0
  34. package/dist/docx/formatting/context.d.ts +31 -0
  35. package/dist/docx/formatting/context.js +42 -0
  36. package/dist/docx/formatting/direct.d.ts +6 -5
  37. package/dist/docx/formatting/direct.js +52 -78
  38. package/dist/docx/formatting/resolve.d.ts +45 -0
  39. package/dist/docx/formatting/resolve.js +140 -0
  40. package/dist/docx/formatting/runProperties.d.ts +93 -0
  41. package/dist/docx/formatting/runProperties.js +316 -0
  42. package/dist/docx/formatting/styles.js +3 -3
  43. package/dist/docx/formatting/tabStops.js +7 -22
  44. package/dist/docx/formatting.d.ts +4 -1
  45. package/dist/docx/formatting.js +4 -1
  46. package/dist/docx/headersFooters.js +6 -13
  47. package/dist/docx/identities.d.ts +67 -0
  48. package/dist/docx/identities.js +174 -0
  49. package/dist/docx/importDocx.d.ts +12 -1
  50. package/dist/docx/importDocx.js +72 -79
  51. package/dist/docx/importParagraph.d.ts +1 -1
  52. package/dist/docx/importParagraph.js +4 -3
  53. package/dist/docx/importTable.d.ts +1 -1
  54. package/dist/docx/importTable.js +17 -1
  55. package/dist/docx/invariants.d.ts +33 -0
  56. package/dist/docx/invariants.js +256 -0
  57. package/dist/docx/media.d.ts +6 -4
  58. package/dist/docx/media.js +6 -37
  59. package/dist/docx/newLists.d.ts +20 -0
  60. package/dist/docx/newLists.js +36 -0
  61. package/dist/docx/notes.js +3 -7
  62. package/dist/docx/numberingPlanner.d.ts +8 -0
  63. package/dist/docx/numberingPlanner.js +19 -0
  64. package/dist/docx/packageParts.d.ts +42 -0
  65. package/dist/docx/packageParts.js +135 -0
  66. package/dist/docx/pageGeometry.d.ts +2 -0
  67. package/dist/docx/pageGeometry.js +18 -9
  68. package/dist/docx/paraProps.d.ts +10 -22
  69. package/dist/docx/paraProps.js +47 -76
  70. package/dist/docx/partPlan.d.ts +36 -0
  71. package/dist/docx/partPlan.js +59 -0
  72. package/dist/docx/protectionPolicy.d.ts +127 -0
  73. package/dist/docx/protectionPolicy.js +169 -0
  74. package/dist/docx/relationships.d.ts +1 -1
  75. package/dist/docx/relationships.js +8 -13
  76. package/dist/docx/runProps.d.ts +9 -22
  77. package/dist/docx/runProps.js +15 -168
  78. package/dist/docx/scan.js +20 -52
  79. package/dist/docx/sdt.js +9 -34
  80. package/dist/docx/sdtProps.d.ts +8 -1
  81. package/dist/docx/sdtProps.js +10 -0
  82. package/dist/docx/serializeBlock.d.ts +2 -0
  83. package/dist/docx/serializeBlock.js +8 -5
  84. package/dist/docx/serializeTable.js +32 -15
  85. package/dist/docx/session.d.ts +42 -14
  86. package/dist/docx/session.js +43 -17
  87. package/dist/docx/tableFormatting/editing.js +108 -139
  88. package/dist/docx/tableFormatting/reading.d.ts +16 -6
  89. package/dist/docx/tableFormatting/reading.js +40 -20
  90. package/dist/docx/tableTemplate.js +18 -7
  91. package/dist/download.d.ts +8 -5
  92. package/dist/download.js +2 -0
  93. package/dist/editor/clipboard/inlineFormatting.js +19 -30
  94. package/dist/editor/commands/comments/editing.d.ts +6 -2
  95. package/dist/editor/commands/comments/editing.js +19 -31
  96. package/dist/editor/commands/exportQueries.d.ts +15 -0
  97. package/dist/editor/commands/exportQueries.js +14 -0
  98. package/dist/editor/commands/fidelityQueries.d.ts +12 -0
  99. package/dist/editor/commands/fidelityQueries.js +8 -0
  100. package/dist/editor/commands/formatting/editing.d.ts +2 -2
  101. package/dist/editor/commands/formatting/editing.js +26 -108
  102. package/dist/editor/commands/formatting/propertyCommands.d.ts +21 -0
  103. package/dist/editor/commands/formatting/propertyCommands.js +103 -0
  104. package/dist/editor/commands/formatting/shared.d.ts +3 -3
  105. package/dist/editor/commands/formatting/shared.js +5 -2
  106. package/dist/editor/commands/indentCommands.js +5 -4
  107. package/dist/editor/commands/index.d.ts +11 -0
  108. package/dist/editor/commands/index.js +5 -0
  109. package/dist/editor/commands/linkCommands.js +5 -6
  110. package/dist/editor/commands/listCommands.js +8 -10
  111. package/dist/editor/commands/lockCommands.d.ts +7 -1
  112. package/dist/editor/commands/lockCommands.js +35 -46
  113. package/dist/editor/commands/paragraphCommands.js +28 -31
  114. package/dist/editor/commands/spacingCommands.js +1 -1
  115. package/dist/editor/createEditor.d.ts +17 -29
  116. package/dist/editor/createEditor.js +58 -58
  117. package/dist/editor/documentStyles.d.ts +11 -35
  118. package/dist/editor/documentStyles.js +9 -52
  119. package/dist/editor/editorDocument.d.ts +56 -0
  120. package/dist/editor/editorDocument.js +74 -0
  121. package/dist/editor/externalClipboard.js +16 -45
  122. package/dist/editor/insertTable.js +4 -3
  123. package/dist/editor/paragraphEdits.d.ts +11 -18
  124. package/dist/editor/paragraphEdits.js +7 -16
  125. package/dist/editor/plugins/displayDerivation.d.ts +47 -0
  126. package/dist/editor/plugins/displayDerivation.js +78 -0
  127. package/dist/editor/plugins/imagePaste.js +4 -3
  128. package/dist/editor/plugins/keymap.js +18 -3
  129. package/dist/editor/plugins/numberingDecorations.d.ts +5 -11
  130. package/dist/editor/plugins/numberingDecorations.js +7 -17
  131. package/dist/editor/plugins/paragraphDisplay.d.ts +7 -0
  132. package/dist/editor/plugins/paragraphDisplay.js +51 -0
  133. package/dist/editor/plugins/tabLayout.js +1 -1
  134. package/dist/editor/plugins/tableDisplay.d.ts +6 -0
  135. package/dist/editor/plugins/tableDisplay.js +16 -0
  136. package/dist/index.d.ts +2 -0
  137. package/dist/model/format.d.ts +30 -8
  138. package/dist/model/format.js +35 -22
  139. package/dist/model/tabStops.d.ts +9 -0
  140. package/dist/model/tabStops.js +18 -0
  141. package/dist/numbering/listTemplate.js +24 -7
  142. package/dist/numbering/parseNumbering.d.ts +14 -1
  143. package/dist/numbering/parseNumbering.js +35 -22
  144. package/dist/numbering/writeNumbering.d.ts +3 -4
  145. package/dist/numbering/writeNumbering.js +14 -25
  146. package/dist/ooxml/childOrder.d.ts +34 -0
  147. package/dist/ooxml/childOrder.js +496 -0
  148. package/dist/ooxml/element.d.ts +24 -11
  149. package/dist/ooxml/element.js +31 -12
  150. package/dist/ooxml/errors.d.ts +3 -2
  151. package/dist/ooxml/image.d.ts +4 -0
  152. package/dist/ooxml/image.js +2 -5
  153. package/dist/ooxml/partSplice.d.ts +67 -0
  154. package/dist/ooxml/partSplice.js +169 -0
  155. package/dist/ooxml/props.d.ts +112 -0
  156. package/dist/{docx/propsXml.js → ooxml/props.js} +72 -200
  157. package/dist/ooxml/simpleTypes.d.ts +103 -0
  158. package/dist/ooxml/simpleTypes.js +182 -0
  159. package/dist/ooxml/tabStops.js +8 -27
  160. package/dist/ooxml/tagScan.d.ts +34 -0
  161. package/dist/ooxml/tagScan.js +108 -0
  162. package/dist/ooxml/units.d.ts +25 -13
  163. package/dist/ooxml/units.js +65 -31
  164. package/dist/ooxml/xml.d.ts +27 -6
  165. package/dist/ooxml/xml.js +37 -5
  166. package/dist/page/blockKinds.d.ts +61 -0
  167. package/dist/page/blockKinds.js +11 -0
  168. package/dist/page/kinds/index.d.ts +6 -0
  169. package/dist/page/kinds/index.js +10 -0
  170. package/dist/page/kinds/paragraphKind.d.ts +10 -0
  171. package/dist/page/kinds/paragraphKind.js +71 -0
  172. package/dist/page/kinds/tableKind.d.ts +10 -0
  173. package/dist/page/kinds/tableKind.js +178 -0
  174. package/dist/page/measureBlocks.d.ts +3 -14
  175. package/dist/page/measureBlocks.js +24 -45
  176. package/dist/page/pageDecorations.d.ts +27 -44
  177. package/dist/page/pageDecorations.js +70 -152
  178. package/dist/page/pageLayout.d.ts +2 -19
  179. package/dist/page/pageLayout.js +36 -6
  180. package/dist/page/usePageLayout.d.ts +2 -17
  181. package/dist/page/usePageLayout.js +3 -39
  182. package/dist/schema/attrRoles.d.ts +25 -18
  183. package/dist/schema/attrRoles.js +102 -67
  184. package/dist/schema/displayDerivation.d.ts +82 -0
  185. package/dist/schema/displayDerivation.js +130 -0
  186. package/dist/schema/docxSchema.d.ts +3 -0
  187. package/dist/schema/docxSchema.js +26 -14
  188. package/dist/schema/editGuard.d.ts +1 -1
  189. package/dist/schema/guards.d.ts +37 -14
  190. package/dist/schema/guards.js +29 -3
  191. package/dist/schema/index.d.ts +2 -0
  192. package/dist/schema/index.js +2 -0
  193. package/dist/schema/locks.d.ts +11 -44
  194. package/dist/schema/locks.js +0 -9
  195. package/dist/schema/preservedGuards.d.ts +26 -6
  196. package/dist/schema/preservedGuards.js +31 -1
  197. package/dist/schema/protection.d.ts +7 -1
  198. package/dist/schema/protection.js +2 -1
  199. package/dist/schema/sourceEquality.d.ts +1 -9
  200. package/dist/schema/sourceEquality.js +1 -27
  201. package/dist/styles/inlineStyle.js +15 -6
  202. package/dist/table/cellFormatting.d.ts +8 -1
  203. package/dist/table/cellFormatting.js +10 -18
  204. package/dist/table/commands.js +20 -18
  205. package/dist/table/format.d.ts +3 -0
  206. package/dist/table/format.js +3 -9
  207. package/dist/table/gridBorders.d.ts +14 -16
  208. package/dist/table/gridBorders.js +3 -41
  209. package/dist/table/merge.d.ts +2 -6
  210. package/dist/table/merge.js +5 -5
  211. package/package.json +7 -3
  212. package/dist/docx/comments/contentTypes.d.ts +0 -7
  213. package/dist/docx/comments/contentTypes.js +0 -38
  214. package/dist/docx/comments/verifying.js +0 -206
  215. package/dist/docx/formatting/effectiveParagraph.d.ts +0 -15
  216. package/dist/docx/formatting/effectiveParagraph.js +0 -81
  217. package/dist/docx/propsXml.d.ts +0 -67
  218. package/dist/docx/uniqueControls.d.ts +0 -14
  219. package/dist/docx/uniqueControls.js +0 -62
  220. package/dist/editor/plugins/commentReservations.d.ts +0 -5
  221. package/dist/editor/plugins/commentReservations.js +0 -26
  222. package/dist/editor/plugins/styledParagraphs.d.ts +0 -15
  223. package/dist/editor/plugins/styledParagraphs.js +0 -65
  224. package/dist/page/tableMeasurements.d.ts +0 -18
  225. package/dist/page/tableMeasurements.js +0 -119
@@ -1,7 +1,7 @@
1
1
  // src/docx/comments/people.ts
2
2
  import { xmlnsAttr } from "../../ooxml/element.js";
3
- import { DocxExportError } from "../../ooxml/errors.js";
4
3
  import { xmlnsDecl } from "../../ooxml/names.js";
4
+ import { splicePart } from "../../ooxml/partSplice.js";
5
5
  import {
6
6
  attributeByLocalName,
7
7
  childByLocalName,
@@ -10,20 +10,9 @@ import {
10
10
  encodeUtf8,
11
11
  parseXml
12
12
  } from "../../ooxml/xml.js";
13
- import {
14
- directoryOf,
15
- readRelationships,
16
- relsPathOf,
17
- resolveTarget
18
- } from "../relationships.js";
19
- import {
20
- COMMENT_AUTHOR_PROVIDER,
21
- CONTENT_TYPES_PATH,
22
- PEOPLE_CONTENT_TYPE,
23
- PEOPLE_REL_TYPE,
24
- W15_NS
25
- } from "./constants.js";
26
- import { withContentType } from "./contentTypes.js";
13
+ import { relatedPartPath } from "../packageParts.js";
14
+ import { directoryOf } from "../relationships.js";
15
+ import { COMMENT_AUTHOR_PROVIDER, PEOPLE_REL_TYPE, W15_NS } from "./constants.js";
27
16
  import { renderPerson } from "./grammar.js";
28
17
  var NO_PEOPLE = {
29
18
  partPath: null,
@@ -32,11 +21,8 @@ var NO_PEOPLE = {
32
21
  byAuthor: /* @__PURE__ */ new Map()
33
22
  };
34
23
  function readPeople(parts, mainPartPath) {
35
- const relationship = readRelationships(parts, relsPathOf(mainPartPath)).find(
36
- (entry) => entry.type === PEOPLE_REL_TYPE && !entry.external
37
- );
38
- if (!relationship) return NO_PEOPLE;
39
- const partPath = resolveTarget(mainPartPath, relationship.target);
24
+ const partPath = relatedPartPath(parts, mainPartPath, PEOPLE_REL_TYPE);
25
+ if (partPath === null) return NO_PEOPLE;
40
26
  const bytes = parts.get(partPath);
41
27
  if (!bytes) return { ...NO_PEOPLE, partPath };
42
28
  const { text, hadBom } = decodeUtf8(bytes);
@@ -85,43 +71,6 @@ function w15Prefix(root) {
85
71
  const prefix = declaration.name.slice("xmlns:".length);
86
72
  return prefix === "" ? "" : `${prefix}:`;
87
73
  }
88
- function rootTagStart(xml) {
89
- let at = 0;
90
- for (; ; ) {
91
- const opens = xml.indexOf("<", at);
92
- if (opens === -1) return -1;
93
- if (xml.startsWith("<!--", opens)) {
94
- at = xml.indexOf("-->", opens + 4) + 3;
95
- continue;
96
- }
97
- if (xml.startsWith("<?", opens)) {
98
- at = xml.indexOf("?>", opens + 2) + 2;
99
- continue;
100
- }
101
- return opens;
102
- }
103
- }
104
- function openTagEnd(xml, at) {
105
- let quote = null;
106
- for (let i = at; i < xml.length; i += 1) {
107
- const character = xml[i];
108
- if (quote !== null) {
109
- if (character === quote) quote = null;
110
- continue;
111
- }
112
- if (character === '"' || character === "'") {
113
- quote = character;
114
- continue;
115
- }
116
- if (character === ">") {
117
- return { end: i + 1, selfClosing: xml[i - 1] === "/" };
118
- }
119
- }
120
- return null;
121
- }
122
- function malformed(detail) {
123
- return new DocxExportError("malformed-xml", `the people part ${detail}`);
124
- }
125
74
  function peopleXml(people, added) {
126
75
  const xml = people.xml;
127
76
  if (xml === null) {
@@ -131,11 +80,7 @@ function peopleXml(people, added) {
131
80
  );
132
81
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w15:people ${xmlnsDecl("w15")}>${persons2.join("")}</w15:people>`;
133
82
  }
134
- const root = parseXml(xml).documentElement;
135
- if (root.localName !== "people") {
136
- throw malformed("has no people root element");
137
- }
138
- const prefix = w15Prefix(root);
83
+ const prefix = w15Prefix(parseXml(xml).documentElement);
139
84
  const persons = Array.from(
140
85
  added,
141
86
  ([author, userId]) => renderPerson(
@@ -145,46 +90,24 @@ function peopleXml(people, added) {
145
90
  prefix === null ? xmlnsAttr("w15") : null
146
91
  )
147
92
  ).join("");
148
- const start = rootTagStart(xml);
149
- const open = start === -1 ? null : openTagEnd(xml, start);
150
- if (open === null) throw malformed("has no people root element");
151
- if (open.selfClosing) {
152
- return `${xml.slice(0, open.end - 2)}>${persons}</${root.nodeName}>` + xml.slice(open.end);
153
- }
154
- const close = xml.lastIndexOf(`</${root.nodeName}`);
155
- if (close === -1) throw malformed("has no closing people tag");
156
- return xml.slice(0, close) + persons + xml.slice(close);
157
- }
158
- function availablePeoplePath(session) {
159
- const directory = directoryOf(session.mainPartPath);
160
- for (let suffix = 0; ; suffix += 1) {
161
- const name = suffix === 0 ? "people.xml" : `people${suffix + 1}.xml`;
162
- const path = directory + name;
163
- if (!session.parts.has(path)) return path;
164
- }
93
+ return splicePart(xml, { root: "people", append: persons });
165
94
  }
166
- function planPeoplePart(bodies, session, relationships, currentContentTypes) {
95
+ function planPeoplePart(part, bodies, session, context) {
167
96
  const people = session.comments.people;
168
97
  const added = unrecordedAuthors(bodies, people);
169
98
  if (added.size === 0) return null;
170
99
  const parts = /* @__PURE__ */ new Map();
171
- const addingPart = people.partPath === null;
172
- const partPath = people.partPath ?? availablePeoplePath(session);
100
+ const addingPart = part.pathIn(session) === null;
101
+ const partPath = part.writePathIn(session);
173
102
  if (addingPart) {
174
- relationships.add({
175
- type: PEOPLE_REL_TYPE,
103
+ context.relationships.add({
104
+ type: part.relType,
176
105
  target: partPath.slice(directoryOf(session.mainPartPath).length)
177
106
  });
178
107
  }
179
108
  parts.set(partPath, encodeUtf8(peopleXml(people, added), people.hadBom));
180
109
  if (addingPart || people.xml === null) {
181
- const contentTypes = withContentType(
182
- session.parts,
183
- partPath,
184
- PEOPLE_CONTENT_TYPE,
185
- currentContentTypes
186
- );
187
- if (contentTypes) parts.set(CONTENT_TYPES_PATH, contentTypes);
110
+ context.contentTypes.addOverride(partPath, part.contentType);
188
111
  }
189
112
  return parts;
190
113
  }
@@ -192,5 +115,6 @@ export {
192
115
  NO_PEOPLE,
193
116
  commentAuthorId,
194
117
  planPeoplePart,
195
- readPeople
118
+ readPeople,
119
+ unrecordedAuthors
196
120
  };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The protection that takes comments and nothing else, as the policy both the part planners and
3
+ * the server verifier read (`../protectionPolicy`).
4
+ */
5
+ type StoryReason = "body-changed" | "comment-not-owned" | "comment-author-forged";
6
+ export declare const commentsPolicy: import("../protectionPolicy").ProtectionPolicy<StoryReason, "comment-markup-rejected">;
7
+ export {};
@@ -0,0 +1,43 @@
1
+ // src/docx/comments/policy.ts
2
+ import {
3
+ commentAdditionsBy,
4
+ commentEditsOwned,
5
+ commentIdentitiesKept,
6
+ withoutComments
7
+ } from "../../schema/protection.js";
8
+ import {
9
+ registerProtectionPolicy
10
+ } from "../protectionPolicy.js";
11
+ import { comparableStory } from "../storyProjection.js";
12
+ import { COMMENT_STORY_PARTS } from "./parts.js";
13
+ var refused = (reason) => ({ ok: false, reason });
14
+ function sameBody(before, after) {
15
+ const was = comparableStory(before, withoutComments);
16
+ const now = comparableStory(after, withoutComments);
17
+ return was !== null && now !== null && was.length === now.length && was.every((block, at) => block === now[at]);
18
+ }
19
+ function commentsStoryKept(before, after, authorId, { editableComments }) {
20
+ if (!sameBody(before, after)) return refused("body-changed");
21
+ if (!commentIdentitiesKept(before.doc, after.doc) || !commentAdditionsBy(before.doc, after.doc, authorId)) {
22
+ return refused("comment-author-forged");
23
+ }
24
+ if (!commentEditsOwned(before.doc, after.doc, {
25
+ protection: "comments",
26
+ authorId,
27
+ editableComments
28
+ })) {
29
+ return refused("comment-not-owned");
30
+ }
31
+ return { ok: true };
32
+ }
33
+ var commentsPolicy = registerProtectionPolicy(
34
+ {
35
+ level: "comments",
36
+ parts: COMMENT_STORY_PARTS,
37
+ rejectedMarkup: "comment-markup-rejected",
38
+ storyKept: commentsStoryKept
39
+ }
40
+ );
41
+ export {
42
+ commentsPolicy
43
+ };
@@ -7,7 +7,7 @@ import {
7
7
  serializeXml,
8
8
  W_NS
9
9
  } from "../../ooxml/xml.js";
10
- import { readRelationships, relsPathOf, resolveTarget } from "../relationships.js";
10
+ import { relatedPartPath } from "../packageParts.js";
11
11
  import { COMMENTS_EXTENDED_REL_TYPE, COMMENTS_REL_TYPE } from "./constants.js";
12
12
  import { lastBodyParagraph } from "./grammar.js";
13
13
  import {
@@ -43,10 +43,12 @@ function lastParagraphId(comment) {
43
43
  return last === null ? null : attributeByLocalName(last, "paraId");
44
44
  }
45
45
  function readCommentExtensions(parts, mainPartPath) {
46
- const relationship = readRelationships(parts, relsPathOf(mainPartPath)).find(
47
- (entry) => entry.type === COMMENTS_EXTENDED_REL_TYPE && !entry.external
46
+ const partPath = relatedPartPath(
47
+ parts,
48
+ mainPartPath,
49
+ COMMENTS_EXTENDED_REL_TYPE
48
50
  );
49
- if (!relationship) {
51
+ if (partPath === null) {
50
52
  return {
51
53
  partPath: null,
52
54
  xml: null,
@@ -55,7 +57,6 @@ function readCommentExtensions(parts, mainPartPath) {
55
57
  ordered: []
56
58
  };
57
59
  }
58
- const partPath = resolveTarget(mainPartPath, relationship.target);
59
60
  const bytes = parts.get(partPath);
60
61
  if (!bytes) {
61
62
  return {
@@ -97,11 +98,8 @@ function readComments(parts, mainPartPath) {
97
98
  extendedHadBom: extensions.hadBom,
98
99
  extendedOrdered: extensions.ordered
99
100
  };
100
- const relationship = readRelationships(parts, relsPathOf(mainPartPath)).find(
101
- (entry) => entry.type === COMMENTS_REL_TYPE && !entry.external
102
- );
103
- if (!relationship) return { ...NO_COMMENTS, ...aside };
104
- const partPath = resolveTarget(mainPartPath, relationship.target);
101
+ const partPath = relatedPartPath(parts, mainPartPath, COMMENTS_REL_TYPE);
102
+ if (partPath === null) return { ...NO_COMMENTS, ...aside };
105
103
  const bytes = parts.get(partPath);
106
104
  if (!bytes) return { ...NO_COMMENTS, partPath, ...aside };
107
105
  const { text, hadBom } = decodeUtf8(bytes);
@@ -2,13 +2,25 @@
2
2
  * Plans changed comment package parts while preserving untouched XML.
3
3
  */
4
4
  import type { Node as PMNode } from "prosemirror-model";
5
- import { type RelationshipWriter } from "../relationships";
5
+ import type { PartPlanner } from "../partPlan";
6
6
  import type { SessionStore } from "../session";
7
- export interface CommentPartChanges {
8
- parts: ReadonlyMap<string, Uint8Array>;
9
- }
7
+ import { type CommentReferenceData, type CommentReplyData } from "./model";
10
8
  /**
11
- * Plans the Comments part, relationship and content type only when comment state changed, and the
12
- * people part beside them for an author whose identity the document has yet to record.
9
+ * Whether the Comments part has to be rewritten: a comment or reply came, went, or changed, or an
10
+ * entry has to gain the key its thread state hangs off. The export invariants ask this as well, so
11
+ * a part the writer would leave alone is one they do not read.
13
12
  */
14
- export declare function planCommentParts(doc: PMNode, session: SessionStore, relationships: RelationshipWriter, currentContentTypes?: Uint8Array): CommentPartChanges | null;
13
+ export declare function commentsChanged(doc: PMNode, session: SessionStore): boolean;
14
+ /**
15
+ * Why the Comments part cannot be rewritten around its root, or null when it can.
16
+ *
17
+ * The same question stands in the export invariant list, so a refusal the writer raises is one a
18
+ * caller could have read there first, in the same words.
19
+ */
20
+ export declare function commentsRootProblem(xml: string): string | null;
21
+ /** Why the extended part cannot be rewritten around its root, or null when it can */
22
+ export declare function extensionsRootProblem(xml: string): string | null;
23
+ /** Whether the extended part has to be rewritten: thread state arrived, changed, or went with a deleted comment */
24
+ export declare function extensionsChanged(doc: PMNode, session: SessionStore): boolean;
25
+ export declare function currentCommentBodies(references: ReadonlyMap<string, CommentReferenceData>): ReadonlyMap<string, CommentReferenceData | CommentReplyData>;
26
+ export declare const commentsPlanner: PartPlanner;
@@ -1,17 +1,13 @@
1
1
  // src/docx/comments/writing.ts
2
2
  import { elementXml, xmlnsAttr } from "../../ooxml/element.js";
3
- import { DocxExportError } from "../../ooxml/errors.js";
4
- import { wName, xmlnsDecl } from "../../ooxml/names.js";
3
+ import { NAMESPACES, wName, xmlnsDecl } from "../../ooxml/names.js";
4
+ import {
5
+ ensureRootDeclarations,
6
+ partRootProblem,
7
+ splicePart
8
+ } from "../../ooxml/partSplice.js";
5
9
  import { encodeUtf8 } from "../../ooxml/xml.js";
6
10
  import { directoryOf } from "../relationships.js";
7
- import {
8
- COMMENTS_CONTENT_TYPE,
9
- COMMENTS_EXTENDED_CONTENT_TYPE,
10
- COMMENTS_EXTENDED_REL_TYPE,
11
- COMMENTS_REL_TYPE,
12
- CONTENT_TYPES_PATH
13
- } from "./constants.js";
14
- import { withContentType } from "./contentTypes.js";
15
11
  import {
16
12
  arrivedEntries,
17
13
  renderCommentBody,
@@ -21,6 +17,7 @@ import {
21
17
  import {
22
18
  commentReferencesIn
23
19
  } from "./model.js";
20
+ import { commentsExtendedPart, commentsPart, peoplePart } from "./parts.js";
24
21
  import { planPeoplePart } from "./people.js";
25
22
  function needsThreadKey(id, comment, session) {
26
23
  return comment.imported && carriesThreadMetadata(comment) && (session.comments.byId.get(id)?.paraId ?? null) === null;
@@ -77,34 +74,18 @@ function extensionsXml(references, comments, originallyReferenced) {
77
74
  if (comments.extendedXml === null) {
78
75
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w15:commentsEx ${xmlnsDecl("w15")}>${pieces.join("")}</w15:commentsEx>`;
79
76
  }
80
- const open = /<(?:[^\s<>/:="']+:)?commentsEx\b[^>]*>/.exec(
81
- comments.extendedXml
82
- );
83
- if (!open) {
84
- throw new DocxExportError(
85
- "malformed-xml",
86
- "the Comments Extended part has no commentsEx root element"
87
- );
88
- }
89
- const name = /^<([^\s>]+)/.exec(open[0])?.[1];
90
- if (name === void 0) {
91
- throw new DocxExportError(
92
- "malformed-xml",
93
- "the Comments Extended part has no commentsEx root element"
94
- );
95
- }
96
- const bodyAt = open.index + open[0].length;
97
- if (open[0].endsWith("/>")) {
98
- return comments.extendedXml.slice(0, open.index) + `${open[0].slice(0, -2)}>${pieces.join("")}</${name}>` + comments.extendedXml.slice(bodyAt);
99
- }
100
- const close = comments.extendedXml.lastIndexOf(`</${name}>`);
101
- if (close === -1) {
102
- throw new DocxExportError(
103
- "malformed-xml",
104
- "the Comments Extended part has no closing commentsEx tag"
105
- );
106
- }
107
- return comments.extendedXml.slice(0, bodyAt) + pieces.join("") + comments.extendedXml.slice(close);
77
+ return splicePart(comments.extendedXml, {
78
+ root: EXTENSIONS_ROOT,
79
+ replaceChildren: pieces.join("")
80
+ });
81
+ }
82
+ var COMMENTS_ROOT = "comments";
83
+ var EXTENSIONS_ROOT = "commentsEx";
84
+ function commentsRootProblem(xml) {
85
+ return partRootProblem(xml, COMMENTS_ROOT);
86
+ }
87
+ function extensionsRootProblem(xml) {
88
+ return partRootProblem(xml, EXTENSIONS_ROOT);
108
89
  }
109
90
  function extensionsChanged(doc, session) {
110
91
  const current = commentReferencesIn(doc);
@@ -150,23 +131,10 @@ function renderedComment(comment, arrivedKeyed, arrived) {
150
131
  [body]
151
132
  );
152
133
  }
153
- function withThreadMarkupCompatibility(openTag) {
154
- let updated = openTag;
155
- if (!/\sxmlns:w14\s*=/.test(updated)) {
156
- updated = updated.replace(/>$/, ` ${xmlnsDecl("w14")}>`);
157
- }
158
- if (!/\sxmlns:mc\s*=/.test(updated)) {
159
- updated = updated.replace(/>$/, ` ${xmlnsDecl("mc")}>`);
160
- }
161
- const ignorable = /\smc:Ignorable\s*=\s*(["'])([^"']*)\1/.exec(updated);
162
- if (!ignorable) {
163
- return updated.replace(/>$/, ' mc:Ignorable="w14">');
164
- }
165
- const tokens = ignorable[2].split(/\s+/).filter(Boolean);
166
- if (tokens.includes("w14")) return updated;
167
- const replacement = ` mc:Ignorable=${ignorable[1]}${[...tokens, "w14"].join(" ")}${ignorable[1]}`;
168
- return updated.slice(0, ignorable.index) + replacement + updated.slice(ignorable.index + ignorable[0].length);
169
- }
134
+ var THREAD_MARKUP = {
135
+ namespaces: { w14: NAMESPACES.w14, mc: NAMESPACES.mc },
136
+ ignorable: ["w14"]
137
+ };
170
138
  function currentCommentBodies(references) {
171
139
  const comments = /* @__PURE__ */ new Map();
172
140
  for (const [id, comment] of references) {
@@ -224,51 +192,24 @@ function commentsXml(references, comments, originallyReferenced) {
224
192
  const compatibility = hasThreadMetadata ? ` ${xmlnsDecl("w14")} ${xmlnsDecl("mc")} mc:Ignorable="w14"` : "";
225
193
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:comments ${xmlnsDecl("w")}${compatibility}>${pieces.join("")}</w:comments>`;
226
194
  }
227
- const open = /<(?:[^\s<>/:="']+:)?comments\b[^>]*>/.exec(comments.xml);
228
- if (!open) {
229
- throw new DocxExportError(
230
- "malformed-xml",
231
- "the Comments part has no comments root element"
232
- );
233
- }
234
- const name = /^<([^\s>]+)/.exec(open[0])?.[1];
235
- const close = name ? comments.xml.lastIndexOf(`</${name}>`) : -1;
236
- if (close === -1) {
237
- throw new DocxExportError(
238
- "malformed-xml",
239
- "the Comments part has no closing comments tag"
240
- );
241
- }
242
- const openTag = hasThreadMetadata ? withThreadMarkupCompatibility(open[0]) : open[0];
243
- return comments.xml.slice(0, open.index) + openTag + pieces.join("") + comments.xml.slice(close);
195
+ const rewritten = splicePart(comments.xml, {
196
+ root: COMMENTS_ROOT,
197
+ replaceChildren: pieces.join("")
198
+ });
199
+ return hasThreadMetadata ? ensureRootDeclarations(rewritten, THREAD_MARKUP) : rewritten;
244
200
  }
245
- function availableCommentsPath(session) {
246
- const directory = directoryOf(session.mainPartPath);
247
- for (let suffix = 0; ; suffix += 1) {
248
- const name = suffix === 0 ? "comments.xml" : `comments${suffix + 1}.xml`;
249
- const path = directory + name;
250
- if (!session.parts.has(path)) return path;
251
- }
252
- }
253
- function availableCommentsExtendedPath(session) {
254
- const directory = directoryOf(session.mainPartPath);
255
- for (let suffix = 0; ; suffix += 1) {
256
- const name = suffix === 0 ? "commentsExtended.xml" : `commentsExtended${suffix + 1}.xml`;
257
- const path = directory + name;
258
- if (!session.parts.has(path)) return path;
259
- }
260
- }
261
- function planCommentParts(doc, session, relationships, currentContentTypes) {
201
+ function planCommentParts(doc, session, context) {
262
202
  const bodyChanged = commentsChanged(doc, session);
263
203
  const threadChanged = extensionsChanged(doc, session);
264
204
  if (!bodyChanged && !threadChanged) return null;
205
+ const { relationships, contentTypes } = context;
265
206
  const references = commentReferencesIn(doc);
266
- const addingPart = session.comments.partPath === null;
267
- const partPath = session.comments.partPath ?? availableCommentsPath(session);
207
+ const addingPart = commentsPart.pathIn(session) === null;
208
+ const partPath = commentsPart.writePathIn(session);
268
209
  const parts = /* @__PURE__ */ new Map();
269
210
  if (addingPart) {
270
211
  const target = partPath.slice(directoryOf(session.mainPartPath).length);
271
- relationships.add({ type: COMMENTS_REL_TYPE, target });
212
+ relationships.add({ type: commentsPart.relType, target });
272
213
  }
273
214
  if (bodyChanged) {
274
215
  parts.set(
@@ -280,22 +221,16 @@ function planCommentParts(doc, session, relationships, currentContentTypes) {
280
221
  );
281
222
  }
282
223
  if (addingPart || session.comments.xml === null) {
283
- const contentTypes = withContentType(
284
- session.parts,
285
- partPath,
286
- COMMENTS_CONTENT_TYPE,
287
- currentContentTypes
288
- );
289
- if (contentTypes) parts.set(CONTENT_TYPES_PATH, contentTypes);
224
+ contentTypes.addOverride(partPath, commentsPart.contentType);
290
225
  }
291
226
  if (threadChanged && (references.size > 0 || session.comments.extendedPartPath !== null)) {
292
- const addingExtendedPart = session.comments.extendedPartPath === null;
293
- const extendedPartPath = session.comments.extendedPartPath ?? availableCommentsExtendedPath(session);
227
+ const addingExtendedPart = commentsExtendedPart.pathIn(session) === null;
228
+ const extendedPartPath = commentsExtendedPart.writePathIn(session);
294
229
  if (addingExtendedPart) {
295
230
  const target = extendedPartPath.slice(
296
231
  directoryOf(session.mainPartPath).length
297
232
  );
298
- relationships.add({ type: COMMENTS_EXTENDED_REL_TYPE, target });
233
+ relationships.add({ type: commentsExtendedPart.relType, target });
299
234
  }
300
235
  parts.set(
301
236
  extendedPartPath,
@@ -309,26 +244,32 @@ function planCommentParts(doc, session, relationships, currentContentTypes) {
309
244
  )
310
245
  );
311
246
  if (addingExtendedPart || session.comments.extendedXml === null) {
312
- const contentTypes = withContentType(
313
- session.parts,
247
+ contentTypes.addOverride(
314
248
  extendedPartPath,
315
- COMMENTS_EXTENDED_CONTENT_TYPE,
316
- parts.get(CONTENT_TYPES_PATH) ?? currentContentTypes
249
+ commentsExtendedPart.contentType
317
250
  );
318
- if (contentTypes) parts.set(CONTENT_TYPES_PATH, contentTypes);
319
251
  }
320
252
  }
321
253
  if (bodyChanged) {
322
254
  const people = planPeoplePart(
255
+ peoplePart,
323
256
  currentCommentBodies(references).values(),
324
257
  session,
325
- relationships,
326
- parts.get(CONTENT_TYPES_PATH) ?? currentContentTypes
258
+ context
327
259
  );
328
260
  for (const [path, bytes] of people ?? []) parts.set(path, bytes);
329
261
  }
330
- return { parts };
262
+ return parts;
331
263
  }
264
+ var commentsPlanner = {
265
+ name: "comments",
266
+ plan: planCommentParts
267
+ };
332
268
  export {
333
- planCommentParts
269
+ commentsChanged,
270
+ commentsPlanner,
271
+ commentsRootProblem,
272
+ currentCommentBodies,
273
+ extensionsChanged,
274
+ extensionsRootProblem
334
275
  };
@@ -2,3 +2,10 @@
2
2
  export declare const DEFAULT_TAB_STOP_PT = 36;
3
3
  /** Reads the document-wide automatic tab interval from settings.xml. */
4
4
  export declare function readDefaultTabStop(settings: Document | null): number | null;
5
+ /** The compatibility settings (`w:compat`) the display reads */
6
+ export interface CompatSettings {
7
+ /** Part 4 §14.8.3.31: no custom tab stop is created at a hanging indent */
8
+ noTabHangInd: boolean;
9
+ }
10
+ export declare const NO_COMPAT: CompatSettings;
11
+ export declare function readCompatSettings(settings: Document | null): CompatSettings;
@@ -1,14 +1,22 @@
1
1
  // src/docx/documentSettings.ts
2
- import { twipsToPt, wAttr } from "../ooxml/units.js";
2
+ import { ST_TwipsMeasure } from "../ooxml/simpleTypes.js";
3
+ import { isOn, twipsToPt, wAttr } from "../ooxml/units.js";
3
4
  import { childByLocalName } from "../ooxml/xml.js";
4
5
  var DEFAULT_TAB_STOP_PT = 36;
5
6
  function readDefaultTabStop(settings) {
6
7
  if (!settings) return null;
7
8
  const setting = childByLocalName(settings.documentElement, "defaultTabStop");
8
- const value = setting ? twipsToPt(wAttr(setting, "val")) : null;
9
+ const value = setting ? twipsToPt(ST_TwipsMeasure.parse(wAttr(setting, "val"))) : null;
9
10
  return value !== null && value > 0 ? value : null;
10
11
  }
12
+ var NO_COMPAT = { noTabHangInd: false };
13
+ function readCompatSettings(settings) {
14
+ const compat = settings ? childByLocalName(settings.documentElement, "compat") : null;
15
+ return { noTabHangInd: compat !== null && isOn(compat, "noTabHangInd") };
16
+ }
11
17
  export {
12
18
  DEFAULT_TAB_STOP_PT,
19
+ NO_COMPAT,
20
+ readCompatSettings,
13
21
  readDefaultTabStop
14
22
  };
@@ -1,15 +1,50 @@
1
1
  /**
2
2
  * Turns the edited document back into docx bytes.
3
3
  *
4
- * The body (document.xml) is always rebuilt from preserved and edited blocks. numbering.xml is
5
- * rewritten only when a list was newly started, media parts only when an image was inserted, and
6
- * the Comments part only when comments changed. Content types and relationships change only when
7
- * one of those additions needs them.
4
+ * The body (document.xml) is always rebuilt from preserved and edited blocks. Every other part is
5
+ * written by a planner (`./partPlan`) that answers only when the document gives it something to
6
+ * write: numbering.xml when a list was newly started, the comment parts when comments changed,
7
+ * media parts when an image was inserted. Content types and relationships change only when one
8
+ * of those additions declares itself through the context every planner shares.
8
9
  *
9
10
  * The body is written before that relationships part, because a link asks for its relationship as
10
11
  * it is written (`docx/hyperlink`), and both writers hand out ids through the one writer so that
11
12
  * they cannot pick the same one.
12
13
  */
13
14
  import type { Node as PMNode } from "prosemirror-model";
15
+ import { type XmlParser } from "../ooxml/xml";
16
+ import { type FidelityNote } from "./fidelity";
17
+ import { type PartPlanner } from "./partPlan";
14
18
  import { type DocxSession } from "./session";
15
- export declare function exportDocx(doc: PMNode, session: DocxSession): Uint8Array;
19
+ /** What a caller may say about a write beyond handing over the document and its session */
20
+ export interface ExportOptions {
21
+ /**
22
+ * The parser the original XML kept in the session is read back through. Left out, the
23
+ * `DOMParser` global is used, and a runtime carrying none refuses the write with
24
+ * `no-xml-parser`.
25
+ */
26
+ xmlParser?: XmlParser;
27
+ }
28
+ export declare function exportDocx(doc: PMNode, session: DocxSession, options?: ExportOptions): Uint8Array;
29
+ /**
30
+ * The file to write, and what writing it could not carry across as it stood.
31
+ *
32
+ * The notes are the ones `documentFidelity` reads off the same document, followed by whatever the
33
+ * writer had to approximate on the way out. A host that hands a file to somebody else can say what
34
+ * that file no longer holds without opening it again.
35
+ *
36
+ * The first problem `exportProblems` reports is thrown before anything is written, so a refusal a
37
+ * caller could have asked about ahead of time arrives with the same code and message it would
38
+ * have read there.
39
+ */
40
+ export declare function exportDocxReport(doc: PMNode, session: DocxSession, options?: ExportOptions): {
41
+ bytes: Uint8Array;
42
+ notes: FidelityNote[];
43
+ };
44
+ /**
45
+ * The same export, folding a planner list of the caller's choosing in place of `PART_PLANNERS`.
46
+ *
47
+ * On no entry point. No document can make a correct planner write a part that does not read back,
48
+ * so this is how a test hands the fold such a planner and watches the export refuse it.
49
+ */
50
+ export declare function exportThroughPlanners(planners: readonly PartPlanner[], doc: PMNode, session: DocxSession, options?: ExportOptions): Uint8Array;