diff --git a/.gitignore b/.gitignore
index b61e1356..5e134baa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@ node_modules
*.log
dist
.output
+.pnpm-store
.nuxt
.env
.DS_Store
diff --git a/components/emoji/Emoji.vue b/components/emoji/Emoji.vue
new file mode 100644
index 00000000..15bbf07d
--- /dev/null
+++ b/components/emoji/Emoji.vue
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
diff --git a/composables/content-render.ts b/composables/content-render.ts
index d0302031..bded3767 100644
--- a/composables/content-render.ts
+++ b/composables/content-render.ts
@@ -6,6 +6,7 @@ import { RouterLink } from 'vue-router'
import { decode } from 'tiny-decode'
import type { ContentParseOptions } from './content-parse'
import { parseMastodonHTML } from './content-parse'
+import Emoji from '~/components/emoji/Emoji.vue'
import ContentCode from '~/components/content/ContentCode.vue'
import ContentMentionGroup from '~/components/content/ContentMentionGroup.vue'
import AccountHoverWrapper from '~/components/account/AccountHoverWrapper.vue'
@@ -19,8 +20,11 @@ function getTextualAstComponents(astChildren: Node[]): string {
}
/**
-* Raw HTML to VNodes
-*/
+ * Raw HTML to VNodes.
+ *
+ * @param content HTML content.
+ * @param options Options.
+ */
export function contentToVNode(
content: string,
options?: ContentParseOptions,
@@ -43,6 +47,17 @@ export function nodeToVNode(node: Node): VNode | string | null {
if (node.name === 'mention-group')
return h(ContentMentionGroup, node.attributes, () => node.children.map(treeToVNode))
+ // add tooltip to emojis
+ if (node.name === 'picture' || (node.name === 'img' && node.attributes?.alt)) {
+ const props = node.attributes ?? {}
+ props.as = node.name
+ return h(
+ Emoji,
+ props,
+ () => node.children.map(treeToVNode),
+ )
+ }
+
if ('children' in node) {
if (node.name === 'a' && (node.attributes.href?.startsWith('/') || node.attributes.href?.startsWith('.'))) {
node.attributes.to = node.attributes.href
diff --git a/package.json b/package.json
index 439e3ede..e30fe632 100644
--- a/package.json
+++ b/package.json
@@ -79,6 +79,7 @@
"js-yaml": "^4.1.0",
"lru-cache": "^10.0.0",
"masto": "^5.11.3",
+ "node-emoji": "^2.1.3",
"nuxt-security": "^0.13.1",
"page-lifecycle": "^0.1.2",
"pinia": "^2.1.4",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7c67cd8c..673424e6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -164,6 +164,9 @@ importers:
masto:
specifier: ^5.11.3
version: 5.11.3
+ node-emoji:
+ specifier: ^2.1.3
+ version: 2.1.3
nuxt-security:
specifier: ^0.13.1
version: 0.13.1(patch_hash=bd6cmp7ukwwiwrxafbbotwkihe)(rollup@2.79.1)
@@ -4141,6 +4144,11 @@ packages:
/@sinclair/typebox@0.27.8:
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
+ /@sindresorhus/is@4.6.0:
+ resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
+ engines: {node: '>=10'}
+ dev: false
+
/@sindresorhus/merge-streams@1.0.0:
resolution: {integrity: sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==}
engines: {node: '>=18'}
@@ -6865,6 +6873,11 @@ packages:
snake-case: 3.0.4
tslib: 2.6.0
+ /char-regex@1.0.2:
+ resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
+ engines: {node: '>=10'}
+ dev: false
+
/char-regex@2.0.1:
resolution: {integrity: sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==}
engines: {node: '>=12.20'}
@@ -7573,6 +7586,10 @@ packages:
/emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+ /emojilib@2.4.0:
+ resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==}
+ dev: false
+
/emoticon@4.0.1:
resolution: {integrity: sha512-dqx7eA9YaqyvYtUhJwT4rC1HIp82j5ybS1/vQ42ur+jBe17dJMwZE4+gvL1XadSFfxaPFFGt3Xsw+Y8akThDlw==}
dev: true
@@ -10773,6 +10790,16 @@ packages:
lodash: 4.17.21
dev: true
+ /node-emoji@2.1.3:
+ resolution: {integrity: sha512-E2WEOVsgs7O16zsURJ/eH8BqhF029wGpEOnv7Urwdo2wmQanOACwJQh0devF9D9RhoZru0+9JXIS0dBXIAz+lA==}
+ engines: {node: '>=18'}
+ dependencies:
+ '@sindresorhus/is': 4.6.0
+ char-regex: 1.0.2
+ emojilib: 2.4.0
+ skin-tone: 2.0.0
+ dev: false
+
/node-fetch-native@1.4.1:
resolution: {integrity: sha512-NsXBU0UgBxo2rQLOeWNZqS3fvflWePMECr8CoSWoSTqCqGbVVsvl9vZu1HfQicYN0g5piV9Gh8RTEvo/uP752w==}
@@ -13007,6 +13034,13 @@ packages:
/sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
+ /skin-tone@2.0.0:
+ resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==}
+ engines: {node: '>=8'}
+ dependencies:
+ unicode-emoji-modifier-base: 1.0.0
+ dev: false
+
/slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
@@ -13837,6 +13871,11 @@ packages:
engines: {node: '>=4'}
dev: false
+ /unicode-emoji-modifier-base@1.0.0:
+ resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==}
+ engines: {node: '>=4'}
+ dev: false
+
/unicode-match-property-ecmascript@2.0.0:
resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==}
engines: {node: '>=4'}
diff --git a/tests/nuxt/__snapshots__/content-rich.test.ts.snap b/tests/nuxt/__snapshots__/content-rich.test.ts.snap
index 3e4e5b2e..db4639a9 100644
--- a/tests/nuxt/__snapshots__/content-rich.test.ts.snap
+++ b/tests/nuxt/__snapshots__/content-rich.test.ts.snap
@@ -58,7 +58,7 @@ exports[`content-rich > code frame no lang 1`] = `"
he
exports[`content-rich > custom emoji 1`] = `
"Daniel Roe
-
"
`;
@@ -129,8 +130,8 @@ exports[`content-rich > link + mention 1`] = `
Happy
we’re now using