@portone/docx-editor 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/CONTRIBUTING.md +4 -0
  3. package/README.md +17 -5
  4. package/dist/DocxEditor.d.ts +34 -10
  5. package/dist/DocxEditor.js +69 -24
  6. package/dist/core.d.ts +2 -0
  7. package/dist/core.js +2 -0
  8. package/dist/docx/commentOnlyChange.d.ts +47 -0
  9. package/dist/docx/commentOnlyChange.js +162 -0
  10. package/dist/docx/comments/constants.d.ts +11 -3
  11. package/dist/docx/comments/constants.js +10 -3
  12. package/dist/docx/comments/contentTypes.d.ts +7 -0
  13. package/dist/docx/comments/contentTypes.js +38 -0
  14. package/dist/docx/comments/grammar.d.ts +80 -0
  15. package/dist/docx/comments/grammar.js +213 -0
  16. package/dist/docx/comments/model.d.ts +2 -0
  17. package/dist/docx/comments/model.js +3 -0
  18. package/dist/docx/comments/people.d.ts +39 -0
  19. package/dist/docx/comments/people.js +196 -0
  20. package/dist/docx/comments/reading.d.ts +18 -1
  21. package/dist/docx/comments/reading.js +33 -20
  22. package/dist/docx/comments/verifying.d.ts +44 -0
  23. package/dist/docx/comments/verifying.js +206 -0
  24. package/dist/docx/comments/writing.d.ts +4 -1
  25. package/dist/docx/comments/writing.js +84 -77
  26. package/dist/docx/exportDocx.js +2 -1
  27. package/dist/docx/formatting/direct.js +3 -6
  28. package/dist/docx/headersFooters.js +8 -10
  29. package/dist/docx/importParagraph.js +1 -0
  30. package/dist/docx/notes.js +3 -5
  31. package/dist/docx/paraProps.js +51 -46
  32. package/dist/docx/propsXml.d.ts +20 -2
  33. package/dist/docx/propsXml.js +72 -32
  34. package/dist/docx/relationships.js +8 -3
  35. package/dist/docx/runProps.js +34 -34
  36. package/dist/docx/sdt.js +16 -14
  37. package/dist/docx/serializeBlock.js +1 -2
  38. package/dist/docx/serializeParagraph.d.ts +2 -1
  39. package/dist/docx/serializeParagraph.js +25 -14
  40. package/dist/docx/serializeTable.js +38 -24
  41. package/dist/docx/storyProjection.d.ts +36 -0
  42. package/dist/docx/storyProjection.js +23 -0
  43. package/dist/docx/tableFormatting/editing.js +49 -73
  44. package/dist/docx/tableTemplate.js +24 -3
  45. package/dist/docx/theme.d.ts +0 -6
  46. package/dist/docx/theme.js +0 -8
  47. package/dist/editor/clipboard/images.js +17 -3
  48. package/dist/editor/clipboard/inlineFormatting.d.ts +7 -0
  49. package/dist/editor/clipboard/inlineFormatting.js +1 -0
  50. package/dist/editor/commands/breakCommands.js +6 -2
  51. package/dist/editor/commands/canRunCommand.d.ts +3 -2
  52. package/dist/editor/commands/canRunCommand.js +2 -2
  53. package/dist/editor/commands/comments/editing.js +9 -10
  54. package/dist/editor/commands/comments/model.d.ts +9 -0
  55. package/dist/editor/commands/comments/reading.d.ts +7 -0
  56. package/dist/editor/commands/comments/reading.js +14 -0
  57. package/dist/editor/commands/formatting/editing.js +6 -1
  58. package/dist/editor/commands/historyCommands.js +14 -5
  59. package/dist/editor/commands/index.d.ts +7 -1
  60. package/dist/editor/commands/index.js +4 -0
  61. package/dist/editor/commands/linkCommands.js +2 -0
  62. package/dist/editor/commands/lockCommands.js +12 -1
  63. package/dist/editor/commands/tabCommands.js +4 -2
  64. package/dist/editor/createEditor.d.ts +13 -3
  65. package/dist/editor/createEditor.js +23 -15
  66. package/dist/editor/externalClipboard.js +166 -4
  67. package/dist/editor/insertImage.js +6 -2
  68. package/dist/editor/insertTable.js +2 -1
  69. package/dist/editor/paragraphEdits.d.ts +2 -0
  70. package/dist/editor/paragraphEdits.js +2 -0
  71. package/dist/editor/plugins/documentProtection.d.ts +21 -0
  72. package/dist/editor/plugins/documentProtection.js +44 -0
  73. package/dist/editor/plugins/imagePaste.js +4 -1
  74. package/dist/editor/plugins/keymap.d.ts +11 -1
  75. package/dist/editor/plugins/keymap.js +24 -4
  76. package/dist/editor/plugins/lockedContent.d.ts +4 -2
  77. package/dist/editor/plugins/lockedContent.js +2 -2
  78. package/dist/editor/plugins/tableContextMenu.js +2 -1
  79. package/dist/editor/plugins/textContextMenu.js +6 -1
  80. package/dist/index.d.ts +2 -0
  81. package/dist/numbering/listTemplate.js +32 -10
  82. package/dist/ooxml/element.d.ts +52 -0
  83. package/dist/ooxml/element.js +49 -0
  84. package/dist/ooxml/fragment.d.ts +53 -0
  85. package/dist/ooxml/fragment.js +76 -0
  86. package/dist/ooxml/image.js +4 -3
  87. package/dist/ooxml/names.d.ts +27 -0
  88. package/dist/ooxml/names.js +29 -0
  89. package/dist/ooxml/precedence.d.ts +31 -0
  90. package/dist/ooxml/precedence.js +51 -0
  91. package/dist/ooxml/xml.d.ts +31 -0
  92. package/dist/ooxml/xml.js +27 -0
  93. package/dist/page/blockKinds.d.ts +42 -0
  94. package/dist/page/blockKinds.js +0 -0
  95. package/dist/page/measureBlocks.d.ts +14 -1
  96. package/dist/page/measureBlocks.js +31 -11
  97. package/dist/page/pageDecorations.d.ts +16 -0
  98. package/dist/page/pageDecorations.js +1 -0
  99. package/dist/page/pageLayout.d.ts +8 -43
  100. package/dist/page/pageLayout.js +28 -51
  101. package/dist/page/tableMeasurements.d.ts +10 -2
  102. package/dist/page/tableMeasurements.js +20 -16
  103. package/dist/page/usePageLayout.d.ts +15 -0
  104. package/dist/page/usePageLayout.js +30 -2
  105. package/dist/schema/attrRoles.d.ts +35 -0
  106. package/dist/schema/attrRoles.js +106 -0
  107. package/dist/schema/docxSchema.d.ts +6 -0
  108. package/dist/schema/docxSchema.js +216 -100
  109. package/dist/schema/editGuard.d.ts +99 -0
  110. package/dist/schema/editGuard.js +43 -0
  111. package/dist/schema/guards.d.ts +68 -0
  112. package/dist/schema/guards.js +82 -0
  113. package/dist/schema/locks.d.ts +9 -8
  114. package/dist/schema/locks.js +32 -16
  115. package/dist/schema/preservedGuards.d.ts +30 -0
  116. package/dist/schema/preservedGuards.js +50 -0
  117. package/dist/schema/protection.d.ts +84 -0
  118. package/dist/schema/protection.js +171 -0
  119. package/dist/schema/protectionState.d.ts +20 -0
  120. package/dist/schema/protectionState.js +37 -0
  121. package/dist/schema/sourceEquality.d.ts +27 -0
  122. package/dist/schema/sourceEquality.js +65 -0
  123. package/dist/table/cellFormatting.js +5 -2
  124. package/dist/table/commands.d.ts +2 -2
  125. package/dist/table/commands.js +2 -2
  126. package/dist/table/index.d.ts +2 -2
  127. package/dist/table/merge.d.ts +2 -2
  128. package/dist/table/merge.js +3 -3
  129. package/dist/ui/CommentsPanel.d.ts +3 -3
  130. package/dist/ui/CommentsPanel.js +26 -18
  131. package/dist/ui/LinkCard.d.ts +1 -2
  132. package/dist/ui/LinkCard.js +3 -4
  133. package/dist/ui/TextMenu.js +15 -10
  134. package/package.json +5 -2
  135. package/dist/editor/plugins/bookmarkProtection.d.ts +0 -3
  136. package/dist/editor/plugins/bookmarkProtection.js +0 -28
  137. package/dist/editor/plugins/noteProtection.d.ts +0 -3
  138. package/dist/editor/plugins/noteProtection.js +0 -25
