@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.
- package/CHANGELOG.md +29 -0
- package/README.md +17 -5
- package/dist/DocxEditor.d.ts +34 -10
- package/dist/DocxEditor.js +69 -24
- package/dist/core.d.ts +2 -0
- package/dist/core.js +2 -0
- package/dist/docx/commentOnlyChange.d.ts +45 -0
- package/dist/docx/commentOnlyChange.js +145 -0
- package/dist/docx/comments/constants.d.ts +8 -0
- package/dist/docx/comments/constants.js +6 -0
- package/dist/docx/comments/contentTypes.d.ts +7 -0
- package/dist/docx/comments/contentTypes.js +38 -0
- package/dist/docx/comments/model.d.ts +2 -0
- package/dist/docx/comments/model.js +3 -0
- package/dist/docx/comments/people.d.ts +39 -0
- package/dist/docx/comments/people.js +198 -0
- package/dist/docx/comments/reading.d.ts +16 -1
- package/dist/docx/comments/reading.js +15 -5
- package/dist/docx/comments/writing.d.ts +4 -1
- package/dist/docx/comments/writing.js +12 -32
- package/dist/docx/importParagraph.js +1 -0
- package/dist/editor/commands/breakCommands.js +4 -1
- package/dist/editor/commands/canRunCommand.d.ts +3 -2
- package/dist/editor/commands/canRunCommand.js +1 -1
- package/dist/editor/commands/comments/editing.js +6 -4
- package/dist/editor/commands/comments/model.d.ts +9 -0
- package/dist/editor/commands/comments/reading.d.ts +7 -0
- package/dist/editor/commands/comments/reading.js +14 -0
- package/dist/editor/commands/formatting/editing.js +6 -1
- package/dist/editor/commands/formatting/reading.js +13 -4
- package/dist/editor/commands/historyCommands.js +14 -6
- package/dist/editor/commands/index.d.ts +7 -1
- package/dist/editor/commands/index.js +4 -0
- package/dist/editor/commands/linkCommands.js +2 -0
- package/dist/editor/commands/lockCommands.js +2 -0
- package/dist/editor/commands/tabCommands.js +2 -1
- package/dist/editor/createEditor.d.ts +13 -3
- package/dist/editor/createEditor.js +21 -7
- package/dist/editor/insertImage.js +4 -1
- package/dist/editor/insertTable.js +2 -1
- package/dist/editor/paragraphEdits.d.ts +2 -0
- package/dist/editor/paragraphEdits.js +2 -0
- package/dist/editor/plugins/documentProtection.d.ts +21 -0
- package/dist/editor/plugins/documentProtection.js +44 -0
- package/dist/editor/plugins/imagePaste.js +4 -1
- package/dist/editor/plugins/keymap.d.ts +11 -1
- package/dist/editor/plugins/keymap.js +24 -4
- package/dist/editor/plugins/lockedContent.d.ts +4 -2
- package/dist/editor/plugins/lockedContent.js +1 -1
- package/dist/editor/plugins/tableContextMenu.js +2 -1
- package/dist/editor/plugins/textContextMenu.js +6 -1
- package/dist/index.d.ts +2 -0
- package/dist/numbering/markers.js +13 -1
- package/dist/schema/docxSchema.js +5 -0
- package/dist/schema/locks.d.ts +31 -3
- package/dist/schema/locks.js +54 -3
- package/dist/schema/protection.d.ts +77 -0
- package/dist/schema/protection.js +170 -0
- package/dist/schema/protectionState.d.ts +20 -0
- package/dist/schema/protectionState.js +37 -0
- package/dist/styles/fontStack.d.ts +9 -0
- package/dist/styles/fontStack.js +9 -8
- package/dist/table/cellFormatting.js +3 -1
- package/dist/table/commands.js +1 -1
- package/dist/table/merge.js +2 -2
- package/dist/ui/CommentsPanel.d.ts +3 -3
- package/dist/ui/CommentsPanel.js +26 -18
- package/dist/ui/FontFamilySelect.js +21 -7
- package/dist/ui/LinkCard.d.ts +1 -2
- package/dist/ui/LinkCard.js +3 -4
- package/dist/ui/TextMenu.js +15 -10
- 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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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 {
|
|
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))
|
|
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
|
|
3
|
-
* dispatching them or triggering the locked-content plugin's side
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|