From b68754a0f47bd824947510587b2621d42aceebde Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 07:21:06 +0000 Subject: [PATCH 1/7] Fix mailto: and tel: links in markdown content makeUrl only passed URLs through untouched when they contained "://" or started with "data:". Every other scheme was treated as a relative path, so a markdown link like [Write us](mailto:a@b.c) was joined with the base path and rendered as href="/mailto:a@b.c". Links configured in hyperbook.json were unaffected, because those hrefs are used verbatim. Add an isExternalUrl helper to @hyperbook/types that matches any RFC 3986 scheme, and use it in the makeUrl implementations of the builder and the VS Code preview. This covers mailto: and tel: as well as sms:, geo: and friends, while https:// and data: keep working as before. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012kkjvqifo7fQ5W6woRWiSW --- .changeset/tricky-donuts-smile.md | 10 ++++++++++ packages/hyperbook/build.ts | 3 ++- packages/markdown/devBuild.mjs | 3 ++- packages/markdown/tests/mock.ts | 4 ++-- packages/markdown/tests/remarkLink.test.ts | 14 ++++++++++++++ packages/types/src/index.ts | 14 ++++++++++++++ platforms/vscode/src/Preview.ts | 10 ++++++++-- 7 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 .changeset/tricky-donuts-smile.md diff --git a/.changeset/tricky-donuts-smile.md b/.changeset/tricky-donuts-smile.md new file mode 100644 index 000000000..817c950a8 --- /dev/null +++ b/.changeset/tricky-donuts-smile.md @@ -0,0 +1,10 @@ +--- +"@hyperbook/types": patch +"hyperbook": patch +"hyperbook-studio": patch +--- + +Fix `mailto:` and `tel:` links in markdown. `makeUrl` only passed through URLs +containing `://` or starting with `data:`, so `[Write us](mailto:a@b.c)` was +resolved against the base path and became `href="/mailto:a@b.c"`. Any URL with +a scheme is now passed through untouched. diff --git a/packages/hyperbook/build.ts b/packages/hyperbook/build.ts index 4d9b0bbbc..390fc349d 100644 --- a/packages/hyperbook/build.ts +++ b/packages/hyperbook/build.ts @@ -21,6 +21,7 @@ import { HyperbookPage, HyperbookSection, Navigation, + isExternalUrl, } from "@hyperbook/types"; import lunr from "lunr"; import { process as hyperbookProcess } from "@hyperbook/markdown"; @@ -456,7 +457,7 @@ export function makeBaseCtx( version: packageJson.version, makeUrl: (p, base, page, options = { versioned: true }) => { if (typeof p === "string") { - if (p.includes("://") || p.startsWith("data:")) { + if (isExternalUrl(p)) { return p; } if (p.endsWith(".md.hbs")) { diff --git a/packages/markdown/devBuild.mjs b/packages/markdown/devBuild.mjs index 5dc585502..9c0081a33 100644 --- a/packages/markdown/devBuild.mjs +++ b/packages/markdown/devBuild.mjs @@ -1,6 +1,7 @@ import fs from "fs/promises"; import path from "path"; import { process } from "./dist/index.js"; +import { isExternalUrl } from "@hyperbook/types"; const markdown = await fs.readFile("dev.md", "utf8"); @@ -54,7 +55,7 @@ const result = await process(markdown, { }, }, makeUrl: (p, base) => { - if (p.includes("://")) return p; + if (typeof p === "string" && isExternalUrl(p)) return p; if (typeof p === "string") { p = [p]; } diff --git a/packages/markdown/tests/mock.ts b/packages/markdown/tests/mock.ts index e4efaa620..8308727b7 100644 --- a/packages/markdown/tests/mock.ts +++ b/packages/markdown/tests/mock.ts @@ -1,4 +1,4 @@ -import { HyperbookContext } from "@hyperbook/types"; +import { HyperbookContext, isExternalUrl } from "@hyperbook/types"; import path from "path"; export const ctx: HyperbookContext = { @@ -25,7 +25,7 @@ export const ctx: HyperbookContext = { }, makeUrl: (p, base) => { if (typeof p === "string") { - if (p.startsWith("http://") || p.startsWith("https://") || p.startsWith("mailto:")) { + if (isExternalUrl(p)) { return p; } if (base === "public" || base === "book") { diff --git a/packages/markdown/tests/remarkLink.test.ts b/packages/markdown/tests/remarkLink.test.ts index a3f68810a..bcaebabf4 100644 --- a/packages/markdown/tests/remarkLink.test.ts +++ b/packages/markdown/tests/remarkLink.test.ts @@ -42,4 +42,18 @@ describe("remarkLink", () => { ), ).toMatchSnapshot(); }); + it("should keep mailto links", () => { + const html = toHtml(`[Write us](mailto:contact@openpatch.org)`, ctx); + expect(html).toContain(`href="mailto:contact@openpatch.org"`); + expect(html).not.toContain("target"); + }); + it("should keep tel links", () => { + const html = toHtml(`[Call us](tel:+4912345678)`, ctx); + expect(html).toContain(`href="tel:+4912345678"`); + }); + it("should keep external links and open them in a new tab", () => { + const html = toHtml(`[OpenPatch](https://openpatch.org)`, ctx); + expect(html).toContain(`href="https://openpatch.org"`); + expect(html).toContain(`target="_blank"`); + }); }); diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 4c13f71f5..578c46056 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -248,3 +248,17 @@ export interface HyperbookContext { */ dependencies?: Set; } + +/** + * Matches a URI scheme at the start of a string, as defined by RFC 3986. + * This covers `https://` and `data:` as well as schemes without an authority + * like `mailto:`, `tel:` or `sms:`. + */ +const URL_SCHEME = /^[a-zA-Z][a-zA-Z0-9+.-]*:/; + +/** + * Checks whether a path is an absolute URL and therefore must be passed + * through untouched by `makeUrl`, instead of being resolved against the + * hyperbook's base path. + */ +export const isExternalUrl = (path: string): boolean => URL_SCHEME.test(path); diff --git a/platforms/vscode/src/Preview.ts b/platforms/vscode/src/Preview.ts index 50087ca5c..6fa2cd6b9 100644 --- a/platforms/vscode/src/Preview.ts +++ b/platforms/vscode/src/Preview.ts @@ -10,7 +10,13 @@ import { import { process } from "@hyperbook/markdown"; import { disposeAll } from "./utils/dispose"; import path, { posix } from "path"; -import { HyperbookContext, HyperbookJson, HyperbookPage, Navigation } from "@hyperbook/types"; +import { + HyperbookContext, + HyperbookJson, + HyperbookPage, + Navigation, + isExternalUrl, +} from "@hyperbook/types"; // Helper function to resolve relative paths const resolveRelativePath = (path: string, page: HyperbookPage): string => { @@ -189,7 +195,7 @@ export default class Preview { makeUrl: (path, base, page) => { if (typeof path === "string") { // Handle absolute URLs - if (path.includes("://") || path.startsWith("data:")) { + if (isExternalUrl(path)) { return path; } From 90350eed4466ba855e802909bd655cb3068374a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 07:49:26 +0000 Subject: [PATCH 2/7] Render emojis as Twemoji images with elements.emoji.style Emojis were resolved to plain Unicode characters and drawn by whatever emoji font the reader's operating system provides, so the same book looked different on Windows, macOS, Android and Linux. Add a rehypeEmoji plugin that swaps emojis for Twemoji images when elements.emoji.style is set to "twemoji". It runs after the shell, so icons configured in hyperbook.json are covered along with the content, and it skips pre, code, script, style, textarea and title. Characters that are text by default, like (c) or (tm), are only replaced when the author asked for the emoji presentation, and an emoji without a Twemoji asset is left as text. The full Twemoji set ships in the package assets, but a build copies only the images a book actually uses into its output, both for a full build and for an incremental rebuild in the dev server. The default stays "native", so existing books render exactly as before. Twemoji graphics are CC-BY 4.0 by Twitter, Inc and other contributors. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012kkjvqifo7fQ5W6woRWiSW --- .changeset/wild-pugs-invite.md | 13 + packages/hyperbook/build.ts | 59 +- packages/hyperbook/incremental.ts | 25 + packages/markdown/assets/content.css | 10 + packages/markdown/assets/shell.css | 10 + packages/markdown/package.json | 3 +- packages/markdown/postbuild.mjs | 17 + packages/markdown/src/emoji-assets.json | 3722 +++++++++++++++++ packages/markdown/src/index.ts | 2 + packages/markdown/src/process.ts | 3 + packages/markdown/src/rehypeEmoji.ts | 162 + packages/markdown/tests/rehypeEmoji.test.ts | 101 + packages/markdown/updateEmojiAssets.mjs | 24 + packages/markdown/updateEmojis.mjs | 48 + packages/types/src/index.ts | 10 + .../vscode/schemas/hyperbook.schema.json | 48 + pnpm-lock.yaml | 8 + website/de/book/elements/emoji.md | 25 + website/en/book/elements/emoji.md | 23 + 19 files changed, 4309 insertions(+), 4 deletions(-) create mode 100644 .changeset/wild-pugs-invite.md create mode 100644 packages/markdown/src/emoji-assets.json create mode 100644 packages/markdown/src/rehypeEmoji.ts create mode 100644 packages/markdown/tests/rehypeEmoji.test.ts create mode 100644 packages/markdown/updateEmojiAssets.mjs diff --git a/.changeset/wild-pugs-invite.md b/.changeset/wild-pugs-invite.md new file mode 100644 index 000000000..f57eeec6e --- /dev/null +++ b/.changeset/wild-pugs-invite.md @@ -0,0 +1,13 @@ +--- +"@hyperbook/markdown": patch +"@hyperbook/types": patch +"hyperbook": patch +"hyperbook-studio": patch +--- + +Add `elements.emoji.style`. Emojis are drawn by the reader's operating system, +so the same emoji looks different on Windows, macOS, Android and Linux. Setting +the style to `twemoji` replaces them with Twemoji images at build time, so a +hyperbook looks the same on every platform. This covers emojis in the content +as well as icons from the config, leaves code untouched, and only copies the +emojis a book actually uses into its output. The default stays `native`. diff --git a/packages/hyperbook/build.ts b/packages/hyperbook/build.ts index 390fc349d..5d28b4688 100644 --- a/packages/hyperbook/build.ts +++ b/packages/hyperbook/build.ts @@ -101,6 +101,8 @@ export type PageResultSink = (href: string, result: SinglePageResult) => void; export interface SinglePageResult { searchDocuments: any[]; directives: string[]; + /** Twemoji file names used on this page, without the .svg extension. */ + emojis: string[]; /** Absolute paths whose contents were inlined into this page. */ dependencies: string[]; } @@ -135,6 +137,7 @@ export async function buildSingleBookPage( const result = await hyperbookProcess(file.markdown.content, ctx); const searchDocuments = [...(result.data.searchDocuments || [])]; const directives = Object.keys(result.data.directives || {}); + const emojis = [...(result.data.emojis || [])]; for (const generated of (result.data.generatedFiles as any[]) || []) { let genDir; @@ -183,7 +186,12 @@ export async function buildSingleBookPage( } await fs.writeFile(fileOut, result.value); - return { searchDocuments, directives, dependencies: [...dependencies] }; + return { + searchDocuments, + directives, + emojis, + dependencies: [...dependencies], + }; } export async function buildSingleGlossaryPage( @@ -233,6 +241,7 @@ export async function buildSingleGlossaryPage( const result = await hyperbookProcess(file.markdown.content, ctx); const searchDocuments = [...(result.data.searchDocuments || [])]; const directives = Object.keys(result.data.directives || {}); + const emojis = [...(result.data.emojis || [])]; for (const generated of (result.data.generatedFiles as any[]) || []) { let genDir; @@ -262,7 +271,12 @@ export async function buildSingleGlossaryPage( } await fs.writeFile(fileOut, result.value); - return { searchDocuments, directives, dependencies: [...dependencies] }; + return { + searchDocuments, + directives, + emojis, + dependencies: [...dependencies], + }; } /** @@ -562,6 +576,7 @@ async function runBuild( const baseCtx = makeBaseCtx(root, hyperbookJson, basePath, rootProject); const directives = new Set([]); + const emojis = new Set([]); const pagesAndSections = await hyperbook.getPagesAndSections(root); const pageList = hyperbook.getPageList( pagesAndSections.sections, @@ -590,6 +605,9 @@ async function runBuild( for (let directive of pageResult.directives) { directives.add(directive); } + for (let emoji of pageResult.emojis) { + emojis.add(emoji); + } onPage?.(file.path.href || file.path.absolute, pageResult); if (!process.env.CI) { @@ -627,6 +645,9 @@ async function runBuild( for (let directive of pageResult.directives) { directives.add(directive); } + for (let emoji of pageResult.emojis) { + emojis.add(emoji); + } onPage?.(file.path.href || file.path.absolute, pageResult); if (!process.env.CI) { @@ -840,12 +861,44 @@ async function runBuild( } process.stdout.write("\n"); + if (emojis.size > 0) { + const emojiPath = path.join(assetsPath, "emoji"); + const emojiOut = path.join(assetsOut, "emoji"); + await mkdir(emojiOut, { recursive: true }); + i = 1; + for (let emoji of emojis) { + if (!process.env.CI) { + readline.clearLine(process.stdout, 0); + readline.cursorTo(process.stdout, 0); + } + process.stdout.write( + `${chalk.blue(`[${prefix}]`)} Copying emojis: [${i++}/${emojis.size}]`, + ); + if (process.env.CI) { + process.stdout.write("\n"); + } + try { + await cp( + path.join(emojiPath, `${emoji}.svg`), + path.join(emojiOut, `${emoji}.svg`), + ); + } catch (e) { + process.stdout.write("\n"); + process.stdout.write( + `${chalk.red(`[${prefix}]`)} Failed copying emoji: ${emoji}`, + ); + process.stdout.write("\n"); + } + } + process.stdout.write("\n"); + } + const mainAssets = await fs.readdir(assetsPath); i = 1; for (let asset of mainAssets) { const assetPath = path.join(assetsPath, asset); const assetOut = path.join(assetsOut, asset); - if (!asset.startsWith("directive-")) { + if (!asset.startsWith("directive-") && asset !== "emoji") { await cp(assetPath, assetOut, { recursive: true, filter: (src) => { diff --git a/packages/hyperbook/incremental.ts b/packages/hyperbook/incremental.ts index db1a25729..48699e8ff 100644 --- a/packages/hyperbook/incremental.ts +++ b/packages/hyperbook/incremental.ts @@ -353,6 +353,7 @@ export class IncrementalBuilder { // Copy any new directive assets await this.copyDirectiveAssets(result.directives); + await this.copyEmojiAssets(result.emojis); const changedHref = changedFile.path.href || "/"; @@ -423,6 +424,7 @@ export class IncrementalBuilder { } await this.copyDirectiveAssets(result.directives); + await this.copyEmojiAssets(result.emojis); const changedHref = changedFile.path.href || "/glossary"; await this.refreshSearchIndex(); @@ -487,6 +489,7 @@ export class IncrementalBuilder { this.directives.add(directive); } await this.copyDirectiveAssets(result.directives); + await this.copyEmojiAssets(result.emojis); return href; } @@ -507,6 +510,7 @@ export class IncrementalBuilder { this.directives.add(directive); } await this.copyDirectiveAssets(result.directives); + await this.copyEmojiAssets(result.emojis); return href; } @@ -529,6 +533,27 @@ export class IncrementalBuilder { ); } + /** + * Emojis are copied one file at a time, so a book only ships the Twemoji + * images it actually uses. + */ + private async copyEmojiAssets(newEmojis: string[]): Promise { + if (newEmojis.length === 0) return; + const emojiPath = path.join(__dirname, "assets", "emoji"); + const emojiOut = path.join(this.assetsOut, "emoji"); + await fs.mkdir(emojiOut, { recursive: true }); + for (const emoji of newEmojis) { + try { + await cp( + path.join(emojiPath, `${emoji}.svg`), + path.join(emojiOut, `${emoji}.svg`), + ); + } catch { + // Emoji has no asset + } + } + } + private async copyDirectiveAssets(newDirectives: string[]): Promise { const assetsPath = path.join(__dirname, "assets"); for (const directive of newDirectives) { diff --git a/packages/markdown/assets/content.css b/packages/markdown/assets/content.css index 2cf1b2976..3b42d27a3 100644 --- a/packages/markdown/assets/content.css +++ b/packages/markdown/assets/content.css @@ -32,6 +32,16 @@ figure { max-width: 100%; } +img.emoji { + height: 1em; + width: 1em; + min-width: 1em; + margin: 0 0.05em 0 0.1em; + vertical-align: -0.1em; + display: inline-block; + max-width: none; +} + .hyperbook-markdown figure.align-left { display: table; float: left; diff --git a/packages/markdown/assets/shell.css b/packages/markdown/assets/shell.css index 1d9cd791e..5d39024d9 100644 --- a/packages/markdown/assets/shell.css +++ b/packages/markdown/assets/shell.css @@ -1267,3 +1267,13 @@ nav.toc li.level-3 { .hidden { display: none !important; } + +img.emoji { + height: 1em; + width: 1em; + min-width: 1em; + margin: 0 0.05em 0 0.1em; + vertical-align: -0.1em; + display: inline-block; + max-width: none; +} diff --git a/packages/markdown/package.json b/packages/markdown/package.json index 012ff8cfb..587d22763 100644 --- a/packages/markdown/package.json +++ b/packages/markdown/package.json @@ -35,7 +35,7 @@ "build": "rimraf dist && pnpm build:pkg && pnpm build:types && node postbuild.mjs", "build:pkg": "node ../../scripts/build.mjs && ncp assets dist/assets", "build:types": "tsc --project tsconfig.build.json --declaration --emitDeclarationOnly", - "emojis:update": "node updateEmojis.mjs" + "emojis:update": "node updateEmojis.mjs && node updateEmojiAssets.mjs" }, "bundle": "all", "dependencies": { @@ -92,6 +92,7 @@ "@learningmap/web-component": "^0.3.7", "@raspberrypifoundation/python-friendly-error-messages": "^0.1.6", "@shikijs/types": "^3.21.0", + "@twemoji/svg": "15.0.0", "@types/hast": "3.0.4", "@types/js-yaml": "^4.0.9", "@types/mdast": "^4.0.4", diff --git a/packages/markdown/postbuild.mjs b/packages/markdown/postbuild.mjs index 2cb31ba26..0ceabff6a 100644 --- a/packages/markdown/postbuild.mjs +++ b/packages/markdown/postbuild.mjs @@ -6,6 +6,7 @@ import { Extract } from "unzipper"; import { createWriteStream, createReadStream } from "fs"; import { createHash } from "crypto"; import { build as esbuild } from "esbuild"; +import { createRequire } from "module"; const CACHE_DIR = path.join(".cache", "downloads"); @@ -381,6 +382,22 @@ async function postbuild() { }); } + // Copy the Twemoji SVGs, which back the "twemoji" emoji style. A build only + // copies the emojis a book actually uses into its output, so shipping the + // full set here costs nothing for the reader. + const twemojiDir = path.dirname( + createRequire(import.meta.url).resolve("@twemoji/svg/package.json"), + ); + const emojiOut = path.join("./dist", "assets", "emoji"); + await mkdir(emojiOut, { recursive: true }); + const emojiFiles = (await readdir(twemojiDir)).filter((f) => + f.endsWith(".svg"), + ); + for (const file of emojiFiles) { + await cp(path.join(twemojiDir, file), path.join(emojiOut, file)); + } + console.log(`Copied ${emojiFiles.length} emojis → ${emojiOut}`); + // Bundle CodeMirror 6 + language packages into a single IIFE for browser use. await mkdir("./dist/assets/codemirror", { recursive: true }); await esbuild({ diff --git a/packages/markdown/src/emoji-assets.json b/packages/markdown/src/emoji-assets.json new file mode 100644 index 000000000..899773924 --- /dev/null +++ b/packages/markdown/src/emoji-assets.json @@ -0,0 +1,3722 @@ +[ + "1f004", + "1f0cf", + "1f170", + "1f171", + "1f17e", + "1f17f", + "1f18e", + "1f191", + "1f192", + "1f193", + "1f194", + "1f195", + "1f196", + "1f197", + "1f198", + "1f199", + "1f19a", + "1f1e6", + "1f1e6-1f1e8", + "1f1e6-1f1e9", + "1f1e6-1f1ea", + "1f1e6-1f1eb", + "1f1e6-1f1ec", + "1f1e6-1f1ee", + "1f1e6-1f1f1", + "1f1e6-1f1f2", + "1f1e6-1f1f4", + "1f1e6-1f1f6", + "1f1e6-1f1f7", + "1f1e6-1f1f8", + "1f1e6-1f1f9", + "1f1e6-1f1fa", + "1f1e6-1f1fc", + "1f1e6-1f1fd", + "1f1e6-1f1ff", + "1f1e7", + "1f1e7-1f1e6", + "1f1e7-1f1e7", + "1f1e7-1f1e9", + "1f1e7-1f1ea", + "1f1e7-1f1eb", + "1f1e7-1f1ec", + "1f1e7-1f1ed", + "1f1e7-1f1ee", + "1f1e7-1f1ef", + "1f1e7-1f1f1", + "1f1e7-1f1f2", + "1f1e7-1f1f3", + "1f1e7-1f1f4", + "1f1e7-1f1f6", + "1f1e7-1f1f7", + "1f1e7-1f1f8", + "1f1e7-1f1f9", + "1f1e7-1f1fb", + "1f1e7-1f1fc", + "1f1e7-1f1fe", + "1f1e7-1f1ff", + "1f1e8", + "1f1e8-1f1e6", + "1f1e8-1f1e8", + "1f1e8-1f1e9", + "1f1e8-1f1eb", + "1f1e8-1f1ec", + "1f1e8-1f1ed", + "1f1e8-1f1ee", + "1f1e8-1f1f0", + "1f1e8-1f1f1", + "1f1e8-1f1f2", + "1f1e8-1f1f3", + "1f1e8-1f1f4", + "1f1e8-1f1f5", + "1f1e8-1f1f7", + "1f1e8-1f1fa", + "1f1e8-1f1fb", + "1f1e8-1f1fc", + "1f1e8-1f1fd", + "1f1e8-1f1fe", + "1f1e8-1f1ff", + "1f1e9", + "1f1e9-1f1ea", + "1f1e9-1f1ec", + "1f1e9-1f1ef", + "1f1e9-1f1f0", + "1f1e9-1f1f2", + "1f1e9-1f1f4", + "1f1e9-1f1ff", + "1f1ea", + "1f1ea-1f1e6", + "1f1ea-1f1e8", + "1f1ea-1f1ea", + "1f1ea-1f1ec", + "1f1ea-1f1ed", + "1f1ea-1f1f7", + "1f1ea-1f1f8", + "1f1ea-1f1f9", + "1f1ea-1f1fa", + "1f1eb", + "1f1eb-1f1ee", + "1f1eb-1f1ef", + "1f1eb-1f1f0", + "1f1eb-1f1f2", + "1f1eb-1f1f4", + "1f1eb-1f1f7", + "1f1ec", + "1f1ec-1f1e6", + "1f1ec-1f1e7", + "1f1ec-1f1e9", + "1f1ec-1f1ea", + "1f1ec-1f1eb", + "1f1ec-1f1ec", + "1f1ec-1f1ed", + "1f1ec-1f1ee", + "1f1ec-1f1f1", + "1f1ec-1f1f2", + "1f1ec-1f1f3", + "1f1ec-1f1f5", + "1f1ec-1f1f6", + "1f1ec-1f1f7", + "1f1ec-1f1f8", + "1f1ec-1f1f9", + "1f1ec-1f1fa", + "1f1ec-1f1fc", + "1f1ec-1f1fe", + "1f1ed", + "1f1ed-1f1f0", + "1f1ed-1f1f2", + "1f1ed-1f1f3", + "1f1ed-1f1f7", + "1f1ed-1f1f9", + "1f1ed-1f1fa", + "1f1ee", + "1f1ee-1f1e8", + "1f1ee-1f1e9", + "1f1ee-1f1ea", + "1f1ee-1f1f1", + "1f1ee-1f1f2", + "1f1ee-1f1f3", + "1f1ee-1f1f4", + "1f1ee-1f1f6", + "1f1ee-1f1f7", + "1f1ee-1f1f8", + "1f1ee-1f1f9", + "1f1ef", + "1f1ef-1f1ea", + "1f1ef-1f1f2", + "1f1ef-1f1f4", + "1f1ef-1f1f5", + "1f1f0", + "1f1f0-1f1ea", + "1f1f0-1f1ec", + "1f1f0-1f1ed", + "1f1f0-1f1ee", + "1f1f0-1f1f2", + "1f1f0-1f1f3", + "1f1f0-1f1f5", + "1f1f0-1f1f7", + "1f1f0-1f1fc", + "1f1f0-1f1fe", + "1f1f0-1f1ff", + "1f1f1", + "1f1f1-1f1e6", + "1f1f1-1f1e7", + "1f1f1-1f1e8", + "1f1f1-1f1ee", + "1f1f1-1f1f0", + "1f1f1-1f1f7", + "1f1f1-1f1f8", + "1f1f1-1f1f9", + "1f1f1-1f1fa", + "1f1f1-1f1fb", + "1f1f1-1f1fe", + "1f1f2", + "1f1f2-1f1e6", + "1f1f2-1f1e8", + "1f1f2-1f1e9", + "1f1f2-1f1ea", + "1f1f2-1f1eb", + "1f1f2-1f1ec", + "1f1f2-1f1ed", + "1f1f2-1f1f0", + "1f1f2-1f1f1", + "1f1f2-1f1f2", + "1f1f2-1f1f3", + "1f1f2-1f1f4", + "1f1f2-1f1f5", + "1f1f2-1f1f6", + "1f1f2-1f1f7", + "1f1f2-1f1f8", + "1f1f2-1f1f9", + "1f1f2-1f1fa", + "1f1f2-1f1fb", + "1f1f2-1f1fc", + "1f1f2-1f1fd", + "1f1f2-1f1fe", + "1f1f2-1f1ff", + "1f1f3", + "1f1f3-1f1e6", + "1f1f3-1f1e8", + "1f1f3-1f1ea", + "1f1f3-1f1eb", + "1f1f3-1f1ec", + "1f1f3-1f1ee", + "1f1f3-1f1f1", + "1f1f3-1f1f4", + "1f1f3-1f1f5", + "1f1f3-1f1f7", + "1f1f3-1f1fa", + "1f1f3-1f1ff", + "1f1f4", + "1f1f4-1f1f2", + "1f1f5", + "1f1f5-1f1e6", + "1f1f5-1f1ea", + "1f1f5-1f1eb", + "1f1f5-1f1ec", + "1f1f5-1f1ed", + "1f1f5-1f1f0", + "1f1f5-1f1f1", + "1f1f5-1f1f2", + "1f1f5-1f1f3", + "1f1f5-1f1f7", + "1f1f5-1f1f8", + "1f1f5-1f1f9", + "1f1f5-1f1fc", + "1f1f5-1f1fe", + "1f1f6", + "1f1f6-1f1e6", + "1f1f7", + "1f1f7-1f1ea", + "1f1f7-1f1f4", + "1f1f7-1f1f8", + "1f1f7-1f1fa", + "1f1f7-1f1fc", + "1f1f8", + "1f1f8-1f1e6", + "1f1f8-1f1e7", + "1f1f8-1f1e8", + "1f1f8-1f1e9", + "1f1f8-1f1ea", + "1f1f8-1f1ec", + "1f1f8-1f1ed", + "1f1f8-1f1ee", + "1f1f8-1f1ef", + "1f1f8-1f1f0", + "1f1f8-1f1f1", + "1f1f8-1f1f2", + "1f1f8-1f1f3", + "1f1f8-1f1f4", + "1f1f8-1f1f7", + "1f1f8-1f1f8", + "1f1f8-1f1f9", + "1f1f8-1f1fb", + "1f1f8-1f1fd", + "1f1f8-1f1fe", + "1f1f8-1f1ff", + "1f1f9", + "1f1f9-1f1e6", + "1f1f9-1f1e8", + "1f1f9-1f1e9", + "1f1f9-1f1eb", + "1f1f9-1f1ec", + "1f1f9-1f1ed", + "1f1f9-1f1ef", + "1f1f9-1f1f0", + "1f1f9-1f1f1", + "1f1f9-1f1f2", + "1f1f9-1f1f3", + "1f1f9-1f1f4", + "1f1f9-1f1f7", + "1f1f9-1f1f9", + "1f1f9-1f1fb", + "1f1f9-1f1fc", + "1f1f9-1f1ff", + "1f1fa", + "1f1fa-1f1e6", + "1f1fa-1f1ec", + "1f1fa-1f1f2", + "1f1fa-1f1f3", + "1f1fa-1f1f8", + "1f1fa-1f1fe", + "1f1fa-1f1ff", + "1f1fb", + "1f1fb-1f1e6", + "1f1fb-1f1e8", + "1f1fb-1f1ea", + "1f1fb-1f1ec", + "1f1fb-1f1ee", + "1f1fb-1f1f3", + "1f1fb-1f1fa", + "1f1fc", + "1f1fc-1f1eb", + "1f1fc-1f1f8", + "1f1fd", + "1f1fd-1f1f0", + "1f1fe", + "1f1fe-1f1ea", + "1f1fe-1f1f9", + "1f1ff", + "1f1ff-1f1e6", + "1f1ff-1f1f2", + "1f1ff-1f1fc", + "1f201", + "1f202", + "1f21a", + "1f22f", + "1f232", + "1f233", + "1f234", + "1f235", + "1f236", + "1f237", + "1f238", + "1f239", + "1f23a", + "1f250", + "1f251", + "1f300", + "1f301", + "1f302", + "1f303", + "1f304", + "1f305", + "1f306", + "1f307", + "1f308", + "1f309", + "1f30a", + "1f30b", + "1f30c", + "1f30d", + "1f30e", + "1f30f", + "1f310", + "1f311", + "1f312", + "1f313", + "1f314", + "1f315", + "1f316", + "1f317", + "1f318", + "1f319", + "1f31a", + "1f31b", + "1f31c", + "1f31d", + "1f31e", + "1f31f", + "1f320", + "1f321", + "1f324", + "1f325", + "1f326", + "1f327", + "1f328", + "1f329", + "1f32a", + "1f32b", + "1f32c", + "1f32d", + "1f32e", + "1f32f", + "1f330", + "1f331", + "1f332", + "1f333", + "1f334", + "1f335", + "1f336", + "1f337", + "1f338", + "1f339", + "1f33a", + "1f33b", + "1f33c", + "1f33d", + "1f33e", + "1f33f", + "1f340", + "1f341", + "1f342", + "1f343", + "1f344", + "1f345", + "1f346", + "1f347", + "1f348", + "1f349", + "1f34a", + "1f34b", + "1f34c", + "1f34d", + "1f34e", + "1f34f", + "1f350", + "1f351", + "1f352", + "1f353", + "1f354", + "1f355", + "1f356", + "1f357", + "1f358", + "1f359", + "1f35a", + "1f35b", + "1f35c", + "1f35d", + "1f35e", + "1f35f", + "1f360", + "1f361", + "1f362", + "1f363", + "1f364", + "1f365", + "1f366", + "1f367", + "1f368", + "1f369", + "1f36a", + "1f36b", + "1f36c", + "1f36d", + "1f36e", + "1f36f", + "1f370", + "1f371", + "1f372", + "1f373", + "1f374", + "1f375", + "1f376", + "1f377", + "1f378", + "1f379", + "1f37a", + "1f37b", + "1f37c", + "1f37d", + "1f37e", + "1f37f", + "1f380", + "1f381", + "1f382", + "1f383", + "1f384", + "1f385", + "1f385-1f3fb", + "1f385-1f3fc", + "1f385-1f3fd", + "1f385-1f3fe", + "1f385-1f3ff", + "1f386", + "1f387", + "1f388", + "1f389", + "1f38a", + "1f38b", + "1f38c", + "1f38d", + "1f38e", + "1f38f", + "1f390", + "1f391", + "1f392", + "1f393", + "1f396", + "1f397", + "1f399", + "1f39a", + "1f39b", + "1f39e", + "1f39f", + "1f3a0", + "1f3a1", + "1f3a2", + "1f3a3", + "1f3a4", + "1f3a5", + "1f3a6", + "1f3a7", + "1f3a8", + "1f3a9", + "1f3aa", + "1f3ab", + "1f3ac", + "1f3ad", + "1f3ae", + "1f3af", + "1f3b0", + "1f3b1", + "1f3b2", + "1f3b3", + "1f3b4", + "1f3b5", + "1f3b6", + "1f3b7", + "1f3b8", + "1f3b9", + "1f3ba", + "1f3bb", + "1f3bc", + "1f3bd", + "1f3be", + "1f3bf", + "1f3c0", + "1f3c1", + "1f3c2", + "1f3c2-1f3fb", + "1f3c2-1f3fc", + "1f3c2-1f3fd", + "1f3c2-1f3fe", + "1f3c2-1f3ff", + "1f3c3", + "1f3c3-1f3fb", + "1f3c3-1f3fb-200d-2640-fe0f", + "1f3c3-1f3fb-200d-2642-fe0f", + "1f3c3-1f3fc", + "1f3c3-1f3fc-200d-2640-fe0f", + "1f3c3-1f3fc-200d-2642-fe0f", + "1f3c3-1f3fd", + "1f3c3-1f3fd-200d-2640-fe0f", + "1f3c3-1f3fd-200d-2642-fe0f", + "1f3c3-1f3fe", + "1f3c3-1f3fe-200d-2640-fe0f", + "1f3c3-1f3fe-200d-2642-fe0f", + "1f3c3-1f3ff", + "1f3c3-1f3ff-200d-2640-fe0f", + "1f3c3-1f3ff-200d-2642-fe0f", + "1f3c3-200d-2640-fe0f", + "1f3c3-200d-2642-fe0f", + "1f3c4", + "1f3c4-1f3fb", + "1f3c4-1f3fb-200d-2640-fe0f", + "1f3c4-1f3fb-200d-2642-fe0f", + "1f3c4-1f3fc", + "1f3c4-1f3fc-200d-2640-fe0f", + "1f3c4-1f3fc-200d-2642-fe0f", + "1f3c4-1f3fd", + "1f3c4-1f3fd-200d-2640-fe0f", + "1f3c4-1f3fd-200d-2642-fe0f", + "1f3c4-1f3fe", + "1f3c4-1f3fe-200d-2640-fe0f", + "1f3c4-1f3fe-200d-2642-fe0f", + "1f3c4-1f3ff", + "1f3c4-1f3ff-200d-2640-fe0f", + "1f3c4-1f3ff-200d-2642-fe0f", + "1f3c4-200d-2640-fe0f", + "1f3c4-200d-2642-fe0f", + "1f3c5", + "1f3c6", + "1f3c7", + "1f3c7-1f3fb", + "1f3c7-1f3fc", + "1f3c7-1f3fd", + "1f3c7-1f3fe", + "1f3c7-1f3ff", + "1f3c8", + "1f3c9", + "1f3ca", + "1f3ca-1f3fb", + "1f3ca-1f3fb-200d-2640-fe0f", + "1f3ca-1f3fb-200d-2642-fe0f", + "1f3ca-1f3fc", + "1f3ca-1f3fc-200d-2640-fe0f", + "1f3ca-1f3fc-200d-2642-fe0f", + "1f3ca-1f3fd", + "1f3ca-1f3fd-200d-2640-fe0f", + "1f3ca-1f3fd-200d-2642-fe0f", + "1f3ca-1f3fe", + "1f3ca-1f3fe-200d-2640-fe0f", + "1f3ca-1f3fe-200d-2642-fe0f", + "1f3ca-1f3ff", + "1f3ca-1f3ff-200d-2640-fe0f", + "1f3ca-1f3ff-200d-2642-fe0f", + "1f3ca-200d-2640-fe0f", + "1f3ca-200d-2642-fe0f", + "1f3cb", + "1f3cb-1f3fb", + "1f3cb-1f3fb-200d-2640-fe0f", + "1f3cb-1f3fb-200d-2642-fe0f", + "1f3cb-1f3fc", + "1f3cb-1f3fc-200d-2640-fe0f", + "1f3cb-1f3fc-200d-2642-fe0f", + "1f3cb-1f3fd", + "1f3cb-1f3fd-200d-2640-fe0f", + "1f3cb-1f3fd-200d-2642-fe0f", + "1f3cb-1f3fe", + "1f3cb-1f3fe-200d-2640-fe0f", + "1f3cb-1f3fe-200d-2642-fe0f", + "1f3cb-1f3ff", + "1f3cb-1f3ff-200d-2640-fe0f", + "1f3cb-1f3ff-200d-2642-fe0f", + "1f3cb-fe0f-200d-2640-fe0f", + "1f3cb-fe0f-200d-2642-fe0f", + "1f3cc", + "1f3cc-1f3fb", + "1f3cc-1f3fb-200d-2640-fe0f", + "1f3cc-1f3fb-200d-2642-fe0f", + "1f3cc-1f3fc", + "1f3cc-1f3fc-200d-2640-fe0f", + "1f3cc-1f3fc-200d-2642-fe0f", + "1f3cc-1f3fd", + "1f3cc-1f3fd-200d-2640-fe0f", + "1f3cc-1f3fd-200d-2642-fe0f", + "1f3cc-1f3fe", + "1f3cc-1f3fe-200d-2640-fe0f", + "1f3cc-1f3fe-200d-2642-fe0f", + "1f3cc-1f3ff", + "1f3cc-1f3ff-200d-2640-fe0f", + "1f3cc-1f3ff-200d-2642-fe0f", + "1f3cc-fe0f-200d-2640-fe0f", + "1f3cc-fe0f-200d-2642-fe0f", + "1f3cd", + "1f3ce", + "1f3cf", + "1f3d0", + "1f3d1", + "1f3d2", + "1f3d3", + "1f3d4", + "1f3d5", + "1f3d6", + "1f3d7", + "1f3d8", + "1f3d9", + "1f3da", + "1f3db", + "1f3dc", + "1f3dd", + "1f3de", + "1f3df", + "1f3e0", + "1f3e1", + "1f3e2", + "1f3e3", + "1f3e4", + "1f3e5", + "1f3e6", + "1f3e7", + "1f3e8", + "1f3e9", + "1f3ea", + "1f3eb", + "1f3ec", + "1f3ed", + "1f3ee", + "1f3ef", + "1f3f0", + "1f3f3", + "1f3f3-fe0f-200d-1f308", + "1f3f3-fe0f-200d-26a7-fe0f", + "1f3f4", + "1f3f4-200d-2620-fe0f", + "1f3f4-e0067-e0062-e0065-e006e-e0067-e007f", + "1f3f4-e0067-e0062-e0073-e0063-e0074-e007f", + "1f3f4-e0067-e0062-e0077-e006c-e0073-e007f", + "1f3f5", + "1f3f7", + "1f3f8", + "1f3f9", + "1f3fa", + "1f3fb", + "1f3fc", + "1f3fd", + "1f3fe", + "1f3ff", + "1f400", + "1f401", + "1f402", + "1f403", + "1f404", + "1f405", + "1f406", + "1f407", + "1f408", + "1f408-200d-2b1b", + "1f409", + "1f40a", + "1f40b", + "1f40c", + "1f40d", + "1f40e", + "1f40f", + "1f410", + "1f411", + "1f412", + "1f413", + "1f414", + "1f415", + "1f415-200d-1f9ba", + "1f416", + "1f417", + "1f418", + "1f419", + "1f41a", + "1f41b", + "1f41c", + "1f41d", + "1f41e", + "1f41f", + "1f420", + "1f421", + "1f422", + "1f423", + "1f424", + "1f425", + "1f426", + "1f426-200d-2b1b", + "1f427", + "1f428", + "1f429", + "1f42a", + "1f42b", + "1f42c", + "1f42d", + "1f42e", + "1f42f", + "1f430", + "1f431", + "1f432", + "1f433", + "1f434", + "1f435", + "1f436", + "1f437", + "1f438", + "1f439", + "1f43a", + "1f43b", + "1f43b-200d-2744-fe0f", + "1f43c", + "1f43d", + "1f43e", + "1f43f", + "1f440", + "1f441", + "1f441-200d-1f5e8", + "1f442", + "1f442-1f3fb", + "1f442-1f3fc", + "1f442-1f3fd", + "1f442-1f3fe", + "1f442-1f3ff", + "1f443", + "1f443-1f3fb", + "1f443-1f3fc", + "1f443-1f3fd", + "1f443-1f3fe", + "1f443-1f3ff", + "1f444", + "1f445", + "1f446", + "1f446-1f3fb", + "1f446-1f3fc", + "1f446-1f3fd", + "1f446-1f3fe", + "1f446-1f3ff", + "1f447", + "1f447-1f3fb", + "1f447-1f3fc", + "1f447-1f3fd", + "1f447-1f3fe", + "1f447-1f3ff", + "1f448", + "1f448-1f3fb", + "1f448-1f3fc", + "1f448-1f3fd", + "1f448-1f3fe", + "1f448-1f3ff", + "1f449", + "1f449-1f3fb", + "1f449-1f3fc", + "1f449-1f3fd", + "1f449-1f3fe", + "1f449-1f3ff", + "1f44a", + "1f44a-1f3fb", + "1f44a-1f3fc", + "1f44a-1f3fd", + "1f44a-1f3fe", + "1f44a-1f3ff", + "1f44b", + "1f44b-1f3fb", + "1f44b-1f3fc", + "1f44b-1f3fd", + "1f44b-1f3fe", + "1f44b-1f3ff", + "1f44c", + "1f44c-1f3fb", + "1f44c-1f3fc", + "1f44c-1f3fd", + "1f44c-1f3fe", + "1f44c-1f3ff", + "1f44d", + "1f44d-1f3fb", + "1f44d-1f3fc", + "1f44d-1f3fd", + "1f44d-1f3fe", + "1f44d-1f3ff", + "1f44e", + "1f44e-1f3fb", + "1f44e-1f3fc", + "1f44e-1f3fd", + "1f44e-1f3fe", + "1f44e-1f3ff", + "1f44f", + "1f44f-1f3fb", + "1f44f-1f3fc", + "1f44f-1f3fd", + "1f44f-1f3fe", + "1f44f-1f3ff", + "1f450", + "1f450-1f3fb", + "1f450-1f3fc", + "1f450-1f3fd", + "1f450-1f3fe", + "1f450-1f3ff", + "1f451", + "1f452", + "1f453", + "1f454", + "1f455", + "1f456", + "1f457", + "1f458", + "1f459", + "1f45a", + "1f45b", + "1f45c", + "1f45d", + "1f45e", + "1f45f", + "1f460", + "1f461", + "1f462", + "1f463", + "1f464", + "1f465", + "1f466", + "1f466-1f3fb", + "1f466-1f3fc", + "1f466-1f3fd", + "1f466-1f3fe", + "1f466-1f3ff", + "1f467", + "1f467-1f3fb", + "1f467-1f3fc", + "1f467-1f3fd", + "1f467-1f3fe", + "1f467-1f3ff", + "1f468", + "1f468-1f3fb", + "1f468-1f3fb-200d-1f33e", + "1f468-1f3fb-200d-1f373", + "1f468-1f3fb-200d-1f37c", + "1f468-1f3fb-200d-1f384", + "1f468-1f3fb-200d-1f393", + "1f468-1f3fb-200d-1f3a4", + "1f468-1f3fb-200d-1f3a8", + "1f468-1f3fb-200d-1f3eb", + "1f468-1f3fb-200d-1f3ed", + "1f468-1f3fb-200d-1f4bb", + "1f468-1f3fb-200d-1f4bc", + "1f468-1f3fb-200d-1f527", + "1f468-1f3fb-200d-1f52c", + "1f468-1f3fb-200d-1f680", + "1f468-1f3fb-200d-1f692", + "1f468-1f3fb-200d-1f91d-200d-1f468-1f3fc", + "1f468-1f3fb-200d-1f91d-200d-1f468-1f3fd", + "1f468-1f3fb-200d-1f91d-200d-1f468-1f3fe", + "1f468-1f3fb-200d-1f91d-200d-1f468-1f3ff", + "1f468-1f3fb-200d-1f9af", + "1f468-1f3fb-200d-1f9b0", + "1f468-1f3fb-200d-1f9b1", + "1f468-1f3fb-200d-1f9b2", + "1f468-1f3fb-200d-1f9b3", + "1f468-1f3fb-200d-1f9bc", + "1f468-1f3fb-200d-1f9bd", + "1f468-1f3fb-200d-2695-fe0f", + "1f468-1f3fb-200d-2696-fe0f", + "1f468-1f3fb-200d-2708-fe0f", + "1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fb", + "1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fc", + "1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fd", + "1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fe", + "1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3ff", + "1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb", + "1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc", + "1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd", + "1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe", + "1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff", + "1f468-1f3fc", + "1f468-1f3fc-200d-1f33e", + "1f468-1f3fc-200d-1f373", + "1f468-1f3fc-200d-1f37c", + "1f468-1f3fc-200d-1f384", + "1f468-1f3fc-200d-1f393", + "1f468-1f3fc-200d-1f3a4", + "1f468-1f3fc-200d-1f3a8", + "1f468-1f3fc-200d-1f3eb", + "1f468-1f3fc-200d-1f3ed", + "1f468-1f3fc-200d-1f4bb", + "1f468-1f3fc-200d-1f4bc", + "1f468-1f3fc-200d-1f527", + "1f468-1f3fc-200d-1f52c", + "1f468-1f3fc-200d-1f680", + "1f468-1f3fc-200d-1f692", + "1f468-1f3fc-200d-1f91d-200d-1f468-1f3fb", + "1f468-1f3fc-200d-1f91d-200d-1f468-1f3fd", + "1f468-1f3fc-200d-1f91d-200d-1f468-1f3fe", + "1f468-1f3fc-200d-1f91d-200d-1f468-1f3ff", + "1f468-1f3fc-200d-1f9af", + "1f468-1f3fc-200d-1f9b0", + "1f468-1f3fc-200d-1f9b1", + "1f468-1f3fc-200d-1f9b2", + "1f468-1f3fc-200d-1f9b3", + "1f468-1f3fc-200d-1f9bc", + "1f468-1f3fc-200d-1f9bd", + "1f468-1f3fc-200d-2695-fe0f", + "1f468-1f3fc-200d-2696-fe0f", + "1f468-1f3fc-200d-2708-fe0f", + "1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fb", + "1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fc", + "1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fd", + "1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fe", + "1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3ff", + "1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb", + "1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc", + "1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd", + "1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe", + "1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff", + "1f468-1f3fd", + "1f468-1f3fd-200d-1f33e", + "1f468-1f3fd-200d-1f373", + "1f468-1f3fd-200d-1f37c", + "1f468-1f3fd-200d-1f384", + "1f468-1f3fd-200d-1f393", + "1f468-1f3fd-200d-1f3a4", + "1f468-1f3fd-200d-1f3a8", + "1f468-1f3fd-200d-1f3eb", + "1f468-1f3fd-200d-1f3ed", + "1f468-1f3fd-200d-1f4bb", + "1f468-1f3fd-200d-1f4bc", + "1f468-1f3fd-200d-1f527", + "1f468-1f3fd-200d-1f52c", + "1f468-1f3fd-200d-1f680", + "1f468-1f3fd-200d-1f692", + "1f468-1f3fd-200d-1f91d-200d-1f468-1f3fb", + "1f468-1f3fd-200d-1f91d-200d-1f468-1f3fc", + "1f468-1f3fd-200d-1f91d-200d-1f468-1f3fe", + "1f468-1f3fd-200d-1f91d-200d-1f468-1f3ff", + "1f468-1f3fd-200d-1f9af", + "1f468-1f3fd-200d-1f9b0", + "1f468-1f3fd-200d-1f9b1", + "1f468-1f3fd-200d-1f9b2", + "1f468-1f3fd-200d-1f9b3", + "1f468-1f3fd-200d-1f9bc", + "1f468-1f3fd-200d-1f9bd", + "1f468-1f3fd-200d-2695-fe0f", + "1f468-1f3fd-200d-2696-fe0f", + "1f468-1f3fd-200d-2708-fe0f", + "1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fb", + "1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fc", + "1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fd", + "1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fe", + "1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3ff", + "1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb", + "1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc", + "1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd", + "1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe", + "1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff", + "1f468-1f3fe", + "1f468-1f3fe-200d-1f33e", + "1f468-1f3fe-200d-1f373", + "1f468-1f3fe-200d-1f37c", + "1f468-1f3fe-200d-1f384", + "1f468-1f3fe-200d-1f393", + "1f468-1f3fe-200d-1f3a4", + "1f468-1f3fe-200d-1f3a8", + "1f468-1f3fe-200d-1f3eb", + "1f468-1f3fe-200d-1f3ed", + "1f468-1f3fe-200d-1f4bb", + "1f468-1f3fe-200d-1f4bc", + "1f468-1f3fe-200d-1f527", + "1f468-1f3fe-200d-1f52c", + "1f468-1f3fe-200d-1f680", + "1f468-1f3fe-200d-1f692", + "1f468-1f3fe-200d-1f91d-200d-1f468-1f3fb", + "1f468-1f3fe-200d-1f91d-200d-1f468-1f3fc", + "1f468-1f3fe-200d-1f91d-200d-1f468-1f3fd", + "1f468-1f3fe-200d-1f91d-200d-1f468-1f3ff", + "1f468-1f3fe-200d-1f9af", + "1f468-1f3fe-200d-1f9b0", + "1f468-1f3fe-200d-1f9b1", + "1f468-1f3fe-200d-1f9b2", + "1f468-1f3fe-200d-1f9b3", + "1f468-1f3fe-200d-1f9bc", + "1f468-1f3fe-200d-1f9bd", + "1f468-1f3fe-200d-2695-fe0f", + "1f468-1f3fe-200d-2696-fe0f", + "1f468-1f3fe-200d-2708-fe0f", + "1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fb", + "1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fc", + "1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fd", + "1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fe", + "1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3ff", + "1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb", + "1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc", + "1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd", + "1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe", + "1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff", + "1f468-1f3ff", + "1f468-1f3ff-200d-1f33e", + "1f468-1f3ff-200d-1f373", + "1f468-1f3ff-200d-1f37c", + "1f468-1f3ff-200d-1f384", + "1f468-1f3ff-200d-1f393", + "1f468-1f3ff-200d-1f3a4", + "1f468-1f3ff-200d-1f3a8", + "1f468-1f3ff-200d-1f3eb", + "1f468-1f3ff-200d-1f3ed", + "1f468-1f3ff-200d-1f4bb", + "1f468-1f3ff-200d-1f4bc", + "1f468-1f3ff-200d-1f527", + "1f468-1f3ff-200d-1f52c", + "1f468-1f3ff-200d-1f680", + "1f468-1f3ff-200d-1f692", + "1f468-1f3ff-200d-1f91d-200d-1f468-1f3fb", + "1f468-1f3ff-200d-1f91d-200d-1f468-1f3fc", + "1f468-1f3ff-200d-1f91d-200d-1f468-1f3fd", + "1f468-1f3ff-200d-1f91d-200d-1f468-1f3fe", + "1f468-1f3ff-200d-1f9af", + "1f468-1f3ff-200d-1f9b0", + "1f468-1f3ff-200d-1f9b1", + "1f468-1f3ff-200d-1f9b2", + "1f468-1f3ff-200d-1f9b3", + "1f468-1f3ff-200d-1f9bc", + "1f468-1f3ff-200d-1f9bd", + "1f468-1f3ff-200d-2695-fe0f", + "1f468-1f3ff-200d-2696-fe0f", + "1f468-1f3ff-200d-2708-fe0f", + "1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fb", + "1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fc", + "1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fd", + "1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fe", + "1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3ff", + "1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb", + "1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc", + "1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd", + "1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe", + "1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff", + "1f468-200d-1f33e", + "1f468-200d-1f373", + "1f468-200d-1f37c", + "1f468-200d-1f384", + "1f468-200d-1f393", + "1f468-200d-1f3a4", + "1f468-200d-1f3a8", + "1f468-200d-1f3eb", + "1f468-200d-1f3ed", + "1f468-200d-1f466", + "1f468-200d-1f466-200d-1f466", + "1f468-200d-1f467", + "1f468-200d-1f467-200d-1f466", + "1f468-200d-1f467-200d-1f467", + "1f468-200d-1f468-200d-1f466", + "1f468-200d-1f468-200d-1f466-200d-1f466", + "1f468-200d-1f468-200d-1f467", + "1f468-200d-1f468-200d-1f467-200d-1f466", + "1f468-200d-1f468-200d-1f467-200d-1f467", + "1f468-200d-1f469-200d-1f466", + "1f468-200d-1f469-200d-1f466-200d-1f466", + "1f468-200d-1f469-200d-1f467", + "1f468-200d-1f469-200d-1f467-200d-1f466", + "1f468-200d-1f469-200d-1f467-200d-1f467", + "1f468-200d-1f4bb", + "1f468-200d-1f4bc", + "1f468-200d-1f527", + "1f468-200d-1f52c", + "1f468-200d-1f680", + "1f468-200d-1f692", + "1f468-200d-1f9af", + "1f468-200d-1f9b0", + "1f468-200d-1f9b1", + "1f468-200d-1f9b2", + "1f468-200d-1f9b3", + "1f468-200d-1f9bc", + "1f468-200d-1f9bd", + "1f468-200d-2695-fe0f", + "1f468-200d-2696-fe0f", + "1f468-200d-2708-fe0f", + "1f468-200d-2764-fe0f-200d-1f468", + "1f468-200d-2764-fe0f-200d-1f48b-200d-1f468", + "1f469", + "1f469-1f3fb", + "1f469-1f3fb-200d-1f33e", + "1f469-1f3fb-200d-1f373", + "1f469-1f3fb-200d-1f37c", + "1f469-1f3fb-200d-1f384", + "1f469-1f3fb-200d-1f393", + "1f469-1f3fb-200d-1f3a4", + "1f469-1f3fb-200d-1f3a8", + "1f469-1f3fb-200d-1f3eb", + "1f469-1f3fb-200d-1f3ed", + "1f469-1f3fb-200d-1f4bb", + "1f469-1f3fb-200d-1f4bc", + "1f469-1f3fb-200d-1f527", + "1f469-1f3fb-200d-1f52c", + "1f469-1f3fb-200d-1f680", + "1f469-1f3fb-200d-1f692", + "1f469-1f3fb-200d-1f91d-200d-1f468-1f3fc", + "1f469-1f3fb-200d-1f91d-200d-1f468-1f3fd", + "1f469-1f3fb-200d-1f91d-200d-1f468-1f3fe", + "1f469-1f3fb-200d-1f91d-200d-1f468-1f3ff", + "1f469-1f3fb-200d-1f91d-200d-1f469-1f3fc", + "1f469-1f3fb-200d-1f91d-200d-1f469-1f3fd", + "1f469-1f3fb-200d-1f91d-200d-1f469-1f3fe", + "1f469-1f3fb-200d-1f91d-200d-1f469-1f3ff", + "1f469-1f3fb-200d-1f9af", + "1f469-1f3fb-200d-1f9b0", + "1f469-1f3fb-200d-1f9b1", + "1f469-1f3fb-200d-1f9b2", + "1f469-1f3fb-200d-1f9b3", + "1f469-1f3fb-200d-1f9bc", + "1f469-1f3fb-200d-1f9bd", + "1f469-1f3fb-200d-2695-fe0f", + "1f469-1f3fb-200d-2696-fe0f", + "1f469-1f3fb-200d-2708-fe0f", + "1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fb", + "1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fc", + "1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fd", + "1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fe", + "1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3ff", + "1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fb", + "1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fc", + "1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fd", + "1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fe", + "1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3ff", + "1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb", + "1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc", + "1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd", + "1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe", + "1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff", + "1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb", + "1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc", + "1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd", + "1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe", + "1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff", + "1f469-1f3fc", + "1f469-1f3fc-200d-1f33e", + "1f469-1f3fc-200d-1f373", + "1f469-1f3fc-200d-1f37c", + "1f469-1f3fc-200d-1f384", + "1f469-1f3fc-200d-1f393", + "1f469-1f3fc-200d-1f3a4", + "1f469-1f3fc-200d-1f3a8", + "1f469-1f3fc-200d-1f3eb", + "1f469-1f3fc-200d-1f3ed", + "1f469-1f3fc-200d-1f4bb", + "1f469-1f3fc-200d-1f4bc", + "1f469-1f3fc-200d-1f527", + "1f469-1f3fc-200d-1f52c", + "1f469-1f3fc-200d-1f680", + "1f469-1f3fc-200d-1f692", + "1f469-1f3fc-200d-1f91d-200d-1f468-1f3fb", + "1f469-1f3fc-200d-1f91d-200d-1f468-1f3fd", + "1f469-1f3fc-200d-1f91d-200d-1f468-1f3fe", + "1f469-1f3fc-200d-1f91d-200d-1f468-1f3ff", + "1f469-1f3fc-200d-1f91d-200d-1f469-1f3fb", + "1f469-1f3fc-200d-1f91d-200d-1f469-1f3fd", + "1f469-1f3fc-200d-1f91d-200d-1f469-1f3fe", + "1f469-1f3fc-200d-1f91d-200d-1f469-1f3ff", + "1f469-1f3fc-200d-1f9af", + "1f469-1f3fc-200d-1f9b0", + "1f469-1f3fc-200d-1f9b1", + "1f469-1f3fc-200d-1f9b2", + "1f469-1f3fc-200d-1f9b3", + "1f469-1f3fc-200d-1f9bc", + "1f469-1f3fc-200d-1f9bd", + "1f469-1f3fc-200d-2695-fe0f", + "1f469-1f3fc-200d-2696-fe0f", + "1f469-1f3fc-200d-2708-fe0f", + "1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fb", + "1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fc", + "1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fd", + "1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fe", + "1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3ff", + "1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fb", + "1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fc", + "1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fd", + "1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fe", + "1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3ff", + "1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb", + "1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc", + "1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd", + "1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe", + "1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff", + "1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb", + "1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc", + "1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd", + "1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe", + "1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff", + "1f469-1f3fd", + "1f469-1f3fd-200d-1f33e", + "1f469-1f3fd-200d-1f373", + "1f469-1f3fd-200d-1f37c", + "1f469-1f3fd-200d-1f384", + "1f469-1f3fd-200d-1f393", + "1f469-1f3fd-200d-1f3a4", + "1f469-1f3fd-200d-1f3a8", + "1f469-1f3fd-200d-1f3eb", + "1f469-1f3fd-200d-1f3ed", + "1f469-1f3fd-200d-1f4bb", + "1f469-1f3fd-200d-1f4bc", + "1f469-1f3fd-200d-1f527", + "1f469-1f3fd-200d-1f52c", + "1f469-1f3fd-200d-1f680", + "1f469-1f3fd-200d-1f692", + "1f469-1f3fd-200d-1f91d-200d-1f468-1f3fb", + "1f469-1f3fd-200d-1f91d-200d-1f468-1f3fc", + "1f469-1f3fd-200d-1f91d-200d-1f468-1f3fe", + "1f469-1f3fd-200d-1f91d-200d-1f468-1f3ff", + "1f469-1f3fd-200d-1f91d-200d-1f469-1f3fb", + "1f469-1f3fd-200d-1f91d-200d-1f469-1f3fc", + "1f469-1f3fd-200d-1f91d-200d-1f469-1f3fe", + "1f469-1f3fd-200d-1f91d-200d-1f469-1f3ff", + "1f469-1f3fd-200d-1f9af", + "1f469-1f3fd-200d-1f9b0", + "1f469-1f3fd-200d-1f9b1", + "1f469-1f3fd-200d-1f9b2", + "1f469-1f3fd-200d-1f9b3", + "1f469-1f3fd-200d-1f9bc", + "1f469-1f3fd-200d-1f9bd", + "1f469-1f3fd-200d-2695-fe0f", + "1f469-1f3fd-200d-2696-fe0f", + "1f469-1f3fd-200d-2708-fe0f", + "1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fb", + "1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fc", + "1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fd", + "1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fe", + "1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3ff", + "1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fb", + "1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fc", + "1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fd", + "1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fe", + "1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3ff", + "1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb", + "1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc", + "1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd", + "1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe", + "1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff", + "1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb", + "1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc", + "1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd", + "1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe", + "1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff", + "1f469-1f3fe", + "1f469-1f3fe-200d-1f33e", + "1f469-1f3fe-200d-1f373", + "1f469-1f3fe-200d-1f37c", + "1f469-1f3fe-200d-1f384", + "1f469-1f3fe-200d-1f393", + "1f469-1f3fe-200d-1f3a4", + "1f469-1f3fe-200d-1f3a8", + "1f469-1f3fe-200d-1f3eb", + "1f469-1f3fe-200d-1f3ed", + "1f469-1f3fe-200d-1f4bb", + "1f469-1f3fe-200d-1f4bc", + "1f469-1f3fe-200d-1f527", + "1f469-1f3fe-200d-1f52c", + "1f469-1f3fe-200d-1f680", + "1f469-1f3fe-200d-1f692", + "1f469-1f3fe-200d-1f91d-200d-1f468-1f3fb", + "1f469-1f3fe-200d-1f91d-200d-1f468-1f3fc", + "1f469-1f3fe-200d-1f91d-200d-1f468-1f3fd", + "1f469-1f3fe-200d-1f91d-200d-1f468-1f3ff", + "1f469-1f3fe-200d-1f91d-200d-1f469-1f3fb", + "1f469-1f3fe-200d-1f91d-200d-1f469-1f3fc", + "1f469-1f3fe-200d-1f91d-200d-1f469-1f3fd", + "1f469-1f3fe-200d-1f91d-200d-1f469-1f3ff", + "1f469-1f3fe-200d-1f9af", + "1f469-1f3fe-200d-1f9b0", + "1f469-1f3fe-200d-1f9b1", + "1f469-1f3fe-200d-1f9b2", + "1f469-1f3fe-200d-1f9b3", + "1f469-1f3fe-200d-1f9bc", + "1f469-1f3fe-200d-1f9bd", + "1f469-1f3fe-200d-2695-fe0f", + "1f469-1f3fe-200d-2696-fe0f", + "1f469-1f3fe-200d-2708-fe0f", + "1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fb", + "1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fc", + "1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fd", + "1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fe", + "1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3ff", + "1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fb", + "1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fc", + "1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fd", + "1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fe", + "1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3ff", + "1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb", + "1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc", + "1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd", + "1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe", + "1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff", + "1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb", + "1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc", + "1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd", + "1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe", + "1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff", + "1f469-1f3ff", + "1f469-1f3ff-200d-1f33e", + "1f469-1f3ff-200d-1f373", + "1f469-1f3ff-200d-1f37c", + "1f469-1f3ff-200d-1f384", + "1f469-1f3ff-200d-1f393", + "1f469-1f3ff-200d-1f3a4", + "1f469-1f3ff-200d-1f3a8", + "1f469-1f3ff-200d-1f3eb", + "1f469-1f3ff-200d-1f3ed", + "1f469-1f3ff-200d-1f4bb", + "1f469-1f3ff-200d-1f4bc", + "1f469-1f3ff-200d-1f527", + "1f469-1f3ff-200d-1f52c", + "1f469-1f3ff-200d-1f680", + "1f469-1f3ff-200d-1f692", + "1f469-1f3ff-200d-1f91d-200d-1f468-1f3fb", + "1f469-1f3ff-200d-1f91d-200d-1f468-1f3fc", + "1f469-1f3ff-200d-1f91d-200d-1f468-1f3fd", + "1f469-1f3ff-200d-1f91d-200d-1f468-1f3fe", + "1f469-1f3ff-200d-1f91d-200d-1f469-1f3fb", + "1f469-1f3ff-200d-1f91d-200d-1f469-1f3fc", + "1f469-1f3ff-200d-1f91d-200d-1f469-1f3fd", + "1f469-1f3ff-200d-1f91d-200d-1f469-1f3fe", + "1f469-1f3ff-200d-1f9af", + "1f469-1f3ff-200d-1f9b0", + "1f469-1f3ff-200d-1f9b1", + "1f469-1f3ff-200d-1f9b2", + "1f469-1f3ff-200d-1f9b3", + "1f469-1f3ff-200d-1f9bc", + "1f469-1f3ff-200d-1f9bd", + "1f469-1f3ff-200d-2695-fe0f", + "1f469-1f3ff-200d-2696-fe0f", + "1f469-1f3ff-200d-2708-fe0f", + "1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fb", + "1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fc", + "1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fd", + "1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fe", + "1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3ff", + "1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fb", + "1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fc", + "1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fd", + "1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fe", + "1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3ff", + "1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb", + "1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc", + "1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd", + "1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe", + "1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff", + "1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb", + "1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc", + "1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd", + "1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe", + "1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff", + "1f469-200d-1f33e", + "1f469-200d-1f373", + "1f469-200d-1f37c", + "1f469-200d-1f384", + "1f469-200d-1f393", + "1f469-200d-1f3a4", + "1f469-200d-1f3a8", + "1f469-200d-1f3eb", + "1f469-200d-1f3ed", + "1f469-200d-1f466", + "1f469-200d-1f466-200d-1f466", + "1f469-200d-1f467", + "1f469-200d-1f467-200d-1f466", + "1f469-200d-1f467-200d-1f467", + "1f469-200d-1f469-200d-1f466", + "1f469-200d-1f469-200d-1f466-200d-1f466", + "1f469-200d-1f469-200d-1f467", + "1f469-200d-1f469-200d-1f467-200d-1f466", + "1f469-200d-1f469-200d-1f467-200d-1f467", + "1f469-200d-1f4bb", + "1f469-200d-1f4bc", + "1f469-200d-1f527", + "1f469-200d-1f52c", + "1f469-200d-1f680", + "1f469-200d-1f692", + "1f469-200d-1f9af", + "1f469-200d-1f9b0", + "1f469-200d-1f9b1", + "1f469-200d-1f9b2", + "1f469-200d-1f9b3", + "1f469-200d-1f9bc", + "1f469-200d-1f9bd", + "1f469-200d-2695-fe0f", + "1f469-200d-2696-fe0f", + "1f469-200d-2708-fe0f", + "1f469-200d-2764-fe0f-200d-1f468", + "1f469-200d-2764-fe0f-200d-1f469", + "1f469-200d-2764-fe0f-200d-1f48b-200d-1f468", + "1f469-200d-2764-fe0f-200d-1f48b-200d-1f469", + "1f46a", + "1f46b", + "1f46b-1f3fb", + "1f46b-1f3fc", + "1f46b-1f3fd", + "1f46b-1f3fe", + "1f46b-1f3ff", + "1f46c", + "1f46c-1f3fb", + "1f46c-1f3fc", + "1f46c-1f3fd", + "1f46c-1f3fe", + "1f46c-1f3ff", + "1f46d", + "1f46d-1f3fb", + "1f46d-1f3fc", + "1f46d-1f3fd", + "1f46d-1f3fe", + "1f46d-1f3ff", + "1f46e", + "1f46e-1f3fb", + "1f46e-1f3fb-200d-2640-fe0f", + "1f46e-1f3fb-200d-2642-fe0f", + "1f46e-1f3fc", + "1f46e-1f3fc-200d-2640-fe0f", + "1f46e-1f3fc-200d-2642-fe0f", + "1f46e-1f3fd", + "1f46e-1f3fd-200d-2640-fe0f", + "1f46e-1f3fd-200d-2642-fe0f", + "1f46e-1f3fe", + "1f46e-1f3fe-200d-2640-fe0f", + "1f46e-1f3fe-200d-2642-fe0f", + "1f46e-1f3ff", + "1f46e-1f3ff-200d-2640-fe0f", + "1f46e-1f3ff-200d-2642-fe0f", + "1f46e-200d-2640-fe0f", + "1f46e-200d-2642-fe0f", + "1f46f", + "1f46f-200d-2640-fe0f", + "1f46f-200d-2642-fe0f", + "1f470", + "1f470-1f3fb", + "1f470-1f3fb-200d-2640-fe0f", + "1f470-1f3fb-200d-2642-fe0f", + "1f470-1f3fc", + "1f470-1f3fc-200d-2640-fe0f", + "1f470-1f3fc-200d-2642-fe0f", + "1f470-1f3fd", + "1f470-1f3fd-200d-2640-fe0f", + "1f470-1f3fd-200d-2642-fe0f", + "1f470-1f3fe", + "1f470-1f3fe-200d-2640-fe0f", + "1f470-1f3fe-200d-2642-fe0f", + "1f470-1f3ff", + "1f470-1f3ff-200d-2640-fe0f", + "1f470-1f3ff-200d-2642-fe0f", + "1f470-200d-2640-fe0f", + "1f470-200d-2642-fe0f", + "1f471", + "1f471-1f3fb", + "1f471-1f3fb-200d-2640-fe0f", + "1f471-1f3fb-200d-2642-fe0f", + "1f471-1f3fc", + "1f471-1f3fc-200d-2640-fe0f", + "1f471-1f3fc-200d-2642-fe0f", + "1f471-1f3fd", + "1f471-1f3fd-200d-2640-fe0f", + "1f471-1f3fd-200d-2642-fe0f", + "1f471-1f3fe", + "1f471-1f3fe-200d-2640-fe0f", + "1f471-1f3fe-200d-2642-fe0f", + "1f471-1f3ff", + "1f471-1f3ff-200d-2640-fe0f", + "1f471-1f3ff-200d-2642-fe0f", + "1f471-200d-2640-fe0f", + "1f471-200d-2642-fe0f", + "1f472", + "1f472-1f3fb", + "1f472-1f3fc", + "1f472-1f3fd", + "1f472-1f3fe", + "1f472-1f3ff", + "1f473", + "1f473-1f3fb", + "1f473-1f3fb-200d-2640-fe0f", + "1f473-1f3fb-200d-2642-fe0f", + "1f473-1f3fc", + "1f473-1f3fc-200d-2640-fe0f", + "1f473-1f3fc-200d-2642-fe0f", + "1f473-1f3fd", + "1f473-1f3fd-200d-2640-fe0f", + "1f473-1f3fd-200d-2642-fe0f", + "1f473-1f3fe", + "1f473-1f3fe-200d-2640-fe0f", + "1f473-1f3fe-200d-2642-fe0f", + "1f473-1f3ff", + "1f473-1f3ff-200d-2640-fe0f", + "1f473-1f3ff-200d-2642-fe0f", + "1f473-200d-2640-fe0f", + "1f473-200d-2642-fe0f", + "1f474", + "1f474-1f3fb", + "1f474-1f3fc", + "1f474-1f3fd", + "1f474-1f3fe", + "1f474-1f3ff", + "1f475", + "1f475-1f3fb", + "1f475-1f3fc", + "1f475-1f3fd", + "1f475-1f3fe", + "1f475-1f3ff", + "1f476", + "1f476-1f3fb", + "1f476-1f3fc", + "1f476-1f3fd", + "1f476-1f3fe", + "1f476-1f3ff", + "1f477", + "1f477-1f3fb", + "1f477-1f3fb-200d-2640-fe0f", + "1f477-1f3fb-200d-2642-fe0f", + "1f477-1f3fc", + "1f477-1f3fc-200d-2640-fe0f", + "1f477-1f3fc-200d-2642-fe0f", + "1f477-1f3fd", + "1f477-1f3fd-200d-2640-fe0f", + "1f477-1f3fd-200d-2642-fe0f", + "1f477-1f3fe", + "1f477-1f3fe-200d-2640-fe0f", + "1f477-1f3fe-200d-2642-fe0f", + "1f477-1f3ff", + "1f477-1f3ff-200d-2640-fe0f", + "1f477-1f3ff-200d-2642-fe0f", + "1f477-200d-2640-fe0f", + "1f477-200d-2642-fe0f", + "1f478", + "1f478-1f3fb", + "1f478-1f3fc", + "1f478-1f3fd", + "1f478-1f3fe", + "1f478-1f3ff", + "1f479", + "1f47a", + "1f47b", + "1f47c", + "1f47c-1f3fb", + "1f47c-1f3fc", + "1f47c-1f3fd", + "1f47c-1f3fe", + "1f47c-1f3ff", + "1f47d", + "1f47e", + "1f47f", + "1f480", + "1f481", + "1f481-1f3fb", + "1f481-1f3fb-200d-2640-fe0f", + "1f481-1f3fb-200d-2642-fe0f", + "1f481-1f3fc", + "1f481-1f3fc-200d-2640-fe0f", + "1f481-1f3fc-200d-2642-fe0f", + "1f481-1f3fd", + "1f481-1f3fd-200d-2640-fe0f", + "1f481-1f3fd-200d-2642-fe0f", + "1f481-1f3fe", + "1f481-1f3fe-200d-2640-fe0f", + "1f481-1f3fe-200d-2642-fe0f", + "1f481-1f3ff", + "1f481-1f3ff-200d-2640-fe0f", + "1f481-1f3ff-200d-2642-fe0f", + "1f481-200d-2640-fe0f", + "1f481-200d-2642-fe0f", + "1f482", + "1f482-1f3fb", + "1f482-1f3fb-200d-2640-fe0f", + "1f482-1f3fb-200d-2642-fe0f", + "1f482-1f3fc", + "1f482-1f3fc-200d-2640-fe0f", + "1f482-1f3fc-200d-2642-fe0f", + "1f482-1f3fd", + "1f482-1f3fd-200d-2640-fe0f", + "1f482-1f3fd-200d-2642-fe0f", + "1f482-1f3fe", + "1f482-1f3fe-200d-2640-fe0f", + "1f482-1f3fe-200d-2642-fe0f", + "1f482-1f3ff", + "1f482-1f3ff-200d-2640-fe0f", + "1f482-1f3ff-200d-2642-fe0f", + "1f482-200d-2640-fe0f", + "1f482-200d-2642-fe0f", + "1f483", + "1f483-1f3fb", + "1f483-1f3fc", + "1f483-1f3fd", + "1f483-1f3fe", + "1f483-1f3ff", + "1f484", + "1f485", + "1f485-1f3fb", + "1f485-1f3fc", + "1f485-1f3fd", + "1f485-1f3fe", + "1f485-1f3ff", + "1f486", + "1f486-1f3fb", + "1f486-1f3fb-200d-2640-fe0f", + "1f486-1f3fb-200d-2642-fe0f", + "1f486-1f3fc", + "1f486-1f3fc-200d-2640-fe0f", + "1f486-1f3fc-200d-2642-fe0f", + "1f486-1f3fd", + "1f486-1f3fd-200d-2640-fe0f", + "1f486-1f3fd-200d-2642-fe0f", + "1f486-1f3fe", + "1f486-1f3fe-200d-2640-fe0f", + "1f486-1f3fe-200d-2642-fe0f", + "1f486-1f3ff", + "1f486-1f3ff-200d-2640-fe0f", + "1f486-1f3ff-200d-2642-fe0f", + "1f486-200d-2640-fe0f", + "1f486-200d-2642-fe0f", + "1f487", + "1f487-1f3fb", + "1f487-1f3fb-200d-2640-fe0f", + "1f487-1f3fb-200d-2642-fe0f", + "1f487-1f3fc", + "1f487-1f3fc-200d-2640-fe0f", + "1f487-1f3fc-200d-2642-fe0f", + "1f487-1f3fd", + "1f487-1f3fd-200d-2640-fe0f", + "1f487-1f3fd-200d-2642-fe0f", + "1f487-1f3fe", + "1f487-1f3fe-200d-2640-fe0f", + "1f487-1f3fe-200d-2642-fe0f", + "1f487-1f3ff", + "1f487-1f3ff-200d-2640-fe0f", + "1f487-1f3ff-200d-2642-fe0f", + "1f487-200d-2640-fe0f", + "1f487-200d-2642-fe0f", + "1f488", + "1f489", + "1f48a", + "1f48b", + "1f48c", + "1f48d", + "1f48e", + "1f48f", + "1f48f-1f3fb", + "1f48f-1f3fc", + "1f48f-1f3fd", + "1f48f-1f3fe", + "1f48f-1f3ff", + "1f490", + "1f491", + "1f491-1f3fb", + "1f491-1f3fc", + "1f491-1f3fd", + "1f491-1f3fe", + "1f491-1f3ff", + "1f492", + "1f493", + "1f494", + "1f495", + "1f496", + "1f497", + "1f498", + "1f499", + "1f49a", + "1f49b", + "1f49c", + "1f49d", + "1f49e", + "1f49f", + "1f4a0", + "1f4a1", + "1f4a2", + "1f4a3", + "1f4a4", + "1f4a5", + "1f4a6", + "1f4a7", + "1f4a8", + "1f4a9", + "1f4aa", + "1f4aa-1f3fb", + "1f4aa-1f3fc", + "1f4aa-1f3fd", + "1f4aa-1f3fe", + "1f4aa-1f3ff", + "1f4ab", + "1f4ac", + "1f4ad", + "1f4ae", + "1f4af", + "1f4b0", + "1f4b1", + "1f4b2", + "1f4b3", + "1f4b4", + "1f4b5", + "1f4b6", + "1f4b7", + "1f4b8", + "1f4b9", + "1f4ba", + "1f4bb", + "1f4bc", + "1f4bd", + "1f4be", + "1f4bf", + "1f4c0", + "1f4c1", + "1f4c2", + "1f4c3", + "1f4c4", + "1f4c5", + "1f4c6", + "1f4c7", + "1f4c8", + "1f4c9", + "1f4ca", + "1f4cb", + "1f4cc", + "1f4cd", + "1f4ce", + "1f4cf", + "1f4d0", + "1f4d1", + "1f4d2", + "1f4d3", + "1f4d4", + "1f4d5", + "1f4d6", + "1f4d7", + "1f4d8", + "1f4d9", + "1f4da", + "1f4db", + "1f4dc", + "1f4dd", + "1f4de", + "1f4df", + "1f4e0", + "1f4e1", + "1f4e2", + "1f4e3", + "1f4e4", + "1f4e5", + "1f4e6", + "1f4e7", + "1f4e8", + "1f4e9", + "1f4ea", + "1f4eb", + "1f4ec", + "1f4ed", + "1f4ee", + "1f4ef", + "1f4f0", + "1f4f1", + "1f4f2", + "1f4f3", + "1f4f4", + "1f4f5", + "1f4f6", + "1f4f7", + "1f4f8", + "1f4f9", + "1f4fa", + "1f4fb", + "1f4fc", + "1f4fd", + "1f4ff", + "1f500", + "1f501", + "1f502", + "1f503", + "1f504", + "1f505", + "1f506", + "1f507", + "1f508", + "1f509", + "1f50a", + "1f50b", + "1f50c", + "1f50d", + "1f50e", + "1f50f", + "1f510", + "1f511", + "1f512", + "1f513", + "1f514", + "1f515", + "1f516", + "1f517", + "1f518", + "1f519", + "1f51a", + "1f51b", + "1f51c", + "1f51d", + "1f51e", + "1f51f", + "1f520", + "1f521", + "1f522", + "1f523", + "1f524", + "1f525", + "1f526", + "1f527", + "1f528", + "1f529", + "1f52a", + "1f52b", + "1f52c", + "1f52d", + "1f52e", + "1f52f", + "1f530", + "1f531", + "1f532", + "1f533", + "1f534", + "1f535", + "1f536", + "1f537", + "1f538", + "1f539", + "1f53a", + "1f53b", + "1f53c", + "1f53d", + "1f549", + "1f54a", + "1f54b", + "1f54c", + "1f54d", + "1f54e", + "1f550", + "1f551", + "1f552", + "1f553", + "1f554", + "1f555", + "1f556", + "1f557", + "1f558", + "1f559", + "1f55a", + "1f55b", + "1f55c", + "1f55d", + "1f55e", + "1f55f", + "1f560", + "1f561", + "1f562", + "1f563", + "1f564", + "1f565", + "1f566", + "1f567", + "1f56f", + "1f570", + "1f573", + "1f574", + "1f574-1f3fb", + "1f574-1f3fb-200d-2640-fe0f", + "1f574-1f3fb-200d-2642-fe0f", + "1f574-1f3fc", + "1f574-1f3fc-200d-2640-fe0f", + "1f574-1f3fc-200d-2642-fe0f", + "1f574-1f3fd", + "1f574-1f3fd-200d-2640-fe0f", + "1f574-1f3fd-200d-2642-fe0f", + "1f574-1f3fe", + "1f574-1f3fe-200d-2640-fe0f", + "1f574-1f3fe-200d-2642-fe0f", + "1f574-1f3ff", + "1f574-1f3ff-200d-2640-fe0f", + "1f574-1f3ff-200d-2642-fe0f", + "1f574-fe0f-200d-2640-fe0f", + "1f574-fe0f-200d-2642-fe0f", + "1f575", + "1f575-1f3fb", + "1f575-1f3fb-200d-2640-fe0f", + "1f575-1f3fb-200d-2642-fe0f", + "1f575-1f3fc", + "1f575-1f3fc-200d-2640-fe0f", + "1f575-1f3fc-200d-2642-fe0f", + "1f575-1f3fd", + "1f575-1f3fd-200d-2640-fe0f", + "1f575-1f3fd-200d-2642-fe0f", + "1f575-1f3fe", + "1f575-1f3fe-200d-2640-fe0f", + "1f575-1f3fe-200d-2642-fe0f", + "1f575-1f3ff", + "1f575-1f3ff-200d-2640-fe0f", + "1f575-1f3ff-200d-2642-fe0f", + "1f575-fe0f-200d-2640-fe0f", + "1f575-fe0f-200d-2642-fe0f", + "1f576", + "1f577", + "1f578", + "1f579", + "1f57a", + "1f57a-1f3fb", + "1f57a-1f3fc", + "1f57a-1f3fd", + "1f57a-1f3fe", + "1f57a-1f3ff", + "1f587", + "1f58a", + "1f58b", + "1f58c", + "1f58d", + "1f590", + "1f590-1f3fb", + "1f590-1f3fc", + "1f590-1f3fd", + "1f590-1f3fe", + "1f590-1f3ff", + "1f595", + "1f595-1f3fb", + "1f595-1f3fc", + "1f595-1f3fd", + "1f595-1f3fe", + "1f595-1f3ff", + "1f596", + "1f596-1f3fb", + "1f596-1f3fc", + "1f596-1f3fd", + "1f596-1f3fe", + "1f596-1f3ff", + "1f5a4", + "1f5a5", + "1f5a8", + "1f5b1", + "1f5b2", + "1f5bc", + "1f5c2", + "1f5c3", + "1f5c4", + "1f5d1", + "1f5d2", + "1f5d3", + "1f5dc", + "1f5dd", + "1f5de", + "1f5e1", + "1f5e3", + "1f5e8", + "1f5ef", + "1f5f3", + "1f5fa", + "1f5fb", + "1f5fc", + "1f5fd", + "1f5fe", + "1f5ff", + "1f600", + "1f601", + "1f602", + "1f603", + "1f604", + "1f605", + "1f606", + "1f607", + "1f608", + "1f609", + "1f60a", + "1f60b", + "1f60c", + "1f60d", + "1f60e", + "1f60f", + "1f610", + "1f611", + "1f612", + "1f613", + "1f614", + "1f615", + "1f616", + "1f617", + "1f618", + "1f619", + "1f61a", + "1f61b", + "1f61c", + "1f61d", + "1f61e", + "1f61f", + "1f620", + "1f621", + "1f622", + "1f623", + "1f624", + "1f625", + "1f626", + "1f627", + "1f628", + "1f629", + "1f62a", + "1f62b", + "1f62c", + "1f62d", + "1f62e", + "1f62e-200d-1f4a8", + "1f62f", + "1f630", + "1f631", + "1f632", + "1f633", + "1f634", + "1f635", + "1f635-200d-1f4ab", + "1f636", + "1f636-200d-1f32b-fe0f", + "1f637", + "1f638", + "1f639", + "1f63a", + "1f63b", + "1f63c", + "1f63d", + "1f63e", + "1f63f", + "1f640", + "1f641", + "1f642", + "1f643", + "1f644", + "1f645", + "1f645-1f3fb", + "1f645-1f3fb-200d-2640-fe0f", + "1f645-1f3fb-200d-2642-fe0f", + "1f645-1f3fc", + "1f645-1f3fc-200d-2640-fe0f", + "1f645-1f3fc-200d-2642-fe0f", + "1f645-1f3fd", + "1f645-1f3fd-200d-2640-fe0f", + "1f645-1f3fd-200d-2642-fe0f", + "1f645-1f3fe", + "1f645-1f3fe-200d-2640-fe0f", + "1f645-1f3fe-200d-2642-fe0f", + "1f645-1f3ff", + "1f645-1f3ff-200d-2640-fe0f", + "1f645-1f3ff-200d-2642-fe0f", + "1f645-200d-2640-fe0f", + "1f645-200d-2642-fe0f", + "1f646", + "1f646-1f3fb", + "1f646-1f3fb-200d-2640-fe0f", + "1f646-1f3fb-200d-2642-fe0f", + "1f646-1f3fc", + "1f646-1f3fc-200d-2640-fe0f", + "1f646-1f3fc-200d-2642-fe0f", + "1f646-1f3fd", + "1f646-1f3fd-200d-2640-fe0f", + "1f646-1f3fd-200d-2642-fe0f", + "1f646-1f3fe", + "1f646-1f3fe-200d-2640-fe0f", + "1f646-1f3fe-200d-2642-fe0f", + "1f646-1f3ff", + "1f646-1f3ff-200d-2640-fe0f", + "1f646-1f3ff-200d-2642-fe0f", + "1f646-200d-2640-fe0f", + "1f646-200d-2642-fe0f", + "1f647", + "1f647-1f3fb", + "1f647-1f3fb-200d-2640-fe0f", + "1f647-1f3fb-200d-2642-fe0f", + "1f647-1f3fc", + "1f647-1f3fc-200d-2640-fe0f", + "1f647-1f3fc-200d-2642-fe0f", + "1f647-1f3fd", + "1f647-1f3fd-200d-2640-fe0f", + "1f647-1f3fd-200d-2642-fe0f", + "1f647-1f3fe", + "1f647-1f3fe-200d-2640-fe0f", + "1f647-1f3fe-200d-2642-fe0f", + "1f647-1f3ff", + "1f647-1f3ff-200d-2640-fe0f", + "1f647-1f3ff-200d-2642-fe0f", + "1f647-200d-2640-fe0f", + "1f647-200d-2642-fe0f", + "1f648", + "1f649", + "1f64a", + "1f64b", + "1f64b-1f3fb", + "1f64b-1f3fb-200d-2640-fe0f", + "1f64b-1f3fb-200d-2642-fe0f", + "1f64b-1f3fc", + "1f64b-1f3fc-200d-2640-fe0f", + "1f64b-1f3fc-200d-2642-fe0f", + "1f64b-1f3fd", + "1f64b-1f3fd-200d-2640-fe0f", + "1f64b-1f3fd-200d-2642-fe0f", + "1f64b-1f3fe", + "1f64b-1f3fe-200d-2640-fe0f", + "1f64b-1f3fe-200d-2642-fe0f", + "1f64b-1f3ff", + "1f64b-1f3ff-200d-2640-fe0f", + "1f64b-1f3ff-200d-2642-fe0f", + "1f64b-200d-2640-fe0f", + "1f64b-200d-2642-fe0f", + "1f64c", + "1f64c-1f3fb", + "1f64c-1f3fc", + "1f64c-1f3fd", + "1f64c-1f3fe", + "1f64c-1f3ff", + "1f64d", + "1f64d-1f3fb", + "1f64d-1f3fb-200d-2640-fe0f", + "1f64d-1f3fb-200d-2642-fe0f", + "1f64d-1f3fc", + "1f64d-1f3fc-200d-2640-fe0f", + "1f64d-1f3fc-200d-2642-fe0f", + "1f64d-1f3fd", + "1f64d-1f3fd-200d-2640-fe0f", + "1f64d-1f3fd-200d-2642-fe0f", + "1f64d-1f3fe", + "1f64d-1f3fe-200d-2640-fe0f", + "1f64d-1f3fe-200d-2642-fe0f", + "1f64d-1f3ff", + "1f64d-1f3ff-200d-2640-fe0f", + "1f64d-1f3ff-200d-2642-fe0f", + "1f64d-200d-2640-fe0f", + "1f64d-200d-2642-fe0f", + "1f64e", + "1f64e-1f3fb", + "1f64e-1f3fb-200d-2640-fe0f", + "1f64e-1f3fb-200d-2642-fe0f", + "1f64e-1f3fc", + "1f64e-1f3fc-200d-2640-fe0f", + "1f64e-1f3fc-200d-2642-fe0f", + "1f64e-1f3fd", + "1f64e-1f3fd-200d-2640-fe0f", + "1f64e-1f3fd-200d-2642-fe0f", + "1f64e-1f3fe", + "1f64e-1f3fe-200d-2640-fe0f", + "1f64e-1f3fe-200d-2642-fe0f", + "1f64e-1f3ff", + "1f64e-1f3ff-200d-2640-fe0f", + "1f64e-1f3ff-200d-2642-fe0f", + "1f64e-200d-2640-fe0f", + "1f64e-200d-2642-fe0f", + "1f64f", + "1f64f-1f3fb", + "1f64f-1f3fc", + "1f64f-1f3fd", + "1f64f-1f3fe", + "1f64f-1f3ff", + "1f680", + "1f681", + "1f682", + "1f683", + "1f684", + "1f685", + "1f686", + "1f687", + "1f688", + "1f689", + "1f68a", + "1f68b", + "1f68c", + "1f68d", + "1f68e", + "1f68f", + "1f690", + "1f691", + "1f692", + "1f693", + "1f694", + "1f695", + "1f696", + "1f697", + "1f698", + "1f699", + "1f69a", + "1f69b", + "1f69c", + "1f69d", + "1f69e", + "1f69f", + "1f6a0", + "1f6a1", + "1f6a2", + "1f6a3", + "1f6a3-1f3fb", + "1f6a3-1f3fb-200d-2640-fe0f", + "1f6a3-1f3fb-200d-2642-fe0f", + "1f6a3-1f3fc", + "1f6a3-1f3fc-200d-2640-fe0f", + "1f6a3-1f3fc-200d-2642-fe0f", + "1f6a3-1f3fd", + "1f6a3-1f3fd-200d-2640-fe0f", + "1f6a3-1f3fd-200d-2642-fe0f", + "1f6a3-1f3fe", + "1f6a3-1f3fe-200d-2640-fe0f", + "1f6a3-1f3fe-200d-2642-fe0f", + "1f6a3-1f3ff", + "1f6a3-1f3ff-200d-2640-fe0f", + "1f6a3-1f3ff-200d-2642-fe0f", + "1f6a3-200d-2640-fe0f", + "1f6a3-200d-2642-fe0f", + "1f6a4", + "1f6a5", + "1f6a6", + "1f6a7", + "1f6a8", + "1f6a9", + "1f6aa", + "1f6ab", + "1f6ac", + "1f6ad", + "1f6ae", + "1f6af", + "1f6b0", + "1f6b1", + "1f6b2", + "1f6b3", + "1f6b4", + "1f6b4-1f3fb", + "1f6b4-1f3fb-200d-2640-fe0f", + "1f6b4-1f3fb-200d-2642-fe0f", + "1f6b4-1f3fc", + "1f6b4-1f3fc-200d-2640-fe0f", + "1f6b4-1f3fc-200d-2642-fe0f", + "1f6b4-1f3fd", + "1f6b4-1f3fd-200d-2640-fe0f", + "1f6b4-1f3fd-200d-2642-fe0f", + "1f6b4-1f3fe", + "1f6b4-1f3fe-200d-2640-fe0f", + "1f6b4-1f3fe-200d-2642-fe0f", + "1f6b4-1f3ff", + "1f6b4-1f3ff-200d-2640-fe0f", + "1f6b4-1f3ff-200d-2642-fe0f", + "1f6b4-200d-2640-fe0f", + "1f6b4-200d-2642-fe0f", + "1f6b5", + "1f6b5-1f3fb", + "1f6b5-1f3fb-200d-2640-fe0f", + "1f6b5-1f3fb-200d-2642-fe0f", + "1f6b5-1f3fc", + "1f6b5-1f3fc-200d-2640-fe0f", + "1f6b5-1f3fc-200d-2642-fe0f", + "1f6b5-1f3fd", + "1f6b5-1f3fd-200d-2640-fe0f", + "1f6b5-1f3fd-200d-2642-fe0f", + "1f6b5-1f3fe", + "1f6b5-1f3fe-200d-2640-fe0f", + "1f6b5-1f3fe-200d-2642-fe0f", + "1f6b5-1f3ff", + "1f6b5-1f3ff-200d-2640-fe0f", + "1f6b5-1f3ff-200d-2642-fe0f", + "1f6b5-200d-2640-fe0f", + "1f6b5-200d-2642-fe0f", + "1f6b6", + "1f6b6-1f3fb", + "1f6b6-1f3fb-200d-2640-fe0f", + "1f6b6-1f3fb-200d-2642-fe0f", + "1f6b6-1f3fc", + "1f6b6-1f3fc-200d-2640-fe0f", + "1f6b6-1f3fc-200d-2642-fe0f", + "1f6b6-1f3fd", + "1f6b6-1f3fd-200d-2640-fe0f", + "1f6b6-1f3fd-200d-2642-fe0f", + "1f6b6-1f3fe", + "1f6b6-1f3fe-200d-2640-fe0f", + "1f6b6-1f3fe-200d-2642-fe0f", + "1f6b6-1f3ff", + "1f6b6-1f3ff-200d-2640-fe0f", + "1f6b6-1f3ff-200d-2642-fe0f", + "1f6b6-200d-2640-fe0f", + "1f6b6-200d-2642-fe0f", + "1f6b7", + "1f6b8", + "1f6b9", + "1f6ba", + "1f6bb", + "1f6bc", + "1f6bd", + "1f6be", + "1f6bf", + "1f6c0", + "1f6c0-1f3fb", + "1f6c0-1f3fc", + "1f6c0-1f3fd", + "1f6c0-1f3fe", + "1f6c0-1f3ff", + "1f6c1", + "1f6c2", + "1f6c3", + "1f6c4", + "1f6c5", + "1f6cb", + "1f6cc", + "1f6cc-1f3fb", + "1f6cc-1f3fc", + "1f6cc-1f3fd", + "1f6cc-1f3fe", + "1f6cc-1f3ff", + "1f6cd", + "1f6ce", + "1f6cf", + "1f6d0", + "1f6d1", + "1f6d2", + "1f6d5", + "1f6d6", + "1f6d7", + "1f6dc", + "1f6dd", + "1f6de", + "1f6df", + "1f6e0", + "1f6e1", + "1f6e2", + "1f6e3", + "1f6e4", + "1f6e5", + "1f6e9", + "1f6eb", + "1f6ec", + "1f6f0", + "1f6f3", + "1f6f4", + "1f6f5", + "1f6f6", + "1f6f7", + "1f6f8", + "1f6f9", + "1f6fa", + "1f6fb", + "1f6fc", + "1f7e0", + "1f7e1", + "1f7e2", + "1f7e3", + "1f7e4", + "1f7e5", + "1f7e6", + "1f7e7", + "1f7e8", + "1f7e9", + "1f7ea", + "1f7eb", + "1f7f0", + "1f90c", + "1f90c-1f3fb", + "1f90c-1f3fc", + "1f90c-1f3fd", + "1f90c-1f3fe", + "1f90c-1f3ff", + "1f90d", + "1f90e", + "1f90f", + "1f90f-1f3fb", + "1f90f-1f3fc", + "1f90f-1f3fd", + "1f90f-1f3fe", + "1f90f-1f3ff", + "1f910", + "1f911", + "1f912", + "1f913", + "1f914", + "1f915", + "1f916", + "1f917", + "1f918", + "1f918-1f3fb", + "1f918-1f3fc", + "1f918-1f3fd", + "1f918-1f3fe", + "1f918-1f3ff", + "1f919", + "1f919-1f3fb", + "1f919-1f3fc", + "1f919-1f3fd", + "1f919-1f3fe", + "1f919-1f3ff", + "1f91a", + "1f91a-1f3fb", + "1f91a-1f3fc", + "1f91a-1f3fd", + "1f91a-1f3fe", + "1f91a-1f3ff", + "1f91b", + "1f91b-1f3fb", + "1f91b-1f3fc", + "1f91b-1f3fd", + "1f91b-1f3fe", + "1f91b-1f3ff", + "1f91c", + "1f91c-1f3fb", + "1f91c-1f3fc", + "1f91c-1f3fd", + "1f91c-1f3fe", + "1f91c-1f3ff", + "1f91d", + "1f91d-1f3fb", + "1f91d-1f3fc", + "1f91d-1f3fd", + "1f91d-1f3fe", + "1f91d-1f3ff", + "1f91e", + "1f91e-1f3fb", + "1f91e-1f3fc", + "1f91e-1f3fd", + "1f91e-1f3fe", + "1f91e-1f3ff", + "1f91f", + "1f91f-1f3fb", + "1f91f-1f3fc", + "1f91f-1f3fd", + "1f91f-1f3fe", + "1f91f-1f3ff", + "1f920", + "1f921", + "1f922", + "1f923", + "1f924", + "1f925", + "1f926", + "1f926-1f3fb", + "1f926-1f3fb-200d-2640-fe0f", + "1f926-1f3fb-200d-2642-fe0f", + "1f926-1f3fc", + "1f926-1f3fc-200d-2640-fe0f", + "1f926-1f3fc-200d-2642-fe0f", + "1f926-1f3fd", + "1f926-1f3fd-200d-2640-fe0f", + "1f926-1f3fd-200d-2642-fe0f", + "1f926-1f3fe", + "1f926-1f3fe-200d-2640-fe0f", + "1f926-1f3fe-200d-2642-fe0f", + "1f926-1f3ff", + "1f926-1f3ff-200d-2640-fe0f", + "1f926-1f3ff-200d-2642-fe0f", + "1f926-200d-2640-fe0f", + "1f926-200d-2642-fe0f", + "1f927", + "1f928", + "1f929", + "1f92a", + "1f92b", + "1f92c", + "1f92d", + "1f92e", + "1f92f", + "1f930", + "1f930-1f3fb", + "1f930-1f3fc", + "1f930-1f3fd", + "1f930-1f3fe", + "1f930-1f3ff", + "1f931", + "1f931-1f3fb", + "1f931-1f3fc", + "1f931-1f3fd", + "1f931-1f3fe", + "1f931-1f3ff", + "1f932", + "1f932-1f3fb", + "1f932-1f3fc", + "1f932-1f3fd", + "1f932-1f3fe", + "1f932-1f3ff", + "1f933", + "1f933-1f3fb", + "1f933-1f3fc", + "1f933-1f3fd", + "1f933-1f3fe", + "1f933-1f3ff", + "1f934", + "1f934-1f3fb", + "1f934-1f3fc", + "1f934-1f3fd", + "1f934-1f3fe", + "1f934-1f3ff", + "1f935", + "1f935-1f3fb", + "1f935-1f3fb-200d-2640-fe0f", + "1f935-1f3fb-200d-2642-fe0f", + "1f935-1f3fc", + "1f935-1f3fc-200d-2640-fe0f", + "1f935-1f3fc-200d-2642-fe0f", + "1f935-1f3fd", + "1f935-1f3fd-200d-2640-fe0f", + "1f935-1f3fd-200d-2642-fe0f", + "1f935-1f3fe", + "1f935-1f3fe-200d-2640-fe0f", + "1f935-1f3fe-200d-2642-fe0f", + "1f935-1f3ff", + "1f935-1f3ff-200d-2640-fe0f", + "1f935-1f3ff-200d-2642-fe0f", + "1f935-200d-2640-fe0f", + "1f935-200d-2642-fe0f", + "1f936", + "1f936-1f3fb", + "1f936-1f3fc", + "1f936-1f3fd", + "1f936-1f3fe", + "1f936-1f3ff", + "1f937", + "1f937-1f3fb", + "1f937-1f3fb-200d-2640-fe0f", + "1f937-1f3fb-200d-2642-fe0f", + "1f937-1f3fc", + "1f937-1f3fc-200d-2640-fe0f", + "1f937-1f3fc-200d-2642-fe0f", + "1f937-1f3fd", + "1f937-1f3fd-200d-2640-fe0f", + "1f937-1f3fd-200d-2642-fe0f", + "1f937-1f3fe", + "1f937-1f3fe-200d-2640-fe0f", + "1f937-1f3fe-200d-2642-fe0f", + "1f937-1f3ff", + "1f937-1f3ff-200d-2640-fe0f", + "1f937-1f3ff-200d-2642-fe0f", + "1f937-200d-2640-fe0f", + "1f937-200d-2642-fe0f", + "1f938", + "1f938-1f3fb", + "1f938-1f3fb-200d-2640-fe0f", + "1f938-1f3fb-200d-2642-fe0f", + "1f938-1f3fc", + "1f938-1f3fc-200d-2640-fe0f", + "1f938-1f3fc-200d-2642-fe0f", + "1f938-1f3fd", + "1f938-1f3fd-200d-2640-fe0f", + "1f938-1f3fd-200d-2642-fe0f", + "1f938-1f3fe", + "1f938-1f3fe-200d-2640-fe0f", + "1f938-1f3fe-200d-2642-fe0f", + "1f938-1f3ff", + "1f938-1f3ff-200d-2640-fe0f", + "1f938-1f3ff-200d-2642-fe0f", + "1f938-200d-2640-fe0f", + "1f938-200d-2642-fe0f", + "1f939", + "1f939-1f3fb", + "1f939-1f3fb-200d-2640-fe0f", + "1f939-1f3fb-200d-2642-fe0f", + "1f939-1f3fc", + "1f939-1f3fc-200d-2640-fe0f", + "1f939-1f3fc-200d-2642-fe0f", + "1f939-1f3fd", + "1f939-1f3fd-200d-2640-fe0f", + "1f939-1f3fd-200d-2642-fe0f", + "1f939-1f3fe", + "1f939-1f3fe-200d-2640-fe0f", + "1f939-1f3fe-200d-2642-fe0f", + "1f939-1f3ff", + "1f939-1f3ff-200d-2640-fe0f", + "1f939-1f3ff-200d-2642-fe0f", + "1f939-200d-2640-fe0f", + "1f939-200d-2642-fe0f", + "1f93a", + "1f93c", + "1f93c-200d-2640-fe0f", + "1f93c-200d-2642-fe0f", + "1f93d", + "1f93d-1f3fb", + "1f93d-1f3fb-200d-2640-fe0f", + "1f93d-1f3fb-200d-2642-fe0f", + "1f93d-1f3fc", + "1f93d-1f3fc-200d-2640-fe0f", + "1f93d-1f3fc-200d-2642-fe0f", + "1f93d-1f3fd", + "1f93d-1f3fd-200d-2640-fe0f", + "1f93d-1f3fd-200d-2642-fe0f", + "1f93d-1f3fe", + "1f93d-1f3fe-200d-2640-fe0f", + "1f93d-1f3fe-200d-2642-fe0f", + "1f93d-1f3ff", + "1f93d-1f3ff-200d-2640-fe0f", + "1f93d-1f3ff-200d-2642-fe0f", + "1f93d-200d-2640-fe0f", + "1f93d-200d-2642-fe0f", + "1f93e", + "1f93e-1f3fb", + "1f93e-1f3fb-200d-2640-fe0f", + "1f93e-1f3fb-200d-2642-fe0f", + "1f93e-1f3fc", + "1f93e-1f3fc-200d-2640-fe0f", + "1f93e-1f3fc-200d-2642-fe0f", + "1f93e-1f3fd", + "1f93e-1f3fd-200d-2640-fe0f", + "1f93e-1f3fd-200d-2642-fe0f", + "1f93e-1f3fe", + "1f93e-1f3fe-200d-2640-fe0f", + "1f93e-1f3fe-200d-2642-fe0f", + "1f93e-1f3ff", + "1f93e-1f3ff-200d-2640-fe0f", + "1f93e-1f3ff-200d-2642-fe0f", + "1f93e-200d-2640-fe0f", + "1f93e-200d-2642-fe0f", + "1f93f", + "1f940", + "1f941", + "1f942", + "1f943", + "1f944", + "1f945", + "1f947", + "1f948", + "1f949", + "1f94a", + "1f94b", + "1f94c", + "1f94d", + "1f94e", + "1f94f", + "1f950", + "1f951", + "1f952", + "1f953", + "1f954", + "1f955", + "1f956", + "1f957", + "1f958", + "1f959", + "1f95a", + "1f95b", + "1f95c", + "1f95d", + "1f95e", + "1f95f", + "1f960", + "1f961", + "1f962", + "1f963", + "1f964", + "1f965", + "1f966", + "1f967", + "1f968", + "1f969", + "1f96a", + "1f96b", + "1f96c", + "1f96d", + "1f96e", + "1f96f", + "1f970", + "1f971", + "1f972", + "1f973", + "1f974", + "1f975", + "1f976", + "1f977", + "1f977-1f3fb", + "1f977-1f3fc", + "1f977-1f3fd", + "1f977-1f3fe", + "1f977-1f3ff", + "1f978", + "1f979", + "1f97a", + "1f97b", + "1f97c", + "1f97d", + "1f97e", + "1f97f", + "1f980", + "1f981", + "1f982", + "1f983", + "1f984", + "1f985", + "1f986", + "1f987", + "1f988", + "1f989", + "1f98a", + "1f98b", + "1f98c", + "1f98d", + "1f98e", + "1f98f", + "1f990", + "1f991", + "1f992", + "1f993", + "1f994", + "1f995", + "1f996", + "1f997", + "1f998", + "1f999", + "1f99a", + "1f99b", + "1f99c", + "1f99d", + "1f99e", + "1f99f", + "1f9a0", + "1f9a1", + "1f9a2", + "1f9a3", + "1f9a4", + "1f9a5", + "1f9a6", + "1f9a7", + "1f9a8", + "1f9a9", + "1f9aa", + "1f9ab", + "1f9ac", + "1f9ad", + "1f9ae", + "1f9af", + "1f9b0", + "1f9b1", + "1f9b2", + "1f9b3", + "1f9b4", + "1f9b5", + "1f9b5-1f3fb", + "1f9b5-1f3fc", + "1f9b5-1f3fd", + "1f9b5-1f3fe", + "1f9b5-1f3ff", + "1f9b6", + "1f9b6-1f3fb", + "1f9b6-1f3fc", + "1f9b6-1f3fd", + "1f9b6-1f3fe", + "1f9b6-1f3ff", + "1f9b7", + "1f9b8", + "1f9b8-1f3fb", + "1f9b8-1f3fb-200d-2640-fe0f", + "1f9b8-1f3fb-200d-2642-fe0f", + "1f9b8-1f3fc", + "1f9b8-1f3fc-200d-2640-fe0f", + "1f9b8-1f3fc-200d-2642-fe0f", + "1f9b8-1f3fd", + "1f9b8-1f3fd-200d-2640-fe0f", + "1f9b8-1f3fd-200d-2642-fe0f", + "1f9b8-1f3fe", + "1f9b8-1f3fe-200d-2640-fe0f", + "1f9b8-1f3fe-200d-2642-fe0f", + "1f9b8-1f3ff", + "1f9b8-1f3ff-200d-2640-fe0f", + "1f9b8-1f3ff-200d-2642-fe0f", + "1f9b8-200d-2640-fe0f", + "1f9b8-200d-2642-fe0f", + "1f9b9", + "1f9b9-1f3fb", + "1f9b9-1f3fb-200d-2640-fe0f", + "1f9b9-1f3fb-200d-2642-fe0f", + "1f9b9-1f3fc", + "1f9b9-1f3fc-200d-2640-fe0f", + "1f9b9-1f3fc-200d-2642-fe0f", + "1f9b9-1f3fd", + "1f9b9-1f3fd-200d-2640-fe0f", + "1f9b9-1f3fd-200d-2642-fe0f", + "1f9b9-1f3fe", + "1f9b9-1f3fe-200d-2640-fe0f", + "1f9b9-1f3fe-200d-2642-fe0f", + "1f9b9-1f3ff", + "1f9b9-1f3ff-200d-2640-fe0f", + "1f9b9-1f3ff-200d-2642-fe0f", + "1f9b9-200d-2640-fe0f", + "1f9b9-200d-2642-fe0f", + "1f9ba", + "1f9bb", + "1f9bb-1f3fb", + "1f9bb-1f3fc", + "1f9bb-1f3fd", + "1f9bb-1f3fe", + "1f9bb-1f3ff", + "1f9bc", + "1f9bd", + "1f9be", + "1f9bf", + "1f9c0", + "1f9c1", + "1f9c2", + "1f9c3", + "1f9c4", + "1f9c5", + "1f9c6", + "1f9c7", + "1f9c8", + "1f9c9", + "1f9ca", + "1f9cb", + "1f9cc", + "1f9cd", + "1f9cd-1f3fb", + "1f9cd-1f3fb-200d-2640-fe0f", + "1f9cd-1f3fb-200d-2642-fe0f", + "1f9cd-1f3fc", + "1f9cd-1f3fc-200d-2640-fe0f", + "1f9cd-1f3fc-200d-2642-fe0f", + "1f9cd-1f3fd", + "1f9cd-1f3fd-200d-2640-fe0f", + "1f9cd-1f3fd-200d-2642-fe0f", + "1f9cd-1f3fe", + "1f9cd-1f3fe-200d-2640-fe0f", + "1f9cd-1f3fe-200d-2642-fe0f", + "1f9cd-1f3ff", + "1f9cd-1f3ff-200d-2640-fe0f", + "1f9cd-1f3ff-200d-2642-fe0f", + "1f9cd-200d-2640-fe0f", + "1f9cd-200d-2642-fe0f", + "1f9ce", + "1f9ce-1f3fb", + "1f9ce-1f3fb-200d-2640-fe0f", + "1f9ce-1f3fb-200d-2642-fe0f", + "1f9ce-1f3fc", + "1f9ce-1f3fc-200d-2640-fe0f", + "1f9ce-1f3fc-200d-2642-fe0f", + "1f9ce-1f3fd", + "1f9ce-1f3fd-200d-2640-fe0f", + "1f9ce-1f3fd-200d-2642-fe0f", + "1f9ce-1f3fe", + "1f9ce-1f3fe-200d-2640-fe0f", + "1f9ce-1f3fe-200d-2642-fe0f", + "1f9ce-1f3ff", + "1f9ce-1f3ff-200d-2640-fe0f", + "1f9ce-1f3ff-200d-2642-fe0f", + "1f9ce-200d-2640-fe0f", + "1f9ce-200d-2642-fe0f", + "1f9cf", + "1f9cf-1f3fb", + "1f9cf-1f3fb-200d-2640-fe0f", + "1f9cf-1f3fb-200d-2642-fe0f", + "1f9cf-1f3fc", + "1f9cf-1f3fc-200d-2640-fe0f", + "1f9cf-1f3fc-200d-2642-fe0f", + "1f9cf-1f3fd", + "1f9cf-1f3fd-200d-2640-fe0f", + "1f9cf-1f3fd-200d-2642-fe0f", + "1f9cf-1f3fe", + "1f9cf-1f3fe-200d-2640-fe0f", + "1f9cf-1f3fe-200d-2642-fe0f", + "1f9cf-1f3ff", + "1f9cf-1f3ff-200d-2640-fe0f", + "1f9cf-1f3ff-200d-2642-fe0f", + "1f9cf-200d-2640-fe0f", + "1f9cf-200d-2642-fe0f", + "1f9d0", + "1f9d1", + "1f9d1-1f3fb", + "1f9d1-1f3fb-200d-1f33e", + "1f9d1-1f3fb-200d-1f373", + "1f9d1-1f3fb-200d-1f37c", + "1f9d1-1f3fb-200d-1f384", + "1f9d1-1f3fb-200d-1f393", + "1f9d1-1f3fb-200d-1f3a4", + "1f9d1-1f3fb-200d-1f3a8", + "1f9d1-1f3fb-200d-1f3eb", + "1f9d1-1f3fb-200d-1f3ed", + "1f9d1-1f3fb-200d-1f4bb", + "1f9d1-1f3fb-200d-1f4bc", + "1f9d1-1f3fb-200d-1f527", + "1f9d1-1f3fb-200d-1f52c", + "1f9d1-1f3fb-200d-1f680", + "1f9d1-1f3fb-200d-1f692", + "1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fb", + "1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fc", + "1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fd", + "1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fe", + "1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3ff", + "1f9d1-1f3fb-200d-1f9af", + "1f9d1-1f3fb-200d-1f9b0", + "1f9d1-1f3fb-200d-1f9b1", + "1f9d1-1f3fb-200d-1f9b2", + "1f9d1-1f3fb-200d-1f9b3", + "1f9d1-1f3fb-200d-1f9bc", + "1f9d1-1f3fb-200d-1f9bd", + "1f9d1-1f3fb-200d-2695-fe0f", + "1f9d1-1f3fb-200d-2696-fe0f", + "1f9d1-1f3fb-200d-2708-fe0f", + "1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc", + "1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd", + "1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe", + "1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff", + "1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3fc", + "1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3fd", + "1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3fe", + "1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3ff", + "1f9d1-1f3fc", + "1f9d1-1f3fc-200d-1f33e", + "1f9d1-1f3fc-200d-1f373", + "1f9d1-1f3fc-200d-1f37c", + "1f9d1-1f3fc-200d-1f384", + "1f9d1-1f3fc-200d-1f393", + "1f9d1-1f3fc-200d-1f3a4", + "1f9d1-1f3fc-200d-1f3a8", + "1f9d1-1f3fc-200d-1f3eb", + "1f9d1-1f3fc-200d-1f3ed", + "1f9d1-1f3fc-200d-1f4bb", + "1f9d1-1f3fc-200d-1f4bc", + "1f9d1-1f3fc-200d-1f527", + "1f9d1-1f3fc-200d-1f52c", + "1f9d1-1f3fc-200d-1f680", + "1f9d1-1f3fc-200d-1f692", + "1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fb", + "1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fc", + "1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fd", + "1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fe", + "1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3ff", + "1f9d1-1f3fc-200d-1f9af", + "1f9d1-1f3fc-200d-1f9b0", + "1f9d1-1f3fc-200d-1f9b1", + "1f9d1-1f3fc-200d-1f9b2", + "1f9d1-1f3fc-200d-1f9b3", + "1f9d1-1f3fc-200d-1f9bc", + "1f9d1-1f3fc-200d-1f9bd", + "1f9d1-1f3fc-200d-2695-fe0f", + "1f9d1-1f3fc-200d-2696-fe0f", + "1f9d1-1f3fc-200d-2708-fe0f", + "1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb", + "1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd", + "1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe", + "1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff", + "1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3fb", + "1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3fd", + "1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3fe", + "1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3ff", + "1f9d1-1f3fd", + "1f9d1-1f3fd-200d-1f33e", + "1f9d1-1f3fd-200d-1f373", + "1f9d1-1f3fd-200d-1f37c", + "1f9d1-1f3fd-200d-1f384", + "1f9d1-1f3fd-200d-1f393", + "1f9d1-1f3fd-200d-1f3a4", + "1f9d1-1f3fd-200d-1f3a8", + "1f9d1-1f3fd-200d-1f3eb", + "1f9d1-1f3fd-200d-1f3ed", + "1f9d1-1f3fd-200d-1f4bb", + "1f9d1-1f3fd-200d-1f4bc", + "1f9d1-1f3fd-200d-1f527", + "1f9d1-1f3fd-200d-1f52c", + "1f9d1-1f3fd-200d-1f680", + "1f9d1-1f3fd-200d-1f692", + "1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fb", + "1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fc", + "1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fd", + "1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fe", + "1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3ff", + "1f9d1-1f3fd-200d-1f9af", + "1f9d1-1f3fd-200d-1f9b0", + "1f9d1-1f3fd-200d-1f9b1", + "1f9d1-1f3fd-200d-1f9b2", + "1f9d1-1f3fd-200d-1f9b3", + "1f9d1-1f3fd-200d-1f9bc", + "1f9d1-1f3fd-200d-1f9bd", + "1f9d1-1f3fd-200d-2695-fe0f", + "1f9d1-1f3fd-200d-2696-fe0f", + "1f9d1-1f3fd-200d-2708-fe0f", + "1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb", + "1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc", + "1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe", + "1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff", + "1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3fb", + "1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3fc", + "1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3fe", + "1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3ff", + "1f9d1-1f3fe", + "1f9d1-1f3fe-200d-1f33e", + "1f9d1-1f3fe-200d-1f373", + "1f9d1-1f3fe-200d-1f37c", + "1f9d1-1f3fe-200d-1f384", + "1f9d1-1f3fe-200d-1f393", + "1f9d1-1f3fe-200d-1f3a4", + "1f9d1-1f3fe-200d-1f3a8", + "1f9d1-1f3fe-200d-1f3eb", + "1f9d1-1f3fe-200d-1f3ed", + "1f9d1-1f3fe-200d-1f4bb", + "1f9d1-1f3fe-200d-1f4bc", + "1f9d1-1f3fe-200d-1f527", + "1f9d1-1f3fe-200d-1f52c", + "1f9d1-1f3fe-200d-1f680", + "1f9d1-1f3fe-200d-1f692", + "1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fb", + "1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fc", + "1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fd", + "1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fe", + "1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3ff", + "1f9d1-1f3fe-200d-1f9af", + "1f9d1-1f3fe-200d-1f9b0", + "1f9d1-1f3fe-200d-1f9b1", + "1f9d1-1f3fe-200d-1f9b2", + "1f9d1-1f3fe-200d-1f9b3", + "1f9d1-1f3fe-200d-1f9bc", + "1f9d1-1f3fe-200d-1f9bd", + "1f9d1-1f3fe-200d-2695-fe0f", + "1f9d1-1f3fe-200d-2696-fe0f", + "1f9d1-1f3fe-200d-2708-fe0f", + "1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb", + "1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc", + "1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd", + "1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff", + "1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3fb", + "1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3fc", + "1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3fd", + "1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3ff", + "1f9d1-1f3ff", + "1f9d1-1f3ff-200d-1f33e", + "1f9d1-1f3ff-200d-1f373", + "1f9d1-1f3ff-200d-1f37c", + "1f9d1-1f3ff-200d-1f384", + "1f9d1-1f3ff-200d-1f393", + "1f9d1-1f3ff-200d-1f3a4", + "1f9d1-1f3ff-200d-1f3a8", + "1f9d1-1f3ff-200d-1f3eb", + "1f9d1-1f3ff-200d-1f3ed", + "1f9d1-1f3ff-200d-1f4bb", + "1f9d1-1f3ff-200d-1f4bc", + "1f9d1-1f3ff-200d-1f527", + "1f9d1-1f3ff-200d-1f52c", + "1f9d1-1f3ff-200d-1f680", + "1f9d1-1f3ff-200d-1f692", + "1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fb", + "1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fc", + "1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fd", + "1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fe", + "1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3ff", + "1f9d1-1f3ff-200d-1f9af", + "1f9d1-1f3ff-200d-1f9b0", + "1f9d1-1f3ff-200d-1f9b1", + "1f9d1-1f3ff-200d-1f9b2", + "1f9d1-1f3ff-200d-1f9b3", + "1f9d1-1f3ff-200d-1f9bc", + "1f9d1-1f3ff-200d-1f9bd", + "1f9d1-1f3ff-200d-2695-fe0f", + "1f9d1-1f3ff-200d-2696-fe0f", + "1f9d1-1f3ff-200d-2708-fe0f", + "1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb", + "1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc", + "1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd", + "1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe", + "1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fb", + "1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fc", + "1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fd", + "1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fe", + "1f9d1-200d-1f33e", + "1f9d1-200d-1f373", + "1f9d1-200d-1f37c", + "1f9d1-200d-1f384", + "1f9d1-200d-1f393", + "1f9d1-200d-1f3a4", + "1f9d1-200d-1f3a8", + "1f9d1-200d-1f3eb", + "1f9d1-200d-1f3ed", + "1f9d1-200d-1f4bb", + "1f9d1-200d-1f4bc", + "1f9d1-200d-1f527", + "1f9d1-200d-1f52c", + "1f9d1-200d-1f680", + "1f9d1-200d-1f692", + "1f9d1-200d-1f91d-200d-1f9d1", + "1f9d1-200d-1f9af", + "1f9d1-200d-1f9b0", + "1f9d1-200d-1f9b1", + "1f9d1-200d-1f9b2", + "1f9d1-200d-1f9b3", + "1f9d1-200d-1f9bc", + "1f9d1-200d-1f9bd", + "1f9d1-200d-2695-fe0f", + "1f9d1-200d-2696-fe0f", + "1f9d1-200d-2708-fe0f", + "1f9d2", + "1f9d2-1f3fb", + "1f9d2-1f3fc", + "1f9d2-1f3fd", + "1f9d2-1f3fe", + "1f9d2-1f3ff", + "1f9d3", + "1f9d3-1f3fb", + "1f9d3-1f3fc", + "1f9d3-1f3fd", + "1f9d3-1f3fe", + "1f9d3-1f3ff", + "1f9d4", + "1f9d4-1f3fb", + "1f9d4-1f3fb-200d-2640-fe0f", + "1f9d4-1f3fb-200d-2642-fe0f", + "1f9d4-1f3fc", + "1f9d4-1f3fc-200d-2640-fe0f", + "1f9d4-1f3fc-200d-2642-fe0f", + "1f9d4-1f3fd", + "1f9d4-1f3fd-200d-2640-fe0f", + "1f9d4-1f3fd-200d-2642-fe0f", + "1f9d4-1f3fe", + "1f9d4-1f3fe-200d-2640-fe0f", + "1f9d4-1f3fe-200d-2642-fe0f", + "1f9d4-1f3ff", + "1f9d4-1f3ff-200d-2640-fe0f", + "1f9d4-1f3ff-200d-2642-fe0f", + "1f9d4-200d-2640-fe0f", + "1f9d4-200d-2642-fe0f", + "1f9d5", + "1f9d5-1f3fb", + "1f9d5-1f3fc", + "1f9d5-1f3fd", + "1f9d5-1f3fe", + "1f9d5-1f3ff", + "1f9d6", + "1f9d6-1f3fb", + "1f9d6-1f3fb-200d-2640-fe0f", + "1f9d6-1f3fb-200d-2642-fe0f", + "1f9d6-1f3fc", + "1f9d6-1f3fc-200d-2640-fe0f", + "1f9d6-1f3fc-200d-2642-fe0f", + "1f9d6-1f3fd", + "1f9d6-1f3fd-200d-2640-fe0f", + "1f9d6-1f3fd-200d-2642-fe0f", + "1f9d6-1f3fe", + "1f9d6-1f3fe-200d-2640-fe0f", + "1f9d6-1f3fe-200d-2642-fe0f", + "1f9d6-1f3ff", + "1f9d6-1f3ff-200d-2640-fe0f", + "1f9d6-1f3ff-200d-2642-fe0f", + "1f9d6-200d-2640-fe0f", + "1f9d6-200d-2642-fe0f", + "1f9d7", + "1f9d7-1f3fb", + "1f9d7-1f3fb-200d-2640-fe0f", + "1f9d7-1f3fb-200d-2642-fe0f", + "1f9d7-1f3fc", + "1f9d7-1f3fc-200d-2640-fe0f", + "1f9d7-1f3fc-200d-2642-fe0f", + "1f9d7-1f3fd", + "1f9d7-1f3fd-200d-2640-fe0f", + "1f9d7-1f3fd-200d-2642-fe0f", + "1f9d7-1f3fe", + "1f9d7-1f3fe-200d-2640-fe0f", + "1f9d7-1f3fe-200d-2642-fe0f", + "1f9d7-1f3ff", + "1f9d7-1f3ff-200d-2640-fe0f", + "1f9d7-1f3ff-200d-2642-fe0f", + "1f9d7-200d-2640-fe0f", + "1f9d7-200d-2642-fe0f", + "1f9d8", + "1f9d8-1f3fb", + "1f9d8-1f3fb-200d-2640-fe0f", + "1f9d8-1f3fb-200d-2642-fe0f", + "1f9d8-1f3fc", + "1f9d8-1f3fc-200d-2640-fe0f", + "1f9d8-1f3fc-200d-2642-fe0f", + "1f9d8-1f3fd", + "1f9d8-1f3fd-200d-2640-fe0f", + "1f9d8-1f3fd-200d-2642-fe0f", + "1f9d8-1f3fe", + "1f9d8-1f3fe-200d-2640-fe0f", + "1f9d8-1f3fe-200d-2642-fe0f", + "1f9d8-1f3ff", + "1f9d8-1f3ff-200d-2640-fe0f", + "1f9d8-1f3ff-200d-2642-fe0f", + "1f9d8-200d-2640-fe0f", + "1f9d8-200d-2642-fe0f", + "1f9d9", + "1f9d9-1f3fb", + "1f9d9-1f3fb-200d-2640-fe0f", + "1f9d9-1f3fb-200d-2642-fe0f", + "1f9d9-1f3fc", + "1f9d9-1f3fc-200d-2640-fe0f", + "1f9d9-1f3fc-200d-2642-fe0f", + "1f9d9-1f3fd", + "1f9d9-1f3fd-200d-2640-fe0f", + "1f9d9-1f3fd-200d-2642-fe0f", + "1f9d9-1f3fe", + "1f9d9-1f3fe-200d-2640-fe0f", + "1f9d9-1f3fe-200d-2642-fe0f", + "1f9d9-1f3ff", + "1f9d9-1f3ff-200d-2640-fe0f", + "1f9d9-1f3ff-200d-2642-fe0f", + "1f9d9-200d-2640-fe0f", + "1f9d9-200d-2642-fe0f", + "1f9da", + "1f9da-1f3fb", + "1f9da-1f3fb-200d-2640-fe0f", + "1f9da-1f3fb-200d-2642-fe0f", + "1f9da-1f3fc", + "1f9da-1f3fc-200d-2640-fe0f", + "1f9da-1f3fc-200d-2642-fe0f", + "1f9da-1f3fd", + "1f9da-1f3fd-200d-2640-fe0f", + "1f9da-1f3fd-200d-2642-fe0f", + "1f9da-1f3fe", + "1f9da-1f3fe-200d-2640-fe0f", + "1f9da-1f3fe-200d-2642-fe0f", + "1f9da-1f3ff", + "1f9da-1f3ff-200d-2640-fe0f", + "1f9da-1f3ff-200d-2642-fe0f", + "1f9da-200d-2640-fe0f", + "1f9da-200d-2642-fe0f", + "1f9db", + "1f9db-1f3fb", + "1f9db-1f3fb-200d-2640-fe0f", + "1f9db-1f3fb-200d-2642-fe0f", + "1f9db-1f3fc", + "1f9db-1f3fc-200d-2640-fe0f", + "1f9db-1f3fc-200d-2642-fe0f", + "1f9db-1f3fd", + "1f9db-1f3fd-200d-2640-fe0f", + "1f9db-1f3fd-200d-2642-fe0f", + "1f9db-1f3fe", + "1f9db-1f3fe-200d-2640-fe0f", + "1f9db-1f3fe-200d-2642-fe0f", + "1f9db-1f3ff", + "1f9db-1f3ff-200d-2640-fe0f", + "1f9db-1f3ff-200d-2642-fe0f", + "1f9db-200d-2640-fe0f", + "1f9db-200d-2642-fe0f", + "1f9dc", + "1f9dc-1f3fb", + "1f9dc-1f3fb-200d-2640-fe0f", + "1f9dc-1f3fb-200d-2642-fe0f", + "1f9dc-1f3fc", + "1f9dc-1f3fc-200d-2640-fe0f", + "1f9dc-1f3fc-200d-2642-fe0f", + "1f9dc-1f3fd", + "1f9dc-1f3fd-200d-2640-fe0f", + "1f9dc-1f3fd-200d-2642-fe0f", + "1f9dc-1f3fe", + "1f9dc-1f3fe-200d-2640-fe0f", + "1f9dc-1f3fe-200d-2642-fe0f", + "1f9dc-1f3ff", + "1f9dc-1f3ff-200d-2640-fe0f", + "1f9dc-1f3ff-200d-2642-fe0f", + "1f9dc-200d-2640-fe0f", + "1f9dc-200d-2642-fe0f", + "1f9dd", + "1f9dd-1f3fb", + "1f9dd-1f3fb-200d-2640-fe0f", + "1f9dd-1f3fb-200d-2642-fe0f", + "1f9dd-1f3fc", + "1f9dd-1f3fc-200d-2640-fe0f", + "1f9dd-1f3fc-200d-2642-fe0f", + "1f9dd-1f3fd", + "1f9dd-1f3fd-200d-2640-fe0f", + "1f9dd-1f3fd-200d-2642-fe0f", + "1f9dd-1f3fe", + "1f9dd-1f3fe-200d-2640-fe0f", + "1f9dd-1f3fe-200d-2642-fe0f", + "1f9dd-1f3ff", + "1f9dd-1f3ff-200d-2640-fe0f", + "1f9dd-1f3ff-200d-2642-fe0f", + "1f9dd-200d-2640-fe0f", + "1f9dd-200d-2642-fe0f", + "1f9de", + "1f9de-200d-2640-fe0f", + "1f9de-200d-2642-fe0f", + "1f9df", + "1f9df-200d-2640-fe0f", + "1f9df-200d-2642-fe0f", + "1f9e0", + "1f9e1", + "1f9e2", + "1f9e3", + "1f9e4", + "1f9e5", + "1f9e6", + "1f9e7", + "1f9e8", + "1f9e9", + "1f9ea", + "1f9eb", + "1f9ec", + "1f9ed", + "1f9ee", + "1f9ef", + "1f9f0", + "1f9f1", + "1f9f2", + "1f9f3", + "1f9f4", + "1f9f5", + "1f9f6", + "1f9f7", + "1f9f8", + "1f9f9", + "1f9fa", + "1f9fb", + "1f9fc", + "1f9fd", + "1f9fe", + "1f9ff", + "1fa70", + "1fa71", + "1fa72", + "1fa73", + "1fa74", + "1fa75", + "1fa76", + "1fa77", + "1fa78", + "1fa79", + "1fa7a", + "1fa7b", + "1fa7c", + "1fa80", + "1fa81", + "1fa82", + "1fa83", + "1fa84", + "1fa85", + "1fa86", + "1fa87", + "1fa88", + "1fa90", + "1fa91", + "1fa92", + "1fa93", + "1fa94", + "1fa95", + "1fa96", + "1fa97", + "1fa98", + "1fa99", + "1fa9a", + "1fa9b", + "1fa9c", + "1fa9d", + "1fa9e", + "1fa9f", + "1faa0", + "1faa1", + "1faa2", + "1faa3", + "1faa4", + "1faa5", + "1faa6", + "1faa7", + "1faa8", + "1faa9", + "1faaa", + "1faab", + "1faac", + "1faad", + "1faae", + "1faaf", + "1fab0", + "1fab1", + "1fab2", + "1fab3", + "1fab4", + "1fab5", + "1fab6", + "1fab7", + "1fab8", + "1fab9", + "1faba", + "1fabb", + "1fabc", + "1fabd", + "1fabf", + "1fac0", + "1fac1", + "1fac2", + "1fac3", + "1fac3-1f3fb", + "1fac3-1f3fc", + "1fac3-1f3fd", + "1fac3-1f3fe", + "1fac3-1f3ff", + "1fac4", + "1fac4-1f3fb", + "1fac4-1f3fc", + "1fac4-1f3fd", + "1fac4-1f3fe", + "1fac4-1f3ff", + "1fac5", + "1fac5-1f3fb", + "1fac5-1f3fc", + "1fac5-1f3fd", + "1fac5-1f3fe", + "1fac5-1f3ff", + "1face", + "1facf", + "1fad0", + "1fad1", + "1fad2", + "1fad3", + "1fad4", + "1fad5", + "1fad6", + "1fad7", + "1fad8", + "1fad9", + "1fada", + "1fadb", + "1fae0", + "1fae1", + "1fae2", + "1fae3", + "1fae4", + "1fae5", + "1fae6", + "1fae7", + "1fae8", + "1faf0", + "1faf0-1f3fb", + "1faf0-1f3fc", + "1faf0-1f3fd", + "1faf0-1f3fe", + "1faf0-1f3ff", + "1faf1", + "1faf1-1f3fb", + "1faf1-1f3fb-200d-1faf2-1f3fc", + "1faf1-1f3fb-200d-1faf2-1f3fd", + "1faf1-1f3fb-200d-1faf2-1f3fe", + "1faf1-1f3fb-200d-1faf2-1f3ff", + "1faf1-1f3fc", + "1faf1-1f3fc-200d-1faf2-1f3fb", + "1faf1-1f3fc-200d-1faf2-1f3fd", + "1faf1-1f3fc-200d-1faf2-1f3fe", + "1faf1-1f3fc-200d-1faf2-1f3ff", + "1faf1-1f3fd", + "1faf1-1f3fd-200d-1faf2-1f3fb", + "1faf1-1f3fd-200d-1faf2-1f3fc", + "1faf1-1f3fd-200d-1faf2-1f3fe", + "1faf1-1f3fd-200d-1faf2-1f3ff", + "1faf1-1f3fe", + "1faf1-1f3fe-200d-1faf2-1f3fb", + "1faf1-1f3fe-200d-1faf2-1f3fc", + "1faf1-1f3fe-200d-1faf2-1f3fd", + "1faf1-1f3fe-200d-1faf2-1f3ff", + "1faf1-1f3ff", + "1faf1-1f3ff-200d-1faf2-1f3fb", + "1faf1-1f3ff-200d-1faf2-1f3fc", + "1faf1-1f3ff-200d-1faf2-1f3fd", + "1faf1-1f3ff-200d-1faf2-1f3fe", + "1faf2", + "1faf2-1f3fb", + "1faf2-1f3fc", + "1faf2-1f3fd", + "1faf2-1f3fe", + "1faf2-1f3ff", + "1faf3", + "1faf3-1f3fb", + "1faf3-1f3fc", + "1faf3-1f3fd", + "1faf3-1f3fe", + "1faf3-1f3ff", + "1faf4", + "1faf4-1f3fb", + "1faf4-1f3fc", + "1faf4-1f3fd", + "1faf4-1f3fe", + "1faf4-1f3ff", + "1faf5", + "1faf5-1f3fb", + "1faf5-1f3fc", + "1faf5-1f3fd", + "1faf5-1f3fe", + "1faf5-1f3ff", + "1faf6", + "1faf6-1f3fb", + "1faf6-1f3fc", + "1faf6-1f3fd", + "1faf6-1f3fe", + "1faf6-1f3ff", + "1faf7", + "1faf7-1f3fb", + "1faf7-1f3fc", + "1faf7-1f3fd", + "1faf7-1f3fe", + "1faf7-1f3ff", + "1faf8", + "1faf8-1f3fb", + "1faf8-1f3fc", + "1faf8-1f3fd", + "1faf8-1f3fe", + "1faf8-1f3ff", + "203c", + "2049", + "2122", + "2139", + "2194", + "2195", + "2196", + "2197", + "2198", + "2199", + "21a9", + "21aa", + "23-20e3", + "231a", + "231b", + "2328", + "23cf", + "23e9", + "23ea", + "23eb", + "23ec", + "23ed", + "23ee", + "23ef", + "23f0", + "23f1", + "23f2", + "23f3", + "23f8", + "23f9", + "23fa", + "24c2", + "25aa", + "25ab", + "25b6", + "25c0", + "25fb", + "25fc", + "25fd", + "25fe", + "2600", + "2601", + "2602", + "2603", + "2604", + "260e", + "2611", + "2614", + "2615", + "2618", + "261d", + "261d-1f3fb", + "261d-1f3fc", + "261d-1f3fd", + "261d-1f3fe", + "261d-1f3ff", + "2620", + "2622", + "2623", + "2626", + "262a", + "262e", + "262f", + "2638", + "2639", + "263a", + "2640", + "2642", + "2648", + "2649", + "264a", + "264b", + "264c", + "264d", + "264e", + "264f", + "2650", + "2651", + "2652", + "2653", + "265f", + "2660", + "2663", + "2665", + "2666", + "2668", + "267b", + "267e", + "267f", + "2692", + "2693", + "2694", + "2695", + "2696", + "2697", + "2699", + "269b", + "269c", + "26a0", + "26a1", + "26a7", + "26aa", + "26ab", + "26b0", + "26b1", + "26bd", + "26be", + "26c4", + "26c5", + "26c8", + "26ce", + "26cf", + "26d1", + "26d3", + "26d4", + "26e9", + "26ea", + "26f0", + "26f1", + "26f2", + "26f3", + "26f4", + "26f5", + "26f7", + "26f7-1f3fb", + "26f7-1f3fc", + "26f7-1f3fd", + "26f7-1f3fe", + "26f7-1f3ff", + "26f8", + "26f9", + "26f9-1f3fb", + "26f9-1f3fb-200d-2640-fe0f", + "26f9-1f3fb-200d-2642-fe0f", + "26f9-1f3fc", + "26f9-1f3fc-200d-2640-fe0f", + "26f9-1f3fc-200d-2642-fe0f", + "26f9-1f3fd", + "26f9-1f3fd-200d-2640-fe0f", + "26f9-1f3fd-200d-2642-fe0f", + "26f9-1f3fe", + "26f9-1f3fe-200d-2640-fe0f", + "26f9-1f3fe-200d-2642-fe0f", + "26f9-1f3ff", + "26f9-1f3ff-200d-2640-fe0f", + "26f9-1f3ff-200d-2642-fe0f", + "26f9-fe0f-200d-2640-fe0f", + "26f9-fe0f-200d-2642-fe0f", + "26fa", + "26fd", + "2702", + "2705", + "2708", + "2709", + "270a", + "270a-1f3fb", + "270a-1f3fc", + "270a-1f3fd", + "270a-1f3fe", + "270a-1f3ff", + "270b", + "270b-1f3fb", + "270b-1f3fc", + "270b-1f3fd", + "270b-1f3fe", + "270b-1f3ff", + "270c", + "270c-1f3fb", + "270c-1f3fc", + "270c-1f3fd", + "270c-1f3fe", + "270c-1f3ff", + "270d", + "270d-1f3fb", + "270d-1f3fc", + "270d-1f3fd", + "270d-1f3fe", + "270d-1f3ff", + "270f", + "2712", + "2714", + "2716", + "271d", + "2721", + "2728", + "2733", + "2734", + "2744", + "2747", + "274c", + "274e", + "2753", + "2754", + "2755", + "2757", + "2763", + "2764", + "2764-fe0f-200d-1f525", + "2764-fe0f-200d-1fa79", + "2795", + "2796", + "2797", + "27a1", + "27b0", + "27bf", + "2934", + "2935", + "2a-20e3", + "2b05", + "2b06", + "2b07", + "2b1b", + "2b1c", + "2b50", + "2b55", + "30-20e3", + "3030", + "303d", + "31-20e3", + "32-20e3", + "3297", + "3299", + "33-20e3", + "34-20e3", + "35-20e3", + "36-20e3", + "37-20e3", + "38-20e3", + "39-20e3", + "a9", + "ae", + "e50a" +] diff --git a/packages/markdown/src/index.ts b/packages/markdown/src/index.ts index f5c7f3684..8542d3344 100644 --- a/packages/markdown/src/index.ts +++ b/packages/markdown/src/index.ts @@ -18,6 +18,8 @@ declare module "vfile" { anchor: string; label: string; }[]; + /** Twemoji file names used on the page, without the .svg extension. */ + emojis: string[]; searchDocuments: { description: string; keywords: string[]; diff --git a/packages/markdown/src/process.ts b/packages/markdown/src/process.ts index 2d33a03ee..acf909bdb 100644 --- a/packages/markdown/src/process.ts +++ b/packages/markdown/src/process.ts @@ -59,6 +59,7 @@ import remarkDirectiveMultievent from "./remarkDirectiveMultievent"; import remarkDirectiveUnpack from "./remarkDirectiveUnpack"; import { makeTransformerCopyButton } from "./rehypePrettyCodeCopyButton"; import { remarkGithubEmoji } from "./remarkGithubEmoji"; +import { rehypeEmoji } from "./rehypeEmoji"; import remarkParse from "./remarkParse"; import remarkSubSup from "./remarkSubSup"; import remarkImageAttrs from "./remarkImageAttrs"; @@ -200,6 +201,8 @@ export const process = (md: string, ctx: HyperbookContext) => { return remark(ctx) .use(rehypePlugins) .use(rehypeShell(ctx)) + /* needs to be after the shell, so icons from the config are covered too */ + .use(rehypeEmoji, ctx) .use(rehypeHtmlStructure(ctx)) .use(rehypeStringify, { allowDangerousCharacters: true, diff --git a/packages/markdown/src/rehypeEmoji.ts b/packages/markdown/src/rehypeEmoji.ts new file mode 100644 index 000000000..ea9b4d98e --- /dev/null +++ b/packages/markdown/src/rehypeEmoji.ts @@ -0,0 +1,162 @@ +import { Plugin } from "unified"; +import { Element, ElementContent, Parent, Root, Text } from "hast"; +import { HyperbookContext } from "@hyperbook/types"; +import { VFile } from "vfile"; + +import emojiAssets from "./emoji-assets.json"; + +const availableEmojis = new Set(emojiAssets); + +/** + * Elements whose text is never emoji: replacing a character inside them would + * either break the code that is being shown or the script that is running. + */ +const skippedTags = new Set([ + "pre", + "code", + "script", + "style", + "textarea", + "title", +]); + +const VS16 = "\\uFE0F"; +const ZWJ = "\\u200D"; +const MODIFIER = "\\p{Emoji_Modifier}"; +const PICTOGRAPHIC = "\\p{Extended_Pictographic}"; +/** Tag sequence, used by the subdivision flags (England, Scotland, Wales). */ +const TAG = "(?:[\\u{E0020}-\\u{E007E}]+\\u{E007F})"; +/** A single emoji, which may be a flag, or a base with a modifier or a tag. */ +const ELEMENT = `(?:\\p{RI}\\p{RI}|${PICTOGRAPHIC}(?:${MODIFIER}|${VS16})?${TAG}?)`; +/** Keycaps are built from plain digits, so they need their own rule. */ +const KEYCAP = `(?:[0-9#*]${VS16}?\\u20E3)`; + +const emojiPattern = new RegExp( + `${KEYCAP}|(?:${ELEMENT}(?:${ZWJ}${ELEMENT})*)`, + "gu", +); + +/** + * Extended_Pictographic also covers characters that are text by default, like + * © or ™. Those should only become an image when the author asked for the + * emoji presentation, which is what a variation selector, a keycap or a + * regional indicator does. + */ +const hasEmojiPresentation = (emoji: string): boolean => + emoji.includes("\uFE0F") || + emoji.includes("\u20E3") || + /^\p{Emoji_Presentation}/u.test(emoji) || + /^\p{RI}/u.test(emoji); + +/** + * Builds the Twemoji file name of an emoji. Twemoji drops the variation + * selector, unless the emoji is a sequence joined by a zero width joiner. + */ +export const toEmojiId = (emoji: string): string => { + const codePoints = Array.from(emoji).map((c) => c.codePointAt(0) as number); + const isSequence = codePoints.includes(0x200d); + return codePoints + .filter((codePoint) => isSequence || codePoint !== 0xfe0f) + .map((codePoint) => codePoint.toString(16)) + .join("-"); +}; + +const makeImage = ( + ctx: HyperbookContext, + emoji: string, + id: string, +): Element => ({ + type: "element", + tagName: "img", + properties: { + class: "emoji", + src: ctx.makeUrl(["emoji", `${id}.svg`], "assets"), + alt: emoji, + draggable: "false", + loading: "lazy", + decoding: "async", + width: 20, + height: 20, + }, + children: [], +}); + +/** + * Replaces emoji characters with Twemoji images, so a hyperbook looks the same + * on every platform instead of picking up the emoji font of the reader's + * operating system. + */ +export const rehypeEmoji: Plugin<[HyperbookContext], Root> = + (ctx: HyperbookContext) => (tree: Root, file: VFile) => { + if (ctx.config.elements?.emoji?.style !== "twemoji") { + return; + } + + const used = new Set(file.data.emojis || []); + + const splitText = (node: Text): ElementContent[] | null => { + let children: ElementContent[] | null = null; + let lastIndex = 0; + + for (const match of node.value.matchAll(emojiPattern)) { + const emoji = match[0]; + const id = toEmojiId(emoji); + if (!hasEmojiPresentation(emoji) || !availableEmojis.has(id)) { + continue; + } + + if (!children) children = []; + if (match.index > lastIndex) { + children.push({ + type: "text", + value: node.value.slice(lastIndex, match.index), + }); + } + children.push(makeImage(ctx, emoji, id)); + used.add(id); + lastIndex = match.index + emoji.length; + } + + if (!children) { + return null; + } + if (lastIndex < node.value.length) { + children.push({ type: "text", value: node.value.slice(lastIndex) }); + } + return children; + }; + + const walk = (parent: Parent) => { + const children: ElementContent[] = []; + let replaced = false; + + for (const child of parent.children as ElementContent[]) { + if (child.type === "text") { + const split = splitText(child); + if (split) { + children.push(...split); + replaced = true; + continue; + } + } else if ( + child.type === "element" && + !skippedTags.has(child.tagName) + ) { + walk(child); + } + children.push(child); + } + + if (replaced) { + parent.children = children; + } + }; + + walk(tree); + + if (used.size > 0) { + file.data.emojis = [...used]; + } + }; + +export default rehypeEmoji; diff --git a/packages/markdown/tests/rehypeEmoji.test.ts b/packages/markdown/tests/rehypeEmoji.test.ts new file mode 100644 index 000000000..0e9d5414f --- /dev/null +++ b/packages/markdown/tests/rehypeEmoji.test.ts @@ -0,0 +1,101 @@ +import { HyperbookContext } from "@hyperbook/types/dist"; +import { describe, expect, it } from "vitest"; +import rehypeStringify from "rehype-stringify"; +import remarkToRehype from "remark-rehype"; +import { unified } from "unified"; +import { VFile } from "vfile"; +import { ctx as baseCtx } from "./mock"; +import { rehypeEmoji, toEmojiId } from "../src/rehypeEmoji"; +import { remarkGithubEmoji } from "../src/remarkGithubEmoji"; +import remarkParse from "../src/remarkParse"; + +const twemojiCtx: HyperbookContext = { + ...baseCtx, + config: { + ...baseCtx.config, + elements: { ...baseCtx.config.elements, emoji: { style: "twemoji" } }, + }, +}; + +const toHtml = (md: string, ctx: HyperbookContext) => { + const file = new VFile(md); + const value = unified() + .use(remarkParse) + .use(remarkGithubEmoji) + .use(remarkToRehype) + .use(rehypeEmoji, ctx) + .use(rehypeStringify, { + allowDangerousCharacters: true, + allowDangerousHtml: true, + }) + .processSync(file).value; + return { html: String(value), file }; +}; + +describe("toEmojiId", () => { + it("should drop the variation selector", () => { + expect(toEmojiId("❤️")).toBe("2764"); + }); + it("should keep the variation selector in a joined sequence", () => { + expect(toEmojiId("❤️‍🔥")).toBe("2764-fe0f-200d-1f525"); + }); + it("should keep skin tone modifiers", () => { + expect(toEmojiId("👍🏻")).toBe("1f44d-1f3fb"); + }); + it("should build keycaps without the variation selector", () => { + expect(toEmojiId("1️⃣")).toBe("31-20e3"); + }); +}); + +describe("rehypeEmoji", () => { + it("should replace a shortcode with an image", () => { + const { html } = toHtml(":penguin:", twemojiCtx); + expect(html).toContain(`src="/assets/emoji/1f427.svg"`); + expect(html).toContain(`alt="🐧"`); + expect(html).toContain(`class="emoji"`); + }); + + it("should replace an emoji that was typed directly", () => { + const { html } = toHtml("Hello 🐧 world", twemojiCtx); + expect(html).toContain(`src="/assets/emoji/1f427.svg"`); + expect(html).toContain("Hello "); + expect(html).toContain(" world"); + }); + + it("should replace flags, keycaps and joined sequences", () => { + const { html } = toHtml("🇩🇪 1️⃣ 👩‍💻 👍🏻", twemojiCtx); + expect(html).toContain(`src="/assets/emoji/1f1e9-1f1ea.svg"`); + expect(html).toContain(`src="/assets/emoji/31-20e3.svg"`); + expect(html).toContain(`src="/assets/emoji/1f469-200d-1f4bb.svg"`); + expect(html).toContain(`src="/assets/emoji/1f44d-1f3fb.svg"`); + }); + + it("should keep characters that are text by default", () => { + const { html } = toHtml("© 2024 — ½ ‼", twemojiCtx); + expect(html).not.toContain(" { + const { html } = toHtml("There are 3 issues in #42", twemojiCtx); + expect(html).not.toContain(" { + const { html } = toHtml("`🐧` and\n\n```\n🐧\n```\n", twemojiCtx); + expect(html).not.toContain(" { + const { file } = toHtml(":penguin: :penguin: :apple:", twemojiCtx); + expect(file.data.emojis?.sort()).toEqual(["1f34e", "1f427"]); + }); + + it("should keep emojis as text when the style is native", () => { + const { html, file } = toHtml(":penguin:", baseCtx); + expect(html).not.toContain(" file.endsWith(".svg")) + .map((file) => file.slice(0, -4)) + .sort(); + +const outputPath = path.join("src", "emoji-assets.json"); +fs.writeFileSync(outputPath, JSON.stringify(files, null, 2) + "\n", "utf8"); + +console.log(`✅ Saved ${files.length} emoji assets to ${outputPath}`); diff --git a/packages/markdown/updateEmojis.mjs b/packages/markdown/updateEmojis.mjs index bd27fb24d..8d3ee3564 100644 --- a/packages/markdown/updateEmojis.mjs +++ b/packages/markdown/updateEmojis.mjs @@ -43,6 +43,29 @@ Just like in most chat apps you can insert an emoji by using its name. \`\`\` :smiley: :apple: :penguin: + +## Consistent Emojis + +Emojis are drawn by the operating system of your reader, so the same emoji looks +different on Windows, macOS, Android and Linux. Set `elements.emoji.style` to +`twemoji` to replace them with [Twemoji](https://github.com/jdecked/twemoji) +images, which look the same everywhere. + +```json +{ + "elements": { + "emoji": { + "style": "twemoji" + } + } +} +``` + +This covers emojis in your content as well as the icons you configure, for +example for links. Emojis in code blocks are never replaced, and only the emojis +you actually use are copied into your build. + +Twemoji graphics are licensed CC-BY 4.0 by Twitter, Inc and other contributors. `; const deBase = `--- @@ -59,6 +82,31 @@ Wie in den meisten Chat-Apps kannst du Emojis durch Eingabe ihres Namens einfüg \`\`\` :smiley: :apple: :penguin: + +## Einheitliche Emojis + +Emojis werden vom Betriebssystem deiner Leser gezeichnet, dasselbe Emoji sieht +daher unter Windows, macOS, Android und Linux unterschiedlich aus. Setze +`elements.emoji.style` auf `twemoji`, um sie durch +[Twemoji](https://github.com/jdecked/twemoji)-Bilder zu ersetzen, die überall +gleich aussehen. + +```json +{ + "elements": { + "emoji": { + "style": "twemoji" + } + } +} +``` + +Das gilt für Emojis in deinen Inhalten und für die Icons, die du konfigurierst, +zum Beispiel für Links. Emojis in Codeblöcken werden nie ersetzt und es werden +nur die Emojis in deinen Build kopiert, die du tatsächlich verwendest. + +Twemoji-Grafiken stehen unter der CC-BY-4.0-Lizenz von Twitter, Inc und weiteren +Mitwirkenden. `; const enHeader = `# GitHub Emoji Cheat Sheet diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 578c46056..805db9c6b 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -203,6 +203,16 @@ export type HyperbookJson = { db?: string; height?: string | number; }; + emoji?: ElementConfig & { + /** + * How emojis are rendered. + * - "native": Emojis are kept as text and drawn with the emoji font of + * the reader's operating system (default). + * - "twemoji": Emojis are replaced with Twemoji images, so they look the + * same on every platform. + */ + style?: "native" | "twemoji"; + }; }; }; diff --git a/platforms/vscode/schemas/hyperbook.schema.json b/platforms/vscode/schemas/hyperbook.schema.json index 7956991fc..c5efbe7e7 100644 --- a/platforms/vscode/schemas/hyperbook.schema.json +++ b/platforms/vscode/schemas/hyperbook.schema.json @@ -194,6 +194,31 @@ } ] }, + "emoji": { + "allOf": [ + { + "properties": { + "version": { + "type": "string" + } + }, + "type": "object" + }, + { + "properties": { + "style": { + "description": "How emojis are rendered.\n- \"native\": Emojis are kept as text and drawn with the emoji font of\n the reader's operating system (default).\n- \"twemoji\": Emojis are replaced with Twemoji images, so they look the\n same on every platform.", + "enum": [ + "native", + "twemoji" + ], + "type": "string" + } + }, + "type": "object" + } + ] + }, "excalidraw": { "allOf": [ { @@ -223,6 +248,29 @@ } ] }, + "kirimoto": { + "allOf": [ + { + "properties": { + "version": { + "type": "string" + } + }, + "type": "object" + }, + { + "properties": { + "height": { + "type": "string" + }, + "settings": { + "type": "string" + } + }, + "type": "object" + } + ] + }, "onlineide": { "allOf": [ { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index baee9568b..d005acea7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -399,6 +399,9 @@ importers: '@shikijs/types': specifier: ^3.21.0 version: 3.21.0 + '@twemoji/svg': + specifier: 15.0.0 + version: 15.0.0 '@types/hast': specifier: 3.0.4 version: 3.0.4 @@ -2585,6 +2588,9 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@twemoji/svg@15.0.0': + resolution: {integrity: sha512-ZSPef2B6nBaYnfgdTbAy4jgW95o7pi2xPGwGCU+WMTxo7J6B1lMPTWwSq/wTuiMq+N0khQ90CcvYp1wFoQpo/w==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -11025,6 +11031,8 @@ snapshots: '@tsconfig/node16@1.0.4': {} + '@twemoji/svg@15.0.0': {} + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 diff --git a/website/de/book/elements/emoji.md b/website/de/book/elements/emoji.md index b43605021..8b3588b6e 100644 --- a/website/de/book/elements/emoji.md +++ b/website/de/book/elements/emoji.md @@ -13,6 +13,31 @@ Wie in den meisten Chat-Apps kannst du Emojis durch Eingabe ihres Namens einfüg :smiley: :apple: :penguin: +## Einheitliche Emojis + +Emojis werden vom Betriebssystem deiner Leser gezeichnet, dasselbe Emoji sieht +daher unter Windows, macOS, Android und Linux unterschiedlich aus. Setze +`elements.emoji.style` auf `twemoji`, um sie durch +[Twemoji](https://github.com/jdecked/twemoji)-Bilder zu ersetzen, die überall +gleich aussehen. + +```json +{ + "elements": { + "emoji": { + "style": "twemoji" + } + } +} +``` + +Das gilt für Emojis in deinen Inhalten und für die Icons, die du konfigurierst, +zum Beispiel für Links. Emojis in Codeblöcken werden nie ersetzt und es werden +nur die Emojis in deinen Build kopiert, die du tatsächlich verwendest. + +Twemoji-Grafiken stehen unter der CC-BY-4.0-Lizenz von Twitter, Inc und weiteren +Mitwirkenden. + # GitHub Emoji Cheat Sheet Diese Datei listet alle unterstützten GitHub-Emoji-Kürzel und ihre entsprechenden Unicode-Zeichen auf. diff --git a/website/en/book/elements/emoji.md b/website/en/book/elements/emoji.md index d71da5577..be2669fea 100644 --- a/website/en/book/elements/emoji.md +++ b/website/en/book/elements/emoji.md @@ -13,6 +13,29 @@ Just like in most chat apps you can insert an emoji by using its name. :smiley: :apple: :penguin: +## Consistent Emojis + +Emojis are drawn by the operating system of your reader, so the same emoji looks +different on Windows, macOS, Android and Linux. Set `elements.emoji.style` to +`twemoji` to replace them with [Twemoji](https://github.com/jdecked/twemoji) +images, which look the same everywhere. + +```json +{ + "elements": { + "emoji": { + "style": "twemoji" + } + } +} +``` + +This covers emojis in your content as well as the icons you configure, for +example for links. Emojis in code blocks are never replaced, and only the emojis +you actually use are copied into your build. + +Twemoji graphics are licensed CC-BY 4.0 by Twitter, Inc and other contributors. + # GitHub Emoji Cheat Sheet This file lists all supported GitHub-style emoji shortcodes and their corresponding Unicode characters. From 6b6ff07d0c80b617b29244550a3416b83e51cc80 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 09:08:02 +0000 Subject: [PATCH 3/7] Store bookmark labels as parts instead of markup A bookmark label was interpolated into the onclick attribute of the bookmark button and rendered with innerHTML. That had three problems: a heading containing a quote or a backslash produced a broken JavaScript string and the button threw, the label was persisted markup that came back through innerHTML, and the label was the markdown text rather than what the page renders, so an emoji drawn as an image was a plain character again in the bookmark list. Labels are now read from the rendered heading when a bookmark is saved and stored as parts, each with its text and, for an emoji, its id. The bookmark list builds its entries from those parts with the DOM, so nothing that was stored is parsed as HTML, emojis keep the look they have on the page, and the id rather than a URL means a bookmark survives a change of the basePath. The button lost its inline onclick and is handled by one delegated listener, which also covers headings that are added later. It carries data-key, a data-label fallback and aria-pressed. The store moves to version 6, which drops the pointless index on the label and migrates existing labels to the new shape. A plain string label is still rendered, so bookmarks from an older version and from an older export keep working. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012kkjvqifo7fQ5W6woRWiSW --- .changeset/olive-rules-repeat.md | 33 +++++++++++++++ packages/markdown/assets/bootstrap.js | 14 +++++++ .../assets/directive-bookmarks/client.js | 28 ++++++++++++- packages/markdown/assets/hyperbook.types.js | 24 ++++++++++- packages/markdown/assets/store.js | 16 +++++++ packages/markdown/assets/ui.js | 42 +++++++++++++++++-- packages/markdown/src/rehypeEmoji.ts | 1 + packages/markdown/src/rehypeHtmlStructure.ts | 19 +++++++++ packages/markdown/src/remarkHeadings.ts | 6 ++- .../tests/__snapshots__/process.test.ts.snap | 2 +- packages/markdown/tests/rehypeEmoji.test.ts | 6 +++ .../markdown/tests/remarkHeadings.test.ts | 38 +++++++++++++---- 12 files changed, 213 insertions(+), 16 deletions(-) create mode 100644 .changeset/olive-rules-repeat.md diff --git a/.changeset/olive-rules-repeat.md b/.changeset/olive-rules-repeat.md new file mode 100644 index 000000000..9d02aae4e --- /dev/null +++ b/.changeset/olive-rules-repeat.md @@ -0,0 +1,33 @@ +--- +"@hyperbook/markdown": minor +"hyperbook": minor +"hyperbook-studio": minor +--- + +Rework how bookmarks store their label, so bookmarks show what the page shows. + +A bookmark label used to be baked into an `onclick` attribute of the bookmark +button and rendered with `innerHTML`. Labels are now read from the rendered +heading when a bookmark is saved, and stored as parts instead of markup: + +```js +[{ text: "Getting started " }, { text: "🐧", emoji: "1f427" }]; +``` + +The bookmark list builds its entries from those parts with the DOM, so nothing +that was stored is parsed as HTML, and an emoji that is rendered as an image +stays an image in the bookmark list. Emojis are stored by id, never by URL, so +bookmarks survive a change of the `basePath`. + +This also fixes bookmarking a heading that contains a quote or a backslash. +Those characters ended up inside a JavaScript string in the `onclick` +attribute and made the button throw a syntax error. + +Breaking: + +- `hyperbook.ui.toggleBookmark(key, label)` no longer takes a label: + `hyperbook.ui.toggleBookmark(key)`. The label comes from the heading. +- Bookmark buttons no longer carry an inline `onclick`. They are handled by a + delegated listener and carry `data-key`, `data-label` and `aria-pressed`. +- The store moves to version 6. Labels of existing bookmarks are migrated to + the new shape, and a plain label is still rendered, so nothing is lost. diff --git a/packages/markdown/assets/bootstrap.js b/packages/markdown/assets/bootstrap.js index f76016bd6..198848935 100644 --- a/packages/markdown/assets/bootstrap.js +++ b/packages/markdown/assets/bootstrap.js @@ -98,11 +98,25 @@ hyperbook.bootstrap = (function () { hyperbook.store.db.bookmarks.get(key).then((bookmark) => { if (bookmark) { bookmarkEl.classList.add("active"); + bookmarkEl.setAttribute("aria-pressed", "true"); } }); } } + /** + * Bookmark buttons are handled by one delegated listener, so headings that + * are added later work too and no heading has to carry its label in an + * inline script. + */ + document.addEventListener("click", (event) => { + const bookmarkEl = event.target.closest?.("button.bookmark"); + const key = bookmarkEl?.getAttribute("data-key"); + if (key) { + hyperbook.ui.toggleBookmark(key); + } + }); + /** * Initialize all hyperbook elements within a root. * @param {HTMLElement} root diff --git a/packages/markdown/assets/directive-bookmarks/client.js b/packages/markdown/assets/directive-bookmarks/client.js index cf066a913..5fe350aed 100644 --- a/packages/markdown/assets/directive-bookmarks/client.js +++ b/packages/markdown/assets/directive-bookmarks/client.js @@ -7,6 +7,32 @@ * @see hyperbook.store */ hyperbook.bookmarks = (function () { + /** + * Render a stored label into a link. Labels are built from parts instead of + * markup, so nothing that was persisted is ever parsed as HTML. + * @param {Element} link + * @param {HyperbookBookmarkLabel | string} label + */ + function renderLabel(link, label) { + // Bookmarks saved before labels became parts. + if (typeof label === "string") { + link.textContent = label; + return; + } + for (let part of label || []) { + if (part.emoji && hyperbook.emoji?.base) { + const img = document.createElement("img"); + img.className = "emoji"; + img.src = `${hyperbook.emoji.base}/${part.emoji}.svg`; + img.alt = part.text || ""; + img.draggable = false; + link.append(img); + } else if (part.text) { + link.append(document.createTextNode(part.text)); + } + } + } + function update(root = document) { hyperbook.store.db.bookmarks .toArray() @@ -15,7 +41,7 @@ hyperbook.bookmarks = (function () { const bookmarkEl = root.createElement("li"); const link = root.createElement("a"); link.href = bookmark.path; - link.innerHTML = bookmark.label; + renderLabel(link, bookmark.label); bookmarkEl.append(link); return bookmarkEl; }); diff --git a/packages/markdown/assets/hyperbook.types.js b/packages/markdown/assets/hyperbook.types.js index 4529c2537..3812630e2 100644 --- a/packages/markdown/assets/hyperbook.types.js +++ b/packages/markdown/assets/hyperbook.types.js @@ -55,11 +55,32 @@ * @property {(id: string) => void} togglePlayPause - Toggle play/pause for an audio element. */ +/** + * One piece of a bookmark label. `text` is what the heading reads, `emoji` is + * the id of a Twemoji image, so the label can be rendered without storing + * markup or a URL. + * @typedef {{ text: string, emoji?: string }} HyperbookBookmarkLabelPart + */ + +/** + * A bookmark label, built from parts. A plain string comes from a bookmark + * that was stored before labels became parts. + * @typedef {HyperbookBookmarkLabelPart[]} HyperbookBookmarkLabel + */ + /** * @typedef {Object} HyperbookBookmarks * @property {(root?: Document) => void} update - Refresh the bookmarks list in the DOM. */ +/** + * Emoji rendering of the hyperbook. Only set when the emojis are rendered as + * images. + * @typedef {Object} HyperbookEmoji + * @property {"twemoji"} style - How emojis are rendered. + * @property {string} base - URL of the folder that holds the emoji images. + */ + /** * @typedef {Object} HyperbookDownload * @property {(root: HTMLElement) => void} init - Initialize download status indicators. @@ -150,7 +171,7 @@ * store: HyperbookStore, * cloud?: HyperbookCloud, * toggleLightbox: (el: HTMLElement) => void, - * toggleBookmark: (key: string, label: string) => void, + * toggleBookmark: (key: string) => void, * navToggle: () => void, * tocToggle: () => void, * searchToggle: () => void, @@ -166,6 +187,7 @@ * archive?: HyperbookArchive, * audio?: HyperbookAudio, * bookmarks?: HyperbookBookmarks, + * emoji?: HyperbookEmoji, * download?: HyperbookDownload, * excalidraw?: HyperbookExcalidraw, * geogebra?: HyperbookGeogebra, diff --git a/packages/markdown/assets/store.js b/packages/markdown/assets/store.js index 5a761898a..02c4cd207 100644 --- a/packages/markdown/assets/store.js +++ b/packages/markdown/assets/store.js @@ -48,6 +48,22 @@ hyperbook.store = (function () { typst: `id,code`, openscad: `id,code,params`, }); + // Bookmark labels became a list of parts, so they can carry emojis without + // storing markup. The label is content, not a key, so it is not indexed. + db.version(6) + .stores({ + bookmarks: `path`, + }) + .upgrade((tx) => + tx + .table("bookmarks") + .toCollection() + .modify((bookmark) => { + if (typeof bookmark.label === "string") { + bookmark.label = [{ text: bookmark.label }]; + } + }), + ); /** @returns {Promise} */ const init = async () => { diff --git a/packages/markdown/assets/ui.js b/packages/markdown/assets/ui.js index 2ef8df924..79db66f27 100644 --- a/packages/markdown/assets/ui.js +++ b/packages/markdown/assets/ui.js @@ -48,22 +48,56 @@ hyperbook.ui = (function () { overlay.style.display = "flex"; } + /** + * Read the label of a heading as bookmark parts. Reading it from the + * rendered heading instead of the markup keeps whatever the page shows, + * so an emoji that is drawn as an image stays an image in the bookmark + * list. Emojis are stored by id, never by URL, so bookmarks survive a + * change of the basePath. + * @param {Element} [buttonEl] - The bookmark button of the heading. + * @returns {HyperbookBookmarkLabel} The label parts. + */ + function readBookmarkLabel(buttonEl) { + const labelEl = buttonEl?.parentElement?.querySelector("a.heading > span"); + const label = []; + for (let node of labelEl ? labelEl.childNodes : []) { + if (node.nodeName === "IMG" && node.dataset.emoji) { + label.push({ text: node.alt || "", emoji: node.dataset.emoji }); + } else if (node.textContent) { + label.push({ text: node.textContent }); + } + } + if (label.length === 0) { + const fallback = buttonEl?.getAttribute("data-label"); + if (fallback) { + label.push({ text: fallback }); + } + } + return label; + } + /** * Toggle a bookmark on/off. * @param {string} key - The bookmark path key. - * @param {string} label - The bookmark label. */ - function toggleBookmark(key, label) { + function toggleBookmark(key) { const el = document.querySelectorAll(`.bookmark[data-key="${key}"]`); hyperbook.store.db.bookmarks.get(key).then((bookmark) => { if (!bookmark) { + const label = readBookmarkLabel(el[0]); hyperbook.store.db.bookmarks.add({ path: key, label }).then(() => { - el.forEach((e) => e.classList.add("active")); + el.forEach((e) => { + e.classList.add("active"); + e.setAttribute("aria-pressed", "true"); + }); hyperbook.bookmarks.update(); }); } else { hyperbook.store.db.bookmarks.delete(key).then(() => { - el.forEach((e) => e.classList.remove("active")); + el.forEach((e) => { + e.classList.remove("active"); + e.setAttribute("aria-pressed", "false"); + }); hyperbook.bookmarks.update(); }); } diff --git a/packages/markdown/src/rehypeEmoji.ts b/packages/markdown/src/rehypeEmoji.ts index ea9b4d98e..cf018f8ac 100644 --- a/packages/markdown/src/rehypeEmoji.ts +++ b/packages/markdown/src/rehypeEmoji.ts @@ -72,6 +72,7 @@ const makeImage = ( class: "emoji", src: ctx.makeUrl(["emoji", `${id}.svg`], "assets"), alt: emoji, + "data-emoji": id, draggable: "false", loading: "lazy", decoding: "async", diff --git a/packages/markdown/src/rehypeHtmlStructure.ts b/packages/markdown/src/rehypeHtmlStructure.ts index 2eb55b6a5..5254a7f9b 100644 --- a/packages/markdown/src/rehypeHtmlStructure.ts +++ b/packages/markdown/src/rehypeHtmlStructure.ts @@ -357,6 +357,25 @@ document.documentElement.classList.remove('no-js');`, }, ], }, + ...(config.elements?.emoji?.style === "twemoji" + ? [ + { + type: "element" as const, + tagName: "script", + properties: {}, + children: [ + { + type: "raw" as const, + // Lets the client rebuild an emoji image from its id, + // for example when it renders a stored bookmark. + value: ` +window.hyperbook = window.hyperbook || {}; +window.hyperbook.emoji = { style: "twemoji", base: "${makeUrl(["emoji"], "assets", undefined, { versioned: false })}" };`, + }, + ], + } as ElementContent, + ] + : []), { type: "element", tagName: "script", diff --git a/packages/markdown/src/remarkHeadings.ts b/packages/markdown/src/remarkHeadings.ts index 9bcfc3750..67480fdfa 100644 --- a/packages/markdown/src/remarkHeadings.ts +++ b/packages/markdown/src/remarkHeadings.ts @@ -97,10 +97,14 @@ export default ({ type: "element", tagName: "button", properties: { + type: "button", class: "bookmark", - onclick: `hyperbook.ui.toggleBookmark("${key}", "${label}")`, title: i18n.get("toggle-bookmark"), + "aria-pressed": "false", "data-key": key, + // Plain text fallback. The label is normally read from the + // rendered heading, so it keeps whatever the page shows. + "data-label": label, }, children: [ { diff --git a/packages/markdown/tests/__snapshots__/process.test.ts.snap b/packages/markdown/tests/__snapshots__/process.test.ts.snap index c4aa9b115..58df0dba8 100644 --- a/packages/markdown/tests/__snapshots__/process.test.ts.snap +++ b/packages/markdown/tests/__snapshots__/process.test.ts.snap @@ -380,7 +380,7 @@ HYPERBOOK_ASSETS = "/assets/" -