@@ -0,0 +1,162 @@
1
+ // src/docx/commentOnlyChange.ts
2
+ import { decodeUtf8, elementChildren, parseXml } from "../ooxml/xml.js";
3
+ import {
4
+ commentAdditionsBy,
5
+ commentEditsOwned,
6
+ commentIdentitiesKept,
7
+ withoutComments
8
+ } from "../schema/protection.js";
9
+ import {
10
+ COMMENTS_CONTENT_TYPE,
11
+ COMMENTS_EXTENDED_CONTENT_TYPE,
12
+ COMMENTS_EXTENDED_REL_TYPE,
13
+ COMMENTS_REL_TYPE,
14
+ CONTENT_TYPES_PATH,
15
+ PEOPLE_CONTENT_TYPE,
16
+ PEOPLE_REL_TYPE
17
+ } from "./comments/constants.js";
18
+ import { commentPartsKept } from "./comments/verifying.js";
19
+ import { importDocx } from "./importDocx.js";
20
+ import {
21
+ readRelationships,
22
+ relsPathOf
23
+ } from "./relationships.js";
24
+ import {
25
+ comparableStory,
26
+ isModelledBlock
27
+ } from "./storyProjection.js";
28
+ var COMMENT_REL_TYPES = [
29
+ COMMENTS_REL_TYPE,
30
+ COMMENTS_EXTENDED_REL_TYPE,
31
+ PEOPLE_REL_TYPE
32
+ ];
33
+ var COMMENT_CONTENT_TYPES = [
34
+ COMMENTS_CONTENT_TYPE,
35
+ COMMENTS_EXTENDED_CONTENT_TYPE,
36
+ PEOPLE_CONTENT_TYPE
37
+ ];
38
+ var refused = (reason) => ({ ok: false, reason });
39
+ function sameBytes(before, after) {
40
+ return before.length === after.length && before.every((byte, index) => byte === after[index]);
41
+ }
42
+ function commentPartsOf(session) {
43
+ const { partPath, extendedPartPath, people } = session.comments;
44
+ return [partPath, extendedPartPath, people.partPath];
45
+ }
46
+ function commentPartPaths(session) {
47
+ return new Set(
48
+ commentPartsOf(session).filter((path) => path !== null)
49
+ );
50
+ }
51
+ function aroundTheStory(session) {
52
+ const preserved = session.blocks.filter((block) => !isModelledBlock(block.node)).map((block) => block.xml).join("");
53
+ return session.documentPrefix + preserved + session.documentSuffix;
54
+ }
55
+ function relationshipsKept(before, after) {
56
+ if (new Set(before.map((entry) => entry.id)).size !== before.length || new Set(after.map((entry) => entry.id)).size !== after.length) {
57
+ return false;
58
+ }
59
+ const now = new Map(after.map((entry) => [entry.id, entry]));
60
+ const kept = before.every((entry) => {
61
+ const current = now.get(entry.id);
62
+ return current !== void 0 && current.type === entry.type && current.target === entry.target && current.external === entry.external;
63
+ });
64
+ const ids = new Set(before.map((entry) => entry.id));
65
+ const parts = after.filter((entry) => !entry.external);
66
+ return kept && after.filter((entry) => !ids.has(entry.id)).every(
67
+ (entry) => COMMENT_REL_TYPES.includes(entry.type) && !entry.external && parts.filter((other) => other.type === entry.type).length === 1
68
+ );
69
+ }
70
+ function contentTypes(bytes) {
71
+ if (bytes === void 0) return /* @__PURE__ */ new Map();
72
+ const declared = /* @__PURE__ */ new Map();
73
+ for (const el of elementChildren(
74
+ parseXml(decodeUtf8(bytes).text).documentElement
75
+ )) {
76
+ const key = el.localName === "Default" ? el.getAttribute("Extension") : el.localName === "Override" ? el.getAttribute("PartName") : null;
77
+ if (key !== null) declared.set(key, el.getAttribute("ContentType") ?? "");
78
+ }
79
+ return declared;
80
+ }
81
+ function contentTypesKept(before, after) {
82
+ for (const [key, type] of before) {
83
+ if (after.get(key) !== type) return false;
84
+ }
85
+ for (const [key, type] of after) {
86
+ if (!before.has(key) && !COMMENT_CONTENT_TYPES.includes(type)) return false;
87
+ }
88
+ return true;
89
+ }
90
+ function gainedCommentPartsAreNew(before, after) {
91
+ const had = commentPartsOf(before);
92
+ return commentPartsOf(after).every(
93
+ (path, kind) => path === null || path === had[kind] || !before.parts.has(path)
94
+ );
95
+ }
96
+ function packageKept(before, after) {
97
+ if (before.mainPartPath !== after.mainPartPath) {
98
+ return { ok: false, reason: "part-changed", part: before.mainPartPath };
99
+ }
100
+ const relsPath = relsPathOf(before.mainPartPath);
101
+ const untouched = /* @__PURE__ */ new Set([
102
+ before.mainPartPath,
103
+ relsPath,
104
+ CONTENT_TYPES_PATH,
105
+ ...commentPartPaths(before),
106
+ ...commentPartPaths(after)
107
+ ]);
108
+ for (const path of /* @__PURE__ */ new Set([...before.parts.keys(), ...after.parts.keys()])) {
109
+ if (untouched.has(path)) continue;
110
+ const was = before.parts.get(path);
111
+ const now = after.parts.get(path);
112
+ if (was === void 0 || now === void 0 || !sameBytes(was, now)) {
113
+ return { ok: false, reason: "part-changed", part: path };
114
+ }
115
+ }
116
+ if (!relationshipsKept(
117
+ readRelationships(before.parts, relsPath),
118
+ readRelationships(after.parts, relsPath)
119
+ ) || !gainedCommentPartsAreNew(before, after)) {
120
+ return { ok: false, reason: "relationship-changed", part: relsPath };
121
+ }
122
+ if (!contentTypesKept(
123
+ contentTypes(before.parts.get(CONTENT_TYPES_PATH)),
124
+ contentTypes(after.parts.get(CONTENT_TYPES_PATH))
125
+ )) {
126
+ return { ok: false, reason: "part-changed", part: CONTENT_TYPES_PATH };
127
+ }
128
+ if (aroundTheStory(before) !== aroundTheStory(after)) {
129
+ return { ok: false, reason: "part-changed", part: before.mainPartPath };
130
+ }
131
+ return { ok: true };
132
+ }
133
+ function sameBody(before, after) {
134
+ const was = comparableStory(before, withoutComments);
135
+ const now = comparableStory(after, withoutComments);
136
+ return was !== null && now !== null && was.length === now.length && was.every((block, at) => block === now[at]);
137
+ }
138
+ function storyKept(before, after, authorId, editableComments) {
139
+ if (!sameBody(before, after)) return refused("body-changed");
140
+ if (!commentIdentitiesKept(before.doc, after.doc) || !commentAdditionsBy(before.doc, after.doc, authorId)) {
141
+ return refused("comment-author-forged");
142
+ }
143
+ if (!commentEditsOwned(before.doc, after.doc, {
144
+ protection: "comments",
145
+ authorId,
146
+ editableComments
147
+ })) {
148
+ return refused("comment-not-owned");
149
+ }
150
+ return { ok: true };
151
+ }
152
+ function onlyCommentsChangedBy(original, submitted, authorId, { editableComments = "own" } = {}) {
153
+ const before = importDocx(original);
154
+ const after = importDocx(submitted);
155
+ const packaged = packageKept(before.session, after.session);
156
+ if (!packaged.ok) return packaged;
157
+ const story = storyKept(before, after, authorId, editableComments);
158
+ return story.ok ? commentPartsKept(before, after, authorId, editableComments) : story;
159
+ }
160
+ export {
161
+ onlyCommentsChangedBy
162
+ };
@@ -3,6 +3,14 @@ export declare const COMMENTS_EXTENDED_REL_TYPE = "http://schemas.microsoft.com/
3
3
  export declare const COMMENTS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml";
