@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 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") || "",
@@ -2677,6 +2735,20 @@ function getEditorMarkdown(editor) {
2677
2735
  // src/controller.ts
2678
2736
  var import_core10 = require("@tiptap/core");
2679
2737
  var import_model2 = require("@tiptap/pm/model");
2738
+ function plainTextToSlice(text, schema) {
2739
+ const nodes = [];
2740
+ text.replace(/\r\n?/g, "\n").split("\n").forEach((line) => {
2741
+ const nodeJson = { type: "paragraph" };
2742
+ if (line.length > 0) {
2743
+ nodeJson.content = [{ type: "text", text: line }];
2744
+ }
2745
+ try {
2746
+ nodes.push(import_model2.Node.fromJSON(schema, nodeJson));
2747
+ } catch {
2748
+ }
2749
+ });
2750
+ return import_model2.Slice.maxOpen(import_model2.Fragment.fromArray(nodes));
2751
+ }
2680
2752
  var EditorController = class {
2681
2753
  editor;
2682
2754
  plainTextMode;
@@ -2732,20 +2804,7 @@ var EditorController = class {
2732
2804
  clipboardTextParser: (text, context, _plain, view) => {
2733
2805
  const schema = context?.doc?.type?.schema || context?.schema || view?.state?.schema || this.editor.schema;
2734
2806
  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));
2807
+ return plainTextToSlice(text, schema);
2749
2808
  }
2750
2809
  }
2751
2810
  });
@@ -2788,14 +2847,29 @@ var EditorController = class {
2788
2847
  this.editor.commands.setContent(html, emitUpdate);
2789
2848
  }
2790
2849
  }
2850
+ /**
2851
+ * Replaces `from`..`to` with literal text: HTML in `text` is not parsed, and
2852
+ * newlines split paragraphs.
2853
+ */
2854
+ insertPlainText(from, to, text) {
2855
+ const { state, view } = this.editor;
2856
+ 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);
2857
+ view.dispatch(tr.scrollIntoView());
2858
+ }
2859
+ /** Inserts text literally at the selection; HTML in `text` is not parsed. */
2791
2860
  insertText(text) {
2792
- this.editor.commands.insertContent(text);
2861
+ const { from, to } = this.editor.state.selection;
2862
+ this.insertPlainText(from, to, text);
2793
2863
  }
2864
+ /** Inserts text literally at `pos`; HTML in `text` is not parsed. */
2794
2865
  insertTextAt(pos, text) {
2795
- this.editor.chain().focus().insertContentAt(pos, text).run();
2866
+ this.insertPlainText(pos, pos, text);
2867
+ this.editor.commands.focus();
2796
2868
  }
2869
+ /** Replaces a range with literal text; HTML in `text` is not parsed. */
2797
2870
  replaceRange(from, to, text) {
2798
- this.editor.chain().focus().deleteRange({ from, to }).insertContentAt(from, text).run();
2871
+ this.insertPlainText(from, to, text);
2872
+ this.editor.commands.focus();
2799
2873
  }
2800
2874
  getCurrentContentString() {
2801
2875
  if (this.plainTextMode) return this.getPlainText();