@portone/docx-editor 0.1.0 → 0.2.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 (72) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +17 -5
  3. package/dist/DocxEditor.d.ts +34 -10
  4. package/dist/DocxEditor.js +69 -24
  5. package/dist/core.d.ts +2 -0
  6. package/dist/core.js +2 -0
  7. package/dist/docx/commentOnlyChange.d.ts +45 -0
  8. package/dist/docx/commentOnlyChange.js +145 -0
  9. package/dist/docx/comments/constants.d.ts +8 -0
  10. package/dist/docx/comments/constants.js +6 -0
  11. package/dist/docx/comments/contentTypes.d.ts +7 -0
  12. package/dist/docx/comments/contentTypes.js +38 -0
  13. package/dist/docx/comments/model.d.ts +2 -0
  14. package/dist/docx/comments/model.js +3 -0
  15. package/dist/docx/comments/people.d.ts +39 -0
  16. package/dist/docx/comments/people.js +198 -0
  17. package/dist/docx/comments/reading.d.ts +16 -1
  18. package/dist/docx/comments/reading.js +15 -5
  19. package/dist/docx/comments/writing.d.ts +4 -1
  20. package/dist/docx/comments/writing.js +12 -32
  21. package/dist/docx/importParagraph.js +1 -0
  22. package/dist/editor/commands/breakCommands.js +4 -1
  23. package/dist/editor/commands/canRunCommand.d.ts +3 -2
  24. package/dist/editor/commands/canRunCommand.js +1 -1
  25. package/dist/editor/commands/comments/editing.js +6 -4
  26. package/dist/editor/commands/comments/model.d.ts +9 -0
  27. package/dist/editor/commands/comments/reading.d.ts +7 -0
  28. package/dist/editor/commands/comments/reading.js +14 -0
  29. package/dist/editor/commands/formatting/editing.js +6 -1
  30. package/dist/editor/commands/formatting/reading.js +13 -4
  31. package/dist/editor/commands/historyCommands.js +14 -6
  32. package/dist/editor/commands/index.d.ts +7 -1
  33. package/dist/editor/commands/index.js +4 -0
  34. package/dist/editor/commands/linkCommands.js +2 -0
  35. package/dist/editor/commands/lockCommands.js +2 -0
  36. package/dist/editor/commands/tabCommands.js +2 -1
  37. package/dist/editor/createEditor.d.ts +13 -3
  38. package/dist/editor/createEditor.js +21 -7
  39. package/dist/editor/insertImage.js +4 -1
  40. package/dist/editor/insertTable.js +2 -1
  41. package/dist/editor/paragraphEdits.d.ts +2 -0
  42. package/dist/editor/paragraphEdits.js +2 -0
  43. package/dist/editor/plugins/documentProtection.d.ts +21 -0
  44. package/dist/editor/plugins/documentProtection.js +44 -0
  45. package/dist/editor/plugins/imagePaste.js +4 -1
  46. package/dist/editor/plugins/keymap.d.ts +11 -1
  47. package/dist/editor/plugins/keymap.js +24 -4
  48. package/dist/editor/plugins/lockedContent.d.ts +4 -2
  49. package/dist/editor/plugins/lockedContent.js +1 -1
  50. package/dist/editor/plugins/tableContextMenu.js +2 -1
  51. package/dist/editor/plugins/textContextMenu.js +6 -1
  52. package/dist/index.d.ts +2 -0
  53. package/dist/numbering/markers.js +13 -1
  54. package/dist/schema/docxSchema.js +5 -0
  55. package/dist/schema/locks.d.ts +31 -3
  56. package/dist/schema/locks.js +54 -3
  57. package/dist/schema/protection.d.ts +77 -0
  58. package/dist/schema/protection.js +170 -0
  59. package/dist/schema/protectionState.d.ts +20 -0
  60. package/dist/schema/protectionState.js +37 -0
  61. package/dist/styles/fontStack.d.ts +9 -0
  62. package/dist/styles/fontStack.js +9 -8
  63. package/dist/table/cellFormatting.js +3 -1
  64. package/dist/table/commands.js +1 -1
  65. package/dist/table/merge.js +2 -2
  66. package/dist/ui/CommentsPanel.d.ts +3 -3
  67. package/dist/ui/CommentsPanel.js +26 -18
  68. package/dist/ui/FontFamilySelect.js +21 -7
  69. package/dist/ui/LinkCard.d.ts +1 -2
  70. package/dist/ui/LinkCard.js +3 -4
  71. package/dist/ui/TextMenu.js +15 -10
  72. package/package.json +2 -2
