@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
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// src/editor/clipboard/htmlReader.ts
|
|
2
2
|
import { Fragment, Slice } from "prosemirror-model";
|
|
3
3
|
import {
|
|
4
|
-
paragraphAttrsFor
|
|
5
|
-
styleIdOf
|
|
4
|
+
paragraphAttrsFor
|
|
6
5
|
} from "../../docx/formatting.js";
|
|
7
6
|
import {
|
|
8
7
|
withListNumbering,
|
|
@@ -18,9 +17,14 @@ import {
|
|
|
18
17
|
MAX_ILVL,
|
|
19
18
|
templateList
|
|
20
19
|
} from "../../numbering/listTemplate.js";
|
|
20
|
+
import { styleIdOf } from "../../ooxml/props.js";
|
|
21
21
|
import { docxSchema } from "../../schema/index.js";
|
|
22
|
+
import { COPIED_STYLE_ATTRIBUTE } from "../../schema/clipboard.js";
|
|
22
23
|
import { editorClassNames } from "../../styles/classNames.js";
|
|
23
24
|
import { numIdsIn } from "../commands/listCommands.js";
|
|
25
|
+
import {
|
|
26
|
+
DEFAULT_BLOCK_READERS
|
|
27
|
+
} from "./blockReaders.js";
|
|
24
28
|
import { PASTED_IMAGE_ATTRIBUTE } from "./images.js";
|
|
25
29
|
import {
|
|
26
30
|
appendInline,
|
|
@@ -28,6 +32,7 @@ import {
|
|
|
28
32
|
marksFor,
|
|
29
33
|
withInlineStyle
|
|
30
34
|
} from "./inlineFormatting.js";
|
|
35
|
+
import { detectHtmlSource } from "./source.js";
|
|
31
36
|
var BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
32
37
|
"ADDRESS",
|
|
33
38
|
"ARTICLE",
|
|
@@ -45,6 +50,7 @@ var BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
|
45
50
|
"PRE",
|
|
46
51
|
"SECTION"
|
|
47
52
|
]);
|
|
53
|
+
var BLOCK_SELECTOR = [...BLOCK_TAGS, "OL", "UL", "TABLE"].map((tag) => tag.toLowerCase()).join(",");
|
|
48
54
|
var HEADING_LEVELS = {
|
|
49
55
|
H1: 1,
|
|
50
56
|
H2: 2,
|
|
@@ -61,7 +67,6 @@ var HEADING_FALLBACKS = {
|
|
|
61
67
|
5: { bold: true, fontSizePt: 10 },
|
|
62
68
|
6: { bold: true, fontSizePt: 8 }
|
|
63
69
|
};
|
|
64
|
-
var COPIED_STYLE_ATTRIBUTE = "data-style";
|
|
65
70
|
function normalizedStyleName(value) {
|
|
66
71
|
return value.toLowerCase().replace(/[\s_-]+/g, "");
|
|
67
72
|
}
|
|
@@ -127,18 +132,24 @@ function paragraphAttrs(formatting, list, paragraph) {
|
|
|
127
132
|
return props ? { pPr: props.pPr, ...paragraphAttrsFor(props.pPr, formatting) } : null;
|
|
128
133
|
}
|
|
129
134
|
var HtmlReader = class {
|
|
130
|
-
constructor(context, preserveParagraphStyles) {
|
|
135
|
+
constructor(context, preserveParagraphStyles, blockReaders) {
|
|
131
136
|
this.context = context;
|
|
132
137
|
this.preserveParagraphStyles = preserveParagraphStyles;
|
|
138
|
+
this.blockReaders = blockReaders;
|
|
133
139
|
this.formatting = context.formatting;
|
|
134
140
|
this.used = new Set(context.numbering.used);
|
|
135
141
|
this.canCreateLists = context.numbering.canCreate;
|
|
136
142
|
}
|
|
137
143
|
context;
|
|
138
144
|
preserveParagraphStyles;
|
|
145
|
+
blockReaders;
|
|
139
146
|
blocks = [];
|
|
140
147
|
used;
|
|
141
148
|
canCreateLists;
|
|
149
|
+
/** How deep the reading stands inside the tables it is reading */
|
|
150
|
+
tableDepth = 0;
|
|
151
|
+
/** The number each list a block reader named has taken, so its items join one list */
|
|
152
|
+
listNumbers = /* @__PURE__ */ new Map();
|
|
142
153
|
/**
|
|
143
154
|
* What the pasted paragraphs are resolved against: the document's context, holding the
|
|
144
155
|
* definition of each pasted list as it is read, so that an item is drawn against the list it
|
|
@@ -155,6 +166,9 @@ var HtmlReader = class {
|
|
|
155
166
|
}
|
|
156
167
|
appendInline(target, node, context) {
|
|
157
168
|
appendInline(target, node, context, (content, element, inline) => {
|
|
169
|
+
if (this.context.source === "word" && element.tagName === "O:P") {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
158
172
|
if (element.tagName !== "IMG") return false;
|
|
159
173
|
const token = element.getAttribute(PASTED_IMAGE_ATTRIBUTE);
|
|
160
174
|
const image = token === null ? void 0 : this.context.images.get(token);
|
|
@@ -173,17 +187,65 @@ var HtmlReader = class {
|
|
|
173
187
|
return true;
|
|
174
188
|
});
|
|
175
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* One paragraph of the reading. An item of a list this document may not start keeps its marker
|
|
192
|
+
* as text, since the list it names is one the document will not be numbering.
|
|
193
|
+
*/
|
|
194
|
+
paragraphNode(content, list = null, paragraph = null) {
|
|
195
|
+
const marker = list && list.numId === null ? [docxSchema.text(list.kind === "bullet" ? "\u2022 " : "1. ")] : [];
|
|
196
|
+
return docxSchema.nodes.paragraph.create(
|
|
197
|
+
paragraphAttrs(this.formatting, list, paragraph),
|
|
198
|
+
[...marker, ...content]
|
|
199
|
+
);
|
|
200
|
+
}
|
|
176
201
|
addParagraph(content, list = null, paragraph = null) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
202
|
+
this.blocks.push(this.paragraphNode(content, list, paragraph));
|
|
203
|
+
}
|
|
204
|
+
/** The blocks a nested reading produces, kept apart from the ones read so far */
|
|
205
|
+
collect(read) {
|
|
206
|
+
const outer = this.blocks;
|
|
207
|
+
this.blocks = [];
|
|
208
|
+
read();
|
|
209
|
+
const inner = this.blocks;
|
|
210
|
+
this.blocks = outer;
|
|
211
|
+
return inner;
|
|
212
|
+
}
|
|
213
|
+
/** What the first reader to answer for this element reads it as, or null where none does */
|
|
214
|
+
readBlock(element, context, host) {
|
|
215
|
+
for (const reader of this.blockReaders) {
|
|
216
|
+
const read = reader.read(element, context, host);
|
|
217
|
+
if (read !== null) return read;
|
|
180
218
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* What a block reader may ask of this reading. One is made per level of the reading, since a
|
|
223
|
+
* reader asks whether it stands inside a table and that answer is the level's own.
|
|
224
|
+
*/
|
|
225
|
+
hostView() {
|
|
226
|
+
return {
|
|
227
|
+
context: this.context,
|
|
228
|
+
inTable: this.tableDepth > 0,
|
|
229
|
+
readCell: (cell, inline) => {
|
|
230
|
+
this.tableDepth += 1;
|
|
231
|
+
const blocks = this.collect(() => this.readFlow(cell, inline));
|
|
232
|
+
this.tableDepth -= 1;
|
|
233
|
+
return blocks;
|
|
234
|
+
},
|
|
235
|
+
readInline: (nodes, inline) => {
|
|
236
|
+
const target = [];
|
|
237
|
+
for (const node of nodes) this.appendInline(target, node, inline);
|
|
238
|
+
return target;
|
|
239
|
+
},
|
|
240
|
+
paragraph: (content, list) => this.paragraphNode(content, list ?? null),
|
|
241
|
+
listNumber: (key, kind) => {
|
|
242
|
+
const known = this.listNumbers.get(key);
|
|
243
|
+
if (known !== void 0) return known;
|
|
244
|
+
const numId = this.takeNumId(kind);
|
|
245
|
+
this.listNumbers.set(key, numId);
|
|
246
|
+
return numId;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
187
249
|
}
|
|
188
250
|
readFlow(parent, context, paragraph = null) {
|
|
189
251
|
let inline = [];
|
|
@@ -192,12 +254,17 @@ var HtmlReader = class {
|
|
|
192
254
|
this.addParagraph(inline, null, paragraph);
|
|
193
255
|
inline = [];
|
|
194
256
|
};
|
|
257
|
+
const host = this.hostView();
|
|
195
258
|
for (const child of parent.childNodes) {
|
|
196
259
|
const element = child.nodeType === child.ELEMENT_NODE ? child : null;
|
|
197
|
-
|
|
260
|
+
const read = element && this.readBlock(element, context, host);
|
|
261
|
+
if (read) {
|
|
262
|
+
flush();
|
|
263
|
+
this.blocks.push(...read);
|
|
264
|
+
} else if (element?.tagName === "UL" || element?.tagName === "OL") {
|
|
198
265
|
flush();
|
|
199
266
|
this.readList(element, context, 0, null);
|
|
200
|
-
} else if (element && BLOCK_TAGS.has(element.tagName)) {
|
|
267
|
+
} else if (element && (BLOCK_TAGS.has(element.tagName) || element.querySelector(BLOCK_SELECTOR) !== null)) {
|
|
201
268
|
flush();
|
|
202
269
|
const before = this.blocks.length;
|
|
203
270
|
const block = blockContext(
|
|
@@ -238,18 +305,23 @@ var HtmlReader = class {
|
|
|
238
305
|
numId: inherited?.kind === kind ? inherited.numId : this.takeNumId(kind),
|
|
239
306
|
level
|
|
240
307
|
};
|
|
241
|
-
const items =
|
|
242
|
-
(child) => child.tagName === "LI"
|
|
308
|
+
const items = [...list.children].flatMap(
|
|
309
|
+
(child) => child instanceof HTMLElement && child.tagName === "LI" ? [child] : []
|
|
243
310
|
);
|
|
311
|
+
const host = this.hostView();
|
|
244
312
|
for (const item of items) {
|
|
245
313
|
const itemContext = contextFor(context, item);
|
|
246
314
|
const content = [];
|
|
315
|
+
const after = [];
|
|
247
316
|
for (const child of item.childNodes) {
|
|
248
|
-
const nested = child
|
|
317
|
+
const nested = child instanceof HTMLElement ? child : null;
|
|
249
318
|
if (nested?.tagName === "UL" || nested?.tagName === "OL") continue;
|
|
250
|
-
this.
|
|
319
|
+
const read = nested && this.readBlock(nested, itemContext, host);
|
|
320
|
+
if (read) after.push(...read);
|
|
321
|
+
else this.appendInline(content, child, itemContext);
|
|
251
322
|
}
|
|
252
323
|
this.addParagraph(content, listContext);
|
|
324
|
+
this.blocks.push(...after);
|
|
253
325
|
for (const nested of Array.from(item.children)) {
|
|
254
326
|
if (nested.tagName === "UL" || nested.tagName === "OL") {
|
|
255
327
|
this.readList(
|
|
@@ -270,12 +342,15 @@ function sliceDepth(root) {
|
|
|
270
342
|
10
|
|
271
343
|
);
|
|
272
344
|
if (Number.isFinite(openStart)) return openStart === 0 ? 0 : 1;
|
|
273
|
-
|
|
274
|
-
return root.querySelector(blocks.join(",")) ? 0 : 1;
|
|
345
|
+
return root.querySelector(BLOCK_SELECTOR) ? 0 : 1;
|
|
275
346
|
}
|
|
276
|
-
function readHtml(root, context) {
|
|
347
|
+
function readHtml(root, context, blockReaders = DEFAULT_BLOCK_READERS) {
|
|
277
348
|
const open = sliceDepth(root);
|
|
278
|
-
const reader = new HtmlReader(
|
|
349
|
+
const reader = new HtmlReader(
|
|
350
|
+
{ ...context, source: detectHtmlSource(root) },
|
|
351
|
+
open === 0,
|
|
352
|
+
blockReaders
|
|
353
|
+
);
|
|
279
354
|
const blocks = reader.read(root);
|
|
280
355
|
return blocks.length === 0 ? null : {
|
|
281
356
|
slice: new Slice(Fragment.fromArray([...blocks]), open, open),
|
|
@@ -295,7 +370,6 @@ function withPastedContent(tr, content) {
|
|
|
295
370
|
);
|
|
296
371
|
}
|
|
297
372
|
export {
|
|
298
|
-
COPIED_STYLE_ATTRIBUTE,
|
|
299
373
|
readHtml,
|
|
300
374
|
readHtmlSlice,
|
|
301
375
|
withPastedContent
|
|
@@ -15,13 +15,6 @@ export interface InlineContext {
|
|
|
15
15
|
preserveWhitespace: boolean;
|
|
16
16
|
}
|
|
17
17
|
export type InlineElementReader = (target: PMNode[], element: HTMLElement, context: InlineContext) => boolean;
|
|
18
|
-
/**
|
|
19
|
-
* The address a link may carry, whichever way it travels.
|
|
20
|
-
*
|
|
21
|
-
* A copy writes one out and a paste reads one in, and an address this rule turns down is one
|
|
22
|
-
* neither end should act on, so both ask here.
|
|
23
|
-
*/
|
|
24
|
-
export declare function safeHref(value: string | null): string | null;
|
|
25
18
|
export declare function contextFor(parent: InlineContext, element: HTMLElement, readElementStyle?: boolean): InlineContext;
|
|
26
19
|
export declare function withInlineStyle(context: InlineContext, style: InlineStyle): InlineContext;
|
|
27
20
|
export declare function marksFor(context: InlineContext): Mark[];
|
|
@@ -3,6 +3,7 @@ import { rPrOf } from "../../docx/formatting.js";
|
|
|
3
3
|
import { readRunProps } from "../../docx/runProps.js";
|
|
4
4
|
import { HALF_POINTS_PER_PT, ST_HpsMeasure } from "../../ooxml/simpleTypes.js";
|
|
5
5
|
import { docxSchema } from "../../schema/index.js";
|
|
6
|
+
import { safeHref } from "../../schema/clipboard.js";
|
|
6
7
|
import { editorClassNames } from "../../styles/classNames.js";
|
|
7
8
|
var IGNORED_TAGS = /* @__PURE__ */ new Set([
|
|
8
9
|
"HEAD",
|
|
@@ -135,12 +136,6 @@ function styleOf(parent, element) {
|
|
|
135
136
|
if (background !== null) style.background = background;
|
|
136
137
|
return style;
|
|
137
138
|
}
|
|
138
|
-
function safeHref(value) {
|
|
139
|
-
const href = value?.trim() ?? "";
|
|
140
|
-
if (href === "") return null;
|
|
141
|
-
if (/^(?:https?|mailto|tel):/i.test(href)) return href;
|
|
142
|
-
return /^(?:[./]|#)/.test(href) ? href : null;
|
|
143
|
-
}
|
|
144
139
|
function contextFor(parent, element, readElementStyle = true) {
|
|
145
140
|
const copiedEditorLink = element.classList.contains(editorClassNames.link) ? element.getAttribute("data-href") : null;
|
|
146
141
|
return {
|
|
@@ -217,6 +212,5 @@ export {
|
|
|
217
212
|
appendInline,
|
|
218
213
|
contextFor,
|
|
219
214
|
marksFor,
|
|
220
|
-
safeHref,
|
|
221
215
|
withInlineStyle
|
|
222
216
|
};
|
|
@@ -1,25 +1,18 @@
|
|
|
1
1
|
// src/editor/clipboard/plugin.ts
|
|
2
|
-
import {
|
|
3
|
-
DOMSerializer,
|
|
4
|
-
Fragment,
|
|
5
|
-
Slice
|
|
6
|
-
} from "prosemirror-model";
|
|
2
|
+
import { Fragment, Slice } from "prosemirror-model";
|
|
7
3
|
import { Plugin } from "prosemirror-state";
|
|
8
|
-
import { styleIdOf } from "../../docx/formatting.js";
|
|
9
4
|
import {
|
|
10
5
|
NEW_LISTS_ATTR,
|
|
11
6
|
NO_NEW_LISTS,
|
|
12
7
|
newListsOf,
|
|
13
8
|
newListsValue
|
|
14
9
|
} from "../../numbering/listRegistry.js";
|
|
15
|
-
import {
|
|
10
|
+
import { clipboardSerializer, clipboardText } from "../../schema/clipboard.js";
|
|
16
11
|
import { numIdsIn } from "../commands/listCommands.js";
|
|
17
12
|
import { documentOf } from "../editorDocument.js";
|
|
18
13
|
import { insertPlainText } from "../plainText.js";
|
|
19
14
|
import { moveCaretToDrop } from "../plugins/dropCaret.js";
|
|
20
15
|
import { documentNumbering } from "../plugins/numberingDecorations.js";
|
|
21
|
-
import { COPIED_STYLE_ATTRIBUTE } from "./htmlReader.js";
|
|
22
|
-
import { safeHref } from "./inlineFormatting.js";
|
|
23
16
|
import {
|
|
24
17
|
INTERNAL_TOKEN_ATTRIBUTE,
|
|
25
18
|
internalTokenOf,
|
|
@@ -35,132 +28,6 @@ import {
|
|
|
35
28
|
DEFAULT_READERS,
|
|
36
29
|
plainTextSlice
|
|
37
30
|
} 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
31
|
function bareWrappers(content, openStart, openEnd) {
|
|
165
32
|
const wrapper = content.firstChild;
|
|
166
33
|
if (openStart <= 1 || openEnd <= 1 || content.childCount !== 1)
|
|
@@ -181,9 +48,6 @@ function copiedSlice(slice) {
|
|
|
181
48
|
slice.openEnd
|
|
182
49
|
);
|
|
183
50
|
}
|
|
184
|
-
function clipboardText(slice) {
|
|
185
|
-
return fragmentText(slice.content);
|
|
186
|
-
}
|
|
187
51
|
function dragCopyModifier() {
|
|
188
52
|
return /Mac|iP(hone|[oa]d)/.test(navigator.platform) ? "altKey" : "ctrlKey";
|
|
189
53
|
}
|
|
@@ -198,7 +62,11 @@ function dropMoves(view, event) {
|
|
|
198
62
|
function docxClipboard(options = {}) {
|
|
199
63
|
const normalizers = options.normalizers ?? DEFAULT_NORMALIZERS;
|
|
200
64
|
let copyToken = null;
|
|
201
|
-
const serializer =
|
|
65
|
+
const serializer = clipboardSerializer((copy) => {
|
|
66
|
+
if (copyToken !== null) {
|
|
67
|
+
copy.firstElementChild?.setAttribute(INTERNAL_TOKEN_ATTRIBUTE, copyToken);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
202
70
|
let host = null;
|
|
203
71
|
const parser = new DocxClipboardParser(
|
|
204
72
|
options.readers ?? DEFAULT_READERS,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { EditorState } from "prosemirror-state";
|
|
2
2
|
import type { FormattingContext, ParagraphStyleOption } from "../../docx/formatting";
|
|
3
|
+
import type { PageGeometry } from "../../docx/pageGeometry";
|
|
3
4
|
import type { ImageToInsert } from "../insertImage";
|
|
5
|
+
import type { HtmlSource } from "./source";
|
|
4
6
|
/**
|
|
5
7
|
* Everything a piece of HTML is read against.
|
|
6
8
|
*
|
|
@@ -13,6 +15,8 @@ export interface HtmlReadContext {
|
|
|
13
15
|
/** The document the markup is read into an element of */
|
|
14
16
|
document: Document;
|
|
15
17
|
formatting: FormattingContext;
|
|
18
|
+
/** The paper a block read into the document lands on, which is the width a pasted table takes */
|
|
19
|
+
geometry: PageGeometry;
|
|
16
20
|
paragraphStyles: readonly ParagraphStyleOption[];
|
|
17
21
|
numbering: {
|
|
18
22
|
used: ReadonlySet<number>;
|
|
@@ -20,5 +24,11 @@ export interface HtmlReadContext {
|
|
|
20
24
|
};
|
|
21
25
|
/** The images already loaded for this read, by the token the markup carries */
|
|
22
26
|
images: ReadonlyMap<string, ImageToInsert>;
|
|
27
|
+
/**
|
|
28
|
+
* The application that wrote the markup being read (`./source`). It is a property of the markup
|
|
29
|
+
* and not of the document, so a context built before any markup is in hand names no application
|
|
30
|
+
* and `readHtml` fills it in from what it is handed.
|
|
31
|
+
*/
|
|
32
|
+
source: HtmlSource;
|
|
23
33
|
}
|
|
24
34
|
export declare function readContextOf(state: EditorState, document: Document, images?: ReadonlyMap<string, ImageToInsert>): HtmlReadContext;
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
// src/editor/clipboard/readContext.ts
|
|
2
2
|
import { numIdsIn } from "../commands/listCommands.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
documentFormatting,
|
|
5
|
+
documentParagraphStyles,
|
|
6
|
+
sectionGeometryAt
|
|
7
|
+
} from "../documentStyles.js";
|
|
4
8
|
import { canStartNewList } from "../plugins/numberingDecorations.js";
|
|
5
9
|
function readContextOf(state, document, images = /* @__PURE__ */ new Map()) {
|
|
6
10
|
return {
|
|
7
11
|
document,
|
|
8
12
|
formatting: documentFormatting(state),
|
|
13
|
+
geometry: sectionGeometryAt(state, state.selection.from),
|
|
9
14
|
paragraphStyles: documentParagraphStyles(state),
|
|
10
15
|
numbering: {
|
|
11
16
|
used: numIdsIn(state.doc),
|
|
12
17
|
canCreate: canStartNewList(state)
|
|
13
18
|
},
|
|
14
|
-
images
|
|
19
|
+
images,
|
|
20
|
+
source: "unknown"
|
|
15
21
|
};
|
|
16
22
|
}
|
|
17
23
|
export {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which application wrote the markup on the clipboard.
|
|
3
|
+
*
|
|
4
|
+
* Every writer of HTML says so somewhere - in a generator meta element, in a class prefix, in the
|
|
5
|
+
* id it wraps a copy in - and what it says decides which readings apply: Word's list paragraphs
|
|
6
|
+
* are lists rather than paragraphs beginning with a bullet character, and its `<o:p>` markers are
|
|
7
|
+
* nothing at all. A reading recognizing no writer stays with what the markup itself states.
|
|
8
|
+
*/
|
|
9
|
+
/** The writers whose markup is read differently from plain HTML */
|
|
10
|
+
export type HtmlSource = "editor" | "word" | "google-docs" | "libreoffice" | "unknown";
|
|
11
|
+
/** The application the markup under this element was written by */
|
|
12
|
+
export declare function detectHtmlSource(root: ParentNode): HtmlSource;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// src/editor/clipboard/source.ts
|
|
2
|
+
import { editorClassNames } from "../../styles/classNames.js";
|
|
3
|
+
import { INTERNAL_TOKEN_ATTRIBUTE } from "./internalChannel.js";
|
|
4
|
+
var SIGNATURES = [
|
|
5
|
+
["editor", `[${INTERNAL_TOKEN_ATTRIBUTE}], .${editorClassNames.paragraph}`],
|
|
6
|
+
["word", 'meta[name="Generator" i][content^="Microsoft Word" i], .MsoNormal'],
|
|
7
|
+
["google-docs", '[id^="docs-internal-guid-"]'],
|
|
8
|
+
[
|
|
9
|
+
"libreoffice",
|
|
10
|
+
'meta[name="generator" i][content*="LibreOffice" i], meta[name="generator" i][content*="OpenOffice" i]'
|
|
11
|
+
]
|
|
12
|
+
];
|
|
13
|
+
function detectHtmlSource(root) {
|
|
14
|
+
for (const [source, selector] of SIGNATURES) {
|
|
15
|
+
if (root.querySelector(selector) !== null) return source;
|
|
16
|
+
}
|
|
17
|
+
return "unknown";
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
detectHtmlSource
|
|
21
|
+
};
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "../../../docx/story.js";
|
|
15
15
|
import { docxSchema } from "../../../schema/index.js";
|
|
16
16
|
import { guardedCommand } from "../../../schema/guards.js";
|
|
17
|
+
import { isWrapperType } from "../../../schema/wrappers.js";
|
|
17
18
|
import {
|
|
18
19
|
reservedCommentIds,
|
|
19
20
|
reservedCommentParaIds
|
|
@@ -93,7 +94,7 @@ function wrapperMarksAt(state, pos, side) {
|
|
|
93
94
|
const resolved = state.doc.resolve(pos);
|
|
94
95
|
const adjacent = side === "before" ? resolved.nodeBefore : resolved.nodeAfter;
|
|
95
96
|
return (adjacent?.marks ?? resolved.marks()).filter(
|
|
96
|
-
(mark) => mark.type
|
|
97
|
+
(mark) => isWrapperType(mark.type)
|
|
97
98
|
);
|
|
98
99
|
}
|
|
99
100
|
function addCommentTransaction(state, comment, at) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/editor/commands/linkCommands.ts
|
|
2
2
|
import { docxSchema } from "../../schema/index.js";
|
|
3
3
|
import { openStretches } from "../../schema/guards.js";
|
|
4
|
+
import { innermostDepth, wrapperMarks, wrapperOf } from "../../schema/wrappers.js";
|
|
4
5
|
var linkType = docxSchema.marks.link;
|
|
5
6
|
function text(value) {
|
|
6
7
|
return typeof value === "string" ? value : null;
|
|
@@ -16,6 +17,15 @@ function settled(address) {
|
|
|
16
17
|
function linkMarkOf(marks) {
|
|
17
18
|
return marks.find((mark) => mark.type === linkType) ?? null;
|
|
18
19
|
}
|
|
20
|
+
function linkOf(node) {
|
|
21
|
+
return wrapperOf(node, linkType);
|
|
22
|
+
}
|
|
23
|
+
function sameLink(a, b) {
|
|
24
|
+
return atDepth(a, 0).eq(atDepth(b, 0));
|
|
25
|
+
}
|
|
26
|
+
function atDepth(mark, depth) {
|
|
27
|
+
return mark.attrs.depth === depth ? mark : mark.type.create({ ...mark.attrs, depth });
|
|
28
|
+
}
|
|
19
29
|
function linkable(node, parent) {
|
|
20
30
|
if (parent === null || !parent.type.allowsMarkType(linkType)) return false;
|
|
21
31
|
return node.isText || node.type.allowsMarkType(linkType);
|
|
@@ -28,7 +38,10 @@ function piecesIn(doc, from, to) {
|
|
|
28
38
|
const start = Math.max(pos, from);
|
|
29
39
|
const end = Math.min(pos + node.nodeSize, to);
|
|
30
40
|
if (start < end) {
|
|
31
|
-
|
|
41
|
+
const link = linkOf(node);
|
|
42
|
+
pieces.push(
|
|
43
|
+
link === null ? { from: start, to: end, link, wrappers: wrapperMarks(node) } : { from: start, to: end, link }
|
|
44
|
+
);
|
|
32
45
|
}
|
|
33
46
|
return false;
|
|
34
47
|
});
|
|
@@ -37,12 +50,12 @@ function piecesIn(doc, from, to) {
|
|
|
37
50
|
function linkSpans(parent, start) {
|
|
38
51
|
const spans = [];
|
|
39
52
|
parent.forEach((child, offset) => {
|
|
40
|
-
const link =
|
|
53
|
+
const link = linkOf(child);
|
|
41
54
|
if (!link) return;
|
|
42
55
|
const from = start + offset;
|
|
43
56
|
const to = from + child.nodeSize;
|
|
44
57
|
const last = spans.at(-1);
|
|
45
|
-
if (last && last.to === from && last.link
|
|
58
|
+
if (last && last.to === from && sameLink(last.link, link)) last.to = to;
|
|
46
59
|
else spans.push({ from, to, link });
|
|
47
60
|
});
|
|
48
61
|
return spans;
|
|
@@ -52,7 +65,7 @@ function caretSpan(state) {
|
|
|
52
65
|
const link = linkMarkOf($from.marks());
|
|
53
66
|
if (!link) return null;
|
|
54
67
|
return linkSpans($from.parent, $from.start()).find(
|
|
55
|
-
(span) => span.link
|
|
68
|
+
(span) => sameLink(span.link, link) && span.from <= $from.pos && $from.pos <= span.to
|
|
56
69
|
) ?? null;
|
|
57
70
|
}
|
|
58
71
|
function selectionSpan(state) {
|
|
@@ -93,7 +106,9 @@ function selectionPieces(state) {
|
|
|
93
106
|
]);
|
|
94
107
|
}
|
|
95
108
|
function linkMarkFor(piece, href) {
|
|
96
|
-
|
|
109
|
+
if (piece.link !== null)
|
|
110
|
+
return linkType.create({ ...piece.link.attrs, href });
|
|
111
|
+
return linkType.create({ href, depth: innermostDepth(piece.wrappers) + 1 });
|
|
97
112
|
}
|
|
98
113
|
function setLink(url) {
|
|
99
114
|
return (state, dispatch) => {
|