@portone/docx-editor 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/CHANGELOG.md +205 -0
  2. package/assets/editor.png +0 -0
  3. package/dist/DocxEditor.d.ts +1 -1
  4. package/dist/DocxEditor.js +45 -25
  5. package/dist/core.d.ts +1 -1
  6. package/dist/docx/cloning.js +4 -9
  7. package/dist/docx/commentOnlyChange.d.ts +5 -3
  8. package/dist/docx/comments/grammar.d.ts +27 -10
  9. package/dist/docx/comments/grammar.js +18 -67
  10. package/dist/docx/comments/model.d.ts +5 -6
  11. package/dist/docx/comments/model.js +0 -9
  12. package/dist/docx/comments/parts.d.ts +16 -10
  13. package/dist/docx/comments/parts.js +65 -13
  14. package/dist/docx/comments/people.d.ts +11 -3
  15. package/dist/docx/comments/people.js +20 -6
  16. package/dist/docx/comments/policy.js +7 -1
  17. package/dist/docx/comments/reading.d.ts +4 -2
  18. package/dist/docx/comments/reading.js +1 -14
  19. package/dist/docx/comments/writing.js +78 -35
  20. package/dist/docx/exportDocx.js +30 -18
  21. package/dist/docx/exportRefs.d.ts +8 -3
  22. package/dist/docx/exportRefs.js +2 -1
  23. package/dist/docx/fidelity.js +24 -10
  24. package/dist/docx/fields.d.ts +42 -0
  25. package/dist/docx/fields.js +76 -0
  26. package/dist/docx/formatting/attrs.d.ts +20 -2
  27. package/dist/docx/formatting/attrs.js +57 -2
  28. package/dist/docx/formatting/context.d.ts +10 -2
  29. package/dist/docx/formatting/context.js +23 -5
  30. package/dist/docx/formatting/resolve.d.ts +10 -4
  31. package/dist/docx/formatting/resolve.js +31 -11
  32. package/dist/docx/formatting/styles.d.ts +25 -2
  33. package/dist/docx/formatting/styles.js +84 -3
  34. package/dist/docx/headersFooters.d.ts +89 -20
  35. package/dist/docx/headersFooters.js +163 -172
  36. package/dist/docx/hyperlink.d.ts +14 -4
  37. package/dist/docx/hyperlink.js +4 -1
  38. package/dist/docx/importDocx.d.ts +1 -1
  39. package/dist/docx/importDocx.js +135 -74
  40. package/dist/docx/importParagraph.d.ts +16 -4
  41. package/dist/docx/importParagraph.js +87 -93
  42. package/dist/docx/importPolicy.d.ts +70 -0
  43. package/dist/docx/importPolicy.js +190 -0
  44. package/dist/docx/importPreserved.d.ts +30 -0
  45. package/dist/docx/importPreserved.js +54 -0
  46. package/dist/docx/importTable.d.ts +2 -2
  47. package/dist/docx/importTable.js +107 -66
  48. package/dist/docx/invariants.js +42 -31
  49. package/dist/docx/newLists.d.ts +30 -0
  50. package/dist/docx/newLists.js +24 -1
  51. package/dist/docx/notes.d.ts +6 -1
  52. package/dist/docx/notes.js +6 -16
  53. package/dist/docx/numberingPlanner.d.ts +6 -3
  54. package/dist/docx/numberingPlanner.js +38 -7
  55. package/dist/docx/pageGeometry.d.ts +5 -14
  56. package/dist/docx/pageGeometry.js +0 -12
  57. package/dist/docx/protectionPolicy.d.ts +13 -5
  58. package/dist/docx/protectionPolicy.js +32 -22
  59. package/dist/docx/scan.d.ts +18 -8
  60. package/dist/docx/scan.js +17 -11
  61. package/dist/docx/sections.d.ts +126 -0
  62. package/dist/docx/sections.js +207 -0
  63. package/dist/docx/serializeBlock.d.ts +13 -4
  64. package/dist/docx/serializeBlock.js +16 -18
  65. package/dist/docx/serializeParagraph.d.ts +4 -0
  66. package/dist/docx/serializeParagraph.js +1 -0
  67. package/dist/docx/serializePreserved.d.ts +14 -0
  68. package/dist/docx/serializePreserved.js +24 -0
  69. package/dist/docx/serializeStory.d.ts +17 -0
  70. package/dist/docx/serializeStory.js +17 -0
  71. package/dist/docx/serializeTable.js +10 -13
  72. package/dist/docx/session.d.ts +24 -7
  73. package/dist/docx/session.js +37 -6
  74. package/dist/docx/story.d.ts +140 -0
  75. package/dist/docx/story.js +237 -0
  76. package/dist/docx/storyProjection.js +6 -1
  77. package/dist/docx/tableFormatting/conditions.d.ts +76 -0
  78. package/dist/docx/tableFormatting/conditions.js +196 -0
  79. package/dist/docx/tableFormatting/editing.d.ts +7 -3
  80. package/dist/docx/tableFormatting/editing.js +4 -5
  81. package/dist/docx/tableFormatting/reading.d.ts +49 -19
  82. package/dist/docx/tableFormatting/reading.js +109 -35
  83. package/dist/docx/tableFormatting.d.ts +1 -0
  84. package/dist/docx/tableFormatting.js +1 -0
  85. package/dist/docx/tableTemplate.js +12 -12
  86. package/dist/editor/clipboard/htmlReader.d.ts +30 -0
  87. package/dist/editor/clipboard/htmlReader.js +302 -0
  88. package/dist/editor/clipboard/internalChannel.d.ts +57 -0
  89. package/dist/editor/clipboard/internalChannel.js +58 -0
  90. package/dist/editor/clipboard/normalizers.d.ts +99 -0
  91. package/dist/editor/clipboard/normalizers.js +199 -0
  92. package/dist/editor/clipboard/parser.d.ts +34 -0
  93. package/dist/editor/clipboard/parser.js +58 -0
  94. package/dist/editor/clipboard/plugin.d.ts +21 -0
  95. package/dist/editor/clipboard/plugin.js +320 -0
  96. package/dist/editor/clipboard/readContext.d.ts +24 -0
  97. package/dist/editor/clipboard/readContext.js +19 -0
  98. package/dist/editor/clipboard/readers.d.ts +33 -0
  99. package/dist/editor/clipboard/readers.js +37 -0
  100. package/dist/editor/commands/comments/editing.d.ts +26 -6
  101. package/dist/editor/commands/comments/editing.js +175 -105
  102. package/dist/editor/commands/comments/model.d.ts +34 -17
  103. package/dist/editor/commands/comments/model.js +11 -0
  104. package/dist/editor/commands/comments/reading.d.ts +5 -3
  105. package/dist/editor/commands/comments/reading.js +8 -58
  106. package/dist/editor/commands/formatting/propertyCommands.js +6 -1
  107. package/dist/editor/commands/index.d.ts +2 -2
  108. package/dist/editor/commands/index.js +2 -0
  109. package/dist/editor/commands/listCommands.d.ts +12 -3
  110. package/dist/editor/commands/listCommands.js +76 -30
  111. package/dist/editor/commands/noteQueries.d.ts +22 -5
  112. package/dist/editor/commands/noteQueries.js +34 -6
  113. package/dist/editor/commands/paragraphCommands.js +6 -1
  114. package/dist/editor/createEditor.js +14 -4
  115. package/dist/editor/documentStyles.d.ts +26 -4
  116. package/dist/editor/documentStyles.js +9 -4
  117. package/dist/editor/editorDocument.d.ts +7 -6
  118. package/dist/editor/editorDocument.js +18 -6
  119. package/dist/editor/imageFiles.d.ts +2 -2
  120. package/dist/editor/imageFiles.js +2 -0
  121. package/dist/editor/insertTable.d.ts +1 -1
  122. package/dist/editor/insertTable.js +2 -2
  123. package/dist/editor/paragraphEdits.d.ts +16 -1
  124. package/dist/editor/paragraphEdits.js +13 -5
  125. package/dist/editor/paragraphPlacement.d.ts +11 -0
  126. package/dist/editor/paragraphPlacement.js +18 -0
  127. package/dist/editor/plainText.d.ts +12 -0
  128. package/dist/editor/plainText.js +2 -1
  129. package/dist/editor/plugins/columnResize.js +6 -3
  130. package/dist/editor/plugins/commentComposer.d.ts +28 -0
  131. package/dist/editor/plugins/commentComposer.js +46 -0
  132. package/dist/editor/plugins/commentDecorations.d.ts +29 -2
  133. package/dist/editor/plugins/commentDecorations.js +89 -15
  134. package/dist/editor/plugins/documentProjection.d.ts +31 -0
  135. package/dist/editor/plugins/documentProjection.js +22 -0
  136. package/dist/editor/plugins/imagePaste.js +20 -10
  137. package/dist/editor/plugins/linkPanel.d.ts +7 -3
  138. package/dist/editor/plugins/linkPanel.js +13 -27
  139. package/dist/editor/plugins/numberingDecorations.d.ts +10 -4
  140. package/dist/editor/plugins/numberingDecorations.js +43 -2
  141. package/dist/editor/plugins/panelState.d.ts +67 -0
  142. package/dist/editor/plugins/panelState.js +54 -0
  143. package/dist/editor/plugins/paragraphDisplay.js +9 -3
  144. package/dist/editor/plugins/tableContextMenu.d.ts +1 -1
  145. package/dist/editor/plugins/tableContextMenu.js +30 -47
  146. package/dist/editor/plugins/tableDisplay.js +19 -5
  147. package/dist/editor/plugins/textContextMenu.d.ts +1 -1
  148. package/dist/editor/plugins/textContextMenu.js +30 -47
  149. package/dist/model/format.d.ts +50 -0
  150. package/dist/model/format.js +65 -0
  151. package/dist/numbering/listRegistry.d.ts +37 -0
  152. package/dist/numbering/listRegistry.js +112 -0
  153. package/dist/numbering/listTemplate.d.ts +26 -17
  154. package/dist/numbering/listTemplate.js +66 -20
  155. package/dist/numbering/markers.d.ts +15 -6
  156. package/dist/numbering/markers.js +26 -59
  157. package/dist/numbering/parseNumbering.d.ts +70 -5
  158. package/dist/numbering/parseNumbering.js +100 -26
  159. package/dist/numbering/spellers.d.ts +15 -0
  160. package/dist/numbering/spellers.js +96 -0
  161. package/dist/numbering/writeNumbering.d.ts +5 -3
  162. package/dist/numbering/writeNumbering.js +15 -15
  163. package/dist/ooxml/conformance.d.ts +28 -0
  164. package/dist/ooxml/conformance.js +20 -0
  165. package/dist/ooxml/errors.d.ts +5 -5
  166. package/dist/ooxml/fragment.d.ts +11 -0
  167. package/dist/ooxml/fragment.js +14 -4
  168. package/dist/ooxml/image.d.ts +3 -4
  169. package/dist/ooxml/image.js +50 -5
  170. package/dist/ooxml/names.d.ts +15 -0
  171. package/dist/ooxml/names.js +10 -1
  172. package/dist/ooxml/partSplice.d.ts +1 -1
  173. package/dist/ooxml/partSplice.js +10 -3
  174. package/dist/ooxml/props.d.ts +8 -0
  175. package/dist/ooxml/props.js +14 -4
  176. package/dist/ooxml/rangeMarkers.d.ts +5 -0
  177. package/dist/ooxml/rangeMarkers.js +24 -0
  178. package/dist/ooxml/xml.d.ts +8 -9
  179. package/dist/ooxml/xml.js +6 -6
  180. package/dist/page/PageGuides.d.ts +8 -4
  181. package/dist/page/PageGuides.js +13 -37
  182. package/dist/page/pageLayout.d.ts +62 -13
  183. package/dist/page/pageLayout.js +91 -37
  184. package/dist/page/usePageLayout.d.ts +14 -11
  185. package/dist/page/usePageLayout.js +21 -29
  186. package/dist/schema/attrRoles.js +44 -19
  187. package/dist/schema/docxSchema.d.ts +9 -1
  188. package/dist/schema/docxSchema.js +220 -139
  189. package/dist/schema/editGuard.d.ts +1 -1
  190. package/dist/schema/guards.js +2 -2
  191. package/dist/schema/index.d.ts +1 -0
  192. package/dist/schema/preservedFragments.d.ts +12 -0
  193. package/dist/schema/preservedFragments.js +38 -0
  194. package/dist/schema/preservedGuards.d.ts +9 -7
  195. package/dist/schema/preservedGuards.js +112 -11
  196. package/dist/schema/protection.d.ts +34 -5
  197. package/dist/schema/protection.js +42 -14
  198. package/dist/schema/rendering.js +3 -1
  199. package/dist/schema/stories.d.ts +49 -0
  200. package/dist/schema/stories.js +78 -0
  201. package/dist/styles/classNames.d.ts +16 -7
  202. package/dist/styles/classNames.js +16 -7
  203. package/dist/styles.css +90 -30
  204. package/dist/table/cellFormatting.js +1 -2
  205. package/dist/table/gridBorders.d.ts +11 -15
  206. package/dist/table/gridBorders.js +32 -18
  207. package/dist/table/resize.d.ts +5 -3
  208. package/dist/ui/CommentsPanel.d.ts +1 -3
  209. package/dist/ui/CommentsPanel.js +24 -15
  210. package/dist/ui/NotesPanel.js +2 -2
  211. package/dist/ui/TextMenu.d.ts +1 -3
  212. package/dist/ui/TextMenu.js +10 -6
  213. package/dist/ui/comments/CommentComposer.d.ts +6 -1
  214. package/dist/ui/comments/CommentComposer.js +26 -3
  215. package/package.json +1 -1
  216. package/dist/editor/externalClipboard.d.ts +0 -11
  217. package/dist/editor/externalClipboard.js +0 -456
