My editor provides footnotes, in a way similar to the footnotes example in Prosemirror documentation.
When you select a text around the footnote marker and toggle a Mark, say bold, the Mark gets toggled not only on the selected span of the main text, but all over the footnote content as well. Luckily you can check that even in the footnotes example.
I wrote a toggleMark variant that can leave the footnote’s content untouched.
That behavior is controlled through two new options in the third argument of toggleMark:
- 
excludeAtomsContent: exclude only the content of atoms, while the atoms themselves get theMarktoggled (in the example above, the note marker would be bold, while the note text would not)
- 
excludeAtomsWithContent: exclude whole atoms that have a content (atoms that accept no content –isLeaf === true– are not excluded)
Here’s the code:
export function toggleMark(markType: MarkType, attrs: Attrs | null = null, options?: {
  /// Controls whether, when part of the selected range has the mark
  /// already and part does not, the mark is removed (`true`, the
  /// default) or added (`false`).
  removeWhenPresent: boolean,
  /// Ignore the content of selected atoms
  /// (Atoms get the mark toggled, their content does not).
  excludeAtomsContent?: boolean,
  /// Ignore selected atoms that have content.
  excludeAtomsWithContent?: boolean
}): Command {
  let removeWhenPresent = (options && options.removeWhenPresent) !== false
  return function (state, dispatch) {
    if (!markType) return false
    let { empty, $cursor, ranges } = state.selection as TextSelection
    if ((empty && !$cursor) || !markApplies(state.doc, ranges, markType)) return false
    if (dispatch) {
      if ($cursor) {
        if (markType.isInSet(state.storedMarks || $cursor.marks()))
          dispatch(state.tr.removeStoredMark(markType))
        else
          dispatch(state.tr.addStoredMark(markType.create(attrs)))
      } else {
        let add, tr = state.tr
        if (removeWhenPresent) {
          add = !ranges.some(r => state.doc.rangeHasMark(r.$from.pos, r.$to.pos, markType))
        } else {
          add = !ranges.every(r => {
            let missing = false
            tr.doc.nodesBetween(r.$from.pos, r.$to.pos, (node, pos, parent) => {
              if (missing) return false
              missing = !markType.isInSet(node.marks) && !!parent && parent.type.allowsMarkType(markType) &&
                !(node.isText && /^\s*$/.test(node.textBetween(Math.max(0, r.$from.pos - pos),
                  Math.min(node.nodeSize, r.$to.pos - pos))))
            })
            return !missing
          })
        }
        // code to exclude atom's content
        let sranges = ranges
        const excludeAtomsContent = options?.excludeAtomsContent
        const excludeAtomsWithContent = options?.excludeAtomsWithContent
        if (excludeAtomsContent || excludeAtomsWithContent) {
          const { doc, selection } = state
          const { from, to } = selection
          const excluded: number[][] = []
          doc.nodesBetween(from, to, (node, pos) => {
            if (node.isAtom && !node.isLeaf) {
              let cFrom: number, cTo: number
              if (excludeAtomsContent) {
                cFrom = Math.max(pos + 1, from)
                cTo = Math.min(pos + 1 + node.content.size, to)
              } else {
                cFrom = Math.max(pos, from)
                cTo = Math.min(pos + node.nodeSize, to)
              }
              excluded.push([cFrom, cTo])
            }
          })
          sranges = excluded.reduce(
            (acc, [eFrom, eTo]) => {
              const newAcc: number[][] = []
              acc.forEach(([aFrom, aTo]) => {
                if (eFrom >= aTo || aFrom >= eTo)
                  newAcc.push([aFrom, aTo])
                else if (eFrom > aFrom && eTo < aTo)
                  newAcc.push([aFrom, eFrom], [eTo, aTo])
                else if (eFrom < aFrom)
                  newAcc.push([eTo, aTo])
                else
                  newAcc.push([aFrom, eFrom])
              })
              return newAcc
            },
            ranges.map(({ $from, $to }) => [$from.pos, $to.pos])
          ).map(([from, to]) => new SelectionRange(doc.resolve(from), doc.resolve(to)))
        }
        // end of code to exclude atom's content
        for (let i = 0; i < sranges.length; i++) {
          let { $from, $to } = sranges[i]
          if (!add) {
            tr.removeMark($from.pos, $to.pos, markType)
          } else {
            let from = $from.pos, to = $to.pos, start = $from.nodeAfter, end = $to.nodeBefore
            let spaceStart = start && start.isText ? /^\s*/.exec(start.text!)![0].length : 0
            let spaceEnd = end && end.isText ? /\s*$/.exec(end.text!)![0].length : 0
            if (from + spaceStart < to) { from += spaceStart; to -= spaceEnd }
            tr.addMark(from, to, markType.create(attrs))
          }
        }
        dispatch(tr.scrollIntoView())
      }
    }
    return true
  }
}
I have some doubts about the options argument:
- 
should removeWhenPresentbecome optional (boolean | undefined)?
- 
maybe a single option excludeAtomsWithContent: "whole" | "content"could be better than two boolean options