<commit_before>/**
 * Copyright (c) Tiny Technologies, Inc. All rights reserved.
 * Licensed under the LGPL or a commercial license.
 * For LGPL see License.txt in the project root for license information.
 * For commercial licenses see https://www.tiny.cloud/
 */

import { Element as DomElement, DocumentFragment, KeyboardEvent } from '@ephox/dom-globals';
import { Arr, Option, Obj } from '@ephox/katamari';
import { PredicateFilter, Element, Css, Node } from '@ephox/sugar';
import Settings from '../api/Settings';
import * as CaretContainer from '../caret/CaretContainer';
import NodeType from '../dom/NodeType';
import TreeWalker from '../api/dom/TreeWalker';
import InsertLi from './InsertLi';
import NewLineUtils from './NewLineUtils';
import NormalizeRange from '../selection/NormalizeRange';
import Zwsp from '../text/Zwsp';
import { isCaretNode } from '../fmt/FormatContainer';
import DOMUtils from '../api/dom/DOMUtils';
import Editor from '../api/Editor';
import { EditorEvent } from '../api/util/EventDispatcher';
import Bookmarks from '../bookmark/Bookmarks';

const trimZwsp = (fragment: DocumentFragment) => {
  Arr.each(PredicateFilter.descendants(Element.fromDom(fragment), Node.isText), (text) => {
    const rawNode = text.dom();
    rawNode.nodeValue = Zwsp.trim(rawNode.nodeValue);
  });
};

const isEmptyAnchor = function (dom: DOMUtils, elm: DomElement) {
  return elm && elm.nodeName === 'A' && dom.isEmpty(elm);
};

const isTableCell = function (node) {
  return node && /^(TD|TH|CAPTION)$/.test(node.nodeName);
};

const emptyBlock = function (elm) {
  elm.innerHTML = '<br data-mce-bogus="1">';
};

const containerAndSiblingName = function (container, nodeName) {
  return container.nodeName === nodeName || (container.previousSibling && container.previousSibling.nodeName === nodeName);
};

// Returns true if the block can be split into two blocks or not
const canSplitBlock = function (dom, node) {
  return node &&
    dom.isBlock(node) &&
    !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) &&
    !/^(fixed|absolute)/i.test(node.style.position) &&
    dom.getContentEditable(node) !== 'true';
};

// Remove the first empty inline element of the block so this: <p><b><em></em></b>x</p> becomes this: <p>x</p>
const trimInlineElementsOnLeftSideOfBlock = function (dom, nonEmptyElementsMap, block) {
  let node = block;
  const firstChilds = [];
  let i;

  if (!node) {
    return;
  }

  // Find inner most first child ex: <p><i><b>*</b></i></p>
  while ((node = node.firstChild)) {
    if (dom.isBlock(node)) {
      return;
    }

    if (NodeType.isElement(node) && !nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
      firstChilds.push(node);
    }
  }

  i = firstChilds.length;
  while (i--) {
    node = firstChilds[i];
    if (!node.hasChildNodes() || (node.firstChild === node.lastChild && node.firstChild.nodeValue === '')) {
      dom.remove(node);
    } else {
      if (isEmptyAnchor(dom, node)) {
        dom.remove(node);
      }
    }
  }
};

const normalizeZwspOffset = function (start, container, offset) {
  if (NodeType.isText(container) === false) {
    return offset;
  } else if (start) {
    return offset === 1 && container.data.charAt(offset - 1) === Zwsp.ZWSP ? 0 : offset;
  } else {
    return offset === container.data.length - 1 && container.data.charAt(offset) === Zwsp.ZWSP ? container.data.length : offset;
  }
};

const includeZwspInRange = function (rng) {
  const newRng = rng.cloneRange();
  newRng.setStart(rng.startContainer, normalizeZwspOffset(true, rng.startContainer, rng.startOffset));
  newRng.setEnd(rng.endContainer, normalizeZwspOffset(false, rng.endContainer, rng.endOffset));
  return newRng;
};

// Trims any linebreaks at the beginning of node user for example when pressing enter in a PRE element
const trimLeadingLineBreaks = function (node) {
  do {
    if (NodeType.isText(node)) {
      node.nodeValue = node.nodeValue.replace(/^[\r\n]+/, '');
    }

    node = node.firstChild;
  } while (node);
};

const getEditableRoot = function (dom, node) {
  const root = dom.getRoot();
  let parent, editableRoot;

  // Get all parents until we hit a non editable parent or the root
  parent = node;
  while (parent !== root && dom.getContentEditable(parent) !== 'false') {
    if (dom.getContentEditable(parent) === 'true') {
      editableRoot = parent;
    }

    parent = parent.parentNode;
  }

  return parent !== root ? editableRoot : root;
};

