@portone/docx-editor 0.2.0 → 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.
- package/CHANGELOG.md +65 -0
- package/CONTRIBUTING.md +4 -0
- package/dist/docx/commentOnlyChange.d.ts +3 -1
- package/dist/docx/commentOnlyChange.js +38 -21
- package/dist/docx/comments/constants.d.ts +3 -3
- package/dist/docx/comments/constants.js +4 -3
- package/dist/docx/comments/grammar.d.ts +80 -0
- package/dist/docx/comments/grammar.js +213 -0
- package/dist/docx/comments/people.js +12 -14
- package/dist/docx/comments/reading.d.ts +5 -3
- package/dist/docx/comments/reading.js +23 -20
- package/dist/docx/comments/verifying.d.ts +44 -0
- package/dist/docx/comments/verifying.js +206 -0
- package/dist/docx/comments/writing.js +73 -46
- package/dist/docx/exportDocx.js +2 -1
- package/dist/docx/formatting/direct.js +3 -6
- package/dist/docx/headersFooters.js +8 -10
- package/dist/docx/notes.js +3 -5
- package/dist/docx/paraProps.js +51 -46
- package/dist/docx/propsXml.d.ts +20 -2
- package/dist/docx/propsXml.js +72 -32
- package/dist/docx/relationships.js +8 -3
- package/dist/docx/runProps.js +34 -34
- package/dist/docx/sdt.js +16 -14
- package/dist/docx/serializeBlock.js +1 -2
- package/dist/docx/serializeParagraph.d.ts +2 -1
- package/dist/docx/serializeParagraph.js +25 -14
- package/dist/docx/serializeTable.js +38 -24
- package/dist/docx/storyProjection.d.ts +36 -0
- package/dist/docx/storyProjection.js +23 -0
- package/dist/docx/tableFormatting/editing.js +49 -73
- package/dist/docx/tableTemplate.js +24 -3
- package/dist/docx/theme.d.ts +0 -6
- package/dist/docx/theme.js +0 -8
- package/dist/editor/clipboard/images.js +17 -3
- package/dist/editor/clipboard/inlineFormatting.d.ts +7 -0
- package/dist/editor/clipboard/inlineFormatting.js +1 -0
- package/dist/editor/commands/breakCommands.js +4 -3
- package/dist/editor/commands/canRunCommand.d.ts +2 -2
- package/dist/editor/commands/canRunCommand.js +1 -1
- package/dist/editor/commands/comments/editing.js +3 -6
- package/dist/editor/commands/historyCommands.js +2 -1
- package/dist/editor/commands/lockCommands.js +10 -1
- package/dist/editor/commands/tabCommands.js +4 -3
- package/dist/editor/createEditor.js +3 -9
- package/dist/editor/externalClipboard.js +166 -4
- package/dist/editor/insertImage.js +4 -3
- package/dist/editor/plugins/documentProtection.d.ts +3 -3
- package/dist/editor/plugins/lockedContent.d.ts +4 -4
- package/dist/editor/plugins/lockedContent.js +1 -1
- package/dist/numbering/listTemplate.js +32 -10
- package/dist/ooxml/element.d.ts +52 -0
- package/dist/ooxml/element.js +49 -0
- package/dist/ooxml/fragment.d.ts +53 -0
- package/dist/ooxml/fragment.js +76 -0
- package/dist/ooxml/image.js +4 -3
- package/dist/ooxml/names.d.ts +27 -0
- package/dist/ooxml/names.js +29 -0
- package/dist/ooxml/precedence.d.ts +31 -0
- package/dist/ooxml/precedence.js +51 -0
- package/dist/ooxml/xml.d.ts +31 -0
- package/dist/ooxml/xml.js +27 -0
- package/dist/page/blockKinds.d.ts +42 -0
- package/dist/page/blockKinds.js +0 -0
- package/dist/page/measureBlocks.d.ts +14 -1
- package/dist/page/measureBlocks.js +31 -11
- package/dist/page/pageDecorations.d.ts +16 -0
- package/dist/page/pageDecorations.js +1 -0
- package/dist/page/pageLayout.d.ts +8 -43
- package/dist/page/pageLayout.js +28 -51
- package/dist/page/tableMeasurements.d.ts +10 -2
- package/dist/page/tableMeasurements.js +20 -16
- package/dist/page/usePageLayout.d.ts +15 -0
- package/dist/page/usePageLayout.js +30 -2
- package/dist/schema/attrRoles.d.ts +35 -0
- package/dist/schema/attrRoles.js +106 -0
- package/dist/schema/docxSchema.d.ts +6 -0
- package/dist/schema/docxSchema.js +212 -101
- package/dist/schema/editGuard.d.ts +99 -0
- package/dist/schema/editGuard.js +43 -0
- package/dist/schema/guards.d.ts +68 -0
- package/dist/schema/guards.js +82 -0
- package/dist/schema/locks.d.ts +8 -35
- package/dist/schema/locks.js +29 -64
- package/dist/schema/preservedGuards.d.ts +30 -0
- package/dist/schema/preservedGuards.js +50 -0
- package/dist/schema/protection.d.ts +10 -3
- package/dist/schema/protection.js +2 -1
- package/dist/schema/sourceEquality.d.ts +27 -0
- package/dist/schema/sourceEquality.js +65 -0
- package/dist/table/cellFormatting.js +2 -1
- package/dist/table/commands.d.ts +2 -2
- package/dist/table/commands.js +1 -1
- package/dist/table/index.d.ts +2 -2
- package/dist/table/merge.d.ts +2 -2
- package/dist/table/merge.js +1 -1
- package/package.json +4 -1
- package/dist/editor/plugins/bookmarkProtection.d.ts +0 -3
- package/dist/editor/plugins/bookmarkProtection.js +0 -28
- package/dist/editor/plugins/noteProtection.d.ts +0 -3
- package/dist/editor/plugins/noteProtection.js +0 -25
|
@@ -41,11 +41,13 @@ export interface ImportedCommentExtension {
|
|
|
41
41
|
resolved: boolean;
|
|
42
42
|
xml: string;
|
|
43
43
|
}
|
|
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;
|
|
44
46
|
/**
|
|
45
47
|
* Reads the Comments part related from the main document story.
|
|
46
48
|
*
|
|
47
|
-
* The people part
|
|
48
|
-
* comment left, and a comment added to it then has to be
|
|
49
|
-
* second one.
|
|
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.
|
|
50
52
|
*/
|
|
51
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,14 +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";
|
|
12
|
+
import { lastBodyParagraph } from "./grammar.js";
|
|
11
13
|
import {
|
|
12
14
|
commentAuthorId,
|
|
13
15
|
NO_PEOPLE,
|
|
14
16
|
readPeople
|
|
15
17
|
} from "./people.js";
|
|
16
|
-
function attribute(el, localName) {
|
|
17
|
-
return Array.from(el.attributes).find((entry) => entry.localName === localName)?.value ?? null;
|
|
18
|
-
}
|
|
19
18
|
function inlineCommentText(node) {
|
|
20
19
|
if (node.localName === "t") return node.textContent ?? "";
|
|
21
20
|
if (node.localName === "tab") return " ";
|
|
@@ -40,8 +39,8 @@ var NO_COMMENTS = {
|
|
|
40
39
|
people: NO_PEOPLE
|
|
41
40
|
};
|
|
42
41
|
function lastParagraphId(comment) {
|
|
43
|
-
const
|
|
44
|
-
return
|
|
42
|
+
const last = lastBodyParagraph(comment);
|
|
43
|
+
return last === null ? null : attributeByLocalName(last, "paraId");
|
|
45
44
|
}
|
|
46
45
|
function readCommentExtensions(parts, mainPartPath) {
|
|
47
46
|
const relationship = readRelationships(parts, relsPathOf(mainPartPath)).find(
|
|
@@ -73,13 +72,13 @@ function readCommentExtensions(parts, mainPartPath) {
|
|
|
73
72
|
const ordered = [];
|
|
74
73
|
for (const el of elementChildren(root)) {
|
|
75
74
|
if (el.localName !== "commentEx") continue;
|
|
76
|
-
const paraId =
|
|
75
|
+
const paraId = attributeByLocalName(el, "paraId");
|
|
77
76
|
if (paraId === null) continue;
|
|
78
77
|
const extension = {
|
|
79
78
|
paraId,
|
|
80
|
-
parentParaId:
|
|
79
|
+
parentParaId: attributeByLocalName(el, "paraIdParent"),
|
|
81
80
|
resolved: ["1", "true", "on"].includes(
|
|
82
|
-
|
|
81
|
+
attributeByLocalName(el, "done")?.toLowerCase() ?? ""
|
|
83
82
|
),
|
|
84
83
|
xml: serializeXml(el)
|
|
85
84
|
};
|
|
@@ -90,29 +89,36 @@ function readCommentExtensions(parts, mainPartPath) {
|
|
|
90
89
|
}
|
|
91
90
|
function readComments(parts, mainPartPath) {
|
|
92
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
|
+
};
|
|
93
100
|
const relationship = readRelationships(parts, relsPathOf(mainPartPath)).find(
|
|
94
101
|
(entry) => entry.type === COMMENTS_REL_TYPE && !entry.external
|
|
95
102
|
);
|
|
96
|
-
if (!relationship) return { ...NO_COMMENTS,
|
|
103
|
+
if (!relationship) return { ...NO_COMMENTS, ...aside };
|
|
97
104
|
const partPath = resolveTarget(mainPartPath, relationship.target);
|
|
98
105
|
const bytes = parts.get(partPath);
|
|
99
|
-
if (!bytes) return { ...NO_COMMENTS, partPath,
|
|
106
|
+
if (!bytes) return { ...NO_COMMENTS, partPath, ...aside };
|
|
100
107
|
const { text, hadBom } = decodeUtf8(bytes);
|
|
101
108
|
const root = parseXml(text).documentElement;
|
|
102
|
-
const extensions = readCommentExtensions(parts, mainPartPath);
|
|
103
109
|
const base = elementChildren(root).filter((el) => el.localName === "comment").flatMap((el) => {
|
|
104
|
-
const id =
|
|
110
|
+
const id = attributeByLocalName(el, "id");
|
|
105
111
|
if (id === null) return [];
|
|
106
112
|
const paraId = lastParagraphId(el);
|
|
107
113
|
const extension = paraId ? extensions.byParaId.get(paraId) : void 0;
|
|
108
|
-
const author =
|
|
114
|
+
const author = attributeByLocalName(el, "author");
|
|
109
115
|
return [
|
|
110
116
|
{
|
|
111
117
|
id,
|
|
112
118
|
author,
|
|
113
119
|
authorId: author === null ? null : commentAuthorId(people, author),
|
|
114
|
-
initials:
|
|
115
|
-
date:
|
|
120
|
+
initials: attributeByLocalName(el, "initials"),
|
|
121
|
+
date: attributeByLocalName(el, "date"),
|
|
116
122
|
text: commentText(el),
|
|
117
123
|
xml: serializeXml(el),
|
|
118
124
|
paraId,
|
|
@@ -146,14 +152,11 @@ function readComments(parts, mainPartPath) {
|
|
|
146
152
|
ordered,
|
|
147
153
|
byId,
|
|
148
154
|
repliesByParentId,
|
|
149
|
-
|
|
150
|
-
extendedXml: extensions.xml,
|
|
151
|
-
extendedHadBom: extensions.hadBom,
|
|
152
|
-
extendedOrdered: extensions.ordered,
|
|
153
|
-
people
|
|
155
|
+
...aside
|
|
154
156
|
};
|
|
155
157
|
}
|
|
156
158
|
export {
|
|
157
159
|
NO_COMMENTS,
|
|
160
|
+
lastParagraphId,
|
|
158
161
|
readComments
|
|
159
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
|
+
};
|
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
// src/docx/comments/writing.ts
|
|
2
|
+
import { elementXml, xmlnsAttr } from "../../ooxml/element.js";
|
|
2
3
|
import { DocxExportError } from "../../ooxml/errors.js";
|
|
3
|
-
import {
|
|
4
|
+
import { wName, xmlnsDecl } from "../../ooxml/names.js";
|
|
5
|
+
import { encodeUtf8 } from "../../ooxml/xml.js";
|
|
4
6
|
import { directoryOf } from "../relationships.js";
|
|
5
7
|
import {
|
|
6
8
|
COMMENTS_CONTENT_TYPE,
|
|
7
9
|
COMMENTS_EXTENDED_CONTENT_TYPE,
|
|
8
10
|
COMMENTS_EXTENDED_REL_TYPE,
|
|
9
11
|
COMMENTS_REL_TYPE,
|
|
10
|
-
CONTENT_TYPES_PATH
|
|
11
|
-
MC_NS,
|
|
12
|
-
W14_NS,
|
|
13
|
-
W15_NS
|
|
12
|
+
CONTENT_TYPES_PATH
|
|
14
13
|
} from "./constants.js";
|
|
15
14
|
import { withContentType } from "./contentTypes.js";
|
|
15
|
+
import {
|
|
16
|
+
arrivedEntries,
|
|
17
|
+
renderCommentBody,
|
|
18
|
+
renderCommentExtension,
|
|
19
|
+
withThreadKey
|
|
20
|
+
} from "./grammar.js";
|
|
16
21
|
import {
|
|
17
22
|
commentReferencesIn
|
|
18
23
|
} from "./model.js";
|
|
19
24
|
import { planPeoplePart } from "./people.js";
|
|
25
|
+
function needsThreadKey(id, comment, session) {
|
|
26
|
+
return comment.imported && carriesThreadMetadata(comment) && (session.comments.byId.get(id)?.paraId ?? null) === null;
|
|
27
|
+
}
|
|
20
28
|
function commentsChanged(doc, session) {
|
|
21
29
|
const current = commentReferencesIn(doc);
|
|
22
30
|
if (current.size !== session.commentReferenceIds.size) return true;
|
|
@@ -36,14 +44,11 @@ function commentsChanged(doc, session) {
|
|
|
36
44
|
for (const comment of current.values()) {
|
|
37
45
|
if (comment.replies.some((reply) => !reply.imported)) return true;
|
|
38
46
|
}
|
|
47
|
+
for (const [id, comment] of current) {
|
|
48
|
+
if (needsThreadKey(id, comment, session)) return true;
|
|
49
|
+
}
|
|
39
50
|
return false;
|
|
40
51
|
}
|
|
41
|
-
function renderedExtension(comment) {
|
|
42
|
-
if (comment.extensionXml !== null) return comment.extensionXml;
|
|
43
|
-
const parent = "parentParaId" in comment ? ` w15:paraIdParent="${escapeXml(comment.parentParaId)}"` : "";
|
|
44
|
-
const done = "resolved" in comment ? ` w15:done="${comment.resolved ? "1" : "0"}"` : "";
|
|
45
|
-
return `<w15:commentEx w15:paraId="${escapeXml(comment.paraId)}"${parent}${done}/>`;
|
|
46
|
-
}
|
|
47
52
|
function extensionsXml(references, comments, originallyReferenced) {
|
|
48
53
|
const current = currentCommentBodies(references);
|
|
49
54
|
const originalThreads = originalThreadIds(comments, originallyReferenced);
|
|
@@ -58,19 +63,23 @@ function extensionsXml(references, comments, originallyReferenced) {
|
|
|
58
63
|
const id = idByParaId.get(original.paraId);
|
|
59
64
|
const item = id === void 0 ? void 0 : current.get(id);
|
|
60
65
|
if (id !== void 0 && item && !written.has(id)) {
|
|
61
|
-
pieces.push(
|
|
66
|
+
pieces.push(renderCommentExtension(item));
|
|
62
67
|
written.add(id);
|
|
63
68
|
} else if (id === void 0 || !originalThreads.has(id)) {
|
|
64
69
|
pieces.push(original.xml);
|
|
65
70
|
}
|
|
66
71
|
}
|
|
67
72
|
for (const [id, comment] of current) {
|
|
68
|
-
if (!written.has(id)
|
|
73
|
+
if (!written.has(id) && carriesThreadMetadata(comment)) {
|
|
74
|
+
pieces.push(renderCommentExtension(comment));
|
|
75
|
+
}
|
|
69
76
|
}
|
|
70
77
|
if (comments.extendedXml === null) {
|
|
71
|
-
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w15:commentsEx
|
|
78
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w15:commentsEx ${xmlnsDecl("w15")}>${pieces.join("")}</w15:commentsEx>`;
|
|
72
79
|
}
|
|
73
|
-
const open = /<(?:[
|
|
80
|
+
const open = /<(?:[^\s<>/:="']+:)?commentsEx\b[^>]*>/.exec(
|
|
81
|
+
comments.extendedXml
|
|
82
|
+
);
|
|
74
83
|
if (!open) {
|
|
75
84
|
throw new DocxExportError(
|
|
76
85
|
"malformed-xml",
|
|
@@ -78,14 +87,23 @@ function extensionsXml(references, comments, originallyReferenced) {
|
|
|
78
87
|
);
|
|
79
88
|
}
|
|
80
89
|
const name = /^<([^\s>]+)/.exec(open[0])?.[1];
|
|
81
|
-
|
|
90
|
+
if (name === void 0) {
|
|
91
|
+
throw new DocxExportError(
|
|
92
|
+
"malformed-xml",
|
|
93
|
+
"the Comments Extended part has no commentsEx root element"
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const bodyAt = open.index + open[0].length;
|
|
97
|
+
if (open[0].endsWith("/>")) {
|
|
98
|
+
return comments.extendedXml.slice(0, open.index) + `${open[0].slice(0, -2)}>${pieces.join("")}</${name}>` + comments.extendedXml.slice(bodyAt);
|
|
99
|
+
}
|
|
100
|
+
const close = comments.extendedXml.lastIndexOf(`</${name}>`);
|
|
82
101
|
if (close === -1) {
|
|
83
102
|
throw new DocxExportError(
|
|
84
103
|
"malformed-xml",
|
|
85
104
|
"the Comments Extended part has no closing commentsEx tag"
|
|
86
105
|
);
|
|
87
106
|
}
|
|
88
|
-
const bodyAt = open.index + open[0].length;
|
|
89
107
|
return comments.extendedXml.slice(0, bodyAt) + pieces.join("") + comments.extendedXml.slice(close);
|
|
90
108
|
}
|
|
91
109
|
function extensionsChanged(doc, session) {
|
|
@@ -103,41 +121,42 @@ function extensionsChanged(doc, session) {
|
|
|
103
121
|
}
|
|
104
122
|
return false;
|
|
105
123
|
}
|
|
106
|
-
function commentTextXml(text) {
|
|
107
|
-
const lines = text.split("\n");
|
|
108
|
-
const pieces = [];
|
|
109
|
-
lines.forEach((line, index) => {
|
|
110
|
-
if (index > 0) pieces.push("<w:br/>");
|
|
111
|
-
if (line.length > 0 || lines.length === 1) {
|
|
112
|
-
pieces.push(`<w:t xml:space="preserve">${escapeXml(line)}</w:t>`);
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
return `<w:r>${pieces.join("")}</w:r>`;
|
|
116
|
-
}
|
|
117
124
|
function carriesThreadMetadata(comment) {
|
|
118
125
|
return comment.extensionXml !== null || "threadImported" in comment && !comment.threadImported || "parentParaId" in comment;
|
|
119
126
|
}
|
|
120
|
-
function
|
|
127
|
+
function keyedEntry(comment, arrivedKeyed) {
|
|
128
|
+
return carriesThreadMetadata(comment) || arrivedKeyed.has(comment.id);
|
|
129
|
+
}
|
|
130
|
+
function renderedComment(comment, arrivedKeyed, arrived) {
|
|
121
131
|
if (comment.imported && comment.commentXml !== null) {
|
|
122
|
-
return comment
|
|
132
|
+
return carriesThreadMetadata(comment) ? withThreadKey(
|
|
133
|
+
comment.commentXml,
|
|
134
|
+
comment.paraId,
|
|
135
|
+
arrived.get(comment.id) ?? null
|
|
136
|
+
) : comment.commentXml;
|
|
123
137
|
}
|
|
124
138
|
const attrs = [
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
comment.
|
|
128
|
-
comment.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
139
|
+
xmlnsAttr("w"),
|
|
140
|
+
[wName("id"), comment.id],
|
|
141
|
+
comment.author === null ? null : [wName("author"), comment.author],
|
|
142
|
+
comment.date === null ? null : [wName("date"), comment.date],
|
|
143
|
+
comment.initials === null ? null : [wName("initials"), comment.initials]
|
|
144
|
+
];
|
|
145
|
+
const paraId = keyedEntry(comment, arrivedKeyed) ? comment.paraId : null;
|
|
146
|
+
const body = renderCommentBody(comment.text, paraId);
|
|
147
|
+
return elementXml(
|
|
148
|
+
wName("comment"),
|
|
149
|
+
attrs.filter((attr) => attr !== null),
|
|
150
|
+
[body]
|
|
151
|
+
);
|
|
133
152
|
}
|
|
134
153
|
function withThreadMarkupCompatibility(openTag) {
|
|
135
154
|
let updated = openTag;
|
|
136
155
|
if (!/\sxmlns:w14\s*=/.test(updated)) {
|
|
137
|
-
updated = updated.replace(/>$/, `
|
|
156
|
+
updated = updated.replace(/>$/, ` ${xmlnsDecl("w14")}>`);
|
|
138
157
|
}
|
|
139
158
|
if (!/\sxmlns:mc\s*=/.test(updated)) {
|
|
140
|
-
updated = updated.replace(/>$/, `
|
|
159
|
+
updated = updated.replace(/>$/, ` ${xmlnsDecl("mc")}>`);
|
|
141
160
|
}
|
|
142
161
|
const ignorable = /\smc:Ignorable\s*=\s*(["'])([^"']*)\1/.exec(updated);
|
|
143
162
|
if (!ignorable) {
|
|
@@ -172,16 +191,22 @@ function originalThreadIds(comments, rootIds) {
|
|
|
172
191
|
}
|
|
173
192
|
function commentsXml(references, comments, originallyReferenced) {
|
|
174
193
|
const currentBodies = currentCommentBodies(references);
|
|
194
|
+
const arrivedKeyed = new Set(
|
|
195
|
+
Array.from(comments.byId.values()).flatMap(
|
|
196
|
+
(entry) => entry.paraId === null ? [] : [entry.id]
|
|
197
|
+
)
|
|
198
|
+
);
|
|
175
199
|
const hasThreadMetadata = Array.from(currentBodies.values()).some(
|
|
176
|
-
|
|
200
|
+
(comment) => keyedEntry(comment, arrivedKeyed)
|
|
177
201
|
);
|
|
202
|
+
const arrived = arrivedEntries(comments.xml);
|
|
178
203
|
const originalThreads = originalThreadIds(comments, originallyReferenced);
|
|
179
204
|
const pieces = [];
|
|
180
205
|
const written = /* @__PURE__ */ new Set();
|
|
181
206
|
for (const original of comments.ordered) {
|
|
182
207
|
const current = currentBodies.get(original.id);
|
|
183
208
|
if (current) {
|
|
184
|
-
pieces.push(renderedComment(current));
|
|
209
|
+
pieces.push(renderedComment(current, arrivedKeyed, arrived));
|
|
185
210
|
written.add(original.id);
|
|
186
211
|
continue;
|
|
187
212
|
}
|
|
@@ -191,13 +216,15 @@ function commentsXml(references, comments, originallyReferenced) {
|
|
|
191
216
|
}
|
|
192
217
|
}
|
|
193
218
|
for (const [id, comment] of currentBodies) {
|
|
194
|
-
if (!written.has(id))
|
|
219
|
+
if (!written.has(id)) {
|
|
220
|
+
pieces.push(renderedComment(comment, arrivedKeyed, arrived));
|
|
221
|
+
}
|
|
195
222
|
}
|
|
196
223
|
if (comments.xml === null) {
|
|
197
|
-
const compatibility = hasThreadMetadata ? `
|
|
198
|
-
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:comments
|
|
224
|
+
const compatibility = hasThreadMetadata ? ` ${xmlnsDecl("w14")} ${xmlnsDecl("mc")} mc:Ignorable="w14"` : "";
|
|
225
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:comments ${xmlnsDecl("w")}${compatibility}>${pieces.join("")}</w:comments>`;
|
|
199
226
|
}
|
|
200
|
-
const open = /<(?:[
|
|
227
|
+
const open = /<(?:[^\s<>/:="']+:)?comments\b[^>]*>/.exec(comments.xml);
|
|
201
228
|
if (!open) {
|
|
202
229
|
throw new DocxExportError(
|
|
203
230
|
"malformed-xml",
|
package/dist/docx/exportDocx.js
CHANGED
|
@@ -4,6 +4,7 @@ import { parseNumbering } from "../numbering/parseNumbering.js";
|
|
|
4
4
|
import { addListDefinitions } from "../numbering/writeNumbering.js";
|
|
5
5
|
import { DocxExportError } from "../ooxml/errors.js";
|
|
6
6
|
import { decodeUtf8, encodeUtf8, parseXml, W_NS } from "../ooxml/xml.js";
|
|
7
|
+
import { sameSource } from "../schema/sourceEquality.js";
|
|
7
8
|
import { planCommentParts } from "./comments.js";
|
|
8
9
|
import { repackParts } from "./container.js";
|
|
9
10
|
import { hyperlinkRefs } from "./hyperlink.js";
|
|
@@ -21,7 +22,7 @@ import {
|
|
|
21
22
|
import { withUniqueControls } from "./uniqueControls.js";
|
|
22
23
|
function blockXml(node, session, refs) {
|
|
23
24
|
const imported = originalBlock(node, session);
|
|
24
|
-
if (imported && node
|
|
25
|
+
if (imported && sameSource(node, imported.node)) return imported.xml;
|
|
25
26
|
return serializeBlock(node, session, refs);
|
|
26
27
|
}
|
|
27
28
|
function buildDocumentXml(doc, session, refs) {
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
isHighlightName,
|
|
4
4
|
isUnderlineKind
|
|
5
5
|
} from "../../model/format.js";
|
|
6
|
+
import { overridingAttrs } from "../../ooxml/precedence.js";
|
|
6
7
|
import { readTabStopDirectives } from "../../ooxml/tabStops.js";
|
|
7
8
|
import {
|
|
8
9
|
ALIGN_BY_JC,
|
|
@@ -18,11 +19,7 @@ import {
|
|
|
18
19
|
wAttr
|
|
19
20
|
} from "../../ooxml/units.js";
|
|
20
21
|
import { childByLocalName } from "../../ooxml/xml.js";
|
|
21
|
-
import {
|
|
22
|
-
NO_THEME_FONTS,
|
|
23
|
-
THEME_ATTRS,
|
|
24
|
-
themeFontName
|
|
25
|
-
} from "../theme.js";
|
|
22
|
+
import { NO_THEME_FONTS, themeFontName } from "../theme.js";
|
|
26
23
|
var VERTICAL_ALIGN_BY_VAL = {
|
|
27
24
|
superscript: "superscript",
|
|
28
25
|
subscript: "subscript"
|
|
@@ -35,7 +32,7 @@ var FONT_SLOTS = ["ascii", "eastAsia", "hAnsi", "cs"];
|
|
|
35
32
|
function slotFontName(rFonts, slot, themeFonts) {
|
|
36
33
|
const written = wAttr(rFonts, slot);
|
|
37
34
|
if (written !== null) return written;
|
|
38
|
-
for (const attr of
|
|
35
|
+
for (const attr of overridingAttrs("rFonts", slot)) {
|
|
39
36
|
const resolved = themeFontName(themeFonts, wAttr(rFonts, attr));
|
|
40
37
|
if (resolved !== null) return resolved;
|
|
41
38
|
}
|