@portone/docx-editor 0.4.0 → 0.5.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 (180) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/CONTRIBUTING.md +2 -0
  3. package/assets/editor.png +0 -0
  4. package/dist/DocxEditor.d.ts +1 -1
  5. package/dist/DocxEditor.js +44 -25
  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 +65 -28
  20. package/dist/docx/exportDocx.js +10 -14
  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/styles.d.ts +0 -2
  27. package/dist/docx/formatting/styles.js +2 -15
  28. package/dist/docx/headersFooters.d.ts +89 -20
  29. package/dist/docx/headersFooters.js +163 -172
  30. package/dist/docx/identities.js +17 -26
  31. package/dist/docx/importDocx.d.ts +1 -1
  32. package/dist/docx/importDocx.js +100 -48
  33. package/dist/docx/importParagraph.d.ts +18 -5
  34. package/dist/docx/importParagraph.js +75 -128
  35. package/dist/docx/importPolicy.d.ts +70 -0
  36. package/dist/docx/importPolicy.js +195 -0
  37. package/dist/docx/importPreserved.d.ts +30 -0
  38. package/dist/docx/importPreserved.js +54 -0
  39. package/dist/docx/importTable.js +56 -12
  40. package/dist/docx/invariants.js +16 -18
  41. package/dist/docx/notes.d.ts +6 -1
  42. package/dist/docx/notes.js +6 -16
  43. package/dist/docx/pageGeometry.d.ts +5 -14
  44. package/dist/docx/pageGeometry.js +0 -12
  45. package/dist/docx/protectionPolicy.d.ts +13 -5
  46. package/dist/docx/protectionPolicy.js +32 -22
  47. package/dist/docx/scan.d.ts +18 -8
  48. package/dist/docx/scan.js +17 -11
  49. package/dist/docx/sections.d.ts +126 -0
  50. package/dist/docx/sections.js +207 -0
  51. package/dist/docx/serializeBlock.d.ts +13 -4
  52. package/dist/docx/serializeBlock.js +16 -18
  53. package/dist/docx/serializeParagraph.d.ts +9 -4
  54. package/dist/docx/serializeParagraph.js +23 -58
  55. package/dist/docx/serializePreserved.d.ts +14 -0
  56. package/dist/docx/serializePreserved.js +24 -0
  57. package/dist/docx/serializeStory.d.ts +17 -0
  58. package/dist/docx/serializeStory.js +17 -0
  59. package/dist/docx/serializeTable.js +10 -13
  60. package/dist/docx/session.d.ts +23 -6
  61. package/dist/docx/session.js +26 -5
  62. package/dist/docx/story.d.ts +140 -0
  63. package/dist/docx/story.js +237 -0
  64. package/dist/docx/storyProjection.js +6 -1
  65. package/dist/docx/tableTemplate.d.ts +25 -0
  66. package/dist/docx/tableTemplate.js +30 -16
  67. package/dist/docx/wrappers.d.ts +67 -0
  68. package/dist/docx/wrappers.js +117 -0
  69. package/dist/editor/clipboard/blockReaders.d.ts +74 -0
  70. package/dist/editor/clipboard/blockReaders.js +193 -0
  71. package/dist/editor/clipboard/htmlReader.d.ts +26 -0
  72. package/dist/editor/clipboard/htmlReader.js +376 -0
  73. package/dist/editor/clipboard/inlineFormatting.d.ts +0 -7
  74. package/dist/editor/clipboard/inlineFormatting.js +1 -7
  75. package/dist/editor/clipboard/internalChannel.d.ts +57 -0
  76. package/dist/editor/clipboard/internalChannel.js +58 -0
  77. package/dist/editor/clipboard/normalizers.d.ts +99 -0
  78. package/dist/editor/clipboard/normalizers.js +199 -0
  79. package/dist/editor/clipboard/parser.d.ts +34 -0
  80. package/dist/editor/clipboard/parser.js +58 -0
  81. package/dist/editor/clipboard/plugin.d.ts +21 -0
  82. package/dist/editor/clipboard/plugin.js +188 -0
  83. package/dist/editor/clipboard/readContext.d.ts +34 -0
  84. package/dist/editor/clipboard/readContext.js +25 -0
  85. package/dist/editor/clipboard/readers.d.ts +33 -0
  86. package/dist/editor/clipboard/readers.js +37 -0
  87. package/dist/editor/clipboard/source.d.ts +12 -0
  88. package/dist/editor/clipboard/source.js +21 -0
  89. package/dist/editor/commands/comments/editing.d.ts +26 -6
  90. package/dist/editor/commands/comments/editing.js +177 -106
  91. package/dist/editor/commands/comments/model.d.ts +34 -17
  92. package/dist/editor/commands/comments/model.js +11 -0
  93. package/dist/editor/commands/comments/reading.d.ts +5 -3
  94. package/dist/editor/commands/comments/reading.js +8 -58
  95. package/dist/editor/commands/index.d.ts +2 -2
  96. package/dist/editor/commands/index.js +2 -0
  97. package/dist/editor/commands/linkCommands.js +20 -5
  98. package/dist/editor/commands/listCommands.d.ts +6 -0
  99. package/dist/editor/commands/listCommands.js +3 -2
  100. package/dist/editor/commands/lockCommands.js +59 -16
  101. package/dist/editor/commands/noteQueries.d.ts +22 -5
  102. package/dist/editor/commands/noteQueries.js +34 -6
  103. package/dist/editor/commands/paragraphCommands.js +2 -2
  104. package/dist/editor/createEditor.js +13 -3
  105. package/dist/editor/documentStyles.d.ts +26 -4
  106. package/dist/editor/documentStyles.js +9 -4
  107. package/dist/editor/imageFiles.d.ts +2 -2
  108. package/dist/editor/imageFiles.js +2 -0
  109. package/dist/editor/insertTable.d.ts +1 -1
  110. package/dist/editor/insertTable.js +2 -2
  111. package/dist/editor/plainText.d.ts +12 -0
  112. package/dist/editor/plainText.js +2 -1
  113. package/dist/editor/plugins/columnResize.js +6 -3
  114. package/dist/editor/plugins/commentComposer.d.ts +28 -0
  115. package/dist/editor/plugins/commentComposer.js +46 -0
  116. package/dist/editor/plugins/commentDecorations.d.ts +29 -2
  117. package/dist/editor/plugins/commentDecorations.js +89 -15
  118. package/dist/editor/plugins/documentProjection.d.ts +31 -0
  119. package/dist/editor/plugins/documentProjection.js +22 -0
  120. package/dist/editor/plugins/imagePaste.js +15 -8
  121. package/dist/editor/plugins/linkPanel.d.ts +7 -3
  122. package/dist/editor/plugins/linkPanel.js +13 -27
  123. package/dist/editor/plugins/panelState.d.ts +67 -0
  124. package/dist/editor/plugins/panelState.js +54 -0
  125. package/dist/editor/plugins/tableContextMenu.d.ts +1 -1
  126. package/dist/editor/plugins/tableContextMenu.js +30 -47
  127. package/dist/editor/plugins/textContextMenu.d.ts +1 -1
  128. package/dist/editor/plugins/textContextMenu.js +30 -47
  129. package/dist/ooxml/fragment.d.ts +11 -0
  130. package/dist/ooxml/fragment.js +14 -4
  131. package/dist/ooxml/image.d.ts +3 -4
  132. package/dist/ooxml/image.js +50 -5
  133. package/dist/ooxml/names.d.ts +6 -0
  134. package/dist/ooxml/names.js +2 -0
  135. package/dist/ooxml/props.d.ts +10 -0
  136. package/dist/ooxml/props.js +28 -4
  137. package/dist/ooxml/rangeMarkers.d.ts +5 -0
  138. package/dist/ooxml/rangeMarkers.js +24 -0
  139. package/dist/page/PageGuides.d.ts +7 -3
  140. package/dist/page/PageGuides.js +13 -24
  141. package/dist/page/pageLayout.d.ts +64 -5
  142. package/dist/page/pageLayout.js +90 -29
  143. package/dist/page/usePageLayout.d.ts +14 -4
  144. package/dist/page/usePageLayout.js +19 -20
  145. package/dist/schema/attrRoles.js +46 -23
  146. package/dist/schema/clipboard.d.ts +72 -0
  147. package/dist/schema/clipboard.js +288 -0
  148. package/dist/schema/docxSchema.d.ts +9 -1
  149. package/dist/schema/docxSchema.js +217 -165
  150. package/dist/schema/editGuard.d.ts +1 -1
  151. package/dist/schema/guards.js +2 -2
  152. package/dist/schema/index.d.ts +1 -0
  153. package/dist/schema/locks.d.ts +3 -0
  154. package/dist/schema/locks.js +29 -12
  155. package/dist/schema/preservedFragments.d.ts +12 -0
  156. package/dist/schema/preservedFragments.js +38 -0
  157. package/dist/schema/preservedGuards.d.ts +9 -7
  158. package/dist/schema/preservedGuards.js +112 -11
  159. package/dist/schema/protection.d.ts +34 -5
  160. package/dist/schema/protection.js +42 -14
  161. package/dist/schema/rendering.d.ts +14 -0
  162. package/dist/schema/rendering.js +27 -12
  163. package/dist/schema/stories.d.ts +49 -0
  164. package/dist/schema/stories.js +78 -0
  165. package/dist/schema/wrappers.d.ts +58 -0
  166. package/dist/schema/wrappers.js +77 -0
  167. package/dist/styles/classNames.d.ts +3 -5
  168. package/dist/styles/classNames.js +3 -5
  169. package/dist/styles.css +74 -12
  170. package/dist/table/resize.d.ts +5 -3
  171. package/dist/ui/CommentsPanel.d.ts +1 -3
  172. package/dist/ui/CommentsPanel.js +24 -15
  173. package/dist/ui/NotesPanel.js +2 -2
  174. package/dist/ui/TextMenu.d.ts +1 -3
  175. package/dist/ui/TextMenu.js +10 -6
  176. package/dist/ui/comments/CommentComposer.d.ts +6 -1
  177. package/dist/ui/comments/CommentComposer.js +26 -3
  178. package/package.json +5 -4
  179. package/dist/editor/externalClipboard.d.ts +0 -22
  180. package/dist/editor/externalClipboard.js +0 -486
