@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.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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
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 (/<p[\s>]/i.test(text)) {
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(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#0*39;/g, "'").replace(/&nbsp;/g, "\xA0").replace(/&amp;/g, "&");
2373
+ }
2340
2374
  function htmlToPlainText(html) {
2341
2375
  if (!html) return "";
2342
- const mentionNormalized = html.replace(
2343
- /<span[^>]*data-type=["']mention["'][^>]*data-label=["']([^"']+)["'][^>]*>.*?<\/span>/gi,
2344
- "@$1"
2345
- ).replace(
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 tempDiv = document.createElement("div");
2351
- tempDiv.innerHTML = mentionNormalized;
2352
- tempDiv.querySelectorAll("br.ProseMirror-trailingBreak").forEach((br) => br.remove());
2353
- tempDiv.querySelectorAll("br").forEach((br) => br.replaceWith("\n"));
2354
- const paragraphs = tempDiv.querySelectorAll("p, div, li, h1, h2, h3, h4, h5, h6");
2355
- if (paragraphs.length > 0) {
2356
- const textParts = [];
2357
- paragraphs.forEach((p) => {
2358
- textParts.push(p.textContent || "");
2359
- });
2360
- return textParts.join("\n");
2361
- }
2362
- return tempDiv.textContent || "";
2363
- }
2364
- 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();
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
- const div = document.createElement("div");
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 { Slice, Fragment as Fragment2, Node as ProsemirrorNode } from "@tiptap/pm/model";
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
- const blocks = text.replace(/\r\n?/g, "\n").split("\n");
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.commands.insertContent(text);
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.editor.chain().focus().insertContentAt(pos, text).run();
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.editor.chain().focus().deleteRange({ from, to }).insertContentAt(from, text).run();
2713
+ this.insertPlainText(from, to, text);
2714
+ this.editor.commands.focus();
2637
2715
  }
2638
2716
  getCurrentContentString() {
2639
2717
  if (this.plainTextMode) return this.getPlainText();