diff --git a/bun.lockb b/bun.lockb index 48d13238c..c65eb21b0 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/emoji-codepoints/.gitignore b/packages/emoji-codepoints/.gitignore new file mode 100644 index 000000000..1e21beece --- /dev/null +++ b/packages/emoji-codepoints/.gitignore @@ -0,0 +1 @@ +index.ts diff --git a/packages/emoji-codepoints/build.ts b/packages/emoji-codepoints/build.ts new file mode 100644 index 000000000..792978996 --- /dev/null +++ b/packages/emoji-codepoints/build.ts @@ -0,0 +1,26 @@ +import fs from 'fs'; +import emojisRaws from 'emoji-assets/emoji.json'; + +interface EmojiData { + code_points: { + base: string; + fully_qualified: string; + }; +} + +const emojis = emojisRaws as Record; +const output: Record = {}; + +Object.entries(emojis).forEach(([key, value]) => { + const emoji = value.code_points?.fully_qualified; + if (emoji && key !== emoji) { + output[key] = emoji; + } else if (!emoji) { + console.log('No emoji for', key); + } +}); + +fs.writeFileSync( + 'index.ts', + `export const emojiCodepoints: Record = ${JSON.stringify(output, null, 4)};`, +); diff --git a/packages/emoji-codepoints/package.json b/packages/emoji-codepoints/package.json new file mode 100644 index 000000000..241c66349 --- /dev/null +++ b/packages/emoji-codepoints/package.json @@ -0,0 +1,13 @@ +{ + "name": "@gitbook/emoji-codepoints", + "description": "Optimized mapping of codepoints to the fully qualified emoji codepoints", + "private": true, + "main": "./index.ts", + "dependencies": {}, + "devDependencies": { + "emoji-assets": "^8.0.0" + }, + "scripts": { + "postinstall": "bun ./build.ts" + } +} diff --git a/src/app/[spaceId]/layout.tsx b/src/app/[spaceId]/layout.tsx index 88b296dcb..6c164af93 100644 --- a/src/app/[spaceId]/layout.tsx +++ b/src/app/[spaceId]/layout.tsx @@ -7,6 +7,7 @@ import { import assertNever from 'assert-never'; import colors from 'tailwindcss/colors'; +import { emojiFontClassName } from '@/components/primitives'; import { fonts, ibmPlexMono } from '@/fonts'; import { getSpaceLanguage } from '@/intl/server'; import { getSpaceContent } from '@/lib/api'; @@ -103,6 +104,7 @@ export default async function SpaceRootLayout(props: { return ( : null - } + leadingIcon={resolved.emoji ? : null} href={resolved.href} preTitle={kind} title={resolved.text} diff --git a/src/components/primitives/Emoji.tsx b/src/components/primitives/Emoji.tsx deleted file mode 100644 index 6abcaf52f..000000000 --- a/src/components/primitives/Emoji.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { ClassValue, tcls } from '@/lib/tailwind'; - -/** - * Render an emoji by its UTF-8 codepoint. - */ -export async function Emoji(props: { code: string; style?: ClassValue }) { - const { code, style } = props; - - const emojiCodepointDecimal = parseInt(code, 16); - const emoji = String.fromCodePoint(emojiCodepointDecimal); - - return {emoji}; -} diff --git a/src/components/primitives/Emoji/Emoji.tsx b/src/components/primitives/Emoji/Emoji.tsx new file mode 100644 index 000000000..6ea0ba60d --- /dev/null +++ b/src/components/primitives/Emoji/Emoji.tsx @@ -0,0 +1,55 @@ +import { emojiCodepoints } from '@gitbook/emoji-codepoints'; +import localFont from 'next/font/local'; + +import { ClassValue, tcls } from '@/lib/tailwind'; + +import './emoji.css'; + +/* Load the 3 variants of the font as CSS variables and let the emoji.css decide on the best one to use */ + +const svgFont = localFont({ + src: './svg.woff2', + preload: false, + variable: '--font-emojis-svg', +}); + +const sbixFont = localFont({ + src: './sbix.woff2', + preload: false, + variable: '--font-emojis-sbix', +}); + +const cbdtFont = localFont({ + src: './cbdt.woff2', + preload: false, + variable: '--font-emojis-cbdt', +}); + +/** + * Class name to set on the tag to use the emoji font. + */ +export const emojiFontClassName = [svgFont.variable, sbixFont.variable, cbdtFont.variable].join( + ' ', +); + +/** + * Render an emoji by its codepoint. + * It renders the UTF-8 character and use the emojione font to display it (fallbacking to default browser font). + */ +export async function Emoji(props: { code: string; style?: ClassValue }) { + const { code, style } = props; + + const fullCode = emojiCodepoints[code] ?? code; + const fallback = getEmojiForCodepoint(fullCode); + + return {fallback}; +} + +function getEmojiForCodepoint(unicode: string): string { + if (!unicode) { + return ''; + } + + const codePoints = unicode.split('-').map((elt) => parseInt(elt, 16)); + return String.fromCodePoint(...codePoints); +} diff --git a/src/components/primitives/Emoji/cbdt.woff2 b/src/components/primitives/Emoji/cbdt.woff2 new file mode 100644 index 000000000..c0e26e354 Binary files /dev/null and b/src/components/primitives/Emoji/cbdt.woff2 differ diff --git a/src/components/primitives/Emoji/emoji.css b/src/components/primitives/Emoji/emoji.css new file mode 100644 index 000000000..b9c135afa --- /dev/null +++ b/src/components/primitives/Emoji/emoji.css @@ -0,0 +1,24 @@ +[class*='emoji'] { + font-family: var(--font-emojis-cbdt); +} + +/* Safari Only CSS here */ +_::-webkit-full-page-media, +_:future, +:root [class*='emoji'] { + font-family: var(--font-emojis-sbix); +} + +/* Chrome Only CSS here */ +@media screen and (-webkit-min-device-pixel-ratio: 0) { + .emoji { + font-family: var(--font-emojis-svg); + } +} + +/* Firefox Only CSS here */ +@-moz-document url-prefix() { + .emoji { + font-family: var(--font-emojis-svg); + } +} diff --git a/src/components/primitives/Emoji/index.ts b/src/components/primitives/Emoji/index.ts new file mode 100644 index 000000000..869b7ac98 --- /dev/null +++ b/src/components/primitives/Emoji/index.ts @@ -0,0 +1 @@ +export * from './Emoji'; diff --git a/src/components/primitives/Emoji/sbix.woff2 b/src/components/primitives/Emoji/sbix.woff2 new file mode 100644 index 000000000..73282d7c2 Binary files /dev/null and b/src/components/primitives/Emoji/sbix.woff2 differ diff --git a/src/components/primitives/Emoji/svg.woff2 b/src/components/primitives/Emoji/svg.woff2 new file mode 100644 index 000000000..82ba95fd2 Binary files /dev/null and b/src/components/primitives/Emoji/svg.woff2 differ diff --git a/tests/visual-testing.ts b/tests/visual-testing.ts index 392e5df04..2650c312e 100644 --- a/tests/visual-testing.ts +++ b/tests/visual-testing.ts @@ -213,6 +213,10 @@ const testCases: TestsCase[] = [ name: 'Marks', url: 'blocks/marks', }, + { + name: 'Emojis', + url: 'blocks/emojis', + }, ], }, ];