@@ -0,0 +1,33 @@
1
+ import { type ResolvedPos, Slice } from "prosemirror-model";
2
+ import { type PastedContent } from "./htmlReader";
3
+ import type { HtmlReadContext } from "./readContext";
4
+ /** What one reader is handed: the markup on the clipboard, and the document it is read into */
5
+ export interface ClipboardInput {
6
+ dom: Node;
7
+ /** The name the markup carries for a slice this editor kept (`./internalChannel`). Null when it carries none */
8
+ token: string | null;
9
+ /** The open document the markup is being read into. Null for a state built without one */
10
+ sessionId: string | null;
11
+ context: HtmlReadContext;
12
+ }
13
+ /**
14
+ * One way of reading a clipboard. The chain is tried in order and the first reader to answer
15
+ * decides what goes in, so a reader that does not recognize what it was handed answers nothing.
16
+ */
17
+ export type ClipboardReader = (input: ClipboardInput) => PastedContent | null;
18
+ /**
19
+ * A copy made in this same open document, handed back as the slice it was.
20
+ *
21
+ * It leads the chain because it is the only reader that loses nothing: everything below reads
22
+ * markup, and markup is all a copy from anywhere else amounts to here.
23
+ */
24
+ export declare const internalSliceReader: ClipboardReader;
25
+ /** Everything the editor did not write itself, read for the formatting the markup states */
26
+ export declare const foreignHtmlReader: ClipboardReader;
27
+ export declare const DEFAULT_READERS: readonly ClipboardReader[];
28
+ /**
29
+ * Plain text as content of this document: docx has no character for a line break or a tab stop,
30
+ * so those become the elements it does have, and the rest keeps the formatting in force where it
31
+ * is put.
32
+ */
33
+ export declare function plainTextSlice(text: string, $context: ResolvedPos): Slice;
@@ -0,0 +1,37 @@
1
+ // src/editor/clipboard/readers.ts
2
+ import { Fragment, Slice } from "prosemirror-model";
3
+ import { NO_NEW_LISTS } from "../../numbering/listRegistry.js";
4
+ import { docxSchema } from "../../schema/index.js";
5
+ import { toPieces } from "../plainText.js";
6
+ import { readHtml } from "./htmlReader.js";
7
+ import { recallCopied } from "./internalChannel.js";
8
+ var internalSliceReader = ({ token, sessionId }) => {
9
+ const recalled = recallCopied(token, sessionId);
10
+ return recalled === null ? null : { ...recalled, newLists: NO_NEW_LISTS };
11
+ };
12
+ var foreignHtmlReader = ({ dom, context }) => dom instanceof Element || dom instanceof DocumentFragment ? readHtml(dom, context) : null;
13
+ var DEFAULT_READERS = [
14
+ internalSliceReader,
15
+ foreignHtmlReader
16
+ ];
17
+ function plainTextSlice(text, $context) {
18
+ const marks = $context.marks();
19
+ const nodes = toPieces(text).map((piece) => {
20
+ if ("text" in piece) return docxSchema.text(piece.text, marks);
21
+ if (piece.node === "hardBreak") {
22
+ return docxSchema.nodes.hardBreak.create(null, null, marks);
23
+ }
24
+ const beside = marks.filter((mark) => mark.type !== docxSchema.marks.tab);
25
+ return docxSchema.text(
26
+ " ",
27
+ docxSchema.marks.tab.create().addToSet(beside)
28
+ );
29
+ });
30
+ return nodes.length === 0 ? Slice.empty : new Slice(Fragment.fromArray(nodes), 0, 0);
31
+ }
32
+ export {
33
+ DEFAULT_READERS,
34
+ foreignHtmlReader,
35
+ internalSliceReader,
36
+ plainTextSlice
37
+ };
@@ -1,16 +1,30 @@
1
1
  /** Edits Word comments through their range markers and reference nodes. */