@@ -0,0 +1,38 @@
1
+ // src/docx/comments/contentTypes.ts
2
+ import { DocxExportError } from "../../ooxml/errors.js";
3
+ import { decodeUtf8, encodeUtf8 } from "../../ooxml/xml.js";
4
+ import { CONTENT_TYPES_PATH } from "./constants.js";
5
+ var TYPES_OPEN_TAG = /<(?:[\w.-]+:)?Types\b[^>]*>/;
6
+ function withContentType(parts, partPath, contentType, current) {
7
+ const original = current ?? parts.get(CONTENT_TYPES_PATH);
8
+ if (!original) {
9
+ throw new DocxExportError(
10
+ "missing-content-types",
11
+ `cannot add a part to a package that has no ${CONTENT_TYPES_PATH}`
12
+ );
13
+ }
14
+ const { text, hadBom } = decodeUtf8(original);
15
+ const partName = `/${partPath}`;
16
+ if (new RegExp(
17
+ `<(?:[\\w.-]+:)?Override[^>]+PartName=["']${partName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`,
18
+ "i"
19
+ ).test(text)) {
20
+ return null;
21
+ }
22
+ const open = TYPES_OPEN_TAG.exec(text);
23
+ if (!open) {
24
+ throw new DocxExportError(
25
+ "malformed-xml",
26
+ `${CONTENT_TYPES_PATH} has no Types element`
27
+ );
28
+ }
29
+ const rootName = /^<([^\s>]+)/.exec(open[0])?.[1] ?? "Types";
30
+ const separator = rootName.indexOf(":");
31
+ const prefix = separator < 0 ? "" : `${rootName.slice(0, separator)}:`;
32
+ const declaration = `<${prefix}Override PartName="${partName}" ContentType="${contentType}"/>`;
33
+ const at = open.index + open[0].length;
34
+ return encodeUtf8(text.slice(0, at) + declaration + text.slice(at), hadBom);
35
+ }
36
+ export {
37
+ withContentType
38
+ };
@@ -6,6 +6,7 @@ import type { ImportedComments } from "./reading";
6
6
  export interface CommentReferenceData {
7
7
  id: string;
8
8
  author: string | null;
9
+ authorId: string | null;
9
10
  initials: string | null;
10
11
  date: string | null;
11
12
  text: string;
@@ -20,6 +21,7 @@ export interface CommentReferenceData {
20
21
  export interface CommentReplyData {
21
22
  id: string;
22
23
  author: string | null;
24
+ authorId: string | null;
23
25
  initials: string | null;
24
26
  date: string | null;
25
27
  text: string;
@@ -23,6 +23,7 @@ function importedCommentReplies(comments, rootId) {
23
23
  replies.push({
24
24
  id: reply.id,
25
25
  author: reply.author,
26
+ authorId: reply.authorId,
26
27
  initials: reply.initials,
27
28
  date: reply.date,
28
29
  text: reply.text,
@@ -55,6 +56,7 @@ function replyData(value) {
55
56
  {
56
57
  id,
57
58
  author: nullableString(entry.author),
59
+ authorId: nullableString(entry.authorId),
58
60
  initials: nullableString(entry.initials),
59
61
  date: nullableString(entry.date),
60
62
  text: nullableString(entry.text) ?? "",
@@ -74,6 +76,7 @@ function referenceData(node) {
74
76
  return {
75
77
  id,
76
78
  author: nullableString(node.attrs.author),
79
+ authorId: nullableString(node.attrs.authorId),
77
80
  initials: nullableString(node.attrs.initials),
78
81
  date: nullableString(node.attrs.date),
79
82
  text: nullableString(node.attrs.text) ?? "",
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The people part, where Word records who a comment author is beyond the display name
3
+ * (`w15:people`): a `w15:person` carrying a `w15:presenceInfo` whose `w15:providerId` names the
4
+ * directory that issued `w15:userId`.
5
+ *
6
+ * This editor records the identity a host application hands it under a provider of its own
7
+ * (`COMMENT_AUTHOR_PROVIDER`). The `w15:author` name is the key both Word and this editor read the
8
+ * part by, so a name stands for one identity per file: a name the part already records is read as
9
+ * it stands and never appended to. Appending a second person for it would leave a file naming two
10
+ * identities for one name, where a reader keying by name hands one author's comments to the other.
11
+ */
12
+ import { type RelationshipWriter } from "../relationships";
13
+ import type { SessionStore } from "../session";
14
+ import type { CommentReferenceData, CommentReplyData } from "./model";
15
+ export interface ImportedPeople {
16
+ partPath: string | null;
17
+ xml: string | null;
18
+ hadBom: boolean;
19
+ /**
20
+ * Every author name the part records, mapped to the identity it stands for. Null where this
21
+ * editor cannot vouch for the name: another provider recorded it, or it is recorded twice over
22
+ * under different identities and nothing in the file says which of them wrote what.
23
+ */
24
+ byAuthor: ReadonlyMap<string, string | null>;
25
+ }
26
+ export declare const NO_PEOPLE: ImportedPeople;
27
+ /** Reads the people part related from the main document story. */
28
+ export declare function readPeople(parts: Map<string, Uint8Array>, mainPartPath: string): ImportedPeople;
29
+ /**
30
+ * The identity this editor recorded for the author of that name. Null for a name it did not
31
+ * record, and for one the part records under more than one identity.
32
+ */
33
+ export declare function commentAuthorId(people: ImportedPeople, author: string): string | null;
34
+ /**
35
+ * Plans the people part, its relationship and its content type for every identity the current
36
+ * comments carry that the document has not recorded. Null when it has recorded them all, which
37
+ * leaves the part as it arrived.
38
+ */
39
+ export declare function planPeoplePart(bodies: Iterable<CommentReferenceData | CommentReplyData>, session: SessionStore, relationships: RelationshipWriter, currentContentTypes: Uint8Array | undefined): ReadonlyMap<string, Uint8Array> | null;
@@ -0,0 +1,198 @@
1
+ // src/docx/comments/people.ts
2
+ import { DocxExportError } from "../../ooxml/errors.js";
3
+ import {
4
+ childByLocalName,
5
+ decodeUtf8,
6
+ elementChildren,
7
+ encodeUtf8,
8
+ escapeXml,
9
+ parseXml
10
+ } from "../../ooxml/xml.js";
11
+ import {
12
+ directoryOf,
13
+ readRelationships,
14
+ relsPathOf,
15
+ resolveTarget
16
+ } from "../relationships.js";
17
+ import {
18
+ COMMENT_AUTHOR_PROVIDER,
19
+ CONTENT_TYPES_PATH,
20
+ PEOPLE_CONTENT_TYPE,
21
+ PEOPLE_REL_TYPE,
22
+ W15_NS
23
+ } from "./constants.js";
24
+ import { withContentType } from "./contentTypes.js";
25
+ var NO_PEOPLE = {
26
+ partPath: null,
27
+ xml: null,
28
+ hadBom: false,
29
+ byAuthor: /* @__PURE__ */ new Map()
30
+ };
31
+ function attribute(el, localName) {
32
+ return Array.from(el.attributes).find((entry) => entry.localName === localName)?.value ?? null;
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 = attribute(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 (attribute(presence, "providerId") !== COMMENT_AUTHOR_PROVIDER) continue;
54
+ const ids = ourIds.get(author) ?? /* @__PURE__ */ new Set();
55
+ ids.add(attribute(presence, "userId"));
56
+ ourIds.set(author, ids);
57
+ }
58
+ for (const [author, ids] of ourIds) {
59
+ if (ids.size !== 1) continue;
60
+ byAuthor.set(author, Array.from(ids)[0] ?? null);
61
+ }
62
+ return { partPath, xml: text, hadBom, byAuthor };
63
+ }
64
+ function commentAuthorId(people, author) {
65
+ return people.byAuthor.get(author) ?? null;
66
+ }
67
+ function personXml(author, userId, prefix, declaration) {
68
+ return `<${prefix}person${declaration} ${prefix}author="${escapeXml(author)}"><${prefix}presenceInfo ${prefix}providerId="${COMMENT_AUTHOR_PROVIDER}" ${prefix}userId="${escapeXml(userId)}"/></${prefix}person>`;
69
+ }
70
+ function unrecordedAuthors(bodies, people) {
71
+ const unrecorded = /* @__PURE__ */ new Map();
72
+ for (const body of bodies) {
73
+ if (body.author === null || body.authorId === null) continue;
74
+ if (people.byAuthor.has(body.author)) continue;
75
+ unrecorded.set(body.author, body.authorId);
76
+ }
77
+ return unrecorded;
78
+ }
79
+ function w15Prefix(root) {
80
+ if (root.namespaceURI === W15_NS) {
81
+ return root.prefix === null ? "" : `${root.prefix}:`;
82
+ }
83
+ const declaration = Array.from(root.attributes).find(
84
+ (attr) => attr.value === W15_NS && (attr.name === "xmlns" || attr.name.startsWith("xmlns:"))
85
+ );
86
+ if (declaration === void 0) return null;
87
+ const prefix = declaration.name.slice("xmlns:".length);
88
+ return prefix === "" ? "" : `${prefix}:`;
89
+ }
90
+ function rootTagStart(xml) {
91
+ let at = 0;
92
+ for (; ; ) {
93
+ const opens = xml.indexOf("<", at);
94
+ if (opens === -1) return -1;
95
+ if (xml.startsWith("<!--", opens)) {
96
+ at = xml.indexOf("-->", opens + 4) + 3;
97
+ continue;
98
+ }
99
+ if (xml.startsWith("<?", opens)) {
100
+ at = xml.indexOf("?>", opens + 2) + 2;
101
+ continue;
102
+ }
103
+ return opens;
104
+ }
105
+ }
106
+ function openTagEnd(xml, at) {
107
+ let quote = null;
108
+ for (let i = at; i < xml.length; i += 1) {
109
+ const character = xml[i];
110
+ if (quote !== null) {
111
+ if (character === quote) quote = null;
112
+ continue;
113
+ }
114
+ if (character === '"' || character === "'") {
115
+ quote = character;
116
+ continue;
117
+ }
118
+ if (character === ">") {
119
+ return { end: i + 1, selfClosing: xml[i - 1] === "/" };
120
+ }
121
+ }
122
+ return null;
123
+ }
124
+ function malformed(detail) {
125
+ return new DocxExportError("malformed-xml", `the people part ${detail}`);
126
+ }
127
+ function peopleXml(people, added) {
128
+ const xml = people.xml;
129
+ if (xml === null) {
130
+ const persons2 = Array.from(
131
+ added,
132
+ ([author, userId]) => personXml(author, userId, "w15:", "")
133
+ );
134
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w15:people xmlns:w15="${W15_NS}">${persons2.join("")}</w15:people>`;
135
+ }
136
+ const root = parseXml(xml).documentElement;
137
+ if (root.localName !== "people") {
138
+ throw malformed("has no people root element");
139
+ }
140
+ const prefix = w15Prefix(root);
141
+ const persons = Array.from(
142
+ added,
143
+ ([author, userId]) => personXml(
144
+ author,
145
+ userId,
146
+ prefix ?? "w15:",
147
+ prefix === null ? ` xmlns:w15="${W15_NS}"` : ""
148
+ )
149
+ ).join("");
150
+ const start = rootTagStart(xml);
151
+ const open = start === -1 ? null : openTagEnd(xml, start);
152
+ if (open === null) throw malformed("has no people root element");
153
+ if (open.selfClosing) {
154
+ return `${xml.slice(0, open.end - 2)}>${persons}</${root.nodeName}>` + xml.slice(open.end);
155
+ }
156
+ const close = xml.lastIndexOf(`</${root.nodeName}`);
157
+ if (close === -1) throw malformed("has no closing people tag");
158
+ return xml.slice(0, close) + persons + xml.slice(close);
159
+ }
160
+ function availablePeoplePath(session) {
161
+ const directory = directoryOf(session.mainPartPath);
162
+ for (let suffix = 0; ; suffix += 1) {
163
+ const name = suffix === 0 ? "people.xml" : `people${suffix + 1}.xml`;
164
+ const path = directory + name;
165
+ if (!session.parts.has(path)) return path;
166
+ }
167
+ }
168
+ function planPeoplePart(bodies, session, relationships, currentContentTypes) {
169
+ const people = session.comments.people;
170
+ const added = unrecordedAuthors(bodies, people);
171
+ if (added.size === 0) return null;
172
+ const parts = /* @__PURE__ */ new Map();
173
+ const addingPart = people.partPath === null;
174
+ const partPath = people.partPath ?? availablePeoplePath(session);
175
+ if (addingPart) {
176
+ relationships.add({
177
+ type: PEOPLE_REL_TYPE,
178
+ target: partPath.slice(directoryOf(session.mainPartPath).length)
179
+ });
180
+ }
181
+ parts.set(partPath, encodeUtf8(peopleXml(people, added), people.hadBom));
182
+ if (addingPart || people.xml === null) {
183
+ const contentTypes = withContentType(
184
+ session.parts,
185
+ partPath,
186
+ PEOPLE_CONTENT_TYPE,
187
+ currentContentTypes
188
+ );
189
+ if (contentTypes) parts.set(CONTENT_TYPES_PATH, contentTypes);
190
+ }
191
+ return parts;
192
+ }
193
+ export {
194
+ NO_PEOPLE,
195
+ commentAuthorId,
196
+ planPeoplePart,
197
+ readPeople
198
+ };
@@ -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,11 @@ 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
+ /**
45
+ * Reads the Comments part related from the main document story.
46
+ *
47
+ * The people part is read whether or not there are comments: a document may carry one with no
48
+ * comment left, and a comment added to it then has to be recorded in that part rather than in a
49
+ * second one.
50
+ */
36
51
  export declare function readComments(parts: Map<string, Uint8Array>, mainPartPath: string): ImportedComments;
@@ -8,6 +8,11 @@ import {
8
8
  } from "../../ooxml/xml.js";
9
9
  import { readRelationships, relsPathOf, resolveTarget } from "../relationships.js";
10
10
  import { COMMENTS_EXTENDED_REL_TYPE, COMMENTS_REL_TYPE } from "./constants.js";
11
+ import {
12
+ commentAuthorId,
13
+ NO_PEOPLE,
14
+ readPeople
15
+ } from "./people.js";
11
16
  function attribute(el, localName) {
12
17
  return Array.from(el.attributes).find((entry) => entry.localName === localName)?.value ?? null;
13
18
  }
@@ -31,7 +36,8 @@ var NO_COMMENTS = {
31
36
  extendedPartPath: null,
32
37
  extendedXml: null,
33
38
  extendedHadBom: false,
34
- extendedOrdered: []
39
+ extendedOrdered: [],
40
+ people: NO_PEOPLE
35
41
  };
36
42
  function lastParagraphId(comment) {
37
43
  const paragraphs = Array.from(comment.getElementsByTagNameNS(W_NS, "p"));
@@ -83,13 +89,14 @@ function readCommentExtensions(parts, mainPartPath) {
83
89
  return { partPath, xml: text, hadBom, byParaId, ordered };
84
90
  }
85
91
  function readComments(parts, mainPartPath) {
92
+ const people = readPeople(parts, mainPartPath);
86
93
  const relationship = readRelationships(parts, relsPathOf(mainPartPath)).find(
87
94
  (entry) => entry.type === COMMENTS_REL_TYPE && !entry.external
88
95
  );
89
- if (!relationship) return NO_COMMENTS;
96
+ if (!relationship) return { ...NO_COMMENTS, people };
90
97
  const partPath = resolveTarget(mainPartPath, relationship.target);
91
98
  const bytes = parts.get(partPath);
92
- if (!bytes) return { ...NO_COMMENTS, partPath };
99
+ if (!bytes) return { ...NO_COMMENTS, partPath, people };
93
100
  const { text, hadBom } = decodeUtf8(bytes);
94
101
  const root = parseXml(text).documentElement;
95
102
  const extensions = readCommentExtensions(parts, mainPartPath);
@@ -98,10 +105,12 @@ function readComments(parts, mainPartPath) {
98
105
  if (id === null) return [];
99
106
  const paraId = lastParagraphId(el);
100
107
  const extension = paraId ? extensions.byParaId.get(paraId) : void 0;
108
+ const author = attribute(el, "author");
101
109
  return [
102
110
  {
103
111
  id,
104
- author: attribute(el, "author"),
112
+ author,
113
+ authorId: author === null ? null : commentAuthorId(people, author),
105
114
  initials: attribute(el, "initials"),
106
115
  date: attribute(el, "date"),
107
116
  text: commentText(el),
@@ -140,7 +149,8 @@ function readComments(parts, mainPartPath) {
140
149
  extendedPartPath: extensions.partPath,
141
150
  extendedXml: extensions.xml,
142
151
  extendedHadBom: extensions.hadBom,
143
- extendedOrdered: extensions.ordered
152
+ extendedOrdered: extensions.ordered,
153
+ people
144
154
  };
145
155
  }
146
156
  export {
@@ -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;
@@ -1,6 +1,6 @@
1
1
  // src/docx/comments/writing.ts
2
2
  import { DocxExportError } from "../../ooxml/errors.js";
3
- import { decodeUtf8, encodeUtf8, escapeXml, W_NS } from "../../ooxml/xml.js";
3
+ import { encodeUtf8, escapeXml, W_NS } from "../../ooxml/xml.js";
4
4
  import { directoryOf } from "../relationships.js";
5
5
  import {
6
6
  COMMENTS_CONTENT_TYPE,
@@ -12,9 +12,11 @@ import {
12
12
  W14_NS,
13
13
  W15_NS
14
14
  } from "./constants.js";
15
+ import { withContentType } from "./contentTypes.js";
15
16
  import {
16
17
  commentReferencesIn
17
18
  } from "./model.js";
19
+ import { planPeoplePart } from "./people.js";
18
20
  function commentsChanged(doc, session) {
19
21
  const current = commentReferencesIn(doc);
20
22
  if (current.size !== session.commentReferenceIds.size) return true;
@@ -213,37 +215,6 @@ function commentsXml(references, comments, originallyReferenced) {
213
215
  const openTag = hasThreadMetadata ? withThreadMarkupCompatibility(open[0]) : open[0];
214
216
  return comments.xml.slice(0, open.index) + openTag + pieces.join("") + comments.xml.slice(close);
215
217
  }
216
- var TYPES_OPEN_TAG = /<(?:[\w.-]+:)?Types\b[^>]*>/;
217
- function withContentType(parts, partPath, contentType, current) {
218
- const original = current ?? parts.get(CONTENT_TYPES_PATH);
219
- if (!original) {
220
- throw new DocxExportError(
221
- "missing-content-types",
222
- `cannot add a Comments part to a package that has no ${CONTENT_TYPES_PATH}`
223
- );
224
- }
225
- const { text, hadBom } = decodeUtf8(original);
226
- const partName = `/${partPath}`;
227
- if (new RegExp(
228
- `<(?:[\\w.-]+:)?Override[^>]+PartName=["']${partName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`,
229
- "i"
230
- ).test(text)) {
231
- return null;
232
- }
233
- const open = TYPES_OPEN_TAG.exec(text);
234
- if (!open) {
235
- throw new DocxExportError(
236
- "malformed-xml",
237
- `${CONTENT_TYPES_PATH} has no Types element`
238
- );
239
- }
240
- const rootName = /^<([^\s>]+)/.exec(open[0])?.[1] ?? "Types";
241
- const separator = rootName.indexOf(":");
242
- const prefix = separator < 0 ? "" : `${rootName.slice(0, separator)}:`;
243
- const declaration = `<${prefix}Override PartName="${partName}" ContentType="${contentType}"/>`;
244
- const at = open.index + open[0].length;
245
- return encodeUtf8(text.slice(0, at) + declaration + text.slice(at), hadBom);
246
- }
247
218
  function availableCommentsPath(session) {
248
219
  const directory = directoryOf(session.mainPartPath);
249
220
  for (let suffix = 0; ; suffix += 1) {
@@ -320,6 +291,15 @@ function planCommentParts(doc, session, relationships, currentContentTypes) {
320
291
  if (contentTypes) parts.set(CONTENT_TYPES_PATH, contentTypes);
321
292
  }
322
293
  }
294
+ if (bodyChanged) {
295
+ const people = planPeoplePart(
296
+ currentCommentBodies(references).values(),
297
+ session,
298
+ relationships,
299
+ parts.get(CONTENT_TYPES_PATH) ?? currentContentTypes
300
+ );
301
+ for (const [path, bytes] of people ?? []) parts.set(path, bytes);
302
+ }
323
303
  return { parts };
324
304
  }
325
305
  export {
@@ -81,6 +81,7 @@ function buildRunChild(el, marks, images, comments, notes, noteLabel) {
81
81
  id,
82
82
  referenceXml: serializeXml(el),
83
83
  author: comment?.author ?? null,
84
+ authorId: comment?.authorId ?? null,
84
85
  initials: comment?.initials ?? null,
85
86
  date: comment?.date ?? null,
86
87
  text: comment?.text ?? "",
@@ -1,9 +1,12 @@
1
1
  // src/editor/commands/breakCommands.ts
2
2
  import { docxSchema } from "../../schema/index.js";
3
3
  import { replacementShut } from "../../schema/locks.js";
4
+ import { editsShut } from "../../schema/protectionState.js";
4
5
  function insertBreak(brAttrs) {
5
6
  return (state, dispatch) => {
6
- if (replacementShut(state.selection, state.doc)) return false;
7
+ if (editsShut(state) || replacementShut(state.selection, state.doc)) {
8
+ return false;
9
+ }
7
10
  if (dispatch) {
8
11
  const br = docxSchema.nodes.hardBreak.create({ brAttrs });
9
12
  dispatch(state.tr.replaceSelectionWith(br).scrollIntoView());
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Dry-runs external commands and checks every produced transaction against document locks without
3
- * dispatching them or triggering the locked-content plugin's side effects.
2
+ * Dry-runs external commands and checks every produced transaction against document locks and the
3
+ * editor's protection without dispatching them or triggering the locked-content plugin's side
4
+ * effects.
4
5
  */
5
6
  import type { Command, EditorState } from "prosemirror-state";
6
7
  export declare function canRunCommand(command: Command, state: EditorState): boolean;
@@ -3,7 +3,7 @@ import { transactionAllowed } from "../../schema/locks.js";
3
3
  function canRunCommand(command, state) {
4
4
  const built = [];
5
5
  if (!command(state, (tr) => built.push(tr))) return false;
6
- return built.every((tr) => transactionAllowed(tr, state.doc));
6
+ return built.every((tr) => transactionAllowed(tr, state));
7
7
  }
8
8
  export {
9
9
  canRunCommand
@@ -93,6 +93,7 @@ function addCommentTransaction(state, comment) {
93
93
  id,
94
94
  referenceXml: null,
95
95
  author: comment.author,
96
+ authorId: comment.authorId ?? null,
96
97
  initials: comment.initials ?? null,
97
98
  date,
98
99
  text: comment.text,
@@ -113,7 +114,7 @@ function addCommentTransaction(state, comment) {
113
114
  startMarks
114
115
  );
115
116
  const transaction = state.tr.insert(to, end).insert(to + 1, reference).insert(from, start);
116
- return transactionAllowed(transaction, state.doc) ? transaction : null;
117
+ return transactionAllowed(transaction, state) ? transaction : null;
117
118
  }
118
119
  function canAddComment(state) {
119
120
  return addCommentTransaction(state, {
@@ -147,7 +148,7 @@ function updateComment(id, text) {
147
148
  }
148
149
  return true;
149
150
  });
150
- if (!changed || !transactionAllowed(transaction, state.doc)) return false;
151
+ if (!changed || !transactionAllowed(transaction, state)) return false;
151
152
  dispatch?.(transaction);
152
153
  return true;
153
154
  };
@@ -167,7 +168,7 @@ function updateReference(id, change) {
167
168
  }
168
169
  return false;
169
170
  });
170
- if (!changed || !transactionAllowed(transaction, state.doc)) return false;
171
+ if (!changed || !transactionAllowed(transaction, state)) return false;
171
172
  dispatch?.(transaction);
172
173
  return true;
173
174
  };
@@ -207,6 +208,7 @@ function addCommentReply(id, reply) {
207
208
  {
208
209
  id: replyId,
209
210
  author: reply.author,
211
+ authorId: reply.authorId ?? null,
210
212
  initials: reply.initials ?? null,
211
213
  date,
212
214
  text: reply.text,
@@ -275,7 +277,7 @@ function removeComment(id) {
275
277
  for (const marker of positions.sort((a, b) => b.pos - a.pos)) {
276
278
  transaction.delete(marker.pos, marker.pos + marker.size);
277
279
  }
278
- if (!transactionAllowed(transaction, state.doc)) return false;
280
+ if (!transactionAllowed(transaction, state)) return false;
279
281
  dispatch?.(transaction);
280
282
  return true;
281
283
  };
@@ -1,11 +1,18 @@
1
1
  /** Public comment values and shared attribute readers. */
2
2
  export interface CommentAuthor {
3
+ /**
4
+ * The identity behind the name, an opaque string the host application chooses. Recorded in the
5
+ * document's people part, and what decides whose comment a comment is (`schema/protection`).
6
+ */
7
+ id: string;
3
8
  name: string;
4
9
  initials?: string;
5
10
  }
6
11
  export interface NewComment {
7
12
  text: string;
8
13
  author: string;
14
+ /** The identity behind `author`. A comment written without one belongs to nobody in particular */
15
+ authorId?: string;
9
16
  initials?: string;
10
17
  /** ISO 8601 timestamp. The current time is used when omitted. */
11
18
  date?: string;
@@ -13,6 +20,7 @@ export interface NewComment {
13
20
  export interface DocumentComment {
14
21
  id: string;
15
22
  author: string | null;
23
+ authorId: string | null;
16
24
  initials: string | null;
17
25
  date: string | null;
18
26
  text: string;
@@ -25,6 +33,7 @@ export interface DocumentComment {
25
33
  export interface DocumentCommentReply {
26
34
  id: string;
27
35
  author: string | null;
36
+ authorId: string | null;
28
37
  initials: string | null;
29
38
  date: string | null;
30
39
  text: string;
@@ -6,3 +6,10 @@ export declare function stringAttr(value: unknown): string | null;
6
6
  export declare function repliesAttr(value: unknown): readonly CommentReplyData[];
7
7
  /** Comments in document order, including point comments that have no explicit range. */
8
8
  export declare function documentComments(state: EditorState): readonly DocumentComment[];
9
+ /**
10
+ * Whether the body of this comment, or of one of its replies, may be edited or deleted here: the
11
+ * protection lets comments be edited at all, and the body is one of one's own to edit
12
+ * (`schema/protection`). Replying and settling a thread are not governed by this; a comment command
13
+ * asked without `dispatch` answers for those.
14
+ */
15
+ export declare function canEditComment(state: EditorState, commentId: string, replyId?: string | null): boolean;