text: /**
   *
   * add, remove className on Range
   * 给range添加、删除指定的类名
   *
   */
  
  'use strict'
  import core from '@/core';
  import { getPrecedingMrTextNode, getNextMrTextNode } from './createAdjacentMergeableTextNodeGetter';
  import Merge from './merge';
  import splitNodeAt from './splitNodeAt';
  
  export default class Applier {
  
    constructor(className = 'highlight', options = {}) {
      this.className = className;
      this.tagName = options.tagName?.toLowerCase() || 'span';
      this.removeEmptyElement = options.removeEmptyElement ?? false;
      this.elAttrs = options.elAttrs || {};
      this.elProps = options.elProps || {};
      this.containerElement = options.containerElement || document.body;
      this.onApplied = function () {};
      if (options.onApplied && typeof options.onApplied === 'function') {
        this.onApplied = options.onApplied;
      }
    }
  
  
    /**
     * applies to then range
     *
     * @param {Range} range
     */
    applies (range) {
      // 获取字符范围
      const characterRange = range.getBookmark(this.containerElement);
  
      // 分割边界
      range.splitBoundaries();
  
      // 从range上获取所有的有效文本节点
      const textNodes = getEffectiveTextNodes(range);
  
      if (textNodes.length) {
        textNodes.forEach(textNode => {
          if (!core.dom.getSelfOrAncestorWithClass(textNode, this.className) && !isWhiteSpaceTextNode(textNode)) {
            this.appliesText(textNode);
          }
        });
  
        const lastTextNode = textNodes[textNodes.length - 1];
        range.setStartAndEnd(textNodes[0], 0, lastTextNode, lastTextNode.length);
  
        normalize(textNodes, range, false);
      }
      // dom change
      // 从characterRange更新range
      range.moveToBookmark(characterRange);
    }
  
    /**
     * undo applies to range
     *
     * @param {Range} range
     */
    unApplies (range) {
      const characterRange = range.getBookmark(this.containerElement);
  
      range.splitBoundaries();
  
      const textNodes = getEffectiveTextNodes(range);
  
      if (textNodes.length) {
        // split boundaries ancestor with class
        splitAncestorWithClass(range, this.className);
        textNodes.forEach(textNode => {
          let ancestorWithClass = core.dom.getSelfOrAncestorWithClass(textNode, this.className);
          if (ancestorWithClass) {
            undoToAncestor(ancestorWithClass, this.className);
          }
        });
  
        normalize(textNodes, range, true);
      }
  
      range.moveToBookmark(characterRange);
    }
  
    /**
     * is applied to range
     * @param {Range} range
     * @return {boolean}
     */
    isApplied (range) {
      if (range.collapsed && range.toString() === '') {
        return !!core.dom.getSelfOrAncestorWithClass(range.commonAncestorContainer, this.className);
      } else {
        const textNodes = getEffectiveTextNodes(range);
        for (let i = 0, textNode; (textNode = textNodes[i]); ++i) {
          if (!core.dom.getSelfOrAncestorWithClass(textNode, this.className) && !isWhiteSpaceTextNode(textNode)) {
            return false;
          }
        }
        return true;
      }
    }
  
    /**
     * applies to then textNode
     *
     * @param {Node} textNode
     */
    appliesText (textNode) {
      const parentNode = textNode.parentNode;
      if (textNode.nodeType === Node.TEXT_NODE) {
        if (parentNode.childNodes.length === 1
          && parentNode.nodeName.toLowerCase() === this.tagName
          && elementHasProps(parentNode, this.elProps)
          && elementHasAttrs(parentNode, this.elAttrs)
        ) {
          core.dom.addClass(parentNode, this.className);
        } else {
          const el = this.createContainer();
          parentNode.insertBefore(el, textNode);
          el.appendChild(textNode);
          this.onApplied(el, this);
        }
      }
    }
  
    /**
     * create surround container
     * @return {HTMLElement}
     */
    createContainer () {
      const el = document.createElement(this.tagName);
      core.dom.addClass(el, this.className);
      mapAttrsToElement(this.elAttrs, el);
      mapPropsToElement(this.elProps, el);
      return el;
    }
  }
  
  /**
   *
   * @param {Node} ancestorWithClass
   * @param {string} className
   * @return {Node[]}
   */
  function undoToAncestor (ancestorWithClass, className) {
    if (isRemovable(ancestorWithClass, className)) {
      let child, children = [],
        parentNode = ancestorWithClass.parentNode,
        index = core.dom.getNodeIndex(ancestorWithClass);
  
      while ((child = ancestorWithClass.firstChild)) {
        // move children to sibling
        core.dom.moveNode(child, parentNode, index++);
        children.push(child);
      }
  
      // remove self
      core.dom.removeNode(ancestorWithClass);
  
      return children;
    } else {
      core.dom.removeClass(ancestorWithClass, className);
    }
  }
  
  /**
   *
   * @param {HTMLElement | Node} ancestorWithClass
   * @param {string} className
   * @return {boolean}
   */
  function isRemovable (ancestorWithClass, className) {
    return core.dom.getClass(ancestorWithClass) === className;
  }
  
  /**
   *
   * @param {Range} range
   * @param {string} className
   */
  function splitAncestorWithClass (range, className) {
    [{ node: range.endContainer, offset: range.endOffset }, { node: range.startContainer, offset: range.startOffset }]
      .forEach(({ node, offset }) => {
        const ancestorWithClass = core.dom.getSelfOrAncestorWithClass(node, className);
        if (ancestorWithClass) {
          splitNodeAt(ancestorWithClass, node, offset);
        }
      });
  }
  
  function hasAllClass (el, className) {
    const classNames = core.dom.classesToArray(className);
    let i = 0, len = classNames.length;
    for (i; i < len; ++i) {
      if (!core.dom.hasClass(el, classNames[i])) {
        return false;
      }
    }
  
    return true;
  }
  
  /**
   *
   * @param {HTMLElement | Node} el
   * @param {Object} props
   * @return {boolean}
   */
  function elementHasProps (el, props) {
    return each(props, function (p, propValue) {
      if (p === 'className') {
        return hasAllClass(el, propValue);
      } else if (core.utils.toType(propValue) === 'object') {
        return elementHasProps(el, propValue);
      } else if (el[p] !== propValue) {
        return false;
      }
    });
  }
  
  /**
   *
   * @param {HTMLElement | Node} el
   * @param {Object} attrs
   * @return {boolean}
   */
  function elementHasAttrs (el, attrs) {
    return each(attrs, function (p, propValue) {
      if (el.getAttribute(p) !== propValue) {
        return false;
      }
    });
  }
  
  function each (obj, callback) {
    let flag = true;
    core.each(obj, function (p, propValue) {
      if (callback(p, propValue) === false) {
        flag = false;
        return flag;
      }
    });
  
    return flag;
  }
  
  /**
   *
   * @param {Object} attrs
   * @param {HTMLElement | Node} el
   */
  function mapAttrsToElement (attrs, el) {
    core.each(attrs, function (attrName, attrValue) {
      if (Object.hasOwn(attrs, attrName) && !/^class(?:Name)?$/i.test(attrName)) {
        el.setAttribute(attrName, attrValue);
      }
    });
  }
  
  /**
   *
   * @param {Object} props
   * @param {HTMLElement | Node} el
   */
  function mapPropsToElement (props, el) {
    core.each(props, function (propName, propValue) {
      if (Object.hasOwn(props, propName)) {
        if (propName === 'className') {
          core.dom.addClass(el, propValue);
        } else {
          el[propName] = propValue;
        }
      }
    });
  }
  
  /**
   * 如果textNode是空白字符, 返回true, 否则false
   * @param {Text} textNode
   * @return {boolean}
   */
  function isWhiteSpaceTextNode (textNode) {
    return textNode && textNode.nodeType === Text.TEXT_NODE && core.utils.stripAndCollapse(textNode.data) === '';
  }
  
  /**
   * @see https://developer.mozilla.org/en-US/docs/Web/API/Node/normalize
   * 规范: range中没有相邻的applier节点
   * @param {Text[]} textNodes - 一个有序的文本节点队列, 这通常应该是从range中遍历出来的.
   * @param {Range} range - Range对象
   * @param {boolean} isUndo
   */
  function normalize (textNodes, range, isUndo) {
    let firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
  
    let currentMerge = null, merges = [];
  
    let rangeStartNode = firstNode, rangeEndNode = lastNode;
    let rangeStartOffset = 0, rangeEndOffset = lastNode.length;
  
    textNodes.forEach(function (textNode) {
      const precedingNode = getPrecedingMrTextNode(textNode, !isUndo);
      // 遍历每一个textNode, 找到他们的前面的可合并节点,
      if (precedingNode) {
        // 以precedingNode为首创建merge对象
        if (currentMerge == null) {
          currentMerge = new Merge(precedingNode);
          merges.push(currentMerge);
        }
        currentMerge.textNodes.push(textNode);
  
        if (rangeStartNode === textNode) {
          rangeStartNode = currentMerge.textNodes[0];
          rangeStartOffset = rangeStartNode.length;
        }
  
        if (rangeEndNode === textNode) {
          rangeEndNode = currentMerge.textNodes[0];
          rangeEndOffset = currentMerge.getLength();
        }
  
      } else {
        // 重置当前merge对象, 建立新的合并
        currentMerge = null;
      }
    });
  
    const nextNode = getNextMrTextNode(lastNode, !isUndo);
    if (nextNode) {
      if (currentMerge == null) {
        currentMerge = new Merge(lastNode);
        merges.push(currentMerge);
      }
      currentMerge.textNodes.push(nextNode);
    }
  
    if (merges.length) {
      merges.forEach(merge => merge.start());
  
      range.setStartAndEnd(rangeStartNode, rangeStartOffset, rangeEndNode, rangeEndOffset);
    }
  
  }
  
  /**
   * range (prev/next) point (last/first)
   * @param {Range} range
   * @return {Text[]}
   */
  function getEffectiveTextNodes (range) {
  
    /** @type {Text[]} */
    const textNodes = range.getNodes(NodeFilter.SHOW_TEXT);
    let start = 0, end = textNodes.length, node;
  
    // 从左往右排除不被包含在range中的textNode
    while ((node = textNodes[start]) && !rangeSelectsAnyText(range, node)) {
      ++start;
    }
  
    // 从右往左排除不被包含在range中的textNode
    while ((node = textNodes[end - 1]) && !rangeSelectsAnyText(range, node)) {
      --end;
    }
  
    return textNodes.slice(start, end);
  }
  
  /**
   * 如果textNode被包含在range内，返回true，否则返回false
   * @param {Range} range
   * @param {Text} textNode
   * @return {boolean}
   */
  function rangeSelectsAnyText(range, textNode) {
    const textNodeRange = document.createRange();
    textNodeRange.selectNodeContents(textNode);
    const intersectionRange = textNodeRange.intersectionRange(range);
    const text = intersectionRange ? intersectionRange.toString() : '';
    return text !== '';
  }
  