@portone/docx-editor 0.2.0 → 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 (101) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/CONTRIBUTING.md +4 -0
  3. package/dist/docx/commentOnlyChange.d.ts +3 -1
  4. package/dist/docx/commentOnlyChange.js +38 -21
  5. package/dist/docx/comments/constants.d.ts +3 -3
  6. package/dist/docx/comments/constants.js +4 -3
  7. package/dist/docx/comments/grammar.d.ts +80 -0
  8. package/dist/docx/comments/grammar.js +213 -0
  9. package/dist/docx/comments/people.js +12 -14
  10. package/dist/docx/comments/reading.d.ts +5 -3
  11. package/dist/docx/comments/reading.js +23 -20
  12. package/dist/docx/comments/verifying.d.ts +44 -0
  13. package/dist/docx/comments/verifying.js +206 -0
  14. package/dist/docx/comments/writing.js +73 -46
  15. package/dist/docx/exportDocx.js +2 -1
  16. package/dist/docx/formatting/direct.js +3 -6
  17. package/dist/docx/headersFooters.js +8 -10
  18. package/dist/docx/notes.js +3 -5
  19. package/dist/docx/paraProps.js +51 -46
  20. package/dist/docx/propsXml.d.ts +20 -2
  21. package/dist/docx/propsXml.js +72 -32
  22. package/dist/docx/relationships.js +8 -3
  23. package/dist/docx/runProps.js +34 -34
  24. package/dist/docx/sdt.js +16 -14
  25. package/dist/docx/serializeBlock.js +1 -2
  26. package/dist/docx/serializeParagraph.d.ts +2 -1
  27. package/dist/docx/serializeParagraph.js +25 -14
  28. package/dist/docx/serializeTable.js +38 -24
  29. package/dist/docx/storyProjection.d.ts +36 -0
  30. package/dist/docx/storyProjection.js +23 -0
  31. package/dist/docx/tableFormatting/editing.js +49 -73
  32. package/dist/docx/tableTemplate.js +24 -3
  33. package/dist/docx/theme.d.ts +0 -6
  34. package/dist/docx/theme.js +0 -8
  35. package/dist/editor/clipboard/images.js +17 -3
  36. package/dist/editor/clipboard/inlineFormatting.d.ts +7 -0
  37. package/dist/editor/clipboard/inlineFormatting.js +1 -0
  38. package/dist/editor/commands/breakCommands.js +4 -3
  39. package/dist/editor/commands/canRunCommand.d.ts +2 -2
  40. package/dist/editor/commands/canRunCommand.js +1 -1
  41. package/dist/editor/commands/comments/editing.js +3 -6
  42. package/dist/editor/commands/historyCommands.js +2 -1
  43. package/dist/editor/commands/lockCommands.js +10 -1
  44. package/dist/editor/commands/tabCommands.js +4 -3
  45. package/dist/editor/createEditor.js +3 -9
  46. package/dist/editor/externalClipboard.js +166 -4
  47. package/dist/editor/insertImage.js +4 -3
  48. package/dist/editor/plugins/documentProtection.d.ts +3 -3
  49. package/dist/editor/plugins/lockedContent.d.ts +4 -4
  50. package/dist/editor/plugins/lockedContent.js +1 -1
  51. package/dist/numbering/listTemplate.js +32 -10
  52. package/dist/ooxml/element.d.ts +52 -0
  53. package/dist/ooxml/element.js +49 -0
  54. package/dist/ooxml/fragment.d.ts +53 -0
  55. package/dist/ooxml/fragment.js +76 -0
  56. package/dist/ooxml/image.js +4 -3
  57. package/dist/ooxml/names.d.ts +27 -0
  58. package/dist/ooxml/names.js +29 -0
  59. package/dist/ooxml/precedence.d.ts +31 -0
  60. package/dist/ooxml/precedence.js +51 -0
  61. package/dist/ooxml/xml.d.ts +31 -0
  62. package/dist/ooxml/xml.js +27 -0
  63. package/dist/page/blockKinds.d.ts +42 -0
  64. package/dist/page/blockKinds.js +0 -0
  65. package/dist/page/measureBlocks.d.ts +14 -1
  66. package/dist/page/measureBlocks.js +31 -11
  67. package/dist/page/pageDecorations.d.ts +16 -0
  68. package/dist/page/pageDecorations.js +1 -0
  69. package/dist/page/pageLayout.d.ts +8 -43
  70. package/dist/page/pageLayout.js +28 -51
  71. package/dist/page/tableMeasurements.d.ts +10 -2
  72. package/dist/page/tableMeasurements.js +20 -16
  73. package/dist/page/usePageLayout.d.ts +15 -0
  74. package/dist/page/usePageLayout.js +30 -2
  75. package/dist/schema/attrRoles.d.ts +35 -0
  76. package/dist/schema/attrRoles.js +106 -0
  77. package/dist/schema/docxSchema.d.ts +6 -0
  78. package/dist/schema/docxSchema.js +212 -101
  79. package/dist/schema/editGuard.d.ts +99 -0
  80. package/dist/schema/editGuard.js +43 -0
  81. package/dist/schema/guards.d.ts +68 -0
  82. package/dist/schema/guards.js +82 -0
  83. package/dist/schema/locks.d.ts +8 -35
  84. package/dist/schema/locks.js +29 -64
  85. package/dist/schema/preservedGuards.d.ts +30 -0
  86. package/dist/schema/preservedGuards.js +50 -0
  87. package/dist/schema/protection.d.ts +10 -3
  88. package/dist/schema/protection.js +2 -1
  89. package/dist/schema/sourceEquality.d.ts +27 -0
  90. package/dist/schema/sourceEquality.js +65 -0
  91. package/dist/table/cellFormatting.js +2 -1
  92. package/dist/table/commands.d.ts +2 -2
  93. package/dist/table/commands.js +1 -1
  94. package/dist/table/index.d.ts +2 -2
  95. package/dist/table/merge.d.ts +2 -2
  96. package/dist/table/merge.js +1 -1
  97. package/package.json +4 -1
  98. package/dist/editor/plugins/bookmarkProtection.d.ts +0 -3
  99. package/dist/editor/plugins/bookmarkProtection.js +0 -28
  100. package/dist/editor/plugins/noteProtection.d.ts +0 -3
  101. package/dist/editor/plugins/noteProtection.js +0 -25
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The one list an edit is judged against.
3
+ *
4
+ * A guard answers, for one rule, whether an edit would leave the document in a state the file
5
+ * cannot be written back from. `transactionAllowed` runs the list over a built transaction, both
6
+ * where an edit is dispatched (`editor/plugins/lockedContent`) and where a command is dry-run
7
+ * (`editor/commands/canRunCommand`), and `editShut` runs it over the intent a command has before
8
+ * it builds anything. Both ask the same list, which is what keeps a disabled control and a refused
9
+ * edit saying the same thing.
10
+ *
11
+ * What a guard is written against - `EditGuard`, `EditIntent` and the reach primitives - stands in
12
+ * `./editGuard` so that a module writing a guard need not read this one, and is handed on from
13
+ * here so that a caller has one door to the whole seam.
14
+ *
15
+ * `editShut` is not yet what every command asks. Only `editor/commands/breakCommands`,
16
+ * `editor/commands/tabCommands` and `editor/insertImage` ask it today; the rest still compose
17
+ * `editsShut` with a lock predicate of their own (`./locks`), and each decides for itself whether
18
+ * a stretch a guard shuts is trimmed out of the edit or refuses the whole of it. Until they move
19
+ * over, a guard added to the list below reaches those commands through `transactionAllowed` alone,
20
+ * which refuses the transaction they built rather than telling them not to build it.
21
+ */
22
+ import type { EditorState, Selection, Transaction } from "prosemirror-state";
23
+ import { type EditGuard, type EditIntent } from "./editGuard";
24
+ import { type ProtectionState } from "./protection";
25
+ export type { EditGuard, EditGuardName, EditIntent } from "./editGuard";
26
+ export { stepReaches } from "./editGuard";
27
+ /**
28
+ * Whether the transaction reaches a comment node, which is how a body, a reply and a resolution
29
+ * change. Every comment lives in its three nodes, so a change that reaches none of them cannot
30
+ * have changed a comment.
31
+ */
32
+ export declare function transactionTouchesComments(tr: Transaction): boolean;
33
+ /**
34
+ * Whether the protection lets this transaction through (`./protection`).
35
+ *
36
+ * The whole-document judgement is reached for only when a comment is touched at all
37
+ * (`transactionTouchesComments`). A change that touches none is a body edit: through under `none`,
38
+ * refused under `comments`, and nothing about ownership to ask.
39
+ */
40
+ export declare function protectionAllowsTransaction(tr: Transaction, rules: ProtectionState): boolean;
41
+ export declare const EDIT_GUARDS: readonly EditGuard[];
42
+ /**
43
+ * Whether every guard would let this transaction through, decided and nothing else.
44
+ *
45
+ * The refusal a guard itself answers with carries a side effect - the composition it ends
46
+ * (`editor/plugins/lockedContent`) - which a query about a button's state may not set off, so the
47
+ * decision stands apart from it and every caller building an edit asks this rather than handing
48
+ * the transaction to a state.
49
+ *
50
+ * The whole-change judgements come first and take no pass, since a pass lifts one guard's reading
51
+ * of a step rather than another guard's reading of the change. What is left is judged step by
52
+ * step, each step over the document it was built against.
53
+ */
54
+ export declare function transactionAllowed(tr: Transaction, state: EditorState): boolean;
55
+ /**
56
+ * Whether any guard shuts this intent where it stands.
57
+ *
58
+ * This is what a command asks before it reports that it applies: a command reporting true and then
59
+ * being refused by the guard draws a live control that swallows the click.
60
+ */
61
+ export declare function editShut(state: EditorState, intent: EditIntent): boolean;
62
+ /**
63
+ * What a command doing this to whatever is selected means to do, one intent per selected stretch.
64
+ *
65
+ * A stretch of no length holds nothing to mark or to put away, so whatever the command would do to
66
+ * a stretch it is an insertion there.
67
+ */
68
+ export declare function selectionIntents(selection: Selection, kind: "mark" | "replace"): EditIntent[];
@@ -0,0 +1,82 @@
1
+ // src/schema/guards.ts
2
+ import {
3
+ transactionReaches
4
+ } from "./editGuard.js";
5
+ import { lockGuard } from "./locks.js";
6
+ import { bookmarkGuard, noteGuard } from "./preservedGuards.js";
7
+ import {
8
+ isCommentNode,
9
+ protectionAllows
10
+ } from "./protection.js";
11
+ import { editsShut, protectionOf } from "./protectionState.js";
12
+ import { stepReaches } from "./editGuard.js";
13
+ function transactionTouchesComments(tr) {
14
+ return transactionReaches(tr, isCommentNode);
15
+ }
16
+ function protectionAllowsTransaction(tr, rules) {
17
+ switch (rules.protection) {
18
+ case "readOnly":
19
+ return false;
20
+ case "none":
21
+ return !transactionTouchesComments(tr) || protectionAllows(tr.before, tr.doc, rules);
22
+ case "comments":
23
+ return transactionTouchesComments(tr) && protectionAllows(tr.before, tr.doc, rules);
24
+ default: {
25
+ const unmodelled = rules.protection;
26
+ return unmodelled;
27
+ }
28
+ }
29
+ }
30
+ var protectionGuard = {
31
+ name: "protection",
32
+ change: (tr, state) => protectionAllowsTransaction(tr, protectionOf(state)),
33
+ shuts: (_intent, state) => editsShut(state)
34
+ };
35
+ var EDIT_GUARDS = [
36
+ protectionGuard,
37
+ lockGuard,
38
+ bookmarkGuard,
39
+ noteGuard
40
+ ];
41
+ function judgesSteps(guard) {
42
+ return guard.step !== void 0;
43
+ }
44
+ function lifted(guard, tr) {
45
+ return guard.liftedBy?.some((pass) => tr.getMeta(pass) === true) === true;
46
+ }
47
+ function transactionAllowed(tr, state) {
48
+ if (!tr.docChanged) return true;
49
+ if (EDIT_GUARDS.some((guard) => guard.change?.(tr, state) === false)) {
50
+ return false;
51
+ }
52
+ return EDIT_GUARDS.filter(judgesSteps).filter((guard) => !lifted(guard, tr)).every(
53
+ (guard) => (
54
+ // Each step counts positions in the document it was built against, which `docs` holds
55
+ tr.steps.every(
56
+ (step, index) => guard.step(
57
+ step,
58
+ tr.docs[index] ?? state.doc,
59
+ tr.docs[index + 1] ?? tr.doc,
60
+ state
61
+ )
62
+ )
63
+ )
64
+ );
65
+ }
66
+ function editShut(state, intent) {
67
+ return EDIT_GUARDS.some((guard) => guard.shuts(intent, state));
68
+ }
69
+ function selectionIntents(selection, kind) {
70
+ return selection.ranges.map(
71
+ (range) => range.$from.pos === range.$to.pos ? { kind: "insert", at: range.$from.pos } : { kind, from: range.$from.pos, to: range.$to.pos }
72
+ );
73
+ }
74
+ export {
75
+ EDIT_GUARDS,
76
+ editShut,
77
+ protectionAllowsTransaction,
78
+ selectionIntents,
79
+ stepReaches,
80
+ transactionAllowed,
81
+ transactionTouchesComments
82
+ };
@@ -2,12 +2,12 @@
2
2
  * Evaluates OOXML content and deletion locks for inline controls and whole table cells. Commands
