function swap(target, content, swapSpec, swapOptions) {
    if (!swapOptions) {
      swapOptions = {}
    }

    target = resolveTarget(target)
    const rootNode = swapOptions.contextElement ? getRootNode(swapOptions.contextElement, false) : getDocument()

    // preserve focus and selection
    const activeElt = document.activeElement
    let selectionInfo = {}
    try {
      selectionInfo = {
        elt: activeElt,
        // @ts-ignore
        start: activeElt ? activeElt.selectionStart : null,
        // @ts-ignore
        end: activeElt ? activeElt.selectionEnd : null
      }
    } catch (e) {
      // safari issue - see https://github.com/microsoft/playwright/issues/5894
    }
    const settleInfo = makeSettleInfo(target)

    // For text content swaps, don't parse the response as HTML, just insert it
    if (swapSpec.swapStyle === 'textContent') {
      target.textContent = content
    // Otherwise, make the fragment and process it
    } else {
      let fragment = makeFragment(content)

      settleInfo.title = fragment.title

      // select-oob swaps
      if (swapOptions.selectOOB) {
        const oobSelectValues = swapOptions.selectOOB.split(',')
        for (let i = 0; i < oobSelectValues.length; i++) {
          const oobSelectValue = oobSelectValues[i].split(':', 2)
          let id = oobSelectValue[0].trim()
          if (id.indexOf('#') === 0) {
            id = id.substring(1)
          }
          const oobValue = oobSelectValue[1] || 'true'
          const oobElement = fragment.querySelector('#' + id)
          if (oobElement) {
            oobSwap(oobValue, oobElement, settleInfo, rootNode)
          }
        }
      }
      // oob swaps
      findAndSwapOobElements(fragment, settleInfo, rootNode)
      forEach(findAll(fragment, 'template'), /** @param {HTMLTemplateElement} template */function(template) {
        if (template.content && findAndSwapOobElements(template.content, settleInfo, rootNode)) {
          // Avoid polluting the DOM with empty templates that were only used to encapsulate oob swap
          template.remove()
        }
      })

      // normal swap
      if (swapOptions.select) {
        const newFragment = getDocument().createDocumentFragment()
        forEach(fragment.querySelectorAll(swapOptions.select), function(node) {
          newFragment.appendChild(node)
        })
        fragment = newFragment
      }
      handlePreservedElements(fragment)
      swapWithStyle(swapSpec.swapStyle, swapOptions.contextElement, target, fragment, settleInfo)
      restorePreservedElements()
    }

    // apply saved focus and selection information to swapped content
    if (selectionInfo.elt &&
      !bodyContains(selectionInfo.elt) &&
      getRawAttribute(selectionInfo.elt, 'id')) {
      const newActiveElt = document.getElementById(getRawAttribute(selectionInfo.elt, 'id'))
      const focusOptions = { preventScroll: swapSpec.focusScroll !== undefined ? !swapSpec.focusScroll : !htmx.config.defaultFocusScroll }
      if (newActiveElt) {
        // @ts-ignore
        if (selectionInfo.start && newActiveElt.setSelectionRange) {
          try {
            // @ts-ignore
            newActiveElt.setSelectionRange(selectionInfo.start, selectionInfo.end)
          } catch (e) {
            // the setSelectionRange method is present on fields that don't support it, so just let this fail
          }
        }
        newActiveElt.focus(focusOptions)
      }
    }

    target.classList.remove(htmx.config.swappingClass)
    forEach(settleInfo.elts, function(elt) {
      if (elt.classList) {
        elt.classList.add(htmx.config.settlingClass)
      }
      triggerEvent(elt, 'htmx:afterSwap', swapOptions.eventInfo)
    })
    if (swapOptions.afterSwapCallback) {
      swapOptions.afterSwapCallback()
    }

    // merge in new title after swap but before settle
    if (!swapSpec.ignoreTitle) {
      handleTitle(settleInfo.title)
    }

    // settle
    const doSettle = function() {
      forEach(settleInfo.tasks, function(task) {
        task.call()
      })
      forEach(settleInfo.elts, function(elt) {
        if (elt.classList) {
          elt.classList.remove(htmx.config.settlingClass)
        }
        triggerEvent(elt, 'htmx:afterSettle', swapOptions.eventInfo)
      })

      if (swapOptions.anchor) {
        const anchorTarget = asElement(resolveTarget('#' + swapOptions.anchor))
        if (anchorTarget) {
          anchorTarget.scrollIntoView({ block: 'start', behavior: 'auto' })
        }
      }

      updateScrollState(settleInfo.elts, swapSpec)
      if (swapOptions.afterSettleCallback) {
        swapOptions.afterSettleCallback()
      }
    }

    if (swapSpec.settleDelay > 0) {
      getWindow().setTimeout(doSettle, swapSpec.settleDelay)
    } else {
      doSettle()
    }
  }
Implements complete swapping pipeline, including: focus and selection preservation,
title updates, scroll, OOB swapping, normal swapping and settling
@param {string|Element} target
@param {string} content
@param {HtmxSwapSpecification} swapSpec
@param {SwapOptions} [swapOptions]