Heading 1:1

+

Heading 1:1

© Copyright 2026
" `; diff --git a/packages/markdown/tests/rehypeEmoji.test.ts b/packages/markdown/tests/rehypeEmoji.test.ts index 0e9d5414f..021d85f17 100644 --- a/packages/markdown/tests/rehypeEmoji.test.ts +++ b/packages/markdown/tests/rehypeEmoji.test.ts @@ -55,6 +55,12 @@ describe("rehypeEmoji", () => { expect(html).toContain(`class="emoji"`); }); + it("should tag the image with the emoji id", () => { + // The id lets the client rebuild the image, for example for a bookmark. + const { html } = toHtml(":penguin:", twemojiCtx); + expect(html).toContain(`data-emoji="1f427"`); + }); + it("should replace an emoji that was typed directly", () => { const { html } = toHtml("Hello 🐧 world", twemojiCtx); expect(html).toContain(`src="/assets/emoji/1f427.svg"`); diff --git a/packages/markdown/tests/remarkHeadings.test.ts b/packages/markdown/tests/remarkHeadings.test.ts index 032face5e..62d4a3a46 100644 --- a/packages/markdown/tests/remarkHeadings.test.ts +++ b/packages/markdown/tests/remarkHeadings.test.ts @@ -24,13 +24,35 @@ describe("remarkHeadingId", () => { `); expect(String(file)).toMatchInlineSnapshot(` - "

