@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,87 @@
1
1
  # @portone/docx-editor
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#53](https://github.com/portone-io/docx-editor/pull/53) [`d264803`](https://github.com/portone-io/docx-editor/commit/d264803381c3272ae31e1af1faa7fd158b95a81f) Thanks [@Deea222](https://github.com/Deea222)! - Stop text copied out of the editor from carrying the document's private data.
8
+
9
+ A copy was drawn the way the editor draws itself, so the HTML it left on the clipboard held the paragraph and run XML, the name and recorded identity of a comment's author, what the comment and its replies say, and the body of a footnote.
10
+ All of it landed in whatever application it was pasted into, and in `comment` mode a reader who may not change the body could take the body out this way.
11
+ A copy now carries only what a reader of it needs, and the paragraph style it names is the style's id rather than the whole `w:pPr`.
12
+ A link goes out as a link, so it can be followed where it lands and comes back as one when it is pasted here again.
13
+
14
+ The plain text beside it says what was copied: a tab is a tab, a line break is a line, a page break is a form feed, and a table's cells stand apart by a tab and its rows by a line, so a table pasted into a spreadsheet arrives as a table.
15
+ A cell holding a line break is the exception, since the line inside it reads as the start of the next row.
16
+
17
+ - [#50](https://github.com/portone-io/docx-editor/pull/50) [`41d82c5`](https://github.com/portone-io/docx-editor/commit/41d82c5bee74597baedd813c43c8dcfcdfe74fbf) Thanks [@Deea222](https://github.com/Deea222)! - Stop `onlyCommentsChangedBy` from excusing a part a submission relates as a comment part.
18
+
19
+ The three comment parts are left out of the byte comparison, and which parts those were came from the submitted file's own relationships.
20
+ A file could relate a second comments, extended comments or people part at any part it liked and have that part go uncompared, so a rewritten styles part, a settings part pointing at a template off the package, replaced image bytes or forged document properties were all reported as a change to nothing but comments.
21
+ The parts left out are now the ones the reader opened, a comment part may be related once, one related for the first time has to be a part the submission brought with it, and a relationship part naming one id twice is turned down.
22
+
23
+ A file that relates an extended comments part but carries no comments part now has that part read, so writing the first comment into it writes that part rather than a second one beside it.
24
+ Two extended parts related at once was a file whose settled threads a reader would lose, since only the first of them is read.
25
+
26
+ This affects 0.2.0. A server that accepted files on this verdict should upgrade and run the check again over what it accepted, where such a file now answers `part-changed` naming the part, or `relationship-changed` naming the relationship part.
27
+
28
+ - [#56](https://github.com/portone-io/docx-editor/pull/56) [`467f6f5`](https://github.com/portone-io/docx-editor/commit/467f6f5781dbf6eb6739dbf3369155c7287ee608) Thanks [@Deea222](https://github.com/Deea222)! - Commands and `canRunCommand` now report false where a bookmark marker or a note reference would be removed, instead of reporting true and changing nothing.
29
+
30
+ A refusal over one of those markers also ends an open IME composition, the way a refusal over a locked control already did.
31
+
32
+ - [#59](https://github.com/portone-io/docx-editor/pull/59) [`6d7b97f`](https://github.com/portone-io/docx-editor/commit/6d7b97fdb89e0aba7ac13057b503ec510a5550c1) Thanks [@Deea222](https://github.com/Deea222)! - Formatting XML is now written through shared primitives that consistently escape attribute values while preserving existing formatting behavior.
33
+
34
+ - [#58](https://github.com/portone-io/docx-editor/pull/58) [`d9edb75`](https://github.com/portone-io/docx-editor/commit/d9edb75fa45e1a6cc81e5741a5242cf500107091) Thanks [@Deea222](https://github.com/Deea222)! - Pagination measures every block as a list of break candidates; no visible change
35
+
36
+ - [#55](https://github.com/portone-io/docx-editor/pull/55) [`7110baa`](https://github.com/portone-io/docx-editor/commit/7110baae68b75323720a39374c6df58db4367896) Thanks [@Deea222](https://github.com/Deea222)! - Turn down a raw OOXML fragment that does not hold its shape as it enters the document, rather than writing it into the exported file.
37
+
38
+ Keep imported formatting when a document uses an inherited default namespace or an alternative WordprocessingML prefix. Reject nested namespace rebinding that could disconnect hyperlinks from their targets on export.
39
+
40
+ The editor draws the original XML of a paragraph, a run, a table, an image or an annotation into the page as a `data-` attribute, so that it can read the live DOM back after an IME composition or a browser edit. A fragment written into one of those attributes from outside, by a consumer plugin or through `view.pasteHTML`, is now held to what that attribute goes back out as: a whole element under an expected name, an opening tag's attributes, or an opening tag the writer closes itself. One that does not hold it is turned down along with the rule reading it, so the content settles one level plainer - a paragraph keeps its text and loses its properties, a run loses its mark, a content control or a hyperlink loses its wrapper - instead of a `data-ppr` reading `</w:p><w:p>...` writing a second paragraph into the exported body, or one that never closed reaching export and failing the whole document with `malformed-xml`.
41
+
42
+ - [#54](https://github.com/portone-io/docx-editor/pull/54) [`8aea477`](https://github.com/portone-io/docx-editor/commit/8aea477dceab5462c15903b25b29c738980aff47) Thanks [@Deea222](https://github.com/Deea222)! - An untouched table opened in the editor is written back byte for byte, its `w:tblGridChange` included.
43
+
44
+ Export decided whether a block was untouched by comparing it with the one import produced, attrs and all.
45
+ Opening a document works some of those attrs out again from the formatting around them - a table's shared cell borders among them - so a table nobody had touched compared unequal and was rebuilt, and the rebuild dropped the markup the writer does not model.
46
+
47
+ Each attr now declares whether the writer writes from it, whether the editor works it out for the screen, or whether it identifies something in the open document, and the comparison ignores the ones worked out for the screen.
48
+ Exporting through `createEditorState` is held to the same byte identity as exporting straight from `importDocx`.
49
+
50
+ - [#48](https://github.com/portone-io/docx-editor/pull/48) [`de84006`](https://github.com/portone-io/docx-editor/commit/de840061dd60f3b652273410bc2d0a85b9451ad3) Thanks [@Deea222](https://github.com/Deea222)! - Stop `onlyCommentsChangedBy` from refusing a comment written into a table cell.
51
+
52
+ A commented table is rebuilt on the way out, so it came back worded the way this editor words it while the original carried the wording its producer chose, and the two were compared word for word.
53
+ The story is now compared as this editor writes it back, so two blocks this editor would write alike are read alike: the attribute order inside a `w:tcW` or a `w:tblW`, a percentage width written as `100%` or as `5000`, runs a producer split that say the same text, and a table's `w:tblGridChange`.
54
+ Formatting properties keep the line breaks, comments and text a producer wrote between them, so rewriting a run's bold, a paragraph's alignment or a cell of a rebuilt table no longer drops them.
55
+ A rebuilt table keeps what stood inside its width and span properties too.
56
+
57
+ - [#52](https://github.com/portone-io/docx-editor/pull/52) [`991ba48`](https://github.com/portone-io/docx-editor/commit/991ba48501e20e60bfc69b3c76862f4a26123b3e) Thanks [@Deea222](https://github.com/Deea222)! - Read the comment parts entry by entry in `onlyCommentsChangedBy`.
58
+
59
+ The three parts a comment is written across are the ones a comment edit may rewrite, so the package comparison passes over their bytes.
60
+ Nothing read them afterwards, which let a submission carry a field pointing at a remote image inside a comment body, a comment attributed to a third author that nothing refers to, or markup wrapped around a body, and still be answered as a file where only comments changed.
61
+ Each entry now has to arrive as it was, or be one this editor writes for an author who could have written it, and an entry nothing refers to has to stay as it was.
62
+ A file that fails is refused as `part-changed` naming the comment part.
63
+
64
+ Settling or replying to a comment that arrived with the file no longer rewrites its entry as plain text.
65
+ The entry keeps what it said, and its last paragraph gains the `w14:paraId` the thread state is written against.
66
+ The extended comments part carries an entry only for a comment that has thread state.
67
+
68
+ ## 0.2.0
69
+
70
+ ### Minor Changes
71
+
72
+ - [#45](https://github.com/portone-io/docx-editor/pull/45) [`3190a35`](https://github.com/portone-io/docx-editor/commit/3190a359e0686bdc14166e22b1d96c761dabe251) Thanks [@Deea222](https://github.com/Deea222)! - Add a `comment` mode and record who wrote each comment.
73
+
74
+ **Breaking:** `mode` is now required and the `commentAuthor` prop is removed.
75
+ `author: { id, name, initials? }` moves into `mode` for its `comment` and `edit` kinds.
76
+ `contextMenus` moves off `mode` onto `DocxEditor` itself, so `mode: { kind: "edit", contextMenus: false }` becomes `contextMenus={false}`.
77
+
78
+ In `comment` mode the text can be selected and copied but not changed, while comments can be written, answered, resolved, and reopened.
79
+ A comment records its author's identity in the document, so two authors sharing a display name stay distinct.
80
+ Only a comment's author may edit or delete it, and anyone may reply, resolve, and reopen.
81
+ `mode.editableComments: "all"` opens every comment to a moderator.
82
+ The `canEditComment` and `editingProtection` queries report what the current mode allows.
83
+ A server can check a file coming back with `onlyCommentsChangedBy` from `@portone/docx-editor/core`, which answers `{ ok: true }` or `{ ok: false, reason }` for whether the file differs in nothing but one author's comments.
84
+
3
85
  ## 0.1.1
4
86
 
5
87
  ### Patch Changes
package/CONTRIBUTING.md CHANGED
@@ -48,12 +48,16 @@ pnpm test:e2e
48
48
 
49
49
  [Testing](https://github.com/portone-io/docx-editor/blob/main/docs/testing.md) explains the scope and prerequisites of every check.
50
50
 
51
+ A command added to `./commands` or `./table` needs a writer probe in `src/docx/__testing__/writerProbes.ts` with a `check` of its immediate effect, so its export is validated before another command can overwrite it, or an entry in `NOT_A_WRITER` giving the reason it reaches no writer.
52
+
51
53
  ## Changesets
52
54
 
53
55
  If you changed anything under `src/`, run `pnpm changeset` and commit the file it writes.
54
56
  It becomes the CHANGELOG entry, so write it for someone reading release notes rather than the diff.
55
57
  Choose `patch` unless the change adds to or breaks the public API.
56
58
  Those are `minor` while the package is below 1.0.
59
+ A declaration added, removed, or rewritten in `etc/*.api.md` after running `pnpm api:update` is what that looks like; a line that only gains or loses an `(undocumented)` marker is not.
60
+ Commit the regenerated report in the same commit as the changeset.
57
61
 
58
62
  By contributing, you agree that your contribution is licensed under the Apache License 2.0 used by this project.
59
63
 
package/README.md CHANGED
@@ -18,21 +18,33 @@ import "@portone/docx-editor/styles.css";
18
18
  import { DocxEditor, type DocxEditorHandle } from "@portone/docx-editor";
19
19
  import { useRef } from "react";
20
20
 
21
- export function Editor({ file }: { file: File }) {
21
+ export function Editor({
22
+ file,
23
+ user,
24
+ }: {
25
+ file: File;
26
+ user: { id: string; name: string };
27
+ }) {
22
28
  const editorRef = useRef<DocxEditorHandle | null>(null);
23
29
 
24
- return <DocxEditor ref={editorRef} document={file} />;
30
+ return (
31
+ <DocxEditor
32
+ ref={editorRef}
33
+ document={file}
34
+ mode={{ kind: "edit", author: { id: user.id, name: user.name } }}
35
+ />
36
+ );
25
37
  }
26
38
  ```
27
39
 
28
- `document` accepts a `File`, `Blob`, `ArrayBuffer`, or `Uint8Array`; use the ref to export the edited document as bytes.
40
+ `document` accepts a `File`, `Blob`, `ArrayBuffer`, or `Uint8Array`; `mode` selects read-only, comment-only, or full editing and names the author new comments are attributed to; use the ref to export the edited document as bytes.
29
41
 
30
42
  ## What it does
31
43
 
32
- - Provides browser editing with built-in controls, read-only mode, and document locking.
44
+ - Provides browser editing with built-in controls, read-only and comment-only modes, and document locking.
33
45
  - Fits the page to the available width by default and lets readers choose a fixed zoom level.
34
46
  - Exports edited documents as DOCX bytes or browser downloads.
35
- - Preserves document structures and package parts across edits.
47
+ - Preserves document structures and package parts across edits, and records who wrote each comment.
36
48
  - Reads document styles, theme fonts, page layout, and CJK font information for display.
37
49
 
38
50
  See [Feature support](https://docx-editor.portone.io/docs/features) for the support matrix. Build [custom controls](https://docx-editor.portone.io/docs/custom-controls) or use [programmatic DOCX import and export](https://docx-editor.portone.io/docs/core).
@@ -13,6 +13,7 @@ import { type CSSProperties, type ReactNode } from "react";
13
13
  import { type DocxSource } from "./docx/importDocx";
14
14
  import { type CommentAuthor } from "./editor/commands/commentCommands";
15
15
  import { DocxImportError } from "./ooxml/errors";
16
+ import type { EditableComments } from "./schema/protection";
16
17
  import type { FontFallbacks } from "./styles/fontStack";
17
18
  import type { DocxEditorPresets } from "./ui/presets";
18
19
  import { type DocxEditorZoom } from "./ui/zoom";
@@ -24,22 +25,37 @@ export interface DocxEditorHandle {
24
25
  /**
25
26
  * What the editor is for, which decides what it offers.
26
27
  *
27
- * A read-only editor takes no edits, so it has no toolbar and no right click menus either: what
28
- * used to be three booleans of which one silently emptied the other two is one choice here.
28
+ * The three kinds are the standings OOXML document protection names (`ST_DocProtect`) and the
29
+ * ones a shared document gives its readers: a reader, a commenter, an editor. A read-only editor
30
+ * takes no edits, so it has no toolbar and no right click menus either: what used to be three
31
+ * booleans of which one silently emptied the other two is one choice here. A `comment` editor is
32
+ * the `comments` protection - the body may not be changed, comments may be written, answered and
33
+ * settled - which is what a reviewer who must not touch the text is handed. Both kinds that write
34
+ * comments name whose they are, so a reader is never asked for an identity it has no use for.
35
+ *
36
+ * `editableComments` is whose comments the panel offers to edit or delete: one's own, which is a
37
+ * comment carrying no recognised identity or the very identity given here, or every one, which
38
+ * is a moderator's standing. Replying and resolving are open to every commenter either way.
39
+ *
29
40
  * `locking` is what a screen where a template is written gets: settling a part of a document is
30
41
  * an authoring act, not something every reader of a form should be handed. A lock the document
31
42
  * already carries holds whichever mode is chosen.
32
43
  *
33
- * `contextMenus: false` leaves the right click to the browser, for a consumer drawing menus of
34
- * its own. The plugins that take the browser's menu away go into the editor state, which is built
35
- * when the editor mounts, so this is read once there like `plugins`.
44
+ * The kind, the author and `editableComments` are read on every render and take effect on the
45
+ * open document. Whether the right click opens the editor's own menus is `contextMenus`, a prop
46
+ * of its own, since the plugins behind it are read when the editor mounts rather than per render.
36
47
  */
37
48
  export type DocxEditorMode = {
38
49
  kind: "readOnly";
50
+ } | {
51
+ kind: "comment";
52
+ author: CommentAuthor;
53
+ editableComments?: EditableComments;
39
54
  } | {
40
55
  kind: "edit";
56
+ author: CommentAuthor;
57
+ editableComments?: EditableComments;
41
58
  toolbar?: boolean;
42
- contextMenus?: boolean;
43
59
  locking?: boolean;
44
60
  };
45
61
  export interface DocxEditorProps {
@@ -58,8 +74,8 @@ export interface DocxEditorProps {
58
74
  * never silent either way. Hand one in to write the refusal in your own words.
59
75
  */
60
76
  renderImportError?: (error: DocxImportError) => ReactNode;
61
- /** What the editor is for. Editing with the toolbar shown and no locking when none is given */
62
- mode?: DocxEditorMode;
77
+ /** What the editor is for: a reader's, a commenter's or an editor's surface */
78
+ mode: DocxEditorMode;
63
79
  /** Whether to draw approximate page boundaries over the document's own paper. Drawn when read only too */
64
80
  showPageGuides?: boolean;
65
81
  /**
@@ -96,8 +112,6 @@ export interface DocxEditorProps {
96
112
  * takes effect without a remount.
97
113
  */
98
114
  presets?: DocxEditorPresets;
99
- /** The identity written by the built-in comment and reply composers */
100
- commentAuthor?: CommentAuthor;
101
115
  /**
102
116
  * ProseMirror plugins handed in by the consumer, which is how mentions, highlights,
103
117
  * autocomplete and the like are added from outside the package.
@@ -110,6 +124,16 @@ export interface DocxEditorProps {
110
124
  * `key` and remount.
111
125
  */
112
126
  plugins?: readonly Plugin[];
127
+ /**
128
+ * Whether the right click opens the editor's own menus. `false` leaves it to the browser,
129
+ * which is what a consumer drawing menus of its own wants. Defaults to `true`.
130
+ *
131
+ * Read once, when the editor mounts, like `plugins`: the plugins that take the browser's menu
132
+ * away go into the editor state, which is built there. A menu the protection has nothing to
133
+ * offer from stands down by itself, so a mode change needs nothing here. Later changes are
134
+ * ignored; to turn the menus around, change the `key` and remount.
135
+ */
136
+ contextMenus?: boolean;
113
137
  className?: string;
114
138
  style?: CSSProperties;
115
139
  onReady?: (view: EditorView) => void;
@@ -15,6 +15,7 @@ import {
15
15
  } from "./editor/commands/commentCommands.js";
16
16
  import { activeLinkSpan } from "./editor/commands/linkCommands.js";
17
17
  import { createEditorState, createEditorView } from "./editor/createEditor.js";
18
+ import { setProtection } from "./editor/plugins/documentProtection.js";
18
19
  import { isLinkPanelOpen } from "./editor/plugins/linkPanel.js";
19
20
  import { tableMenuAnchor } from "./editor/plugins/tableContextMenu.js";
20
21
  import { textMenuAnchor } from "./editor/plugins/textContextMenu.js";
@@ -23,6 +24,7 @@ import { DocxImportError } from "./ooxml/errors.js";
23
24
  import { PageGuides } from "./page/PageGuides.js";
24
25
  import { A4_PAGE_PIXELS, pagePixels } from "./page/pageLayout.js";
25
26
  import { usePageLayout } from "./page/usePageLayout.js";
27
+ import { editingProtection, protectionOf } from "./schema/protectionState.js";
26
28
  import { editorClassNames } from "./styles/classNames.js";
27
29
  import { CommentsPanel } from "./ui/CommentsPanel.js";
28
30
  import { LinkCard } from "./ui/LinkCard.js";
@@ -34,6 +36,38 @@ import { Toolbar } from "./ui/Toolbar.js";
34
36
  import { useFitWidthZoom } from "./ui/useFitWidthZoom.js";
35
37
  import { normalizeZoom } from "./ui/zoom.js";
36
38
  import { jsx, jsxs } from "react/jsx-runtime";
39
+ function affordancesOf(mode) {
40
+ switch (mode.kind) {
41
+ case "readOnly":
42
+ return {
43
+ protection: "readOnly",
44
+ author: null,
45
+ editableComments: "own",
46
+ toolbar: false,
47
+ locking: false
48
+ };
49
+ case "comment":
50
+ return {
51
+ protection: "comments",
52
+ author: mode.author ?? null,
53
+ editableComments: mode.editableComments ?? "own",
54
+ toolbar: false,
55
+ locking: false
56
+ };
57
+ case "edit":
58
+ return {
59
+ protection: "none",
60
+ author: mode.author ?? null,
61
+ editableComments: mode.editableComments ?? "own",
62
+ toolbar: mode.toolbar ?? true,
63
+ locking: mode.locking ?? false
64
+ };
65
+ default: {
66
+ const unmodelled = mode;
67
+ return unmodelled;
68
+ }
69
+ }
70
+ }
37
71
  function openDocument(bytes) {
38
72
  try {
39
73
  const { doc, session } = importDocx(bytes);
@@ -97,32 +131,28 @@ function zoomVariable(factor) {
97
131
  function DocxEditorSurface({
98
132
  document: source,
99
133
  renderImportError,
100
- mode = { kind: "edit" },
134
+ mode,
101
135
  showPageGuides = true,
102
136
  zoom,
103
137
  defaultZoom = "fit-width",
104
138
  onZoomChange,
105
139
  fontFallbacks,
106
140
  presets,
107
- commentAuthor = { name: "Anonymous" },
108
141
  plugins,
142
+ contextMenus,
109
143
  className,
110
144
  style,
111
145
  onReady,
112
146
  onChange
113
147
  }, ref) {
114
- const readOnly = mode.kind === "readOnly";
115
- const showToolbar = mode.kind === "edit" && (mode.toolbar ?? true);
116
- const allowLocking = mode.kind === "edit" && (mode.locking ?? false);
148
+ const { protection, author, editableComments, toolbar, locking } = affordancesOf(mode);
117
149
  const bytes = useDocumentBytes(source);
118
150
  const opened = useMemo(
119
151
  () => bytes === null ? null : openDocument(bytes),
120
152
  [bytes]
121
153
  );
122
154
  const mountedPlugins = useRef(plugins).current;
123
- const mountedContextMenus = useRef(
124
- mode.kind !== "edit" || (mode.contextMenus ?? true)
125
- ).current;
155
+ const mountedContextMenus = useRef(contextMenus ?? true).current;
126
156
  const mountedFontFallbacks = useRef(fontFallbacks).current;
127
157
  const layerRef = useRef(null);
128
158
  const rootRef = useRef(null);
@@ -173,11 +203,13 @@ function DocxEditorSurface({
173
203
  ...opened.session.comments.extendedOrdered.map(
174
204
  (extension) => extension.paraId
175
205
  )
176
- ]
206
+ ],
207
+ protection,
208
+ author,
209
+ editableComments
177
210
  }),
178
211
  defaults: opened.session.defaults,
179
212
  geometry: opened.session.geometry,
180
- readOnly,
181
213
  fontFallbacks: mountedFontFallbacks,
182
214
  onStateChange: (state) => {
183
215
  keptState.current = { of: opened, state };
@@ -196,19 +228,32 @@ function DocxEditorSurface({
196
228
  };
197
229
  }, [
198
230
  opened,
199
- readOnly,
200
231
  mountedContextMenus,
201
232
  mountedFontFallbacks,
202
233
  mountedPlugins,
203
234
  latestOnReady,
204
235
  latestOnChange
205
236
  ]);
237
+ const authorId = author?.id ?? null;
238
+ const mounted = live !== null;
239
+ useLayoutEffect(() => {
240
+ if (protection === "readOnly") setCommentComposerOpen(false);
241
+ const view = viewRef.current;
242
+ if (!view || !mounted) return;
243
+ const held = protectionOf(view.state);
244
+ if (held.protection === protection && held.authorId === authorId && held.editableComments === editableComments) {
245
+ return;
246
+ }
247
+ view.dispatch(
248
+ setProtection(view.state.tr, { protection, author, editableComments })
249
+ );
250
+ }, [mounted, protection, authorId, editableComments]);
206
251
  useImperativeHandle(ref, () => {
207
252
  const view = viewRef.current;
208
253
  if (!view || opened?.status !== "opened") return null;
209
254
  const session = opened.session;
210
255
  return { view, exportBytes: () => exportDocx(view.state.doc, session) };
211
- }, [opened, readOnly]);
256
+ }, [opened]);
212
257
  const overlay = usePageLayout({
213
258
  view: live?.view ?? null,
214
259
  layer: layerRef,
@@ -219,21 +264,22 @@ function DocxEditorSurface({
219
264
  if (opened?.status === "rejected") {
220
265
  return renderImportError ? renderImportError(opened.error) : /* @__PURE__ */ jsx(ImportRejection, { error: opened.error });
221
266
  }
222
- const textAnchor = live && !readOnly ? textMenuAnchor(live.state) : null;
223
- const tableAnchor = live && !readOnly && !textAnchor ? tableMenuAnchor(live.state) : null;
267
+ const bodyOpen = live !== null && editingProtection(live.state) === "none";
268
+ const commentsOpenToWrite = live !== null && editingProtection(live.state) !== "readOnly";
269
+ const textAnchor = live ? textMenuAnchor(live.state) : null;
270
+ const tableAnchor = live && !textAnchor ? tableMenuAnchor(live.state) : null;
224
271
  const linkAtCursor = live && !live.view.composing && !isLinkPanelOpen(live.state) ? activeLinkSpan(live.state) : null;
225
272
  const comments = live?.state === void 0 ? [] : documentComments(live.state);
226
273
  const hasUnresolvedComments = comments.some((comment) => !comment.resolved);
227
- const effectiveComposerOpen = commentComposerOpen && !readOnly;
274
+ const effectiveComposerOpen = commentComposerOpen && commentsOpenToWrite;
228
275
  const showComments = live !== null && (commentsOpen || effectiveComposerOpen || hasUnresolvedComments);
229
276
  const commentsPanel = live && showComments && /* @__PURE__ */ jsx(
230
277
  CommentsPanel,
231
278
  {
232
279
  view: live.view,
233
280
  state: live.state,
234
- readOnly,
235
281
  composerOpen: effectiveComposerOpen,
236
- author: commentAuthor,
282
+ author,
237
283
  closeComposer: () => setCommentComposerOpen(false),
238
284
  scrollContainer: rootRef.current,
239
285
  allCommentsOpen: commentsOpen
@@ -245,7 +291,7 @@ function DocxEditorSurface({
245
291
  className: [editorClassNames.frame, className].filter(Boolean).join(" "),
246
292
  style,
247
293
  children: [
248
- live && showToolbar && /* @__PURE__ */ jsx(
294
+ live && toolbar && /* @__PURE__ */ jsx(
249
295
  Toolbar,
250
296
  {
251
297
  view: live.view,
@@ -270,7 +316,7 @@ function DocxEditorSurface({
270
316
  "data-comments": showComments ? "visible" : void 0,
271
317
  style: zoomVariable(effectiveZoom),
272
318
  children: [
273
- live && readOnly && comments.length > 0 && /* @__PURE__ */ jsx(
319
+ live && !bodyOpen && comments.length > 0 && /* @__PURE__ */ jsx(
274
320
  "button",
275
321
  {
276
322
  type: "button",
@@ -306,14 +352,13 @@ function DocxEditorSurface({
306
352
  ]
307
353
  }
308
354
  ),
309
- live && !readOnly && isLinkPanelOpen(live.state) && /* @__PURE__ */ jsx(LinkPanel, { view: live.view, state: live.state }),
355
+ live && bodyOpen && isLinkPanelOpen(live.state) && /* @__PURE__ */ jsx(LinkPanel, { view: live.view, state: live.state }),
310
356
  live && linkAtCursor && /* @__PURE__ */ jsx(
311
357
  LinkCard,
312
358
  {
313
359
  view: live.view,
314
360
  state: live.state,
315
- link: linkAtCursor,
316
- readOnly
361
+ link: linkAtCursor
317
362
  },
318
363
  linkAtCursor.from
319
364
  ),
@@ -323,7 +368,7 @@ function DocxEditorSurface({
323
368
  view: live.view,
324
369
  state: live.state,
325
370
  anchor: textAnchor,
326
- allowLocking,
371
+ allowLocking: locking,
327
372
  onAddComment: () => {
328
373
  setCommentComposerOpen(true);
329
374
  }
@@ -335,7 +380,7 @@ function DocxEditorSurface({
335
380
  view: live.view,
336
381
  state: live.state,
337
382
  anchor: tableAnchor,
338
- allowLocking
383
+ allowLocking: locking
339
384
  }
340
385
  )
341
386
  ]
package/dist/core.d.ts CHANGED
@@ -5,6 +5,8 @@
5
5
  import type { Node as PMNode } from "prosemirror-model";
6
6
  import { type DocxBytes } from "./docx/importDocx";
7
7
  import type { DocxSession } from "./docx/session";
8
+ export type { CommentOnlyVerdict } from "./docx/commentOnlyChange";
9
+ export { onlyCommentsChangedBy } from "./docx/commentOnlyChange";
8
10
  export { exportDocx } from "./docx/exportDocx";
9
11
  export type { ParagraphStyleOption } from "./docx/formatting";
10
12
  export type { DocxBytes } from "./docx/importDocx";
package/dist/core.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/core.ts
2
2
  import { importDocx as openDocx } from "./docx/importDocx.js";
3
+ import { onlyCommentsChangedBy } from "./docx/commentOnlyChange.js";
3
4
  import { exportDocx } from "./docx/exportDocx.js";
4
5
  import { documentNumbering, documentPartPath } from "./docx/session.js";
5
6
  import {
@@ -26,6 +27,7 @@ export {
26
27
  emuToPx,
27
28
  exportDocx,
28
29
  importDocx,
30
+ onlyCommentsChangedBy,
29
31
  parseNumbering,
30
32
  pxToEmu,
31
33
  toCellFormat,
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Whether a file handed back changed in nothing but comments one author is allowed to have made.
3
+ *
4
+ * The editor's own refusal under a comment protection (`schema/protection`) is a courtesy to the
5
+ * user: the browser holds the file, so the rule is held again here, where a server takes it in.
6
+ * The judgement is over the package rather than over the document alone, since a submission is
7
+ * free to rewrite anything the story does not carry - the paper the document is written on, its
8
+ * styles, its headers - and a document comparison would see none of it.
9
+ */
10
+ import { type EditableComments } from "../schema/protection";
11
+ import { type DocxBytes } from "./importDocx";
12
+ /**
13
+ * Why a file is not the one it claims to be. `part-changed` and `relationship-changed` name the
14
+ * part they were reached over; the other three are about the document story itself.
15
+ */
16
+ export type CommentOnlyVerdict = {
17
+ ok: true;
18
+ } | {
19
+ ok: false;
20
+ reason: "body-changed" | "comment-not-owned" | "comment-author-forged";
21
+ } | {
22
+ ok: false;
23
+ reason: "part-changed" | "relationship-changed";
24
+ part: string;
25
+ };
26
+ /**
27
+ * Whether the submitted file differs from the original in nothing but comments, every one of them
28
+ * added, edited, moved, deleted, replied to or settled by the author with this identity.
29
+ *
30
+ * Every part of the package has to arrive as it left, save for the three a comment is written
31
+ * across and the relationship and content type they are declared with; the document story itself
32
+ * has to read as it did, comments aside. Those three parts are read entry by entry instead
33
+ * (`./comments/verifying`), since a comment edit is free to rewrite them and something has to say
34
+ * what it may have written there. A comment carrying no recorded identity is everyone's to
35
+ * edit here as it is in the editor (`schema/protection`), while a comment that appeared has to
36
+ * carry this identity: a file can claim any author, and the editor's own hand in writing it is
37
+ * not there to vouch for it. An identity already recorded is nobody's to rewrite.
38
+ *
39
+ * `editableComments: "all"` judges the file of an editor opened for a moderator, where every
40
+ * comment was theirs to edit; an identity is nobody's to rewrite under either setting.
41
+ *
42
+ * Bytes that are not a readable docx are turned down the way opening one is, with a
43
+ * `DocxImportError`, rather than being answered as a file that changed.
44
+ */
45
+ export declare function onlyCommentsChangedBy(original: DocxBytes, submitted: DocxBytes, authorId: string, { editableComments }?: {
46
+ editableComments?: EditableComments;
47
+ }): CommentOnlyVerdict;