OverviewFilesCommitsIssuesDocsReleasesPeers

holepunchto-deps / @holepunchto / keet-editor / src / extensions / emojis-fixer.js

82 lines · javascript ref:
import { Extension } from '@tiptap/core'
import { Plugin, PluginKey } from '@tiptap/pm/state'

const FE0F = '\uFE0F'
const ZWJ = '\u200D'
const META_KEY = 'emojisFixer'

const EMOJI_RE = /\p{Extended_Pictographic}/u

export function normalizeEmoji (str) {
  if (!EMOJI_RE.test(str)) return str

  const array = Array.from(str)
  let changed = false

  for (let i = 0; i < array.length; i += 1) {
    const c = array[i]
    const isBmpEmoji = EMOJI_RE.test(c) && c.length === 1 && c.codePointAt(0) < 0xffff
    if (!isBmpEmoji) continue

    const nextChar = array[i + 1]
    const prevChar = array[i - 1]
    const isPartOfCombinedEmoji = prevChar === ZWJ || nextChar === ZWJ
    if (isPartOfCombinedEmoji) continue

    const shouldFix = nextChar !== FE0F
    if (!shouldFix) continue

    array[i] = c + FE0F
    changed = true
  }

  return changed ? array.join('') : str
}

export const EmojisFixer = Extension.create({
  name: 'emojisFixer',

  addProseMirrorPlugins () {
    return [
      new Plugin({
        key: new PluginKey('emojisFixer'),

        props: {
          transformPastedText: (text) => normalizeEmoji(text),

          handleTextInput (view, from, to, text) {
            const fixed = normalizeEmoji(text)
            if (fixed === text) return false
            const tr = view.state.tr.insertText(fixed, from, to).setMeta(META_KEY, true)
            view.dispatch(tr)
            return true
          }
        },

        appendTransaction (transactions, oldState, newState) {
          const incomingHasMeta = transactions.some(t => t.getMeta(META_KEY))
          if (incomingHasMeta) return null

          let tr = newState.tr
          let changed = false
          const { schema } = newState

          newState.doc.descendants((node, pos) => {
            if (!node.isText) return
            const fixed = normalizeEmoji(node.text)
            if (fixed === node.text) return

            changed = true
            const textNode = schema.text(fixed, node.marks)
            tr = tr.replaceWith(pos, pos + node.nodeSize, textNode)
          })

          if (!changed) return null

          return tr.setMeta(META_KEY, true)
        }
      })
    ]
  }
})