@portone/docx-editor 0.4.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 (155) hide show
  1. package/CHANGELOG.md +159 -0
  2. package/assets/editor.png +0 -0
  3. package/dist/DocxEditor.d.ts +1 -1
  4. package/dist/DocxEditor.js +44 -25
  5. package/dist/docx/cloning.js +4 -9
  6. package/dist/docx/commentOnlyChange.d.ts +5 -3
  7. package/dist/docx/comments/grammar.d.ts +27 -10
  8. package/dist/docx/comments/grammar.js +18 -67
  9. package/dist/docx/comments/model.d.ts +5 -6
  10. package/dist/docx/comments/model.js +0 -9
  11. package/dist/docx/comments/parts.d.ts +16 -10
  12. package/dist/docx/comments/parts.js +65 -13
  13. package/dist/docx/comments/people.d.ts +11 -3
  14. package/dist/docx/comments/people.js +20 -6
  15. package/dist/docx/comments/policy.js +7 -1
  16. package/dist/docx/comments/reading.d.ts +4 -2
  17. package/dist/docx/comments/reading.js +1 -14
  18. package/dist/docx/comments/writing.js +65 -28
  19. package/dist/docx/exportDocx.js +10 -14
  20. package/dist/docx/exportRefs.d.ts +8 -3
  21. package/dist/docx/exportRefs.js +2 -1
  22. package/dist/docx/fidelity.js +24 -10
  23. package/dist/docx/fields.d.ts +42 -0
  24. package/dist/docx/fields.js +76 -0
  25. package/dist/docx/headersFooters.d.ts +89 -20
  26. package/dist/docx/headersFooters.js +163 -172
  27. package/dist/docx/importDocx.d.ts +1 -1
  28. package/dist/docx/importDocx.js +100 -48
  29. package/dist/docx/importParagraph.d.ts +16 -4
  30. package/dist/docx/importParagraph.js +87 -93
  31. package/dist/docx/importPolicy.d.ts +70 -0
  32. package/dist/docx/importPolicy.js +190 -0
  33. package/dist/docx/importPreserved.d.ts +30 -0
  34. package/dist/docx/importPreserved.js +54 -0
  35. package/dist/docx/importTable.js +56 -12
  36. package/dist/docx/invariants.js +16 -18
  37. package/dist/docx/notes.d.ts +6 -1
  38. package/dist/docx/notes.js +6 -16
  39. package/dist/docx/pageGeometry.d.ts +5 -14
  40. package/dist/docx/pageGeometry.js +0 -12
  41. package/dist/docx/protectionPolicy.d.ts +13 -5
  42. package/dist/docx/protectionPolicy.js +32 -22
  43. package/dist/docx/scan.d.ts +18 -8
  44. package/dist/docx/scan.js +17 -11
  45. package/dist/docx/sections.d.ts +126 -0
  46. package/dist/docx/sections.js +207 -0
  47. package/dist/docx/serializeBlock.d.ts +13 -4
  48. package/dist/docx/serializeBlock.js +16 -18
  49. package/dist/docx/serializeParagraph.d.ts +4 -0
  50. package/dist/docx/serializeParagraph.js +1 -0
  51. package/dist/docx/serializePreserved.d.ts +14 -0
  52. package/dist/docx/serializePreserved.js +24 -0
  53. package/dist/docx/serializeStory.d.ts +17 -0
  54. package/dist/docx/serializeStory.js +17 -0
  55. package/dist/docx/serializeTable.js +10 -13
  56. package/dist/docx/session.d.ts +23 -6
  57. package/dist/docx/session.js +26 -5
  58. package/dist/docx/story.d.ts +140 -0
  59. package/dist/docx/story.js +237 -0
  60. package/dist/docx/storyProjection.js +6 -1
  61. package/dist/editor/clipboard/htmlReader.d.ts +30 -0
  62. package/dist/editor/{externalClipboard.js → clipboard/htmlReader.js} +39 -223
  63. package/dist/editor/clipboard/internalChannel.d.ts +57 -0
  64. package/dist/editor/clipboard/internalChannel.js +58 -0
  65. package/dist/editor/clipboard/normalizers.d.ts +99 -0
  66. package/dist/editor/clipboard/normalizers.js +199 -0
  67. package/dist/editor/clipboard/parser.d.ts +34 -0
  68. package/dist/editor/clipboard/parser.js +58 -0
  69. package/dist/editor/clipboard/plugin.d.ts +21 -0
  70. package/dist/editor/clipboard/plugin.js +320 -0
  71. package/dist/editor/clipboard/readContext.d.ts +24 -0
  72. package/dist/editor/clipboard/readContext.js +19 -0
  73. package/dist/editor/clipboard/readers.d.ts +33 -0
  74. package/dist/editor/clipboard/readers.js +37 -0
  75. package/dist/editor/commands/comments/editing.d.ts +26 -6
  76. package/dist/editor/commands/comments/editing.js +175 -105
  77. package/dist/editor/commands/comments/model.d.ts +34 -17
  78. package/dist/editor/commands/comments/model.js +11 -0
  79. package/dist/editor/commands/comments/reading.d.ts +5 -3
  80. package/dist/editor/commands/comments/reading.js +8 -58
  81. package/dist/editor/commands/index.d.ts +2 -2
  82. package/dist/editor/commands/index.js +2 -0
  83. package/dist/editor/commands/listCommands.d.ts +6 -0
  84. package/dist/editor/commands/listCommands.js +3 -2
  85. package/dist/editor/commands/noteQueries.d.ts +22 -5
  86. package/dist/editor/commands/noteQueries.js +34 -6
  87. package/dist/editor/createEditor.js +13 -3
  88. package/dist/editor/documentStyles.d.ts +26 -4
  89. package/dist/editor/documentStyles.js +9 -4
  90. package/dist/editor/imageFiles.d.ts +2 -2
  91. package/dist/editor/imageFiles.js +2 -0
  92. package/dist/editor/insertTable.d.ts +1 -1
  93. package/dist/editor/insertTable.js +2 -2
  94. package/dist/editor/plainText.d.ts +12 -0
  95. package/dist/editor/plainText.js +2 -1
  96. package/dist/editor/plugins/columnResize.js +6 -3
  97. package/dist/editor/plugins/commentComposer.d.ts +28 -0
  98. package/dist/editor/plugins/commentComposer.js +46 -0
  99. package/dist/editor/plugins/commentDecorations.d.ts +29 -2
  100. package/dist/editor/plugins/commentDecorations.js +89 -15
  101. package/dist/editor/plugins/documentProjection.d.ts +31 -0
  102. package/dist/editor/plugins/documentProjection.js +22 -0
  103. package/dist/editor/plugins/imagePaste.js +15 -8
  104. package/dist/editor/plugins/linkPanel.d.ts +7 -3
  105. package/dist/editor/plugins/linkPanel.js +13 -27
  106. package/dist/editor/plugins/panelState.d.ts +67 -0
  107. package/dist/editor/plugins/panelState.js +54 -0
  108. package/dist/editor/plugins/tableContextMenu.d.ts +1 -1
  109. package/dist/editor/plugins/tableContextMenu.js +30 -47
  110. package/dist/editor/plugins/textContextMenu.d.ts +1 -1
  111. package/dist/editor/plugins/textContextMenu.js +30 -47
  112. package/dist/ooxml/fragment.d.ts +11 -0
  113. package/dist/ooxml/fragment.js +14 -4
  114. package/dist/ooxml/image.d.ts +3 -4
  115. package/dist/ooxml/image.js +50 -5
  116. package/dist/ooxml/names.d.ts +6 -0
  117. package/dist/ooxml/names.js +2 -0
  118. package/dist/ooxml/props.d.ts +8 -0
  119. package/dist/ooxml/props.js +14 -4
  120. package/dist/ooxml/rangeMarkers.d.ts +5 -0
  121. package/dist/ooxml/rangeMarkers.js +24 -0
  122. package/dist/page/PageGuides.d.ts +7 -3
  123. package/dist/page/PageGuides.js +13 -24
  124. package/dist/page/pageLayout.d.ts +64 -5
  125. package/dist/page/pageLayout.js +90 -29
  126. package/dist/page/usePageLayout.d.ts +14 -4
  127. package/dist/page/usePageLayout.js +19 -20
  128. package/dist/schema/attrRoles.js +38 -20
  129. package/dist/schema/docxSchema.d.ts +9 -1
  130. package/dist/schema/docxSchema.js +191 -139
  131. package/dist/schema/editGuard.d.ts +1 -1
  132. package/dist/schema/guards.js +2 -2
  133. package/dist/schema/index.d.ts +1 -0
  134. package/dist/schema/preservedFragments.d.ts +12 -0
  135. package/dist/schema/preservedFragments.js +38 -0
  136. package/dist/schema/preservedGuards.d.ts +9 -7
  137. package/dist/schema/preservedGuards.js +112 -11
  138. package/dist/schema/protection.d.ts +34 -5
  139. package/dist/schema/protection.js +42 -14
  140. package/dist/schema/rendering.js +3 -1
  141. package/dist/schema/stories.d.ts +49 -0
  142. package/dist/schema/stories.js +78 -0
  143. package/dist/styles/classNames.d.ts +3 -5
  144. package/dist/styles/classNames.js +3 -5
  145. package/dist/styles.css +74 -12
  146. package/dist/table/resize.d.ts +5 -3
  147. package/dist/ui/CommentsPanel.d.ts +1 -3
  148. package/dist/ui/CommentsPanel.js +24 -15
  149. package/dist/ui/NotesPanel.js +2 -2
  150. package/dist/ui/TextMenu.d.ts +1 -3
  151. package/dist/ui/TextMenu.js +10 -6
  152. package/dist/ui/comments/CommentComposer.d.ts +6 -1
  153. package/dist/ui/comments/CommentComposer.js +26 -3
  154. package/package.json +1 -1
  155. package/dist/editor/externalClipboard.d.ts +0 -22
