@portone/docx-editor 0.4.0 → 0.5.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 (155) hide show
  1. package/CHANGELOG.md +159 -0
  2. package/assets/editor.png +0 -0
  3. package/dist/DocxEditor.d.ts +1 -1
  4. package/dist/DocxEditor.js +44 -25
  5. package/dist/docx/cloning.js +4 -9
  6. package/dist/docx/commentOnlyChange.d.ts +5 -3
  7. package/dist/docx/comments/grammar.d.ts +27 -10
  8. package/dist/docx/comments/grammar.js +18 -67
  9. package/dist/docx/comments/model.d.ts +5 -6
  10. package/dist/docx/comments/model.js +0 -9
  11. package/dist/docx/comments/parts.d.ts +16 -10
  12. package/dist/docx/comments/parts.js +65 -13
  13. package/dist/docx/comments/people.d.ts +11 -3
  14. package/dist/docx/comments/people.js +20 -6
  15. package/dist/docx/comments/policy.js +7 -1
  16. package/dist/docx/comments/reading.d.ts +4 -2
  17. package/dist/docx/comments/reading.js +1 -14
  18. package/dist/docx/comments/writing.js +65 -28
  19. package/dist/docx/exportDocx.js +10 -14
  20. package/dist/docx/exportRefs.d.ts +8 -3
  21. package/dist/docx/exportRefs.js +2 -1
  22. package/dist/docx/fidelity.js +24 -10
  23. package/dist/docx/fields.d.ts +42 -0
  24. package/dist/docx/fields.js +76 -0
  25. package/dist/docx/headersFooters.d.ts +89 -20
  26. package/dist/docx/headersFooters.js +163 -172
  27. package/dist/docx/importDocx.d.ts +1 -1
  28. package/dist/docx/importDocx.js +100 -48
  29. package/dist/docx/importParagraph.d.ts +16 -4
  30. package/dist/docx/importParagraph.js +87 -93
  31. package/dist/docx/importPolicy.d.ts +70 -0
  32. package/dist/docx/importPolicy.js +190 -0
  33. package/dist/docx/importPreserved.d.ts +30 -0
  34. package/dist/docx/importPreserved.js +54 -0
  35. package/dist/docx/importTable.js +56 -12
  36. package/dist/docx/invariants.js +16 -18
  37. package/dist/docx/notes.d.ts +6 -1
  38. package/dist/docx/notes.js +6 -16
  39. package/dist/docx/pageGeometry.d.ts +5 -14
  40. package/dist/docx/pageGeometry.js +0 -12
  41. package/dist/docx/protectionPolicy.d.ts +13 -5
  42. package/dist/docx/protectionPolicy.js +32 -22
  43. package/dist/docx/scan.d.ts +18 -8
  44. package/dist/docx/scan.js +17 -11
  45. package/dist/docx/sections.d.ts +126 -0
  46. package/dist/docx/sections.js +207 -0
  47. package/dist/docx/serializeBlock.d.ts +13 -4
  48. package/dist/docx/serializeBlock.js +16 -18
  49. package/dist/docx/serializeParagraph.d.ts +4 -0
  50. package/dist/docx/serializeParagraph.js +1 -0
  51. package/dist/docx/serializePreserved.d.ts +14 -0
  52. package/dist/docx/serializePreserved.js +24 -0
  53. package/dist/docx/serializeStory.d.ts +17 -0
  54. package/dist/docx/serializeStory.js +17 -0
  55. package/dist/docx/serializeTable.js +10 -13
  56. package/dist/docx/session.d.ts +23 -6
  57. package/dist/docx/session.js +26 -5
  58. package/dist/docx/story.d.ts +140 -0
  59. package/dist/docx/story.js +237 -0
  60. package/dist/docx/storyProjection.js +6 -1
  61. package/dist/editor/clipboard/htmlReader.d.ts +30 -0
  62. package/dist/editor/{externalClipboard.js → clipboard/htmlReader.js} +39 -223
  63. package/dist/editor/clipboard/internalChannel.d.ts +57 -0
  64. package/dist/editor/clipboard/internalChannel.js +58 -0
  65. package/dist/editor/clipboard/normalizers.d.ts +99 -0
  66. package/dist/editor/clipboard/normalizers.js +199 -0
  67. package/dist/editor/clipboard/parser.d.ts +34 -0
  68. package/dist/editor/clipboard/parser.js +58 -0
  69. package/dist/editor/clipboard/plugin.d.ts +21 -0
  70. package/dist/editor/clipboard/plugin.js +320 -0
  71. package/dist/editor/clipboard/readContext.d.ts +24 -0
  72. package/dist/editor/clipboard/readContext.js +19 -0
  73. package/dist/editor/clipboard/readers.d.ts +33 -0
  74. package/dist/editor/clipboard/readers.js +37 -0
  75. package/dist/editor/commands/comments/editing.d.ts +26 -6
  76. package/dist/editor/commands/comments/editing.js +175 -105
  77. package/dist/editor/commands/comments/model.d.ts +34 -17
  78. package/dist/editor/commands/comments/model.js +11 -0
  79. package/dist/editor/commands/comments/reading.d.ts +5 -3
  80. package/dist/editor/commands/comments/reading.js +8 -58
  81. package/dist/editor/commands/index.d.ts +2 -2
  82. package/dist/editor/commands/index.js +2 -0
  83. package/dist/editor/commands/listCommands.d.ts +6 -0
  84. package/dist/editor/commands/listCommands.js +3 -2
  85. package/dist/editor/commands/noteQueries.d.ts +22 -5
  86. package/dist/editor/commands/noteQueries.js +34 -6
  87. package/dist/editor/createEditor.js +13 -3
  88. package/dist/editor/documentStyles.d.ts +26 -4
  89. package/dist/editor/documentStyles.js +9 -4
  90. package/dist/editor/imageFiles.d.ts +2 -2
  91. package/dist/editor/imageFiles.js +2 -0
  92. package/dist/editor/insertTable.d.ts +1 -1
  93. package/dist/editor/insertTable.js +2 -2
  94. package/dist/editor/plainText.d.ts +12 -0
  95. package/dist/editor/plainText.js +2 -1
  96. package/dist/editor/plugins/columnResize.js +6 -3
  97. package/dist/editor/plugins/commentComposer.d.ts +28 -0
  98. package/dist/editor/plugins/commentComposer.js +46 -0
  99. package/dist/editor/plugins/commentDecorations.d.ts +29 -2
  100. package/dist/editor/plugins/commentDecorations.js +89 -15
  101. package/dist/editor/plugins/documentProjection.d.ts +31 -0
  102. package/dist/editor/plugins/documentProjection.js +22 -0
  103. package/dist/editor/plugins/imagePaste.js +15 -8
  104. package/dist/editor/plugins/linkPanel.d.ts +7 -3
  105. package/dist/editor/plugins/linkPanel.js +13 -27
  106. package/dist/editor/plugins/panelState.d.ts +67 -0
  107. package/dist/editor/plugins/panelState.js +54 -0
  108. package/dist/editor/plugins/tableContextMenu.d.ts +1 -1
  109. package/dist/editor/plugins/tableContextMenu.js +30 -47
  110. package/dist/editor/plugins/textContextMenu.d.ts +1 -1
  111. package/dist/editor/plugins/textContextMenu.js +30 -47
  112. package/dist/ooxml/fragment.d.ts +11 -0
  113. package/dist/ooxml/fragment.js +14 -4
  114. package/dist/ooxml/image.d.ts +3 -4
  115. package/dist/ooxml/image.js +50 -5
  116. package/dist/ooxml/names.d.ts +6 -0
  117. package/dist/ooxml/names.js +2 -0
  118. package/dist/ooxml/props.d.ts +8 -0
  119. package/dist/ooxml/props.js +14 -4
  120. package/dist/ooxml/rangeMarkers.d.ts +5 -0
  121. package/dist/ooxml/rangeMarkers.js +24 -0
  122. package/dist/page/PageGuides.d.ts +7 -3
  123. package/dist/page/PageGuides.js +13 -24
  124. package/dist/page/pageLayout.d.ts +64 -5
  125. package/dist/page/pageLayout.js +90 -29
  126. package/dist/page/usePageLayout.d.ts +14 -4
  127. package/dist/page/usePageLayout.js +19 -20
  128. package/dist/schema/attrRoles.js +38 -20
  129. package/dist/schema/docxSchema.d.ts +9 -1
  130. package/dist/schema/docxSchema.js +191 -139
  131. package/dist/schema/editGuard.d.ts +1 -1
  132. package/dist/schema/guards.js +2 -2
  133. package/dist/schema/index.d.ts +1 -0
  134. package/dist/schema/preservedFragments.d.ts +12 -0
  135. package/dist/schema/preservedFragments.js +38 -0
  136. package/dist/schema/preservedGuards.d.ts +9 -7
  137. package/dist/schema/preservedGuards.js +112 -11
  138. package/dist/schema/protection.d.ts +34 -5
  139. package/dist/schema/protection.js +42 -14
  140. package/dist/schema/rendering.js +3 -1
  141. package/dist/schema/stories.d.ts +49 -0
  142. package/dist/schema/stories.js +78 -0
  143. package/dist/styles/classNames.d.ts +3 -5
  144. package/dist/styles/classNames.js +3 -5
  145. package/dist/styles.css +74 -12
  146. package/dist/table/resize.d.ts +5 -3
  147. package/dist/ui/CommentsPanel.d.ts +1 -3
  148. package/dist/ui/CommentsPanel.js +24 -15
  149. package/dist/ui/NotesPanel.js +2 -2
  150. package/dist/ui/TextMenu.d.ts +1 -3
  151. package/dist/ui/TextMenu.js +10 -6
  152. package/dist/ui/comments/CommentComposer.d.ts +6 -1
  153. package/dist/ui/comments/CommentComposer.js +26 -3
  154. package/package.json +1 -1
  155. package/dist/editor/externalClipboard.d.ts +0 -22
