@portone/docx-editor 0.1.1 → 0.2.1

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 (138) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/CONTRIBUTING.md +4 -0
  3. package/README.md +17 -5
  4. package/dist/DocxEditor.d.ts +34 -10
  5. package/dist/DocxEditor.js +69 -24
  6. package/dist/core.d.ts +2 -0
  7. package/dist/core.js +2 -0
  8. package/dist/docx/commentOnlyChange.d.ts +47 -0
  9. package/dist/docx/commentOnlyChange.js +162 -0
  10. package/dist/docx/comments/constants.d.ts +11 -3
  11. package/dist/docx/comments/constants.js +10 -3
  12. package/dist/docx/comments/contentTypes.d.ts +7 -0
  13. package/dist/docx/comments/contentTypes.js +38 -0
  14. package/dist/docx/comments/grammar.d.ts +80 -0
  15. package/dist/docx/comments/grammar.js +213 -0
  16. package/dist/docx/comments/model.d.ts +2 -0
  17. package/dist/docx/comments/model.js +3 -0
  18. package/dist/docx/comments/people.d.ts +39 -0
  19. package/dist/docx/comments/people.js +196 -0
  20. package/dist/docx/comments/reading.d.ts +18 -1
  21. package/dist/docx/comments/reading.js +33 -20
  22. package/dist/docx/comments/verifying.d.ts +44 -0
  23. package/dist/docx/comments/verifying.js +206 -0
  24. package/dist/docx/comments/writing.d.ts +4 -1
  25. package/dist/docx/comments/writing.js +84 -77
  26. package/dist/docx/exportDocx.js +2 -1
  27. package/dist/docx/formatting/direct.js +3 -6
  28. package/dist/docx/headersFooters.js +8 -10
  29. package/dist/docx/importParagraph.js +1 -0
  30. package/dist/docx/notes.js +3 -5
  31. package/dist/docx/paraProps.js +51 -46
  32. package/dist/docx/propsXml.d.ts +20 -2
  33. package/dist/docx/propsXml.js +72 -32
  34. package/dist/docx/relationships.js +8 -3
  35. package/dist/docx/runProps.js +34 -34
  36. package/dist/docx/sdt.js +16 -14
  37. package/dist/docx/serializeBlock.js +1 -2
  38. package/dist/docx/serializeParagraph.d.ts +2 -1
  39. package/dist/docx/serializeParagraph.js +25 -14
  40. package/dist/docx/serializeTable.js +38 -24
  41. package/dist/docx/storyProjection.d.ts +36 -0
  42. package/dist/docx/storyProjection.js +23 -0
  43. package/dist/docx/tableFormatting/editing.js +49 -73
  44. package/dist/docx/tableTemplate.js +24 -3
  45. package/dist/docx/theme.d.ts +0 -6
  46. package/dist/docx/theme.js +0 -8
  47. package/dist/editor/clipboard/images.js +17 -3
  48. package/dist/editor/clipboard/inlineFormatting.d.ts +7 -0
  49. package/dist/editor/clipboard/inlineFormatting.js +1 -0
  50. package/dist/editor/commands/breakCommands.js +6 -2
  51. package/dist/editor/commands/canRunCommand.d.ts +3 -2
  52. package/dist/editor/commands/canRunCommand.js +2 -2
  53. package/dist/editor/commands/comments/editing.js +9 -10
  54. package/dist/editor/commands/comments/model.d.ts +9 -0
  55. package/dist/editor/commands/comments/reading.d.ts +7 -0
  56. package/dist/editor/commands/comments/reading.js +14 -0
  57. package/dist/editor/commands/formatting/editing.js +6 -1
  58. package/dist/editor/commands/historyCommands.js +14 -5
  59. package/dist/editor/commands/index.d.ts +7 -1
  60. package/dist/editor/commands/index.js +4 -0
  61. package/dist/editor/commands/linkCommands.js +2 -0
  62. package/dist/editor/commands/lockCommands.js +12 -1
  63. package/dist/editor/commands/tabCommands.js +4 -2
  64. package/dist/editor/createEditor.d.ts +13 -3
  65. package/dist/editor/createEditor.js +23 -15
  66. package/dist/editor/externalClipboard.js +166 -4
  67. package/dist/editor/insertImage.js +6 -2
  68. package/dist/editor/insertTable.js +2 -1
  69. package/dist/editor/paragraphEdits.d.ts +2 -0
  70. package/dist/editor/paragraphEdits.js +2 -0
  71. package/dist/editor/plugins/documentProtection.d.ts +21 -0
  72. package/dist/editor/plugins/documentProtection.js +44 -0
  73. package/dist/editor/plugins/imagePaste.js +4 -1
  74. package/dist/editor/plugins/keymap.d.ts +11 -1
  75. package/dist/editor/plugins/keymap.js +24 -4
  76. package/dist/editor/plugins/lockedContent.d.ts +4 -2
  77. package/dist/editor/plugins/lockedContent.js +2 -2
  78. package/dist/editor/plugins/tableContextMenu.js +2 -1
  79. package/dist/editor/plugins/textContextMenu.js +6 -1
  80. package/dist/index.d.ts +2 -0
  81. package/dist/numbering/listTemplate.js +32 -10
  82. package/dist/ooxml/element.d.ts +52 -0
  83. package/dist/ooxml/element.js +49 -0
  84. package/dist/ooxml/fragment.d.ts +53 -0
  85. package/dist/ooxml/fragment.js +76 -0
  86. package/dist/ooxml/image.js +4 -3
  87. package/dist/ooxml/names.d.ts +27 -0
  88. package/dist/ooxml/names.js +29 -0
  89. package/dist/ooxml/precedence.d.ts +31 -0
  90. package/dist/ooxml/precedence.js +51 -0
  91. package/dist/ooxml/xml.d.ts +31 -0
  92. package/dist/ooxml/xml.js +27 -0
  93. package/dist/page/blockKinds.d.ts +42 -0
  94. package/dist/page/blockKinds.js +0 -0
  95. package/dist/page/measureBlocks.d.ts +14 -1
  96. package/dist/page/measureBlocks.js +31 -11
  97. package/dist/page/pageDecorations.d.ts +16 -0
  98. package/dist/page/pageDecorations.js +1 -0
  99. package/dist/page/pageLayout.d.ts +8 -43
  100. package/dist/page/pageLayout.js +28 -51
  101. package/dist/page/tableMeasurements.d.ts +10 -2
  102. package/dist/page/tableMeasurements.js +20 -16
  103. package/dist/page/usePageLayout.d.ts +15 -0
  104. package/dist/page/usePageLayout.js +30 -2
  105. package/dist/schema/attrRoles.d.ts +35 -0
  106. package/dist/schema/attrRoles.js +106 -0
  107. package/dist/schema/docxSchema.d.ts +6 -0
  108. package/dist/schema/docxSchema.js +216 -100
  109. package/dist/schema/editGuard.d.ts +99 -0
  110. package/dist/schema/editGuard.js +43 -0
  111. package/dist/schema/guards.d.ts +68 -0
  112. package/dist/schema/guards.js +82 -0
  113. package/dist/schema/locks.d.ts +9 -8
  114. package/dist/schema/locks.js +32 -16
  115. package/dist/schema/preservedGuards.d.ts +30 -0
  116. package/dist/schema/preservedGuards.js +50 -0
  117. package/dist/schema/protection.d.ts +84 -0
  118. package/dist/schema/protection.js +171 -0
  119. package/dist/schema/protectionState.d.ts +20 -0
  120. package/dist/schema/protectionState.js +37 -0
  121. package/dist/schema/sourceEquality.d.ts +27 -0
  122. package/dist/schema/sourceEquality.js +65 -0
  123. package/dist/table/cellFormatting.js +5 -2
  124. package/dist/table/commands.d.ts +2 -2
  125. package/dist/table/commands.js +2 -2
  126. package/dist/table/index.d.ts +2 -2
  127. package/dist/table/merge.d.ts +2 -2
  128. package/dist/table/merge.js +3 -3
  129. package/dist/ui/CommentsPanel.d.ts +3 -3
  130. package/dist/ui/CommentsPanel.js +26 -18
  131. package/dist/ui/LinkCard.d.ts +1 -2
  132. package/dist/ui/LinkCard.js +3 -4
  133. package/dist/ui/TextMenu.js +15 -10
  134. package/package.json +5 -2
  135. package/dist/editor/plugins/bookmarkProtection.d.ts +0 -3
  136. package/dist/editor/plugins/bookmarkProtection.js +0 -28
  137. package/dist/editor/plugins/noteProtection.d.ts +0 -3
  138. package/dist/editor/plugins/noteProtection.js +0 -25