2
+ import type { Node as PMNode } from "prosemirror-model";
2
3
  import { type Command, type EditorState } from "prosemirror-state";
3
- import type { NewComment } from "./model";
4
- /** Adds a plain-text comment to the current text selection. */
5
- export declare function addComment(comment: NewComment): Command;
4
+ import { type CommentRange, type NewComment } from "./model";
6
5
  /**
7
- * Whether a non-empty selection in one paragraph can receive a comment.
6
+ * Adds a plain-text comment to a stretch of text: the one given, or the current text selection.
7
+ *
8
+ * A composer that was opened over one stretch and submitted later names it, so that what the
9
+ * comment marks is the text it was written about rather than wherever the caret has since gone.
10
+ */
11
+ export declare function addComment(comment: NewComment, at?: CommentRange): Command;
12
+ /**
13
+ * Whether that stretch of text, or the current selection, can receive a comment.
8
14
  * This is the command itself asked without a dispatch, over a comment standing in for the one the
9
15
  * user would write, so the button and the click cannot answer differently.
10
16
  */
11
- export declare function canAddComment(state: EditorState): boolean;
17
+ export declare function canAddComment(state: EditorState, at?: CommentRange): boolean;
12
18
  /** Replaces the plain-text body of one comment, retaining its author and anchor. */