3
3
  * query these predicates before editing; `editor/plugins/lockedContent` enforces them at runtime.
4
4
  *
5
- * `transactionAllowed` is the one guard, and it answers for the protection the editor runs under
6
- * (`./protection`) as well as for the locks, so that a command asking it asks both.
5
+ * `lockGuard` is what `./guards` registers all of this as, so that a caller asking the one guard
6
+ * list asks the locks along with every other rule an edit is judged by.
7
7
  */
8
8
  import type { Mark, Node as PMNode } from "prosemirror-model";
9
- import { type EditorState, PluginKey, type Selection, type Transaction } from "prosemirror-state";
10
- import { type ProtectionState } from "./protection";
9
+ import { PluginKey, type Selection } from "prosemirror-state";
10
+ import type { StepGuard } from "./editGuard";
11
11
  /**
12
12
  * The pass that lets a transaction through the guard, which is how a lock can be lifted at all.
13
13
  * A plugin key is used as the name so that it cannot collide with a consumer's own metadata.
@@ -104,37 +104,10 @@ export declare function selectionShut(selection: Selection, doc: PMNode): boolea
104
104
  */
105
105
  export declare function replacementShut(selection: Selection, doc: PMNode): boolean;
106
106
  /**
107
- * Whether any step of the transaction reaches a comment node: puts one in, takes one out, or
108
- * rewrites the one where it stands, which is how a body, a reply and a resolution change.
107
+ * The locks the document carries, as `./guards` registers them.
109
108
  *
110
- * Every comment lives in its three nodes, so a change that reaches none of them cannot have
111
- * changed a comment. That is what lets the guard settle the common transaction - typing, and
112
- * nothing more - over the stretch it rewrote rather than over the whole document.
113
- *
114
- * A step of a kind this does not know - one a consumer brought - is answered as reaching one,
115
- * since what it rewrote is not known either. The whole-document judgement then has the say, and
116
- * an unknown step costs a comparison rather than a hole in the guard.
117
- */
118
- export declare function transactionTouchesComments(tr: Transaction): boolean;
119
- /**
120
- * Whether the protection lets this transaction through (`./protection`).
121
- *
122
- * The whole-document judgement is reached for only when a comment is touched at all
123
- * (`transactionTouchesComments`). A change that touches none is a body edit: through under `none`,
124
- * refused under `comments`, and nothing about ownership to ask.
125
- */
126
- export declare function protectionAllowsTransaction(tr: Transaction, rules: ProtectionState): boolean;
127
- /**
128
- * Whether the guard would let this transaction through, decided and nothing else.
129
- *
130
- * The refusal the guard itself answers with carries a side effect - the composition it ends
131
- * (`editor/plugins/lockedContent`) - which a query about a button's state may not set off, so the
132
- * decision stands apart from it and every caller building an edit asks this rather than handing
133
- * the transaction to a state.
134
- *
135
- * The protection is judged before the passes and without them: a replayed edit is still an edit,
136
- * and a pass that lifts a lock lifts no protection. Its judgement is over the whole change rather
137
- * than step by step, which is what lets it tell a comment from everything else.
109
+ * Both passes lift it: unlocking is the one edit that may reach into a lock, and every step the
110
+ * history replays is the reverse of a step that passed the guard when it was made.
138
111
  */