@@ -1,9 +1,26 @@
1
1
  // src/schema/preservedGuards.ts
2
+ import {
3
+ AddMarkStep,
4
+ AddNodeMarkStep,
5
+ AttrStep,
6
+ RemoveMarkStep,
7
+ RemoveNodeMarkStep,
8
+ ReplaceAroundStep,
9
+ ReplaceStep
10
+ } from "prosemirror-transform";
11
+ import { W_NS } from "../ooxml/names.js";
2
12
  import { parseProps, propsChild } from "../ooxml/props.js";
13
+ import {
14
+ COMMENT_RANGE_MARKERS,
15
+ PERMISSION_MARKERS,
16
+ RANGE_MARKERS
17
+ } from "../ooxml/rangeMarkers.js";
18
+ import { parseAttrs } from "../ooxml/tagScan.js";
3
19
  import {
4
20
  rangeHolds,
5
21
  transactionReaches
6
22
  } from "./editGuard.js";
23
+ import { visitPreservedFragments } from "./preservedFragments.js";
7
24
  function signatures(doc, holds, signature) {
8
25
  const found = [];
9
26
  doc.descendants((node) => {
@@ -23,19 +40,103 @@ function preservedNodeGuard(name, holds, signature) {
23
40
  shuts: (intent, state) => intent.kind === "replace" && rangeHolds(state.doc, intent.from, intent.to, holds)
24
41
  };
25
42
  }
26
- var BOOKMARK_XML = /<(?:[\w.-]+:)?bookmark(?:Start|End)\b/;
27
- function isBookmarkMarker(node) {
28
- if (node.type.name === "bookmarkBlock") return true;
29
- return node.type.name === "rawInline" && typeof node.attrs.xml === "string" && BOOKMARK_XML.test(node.attrs.xml);
43
+ function isGuardedFragment(node) {
44
+ return node.attrs.guarded === true;
30
45
  }
31
- function bookmarkSignature(node) {
32
- return node.type.name === "bookmarkBlock" ? `block:${node.attrs.srcId}:${node.attrs.name}` : `inline:${node.attrs.xml}`;
46
+ function preservedSignature(node) {
47
+ return `${node.type.name}:${node.attrs.name}:${node.attrs.srcId}:${node.attrs.xml}`;
33
48
  }
34
- var bookmarkGuard = preservedNodeGuard(
35
- "bookmark",
36
- isBookmarkMarker,
37
- bookmarkSignature
49
+ var guardedNodes = preservedNodeGuard(
50
+ "preserved",
51
+ isGuardedFragment,
52
+ preservedSignature
38
53
  );
54
+ var rangeMarkers = /* @__PURE__ */ new Set([
55
+ ...RANGE_MARKERS,
56
+ ...PERMISSION_MARKERS,
57
+ ...COMMENT_RANGE_MARKERS
58
+ ]);
59
+ function markerFragments(xml) {
60
+ const children = parseProps(`<markers>${xml}</markers>`)?.children ?? [];
61
+ return children.flatMap((child) => {
62
+ if (!rangeMarkers.has(child.name)) return [];
63
+ const props = parseProps(child.xml);
64
+ if (!props) return [];
65
+ const prefixEnd = props.tag.indexOf(":");
66
+ const binding = prefixEnd < 0 ? "xmlns" : `xmlns:${props.tag.slice(0, prefixEnd)}`;
67
+ const namespace = parseAttrs(props.attrs ?? "")?.find(
68
+ ([name]) => name === binding
69
+ )?.[1];
70
+ if (namespace !== void 0 && namespace !== W_NS) return [];
71
+ return [child.xml];
72
+ });
73
+ }
74
+ function hasMarkerAttrs(node) {
75
+ return (node.type.name === "table" || node.type.name === "tableRow") && typeof node.attrs.leadingXml === "string" || (node.type.name === "tableCell" || node.type.name === "tableRow") && typeof node.attrs.trailingXml === "string";
76
+ }
77
+ function carrierBoundaryIn(doc, from, to) {
78
+ let found = false;
79
+ doc.nodesBetween(from, to, (node, pos) => {
80
+ if (hasMarkerAttrs(node) && (from <= pos || to >= pos + node.nodeSize)) {
81
+ found = true;
82
+ }
83
+ return !found;
84
+ });
85
+ return found;
86
+ }
87
+ function markerAttrsReached(tr) {
88
+ return tr.steps.some((step, index) => {
89
+ const before = tr.docs[index];
90
+ const after = tr.docs[index + 1] ?? tr.doc;
91
+ if (step instanceof AttrStep || step instanceof AddNodeMarkStep || step instanceof RemoveNodeMarkStep) {
92
+ const old = before.nodeAt(step.pos);
93
+ const next = after.nodeAt(step.pos);
94
+ return old !== null && hasMarkerAttrs(old) || next !== null && hasMarkerAttrs(next);
95
+ }
96
+ if (step instanceof AddMarkStep || step instanceof RemoveMarkStep)
97
+ return false;
98
+ if (!(step instanceof ReplaceStep || step instanceof ReplaceAroundStep))
99
+ return true;
100
+ let reached = false;
101
+ step.getMap().forEach((oldStart, oldEnd, newStart, newEnd) => {
102
+ reached ||= carrierBoundaryIn(before, oldStart, oldEnd) || carrierBoundaryIn(after, newStart, newEnd);
103
+ });
104
+ return reached;
105
+ });
106
+ }
107
+ function preservedFragments(doc) {
108
+ const found = [];
109
+ visitPreservedFragments(doc, (node, _pos, xml, attr) => {
110
+ if (attr !== void 0 && xml !== null) {
111
+ found.push(
112
+ ...markerFragments(xml).map((fragment) => `marker:${fragment}`)
113
+ );
114
+ } else if (isGuardedFragment(node)) {
115
+ found.push(preservedSignature(node));
116
+ }
117
+ });
118
+ return found;
119
+ }
120
+ var preservedGuard = {
121
+ name: "preserved",
122
+ change: (tr) => !(transactionReaches(tr, isGuardedFragment) || markerAttrsReached(tr)) || same(preservedFragments(tr.before), preservedFragments(tr.doc)),
123
+ shuts: (intent, state) => {
124
+ if (guardedNodes.shuts(intent, state)) return true;
125
+ if (intent.kind !== "replace") return false;
126
+ let shut = false;
127
+ state.doc.nodesBetween(intent.from, intent.to, (node, pos) => {
128
+ if (intent.from > pos && intent.to < pos + node.nodeSize) return true;
129
+ if (!hasMarkerAttrs(node)) return true;
130
+ for (const attr of ["leadingXml", "trailingXml"]) {
131
+ const xml = node.attrs[attr];
132
+ if (typeof xml === "string" && markerFragments(xml).length > 0)
133
+ shut = true;
134
+ }
135
+ return !shut;
136
+ });
137
+ return shut;
138
+ }
139
+ };
39
140
  function isNoteReference(node) {
40
141
  return node.type.name === "noteReference";
41
142
  }
@@ -73,8 +174,8 @@ var sectionGuard = {
73
174
  }
74
175
  };
75
176
  export {
76
- bookmarkGuard,
77
177
  noteGuard,
178
+ preservedGuard,
78
179
  preservedNodeGuard,
79
180
  sectionGuard
80
181
  };
@@ -44,6 +44,11 @@ export declare function withoutComments(node: PMNode): PMNode;
44
44
  * The comparison is by source (`./sourceEquality`): a display value worked out again beside a
45
45
  * comment is no content change, so it neither turns a comment edit into a body edit nor takes the
46
46
  * ownership question off it.
47
+ *
48
+ * What a comment says is a story on the document node (`./stories`), so the comment stories come
49
+ * off before the markers do; without that, writing one of them would move the document node and
50
+ * every comment edit would read as a body edit. The footnote and header stories stay: rewriting
51
+ * one of those is a change to what the document says, and a comment protection turns it down.
47
52
  */
48
53
  export declare function changesOnlyComments(before: PMNode, after: PMNode): boolean;
49
54
  /**
@@ -82,9 +87,33 @@ export declare function commentEditsOwned(before: PMNode, after: PMNode, rules:
82
87
  */
83
88
  export declare function protectionAllows(before: PMNode, after: PMNode, rules: ProtectionState): boolean;
84
89
  /**
85
- * Whether every comment and reply that appeared between the two documents was written under this
86
- * identity, and every one that was already there kept the identity it was written under. The
87
- * editor takes an addition from anyone, since it writes the author itself; a server taking a file
88
- * back does not, since the file could claim any author.
90
+ * The display names these comment bodies write under while recording nobody for them.
91
+ *
92
+ * A name here is one no comment in the file can be attributed through, so a comment that appears
93
+ * under it names no more of a person than the ones already there do.
94
+ */
95
+ export declare function unattributedCommentAuthors(bodies: Iterable<{
96
+ author: string | null;
97
+ authorId: string | null;
98
+ }>): ReadonlySet<string>;
99
+ /**
100
+ * Whether a comment or reply that appeared is one this author was in a position to write.
101
+ *
102
+ * One carrying this identity is, since a file can claim any author and only the requester's own
103
+ * identity vouches for itself. So is one carrying no identity at all under a name the original
104
+ * already holds unattributed: the writer records no person for such a name, precisely so that the
105
+ * comments already written under it are not handed to whoever comments next (`docx/comments/people`),
106
+ * and a submission the writer produced has to be one the verifier takes. Such a comment claims
107
+ * nobody and stays everyone's to edit, and the name it shows was already in the file.
108
+ */
109
+ export declare function commentAdditionAllowed(added: {
110
+ author: string | null;
111
+ authorId: string | null;
112
+ }, authorId: string, unattributed: ReadonlySet<string>): boolean;
113
+ /**
114
+ * Whether every comment and reply that appeared between the two documents was one this identity
115
+ * could have written, and every one that was already there kept the identity it was written under.
116
+ * The editor takes an addition from anyone, since it writes the author itself; a server taking a
117
+ * file back does not, since the file could claim any author.
89
118
  */
90
- export declare function commentAdditionsBy(before: PMNode, after: PMNode, authorId: string): boolean;
119
+ export declare function commentAdditionsBy(before: PMNode, after: PMNode, authorId: string, unattributed?: ReadonlySet<string>): boolean;
@@ -1,6 +1,12 @@
1
1
  // src/schema/protection.ts
2
2
  import { Fragment } from "prosemirror-model";
3
3
  import { sameSource } from "./sourceEquality.js";
4
+ import {
5
+ sameStory,
6
+ storyKey,
7
+ storyNodeOf,
8
+ withoutCommentStories
9
+ } from "./stories.js";
4
10
  var COMMENT_NODES = /* @__PURE__ */ new Set([
5
11
  "commentStart",
6
12
  "commentEnd",
@@ -18,21 +24,25 @@ function withoutComments(node) {
18
24
  return node.copy(Fragment.fromArray(kept));
19
25
  }
20
26
  function changesOnlyComments(before, after) {
21
- return sameSource(withoutComments(before), withoutComments(after));
27
+ return sameSource(
28
+ withoutComments(withoutCommentStories(before)),
29
+ withoutComments(withoutCommentStories(after))
30
+ );
22
31
  }
23
32
  function stringOrNull(value) {
24
33
  return typeof value === "string" ? value : null;
25
34
  }
26
- function bodyOf(attrs) {
35
+ function bodyOf(attrs, doc) {
36
+ const id = stringOrNull(attrs.id);
27
37
  return {
28
38
  authorId: stringOrNull(attrs.authorId),
29
39
  author: stringOrNull(attrs.author),
30
40
  initials: stringOrNull(attrs.initials),
31
41
  date: stringOrNull(attrs.date),
32
- text: stringOrNull(attrs.text) ?? ""
42
+ body: id === null ? null : storyNodeOf(doc, storyKey("comment", id))
33
43
  };
34
44
  }
35
- function replyBodies(value) {
45
+ function replyBodies(value, doc) {
36
46
  const replies = /* @__PURE__ */ new Map();
37
47
  if (!Array.isArray(value)) return replies;
38
48
  for (const entry of value) {
@@ -40,7 +50,7 @@ function replyBodies(value) {
40
50
  const reply = entry;
41
51
  const id = stringOrNull(reply.id);
42
52
  if (id === null) continue;
43
- replies.set(id, bodyOf(reply));
53
+ replies.set(id, bodyOf(reply, doc));
44
54
  }
45
55
  return replies;
46
56
  }
@@ -51,8 +61,8 @@ function threadsIn(doc) {
51
61
  const id = stringOrNull(node.attrs.id);
52
62
  if (id === null || threads.has(id)) return true;
53
63
  threads.set(id, {
54
- ...bodyOf(node.attrs),
55
- replies: replyBodies(node.attrs.replies)
64
+ ...bodyOf(node.attrs, doc),
65
+ replies: replyBodies(node.attrs.replies, doc)
56
66
  });
57
67
  return true;
58
68
  });
@@ -90,7 +100,7 @@ function sameIdentity(was, now) {
90
100
  return was.authorId === now.authorId && was.author === now.author;
91
101
  }
92
102
  function sameWords(was, now) {
93
- return was.text === now.text && was.initials === now.initials && was.date === now.date;
103
+ return sameStory(was.body, now.body) && was.initials === now.initials && was.date === now.date;
94
104
  }
95
105
  function commentIdentitiesKept(before, after) {
96
106
  const now = threadsIn(after);
@@ -140,33 +150,51 @@ function protectionAllows(before, after, rules) {
140
150
  }
141
151
  }
142
152
  }
143
- function commentAdditionsBy(before, after, authorId) {
153
+ function unattributedCommentAuthors(bodies) {
154
+ const names = /* @__PURE__ */ new Set();
155
+ for (const body of bodies) {
156
+ if (body.authorId === null && body.author !== null) names.add(body.author);
157
+ }
158
+ return names;
159
+ }
160
+ function commentAdditionAllowed(added, authorId, unattributed) {
161
+ if (added.authorId !== null) return added.authorId === authorId;
162
+ return added.author !== null && unattributed.has(added.author);
163
+ }
164
+ function commentAdditionsBy(before, after, authorId, unattributed) {
144
165
  if (!commentIdentitiesKept(before, after)) return false;
145
166
  const was = threadsIn(before);
167
+ const names = unattributed ?? unattributedCommentAuthors(
168
+ Array.from(was.values()).flatMap((thread) => [
169
+ thread,
170
+ ...thread.replies.values()
171
+ ])
172
+ );
173
+ const allowed = (added) => commentAdditionAllowed(added, authorId, names);
146
174
  for (const [id, thread] of threadsIn(after)) {
147
175
  const earlier = was.get(id);
148
176
  if (earlier === void 0) {
149
- if (thread.authorId !== authorId) return false;
177
+ if (!allowed(thread)) return false;
150
178
  for (const reply of thread.replies.values()) {
151
- if (reply.authorId !== authorId) return false;
179
+ if (!allowed(reply)) return false;
152
180
  }
153
181
  continue;
154
182
  }
155
183
  for (const [replyId, reply] of thread.replies) {
156
- if (!earlier.replies.has(replyId) && reply.authorId !== authorId) {
157
- return false;
158
- }
184
+ if (!earlier.replies.has(replyId) && !allowed(reply)) return false;
159
185
  }
160
186
  }
161
187
  return true;
162
188
  }
163
189
  export {
164
190
  changesOnlyComments,
191
+ commentAdditionAllowed,
165
192
  commentAdditionsBy,
166
193
  commentEditsOwned,
167
194
  commentIdentitiesKept,
168
195
  commentOwned,
169
196
  isCommentNode,
170
197
  protectionAllows,
198
+ unattributedCommentAuthors,
171
199
  withoutComments
172
200
  };
@@ -20,7 +20,9 @@ function runMarkSpec(attrs, fontFallbacks) {
20
20
  lang: format?.lang,
21
21
  "data-rattrs": text(attrs.rAttrs),
22
22
  "data-rpr": text(attrs.rPr),
23
- "data-fmt": formatJson(format)
23
+ "data-fmt": formatJson(format),
24
+ // Read by `styles/editor.css` to stand a link's own line down
25
+ "data-underline": format?.underline && format.underline !== "none" ? "" : void 0
24
26
  },
25
27
  0
26
28
  ];
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The side stories a document holds beside its body: a comment's text, a footnote's, a header's.
3
+ *
4
+ * Each is a document of this same schema, held on the document node under the key that names it,
5
+ * so an edit to one rides a transaction and lands in the history the way a body edit does. What
6
+ * turns one into XML and back is `docx/story`; here is only what the schema layer needs, which is
7
+ * the key, the value on the node, and the two comparisons the protection rules are written over.
8
+ */
9
+ import type { Node as PMNode } from "prosemirror-model";
10
+ export type StoryKind = "comment" | "footnote" | "endnote" | "header" | "footer";
11
+ /** `comment:4`, `footnote:2`, `header:word/header1.xml` */
12
+ export type StoryKey = `${StoryKind}:${string}`;
13
+ /** One story as the document node holds it, which is `Node.toJSON` of a document of this schema */
14
+ export interface StoryJson {
15
+ readonly type: string;
16
+ readonly attrs?: Readonly<Record<string, unknown>>;
17
+ readonly content?: readonly unknown[];
18
+ }
19
+ /** The attr the stories stand on, named here so the schema and the readers cannot spell it apart */
20
+ export declare const STORIES_ATTR = "stories";
21
+ export declare function storyKey(kind: StoryKind, id: string): StoryKey;
22
+ /**
23
+ * The key this text spells, or null for one that names no kind of story.
24
+ *
25
+ * An id carries colons of its own - a header is named by its part path - so the kind is read off
26
+ * the front and everything after the first colon is the id.
27
+ */
28
+ export declare function asStoryKey(text: string): StoryKey | null;
29
+ /** The stories this document holds, under the key naming each */
30
+ export declare function storiesOf(doc: PMNode): Readonly<Record<string, StoryJson>>;
31
+ /** The story this key names as a document node, and null where the document holds none */
32
+ export declare function storyNodeOf(doc: PMNode, key: StoryKey): PMNode | null;
33
+ /**
34
+ * Whether the two stories say the same thing.
35
+ *
36
+ * The block keys are left out of it, because they name the document each story was read from and
37
+ * two files opened side by side never share one. Everything else a block would be written back as
38
+ * is compared (`./sourceEquality`), so a body that gained a bold run reads as changed and one
39
+ * whose display values were worked out again does not.
40
+ */
41
+ export declare function sameStory(a: PMNode | null, b: PMNode | null): boolean;
42
+ /**
43
+ * The document with its comment stories taken out of it, replies included.
44
+ *
45
+ * A comment edit now moves the document node, and a comparison that could not see past that would
46
+ * call every comment edit a body edit. The footnote and header stories stay: changing one of them
47
+ * is a change to what the document says, and a comment protection is meant to turn it down.
48
+ */
49
+ export declare function withoutCommentStories(doc: PMNode): PMNode;
@@ -0,0 +1,78 @@
1
+ // src/schema/stories.ts
2
+ import { sameSource } from "./sourceEquality.js";
3
+ var STORIES_ATTR = "stories";
4
+ var STORY_KINDS = [
5
+ "comment",
6
+ "footnote",
7
+ "endnote",
8
+ "header",
9
+ "footer"
10
+ ];
11
+ function storyKey(kind, id) {
12
+ return `${kind}:${id}`;
13
+ }
14
+ function asStoryKey(text) {
15
+ const at = text.indexOf(":");
16
+ if (at < 1) return null;
17
+ const named = text.slice(0, at);
18
+ const kind = STORY_KINDS.find((candidate) => candidate === named);
19
+ return kind === void 0 ? null : storyKey(kind, text.slice(at + 1));
20
+ }
21
+ function isStoryJson(value) {
22
+ return typeof value === "object" && value !== null && typeof Reflect.get(value, "type") === "string";
23
+ }
24
+ function storiesOf(doc) {
25
+ const held = doc.attrs[STORIES_ATTR];
26
+ if (typeof held !== "object" || held === null) return {};
27
+ const stories = {};
28
+ for (const [key, value] of Object.entries(held)) {
29
+ if (asStoryKey(key) !== null && isStoryJson(value)) stories[key] = value;
30
+ }
31
+ return stories;
32
+ }
33
+ var built = /* @__PURE__ */ new WeakMap();
34
+ function storyNodeOf(doc, key) {
35
+ const json = storiesOf(doc)[key];
36
+ if (json === void 0) return null;
37
+ const cached = built.get(json);
38
+ if (cached) return cached;
39
+ const node = doc.type.schema.nodeFromJSON(json);
40
+ built.set(json, node);
41
+ return node;
42
+ }
43
+ var unkeyed = /* @__PURE__ */ new WeakMap();
44
+ function withoutBlockKeys(node) {
45
+ if (node.isText) return node;
46
+ const cached = unkeyed.get(node);
47
+ if (cached) return cached;
48
+ const children = node.children.map(withoutBlockKeys);
49
+ const attrs = node.attrs.srcId === null || node.attrs.srcId === void 0 ? node.attrs : { ...node.attrs, srcId: null };
50
+ const stripped = attrs === node.attrs && children.every((child, at) => child === node.child(at)) ? node : node.type.create(attrs, children, node.marks);
51
+ unkeyed.set(node, stripped);
52
+ return stripped;
53
+ }
54
+ function sameStory(a, b) {
55
+ if (a === null || b === null) return a === b;
56
+ return sameSource(withoutBlockKeys(a), withoutBlockKeys(b));
57
+ }
58
+ function withoutCommentStories(doc) {
59
+ const stories = storiesOf(doc);
60
+ const kept = Object.fromEntries(
61
+ Object.entries(stories).filter(([key]) => !key.startsWith("comment:"))
62
+ );
63
+ if (Object.keys(kept).length === Object.keys(stories).length) return doc;
64
+ return doc.type.create(
65
+ { ...doc.attrs, [STORIES_ATTR]: kept },
66
+ doc.content,
67
+ doc.marks
68
+ );
69
+ }
70
+ export {
71
+ STORIES_ATTR,
72
+ asStoryKey,
73
+ sameStory,
74
+ storiesOf,
75
+ storyKey,
76
+ storyNodeOf,
77
+ withoutCommentStories
78
+ };
@@ -46,13 +46,11 @@ export declare const editorClassNames: {
46
46
  readonly commentMarker: "docx-editor-comment-marker";
47
47
  /** A superscript footnote or endnote reference */
48
48
  readonly noteReference: "docx-editor-note-reference";
49
- /** An invisible bookmark marker that occurs between body blocks */
50
- readonly bookmarkBlock: "docx-editor-bookmark-block";
51
49
  readonly rawInline: "docx-editor-raw-inline";
50
+ /** A preserved child of a run, drawn by the `display` its policy gave it */
51
+ readonly rawRunContent: "docx-editor-raw-run";
52
+ /** A preserved block, drawn by the `display` its policy gave it */
52
53
  readonly rawBlock: "docx-editor-raw-block";
53
- /** A preserved block that carries the original XML directly (inside a table cell, and so on) */
54
- readonly rawXmlBlock: "docx-editor-raw-xml";
55
- readonly tablePlaceholder: "docx-editor-table";
56
54
  /** The box holding both the paper and the marks overlaid on it */
57
55
  readonly pageLayer: "docx-editor-page-layer";
58
56
  /** All of the page marks drawn over the paper */
@@ -41,13 +41,11 @@ var editorClassNames = {
41
41
  commentMarker: `${PREFIX}-comment-marker`,
42
42
  /** A superscript footnote or endnote reference */
43
43
  noteReference: `${PREFIX}-note-reference`,
44
- /** An invisible bookmark marker that occurs between body blocks */
45
- bookmarkBlock: `${PREFIX}-bookmark-block`,
46
44
  rawInline: `${PREFIX}-raw-inline`,
45
+ /** A preserved child of a run, drawn by the `display` its policy gave it */
46
+ rawRunContent: `${PREFIX}-raw-run`,
47
+ /** A preserved block, drawn by the `display` its policy gave it */
47
48
  rawBlock: `${PREFIX}-raw-block`,
48
- /** A preserved block that carries the original XML directly (inside a table cell, and so on) */
49
- rawXmlBlock: `${PREFIX}-raw-xml`,
50
- tablePlaceholder: `${PREFIX}-table`,
51
49
  /** The box holding both the paper and the marks overlaid on it */
52
50
  pageLayer: `${PREFIX}-page-layer`,
53
51
  /** All of the page marks drawn over the paper */
package/dist/styles.css CHANGED
@@ -62,11 +62,6 @@
62
62
  overflow: hidden;
63
63
  }
64
64
 
65
- /* A body-level bookmark occupies a document-model position but draws no placeholder. */
66
- .docx-editor-bookmark-block {
67
- display: none;
68
- }
69
-
70
65
  .docx-editor-comment-range {
71
66
  background: #fff2a8;
72
67
  box-shadow: inset 0 -2px #e2b714;
@@ -588,10 +583,12 @@ button.docx-editor-comment-body {
588
583
  * is the same paper `A4_PORTRAIT` names in `docx/pageGeometry.ts`.
589
584
  *
590
585
  * The height is used only as a minimum, so that even short text feels like a full
591
- * sheet and longer text simply grows it.
592
- * The body left inside these margins is the width of a new table and the height
593
- * between page boundaries; `page/pageLayout.ts` works both out from the very same
594
- * geometry, so the divider on screen lands where the sheet actually ends.
586
+ * sheet and longer text simply grows it, and the editor supplies one grown to the
587
+ * last page's own bottom margin where a later section's is deeper than this one's.
588
+ * The body left inside these margins is what the sheet itself is drawn at. The width
589
+ * of a new table and the height between two page boundaries come from the paper of
590
+ * the section they stand in, which `page/pageLayout.ts` works out from the very same
591
+ * kind of geometry, so the divider on screen lands where that page actually ends.
595
592
  */
596
593
  --docx-editor-page-width: 21cm;
597
594
  --docx-editor-page-height: 29.7cm;
@@ -677,7 +674,10 @@ button.docx-editor-comment-body {
677
674
  border-top: 1px dotted rgb(26 79 208 / 40%);
678
675
  }
679
676
 
680
- /* Display-only projections of the first section's header and footer stories. */
677
+ /*
678
+ * Display-only projections of the header and footer stories of the section each page
679
+ * belongs to (`page/PageGuides.tsx`).
680
+ */
681
681
  .docx-editor-page-header,
682
682
  .docx-editor-page-footer {
683
683
  position: absolute;
@@ -928,12 +928,21 @@ button.docx-editor-comment-body {
928
928
  * held as data rather than as an `href`, so a click here places the caret the way a click on any
929
929
  * other text does, and opening the address is the link panel's business.
930
930
  * A color the run itself carries paints over this one, the same as any other run formatting.
931
+ * The line is a border rather than an underline so that it runs on across a tab, which a text
932
+ * decoration is not painted over.
931
933
  */
932
934
  .docx-editor-link {
933
935
  color: #1a56db;
934
936
  border-bottom: 1px solid currentcolor;
935
937
  }
936
938
 
939
+ /* A word processor underlines a link through the `Hyperlink` style, so the line above would be a second one */
940
+ .docx-editor-link:has([data-underline]):not(
941
+ :has(.docx-editor-run:not([data-underline]))
942
+ ) {
943
+ border-bottom: none;
944
+ }
945
+
937
946
  /*
938
947
  * A content control that says its contents may not be edited (`editor/plugins/lockedContent` refuses
939
948
  * the edits). Yellow is what the contract templates already paint their settled parts with.
@@ -957,15 +966,63 @@ button.docx-editor-comment-body {
957
966
  cursor: not-allowed;
958
967
  }
959
968
 
969
+ /*
970
+ * A preserved fragment inside a paragraph or inside a run. What it draws is the `display` the
971
+ * preservation rule gave it (`docx/importPolicy`), so the rule is read here as an attribute
972
+ * rather than as a second list of element names.
973
+ */
974
+ .docx-editor-raw-inline,
975
+ .docx-editor-raw-run {
976
+ vertical-align: baseline;
977
+ }
978
+
960
979
  /* Elements that, like bookmarks, must not be visible */
961
- .docx-editor-raw-inline {
980
+ .docx-editor-raw-inline[data-display="hidden"],
981
+ .docx-editor-raw-run[data-display="hidden"] {
962
982
  display: inline-block;
963
983
  width: 0;
964
984
  overflow: hidden;
985
+ }
986
+
987
+ /* A character the file stands for and the editor cannot type, drawn as itself */
988
+ .docx-editor-raw-inline[data-display="text"],
989
+ .docx-editor-raw-run[data-display="text"] {
990
+ white-space: pre;
991
+ }
992
+
993
+ /*
994
+ * Content carried through whole that the editor has no model for: a field, a tracked change, a
995
+ * symbol. Small enough to read the line around it, outlined so it is plainly not ordinary text.
996
+ *
997
+ * Deliberately not `user-select: none`. A chip that carries its content whole can be selected and
998
+ * deleted whole, and taking it out of the selection would leave a reader dragging across it with
999
+ * no way to reach it.
1000
+ */
1001
+ .docx-editor-raw-inline[data-display="chip"],
1002
+ .docx-editor-raw-run[data-display="chip"] {
1003
+ display: inline-block;
1004
+ max-width: 16em;
1005
+ overflow: hidden;
1006
+ padding: 0 3px;
1007
+ border: 1px dashed #9a9a9a;
1008
+ border-radius: 3px;
1009
+ background: #f2f2f2;
1010
+ color: #595959;
1011
+ font-size: 0.85em;
1012
+ font-family: system-ui, sans-serif;
1013
+ line-height: 1.4;
1014
+ white-space: nowrap;
1015
+ text-overflow: ellipsis;
965
1016
  vertical-align: baseline;
966
1017
  }
967
1018
 
968
- .docx-editor-table,
1019
+ /* The chip while it is the selection, which is how a reader sees what a delete would take */
1020
+ .docx-editor-raw-inline[data-display="chip"].ProseMirror-selectednode,
1021
+ .docx-editor-raw-run[data-display="chip"].ProseMirror-selectednode {
1022
+ outline: 2px solid #1a73e8;
1023
+ outline-offset: 1px;
1024
+ }
1025
+
969
1026
  .docx-editor-raw-block {
970
1027
  margin: 8pt 0;
971
1028
  padding: 10pt 12pt;
@@ -978,6 +1035,11 @@ button.docx-editor-comment-body {
978
1035
  user-select: none;
979
1036
  }
980
1037
 
1038
+ /* A range marker holds a spot between blocks and stands for nothing on the page. */
1039
+ .docx-editor-raw-block[data-display="hidden"] {
1040
+ display: none;
1041
+ }
1042
+
981
1043
  /*
982
1044
  * The toolbar.
983
1045
  *
@@ -29,7 +29,8 @@ export interface ColumnEdge {
29
29
  /**
30
30
  * The limit a table can be widened to (dxa).
31
31
  *
32
- * The body width of the paper the document names, minus the table indent (`w:tblInd`).
32
+ * The body width of the paper handed in, which is the section's the table stands in, minus
33
+ * the table indent (`w:tblInd`).
33
34
  * For a table already stored wider than that, its current width is the limit, so
34
35
  * dragging can only narrow it.
35
36
  */
@@ -49,7 +50,8 @@ export declare function resizedGridCols(gridCols: number[], col: number, width:
49
50
  * Because it is one single transaction, a single undo returns everything to the state
50
51
  * before the drag.
51
52
  *
52
- * The geometry is the paper the open document names, which is what the table is kept
53
- * inside. The editor layer reads it off the state; this layer is handed it.
53
+ * The geometry is the paper of the section the table stands in, which is what the table is
54
+ * kept inside. The editor layer reads it off the state (`editor/documentStyles`); this layer
55
+ * is handed it.
54
56
  */
55
57
  export declare function buildResizeColumnTransaction(state: EditorState, edge: ColumnEdge, geometry: PageGeometry): Transaction | null;