const applyAttributes = (editor: Editor, node: DomElement, forcedRootBlockAttrs: Record<string, string>) => {
  // Merge and apply style attribute
  Option.from(forcedRootBlockAttrs.style)
    .map(editor.dom.parseStyle)
    .each((attrStyles) => {
      const currentStyles = Css.getAllRaw(Element.fromDom(node));
      const newStyles = { ...currentStyles, ...attrStyles };
      editor.dom.setStyles(node, newStyles);
    });

  // Merge and apply class attribute
  Option.from(forcedRootBlockAttrs.class).map((attrClasses) => attrClasses.split(/\s+/)).each((attrClasses) => {
    Option.from(node.className)
      .map((currentClasses) => Arr.filter(currentClasses.split(/\s+/), (c) => c !== ''))
      .each((currentClasses) => {
        const filteredClasses = Arr.filter(currentClasses, (c) => !Arr.contains(attrClasses, c));
        const newClasses = [...attrClasses, ...filteredClasses];
        editor.dom.setAttrib(node, 'class', newClasses.join(' '));
      });
  });

  // Apply any remaining forced root block attributes
  const appliedAttrs = ['style', 'class'];
  const remainingAttrs = Obj.filter(forcedRootBlockAttrs, (_, attrs) => !Arr.contains(appliedAttrs, attrs));
  editor.dom.setAttribs(node, remainingAttrs);
};

const setForcedBlockAttrs = function (editor: Editor, node) {
  const forcedRootBlockName = Settings.getForcedRootBlock(editor);

  if (forcedRootBlockName && forcedRootBlockName.toLowerCase() === node.tagName.toLowerCase()) {
    const forcedRootBlockAttrs = Settings.getForcedRootBlockAttrs(editor);
    applyAttributes(editor, node, forcedRootBlockAttrs);
  }
};

