@portone/docx-editor 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/CHANGELOG.md +205 -0
  2. package/assets/editor.png +0 -0
  3. package/dist/DocxEditor.d.ts +1 -1
  4. package/dist/DocxEditor.js +45 -25
  5. package/dist/core.d.ts +1 -1
  6. package/dist/docx/cloning.js +4 -9
  7. package/dist/docx/commentOnlyChange.d.ts +5 -3
  8. package/dist/docx/comments/grammar.d.ts +27 -10
  9. package/dist/docx/comments/grammar.js +18 -67
  10. package/dist/docx/comments/model.d.ts +5 -6
  11. package/dist/docx/comments/model.js +0 -9
  12. package/dist/docx/comments/parts.d.ts +16 -10
  13. package/dist/docx/comments/parts.js +65 -13
  14. package/dist/docx/comments/people.d.ts +11 -3
  15. package/dist/docx/comments/people.js +20 -6
  16. package/dist/docx/comments/policy.js +7 -1
  17. package/dist/docx/comments/reading.d.ts +4 -2
  18. package/dist/docx/comments/reading.js +1 -14
  19. package/dist/docx/comments/writing.js +78 -35
  20. package/dist/docx/exportDocx.js +30 -18
  21. package/dist/docx/exportRefs.d.ts +8 -3
  22. package/dist/docx/exportRefs.js +2 -1
  23. package/dist/docx/fidelity.js +24 -10
  24. package/dist/docx/fields.d.ts +42 -0
  25. package/dist/docx/fields.js +76 -0
  26. package/dist/docx/formatting/attrs.d.ts +20 -2
  27. package/dist/docx/formatting/attrs.js +57 -2
  28. package/dist/docx/formatting/context.d.ts +10 -2
  29. package/dist/docx/formatting/context.js +23 -5
  30. package/dist/docx/formatting/resolve.d.ts +10 -4
  31. package/dist/docx/formatting/resolve.js +31 -11
  32. package/dist/docx/formatting/styles.d.ts +25 -2
  33. package/dist/docx/formatting/styles.js +84 -3
  34. package/dist/docx/headersFooters.d.ts +89 -20
  35. package/dist/docx/headersFooters.js +163 -172
  36. package/dist/docx/hyperlink.d.ts +14 -4
  37. package/dist/docx/hyperlink.js +4 -1
  38. package/dist/docx/importDocx.d.ts +1 -1
  39. package/dist/docx/importDocx.js +135 -74
  40. package/dist/docx/importParagraph.d.ts +16 -4
  41. package/dist/docx/importParagraph.js +87 -93
  42. package/dist/docx/importPolicy.d.ts +70 -0
  43. package/dist/docx/importPolicy.js +190 -0
  44. package/dist/docx/importPreserved.d.ts +30 -0
  45. package/dist/docx/importPreserved.js +54 -0
  46. package/dist/docx/importTable.d.ts +2 -2
  47. package/dist/docx/importTable.js +107 -66
  48. package/dist/docx/invariants.js +42 -31
  49. package/dist/docx/newLists.d.ts +30 -0
  50. package/dist/docx/newLists.js +24 -1
  51. package/dist/docx/notes.d.ts +6 -1
  52. package/dist/docx/notes.js +6 -16
  53. package/dist/docx/numberingPlanner.d.ts +6 -3
  54. package/dist/docx/numberingPlanner.js +38 -7
  55. package/dist/docx/pageGeometry.d.ts +5 -14
  56. package/dist/docx/pageGeometry.js +0 -12
  57. package/dist/docx/protectionPolicy.d.ts +13 -5
  58. package/dist/docx/protectionPolicy.js +32 -22
  59. package/dist/docx/scan.d.ts +18 -8
  60. package/dist/docx/scan.js +17 -11
  61. package/dist/docx/sections.d.ts +126 -0
  62. package/dist/docx/sections.js +207 -0
  63. package/dist/docx/serializeBlock.d.ts +13 -4
  64. package/dist/docx/serializeBlock.js +16 -18
  65. package/dist/docx/serializeParagraph.d.ts +4 -0
  66. package/dist/docx/serializeParagraph.js +1 -0
  67. package/dist/docx/serializePreserved.d.ts +14 -0
  68. package/dist/docx/serializePreserved.js +24 -0
  69. package/dist/docx/serializeStory.d.ts +17 -0
  70. package/dist/docx/serializeStory.js +17 -0
  71. package/dist/docx/serializeTable.js +10 -13
  72. package/dist/docx/session.d.ts +24 -7
  73. package/dist/docx/session.js +37 -6
  74. package/dist/docx/story.d.ts +140 -0
  75. package/dist/docx/story.js +237 -0
  76. package/dist/docx/storyProjection.js +6 -1
  77. package/dist/docx/tableFormatting/conditions.d.ts +76 -0
  78. package/dist/docx/tableFormatting/conditions.js +196 -0
  79. package/dist/docx/tableFormatting/editing.d.ts +7 -3
  80. package/dist/docx/tableFormatting/editing.js +4 -5
  81. package/dist/docx/tableFormatting/reading.d.ts +49 -19
  82. package/dist/docx/tableFormatting/reading.js +109 -35
  83. package/dist/docx/tableFormatting.d.ts +1 -0
  84. package/dist/docx/tableFormatting.js +1 -0
  85. package/dist/docx/tableTemplate.js +12 -12
  86. package/dist/editor/clipboard/htmlReader.d.ts +30 -0
  87. package/dist/editor/clipboard/htmlReader.js +302 -0
  88. package/dist/editor/clipboard/internalChannel.d.ts +57 -0
  89. package/dist/editor/clipboard/internalChannel.js +58 -0
  90. package/dist/editor/clipboard/normalizers.d.ts +99 -0
  91. package/dist/editor/clipboard/normalizers.js +199 -0
  92. package/dist/editor/clipboard/parser.d.ts +34 -0
  93. package/dist/editor/clipboard/parser.js +58 -0
  94. package/dist/editor/clipboard/plugin.d.ts +21 -0
  95. package/dist/editor/clipboard/plugin.js +320 -0
  96. package/dist/editor/clipboard/readContext.d.ts +24 -0
  97. package/dist/editor/clipboard/readContext.js +19 -0
  98. package/dist/editor/clipboard/readers.d.ts +33 -0
  99. package/dist/editor/clipboard/readers.js +37 -0
  100. package/dist/editor/commands/comments/editing.d.ts +26 -6
  101. package/dist/editor/commands/comments/editing.js +175 -105
  102. package/dist/editor/commands/comments/model.d.ts +34 -17
  103. package/dist/editor/commands/comments/model.js +11 -0
  104. package/dist/editor/commands/comments/reading.d.ts +5 -3
  105. package/dist/editor/commands/comments/reading.js +8 -58
  106. package/dist/editor/commands/formatting/propertyCommands.js +6 -1
  107. package/dist/editor/commands/index.d.ts +2 -2
  108. package/dist/editor/commands/index.js +2 -0
  109. package/dist/editor/commands/listCommands.d.ts +12 -3
  110. package/dist/editor/commands/listCommands.js +76 -30
  111. package/dist/editor/commands/noteQueries.d.ts +22 -5
  112. package/dist/editor/commands/noteQueries.js +34 -6
  113. package/dist/editor/commands/paragraphCommands.js +6 -1
  114. package/dist/editor/createEditor.js +14 -4
  115. package/dist/editor/documentStyles.d.ts +26 -4
  116. package/dist/editor/documentStyles.js +9 -4
  117. package/dist/editor/editorDocument.d.ts +7 -6
  118. package/dist/editor/editorDocument.js +18 -6
  119. package/dist/editor/imageFiles.d.ts +2 -2
  120. package/dist/editor/imageFiles.js +2 -0
  121. package/dist/editor/insertTable.d.ts +1 -1
  122. package/dist/editor/insertTable.js +2 -2
  123. package/dist/editor/paragraphEdits.d.ts +16 -1
  124. package/dist/editor/paragraphEdits.js +13 -5
  125. package/dist/editor/paragraphPlacement.d.ts +11 -0
  126. package/dist/editor/paragraphPlacement.js +18 -0
  127. package/dist/editor/plainText.d.ts +12 -0
  128. package/dist/editor/plainText.js +2 -1
  129. package/dist/editor/plugins/columnResize.js +6 -3
  130. package/dist/editor/plugins/commentComposer.d.ts +28 -0
  131. package/dist/editor/plugins/commentComposer.js +46 -0
  132. package/dist/editor/plugins/commentDecorations.d.ts +29 -2
  133. package/dist/editor/plugins/commentDecorations.js +89 -15
  134. package/dist/editor/plugins/documentProjection.d.ts +31 -0
  135. package/dist/editor/plugins/documentProjection.js +22 -0
  136. package/dist/editor/plugins/imagePaste.js +20 -10
  137. package/dist/editor/plugins/linkPanel.d.ts +7 -3
  138. package/dist/editor/plugins/linkPanel.js +13 -27
  139. package/dist/editor/plugins/numberingDecorations.d.ts +10 -4
  140. package/dist/editor/plugins/numberingDecorations.js +43 -2
  141. package/dist/editor/plugins/panelState.d.ts +67 -0
  142. package/dist/editor/plugins/panelState.js +54 -0
  143. package/dist/editor/plugins/paragraphDisplay.js +9 -3
  144. package/dist/editor/plugins/tableContextMenu.d.ts +1 -1
  145. package/dist/editor/plugins/tableContextMenu.js +30 -47
  146. package/dist/editor/plugins/tableDisplay.js +19 -5
  147. package/dist/editor/plugins/textContextMenu.d.ts +1 -1
  148. package/dist/editor/plugins/textContextMenu.js +30 -47
  149. package/dist/model/format.d.ts +50 -0
  150. package/dist/model/format.js +65 -0
  151. package/dist/numbering/listRegistry.d.ts +37 -0
  152. package/dist/numbering/listRegistry.js +112 -0
  153. package/dist/numbering/listTemplate.d.ts +26 -17
  154. package/dist/numbering/listTemplate.js +66 -20
  155. package/dist/numbering/markers.d.ts +15 -6
  156. package/dist/numbering/markers.js +26 -59
  157. package/dist/numbering/parseNumbering.d.ts +70 -5
  158. package/dist/numbering/parseNumbering.js +100 -26
  159. package/dist/numbering/spellers.d.ts +15 -0
  160. package/dist/numbering/spellers.js +96 -0
  161. package/dist/numbering/writeNumbering.d.ts +5 -3
  162. package/dist/numbering/writeNumbering.js +15 -15
  163. package/dist/ooxml/conformance.d.ts +28 -0
  164. package/dist/ooxml/conformance.js +20 -0
  165. package/dist/ooxml/errors.d.ts +5 -5
  166. package/dist/ooxml/fragment.d.ts +11 -0
  167. package/dist/ooxml/fragment.js +14 -4
  168. package/dist/ooxml/image.d.ts +3 -4
  169. package/dist/ooxml/image.js +50 -5
  170. package/dist/ooxml/names.d.ts +15 -0
  171. package/dist/ooxml/names.js +10 -1
  172. package/dist/ooxml/partSplice.d.ts +1 -1
  173. package/dist/ooxml/partSplice.js +10 -3
  174. package/dist/ooxml/props.d.ts +8 -0
  175. package/dist/ooxml/props.js +14 -4
  176. package/dist/ooxml/rangeMarkers.d.ts +5 -0
  177. package/dist/ooxml/rangeMarkers.js +24 -0
  178. package/dist/ooxml/xml.d.ts +8 -9
  179. package/dist/ooxml/xml.js +6 -6
  180. package/dist/page/PageGuides.d.ts +8 -4
  181. package/dist/page/PageGuides.js +13 -37
  182. package/dist/page/pageLayout.d.ts +62 -13
  183. package/dist/page/pageLayout.js +91 -37
  184. package/dist/page/usePageLayout.d.ts +14 -11
  185. package/dist/page/usePageLayout.js +21 -29
  186. package/dist/schema/attrRoles.js +44 -19
  187. package/dist/schema/docxSchema.d.ts +9 -1
  188. package/dist/schema/docxSchema.js +220 -139
  189. package/dist/schema/editGuard.d.ts +1 -1
  190. package/dist/schema/guards.js +2 -2
  191. package/dist/schema/index.d.ts +1 -0
  192. package/dist/schema/preservedFragments.d.ts +12 -0
  193. package/dist/schema/preservedFragments.js +38 -0
  194. package/dist/schema/preservedGuards.d.ts +9 -7
  195. package/dist/schema/preservedGuards.js +112 -11
  196. package/dist/schema/protection.d.ts +34 -5
  197. package/dist/schema/protection.js +42 -14
  198. package/dist/schema/rendering.js +3 -1
  199. package/dist/schema/stories.d.ts +49 -0
  200. package/dist/schema/stories.js +78 -0
  201. package/dist/styles/classNames.d.ts +16 -7
  202. package/dist/styles/classNames.js +16 -7
  203. package/dist/styles.css +90 -30
  204. package/dist/table/cellFormatting.js +1 -2
  205. package/dist/table/gridBorders.d.ts +11 -15
  206. package/dist/table/gridBorders.js +32 -18
  207. package/dist/table/resize.d.ts +5 -3
  208. package/dist/ui/CommentsPanel.d.ts +1 -3
  209. package/dist/ui/CommentsPanel.js +24 -15
  210. package/dist/ui/NotesPanel.js +2 -2
  211. package/dist/ui/TextMenu.d.ts +1 -3
  212. package/dist/ui/TextMenu.js +10 -6
  213. package/dist/ui/comments/CommentComposer.d.ts +6 -1
  214. package/dist/ui/comments/CommentComposer.js +26 -3
  215. package/package.json +1 -1
  216. package/dist/editor/externalClipboard.d.ts +0 -11
  217. package/dist/editor/externalClipboard.js +0 -456
