import { Plugin, PluginKey, Transaction } from "prosemirror-state" import { BlockInfo, getBlockInfoFromPos } from "../helpers/getBlockInfoFromPos" // ProseMirror Plugin which automatically assigns indices to ordered list items per nesting level. const PLUGIN_KEY = new PluginKey(`numbered-list-indexing`) interface Options { getListCharacter?: (positionDetails: { depth: number; index: number }) => string } const defaultGetListCharacter = (position: { index: number }) => position.index.toString() export const NumberedListIndexingPlugin = (opts: Options = {}) => { const getListCharacter = opts.getListCharacter || defaultGetListCharacter return new Plugin({ key: PLUGIN_KEY, appendTransaction: (_transactions, _oldState, newState) => { const tr = newState.tr tr.setMeta("numberedListIndexing", true) let modified = false // Traverses each node the doc using DFS, so blocks which are on the same nesting level will be traversed in the // same order they appear. This means the index of each list item block can be calculated by incrementing the // index of the previous list item block. newState.doc.descendants((node, pos) => { if (node.type.name === "blockContainer" && node.firstChild!.type.name === "numberedListItem") { const blockInfo = getBlockInfoFromPos(tr.doc, pos + 1)! if (blockInfo === undefined) { return } let firstListBlock = blockInfo let blockIndex = 1 const parentBlock = findParentBlockOrSelf(firstListBlock.startPos, blockInfo, tr.doc) // Divide by 2 because each block has a nesting around it const depth = (blockInfo.depth - parentBlock.depth) / 2 + 1 while (firstListBlock) { const prevBlockInfo = getBlockInfoFromPos(tr.doc, firstListBlock.startPos - 2)! if ( prevBlockInfo && prevBlockInfo.id !== firstListBlock.id && prevBlockInfo.depth === firstListBlock.depth && prevBlockInfo.contentType === firstListBlock.contentType ) { blockIndex++ firstListBlock = prevBlockInfo } else { break } } const newIndex = getListCharacter({ depth, index: blockIndex }) const contentNode = blockInfo.contentNode const index = contentNode.attrs["index"] if (index !== newIndex) { modified = true tr.setNodeMarkup(pos + 1, undefined, { index: newIndex }) } } }) return modified ? tr : null } }) } function findParentBlockOrSelf(startPos: number, blockInfo: BlockInfo, doc: Transaction["doc"]) { let currIndex = startPos let parentBlock = blockInfo while (currIndex >= 0) { currIndex -= 2 const maybeParent = getBlockInfoFromPos(doc, currIndex)! const isDeeper = maybeParent.depth < parentBlock.depth const isSameType = maybeParent.contentType === blockInfo.contentType if (isDeeper && !isSameType) { break } else if (isDeeper && isSameType) { // If the block depth is less than the current block, it must be the next parent parentBlock = maybeParent } } return parentBlock }