13
19
  export declare function updateComment(id: string, text: string): Command;
20
+ /**
21
+ * Replaces the body of one comment with a formatted one, retaining its author and anchor.
22
+ *
23
+ * The body is a document of `docxSchema`, the same schema the page is edited in, so a paragraph
24
+ * style, a bold run and a second paragraph all survive being written, exported and read back.
25
+ * `updateComment` is the same command for a body that is only text.
26
+ */
27
+ export declare function setCommentBody(id: string, body: PMNode): Command;
14
28
  /** Marks a comment thread resolved or open without deleting it. */
15
29
  export declare function setCommentResolved(id: string, resolved: boolean): Command;
16
30
  /** Adds a plain-text reply and reopens the thread when it was resolved. */
@@ -19,7 +33,13 @@ export declare function addCommentReply(id: string, reply: NewComment): Command;
19
33
  export declare function updateCommentReply(commentId: string, replyId: string, text: string): Command;
20
34
  /** Removes one reply while retaining its root comment and anchor. */
21
35
  export declare function removeCommentReply(commentId: string, replyId: string): Command;
22
- /** Removes a comment's range markers, reference and Comments-part entry. */
36
+ /**
37
+ * Removes a comment's range markers, reference and Comments-part entry.
38
+ *
39
+ * What the comment and its replies said goes with them: a body is a story on the document node,
40
+ * and one left behind for a comment nothing refers to would ride every later comparison of the
41
+ * document without standing for anything.
42
+ */
23
43
  export declare function removeComment(id: string): Command;
24
44
  /** Selects the text anchored by a comment, or places the caret at a point comment. */
25
45
  export declare function selectComment(id: string): Command;
@@ -3,16 +3,39 @@ import {
3
3
  TextSelection
4
4
  } from "prosemirror-state";
5
5
  import { commentParaId } from "../../../docx/comments.js";
6
+ import {
7
+ sameStory,
8
+ setStory,
9
+ storyFromText,
10
+ storyKey,
11
+ storyOf,
12
+ storyText,
13
+ withoutStories
14
+ } from "../../../docx/story.js";
6
15
  import { docxSchema } from "../../../schema/index.js";
7
16
  import { guardedCommand } from "../../../schema/guards.js";
8
17
  import {
9
18
  reservedCommentIds,
10
19
  reservedCommentParaIds
11
20
  } from "../../editorDocument.js";
