@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
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// src/editor/clipboard/internalChannel.ts
|
|
2
|
+
import { docxSchema } from "../../schema/index.js";
|
|
3
|
+
import { listKindOf, listRefOf } from "../commands/listCommands.js";
|
|
4
|
+
var INTERNAL_TOKEN_ATTRIBUTE = "data-docx-clip";
|
|
5
|
+
var NO_LIST_KINDS = /* @__PURE__ */ new Map();
|
|
6
|
+
var copied = /* @__PURE__ */ new Map();
|
|
7
|
+
var copies = 0;
|
|
8
|
+
function nextToken() {
|
|
9
|
+
copies += 1;
|
|
10
|
+
return `c${copies}-${Math.random().toString(36).slice(2, 10)}`;
|
|
11
|
+
}
|
|
12
|
+
function rememberCopied(route, slice, sessionId, numbering) {
|
|
13
|
+
if (sessionId === null) return null;
|
|
14
|
+
const token = nextToken();
|
|
15
|
+
copied.set(route, {
|
|
16
|
+
token,
|
|
17
|
+
sessionId,
|
|
18
|
+
slice,
|
|
19
|
+
listKinds: listKindsIn(slice, numbering)
|
|
20
|
+
});
|
|
21
|
+
return token;
|
|
22
|
+
}
|
|
23
|
+
function recallCopied(token, sessionId) {
|
|
24
|
+
if (token === null || sessionId === null) return null;
|
|
25
|
+
const found = [...copied.values()].find(
|
|
26
|
+
(copy) => copy.token === token && copy.sessionId === sessionId
|
|
27
|
+
);
|
|
28
|
+
return found === void 0 ? null : { slice: found.slice, listKinds: found.listKinds };
|
|
29
|
+
}
|
|
30
|
+
function listKindsIn(slice, numbering) {
|
|
31
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
32
|
+
const read = (node) => {
|
|
33
|
+
if (node.type === docxSchema.nodes.paragraph) {
|
|
34
|
+
const ref = listRefOf(node);
|
|
35
|
+
const kind = ref === null ? null : listKindOf(numbering, ref);
|
|
36
|
+
if (ref !== null && kind !== null) kinds.set(ref.numId, kind);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
node.forEach(read);
|
|
40
|
+
};
|
|
41
|
+
slice.content.forEach(read);
|
|
42
|
+
return kinds.size === 0 ? NO_LIST_KINDS : kinds;
|
|
43
|
+
}
|
|
44
|
+
function internalTokenOf(dom) {
|
|
45
|
+
if (dom instanceof Element) {
|
|
46
|
+
const own = dom.getAttribute(INTERNAL_TOKEN_ATTRIBUTE);
|
|
47
|
+
if (own !== null) return own;
|
|
48
|
+
}
|
|
49
|
+
const first = dom instanceof Element || dom instanceof DocumentFragment ? dom.firstElementChild : null;
|
|
50
|
+
return first?.getAttribute(INTERNAL_TOKEN_ATTRIBUTE) ?? null;
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
INTERNAL_TOKEN_ATTRIBUTE,
|
|
54
|
+
NO_LIST_KINDS,
|
|
55
|
+
internalTokenOf,
|
|
56
|
+
recallCopied,
|
|
57
|
+
rememberCopied
|
|
58
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a slice has to obey before it may be put into this document.
|
|
3
|
+
*
|
|
4
|
+
* A slice reaching a paste says where its blocks came from, which list its paragraphs belong to,
|
|
5
|
+
* which comment its markers open, and what everything is drawn with. None of those mean anything
|
|
6
|
+
* outside the document that wrote them: a block naming a fragment of the file would be written
|
|
7
|
+
* from that fragment a second time, a list number naming no definition here would be exported
|
|
8
|
+
* bare, and a comment marker copied along would open a range the comment part never opens. So a
|
|
9
|
+
* paste is a normalization rather than an insertion, and each rule stands as one normalizer over
|
|
10
|
+
* the whole slice, in the order they are declared.
|
|
11
|
+
*
|
|
12
|
+
* A move drop is the exception the first two rules read: content dragged from one place in this
|
|
13
|
+
* very document to another is the same content, so it keeps the identity it had and the anchors it
|
|
14
|
+
* stood in. The source of a move is deleted along with the drop, so nothing is duplicated.
|
|
15
|
+
*/
|
|
16
|
+
import { type Node as PMNode, Slice } from "prosemirror-model";
|
|
17
|
+
import type { EditorState } from "prosemirror-state";
|
|
18
|
+
import { type ListKind } from "../../numbering/listTemplate";
|
|
19
|
+
import type { PastedContent } from "./htmlReader";
|
|
20
|
+
import { type ListKinds } from "./internalChannel";
|
|
21
|
+
/** What one normalizer answers about the document the slice is going into */
|
|
22
|
+
export interface NormalizeContext {
|
|
23
|
+
readonly state: EditorState;
|
|
24
|
+
/** A drop that moves what it carries rather than copying it, which keeps its source identity */
|
|
25
|
+
readonly move: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* The list numbers this document answers for: the ones it defines, the ones a list started
|
|
28
|
+
* while editing was registered under, the ones its paragraphs wear, and the ones this very
|
|
29
|
+
* paste has started.
|
|
30
|
+
*/
|
|
31
|
+
readonly knownLists: ReadonlySet<number>;
|
|
32
|
+
/**
|
|
33
|
+
* What the numbers the slice names meant where it was copied, for the numbers this document
|
|
34
|
+
* cannot answer for. Empty for a slice that came in as markup, whose lists were given numbers
|
|
35
|
+
* of this document as they were read.
|
|
36
|
+
*/
|
|
37
|
+
readonly listKinds: ListKinds;
|
|
38
|
+
/**
|
|
39
|
+
* A number for a list this slice starts, registered with the definition it is started with, as
|
|
40
|
+
* the list button starts one. Null in a document with nowhere to write a definition.
|
|
41
|
+
*/
|
|
42
|
+
startList(kind: ListKind): number | null;
|
|
43
|
+
}
|
|
44
|
+
export type SliceNormalizer = (slice: Slice, context: NormalizeContext) => Slice;
|
|
45
|
+
/** The slice with every node rewritten depth first, and the ones mapped to null taken out */
|
|
46
|
+
export declare function mapSliceNodes(slice: Slice, f: (node: PMNode) => PMNode | null): Slice;
|
|
47
|
+
/**
|
|
48
|
+
* Takes the source identity off what is pasted.
|
|
49
|
+
*
|
|
50
|
+
* A block names the fragment of the opened file it came from, and the export writes that fragment
|
|
51
|
+
* back verbatim rather than building one. Two blocks naming the same fragment would therefore go
|
|
52
|
+
* out as the same bytes twice, identifiers and all, so a pasted copy names none and is written
|
|
53
|
+
* from the model instead. A preserved block holding its own XML is that XML wherever it stands and
|
|
54
|
+
* travels whole; one that only names a fragment has nothing of its own to travel with, so it goes.
|
|
55
|
+
*/
|
|
56
|
+
export declare const dropSourceIdentity: SliceNormalizer;
|
|
57
|
+
/**
|
|
58
|
+
* Gives a pasted list a number this document defines.
|
|
59
|
+
*
|
|
60
|
+
* A number the destination knows nothing about is exported as a list nothing defines, which the
|
|
61
|
+
* export invariants refuse. The paragraphs that shared such a number still share the one they are
|
|
62
|
+
* given, so a pasted list stays one list, started the way the list button starts one and drawn and
|
|
63
|
+
* written from the destination's own definition from here on.
|
|
64
|
+
*
|
|
65
|
+
* Which of the two kinds it is started as is the one thing the number alone does not say. A copy
|
|
66
|
+
* made in this same session was remembered alongside what its numbers meant there
|
|
67
|
+
* (`./internalChannel`), so a bulleted list comes back bulleted; a number arriving any other way
|
|
68
|
+
* has no kind to be read off it and counts.
|
|
69
|
+
*/
|
|
70
|
+
export declare const rekeyNumbering: SliceNormalizer;
|
|
71
|
+
/**
|
|
72
|
+
* Takes the anchors off what is pasted.
|
|
73
|
+
*
|
|
74
|
+
* A comment marker, a bookmark and a note reference all point at something written elsewhere in
|
|
75
|
+
* the package: the comment, the bookmark's other end, the note's body. A copy of the marker alone
|
|
76
|
+
* points at the same thing a second time, which is a duplicate identifier the file may not hold
|
|
77
|
+
* and a range the reader cannot close. Nothing here duplicates what they point at, so the anchor
|
|
78
|
+
* goes and the text it stood in stays, which is what Word does with a note it has no body for.
|
|
79
|
+
*
|
|
80
|
+
* A move drop duplicates nothing: the source goes as the drop lands, so the one anchor there was
|
|
81
|
+
* travels with the text it opened. Taking it off would delete a marker the document has no way to
|
|
82
|
+
* write back, which is a change the preserved guard refuses whole, and the drag would do nothing.
|
|
83
|
+
*/
|
|
84
|
+
export declare const detachAnchors: SliceNormalizer;
|
|
85
|
+
/**
|
|
86
|
+
* Works out again what the pasted paragraphs are drawn with.
|
|
87
|
+
*
|
|
88
|
+
* The values a paragraph and the runs in it carry were resolved against the styles, the defaults
|
|
89
|
+
* and the list definitions of wherever the slice came from, this document's own list definitions
|
|
90
|
+
* after the numbers have just been reissued among them. Resolving them here is what makes the
|
|
91
|
+
* numbering the paste registers the numbering the document is then found to wear.
|
|
92
|
+
*/
|
|
93
|
+
export declare const rederiveDisplay: SliceNormalizer;
|
|
94
|
+
export declare const DEFAULT_NORMALIZERS: readonly SliceNormalizer[];
|
|
95
|
+
/**
|
|
96
|
+
* The pasted content as this document may hold it, and the definitions of the lists the paste
|
|
97
|
+
* starts - the ones a reading started, and the ones a reissued number was taken for.
|
|
98
|
+
*/
|
|
99
|
+
export declare function normalizePasted(content: PastedContent, state: EditorState, move: boolean, normalizers?: readonly SliceNormalizer[]): PastedContent;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// src/editor/clipboard/normalizers.ts
|
|
2
|
+
import {
|
|
3
|
+
Fragment,
|
|
4
|
+
Mark,
|
|
5
|
+
Slice
|
|
6
|
+
} from "prosemirror-model";
|
|
7
|
+
import {
|
|
8
|
+
paragraphAttrsOf,
|
|
9
|
+
resolveParagraph,
|
|
10
|
+
resolveRun
|
|
11
|
+
} from "../../docx/formatting.js";
|
|
12
|
+
import { withListNumbering } from "../../docx/paraProps.js";
|
|
13
|
+
import {
|
|
14
|
+
allocateList,
|
|
15
|
+
templateList
|
|
16
|
+
} from "../../numbering/listTemplate.js";
|
|
17
|
+
import {
|
|
18
|
+
COMMENT_RANGE_MARKERS,
|
|
19
|
+
PERMISSION_MARKERS,
|
|
20
|
+
RANGE_MARKERS
|
|
21
|
+
} from "../../ooxml/rangeMarkers.js";
|
|
22
|
+
import { docxSchema } from "../../schema/index.js";
|
|
23
|
+
import { listRefOf, numIdsIn } from "../commands/listCommands.js";
|
|
24
|
+
import { documentFormatting } from "../documentStyles.js";
|
|
25
|
+
import {
|
|
26
|
+
canStartNewList,
|
|
27
|
+
documentNumbering
|
|
28
|
+
} from "../plugins/numberingDecorations.js";
|
|
29
|
+
import { NO_LIST_KINDS } from "./internalChannel.js";
|
|
30
|
+
function xmlAttr(value) {
|
|
31
|
+
return typeof value === "string" ? value : null;
|
|
32
|
+
}
|
|
33
|
+
function withAttrs(node, attrs) {
|
|
34
|
+
return node.type.create(attrs, node.content, node.marks);
|
|
35
|
+
}
|
|
36
|
+
function mapFragment(fragment, openStart, openEnd, f) {
|
|
37
|
+
const mapped = [];
|
|
38
|
+
fragment.forEach((child, _offset, index) => {
|
|
39
|
+
const first = index === 0;
|
|
40
|
+
const last = index === fragment.childCount - 1;
|
|
41
|
+
const inside = child.isText ? child : child.type.create(
|
|
42
|
+
child.attrs,
|
|
43
|
+
mapFragment(
|
|
44
|
+
child.content,
|
|
45
|
+
first ? openStart - 1 : 0,
|
|
46
|
+
last ? openEnd - 1 : 0,
|
|
47
|
+
f
|
|
48
|
+
),
|
|
49
|
+
child.marks
|
|
50
|
+
);
|
|
51
|
+
const next = f(inside);
|
|
52
|
+
if (next !== null) mapped.push(next);
|
|
53
|
+
else if (first && openStart > 0 || last && openEnd > 0) {
|
|
54
|
+
mapped.push(docxSchema.nodes.paragraph.create());
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
return Fragment.fromArray(mapped);
|
|
58
|
+
}
|
|
59
|
+
function mapSliceNodes(slice, f) {
|
|
60
|
+
return new Slice(
|
|
61
|
+
mapFragment(slice.content, slice.openStart, slice.openEnd, f),
|
|
62
|
+
slice.openStart,
|
|
63
|
+
slice.openEnd
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
var dropSourceIdentity = (slice, { move }) => move ? slice : mapSliceNodes(slice, (node) => {
|
|
67
|
+
if (node.type === docxSchema.nodes.rawBlock) {
|
|
68
|
+
return node.attrs.xml === null ? null : withAttrs(node, { ...node.attrs, srcId: null });
|
|
69
|
+
}
|
|
70
|
+
if (node.type !== docxSchema.nodes.paragraph && node.type !== docxSchema.nodes.table) {
|
|
71
|
+
return node;
|
|
72
|
+
}
|
|
73
|
+
return node.attrs.srcId === null ? node : withAttrs(node, { ...node.attrs, srcId: null });
|
|
74
|
+
});
|
|
75
|
+
var rekeyNumbering = (slice, context) => {
|
|
76
|
+
const given = /* @__PURE__ */ new Map();
|
|
77
|
+
return mapSliceNodes(slice, (node) => {
|
|
78
|
+
if (node.type !== docxSchema.nodes.paragraph) return node;
|
|
79
|
+
const ref = listRefOf(node);
|
|
80
|
+
if (ref === null || context.knownLists.has(ref.numId)) return node;
|
|
81
|
+
const taken = given.has(ref.numId) ? given.get(ref.numId) ?? null : context.startList(context.listKinds.get(ref.numId) ?? "numbered");
|
|
82
|
+
given.set(ref.numId, taken);
|
|
83
|
+
const props = taken === null ? withListNumbering(node.attrs.pPr, {
|
|
84
|
+
numbering: null,
|
|
85
|
+
indent: { kind: "clearHanging" }
|
|
86
|
+
}) : withListNumbering(node.attrs.pPr, {
|
|
87
|
+
numbering: { numId: taken, ilvl: ref.ilvl },
|
|
88
|
+
indent: { kind: "keep" }
|
|
89
|
+
});
|
|
90
|
+
return props === null ? node : withAttrs(node, { ...node.attrs, pPr: props.pPr });
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
var ANCHOR_NODES = /* @__PURE__ */ new Set([
|
|
94
|
+
"commentStart",
|
|
95
|
+
"commentEnd",
|
|
96
|
+
"commentReference",
|
|
97
|
+
"noteReference"
|
|
98
|
+
]);
|
|
99
|
+
var REFERENCE_ELEMENTS = [
|
|
100
|
+
"commentReference",
|
|
101
|
+
"footnoteReference",
|
|
102
|
+
"endnoteReference"
|
|
103
|
+
];
|
|
104
|
+
var ANCHOR_ELEMENTS = /* @__PURE__ */ new Set([
|
|
105
|
+
...RANGE_MARKERS,
|
|
106
|
+
...PERMISSION_MARKERS,
|
|
107
|
+
...COMMENT_RANGE_MARKERS,
|
|
108
|
+
...REFERENCE_ELEMENTS
|
|
109
|
+
]);
|
|
110
|
+
var PRESERVED_INLINE = /* @__PURE__ */ new Set([
|
|
111
|
+
"rawInline",
|
|
112
|
+
"rawRunContent"
|
|
113
|
+
]);
|
|
114
|
+
function preservedName(node) {
|
|
115
|
+
const xml = node.attrs.xml;
|
|
116
|
+
const opening = typeof xml === "string" ? /^<\s*(?:[^\s:>/]+:)?([^\s>/]+)/.exec(xml) : null;
|
|
117
|
+
if (opening?.[1] !== void 0) return opening[1];
|
|
118
|
+
const element = node.attrs.element;
|
|
119
|
+
return typeof element === "string" ? element : null;
|
|
120
|
+
}
|
|
121
|
+
var detachAnchors = (slice, { move }) => move ? slice : mapSliceNodes(slice, (node) => {
|
|
122
|
+
if (ANCHOR_NODES.has(node.type.name)) return null;
|
|
123
|
+
if (!PRESERVED_INLINE.has(node.type.name)) return node;
|
|
124
|
+
const name = preservedName(node);
|
|
125
|
+
return name !== null && ANCHOR_ELEMENTS.has(name) ? null : node;
|
|
126
|
+
});
|
|
127
|
+
var rederiveDisplay = (slice, { state }) => {
|
|
128
|
+
const formatting = documentFormatting(state);
|
|
129
|
+
return mapSliceNodes(slice, (node) => {
|
|
130
|
+
if (node.type !== docxSchema.nodes.paragraph) return node;
|
|
131
|
+
const paragraph = resolveParagraph(xmlAttr(node.attrs.pPr), formatting);
|
|
132
|
+
const inline = node.children.map((child) => {
|
|
133
|
+
const marks = child.marks.map(
|
|
134
|
+
(mark) => mark.type === docxSchema.marks.run ? mark.type.create({
|
|
135
|
+
...mark.attrs,
|
|
136
|
+
format: resolveRun(
|
|
137
|
+
xmlAttr(mark.attrs.rPr),
|
|
138
|
+
paragraph,
|
|
139
|
+
formatting
|
|
140
|
+
)
|
|
141
|
+
}) : mark
|
|
142
|
+
);
|
|
143
|
+
return Mark.sameSet(marks, child.marks) ? child : child.mark(marks);
|
|
144
|
+
});
|
|
145
|
+
return node.type.create(
|
|
146
|
+
{ ...node.attrs, ...paragraphAttrsOf(paragraph) },
|
|
147
|
+
Fragment.fromArray(inline),
|
|
148
|
+
node.marks
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
var DEFAULT_NORMALIZERS = [
|
|
153
|
+
dropSourceIdentity,
|
|
154
|
+
detachAnchors,
|
|
155
|
+
rekeyNumbering,
|
|
156
|
+
rederiveDisplay
|
|
157
|
+
];
|
|
158
|
+
function normalizePasted(content, state, move, normalizers = DEFAULT_NORMALIZERS) {
|
|
159
|
+
let numbering = documentNumbering(state);
|
|
160
|
+
const known = /* @__PURE__ */ new Set([
|
|
161
|
+
...numbering.lists.keys(),
|
|
162
|
+
...numbering.added.keys(),
|
|
163
|
+
...numIdsIn(state.doc),
|
|
164
|
+
...content.newLists.keys()
|
|
165
|
+
]);
|
|
166
|
+
const started = /* @__PURE__ */ new Map();
|
|
167
|
+
const context = {
|
|
168
|
+
state,
|
|
169
|
+
move,
|
|
170
|
+
knownLists: known,
|
|
171
|
+
listKinds: content.listKinds ?? NO_LIST_KINDS,
|
|
172
|
+
startList(kind) {
|
|
173
|
+
if (!canStartNewList(state)) return null;
|
|
174
|
+
const list = templateList(kind);
|
|
175
|
+
const taken = allocateList(numbering, known, list);
|
|
176
|
+
numbering = taken.numbering;
|
|
177
|
+
known.add(taken.numId);
|
|
178
|
+
started.set(taken.numId, list);
|
|
179
|
+
return taken.numId;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const slice = normalizers.reduce(
|
|
183
|
+
(current, normalize) => normalize(current, context),
|
|
184
|
+
content.slice
|
|
185
|
+
);
|
|
186
|
+
return {
|
|
187
|
+
slice,
|
|
188
|
+
newLists: started.size === 0 ? content.newLists : new Map([...content.newLists, ...started])
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
export {
|
|
192
|
+
DEFAULT_NORMALIZERS,
|
|
193
|
+
detachAnchors,
|
|
194
|
+
dropSourceIdentity,
|
|
195
|
+
mapSliceNodes,
|
|
196
|
+
normalizePasted,
|
|
197
|
+
rederiveDisplay,
|
|
198
|
+
rekeyNumbering
|
|
199
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { DOMParser, Slice } from "prosemirror-model";
|
|
2
|
+
import type { EditorView } from "prosemirror-view";
|
|
3
|
+
import type { PastedContent } from "./htmlReader";
|
|
4
|
+
import type { ClipboardInput, ClipboardReader } from "./readers";
|
|
5
|
+
/**
|
|
6
|
+
* The parser ProseMirror hands the clipboard to.
|
|
7
|
+
*
|
|
8
|
+
* It is a `DOMParser` because that is what the `clipboardParser` prop is, but it holds no rules:
|
|
9
|
+
* clipboard markup is read by the reader chain alone, so a `parseDOM` rule of the schema - which
|
|
10
|
+
* would take the document's own XML back out of a private attribute - never sees it.
|
|
11
|
+
*/
|
|
12
|
+
export declare class DocxClipboardParser extends DOMParser {
|
|
13
|
+
private readonly readers;
|
|
14
|
+
private readonly inputOf;
|
|
15
|
+
private read;
|
|
16
|
+
private textOnly;
|
|
17
|
+
private pasteEvent;
|
|
18
|
+
constructor(readers: readonly ClipboardReader[], inputOf: (dom: Node) => ClipboardInput | null);
|
|
19
|
+
parseSlice(dom: Node): Slice;
|
|
20
|
+
/**
|
|
21
|
+
* What the last parse read, given up as it is taken.
|
|
22
|
+
*
|
|
23
|
+
* ProseMirror widens whatever a parser returns to the openness the surrounding markup implies
|
|
24
|
+
* and wraps it in the nodes a `data-pm-slice` context names, neither of which the reader asked
|
|
25
|
+
* for. The plugin puts this back in `transformPasted`, and takes it so that a later drag - which
|
|
26
|
+
* reaches `transformPasted` without a parse - never picks up a stale reading.
|
|
27
|
+
*/
|
|
28
|
+
takeRead(): PastedContent | null;
|
|
29
|
+
/** A new reading replaces the mode left by the previous clipboard event */
|
|
30
|
+
setPlainText(plain: boolean): void;
|
|
31
|
+
isPlainText(event: ClipboardEvent): boolean;
|
|
32
|
+
}
|
|
33
|
+
/** Whether the current paste chose text, so image handlers must leave its HTML and files alone */
|
|
34
|
+
export declare function plainTextPaste(view: EditorView, event: ClipboardEvent): boolean;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// src/editor/clipboard/parser.ts
|
|
2
|
+
import { DOMParser, Slice } from "prosemirror-model";
|
|
3
|
+
import { docxSchema } from "../../schema/index.js";
|
|
4
|
+
var DocxClipboardParser = class extends DOMParser {
|
|
5
|
+
constructor(readers, inputOf) {
|
|
6
|
+
super(docxSchema, []);
|
|
7
|
+
this.readers = readers;
|
|
8
|
+
this.inputOf = inputOf;
|
|
9
|
+
}
|
|
10
|
+
readers;
|
|
11
|
+
inputOf;
|
|
12
|
+
read = null;
|
|
13
|
+
textOnly = false;
|
|
14
|
+
pasteEvent = null;
|
|
15
|
+
parseSlice(dom) {
|
|
16
|
+
this.read = null;
|
|
17
|
+
this.setPlainText(false);
|
|
18
|
+
const input = this.inputOf(dom);
|
|
19
|
+
if (input === null) return Slice.empty;
|
|
20
|
+
for (const reader of this.readers) {
|
|
21
|
+
const content = reader(input);
|
|
22
|
+
if (content === null) continue;
|
|
23
|
+
this.read = content;
|
|
24
|
+
return content.slice;
|
|
25
|
+
}
|
|
26
|
+
return Slice.empty;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* What the last parse read, given up as it is taken.
|
|
30
|
+
*
|
|
31
|
+
* ProseMirror widens whatever a parser returns to the openness the surrounding markup implies
|
|
32
|
+
* and wraps it in the nodes a `data-pm-slice` context names, neither of which the reader asked
|
|
33
|
+
* for. The plugin puts this back in `transformPasted`, and takes it so that a later drag - which
|
|
34
|
+
* reaches `transformPasted` without a parse - never picks up a stale reading.
|
|
35
|
+
*/
|
|
36
|
+
takeRead() {
|
|
37
|
+
const read = this.read;
|
|
38
|
+
this.read = null;
|
|
39
|
+
return read;
|
|
40
|
+
}
|
|
41
|
+
/** A new reading replaces the mode left by the previous clipboard event */
|
|
42
|
+
setPlainText(plain) {
|
|
43
|
+
this.textOnly = plain;
|
|
44
|
+
this.pasteEvent = null;
|
|
45
|
+
}
|
|
46
|
+
isPlainText(event) {
|
|
47
|
+
if (this.pasteEvent === null) this.pasteEvent = event;
|
|
48
|
+
return this.pasteEvent === event && this.textOnly;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
function plainTextPaste(view, event) {
|
|
52
|
+
const parser = view.someProp("clipboardParser");
|
|
53
|
+
return parser instanceof DocxClipboardParser && parser.isPlainText(event);
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
DocxClipboardParser,
|
|
57
|
+
plainTextPaste
|
|
58
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one plugin holding the editor's clipboard.
|
|
3
|
+
*
|
|
4
|
+
* Everything the clipboard carries in or out passes through the props declared here: what a copy
|
|
5
|
+
* is written as, what a paste is read as, and what a drop from outside becomes. ProseMirror asks
|
|
6
|
+
* these before it asks any `handlePaste`. This plugin handles only an empty reading, preserving
|
|
7
|
+
* the selection or retrying the text fallback. Content goes in through ProseMirror's insertion
|
|
8
|
+
* with the `paste` and `uiEvent` meta a plugin
|
|
9
|
+
* watching for one reads (`prosemirror-view`'s `doPaste`), and a table decides for itself what a
|
|
10
|
+
* paste over a cell selection means (`prosemirror-tables`).
|
|
11
|
+
*/
|
|
12
|
+
import { Plugin } from "prosemirror-state";
|
|
13
|
+
import { type SliceNormalizer } from "./normalizers";
|
|
14
|
+
import { type ClipboardReader } from "./readers";
|
|
15
|
+
export interface ClipboardOptions {
|
|
16
|
+
/** Tried from the front. The first reader to answer decides what a paste puts in */
|
|
17
|
+
readers?: readonly ClipboardReader[];
|
|
18
|
+
/** Run in order over every pasted and dropped slice before it is put in */
|
|
19
|
+
normalizers?: readonly SliceNormalizer[];
|
|
20
|
+
}
|
|
21
|
+
export declare function docxClipboard(options?: ClipboardOptions): Plugin;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// src/editor/clipboard/plugin.ts
|
|
2
|
+
import { Fragment, Slice } from "prosemirror-model";
|
|
3
|
+
import { Plugin } from "prosemirror-state";
|
|
4
|
+
import {
|
|
5
|
+
NEW_LISTS_ATTR,
|
|
6
|
+
NO_NEW_LISTS,
|
|
7
|
+
newListsOf,
|
|
8
|
+
newListsValue
|
|
9
|
+
} from "../../numbering/listRegistry.js";
|
|
10
|
+
import { clipboardSerializer, clipboardText } from "../../schema/clipboard.js";
|
|
11
|
+
import { numIdsIn } from "../commands/listCommands.js";
|
|
12
|
+
import { documentOf } from "../editorDocument.js";
|
|
13
|
+
import { insertPlainText } from "../plainText.js";
|
|
14
|
+
import { moveCaretToDrop } from "../plugins/dropCaret.js";
|
|
15
|
+
import { documentNumbering } from "../plugins/numberingDecorations.js";
|
|
16
|
+
import {
|
|
17
|
+
INTERNAL_TOKEN_ATTRIBUTE,
|
|
18
|
+
internalTokenOf,
|
|
19
|
+
rememberCopied
|
|
20
|
+
} from "./internalChannel.js";
|
|
21
|
+
import {
|
|
22
|
+
DEFAULT_NORMALIZERS,
|
|
23
|
+
normalizePasted
|
|
24
|
+
} from "./normalizers.js";
|
|
25
|
+
import { DocxClipboardParser } from "./parser.js";
|
|
26
|
+
import { readContextOf } from "./readContext.js";
|
|
27
|
+
import {
|
|
28
|
+
DEFAULT_READERS,
|
|
29
|
+
plainTextSlice
|
|
30
|
+
} from "./readers.js";
|
|
31
|
+
function bareWrappers(content, openStart, openEnd) {
|
|
32
|
+
const wrapper = content.firstChild;
|
|
33
|
+
if (openStart <= 1 || openEnd <= 1 || content.childCount !== 1)
|
|
34
|
+
return content;
|
|
35
|
+
if (wrapper === null || wrapper.childCount !== 1) return content;
|
|
36
|
+
return Fragment.from(
|
|
37
|
+
wrapper.type.create(
|
|
38
|
+
null,
|
|
39
|
+
bareWrappers(wrapper.content, openStart - 1, openEnd - 1),
|
|
40
|
+
wrapper.marks
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
function copiedSlice(slice) {
|
|
45
|
+
return new Slice(
|
|
46
|
+
bareWrappers(slice.content, slice.openStart, slice.openEnd),
|
|
47
|
+
slice.openStart,
|
|
48
|
+
slice.openEnd
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
function dragCopyModifier() {
|
|
52
|
+
return /Mac|iP(hone|[oa]d)/.test(navigator.platform) ? "altKey" : "ctrlKey";
|
|
53
|
+
}
|
|
54
|
+
function dropMoves(view, event) {
|
|
55
|
+
if (view.dragging === null) return false;
|
|
56
|
+
let copies;
|
|
57
|
+
view.someProp("dragCopies", (test) => {
|
|
58
|
+
copies = copies === true || test(event);
|
|
59
|
+
});
|
|
60
|
+
return copies === void 0 ? !event[dragCopyModifier()] : !copies;
|
|
61
|
+
}
|
|
62
|
+
function docxClipboard(options = {}) {
|
|
63
|
+
const normalizers = options.normalizers ?? DEFAULT_NORMALIZERS;
|
|
64
|
+
let copyToken = null;
|
|
65
|
+
const serializer = clipboardSerializer((copy) => {
|
|
66
|
+
if (copyToken !== null) {
|
|
67
|
+
copy.firstElementChild?.setAttribute(INTERNAL_TOKEN_ATTRIBUTE, copyToken);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
let host = null;
|
|
71
|
+
const parser = new DocxClipboardParser(
|
|
72
|
+
options.readers ?? DEFAULT_READERS,
|
|
73
|
+
(dom) => host === null ? null : {
|
|
74
|
+
dom,
|
|
75
|
+
token: internalTokenOf(dom),
|
|
76
|
+
sessionId: documentOf(host.state).session?.sessionId ?? null,
|
|
77
|
+
context: readContextOf(host.state, host.dom.ownerDocument)
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
let started = null;
|
|
81
|
+
let dropMove = false;
|
|
82
|
+
let draggingOut = false;
|
|
83
|
+
return new Plugin({
|
|
84
|
+
view(view) {
|
|
85
|
+
host = view;
|
|
86
|
+
return {
|
|
87
|
+
destroy() {
|
|
88
|
+
if (host === view) host = null;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
/**
|
|
93
|
+
* The definitions of the lists a paste began, registered on the document once the paste is
|
|
94
|
+
* in. A list the pasted markup started is numbered as it is read, and only the edit that
|
|
95
|
+
* lands says whether that number is worn in the end.
|
|
96
|
+
*/
|
|
97
|
+
appendTransaction(transactions, _before, after) {
|
|
98
|
+
const lists = started;
|
|
99
|
+
started = null;
|
|
100
|
+
if (lists === null || lists.size === 0 || !transactions.some((tr) => tr.docChanged)) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const worn = numIdsIn(after.doc);
|
|
104
|
+
const registered = newListsOf(after.doc.attrs[NEW_LISTS_ATTR]);
|
|
105
|
+
const missing = [...lists].filter(
|
|
106
|
+
([numId]) => worn.has(numId) && !registered.has(numId)
|
|
107
|
+
);
|
|
108
|
+
if (missing.length === 0) return null;
|
|
109
|
+
return after.tr.setDocAttribute(
|
|
110
|
+
NEW_LISTS_ATTR,
|
|
111
|
+
newListsValue(new Map([...registered, ...missing]))
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
props: {
|
|
115
|
+
handleDOMEvents: {
|
|
116
|
+
paste() {
|
|
117
|
+
parser.setPlainText(false);
|
|
118
|
+
return false;
|
|
119
|
+
},
|
|
120
|
+
// ProseMirror writes and reads the dragged slice inside the very event the two below
|
|
121
|
+
// answer for, through props it hands the view and not the event, and it runs these first.
|
|
122
|
+
// A microtask is the first thing to run once the event is over, so neither answer is left
|
|
123
|
+
// standing for whatever comes next
|
|
124
|
+
dragstart() {
|
|
125
|
+
draggingOut = true;
|
|
126
|
+
queueMicrotask(() => {
|
|
127
|
+
draggingOut = false;
|
|
128
|
+
});
|
|
129
|
+
return false;
|
|
130
|
+
},
|
|
131
|
+
drop(view, event) {
|
|
132
|
+
dropMove = dropMoves(view, event);
|
|
133
|
+
queueMicrotask(() => {
|
|
134
|
+
dropMove = false;
|
|
135
|
+
});
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
clipboardParser: parser,
|
|
140
|
+
clipboardSerializer: serializer,
|
|
141
|
+
clipboardTextParser(text, context, plain) {
|
|
142
|
+
parser.setPlainText(plain);
|
|
143
|
+
return plainTextSlice(text, context);
|
|
144
|
+
},
|
|
145
|
+
clipboardTextSerializer: clipboardText,
|
|
146
|
+
transformCopied(slice, view) {
|
|
147
|
+
const route = draggingOut ? "drag" : "clipboard";
|
|
148
|
+
copyToken = rememberCopied(
|
|
149
|
+
route,
|
|
150
|
+
slice,
|
|
151
|
+
documentOf(view.state).session?.sessionId ?? null,
|
|
152
|
+
documentNumbering(view.state)
|
|
153
|
+
);
|
|
154
|
+
return copiedSlice(slice);
|
|
155
|
+
},
|
|
156
|
+
transformPasted(slice, view, plain) {
|
|
157
|
+
if (!plain) parser.setPlainText(false);
|
|
158
|
+
const read = parser.takeRead();
|
|
159
|
+
const content = normalizePasted(
|
|
160
|
+
read ?? { slice, newLists: NO_NEW_LISTS },
|
|
161
|
+
view.state,
|
|
162
|
+
dropMove,
|
|
163
|
+
normalizers
|
|
164
|
+
);
|
|
165
|
+
started = content.newLists;
|
|
166
|
+
return content.slice;
|
|
167
|
+
},
|
|
168
|
+
handlePaste(view, event, slice) {
|
|
169
|
+
if (slice.content.size > 0) return false;
|
|
170
|
+
const text = event.clipboardData?.getData("text/plain") ?? "";
|
|
171
|
+
if (!parser.isPlainText(event) && text) view.pasteText(text, event);
|
|
172
|
+
return true;
|
|
173
|
+
},
|
|
174
|
+
handleDrop(view, event) {
|
|
175
|
+
if (view.dragging) return false;
|
|
176
|
+
const text = event.dataTransfer?.getData("text/plain") ?? "";
|
|
177
|
+
if (text) {
|
|
178
|
+
moveCaretToDrop(view, event);
|
|
179
|
+
insertPlainText(view, text);
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
export {
|
|
187
|
+
docxClipboard
|
|
188
|
+
};
|