head

-

cus head1

-

cus head2

-

cus head3

" + "

head

+

cus head1

+

cus head2

+

cus head3

" `); }); + it("should not put the label into executable markup", async () => { + // A quote or a backslash in a heading used to end up inside a JavaScript + // string in an onclick attribute, which made the bookmark button throw. + let file = await unified() + .use(remarkParse) + .use(remarkHeadings(ctx)) + .use(remarkGfm) + .use(remarkRehype) + .use(rehypeStringify, { + allowDangerousCharacters: true, + allowDangerousHtml: true, + }) + .process(`# He said "hi" and \\ backslash`); + + const html = String(file); + expect(html).not.toContain("onclick"); + expect(html).toContain( + `data-label="He said "hi" and \\ backslash"`, + ); + expect(html).toContain(`aria-pressed="false"`); + }); + it("should parse well which contains inline syntax", async () => { let file = await unified() .data("settings", {}) @@ -48,9 +70,9 @@ describe("remarkHeadingId", () => { `); expect(String(file)).toMatchInlineSnapshot(` - "

cus head1

-

cus head2

-

cus head2

" + "

cus head1

+

cus head2

+

cus head2

" `); }); @@ -67,7 +89,7 @@ describe("remarkHeadingId", () => { `); expect(String(file)).toMatchInlineSnapshot( - `"

