@pixerate/editor 0.13.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
@@ -66,6 +66,8 @@ __export(src_exports, {
66
66
  SmilieReplacer: () => SmilieReplacer,
67
67
  SpreadsheetController: () => SpreadsheetController,
68
68
  StreamingLayoutManager: () => StreamingLayoutManager,
69
+ TEMPLATE_DEPTH_ERROR: () => TEMPLATE_DEPTH_ERROR,
70
+ TEMPLATE_LOOP_ERROR: () => TEMPLATE_LOOP_ERROR,
69
71
  TEMPLATE_REGEX: () => TEMPLATE_REGEX,
70
72
  TemplateSuggestions: () => TemplateSuggestions,
71
73
  VARIABLE_REGEX: () => VARIABLE_REGEX,
@@ -111,6 +113,7 @@ __export(src_exports, {
111
113
  getEditorText: () => getEditorText,
112
114
  getLatestVersion: () => getLatestVersion,
113
115
  getLayoutedNodes: () => getLayoutedNodes,
116
+ getTemplateBody: () => getTemplateBody,
114
117
  getViewportMetrics: () => getViewportMetrics,
115
118
  globalImageCache: () => globalImageCache,
116
119
  handleImageInsertion: () => handleImageInsertion,
@@ -280,6 +283,18 @@ function getLatestVersion(template) {
280
283
  }
281
284
  return template.versions[template.versions.length - 1];
282
285
  }
286
+ var TEMPLATE_LOOP_ERROR = "[Template loop detected]";
287
+ var TEMPLATE_DEPTH_ERROR = "[Template max depth exceeded]";
288
+ function getTemplateBody(template, bindings) {
289
+ const boundVersionId = bindings?.[template.name];
290
+ if (boundVersionId && template.versions) {
291
+ const boundVersion = template.versions.find(
292
+ (v) => v.id === boundVersionId || v.version === boundVersionId
293
+ );
294
+ return boundVersion?.body;
295
+ }
296
+ return getLatestVersion(template)?.body ?? template.body;
297
+ }
283
298
  function resolveTemplates(text, templates = [], options) {
284
299
  const templateMap = /* @__PURE__ */ new Map();
285
300
  for (const t of templates) {
@@ -287,50 +302,27 @@ function resolveTemplates(text, templates = [], options) {
287
302
  }
288
303
  const maxDepth = options?.maxDepth ?? 10;
289
304
  function resolveRecursive(currentText, visited, depth) {
290
- if (depth > maxDepth) {
291
- return "[Template max depth exceeded]";
292
- }
293
- let result = currentText;
294
305
  const regex = new RegExp(TEMPLATE_REGEX.source, TEMPLATE_REGEX.flags);
295
- let match;
296
- while ((match = regex.exec(result)) !== null) {
297
- const [fullMatch, rawTemplateName] = match;
306
+ return currentText.replace(regex, (fullMatch, rawTemplateName) => {
298
307
  const templateName = rawTemplateName.trim();
299
308
  if (visited.has(templateName)) {
300
- return "[Template loop detected]";
309
+ return TEMPLATE_LOOP_ERROR;
301
310
  }
302
311
  const template = templateMap.get(templateName);
303
312
  if (!template) {
304
- continue;
305
- }
306
- let versionBody;
307
- const boundVersionId = options?.bindings?.[templateName];
308
- if (boundVersionId && template.versions) {
309
- const boundVersion = template.versions.find(
310
- (v) => v.id === boundVersionId || v.version === boundVersionId
311
- );
312
- versionBody = boundVersion?.body;
313
- } else {
314
- const latest = getLatestVersion(template);
315
- versionBody = latest?.body ?? template.body;
313
+ return fullMatch;
316
314
  }
315
+ const versionBody = getTemplateBody(template, options?.bindings);
317
316
  if (versionBody === void 0) {
318
- continue;
317
+ return fullMatch;
318
+ }
319
+ if (depth + 1 > maxDepth) {
320
+ return TEMPLATE_DEPTH_ERROR;
319
321
  }
320
322
  const nextVisited = new Set(visited);
321
323
  nextVisited.add(templateName);
322
- const resolvedBody = resolveRecursive(
323
- versionBody,
324
- nextVisited,
325
- depth + 1
326
- );
327
- if (resolvedBody === "[Template loop detected]" || resolvedBody === "[Template max depth exceeded]") {
328
- return resolvedBody;
329
- }
330
- result = result.replace(fullMatch, resolvedBody);
331
- regex.lastIndex = 0;
332
- }
333
- return result;
324
+ return resolveRecursive(versionBody, nextVisited, depth + 1);
325
+ });
334
326
  }
335
327
  return resolveRecursive(text, /* @__PURE__ */ new Set(), 0);
336
328
  }
@@ -379,7 +371,7 @@ function extractInstructionNames(text) {
379
371
  }
380
372
 
381
373
  // src/grammar/sourcemap.ts
382
- function resolveTemplatesWithMapping(text, templates = [], templateBindings) {
374
+ function resolveTemplatesWithMapping(text, templates = [], templateBindings, options) {
383
375
  if (!templates || templates.length === 0 || !text.includes("{{")) {
384
376
  return {
385
377
  text,
@@ -395,7 +387,7 @@ function resolveTemplatesWithMapping(text, templates = [], templateBindings) {
395
387
  };
396
388
  }
397
389
  const templateMap = new Map(templates.map((t) => [t.name, t]));
398
- const regex = /{{\s*([^}]+?)\s*}}/g;
390
+ const regex = new RegExp(TEMPLATE_REGEX.source, TEMPLATE_REGEX.flags);
399
391
  let match;
400
392
  let lastIndex = 0;
401
393
  let resolvedText = "";
@@ -417,25 +409,13 @@ function resolveTemplatesWithMapping(text, templates = [], templateBindings) {
417
409
  }
418
410
  let resolvedBody = fullMatch;
419
411
  let isTemplate = false;
420
- if (templateMap.has(templateName)) {
421
- const template = templateMap.get(templateName);
422
- let versionBody;
423
- const boundVersionId = templateBindings?.[templateName];
424
- if (boundVersionId && template.versions) {
425
- const boundVersion = template.versions.find(
426
- (v) => v.id === boundVersionId || v.version === boundVersionId
427
- );
428
- versionBody = boundVersion?.body;
429
- } else {
430
- const latestVersion = getLatestVersion(template);
431
- versionBody = latestVersion?.body ?? template.body;
432
- }
433
- if (versionBody !== void 0) {
434
- resolvedBody = resolveTemplates(versionBody, templates, {
435
- bindings: templateBindings
436
- });
437
- isTemplate = true;
438
- }
412
+ const template = templateMap.get(templateName);
413
+ if (template && getTemplateBody(template, templateBindings) !== void 0) {
414
+ resolvedBody = resolveTemplates(fullMatch, templates, {
415
+ bindings: templateBindings,
416
+ maxDepth: options?.maxDepth
417
+ });
418
+ isTemplate = true;
439
419
  }
440
420
  if (isTemplate) {
441
421
  map.push({
@@ -480,9 +460,11 @@ function mapResolvedOffsetToRaw(resolvedOffset, map) {
480
460
  if (inRange) {
481
461
  if (seg.source === "raw") {
482
462
  return seg.rawStart + (resolvedOffset - seg.resolvedStart);
483
- } else {
484
- return seg.rawStart;
485
463
  }
464
+ if (resolvedOffset === seg.resolvedEnd) {
465
+ return seg.rawEnd;
466
+ }
467
+ return seg.rawStart;
486
468
  }
487
469
  }
488
470
  return resolvedOffset;
@@ -495,9 +477,11 @@ function mapRawOffsetToResolved(rawOffset, map) {
495
477
  if (inRange) {
496
478
  if (seg.source === "raw") {
497
479
  return seg.resolvedStart + (rawOffset - seg.rawStart);
498
- } else {
499
- return seg.resolvedStart;
500
480
  }
481
+ if (rawOffset === seg.rawEnd) {
482
+ return seg.resolvedEnd;
483
+ }
484
+ return seg.resolvedStart;
501
485
  }
502
486
  }
503
487
  return rawOffset;
@@ -2508,38 +2492,98 @@ function escapeHtml(text) {
2508
2492
  if (!text) return "";
2509
2493
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
2510
2494
  }
2495
+ var PARAGRAPH_HTML_REGEX = /^\s*<p[\s>][\s\S]*<\/p>\s*$/i;
2511
2496
  function plainTextToTipTapHtml(text) {
2512
2497
  if (!text) return "<p></p>";
2513
- if (/<p[\s>]/i.test(text)) {
2498
+ if (PARAGRAPH_HTML_REGEX.test(text)) {
2514
2499
  return text;
2515
2500
  }
2516
2501
  return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n").map((line) => `<p>${escapeHtml(line)}</p>`).join("");
2517
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
+ }
2518
2536
  function htmlToPlainText(html) {
2519
2537
  if (!html) return "";
2520
- const mentionNormalized = html.replace(
2521
- /<span[^>]*data-type=["']mention["'][^>]*data-label=["']([^"']+)["'][^>]*>.*?<\/span>/gi,
2522
- "@$1"
2523
- ).replace(
2524
- /<span[^>]*data-type=["']mention["'][^>]*data-id=["']([^"']+)["'][^>]*>.*?<\/span>/gi,
2525
- "@$1"
2526
- );
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
+ });
2527
2542
  if (typeof document !== "undefined") {
2528
- const tempDiv = document.createElement("div");
2529
- tempDiv.innerHTML = mentionNormalized;
2530
- tempDiv.querySelectorAll("br.ProseMirror-trailingBreak").forEach((br) => br.remove());
2531
- tempDiv.querySelectorAll("br").forEach((br) => br.replaceWith("\n"));
2532
- const paragraphs = tempDiv.querySelectorAll("p, div, li, h1, h2, h3, h4, h5, h6");
2533
- if (paragraphs.length > 0) {
2534
- const textParts = [];
2535
- paragraphs.forEach((p) => {
2536
- textParts.push(p.textContent || "");
2537
- });
2538
- return textParts.join("\n");
2539
- }
2540
- return tempDiv.textContent || "";
2541
- }
2542
- 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$/, "");
2543
2587
  }
2544
2588
  function getEditorText(editor) {
2545
2589
  if (!editor) return "";
@@ -2598,9 +2642,7 @@ function extractMentionsFromDoc(docOrEditor) {
2598
2642
  }
2599
2643
  if (typeof docOrEditor === "string") {
2600
2644
  if (typeof document !== "undefined") {
2601
- const div = document.createElement("div");
2602
- div.innerHTML = docOrEditor;
2603
- div.querySelectorAll('span[data-type="mention"]').forEach((el) => {
2645
+ parseHtmlFragment(docOrEditor).querySelectorAll('span[data-type="mention"]').forEach((el) => {
2604
2646
  mentions.push({
2605
2647
  id: el.getAttribute("data-id") || "",
2606
2648
  label: el.getAttribute("data-label") || "",
@@ -2693,6 +2735,20 @@ function getEditorMarkdown(editor) {
2693
2735
  // src/controller.ts
2694
2736
  var import_core10 = require("@tiptap/core");
2695
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
+ }
2696
2752
  var EditorController = class {
2697
2753
  editor;
2698
2754
  plainTextMode;
@@ -2748,20 +2804,7 @@ var EditorController = class {
2748
2804
  clipboardTextParser: (text, context, _plain, view) => {
2749
2805
  const schema = context?.doc?.type?.schema || context?.schema || view?.state?.schema || this.editor.schema;
2750
2806
  if (!schema) return import_model2.Slice.empty;
2751
- const blocks = text.replace(/\r\n?/g, "\n").split("\n");
2752
- const nodes = [];
2753
- blocks.forEach((line) => {
2754
- const nodeJson = { type: "paragraph" };
2755
- if (line.length > 0) {
2756
- nodeJson.content = [{ type: "text", text: line }];
2757
- }
2758
- try {
2759
- const node = import_model2.Node.fromJSON(schema, nodeJson);
2760
- nodes.push(node);
2761
- } catch {
2762
- }
2763
- });
2764
- return import_model2.Slice.maxOpen(import_model2.Fragment.fromArray(nodes));
2807
+ return plainTextToSlice(text, schema);
2765
2808
  }
2766
2809
  }
2767
2810
  });
@@ -2804,14 +2847,29 @@ var EditorController = class {
2804
2847
  this.editor.commands.setContent(html, emitUpdate);
2805
2848
  }
2806
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. */
2807
2860
  insertText(text) {
2808
- this.editor.commands.insertContent(text);
2861
+ const { from, to } = this.editor.state.selection;
2862
+ this.insertPlainText(from, to, text);
2809
2863
  }
2864
+ /** Inserts text literally at `pos`; HTML in `text` is not parsed. */
2810
2865
  insertTextAt(pos, text) {
2811
- this.editor.chain().focus().insertContentAt(pos, text).run();
2866
+ this.insertPlainText(pos, pos, text);
2867
+ this.editor.commands.focus();
2812
2868
  }
2869
+ /** Replaces a range with literal text; HTML in `text` is not parsed. */
2813
2870
  replaceRange(from, to, text) {
2814
- this.editor.chain().focus().deleteRange({ from, to }).insertContentAt(from, text).run();
2871
+ this.insertPlainText(from, to, text);
2872
+ this.editor.commands.focus();
2815
2873
  }
2816
2874
  getCurrentContentString() {
2817
2875
  if (this.plainTextMode) return this.getPlainText();
@@ -7025,6 +7083,8 @@ function hitTestAnnotation(imgPt, ann, padding = 10) {
7025
7083
  SmilieReplacer,
7026
7084
  SpreadsheetController,
7027
7085
  StreamingLayoutManager,
7086
+ TEMPLATE_DEPTH_ERROR,
7087
+ TEMPLATE_LOOP_ERROR,
7028
7088
  TEMPLATE_REGEX,
7029
7089
  TemplateSuggestions,
7030
7090
  VARIABLE_REGEX,
@@ -7070,6 +7130,7 @@ function hitTestAnnotation(imgPt, ann, padding = 10) {
7070
7130
  getEditorText,
7071
7131
  getLatestVersion,
7072
7132
  getLayoutedNodes,
7133
+ getTemplateBody,
7073
7134
  getViewportMetrics,
7074
7135
  globalImageCache,
7075
7136
  handleImageInsertion,