@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.
Files changed (101) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/CONTRIBUTING.md +4 -0
  3. package/dist/docx/commentOnlyChange.d.ts +3 -1
  4. package/dist/docx/commentOnlyChange.js +38 -21
  5. package/dist/docx/comments/constants.d.ts +3 -3
  6. package/dist/docx/comments/constants.js +4 -3
  7. package/dist/docx/comments/grammar.d.ts +80 -0
  8. package/dist/docx/comments/grammar.js +213 -0
  9. package/dist/docx/comments/people.js +12 -14
  10. package/dist/docx/comments/reading.d.ts +5 -3
  11. package/dist/docx/comments/reading.js +23 -20
  12. package/dist/docx/comments/verifying.d.ts +44 -0
  13. package/dist/docx/comments/verifying.js +206 -0
  14. package/dist/docx/comments/writing.js +73 -46
  15. package/dist/docx/exportDocx.js +2 -1
  16. package/dist/docx/formatting/direct.js +3 -6
  17. package/dist/docx/headersFooters.js +8 -10
  18. package/dist/docx/notes.js +3 -5
  19. package/dist/docx/paraProps.js +51 -46
  20. package/dist/docx/propsXml.d.ts +20 -2
  21. package/dist/docx/propsXml.js +72 -32
  22. package/dist/docx/relationships.js +8 -3
  23. package/dist/docx/runProps.js +34 -34
  24. package/dist/docx/sdt.js +16 -14
  25. package/dist/docx/serializeBlock.js +1 -2
  26. package/dist/docx/serializeParagraph.d.ts +2 -1
  27. package/dist/docx/serializeParagraph.js +25 -14
  28. package/dist/docx/serializeTable.js +38 -24
  29. package/dist/docx/storyProjection.d.ts +36 -0
  30. package/dist/docx/storyProjection.js +23 -0
  31. package/dist/docx/tableFormatting/editing.js +49 -73
  32. package/dist/docx/tableTemplate.js +24 -3
  33. package/dist/docx/theme.d.ts +0 -6
  34. package/dist/docx/theme.js +0 -8
  35. package/dist/editor/clipboard/images.js +17 -3
  36. package/dist/editor/clipboard/inlineFormatting.d.ts +7 -0
  37. package/dist/editor/clipboard/inlineFormatting.js +1 -0
  38. package/dist/editor/commands/breakCommands.js +4 -3
  39. package/dist/editor/commands/canRunCommand.d.ts +2 -2
  40. package/dist/editor/commands/canRunCommand.js +1 -1
  41. package/dist/editor/commands/comments/editing.js +3 -6
  42. package/dist/editor/commands/historyCommands.js +2 -1
  43. package/dist/editor/commands/lockCommands.js +10 -1
  44. package/dist/editor/commands/tabCommands.js +4 -3
  45. package/dist/editor/createEditor.js +3 -9
  46. package/dist/editor/externalClipboard.js +166 -4
  47. package/dist/editor/insertImage.js +4 -3
  48. package/dist/editor/plugins/documentProtection.d.ts +3 -3
  49. package/dist/editor/plugins/lockedContent.d.ts +4 -4
  50. package/dist/editor/plugins/lockedContent.js +1 -1
  51. package/dist/numbering/listTemplate.js +32 -10
  52. package/dist/ooxml/element.d.ts +52 -0
  53. package/dist/ooxml/element.js +49 -0
  54. package/dist/ooxml/fragment.d.ts +53 -0
  55. package/dist/ooxml/fragment.js +76 -0
  56. package/dist/ooxml/image.js +4 -3
  57. package/dist/ooxml/names.d.ts +27 -0
  58. package/dist/ooxml/names.js +29 -0
  59. package/dist/ooxml/precedence.d.ts +31 -0
  60. package/dist/ooxml/precedence.js +51 -0
  61. package/dist/ooxml/xml.d.ts +31 -0
  62. package/dist/ooxml/xml.js +27 -0
  63. package/dist/page/blockKinds.d.ts +42 -0
  64. package/dist/page/blockKinds.js +0 -0
  65. package/dist/page/measureBlocks.d.ts +14 -1
  66. package/dist/page/measureBlocks.js +31 -11
  67. package/dist/page/pageDecorations.d.ts +16 -0
  68. package/dist/page/pageDecorations.js +1 -0
  69. package/dist/page/pageLayout.d.ts +8 -43
  70. package/dist/page/pageLayout.js +28 -51
  71. package/dist/page/tableMeasurements.d.ts +10 -2
  72. package/dist/page/tableMeasurements.js +20 -16
  73. package/dist/page/usePageLayout.d.ts +15 -0
  74. package/dist/page/usePageLayout.js +30 -2
  75. package/dist/schema/attrRoles.d.ts +35 -0
  76. package/dist/schema/attrRoles.js +106 -0
  77. package/dist/schema/docxSchema.d.ts +6 -0
  78. package/dist/schema/docxSchema.js +212 -101
  79. package/dist/schema/editGuard.d.ts +99 -0
  80. package/dist/schema/editGuard.js +43 -0
  81. package/dist/schema/guards.d.ts +68 -0
  82. package/dist/schema/guards.js +82 -0
  83. package/dist/schema/locks.d.ts +8 -35
  84. package/dist/schema/locks.js +29 -64
  85. package/dist/schema/preservedGuards.d.ts +30 -0
  86. package/dist/schema/preservedGuards.js +50 -0
  87. package/dist/schema/protection.d.ts +10 -3
  88. package/dist/schema/protection.js +2 -1
  89. package/dist/schema/sourceEquality.d.ts +27 -0
  90. package/dist/schema/sourceEquality.js +65 -0
  91. package/dist/table/cellFormatting.js +2 -1
  92. package/dist/table/commands.d.ts +2 -2
  93. package/dist/table/commands.js +1 -1
  94. package/dist/table/index.d.ts +2 -2
  95. package/dist/table/merge.d.ts +2 -2
  96. package/dist/table/merge.js +1 -1
  97. package/package.json +4 -1
  98. package/dist/editor/plugins/bookmarkProtection.d.ts +0 -3
  99. package/dist/editor/plugins/bookmarkProtection.js +0 -28
  100. package/dist/editor/plugins/noteProtection.d.ts +0 -3
  101. 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 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.
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 paragraphs = Array.from(comment.getElementsByTagNameNS(W_NS, "p"));
44
- return paragraphs.length === 0 ? null : attribute(paragraphs[paragraphs.length - 1], "paraId");
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 = attribute(el, "paraId");
75
+ const paraId = attributeByLocalName(el, "paraId");
77
76
  if (paraId === null) continue;
