@portone/docx-editor 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/CONTRIBUTING.md +4 -0
- package/README.md +17 -5
- package/dist/DocxEditor.d.ts +34 -10
- package/dist/DocxEditor.js +69 -24
- package/dist/core.d.ts +2 -0
- package/dist/core.js +2 -0
- package/dist/docx/commentOnlyChange.d.ts +47 -0
- package/dist/docx/commentOnlyChange.js +162 -0
- package/dist/docx/comments/constants.d.ts +11 -3
- package/dist/docx/comments/constants.js +10 -3
- package/dist/docx/comments/contentTypes.d.ts +7 -0
- package/dist/docx/comments/contentTypes.js +38 -0
- package/dist/docx/comments/grammar.d.ts +80 -0
- package/dist/docx/comments/grammar.js +213 -0
- package/dist/docx/comments/model.d.ts +2 -0
- package/dist/docx/comments/model.js +3 -0
- package/dist/docx/comments/people.d.ts +39 -0
- package/dist/docx/comments/people.js +196 -0
- package/dist/docx/comments/reading.d.ts +18 -1
- package/dist/docx/comments/reading.js +33 -20
- package/dist/docx/comments/verifying.d.ts +44 -0
- package/dist/docx/comments/verifying.js +206 -0
- package/dist/docx/comments/writing.d.ts +4 -1
- package/dist/docx/comments/writing.js +84 -77
- package/dist/docx/exportDocx.js +2 -1
- package/dist/docx/formatting/direct.js +3 -6
- package/dist/docx/headersFooters.js +8 -10
- package/dist/docx/importParagraph.js +1 -0
- package/dist/docx/notes.js +3 -5
- package/dist/docx/paraProps.js +51 -46
- package/dist/docx/propsXml.d.ts +20 -2
- package/dist/docx/propsXml.js +72 -32
- package/dist/docx/relationships.js +8 -3
- package/dist/docx/runProps.js +34 -34
- package/dist/docx/sdt.js +16 -14
- package/dist/docx/serializeBlock.js +1 -2
- package/dist/docx/serializeParagraph.d.ts +2 -1
- package/dist/docx/serializeParagraph.js +25 -14
- package/dist/docx/serializeTable.js +38 -24
- package/dist/docx/storyProjection.d.ts +36 -0
- package/dist/docx/storyProjection.js +23 -0
- package/dist/docx/tableFormatting/editing.js +49 -73
- package/dist/docx/tableTemplate.js +24 -3
- package/dist/docx/theme.d.ts +0 -6
- package/dist/docx/theme.js +0 -8
- package/dist/editor/clipboard/images.js +17 -3
- package/dist/editor/clipboard/inlineFormatting.d.ts +7 -0
- package/dist/editor/clipboard/inlineFormatting.js +1 -0
- package/dist/editor/commands/breakCommands.js +6 -2
- package/dist/editor/commands/canRunCommand.d.ts +3 -2
- package/dist/editor/commands/canRunCommand.js +2 -2
- package/dist/editor/commands/comments/editing.js +9 -10
- package/dist/editor/commands/comments/model.d.ts +9 -0
- package/dist/editor/commands/comments/reading.d.ts +7 -0
- package/dist/editor/commands/comments/reading.js +14 -0
- package/dist/editor/commands/formatting/editing.js +6 -1
- package/dist/editor/commands/historyCommands.js +14 -5
- package/dist/editor/commands/index.d.ts +7 -1
- package/dist/editor/commands/index.js +4 -0
- package/dist/editor/commands/linkCommands.js +2 -0
- package/dist/editor/commands/lockCommands.js +12 -1
- package/dist/editor/commands/tabCommands.js +4 -2
- package/dist/editor/createEditor.d.ts +13 -3
- package/dist/editor/createEditor.js +23 -15
- package/dist/editor/externalClipboard.js +166 -4
- package/dist/editor/insertImage.js +6 -2
- package/dist/editor/insertTable.js +2 -1
- package/dist/editor/paragraphEdits.d.ts +2 -0
- package/dist/editor/paragraphEdits.js +2 -0
- package/dist/editor/plugins/documentProtection.d.ts +21 -0
- package/dist/editor/plugins/documentProtection.js +44 -0
- package/dist/editor/plugins/imagePaste.js +4 -1
- package/dist/editor/plugins/keymap.d.ts +11 -1
- package/dist/editor/plugins/keymap.js +24 -4
- package/dist/editor/plugins/lockedContent.d.ts +4 -2
- package/dist/editor/plugins/lockedContent.js +2 -2
- package/dist/editor/plugins/tableContextMenu.js +2 -1
- package/dist/editor/plugins/textContextMenu.js +6 -1
- package/dist/index.d.ts +2 -0
- package/dist/numbering/listTemplate.js +32 -10
- package/dist/ooxml/element.d.ts +52 -0
- package/dist/ooxml/element.js +49 -0
- package/dist/ooxml/fragment.d.ts +53 -0
- package/dist/ooxml/fragment.js +76 -0
- package/dist/ooxml/image.js +4 -3
- package/dist/ooxml/names.d.ts +27 -0
- package/dist/ooxml/names.js +29 -0
- package/dist/ooxml/precedence.d.ts +31 -0
- package/dist/ooxml/precedence.js +51 -0
- package/dist/ooxml/xml.d.ts +31 -0
- package/dist/ooxml/xml.js +27 -0
- package/dist/page/blockKinds.d.ts +42 -0
- package/dist/page/blockKinds.js +0 -0
- package/dist/page/measureBlocks.d.ts +14 -1
- package/dist/page/measureBlocks.js +31 -11
- package/dist/page/pageDecorations.d.ts +16 -0
- package/dist/page/pageDecorations.js +1 -0
- package/dist/page/pageLayout.d.ts +8 -43
- package/dist/page/pageLayout.js +28 -51
- package/dist/page/tableMeasurements.d.ts +10 -2
- package/dist/page/tableMeasurements.js +20 -16
- package/dist/page/usePageLayout.d.ts +15 -0
- package/dist/page/usePageLayout.js +30 -2
- package/dist/schema/attrRoles.d.ts +35 -0
- package/dist/schema/attrRoles.js +106 -0
- package/dist/schema/docxSchema.d.ts +6 -0
- package/dist/schema/docxSchema.js +216 -100
- package/dist/schema/editGuard.d.ts +99 -0
- package/dist/schema/editGuard.js +43 -0
- package/dist/schema/guards.d.ts +68 -0
- package/dist/schema/guards.js +82 -0
- package/dist/schema/locks.d.ts +9 -8
- package/dist/schema/locks.js +32 -16
- package/dist/schema/preservedGuards.d.ts +30 -0
- package/dist/schema/preservedGuards.js +50 -0
- package/dist/schema/protection.d.ts +84 -0
- package/dist/schema/protection.js +171 -0
- package/dist/schema/protectionState.d.ts +20 -0
- package/dist/schema/protectionState.js +37 -0
- package/dist/schema/sourceEquality.d.ts +27 -0
- package/dist/schema/sourceEquality.js +65 -0
- package/dist/table/cellFormatting.js +5 -2
- package/dist/table/commands.d.ts +2 -2
- package/dist/table/commands.js +2 -2
- package/dist/table/index.d.ts +2 -2
- package/dist/table/merge.d.ts +2 -2
- package/dist/table/merge.js +3 -3
- package/dist/ui/CommentsPanel.d.ts +3 -3
- package/dist/ui/CommentsPanel.js +26 -18
- package/dist/ui/LinkCard.d.ts +1 -2
- package/dist/ui/LinkCard.js +3 -4
- package/dist/ui/TextMenu.js +15 -10
- package/package.json +5 -2
- package/dist/editor/plugins/bookmarkProtection.d.ts +0 -3
- package/dist/editor/plugins/bookmarkProtection.js +0 -28
- package/dist/editor/plugins/noteProtection.d.ts +0 -3
- 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
|
+
};
|
package/dist/schema/locks.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/**
|
|
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
|
+
*
|
|
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.
|
|
4
7
|
*/
|
|
5
8
|
import type { Mark, Node as PMNode } from "prosemirror-model";
|
|
6
|
-
import { PluginKey, type Selection
|
|
9
|
+
import { PluginKey, type Selection } from "prosemirror-state";
|
|
10
|
+
import type { StepGuard } from "./editGuard";
|
|
7
11
|
/**
|
|
8
12
|
* The pass that lets a transaction through the guard, which is how a lock can be lifted at all.
|
|
9
13
|
* A plugin key is used as the name so that it cannot collide with a consumer's own metadata.
|
|
@@ -100,13 +104,10 @@ export declare function selectionShut(selection: Selection, doc: PMNode): boolea
|
|
|
100
104
|
*/
|
|
101
105
|
export declare function replacementShut(selection: Selection, doc: PMNode): boolean;
|
|
102
106
|
/**
|
|
103
|
-
*
|
|
107
|
+
* The locks the document carries, as `./guards` registers them.
|
|
104
108
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* decision stands apart from it and every caller building an edit asks this rather than handing
|
|
108
|
-
* the transaction to a state.
|
|
109
|
-
* `doc` only stands in for a step the transaction kept no document for.
|
|
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.
|
|
110
111
|
*/
|
|
111
|
-
export declare
|
|
112
|
+
export declare const lockGuard: StepGuard;
|
|
112
113
|
export {};
|
package/dist/schema/locks.js
CHANGED
|
@@ -116,17 +116,21 @@ function insertionInsideLocked(doc, pos) {
|
|
|
116
116
|
const after = lockedMarkOf($pos.nodeAfter);
|
|
117
117
|
return before !== null && after !== null && before.eq(after);
|
|
118
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
|
+
}
|
|
119
125
|
function selectionShut(selection, doc) {
|
|
120
126
|
return selection.ranges.some(
|
|
121
|
-
(range) =>
|
|
127
|
+
(range) => markShut(doc, range.$from.pos, range.$to.pos)
|
|
122
128
|
);
|
|
123
129
|
}
|
|
124
130
|
function replacementShut(selection, doc) {
|
|
125
|
-
return selection.ranges.some(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return from === to ? insertionInsideLocked(doc, from) : rangeShut(doc, { from, to, takesAway: true });
|
|
129
|
-
});
|
|
131
|
+
return selection.ranges.some(
|
|
132
|
+
(range) => replaceShut(doc, range.$from.pos, range.$to.pos)
|
|
133
|
+
);
|
|
130
134
|
}
|
|
131
135
|
function editedRanges(step) {
|
|
132
136
|
if (step instanceof ReplaceAroundStep) {
|
|
@@ -185,16 +189,28 @@ function stepAllowed(step, doc) {
|
|
|
185
189
|
}
|
|
186
190
|
return true;
|
|
187
191
|
}
|
|
188
|
-
function
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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);
|
|
202
|
+
default: {
|
|
203
|
+
const unmodelled = intent;
|
|
204
|
+
return unmodelled;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
197
207
|
}
|
|
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
|
+
};
|
|
198
214
|
export {
|
|
199
215
|
carriesLock,
|
|
200
216
|
controlSpans,
|
|
@@ -202,10 +218,10 @@ export {
|
|
|
202
218
|
insertionInsideLocked,
|
|
203
219
|
insideLockedCell,
|
|
204
220
|
isLockedCell,
|
|
221
|
+
lockGuard,
|
|
205
222
|
lockedMarkOf,
|
|
206
223
|
rangeTouchesLocked,
|
|
207
224
|
replacementShut,
|
|
208
225
|
selectionShut,
|
|
209
|
-
transactionAllowed,
|
|
210
226
|
unlockAllowed
|
|
211
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
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The protection an editor runs under: what the document as a whole may receive, over and above
|
|
3
|
+
* the locks its own controls carry (`./locks`).
|
|
4
|
+
*
|
|
5
|
+
* The levels are named after OOXML `ST_DocProtect`, the enumeration `w:documentProtection/@w:edit`
|
|
6
|
+
* takes (ECMA-376 Part 1 §17.15.1.29): `readOnly` takes no edit at all, `comments` takes comments
|
|
7
|
+
* and nothing else, `none` takes every body edit. No level lifts the ownership rule over comments:
|
|
8
|
+
* whose comment may be edited is `editableComments`' question at every level, `none` included.
|
|
9
|
+
* `trackedChanges` and `forms`, the two values of the enumeration not modelled yet, would be added
|
|
10
|
+
* here.
|
|
11
|
+
*
|
|
12
|
+
* The protection is editor state rather than document state: it is the reader's standing, not the
|
|
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 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
|
+
*/
|
|
18
|
+
import { type Node as PMNode } from "prosemirror-model";
|
|
19
|
+
export type EditingProtection = "none" | "readOnly" | "comments";
|
|
20
|
+
/**
|
|
21
|
+
* Whose comments may be edited or deleted. `own` is a comment carrying no recognised identity, or
|
|
22
|
+
* the very identity comments are being written under; `all` is every comment there is, which is
|
|
23
|
+
* the standing of a moderator.
|
|
24
|
+
*/
|
|
25
|
+
export type EditableComments = "own" | "all";
|
|
26
|
+
export interface ProtectionState {
|
|
27
|
+
protection: EditingProtection;
|
|
28
|
+
/** The identity comments are written under. Null where nobody is writing any, a reader above all */
|
|
29
|
+
authorId: string | null;
|
|
30
|
+
editableComments: EditableComments;
|
|
31
|
+
}
|
|
32
|
+
/** Whether this node is one of the three a comment stands in the story as */
|
|
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;
|
|
41
|
+
/** Whether the two documents differ in nothing but their comments */
|
|
42
|
+
export declare function changesOnlyComments(before: PMNode, after: PMNode): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Whether a comment written under this identity may be edited or deleted.
|
|
45
|
+
*
|
|
46
|
+
* A comment carrying no recognised identity belongs to nobody in particular and stays open to
|
|
47
|
+
* everyone, which is what every comment was before identities were recorded and what a comment
|
|
48
|
+
* made in Word is; one carrying an identity is its author's alone.
|
|
49
|
+
*/
|
|
50
|
+
export declare function commentOwned(rules: ProtectionState, authorId: string | null): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Whether every comment and reply that was already there kept the identity it was written under.
|
|
53
|
+
*
|
|
54
|
+
* An identity is not an edit anyone may make, its own author included: a comment rewritten into
|
|
55
|
+
* another author's name would be that author's to edit from then on, and one rewritten into
|
|
56
|
+
* nobody's would be everyone's. Only a comment that appears carries an identity chosen for it.
|
|
57
|
+
*/
|
|
58
|
+
export declare function commentIdentitiesKept(before: PMNode, after: PMNode): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Whether every comment edited, moved or taken away between the two documents belonged to whoever
|
|
61
|
+
* did it. Adding a comment, replying, and settling a thread as resolved or open belong to
|
|
62
|
+
* everyone.
|
|
63
|
+
*
|
|
64
|
+
* Moving a comment onto other text is an edit of that comment rather than of the body, since the
|
|
65
|
+
* body reads the same afterwards, and it is the owner's to make.
|
|
66
|
+
* A root taken away takes its replies with it whoever wrote them: a thread hangs off its root,
|
|
67
|
+
* and the root's owner may take the thread down.
|
|
68
|
+
*/
|
|
69
|
+
export declare function commentEditsOwned(before: PMNode, after: PMNode, rules: ProtectionState): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Whether the protection lets a document go from `before` to `after`.
|
|
72
|
+
*
|
|
73
|
+
* Ownership is judged only over a change that is nothing but comments. A body edit that sweeps a
|
|
74
|
+
* comment away with the text it was anchored to is a body edit, and the standing to make one is
|
|
75
|
+
* the protection's question; under `comments` no such edit goes through in the first place.
|
|
76
|
+
*/
|
|
77
|
+
export declare function protectionAllows(before: PMNode, after: PMNode, rules: ProtectionState): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Whether every comment and reply that appeared between the two documents was written under this
|
|
80
|
+
* identity, and every one that was already there kept the identity it was written under. The
|
|
81
|
+
* editor takes an addition from anyone, since it writes the author itself; a server taking a file
|
|
82
|
+
* back does not, since the file could claim any author.
|
|
83
|
+
*/
|
|
84
|
+
export declare function commentAdditionsBy(before: PMNode, after: PMNode, authorId: string): boolean;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// src/schema/protection.ts
|
|
2
|
+
import { Fragment } from "prosemirror-model";
|
|
3
|
+
var COMMENT_NODES = /* @__PURE__ */ new Set([
|
|
4
|
+
"commentStart",
|
|
5
|
+
"commentEnd",
|
|
6
|
+
"commentReference"
|
|
7
|
+
]);
|
|
8
|
+
function isCommentNode(node) {
|
|
9
|
+
return COMMENT_NODES.has(node.type.name);
|
|
10
|
+
}
|
|
11
|
+
function withoutComments(node) {
|
|
12
|
+
if (node.isLeaf) return node;
|
|
13
|
+
const kept = [];
|
|
14
|
+
node.forEach((child) => {
|
|
15
|
+
if (!isCommentNode(child)) kept.push(withoutComments(child));
|
|
16
|
+
});
|
|
17
|
+
return node.copy(Fragment.fromArray(kept));
|
|
18
|
+
}
|
|
19
|
+
function changesOnlyComments(before, after) {
|
|
20
|
+
return withoutComments(before).eq(withoutComments(after));
|
|
21
|
+
}
|
|
22
|
+
function stringOrNull(value) {
|
|
23
|
+
return typeof value === "string" ? value : null;
|
|
24
|
+
}
|
|
25
|
+
function bodyOf(attrs) {
|
|
26
|
+
return {
|
|
27
|
+
authorId: stringOrNull(attrs.authorId),
|
|
28
|
+
author: stringOrNull(attrs.author),
|
|
29
|
+
initials: stringOrNull(attrs.initials),
|
|
30
|
+
date: stringOrNull(attrs.date),
|
|
31
|
+
text: stringOrNull(attrs.text) ?? ""
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function replyBodies(value) {
|
|
35
|
+
const replies = /* @__PURE__ */ new Map();
|
|
36
|
+
if (!Array.isArray(value)) return replies;
|
|
37
|
+
for (const entry of value) {
|
|
38
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
39
|
+
const reply = entry;
|
|
40
|
+
const id = stringOrNull(reply.id);
|
|
41
|
+
if (id === null) continue;
|
|
42
|
+
replies.set(id, bodyOf(reply));
|
|
43
|
+
}
|
|
44
|
+
return replies;
|
|
45
|
+
}
|
|
46
|
+
function threadsIn(doc) {
|
|
47
|
+
const threads = /* @__PURE__ */ new Map();
|
|
48
|
+
doc.descendants((node) => {
|
|
49
|
+
if (node.type.name !== "commentReference") return true;
|
|
50
|
+
const id = stringOrNull(node.attrs.id);
|
|
51
|
+
if (id === null || threads.has(id)) return true;
|
|
52
|
+
threads.set(id, {
|
|
53
|
+
...bodyOf(node.attrs),
|
|
54
|
+
replies: replyBodies(node.attrs.replies)
|
|
55
|
+
});
|
|
56
|
+
return true;
|
|
57
|
+
});
|
|
58
|
+
return threads;
|
|
59
|
+
}
|
|
60
|
+
function commentOwned(rules, authorId) {
|
|
61
|
+
if (rules.editableComments === "all") return true;
|
|
62
|
+
return authorId === null || authorId === rules.authorId;
|
|
63
|
+
}
|
|
64
|
+
function commentAnchors(doc) {
|
|
65
|
+
const markers = /* @__PURE__ */ new Map();
|
|
66
|
+
const visit = (node, start) => {
|
|
67
|
+
if (isCommentNode(node)) {
|
|
68
|
+
const id = stringOrNull(node.attrs.id);
|
|
69
|
+
if (id !== null) {
|
|
70
|
+
const at = markers.get(id) ?? [];
|
|
71
|
+
at.push(`${node.type.name}@${start}`);
|
|
72
|
+
markers.set(id, at);
|
|
73
|
+
}
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
if (node.isLeaf) return node.nodeSize;
|
|
77
|
+
let content = 0;
|
|
78
|
+
node.forEach((child) => {
|
|
79
|
+
content += visit(child, start + 1 + content);
|
|
80
|
+
});
|
|
81
|
+
return content + 2;
|
|
82
|
+
};
|
|
83
|
+
visit(doc, -1);
|
|
84
|
+
return new Map(
|
|
85
|
+
Array.from(markers, ([id, at]) => [id, at.join(" ")])
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
function sameIdentity(was, now) {
|
|
89
|
+
return was.authorId === now.authorId && was.author === now.author;
|
|
90
|
+
}
|
|
91
|
+
function sameWords(was, now) {
|
|
92
|
+
return was.text === now.text && was.initials === now.initials && was.date === now.date;
|
|
93
|
+
}
|
|
94
|
+
function commentIdentitiesKept(before, after) {
|
|
95
|
+
const now = threadsIn(after);
|
|
96
|
+
for (const [id, thread] of threadsIn(before)) {
|
|
97
|
+
const current = now.get(id);
|
|
98
|
+
if (current === void 0) continue;
|
|
99
|
+
if (!sameIdentity(thread, current)) return false;
|
|
100
|
+
for (const [replyId, reply] of thread.replies) {
|
|
101
|
+
const currentReply = current.replies.get(replyId);
|
|
102
|
+
if (currentReply !== void 0 && !sameIdentity(reply, currentReply)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
function commentEditsOwned(before, after, rules) {
|
|
110
|
+
if (!commentIdentitiesKept(before, after)) return false;
|
|
111
|
+
if (rules.editableComments === "all") return true;
|
|
112
|
+
const now = threadsIn(after);
|
|
113
|
+
const anchoredBefore = commentAnchors(before);
|
|
114
|
+
const anchoredAfter = commentAnchors(after);
|
|
115
|
+
for (const [id, thread] of threadsIn(before)) {
|
|
116
|
+
const current = now.get(id);
|
|
117
|
+
const changed = current === void 0 || !sameWords(thread, current) || anchoredBefore.get(id) !== anchoredAfter.get(id);
|
|
118
|
+
if (changed && !commentOwned(rules, thread.authorId)) return false;
|
|
119
|
+
if (current === void 0) continue;
|
|
120
|
+
for (const [replyId, reply] of thread.replies) {
|
|
121
|
+
const currentReply = current.replies.get(replyId);
|
|
122
|
+
const replyChanged = currentReply === void 0 || !sameWords(reply, currentReply);
|
|
123
|
+
if (replyChanged && !commentOwned(rules, reply.authorId)) return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
function protectionAllows(before, after, rules) {
|
|
129
|
+
switch (rules.protection) {
|
|
130
|
+
case "readOnly":
|
|
131
|
+
return false;
|
|
132
|
+
case "comments":
|
|
133
|
+
return changesOnlyComments(before, after) && commentEditsOwned(before, after, rules);
|
|
134
|
+
case "none":
|
|
135
|
+
return !changesOnlyComments(before, after) || commentEditsOwned(before, after, rules);
|
|
136
|
+
default: {
|
|
137
|
+
const unmodelled = rules.protection;
|
|
138
|
+
return unmodelled;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function commentAdditionsBy(before, after, authorId) {
|
|
143
|
+
if (!commentIdentitiesKept(before, after)) return false;
|
|
144
|
+
const was = threadsIn(before);
|
|
145
|
+
for (const [id, thread] of threadsIn(after)) {
|
|
146
|
+
const earlier = was.get(id);
|
|
147
|
+
if (earlier === void 0) {
|
|
148
|
+
if (thread.authorId !== authorId) return false;
|
|
149
|
+
for (const reply of thread.replies.values()) {
|
|
150
|
+
if (reply.authorId !== authorId) return false;
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
for (const [replyId, reply] of thread.replies) {
|
|
155
|
+
if (!earlier.replies.has(replyId) && reply.authorId !== authorId) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
export {
|
|
163
|
+
changesOnlyComments,
|
|
164
|
+
commentAdditionsBy,
|
|
165
|
+
commentEditsOwned,
|
|
166
|
+
commentIdentitiesKept,
|
|
167
|
+
commentOwned,
|
|
168
|
+
isCommentNode,
|
|
169
|
+
protectionAllows,
|
|
170
|
+
withoutComments
|
|
171
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the protection (`./protection`) stands in an editor state, and the questions commands ask
|
|
3
|
+
* of it. Kept apart from the judgements so that those stay free of editor state for the `./core`
|
|
4
|
+
* entry.
|
|
5
|
+
*/
|
|
6
|
+
import { type EditorState, PluginKey } from "prosemirror-state";
|
|
7
|
+
import type { EditingProtection, ProtectionState } from "./protection";
|
|
8
|
+
/** What a state built without the protection plugin runs under, which is what every consumer had before it existed */
|
|
9
|
+
export declare const UNPROTECTED: ProtectionState;
|
|
10
|
+
export declare const protectionKey: PluginKey<ProtectionState>;
|
|
11
|
+
export declare function protectionOf(state: EditorState): ProtectionState;
|
|
12
|
+
export declare function editingProtection(state: EditorState): EditingProtection;
|
|
13
|
+
/**
|
|
14
|
+
* Whether the protection shuts every edit to the body, comments aside.
|
|
15
|
+
*
|
|
16
|
+
* This is the question a command that changes text, formatting or structure asks before it reports
|
|
17
|
+
* that it applies, the way it asks the lock predicates (`./locks`): a command reporting true and
|
|
18
|
+
* then being refused by the guard draws a live control that swallows the click.
|
|
19
|
+
*/
|
|
20
|
+
export declare function editsShut(state: EditorState): boolean;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/schema/protectionState.ts
|
|
2
|
+
import { PluginKey } from "prosemirror-state";
|
|
3
|
+
var UNPROTECTED = {
|
|
4
|
+
protection: "none",
|
|
5
|
+
authorId: null,
|
|
6
|
+
editableComments: "own"
|
|
7
|
+
};
|
|
8
|
+
var protectionKey = new PluginKey(
|
|
9
|
+
"docxEditorProtection"
|
|
10
|
+
);
|
|
11
|
+
function protectionOf(state) {
|
|
12
|
+
return protectionKey.getState(state) ?? UNPROTECTED;
|
|
13
|
+
}
|
|
14
|
+
function editingProtection(state) {
|
|
15
|
+
return protectionOf(state).protection;
|
|
16
|
+
}
|
|
17
|
+
function editsShut(state) {
|
|
18
|
+
const protection = editingProtection(state);
|
|
19
|
+
switch (protection) {
|
|
20
|
+
case "none":
|
|
21
|
+
return false;
|
|
22
|
+
case "readOnly":
|
|
23
|
+
case "comments":
|
|
24
|
+
return true;
|
|
25
|
+
default: {
|
|
26
|
+
const unmodelled = protection;
|
|
27
|
+
return unmodelled;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
UNPROTECTED,
|
|
33
|
+
editingProtection,
|
|
34
|
+
editsShut,
|
|
35
|
+
protectionKey,
|
|
36
|
+
protectionOf
|
|
37
|
+
};
|