vitest-prosemirror 0.3.2 → 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/dist/vitest-prosemirror.d.ts +42 -16
- package/dist/vitest-prosemirror.js +640 -219
- package/package.json +7 -4
|
@@ -1,44 +1,70 @@
|
|
|
1
1
|
import 'vitest';
|
|
2
|
+
import { Command } from 'prosemirror-state';
|
|
3
|
+
import { DirectEditorProps } from 'prosemirror-view';
|
|
4
|
+
import { EditorState } from 'prosemirror-state';
|
|
2
5
|
import { Node as Node_2 } from 'prosemirror-model';
|
|
3
|
-
import {
|
|
6
|
+
import { ParseOptions } from 'prosemirror-model';
|
|
4
7
|
import { Schema } from 'prosemirror-model';
|
|
5
8
|
import { Selection as Selection_2 } from 'prosemirror-state';
|
|
6
9
|
|
|
10
|
+
declare interface Clipboard_2 {
|
|
11
|
+
html: string;
|
|
12
|
+
text: string;
|
|
13
|
+
}
|
|
14
|
+
export { Clipboard_2 as Clipboard }
|
|
15
|
+
|
|
7
16
|
export declare interface CustomMatchers<R = unknown> {
|
|
8
17
|
toEqualProseMirrorNode(expected: Node_2): R;
|
|
18
|
+
toHaveSelection(expected: TesterSelection): R;
|
|
9
19
|
}
|
|
10
20
|
|
|
11
|
-
export declare interface
|
|
12
|
-
|
|
13
|
-
ctrlKey?: boolean;
|
|
14
|
-
metaKey?: boolean;
|
|
15
|
-
shiftKey?: boolean;
|
|
21
|
+
export declare interface Options extends Omit<DirectEditorProps, "dispatchTransaction" | "state"> {
|
|
22
|
+
autoCleanup: boolean;
|
|
16
23
|
}
|
|
17
24
|
|
|
18
|
-
export declare
|
|
19
|
-
|
|
25
|
+
export declare function parseHTML(html: string, schema: Schema, options?: ParseOptions): Node_2;
|
|
26
|
+
|
|
27
|
+
export { ParseOptions }
|
|
28
|
+
|
|
29
|
+
export declare interface PasteContent {
|
|
30
|
+
files?: Array<File>;
|
|
31
|
+
html?: string;
|
|
32
|
+
plainText?: boolean;
|
|
33
|
+
text?: string;
|
|
20
34
|
}
|
|
21
35
|
|
|
36
|
+
export declare type PasteInput = PasteContent | Node_2 | string;
|
|
37
|
+
|
|
22
38
|
export declare class ProseMirrorTester {
|
|
23
39
|
get doc(): Node_2;
|
|
24
|
-
get
|
|
40
|
+
get html(): string;
|
|
41
|
+
get state(): EditorState;
|
|
42
|
+
get text(): string;
|
|
43
|
+
private destroyed;
|
|
44
|
+
private readonly mountPoint;
|
|
25
45
|
private readonly view;
|
|
26
46
|
constructor(documentRoot: Node_2, options?: Partial<Options>);
|
|
27
|
-
|
|
47
|
+
click(target: Element | string): boolean;
|
|
48
|
+
command(command: Command): boolean;
|
|
49
|
+
copy(): Clipboard_2;
|
|
50
|
+
destroy(): void;
|
|
51
|
+
element(selector: string): HTMLElement;
|
|
52
|
+
elements(selector: string): Array<HTMLElement>;
|
|
53
|
+
insertText(text: string): void;
|
|
54
|
+
paste(content: PasteInput): void;
|
|
28
55
|
selectText(selection: TesterSelection): void;
|
|
56
|
+
private assertAlive;
|
|
29
57
|
}
|
|
30
58
|
|
|
31
59
|
export declare type TesterSelection = "all" | "end" | "start" | {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
} | Selection_2 | number;
|
|
60
|
+
anchor: number | string;
|
|
61
|
+
head: number | string;
|
|
62
|
+
} | (string & {}) | Selection_2 | number;
|
|
35
63
|
|
|
36
64
|
export { }
|
|
37
65
|
|
|
38
66
|
|
|
39
67
|
declare module "vitest" {
|
|
40
|
-
interface
|
|
41
|
-
}
|
|
42
|
-
interface AsymmetricMatchersContaining extends CustomMatchers {
|
|
68
|
+
interface Matchers<R extends Promise<void> | void = Promise<void> | void, T = unknown> extends CustomMatchers<R> {
|
|
43
69
|
}
|
|
44
70
|
}
|
|
@@ -1,146 +1,35 @@
|
|
|
1
|
-
import { expect } from "vitest";
|
|
2
|
-
import stringifyObject from "stringify-object";
|
|
1
|
+
import { afterEach, expect } from "vitest";
|
|
3
2
|
import { AllSelection, EditorState, Selection, TextSelection } from "prosemirror-state";
|
|
4
3
|
import { EditorView } from "prosemirror-view";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
content = `${mark.type.name}(${items.join(", ")})`;
|
|
16
|
-
}
|
|
17
|
-
return content;
|
|
18
|
-
}
|
|
19
|
-
var renamedTypes = {
|
|
20
|
-
hardBreak: "br",
|
|
21
|
-
heading: "h",
|
|
22
|
-
horizontalRule: "hr",
|
|
23
|
-
paragraph: "p"
|
|
24
|
-
};
|
|
25
|
-
function stringifyProseMirrorNode(node, indentation = "") {
|
|
26
|
-
if (node.type.name === "text") return `${indentation}${getMarks(node.marks, node.text ?? "")}`;
|
|
27
|
-
const type = renamedTypes[node.type.name] ?? node.type.name;
|
|
28
|
-
const content = [];
|
|
29
|
-
const nextIndentation = `${indentation} `;
|
|
30
|
-
if (Object.keys(node.attrs).length > 0) content.push(`${nextIndentation}${stringifyObject(node.attrs, {
|
|
31
|
-
indent: " ",
|
|
32
|
-
inlineCharacterLimit: 1e3
|
|
33
|
-
})},`);
|
|
34
|
-
node.content.forEach((item) => {
|
|
35
|
-
content.push(`${stringifyProseMirrorNode(item, nextIndentation)},`);
|
|
36
|
-
});
|
|
37
|
-
if (content.length === 0) return `${indentation}${type}()`;
|
|
38
|
-
if (content.length === 1 && node.content.firstChild?.type.name === "text") return `${indentation}${type}(${stringifyProseMirrorNode(node.content.firstChild, "")})`;
|
|
39
|
-
return `${indentation}${type}(\n${content.join("\n")}\n${indentation})`;
|
|
40
|
-
}
|
|
41
|
-
//#endregion
|
|
42
|
-
//#region src/utils/mockRangeRects.ts
|
|
43
|
-
var mockRangeRects = () => {
|
|
44
|
-
const zeroRect = () => new DOMRect(0, 0, 0, 0);
|
|
45
|
-
const emptyRectList = () => [];
|
|
46
|
-
const rangePrototype = Range.prototype;
|
|
47
|
-
if (typeof rangePrototype.getBoundingClientRect !== "function") Range.prototype.getBoundingClientRect = zeroRect;
|
|
48
|
-
if (typeof rangePrototype.getClientRects !== "function") Range.prototype.getClientRects = emptyRectList;
|
|
49
|
-
};
|
|
50
|
-
//#endregion
|
|
51
|
-
//#region src/utils/MutationObserverMock.ts
|
|
52
|
-
var MutationObserverMock = class MutationObserverMock {
|
|
53
|
-
static activeObservers = /* @__PURE__ */ new Map();
|
|
54
|
-
callback;
|
|
55
|
-
target;
|
|
56
|
-
constructor(callback) {
|
|
57
|
-
this.callback = callback;
|
|
58
|
-
this.target = void 0;
|
|
59
|
-
}
|
|
60
|
-
static createMutation(target, mutationRecords) {
|
|
61
|
-
const observer = MutationObserverMock.activeObservers.get(target);
|
|
62
|
-
if (observer === void 0) return;
|
|
63
|
-
observer.callback(mutationRecords.map((record) => ({
|
|
64
|
-
addedNodes: [],
|
|
65
|
-
attributeName: null,
|
|
66
|
-
attributeNamespace: null,
|
|
67
|
-
nextSibling: null,
|
|
68
|
-
oldValue: null,
|
|
69
|
-
previousSibling: null,
|
|
70
|
-
removedNodes: [],
|
|
71
|
-
...record
|
|
72
|
-
})), observer);
|
|
73
|
-
}
|
|
74
|
-
disconnect() {
|
|
75
|
-
if (this.target !== void 0) MutationObserverMock.activeObservers.delete(this.target);
|
|
76
|
-
this.target = void 0;
|
|
77
|
-
}
|
|
78
|
-
observe(target) {
|
|
79
|
-
this.target = target;
|
|
80
|
-
MutationObserverMock.activeObservers.set(target, this);
|
|
81
|
-
}
|
|
82
|
-
takeRecords() {
|
|
83
|
-
return [];
|
|
84
|
-
}
|
|
4
|
+
import { DOMParser, Fragment, Node, Slice } from "prosemirror-model";
|
|
5
|
+
import stringifyObject from "stringify-object";
|
|
6
|
+
//#region src/isProseMirrorNode.ts
|
|
7
|
+
var isProseMirrorNode = (val) => {
|
|
8
|
+
if (typeof val !== "object" || val === null) return false;
|
|
9
|
+
const node = val;
|
|
10
|
+
const type = node["type"];
|
|
11
|
+
if (typeof type !== "object" || type === null) return false;
|
|
12
|
+
const typeRecord = type;
|
|
13
|
+
return typeof typeRecord["name"] === "string" && typeof typeRecord["schema"] === "object" && "marks" in node && "attrs" in node && "content" in node;
|
|
85
14
|
};
|
|
86
15
|
//#endregion
|
|
87
|
-
//#region src/
|
|
88
|
-
var
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
let pos = 0;
|
|
93
|
-
if (selection === "end") pos = doc.nodeSize - 2;
|
|
94
|
-
else if (selection !== "start") pos = selection;
|
|
95
|
-
return TextSelection.near(doc.resolve(pos));
|
|
16
|
+
//#region src/isEditorState.ts
|
|
17
|
+
var isEditorState = (val) => {
|
|
18
|
+
if (typeof val !== "object" || val === null) return false;
|
|
19
|
+
const state = val;
|
|
20
|
+
return isProseMirrorNode(state["doc"]) && typeof state["selection"] === "object" && state["selection"] !== null && "storedMarks" in state;
|
|
96
21
|
};
|
|
97
22
|
//#endregion
|
|
98
|
-
//#region src/
|
|
99
|
-
var
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const after = node.childNodes[offset];
|
|
105
|
-
if (after instanceof CharacterData) return {
|
|
106
|
-
offset: 0,
|
|
107
|
-
target: after
|
|
23
|
+
//#region src/clipboard/copy.ts
|
|
24
|
+
var copy = (view) => {
|
|
25
|
+
const { dom, text } = view.serializeForClipboard(view.state.selection.content());
|
|
26
|
+
return {
|
|
27
|
+
html: dom.innerHTML,
|
|
28
|
+
text
|
|
108
29
|
};
|
|
109
|
-
return null;
|
|
110
30
|
};
|
|
111
31
|
//#endregion
|
|
112
|
-
//#region src/
|
|
113
|
-
function tokenizeKeyboardInput(input) {
|
|
114
|
-
const output = [];
|
|
115
|
-
let currentGroupOpener = null;
|
|
116
|
-
let group = "";
|
|
117
|
-
for (const char of input) if (currentGroupOpener !== null) {
|
|
118
|
-
if (["]", "}"].includes(char)) {
|
|
119
|
-
if (group.endsWith("\\") && char === matchingBrace(currentGroupOpener)) group = group.slice(0, -2) + char;
|
|
120
|
-
else if (char === matchingBrace(currentGroupOpener)) {
|
|
121
|
-
if (group.length === 4 && group.startsWith("Key")) output.push(group.slice(3).toLowerCase());
|
|
122
|
-
else output.push(group);
|
|
123
|
-
currentGroupOpener = null;
|
|
124
|
-
group = "";
|
|
125
|
-
} else group += char;
|
|
126
|
-
} else if (group === "" && currentGroupOpener === char) {
|
|
127
|
-
output.push(char);
|
|
128
|
-
currentGroupOpener = null;
|
|
129
|
-
group = "";
|
|
130
|
-
} else group += char;
|
|
131
|
-
} else if (["[", "{"].includes(char)) currentGroupOpener = char;
|
|
132
|
-
else output.push(char);
|
|
133
|
-
output.forEach(assertSupported);
|
|
134
|
-
return output;
|
|
135
|
-
}
|
|
136
|
-
function assertSupported(character) {
|
|
137
|
-
if (/^\/.+/u.exec(character) || /.+>[\d]*\/?$/u.exec(character)) throw new Error("Unsupported keyboard input");
|
|
138
|
-
}
|
|
139
|
-
function matchingBrace(opener) {
|
|
140
|
-
return opener === "{" ? "}" : "]";
|
|
141
|
-
}
|
|
142
|
-
//#endregion
|
|
143
|
-
//#region src/utils/keyIdentity.ts
|
|
32
|
+
//#region src/typing/keyIdentity.ts
|
|
144
33
|
var namedKeys = new Map(Object.entries({
|
|
145
34
|
Alt: 18,
|
|
146
35
|
ArrowDown: 40,
|
|
@@ -457,7 +346,352 @@ function keyIdentity(key) {
|
|
|
457
346
|
};
|
|
458
347
|
}
|
|
459
348
|
//#endregion
|
|
460
|
-
//#region src/
|
|
349
|
+
//#region src/clipboard/ClipboardEventMock.ts
|
|
350
|
+
var ClipboardEventMock = class extends Event {
|
|
351
|
+
clipboardData;
|
|
352
|
+
constructor(type, clipboardData) {
|
|
353
|
+
super(type, {
|
|
354
|
+
bubbles: true,
|
|
355
|
+
cancelable: true,
|
|
356
|
+
composed: true
|
|
357
|
+
});
|
|
358
|
+
this.clipboardData = clipboardData;
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
//#endregion
|
|
362
|
+
//#region src/clipboard/DataTransferItemMock.ts
|
|
363
|
+
var DataTransferItemMock = class {
|
|
364
|
+
kind = "file";
|
|
365
|
+
type;
|
|
366
|
+
file;
|
|
367
|
+
constructor(file) {
|
|
368
|
+
this.file = file;
|
|
369
|
+
this.type = file.type;
|
|
370
|
+
}
|
|
371
|
+
getAsFile() {
|
|
372
|
+
return this.file;
|
|
373
|
+
}
|
|
374
|
+
getAsString() {}
|
|
375
|
+
webkitGetAsEntry() {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
//#endregion
|
|
380
|
+
//#region src/clipboard/DataTransferMock.ts
|
|
381
|
+
var normalizeType = (format) => format.toLowerCase() === "text" ? "text/plain" : format.toLowerCase();
|
|
382
|
+
var DataTransferMock = class {
|
|
383
|
+
dropEffect = "none";
|
|
384
|
+
effectAllowed = "uninitialized";
|
|
385
|
+
files;
|
|
386
|
+
items;
|
|
387
|
+
get types() {
|
|
388
|
+
return this.files.length > 0 ? [...this.store.keys(), "Files"] : [...this.store.keys()];
|
|
389
|
+
}
|
|
390
|
+
store = /* @__PURE__ */ new Map();
|
|
391
|
+
constructor(files = []) {
|
|
392
|
+
this.files = Object.assign([...files], { item: (index) => files[index] ?? null });
|
|
393
|
+
this.items = Object.assign(files.map((file) => new DataTransferItemMock(file)), {
|
|
394
|
+
add: () => null,
|
|
395
|
+
clear: () => void 0,
|
|
396
|
+
remove: () => {
|
|
397
|
+
throw new DOMException("The DataTransferItemList is not in the read/write mode.", "InvalidStateError");
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
clearData(format) {
|
|
402
|
+
if (format === void 0) this.store.clear();
|
|
403
|
+
else this.store.delete(normalizeType(format));
|
|
404
|
+
}
|
|
405
|
+
getData(format) {
|
|
406
|
+
return this.store.get(normalizeType(format)) ?? "";
|
|
407
|
+
}
|
|
408
|
+
setData(format, data) {
|
|
409
|
+
this.store.set(normalizeType(format), data);
|
|
410
|
+
}
|
|
411
|
+
setDragImage() {
|
|
412
|
+
throw new Error("setDragImage is not supported: ProseMirrorTester models clipboard paste/copy, not drag-and-drop.");
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
//#endregion
|
|
416
|
+
//#region src/clipboard/paste.ts
|
|
417
|
+
var dispatchShift = (view, type) => {
|
|
418
|
+
const identity = keyIdentity("Shift");
|
|
419
|
+
view.dispatchEvent(new KeyboardEvent(type, {
|
|
420
|
+
bubbles: true,
|
|
421
|
+
cancelable: true,
|
|
422
|
+
code: identity.code,
|
|
423
|
+
composed: true,
|
|
424
|
+
key: identity.key,
|
|
425
|
+
keyCode: identity.keyCode,
|
|
426
|
+
location: identity.location,
|
|
427
|
+
shiftKey: true
|
|
428
|
+
}));
|
|
429
|
+
};
|
|
430
|
+
var normalize = (view, content) => {
|
|
431
|
+
if (typeof content === "string") return { text: content };
|
|
432
|
+
if (content instanceof Node) {
|
|
433
|
+
const { dom, text } = view.serializeForClipboard(new Slice(Fragment.from(content), 0, 0));
|
|
434
|
+
return {
|
|
435
|
+
html: dom.innerHTML,
|
|
436
|
+
text
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return content;
|
|
440
|
+
};
|
|
441
|
+
var paste = (view, content) => {
|
|
442
|
+
const { files, html, plainText, text } = normalize(view, content);
|
|
443
|
+
const data = new DataTransferMock(files);
|
|
444
|
+
if (text !== void 0) data.setData("text/plain", text);
|
|
445
|
+
if (html !== void 0) data.setData("text/html", html);
|
|
446
|
+
if (plainText === true) dispatchShift(view, "keydown");
|
|
447
|
+
view.dom.dispatchEvent(new ClipboardEventMock("paste", data));
|
|
448
|
+
if (plainText === true) dispatchShift(view, "keyup");
|
|
449
|
+
};
|
|
450
|
+
//#endregion
|
|
451
|
+
//#region src/domInteraction.ts
|
|
452
|
+
var clickHandlerProps = [
|
|
453
|
+
"handleClick",
|
|
454
|
+
"handleClickOn",
|
|
455
|
+
"handleDoubleClick",
|
|
456
|
+
"handleDoubleClickOn",
|
|
457
|
+
"handleTripleClick",
|
|
458
|
+
"handleTripleClickOn"
|
|
459
|
+
];
|
|
460
|
+
var element = (view, selector) => {
|
|
461
|
+
const found = view.dom.querySelector(selector);
|
|
462
|
+
if (found === null) throw new Error(`No element matching "${selector}" was found in the editor DOM.`);
|
|
463
|
+
return found;
|
|
464
|
+
};
|
|
465
|
+
var elements = (view, selector) => [...view.dom.querySelectorAll(selector)];
|
|
466
|
+
var click = (view, target) => {
|
|
467
|
+
if (clickHandlerProps.some((prop) => view.someProp(prop, () => true) === true)) throw new Error("click() cannot reach handleClick / handleClickOn / handleDoubleClick* / handleTripleClick* handlers: jsdom has no layout, so posAtCoords is always null and ProseMirror's mousedown handler bails before calling them. Click a node view's own DOM element instead.");
|
|
468
|
+
const target_ = typeof target === "string" ? element(view, target) : target;
|
|
469
|
+
const event = new MouseEvent("click", {
|
|
470
|
+
bubbles: true,
|
|
471
|
+
cancelable: true
|
|
472
|
+
});
|
|
473
|
+
target_.dispatchEvent(event);
|
|
474
|
+
return event.defaultPrevented;
|
|
475
|
+
};
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region src/mockRangeRects.ts
|
|
478
|
+
var mockRangeRects = () => {
|
|
479
|
+
const zeroRect = () => new DOMRect(0, 0, 0, 0);
|
|
480
|
+
const emptyRectList = () => [];
|
|
481
|
+
const rangePrototype = Range.prototype;
|
|
482
|
+
if (typeof rangePrototype.getBoundingClientRect !== "function") Range.prototype.getBoundingClientRect = zeroRect;
|
|
483
|
+
if (typeof rangePrototype.getClientRects !== "function") Range.prototype.getClientRects = emptyRectList;
|
|
484
|
+
};
|
|
485
|
+
//#endregion
|
|
486
|
+
//#region src/MutationObserverMock.ts
|
|
487
|
+
var MutationObserverMock = class MutationObserverMock {
|
|
488
|
+
static activeObservers = /* @__PURE__ */ new Map();
|
|
489
|
+
callback;
|
|
490
|
+
target;
|
|
491
|
+
constructor(callback) {
|
|
492
|
+
this.callback = callback;
|
|
493
|
+
this.target = void 0;
|
|
494
|
+
}
|
|
495
|
+
static createMutation(target, mutationRecords) {
|
|
496
|
+
const observer = MutationObserverMock.activeObservers.get(target);
|
|
497
|
+
if (observer === void 0) return;
|
|
498
|
+
observer.callback(mutationRecords.map((record) => ({
|
|
499
|
+
addedNodes: [],
|
|
500
|
+
attributeName: null,
|
|
501
|
+
attributeNamespace: null,
|
|
502
|
+
nextSibling: null,
|
|
503
|
+
oldValue: null,
|
|
504
|
+
previousSibling: null,
|
|
505
|
+
removedNodes: [],
|
|
506
|
+
...record
|
|
507
|
+
})), observer);
|
|
508
|
+
}
|
|
509
|
+
disconnect() {
|
|
510
|
+
if (this.target !== void 0) MutationObserverMock.activeObservers.delete(this.target);
|
|
511
|
+
this.target = void 0;
|
|
512
|
+
}
|
|
513
|
+
observe(target) {
|
|
514
|
+
this.target = target;
|
|
515
|
+
MutationObserverMock.activeObservers.set(target, this);
|
|
516
|
+
}
|
|
517
|
+
takeRecords() {
|
|
518
|
+
return [];
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
//#endregion
|
|
522
|
+
//#region src/selection.ts
|
|
523
|
+
var tagsOf = (doc) => doc.tag ?? {};
|
|
524
|
+
var resolvePos = (doc, value) => {
|
|
525
|
+
if (typeof value === "number") return value;
|
|
526
|
+
const tags = tagsOf(doc);
|
|
527
|
+
if (value in tags) return tags[value];
|
|
528
|
+
const available = Object.keys(tags);
|
|
529
|
+
throw new Error(`selectText: no tag named "${value}" in the document. ${available.length > 0 ? `Available tags: ${available.map((tag) => `"${tag}"`).join(", ")}.` : "The document has no tags."}`);
|
|
530
|
+
};
|
|
531
|
+
var resolveSelection = (doc, selection) => {
|
|
532
|
+
if (typeof selection === "object" && "$anchor" in selection && "$head" in selection) return selection;
|
|
533
|
+
if (typeof selection === "object" && "anchor" in selection && "head" in selection) return TextSelection.between(doc.resolve(resolvePos(doc, selection.anchor)), doc.resolve(resolvePos(doc, selection.head)));
|
|
534
|
+
if (typeof selection === "object" && "from" in selection && "to" in selection) throw new Error("selectText no longer accepts { from, to } — use { anchor, head } instead. The values map directly: { from: a, to: b } becomes { anchor: a, head: b }.");
|
|
535
|
+
if (typeof selection === "number") return TextSelection.near(doc.resolve(selection));
|
|
536
|
+
const tags = tagsOf(doc);
|
|
537
|
+
if ([
|
|
538
|
+
"all",
|
|
539
|
+
"end",
|
|
540
|
+
"start"
|
|
541
|
+
].includes(selection)) {
|
|
542
|
+
if (selection in tags) throw new Error(`selectText: "${selection}" is both a reserved selection and a tag in the document. Use { anchor, head } to select the tag, or rename the tag.`);
|
|
543
|
+
if (selection === "all") return new AllSelection(doc);
|
|
544
|
+
return TextSelection.near(doc.resolve(selection === "end" ? doc.nodeSize - 2 : 0));
|
|
545
|
+
}
|
|
546
|
+
return TextSelection.near(doc.resolve(resolvePos(doc, selection)));
|
|
547
|
+
};
|
|
548
|
+
//#endregion
|
|
549
|
+
//#region src/typing/direction.ts
|
|
550
|
+
var rtl = /[\p{Script=Hebrew}\p{Script=Arabic}\p{Script=Syriac}\p{Script=Thaana}\p{Script=Nko}\p{Script=Samaritan}\p{Script=Mandaic}]/u;
|
|
551
|
+
function containsRTL(text) {
|
|
552
|
+
return rtl.test(text);
|
|
553
|
+
}
|
|
554
|
+
//#endregion
|
|
555
|
+
//#region src/typing/dom.ts
|
|
556
|
+
var characterDataAt = (node, offset) => {
|
|
557
|
+
if (node instanceof CharacterData) return {
|
|
558
|
+
offset,
|
|
559
|
+
target: node
|
|
560
|
+
};
|
|
561
|
+
const after = node.childNodes[offset];
|
|
562
|
+
if (after instanceof CharacterData) return {
|
|
563
|
+
offset: 0,
|
|
564
|
+
target: after
|
|
565
|
+
};
|
|
566
|
+
return null;
|
|
567
|
+
};
|
|
568
|
+
//#endregion
|
|
569
|
+
//#region src/typing/grapheme.ts
|
|
570
|
+
var segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
571
|
+
var graphemeBoundary = (text, offset, direction) => {
|
|
572
|
+
const segment = segmenter.segment(text).containing(direction > 0 ? offset : offset - 1);
|
|
573
|
+
if (segment === void 0) return null;
|
|
574
|
+
return direction > 0 ? segment.index + segment.segment.length : segment.index;
|
|
575
|
+
};
|
|
576
|
+
function deleteText(view, direction) {
|
|
577
|
+
const { selection } = view.state;
|
|
578
|
+
const from = selection.empty && direction === -1 ? selection.from - 1 : selection.from;
|
|
579
|
+
const to = selection.empty && direction === 1 ? selection.to + 1 : selection.to;
|
|
580
|
+
const fromDOM = view.domAtPos(from, -1);
|
|
581
|
+
const target = characterDataAt(fromDOM.node, fromDOM.offset)?.target;
|
|
582
|
+
if (target === void 0) throw new Error("Cannot simulate deleting a range that is not inside a single text node");
|
|
583
|
+
const nodeStart = view.posAtDOM(target, 0);
|
|
584
|
+
const fromOffset = from - nodeStart;
|
|
585
|
+
const toOffset = to - nodeStart;
|
|
586
|
+
if (fromOffset < 0 || toOffset > target.data.length) throw new Error("Cannot simulate deleting a range that is not inside a single text node");
|
|
587
|
+
const oldValue = target.data;
|
|
588
|
+
target.data = target.data.slice(0, fromOffset) + target.data.slice(toOffset);
|
|
589
|
+
MutationObserverMock.createMutation(view.dom, [{
|
|
590
|
+
oldValue,
|
|
591
|
+
target,
|
|
592
|
+
type: "characterData"
|
|
593
|
+
}]);
|
|
594
|
+
}
|
|
595
|
+
function moveCaret(view, direction, extend) {
|
|
596
|
+
const { doc, selection } = view.state;
|
|
597
|
+
if (extend) {
|
|
598
|
+
const head = graphemeStep(view, selection.head, direction);
|
|
599
|
+
view.dispatch(view.state.tr.setSelection(TextSelection.between(doc.resolve(selection.anchor), doc.resolve(head), direction)));
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
let pos = direction === -1 ? selection.from : selection.to;
|
|
603
|
+
if (selection.empty) pos = graphemeStep(view, pos, direction);
|
|
604
|
+
else if (containsRTL(doc.textBetween(selection.from, selection.to))) throwRTL(direction);
|
|
605
|
+
view.dispatch(view.state.tr.setSelection(Selection.near(doc.resolve(pos), direction)));
|
|
606
|
+
}
|
|
607
|
+
function typeCharacter(view, character) {
|
|
608
|
+
if (!view.state.selection.empty) deleteText(view, -1);
|
|
609
|
+
const { node, offset: domOffset } = view.domAtPos(view.state.selection.from, -1);
|
|
610
|
+
const point = characterDataAt(node, domOffset);
|
|
611
|
+
if (point !== null) {
|
|
612
|
+
const { offset, target } = point;
|
|
613
|
+
const oldValue = target.data;
|
|
614
|
+
target.data = target.data.slice(0, offset) + character + target.data.slice(offset);
|
|
615
|
+
MutationObserverMock.createMutation(view.dom, [{
|
|
616
|
+
oldValue,
|
|
617
|
+
target,
|
|
618
|
+
type: "characterData"
|
|
619
|
+
}]);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const textNode = new Text(character);
|
|
623
|
+
const nextSibling = node.childNodes.item(domOffset);
|
|
624
|
+
node.insertBefore(textNode, nextSibling);
|
|
625
|
+
MutationObserverMock.createMutation(view.dom, [{
|
|
626
|
+
addedNodes: [textNode],
|
|
627
|
+
nextSibling,
|
|
628
|
+
previousSibling: textNode.previousSibling,
|
|
629
|
+
target: node,
|
|
630
|
+
type: "childList"
|
|
631
|
+
}]);
|
|
632
|
+
}
|
|
633
|
+
function graphemeStep(view, pos, direction) {
|
|
634
|
+
const dom = view.domAtPos(pos, direction);
|
|
635
|
+
const point = characterDataAt(dom.node, dom.offset);
|
|
636
|
+
if (point === null) return pos + direction;
|
|
637
|
+
if (containsRTL(point.target.data)) throwRTL(direction);
|
|
638
|
+
const nodeStart = view.posAtDOM(point.target, 0);
|
|
639
|
+
const boundary = graphemeBoundary(point.target.data, pos - nodeStart, direction);
|
|
640
|
+
return boundary === null ? pos + direction : nodeStart + boundary;
|
|
641
|
+
}
|
|
642
|
+
function throwRTL(direction) {
|
|
643
|
+
throw new Error(`Cannot simulate the "${direction === -1 ? "ArrowLeft" : "ArrowRight"}" key in right-to-left text.`);
|
|
644
|
+
}
|
|
645
|
+
//#endregion
|
|
646
|
+
//#region src/typing/keyboardInput.ts
|
|
647
|
+
function tokenizeKeyboardInput(input) {
|
|
648
|
+
const output = [];
|
|
649
|
+
let currentGroupOpener = null;
|
|
650
|
+
let group = "";
|
|
651
|
+
for (const char of input) if (currentGroupOpener !== null) {
|
|
652
|
+
if (["]", "}"].includes(char)) {
|
|
653
|
+
if (group.endsWith("\\") && char === matchingBrace(currentGroupOpener)) group = group.slice(0, -2) + char;
|
|
654
|
+
else if (char === matchingBrace(currentGroupOpener)) {
|
|
655
|
+
if (group.length === 4 && group.startsWith("Key")) output.push(group.slice(3).toLowerCase());
|
|
656
|
+
else output.push(group);
|
|
657
|
+
currentGroupOpener = null;
|
|
658
|
+
group = "";
|
|
659
|
+
} else group += char;
|
|
660
|
+
} else if (group === "" && currentGroupOpener === char) {
|
|
661
|
+
output.push(char);
|
|
662
|
+
currentGroupOpener = null;
|
|
663
|
+
group = "";
|
|
664
|
+
} else group += char;
|
|
665
|
+
} else if (["[", "{"].includes(char)) currentGroupOpener = char;
|
|
666
|
+
else output.push(char);
|
|
667
|
+
output.forEach(assertSupported);
|
|
668
|
+
return output;
|
|
669
|
+
}
|
|
670
|
+
function assertSupported(character) {
|
|
671
|
+
if (/^\/.+/u.exec(character) || /.+>[\d]*\/?$/u.exec(character)) throw new Error("Unsupported keyboard input");
|
|
672
|
+
}
|
|
673
|
+
function matchingBrace(opener) {
|
|
674
|
+
return opener === "{" ? "}" : "]";
|
|
675
|
+
}
|
|
676
|
+
//#endregion
|
|
677
|
+
//#region src/typing/keyChord.ts
|
|
678
|
+
function parseKeyChord(token) {
|
|
679
|
+
const parts = token.split(/-(?!$)/u);
|
|
680
|
+
const key = parts[parts.length - 1] === "Space" ? " " : parts[parts.length - 1];
|
|
681
|
+
const modifiers = {};
|
|
682
|
+
for (const modifier of parts.slice(0, -1)) if (/^(cmd|meta|m)$/iu.test(modifier)) modifiers.metaKey = true;
|
|
683
|
+
else if (/^a(lt)?$/iu.test(modifier)) modifiers.altKey = true;
|
|
684
|
+
else if (/^(c|ctrl|control)$/iu.test(modifier)) modifiers.ctrlKey = true;
|
|
685
|
+
else if (/^s(hift)?$/iu.test(modifier)) modifiers.shiftKey = true;
|
|
686
|
+
else if (/^mod$/iu.test(modifier)) modifiers.ctrlKey = true;
|
|
687
|
+
else throw new Error(`Unrecognized modifier name: ${modifier}`);
|
|
688
|
+
return {
|
|
689
|
+
key,
|
|
690
|
+
modifiers
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
//#endregion
|
|
694
|
+
//#region src/typing/typing.ts
|
|
461
695
|
var ignoredKeys = /* @__PURE__ */ new Set([
|
|
462
696
|
"Alt",
|
|
463
697
|
"CapsLock",
|
|
@@ -466,15 +700,14 @@ var ignoredKeys = /* @__PURE__ */ new Set([
|
|
|
466
700
|
"Shift",
|
|
467
701
|
"Tab"
|
|
468
702
|
]);
|
|
469
|
-
var backward = -1;
|
|
470
|
-
var forward = 1;
|
|
471
703
|
var hasModifiers = (modifiers) => modifiers !== void 0 && Object.values(modifiers).includes(true);
|
|
472
704
|
var suppressesCharacter = (modifiers) => modifiers?.altKey === true || modifiers?.ctrlKey === true || modifiers?.metaKey === true;
|
|
473
705
|
var isLetter = (key) => /^[a-z]$/iu.test(key);
|
|
474
|
-
function insertText(view, text
|
|
475
|
-
for (const
|
|
476
|
-
const
|
|
477
|
-
const
|
|
706
|
+
function insertText(view, text) {
|
|
707
|
+
for (const token of tokenizeKeyboardInput(text)) {
|
|
708
|
+
const { key, modifiers } = parseKeyChord(token);
|
|
709
|
+
const character = isLetter(key) && modifiers.shiftKey === true ? key.toUpperCase() : key;
|
|
710
|
+
const shiftKey = isLetter(character) ? character === character.toUpperCase() : modifiers.shiftKey ?? false;
|
|
478
711
|
const identity = keyIdentity(character);
|
|
479
712
|
const eventInit = {
|
|
480
713
|
bubbles: true,
|
|
@@ -493,7 +726,7 @@ function insertText(view, text, modifiers) {
|
|
|
493
726
|
if (!keydownEvent.defaultPrevented) {
|
|
494
727
|
const action = keyAction(character, modifiers);
|
|
495
728
|
if (action.type === "delete") deleteText(view, action.direction);
|
|
496
|
-
else if (action.type === "moveCaret") moveCaret(view, action.direction);
|
|
729
|
+
else if (action.type === "moveCaret") moveCaret(view, action.direction, action.extend);
|
|
497
730
|
else if (action.type === "type") {
|
|
498
731
|
view.dispatchEvent(new KeyboardEvent("keypress", {
|
|
499
732
|
...eventInit,
|
|
@@ -506,39 +739,29 @@ function insertText(view, text, modifiers) {
|
|
|
506
739
|
view.dispatchEvent(new KeyboardEvent("keyup", eventInit));
|
|
507
740
|
}
|
|
508
741
|
}
|
|
509
|
-
function deleteText(view, direction) {
|
|
510
|
-
const { selection } = view.state;
|
|
511
|
-
const from = selection.empty && direction === backward ? selection.from - 1 : selection.from;
|
|
512
|
-
const to = selection.empty && direction === forward ? selection.to + 1 : selection.to;
|
|
513
|
-
const fromDOM = view.domAtPos(from, backward);
|
|
514
|
-
const target = characterDataAt(fromDOM.node, fromDOM.offset)?.target;
|
|
515
|
-
if (target === void 0) throw new Error("Cannot simulate deleting a range that is not inside a single text node");
|
|
516
|
-
const nodeStart = view.posAtDOM(target, 0);
|
|
517
|
-
const fromOffset = from - nodeStart;
|
|
518
|
-
const toOffset = to - nodeStart;
|
|
519
|
-
if (fromOffset < 0 || toOffset > target.data.length) throw new Error("Cannot simulate deleting a range that is not inside a single text node");
|
|
520
|
-
const oldValue = target.data;
|
|
521
|
-
target.data = target.data.slice(0, fromOffset) + target.data.slice(toOffset);
|
|
522
|
-
MutationObserverMock.createMutation(view.dom, [{
|
|
523
|
-
oldValue,
|
|
524
|
-
target,
|
|
525
|
-
type: "characterData"
|
|
526
|
-
}]);
|
|
527
|
-
}
|
|
528
742
|
function keyAction(key, modifiers) {
|
|
529
743
|
if (ignoredKeys.has(key)) return { type: "ignore" };
|
|
530
744
|
if (key === "Backspace") return {
|
|
531
|
-
direction:
|
|
745
|
+
direction: -1,
|
|
532
746
|
type: "delete"
|
|
533
747
|
};
|
|
534
748
|
if (key === "Delete") return {
|
|
535
|
-
direction:
|
|
749
|
+
direction: 1,
|
|
536
750
|
type: "delete"
|
|
537
751
|
};
|
|
538
|
-
if (
|
|
539
|
-
direction
|
|
540
|
-
|
|
541
|
-
|
|
752
|
+
if (key === "ArrowLeft" || key === "ArrowRight") {
|
|
753
|
+
const direction = key === "ArrowLeft" ? -1 : 1;
|
|
754
|
+
if (!hasModifiers(modifiers)) return {
|
|
755
|
+
direction,
|
|
756
|
+
extend: false,
|
|
757
|
+
type: "moveCaret"
|
|
758
|
+
};
|
|
759
|
+
if (modifiers?.shiftKey === true && !suppressesCharacter(modifiers)) return {
|
|
760
|
+
direction,
|
|
761
|
+
extend: true,
|
|
762
|
+
type: "moveCaret"
|
|
763
|
+
};
|
|
764
|
+
}
|
|
542
765
|
if (key.length === 1) {
|
|
543
766
|
if (suppressesCharacter(modifiers)) return { type: "ignore" };
|
|
544
767
|
return {
|
|
@@ -548,79 +771,277 @@ function keyAction(key, modifiers) {
|
|
|
548
771
|
}
|
|
549
772
|
throw new Error(`Cannot simulate the "${key}" key`);
|
|
550
773
|
}
|
|
551
|
-
function moveCaret(view, direction) {
|
|
552
|
-
const { selection } = view.state;
|
|
553
|
-
let pos = direction === backward ? selection.from : selection.to;
|
|
554
|
-
if (selection.empty) pos += direction;
|
|
555
|
-
view.dispatch(view.state.tr.setSelection(Selection.near(view.state.doc.resolve(pos), direction)));
|
|
556
|
-
}
|
|
557
|
-
function typeCharacter(view, character) {
|
|
558
|
-
const { node, offset: domOffset } = view.domAtPos(view.state.selection.from, backward);
|
|
559
|
-
const point = characterDataAt(node, domOffset);
|
|
560
|
-
if (point !== null) {
|
|
561
|
-
const { offset, target } = point;
|
|
562
|
-
const oldValue = target.data;
|
|
563
|
-
target.data = target.data.slice(0, offset) + character + target.data.slice(offset);
|
|
564
|
-
MutationObserverMock.createMutation(view.dom, [{
|
|
565
|
-
oldValue,
|
|
566
|
-
target,
|
|
567
|
-
type: "characterData"
|
|
568
|
-
}]);
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
const textNode = new Text(character);
|
|
572
|
-
const nextSibling = node.childNodes.item(domOffset);
|
|
573
|
-
node.insertBefore(textNode, nextSibling);
|
|
574
|
-
MutationObserverMock.createMutation(view.dom, [{
|
|
575
|
-
addedNodes: [textNode],
|
|
576
|
-
nextSibling,
|
|
577
|
-
previousSibling: textNode.previousSibling,
|
|
578
|
-
target: node,
|
|
579
|
-
type: "childList"
|
|
580
|
-
}]);
|
|
581
|
-
}
|
|
582
774
|
//#endregion
|
|
583
775
|
//#region src/ProseMirrorTester.ts
|
|
776
|
+
var originalMutationObserver = global.MutationObserver;
|
|
777
|
+
var liveTesters = /* @__PURE__ */ new Set();
|
|
778
|
+
var autoCleanupTesters = /* @__PURE__ */ new Set();
|
|
779
|
+
var cleanupTesters = () => {
|
|
780
|
+
for (const tester of [...autoCleanupTesters]) tester.destroy();
|
|
781
|
+
};
|
|
584
782
|
var ProseMirrorTester = class {
|
|
585
783
|
get doc() {
|
|
784
|
+
this.assertAlive();
|
|
586
785
|
return this.view.state.doc;
|
|
587
786
|
}
|
|
588
|
-
get
|
|
589
|
-
|
|
787
|
+
get html() {
|
|
788
|
+
this.assertAlive();
|
|
789
|
+
return this.view.dom.innerHTML;
|
|
790
|
+
}
|
|
791
|
+
get state() {
|
|
792
|
+
this.assertAlive();
|
|
793
|
+
return this.view.state;
|
|
590
794
|
}
|
|
795
|
+
get text() {
|
|
796
|
+
this.assertAlive();
|
|
797
|
+
return this.view.dom.textContent;
|
|
798
|
+
}
|
|
799
|
+
destroyed = false;
|
|
800
|
+
mountPoint;
|
|
591
801
|
view;
|
|
592
802
|
constructor(documentRoot, options = {}) {
|
|
593
803
|
if (typeof document === "undefined") throw new Error("TODO");
|
|
594
|
-
|
|
595
|
-
document.body.append(
|
|
804
|
+
this.mountPoint = document.createElement("div");
|
|
805
|
+
document.body.append(this.mountPoint);
|
|
806
|
+
const { autoCleanup = true, plugins = [], ...editorProps } = options;
|
|
596
807
|
const state = EditorState.create({
|
|
597
808
|
doc: documentRoot,
|
|
598
|
-
plugins
|
|
809
|
+
plugins
|
|
599
810
|
});
|
|
600
811
|
global.MutationObserver = MutationObserverMock;
|
|
601
812
|
mockRangeRects();
|
|
602
|
-
this.view = new EditorView(
|
|
813
|
+
this.view = new EditorView(this.mountPoint, {
|
|
814
|
+
state,
|
|
815
|
+
...editorProps
|
|
816
|
+
});
|
|
817
|
+
liveTesters.add(this);
|
|
818
|
+
if (autoCleanup) autoCleanupTesters.add(this);
|
|
819
|
+
}
|
|
820
|
+
click(target) {
|
|
821
|
+
this.assertAlive();
|
|
822
|
+
return click(this.view, target);
|
|
823
|
+
}
|
|
824
|
+
command(command) {
|
|
825
|
+
this.assertAlive();
|
|
826
|
+
return command(this.view.state, this.view.dispatch.bind(this.view), this.view);
|
|
827
|
+
}
|
|
828
|
+
copy() {
|
|
829
|
+
this.assertAlive();
|
|
830
|
+
return copy(this.view);
|
|
603
831
|
}
|
|
604
|
-
|
|
605
|
-
|
|
832
|
+
destroy() {
|
|
833
|
+
if (this.destroyed) return;
|
|
834
|
+
this.destroyed = true;
|
|
835
|
+
this.view.destroy();
|
|
836
|
+
this.mountPoint.remove();
|
|
837
|
+
document.getSelection()?.removeAllRanges();
|
|
838
|
+
autoCleanupTesters.delete(this);
|
|
839
|
+
liveTesters.delete(this);
|
|
840
|
+
if (liveTesters.size === 0) global.MutationObserver = originalMutationObserver;
|
|
841
|
+
}
|
|
842
|
+
element(selector) {
|
|
843
|
+
this.assertAlive();
|
|
844
|
+
return element(this.view, selector);
|
|
845
|
+
}
|
|
846
|
+
elements(selector) {
|
|
847
|
+
this.assertAlive();
|
|
848
|
+
return elements(this.view, selector);
|
|
849
|
+
}
|
|
850
|
+
insertText(text) {
|
|
851
|
+
this.assertAlive();
|
|
852
|
+
insertText(this.view, text);
|
|
853
|
+
}
|
|
854
|
+
paste(content) {
|
|
855
|
+
this.assertAlive();
|
|
856
|
+
paste(this.view, content);
|
|
606
857
|
}
|
|
607
858
|
selectText(selection) {
|
|
859
|
+
this.assertAlive();
|
|
608
860
|
this.view.dispatch(this.view.state.tr.setSelection(resolveSelection(this.doc, selection)));
|
|
609
861
|
}
|
|
862
|
+
assertAlive() {
|
|
863
|
+
if (this.destroyed) throw new Error("This ProseMirrorTester has been destroyed. Testers are destroyed automatically after each test; pass { autoCleanup: false } to keep one alive (e.g. across a beforeAll).");
|
|
864
|
+
}
|
|
610
865
|
};
|
|
611
866
|
//#endregion
|
|
612
|
-
//#region src/
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
867
|
+
//#region src/stringifyMark.ts
|
|
868
|
+
var stringifyObjectOptions = {
|
|
869
|
+
indent: " ",
|
|
870
|
+
inlineCharacterLimit: 1e3
|
|
871
|
+
};
|
|
872
|
+
var stringifyMark = (mark) => Object.keys(mark.attrs).length > 0 ? `${mark.type.name}(${stringifyObject(mark.attrs, stringifyObjectOptions)})` : mark.type.name;
|
|
873
|
+
//#endregion
|
|
874
|
+
//#region src/stringifyProseMirrorNode.ts
|
|
875
|
+
function stringifyProseMirrorNode(node, selection, indentation = "") {
|
|
876
|
+
const markers = /* @__PURE__ */ new Map();
|
|
877
|
+
if (selection !== void 0) {
|
|
878
|
+
if (selection.anchor === selection.head) markers.set(selection.anchor, "<cursor>");
|
|
879
|
+
else {
|
|
880
|
+
markers.set(selection.anchor, "<anchor>");
|
|
881
|
+
markers.set(selection.head, "<head>");
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return render(node, markers, indentation, 0);
|
|
885
|
+
}
|
|
886
|
+
function escapeText(text) {
|
|
887
|
+
return text.replace(/[\p{Cc}\p{Cf}'\\]/gu, (char) => {
|
|
888
|
+
switch (char) {
|
|
889
|
+
case "\f": return "\\f";
|
|
890
|
+
case "\n": return "\\n";
|
|
891
|
+
case "\r": return "\\r";
|
|
892
|
+
case " ": return "\\t";
|
|
893
|
+
case "\b": return "\\b";
|
|
894
|
+
case "'": return "\\'";
|
|
895
|
+
case "\\": return "\\\\";
|
|
896
|
+
default: return `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`;
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
function quoteWithMarkers(text, start, markers) {
|
|
901
|
+
const inside = [...markers.entries()].filter(([pos]) => pos >= start && pos <= start + text.length).sort(([a], [b]) => a - b);
|
|
902
|
+
const wrapMarker = containsRTL(text) ? (notation) => `\u2066${notation}\u2069` : (notation) => notation;
|
|
903
|
+
let quoted = "";
|
|
904
|
+
let last = 0;
|
|
905
|
+
for (const [pos, notation] of inside) {
|
|
906
|
+
markers.delete(pos);
|
|
907
|
+
const offset = pos - start;
|
|
908
|
+
quoted += escapeText(text.slice(last, offset)) + wrapMarker(notation);
|
|
909
|
+
last = offset;
|
|
910
|
+
}
|
|
911
|
+
quoted += escapeText(text.slice(last));
|
|
912
|
+
return `'${quoted}'`;
|
|
913
|
+
}
|
|
914
|
+
function render(node, markers, indentation, contentStart) {
|
|
915
|
+
if (node.type.spec.toDebugString !== void 0) return `${indentation}${wrapMarks(node.marks, node.type.spec.toDebugString(node))}`;
|
|
916
|
+
if (node.type.name === "text") return `${indentation}${wrapMarks(node.marks, quoteWithMarkers(node.textContent, contentStart, markers))}`;
|
|
917
|
+
return renderElement(node, markers, indentation, contentStart);
|
|
918
|
+
}
|
|
919
|
+
function renderElement(node, markers, indentation, contentStart) {
|
|
920
|
+
const type = node.type.name;
|
|
921
|
+
const hasAttrs = Object.keys(node.attrs).length > 0;
|
|
922
|
+
const nextIndentation = `${indentation} `;
|
|
923
|
+
const args = [];
|
|
924
|
+
const pushArg = (content) => {
|
|
925
|
+
args.push(`${nextIndentation}${content},`);
|
|
926
|
+
};
|
|
927
|
+
const pushMarkerAt = (pos) => {
|
|
928
|
+
const notation = markers.get(pos);
|
|
929
|
+
if (notation !== void 0) {
|
|
930
|
+
markers.delete(pos);
|
|
931
|
+
pushArg(`'${notation}'`);
|
|
932
|
+
}
|
|
623
933
|
};
|
|
624
|
-
|
|
934
|
+
if (hasAttrs) pushArg(stringifyObject(node.attrs, stringifyObjectOptions));
|
|
935
|
+
if (!node.type.isLeaf) {
|
|
936
|
+
if (node.content.childCount === 0) pushMarkerAt(contentStart);
|
|
937
|
+
node.content.forEach((child, offset, index) => {
|
|
938
|
+
const childStart = contentStart + offset;
|
|
939
|
+
const isText = child.type.name === "text";
|
|
940
|
+
if (!(index > 0 && node.content.child(index - 1).type.name === "text") && !isText) pushMarkerAt(childStart);
|
|
941
|
+
if (isText) pushArg(wrapMarks(child.marks, quoteWithMarkers(child.textContent, childStart, markers)));
|
|
942
|
+
else args.push(`${render(child, markers, nextIndentation, childStart + 1)},`);
|
|
943
|
+
});
|
|
944
|
+
if (node.content.lastChild?.type.name !== "text") pushMarkerAt(contentStart + node.content.size);
|
|
945
|
+
}
|
|
946
|
+
const body = !hasAttrs && (node.content.childCount === 0 || node.content.childCount === 1 && node.content.firstChild?.type.name === "text") ? `${type}(${args.map((arg) => arg.slice(nextIndentation.length, -1)).join("")})` : `${type}(\n${args.join("\n")}\n${indentation})`;
|
|
947
|
+
return `${indentation}${wrapMarks(node.marks, body)}`;
|
|
948
|
+
}
|
|
949
|
+
function wrapMarks(marks, origContent) {
|
|
950
|
+
let content = origContent;
|
|
951
|
+
for (const mark of [...marks].reverse()) {
|
|
952
|
+
const hasAttrs = Object.keys(mark.attrs).length > 0;
|
|
953
|
+
const items = [content];
|
|
954
|
+
if (hasAttrs) items.unshift(stringifyObject(mark.attrs, stringifyObjectOptions));
|
|
955
|
+
content = `${mark.type.name}(${items.join(", ")})`;
|
|
956
|
+
}
|
|
957
|
+
return content;
|
|
958
|
+
}
|
|
959
|
+
//#endregion
|
|
960
|
+
//#region src/stringifyEditorState.ts
|
|
961
|
+
function stringifyEditorState(state, indentation = "") {
|
|
962
|
+
const { doc, selection, storedMarks } = state;
|
|
963
|
+
const lines = [stringifyProseMirrorNode(doc, selection, indentation), `${indentation}# selection: ${selection.constructor.name} ${String(selection.from)}..${String(selection.to)}`];
|
|
964
|
+
if (storedMarks !== null) lines.push(`${indentation}# storedMarks: [${storedMarks.map(stringifyMark).join(", ")}]`);
|
|
965
|
+
return lines.join("\n");
|
|
966
|
+
}
|
|
967
|
+
//#endregion
|
|
968
|
+
//#region src/parseHTML.ts
|
|
969
|
+
var wrapMap = {
|
|
970
|
+
caption: ["table"],
|
|
971
|
+
col: ["table", "colgroup"],
|
|
972
|
+
colgroup: ["table"],
|
|
973
|
+
tbody: ["table"],
|
|
974
|
+
td: [
|
|
975
|
+
"table",
|
|
976
|
+
"tbody",
|
|
977
|
+
"tr"
|
|
978
|
+
],
|
|
979
|
+
tfoot: ["table"],
|
|
980
|
+
th: [
|
|
981
|
+
"table",
|
|
982
|
+
"tbody",
|
|
983
|
+
"tr"
|
|
984
|
+
],
|
|
985
|
+
thead: ["table"],
|
|
986
|
+
tr: ["table", "tbody"]
|
|
987
|
+
};
|
|
988
|
+
function parseHTML(html, schema, options) {
|
|
989
|
+
return DOMParser.fromSchema(schema).parse(readHTML(html), options);
|
|
990
|
+
}
|
|
991
|
+
function readHTML(rawHtml) {
|
|
992
|
+
const metas = /^(\s*<meta [^>]*>)*/u.exec(rawHtml);
|
|
993
|
+
const stripped = metas ? rawHtml.slice(metas[0].length) : rawHtml;
|
|
994
|
+
const firstTag = /<([a-z][^>\s]+)/iu.exec(stripped);
|
|
995
|
+
const wrap = firstTag ? wrapMap[firstTag[1].toLowerCase()] : void 0;
|
|
996
|
+
const html = wrap === void 0 ? stripped : wrap.map((n) => `<${n}>`).join("") + stripped + wrap.map((n) => `</${n}>`).reverse().join("");
|
|
997
|
+
let elt = document.implementation.createHTMLDocument("title").body;
|
|
998
|
+
elt.innerHTML = html;
|
|
999
|
+
if (wrap) for (const n of wrap) elt = elt.querySelector(n) ?? elt;
|
|
1000
|
+
return elt;
|
|
1001
|
+
}
|
|
1002
|
+
//#endregion
|
|
1003
|
+
//#region src/index.ts
|
|
1004
|
+
afterEach(cleanupTesters);
|
|
1005
|
+
expect.extend({
|
|
1006
|
+
toEqualProseMirrorNode(received, expected) {
|
|
1007
|
+
const receivedDoc = `\n${stringifyProseMirrorNode(received)}\n`;
|
|
1008
|
+
const expectedDoc = `\n${stringifyProseMirrorNode(expected)}\n`;
|
|
1009
|
+
const sameSchema = received.type.schema === expected.type.schema;
|
|
1010
|
+
const pass = sameSchema && this.equals(receivedDoc, expectedDoc);
|
|
1011
|
+
return {
|
|
1012
|
+
message: pass ? () => `${this.utils.matcherHint(".not.toEqualProseMirrorNode")}\n\nExpected value of document to not equal:\n ${this.utils.printExpected(expectedDoc)}\nActual:\n ${this.utils.printReceived(receivedDoc)}` : () => {
|
|
1013
|
+
if (!sameSchema && this.equals(receivedDoc, expectedDoc)) return `${this.utils.matcherHint(".toEqualProseMirrorNode")}\n\nThe documents stringify identically but come from different schemas:\n${this.utils.printReceived(receivedDoc)}`;
|
|
1014
|
+
const diffString = this.utils.diff(expectedDoc, receivedDoc);
|
|
1015
|
+
return `${this.utils.matcherHint(".toEqualProseMirrorNode")}\n\nExpected value of document to equal:\n${this.utils.printExpected(expectedDoc)}\nActual:\n${this.utils.printReceived(receivedDoc)}${diffString === void 0 ? "" : `\n\nDifference:\n\n${diffString}`}`;
|
|
1016
|
+
},
|
|
1017
|
+
pass
|
|
1018
|
+
};
|
|
1019
|
+
},
|
|
1020
|
+
toHaveSelection(received, expected) {
|
|
1021
|
+
const state = received instanceof ProseMirrorTester ? received.state : received;
|
|
1022
|
+
const receivedSelection = state.selection;
|
|
1023
|
+
const expectedSelection = resolveSelection(state.doc, expected);
|
|
1024
|
+
const receivedDoc = `\n${stringifyProseMirrorNode(state.doc, receivedSelection)}\n`;
|
|
1025
|
+
const expectedDoc = `\n${stringifyProseMirrorNode(state.doc, expectedSelection)}\n`;
|
|
1026
|
+
const pass = receivedSelection.eq(expectedSelection);
|
|
1027
|
+
return {
|
|
1028
|
+
message: pass ? () => `${this.utils.matcherHint(".not.toHaveSelection")}\n\nExpected selection to not equal:\n${this.utils.printExpected(expectedDoc)}\nActual:\n${this.utils.printReceived(receivedDoc)}` : () => {
|
|
1029
|
+
const diffString = this.utils.diff(expectedDoc, receivedDoc);
|
|
1030
|
+
return `${this.utils.matcherHint(".toHaveSelection")}\n\nExpected selection to equal (${this.utils.printExpected(expectedSelection.constructor.name)}):\n${this.utils.printExpected(expectedDoc)}\nActual (${this.utils.printReceived(receivedSelection.constructor.name)}):\n${this.utils.printReceived(receivedDoc)}${diffString === void 0 ? "" : `\n\nDifference:\n\n${diffString}`}`;
|
|
1031
|
+
},
|
|
1032
|
+
pass
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
expect.addSnapshotSerializer({
|
|
1037
|
+
serialize: (val, _config, indentation) => stringifyProseMirrorNode(val, void 0, indentation).slice(indentation.length),
|
|
1038
|
+
test: isProseMirrorNode
|
|
1039
|
+
});
|
|
1040
|
+
expect.addSnapshotSerializer({
|
|
1041
|
+
serialize: (val, _config, indentation) => {
|
|
1042
|
+
return stringifyEditorState(val instanceof ProseMirrorTester ? val.state : val, indentation).slice(indentation.length);
|
|
1043
|
+
},
|
|
1044
|
+
test: (val) => val instanceof ProseMirrorTester || isEditorState(val)
|
|
1045
|
+
});
|
|
625
1046
|
//#endregion
|
|
626
|
-
export { ProseMirrorTester };
|
|
1047
|
+
export { ProseMirrorTester, parseHTML };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitest-prosemirror",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A plugin for Vitest that enables you to write tests using the ProseMirror editor",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"vitest",
|
|
@@ -61,8 +61,9 @@
|
|
|
61
61
|
"@microsoft/api-extractor": "^7.58.7",
|
|
62
62
|
"@types/node": "^26.0.0",
|
|
63
63
|
"@types/stringify-object": "^4.0.5",
|
|
64
|
-
"@vitest/coverage-v8": "^
|
|
64
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
65
65
|
"@vitest/eslint-plugin": "^1.3.26",
|
|
66
|
+
"blns": "^2.0.4",
|
|
66
67
|
"es-check": "^9.6.4",
|
|
67
68
|
"eslint": "^10.0.2",
|
|
68
69
|
"eslint-config-prettier": "^10.0.1",
|
|
@@ -70,6 +71,7 @@
|
|
|
70
71
|
"eslint-plugin-perfectionist": "^5.0.0",
|
|
71
72
|
"eslint-plugin-prefer-arrow-functions": "^3.9.1",
|
|
72
73
|
"eslint-plugin-prettier": "^5.1.3",
|
|
74
|
+
"jiti": "^2.7.0",
|
|
73
75
|
"jsdom": "^30.0.0",
|
|
74
76
|
"npm-run-all2": "^9.0.3",
|
|
75
77
|
"prettier": "^3.3.0",
|
|
@@ -77,16 +79,17 @@
|
|
|
77
79
|
"prosemirror-inputrules": "^1.5.0",
|
|
78
80
|
"prosemirror-keymap": "^1.2.3",
|
|
79
81
|
"prosemirror-schema-basic": "^1.2.3",
|
|
82
|
+
"prosemirror-test-builder": "^1.1.1",
|
|
80
83
|
"rimraf": "^6.0.1",
|
|
81
84
|
"typescript": "^6.0.2",
|
|
82
85
|
"typescript-eslint": "^8.58.0",
|
|
83
86
|
"unplugin-dts": "^1.0.1",
|
|
84
87
|
"vite": "^8.0.0",
|
|
85
|
-
"vitest": "^
|
|
88
|
+
"vitest": "^5.0.0",
|
|
86
89
|
"w3c-keyname": "^2.2.8"
|
|
87
90
|
},
|
|
88
91
|
"peerDependencies": {
|
|
89
|
-
"vitest": "^2.1.9 || ^3.0.7 || ^4.0.4"
|
|
92
|
+
"vitest": "^2.1.9 || ^3.0.7 || ^4.0.4 || ^5.0.0"
|
|
90
93
|
},
|
|
91
94
|
"peerDependenciesMeta": {
|
|
92
95
|
"vitest": {
|