"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = consolidateListNodes;

var _isOrderedListNode = _interopRequireDefault(require("./isOrderedListNode"));

var _isListNode = _interopRequireDefault(require("./isListNode"));

var _prosemirrorModel = require("prosemirror-model");

var _prosemirrorTransform = require("prosemirror-transform");

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; }

function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

// This function consolidates list nodes among the same "Lists Island".
//
// ## Definition of a "Lists Island"
//   Separate list that are adjacent to each other are grouped as
//   a "Lists Island".
//   For example, the following HTML snippets contains two "Lists Island":
//
//     <h1>text</h1>
//     <!-- Lists Island Starts -->
//     <ul><li>text</li><li>text</li></ul>
//     <ol><li>text</li><li>text</li></ul>
//     <!-- Lists Island Ends -->
//     <p>text</p>
//     <!-- Lists Island Starts -->
//     <ul><li>text</li><li>text</li></ul>
//     <ol><li>text</li><li>text</li></ul>
//     <!-- Lists Island Ends -->
//     <p>text</p>
//
// List nodes with the same list type and indent level among the same Lists
// Island will be joined into one list node.
// Note that this transform may change the current user selection.
function consolidateListNodes(tr) {
  if (tr.getMeta('dryrun')) {
    // This transform is potentially expensive to perform, so skip it if
    // the transform is performed as "dryrun".
    return tr;
  }

  let prevJointInfo; // Keep the loop running until there's no more list nodes that can be joined.

  while (true) {
    const jointInfo = traverseDocAndFindJointInfo(tr.doc, prevJointInfo);

    if (jointInfo) {
      const {
        deleteFrom,
        deleteTo,
        insertAt,
        content
      } = jointInfo;
      tr = tr.delete(deleteFrom, deleteTo);
      tr = tr.insert(insertAt, content);
      prevJointInfo = jointInfo;
    } else {
      tr = linkOrderedListCounters(tr);
      break;
    }
  }

  return tr;
}
/**
 * This ensures that ordered lists with the same indent level among the same
 * Lists Island share the same counter.
 *
 * For example, the following three lists:
 *   --------
 *   1. AAA
 *   2. BBB
 *   --------
 *     a. CCC
 *     d. DDD
 *   --------
 *   1. EEE
 *   2. FFF
 *   --------
 * Will transform into
 *   --------
 *   1. AAA
 *   2. BBB
 *   --------
 *     a. CCC
 *     d. DDD
 *   --------
 *   3. EEE
 *   4. FFF
 *   --------
 * This means that the 1st and the 3rd lists are linked.
 */


function linkOrderedListCounters(tr) {
  const from = 1;
  const to = tr.doc.nodeSize - 2;

  if (from >= to) {
    return tr;
  }

  const namedLists = new Set();
  let listsBefore = null;
  tr.doc.nodesBetween(from, to, (node, pos, parentNode) => {
    let willTraverseNodeChildren = true;

    if ((0, _isListNode.default)(node)) {
      // List Node can't be nested, no need to traverse its children.
      willTraverseNodeChildren = false;
      const indent = node.attrs.indent || 0;
      const start = node.attrs.start || 1;
      const {
        name,
        following
      } = node.attrs;

      if (name) {
        namedLists.add(name);
      }

      if (listsBefore) {
        if (start === 1 && (0, _isOrderedListNode.default)(node)) {
          // Look backward until we could find another ordered list node to
          // link with.
          let counterIsLinked;
          listsBefore.some((list, index) => {
            if (list.node.type !== node.type && list.indent === indent) {
              // This encounters different type of list node (e.g a bullet
              // list node), we need to restart the counter.
              // ------
              // 1. AAA
              // 2. BBB
              // ------
              // -. CCC
              // -. DDD
              // ------
              // 1. DDD <- Counter restarts here.
              // 2. EEE
              // ------
              counterIsLinked = false;
              return true;
            } else if (list.indent < indent) {
              // This encounters an ordered list node that has less indent.
              // we need to restart the counter.
              // ------
              // 1. AAA
              // 2. BBB
              // ------
              //   1. DDD <- Counter restarts here.
              //   2. EEE
              // ------
              counterIsLinked = false;
              return true;
            } else if (list.indent === indent) {
              // This encounters an ordered list node that has same indent.
              // Do not Restart the counter.
              // ------
              // 1. AAA
              // 2. BBB
              // ------
              // 3. DDD <- Counter continues here.
              // 4. EEE
              // ------
              counterIsLinked = true;
              return true;
            }

            return false;
          });

          if (counterIsLinked !== undefined) {
            tr = setCounterLinked(tr, pos, counterIsLinked);
          }
        }
      } else {
        // Found the first list among a new Lists Island.
        // ------
        // 1. AAA <- Counter restarts here.
        // 2. BBB
        listsBefore = [];

        if ((0, _isOrderedListNode.default)(node)) {
          // The list may follow a previous list that is among another Lists
          // Island. If so, do not reset the list counter.
          const counterIsLinked = namedLists.has(following);
          tr = setCounterLinked(tr, pos, counterIsLinked);
        }
      }

      listsBefore.unshift({
        parentNode,
        indent,
        node
      });
    } else {
      // Not traversing within any list node. No lists need to be updated.
      listsBefore = null;
    }

    return willTraverseNodeChildren;
  });
  return tr;
}

function setCounterLinked(tr, pos, linked) {
  const node = tr.doc.nodeAt(pos);
  const currentValue = node.attrs.counterReset || null;
  const nextValue = linked ? 'none' : null;

  if (nextValue !== currentValue) {
    const nodeAttrs = _objectSpread(_objectSpread({}, node.attrs), {}, {
      counterReset: nextValue
    });

    tr = tr.setNodeMarkup(pos, node.type, nodeAttrs, node.marks);
  }

  return tr;
}

function traverseDocAndFindJointInfo(doc, prevJointInfo) {
  const minFrom = 1;
  const from = prevJointInfo ? Math.max(minFrom, prevJointInfo.firstListNodePos) : minFrom;
  const to = doc.nodeSize - 2;

  if (to <= from) {
    return null;
  }

  let prevNode = null;
  let jointInfo = null;
  let firstListNodePos = 0; // Perform the breadth-first traversal.

  doc.nodesBetween(from, to, (node, pos) => {
    if (jointInfo) {
      // We've found the list to merge. Stop traversing deeper.
      return false;
    } else if ((0, _isListNode.default)(node)) {
      firstListNodePos = firstListNodePos === 0 ? pos : firstListNodePos;
      jointInfo = resolveJointInfo(node, pos, prevNode, firstListNodePos);
      prevNode = node; // Stop the traversing recursively inside the this list node because
      // its content only contains inline nodes.

      return false;
    } else {
      prevNode = node; // This is not a list node, will keep traversing deeper until we've found
      // a list node or reach the leaf node.

      return true;
    }
  });

  if (jointInfo) {
    // Reduce the range of the next traversal so it could run faster.
    jointInfo.firstListNodePos = firstListNodePos;
  }

  return jointInfo;
} // If two siblings nodes that can be joined as single list, returns
// the information of how to join them.


function resolveJointInfo(node, pos, prevNode, firstListNodePos) {
  if (!prevNode || !canJoinListNodes(node, prevNode)) {
    return null;
  }

  return {
    deleteFrom: pos,
    deleteTo: pos + node.nodeSize,
    insertAt: pos - 1,
    content: node.content,
    firstListNodePos
  };
}

function canJoinListNodes(one, two) {
  return !!(one.type === two.type && one.attrs.indent === two.attrs.indent && (0, _isListNode.default)(one) && (0, _isListNode.default)(two));
}