Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: replace emoji with SVGs (#129) #584

Merged
merged 7 commits into from
Jan 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dist
.netlify/

public/shiki
public/emojis

*~
*swp
Expand Down
54 changes: 33 additions & 21 deletions composables/content-parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
import type { Emoji } from 'masto'
import type { Node } from 'ultrahtml'
import { TEXT_NODE, parse, render, walkSync } from 'ultrahtml'
import createEmojiRegex from 'emoji-regex'

export const EMOJI_REGEX = createEmojiRegex()
import { findAndReplaceEmojisInText } from '@iconify/utils'
import { emojiRegEx, getEmojiAttributes } from '../config/emojis'

const decoder = process.client ? document.createElement('textarea') : null as any as HTMLTextAreaElement
export function decodeHtml(text: string) {
Expand All @@ -16,17 +15,17 @@ export function decodeHtml(text: string) {
* Parse raw HTML form Mastodon server to AST,
* with interop of custom emojis and inline Markdown syntax
*/
export function parseMastodonHTML(html: string, customEmojis: Record<string, Emoji> = {}, markdown = true) {
let processed = html
// custom emojis
.replace(/:([\w-]+?):/g, (_, name) => {
const emoji = customEmojis[name]

return emoji
? `<img src="${emoji.url}" alt=":${name}:" class="custom-emoji" data-emoji-id="${name}" />`
: `:${name}:`
})
.replace(EMOJI_REGEX, '<em-emoji native="$&" fallback="$&" />')
export function parseMastodonHTML(html: string, customEmojis: Record<string, Emoji> = {}, markdown = true, forTiptap = false) {
// unicode emojis to images, but only if not converting HTML for Tiptap
let processed = forTiptap ? html : replaceUnicodeEmoji(html)

// custom emojis
processed = processed.replace(/:([\w-]+?):/g, (_, name) => {
const emoji = customEmojis[name]
if (emoji)
return `<img src="${emoji.url}" alt=":${name}:" class="custom-emoji" data-emoji-id="${name}" />`
return `:${name}:`
})

if (markdown) {
// handle code blocks
Expand Down Expand Up @@ -66,8 +65,11 @@ export function parseMastodonHTML(html: string, customEmojis: Record<string, Emo
return parse(processed)
}

/**
* Converts raw HTML form Mastodon server to HTML for Tiptap editor
*/
export function convertMastodonHTML(html: string, customEmojis: Record<string, Emoji> = {}) {
const tree = parseMastodonHTML(html, customEmojis)
const tree = parseMastodonHTML(html, customEmojis, true, true)
return render(tree)
}

Expand Down Expand Up @@ -118,12 +120,22 @@ export function treeToText(input: Node): string {
if ('children' in input)
body = (input.children as Node[]).map(n => treeToText(n)).join('')

// add spaces around emoji to prevent parsing errors: 2 or more consecutive emojis will not be parsed
if (input.name === 'img' && input.attributes.class?.includes('custom-emoji'))
return ` :${input.attributes['data-emoji-id']}: `

if (input.name === 'em-emoji')
return `${input.attributes.native}`
if (input.name === 'img') {
if (input.attributes.class?.includes('custom-emoji'))
return `:${input.attributes['data-emoji-id']}:`
if (input.attributes.class?.includes('iconify-emoji'))
return input.attributes.alt
}

return pre + body + post
}

/**
* Replace unicode emojis with locally hosted images
*/
export function replaceUnicodeEmoji(html: string) {
return findAndReplaceEmojisInText(emojiRegEx, html, (match) => {
const attrs = getEmojiAttributes(match)
return `<img src="${attrs.src}" alt="${attrs.alt}" class="${attrs.class}" />`
}) || html
}
26 changes: 12 additions & 14 deletions composables/tiptap/emoji.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
mergeAttributes,
nodeInputRule,
} from '@tiptap/core'
import { emojiRegEx, getEmojiAttributes } from '~/config/emojis'

export const Emoji = Node.create({
name: 'em-emoji',
Expand All @@ -14,50 +15,47 @@ export const Emoji = Node.create({
parseHTML() {
return [
{
tag: 'em-emoji[native]',
tag: 'img.iconify-emoji',
},
]
},

addAttributes() {
return {
native: {
alt: {
default: null,
},
fallback: {
src: {
default: null,
},
class: {
default: null,
},
}
},

renderHTML(args) {
return ['em-emoji', mergeAttributes(this.options.HTMLAttributes, args.HTMLAttributes)]
return ['img', mergeAttributes(this.options.HTMLAttributes, args.HTMLAttributes)]
},

addCommands() {
return {
insertEmoji: name => ({ commands }) => {
insertEmoji: code => ({ commands }) => {
return commands.insertContent({
type: this.name,
attrs: {
native: name,
fallback: name,
},
attrs: getEmojiAttributes(code),
})
},
}
},

addInputRules() {
const inputRule = nodeInputRule({
find: EMOJI_REGEX,
find: emojiRegEx as RegExp,
type: this.type,
getAttributes: (match) => {
const [native] = match
return {
native,
fallback: native,
}
return getEmojiAttributes(native)
},
})
// Error catch for unsupported emoji
Expand Down
22 changes: 22 additions & 0 deletions config/emojis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { emojiFilename, emojiPrefix, emojiRegEx } from '@iconify-emoji/twemoji'
import type { EmojiRegexMatch } from '@iconify/utils/lib/emoji/replace/find'
import { getEmojiMatchesInText } from '@iconify/utils/lib/emoji/replace/find'

// Re-export everything from package
export * from '@iconify-emoji/twemoji'

// Package name
export const iconifyEmojiPackage = '@iconify-emoji/twemoji'

export function getEmojiAttributes(input: EmojiRegexMatch | string) {
const match = typeof input === 'string'
? getEmojiMatchesInText(emojiRegEx, input)?.[0]
: input
const file = emojiFilename(match)
const className = `iconify-emoji iconify-emoji--${emojiPrefix}${file.padding ? ' iconify-emoji-padded' : ''}`
return {
class: className,
src: `/emojis/${emojiPrefix}/${file.filename}`,
alt: match.match,
}
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
},
"dependencies": {
"@fnando/sparkline": "^0.3.10",
"@iconify-emoji/twemoji": "^1.0.2",
"@iconify/utils": "^2.0.7",
"@nuxtjs/color-mode": "^3.2.0",
"@tiptap/extension-character-count": "2.0.0-beta.204",
"@tiptap/extension-code-block": "2.0.0-beta.204",
Expand Down
9 changes: 0 additions & 9 deletions plugins/setup-emojis.ts

This file was deleted.

Loading