@portone/docx-editor 0.5.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 +42 -0
- package/CONTRIBUTING.md +2 -0
- package/dist/docx/formatting/styles.d.ts +0 -2
- package/dist/docx/formatting/styles.js +2 -15
- package/dist/docx/identities.js +17 -26
- package/dist/docx/importParagraph.d.ts +6 -5
- package/dist/docx/importParagraph.js +15 -62
- package/dist/docx/importPolicy.js +10 -5
- package/dist/docx/serializeParagraph.d.ts +5 -4
- package/dist/docx/serializeParagraph.js +22 -58
- 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 +2 -6
- package/dist/editor/clipboard/htmlReader.js +98 -24
- package/dist/editor/clipboard/inlineFormatting.d.ts +0 -7
- package/dist/editor/clipboard/inlineFormatting.js +1 -7
- package/dist/editor/clipboard/plugin.js +7 -139
- package/dist/editor/clipboard/readContext.d.ts +10 -0
- package/dist/editor/clipboard/readContext.js +8 -2
- package/dist/editor/clipboard/source.d.ts +12 -0
- package/dist/editor/clipboard/source.js +21 -0
- package/dist/editor/commands/comments/editing.js +2 -1
- package/dist/editor/commands/linkCommands.js +20 -5
- package/dist/editor/commands/lockCommands.js +59 -16
- package/dist/editor/commands/paragraphCommands.js +2 -2
- package/dist/ooxml/props.d.ts +2 -0
- package/dist/ooxml/props.js +14 -0
- package/dist/schema/attrRoles.js +8 -3
- package/dist/schema/clipboard.d.ts +72 -0
- package/dist/schema/clipboard.js +288 -0
- package/dist/schema/docxSchema.js +27 -27
- package/dist/schema/locks.d.ts +3 -0
- package/dist/schema/locks.js +29 -12
- package/dist/schema/rendering.d.ts +14 -0
- package/dist/schema/rendering.js +24 -11
- package/dist/schema/wrappers.d.ts +58 -0
- package/dist/schema/wrappers.js +77 -0
- package/package.json +5 -4
|
@@ -14,9 +14,11 @@ import {
|
|
|
14
14
|
unlockAllowed
|
|
15
15
|
} from "../../schema/locks.js";
|
|
16
16
|
import { editsShut } from "../../schema/protectionState.js";
|
|
17
|
-
|
|
17
|
+
import { innermostDepth, sharedWrappers } from "../../schema/wrappers.js";
|
|
18
|
+
function lockedControlMark(id, depth) {
|
|
18
19
|
return docxSchema.marks.sdt.create({
|
|
19
20
|
sdtPrefix: lockedControlPrefix(id),
|
|
21
|
+
depth,
|
|
20
22
|
contentsLocked: true,
|
|
21
23
|
deletionLocked: true
|
|
22
24
|
});
|
|
@@ -45,11 +47,26 @@ function overlaps(a, b) {
|
|
|
45
47
|
function reaches(a, b) {
|
|
46
48
|
return a.from <= b.to && a.to >= b.from;
|
|
47
49
|
}
|
|
50
|
+
function contains(a, b) {
|
|
51
|
+
return a.from <= b.from && a.to >= b.to;
|
|
52
|
+
}
|
|
48
53
|
function stretchIn(block, reach) {
|
|
49
54
|
const from = Math.max(block.start, reach.from);
|
|
50
55
|
const to = Math.min(block.start + block.node.content.size, reach.to);
|
|
51
56
|
return from < to ? { from, to } : null;
|
|
52
57
|
}
|
|
58
|
+
function wrapperDepthOver(block, stretch, replaced = null) {
|
|
59
|
+
const covered = [];
|
|
60
|
+
block.node.forEach((child, offset) => {
|
|
61
|
+
const from = block.start + offset;
|
|
62
|
+
if (from < stretch.to && stretch.from < from + child.nodeSize) {
|
|
63
|
+
covered.push(child);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
const shared = sharedWrappers(covered);
|
|
67
|
+
const at = replaced === null ? -1 : shared.findIndex((wrapper) => wrapper.eq(replaced));
|
|
68
|
+
return innermostDepth(at === -1 ? shared : shared.slice(0, at)) + 1;
|
|
69
|
+
}
|
|
53
70
|
function gapsIn(stretch, spans) {
|
|
54
71
|
const gaps = [];
|
|
55
72
|
let at = stretch.from;
|
|
@@ -61,38 +78,56 @@ function gapsIn(stretch, spans) {
|
|
|
61
78
|
if (at < stretch.to) gaps.push({ from: at, to: stretch.to });
|
|
62
79
|
return gaps;
|
|
63
80
|
}
|
|
64
|
-
function takeover(met, stretch, open) {
|
|
81
|
+
function takeover(block, met, stretch, open) {
|
|
65
82
|
const only = met.length === 1 ? met[0] : null;
|
|
66
83
|
const prefix = only && prefixOf(only.mark.attrs);
|
|
67
84
|
if (!only || isLocked(only.mark) || !prefix || !namesNothing(prefix)) {
|
|
68
85
|
return null;
|
|
69
86
|
}
|
|
70
|
-
const
|
|
71
|
-
|
|
87
|
+
const locked = open ? withLock(only.mark, true) : null;
|
|
88
|
+
if (!locked) return null;
|
|
89
|
+
const widened = {
|
|
72
90
|
from: Math.min(stretch.from, only.from),
|
|
73
|
-
to: Math.max(stretch.to, only.to)
|
|
74
|
-
|
|
75
|
-
|
|
91
|
+
to: Math.max(stretch.to, only.to)
|
|
92
|
+
};
|
|
93
|
+
const depth = wrapperDepthOver(block, widened, only.mark);
|
|
94
|
+
return {
|
|
95
|
+
...widened,
|
|
96
|
+
mark: depth === locked.attrs.depth ? locked : locked.type.create({ ...locked.attrs, depth }),
|
|
97
|
+
replaces: only.mark
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function outermost(spans) {
|
|
101
|
+
const kept = [];
|
|
102
|
+
for (const span of spans) {
|
|
103
|
+
if (!kept.some((other) => overlaps(other, span))) kept.push(span);
|
|
104
|
+
}
|
|
105
|
+
return kept;
|
|
76
106
|
}
|
|
77
107
|
function blockLockEdits(state, block, spans, reach) {
|
|
78
108
|
const stretch = stretchIn(block, reach);
|
|
79
109
|
if (!stretch) return [];
|
|
80
110
|
const open = (of) => openStretches(state, of, "mark");
|
|
81
|
-
const met = spans.filter((span) => overlaps(span, stretch));
|
|
82
|
-
const taken = takeover(met, stretch, open([stretch]).length > 0);
|
|
111
|
+
const met = outermost(spans.filter((span) => overlaps(span, stretch)));
|
|
112
|
+
const taken = takeover(block, met, stretch, open([stretch]).length > 0);
|
|
83
113
|
if (taken) return [taken];
|
|
84
114
|
const shut = open(met.filter((span) => !isLocked(span.mark))).flatMap(
|
|
85
115
|
(span) => {
|
|
86
116
|
const mark = withLock(span.mark, true);
|
|
87
|
-
return mark ? [{ from: span.from, to: span.to, mark }] : [];
|
|
117
|
+
return mark ? [{ from: span.from, to: span.to, mark, replaces: span.mark }] : [];
|
|
88
118
|
}
|
|
89
119
|
);
|
|
90
120
|
const fresh = open(gapsIn(stretch, spans)).map((gap) => ({
|
|
91
121
|
...gap,
|
|
92
|
-
mark: lockedControlMark(newControlId())
|
|
122
|
+
mark: lockedControlMark(newControlId(), wrapperDepthOver(block, gap))
|
|
93
123
|
}));
|
|
94
124
|
return [...shut, ...fresh];
|
|
95
125
|
}
|
|
126
|
+
function liftable(locked, reach) {
|
|
127
|
+
return locked.filter(
|
|
128
|
+
(span, index) => contains(reach, span) || !locked.slice(index + 1).some((inner) => contains(span, inner))
|
|
129
|
+
);
|
|
130
|
+
}
|
|
96
131
|
function selectionLockDetail(state) {
|
|
97
132
|
const selection = state.selection;
|
|
98
133
|
const locking = !selection.empty && selection instanceof TextSelection;
|
|
@@ -106,9 +141,14 @@ function selectionLockDetail(state) {
|
|
|
106
141
|
if (!node.isTextblock) return true;
|
|
107
142
|
const block = { node, start: pos + 1 };
|
|
108
143
|
const controls = controlSpans(block);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
144
|
+
spans.push(
|
|
145
|
+
...liftable(
|
|
146
|
+
controls.filter(
|
|
147
|
+
(span) => isLocked(span.mark) && reaches(span, reach)
|
|
148
|
+
),
|
|
149
|
+
reach
|
|
150
|
+
)
|
|
151
|
+
);
|
|
112
152
|
if (locking) {
|
|
113
153
|
edits.push(...blockLockEdits(state, block, controls, reach));
|
|
114
154
|
}
|
|
@@ -153,7 +193,10 @@ var lockSelection = guardedCommand((state) => {
|
|
|
153
193
|
const edits = lockEditsOf(selectionLockDetail(state));
|
|
154
194
|
if (edits.length === 0) return null;
|
|
155
195
|
const tr = state.tr;
|
|
156
|
-
for (const edit of edits)
|
|
196
|
+
for (const edit of edits) {
|
|
197
|
+
if (edit.replaces) tr.removeMark(edit.from, edit.to, edit.replaces);
|
|
198
|
+
tr.addMark(edit.from, edit.to, edit.mark);
|
|
199
|
+
}
|
|
157
200
|
return tr;
|
|
158
201
|
});
|
|
159
202
|
function unlockCell(tr, cell) {
|
|
@@ -171,8 +214,8 @@ var unlockSelection = guardedCommand((state) => {
|
|
|
171
214
|
const tr = state.tr.setMeta(unlockAllowed, true);
|
|
172
215
|
for (const span of spans) {
|
|
173
216
|
const opened = withLock(span.mark, false);
|
|
217
|
+
tr.removeMark(span.from, span.to, span.mark);
|
|
174
218
|
if (opened) tr.addMark(span.from, span.to, opened);
|
|
175
|
-
else tr.removeMark(span.from, span.to, span.mark);
|
|
176
219
|
}
|
|
177
220
|
for (const cell of cells) unlockCell(tr, cell);
|
|
178
221
|
return tr;
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
import {
|
|
3
3
|
paragraphAttrsOf,
|
|
4
4
|
resolveParagraph,
|
|
5
|
-
runMarkUnder
|
|
6
|
-
styleIdOf
|
|
5
|
+
runMarkUnder
|
|
7
6
|
} from "../../docx/formatting.js";
|
|
8
7
|
import {
|
|
9
8
|
withParagraphAlign,
|
|
10
9
|
withParagraphStyle
|
|
11
10
|
} from "../../docx/paraProps.js";
|
|
12
11
|
import { toParagraphFormat } from "../../model/format.js";
|
|
12
|
+
import { styleIdOf } from "../../ooxml/props.js";
|
|
13
13
|
import { docxSchema } from "../../schema/index.js";
|
|
14
14
|
import { lockedMarkOf } from "../../schema/locks.js";
|
|
15
15
|
import { documentFormatting } from "../documentStyles.js";
|
package/dist/ooxml/props.d.ts
CHANGED
|
@@ -118,3 +118,5 @@ export declare function editChild(props: Props, path: readonly [string, ...strin
|
|
|
118
118
|
* null if its shape cannot be made out, in which case the caller leaves the display values alone.
|
|
119
119
|
*/
|
|
120
120
|
export declare function parsePropsXml(xml: string): Element | null;
|
|
121
|
+
/** The style name the paragraph formatting XML points at. null if it points at none */
|
|
122
|
+
export declare function styleIdOf(pPr: unknown): string | null;
|
package/dist/ooxml/props.js
CHANGED
|
@@ -3,6 +3,7 @@ import { childOrderOf } from "./childOrder.js";
|
|
|
3
3
|
import { attrsText, emptyTagXml } from "./element.js";
|
|
4
4
|
import { wName } from "./names.js";
|
|
5
5
|
import { parseAttrs, readTag } from "./tagScan.js";
|
|
6
|
+
import { childValue } from "./units.js";
|
|
6
7
|
import { elementChildren, localPart, namespaceDecls, parseXml } from "./xml.js";
|
|
7
8
|
function rawAttrs(source, tag) {
|
|
8
9
|
const closeLength = tag.kind === "empty" ? 2 : 1;
|
|
@@ -207,6 +208,18 @@ function parsePropsXml(xml) {
|
|
|
207
208
|
return null;
|
|
208
209
|
}
|
|
209
210
|
}
|
|
211
|
+
var styleIdsByPPr = /* @__PURE__ */ new Map();
|
|
212
|
+
var STYLE_ID_CACHE_LIMIT = 2e3;
|
|
213
|
+
function styleIdOf(pPr) {
|
|
214
|
+
if (typeof pPr !== "string" || !pPr.includes("pStyle")) return null;
|
|
215
|
+
const known = styleIdsByPPr.get(pPr);
|
|
216
|
+
if (known !== void 0) return known;
|
|
217
|
+
const el = parsePropsXml(pPr);
|
|
218
|
+
const id = el ? childValue(el, "pStyle") : null;
|
|
219
|
+
if (styleIdsByPPr.size >= STYLE_ID_CACHE_LIMIT) styleIdsByPPr.clear();
|
|
220
|
+
styleIdsByPPr.set(pPr, id);
|
|
221
|
+
return id;
|
|
222
|
+
}
|
|
210
223
|
export {
|
|
211
224
|
attrsOf,
|
|
212
225
|
childElement,
|
|
@@ -220,5 +233,6 @@ export {
|
|
|
220
233
|
renderProps,
|
|
221
234
|
setChild,
|
|
222
235
|
setChildren,
|
|
236
|
+
styleIdOf,
|
|
223
237
|
withAttrs
|
|
224
238
|
};
|
package/dist/schema/attrRoles.js
CHANGED
|
@@ -124,6 +124,12 @@ var NODE_ATTR_ROLES = {
|
|
|
124
124
|
guarded: { role: "session", class: "derived" }
|
|
125
125
|
}
|
|
126
126
|
};
|
|
127
|
+
var WRAPPER_ATTR_ROLES = {
|
|
128
|
+
// Which wrapper of its kind this is, counted through the document as it was opened
|
|
129
|
+
key: { role: "session", class: "identity" },
|
|
130
|
+
// How deep inside the other wrappers this one stood in the file
|
|
131
|
+
depth: { role: "session", class: "identity" }
|
|
132
|
+
};
|
|
127
133
|
var MARK_ATTR_ROLES = {
|
|
128
134
|
run: {
|
|
129
135
|
rPr: { role: "source", class: "preserved" },
|
|
@@ -132,15 +138,14 @@ var MARK_ATTR_ROLES = {
|
|
|
132
138
|
},
|
|
133
139
|
sdt: {
|
|
134
140
|
sdtPrefix: { role: "source", class: "preserved" },
|
|
135
|
-
|
|
136
|
-
sdtKey: { role: "session", class: "identity" },
|
|
141
|
+
...WRAPPER_ATTR_ROLES,
|
|
137
142
|
contentsLocked: { role: "source", class: "derived" },
|
|
138
143
|
deletionLocked: { role: "source", class: "derived" }
|
|
139
144
|
},
|
|
140
145
|
link: {
|
|
141
146
|
linkPrefix: { role: "source", class: "preserved" },
|
|
142
147
|
href: { role: "source", class: "model" },
|
|
143
|
-
|
|
148
|
+
...WRAPPER_ATTR_ROLES
|
|
144
149
|
},
|
|
145
150
|
tab: { tabAttrs: { role: "source", class: "preserved" } }
|
|
146
151
|
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each node and mark looks like once it has left the editor.
|
|
3
|
+
*
|
|
4
|
+
* A node draws itself twice. `toDOM` draws the page, where the editor reads its own attributes
|
|
5
|
+
* back (an IME composition, a browser edit), so everything the document is written from stands on
|
|
6
|
+
* the element. A copy lands in Word, in a spreadsheet, in a mail: none of them can read a `w:pPr`,
|
|
7
|
+
* and none of them should be handed a comment's author or the body of a footnote. So the shape a
|
|
8
|
+
* copy takes is declared here, beside the schema, rather than worked out afterwards by taking the
|
|
9
|
+
* page's drawing apart.
|
|
10
|
+
*
|
|
11
|
+
* The two cannot drift apart unnoticed: `Readonly<Record<DocxNodeName, ...>>` refuses to compile
|
|
12
|
+
* until a node added to the schema says what it leaves as, and `clipboard.test.ts` asks the schema
|
|
13
|
+
* itself the same question at runtime.
|
|
14
|
+
*/
|
|
15
|
+
import { type DOMOutputSpec, DOMSerializer, type Mark, type Node as PMNode, type Schema, type Slice } from "prosemirror-model";
|
|
16
|
+
import { type docxSchema } from "./docxSchema";
|
|
17
|
+
/**
|
|
18
|
+
* The paragraph style a copy carries, which is the one thing a paste needs that the drawing does
|
|
19
|
+
* not already say. It is the style's id alone, where the editor draws the whole `w:pPr`.
|
|
20
|
+
*/
|
|
21
|
+
export declare const COPIED_STYLE_ATTRIBUTE = "data-style";
|
|
22
|
+
/**
|
|
23
|
+
* The address a link may carry, whichever way it travels.
|
|
24
|
+
*
|
|
25
|
+
* A copy writes one out and a paste reads one in, and an address this rule turns down is one
|
|
26
|
+
* neither end should act on, so both ask here.
|
|
27
|
+
*/
|
|
28
|
+
export declare function safeHref(value: string | null): string | null;
|
|
29
|
+
export interface ClipboardNodeSpec {
|
|
30
|
+
/**
|
|
31
|
+
* The HTML a copy carries. `null` in place of a function says this kind of node never leaves the
|
|
32
|
+
* editor; a function answering null says this one leaves nothing. Either way neither the node
|
|
33
|
+
* nor anything inside it is written.
|
|
34
|
+
*/
|
|
35
|
+
toClipboardDOM: ((node: PMNode) => DOMOutputSpec | null) | null;
|
|
36
|
+
/**
|
|
37
|
+
* The plain text a copy carries, given what its children said. A null answer leaves the node out
|
|
38
|
+
* of the text entirely, separator and all, so a block standing for something invisible does not
|
|
39
|
+
* open a blank line where it stood.
|
|
40
|
+
*/
|
|
41
|
+
toClipboardText: ((node: PMNode, children: readonly string[]) => string | null) | null;
|
|
42
|
+
}
|
|
43
|
+
export interface ClipboardMarkSpec {
|
|
44
|
+
/** null writes no wrapper, and the content it covered goes out on its own */
|
|
45
|
+
toClipboardDOM: ((mark: Mark) => DOMOutputSpec) | null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `Schema.nodes` is indexed by `string` as well as by the names it was built from, so `keyof` over
|
|
49
|
+
* it widens to `string`. The generic argument the schema literal was inferred with keeps them.
|
|
50
|
+
*/
|
|
51
|
+
export type DocxNodeName = typeof docxSchema extends Schema<infer N, string> ? N : never;
|
|
52
|
+
export type DocxMarkName = typeof docxSchema extends Schema<string, infer M> ? M : never;
|
|
53
|
+
export declare const clipboardSpecs: {
|
|
54
|
+
nodes: Readonly<Record<DocxNodeName, ClipboardNodeSpec>>;
|
|
55
|
+
marks: Readonly<Record<DocxMarkName, ClipboardMarkSpec>>;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The serializer a copy is written with.
|
|
59
|
+
*
|
|
60
|
+
* `stampCopy` is handed the whole copy once it stands, and only once: `DOMSerializer` draws a
|
|
61
|
+
* node's children by calling `serializeFragment` again with the element it drew as the target, so
|
|
62
|
+
* a call carrying one is inside the copy rather than around it.
|
|
63
|
+
*/
|
|
64
|
+
export declare function clipboardSerializer(stampCopy?: (copy: HTMLElement | DocumentFragment) => void): DOMSerializer;
|
|
65
|
+
/**
|
|
66
|
+
* The plain text a copy leaves beside the HTML.
|
|
67
|
+
*
|
|
68
|
+
* ProseMirror's own answer is the text content with a line between blocks, which loses a tab, a
|
|
69
|
+
* page break, and the difference between the next cell and the next row. Somewhere those are the
|
|
70
|
+
* whole of what was copied, a table pasted into a spreadsheet above all.
|
|
71
|
+
*/
|
|
72
|
+
export declare function clipboardText(slice: Slice): string;
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// src/schema/clipboard.ts
|
|
2
|
+
import {
|
|
3
|
+
DOMSerializer,
|
|
4
|
+
Fragment
|
|
5
|
+
} from "prosemirror-model";
|
|
6
|
+
import {
|
|
7
|
+
spanCount,
|
|
8
|
+
toCellFormat,
|
|
9
|
+
toGridCols,
|
|
10
|
+
toParagraphFormat,
|
|
11
|
+
toRowFormat,
|
|
12
|
+
toRunFormat,
|
|
13
|
+
toTableFormat,
|
|
14
|
+
toTableWidth
|
|
15
|
+
} from "../model/format.js";
|
|
16
|
+
import { styleIdOf } from "../ooxml/props.js";
|
|
17
|
+
import { editorClassNames } from "../styles/classNames.js";
|
|
18
|
+
import { DEFAULT_FONT_FALLBACKS } from "../styles/fontStack.js";
|
|
19
|
+
import {
|
|
20
|
+
cellStyle,
|
|
21
|
+
columnWidthPx,
|
|
22
|
+
paragraphStyle,
|
|
23
|
+
rowStyle,
|
|
24
|
+
tableStyle
|
|
25
|
+
} from "../styles/inlineStyle.js";
|
|
26
|
+
import { isPageBreak } from "./docxSchema.js";
|
|
27
|
+
import { imageAttrs, runAttrs } from "./rendering.js";
|
|
28
|
+
var COPIED_STYLE_ATTRIBUTE = "data-style";
|
|
29
|
+
function safeHref(value) {
|
|
30
|
+
const href = value?.trim() ?? "";
|
|
31
|
+
if (href === "") return null;
|
|
32
|
+
if (/^(?:https?|mailto|tel):/i.test(href)) return href;
|
|
33
|
+
return /^(?:[./]|#)/.test(href) ? href : null;
|
|
34
|
+
}
|
|
35
|
+
function textAttr(value) {
|
|
36
|
+
return typeof value === "string" ? value : void 0;
|
|
37
|
+
}
|
|
38
|
+
function noteLabel(node) {
|
|
39
|
+
return node.attrs.customMarkFollows === true ? "" : textAttr(node.attrs.label) ?? "";
|
|
40
|
+
}
|
|
41
|
+
function oneLine(text) {
|
|
42
|
+
return text.replace(/[\n\f]+/g, " ");
|
|
43
|
+
}
|
|
44
|
+
function spanAttribute(count) {
|
|
45
|
+
const span = spanCount(count);
|
|
46
|
+
return span > 1 ? `${span}` : void 0;
|
|
47
|
+
}
|
|
48
|
+
function preservedText(node) {
|
|
49
|
+
const display = node.attrs.display;
|
|
50
|
+
if (display === "break") return "\n";
|
|
51
|
+
if (display !== "text" && display !== "chip") return null;
|
|
52
|
+
const drawn = textAttr(node.attrs.text);
|
|
53
|
+
return drawn === void 0 || drawn === "" ? null : drawn;
|
|
54
|
+
}
|
|
55
|
+
var PRESERVED_SPEC = {
|
|
56
|
+
toClipboardDOM: (node) => {
|
|
57
|
+
const said = preservedText(node);
|
|
58
|
+
if (said === null) return null;
|
|
59
|
+
return said === "\n" ? ["br"] : ["span", said];
|
|
60
|
+
},
|
|
61
|
+
toClipboardText: preservedText
|
|
62
|
+
};
|
|
63
|
+
var WRITES_NOTHING = {
|
|
64
|
+
toClipboardDOM: null,
|
|
65
|
+
toClipboardText: null
|
|
66
|
+
};
|
|
67
|
+
var CLIPBOARD_NODES = {
|
|
68
|
+
// A slice carries blocks, never the document itself, so neither answer is ever asked for
|
|
69
|
+
doc: WRITES_NOTHING,
|
|
70
|
+
paragraph: {
|
|
71
|
+
toClipboardDOM: (node) => [
|
|
72
|
+
"p",
|
|
73
|
+
{
|
|
74
|
+
class: editorClassNames.paragraph,
|
|
75
|
+
style: paragraphStyle(
|
|
76
|
+
toParagraphFormat(node.attrs.format),
|
|
77
|
+
toRunFormat(node.attrs.styleRun)
|
|
78
|
+
),
|
|
79
|
+
[COPIED_STYLE_ATTRIBUTE]: styleIdOf(node.attrs.pPr) ?? void 0
|
|
80
|
+
},
|
|
81
|
+
0
|
|
82
|
+
],
|
|
83
|
+
toClipboardText: (_node, children) => children.join("")
|
|
84
|
+
},
|
|
85
|
+
table: {
|
|
86
|
+
toClipboardDOM: (node) => {
|
|
87
|
+
const attrs = {
|
|
88
|
+
class: editorClassNames.table,
|
|
89
|
+
style: tableStyle(
|
|
90
|
+
toTableFormat(node.attrs.format),
|
|
91
|
+
toTableWidth(node.attrs.tblW)
|
|
92
|
+
)
|
|
93
|
+
};
|
|
94
|
+
const body = ["tbody", 0];
|
|
95
|
+
const columns = toGridCols(node.attrs.gridCols).map((dxa) => [
|
|
96
|
+
"col",
|
|
97
|
+
{ style: `width:${columnWidthPx(dxa)}px` }
|
|
98
|
+
]);
|
|
99
|
+
if (columns.length === 0) return ["table", attrs, body];
|
|
100
|
+
return ["table", attrs, ["colgroup", ...columns], body];
|
|
101
|
+
},
|
|
102
|
+
toClipboardText: (_node, children) => children.join("\n")
|
|
103
|
+
},
|
|
104
|
+
tableRow: {
|
|
105
|
+
toClipboardDOM: (node) => [
|
|
106
|
+
"tr",
|
|
107
|
+
{
|
|
108
|
+
class: editorClassNames.tableRow,
|
|
109
|
+
style: rowStyle(toRowFormat(node.attrs.format))
|
|
110
|
+
},
|
|
111
|
+
0
|
|
112
|
+
],
|
|
113
|
+
toClipboardText: (_node, children) => children.join(" ")
|
|
114
|
+
},
|
|
115
|
+
tableCell: {
|
|
116
|
+
toClipboardDOM: (node) => [
|
|
117
|
+
"td",
|
|
118
|
+
{
|
|
119
|
+
class: editorClassNames.tableCell,
|
|
120
|
+
style: cellStyle(toCellFormat(node.attrs.format)),
|
|
121
|
+
colspan: spanAttribute(node.attrs.colspan),
|
|
122
|
+
rowspan: spanAttribute(node.attrs.rowspan)
|
|
123
|
+
},
|
|
124
|
+
0
|
|
125
|
+
],
|
|
126
|
+
// A tab stands between two cells and a line between two rows wherever a table is pasted as
|
|
127
|
+
// text, so nothing inside a cell may write a line: several paragraphs, a line break of their
|
|
128
|
+
// own and a nested table all say what they hold on the one line this cell stands on
|
|
129
|
+
toClipboardText: (_node, children) => oneLine(children.join(" "))
|
|
130
|
+
},
|
|
131
|
+
rawBlock: WRITES_NOTHING,
|
|
132
|
+
hardBreak: {
|
|
133
|
+
toClipboardDOM: () => ["br"],
|
|
134
|
+
// A page break ends more than a line, and the form feed is what says so in plain text
|
|
135
|
+
toClipboardText: (node) => isPageBreak(node.attrs.brAttrs) ? "\f" : "\n"
|
|
136
|
+
},
|
|
137
|
+
image: {
|
|
138
|
+
// The measure the document keeps stays behind. The pixels it was drawn at are what another
|
|
139
|
+
// application reads a size from, and what a paste back into this editor reads one from
|
|
140
|
+
toClipboardDOM: (node) => ["img", imageAttrs(node.attrs)],
|
|
141
|
+
// A picture is not text. But a copy may be read where no picture can follow, and there the
|
|
142
|
+
// words the document gave it for that very case are better than a gap
|
|
143
|
+
toClipboardText: (node) => textAttr(node.attrs.alt) ?? ""
|
|
144
|
+
},
|
|
145
|
+
commentStart: WRITES_NOTHING,
|
|
146
|
+
commentEnd: WRITES_NOTHING,
|
|
147
|
+
commentReference: WRITES_NOTHING,
|
|
148
|
+
noteReference: {
|
|
149
|
+
// A note whose own mark follows draws no number, and an empty superscript would stand for
|
|
150
|
+
// nothing wherever the copy lands
|
|
151
|
+
toClipboardDOM: (node) => {
|
|
152
|
+
const label = noteLabel(node);
|
|
153
|
+
if (label === "") return null;
|
|
154
|
+
const kind = node.attrs.kind === "endnote" ? "Endnote" : "Footnote";
|
|
155
|
+
return ["sup", { "aria-label": `${kind} ${label}` }, label];
|
|
156
|
+
},
|
|
157
|
+
toClipboardText: noteLabel
|
|
158
|
+
},
|
|
159
|
+
rawRunContent: PRESERVED_SPEC,
|
|
160
|
+
rawInline: PRESERVED_SPEC,
|
|
161
|
+
text: {
|
|
162
|
+
// `DOMSerializer` writes a text node itself and never asks a spec for one, so there is no
|
|
163
|
+
// drawing to declare. The characters travel as they stand
|
|
164
|
+
toClipboardDOM: null,
|
|
165
|
+
toClipboardText: (node) => node.text ?? ""
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
var CLIPBOARD_MARKS = {
|
|
169
|
+
// A content control is a wrapper the file keeps and nothing outside this editor reads back, so
|
|
170
|
+
// the text it held leaves on its own rather than inside a span standing for nothing
|
|
171
|
+
sdt: { toClipboardDOM: null },
|
|
172
|
+
link: {
|
|
173
|
+
// The editor draws the address as data so that a click inside the text places the caret.
|
|
174
|
+
// Anywhere else an anchor is what a link is: it follows in mail or a document, and it comes
|
|
175
|
+
// back as a link when it is pasted here again
|
|
176
|
+
toClipboardDOM: (mark) => {
|
|
177
|
+
const href = safeHref(textAttr(mark.attrs.href) ?? null);
|
|
178
|
+
const attrs = { class: editorClassNames.link, href: href ?? void 0 };
|
|
179
|
+
return href === null ? ["span", attrs, 0] : ["a", attrs, 0];
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
run: {
|
|
183
|
+
// The built-in fallbacks rather than the ones this editor was given: a copy is read where the
|
|
184
|
+
// host's font list means nothing, and the schema cannot see which editor it is drawing for
|
|
185
|
+
toClipboardDOM: (mark) => [
|
|
186
|
+
"span",
|
|
187
|
+
runAttrs(mark.attrs, DEFAULT_FONT_FALLBACKS),
|
|
188
|
+
0
|
|
189
|
+
]
|
|
190
|
+
},
|
|
191
|
+
tab: { toClipboardDOM: () => ["span", { class: editorClassNames.tab }, 0] }
|
|
192
|
+
};
|
|
193
|
+
var clipboardSpecs = { nodes: CLIPBOARD_NODES, marks: CLIPBOARD_MARKS };
|
|
194
|
+
var NODES_BY_NAME = CLIPBOARD_NODES;
|
|
195
|
+
function nodeSpec(node) {
|
|
196
|
+
return NODES_BY_NAME[node.type.name];
|
|
197
|
+
}
|
|
198
|
+
function clipboardDOMOf(node) {
|
|
199
|
+
const spec = nodeSpec(node);
|
|
200
|
+
if (spec === void 0 || spec.toClipboardDOM === null) return null;
|
|
201
|
+
return spec.toClipboardDOM(node);
|
|
202
|
+
}
|
|
203
|
+
function written(fragment) {
|
|
204
|
+
const kept = [];
|
|
205
|
+
fragment.forEach((child) => {
|
|
206
|
+
if (child.isText || clipboardDOMOf(child) !== null) kept.push(child);
|
|
207
|
+
});
|
|
208
|
+
return kept.length === fragment.childCount ? fragment : Fragment.from(kept);
|
|
209
|
+
}
|
|
210
|
+
var ClipboardSerializer = class extends DOMSerializer {
|
|
211
|
+
constructor(stampCopy) {
|
|
212
|
+
super(clipboardNodes(), clipboardMarks());
|
|
213
|
+
this.stampCopy = stampCopy;
|
|
214
|
+
}
|
|
215
|
+
stampCopy;
|
|
216
|
+
serializeFragment(fragment, options, target) {
|
|
217
|
+
const dom = super.serializeFragment(written(fragment), options, target);
|
|
218
|
+
if (target !== void 0) return dom;
|
|
219
|
+
if (dom.firstChild === null && fragment.childCount > 0) {
|
|
220
|
+
carrier(dom, options?.document);
|
|
221
|
+
}
|
|
222
|
+
this.stampCopy?.(dom);
|
|
223
|
+
return dom;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
function carrier(copy, document) {
|
|
227
|
+
const owner = document ?? copy.ownerDocument;
|
|
228
|
+
if (owner !== null) copy.appendChild(owner.createElement("span"));
|
|
229
|
+
}
|
|
230
|
+
function clipboardNodes() {
|
|
231
|
+
return Object.fromEntries(
|
|
232
|
+
Object.entries(CLIPBOARD_NODES).flatMap(([name, spec]) => {
|
|
233
|
+
const draw = spec.toClipboardDOM;
|
|
234
|
+
return draw === null ? [] : [
|
|
235
|
+
[
|
|
236
|
+
name,
|
|
237
|
+
(node) => {
|
|
238
|
+
const drawn = draw(node);
|
|
239
|
+
if (drawn === null) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`${name} was serialized after drawing nothing`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return drawn;
|
|
245
|
+
}
|
|
246
|
+
]
|
|
247
|
+
];
|
|
248
|
+
})
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
function clipboardMarks() {
|
|
252
|
+
return Object.fromEntries(
|
|
253
|
+
Object.entries(CLIPBOARD_MARKS).flatMap(
|
|
254
|
+
([name, spec]) => (
|
|
255
|
+
// A mark with no drawing of its own is left out, and `DOMSerializer` writes the content it
|
|
256
|
+
// covered without a wrapper
|
|
257
|
+
spec.toClipboardDOM === null ? [] : [[name, spec.toClipboardDOM]]
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
function clipboardSerializer(stampCopy) {
|
|
263
|
+
return new ClipboardSerializer(stampCopy);
|
|
264
|
+
}
|
|
265
|
+
function textPieces(fragment) {
|
|
266
|
+
const pieces = [];
|
|
267
|
+
fragment.forEach((child) => {
|
|
268
|
+
const piece = clipboardTextOf(child);
|
|
269
|
+
if (piece !== null) pieces.push(piece);
|
|
270
|
+
});
|
|
271
|
+
return pieces;
|
|
272
|
+
}
|
|
273
|
+
function clipboardTextOf(node) {
|
|
274
|
+
const spec = nodeSpec(node);
|
|
275
|
+
if (spec === void 0 || spec.toClipboardText === null) return null;
|
|
276
|
+
return spec.toClipboardText(node, textPieces(node.content));
|
|
277
|
+
}
|
|
278
|
+
function clipboardText(slice) {
|
|
279
|
+
const pieces = textPieces(slice.content);
|
|
280
|
+
return pieces.join(slice.content.firstChild?.isInline === true ? "" : "\n");
|
|
281
|
+
}
|
|
282
|
+
export {
|
|
283
|
+
COPIED_STYLE_ATTRIBUTE,
|
|
284
|
+
clipboardSerializer,
|
|
285
|
+
clipboardSpecs,
|
|
286
|
+
clipboardText,
|
|
287
|
+
safeHref
|
|
288
|
+
};
|