@pixerate/editor 0.14.0 → 0.14.2
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 +228 -46
- 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 +233 -47
- package/dist/index.js.map +1 -1
- package/dist/serializers/index.cjs +194 -28
- package/dist/serializers/index.cjs.map +1 -1
- package/dist/serializers/index.d.cts +9 -0
- package/dist/serializers/index.d.ts +9 -0
- package/dist/serializers/index.js +194 -28
- 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") || "",
|
|
@@ -2478,6 +2536,114 @@ md2.validateLink = (url) => {
|
|
|
2478
2536
|
}
|
|
2479
2537
|
return defaultValidateLink.call(md2, url);
|
|
2480
2538
|
};
|
|
2539
|
+
var ALLOWED_HTML_TAGS = /* @__PURE__ */ new Set([
|
|
2540
|
+
"a",
|
|
2541
|
+
"abbr",
|
|
2542
|
+
"b",
|
|
2543
|
+
"blockquote",
|
|
2544
|
+
"br",
|
|
2545
|
+
"code",
|
|
2546
|
+
"del",
|
|
2547
|
+
"details",
|
|
2548
|
+
"div",
|
|
2549
|
+
"em",
|
|
2550
|
+
"h1",
|
|
2551
|
+
"h2",
|
|
2552
|
+
"h3",
|
|
2553
|
+
"h4",
|
|
2554
|
+
"h5",
|
|
2555
|
+
"h6",
|
|
2556
|
+
"hr",
|
|
2557
|
+
"i",
|
|
2558
|
+
"img",
|
|
2559
|
+
"input",
|
|
2560
|
+
"kbd",
|
|
2561
|
+
"li",
|
|
2562
|
+
"mark",
|
|
2563
|
+
"ol",
|
|
2564
|
+
"p",
|
|
2565
|
+
"pre",
|
|
2566
|
+
"s",
|
|
2567
|
+
"span",
|
|
2568
|
+
"strong",
|
|
2569
|
+
"sub",
|
|
2570
|
+
"summary",
|
|
2571
|
+
"sup",
|
|
2572
|
+
"table",
|
|
2573
|
+
"tbody",
|
|
2574
|
+
"td",
|
|
2575
|
+
"tfoot",
|
|
2576
|
+
"th",
|
|
2577
|
+
"thead",
|
|
2578
|
+
"tr",
|
|
2579
|
+
"u",
|
|
2580
|
+
"ul"
|
|
2581
|
+
]);
|
|
2582
|
+
var ALLOWED_HTML_ATTRS = /* @__PURE__ */ new Set([
|
|
2583
|
+
"alt",
|
|
2584
|
+
"align",
|
|
2585
|
+
"checked",
|
|
2586
|
+
"class",
|
|
2587
|
+
"colspan",
|
|
2588
|
+
"disabled",
|
|
2589
|
+
"height",
|
|
2590
|
+
"href",
|
|
2591
|
+
"rel",
|
|
2592
|
+
"rowspan",
|
|
2593
|
+
"src",
|
|
2594
|
+
"start",
|
|
2595
|
+
"target",
|
|
2596
|
+
"title",
|
|
2597
|
+
"type",
|
|
2598
|
+
"width"
|
|
2599
|
+
]);
|
|
2600
|
+
var HTML_TAG_REGEX = /<(\/?)([a-zA-Z][a-zA-Z0-9-]*)((?:\s+[^\s"'>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*)\s*\/?>/g;
|
|
2601
|
+
var HTML_ATTR_REGEX = /([^\s"'>\/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
2602
|
+
function sanitizeTag(tagText, isClosing, rawTagName, rawAttrs) {
|
|
2603
|
+
const tagName = rawTagName.toLowerCase();
|
|
2604
|
+
if (!ALLOWED_HTML_TAGS.has(tagName)) return escapeHtml(tagText);
|
|
2605
|
+
if (isClosing) return `</${tagName}>`;
|
|
2606
|
+
if (tagName === "input" && !/\stype\s*=\s*["']?checkbox["']?[\s>/]/i.test(tagText)) {
|
|
2607
|
+
return escapeHtml(tagText);
|
|
2608
|
+
}
|
|
2609
|
+
let attrs = "";
|
|
2610
|
+
let attrMatch;
|
|
2611
|
+
HTML_ATTR_REGEX.lastIndex = 0;
|
|
2612
|
+
while ((attrMatch = HTML_ATTR_REGEX.exec(rawAttrs)) !== null) {
|
|
2613
|
+
const name = attrMatch[1].toLowerCase();
|
|
2614
|
+
if (!ALLOWED_HTML_ATTRS.has(name) && !/^data-[a-z0-9_.-]+$/.test(name)) {
|
|
2615
|
+
continue;
|
|
2616
|
+
}
|
|
2617
|
+
const rawValue = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4];
|
|
2618
|
+
if (rawValue === void 0) {
|
|
2619
|
+
attrs += ` ${name}`;
|
|
2620
|
+
continue;
|
|
2621
|
+
}
|
|
2622
|
+
const value = md2.utils.unescapeAll(rawValue);
|
|
2623
|
+
if ((name === "href" || name === "src") && // Browsers ignore control characters and whitespace inside schemes.
|
|
2624
|
+
!md2.validateLink(value.replace(/[\u0000-\u0020\u007f]/g, ""))) {
|
|
2625
|
+
continue;
|
|
2626
|
+
}
|
|
2627
|
+
attrs += ` ${name}="${escapeHtml(value)}"`;
|
|
2628
|
+
}
|
|
2629
|
+
return `<${tagName}${attrs}>`;
|
|
2630
|
+
}
|
|
2631
|
+
function sanitizeEmbeddedHtml(html) {
|
|
2632
|
+
let result = "";
|
|
2633
|
+
let lastIndex = 0;
|
|
2634
|
+
let match;
|
|
2635
|
+
HTML_TAG_REGEX.lastIndex = 0;
|
|
2636
|
+
while ((match = HTML_TAG_REGEX.exec(html)) !== null) {
|
|
2637
|
+
result += html.slice(lastIndex, match.index).replace(/</g, "<");
|
|
2638
|
+
result += sanitizeTag(match[0], match[1], match[2], match[3]);
|
|
2639
|
+
lastIndex = match.index + match[0].length;
|
|
2640
|
+
}
|
|
2641
|
+
return result + html.slice(lastIndex).replace(/</g, "<");
|
|
2642
|
+
}
|
|
2643
|
+
var defaultHtmlInline = md2.renderer.rules.html_inline;
|
|
2644
|
+
var defaultHtmlBlock = md2.renderer.rules.html_block;
|
|
2645
|
+
md2.renderer.rules.html_inline = (tokens, idx, options, env, self) => env?.allowUnsafeHtml ? defaultHtmlInline(tokens, idx, options, env, self) : sanitizeEmbeddedHtml(tokens[idx].content);
|
|
2646
|
+
md2.renderer.rules.html_block = (tokens, idx, options, env, self) => env?.allowUnsafeHtml ? defaultHtmlBlock(tokens, idx, options, env, self) : sanitizeEmbeddedHtml(tokens[idx].content);
|
|
2481
2647
|
function preprocessMarkdownFileLinks(content) {
|
|
2482
2648
|
if (!content) return "";
|
|
2483
2649
|
return content.replace(
|
|
@@ -2485,8 +2651,8 @@ function preprocessMarkdownFileLinks(content) {
|
|
|
2485
2651
|
(_match, rawLabel, href) => {
|
|
2486
2652
|
const trimmed = rawLabel.trim();
|
|
2487
2653
|
const codeMatch = trimmed.match(/^`([^`]+)`$/);
|
|
2488
|
-
const labelContent = codeMatch ? `<code>${codeMatch[1]}</code>` : trimmed;
|
|
2489
|
-
return `<a href="${href}" data-file-link="true" title="Click to copy file path">${labelContent}</a>`;
|
|
2654
|
+
const labelContent = codeMatch ? `<code>${escapeHtml(codeMatch[1])}</code>` : escapeHtml(trimmed);
|
|
2655
|
+
return `<a href="${escapeHtml(href)}" data-file-link="true" title="Click to copy file path">${labelContent}</a>`;
|
|
2490
2656
|
}
|
|
2491
2657
|
);
|
|
2492
2658
|
}
|
|
@@ -2502,7 +2668,7 @@ function markdownToTipTapHtml(markdown, options) {
|
|
|
2502
2668
|
};
|
|
2503
2669
|
content = preprocessMarkdownMentions(content, mentionOpts);
|
|
2504
2670
|
}
|
|
2505
|
-
return md2.render(content);
|
|
2671
|
+
return md2.render(content, { allowUnsafeHtml: options?.allowUnsafeHtml });
|
|
2506
2672
|
}
|
|
2507
2673
|
function getEditorMarkdown(editor) {
|
|
2508
2674
|
if (!editor) return "";
|
|
@@ -2514,7 +2680,25 @@ function getEditorMarkdown(editor) {
|
|
|
2514
2680
|
|
|
2515
2681
|
// src/controller.ts
|
|
2516
2682
|
import { Editor } from "@tiptap/core";
|
|
2517
|
-
import {
|
|
2683
|
+
import {
|
|
2684
|
+
Slice,
|
|
2685
|
+
Fragment as Fragment2,
|
|
2686
|
+
Node as ProsemirrorNode
|
|
2687
|
+
} from "@tiptap/pm/model";
|
|
2688
|
+
function plainTextToSlice(text, schema) {
|
|
2689
|
+
const nodes = [];
|
|
2690
|
+
text.replace(/\r\n?/g, "\n").split("\n").forEach((line) => {
|
|
2691
|
+
const nodeJson = { type: "paragraph" };
|
|
2692
|
+
if (line.length > 0) {
|
|
2693
|
+
nodeJson.content = [{ type: "text", text: line }];
|
|
2694
|
+
}
|
|
2695
|
+
try {
|
|
2696
|
+
nodes.push(ProsemirrorNode.fromJSON(schema, nodeJson));
|
|
2697
|
+
} catch {
|
|
2698
|
+
}
|
|
2699
|
+
});
|
|
2700
|
+
return Slice.maxOpen(Fragment2.fromArray(nodes));
|
|
2701
|
+
}
|
|
2518
2702
|
var EditorController = class {
|
|
2519
2703
|
editor;
|
|
2520
2704
|
plainTextMode;
|
|
@@ -2570,20 +2754,7 @@ var EditorController = class {
|
|
|
2570
2754
|
clipboardTextParser: (text, context, _plain, view) => {
|
|
2571
2755
|
const schema = context?.doc?.type?.schema || context?.schema || view?.state?.schema || this.editor.schema;
|
|
2572
2756
|
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));
|
|
2757
|
+
return plainTextToSlice(text, schema);
|
|
2587
2758
|
}
|
|
2588
2759
|
}
|
|
2589
2760
|
});
|
|
@@ -2626,14 +2797,29 @@ var EditorController = class {
|
|
|
2626
2797
|
this.editor.commands.setContent(html, emitUpdate);
|
|
2627
2798
|
}
|
|
2628
2799
|
}
|
|
2800
|
+
/**
|
|
2801
|
+
* Replaces `from`..`to` with literal text: HTML in `text` is not parsed, and
|
|
2802
|
+
* newlines split paragraphs.
|
|
2803
|
+
*/
|
|
2804
|
+
insertPlainText(from, to, text) {
|
|
2805
|
+
const { state, view } = this.editor;
|
|
2806
|
+
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);
|
|
2807
|
+
view.dispatch(tr.scrollIntoView());
|
|
2808
|
+
}
|
|
2809
|
+
/** Inserts text literally at the selection; HTML in `text` is not parsed. */
|
|
2629
2810
|
insertText(text) {
|
|
2630
|
-
this.editor.
|
|
2811
|
+
const { from, to } = this.editor.state.selection;
|
|
2812
|
+
this.insertPlainText(from, to, text);
|
|
2631
2813
|
}
|
|
2814
|
+
/** Inserts text literally at `pos`; HTML in `text` is not parsed. */
|
|
2632
2815
|
insertTextAt(pos, text) {
|
|
2633
|
-
this.
|
|
2816
|
+
this.insertPlainText(pos, pos, text);
|
|
2817
|
+
this.editor.commands.focus();
|
|
2634
2818
|
}
|
|
2819
|
+
/** Replaces a range with literal text; HTML in `text` is not parsed. */
|
|
2635
2820
|
replaceRange(from, to, text) {
|
|
2636
|
-
this.
|
|
2821
|
+
this.insertPlainText(from, to, text);
|
|
2822
|
+
this.editor.commands.focus();
|
|
2637
2823
|
}
|
|
2638
2824
|
getCurrentContentString() {
|
|
2639
2825
|
if (this.plainTextMode) return this.getPlainText();
|