@@ -0,0 +1,196 @@
1
+ // src/docx/comments/people.ts
2
+ import { xmlnsAttr } from "../../ooxml/element.js";
3
+ import { DocxExportError } from "../../ooxml/errors.js";
4
+ import { xmlnsDecl } from "../../ooxml/names.js";
5
+ import {
6
+ attributeByLocalName,
7
+ childByLocalName,
8
+ decodeUtf8,
9
+ elementChildren,
10
+ encodeUtf8,
11
+ parseXml
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";
27
+ import { renderPerson } from "./grammar.js";
28
+ var NO_PEOPLE = {
29
+ partPath: null,
30
+ xml: null,
31
+ hadBom: false,
32
+ byAuthor: /* @__PURE__ */ new Map()
33
+ };
34
+ 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);
40
+ const bytes = parts.get(partPath);
41
+ if (!bytes) return { ...NO_PEOPLE, partPath };
42
+ const { text, hadBom } = decodeUtf8(bytes);
43
+ const root = parseXml(text).documentElement;
44
+ const ourIds = /* @__PURE__ */ new Map();
45
+ const byAuthor = /* @__PURE__ */ new Map();
46
+ for (const el of elementChildren(root)) {
47
+ if (el.localName !== "person") continue;
48
+ const author = attributeByLocalName(el, "author");
49
+ if (author === null) continue;
50
+ byAuthor.set(author, null);
51
+ const presence = childByLocalName(el, "presenceInfo");
52
+ if (presence === null) continue;
53
+ if (attributeByLocalName(presence, "providerId") !== COMMENT_AUTHOR_PROVIDER)
54
+ continue;
55
+ const ids = ourIds.get(author) ?? /* @__PURE__ */ new Set();
56
+ ids.add(attributeByLocalName(presence, "userId"));
57
+ ourIds.set(author, ids);
58
+ }
59
+ for (const [author, ids] of ourIds) {
60
+ if (ids.size !== 1) continue;
61
+ byAuthor.set(author, Array.from(ids)[0] ?? null);
62
+ }
63
+ return { partPath, xml: text, hadBom, byAuthor };
64
+ }
65
+ function commentAuthorId(people, author) {
66
+ return people.byAuthor.get(author) ?? null;
67
+ }
68
+ function unrecordedAuthors(bodies, people) {
69
+ const unrecorded = /* @__PURE__ */ new Map();
70
+ for (const body of bodies) {
71
+ if (body.author === null || body.authorId === null) continue;
72
+ if (people.byAuthor.has(body.author)) continue;
73
+ unrecorded.set(body.author, body.authorId);
74
+ }
75
+ return unrecorded;
76
+ }
77
+ function w15Prefix(root) {
78
+ if (root.namespaceURI === W15_NS) {
79
+ return root.prefix === null ? "" : `${root.prefix}:`;
80
+ }
81
+ const declaration = Array.from(root.attributes).find(
82
+ (attr) => attr.value === W15_NS && (attr.name === "xmlns" || attr.name.startsWith("xmlns:"))
83
+ );
84
+ if (declaration === void 0) return null;
85
+ const prefix = declaration.name.slice("xmlns:".length);
86
+ return prefix === "" ? "" : `${prefix}:`;
87
+ }
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
+ function peopleXml(people, added) {
126
+ const xml = people.xml;
127
+ if (xml === null) {
128
+ const persons2 = Array.from(
129
+ added,
130
+ ([author, userId]) => renderPerson(author, userId, "w15:", null)
131
+ );
132
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w15:people ${xmlnsDecl("w15")}>${persons2.join("")}</w15:people>`;
133
+ }
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);
139
+ const persons = Array.from(
140
+ added,
141
+ ([author, userId]) => renderPerson(
142
+ author,
143
+ userId,
144
+ prefix ?? "w15:",
145
+ prefix === null ? xmlnsAttr("w15") : null
146
+ )
147
+ ).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
+ }
165
+ }
166
+ function planPeoplePart(bodies, session, relationships, currentContentTypes) {
167
+ const people = session.comments.people;
168
+ const added = unrecordedAuthors(bodies, people);
169
+ if (added.size === 0) return null;
170
+ const parts = /* @__PURE__ */ new Map();
171
+ const addingPart = people.partPath === null;
172
+ const partPath = people.partPath ?? availablePeoplePath(session);
173
+ if (addingPart) {
174
+ relationships.add({
175
+ type: PEOPLE_REL_TYPE,
176
+ target: partPath.slice(directoryOf(session.mainPartPath).length)
177
+ });
178
+ }
179
+ parts.set(partPath, encodeUtf8(peopleXml(people, added), people.hadBom));
180
+ 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);
188
+ }
189
+ return parts;
190
+ }
191
+ export {
192
+ NO_PEOPLE,
193
+ commentAuthorId,
194
+ planPeoplePart,
195
+ readPeople
196
+ };
@@ -1,9 +1,16 @@
1
1
  /**
2
2
  * Reads comment and comment-extension package parts.
3
3
  */
4
+ import { type ImportedPeople } from "./people";
4
5
  export interface ImportedComment {
5
6
  id: string;
6
7
  author: string | null;
8
+ /**
9
+ * The identity the people part records for `author` under this editor's provider. Null when it
10
+ * records none, and null as well when it records that name under more than one identity, which
11
+ * leaves no way to tell whose comment this is.
12
+ */
13
+ authorId: string | null;
7
14
  initials: string | null;
8
15
  date: string | null;
9
16
  text: string;
@@ -24,6 +31,8 @@ export interface ImportedComments {
24
31
  extendedXml: string | null;
25
32
  extendedHadBom: boolean;
26
33
  extendedOrdered: readonly ImportedCommentExtension[];
34
+ /** The people part as opened, which is where an author's identity is looked up */
35
+ people: ImportedPeople;
27
36
  }
28
37
  export declare const NO_COMMENTS: ImportedComments;
29
38
  export interface ImportedCommentExtension {
@@ -32,5 +41,13 @@ export interface ImportedCommentExtension {
32
41
  resolved: boolean;
33
42
  xml: string;
34
43
  }
35
- /** Reads the Comments part related from the main document story. */
44
+ /** The thread key of a comment, which the writer puts on the last paragraph of its body */
45
+ export declare function lastParagraphId(comment: Element): string | null;
46
+ /**
47
+ * Reads the Comments part related from the main document story.
48
+ *
49
+ * The people part and the extended part are read whether or not there are comments: a document may
50
+ * carry either with no comment left, and a comment added to it then has to be written into the
51
+ * part it already has rather than into a second one beside it.
52
+ */
36
53
  export declare function readComments(parts: Map<string, Uint8Array>, mainPartPath: string): ImportedComments;
@@ -1,5 +1,6 @@
1
1
  // src/docx/comments/reading.ts
2
2
  import {
3
+ attributeByLocalName,
3
4
  decodeUtf8,
4
5
  elementChildren,
5
6
  parseXml,
@@ -8,9 +9,12 @@ import {
8
9
  } from "../../ooxml/xml.js";
9
10
  import { readRelationships, relsPathOf, resolveTarget } from "../relationships.js";
10
11
  import { COMMENTS_EXTENDED_REL_TYPE, COMMENTS_REL_TYPE } from "./constants.js";
11
- function attribute(el, localName) {
12
- return Array.from(el.attributes).find((entry) => entry.localName === localName)?.value ?? null;
13
- }
12
+ import { lastBodyParagraph } from "./grammar.js";
13
+ import {
14
+ commentAuthorId,
15
+ NO_PEOPLE,
16
+ readPeople
17
+ } from "./people.js";
14
18
  function inlineCommentText(node) {
15
19
  if (node.localName === "t") return node.textContent ?? "";
16
20
  if (node.localName === "tab") return " ";
@@ -31,11 +35,12 @@ var NO_COMMENTS = {
31
35
  extendedPartPath: null,
32
36
  extendedXml: null,
33
37
  extendedHadBom: false,
34
- extendedOrdered: []
38
+ extendedOrdered: [],
39
+ people: NO_PEOPLE
35
40
  };
36
41
  function lastParagraphId(comment) {
37
- const paragraphs = Array.from(comment.getElementsByTagNameNS(W_NS, "p"));
38
- return paragraphs.length === 0 ? null : attribute(paragraphs[paragraphs.length - 1], "paraId");
42
+ const last = lastBodyParagraph(comment);
43
+ return last === null ? null : attributeByLocalName(last, "paraId");
39
44
  }
40
45
  function readCommentExtensions(parts, mainPartPath) {
41
46
  const relationship = readRelationships(parts, relsPathOf(mainPartPath)).find(
@@ -67,13 +72,13 @@ function readCommentExtensions(parts, mainPartPath) {
67
72
  const ordered = [];
68
73
  for (const el of elementChildren(root)) {
69
74
  if (el.localName !== "commentEx") continue;
70
- const paraId = attribute(el, "paraId");
75
+ const paraId = attributeByLocalName(el, "paraId");
71
76
  if (paraId === null) continue;
72
77
  const extension = {
73
78
  paraId,
74
- parentParaId: attribute(el, "paraIdParent"),
79
+ parentParaId: attributeByLocalName(el, "paraIdParent"),
75
80
  resolved: ["1", "true", "on"].includes(
76
- attribute(el, "done")?.toLowerCase() ?? ""
81
+ attributeByLocalName(el, "done")?.toLowerCase() ?? ""
77
82
  ),
78
83
  xml: serializeXml(el)
79
84
  };
@@ -83,27 +88,37 @@ function readCommentExtensions(parts, mainPartPath) {
83
88
  return { partPath, xml: text, hadBom, byParaId, ordered };
84
89
  }
85
90
  function readComments(parts, mainPartPath) {
91
+ const people = readPeople(parts, mainPartPath);
92
+ const extensions = readCommentExtensions(parts, mainPartPath);
93
+ const aside = {
94
+ people,
95
+ extendedPartPath: extensions.partPath,
96
+ extendedXml: extensions.xml,
97
+ extendedHadBom: extensions.hadBom,
98
+ extendedOrdered: extensions.ordered
99
+ };
86
100
  const relationship = readRelationships(parts, relsPathOf(mainPartPath)).find(
87
101
  (entry) => entry.type === COMMENTS_REL_TYPE && !entry.external
88
102
  );
89
- if (!relationship) return NO_COMMENTS;
103
+ if (!relationship) return { ...NO_COMMENTS, ...aside };
90
104
  const partPath = resolveTarget(mainPartPath, relationship.target);
91
105
  const bytes = parts.get(partPath);
92
- if (!bytes) return { ...NO_COMMENTS, partPath };
106
+ if (!bytes) return { ...NO_COMMENTS, partPath, ...aside };
93
107
  const { text, hadBom } = decodeUtf8(bytes);
94
108
  const root = parseXml(text).documentElement;
95
- const extensions = readCommentExtensions(parts, mainPartPath);
96
109
  const base = elementChildren(root).filter((el) => el.localName === "comment").flatMap((el) => {
97
- const id = attribute(el, "id");
110
+ const id = attributeByLocalName(el, "id");
98
111
  if (id === null) return [];
99
112
  const paraId = lastParagraphId(el);
100
113
  const extension = paraId ? extensions.byParaId.get(paraId) : void 0;
114
+ const author = attributeByLocalName(el, "author");
101
115
  return [
102
116
  {
103
117
  id,
104
- author: attribute(el, "author"),
105
- initials: attribute(el, "initials"),
106
- date: attribute(el, "date"),
118
+ author,
119
+ authorId: author === null ? null : commentAuthorId(people, author),
120
+ initials: attributeByLocalName(el, "initials"),
121
+ date: attributeByLocalName(el, "date"),
107
122
  text: commentText(el),
108
123
  xml: serializeXml(el),
109
124
  paraId,
@@ -137,13 +152,11 @@ function readComments(parts, mainPartPath) {
137
152
  ordered,
138
153
  byId,
139
154
  repliesByParentId,
140
- extendedPartPath: extensions.partPath,
141
- extendedXml: extensions.xml,
142
- extendedHadBom: extensions.hadBom,
143
- extendedOrdered: extensions.ordered
155
+ ...aside
144
156
  };
145
157
  }
146
158
  export {
147
159
  NO_COMMENTS,
160
+ lastParagraphId,
148
161
  readComments
149
162
  };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Whether every entry of the three comment parts is one this editor wrote, for an author who was
3
+ * in a position to write it.
4
+ *
5
+ * The parts a comment is written across are the ones a submission may rewrite, so the package
6
+ * comparison (`../commentOnlyChange`) leaves their bytes alone. Something has to read them, or a
7
+ * file could carry anything at all inside a comment and still be answered as unchanged. What that
8
+ * reading holds an entry to is two separate questions. Grammar asks whether this editor's writer
9
+ * could have put the entry out, whoever it belongs to. Permission asks whether this author could
10
+ * have made the change. Asking them as one would refuse a moderator rewriting somebody else's
11
+ * comment under `editableComments: "all"`, which is a thing the editor lets them do.
12
+ */
13
+ import type { EditableComments } from "../../schema/protection";
14
+ import type { CommentOnlyVerdict } from "../commentOnlyChange";
15
+ import type { Story } from "../storyProjection";
16
+ import { type ImportedPeople } from "./people";
17
+ /**
18
+ * Whether this editor's writer could have put the entry out, whoever it belongs to.
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`).
22
+ */
23
+ export declare function wellFormedEntry(entry: Element): boolean;
24
+ /**
25
+ * Whether `authorId` could have written or rewritten this entry.
26
+ *
27
+ * Permission alone: whether the shape is one this editor writes is `wellFormedEntry`'s question.
28
+ * An entry that appeared has to be this author's. One that arrived keeps the identity it arrived
29
+ * with; settling its thread or replying to it belong to everyone and leave what it says alone,
30
+ * while rewriting what it says is its author's, a moderator's, or anyone's where no identity was
31
+ * recorded for it. That last is the rule the editor holds to (`schema/protection`), and the two
32
+ * have to answer alike or a file the editor wrote would be turned down here.
33
+ */
34
+ export declare function entryAllowed(entry: Element, original: Element | null, authorId: string, editableComments: EditableComments, people: ImportedPeople): boolean;
35
+ /**
36
+ * Whether the three comment parts came back holding only entries this editor writes, each of them
37
+ * one this author was in a position to write.
38
+ *
39
+ * This runs after the story has been judged, so a comment rewritten, re-anchored or deleted by the
40
+ * wrong hand is already named for what it is (`../commentOnlyChange`). What is left to this is
41
+ * everything the story cannot show: markup forged into a body, an entry nothing refers to, an
42
+ * identity recorded for somebody else.
43
+ */
44
+ export declare function commentPartsKept(before: Story, after: Story, authorId: string, editableComments: EditableComments): CommentOnlyVerdict;
@@ -0,0 +1,206 @@
1
+ // src/docx/comments/verifying.ts
2
+ import {
3
+ attributeByLocalName,
4
+ elementChildren,
5
+ isElement,
6
+ parseXml,
7
+ serializeXml,
8
+ W_NS
9
+ } from "../../ooxml/xml.js";
10
+ import { W14_NS, W15_NS } from "./constants.js";
11
+ import {
12
+ attributesWithin,
13
+ COMMENT_ATTRIBUTES,
14
+ lastBodyParagraph,
15
+ readStrictCommentBody,
16
+ recordedIdentity,
17
+ wellFormedCommentExtension,
18
+ wellFormedPerson
19
+ } from "./grammar.js";
20
+ import { commentReferencesIn } from "./model.js";
21
+ import { commentAuthorId } from "./people.js";
22
+ import { lastParagraphId } from "./reading.js";
23
+ var COMMENT_IDENTITY = [
24
+ "id",
25
+ "author",
26
+ "date",
27
+ "initials"
28
+ ];
29
+ function sameAttributes(entry, original, names) {
30
+ return names.every(
31
+ (name) => attributeByLocalName(entry, name) === attributeByLocalName(original, name)
32
+ );
33
+ }
34
+ function arrivedEntries(xml, localName, idAttr) {
35
+ const entries = /* @__PURE__ */ new Map();
36
+ if (xml === null) return entries;
37
+ for (const el of elementChildren(parseXml(xml).documentElement)) {
38
+ if (el.localName !== localName) continue;
39
+ const id = attributeByLocalName(el, idAttr);
40
+ if (id === null || entries.has(id)) continue;
41
+ entries.set(id, { el, xml: serializeXml(el) });
42
+ }
43
+ return entries;
44
+ }
45
+ function submittedEntries(xml, namespace, localName, idAttr) {
46
+ if (xml === null) return [];
47
+ const entries = [];
48
+ const seen = /* @__PURE__ */ new Set();
49
+ for (const el of elementChildren(parseXml(xml).documentElement)) {
50
+ if (el.namespaceURI !== namespace || el.localName !== localName)
51
+ return null;
52
+ const id = attributeByLocalName(el, idAttr);
53
+ if (id === null || seen.has(id)) return null;
54
+ seen.add(id);
55
+ entries.push({ id, el, xml: serializeXml(el) });
56
+ }
57
+ return entries;
58
+ }
59
+ function referencedCommentIds(doc) {
60
+ const ids = /* @__PURE__ */ new Set();
61
+ for (const [id, comment] of commentReferencesIn(doc)) {
62
+ ids.add(id);
63
+ for (const reply of comment.replies) ids.add(reply.id);
64
+ }
65
+ return ids;
66
+ }
67
+ function wellFormedEntry(entry) {
68
+ if (entry.namespaceURI === W_NS && entry.localName === "comment") {
69
+ return attributesWithin(entry, COMMENT_ATTRIBUTES) && readStrictCommentBody(entry) !== null;
70
+ }
71
+ if (entry.namespaceURI === W15_NS && entry.localName === "commentEx") {
72
+ return wellFormedCommentExtension(entry);
73
+ }
74
+ if (entry.namespaceURI === W15_NS && entry.localName === "person") {
75
+ return wellFormedPerson(entry);
76
+ }
77
+ return false;
78
+ }
79
+ function withoutThreadKey(entry) {
80
+ const copy = entry.cloneNode(true);
81
+ if (!isElement(copy)) return serializeXml(entry);
82
+ const last = lastBodyParagraph(copy);
83
+ last?.removeAttributeNS(W14_NS, "paraId");
84
+ return serializeXml(copy);
85
+ }
86
+ function threadKeyAlone(entry, original) {
87
+ const before = lastParagraphId(original);
88
+ const after = lastParagraphId(entry);
89
+ const kept = before === null || after === before;
90
+ return kept && withoutThreadKey(entry) === withoutThreadKey(original);
91
+ }
92
+ function entryAllowed(entry, original, authorId, editableComments, people) {
93
+ if (entry.namespaceURI === W_NS && entry.localName === "comment") {
94
+ const author = attributeByLocalName(entry, "author");
95
+ const recorded = author === null ? null : commentAuthorId(people, author);
96
+ if (original === null) return recorded === authorId;
97
+ const paraId = lastParagraphId(original);
98
+ if (!sameAttributes(entry, original, COMMENT_IDENTITY) || paraId !== null && lastParagraphId(entry) !== paraId) {
99
+ return false;
100
+ }
101
+ return editableComments === "all" || recorded === null || recorded === authorId;
102
+ }
103
+ if (entry.namespaceURI === W15_NS && entry.localName === "commentEx") {
104
+ if (original === null) return true;
105
+ return sameAttributes(entry, original, ["paraId", "paraIdParent"]);
106
+ }
107
+ if (entry.namespaceURI === W15_NS && entry.localName === "person") {
108
+ return original === null && recordedIdentity(entry) === authorId;
109
+ }
110
+ return false;
111
+ }
112
+ function commentEntries(story) {
113
+ return Array.from(
114
+ arrivedEntries(story.session.comments.xml, "comment", "id").values(),
115
+ (entry) => entry.el
116
+ );
117
+ }
118
+ var COMMENT_PARTS = [
119
+ {
120
+ namespace: W_NS,
121
+ localName: "comment",
122
+ idAttr: "id",
123
+ xmlOf: (story) => story.session.comments.xml,
124
+ pathOf: (story) => story.session.comments.partPath,
125
+ referents: (story) => referencedCommentIds(story.doc)
126
+ },
127
+ {
128
+ namespace: W15_NS,
129
+ localName: "commentEx",
130
+ idAttr: "paraId",
131
+ xmlOf: (story) => story.session.comments.extendedXml,
132
+ pathOf: (story) => story.session.comments.extendedPartPath,
133
+ // Thread state stands for a comment, and it is keyed by the comment's own thread key
134
+ referents: (story) => new Set(
135
+ commentEntries(story).flatMap((el) => {
136
+ const key = lastParagraphId(el);
137
+ return key === null ? [] : [key];
138
+ })
139
+ )
140
+ },
141
+ {
142
+ namespace: W15_NS,
143
+ localName: "person",
144
+ idAttr: "author",
145
+ xmlOf: (story) => story.session.comments.people.xml,
146
+ pathOf: (story) => story.session.comments.people.partPath,
147
+ // An identity stands for a name somebody writes comments under
148
+ referents: (story) => new Set(
149
+ commentEntries(story).flatMap((el) => {
150
+ const author = attributeByLocalName(el, "author");
151
+ return author === null ? [] : [author];
152
+ })
153
+ )
154
+ }
155
+ ];
156
+ function partKept(kind, before, after, authorId, editableComments) {
157
+ const arrived = arrivedEntries(
158
+ kind.xmlOf(before),
159
+ kind.localName,
160
+ kind.idAttr
161
+ );
162
+ const submitted = submittedEntries(
163
+ kind.xmlOf(after),
164
+ kind.namespace,
165
+ kind.localName,
166
+ kind.idAttr
167
+ );
168
+ if (submitted === null) return false;
169
+ const stoodBehindNow = kind.referents(after);
170
+ const people = after.session.comments.people;
171
+ for (const entry of submitted) {
172
+ const original = arrived.get(entry.id);
173
+ if (original && original.xml === entry.xml) continue;
174
+ if (!stoodBehindNow.has(entry.id)) return false;
175
+ if (original && threadKeyAlone(entry.el, original.el)) continue;
176
+ if (!wellFormedEntry(entry.el) || !entryAllowed(
177
+ entry.el,
178
+ original?.el ?? null,
179
+ authorId,
180
+ editableComments,
181
+ people
182
+ )) {
183
+ return false;
184
+ }
185
+ }
186
+ const held = new Set(submitted.map((entry) => entry.id));
187
+ const stoodBehindBefore = kind.referents(before);
188
+ return Array.from(arrived.keys()).every(
189
+ (id) => stoodBehindBefore.has(id) || held.has(id)
190
+ );
191
+ }
192
+ function commentPartsKept(before, after, authorId, editableComments) {
193
+ for (const kind of COMMENT_PARTS) {
194
+ const path = kind.pathOf(after) ?? kind.pathOf(before);
195
+ if (path === null) continue;
196
+ if (!partKept(kind, before, after, authorId, editableComments)) {
197
+ return { ok: false, reason: "part-changed", part: path };
198
+ }
199
+ }
200
+ return { ok: true };
201
+ }
202
+ export {
203
+ commentPartsKept,
204
+ entryAllowed,
205
+ wellFormedEntry
206
+ };
@@ -7,5 +7,8 @@ import type { SessionStore } from "../session";
7
7
  export interface CommentPartChanges {
8
8
  parts: ReadonlyMap<string, Uint8Array>;
9
9
  }
10
- /** Plans the Comments part, relationship and content type only when comment state changed. */
10
+ /**
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.
13
+ */
11
14
  export declare function planCommentParts(doc: PMNode, session: SessionStore, relationships: RelationshipWriter, currentContentTypes?: Uint8Array): CommentPartChanges | null;