@@ -26,9 +26,6 @@ function importedCommentReplies(comments, rootId) {
26
26
  authorId: reply.authorId,
27
27
  initials: reply.initials,
28
28
  date: reply.date,
29
- text: reply.text,
30
- commentXml: reply.xml,
31
- imported: true,
32
29
  paraId,
33
30
  parentParaId,
34
31
  extensionXml: reply.extensionXml
@@ -59,9 +56,6 @@ function replyData(value) {
59
56
  authorId: nullableString(entry.authorId),
60
57
  initials: nullableString(entry.initials),
61
58
  date: nullableString(entry.date),
62
- text: nullableString(entry.text) ?? "",
63
- commentXml: nullableString(entry.commentXml),
64
- imported: entry.imported === true,
65
59
  paraId,
66
60
  parentParaId,
67
61
  extensionXml: nullableString(entry.extensionXml)
@@ -79,9 +73,6 @@ function referenceData(node) {
79
73
  authorId: nullableString(node.attrs.authorId),
80
74
  initials: nullableString(node.attrs.initials),
81
75
  date: nullableString(node.attrs.date),
82
- text: nullableString(node.attrs.text) ?? "",
83
- commentXml: nullableString(node.attrs.commentXml),
84
- imported: node.attrs.imported === true,
85
76
  paraId: nullableString(node.attrs.paraId) ?? "00000001",
86
77
  resolved: node.attrs.resolved === true,
87
78
  extensionXml: nullableString(node.attrs.extensionXml),
@@ -11,16 +11,17 @@
11
11
  * have made the change. Asking them as one would refuse a moderator rewriting somebody else's
12
12
  * comment under `editableComments: "all"`, which is a thing the editor lets them do.
13
13
  */
14
- import type { EditableComments } from "../../schema/protection";
14
+ import { type EditableComments } from "../../schema/protection";
15
15
  import type { StoryPartKind } from "../protectionPolicy";
16
16
  import { type ImportedPeople } from "./people";
17
17
  /**
18
18
  * Whether this editor's writer could have put the entry out, whoever it belongs to.
19
19
  *
20
- * Shape alone: which attributes it carries and what stands inside it. Who may have written it is
21
- * `entryAllowed`'s question. Each kind is read where it is written (`./grammar`).
20
+ * Shape alone: which attributes it carries and what stands inside it, judged against the entry the
21
+ * original file held under the same id. Who may have written it is `entryAllowed`'s question.
22
+ * Each kind is read where it is written (`./grammar`).
22
23
  */
23
- export declare function wellFormedEntry(entry: Element): boolean;
24
+ export declare function wellFormedEntry(entry: Element, original: Element | null): boolean;
24
25
  /**
25
26
  * Whether the only thing between the two is that a thread gained its key.
26
27
  *
@@ -34,13 +35,18 @@ export declare function threadKeyAlone(entry: Element, original: Element): boole
34
35
  * Whether `authorId` could have written or rewritten this entry.
35
36
  *
36
37
  * Permission alone: whether the shape is one this editor writes is `wellFormedEntry`'s question.
37
- * An entry that appeared has to be this author's. One that arrived keeps the identity it arrived
38
- * with; settling its thread or replying to it belong to everyone and leave what it says alone,
39
- * while rewriting what it says is its author's, a moderator's, or anyone's where no identity was
40
- * recorded for it. That last is the rule the editor holds to (`schema/protection`), and the two
41
- * have to answer alike or a file the editor wrote would be turned down here.
38
+ * An entry that appeared has to be one this author could have written, which
39
+ * `commentAdditionAllowed` decides here as it does over the story. One that arrived keeps the
40
+ * identity it arrived with; settling its thread or replying to it belong to everyone and leave
41
+ * what it says alone, while rewriting what it says is its author's, a moderator's, or anyone's
42
+ * where no identity was recorded for it. Both are the rules the editor holds to
43
+ * (`schema/protection`), and the two have to answer alike or a file the editor wrote would be
44
+ * turned down here.
45
+ *
46
+ * `unattributed` is read from the file that arrived, since it says what the submission is judged
47
+ * against; `people` is the submission's, since it says who the submission names.
42
48
  */
43
- export declare function entryAllowed(entry: Element, original: Element | null, authorId: string, editableComments: EditableComments, people: ImportedPeople): boolean;
49
+ export declare function entryAllowed(entry: Element, original: Element | null, authorId: string, editableComments: EditableComments, people: ImportedPeople, unattributed: ReadonlySet<string>): boolean;
44
50
  export declare const commentsPart: StoryPartKind;
45
51
  export declare const commentsExtendedPart: StoryPartKind;
46
52
  export declare const peoplePart: StoryPartKind;
@@ -7,7 +7,16 @@ import {
7
7
  serializeXml,
8
8
  W_NS
9
9
  } from "../../ooxml/xml.js";
10
+ import { isPreservedNode } from "../../schema/preservedFragments.js";
11
+ import {
12
+ commentAdditionAllowed
13
+ } from "../../schema/protection.js";
14
+ import { NO_FORMATTING } from "../formatting.js";
15
+ import { NO_IMPORT_SOURCES } from "../importParagraph.js";
10
16
  import { availablePartPath } from "../packageParts.js";
17
+ import { serializeBlock } from "../serializeBlock.js";
18
+ import { buildBlock } from "../story.js";
19
+ import { isModelledBlock } from "../storyProjection.js";
11
20
  import {
12
21
  COMMENTS_CONTENT_TYPE,
13
22
  COMMENTS_EXTENDED_CONTENT_TYPE,
@@ -21,9 +30,11 @@ import {
21
30
  import {
22
31
  attributesWithin,
23
32
  COMMENT_ATTRIBUTES,
33
+ declarationsWritten,
24
34
  declarationWritten,
35
+ holdsElementsAndLayout,
36
+ isLayoutText,
25
37
  lastBodyParagraph,
26
- readStrictCommentBody,
27
38
  recordedIdentity,
28
39
  wellFormedCommentExtension,
29
40
  wellFormedPerson
@@ -42,9 +53,46 @@ function sameAttributes(entry, original, names) {
42
53
  (name) => attributeByLocalName(entry, name) === attributeByLocalName(original, name)
43
54
  );
44
55
  }
45
- function wellFormedEntry(entry) {
56
+ function holdsPreserved(node) {
57
+ if (isPreservedNode(node)) return true;
58
+ let found = false;
59
+ node.descendants((child) => {
60
+ if (isPreservedNode(child)) found = true;
61
+ return !found;
62
+ });
63
+ return found;
64
+ }
65
+ var BLOCK_DECLARATIONS = Object.entries(NAMESPACES).map(([prefix, uri]) => `xmlns:${prefix}="${uri}"`).join(" ");
66
+ function normalizedBlock(xml) {
67
+ try {
68
+ const root = parseXml(
69
+ `<b ${BLOCK_DECLARATIONS}>${xml}</b>`
70
+ ).documentElement;
71
+ const [block] = Array.from(root.children);
72
+ return block === void 0 || root.children.length !== 1 ? null : serializeXml(block);
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+ function writtenBlock(el) {
78
+ if (!declarationsWritten(el)) return false;
79
+ const node = buildBlock(el, "", NO_IMPORT_SOURCES, NO_FORMATTING);
80
+ if (!isModelledBlock(node) || holdsPreserved(node)) return false;
81
+ const written = normalizedBlock(serializeBlock(node));
82
+ return written !== null && written === normalizedBlock(serializeXml(el));
83
+ }
84
+ function wellFormedStoryBody(entry, original) {
85
+ if (!holdsElementsAndLayout(entry)) return false;
86
+ const kept = new Set(
87
+ original === null ? [] : Array.from(original.children, serializeXml)
88
+ );
89
+ return Array.from(entry.children).every(
90
+ (block) => kept.has(serializeXml(block)) || writtenBlock(block)
91
+ );
92
+ }
93
+ function wellFormedEntry(entry, original) {
46
94
  if (entry.namespaceURI === W_NS && entry.localName === "comment") {
47
- return attributesWithin(entry, COMMENT_ATTRIBUTES) && readStrictCommentBody(entry) !== null;
95
+ return attributesWithin(entry, COMMENT_ATTRIBUTES) && wellFormedStoryBody(entry, original);
48
96
  }
49
97
  if (entry.namespaceURI === W15_NS && entry.localName === "commentEx") {
50
98
  return wellFormedCommentExtension(entry);
@@ -67,11 +115,17 @@ function threadKeyAlone(entry, original) {
67
115
  const kept = before === null || after === before;
68
116
  return kept && withoutThreadKey(entry) === withoutThreadKey(original);
69
117
  }
70
- function entryAllowed(entry, original, authorId, editableComments, people) {
118
+ function entryAllowed(entry, original, authorId, editableComments, people, unattributed) {
71
119
  if (entry.namespaceURI === W_NS && entry.localName === "comment") {
72
120
  const author = attributeByLocalName(entry, "author");
73
121
  const recorded = author === null ? null : commentAuthorId(people, author);
74
- if (original === null) return recorded === authorId;
122
+ if (original === null) {
123
+ return commentAdditionAllowed(
124
+ { author, authorId: recorded },
125
+ authorId,
126
+ unattributed
127
+ );
128
+ }
75
129
  const paraId = lastParagraphId(original);
76
130
  if (!sameAttributes(entry, original, COMMENT_IDENTITY) || paraId !== null && lastParagraphId(entry) !== paraId) {
77
131
  return false;
@@ -83,23 +137,20 @@ function entryAllowed(entry, original, authorId, editableComments, people) {
83
137
  return sameAttributes(entry, original, ["paraId", "paraIdParent"]);
84
138
  }
85
139
  if (entry.namespaceURI === W15_NS && entry.localName === "person") {
86
- return original === null && recordedIdentity(entry) === authorId;
140
+ const author = attributeByLocalName(entry, "author");
141
+ return original === null && recordedIdentity(entry) === authorId && author !== null && !unattributed.has(author);
87
142
  }
88
143
  return false;
89
144
  }
90
- var TEXT_NODE = 3;
91
145
  var COMPATIBILITY = /* @__PURE__ */ new Map([
92
146
  ["xmlns:w14", NAMESPACES.w14],
93
147
  ["xmlns:mc", NAMESPACES.mc]
94
148
  ]);
95
149
  var IGNORABLE = "mc:Ignorable";
96
150
  var IGNORED = "w14";
97
- function isLayout(node) {
98
- return node.nodeType === TEXT_NODE && /^[ \t\r\n]*$/.test(node.nodeValue ?? "");
99
- }
100
151
  function surroundingNodes(parent) {
101
152
  return parent === null ? [] : Array.from(parent.childNodes).filter(
102
- (node) => !isElement(node) && !isLayout(node)
153
+ (node) => !isElement(node) && !isLayoutText(node)
103
154
  );
104
155
  }
105
156
  function sameNodes(now, was) {
@@ -192,12 +243,13 @@ function storyPart(shape) {
192
243
  referents: shape.referents,
193
244
  wellFormed: wellFormedEntry,
194
245
  anyonesChange: threadKeyAlone,
195
- allowed: (entry, original, authorId, options, session) => entryAllowed(
246
+ allowed: (entry, original, authorId, options, session, unattributed) => entryAllowed(
196
247
  entry,
197
248
  original,
198
249
  authorId,
199
250
  options.editableComments,
200
- session.comments.people
251
+ session.comments.people,
252
+ unattributed
201
253
  )
202
254
  };
203
255
  }
@@ -8,6 +8,8 @@
8
8
  * part by, so a name stands for one identity per file: a name the part already records is read as
9
9
  * it stands and never appended to. Appending a second person for it would leave a file naming two
10
10
  * identities for one name, where a reader keying by name hands one author's comments to the other.
11
+ * A name the file's comments already write under carries the same weight, recorded or not: a
12
+ * person appended for it would claim those comments too.
11
13
  */
12
14
  import type { PartPlanContext } from "../partPlan";
13
15
  import type { StoryPartKind } from "../protectionPolicy";
@@ -33,10 +35,16 @@ export declare function readPeople(parts: Map<string, Uint8Array>, mainPartPath:
33
35
  */
34
36
  export declare function commentAuthorId(people: ImportedPeople, author: string): string | null;
35
37
  /**
36
- * The authors the part records no person for at all, each under the name they write as. A name it
37
- * already records is left as it stands, whichever provider recorded it and whatever it resolves to.
38
+ * The authors to record a person for, each under the name they write as: the ones the comments
39
+ * name one identity for and the part does not record already. A name the part records is left as
40
+ * it stands, whichever provider recorded it and whatever it resolves to. So is a name the comments
41
+ * carry under more than one identity, one identity and none among them: recording it would hand
42
+ * the comments carrying no identity to whoever the name then resolved to, and a document saved by
43
+ * a Word that wrote no people part carries every one of its comments that way.
44
+ * `unattributed` also reserves names from the original Comments part, including preserved entries
45
+ * the current editable bodies do not carry. The planner and export invariants use this same set.
38
46
  */
39
- export declare function unrecordedAuthors(bodies: Iterable<CommentReferenceData | CommentReplyData>, people: ImportedPeople): Map<string, string>;
47
+ export declare function unrecordedAuthors(bodies: Iterable<CommentReferenceData | CommentReplyData>, people: ImportedPeople, unattributed: ReadonlySet<string>): Map<string, string>;
40
48
  /**
41
49
  * Plans the people part, its relationship and its content type for every identity the current
42
50
  * comments carry that the document has not recorded. Null when it has recorded them all, which
@@ -10,6 +10,7 @@ import {
10
10
  encodeUtf8,
11
11
  parseXml
12
12
  } from "../../ooxml/xml.js";
13
+ import { unattributedCommentAuthors } from "../../schema/protection.js";
13
14
  import { relatedPartPath } from "../packageParts.js";
14
15
  import { directoryOf } from "../relationships.js";
15
16
  import { COMMENT_AUTHOR_PROVIDER, PEOPLE_REL_TYPE, W15_NS } from "./constants.js";
@@ -51,12 +52,21 @@ function readPeople(parts, mainPartPath) {
51
52
  function commentAuthorId(people, author) {
52
53
  return people.byAuthor.get(author) ?? null;
53
54
  }
54
- function unrecordedAuthors(bodies, people) {
55
- const unrecorded = /* @__PURE__ */ new Map();
55
+ function unrecordedAuthors(bodies, people, unattributed) {
56
+ const identities = /* @__PURE__ */ new Map();
56
57
  for (const body of bodies) {
57
- if (body.author === null || body.authorId === null) continue;
58
- if (people.byAuthor.has(body.author)) continue;
59
- unrecorded.set(body.author, body.authorId);
58
+ if (body.author === null) continue;
59
+ if (people.byAuthor.has(body.author) || unattributed.has(body.author))
60
+ continue;
61
+ const ids = identities.get(body.author) ?? /* @__PURE__ */ new Set();
62
+ ids.add(body.authorId);
63
+ identities.set(body.author, ids);
64
+ }
65
+ const unrecorded = /* @__PURE__ */ new Map();
66
+ for (const [author, ids] of identities) {
67
+ if (ids.size !== 1) continue;
68
+ const only = Array.from(ids)[0] ?? null;
69
+ if (only !== null) unrecorded.set(author, only);
60
70
  }
61
71
  return unrecorded;
62
72
  }
@@ -94,7 +104,11 @@ function peopleXml(people, added) {
94
104
  }
95
105
  function planPeoplePart(part, bodies, session, context) {
96
106
  const people = session.comments.people;
97
- const added = unrecordedAuthors(bodies, people);
107
+ const added = unrecordedAuthors(
108
+ bodies,
109
+ people,
110
+ unattributedCommentAuthors(session.comments.ordered)
111
+ );
98
112
  if (added.size === 0) return null;
99
113
  const parts = /* @__PURE__ */ new Map();
100
114
  const addingPart = part.pathIn(session) === null;
@@ -3,6 +3,7 @@ import {
3
3
  commentAdditionsBy,
4
4
  commentEditsOwned,
5
5
  commentIdentitiesKept,
6
+ unattributedCommentAuthors,
6
7
  withoutComments
7
8
  } from "../../schema/protection.js";
8
9
  import {
@@ -18,7 +19,12 @@ function sameBody(before, after) {
18
19
  }
19
20
  function commentsStoryKept(before, after, authorId, { editableComments }) {
20
21
  if (!sameBody(before, after)) return refused("body-changed");
21
- if (!commentIdentitiesKept(before.doc, after.doc) || !commentAdditionsBy(before.doc, after.doc, authorId)) {
22
+ if (!commentIdentitiesKept(before.doc, after.doc) || !commentAdditionsBy(
23
+ before.doc,
24
+ after.doc,
25
+ authorId,
26
+ unattributedCommentAuthors(before.session.comments.ordered)
27
+ )) {
22
28
  return refused("comment-author-forged");
23
29
  }
24
30
  if (!commentEditsOwned(before.doc, after.doc, {
@@ -2,6 +2,10 @@
2
2
  * Reads comment and comment-extension package parts.
3
3
  */
4
4
  import { type ImportedPeople } from "./people";
5
+ /**
6
+ * One entry of the Comments part as it arrived, everything about it but what it says: that is a
7
+ * story of its own, sliced and read the way a body block is (`docx/story`).
8
+ */
5
9
  export interface ImportedComment {
6
10
  id: string;
7
11
  author: string | null;
@@ -13,8 +17,6 @@ export interface ImportedComment {
13
17
  authorId: string | null;
14
18
  initials: string | null;
15
19
  date: string | null;
16
- text: string;
17
- xml: string;
18
20
  paraId: string | null;
19
21
  parentParaId: string | null;
20
22
  resolved: boolean;
@@ -4,8 +4,7 @@ import {
4
4
  decodeUtf8,
5
5
  elementChildren,
6
6
  parseXml,
7
- serializeXml,
8
- W_NS
7
+ serializeXml
9
8
  } from "../../ooxml/xml.js";
10
9
  import { relatedPartPath } from "../packageParts.js";
11
10
  import { COMMENTS_EXTENDED_REL_TYPE, COMMENTS_REL_TYPE } from "./constants.js";
@@ -15,16 +14,6 @@ import {
15
14
  NO_PEOPLE,
16
15
  readPeople
17
16
  } from "./people.js";
18
- function inlineCommentText(node) {
19
- if (node.localName === "t") return node.textContent ?? "";
20
- if (node.localName === "tab") return " ";
21
- if (node.localName === "br" || node.localName === "cr") return "\n";
22
- return elementChildren(node).map(inlineCommentText).join("");
23
- }
24
- function commentText(el) {
25
- const paragraphs = Array.from(el.getElementsByTagNameNS(W_NS, "p"));
26
- return paragraphs.map(inlineCommentText).join("\n");
27
- }
28
17
  var NO_COMMENTS = {
29
18
  partPath: null,
30
19
  xml: null,
@@ -117,8 +106,6 @@ function readComments(parts, mainPartPath) {
117
106
  authorId: author === null ? null : commentAuthorId(people, author),
118
107
  initials: attributeByLocalName(el, "initials"),
119
108
  date: attributeByLocalName(el, "date"),
120
- text: commentText(el),
121
- xml: serializeXml(el),
122
109
  paraId,
123
110
  parentParaId: extension?.parentParaId ?? null,
124
111
  resolved: extension?.resolved ?? false,
@@ -1,5 +1,5 @@
1
1
  // src/docx/comments/writing.ts
2
- import { elementXml } from "../../ooxml/element.js";
2
+ import { attrsText } from "../../ooxml/element.js";
3
3
  import { NAMESPACES, wName, xmlnsDecl } from "../../ooxml/names.js";
4
4
  import {
5
5
  ensureRootDeclarations,
@@ -7,10 +7,18 @@ import {
7
7
  splicePart
8
8
  } from "../../ooxml/partSplice.js";
9
9
  import { encodeUtf8 } from "../../ooxml/xml.js";
10
+ import { sameSource } from "../../schema/sourceEquality.js";
11
+ import { NO_EXPORT_REFS } from "../exportRefs.js";
10
12
  import { directoryOf } from "../relationships.js";
13
+ import { serializeStory } from "../serializeStory.js";
14
+ import {
15
+ storyFromText,
16
+ storyKey,
17
+ storyOf,
18
+ withThreadKeyOn
19
+ } from "../story.js";
11
20
  import {
12
21
  arrivedEntries,
13
- renderCommentBody,
14
22
  renderCommentExtension,
15
23
  withThreadKey
16
24
  } from "./grammar.js";
@@ -20,7 +28,26 @@ import {
20
28
  import { commentsExtendedPart, commentsPart, peoplePart } from "./parts.js";
21
29
  import { planPeoplePart } from "./people.js";
22
30
  function needsThreadKey(id, comment, session) {
23
- return comment.imported && carriesThreadMetadata(comment) && (session.comments.byId.get(id)?.paraId ?? null) === null;
31
+ return session.comments.byId.has(id) && carriesThreadMetadata(comment) && (session.comments.byId.get(id)?.paraId ?? null) === null;
32
+ }
33
+ function bodyStory(doc, id) {
34
+ return storyOf(doc, storyKey("comment", id));
35
+ }
36
+ function importedBody(session, id) {
37
+ return session.stories.get(storyKey("comment", id)) ?? null;
38
+ }
39
+ function importedEntry(comment, session) {
40
+ const arrived = session.comments.byId.get(comment.id);
41
+ if (arrived === void 0 || arrived.author !== comment.author || arrived.date !== comment.date || arrived.initials !== comment.initials) {
42
+ return null;
43
+ }
44
+ return importedBody(session, comment.id);
45
+ }
46
+ function bodyKept(doc, comment, session) {
47
+ const imported = importedEntry(comment, session);
48
+ if (imported === null) return null;
49
+ const current = bodyStory(doc, comment.id);
50
+ return current === null || sameSource(current, imported.doc) ? imported : null;
24
51
  }
25
52
  function commentsChanged(doc, session) {
26
53
  const current = commentReferencesIn(doc);
@@ -34,12 +61,8 @@ function commentsChanged(doc, session) {
34
61
  for (const id of originalBodies) {
35
62
  if (!currentBodies.has(id)) return true;
36
63
  }
37
- for (const id of session.commentReferenceIds) {
38
- const comment = current.get(id);
39
- if (!comment?.imported) return true;
40
- }
41
- for (const comment of current.values()) {
42
- if (comment.replies.some((reply) => !reply.imported)) return true;
64
+ for (const comment of currentBodies.values()) {
65
+ if (bodyKept(doc, comment, session) === null) return true;
43
66
  }
44
67
  for (const [id, comment] of current) {
45
68
  if (needsThreadKey(id, comment, session)) return true;
@@ -111,27 +134,34 @@ function carriesThreadMetadata(comment) {
111
134
  function keyedEntry(comment, arrivedKeyed) {
112
135
  return carriesThreadMetadata(comment) || arrivedKeyed.has(comment.id);
113
136
  }
114
- function renderedComment(comment, arrivedKeyed, arrived) {
115
- if (comment.imported && comment.commentXml !== null) {
116
- return carriesThreadMetadata(comment) ? withThreadKey(
117
- comment.commentXml,
118
- comment.paraId,
119
- arrived.get(comment.id) ?? null
120
- ) : comment.commentXml;
121
- }
137
+ function writtenContainer(comment) {
122
138
  const attrs = [
123
139
  [wName("id"), comment.id],
124
140
  comment.author === null ? null : [wName("author"), comment.author],
125
141
  comment.date === null ? null : [wName("date"), comment.date],
126
142
  comment.initials === null ? null : [wName("initials"), comment.initials]
127
143
  ];
128
- const paraId = keyedEntry(comment, arrivedKeyed) ? comment.paraId : null;
129
- const body = renderCommentBody(comment.text, paraId);
130
- return elementXml(
131
- wName("comment"),
132
- attrs.filter((attr) => attr !== null),
133
- [body]
144
+ const text = attrsText(
145
+ attrs.filter((attr) => attr !== null)
134
146
  );
147
+ return {
148
+ open: text === "" ? `<${wName("comment")}>` : `<${wName("comment")} ${text}>`,
149
+ close: `</${wName("comment")}>`
150
+ };
151
+ }
152
+ function renderedComment(comment, arrivedKeyed, arrived, doc, session) {
153
+ const kept = bodyKept(doc, comment, session);
154
+ if (kept !== null) {
155
+ return carriesThreadMetadata(comment) ? withThreadKey(kept.xml, comment.paraId, arrived.get(comment.id) ?? null) : kept.xml;
156
+ }
157
+ const paraId = keyedEntry(comment, arrivedKeyed) ? comment.paraId : null;
158
+ const entry = importedEntry(comment, session);
159
+ const said = bodyStory(doc, comment.id) ?? storyFromText("");
160
+ const body = paraId === null ? said : withThreadKeyOn(said, paraId);
161
+ return serializeStory(body, null, entry ?? writtenContainer(comment), {
162
+ ...NO_EXPORT_REFS,
163
+ session
164
+ });
135
165
  }
136
166
  var COMMENT_MARKUP = { namespaces: { w: NAMESPACES.w } };
137
167
  var THREAD_MARKUP = {
@@ -160,7 +190,8 @@ function originalThreadIds(comments, rootIds) {
160
190
  }
161
191
  return ids;
162
192
  }
163
- function commentsXml(references, comments, originallyReferenced) {
193
+ function commentsXml(doc, session, references, originallyReferenced) {
194
+ const comments = session.comments;
164
195
  const currentBodies = currentCommentBodies(references);
165
196
  const arrivedKeyed = new Set(
166
197
  Array.from(comments.byId.values()).flatMap(
@@ -175,20 +206,26 @@ function commentsXml(references, comments, originallyReferenced) {
175
206
  const pieces = [];
176
207
  const written = /* @__PURE__ */ new Set();
177
208
  for (const original of comments.ordered) {
209
+ if (written.has(original.id)) continue;
178
210
  const current = currentBodies.get(original.id);
179
211
  if (current) {
180
- pieces.push(renderedComment(current, arrivedKeyed, arrived));
212
+ pieces.push(
213
+ renderedComment(current, arrivedKeyed, arrived, doc, session)
214
+ );
181
215
  written.add(original.id);
182
216
  continue;
183
217
  }
184
218
  if (!originalThreads.has(original.id)) {
185
- pieces.push(original.xml);
219
+ const story = importedBody(session, original.id);
220
+ if (story !== null) pieces.push(story.xml);
186
221
  written.add(original.id);
187
222
  }
188
223
  }
189
224
  for (const [id, comment] of currentBodies) {
190
225
  if (!written.has(id)) {
191
- pieces.push(renderedComment(comment, arrivedKeyed, arrived));
226
+ pieces.push(
227
+ renderedComment(comment, arrivedKeyed, arrived, doc, session)
228
+ );
192
229
  }
193
230
  }
194
231
  if (comments.xml === null) {
@@ -221,7 +258,7 @@ function planCommentParts(doc, session, context) {
221
258
  parts.set(
222
259
  partPath,
223
260
  encodeUtf8(
224
- commentsXml(references, session.comments, session.commentReferenceIds),
261
+ commentsXml(doc, session, references, session.commentReferenceIds),
225
262
  session.comments.hadBom
226
263
  )
227
264
  );
@@ -10,12 +10,12 @@ import {
10
10
  W_NS,
11
11
  withXmlParser
12
12
  } from "../ooxml/xml.js";
13
- import { sameSource } from "../schema/sourceEquality.js";
14
13
  import { commentsPlanner } from "./comments.js";
15
14
  import { repackParts } from "./container.js";
16
15
  import {
17
16
  fidelityNotesOf
18
17
  } from "./fidelity.js";
18
+ import { headerFooterPlanner } from "./headersFooters.js";
19
19
  import { hyperlinkRefs } from "./hyperlink.js";
20
20
  import { withUniqueIdentities } from "./identities.js";
21
21
  import { problemsOf } from "./invariants.js";
@@ -31,21 +31,15 @@ import {
31
31
  relationshipWriter,
32
32
  relsPathOf
33
33
  } from "./relationships.js";
34
- import { serializeBlock } from "./serializeBlock.js";
35
- import {
36
- originalBlock,
37
- sessionOf
38
- } from "./session.js";
39
- function blockXml(node, session, refs) {
40
- const imported = originalBlock(node, session);
41
- if (imported && sameSource(node, imported.node)) return imported.xml;
42
- return serializeBlock(node, session, refs);
43
- }
34
+ import { blockXml } from "./serializeBlock.js";
35
+ import { sessionOf } from "./session.js";
44
36
  function buildDocumentXml(doc, session, refs) {
45
37
  const pieces = [session.documentPrefix];
46
38
  withUniqueIdentities(doc).forEach((child) => {
47
- pieces.push(blockXml(child, session, refs));
39
+ pieces.push(blockXml(child, refs, doc.childCount === 1));
48
40
  });
41
+ const sectPr = doc.attrs.sectPr;
42
+ if (typeof sectPr === "string") pieces.push(sectPr);
49
43
  pieces.push(session.documentSuffix);
50
44
  return pieces.join("");
51
45
  }
@@ -113,7 +107,8 @@ function assertMainPart(documentXml, wroteLinks) {
113
107
  var LINK_MARKUP = { namespaces: { r: R_NS } };
114
108
  var PART_PLANNERS = [
115
109
  numberingPlanner,
116
- commentsPlanner
110
+ commentsPlanner,
111
+ headerFooterPlanner
117
112
  ];
118
113
  function exportDocx(doc, session, options) {
119
114
  return exportDocxReport(doc, session, options).bytes;
@@ -153,7 +148,8 @@ function writeDocx(doc, store, notes, planners) {
153
148
  const body = buildDocumentXml(doc, store, {
154
149
  images: media?.refs ?? NO_IMAGE_REFS,
155
150
  links,
156
- notes
151
+ notes,
152
+ session: store
157
153
  });
158
154
  const documentXml = links.addedRelId() ? ensureRootDeclarations(body, LINK_MARKUP) : body;
159
155
  assertMainPart(documentXml, links.addedRelId());
@@ -1,19 +1,24 @@
1
1
  /**
2
2
  * What the serializers need from the rest of the package while a body is being written: which
3
3
  * relationship an inserted image ended up on (`docx/media`), which one each link's address lives
4
- * on (`docx/hyperlink`), and where to record an approximation a writer had to make
5
- * (`docx/fidelity`).
4
+ * on (`docx/hyperlink`), where to record an approximation a writer had to make (`docx/fidelity`),
5
+ * and the fragments the document was opened from (`docx/session`).
6
6
  *
7
7
  * They travel as one value so that a serializer taking a body apart passes on everything the
8
- * pieces of it may need, whatever a later kind of content turns out to require.
8
+ * pieces of it may need, whatever a later kind of content turns out to require. The session is
9
+ * among them because a preserved block names the fragment it stands for wherever it ends up: an
10
+ * edit may move one into a table cell, and the cell writer has no other way to reach the original.
9
11
  */
10
12
  import { type FidelityCollector } from "./fidelity";
11
13
  import { type LinkRefs } from "./hyperlink";
12
14
  import { type ImageRefs } from "./media";
15
+ import type { SessionStore } from "./session";
13
16
  export interface ExportRefs {
14
17
  images: ImageRefs;
15
18
  links: LinkRefs;
16
19
  notes: FidelityCollector;
20
+ /** null for a serializer called on a document nobody opened */
21
+ session: SessionStore | null;
17
22
  }
18
23
  /** What a serializer called on its own, outside an export, has to work with */
19
24
  export declare const NO_EXPORT_REFS: ExportRefs;
@@ -5,7 +5,8 @@ import { NO_IMAGE_REFS } from "./media.js";
5
5
  var NO_EXPORT_REFS = {
6
6
  images: NO_IMAGE_REFS,
7
7
  links: NO_LINK_REFS,
8
- notes: NO_FIDELITY_COLLECTOR
8
+ notes: NO_FIDELITY_COLLECTOR,
9
+ session: null
9
10
  };
10
11
  export {
11
12
  NO_EXPORT_REFS