139
- export declare function transactionAllowed(tr: Transaction, state: EditorState): boolean;
112
+ export declare const lockGuard: StepGuard;
140
113
  export {};
@@ -1,7 +1,5 @@
1
1
  // src/schema/locks.ts
2
- import {
3
- PluginKey
4
- } from "prosemirror-state";
2
+ import { PluginKey } from "prosemirror-state";
5
3
  import {
6
4
  AddMarkStep,
7
5
  AddNodeMarkStep,
@@ -12,11 +10,6 @@ import {
12
10
  ReplaceStep
13
11
  } from "prosemirror-transform";
14
12
  import { docxSchema } from "./index.js";
15
- import {
16
- isCommentNode,
17
- protectionAllows
18
- } from "./protection.js";
19
- import { protectionOf } from "./protectionState.js";
20
13
  var unlockAllowed = new PluginKey("docxEditorUnlockAllowed");
21
14
  var historyReplay = new PluginKey("docxEditorHistoryReplay");
22
15
  var OPEN = { contents: false, deletion: false };
@@ -123,17 +116,21 @@ function insertionInsideLocked(doc, pos) {
123
116
  const after = lockedMarkOf($pos.nodeAfter);
124
117
  return before !== null && after !== null && before.eq(after);
125
118
  }
119
+ function markShut(doc, from, to) {
120
+ return from === to ? insertionInsideLocked(doc, from) : rangeTouchesLocked(doc, from, to);
121
+ }
122
+ function replaceShut(doc, from, to) {
123
+ return from === to ? insertionInsideLocked(doc, from) : rangeShut(doc, { from, to, takesAway: true });
124
+ }
126
125
  function selectionShut(selection, doc) {
127
126
  return selection.ranges.some(
128
- (range) => range.$from.pos === range.$to.pos ? insertionInsideLocked(doc, range.$from.pos) : rangeTouchesLocked(doc, range.$from.pos, range.$to.pos)
127
+ (range) => markShut(doc, range.$from.pos, range.$to.pos)
129
128
  );
130
129
  }
131
130
  function replacementShut(selection, doc) {
132
- return selection.ranges.some((range) => {
133
- const from = range.$from.pos;
134
- const to = range.$to.pos;
135
- return from === to ? insertionInsideLocked(doc, from) : rangeShut(doc, { from, to, takesAway: true });
136
- });
131
+ return selection.ranges.some(
132
+ (range) => replaceShut(doc, range.$from.pos, range.$to.pos)
133
+ );
137
134
  }
138
135
  function editedRanges(step) {
139
136
  if (step instanceof ReplaceAroundStep) {
@@ -192,58 +189,28 @@ function stepAllowed(step, doc) {
192
189
  }
193
190
  return true;
194
191
  }
195
- function carriesPass(tr) {
196
- return tr.getMeta(unlockAllowed) === true || tr.getMeta(historyReplay) === true;
197
- }
198
- function rangeHoldsComment(doc, from, to) {
199
- let found = false;
200
- doc.nodesBetween(from, to, (node) => {
201
- if (found) return false;
202
- if (isCommentNode(node)) found = true;
203
- return !found;
204
- });
205
- return found;
206
- }
207
- function transactionTouchesComments(tr) {
208
- return tr.steps.some((step, index) => {
209
- const before = tr.docs[index];
210
- const after = tr.docs[index + 1] ?? tr.doc;
211
- if (step instanceof AttrStep || step instanceof AddNodeMarkStep || step instanceof RemoveNodeMarkStep) {
212
- const node = before.nodeAt(step.pos);
213
- return node !== null && isCommentNode(node);
214
- }
215
- if (!(step instanceof ReplaceStep || step instanceof ReplaceAroundStep || step instanceof AddMarkStep || step instanceof RemoveMarkStep)) {
216
- return true;
217
- }
218
- let touched = false;
219
- step.getMap().forEach((oldStart, oldEnd, newStart, newEnd) => {
220
- touched ||= rangeHoldsComment(before, oldStart, oldEnd) || rangeHoldsComment(after, newStart, newEnd);
221
- });
222
- return touched;
223
- });
224
- }
225
- function protectionAllowsTransaction(tr, rules) {
226
- switch (rules.protection) {
227
- case "readOnly":
228
- return false;
229
- case "none":
230
- return !transactionTouchesComments(tr) || protectionAllows(tr.before, tr.doc, rules);
231
- case "comments":
232
- return transactionTouchesComments(tr) && protectionAllows(tr.before, tr.doc, rules);
192
+ function intentShut(doc, intent) {
193
+ switch (intent.kind) {
194
+ case "insert":
195
+ return insertionInsideLocked(doc, intent.at);
196
+ case "block":
197
+ return insideLockedCell(doc, intent.at);
198
+ case "mark":
199
+ return markShut(doc, intent.from, intent.to);
200
+ case "replace":
201
+ return replaceShut(doc, intent.from, intent.to);
233
202
  default: {
234
- const unmodelled = rules.protection;
203
+ const unmodelled = intent;
235
204
  return unmodelled;
236
205
  }
237
206
  }
238
207
  }
239
- function transactionAllowed(tr, state) {
240
- if (!tr.docChanged) return true;
241
- if (!protectionAllowsTransaction(tr, protectionOf(state))) return false;
242
- if (carriesPass(tr)) return true;
243
- return tr.steps.every(
244
- (step, index) => stepAllowed(step, tr.docs[index] ?? state.doc)
245
- );
246
- }
208
+ var lockGuard = {
209
+ name: "lock",
210
+ liftedBy: [unlockAllowed, historyReplay],
211
+ step: (step, before) => stepAllowed(step, before),
212
+ shuts: (intent, state) => intentShut(state.doc, intent)
213
+ };
247
214
  export {
248
215
  carriesLock,
249
216
  controlSpans,
@@ -251,12 +218,10 @@ export {
251
218
  insertionInsideLocked,
252
219
  insideLockedCell,
253
220
  isLockedCell,
221
+ lockGuard,
254
222
  lockedMarkOf,
255
- protectionAllowsTransaction,
256
223
  rangeTouchesLocked,
257
224
  replacementShut,
258
225
  selectionShut,
259
- transactionAllowed,
260
- transactionTouchesComments,
261
226
  unlockAllowed
262
227
  };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The guards over the nodes a document is opened with and the editor only preserves: the two ends
3
+ * of a bookmark range, and the reference standing where a footnote or an endnote is called.
4
+ *
5
+ * Neither is content the editor writes, so nothing in it can put such a node back once it is gone,
6
+ * and a document that lost one of a bookmark's two ends cannot be written back as a file at all
7
+ * (`docx/exportDocx` refuses it). What each guard holds is therefore the whole list of the nodes it
8
+ * answers for, in the order the document carries them.
9
+ */
10
+ import type { Node as PMNode } from "prosemirror-model";
11
+ import { type ChangeGuard, type EditGuardName } from "./editGuard";
12
+ /** Everything about one preserved node that has to read the same after a change as before it */
13
+ type Signature = (node: PMNode) => string;
14
+ /**
15
+ * A guard that lets a change through only while it leaves the list of these nodes reading exactly
16
+ * as it did, which holds their number, their contents and their order at once.
17
+ *
18
+ * The comparison walks the whole document, so it is reached for only once a step of the change has
19
+ * touched such a node: ordinary typing costs the stretches its own steps rewrote and nothing more.
20
+ * It is judged over the change rather than step by step because a marker moved whole is two steps,
21
+ * one taking it out and one putting it back, and neither of them alone leaves the list as it was.
22
+ *
23
+ * Nothing lifts it. A pass is how an edit reaches past a lock the document put on itself, and
24
+ * there is no such thing to reach past here: the file simply has no way to say what a lost marker
25
+ * was.
26
+ */
27
+ export declare function preservedNodeGuard(name: EditGuardName, holds: (node: PMNode) => boolean, signature: Signature): ChangeGuard;
28
+ export declare const bookmarkGuard: ChangeGuard;
29
+ export declare const noteGuard: ChangeGuard;
30
+ export {};
@@ -0,0 +1,50 @@
1
+ // src/schema/preservedGuards.ts
2
+ import {
3
+ rangeHolds,
4
+ transactionReaches
5
+ } from "./editGuard.js";
6
+ function signatures(doc, holds, signature) {
7
+ const found = [];
8
+ doc.descendants((node) => {
9
+ if (holds(node)) found.push(signature(node));
10
+ return true;
11
+ });
12
+ return found;
13
+ }
14
+ function same(a, b) {
15
+ return a.length === b.length && a.every((value, index) => value === b[index]);
16
+ }
17
+ function preservedNodeGuard(name, holds, signature) {
18
+ const listOf = (doc) => signatures(doc, holds, signature);
19
+ return {
20
+ name,
21
+ change: (tr) => !transactionReaches(tr, holds) || same(listOf(tr.before), listOf(tr.doc)),
22
+ shuts: (intent, state) => intent.kind === "replace" && rangeHolds(state.doc, intent.from, intent.to, holds)
23
+ };
24
+ }
25
+ var BOOKMARK_XML = /<(?:[\w.-]+:)?bookmark(?:Start|End)\b/;
26
+ function isBookmarkMarker(node) {
27
+ if (node.type.name === "bookmarkBlock") return true;
28
+ return node.type.name === "rawInline" && typeof node.attrs.xml === "string" && BOOKMARK_XML.test(node.attrs.xml);
29
+ }
30
+ function bookmarkSignature(node) {
31
+ return node.type.name === "bookmarkBlock" ? `block:${node.attrs.srcId}:${node.attrs.name}` : `inline:${node.attrs.xml}`;
32
+ }
33
+ var bookmarkGuard = preservedNodeGuard(
34
+ "bookmark",
35
+ isBookmarkMarker,
36
+ bookmarkSignature
37
+ );
38
+ function isNoteReference(node) {
39
+ return node.type.name === "noteReference";
40
+ }
41
+ var noteGuard = preservedNodeGuard(
42
+ "note",
43
+ isNoteReference,
44
+ (node) => JSON.stringify(node.attrs)
45
+ );
46
+ export {
47
+ bookmarkGuard,
48
+ noteGuard,
49
+ preservedNodeGuard
50
+ };
@@ -11,9 +11,9 @@
11
11
  *
12
12
  * The protection is editor state rather than document state: it is the reader's standing, not the
13
13
  * file's, so nothing here is written to or read from OOXML. `editor/plugins/documentProtection`
14
- * holds it in the state and `./protectionState` reads it back; the guard (`transactionAllowed` in
15
- * `./locks`) asks the judgements here. They take documents alone, so that the `./core` entry can
16
- * make the same judgement over a file without an editor.
14
+ * holds it in the state and `./protectionState` reads it back; the guard list (`./guards`) asks the
15
+ * judgements here. They take documents alone, so that the `./core` entry can make the same
16
+ * judgement over a file without an editor.
17
17
  */
18
18
  import { type Node as PMNode } from "prosemirror-model";
19
19
  export type EditingProtection = "none" | "readOnly" | "comments";
@@ -31,6 +31,13 @@ export interface ProtectionState {
31
31
  }
32
32
  /** Whether this node is one of the three a comment stands in the story as */
33
33
  export declare function isCommentNode(node: PMNode): boolean;
34
+ /**
35
+ * The document with every comment taken out of it.
36
+ *
37
+ * `Fragment.fromArray` joins the text a marker had split, so a document a comment was put into
38
+ * reads the same as it did before the comment, and the two compare equal.
39
+ */
40
+ export declare function withoutComments(node: PMNode): PMNode;
34
41
  /** Whether the two documents differ in nothing but their comments */
35
42
  export declare function changesOnlyComments(before: PMNode, after: PMNode): boolean;
36
43
  /**
@@ -166,5 +166,6 @@ export {
166
166
  commentIdentitiesKept,
167
167
  commentOwned,
168
168
  isCommentNode,
169
- protectionAllows
169
+ protectionAllows,
170
+ withoutComments
170
171
  };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Whether two nodes would be written back as the same XML.
3
+ *
4
+ * `Node.eq` answers a different question: whether they are the same node, display values and all.
5
+ * That is the right question inside an editing session, where both sides came from one import and
6
+ * a difference can only be an edit. It is the wrong one across a re-derivation, because the values
7
+ * worked out from the surroundings (`./attrRoles`) move without the document moving, and a block
8
+ * judged changed is a block rewritten, which costs it the markup the writer does not model.
9
+ *
10
+ * So the comparison is `Node.eq` with the display attrs left out, and nothing else left out: the
11
+ * type, the text, the marks and the source and session attrs all have to agree, and the recursion
12
+ * runs the same way, so a node that says the same thing in a different shape is still a change.
13
+ */
14
+ import { type Node as PMNode } from "prosemirror-model";
15
+ /**
16
+ * Whether the two would write the same XML: the same type, text, marks and source attrs, all the
17
+ * way down. What the editor works out for the screen is not compared.
18
+ */
19
+ export declare function sameSource(a: PMNode, b: PMNode): boolean;
20
+ /**
21
+ * The node with every worked-out attr back at the value the schema starts it on, on itself and on
22
+ * everything inside it.
23
+ *
24
+ * `sameSource` needs no such copy, and takes none. This is for a caller that has to hand a whole
25
+ * document to something comparing the ordinary way.
26
+ */
27
+ export declare function withoutDisplayAttrs(node: PMNode): PMNode;
@@ -0,0 +1,65 @@
1
+ // src/schema/sourceEquality.ts
2
+ import {
3
+ Fragment
4
+ } from "prosemirror-model";
5
+ import { displayAttrsOf } from "./attrRoles.js";
6
+ function sameValue(a, b) {
7
+ if (a === b) return true;
8
+ if (Array.isArray(a) || Array.isArray(b)) {
9
+ return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, at) => sameValue(item, b[at]));
10
+ }
11
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
12
+ return false;
13
+ }
14
+ const left = Object.entries(a);
15
+ return left.length === Object.keys(b).length && left.every(
16
+ ([key, value]) => key in b && sameValue(value, b[key])
17
+ );
18
+ }
19
+ function sameAttrs(display, a, b) {
20
+ return Object.keys(a).filter((name) => !display.includes(name)).every((name) => sameValue(a[name], b[name]));
21
+ }
22
+ function sameMarks(a, b) {
23
+ return a.length === b.length && a.every((mark, at) => {
24
+ const other = b[at];
25
+ return other !== void 0 && mark.type === other.type && sameAttrs(displayAttrsOf(mark.type), mark.attrs, other.attrs);
26
+ });
27
+ }
28
+ function sameSource(a, b) {
29
+ if (a === b) return true;
30
+ if (a.type !== b.type || !sameMarks(a.marks, b.marks) || !sameAttrs(displayAttrsOf(a.type), a.attrs, b.attrs)) {
31
+ return false;
32
+ }
33
+ if (a.isText) return a.text === b.text;
34
+ if (a.childCount !== b.childCount) return false;
35
+ for (let at = 0; at < a.childCount; at += 1) {
36
+ if (!sameSource(a.child(at), b.child(at))) return false;
37
+ }
38
+ return true;
39
+ }
40
+ function withoutDisplay(type, attrs) {
41
+ return Object.fromEntries(
42
+ Object.entries(attrs).map(
43
+ ([name, value]) => displayAttrsOf(type).includes(name) ? [name, type.spec.attrs?.[name]?.default ?? null] : [name, value]
44
+ )
45
+ );
46
+ }
47
+ function withoutDisplayMarkAttrs(mark) {
48
+ return displayAttrsOf(mark.type).length === 0 ? mark : mark.type.create(withoutDisplay(mark.type, mark.attrs));
49
+ }
50
+ var childrenOf = (node) => Array.from({ length: node.childCount }, (_, at) => node.child(at));
51
+ function withoutDisplayAttrs(node) {
52
+ const marks = node.marks.map(withoutDisplayMarkAttrs);
53
+ if (node.isText) return node.mark(marks);
54
+ return node.type.create(
55
+ withoutDisplay(node.type, node.attrs),
56
+ Fragment.fromArray(
57
+ childrenOf(node).map((child) => withoutDisplayAttrs(child))
58
+ ),
59
+ marks
60
+ );
61
+ }
62
+ export {
63
+ sameSource,
64
+ withoutDisplayAttrs
65
+ };
@@ -8,7 +8,8 @@ import {
8
8
  NO_FILL,
9
9
  toCellFormat
10
10
  } from "../model/format.js";
11
- import { isLockedCell, transactionAllowed } from "../schema/locks.js";
11
+ import { transactionAllowed } from "../schema/guards.js";
12
+ import { isLockedCell } from "../schema/locks.js";
12
13
  import { editsShut } from "../schema/protectionState.js";
13
14
  import { cellDefaultsAt, tableCellSources } from "./gridBorders.js";
14
15
  function text(value) {
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Building a transaction and running it are kept apart.
3
3
  * Following the ProseMirror convention, calling a command without `dispatch` only
4
- * reports whether it can run right now, and that answer takes in the lock guard
5
- * (`schema/locks`), so no caller has to ask about locks separately.
4
+ * reports whether it can run right now, and that answer takes in every guard an edit
5
+ * is judged by (`schema/guards`), so no caller has to ask about them separately.
6
6
  */
7
7
  import type { EditorState, Transaction } from "prosemirror-state";
8
8
  export type TableCommand = (state: EditorState, dispatch?: (tr: Transaction) => void) => boolean;
@@ -3,7 +3,7 @@ import {
3
3
  deleteTable as pmDeleteTable,
4
4
  isInTable as pmIsInTable
5
5
  } from "prosemirror-tables";
6
- import { transactionAllowed } from "../schema/locks.js";
6
+ import { transactionAllowed } from "../schema/guards.js";
7
7
  import {
8
8
  buildAddColumnAfterTransaction,
9
9
  buildAddColumnBeforeTransaction,
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Only what the toolbar needs is exported. Following the ProseMirror convention,
5
5
  * calling a command without `dispatch` only reports whether it can run right now, and that answer
6
- * takes in the lock: a structural edit that would carry a locked cell off reports that it does not
7
- * apply, rather than being refused after the click (`schema/locks`).
6
+ * takes in every guard an edit is judged by (`schema/guards`): a structural edit that would carry a
7
+ * locked cell off reports that it does not apply, rather than being refused after the click.
8
8
  *
9
9
  * Everything here works on a table already in the document; putting a new one into the body is
10
10
  * `insertTable` in `./commands`, which reads the page geometry the editor holds.
@@ -6,7 +6,7 @@ import type { EditorState, Transaction } from "prosemirror-state";
6
6
  /**
7
7
  * Whether several cells can be merged into one.
8
8
  * The selection has to be a rectangle spanning more than one cell, no merged cell may stick out
9
- * past that rectangle, and the lock guard has to let the merge through: a locked cell may not be
9
+ * past that rectangle, and the guards have to let the merge through: a locked cell may not be
10
10
  * swallowed by another (`schema/locks`).
11
11
  *
12
12
  * The two queries are defined from the very transaction the commands run, so they cannot drift
@@ -16,7 +16,7 @@ import type { EditorState, Transaction } from "prosemirror-state";
16
16
  export declare function canMergeCells(state: EditorState): boolean;
17
17
  /**
18
18
  * Whether a split is possible. The cursor has to sit inside a cell that is merged horizontally or
19
- * vertically, and the lock guard has to let the split through: the cells a split makes would each
19
+ * vertically, and the guards have to let the split through: the cells a split makes would each
20
20
  * carry the original's lock, which is a lock planted in places it was never put (`schema/locks`).
21
21
  */
22
22
  export declare function canSplitCell(state: EditorState): boolean;
@@ -11,7 +11,7 @@ import {
11
11
  widthNumber,
12
12
  withWidthNumber
13
13
  } from "../model/format.js";
14
- import { transactionAllowed } from "../schema/locks.js";
14
+ import { transactionAllowed } from "../schema/guards.js";
15
15
  import { inheritCellAttrs } from "./format.js";
16
16
  import { cellWidthForGridCol, gridSpanWidth, tableGridCols } from "./widths.js";
17
17
  function tableAfter(tr, tableStart) {