4
4
  export declare const COMMENTS_EXTENDED_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml";
5
5
  export declare const CONTENT_TYPES_PATH = "[Content_Types].xml";
6
- export declare const W14_NS = "http://schemas.microsoft.com/office/word/2010/wordml";
7
- export declare const W15_NS = "http://schemas.microsoft.com/office/word/2012/wordml";
8
- export declare const MC_NS = "http://schemas.openxmlformats.org/markup-compatibility/2006";
6
+ export declare const W14_NS: "http://schemas.microsoft.com/office/word/2010/wordml";
7
+ export declare const W15_NS: "http://schemas.microsoft.com/office/word/2012/wordml";
8
+ export declare const MC_NS: "http://schemas.openxmlformats.org/markup-compatibility/2006";
9
+ export declare const PEOPLE_REL_TYPE = "http://schemas.microsoft.com/office/2011/relationships/people";
10
+ export declare const PEOPLE_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.people+xml";
11
+ /**
12
+ * The `w15:providerId` under which this editor records a comment author's identity. An identity
13
+ * another provider recorded, Word's directory above all, is one this editor cannot vouch for and
14
+ * is read as none.
15
+ */
16
+ export declare const COMMENT_AUTHOR_PROVIDER = "portone-docx-editor";
@@ -1,20 +1,27 @@
1
1
  // src/docx/comments/constants.ts