@@ -1,11 +1,13 @@
1
1
  /**
2
- * The guards over what a document is opened with and the editor only preserves: the two ends of a
3
- * bookmark range, the reference standing where a footnote or an endnote is called, and the
4
- * paragraph that ends a section.
2
+ * The guards over what a document is opened with and the editor only preserves: the fragments a
3
+ * file falls apart without, the reference standing where a footnote or an endnote is called, and
4
+ * the paragraph that ends a section.
5
5
  *
6
- * None of it is content the editor writes, so nothing in it can put such a thing back once it is
7
- * gone, and a document that lost one of a bookmark's two ends cannot be written back as a file at
8
- * all (`docx/exportDocx` refuses it). What the first two guards hold is therefore the whole list of
6
+ * The first of those is the two ends of a bookmark range, the two ends of a permission or move
7
+ * range, and the pieces of a field, which say what they say only in the order they stand in. None
8
+ * of it is content the editor writes, so nothing in it can put such a thing back once it is gone,
9
+ * and a document that lost one of a bookmark's two ends cannot be written back as a file at all
10
+ * (`docx/exportDocx` refuses it). What the first two guards hold is therefore the whole list of
9
11
  * the nodes they answer for, in the order the document carries them; a section break, which an
10
12
  * edit may legitimately move from one paragraph to another, is held by its number instead.
11
13
  */
