Skip to content

Instantly share code, notes, and snippets.

@sb8244
Created August 5, 2024 06:40
Show Gist options
  • Save sb8244/c59acf0836eda8c79852e623afe936d1 to your computer and use it in GitHub Desktop.
Save sb8244/c59acf0836eda8c79852e623afe936d1 to your computer and use it in GitHub Desktop.
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
}
@sb8244
Copy link
Author

sb8244 commented Mar 7, 2025

Used like:

defaultBlockSpecs.numberedListItem.implementation.node.config.addProseMirrorPlugins = () => {
  return [
    NumberedListIndexingPlugin({
      getListCharacter
    })
  ]
}



export function getListCharacter(positionDetails: { depth: number; index: number }) {
  const style = (positionDetails.depth - 1) % 3

  if (style === 1) {
    return getColumnLetters(positionDetails.index)
  } else if (style === 2) {
    return romanize(positionDetails.index).toLowerCase()
  } else {
    return `${positionDetails.index}`
  }
}

// prettier-ignore
const ALPHABET = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']

function getColumnLetters(columnIndexStartFromOne: number): string {
  if (columnIndexStartFromOne < 27) {
    return ALPHABET[columnIndexStartFromOne - 1]
  } else {
    var res = columnIndexStartFromOne % 26
    var div = Math.floor(columnIndexStartFromOne / 26)
    if (res === 0) {
      div = div - 1
      res = 26
    }
    return getColumnLetters(div) + ALPHABET[res - 1]
  }
}

function romanize(num: number): string {
  const digits = `${num}`.split("")
  // prettier-ignore
  const key = ["","C","CC","CCC","CD","D","DC","DCC","DCCC","CM",
             "","X","XX","XXX","XL","L","LX","LXX","LXXX","XC",
             "","I","II","III","IV","V","VI","VII","VIII","IX"]
  let roman = ""
  let i = 3
  while (i--) roman = (key[+digits.pop()! + i * 10] || "") + roman
  return Array(+digits.join("") + 1).join("M") + roman
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment