@pixerate/editor 0.14.0 → 0.14.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/dist/index.cjs +117 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +122 -44
- package/dist/index.js.map +1 -1
- package/dist/serializers/index.cjs +83 -25
- package/dist/serializers/index.cjs.map +1 -1
- package/dist/serializers/index.d.cts +1 -0
- package/dist/serializers/index.d.ts +1 -0
- package/dist/serializers/index.js +83 -25
- package/dist/serializers/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -55,8 +55,16 @@ declare class EditorController {
|
|
|
55
55
|
setPlainText(text: string, emitUpdate?: boolean): void;
|
|
56
56
|
getMarkdown(): string;
|
|
57
57
|
setMarkdown(markdown: string, emitUpdate?: boolean): void;
|
|
58
|
+
/**
|
|
59
|
+
* Replaces `from`..`to` with literal text: HTML in `text` is not parsed, and
|
|
60
|
+
* newlines split paragraphs.
|
|
61
|
+
*/
|
|
62
|
+
private insertPlainText;
|
|
63
|
+
/** Inserts text literally at the selection; HTML in `text` is not parsed. */
|
|
58
64
|
insertText(text: string): void;
|
|
65
|
+
/** Inserts text literally at `pos`; HTML in `text` is not parsed. */
|
|
59
66
|
insertTextAt(pos: number, text: string): void;
|
|
67
|
+
/** Replaces a range with literal text; HTML in `text` is not parsed. */
|
|
60
68
|
replaceRange(from: number, to: number, text: string): void;
|
|
61
69
|
getCurrentContentString(): string;
|
|
62
70
|
isDirty(): boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -55,8 +55,16 @@ declare class EditorController {
|
|
|
55
55
|
setPlainText(text: string, emitUpdate?: boolean): void;
|
|
56
56
|
getMarkdown(): string;
|
|
57
57
|
setMarkdown(markdown: string, emitUpdate?: boolean): void;
|
|
58
|
+
/**
|
|
59
|
+
* Replaces `from`..`to` with literal text: HTML in `text` is not parsed, and
|
|
60
|
+
* newlines split paragraphs.
|
|
61
|
+
*/
|
|
62
|
+
private insertPlainText;
|
|
63
|
+
/** Inserts text literally at the selection; HTML in `text` is not parsed. */
|
|
58
64
|
insertText(text: string): void;
|
|
65
|
+
/** Inserts text literally at `pos`; HTML in `text` is not parsed. */
|
|
59
66
|
insertTextAt(pos: number, text: string): void;
|
|
67
|
+
/** Replaces a range with literal text; HTML in `text` is not parsed. */
|
|
60
68
|
replaceRange(from: number, to: number, text: string): void;
|
|
61
69
|
getCurrentContentString(): string;
|
|
62
70
|
isDirty(): boolean;
|
package/dist/index.js
CHANGED
|
@@ -2330,38 +2330,98 @@ function escapeHtml(text) {
|
|
|
2330
2330
|
if (!text) return "";
|
|
2331
2331
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2332
2332
|
}
|
|
2333
|
+
var PARAGRAPH_HTML_REGEX = /^\s*<p[\s>][\s\S]*<\/p>\s*$/i;
|
|
2333
2334
|
function plainTextToTipTapHtml(text) {
|
|
2334
2335
|
if (!text) return "<p></p>";
|
|
2335
|
-
if (
|
|
2336
|
+
if (PARAGRAPH_HTML_REGEX.test(text)) {
|
|
2336
2337
|
return text;
|
|
2337
2338
|
}
|
|
2338
2339
|
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n").map((line) => `<p>${escapeHtml(line)}</p>`).join("");
|
|
2339
2340
|
}
|
|
2341
|
+
var BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
2342
|
+
"P",
|
|
2343
|
+
"DIV",
|
|
2344
|
+
"LI",
|
|
2345
|
+
"UL",
|
|
2346
|
+
"OL",
|
|
2347
|
+
"PRE",
|
|
2348
|
+
"BLOCKQUOTE",
|
|
2349
|
+
"TABLE",
|
|
2350
|
+
"TR",
|
|
2351
|
+
"H1",
|
|
2352
|
+
"H2",
|
|
2353
|
+
"H3",
|
|
2354
|
+
"H4",
|
|
2355
|
+
"H5",
|
|
2356
|
+
"H6",
|
|
2357
|
+
"HR"
|
|
2358
|
+
]);
|
|
2359
|
+
var MENTION_SPAN_REGEX = /<span\b(?=[^>]*\bdata-type=["']mention["'])([^>]*)>[\s\S]*?<\/span>/gi;
|
|
2360
|
+
function getHtmlAttribute(attrs, name) {
|
|
2361
|
+
const match = new RegExp(`\\b${name}=(?:"([^"]*)"|'([^']*)')`, "i").exec(
|
|
2362
|
+
attrs
|
|
2363
|
+
);
|
|
2364
|
+
return match ? match[1] ?? match[2] : void 0;
|
|
2365
|
+
}
|
|
2366
|
+
function parseHtmlFragment(html) {
|
|
2367
|
+
const template = document.createElement("template");
|
|
2368
|
+
template.innerHTML = html;
|
|
2369
|
+
return template.content;
|
|
2370
|
+
}
|
|
2371
|
+
function decodeHtmlEntities(text) {
|
|
2372
|
+
return text.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/�*39;/g, "'").replace(/ /g, "\xA0").replace(/&/g, "&");
|
|
2373
|
+
}
|
|
2340
2374
|
function htmlToPlainText(html) {
|
|
2341
2375
|
if (!html) return "";
|
|
2342
|
-
const mentionNormalized = html.replace(
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
)
|
|
2346
|
-
/<span[^>]*data-type=["']mention["'][^>]*data-id=["']([^"']+)["'][^>]*>.*?<\/span>/gi,
|
|
2347
|
-
"@$1"
|
|
2348
|
-
);
|
|
2376
|
+
const mentionNormalized = html.replace(MENTION_SPAN_REGEX, (match, attrs) => {
|
|
2377
|
+
const label = getHtmlAttribute(attrs, "data-label") || getHtmlAttribute(attrs, "data-id");
|
|
2378
|
+
return label ? `@${label}` : match;
|
|
2379
|
+
});
|
|
2349
2380
|
if (typeof document !== "undefined") {
|
|
2350
|
-
const
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
const
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2381
|
+
const fragment = parseHtmlFragment(mentionNormalized);
|
|
2382
|
+
fragment.querySelectorAll("br.ProseMirror-trailingBreak").forEach((br) => br.remove());
|
|
2383
|
+
const lines = [];
|
|
2384
|
+
let current = null;
|
|
2385
|
+
const flush = () => {
|
|
2386
|
+
if (current !== null) {
|
|
2387
|
+
lines.push(current);
|
|
2388
|
+
current = null;
|
|
2389
|
+
}
|
|
2390
|
+
};
|
|
2391
|
+
const visit = (node) => {
|
|
2392
|
+
if (node.nodeType === 3) {
|
|
2393
|
+
const value = node.nodeValue ?? "";
|
|
2394
|
+
if (current === null && value.trim() === "") return;
|
|
2395
|
+
current = (current ?? "") + value;
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
if (node.nodeType !== 1) return;
|
|
2399
|
+
const tag = node.tagName.toUpperCase();
|
|
2400
|
+
if (tag === "BR") {
|
|
2401
|
+
current = (current ?? "") + "\n";
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
if (!BLOCK_TAGS.has(tag)) {
|
|
2405
|
+
node.childNodes.forEach(visit);
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2408
|
+
flush();
|
|
2409
|
+
const linesBefore = lines.length;
|
|
2410
|
+
node.childNodes.forEach(visit);
|
|
2411
|
+
flush();
|
|
2412
|
+
if (lines.length === linesBefore && tag !== "HR") {
|
|
2413
|
+
lines.push("");
|
|
2414
|
+
}
|
|
2415
|
+
};
|
|
2416
|
+
fragment.childNodes.forEach(visit);
|
|
2417
|
+
flush();
|
|
2418
|
+
return lines.join("\n");
|
|
2419
|
+
}
|
|
2420
|
+
const text = mentionNormalized.replace(/<br\s*\/?>/gi, "\n").replace(
|
|
2421
|
+
/<\/(?:p|div|li|pre|blockquote|h[1-6])>(?:\s*<\/(?:li|ul|ol|blockquote|div)>)*/gi,
|
|
2422
|
+
"\n"
|
|
2423
|
+
).replace(/<[^>]+>/g, "");
|
|
2424
|
+
return decodeHtmlEntities(text).replace(/\n$/, "");
|
|
2365
2425
|
}
|
|
2366
2426
|
function getEditorText(editor) {
|
|
2367
2427
|
if (!editor) return "";
|
|
@@ -2420,9 +2480,7 @@ function extractMentionsFromDoc(docOrEditor) {
|
|
|
2420
2480
|
}
|
|
2421
2481
|
if (typeof docOrEditor === "string") {
|
|
2422
2482
|
if (typeof document !== "undefined") {
|
|
2423
|
-
|
|
2424
|
-
div.innerHTML = docOrEditor;
|
|
2425
|
-
div.querySelectorAll('span[data-type="mention"]').forEach((el) => {
|
|
2483
|
+
parseHtmlFragment(docOrEditor).querySelectorAll('span[data-type="mention"]').forEach((el) => {
|
|
2426
2484
|
mentions.push({
|
|
2427
2485
|
id: el.getAttribute("data-id") || "",
|
|
2428
2486
|
label: el.getAttribute("data-label") || "",
|
|
@@ -2514,7 +2572,25 @@ function getEditorMarkdown(editor) {
|
|
|
2514
2572
|
|
|
2515
2573
|
// src/controller.ts
|
|
2516
2574
|
import { Editor } from "@tiptap/core";
|
|
2517
|
-
import {
|
|
2575
|
+
import {
|
|
2576
|
+
Slice,
|
|
2577
|
+
Fragment as Fragment2,
|
|
2578
|
+
Node as ProsemirrorNode
|
|
2579
|
+
} from "@tiptap/pm/model";
|
|
2580
|
+
function plainTextToSlice(text, schema) {
|
|
2581
|
+
const nodes = [];
|
|
2582
|
+
text.replace(/\r\n?/g, "\n").split("\n").forEach((line) => {
|
|
2583
|
+
const nodeJson = { type: "paragraph" };
|
|
2584
|
+
if (line.length > 0) {
|
|
2585
|
+
nodeJson.content = [{ type: "text", text: line }];
|
|
2586
|
+
}
|
|
2587
|
+
try {
|
|
2588
|
+
nodes.push(ProsemirrorNode.fromJSON(schema, nodeJson));
|
|
2589
|
+
} catch {
|
|
2590
|
+
}
|
|
2591
|
+
});
|
|
2592
|
+
return Slice.maxOpen(Fragment2.fromArray(nodes));
|
|
2593
|
+
}
|
|
2518
2594
|
var EditorController = class {
|
|
2519
2595
|
editor;
|
|
2520
2596
|
plainTextMode;
|
|
@@ -2570,20 +2646,7 @@ var EditorController = class {
|
|
|
2570
2646
|
clipboardTextParser: (text, context, _plain, view) => {
|
|
2571
2647
|
const schema = context?.doc?.type?.schema || context?.schema || view?.state?.schema || this.editor.schema;
|
|
2572
2648
|
if (!schema) return Slice.empty;
|
|
2573
|
-
|
|
2574
|
-
const nodes = [];
|
|
2575
|
-
blocks.forEach((line) => {
|
|
2576
|
-
const nodeJson = { type: "paragraph" };
|
|
2577
|
-
if (line.length > 0) {
|
|
2578
|
-
nodeJson.content = [{ type: "text", text: line }];
|
|
2579
|
-
}
|
|
2580
|
-
try {
|
|
2581
|
-
const node = ProsemirrorNode.fromJSON(schema, nodeJson);
|
|
2582
|
-
nodes.push(node);
|
|
2583
|
-
} catch {
|
|
2584
|
-
}
|
|
2585
|
-
});
|
|
2586
|
-
return Slice.maxOpen(Fragment2.fromArray(nodes));
|
|
2649
|
+
return plainTextToSlice(text, schema);
|
|
2587
2650
|
}
|
|
2588
2651
|
}
|
|
2589
2652
|
});
|
|
@@ -2626,14 +2689,29 @@ var EditorController = class {
|
|
|
2626
2689
|
this.editor.commands.setContent(html, emitUpdate);
|
|
2627
2690
|
}
|
|
2628
2691
|
}
|
|
2692
|
+
/**
|
|
2693
|
+
* Replaces `from`..`to` with literal text: HTML in `text` is not parsed, and
|
|
2694
|
+
* newlines split paragraphs.
|
|
2695
|
+
*/
|
|
2696
|
+
insertPlainText(from, to, text) {
|
|
2697
|
+
const { state, view } = this.editor;
|
|
2698
|
+
const tr = text.includes("\n") || text.includes("\r") ? state.tr.replace(from, to, plainTextToSlice(text, state.schema)) : text ? state.tr.insertText(text, from, to) : state.tr.delete(from, to);
|
|
2699
|
+
view.dispatch(tr.scrollIntoView());
|
|
2700
|
+
}
|
|
2701
|
+
/** Inserts text literally at the selection; HTML in `text` is not parsed. */
|
|
2629
2702
|
insertText(text) {
|
|
2630
|
-
this.editor.
|
|
2703
|
+
const { from, to } = this.editor.state.selection;
|
|
2704
|
+
this.insertPlainText(from, to, text);
|
|
2631
2705
|
}
|
|
2706
|
+
/** Inserts text literally at `pos`; HTML in `text` is not parsed. */
|
|
2632
2707
|
insertTextAt(pos, text) {
|
|
2633
|
-
this.
|
|
2708
|
+
this.insertPlainText(pos, pos, text);
|
|
2709
|
+
this.editor.commands.focus();
|
|
2634
2710
|
}
|
|
2711
|
+
/** Replaces a range with literal text; HTML in `text` is not parsed. */
|
|
2635
2712
|
replaceRange(from, to, text) {
|
|
2636
|
-
this.
|
|
2713
|
+
this.insertPlainText(from, to, text);
|
|
2714
|
+
this.editor.commands.focus();
|
|
2637
2715
|
}
|
|
2638
2716
|
getCurrentContentString() {
|
|
2639
2717
|
if (this.plainTextMode) return this.getPlainText();
|