Heading 1:1

"`, + `"

Heading 1:1

"`, ); }); }); From 5cbb4b9e5df2b163fb9b4d9db0baf2f19b3ad9db Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 10:07:45 +0000 Subject: [PATCH 4/7] Draw the bookmark indicator from the stylesheet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The indicator was the 🔖 character in two places: as the content of the button on a heading, and as the content of a ::before in the bookmark list. Both were drawn with the emoji font of the reader, so the control looked different on every platform. The one in the button also changed into a Twemoji image once a hyperbook set the emoji style, which made a piece of interface follow a content setting. Both are now a mask over the feather bookmark icon, so the control looks the same everywhere and stays a control. It takes the color of its surroundings, which keeps it readable in light and dark mode, and a bookmarked heading shows a filled icon instead of only a less transparent one. The button carries an aria-label now, since it no longer has content to name it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012kkjvqifo7fQ5W6woRWiSW --- .changeset/olive-rules-repeat.md | 12 ++++++++- packages/markdown/assets/content.css | 25 +++++++++++++++++-- .../assets/directive-bookmarks/style.css | 10 ++++++-- packages/markdown/src/remarkHeadings.ts | 11 ++++---- .../tests/__snapshots__/process.test.ts.snap | 2 +- .../markdown/tests/remarkHeadings.test.ts | 19 ++++++++------ 6 files changed, 59 insertions(+), 20 deletions(-) diff --git a/.changeset/olive-rules-repeat.md b/.changeset/olive-rules-repeat.md index 9d02aae4e..f196bd235 100644 --- a/.changeset/olive-rules-repeat.md +++ b/.changeset/olive-rules-repeat.md @@ -23,11 +23,21 @@ This also fixes bookmarking a heading that contains a quote or a backslash. Those characters ended up inside a JavaScript string in the `onclick` attribute and made the button throw a syntax error. +The bookmark indicator is no longer the 🔖 emoji. Both the button on a heading +and the marker in the bookmark list are drawn from the stylesheet with a mask, +so the control looks the same on every platform, takes the color of its +surroundings in light and dark mode, and does not change when the emoji style +changes. A bookmarked heading now shows a filled icon instead of only a less +transparent one. + Breaking: - `hyperbook.ui.toggleBookmark(key, label)` no longer takes a label: `hyperbook.ui.toggleBookmark(key)`. The label comes from the heading. - Bookmark buttons no longer carry an inline `onclick`. They are handled by a - delegated listener and carry `data-key`, `data-label` and `aria-pressed`. + delegated listener and carry `data-key`, `data-label`, `aria-label` and + `aria-pressed`. +- Bookmark buttons are empty. The icon comes from `.bookmark` in the + stylesheet, so a custom style that replaced the emoji has to be updated. - The store moves to version 6. Labels of existing bookmarks are migrated to the new shape, and a plain label is still rendered, so nothing is lost. diff --git a/packages/markdown/assets/content.css b/packages/markdown/assets/content.css index 3b42d27a3..a6e30db7e 100644 --- a/packages/markdown/assets/content.css +++ b/packages/markdown/assets/content.css @@ -331,12 +331,32 @@ img.emoji { align-items: center; } +/* The bookmark icon is drawn from the stylesheet, so it looks the same on + every platform and takes the color of its heading. */ +:root { + --bookmark-icon: url('data:image/svg+xml,'); + --bookmark-icon-active: url('data:image/svg+xml,'); +} + .hyperbook-markdown .bookmark { margin-left: 10px; - background: none; border: none; - opacity: 0.5; + padding: 0; + /* A button does not inherit the color of its heading on its own, and the + icon is painted with currentColor. */ + color: inherit; + cursor: pointer; + display: inline-block; + vertical-align: middle; + width: 1em; + height: 1em; font-size: 1rem; + opacity: 0.5; + background-color: currentColor; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: var(--bookmark-icon); } .hyperbook-markdown .bookmark:hover { @@ -345,6 +365,7 @@ img.emoji { .hyperbook-markdown .bookmark.active { opacity: 1; + mask-image: var(--bookmark-icon-active); } .hyperbook-markdown ul.bookmarks { diff --git a/packages/markdown/assets/directive-bookmarks/style.css b/packages/markdown/assets/directive-bookmarks/style.css index 547a6cc92..6acd539b4 100644 --- a/packages/markdown/assets/directive-bookmarks/style.css +++ b/packages/markdown/assets/directive-bookmarks/style.css @@ -34,12 +34,18 @@ ul.directive-bookmarks li:last-of-type { } ul.directive-bookmarks a::before { - content: "🔖"; + content: ""; position: absolute; - font-size: 0.75rem; + width: 0.75rem; + height: 0.75rem; left: 12px; top: 50%; transform: translateY(-50%); + background-color: currentColor; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: var(--bookmark-icon-active); } ul.directive-bookmarks li { diff --git a/packages/markdown/src/remarkHeadings.ts b/packages/markdown/src/remarkHeadings.ts index 67480fdfa..e9a800c67 100644 --- a/packages/markdown/src/remarkHeadings.ts +++ b/packages/markdown/src/remarkHeadings.ts @@ -100,18 +100,17 @@ export default ({ type: "button", class: "bookmark", title: i18n.get("toggle-bookmark"), + "aria-label": i18n.get("toggle-bookmark"), "aria-pressed": "false", "data-key": key, // Plain text fallback. The label is normally read from the // rendered heading, so it keeps whatever the page shows. "data-label": label, }, - children: [ - { - type: "text", - value: "🔖", - }, - ], + // The icon comes from the stylesheet, so it does not depend on + // the emoji font of the reader and does not change with the + // emoji style of the hyperbook. + children: [], }); } }); diff --git a/packages/markdown/tests/__snapshots__/process.test.ts.snap b/packages/markdown/tests/__snapshots__/process.test.ts.snap index 58df0dba8..d811d1025 100644 --- a/packages/markdown/tests/__snapshots__/process.test.ts.snap +++ b/packages/markdown/tests/__snapshots__/process.test.ts.snap @@ -380,7 +380,7 @@ HYPERBOOK_ASSETS = "/assets/" -

