@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.cjs
CHANGED
|
@@ -2492,38 +2492,98 @@ function escapeHtml(text) {
|
|
|
2492
2492
|
if (!text) return "";
|
|
2493
2493
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2494
2494
|
}
|
|
2495
|
+
var PARAGRAPH_HTML_REGEX = /^\s*<p[\s>][\s\S]*<\/p>\s*$/i;
|
|
2495
2496
|
function plainTextToTipTapHtml(text) {
|
|
2496
2497
|
if (!text) return "<p></p>";
|
|
2497
|
-
if (
|
|
2498
|
+
if (PARAGRAPH_HTML_REGEX.test(text)) {
|
|
2498
2499
|
return text;
|
|
2499
2500
|
}
|
|
2500
2501
|
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n").map((line) => `<p>${escapeHtml(line)}</p>`).join("");
|
|
2501
2502
|
}
|
|
2503
|
+
var BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
2504
|
+
"P",
|
|
2505
|
+
"DIV",
|
|
2506
|
+
"LI",
|
|
2507
|
+
"UL",
|
|
2508
|
+
"OL",
|
|
2509
|
+
"PRE",
|
|
2510
|
+
"BLOCKQUOTE",
|
|
2511
|
+
"TABLE",
|
|
2512
|
+
"TR",
|
|
2513
|
+
"H1",
|
|
2514
|
+
"H2",
|
|
2515
|
+
"H3",
|
|
2516
|
+
"H4",
|
|
2517
|
+
"H5",
|
|
2518
|
+
"H6",
|
|
2519
|
+
"HR"
|
|
2520
|
+
]);
|
|
2521
|
+
var MENTION_SPAN_REGEX = /<span\b(?=[^>]*\bdata-type=["']mention["'])([^>]*)>[\s\S]*?<\/span>/gi;
|
|
2522
|
+
function getHtmlAttribute(attrs, name) {
|
|
2523
|
+
const match = new RegExp(`\\b${name}=(?:"([^"]*)"|'([^']*)')`, "i").exec(
|
|
2524
|
+
attrs
|
|
2525
|
+
);
|
|
2526
|
+
return match ? match[1] ?? match[2] : void 0;
|
|
2527
|
+
}
|
|
2528
|
+
function parseHtmlFragment(html) {
|
|
2529
|
+
const template = document.createElement("template");
|
|
2530
|
+
template.innerHTML = html;
|
|
2531
|
+
return template.content;
|
|
2532
|
+
}
|
|
2533
|
+
function decodeHtmlEntities(text) {
|
|
2534
|
+
return text.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/�*39;/g, "'").replace(/ /g, "\xA0").replace(/&/g, "&");
|
|
2535
|
+
}
|
|
2502
2536
|
function htmlToPlainText(html) {
|
|
2503
2537
|
if (!html) return "";
|
|
2504
|
-
const mentionNormalized = html.replace(
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
)
|
|
2508
|
-
/<span[^>]*data-type=["']mention["'][^>]*data-id=["']([^"']+)["'][^>]*>.*?<\/span>/gi,
|
|
2509
|
-
"@$1"
|
|
2510
|
-
);
|
|
2538
|
+
const mentionNormalized = html.replace(MENTION_SPAN_REGEX, (match, attrs) => {
|
|
2539
|
+
const label = getHtmlAttribute(attrs, "data-label") || getHtmlAttribute(attrs, "data-id");
|
|
2540
|
+
return label ? `@${label}` : match;
|
|
2541
|
+
});
|
|
2511
2542
|
if (typeof document !== "undefined") {
|
|
2512
|
-
const
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
const
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2543
|
+
const fragment = parseHtmlFragment(mentionNormalized);
|
|
2544
|
+
fragment.querySelectorAll("br.ProseMirror-trailingBreak").forEach((br) => br.remove());
|
|
2545
|
+
const lines = [];
|
|
2546
|
+
let current = null;
|
|
2547
|
+
const flush = () => {
|
|
2548
|
+
if (current !== null) {
|
|
2549
|
+
lines.push(current);
|
|
2550
|
+
current = null;
|
|
2551
|
+
}
|
|
2552
|
+
};
|
|
2553
|
+
const visit = (node) => {
|
|
2554
|
+
if (node.nodeType === 3) {
|
|
2555
|
+
const value = node.nodeValue ?? "";
|
|
2556
|
+
if (current === null && value.trim() === "") return;
|
|
2557
|
+
current = (current ?? "") + value;
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
if (node.nodeType !== 1) return;
|
|
2561
|
+
const tag = node.tagName.toUpperCase();
|
|
2562
|
+
if (tag === "BR") {
|
|
2563
|
+
current = (current ?? "") + "\n";
|
|
2564
|
+
return;
|
|
2565
|
+
}
|
|
2566
|
+
if (!BLOCK_TAGS.has(tag)) {
|
|
2567
|
+
node.childNodes.forEach(visit);
|
|
2568
|
+
return;
|
|
2569
|
+
}
|
|
2570
|
+
flush();
|
|
2571
|
+
const linesBefore = lines.length;
|
|
2572
|
+
node.childNodes.forEach(visit);
|
|
2573
|
+
flush();
|
|
2574
|
+
if (lines.length === linesBefore && tag !== "HR") {
|
|
2575
|
+
lines.push("");
|
|
2576
|
+
}
|
|
2577
|
+
};
|
|
2578
|
+
fragment.childNodes.forEach(visit);
|
|
2579
|
+
flush();
|
|
2580
|
+
return lines.join("\n");
|
|
2581
|
+
}
|
|
2582
|
+
const text = mentionNormalized.replace(/<br\s*\/?>/gi, "\n").replace(
|
|
2583
|
+
/<\/(?:p|div|li|pre|blockquote|h[1-6])>(?:\s*<\/(?:li|ul|ol|blockquote|div)>)*/gi,
|
|
2584
|
+
"\n"
|
|
2585
|
+
).replace(/<[^>]+>/g, "");
|
|
2586
|
+
return decodeHtmlEntities(text).replace(/\n$/, "");
|
|
2527
2587
|
}
|
|
2528
2588
|
function getEditorText(editor) {
|
|
2529
2589
|
if (!editor) return "";
|
|
@@ -2582,9 +2642,7 @@ function extractMentionsFromDoc(docOrEditor) {
|
|
|
2582
2642
|
}
|
|
2583
2643
|
if (typeof docOrEditor === "string") {
|
|
2584
2644
|
if (typeof document !== "undefined") {
|
|
2585
|
-
|
|
2586
|
-
div.innerHTML = docOrEditor;
|
|
2587
|
-
div.querySelectorAll('span[data-type="mention"]').forEach((el) => {
|
|
2645
|
+
parseHtmlFragment(docOrEditor).querySelectorAll('span[data-type="mention"]').forEach((el) => {
|
|
2588
2646
|
mentions.push({
|
|
2589
2647
|
id: el.getAttribute("data-id") || "",
|
|
2590
2648
|
label: el.getAttribute("data-label") || "",
|
|
@@ -2640,6 +2698,114 @@ md2.validateLink = (url) => {
|
|
|
2640
2698
|
}
|
|
2641
2699
|
return defaultValidateLink.call(md2, url);
|
|
2642
2700
|
};
|
|
2701
|
+
var ALLOWED_HTML_TAGS = /* @__PURE__ */ new Set([
|
|
2702
|
+
"a",
|
|
2703
|
+
"abbr",
|
|
2704
|
+
"b",
|
|
2705
|
+
"blockquote",
|
|
2706
|
+
"br",
|
|
2707
|
+
"code",
|
|
2708
|
+
"del",
|
|
2709
|
+
"details",
|
|
2710
|
+
"div",
|
|
2711
|
+
"em",
|
|
2712
|
+
"h1",
|
|
2713
|
+
"h2",
|
|
2714
|
+
"h3",
|
|
2715
|
+
"h4",
|
|
2716
|
+
"h5",
|
|
2717
|
+
"h6",
|
|
2718
|
+
"hr",
|
|
2719
|
+
"i",
|
|
2720
|
+
"img",
|
|
2721
|
+
"input",
|
|
2722
|
+
"kbd",
|
|
2723
|
+
"li",
|
|
2724
|
+
"mark",
|
|
2725
|
+
"ol",
|
|
2726
|
+
"p",
|
|
2727
|
+
"pre",
|
|
2728
|
+
"s",
|
|
2729
|
+
"span",
|
|
2730
|
+
"strong",
|
|
2731
|
+
"sub",
|
|
2732
|
+
"summary",
|
|
2733
|
+
"sup",
|
|
2734
|
+
"table",
|
|
2735
|
+
"tbody",
|
|
2736
|
+
"td",
|
|
2737
|
+
"tfoot",
|
|
2738
|
+
"th",
|
|
2739
|
+
"thead",
|
|
2740
|
+
"tr",
|
|
2741
|
+
"u",
|
|
2742
|
+
"ul"
|
|
2743
|
+
]);
|
|
2744
|
+
var ALLOWED_HTML_ATTRS = /* @__PURE__ */ new Set([
|
|
2745
|
+
"alt",
|
|
2746
|
+
"align",
|
|
2747
|
+
"checked",
|
|
2748
|
+
"class",
|
|
2749
|
+
"colspan",
|
|
2750
|
+
"disabled",
|
|
2751
|
+
"height",
|
|
2752
|
+
"href",
|
|
2753
|
+
"rel",
|
|
2754
|
+
"rowspan",
|
|
2755
|
+
"src",
|
|
2756
|
+
"start",
|
|
2757
|
+
"target",
|
|
2758
|
+
"title",
|
|
2759
|
+
"type",
|
|
2760
|
+
"width"
|
|
2761
|
+
]);
|
|
2762
|
+
var HTML_TAG_REGEX = /<(\/?)([a-zA-Z][a-zA-Z0-9-]*)((?:\s+[^\s"'>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*)\s*\/?>/g;
|
|
2763
|
+
var HTML_ATTR_REGEX = /([^\s"'>\/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
2764
|
+
function sanitizeTag(tagText, isClosing, rawTagName, rawAttrs) {
|
|
2765
|
+
const tagName = rawTagName.toLowerCase();
|
|
2766
|
+
if (!ALLOWED_HTML_TAGS.has(tagName)) return escapeHtml(tagText);
|
|
2767
|
+
if (isClosing) return `</${tagName}>`;
|
|
2768
|
+
if (tagName === "input" && !/\stype\s*=\s*["']?checkbox["']?[\s>/]/i.test(tagText)) {
|
|
2769
|
+
return escapeHtml(tagText);
|
|
2770
|
+
}
|
|
2771
|
+
let attrs = "";
|
|
2772
|
+
let attrMatch;
|
|
2773
|
+
HTML_ATTR_REGEX.lastIndex = 0;
|
|
2774
|
+
while ((attrMatch = HTML_ATTR_REGEX.exec(rawAttrs)) !== null) {
|
|
2775
|
+
const name = attrMatch[1].toLowerCase();
|
|
2776
|
+
if (!ALLOWED_HTML_ATTRS.has(name) && !/^data-[a-z0-9_.-]+$/.test(name)) {
|
|
2777
|
+
continue;
|
|
2778
|
+
}
|
|
2779
|
+
const rawValue = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4];
|
|
2780
|
+
if (rawValue === void 0) {
|
|
2781
|
+
attrs += ` ${name}`;
|
|
2782
|
+
continue;
|
|
2783
|
+
}
|
|
2784
|
+
const value = md2.utils.unescapeAll(rawValue);
|
|
2785
|
+
if ((name === "href" || name === "src") && // Browsers ignore control characters and whitespace inside schemes.
|
|
2786
|
+
!md2.validateLink(value.replace(/[\u0000-\u0020\u007f]/g, ""))) {
|
|
2787
|
+
continue;
|
|
2788
|
+
}
|
|
2789
|
+
attrs += ` ${name}="${escapeHtml(value)}"`;
|
|
2790
|
+
}
|
|
2791
|
+
return `<${tagName}${attrs}>`;
|
|
2792
|
+
}
|
|
2793
|
+
function sanitizeEmbeddedHtml(html) {
|
|
2794
|
+
let result = "";
|
|
2795
|
+
let lastIndex = 0;
|
|
2796
|
+
let match;
|
|
2797
|
+
HTML_TAG_REGEX.lastIndex = 0;
|
|
2798
|
+
while ((match = HTML_TAG_REGEX.exec(html)) !== null) {
|
|
2799
|
+
result += html.slice(lastIndex, match.index).replace(/</g, "<");
|
|
2800
|
+
result += sanitizeTag(match[0], match[1], match[2], match[3]);
|
|
2801
|
+
lastIndex = match.index + match[0].length;
|
|
2802
|
+
}
|
|
2803
|
+
return result + html.slice(lastIndex).replace(/</g, "<");
|
|
2804
|
+
}
|
|
2805
|
+
var defaultHtmlInline = md2.renderer.rules.html_inline;
|
|
2806
|
+
var defaultHtmlBlock = md2.renderer.rules.html_block;
|
|
2807
|
+
md2.renderer.rules.html_inline = (tokens, idx, options, env, self) => env?.allowUnsafeHtml ? defaultHtmlInline(tokens, idx, options, env, self) : sanitizeEmbeddedHtml(tokens[idx].content);
|
|
2808
|
+
md2.renderer.rules.html_block = (tokens, idx, options, env, self) => env?.allowUnsafeHtml ? defaultHtmlBlock(tokens, idx, options, env, self) : sanitizeEmbeddedHtml(tokens[idx].content);
|
|
2643
2809
|
function preprocessMarkdownFileLinks(content) {
|
|
2644
2810
|
if (!content) return "";
|
|
2645
2811
|
return content.replace(
|
|
@@ -2647,8 +2813,8 @@ function preprocessMarkdownFileLinks(content) {
|
|
|
2647
2813
|
(_match, rawLabel, href) => {
|
|
2648
2814
|
const trimmed = rawLabel.trim();
|
|
2649
2815
|
const codeMatch = trimmed.match(/^`([^`]+)`$/);
|
|
2650
|
-
const labelContent = codeMatch ? `<code>${codeMatch[1]}</code>` : trimmed;
|
|
2651
|
-
return `<a href="${href}" data-file-link="true" title="Click to copy file path">${labelContent}</a>`;
|
|
2816
|
+
const labelContent = codeMatch ? `<code>${escapeHtml(codeMatch[1])}</code>` : escapeHtml(trimmed);
|
|
2817
|
+
return `<a href="${escapeHtml(href)}" data-file-link="true" title="Click to copy file path">${labelContent}</a>`;
|
|
2652
2818
|
}
|
|
2653
2819
|
);
|
|
2654
2820
|
}
|
|
@@ -2664,7 +2830,7 @@ function markdownToTipTapHtml(markdown, options) {
|
|
|
2664
2830
|
};
|
|
2665
2831
|
content = preprocessMarkdownMentions(content, mentionOpts);
|
|
2666
2832
|
}
|
|
2667
|
-
return md2.render(content);
|
|
2833
|
+
return md2.render(content, { allowUnsafeHtml: options?.allowUnsafeHtml });
|
|
2668
2834
|
}
|
|
2669
2835
|
function getEditorMarkdown(editor) {
|
|
2670
2836
|
if (!editor) return "";
|
|
@@ -2677,6 +2843,20 @@ function getEditorMarkdown(editor) {
|
|
|
2677
2843
|
// src/controller.ts
|
|
2678
2844
|
var import_core10 = require("@tiptap/core");
|
|
2679
2845
|
var import_model2 = require("@tiptap/pm/model");
|
|
2846
|
+
function plainTextToSlice(text, schema) {
|
|
2847
|
+
const nodes = [];
|
|
2848
|
+
text.replace(/\r\n?/g, "\n").split("\n").forEach((line) => {
|
|
2849
|
+
const nodeJson = { type: "paragraph" };
|
|
2850
|
+
if (line.length > 0) {
|
|
2851
|
+
nodeJson.content = [{ type: "text", text: line }];
|
|
2852
|
+
}
|
|
2853
|
+
try {
|
|
2854
|
+
nodes.push(import_model2.Node.fromJSON(schema, nodeJson));
|
|
2855
|
+
} catch {
|
|
2856
|
+
}
|
|
2857
|
+
});
|
|
2858
|
+
return import_model2.Slice.maxOpen(import_model2.Fragment.fromArray(nodes));
|
|
2859
|
+
}
|
|
2680
2860
|
var EditorController = class {
|
|
2681
2861
|
editor;
|
|
2682
2862
|
plainTextMode;
|
|
@@ -2732,20 +2912,7 @@ var EditorController = class {
|
|
|
2732
2912
|
clipboardTextParser: (text, context, _plain, view) => {
|
|
2733
2913
|
const schema = context?.doc?.type?.schema || context?.schema || view?.state?.schema || this.editor.schema;
|
|
2734
2914
|
if (!schema) return import_model2.Slice.empty;
|
|
2735
|
-
|
|
2736
|
-
const nodes = [];
|
|
2737
|
-
blocks.forEach((line) => {
|
|
2738
|
-
const nodeJson = { type: "paragraph" };
|
|
2739
|
-
if (line.length > 0) {
|
|
2740
|
-
nodeJson.content = [{ type: "text", text: line }];
|
|
2741
|
-
}
|
|
2742
|
-
try {
|
|
2743
|
-
const node = import_model2.Node.fromJSON(schema, nodeJson);
|
|
2744
|
-
nodes.push(node);
|
|
2745
|
-
} catch {
|
|
2746
|
-
}
|
|
2747
|
-
});
|
|
2748
|
-
return import_model2.Slice.maxOpen(import_model2.Fragment.fromArray(nodes));
|
|
2915
|
+
return plainTextToSlice(text, schema);
|
|
2749
2916
|
}
|
|
2750
2917
|
}
|
|
2751
2918
|
});
|
|
@@ -2788,14 +2955,29 @@ var EditorController = class {
|
|
|
2788
2955
|
this.editor.commands.setContent(html, emitUpdate);
|
|
2789
2956
|
}
|
|
2790
2957
|
}
|
|
2958
|
+
/**
|
|
2959
|
+
* Replaces `from`..`to` with literal text: HTML in `text` is not parsed, and
|
|
2960
|
+
* newlines split paragraphs.
|
|
2961
|
+
*/
|
|
2962
|
+
insertPlainText(from, to, text) {
|
|
2963
|
+
const { state, view } = this.editor;
|
|
2964
|
+
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);
|
|
2965
|
+
view.dispatch(tr.scrollIntoView());
|
|
2966
|
+
}
|
|
2967
|
+
/** Inserts text literally at the selection; HTML in `text` is not parsed. */
|
|
2791
2968
|
insertText(text) {
|
|
2792
|
-
this.editor.
|
|
2969
|
+
const { from, to } = this.editor.state.selection;
|
|
2970
|
+
this.insertPlainText(from, to, text);
|
|
2793
2971
|
}
|
|
2972
|
+
/** Inserts text literally at `pos`; HTML in `text` is not parsed. */
|
|
2794
2973
|
insertTextAt(pos, text) {
|
|
2795
|
-
this.
|
|
2974
|
+
this.insertPlainText(pos, pos, text);
|
|
2975
|
+
this.editor.commands.focus();
|
|
2796
2976
|
}
|
|
2977
|
+
/** Replaces a range with literal text; HTML in `text` is not parsed. */
|
|
2797
2978
|
replaceRange(from, to, text) {
|
|
2798
|
-
this.
|
|
2979
|
+
this.insertPlainText(from, to, text);
|
|
2980
|
+
this.editor.commands.focus();
|
|
2799
2981
|
}
|
|
2800
2982
|
getCurrentContentString() {
|
|
2801
2983
|
if (this.plainTextMode) return this.getPlainText();
|