@portone/docx-editor 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +159 -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/headersFooters.d.ts +89 -20
- package/dist/docx/headersFooters.js +163 -172
- package/dist/docx/importDocx.d.ts +1 -1
- package/dist/docx/importDocx.js +100 -48
- package/dist/docx/importParagraph.d.ts +16 -4
- package/dist/docx/importParagraph.js +87 -93
- package/dist/docx/importPolicy.d.ts +70 -0
- package/dist/docx/importPolicy.js +190 -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 +4 -0
- package/dist/docx/serializeParagraph.js +1 -0
- 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/editor/clipboard/htmlReader.d.ts +30 -0
- package/dist/editor/{externalClipboard.js → clipboard/htmlReader.js} +39 -223
- 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 +320 -0
- package/dist/editor/clipboard/readContext.d.ts +24 -0
- package/dist/editor/clipboard/readContext.js +19 -0
- package/dist/editor/clipboard/readers.d.ts +33 -0
- package/dist/editor/clipboard/readers.js +37 -0
- package/dist/editor/commands/comments/editing.d.ts +26 -6
- package/dist/editor/commands/comments/editing.js +175 -105
- 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/listCommands.d.ts +6 -0
- package/dist/editor/commands/listCommands.js +3 -2
- package/dist/editor/commands/noteQueries.d.ts +22 -5
- package/dist/editor/commands/noteQueries.js +34 -6
- 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 +8 -0
- package/dist/ooxml/props.js +14 -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 +38 -20
- package/dist/schema/docxSchema.d.ts +9 -1
- package/dist/schema/docxSchema.js +191 -139
- 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/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.js +3 -1
- package/dist/schema/stories.d.ts +49 -0
- package/dist/schema/stories.js +78 -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 +1 -1
- package/dist/editor/externalClipboard.d.ts +0 -22
|
@@ -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,320 @@
|
|
|
1
|
+
// src/editor/clipboard/plugin.ts
|
|
2
|
+
import {
|
|
3
|
+
DOMSerializer,
|
|
4
|
+
Fragment,
|
|
5
|
+
Slice
|
|
6
|
+
} from "prosemirror-model";
|
|
7
|
+
import { Plugin } from "prosemirror-state";
|
|
8
|
+
import { styleIdOf } from "../../docx/formatting.js";
|
|
9
|
+
import {
|
|
10
|
+
NEW_LISTS_ATTR,
|
|
11
|
+
NO_NEW_LISTS,
|
|
12
|
+
newListsOf,
|
|
13
|
+
newListsValue
|
|
14
|
+
} from "../../numbering/listRegistry.js";
|
|
15
|
+
import { docxSchema, isPageBreak } from "../../schema/index.js";
|
|
16
|
+
import { numIdsIn } from "../commands/listCommands.js";
|
|
17
|
+
import { documentOf } from "../editorDocument.js";
|
|
18
|
+
import { insertPlainText } from "../plainText.js";
|
|
19
|
+
import { moveCaretToDrop } from "../plugins/dropCaret.js";
|
|
20
|
+
import { documentNumbering } from "../plugins/numberingDecorations.js";
|
|
21
|
+
import { COPIED_STYLE_ATTRIBUTE } from "./htmlReader.js";
|
|
22
|
+
import { safeHref } from "./inlineFormatting.js";
|
|
23
|
+
import {
|
|
24
|
+
INTERNAL_TOKEN_ATTRIBUTE,
|
|
25
|
+
internalTokenOf,
|
|
26
|
+
rememberCopied
|
|
27
|
+
} from "./internalChannel.js";
|
|
28
|
+
import {
|
|
29
|
+
DEFAULT_NORMALIZERS,
|
|
30
|
+
normalizePasted
|
|
31
|
+
} from "./normalizers.js";
|
|
32
|
+
import { DocxClipboardParser } from "./parser.js";
|
|
33
|
+
import { readContextOf } from "./readContext.js";
|
|
34
|
+
import {
|
|
35
|
+
DEFAULT_READERS,
|
|
36
|
+
plainTextSlice
|
|
37
|
+
} from "./readers.js";
|
|
38
|
+
var PUBLIC_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
39
|
+
"alt",
|
|
40
|
+
"aria-label",
|
|
41
|
+
"class",
|
|
42
|
+
"colspan",
|
|
43
|
+
"height",
|
|
44
|
+
"href",
|
|
45
|
+
"lang",
|
|
46
|
+
"rowspan",
|
|
47
|
+
"src",
|
|
48
|
+
"style",
|
|
49
|
+
"width"
|
|
50
|
+
]);
|
|
51
|
+
function isAttributes(value) {
|
|
52
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Node);
|
|
53
|
+
}
|
|
54
|
+
function publicAttributes(attrs) {
|
|
55
|
+
return Object.fromEntries(
|
|
56
|
+
Object.entries(attrs).filter(([name]) => PUBLIC_ATTRIBUTES.has(name))
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
function isSpecArray(value) {
|
|
60
|
+
return Array.isArray(value) && typeof value[0] === "string";
|
|
61
|
+
}
|
|
62
|
+
function stripPrivateAttributes(spec) {
|
|
63
|
+
if (!isSpecArray(spec)) return spec;
|
|
64
|
+
const [tag, ...rest] = spec;
|
|
65
|
+
const attrs = isAttributes(rest[0]) ? publicAttributes(rest[0]) : null;
|
|
66
|
+
const children = (attrs === null ? rest : rest.slice(1)).map(
|
|
67
|
+
(child) => isSpecArray(child) ? stripPrivateAttributes(child) : child
|
|
68
|
+
);
|
|
69
|
+
return attrs === null ? [tag, ...children] : [tag, attrs, ...children];
|
|
70
|
+
}
|
|
71
|
+
function withDomAttribute(spec, name, value) {
|
|
72
|
+
if (!isSpecArray(spec)) return spec;
|
|
73
|
+
const [tag, ...rest] = spec;
|
|
74
|
+
return isAttributes(rest[0]) ? [tag, { ...rest[0], [name]: value }, ...rest.slice(1)] : [tag, { [name]: value }, ...rest];
|
|
75
|
+
}
|
|
76
|
+
function asTag(spec, tag) {
|
|
77
|
+
return isSpecArray(spec) ? [tag, ...spec.slice(1)] : spec;
|
|
78
|
+
}
|
|
79
|
+
function copiedMarkSpec(mark, spec) {
|
|
80
|
+
const stripped = stripPrivateAttributes(spec);
|
|
81
|
+
if (mark.type !== docxSchema.marks.link) return stripped;
|
|
82
|
+
const href = safeHref(
|
|
83
|
+
typeof mark.attrs.href === "string" ? mark.attrs.href : null
|
|
84
|
+
);
|
|
85
|
+
return href === null ? stripped : asTag(withDomAttribute(stripped, "href", href), "a");
|
|
86
|
+
}
|
|
87
|
+
function copiedNodeSpec(node, spec) {
|
|
88
|
+
const stripped = stripPrivateAttributes(spec);
|
|
89
|
+
if (node.type !== docxSchema.nodes.paragraph) return stripped;
|
|
90
|
+
const styleId = styleIdOf(node.attrs.pPr);
|
|
91
|
+
return styleId === null ? stripped : withDomAttribute(stripped, COPIED_STYLE_ATTRIBUTE, styleId);
|
|
92
|
+
}
|
|
93
|
+
function copiedNodes() {
|
|
94
|
+
const drawn = DOMSerializer.fromSchema(docxSchema);
|
|
95
|
+
return Object.fromEntries(
|
|
96
|
+
Object.entries(drawn.nodes).map(([name, toDOM]) => [
|
|
97
|
+
name,
|
|
98
|
+
(node) => copiedNodeSpec(node, toDOM(node))
|
|
99
|
+
])
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
function copiedMarks() {
|
|
103
|
+
const drawn = DOMSerializer.fromSchema(docxSchema);
|
|
104
|
+
return Object.fromEntries(
|
|
105
|
+
Object.entries(drawn.marks).map(([name, toDOM]) => [
|
|
106
|
+
name,
|
|
107
|
+
(mark, inline) => copiedMarkSpec(mark, toDOM(mark, inline))
|
|
108
|
+
])
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
var ClipboardSerializer = class extends DOMSerializer {
|
|
112
|
+
constructor(tokenOf) {
|
|
113
|
+
super(copiedNodes(), copiedMarks());
|
|
114
|
+
this.tokenOf = tokenOf;
|
|
115
|
+
}
|
|
116
|
+
tokenOf;
|
|
117
|
+
serializeFragment(fragment, options, target) {
|
|
118
|
+
const dom = super.serializeFragment(fragment, options, target);
|
|
119
|
+
const token = target === void 0 ? this.tokenOf() : null;
|
|
120
|
+
if (token !== null) {
|
|
121
|
+
dom.firstElementChild?.setAttribute(INTERNAL_TOKEN_ATTRIBUTE, token);
|
|
122
|
+
}
|
|
123
|
+
return dom;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
var UNREADABLE_BLOCKS = /* @__PURE__ */ new Set(["rawBlock"]);
|
|
127
|
+
function stringAttr(value) {
|
|
128
|
+
return typeof value === "string" ? value : "";
|
|
129
|
+
}
|
|
130
|
+
function inlineText(node) {
|
|
131
|
+
if (node.isText) return node.text ?? "";
|
|
132
|
+
if (node.type === docxSchema.nodes.hardBreak) {
|
|
133
|
+
return isPageBreak(node.attrs.brAttrs) ? "\f" : "\n";
|
|
134
|
+
}
|
|
135
|
+
if (node.type === docxSchema.nodes.image) return stringAttr(node.attrs.alt);
|
|
136
|
+
if (node.type === docxSchema.nodes.noteReference) {
|
|
137
|
+
return node.attrs.customMarkFollows === true ? "" : stringAttr(node.attrs.label);
|
|
138
|
+
}
|
|
139
|
+
return "";
|
|
140
|
+
}
|
|
141
|
+
function rowsText(table) {
|
|
142
|
+
const rows = [];
|
|
143
|
+
table.forEach((row) => {
|
|
144
|
+
const cells = [];
|
|
145
|
+
row.forEach((cell) => {
|
|
146
|
+
cells.push(fragmentText(cell.content));
|
|
147
|
+
});
|
|
148
|
+
rows.push(cells.join(" "));
|
|
149
|
+
});
|
|
150
|
+
return rows.join("\n");
|
|
151
|
+
}
|
|
152
|
+
function blockText(node) {
|
|
153
|
+
if (node.type === docxSchema.nodes.table) return rowsText(node);
|
|
154
|
+
if (UNREADABLE_BLOCKS.has(node.type.name)) return "";
|
|
155
|
+
return fragmentText(node.content);
|
|
156
|
+
}
|
|
157
|
+
function fragmentText(fragment) {
|
|
158
|
+
const pieces = [];
|
|
159
|
+
fragment.forEach((child) => {
|
|
160
|
+
pieces.push(child.isInline ? inlineText(child) : blockText(child));
|
|
161
|
+
});
|
|
162
|
+
return pieces.join(fragment.firstChild?.isInline === true ? "" : "\n");
|
|
163
|
+
}
|
|
164
|
+
function bareWrappers(content, openStart, openEnd) {
|
|
165
|
+
const wrapper = content.firstChild;
|
|
166
|
+
if (openStart <= 1 || openEnd <= 1 || content.childCount !== 1)
|
|
167
|
+
return content;
|
|
168
|
+
if (wrapper === null || wrapper.childCount !== 1) return content;
|
|
169
|
+
return Fragment.from(
|
|
170
|
+
wrapper.type.create(
|
|
171
|
+
null,
|
|
172
|
+
bareWrappers(wrapper.content, openStart - 1, openEnd - 1),
|
|
173
|
+
wrapper.marks
|
|
174
|
+
)
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
function copiedSlice(slice) {
|
|
178
|
+
return new Slice(
|
|
179
|
+
bareWrappers(slice.content, slice.openStart, slice.openEnd),
|
|
180
|
+
slice.openStart,
|
|
181
|
+
slice.openEnd
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
function clipboardText(slice) {
|
|
185
|
+
return fragmentText(slice.content);
|
|
186
|
+
}
|
|
187
|
+
function dragCopyModifier() {
|
|
188
|
+
return /Mac|iP(hone|[oa]d)/.test(navigator.platform) ? "altKey" : "ctrlKey";
|
|
189
|
+
}
|
|
190
|
+
function dropMoves(view, event) {
|
|
191
|
+
if (view.dragging === null) return false;
|
|
192
|
+
let copies;
|
|
193
|
+
view.someProp("dragCopies", (test) => {
|
|
194
|
+
copies = copies === true || test(event);
|
|
195
|
+
});
|
|
196
|
+
return copies === void 0 ? !event[dragCopyModifier()] : !copies;
|
|
197
|
+
}
|
|
198
|
+
function docxClipboard(options = {}) {
|
|
199
|
+
const normalizers = options.normalizers ?? DEFAULT_NORMALIZERS;
|
|
200
|
+
let copyToken = null;
|
|
201
|
+
const serializer = new ClipboardSerializer(() => copyToken);
|
|
202
|
+
let host = null;
|
|
203
|
+
const parser = new DocxClipboardParser(
|
|
204
|
+
options.readers ?? DEFAULT_READERS,
|
|
205
|
+
(dom) => host === null ? null : {
|
|
206
|
+
dom,
|
|
207
|
+
token: internalTokenOf(dom),
|
|
208
|
+
sessionId: documentOf(host.state).session?.sessionId ?? null,
|
|
209
|
+
context: readContextOf(host.state, host.dom.ownerDocument)
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
let started = null;
|
|
213
|
+
let dropMove = false;
|
|
214
|
+
let draggingOut = false;
|
|
215
|
+
return new Plugin({
|
|
216
|
+
view(view) {
|
|
217
|
+
host = view;
|
|
218
|
+
return {
|
|
219
|
+
destroy() {
|
|
220
|
+
if (host === view) host = null;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
/**
|
|
225
|
+
* The definitions of the lists a paste began, registered on the document once the paste is
|
|
226
|
+
* in. A list the pasted markup started is numbered as it is read, and only the edit that
|
|
227
|
+
* lands says whether that number is worn in the end.
|
|
228
|
+
*/
|
|
229
|
+
appendTransaction(transactions, _before, after) {
|
|
230
|
+
const lists = started;
|
|
231
|
+
started = null;
|
|
232
|
+
if (lists === null || lists.size === 0 || !transactions.some((tr) => tr.docChanged)) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
const worn = numIdsIn(after.doc);
|
|
236
|
+
const registered = newListsOf(after.doc.attrs[NEW_LISTS_ATTR]);
|
|
237
|
+
const missing = [...lists].filter(
|
|
238
|
+
([numId]) => worn.has(numId) && !registered.has(numId)
|
|
239
|
+
);
|
|
240
|
+
if (missing.length === 0) return null;
|
|
241
|
+
return after.tr.setDocAttribute(
|
|
242
|
+
NEW_LISTS_ATTR,
|
|
243
|
+
newListsValue(new Map([...registered, ...missing]))
|
|
244
|
+
);
|
|
245
|
+
},
|
|
246
|
+
props: {
|
|
247
|
+
handleDOMEvents: {
|
|
248
|
+
paste() {
|
|
249
|
+
parser.setPlainText(false);
|
|
250
|
+
return false;
|
|
251
|
+
},
|
|
252
|
+
// ProseMirror writes and reads the dragged slice inside the very event the two below
|
|
253
|
+
// answer for, through props it hands the view and not the event, and it runs these first.
|
|
254
|
+
// A microtask is the first thing to run once the event is over, so neither answer is left
|
|
255
|
+
// standing for whatever comes next
|
|
256
|
+
dragstart() {
|
|
257
|
+
draggingOut = true;
|
|
258
|
+
queueMicrotask(() => {
|
|
259
|
+
draggingOut = false;
|
|
260
|
+
});
|
|
261
|
+
return false;
|
|
262
|
+
},
|
|
263
|
+
drop(view, event) {
|
|
264
|
+
dropMove = dropMoves(view, event);
|
|
265
|
+
queueMicrotask(() => {
|
|
266
|
+
dropMove = false;
|
|
267
|
+
});
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
clipboardParser: parser,
|
|
272
|
+
clipboardSerializer: serializer,
|
|
273
|
+
clipboardTextParser(text, context, plain) {
|
|
274
|
+
parser.setPlainText(plain);
|
|
275
|
+
return plainTextSlice(text, context);
|
|
276
|
+
},
|
|
277
|
+
clipboardTextSerializer: clipboardText,
|
|
278
|
+
transformCopied(slice, view) {
|
|
279
|
+
const route = draggingOut ? "drag" : "clipboard";
|
|
280
|
+
copyToken = rememberCopied(
|
|
281
|
+
route,
|
|
282
|
+
slice,
|
|
283
|
+
documentOf(view.state).session?.sessionId ?? null,
|
|
284
|
+
documentNumbering(view.state)
|
|
285
|
+
);
|
|
286
|
+
return copiedSlice(slice);
|
|
287
|
+
},
|
|
288
|
+
transformPasted(slice, view, plain) {
|
|
289
|
+
if (!plain) parser.setPlainText(false);
|
|
290
|
+
const read = parser.takeRead();
|
|
291
|
+
const content = normalizePasted(
|
|
292
|
+
read ?? { slice, newLists: NO_NEW_LISTS },
|
|
293
|
+
view.state,
|
|
294
|
+
dropMove,
|
|
295
|
+
normalizers
|
|
296
|
+
);
|
|
297
|
+
started = content.newLists;
|
|
298
|
+
return content.slice;
|
|
299
|
+
},
|
|
300
|
+
handlePaste(view, event, slice) {
|
|
301
|
+
if (slice.content.size > 0) return false;
|
|
302
|
+
const text = event.clipboardData?.getData("text/plain") ?? "";
|
|
303
|
+
if (!parser.isPlainText(event) && text) view.pasteText(text, event);
|
|
304
|
+
return true;
|
|
305
|
+
},
|
|
306
|
+
handleDrop(view, event) {
|
|
307
|
+
if (view.dragging) return false;
|
|
308
|
+
const text = event.dataTransfer?.getData("text/plain") ?? "";
|
|
309
|
+
if (text) {
|
|
310
|
+
moveCaretToDrop(view, event);
|
|
311
|
+
insertPlainText(view, text);
|
|
312
|
+
}
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
export {
|
|
319
|
+
docxClipboard
|
|
320
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { EditorState } from "prosemirror-state";
|
|
2
|
+
import type { FormattingContext, ParagraphStyleOption } from "../../docx/formatting";
|
|
3
|
+
import type { ImageToInsert } from "../insertImage";
|
|
4
|
+
/**
|
|
5
|
+
* Everything a piece of HTML is read against.
|
|
6
|
+
*
|
|
7
|
+
* The reader answers about the document it is read into - which styles it may name, which list
|
|
8
|
+
* numbers are taken, whether a list may be started at all - and nothing about the editor holding
|
|
9
|
+
* it. Gathering those in one place is what keeps the reader off the editing state, so the same
|
|
10
|
+
* reading can be asked for from a paste, from a drop, and from a test.
|
|
11
|
+
*/
|
|
12
|
+
export interface HtmlReadContext {
|
|
13
|
+
/** The document the markup is read into an element of */
|
|
14
|
+
document: Document;
|
|
15
|
+
formatting: FormattingContext;
|
|
16
|
+
paragraphStyles: readonly ParagraphStyleOption[];
|
|
17
|
+
numbering: {
|
|
18
|
+
used: ReadonlySet<number>;
|
|
19
|
+
canCreate: boolean;
|
|
20
|
+
};
|
|
21
|
+
/** The images already loaded for this read, by the token the markup carries */
|
|
22
|
+
images: ReadonlyMap<string, ImageToInsert>;
|
|
23
|
+
}
|
|
24
|
+
export declare function readContextOf(state: EditorState, document: Document, images?: ReadonlyMap<string, ImageToInsert>): HtmlReadContext;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/editor/clipboard/readContext.ts
|
|
2
|
+
import { numIdsIn } from "../commands/listCommands.js";
|
|
3
|
+
import { documentFormatting, documentParagraphStyles } from "../documentStyles.js";
|
|
4
|
+
import { canStartNewList } from "../plugins/numberingDecorations.js";
|
|
5
|
+
function readContextOf(state, document, images = /* @__PURE__ */ new Map()) {
|
|
6
|
+
return {
|
|
7
|
+
document,
|
|
8
|
+
formatting: documentFormatting(state),
|
|
9
|
+
paragraphStyles: documentParagraphStyles(state),
|
|
10
|
+
numbering: {
|
|
11
|
+
used: numIdsIn(state.doc),
|
|
12
|
+
canCreate: canStartNewList(state)
|
|
13
|
+
},
|
|
14
|
+
images
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
readContextOf
|
|
19
|
+
};
|