// Wraps any text nodes or inline elements in the specified forced root block name
const wrapSelfAndSiblingsInDefaultBlock = function (editor: Editor, newBlockName, rng, container, offset) {
  let newBlock, parentBlock, startNode, node, next, rootBlockName;
  const blockName = newBlockName || 'P';
  const dom = editor.dom, editableRoot = getEditableRoot(dom, container);

  // Not in a block element or in a table cell or caption
  parentBlock = dom.getParent(container, dom.isBlock);
  if (!parentBlock || !canSplitBlock(dom, parentBlock)) {<commit_msg>TINY-4564: Refactor code using lift2<commit_after>/**
 * Copyright (c) Tiny Technologies, Inc. All rights reserved.
 * Licensed under the LGPL or a commercial license.
 * For LGPL see License.txt in the project root for license information.
 * For commercial licenses see https://www.tiny.cloud/
 */

import { Element as DomElement, DocumentFragment, KeyboardEvent } from '@ephox/dom-globals';
import { Arr, Option, Obj, Options } from '@ephox/katamari';
import { PredicateFilter, Element, Css, Node } from '@ephox/sugar';
import Settings from '../api/Settings';
import * as CaretContainer from '../caret/CaretContainer';
import NodeType from '../dom/NodeType';
import TreeWalker from '../api/dom/TreeWalker';
import InsertLi from './InsertLi';
import NewLineUtils from './NewLineUtils';
import NormalizeRange from '../selection/NormalizeRange';
import Zwsp from '../text/Zwsp';
import { isCaretNode } from '../fmt/FormatContainer';
import DOMUtils from '../api/dom/DOMUtils';
import Editor from '../api/Editor';
import { EditorEvent } from '../api/util/EventDispatcher';
import Bookmarks from '../bookmark/Bookmarks';

const trimZwsp = (fragment: DocumentFragment) => {
  Arr.each(PredicateFilter.descendants(Element.fromDom(fragment), Node.isText), (text) => {
    const rawNode = text.dom();
    rawNode.nodeValue = Zwsp.trim(rawNode.nodeValue);
  });
};

const isEmptyAnchor = function (dom: DOMUtils, elm: DomElement) {
  return elm && elm.nodeName === 'A' && dom.isEmpty(elm);
};

const isTableCell = function (node) {
  return node && /^(TD|TH|CAPTION)$/.test(node.nodeName);
};

const emptyBlock = function (elm) {
  elm.innerHTML = '<br data-mce-bogus="1">';
};

const containerAndSiblingName = function (container, nodeName) {
  return container.nodeName === nodeName || (container.previousSibling && container.previousSibling.nodeName === nodeName);
};

// Returns true if the block can be split into two blocks or not
const canSplitBlock = function (dom, node) {
  return node &&
    dom.isBlock(node) &&
    !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) &&
    !/^(fixed|absolute)/i.test(node.style.position) &&
    dom.getContentEditable(node) !== 'true';
};

// Remove the first empty inline element of the block so this: <p><b><em></em></b>x</p> becomes this: <p>x</p>
const trimInlineElementsOnLeftSideOfBlock = function (dom, nonEmptyElementsMap, block) {
  let node = block;
  const firstChilds = [];
  let i;

  if (!node) {
    return;
  }

  // Find inner most first child ex: <p><i><b>*</b></i></p>
  while ((node = node.firstChild)) {
    if (dom.isBlock(node)) {
      return;
    }

    if (NodeType.isElement(node) && !nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
      firstChilds.push(node);
    }
  }

  i = firstChilds.length;
  while (i--) {
    node = firstChilds[i];
    if (!node.hasChildNodes() || (node.firstChild === node.lastChild && node.firstChild.nodeValue === '')) {
      dom.remove(node);
    } else {
      if (isEmptyAnchor(dom, node)) {
        dom.remove(node);
      }
    }
  }
};

const normalizeZwspOffset = function (start, container, offset) {
  if (NodeType.isText(container) === false) {
    return offset;
  } else if (start) {
    return offset === 1 && container.data.charAt(offset - 1) === Zwsp.ZWSP ? 0 : offset;
  } else {
    return offset === container.data.length - 1 && container.data.charAt(offset) === Zwsp.ZWSP ? container.data.length : offset;
  }
};

const includeZwspInRange = function (rng) {
  const newRng = rng.cloneRange();
  newRng.setStart(rng.startContainer, normalizeZwspOffset(true, rng.startContainer, rng.startOffset));
  newRng.setEnd(rng.endContainer, normalizeZwspOffset(false, rng.endContainer, rng.endOffset));
  return newRng;
};

// Trims any linebreaks at the beginning of node user for example when pressing enter in a PRE element
const trimLeadingLineBreaks = function (node) {
  do {
    if (NodeType.isText(node)) {
      node.nodeValue = node.nodeValue.replace(/^[\r\n]+/, '');
    }

    node = node.firstChild;
  } while (node);
};

const getEditableRoot = function (dom, node) {
  const root = dom.getRoot();
  let parent, editableRoot;

  // Get all parents until we hit a non editable parent or the root
  parent = node;
  while (parent !== root && dom.getContentEditable(parent) !== 'false') {
    if (dom.getContentEditable(parent) === 'true') {
      editableRoot = parent;
    }

    parent = parent.parentNode;
  }

  return parent !== root ? editableRoot : root;
};

const applyAttributes = (editor: Editor, node: DomElement, forcedRootBlockAttrs: Record<string, string>) => {
  // Merge and apply style attribute
  Option.from(forcedRootBlockAttrs.style)
    .map(editor.dom.parseStyle)
    .each((attrStyles) => {
      const currentStyles = Css.getAllRaw(Element.fromDom(node));
      const newStyles = { ...currentStyles, ...attrStyles };
      editor.dom.setStyles(node, newStyles);
    });

  // Merge and apply class attribute
  const attrClasses = Option.from(forcedRootBlockAttrs.class).map((attrC) => attrC.split(/\s+/));
  const currentClasses = Option.from(node.className).map((currentC) => Arr.filter(currentC.split(/\s+/), (c) => c !== ''));
  Options.lift2(attrClasses, currentClasses, (attrC, currentC) => {
    const filteredClasses = Arr.filter(currentC, (c) => !Arr.contains(attrC, c));
    const newClasses = [...attrC, ...filteredClasses];
    editor.dom.setAttrib(node, 'class', newClasses.join(' '));
  });

  // Apply any remaining forced root block attributes
  const appliedAttrs = ['style', 'class'];
  const remainingAttrs = Obj.filter(forcedRootBlockAttrs, (_, attrs) => !Arr.contains(appliedAttrs, attrs));
  editor.dom.setAttribs(node, remainingAttrs);
};

const setForcedBlockAttrs = function (editor: Editor, node) {
  const forcedRootBlockName = Settings.getForcedRootBlock(editor);

  if (forcedRootBlockName && forcedRootBlockName.toLowerCase() === node.tagName.toLowerCase()) {
    const forcedRootBlockAttrs = Settings.getForcedRootBlockAttrs(editor);
    applyAttributes(editor, node, forcedRootBlockAttrs);
  }
};

// Wraps any text nodes or inline elements in the specified forced root block name
const wrapSelfAndSiblingsInDefaultBlock = function (editor: Editor, newBlockName, rng, container, offset) {
  let newBlock, parentBlock, startNode, node, next, rootBlockName;
  const blockName = newBlockName || 'P';
  const dom = editor.dom, editableRoot = getEditableRoot(dom, container);

  // Not in a block element or in a table cell or caption
  parentBlock = dom.getParent(container, dom.isBlock);
  if (!parentBlock || !canSplitBlock(dom, parentBlock)) {
{'fraction_non_alphanumeric': 0.1483050847457627, 'fraction_numerical': 0.0012106537530266344, 'mean_word_length': 4.065925642008432, 'pattern_counts': {'":': 0, '<': 35, '<?xml version=': 0, '>': 56, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3398181', 'n_tokens_mistral': 4157, 'n_tokens_neox': 4151, 'n_words': 1045}