All files / mutxt-react/src/MuTxt/behavior embed.ts

85% Statements 85/100
69.38% Branches 34/49
100% Functions 17/17
93.24% Lines 69/74

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 1122x 2x     2x 16x   2x 4x 4x 4x 4x 4x 4x 3x           2x 2x 2x     2x 1x 1x         1x 1x 1x     4x         2x 7x 7x 7x   14x   7x     3x   2x 4x 4x 4x 4x 4x 4x 4x     2x 1x 1x 1x 1x 1x 1x 1x 1x   1x     2x 1x 1x 1x 1x 1x     2x 1x 1x 1x     2x 8x 13x 8x 1x     8x 1x     8x 1x 1x 1x       8x    
import {Editor, Element as SlateElement, Node, Path, Transforms} from 'slate';
import {insertVoidBlock} from './voidInsert';
import type {CustomElement, CustomText, EmbedElement} from '../types';
 
export const isEmbedElement = (node: unknown): node is EmbedElement =>
  SlateElement.isElement(node) && node.type === 'embed';
 
export const normalizeEmbedUrl = (href: string): string => {
  const value = href.trim();
  Iif (!value) return '';
  const nextValue = /^[a-z][a-z0-9+.-]*:/i.test(value) ? value : `https://${value}`;
  try {
    const url = new URL(nextValue);
    if (url.protocol !== 'http:' && url.protocol !== 'https:') return '';
    return url.toString();
  } catch {
    return '';
  }
};
 
const normalizeCaption = (caption?: string): string | undefined => {
  const value = caption?.trim();
  return value ? value : undefined;
};
 
const createEmbedElement = (url: string, caption?: string): EmbedElement => {
  const children: CustomText[] = [{text: ''}];
  const element: EmbedElement = {
    type: 'embed',
    url,
    children,
  };
  const nextCaption = normalizeCaption(caption);
  Iif (nextCaption) element.caption = nextCaption;
  return element;
};
 
const createParagraphElement = (): CustomElement => ({
  type: 'p',
  children: [{text: ''}],
});
 
export const getActiveEmbedEntry = (editor: Editor): [EmbedElement, Path] | null => {
  const {selection} = editor;
  Iif (!selection) return null;
  const match = Editor.above(editor, {
    at: Editor.unhangRange(editor, selection),
    match: (node) => isEmbedElement(node),
  });
  return (match as [EmbedElement, Path] | undefined) ?? null;
};
 
export const getActiveEmbed = (editor: Editor): EmbedElement | null => getActiveEmbedEntry(editor)?.[0] ?? null;
 
export const insertParagraphNearActiveEmbed = (editor: Editor, position: 'above' | 'below' = 'below'): Path | null => {
  const entry = getActiveEmbedEntry(editor);
  Iif (!entry) return null;
  const [, path] = entry;
  const targetPath = position === 'above' ? path : Path.next(path);
  Transforms.insertNodes(editor, createParagraphElement(), {at: targetPath});
  Transforms.select(editor, Editor.start(editor, targetPath));
  return targetPath;
};
 
export const updateEmbedAtPath = (editor: Editor, path: Path, url: string, caption?: string): boolean => {
  const normalizedUrl = normalizeEmbedUrl(url);
  Iif (!normalizedUrl) return false;
  Iif (!Node.has(editor, path)) return false;
  const node = Node.get(editor, path);
  Iif (!isEmbedElement(node)) return false;
  Transforms.setNodes(editor, {url: normalizedUrl} as Partial<EmbedElement>, {at: path});
  const nextCaption = normalizeCaption(caption);
  if (nextCaption) Transforms.setNodes(editor, {caption: nextCaption} as Partial<EmbedElement>, {at: path});
  else ETransforms.unsetNodes(editor, 'caption', {at: path});
  return true;
};
 
export const removeEmbedAtPath = (editor: Editor, path: Path): boolean => {
  Iif (!Node.has(editor, path)) return false;
  const node = Node.get(editor, path);
  Iif (!isEmbedElement(node)) return false;
  Transforms.removeNodes(editor, {at: path});
  return true;
};
 
export const insertEmbed = (editor: Editor, url: string, caption?: string): EmbedElement | null => {
  const normalizedUrl = normalizeEmbedUrl(url);
  Iif (!normalizedUrl) return null;
  return insertVoidBlock(editor, createEmbedElement(normalizedUrl, caption));
};
 
export const withEmbeds = <T extends Editor>(editor: T): T => {
  const {isVoid, insertBreak, insertSoftBreak, insertText} = editor;
  editor.isVoid = (element) => (element.type === 'embed' ? true : isVoid(element));
  editor.insertBreak = () => {
    Eif (insertParagraphNearActiveEmbed(editor, 'below')) return;
    insertBreak();
  };
  editor.insertSoftBreak = () => {
    Eif (insertParagraphNearActiveEmbed(editor, 'above')) return;
    insertSoftBreak();
  };
  editor.insertText = (text) => {
    Eif (text && insertParagraphNearActiveEmbed(editor, 'below')) {
      insertText(text);
      return;
    }
    insertText(text);
  };
  return editor;
};