2
+ import { NAMESPACES } from "../../ooxml/names.js";
2
3
  import { R_NS } from "../../ooxml/xml.js";
3
4
  var COMMENTS_REL_TYPE = `${R_NS}/comments`;
4
5
  var COMMENTS_EXTENDED_REL_TYPE = "http://schemas.microsoft.com/office/2011/relationships/commentsExtended";
5
6
  var COMMENTS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml";
6
7
  var COMMENTS_EXTENDED_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml";
7
8
  var CONTENT_TYPES_PATH = "[Content_Types].xml";
8
- var W14_NS = "http://schemas.microsoft.com/office/word/2010/wordml";
9
- var W15_NS = "http://schemas.microsoft.com/office/word/2012/wordml";
10
- var MC_NS = "http://schemas.openxmlformats.org/markup-compatibility/2006";
9
+ var W14_NS = NAMESPACES.w14;
10
+ var W15_NS = NAMESPACES.w15;
11
+ var MC_NS = NAMESPACES.mc;
12
+ var PEOPLE_REL_TYPE = "http://schemas.microsoft.com/office/2011/relationships/people";
13
+ var PEOPLE_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.people+xml";
14
+ var COMMENT_AUTHOR_PROVIDER = "portone-docx-editor";
11
15
  export {
12
16
  COMMENTS_CONTENT_TYPE,
13
17
  COMMENTS_EXTENDED_CONTENT_TYPE,
14
18
  COMMENTS_EXTENDED_REL_TYPE,
15
19
  COMMENTS_REL_TYPE,
20
+ COMMENT_AUTHOR_PROVIDER,
16
21
  CONTENT_TYPES_PATH,
17
22
  MC_NS,
23
+ PEOPLE_CONTENT_TYPE,
24
+ PEOPLE_REL_TYPE,
18
25
  W14_NS,
19
26
  W15_NS
20
27
  };
