@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 CHANGED
@@ -2492,38 +2492,98 @@ function escapeHtml(text) {
2492
2492
  if (!text) return "";
2493
2493
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
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 (/<p[\s>]/i.test(text)) {
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(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#0*39;/g, "'").replace(/&nbsp;/g, "\xA0").replace(/&amp;/g, "&");
2535
+ }
2502
2536
  function htmlToPlainText(html) {
2503
2537
  if (!html) return "";
2504
- const mentionNormalized = html.replace(
2505
- /<span[^>]*data-type=["']mention["'][^>]*data-label=["']([^"']+)["'][^>]*>.*?<\/span>/gi,
2506
- "@$1"
2507
- ).replace(
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 tempDiv = document.createElement("div");
2513
- tempDiv.innerHTML = mentionNormalized;
2514
- tempDiv.querySelectorAll("br.ProseMirror-trailingBreak").forEach((br) => br.remove());
2515
- tempDiv.querySelectorAll("br").forEach((br) => br.replaceWith("\n"));
2516
- const paragraphs = tempDiv.querySelectorAll("p, div, li, h1, h2, h3, h4, h5, h6");
2517
- if (paragraphs.length > 0) {
2518
- const textParts = [];
2519
- paragraphs.forEach((p) => {
2520
- textParts.push(p.textContent || "");
2521
- });
2522
- return textParts.join("\n");
2523
- }
2524
- return tempDiv.textContent || "";
2525
- }
2526
- return mentionNormalized.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<\/div>/gi, "\n").replace(/<\/li>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#039;/g, "'").trim();
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
- const div = document.createElement("div");
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, "&lt;");
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, "&lt;");
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
- const blocks = text.replace(/\r\n?/g, "\n").split("\n");
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.commands.insertContent(text);
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.editor.chain().focus().insertContentAt(pos, text).run();
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.editor.chain().focus().deleteRange({ from, to }).insertContentAt(from, text).run();
2979
+ this.insertPlainText(from, to, text);
2980
+ this.editor.commands.focus();
2799
2981
  }
2800
2982
  getCurrentContentString() {
2801
2983
  if (this.plainTextMode) return this.getPlainText();