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)
}
})
]
}
})