12
- import { documentComments, repliesAttr, stringAttr } from "./reading.js";
13
- function validCommentSelection(state) {
21
+ import {
22
+ repliesAttr,
23
+ stringAttr
24
+ } from "./model.js";
25
+ import { commentById } from "./reading.js";
26
+ function anchorableRange(doc, { from, to }) {
27
+ if (!Number.isInteger(from) || !Number.isInteger(to) || from >= to || from < 0 || to > doc.content.size)
28
+ return null;
29
+ const $from = doc.resolve(from);
30
+ if (!$from.sameParent(doc.resolve(to))) return null;
31
+ if ($from.parent.type.name !== "paragraph") return null;
32
+ return { from, to };
33
+ }
34
+ function commentedRange(state, at) {
35
+ if (at) return anchorableRange(state.doc, at);
14
36
  const { selection } = state;
15
- return selection instanceof TextSelection && !selection.empty && selection.$from.sameParent(selection.$to) && selection.$from.parent.type.name === "paragraph";
37
+ if (!(selection instanceof TextSelection)) return null;
38
+ return anchorableRange(state.doc, selection);
16
39
  }
17
40
  function nextCommentId(state) {
18
41
  const taken = /* @__PURE__ */ new Set();
@@ -73,14 +96,13 @@ function wrapperMarksAt(state, pos, side) {
73
96
  (mark) => mark.type.name === "sdt" || mark.type.name === "link"
74
97
  );
75
98
  }
76
- function addCommentTransaction(state, comment) {
77
- if (!validCommentSelection(state) || comment.text.trim().length === 0) {
78
- return null;
79
- }
99
+ function addCommentTransaction(state, comment, at) {
100
+ const range = commentedRange(state, at);
101
+ if (range === null || comment.text.trim().length === 0) return null;
80
102
  const id = nextCommentId(state);
81
103
  const date = comment.date ?? (/* @__PURE__ */ new Date()).toISOString();
82
104
  const paraId = nextCommentParaId(state, `comment-${id}-${date}`);
83
- const { from, to } = state.selection;
105
+ const { from, to } = range;
84
106
  const startMarks = wrapperMarksAt(state, from, "after");
85
107
  const endMarks = wrapperMarksAt(state, to, "before");
86
108
  const end = docxSchema.nodes.commentEnd.create(
@@ -96,9 +118,6 @@ function addCommentTransaction(state, comment) {
96
118
  authorId: comment.authorId ?? null,
97
119
  initials: comment.initials ?? null,
98
120
  date,
99
- text: comment.text,
100
- commentXml: null,
101
- imported: false,
102
121
  paraId,
103
122
  resolved: false,
104
123
  extensionXml: null,
@@ -113,39 +132,63 @@ function addCommentTransaction(state, comment) {
113
132
  null,
114
133
  startMarks
115
134
  );
116
- return state.tr.insert(to, end).insert(to + 1, reference).insert(from, start);
135
+ return setStory(
136
+ state.tr.insert(to, end).insert(to + 1, reference).insert(from, start),
137
+ storyKey("comment", id),
138
+ storyFromText(comment.text)
139
+ );
117
140
  }
118
- function addComment(comment) {
119
- return guardedCommand((state) => addCommentTransaction(state, comment));
141
+ function addComment(comment, at) {
142
+ return guardedCommand((state) => addCommentTransaction(state, comment, at));
120
143
  }
121
- function canAddComment(state) {
122
- return addComment({
123
- text: "comment",
124
- author: "Author",
125
- date: "1970-01-01T00:00:00.000Z"
126
- })(state);
144
+ function canAddComment(state, at) {
145
+ return addComment(
146
+ {
147
+ text: "comment",
148
+ author: "Author",
149
+ date: "1970-01-01T00:00:00.000Z"
150
+ },
151
+ at
152
+ )(state);
127
153
  }
128
- function updateComment(id, text) {
129
- return guardedCommand((state) => {
130
- if (text.trim().length === 0) return null;
131
- let transaction = state.tr;
132
- let changed = false;
133
- state.doc.descendants((node, pos) => {
134
- if (node.type.name === "commentReference" && stringAttr(node.attrs.id) === id && stringAttr(node.attrs.text) !== text) {
135
- transaction = transaction.setNodeMarkup(pos, null, {
136
- ...node.attrs,
137
- text,
138
- commentXml: null,
139
- imported: false
140
- });
141
- changed = true;
142
- }
143
- return true;
144
- });
145
- return changed ? transaction : null;
154
+ function referenceAt(state, id) {
155
+ let at = null;
156
+ state.doc.descendants((node, pos) => {
157
+ if (at !== null) return false;
158
+ if (node.type.name === "commentReference" && stringAttr(node.attrs.id) === id) {
159
+ at = pos;
160
+ }
161
+ return at === null;
146
162
  });
163
+ return at;
164
+ }
165
+ function bodyTransaction(state, referenceId, bodyId, body) {
166
+ if (body.type !== docxSchema.nodes.doc) return null;
167
+ if (storyText(body).trim().length === 0) return null;
168
+ const at = referenceAt(state, referenceId);
169
+ const reference = at === null ? null : state.doc.nodeAt(at);
170
+ if (at === null || reference === null) return null;
171
+ const key = storyKey("comment", bodyId);
172
+ if (sameStory(storyOf(state.doc, key), body)) return null;
173
+ return setStory(state.tr, key, body).setNodeMarkup(
174
+ at,
175
+ null,
176
+ reference.attrs,
177
+ reference.marks
178
+ );
147
179
  }
148
- function updateReference(id, change) {
180
+ function textBodyTransaction(state, referenceId, bodyId, text) {
181
+ const held = storyOf(state.doc, storyKey("comment", bodyId));
182
+ if (held !== null && storyText(held) === text) return null;
183
+ return bodyTransaction(state, referenceId, bodyId, storyFromText(text));
184
+ }
185
+ function updateComment(id, text) {
186
+ return guardedCommand((state) => textBodyTransaction(state, id, id, text));
187
+ }
188
+ function setCommentBody(id, body) {
189
+ return guardedCommand((state) => bodyTransaction(state, id, id, body));
190
+ }
191
+ function updateReference(id, change, alongside = (tr) => tr) {
149
192
  return guardedCommand((state) => {
150
193
  let transaction = state.tr;
151
194
  let changed = false;
@@ -160,7 +203,7 @@ function updateReference(id, change) {
160
203
  }
161
204
  return false;
162
205
  });
163
- return changed ? transaction : null;
206
+ return changed ? alongside(transaction) : null;
164
207
  });
165
208
  }
166
209
  function setCommentResolved(id, resolved) {
@@ -179,84 +222,110 @@ function addCommentReply(id, reply) {
179
222
  if (reply.text.trim().length === 0) return false;
180
223
  const replyId = nextCommentId(state);
181
224
  const date = reply.date ?? (/* @__PURE__ */ new Date()).toISOString();
182
- return updateReference(id, (node) => {
183
- const parentParaId = stringAttr(node.attrs.paraId) ?? nextCommentParaId(state, `comment-${id}`);
184
- const paraId = nextCommentParaId(state, `comment-${replyId}-${date}`, [
185
- parentParaId
186
- ]);
187
- return {
188
- ...node.attrs,
189
- paraId: parentParaId,
190
- resolved: false,
191
- extensionXml: null,
192
- threadImported: false,
193
- replies: [
194
- ...repliesAttr(node.attrs.replies),
195
- {
196
- id: replyId,
197
- author: reply.author,
198
- authorId: reply.authorId ?? null,
199
- initials: reply.initials ?? null,
200
- date,
201
- text: reply.text,
202
- commentXml: null,
203
- imported: false,
204
- paraId,
205
- parentParaId,
206
- extensionXml: null
207
- }
208
- ]
209
- };
210
- })(state, dispatch);
225
+ return updateReference(
226
+ id,
227
+ (node) => {
228
+ const parentParaId = stringAttr(node.attrs.paraId) ?? nextCommentParaId(state, `comment-${id}`);
229
+ const paraId = nextCommentParaId(state, `comment-${replyId}-${date}`, [
230
+ parentParaId
231
+ ]);
232
+ return {
233
+ ...node.attrs,
234
+ paraId: parentParaId,
235
+ resolved: false,
236
+ extensionXml: null,
237
+ threadImported: false,
238
+ replies: [
239
+ ...repliesAttr(node.attrs.replies),
240
+ {
241
+ id: replyId,
242
+ author: reply.author,
243
+ authorId: reply.authorId ?? null,
244
+ initials: reply.initials ?? null,
245
+ date,
246
+ paraId,
247
+ parentParaId,
248
+ extensionXml: null
249
+ }
250
+ ]
251
+ };
252
+ },
253
+ (tr) => setStory(tr, storyKey("comment", replyId), storyFromText(reply.text))
254
+ )(state, dispatch);
211
255
  };
212
256
  }
213
257
  function updateCommentReply(commentId, replyId, text) {
214
- return updateReference(commentId, (node) => {
215
- if (text.trim().length === 0) return null;
216
- const replies = repliesAttr(node.attrs.replies);
217
- const index = replies.findIndex((reply) => reply.id === replyId);
218
- if (index < 0 || replies[index].text === text) return null;
219
- return {
220
- ...node.attrs,
221
- replies: replies.map(
222
- (reply, at) => at === index ? { ...reply, text, commentXml: null, imported: false } : reply
223
- )
224
- };
258
+ return guardedCommand((state) => {
259
+ const holdsReply = repliesOf(state, commentId).some(
260
+ (reply) => reply.id === replyId
261
+ );
262
+ if (!holdsReply) return null;
263
+ return textBodyTransaction(state, commentId, replyId, text);
225
264
  });
226
265
  }
227
- function removeCommentReply(commentId, replyId) {
228
- return updateReference(commentId, (node) => {
229
- const replies = repliesAttr(node.attrs.replies);
230
- if (!replies.some((reply) => reply.id === replyId)) return null;
231
- const removedIds = /* @__PURE__ */ new Set([replyId]);
232
- const removedParaIds = new Set(
233
- replies.filter((reply) => removedIds.has(reply.id)).map((reply) => reply.paraId)
234
- );
235
- let changed = true;
236
- while (changed) {
237
- changed = false;
238
- for (const reply of replies) {
239
- if (!removedIds.has(reply.id) && removedParaIds.has(reply.parentParaId)) {
240
- removedIds.add(reply.id);
241
- removedParaIds.add(reply.paraId);
242
- changed = true;
243
- }
244
- }
266
+ function repliesOf(state, commentId) {
267
+ let replies = [];
268
+ state.doc.descendants((node) => {
269
+ if (node.type.name !== "commentReference" || stringAttr(node.attrs.id) !== commentId) {
270
+ return true;
245
271
  }
246
- return {
247
- ...node.attrs,
248
- replies: replies.filter((reply) => !removedIds.has(reply.id)),
249
- threadImported: false
250
- };
272
+ replies = repliesAttr(node.attrs.replies);
273
+ return false;
251
274
  });
275
+ return replies;
276
+ }
277
+ function dropBodies(ids) {
278
+ return (tr) => withoutStories(
279
+ tr,
280
+ Array.from(ids, (id) => storyKey("comment", id))
281
+ );
282
+ }
283
+ function removeCommentReply(commentId, replyId) {
284
+ return (state, dispatch) => {
285
+ const removedReplyIds = /* @__PURE__ */ new Set();
286
+ return updateReference(
287
+ commentId,
288
+ (node) => {
289
+ const replies = repliesAttr(node.attrs.replies);
290
+ if (!replies.some((reply) => reply.id === replyId)) return null;
291
+ const removedIds = /* @__PURE__ */ new Set([replyId]);
292
+ const removedParaIds = new Set(
293
+ replies.filter((reply) => removedIds.has(reply.id)).map((reply) => reply.paraId)
294
+ );
295
+ let changed = true;
296
+ while (changed) {
297
+ changed = false;
298
+ for (const reply of replies) {
299
+ if (!removedIds.has(reply.id) && removedParaIds.has(reply.parentParaId)) {
300
+ removedIds.add(reply.id);
301
+ removedParaIds.add(reply.paraId);
302
+ changed = true;
303
+ }
304
+ }
305
+ }
306
+ for (const id of removedIds) removedReplyIds.add(id);
307
+ return {
308
+ ...node.attrs,
309
+ replies: replies.filter((reply) => !removedIds.has(reply.id)),
310
+ threadImported: false
311
+ };
312
+ },
313
+ dropBodies(removedReplyIds)
314
+ )(state, dispatch);
315
+ };
252
316
  }
253
317
  function removeComment(id) {
254
318
  return guardedCommand((state) => {
255
319
  const positions = [];
320
+ const bodies = /* @__PURE__ */ new Set([id]);
256
321
  state.doc.descendants((node, pos) => {
257
322
  if ((node.type.name === "commentStart" || node.type.name === "commentEnd" || node.type.name === "commentReference") && stringAttr(node.attrs.id) === id) {
258
323
  positions.push({ pos, size: node.nodeSize });
259
324
  }
325
+ if (node.type.name === "commentReference" && stringAttr(node.attrs.id) === id) {
326
+ for (const reply of repliesAttr(node.attrs.replies))
327
+ bodies.add(reply.id);
328
+ }
260
329
  return true;
261
330
  });
262
331
  if (positions.length === 0) return null;
@@ -264,12 +333,12 @@ function removeComment(id) {
264
333
  for (const marker of positions.sort((a, b) => b.pos - a.pos)) {
265
334
  transaction.delete(marker.pos, marker.pos + marker.size);
266
335
  }
267
- return transaction;
336
+ return dropBodies(bodies)(transaction);
268
337
  });
269
338
  }
270
339
  function selectComment(id) {
271
340
  return (state, dispatch) => {
272
- const comment = documentComments(state).find((entry) => entry.id === id);
341
+ const comment = commentById(state, id);
273
342
  if (!comment) return false;
274
343
  dispatch?.(
275
344
  state.tr.setSelection(
@@ -286,6 +355,7 @@ export {
286
355
  removeComment,
287
356
  removeCommentReply,
288
357
  selectComment,
358
+ setCommentBody,
289
359
  setCommentResolved,
290
360
  updateComment,
291
361
  updateCommentReply
@@ -1,4 +1,6 @@
1
1
  /** Public comment values and shared attribute readers. */
2
+ import type { CommentReplyData } from "../../../docx/comments";
3
+ /** Who a comment is written by: the name it is shown under, and the identity behind it. */
2
4
  export interface CommentAuthor {
3
5
  /**
4
6
  * The identity behind the name, an opaque string the host application chooses. Recorded in the
@@ -8,6 +10,14 @@ export interface CommentAuthor {
8
10
  name: string;
9
11
  initials?: string;
10
12
  }
13
+ /**
14
+ * A stretch of text a comment is written for: a pair of document positions, the way a comment
15
+ * already in the document reports its own (`DocumentComment.from`, `.to`).
16
+ */
17
+ export interface CommentRange {
18
+ from: number;
19
+ to: number;
20
+ }
11
21
  export interface NewComment {
12
22
  text: string;
13
23
  author: string;
@@ -17,24 +27,31 @@ export interface NewComment {
17
27
  /** ISO 8601 timestamp. The current time is used when omitted. */
18
28
  date?: string;
19
29
  }
30
+ /**
31
+ * One comment as a reader of the document sees it. Every field is readonly: the record is the
32
+ * editor's own, worked out once per edit and handed to whoever asks (`editor/plugins/documentProjection`).
33
+ */
20
34
  export interface DocumentComment {
21
- id: string;
22
- author: string | null;
23
- authorId: string | null;
24
- initials: string | null;
25
- date: string | null;
26
- text: string;
27
- from: number;
28
- to: number;
29
- referencePos: number;
30
- resolved: boolean;
31
- replies: readonly DocumentCommentReply[];
35
+ readonly id: string;
36
+ readonly author: string | null;
37
+ readonly authorId: string | null;
38
+ readonly initials: string | null;
39
+ readonly date: string | null;
40
+ /** What it says, as plain text. The formatting behind it is `setCommentBody`'s to write */
41
+ readonly text: string;
42
+ readonly from: number;
43
+ readonly to: number;
44
+ readonly referencePos: number;
45
+ readonly resolved: boolean;
46
+ readonly replies: readonly DocumentCommentReply[];
32
47
  }
33
48
  export interface DocumentCommentReply {
34
- id: string;
35
- author: string | null;
36
- authorId: string | null;
37
- initials: string | null;
38
- date: string | null;
39
- text: string;
49
+ readonly id: string;
50
+ readonly author: string | null;
51
+ readonly authorId: string | null;
52
+ readonly initials: string | null;
53
+ readonly date: string | null;
54
+ readonly text: string;
40
55
  }
56
+ export declare function stringAttr(value: unknown): string | null;
57
+ export declare function repliesAttr(value: unknown): readonly CommentReplyData[];
@@ -0,0 +1,11 @@
1
+ // src/editor/commands/comments/model.ts
2
+ function stringAttr(value) {
3
+ return typeof value === "string" ? value : null;
4
+ }
5
+ function repliesAttr(value) {
6
+ return Array.isArray(value) ? value : [];
7
+ }
8
+ export {
9
+ repliesAttr,
10
+ stringAttr
11
+ };
@@ -1,11 +1,13 @@
1
1
  /** Reads comment threads and anchors from the editor document. */
2
2
  import type { EditorState } from "prosemirror-state";
3
- import type { CommentReplyData } from "../../../docx/comments";
4
3
  import type { DocumentComment } from "./model";
5
- export declare function stringAttr(value: unknown): string | null;
6
- export declare function repliesAttr(value: unknown): readonly CommentReplyData[];
7
4
  /** Comments in document order, including point comments that have no explicit range. */
8
5
  export declare function documentComments(state: EditorState): readonly DocumentComment[];
6
+ /**
7
+ * The comment this id names, or undefined where the document holds none. Answered off the lookup
8
+ * the projection built, so a command asking after one comment does not read the whole list.
9
+ */
10
+ export declare function commentById(state: EditorState, id: string): DocumentComment | undefined;
9
11
  /**
10
12
  * Whether the body of this comment, or of one of its replies, may be edited or deleted here: the
11
13
  * protection lets comments be edited at all, and the body is one of one's own to edit
@@ -1,72 +1,22 @@
1
1
  // src/editor/commands/comments/reading.ts
2
2
  import { commentOwned } from "../../../schema/protection.js";
3
3
  import { protectionOf } from "../../../schema/protectionState.js";
4
- function stringAttr(value) {
5
- return typeof value === "string" ? value : null;
6
- }
7
- function repliesAttr(value) {
8
- return Array.isArray(value) ? value : [];
9
- }
10
- function markerPositions(doc) {
11
- const starts = /* @__PURE__ */ new Map();
12
- const ends = /* @__PURE__ */ new Map();
13
- const references = [];
14
- doc.descendants((node, pos) => {
15
- const id = stringAttr(node.attrs.id);
16
- if (id === null) return true;
17
- if (node.type.name === "commentStart" && !starts.has(id)) {
18
- starts.set(id, pos);
19
- } else if (node.type.name === "commentEnd" && !ends.has(id)) {
20
- ends.set(id, pos);
21
- } else if (node.type.name === "commentReference") {
22
- references.push({ node, pos });
23
- }
24
- return true;
25
- });
26
- return { starts, ends, references };
27
- }
4
+ import { commentProjection } from "../../plugins/commentDecorations.js";
28
5
  function documentComments(state) {
29
- const markers = markerPositions(state.doc);
30
- return markers.references.map(({ node, pos }) => {
31
- const id = stringAttr(node.attrs.id) ?? "";
32
- const start = markers.starts.get(id);
33
- const end = markers.ends.get(id);
34
- const hasRange = start !== void 0 && end !== void 0 && start < end;
35
- const point = start ?? end ?? pos;
36
- return {
37
- id,
38
- author: stringAttr(node.attrs.author),
39
- authorId: stringAttr(node.attrs.authorId),
40
- initials: stringAttr(node.attrs.initials),
41
- date: stringAttr(node.attrs.date),
42
- text: stringAttr(node.attrs.text) ?? "",
43
- from: hasRange ? start + 1 : point,
44
- to: hasRange ? end : point,
45
- referencePos: pos,
46
- resolved: node.attrs.resolved === true,
47
- replies: repliesAttr(node.attrs.replies).map((reply) => ({
48
- id: reply.id,
49
- author: reply.author,
50
- authorId: reply.authorId,
51
- initials: reply.initials,
52
- date: reply.date,
53
- text: reply.text
54
- }))
55
- };
56
- });
6
+ return commentProjection.read(state).comments;
7
+ }
8
+ function commentById(state, id) {
9
+ return commentProjection.read(state).byId.get(id);
57
10
  }
58
11
  function canEditComment(state, commentId, replyId = null) {
59
12
  const rules = protectionOf(state);
60
13
  if (rules.protection === "readOnly") return false;
61
- const comment = documentComments(state).find(
62
- (entry) => entry.id === commentId
63
- );
14
+ const comment = commentById(state, commentId);
64
15
  const body = replyId === null ? comment : comment?.replies.find((reply) => reply.id === replyId);
65
16
  return body !== void 0 && commentOwned(rules, body.authorId);
66
17
  }
67
18
  export {
68
19
  canEditComment,
69
- documentComments,
70
- repliesAttr,
71
- stringAttr
20
+ commentById,
21
+ documentComments
72
22
  };