@@ -0,0 +1,34 @@
1
+ import type { EditorState } from "prosemirror-state";
2
+ import type { FormattingContext, ParagraphStyleOption } from "../../docx/formatting";
3
+ import type { PageGeometry } from "../../docx/pageGeometry";
4
+ import type { ImageToInsert } from "../insertImage";
5
+ import type { HtmlSource } from "./source";
6
+ /**
7
+ * Everything a piece of HTML is read against.
8
+ *
9
+ * The reader answers about the document it is read into - which styles it may name, which list
10
+ * numbers are taken, whether a list may be started at all - and nothing about the editor holding
11
+ * it. Gathering those in one place is what keeps the reader off the editing state, so the same
12
+ * reading can be asked for from a paste, from a drop, and from a test.
13
+ */
14
+ export interface HtmlReadContext {
15
+ /** The document the markup is read into an element of */
16
+ document: Document;
17
+ formatting: FormattingContext;
18
+ /** The paper a block read into the document lands on, which is the width a pasted table takes */
19
+ geometry: PageGeometry;
20
+ paragraphStyles: readonly ParagraphStyleOption[];
21
+ numbering: {
22
+ used: ReadonlySet<number>;
23
+ canCreate: boolean;
24
+ };
25
+ /** The images already loaded for this read, by the token the markup carries */
26
+ images: ReadonlyMap<string, ImageToInsert>;
27
+ /**
28
+ * The application that wrote the markup being read (`./source`). It is a property of the markup
29
+ * and not of the document, so a context built before any markup is in hand names no application
30
+ * and `readHtml` fills it in from what it is handed.
31
+ */
32
+ source: HtmlSource;
33
+ }
34
+ export declare function readContextOf(state: EditorState, document: Document, images?: ReadonlyMap<string, ImageToInsert>): HtmlReadContext;
@@ -0,0 +1,25 @@
1
+ // src/editor/clipboard/readContext.ts
2
+ import { numIdsIn } from "../commands/listCommands.js";
3
+ import {
4
+ documentFormatting,
5
+ documentParagraphStyles,
6
+ sectionGeometryAt
7
+ } from "../documentStyles.js";
8
+ import { canStartNewList } from "../plugins/numberingDecorations.js";
9
+ function readContextOf(state, document, images = /* @__PURE__ */ new Map()) {
10
+ return {
11
+ document,
12
+ formatting: documentFormatting(state),
13
+ geometry: sectionGeometryAt(state, state.selection.from),
14
+ paragraphStyles: documentParagraphStyles(state),
15
+ numbering: {
16
+ used: numIdsIn(state.doc),
17
+ canCreate: canStartNewList(state)
18
+ },
19
+ images,
20
+ source: "unknown"
21
+ };
22
+ }
23
+ export {
24
+ readContextOf
25
+ };
@@ -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
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Which application wrote the markup on the clipboard.
3
+ *
4
+ * Every writer of HTML says so somewhere - in a generator meta element, in a class prefix, in the
5
+ * id it wraps a copy in - and what it says decides which readings apply: Word's list paragraphs
6
+ * are lists rather than paragraphs beginning with a bullet character, and its `<o:p>` markers are
7
+ * nothing at all. A reading recognizing no writer stays with what the markup itself states.
8
+ */
9
+ /** The writers whose markup is read differently from plain HTML */
10
+ export type HtmlSource = "editor" | "word" | "google-docs" | "libreoffice" | "unknown";
11
+ /** The application the markup under this element was written by */
12
+ export declare function detectHtmlSource(root: ParentNode): HtmlSource;
@@ -0,0 +1,21 @@
1
+ // src/editor/clipboard/source.ts
2
+ import { editorClassNames } from "../../styles/classNames.js";
3
+ import { INTERNAL_TOKEN_ATTRIBUTE } from "./internalChannel.js";
4
+ var SIGNATURES = [
5
+ ["editor", `[${INTERNAL_TOKEN_ATTRIBUTE}], .${editorClassNames.paragraph}`],
6
+ ["word", 'meta[name="Generator" i][content^="Microsoft Word" i], .MsoNormal'],
7
+ ["google-docs", '[id^="docs-internal-guid-"]'],
8
+ [
9
+ "libreoffice",
10
+ 'meta[name="generator" i][content*="LibreOffice" i], meta[name="generator" i][content*="OpenOffice" i]'
11
+ ]
12
+ ];
13
+ function detectHtmlSource(root) {
14
+ for (const [source, selector] of SIGNATURES) {
15
+ if (root.querySelector(selector) !== null) return source;
16
+ }
17
+ return "unknown";
18
+ }
19
+ export {
20
+ detectHtmlSource
21
+ };
@@ -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,40 @@ 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";
17
+ import { isWrapperType } from "../../../schema/wrappers.js";
8
18
  import {
9
19
  reservedCommentIds,
10
20
  reservedCommentParaIds
11
21
  } from "../../editorDocument.js";