@@ -27,7 +29,7 @@ type Signature = (node: PMNode) => string;
27
29
  * was.
28
30
  */
29
31
  export declare function preservedNodeGuard(name: EditGuardName, holds: (node: PMNode) => boolean, signature: Signature): ChangeGuard;
30
- export declare const bookmarkGuard: ChangeGuard;
32
+ export declare const preservedGuard: ChangeGuard;
31
33
  export declare const noteGuard: ChangeGuard;
32
34
  /**
33
35
  * A guard that lets a change through only while it leaves at least as many section breaks standing
@@ -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 */
@@ -61,8 +59,6 @@ export declare const editorClassNames: {
61
59
  readonly pageSplit: "docx-editor-page-split";
62
60
  /** The line drawn where a block that cannot be pushed down crosses a page */
63
61
  readonly pageCrossed: "docx-editor-page-crossed";
64
- /** The page number laid on the top corner of a page */
65
- readonly pageBadge: "docx-editor-page-badge";
66
62
  /** A first-section header story projected into a visual page margin */
67
63
  readonly pageHeader: "docx-editor-page-header";
68
64
  /** A first-section footer story projected into a visual page margin */
@@ -165,6 +161,19 @@ export declare const editorCssVariables: {
165
161
  readonly lineHeight: "--docx-editor-line-height";
166
162
  /** The hanging indent width the list marker sits in */
167
163
  readonly markerWidth: "--docx-editor-marker-width";
164
+ /** The room kept after the list marker, which a level asking for no tab closes up (`w:suff`) */
165
+ readonly markerGap: "--docx-editor-marker-gap";
166
+ /** Where the marker sits inside the width kept for it (`w:lvlJc`) */
167
+ readonly markerAlign: "--docx-editor-marker-align";
168
+ /**
169
+ * The formatting the numbering level puts on its marker (`lvl/rPr`). It dresses the marker
170
+ * alone, so it reaches the pseudo-element that draws it and nothing the paragraph holds
171
+ */
172
+ readonly markerFontWeight: "--docx-editor-marker-font-weight";
173
+ readonly markerFontStyle: "--docx-editor-marker-font-style";
174
+ readonly markerColor: "--docx-editor-marker-color";
175
+ readonly markerFontSize: "--docx-editor-marker-font-size";
176
+ readonly markerFontFamily: "--docx-editor-marker-font-family";
168
177
  /** The width calculated for a tab using custom paragraph stops. */
169
178
  readonly tabWidth: "--docx-editor-tab-width";
170
179
  /** The paper height, grown to match the page count */
@@ -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 */
@@ -56,8 +54,6 @@ var editorClassNames = {
56
54
  pageSplit: `${PREFIX}-page-split`,
57
55
  /** The line drawn where a block that cannot be pushed down crosses a page */
58
56
  pageCrossed: `${PREFIX}-page-crossed`,
59
- /** The page number laid on the top corner of a page */
60
- pageBadge: `${PREFIX}-page-badge`,
61
57
  /** A first-section header story projected into a visual page margin */
62
58
  pageHeader: `${PREFIX}-page-header`,
63
59
  /** A first-section footer story projected into a visual page margin */
@@ -161,6 +157,19 @@ var editorCssVariables = {
161
157
  lineHeight: `--${PREFIX}-line-height`,
162
158
  /** The hanging indent width the list marker sits in */
163
159
  markerWidth: `--${PREFIX}-marker-width`,
160
+ /** The room kept after the list marker, which a level asking for no tab closes up (`w:suff`) */
161
+ markerGap: `--${PREFIX}-marker-gap`,
162
+ /** Where the marker sits inside the width kept for it (`w:lvlJc`) */
163
+ markerAlign: `--${PREFIX}-marker-align`,
164
+ /**
165
+ * The formatting the numbering level puts on its marker (`lvl/rPr`). It dresses the marker
166
+ * alone, so it reaches the pseudo-element that draws it and nothing the paragraph holds
167
+ */
168
+ markerFontWeight: `--${PREFIX}-marker-font-weight`,
169
+ markerFontStyle: `--${PREFIX}-marker-font-style`,
170
+ markerColor: `--${PREFIX}-marker-color`,
171
+ markerFontSize: `--${PREFIX}-marker-font-size`,
172
+ markerFontFamily: `--${PREFIX}-marker-font-family`,
164
173
  /** The width calculated for a tab using custom paragraph stops. */
165
174
  tabWidth: `--${PREFIX}-tab-width`,
166
175
  /** The paper height, grown to match the page count */