78
77
  const extension = {
79
78
  paraId,
80
- parentParaId: attribute(el, "paraIdParent"),
79
+ parentParaId: attributeByLocalName(el, "paraIdParent"),
81
80
  resolved: ["1", "true", "on"].includes(
82
- attribute(el, "done")?.toLowerCase() ?? ""
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, people };
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, people };
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 = attribute(el, "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 = attribute(el, "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: attribute(el, "initials"),
115
- date: attribute(el, "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
- extendedPartPath: extensions.partPath,
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 { encodeUtf8, escapeXml, W_NS } from "../../ooxml/xml.js";
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(renderedExtension(item));
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)) pieces.push(renderedExtension(comment));
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 xmlns:w15="${W15_NS}">${pieces.join("")}</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 = /<(?:[\w.-]+:)?commentsEx\b[^>]*>/.exec(comments.extendedXml);
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
- const close = name ? comments.extendedXml.lastIndexOf(`</${name}>`) : -1;
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 renderedComment(comment) {
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.commentXml;
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
- `w:id="${escapeXml(comment.id)}"`,
126
- comment.author === null ? null : `w:author="${escapeXml(comment.author)}"`,
127
- comment.date === null ? null : `w:date="${escapeXml(comment.date)}"`,
128
- comment.initials === null ? null : `w:initials="${escapeXml(comment.initials)}"`
129
- ].filter((entry) => entry !== null).join(" ");
130
- const threaded = carriesThreadMetadata(comment);
131
- const paragraphAttrs = threaded ? ` xmlns:w14="${W14_NS}" w14:paraId="${escapeXml(comment.paraId)}"` : "";
132
- return `<w:comment xmlns:w="${W_NS}" ${attrs}><w:p${paragraphAttrs}>${commentTextXml(comment.text)}</w:p></w:comment>`;
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(/>$/, ` xmlns:w14="${W14_NS}">`);
156
+ updated = updated.replace(/>$/, ` ${xmlnsDecl("w14")}>`);
138
157
  }
139
158
  if (!/\sxmlns:mc\s*=/.test(updated)) {
140
- updated = updated.replace(/>$/, ` xmlns:mc="${MC_NS}">`);
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
- carriesThreadMetadata
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)) pieces.push(renderedComment(comment));
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 ? ` xmlns:w14="${W14_NS}" xmlns:mc="${MC_NS}" mc:Ignorable="w14"` : "";
198
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:comments xmlns:w="${W_NS}"${compatibility}>${pieces.join("")}</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 = /<(?:[\w.-]+:)?comments\b[^>]*>/.exec(comments.xml);
227
+ const open = /<(?:[^\s<>/:="']+:)?comments\b[^>]*>/.exec(comments.xml);
201
228
  if (!open) {
202
229
  throw new DocxExportError(
203
230
  "malformed-xml",
@@ -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.eq(imported.node)) return imported.xml;
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 THEME_ATTRS[slot] ?? []) {
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
  }