@portone/docx-editor 0.4.0 → 0.5.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 +201 -0
- package/CONTRIBUTING.md +2 -0
- package/assets/editor.png +0 -0
- package/dist/DocxEditor.d.ts +1 -1
- package/dist/DocxEditor.js +44 -25
- package/dist/docx/cloning.js +4 -9
- package/dist/docx/commentOnlyChange.d.ts +5 -3
- package/dist/docx/comments/grammar.d.ts +27 -10
- package/dist/docx/comments/grammar.js +18 -67
- package/dist/docx/comments/model.d.ts +5 -6
- package/dist/docx/comments/model.js +0 -9
- package/dist/docx/comments/parts.d.ts +16 -10
- package/dist/docx/comments/parts.js +65 -13
- package/dist/docx/comments/people.d.ts +11 -3
- package/dist/docx/comments/people.js +20 -6
- package/dist/docx/comments/policy.js +7 -1
- package/dist/docx/comments/reading.d.ts +4 -2
- package/dist/docx/comments/reading.js +1 -14
- package/dist/docx/comments/writing.js +65 -28
- package/dist/docx/exportDocx.js +10 -14
- package/dist/docx/exportRefs.d.ts +8 -3
- package/dist/docx/exportRefs.js +2 -1
- package/dist/docx/fidelity.js +24 -10
- package/dist/docx/fields.d.ts +42 -0
- package/dist/docx/fields.js +76 -0
- package/dist/docx/formatting/styles.d.ts +0 -2
- package/dist/docx/formatting/styles.js +2 -15
- package/dist/docx/headersFooters.d.ts +89 -20
- package/dist/docx/headersFooters.js +163 -172
- package/dist/docx/identities.js +17 -26
- package/dist/docx/importDocx.d.ts +1 -1
- package/dist/docx/importDocx.js +100 -48
- package/dist/docx/importParagraph.d.ts +18 -5
- package/dist/docx/importParagraph.js +75 -128
- package/dist/docx/importPolicy.d.ts +70 -0
- package/dist/docx/importPolicy.js +195 -0
- package/dist/docx/importPreserved.d.ts +30 -0
- package/dist/docx/importPreserved.js +54 -0
- package/dist/docx/importTable.js +56 -12
- package/dist/docx/invariants.js +16 -18
- package/dist/docx/notes.d.ts +6 -1
- package/dist/docx/notes.js +6 -16
- package/dist/docx/pageGeometry.d.ts +5 -14
- package/dist/docx/pageGeometry.js +0 -12
- package/dist/docx/protectionPolicy.d.ts +13 -5
- package/dist/docx/protectionPolicy.js +32 -22
- package/dist/docx/scan.d.ts +18 -8
- package/dist/docx/scan.js +17 -11
- package/dist/docx/sections.d.ts +126 -0
- package/dist/docx/sections.js +207 -0
- package/dist/docx/serializeBlock.d.ts +13 -4
- package/dist/docx/serializeBlock.js +16 -18
- package/dist/docx/serializeParagraph.d.ts +9 -4
- package/dist/docx/serializeParagraph.js +23 -58
- package/dist/docx/serializePreserved.d.ts +14 -0
- package/dist/docx/serializePreserved.js +24 -0
- package/dist/docx/serializeStory.d.ts +17 -0
- package/dist/docx/serializeStory.js +17 -0
- package/dist/docx/serializeTable.js +10 -13
- package/dist/docx/session.d.ts +23 -6
- package/dist/docx/session.js +26 -5
- package/dist/docx/story.d.ts +140 -0
- package/dist/docx/story.js +237 -0
- package/dist/docx/storyProjection.js +6 -1
- package/dist/docx/tableTemplate.d.ts +25 -0
- package/dist/docx/tableTemplate.js +30 -16
- package/dist/docx/wrappers.d.ts +67 -0
- package/dist/docx/wrappers.js +117 -0
- package/dist/editor/clipboard/blockReaders.d.ts +74 -0
- package/dist/editor/clipboard/blockReaders.js +193 -0
- package/dist/editor/clipboard/htmlReader.d.ts +26 -0
- package/dist/editor/clipboard/htmlReader.js +376 -0
- package/dist/editor/clipboard/inlineFormatting.d.ts +0 -7
- package/dist/editor/clipboard/inlineFormatting.js +1 -7
- package/dist/editor/clipboard/internalChannel.d.ts +57 -0
- package/dist/editor/clipboard/internalChannel.js +58 -0
- package/dist/editor/clipboard/normalizers.d.ts +99 -0
- package/dist/editor/clipboard/normalizers.js +199 -0
- package/dist/editor/clipboard/parser.d.ts +34 -0
- package/dist/editor/clipboard/parser.js +58 -0
- package/dist/editor/clipboard/plugin.d.ts +21 -0
- package/dist/editor/clipboard/plugin.js +188 -0
- package/dist/editor/clipboard/readContext.d.ts +34 -0
- package/dist/editor/clipboard/readContext.js +25 -0
- package/dist/editor/clipboard/readers.d.ts +33 -0
- package/dist/editor/clipboard/readers.js +37 -0
- package/dist/editor/clipboard/source.d.ts +12 -0
- package/dist/editor/clipboard/source.js +21 -0
- package/dist/editor/commands/comments/editing.d.ts +26 -6
- package/dist/editor/commands/comments/editing.js +177 -106
- package/dist/editor/commands/comments/model.d.ts +34 -17
- package/dist/editor/commands/comments/model.js +11 -0
- package/dist/editor/commands/comments/reading.d.ts +5 -3
- package/dist/editor/commands/comments/reading.js +8 -58
- package/dist/editor/commands/index.d.ts +2 -2
- package/dist/editor/commands/index.js +2 -0
- package/dist/editor/commands/linkCommands.js +20 -5
- package/dist/editor/commands/listCommands.d.ts +6 -0
- package/dist/editor/commands/listCommands.js +3 -2
- package/dist/editor/commands/lockCommands.js +59 -16
- package/dist/editor/commands/noteQueries.d.ts +22 -5
- package/dist/editor/commands/noteQueries.js +34 -6
- package/dist/editor/commands/paragraphCommands.js +2 -2
- package/dist/editor/createEditor.js +13 -3
- package/dist/editor/documentStyles.d.ts +26 -4
- package/dist/editor/documentStyles.js +9 -4
- package/dist/editor/imageFiles.d.ts +2 -2
- package/dist/editor/imageFiles.js +2 -0
- package/dist/editor/insertTable.d.ts +1 -1
- package/dist/editor/insertTable.js +2 -2
- package/dist/editor/plainText.d.ts +12 -0
- package/dist/editor/plainText.js +2 -1
- package/dist/editor/plugins/columnResize.js +6 -3
- package/dist/editor/plugins/commentComposer.d.ts +28 -0
- package/dist/editor/plugins/commentComposer.js +46 -0
- package/dist/editor/plugins/commentDecorations.d.ts +29 -2
- package/dist/editor/plugins/commentDecorations.js +89 -15
- package/dist/editor/plugins/documentProjection.d.ts +31 -0
- package/dist/editor/plugins/documentProjection.js +22 -0
- package/dist/editor/plugins/imagePaste.js +15 -8
- package/dist/editor/plugins/linkPanel.d.ts +7 -3
- package/dist/editor/plugins/linkPanel.js +13 -27
- package/dist/editor/plugins/panelState.d.ts +67 -0
- package/dist/editor/plugins/panelState.js +54 -0
- package/dist/editor/plugins/tableContextMenu.d.ts +1 -1
- package/dist/editor/plugins/tableContextMenu.js +30 -47
- package/dist/editor/plugins/textContextMenu.d.ts +1 -1
- package/dist/editor/plugins/textContextMenu.js +30 -47
- package/dist/ooxml/fragment.d.ts +11 -0
- package/dist/ooxml/fragment.js +14 -4
- package/dist/ooxml/image.d.ts +3 -4
- package/dist/ooxml/image.js +50 -5
- package/dist/ooxml/names.d.ts +6 -0
- package/dist/ooxml/names.js +2 -0
- package/dist/ooxml/props.d.ts +10 -0
- package/dist/ooxml/props.js +28 -4
- package/dist/ooxml/rangeMarkers.d.ts +5 -0
- package/dist/ooxml/rangeMarkers.js +24 -0
- package/dist/page/PageGuides.d.ts +7 -3
- package/dist/page/PageGuides.js +13 -24
- package/dist/page/pageLayout.d.ts +64 -5
- package/dist/page/pageLayout.js +90 -29
- package/dist/page/usePageLayout.d.ts +14 -4
- package/dist/page/usePageLayout.js +19 -20
- package/dist/schema/attrRoles.js +46 -23
- package/dist/schema/clipboard.d.ts +72 -0
- package/dist/schema/clipboard.js +288 -0
- package/dist/schema/docxSchema.d.ts +9 -1
- package/dist/schema/docxSchema.js +217 -165
- package/dist/schema/editGuard.d.ts +1 -1
- package/dist/schema/guards.js +2 -2
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/locks.d.ts +3 -0
- package/dist/schema/locks.js +29 -12
- package/dist/schema/preservedFragments.d.ts +12 -0
- package/dist/schema/preservedFragments.js +38 -0
- package/dist/schema/preservedGuards.d.ts +9 -7
- package/dist/schema/preservedGuards.js +112 -11
- package/dist/schema/protection.d.ts +34 -5
- package/dist/schema/protection.js +42 -14
- package/dist/schema/rendering.d.ts +14 -0
- package/dist/schema/rendering.js +27 -12
- package/dist/schema/stories.d.ts +49 -0
- package/dist/schema/stories.js +78 -0
- package/dist/schema/wrappers.d.ts +58 -0
- package/dist/schema/wrappers.js +77 -0
- package/dist/styles/classNames.d.ts +3 -5
- package/dist/styles/classNames.js +3 -5
- package/dist/styles.css +74 -12
- package/dist/table/resize.d.ts +5 -3
- package/dist/ui/CommentsPanel.d.ts +1 -3
- package/dist/ui/CommentsPanel.js +24 -15
- package/dist/ui/NotesPanel.js +2 -2
- package/dist/ui/TextMenu.d.ts +1 -3
- package/dist/ui/TextMenu.js +10 -6
- package/dist/ui/comments/CommentComposer.d.ts +6 -1
- package/dist/ui/comments/CommentComposer.js +26 -3
- package/package.json +5 -4
- package/dist/editor/externalClipboard.d.ts +0 -22
- package/dist/editor/externalClipboard.js +0 -486
|
@@ -46,7 +46,7 @@ export type EditIntent =
|
|
|
46
46
|
* against the places it puts that guard to the test, and a place naming a guard that no longer
|
|
47
47
|
* stands has to be a mistake the compiler catches rather than an annotation answering for nothing.
|
|
48
48
|
*/
|
|
49
|
-
export type EditGuardName = "protection" | "lock" | "
|
|
49
|
+
export type EditGuardName = "protection" | "lock" | "preserved" | "note" | "section";
|
|
50
50
|
/** What every guard answers, whichever of the two judgements it is written as */
|
|
51
51
|
interface GuardCommon {
|
|
52
52
|
readonly name: EditGuardName;
|
package/dist/schema/guards.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
transactionReaches
|
|
5
5
|
} from "./editGuard.js";
|
|
6
6
|
import { lockGuard } from "./locks.js";
|
|
7
|
-
import {
|
|
7
|
+
import { noteGuard, preservedGuard, sectionGuard } from "./preservedGuards.js";
|
|
8
8
|
import {
|
|
9
9
|
isCommentNode,
|
|
10
10
|
protectionAllows
|
|
@@ -36,7 +36,7 @@ var protectionGuard = {
|
|
|
36
36
|
var EDIT_GUARDS = [
|
|
37
37
|
protectionGuard,
|
|
38
38
|
lockGuard,
|
|
39
|
-
|
|
39
|
+
preservedGuard,
|
|
40
40
|
noteGuard,
|
|
41
41
|
sectionGuard
|
|
42
42
|
];
|
package/dist/schema/index.d.ts
CHANGED
package/dist/schema/locks.d.ts
CHANGED
|
@@ -56,6 +56,9 @@ export interface ControlSpan extends StepRange {
|
|
|
56
56
|
* A control that wrapped several runs comes in as several inlines wearing the very same mark, and
|
|
57
57
|
* what a lock answers for is the control rather than the run: a stretch covering one run of a
|
|
58
58
|
* control whole still covers only a part of the control.
|
|
59
|
+
*
|
|
60
|
+
* A control standing inside another gives a span of its own, the outer one first, and the two
|
|
61
|
+
* overlap: each locks on its own terms.
|
|
59
62
|
*/
|
|
60
63
|
export declare function controlSpans(block: Textblock): ControlSpan[];
|
|
61
64
|
/**
|
package/dist/schema/locks.js
CHANGED
|
@@ -10,11 +10,12 @@ import {
|
|
|
10
10
|
ReplaceStep
|
|
11
11
|
} from "prosemirror-transform";
|
|
12
12
|
import { docxSchema } from "./index.js";
|
|
13
|
+
import { wrappersOf } from "./wrappers.js";
|
|
13
14
|
var unlockAllowed = new PluginKey("docxEditorUnlockAllowed");
|
|
14
15
|
var historyReplay = new PluginKey("docxEditorHistoryReplay");
|
|
15
16
|
var OPEN = { contents: false, deletion: false };
|
|
16
|
-
function
|
|
17
|
-
return node
|
|
17
|
+
function sdtMarksOf(node) {
|
|
18
|
+
return node ? wrappersOf(node, docxSchema.marks.sdt) : [];
|
|
18
19
|
}
|
|
19
20
|
function markLocks(mark) {
|
|
20
21
|
return {
|
|
@@ -23,8 +24,7 @@ function markLocks(mark) {
|
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
function lockedMarkOf(node) {
|
|
26
|
-
|
|
27
|
-
return mark !== null && markLocks(mark).contents ? mark : null;
|
|
27
|
+
return sdtMarksOf(node).find((mark) => markLocks(mark).contents) ?? null;
|
|
28
28
|
}
|
|
29
29
|
var CELL_CONTENTS_ATTR = "sdtContentsLocked";
|
|
30
30
|
var CELL_DELETION_ATTR = "sdtDeletionLocked";
|
|
@@ -42,20 +42,37 @@ function isLockedCell(node) {
|
|
|
42
42
|
return cellLocks(node).contents;
|
|
43
43
|
}
|
|
44
44
|
function carriesLock(node) {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
const marks = node.isInline ? sdtMarksOf(node) : [];
|
|
46
|
+
if (marks.length === 0) {
|
|
47
|
+
const locks = cellLocks(node);
|
|
48
|
+
return locks.contents || locks.deletion;
|
|
49
|
+
}
|
|
50
|
+
return marks.some((mark) => {
|
|
51
|
+
const locks = markLocks(mark);
|
|
52
|
+
return locks.contents || locks.deletion;
|
|
53
|
+
});
|
|
48
54
|
}
|
|
49
55
|
function controlSpans(block) {
|
|
50
56
|
const spans = [];
|
|
57
|
+
let reaching = [];
|
|
51
58
|
block.node.forEach((child, offset) => {
|
|
52
|
-
const mark = sdtMarkOf(child);
|
|
53
|
-
if (!mark) return;
|
|
54
59
|
const from = block.start + offset;
|
|
55
60
|
const to = from + child.nodeSize;
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
const standing = [];
|
|
62
|
+
for (const mark of sdtMarksOf(child)) {
|
|
63
|
+
const carried = reaching.find(
|
|
64
|
+
(span) => span.to === from && span.mark.eq(mark)
|
|
65
|
+
);
|
|
66
|
+
if (carried) {
|
|
67
|
+
carried.to = to;
|
|
68
|
+
standing.push(carried);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const started = { from, to, mark };
|
|
72
|
+
spans.push(started);
|
|
73
|
+
standing.push(started);
|
|
74
|
+
}
|
|
75
|
+
reaching = standing;
|
|
59
76
|
});
|
|
60
77
|
return spans;
|
|
61
78
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Node as PMNode } from "prosemirror-model";
|
|
2
|
+
type MarkerAttr = "leadingXml" | "trailingXml";
|
|
3
|
+
/**
|
|
4
|
+
* Whether this node is content the editor keeps as it came rather than one it models.
|
|
5
|
+
*
|
|
6
|
+
* The block placeholder declares it as a group; the two inline ones cannot, since their group says
|
|
7
|
+
* where they may stand, so they are named here and asked for by name everywhere else.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isPreservedNode(node: PMNode): boolean;
|
|
10
|
+
/** Visit preserved XML in the order the writer places it around editable content. */
|
|
11
|
+
export declare function visitPreservedFragments(doc: PMNode, visit: (node: PMNode, pos: number, xml: string | null, attr?: MarkerAttr) => void): void;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// src/schema/preservedFragments.ts
|
|
2
|
+
function isPreservedNode(node) {
|
|
3
|
+
return node.type.name === "rawInline" || node.type.name === "rawRunContent" || node.type.isInGroup("preserved");
|
|
4
|
+
}
|
|
5
|
+
function visitPreservedFragments(doc, visit) {
|
|
6
|
+
const visitAttr = (node, pos, attr) => {
|
|
7
|
+
const xml = node.attrs[attr];
|
|
8
|
+
if (typeof xml === "string" && xml.length > 0) {
|
|
9
|
+
visit(node, pos, xml, attr);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const walk = (node, pos) => {
|
|
13
|
+
if (isPreservedNode(node)) {
|
|
14
|
+
visit(
|
|
15
|
+
node,
|
|
16
|
+
pos,
|
|
17
|
+
typeof node.attrs.xml === "string" ? node.attrs.xml : null
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
if (node.type.name === "table" || node.type.name === "tableRow") {
|
|
21
|
+
visitAttr(node, pos, "leadingXml");
|
|
22
|
+
}
|
|
23
|
+
node.forEach((child, offset) => {
|
|
24
|
+
walk(child, pos + 1 + offset);
|
|
25
|
+
});
|
|
26
|
+
if (node.type.name === "tableRow" || node.type.name === "tableCell") {
|
|
27
|
+
visitAttr(node, pos, "trailingXml");
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
doc.descendants((node, pos) => {
|
|
31
|
+
walk(node, pos);
|
|
32
|
+
return false;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
isPreservedNode,
|
|
37
|
+
visitPreservedFragments
|
|
38
|
+
};
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The guards over what a document is opened with and the editor only preserves: the
|
|
3
|
-
*
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
32
|
-
return node.type.name
|
|
46
|
+
function preservedSignature(node) {
|
|
47
|
+
return `${node.type.name}:${node.attrs.name}:${node.attrs.srcId}:${node.attrs.xml}`;
|
|
33
48
|
}
|
|
34
|
-
var
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
177
|
+
if (!allowed(thread)) return false;
|
|
150
178
|
for (const reply of thread.replies.values()) {
|
|
151
|
-
if (reply
|
|
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
|
|
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
|
};
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import type { Attrs, DOMOutputSpec } from "prosemirror-model";
|
|
2
2
|
import type { FontFallbacks } from "../styles/fontStack";
|
|
3
|
+
/**
|
|
4
|
+
* What a run says to anyone reading it, the page and a copy of it alike.
|
|
5
|
+
*
|
|
6
|
+
* The rest of the drawing is this editor talking to itself - the original XML, the worked-out
|
|
7
|
+
* formatting it was drawn from - and `schema/clipboard` publishes only what is here.
|
|
8
|
+
*/
|
|
9
|
+
export declare function runAttrs(attrs: Attrs, fontFallbacks: FontFallbacks): Record<string, string | undefined>;
|
|
3
10
|
/** The DOM representation shared by the schema and the editor's custom run mark view. */
|
|
4
11
|
export declare function runMarkSpec(attrs: Attrs, fontFallbacks: FontFallbacks): DOMOutputSpec;
|
|
12
|
+
/**
|
|
13
|
+
* What an image says to anyone reading it, the page and a copy of it alike.
|
|
14
|
+
*
|
|
15
|
+
* The size is given in the pixels it is drawn at rather than as the document's own measure in EMU,
|
|
16
|
+
* which is what makes it the half another application can read.
|
|
17
|
+
*/
|
|
18
|
+
export declare function imageAttrs(attrs: Attrs): Record<string, string | undefined>;
|
|
5
19
|
/** The image element shared by the schema and the resizable image node view. */
|
|
6
20
|
export declare function imageNodeSpec(attrs: Attrs): DOMOutputSpec;
|
package/dist/schema/rendering.js
CHANGED
|
@@ -9,38 +9,53 @@ function text(value) {
|
|
|
9
9
|
function formatJson(format) {
|
|
10
10
|
return format ? JSON.stringify(format) : void 0;
|
|
11
11
|
}
|
|
12
|
+
function runAttrs(attrs, fontFallbacks) {
|
|
13
|
+
const format = toRunFormat(attrs.format);
|
|
14
|
+
return {
|
|
15
|
+
class: editorClassNames.run,
|
|
16
|
+
style: runStyle(format, fontFallbacks),
|
|
17
|
+
// Which shape of a Han character the browser draws is decided by this and nothing else
|
|
18
|
+
lang: format?.lang
|
|
19
|
+
};
|
|
20
|
+
}
|
|
12
21
|
function runMarkSpec(attrs, fontFallbacks) {
|
|
13
22
|
const format = toRunFormat(attrs.format);
|
|
14
23
|
return [
|
|
15
24
|
"span",
|
|
16
25
|
{
|
|
17
|
-
|
|
18
|
-
style: runStyle(format, fontFallbacks),
|
|
19
|
-
// Which shape of a Han character the browser draws is decided by this and nothing else
|
|
20
|
-
lang: format?.lang,
|
|
26
|
+
...runAttrs(attrs, fontFallbacks),
|
|
21
27
|
"data-rattrs": text(attrs.rAttrs),
|
|
22
28
|
"data-rpr": text(attrs.rPr),
|
|
23
|
-
"data-fmt": formatJson(format)
|
|
29
|
+
"data-fmt": formatJson(format),
|
|
30
|
+
// Read by `styles/editor.css` to stand a link's own line down
|
|
31
|
+
"data-underline": format?.underline && format.underline !== "none" ? "" : void 0
|
|
24
32
|
},
|
|
25
33
|
0
|
|
26
34
|
];
|
|
27
35
|
}
|
|
28
|
-
function
|
|
36
|
+
function imageAttrs(attrs) {
|
|
29
37
|
const extent = toImageExtent(attrs.extent);
|
|
38
|
+
return {
|
|
39
|
+
class: editorClassNames.image,
|
|
40
|
+
src: toImageSrc(attrs.src) ?? void 0,
|
|
41
|
+
alt: text(attrs.alt) ?? "",
|
|
42
|
+
width: extent ? `${Math.round(emuToPx(extent.cx))}` : void 0,
|
|
43
|
+
height: extent ? `${Math.round(emuToPx(extent.cy))}` : void 0
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function imageNodeSpec(attrs) {
|
|
30
47
|
return [
|
|
31
48
|
"img",
|
|
32
49
|
{
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
alt: text(attrs.alt) ?? "",
|
|
36
|
-
width: extent ? `${Math.round(emuToPx(extent.cx))}` : void 0,
|
|
37
|
-
height: extent ? `${Math.round(emuToPx(extent.cy))}` : void 0,
|
|
38
|
-
"data-extent": formatJson(extent),
|
|
50
|
+
...imageAttrs(attrs),
|
|
51
|
+
"data-extent": formatJson(toImageExtent(attrs.extent)),
|
|
39
52
|
"data-xml": text(attrs.xml)
|
|
40
53
|
}
|
|
41
54
|
];
|
|
42
55
|
}
|
|
43
56
|
export {
|
|
57
|
+
imageAttrs,
|
|
44
58
|
imageNodeSpec,
|
|
59
|
+
runAttrs,
|
|
45
60
|
runMarkSpec
|
|
46
61
|
};
|
|
@@ -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;
|