@@ -0,0 +1,7 @@
1
+ /** Declares a part an export adds in `[Content_Types].xml`, leaving the original text as it came. */
2
+ /**
3
+ * The content types part with an override for this part, or null when it already declares one.
4
+ * `current` is the part as an earlier addition in the same export left it, so that two additions
5
+ * do not each write over the other's declaration.
6
+ */
7
+ export declare function withContentType(parts: Map<string, Uint8Array>, partPath: string, contentType: string, current: Uint8Array | undefined): Uint8Array | null;
@@ -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
+ };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The shapes this editor writes the three comment parts in, and the reading of those shapes.
3
+ *
4
+ * Each entry is written here and judged here, so the two halves cannot drift apart: a body written
5
+ * by hand can be told from one this editor put out only where the writer and the reader agree on
6
+ * what it puts out. `./reading` keeps a lenient flattener for display, which takes whatever a
7
+ * producer wrote and shows what it can; this one judges an entry that came back and says no to
8
+ * everything else.
9
+ */
10
+ import { type XmlAttr } from "../../ooxml/element";
11
+ import type { CommentReferenceData, CommentReplyData } from "./model";
12
+ /** The attributes this editor writes on a `w:comment`. The thread key goes on the body's paragraph */
13
+ export declare const COMMENT_ATTRIBUTES: ReadonlySet<string>;
14
+ /** The attributes this editor writes on a `w15:commentEx` */
15
+ export declare const COMMENT_EX_ATTRIBUTES: ReadonlySet<string>;
16
+ /**
17
+ * Whether the element carries no attribute outside this set.
18
+ *
19
+ * A namespace declaration is not an attribute of the element in this sense: it says where the
20
+ * names come from rather than anything about the entry, and where a producer puts one is its own
21
+ * business.
22
+ */
23
+ export declare function attributesWithin(el: Element, allowed: ReadonlySet<string>): boolean;
24
+ /** The body of a comment, as a paragraph of one run holding the text and the breaks in it */
25
+ export declare function renderCommentBody(text: string, paraId: string | null): string;
26
+ /**
27
+ * The paragraph a thread key belongs on, which is the last one of the body.
28
+ *
29
+ * A body may hold a paragraph of another vocabulary, a picture's DrawingML `a:p` among them, and a
30
+ * key put on that says nothing about the thread. The namespace decides, since a prefix means only
31
+ * what the element it sits on binds it to, and a document may bind the same prefix twice over.
32
+ */
33
+ export declare function lastBodyParagraph(comment: Element): Element | null;
34
+ /**
35
+ * The entries a part arrived holding, under the id each carries.
36
+ *
37
+ * The writer asks the document which paragraph a key goes on rather than asking the text, because
38
+ * only the document knows what a prefix means where it is written.
39
+ */
40
+ export declare function arrivedEntries(partXml: string | null): ReadonlyMap<string, Element>;
41
+ /**
42
+ * The entry with the thread key on its body's last paragraph, and unchanged where it has one.
43
+ *
44
+ * Settling a thread or replying to it hangs the state off that key, and an entry that arrived
45
+ * without one has to gain it. Writing the entry afresh instead would put back only what this
46
+ * editor models, so a body holding more than plain text would lose it to a change nobody asked
47
+ * for and nobody made.
48
+ *
49
+ * `arrived` is that entry as the document has it, which is what says which paragraph is a
50
+ * WordprocessingML one and whether it already carries a key under some prefix. The text is only
51
+ * asked where that paragraph is: its opening tag is the one at the same place among the tags named
52
+ * `p`, counted the same way in both.
53
+ *
54
+ * The `w14` prefix is declared on the part rather than here: a part holding any thread state
55
+ * declares it on its root along with the compatibility markup that goes with it (`./writing`).
56
+ */
57
+ export declare function withThreadKey(commentXml: string, paraId: string, arrived: Element | null): string;
58
+ /**
59
+ * The text of a comment written in that shape, and null for an entry holding anything else: a
60
+ * field, a second run, markup a producer wrapped it in, an attribute this editor does not write.
61
+ *
62
+ * An entry that arrived and was not edited is compared as it stands rather than read here, so
63
+ * saying no to a shape this editor would not have written turns down only a rewrite.
64
+ */
65
+ /** Whether this editor's writer could have put out this thread state */
66
+ export declare function wellFormedCommentExtension(entry: Element): boolean;
67
+ /** Whether this editor's writer could have put out this recorded identity */
68
+ export declare function wellFormedPerson(entry: Element): boolean;
69
+ /** The identity a recorded person stands for, and null for an entry recording none */
70
+ export declare function recordedIdentity(person: Element): string | null;
71
+ export declare function readStrictCommentBody(comment: Element): string | null;
72
+ /** The thread state of one comment, as this editor writes it into the extended part */
73
+ export declare function renderCommentExtension(comment: CommentReferenceData | CommentReplyData): string;
74
+ /**
75
+ * The identity this editor records for an author, as it writes it into the people part.
76
+ *
77
+ * The prefix and the declaration come from the caller, which is writing into a part whose root
78
+ * already binds them or is being written from nothing.
79
+ */
80
+ export declare function renderPerson(author: string, userId: string, prefix: string, declaration: XmlAttr | null): string;
@@ -0,0 +1,213 @@
1
+ // src/docx/comments/grammar.ts
2
+ import {
3
+ attrsText,
4
+ elementXml,
5
+ xmlnsAttr
6
+ } from "../../ooxml/element.js";
7
+ import { qualify, wName } from "../../ooxml/names.js";
8
+ import {
9
+ attributeByLocalName,
10
+ elementChildren,
11
+ escapeXml,
12
+ parseXml,
13
+ W_NS
14
+ } from "../../ooxml/xml.js";
15
+ import { COMMENT_AUTHOR_PROVIDER, W14_NS, W15_NS } from "./constants.js";
16
+ var ELEMENT_NODE = 1;
17
+ var TEXT_NODE = 3;
18
+ var XMLNS_NS = "http://www.w3.org/2000/xmlns/";
19
+ var XML_NS = "http://www.w3.org/XML/1998/namespace";
20
+ var nameKey = (namespace, localName) => `${namespace ?? ""} ${localName}`;
21
+ var COMMENT_ATTRIBUTES = /* @__PURE__ */ new Set([
22
+ nameKey(W_NS, "id"),
23
+ nameKey(W_NS, "author"),
24
+ nameKey(W_NS, "date"),
25
+ nameKey(W_NS, "initials")
26
+ ]);
27
+ var COMMENT_EX_ATTRIBUTES = /* @__PURE__ */ new Set([
28
+ nameKey(W15_NS, "paraId"),
29
+ nameKey(W15_NS, "paraIdParent"),
30
+ nameKey(W15_NS, "done")
31
+ ]);
32
+ var PARAGRAPH_ATTRIBUTES = /* @__PURE__ */ new Set([
33
+ nameKey(W14_NS, "paraId")
34
+ ]);
35
+ var TEXT_ATTRIBUTES = /* @__PURE__ */ new Set([
36
+ nameKey(XML_NS, "space")
37
+ ]);
38
+ var PERSON_ATTRIBUTES = /* @__PURE__ */ new Set([
39
+ nameKey(W15_NS, "author")
40
+ ]);
41
+ var PRESENCE_ATTRIBUTES = /* @__PURE__ */ new Set([
42
+ nameKey(W15_NS, "providerId"),
43
+ nameKey(W15_NS, "userId")
44
+ ]);
45
+ var NO_ATTRIBUTES = /* @__PURE__ */ new Set();
46
+ var THREAD_KEY = /^[0-9A-Fa-f]{8}$/;
47
+ function threadKeyOrNone(el, localName) {
48
+ const value = attributeByLocalName(el, localName);
49
+ return value === null || THREAD_KEY.test(value);
50
+ }
51
+ function attributesWithin(el, allowed) {
52
+ return Array.from(el.attributes).every(
53
+ (attr) => attr.namespaceURI === XMLNS_NS || attr.name === "xmlns" || allowed.has(nameKey(attr.namespaceURI, attr.localName))
54
+ );
55
+ }
56
+ function isNamed(el, namespace, localName) {
57
+ return el.namespaceURI === namespace && el.localName === localName;
58
+ }
59
+ function holdsElementsOnly(el) {
60
+ return Array.from(el.childNodes).every(
61
+ (node) => node.nodeType === ELEMENT_NODE
62
+ );
63
+ }
64
+ function holdsTextOnly(el) {
65
+ return Array.from(el.childNodes).every((node) => node.nodeType === TEXT_NODE);
66
+ }
67
+ function renderCommentBody(text, paraId) {
68
+ const lines = text.split("\n");
69
+ const pieces = [];
70
+ lines.forEach((line, index) => {
71
+ if (index > 0) pieces.push(elementXml(wName("br"), []));
72
+ if (line.length > 0 || lines.length === 1) {
73
+ pieces.push(
74
+ elementXml(wName("t"), [["xml:space", "preserve"]], [escapeXml(line)])
75
+ );
76
+ }
77
+ });
78
+ const key = paraId === null ? [] : [xmlnsAttr("w14"), [qualify("w14", "paraId"), paraId]];
79
+ return elementXml(wName("p"), key, [elementXml(wName("r"), [], pieces)]);
80
+ }
81
+ var PARAGRAPH_OR_SKIPPED = /<!--[\s\S]*?-->|<!\[CDATA\[[\s\S]*?\]\]>|<([^\s<>/:="']+:)?p(?=[\s/>])[^>]*>/g;
82
+ function lastBodyParagraph(comment) {
83
+ const paragraphs = comment.getElementsByTagNameNS(W_NS, "p");
84
+ return paragraphs.length === 0 ? null : paragraphs[paragraphs.length - 1];
85
+ }
86
+ function namedParagraphs(comment) {
87
+ return Array.from(comment.getElementsByTagName("*")).filter(
88
+ (element) => element.localName === "p"
89
+ );
90
+ }
91
+ function arrivedEntries(partXml) {
92
+ const entries = /* @__PURE__ */ new Map();
93
+ if (partXml === null) return entries;
94
+ let root;
95
+ try {
96
+ root = parseXml(partXml).documentElement;
97
+ } catch {
98
+ return entries;
99
+ }
100
+ for (const entry of elementChildren(root)) {
101
+ if (entry.namespaceURI !== W_NS || entry.localName !== "comment") continue;
102
+ const id = attributeByLocalName(entry, "id");
103
+ if (id !== null && !entries.has(id)) entries.set(id, entry);
104
+ }
105
+ return entries;
106
+ }
107
+ function withThreadKey(commentXml, paraId, arrived) {
108
+ if (arrived === null) return commentXml;
109
+ const target = lastBodyParagraph(arrived);
110
+ if (target === null) return commentXml;
111
+ if (target.getAttributeNS(W14_NS, "paraId") !== null || target.hasAttribute("w14:paraId")) {
112
+ return commentXml;
113
+ }
114
+ const at = namedParagraphs(arrived).indexOf(target);
115
+ const openings = Array.from(commentXml.matchAll(PARAGRAPH_OR_SKIPPED)).filter(
116
+ (match) => !match[0].startsWith("<!")
117
+ );
118
+ const opening = openings[at];
119
+ if (at === -1 || opening === void 0 || opening.index === void 0) {
120
+ return commentXml;
121
+ }
122
+ const selfClosing = opening[0].endsWith("/>");
123
+ const keyed = opening[0].slice(0, selfClosing ? -2 : -1) + ` ${attrsText([[qualify("w14", "paraId"), paraId]])}` + (selfClosing ? "/>" : ">");
124
+ return commentXml.slice(0, opening.index) + keyed + commentXml.slice(opening.index + opening[0].length);
125
+ }
126
+ function readRunText(run) {
127
+ if (!holdsElementsOnly(run)) return null;
128
+ const pieces = [];
129
+ for (const child of Array.from(run.children)) {
130
+ if (isNamed(child, W_NS, "br")) {
131
+ if (!attributesWithin(child, NO_ATTRIBUTES) || child.childNodes.length > 0) {
132
+ return null;
133
+ }
134
+ pieces.push("\n");
135
+ continue;
136
+ }
137
+ if (!isNamed(child, W_NS, "t")) return null;
138
+ if (!attributesWithin(child, TEXT_ATTRIBUTES) || child.getAttributeNS(XML_NS, "space") !== "preserve" || !holdsTextOnly(child)) {
139
+ return null;
140
+ }
141
+ pieces.push(child.textContent ?? "");
142
+ }
143
+ return pieces.join("");
144
+ }
145
+ function wellFormedCommentExtension(entry) {
146
+ return attributesWithin(entry, COMMENT_EX_ATTRIBUTES) && entry.childNodes.length === 0 && threadKeyOrNone(entry, "paraId") && threadKeyOrNone(entry, "paraIdParent") && // The writer says a thread is settled or open, and nothing else `ST_OnOff` would take
147
+ ["0", "1", null].includes(attributeByLocalName(entry, "done"));
148
+ }
149
+ function wellFormedPerson(entry) {
150
+ if (!attributesWithin(entry, PERSON_ATTRIBUTES)) return false;
151
+ if (!holdsElementsOnly(entry)) return false;
152
+ const children = Array.from(entry.children);
153
+ if (children.length !== 1) return false;
154
+ const [presence] = children;
155
+ return isNamed(presence, W15_NS, "presenceInfo") && attributesWithin(presence, PRESENCE_ATTRIBUTES) && attributeByLocalName(presence, "providerId") === COMMENT_AUTHOR_PROVIDER && attributeByLocalName(presence, "userId") !== null && presence.childNodes.length === 0;
156
+ }
157
+ function recordedIdentity(person) {
158
+ const [presence] = Array.from(person.children);
159
+ return presence === void 0 ? null : attributeByLocalName(presence, "userId");
160
+ }
161
+ function readStrictCommentBody(comment) {
162
+ if (!holdsElementsOnly(comment)) return null;
163
+ const paragraphs = Array.from(comment.children);
164
+ if (paragraphs.length !== 1) return null;
165
+ const [paragraph] = paragraphs;
166
+ if (!isNamed(paragraph, W_NS, "p")) return null;
167
+ if (!attributesWithin(paragraph, PARAGRAPH_ATTRIBUTES)) return null;
168
+ if (!holdsElementsOnly(paragraph)) return null;
169
+ const runs = Array.from(paragraph.children);
170
+ if (runs.length !== 1) return null;
171
+ const [run] = runs;
172
+ if (!isNamed(run, W_NS, "r")) return null;
173
+ if (!attributesWithin(run, NO_ATTRIBUTES)) return null;
174
+ return readRunText(run);
175
+ }
176
+ function renderCommentExtension(comment) {
177
+ if (comment.extensionXml !== null) return comment.extensionXml;
178
+ const parent = "parentParaId" in comment ? [[qualify("w15", "paraIdParent"), comment.parentParaId]] : [];
179
+ const done = "resolved" in comment ? [[qualify("w15", "done"), comment.resolved ? "1" : "0"]] : [];
180
+ return elementXml(qualify("w15", "commentEx"), [
181
+ [qualify("w15", "paraId"), comment.paraId],
182
+ ...parent,
183
+ ...done
184
+ ]);
185
+ }
186
+ function renderPerson(author, userId, prefix, declaration) {
187
+ const declared = declaration === null ? [] : [declaration];
188
+ return elementXml(
189
+ `${prefix}person`,
190
+ [...declared, [`${prefix}author`, author]],
191
+ [
192
+ elementXml(`${prefix}presenceInfo`, [
193
+ [`${prefix}providerId`, COMMENT_AUTHOR_PROVIDER],
194
+ [`${prefix}userId`, userId]
195
+ ])
196
+ ]
197
+ );
198
+ }
199
+ export {
200
+ COMMENT_ATTRIBUTES,
201
+ COMMENT_EX_ATTRIBUTES,
202
+ arrivedEntries,
203
+ attributesWithin,
204
+ lastBodyParagraph,
205
+ readStrictCommentBody,
206
+ recordedIdentity,
207
+ renderCommentBody,
208
+ renderCommentExtension,
209
+ renderPerson,
210
+ wellFormedCommentExtension,
211
+ wellFormedPerson,
212
+ withThreadKey
213
+ };
@@ -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;