12
- import { documentComments, repliesAttr, stringAttr } from "./reading.js";
13
- function validCommentSelection(state) {
22
+ import {
23
+ repliesAttr,
24
+ stringAttr
25
+ } from "./model.js";
26
+ import { commentById } from "./reading.js";
27
+ function anchorableRange(doc, { from, to }) {
28
+ if (!Number.isInteger(from) || !Number.isInteger(to) || from >= to || from < 0 || to > doc.content.size)
29
+ return null;
30
+ const $from = doc.resolve(from);
31
+ if (!$from.sameParent(doc.resolve(to))) return null;
32
+ if ($from.parent.type.name !== "paragraph") return null;
33
+ return { from, to };
34
+ }
35
+ function commentedRange(state, at) {
36
+ if (at) return anchorableRange(state.doc, at);
14
37
  const { selection } = state;
15
- return selection instanceof TextSelection && !selection.empty && selection.$from.sameParent(selection.$to) && selection.$from.parent.type.name === "paragraph";
38
+ if (!(selection instanceof TextSelection)) return null;
39
+ return anchorableRange(state.doc, selection);
16
40
  }
17
41
  function nextCommentId(state) {
18
42
  const taken = /* @__PURE__ */ new Set();
@@ -70,17 +94,16 @@ function wrapperMarksAt(state, pos, side) {
70
94
  const resolved = state.doc.resolve(pos);
71
95
  const adjacent = side === "before" ? resolved.nodeBefore : resolved.nodeAfter;
72
96
  return (adjacent?.marks ?? resolved.marks()).filter(
73
- (mark) => mark.type.name === "sdt" || mark.type.name === "link"
97
+ (mark) => isWrapperType(mark.type)
74
98
  );
75
99
  }
76
- function addCommentTransaction(state, comment) {
77
- if (!validCommentSelection(state) || comment.text.trim().length === 0) {
78
- return null;
79
- }
100
+ function addCommentTransaction(state, comment, at) {
101
+ const range = commentedRange(state, at);
102
+ if (range === null || comment.text.trim().length === 0) return null;
80
103
  const id = nextCommentId(state);
81
104
  const date = comment.date ?? (/* @__PURE__ */ new Date()).toISOString();
82
105
  const paraId = nextCommentParaId(state, `comment-${id}-${date}`);
83
- const { from, to } = state.selection;
106
+ const { from, to } = range;
84
107
  const startMarks = wrapperMarksAt(state, from, "after");
85
108
  const endMarks = wrapperMarksAt(state, to, "before");
86
109
  const end = docxSchema.nodes.commentEnd.create(
@@ -96,9 +119,6 @@ function addCommentTransaction(state, comment) {
96
119
  authorId: comment.authorId ?? null,
97
120
  initials: comment.initials ?? null,
98
121
  date,
99
- text: comment.text,
100
- commentXml: null,
101
- imported: false,
102
122
  paraId,
103
123
  resolved: false,
104
124
  extensionXml: null,
@@ -113,39 +133,63 @@ function addCommentTransaction(state, comment) {
113
133
  null,
114
134
  startMarks
115
135
  );
116
- return state.tr.insert(to, end).insert(to + 1, reference).insert(from, start);
136
+ return setStory(
137
+ state.tr.insert(to, end).insert(to + 1, reference).insert(from, start),
138
+ storyKey("comment", id),
139
+ storyFromText(comment.text)
140
+ );
117
141
  }
118
- function addComment(comment) {
119
- return guardedCommand((state) => addCommentTransaction(state, comment));
142
+ function addComment(comment, at) {
143
+ return guardedCommand((state) => addCommentTransaction(state, comment, at));
120
144
  }
121
- function canAddComment(state) {
122
- return addComment({
123
- text: "comment",
124
- author: "Author",
125
- date: "1970-01-01T00:00:00.000Z"
126
- })(state);
145
+ function canAddComment(state, at) {
146
+ return addComment(
147
+ {
148
+ text: "comment",
149
+ author: "Author",
150
+ date: "1970-01-01T00:00:00.000Z"
151
+ },
152
+ at
153
+ )(state);
127
154
  }
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;
155
+ function referenceAt(state, id) {
156
+ let at = null;
157
+ state.doc.descendants((node, pos) => {
158
+ if (at !== null) return false;
159
+ if (node.type.name === "commentReference" && stringAttr(node.attrs.id) === id) {
160
+ at = pos;
161
+ }
162
+ return at === null;
146
163
  });
164
+ return at;
165
+ }
166
+ function bodyTransaction(state, referenceId, bodyId, body) {
167
+ if (body.type !== docxSchema.nodes.doc) return null;
168
+ if (storyText(body).trim().length === 0) return null;
169
+ const at = referenceAt(state, referenceId);
170
+ const reference = at === null ? null : state.doc.nodeAt(at);
171
+ if (at === null || reference === null) return null;
172
+ const key = storyKey("comment", bodyId);
173
+ if (sameStory(storyOf(state.doc, key), body)) return null;
174
+ return setStory(state.tr, key, body).setNodeMarkup(
175
+ at,
176
+ null,
177
+ reference.attrs,
178
+ reference.marks
179
+ );
147
180
  }
148
- function updateReference(id, change) {
181
+ function textBodyTransaction(state, referenceId, bodyId, text) {
182
+ const held = storyOf(state.doc, storyKey("comment", bodyId));
183
+ if (held !== null && storyText(held) === text) return null;
184
+ return bodyTransaction(state, referenceId, bodyId, storyFromText(text));
185
+ }
186
+ function updateComment(id, text) {
187
+ return guardedCommand((state) => textBodyTransaction(state, id, id, text));
188
+ }
189
+ function setCommentBody(id, body) {
190
+ return guardedCommand((state) => bodyTransaction(state, id, id, body));
191
+ }
192
+ function updateReference(id, change, alongside = (tr) => tr) {
149
193
  return guardedCommand((state) => {
150
194
  let transaction = state.tr;
151
195
  let changed = false;
@@ -160,7 +204,7 @@ function updateReference(id, change) {
160
204
  }
161
205
  return false;
162
206
  });
163
- return changed ? transaction : null;
207
+ return changed ? alongside(transaction) : null;
164
208
  });
165
209
  }
166
210
  function setCommentResolved(id, resolved) {
@@ -179,84 +223,110 @@ function addCommentReply(id, reply) {
179
223
  if (reply.text.trim().length === 0) return false;
180
224
  const replyId = nextCommentId(state);
181
225
  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);
226
+ return updateReference(
227
+ id,
228
+ (node) => {
229
+ const parentParaId = stringAttr(node.attrs.paraId) ?? nextCommentParaId(state, `comment-${id}`);
230
+ const paraId = nextCommentParaId(state, `comment-${replyId}-${date}`, [
231
+ parentParaId
232
+ ]);
233
+ return {
234
+ ...node.attrs,
235
+ paraId: parentParaId,
236
+ resolved: false,
237
+ extensionXml: null,
238
+ threadImported: false,
239
+ replies: [
240
+ ...repliesAttr(node.attrs.replies),
241
+ {
242
+ id: replyId,
243
+ author: reply.author,
244
+ authorId: reply.authorId ?? null,
245
+ initials: reply.initials ?? null,
246
+ date,
247
+ paraId,
248
+ parentParaId,
249
+ extensionXml: null
250
+ }
251
+ ]
252
+ };
253
+ },
254
+ (tr) => setStory(tr, storyKey("comment", replyId), storyFromText(reply.text))
255
+ )(state, dispatch);
211
256
  };
212
257
  }
213
258
  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
- };
259
+ return guardedCommand((state) => {
260
+ const holdsReply = repliesOf(state, commentId).some(
261
+ (reply) => reply.id === replyId
262
+ );
263
+ if (!holdsReply) return null;
264
+ return textBodyTransaction(state, commentId, replyId, text);
225
265
  });
226
266
  }
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
- }
267
+ function repliesOf(state, commentId) {
268
+ let replies = [];
269
+ state.doc.descendants((node) => {
270
+ if (node.type.name !== "commentReference" || stringAttr(node.attrs.id) !== commentId) {
271
+ return true;
245
272
  }
246
- return {
247
- ...node.attrs,
248
- replies: replies.filter((reply) => !removedIds.has(reply.id)),
249
- threadImported: false
250
- };
273
+ replies = repliesAttr(node.attrs.replies);
274
+ return false;
251
275
  });
276
+ return replies;
277
+ }
278
+ function dropBodies(ids) {
279
+ return (tr) => withoutStories(
280
+ tr,
281
+ Array.from(ids, (id) => storyKey("comment", id))
282
+ );
283
+ }
284
+ function removeCommentReply(commentId, replyId) {
285
+ return (state, dispatch) => {
286
+ const removedReplyIds = /* @__PURE__ */ new Set();
287
+ return updateReference(
288
+ commentId,
289
+ (node) => {
290
+ const replies = repliesAttr(node.attrs.replies);
291
+ if (!replies.some((reply) => reply.id === replyId)) return null;
292
+ const removedIds = /* @__PURE__ */ new Set([replyId]);
293
+ const removedParaIds = new Set(
294
+ replies.filter((reply) => removedIds.has(reply.id)).map((reply) => reply.paraId)
295
+ );
296
+ let changed = true;
297
+ while (changed) {
298
+ changed = false;
299
+ for (const reply of replies) {
300
+ if (!removedIds.has(reply.id) && removedParaIds.has(reply.parentParaId)) {
301
+ removedIds.add(reply.id);
302
+ removedParaIds.add(reply.paraId);
303
+ changed = true;
304
+ }
305
+ }
306
+ }
307
+ for (const id of removedIds) removedReplyIds.add(id);
308
+ return {
309
+ ...node.attrs,
310
+ replies: replies.filter((reply) => !removedIds.has(reply.id)),
311
+ threadImported: false
312
+ };
313
+ },
314
+ dropBodies(removedReplyIds)
315
+ )(state, dispatch);
316
+ };
252
317
  }
253
318
  function removeComment(id) {
254
319
  return guardedCommand((state) => {
255
320
  const positions = [];
321
+ const bodies = /* @__PURE__ */ new Set([id]);
256
322
  state.doc.descendants((node, pos) => {
257
323
  if ((node.type.name === "commentStart" || node.type.name === "commentEnd" || node.type.name === "commentReference") && stringAttr(node.attrs.id) === id) {
258
324
  positions.push({ pos, size: node.nodeSize });
259
325
  }
326
+ if (node.type.name === "commentReference" && stringAttr(node.attrs.id) === id) {
327
+ for (const reply of repliesAttr(node.attrs.replies))
328
+ bodies.add(reply.id);
329
+ }
260
330
  return true;
261
331
  });
262
332
  if (positions.length === 0) return null;
@@ -264,12 +334,12 @@ function removeComment(id) {
264
334
  for (const marker of positions.sort((a, b) => b.pos - a.pos)) {
265
335
  transaction.delete(marker.pos, marker.pos + marker.size);
266
336
  }
267
- return transaction;
337
+ return dropBodies(bodies)(transaction);
268
338
  });
269
339
  }
270
340
  function selectComment(id) {
271
341
  return (state, dispatch) => {
272
- const comment = documentComments(state).find((entry) => entry.id === id);
342
+ const comment = commentById(state, id);
273
343
  if (!comment) return false;
274
344
  dispatch?.(
275
345
  state.tr.setSelection(
@@ -286,6 +356,7 @@ export {
286
356
  removeComment,
287
357
  removeCommentReply,
288
358
  selectComment,
359
+ setCommentBody,
289
360
  setCommentResolved,
290
361
  updateComment,
291
362
  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[];