Heading 1:1

+

Heading 1:1

© Copyright 2026
" `; diff --git a/packages/markdown/tests/remarkHeadings.test.ts b/packages/markdown/tests/remarkHeadings.test.ts index 62d4a3a46..cfdb77089 100644 --- a/packages/markdown/tests/remarkHeadings.test.ts +++ b/packages/markdown/tests/remarkHeadings.test.ts @@ -24,10 +24,10 @@ describe("remarkHeadingId", () => { `); expect(String(file)).toMatchInlineSnapshot(` - "

head

-

cus head1

-

cus head2

-

cus head3

" + "

head

+

cus head1

+

cus head2

+

cus head3

" `); }); @@ -47,6 +47,9 @@ describe("remarkHeadingId", () => { const html = String(file); expect(html).not.toContain("onclick"); + // The icon is drawn by the stylesheet, not by the emoji font. + expect(html).toContain(` -

cus head2

-

cus head2

" + "

cus head1

+

cus head2

+

cus head2

" `); }); @@ -89,7 +92,7 @@ describe("remarkHeadingId", () => { `); expect(String(file)).toMatchInlineSnapshot( - `"

Heading 1:1

"`, + `"

Heading 1:1

"`, ); }); }); From 77d210056b2494ae5c1e9a70221aa631fe51f5ce Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 10:12:30 +0000 Subject: [PATCH 5/7] Color the bookmark icon like the heading it belongs to The icon inherited its color, which picked up the color of the h1 rather than the color the heading appears in: the heading text is a link, and the button is its sibling, so inheriting missed the brand color the reader actually sees. Take the same color the heading link takes, which also keeps the icon in step with the brand color of the hyperbook and with the dark mode variant of it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012kkjvqifo7fQ5W6woRWiSW --- .changeset/olive-rules-repeat.md | 6 +++--- packages/markdown/assets/content.css | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.changeset/olive-rules-repeat.md b/.changeset/olive-rules-repeat.md index f196bd235..d9865cbb2 100644 --- a/.changeset/olive-rules-repeat.md +++ b/.changeset/olive-rules-repeat.md @@ -25,9 +25,9 @@ attribute and made the button throw a syntax error. The bookmark indicator is no longer the 🔖 emoji. Both the button on a heading and the marker in the bookmark list are drawn from the stylesheet with a mask, -so the control looks the same on every platform, takes the color of its -surroundings in light and dark mode, and does not change when the emoji style -changes. A bookmarked heading now shows a filled icon instead of only a less +so the control looks the same on every platform, takes the color of the +heading it belongs to in light and dark mode, and does not change when the +emoji style changes. A bookmarked heading now shows a filled icon instead of only a less transparent one. Breaking: diff --git a/packages/markdown/assets/content.css b/packages/markdown/assets/content.css index a6e30db7e..cda02636a 100644 --- a/packages/markdown/assets/content.css +++ b/packages/markdown/assets/content.css @@ -342,9 +342,10 @@ img.emoji { margin-left: 10px; border: none; padding: 0; - /* A button does not inherit the color of its heading on its own, and the - icon is painted with currentColor. */ - color: inherit; + /* The heading text is a link, and the button is its sibling, so inheriting + would pick up the color of the h1 rather than the color the heading + appears in. Take the same color the heading link takes. */ + color: var(--color-brand); cursor: pointer; display: inline-block; vertical-align: middle; From 8022ef69fdad9cf3753a1172188380122238c12d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 10:30:30 +0000 Subject: [PATCH 6/7] Render the website with twemoji Dogfoods the emoji style on the documentation, so the emojis in the docs, the language flags of the library and the icons of the custom links look the same for every reader. Also fixes a crash the website build turned up: an element that carries no children at all, which a directive can produce for a void element like an img, made the emoji walk throw. It is skipped now. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012kkjvqifo7fQ5W6woRWiSW --- packages/markdown/src/rehypeEmoji.ts | 6 ++++++ packages/markdown/tests/rehypeEmoji.test.ts | 15 +++++++++++++++ website/de/hyperbook.json | 3 +++ website/en/hyperbook.json | 3 +++ 4 files changed, 27 insertions(+) diff --git a/packages/markdown/src/rehypeEmoji.ts b/packages/markdown/src/rehypeEmoji.ts index cf018f8ac..57f544b74 100644 --- a/packages/markdown/src/rehypeEmoji.ts +++ b/packages/markdown/src/rehypeEmoji.ts @@ -128,6 +128,12 @@ export const rehypeEmoji: Plugin<[HyperbookContext], Root> = }; const walk = (parent: Parent) => { + // Not every element carries children. A directive that builds a void + // element like an img can leave them out. + if (!Array.isArray(parent.children)) { + return; + } + const children: ElementContent[] = []; let replaced = false; diff --git a/packages/markdown/tests/rehypeEmoji.test.ts b/packages/markdown/tests/rehypeEmoji.test.ts index 021d85f17..e34a65d65 100644 --- a/packages/markdown/tests/rehypeEmoji.test.ts +++ b/packages/markdown/tests/rehypeEmoji.test.ts @@ -98,6 +98,21 @@ describe("rehypeEmoji", () => { expect(file.data.emojis?.sort()).toEqual(["1f34e", "1f427"]); }); + it("should walk past an element without children", () => { + // Directives build hast by hand and may leave children out entirely. + const tree: any = { + type: "root", + children: [ + { type: "element", tagName: "img", properties: { src: "/a.png" } }, + { type: "text", value: "🐧" }, + ], + }; + const file = new VFile(""); + expect(() => (rehypeEmoji as any)(twemojiCtx)(tree, file)).not.toThrow(); + expect(tree.children[1].tagName).toBe("img"); + expect(tree.children[1].properties["data-emoji"]).toBe("1f427"); + }); + it("should keep emojis as text when the style is native", () => { const { html, file } = toHtml(":penguin:", baseCtx); expect(html).not.toContain(" Date: Fri, 7 Aug 2026 10:37:56 +0000 Subject: [PATCH 7/7] Add a changelog entry for the emoji and bookmark changes Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012kkjvqifo7fQ5W6woRWiSW --- website/en/book/changelog.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/website/en/book/changelog.md b/website/en/book/changelog.md index a392ab897..e1dcd2c5e 100644 --- a/website/en/book/changelog.md +++ b/website/en/book/changelog.md @@ -38,6 +38,34 @@ If you need a new feature, open an [issue](https://github.com/openpatch/hyperboo :::: --> +## v0.101.0 + +::::tabs + +:::tab{title="New :rocket:" id="new"} + +**emoji**: Emojis can be rendered as images, so they look the same on every platform instead of being drawn with the emoji font of the reader's operating system. Set `elements.emoji.style` to `twemoji` in your `hyperbook.json`. Only the emojis your book uses end up in your build. + +::: + +:::tab{title="Improved :+1:" id="improved"} + +**bookmarks**: The bookmark icon is drawn by the stylesheet instead of being an emoji. It looks the same everywhere, takes the color of its heading, and a saved bookmark shows a filled icon. + +**bookmarks**: An entry in the bookmark list shows the same emojis as the heading it points at. If you call `hyperbook.ui.toggleBookmark` yourself, it no longer takes a label. + +::: + +:::tab{title="Fixed :bug:" id="fixed"} + +**markdown**: `mailto:` and `tel:` links in your content kept the base path of the book in front of them, so `[Write us](mailto:hi@example.org)` became `/mailto:hi@example.org`. + +**bookmarks**: A heading that contains a quote or a backslash can be bookmarked again. + +::: + +:::: + ## v0.100.5 ::::tabs