Skip to content

Commit

Permalink
feat(core): add new methods to Moji
Browse files Browse the repository at this point in the history
  • Loading branch information
ifiokjr committed Jan 16, 2021
1 parent 7290151 commit b549183
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 155 deletions.
12 changes: 12 additions & 0 deletions .changeset/loud-ties-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@svgmoji/core': minor
---

While this library is in `0.x` breaking changes can occur during minor version upgrades. This release contains breaking changes.

- **BREAKING**: Rename `getUrl` method on `Moji` abstract class to `url`.
- **BREAKING**: Store `fallback` as a `FlatEmoji` rather than the provided string.
- Add `fallbackUrl` property to `Moji` abstract class.
- Add `find` method to `Moji` to allow searching for emoji by `unicode`, `hexcode` or `emoticon`.
- Add `search` method which allows fuzzy searching the emoji. The search algorithm is provided the library [`match-sorter`](https://github.com/kentcdodds/match-sorter) and may be adapted in future releases.
- Clean up dependencies.
9 changes: 5 additions & 4 deletions packages/svgmoji__core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,15 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/object.omit": "^3.0.0",
"emojibase": "^5.1.0",
"emojibase-data": "^6.1.0",
"emojibase-regex": "^5.1.0",
"idb-keyval": "^5.0.1",
"isomorphic-fetch": "^3.0.0",
"object.omit": "^3.0.0",
"match-sorter": "^6.1.0",
"type-fest": "^0.20.2"
},
"devDependencies": {
"emojibase-data": "^6.1.0"
},
"publishConfig": {
"access": "public"
},
Expand Down
143 changes: 117 additions & 26 deletions packages/svgmoji__core/src/base-moji.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { fromUnicodeToHexcode } from 'emojibase';
import { groups, subgroups } from 'emojibase-data/meta/groups.json';
import { generateEmoticonPermutations } from 'emojibase';
import { loadJson } from 'json.macro';
import { matchSorter, rankings } from 'match-sorter';

import type { SpriteCollectionType } from './constants';
import { SpriteCollection } from './constants';
import { isMinifiedEmojiList } from './core-utils';
import type { FlatEmoji } from './flatten-emoji-data';
import type { MinifiedEmoji } from './minify-emoji';
import { populateMinifiedEmoji } from './populate-minified-emoji';
import { EMOJI_REGEX, EMOTICON_REGEX } from './regexp';
import type { FlatEmoji, MinifiedEmoji } from './types';

const groups: Record<number, string> = loadJson('emojibase-data/meta/groups.json', 'groups');
const subgroups: Record<number, string> = loadJson('emojibase-data/meta/groups.json', 'subgroups');

interface MojiProps {
/**
Expand Down Expand Up @@ -41,60 +45,147 @@ export abstract class Moji {
/**
* All the available emoji.
*/
data: FlatEmoji[];
readonly data: FlatEmoji[];

/**
* Only data without tones included.
*/
readonly tonelessData: FlatEmoji[];

/**
* The type of sprite to load.
*/
type: SpriteCollectionType;

/**
* The fallback svg to use when none can be found.
* The fallback emoji to use when none can be found.
*/
fallback: string;
fallback: FlatEmoji;

get cdn(): string {
return `https://cdn.jsdelivr.net/npm/@svgmoji/${this.name}@${this.version}/`;
}

get fallbackUrl(): string {
return `${this.cdn}/svg/${this.fallback.hexcode}.svg`;
}

/**
* @param data - data which is used to lookup the groups and subgroups for the emoji instance
* @param fallback - the default hexcode to use when none can be found.
*/
constructor({ data, type, fallback = '1F44D' }: MojiProps) {
this.data = isMinifiedEmojiList(data) ? populateMinifiedEmoji(data) : data;
this.type = type;
this.fallback = fallback;
}
this.data = isMinifiedEmojiList(data) ? populateMinifiedEmoji(data) : data;
this.tonelessData = this.data.filter((emoji) => !emoji.tone);

get cdn(): string {
return `https://cdn.jsdelivr.net/npm/@svgmoji/${this.name}@${this.version}/`;
const fallbackEmoji = this.find(fallback);

if (!fallbackEmoji) {
throw new Error(`❌ No emoji exists for the provided fallback value: '${fallback}'`);
}

this.fallback = fallbackEmoji;
}

/**
* Get the CDN url from the provided emoji.
* Get the CDN url from the provided emoji hexcode, emoticon or unicode string.
*/
getUrl(emoji: string) {
return this.getUrlFromHexcode(fromUnicodeToHexcode(emoji));
}
url(code: string, options: { fallback: false }): string | undefined;
url(code: string, options?: { fallback?: true }): string;
url(code: string, options: { fallback?: boolean } = {}): string | undefined {
const { fallback = true } = options;
const emoji = this.find(code);
const fallbackUrl = fallback ? this.fallbackUrl : undefined;

if (!emoji) {
return fallbackUrl;
}

getUrlFromHexcode(hexcode: string): string {
if (this.type === SpriteCollection.All) {
return `${this.cdn}/sprites/all.svg#${hexcode}`;
return `${this.cdn}/sprites/all.svg#${emoji.hexcode}`;
}

if (this.type === SpriteCollection.Individual) {
return `${this.cdn}/svg/${hexcode}.svg`;
return `${this.cdn}/svg/${emoji.hexcode}.svg`;
}

const emoji = this.data.find((emoji) => emoji.hexcode === hexcode);

if (this.type === SpriteCollection.Group && emoji?.group) {
const name = groups[`${emoji.group}` as keyof typeof groups];
return `${this.cdn}/sprites/group/${name}.svg#${hexcode}`;
const name = groups[emoji.group];
return name ? `${this.cdn}/sprites/group/${name}.svg#${emoji.hexcode}` : fallbackUrl;
}

if (this.type === SpriteCollection.Subgroup && emoji?.subgroup) {
const name = subgroups[`${emoji.subgroup}` as keyof typeof subgroups];
return `${this.cdn}/sprites/subgroup/${name}.svg#${hexcode}`;
const name = subgroups[emoji.subgroup];
return name ? `${this.cdn}/sprites/subgroup/${name}.svg#${emoji.hexcode}` : fallbackUrl;
}

return `${this.cdn}/svg/${this.fallback}.svg`;
return fallbackUrl;
}

/**
* Get an the emoji object of a value by it's hexcode, emoticon or unicode string.
*/
find(code: string): FlatEmoji | undefined {
if (EMOJI_REGEX.test(code)) {
return this.data.find((emoji) => emoji.emoji === code);
}

if (EMOTICON_REGEX.test(code)) {
return this.data.find(
(emoji) => !!emoji.emoticon && generateEmoticonPermutations(emoji.emoticon).includes(code),
);
}

return this.data.find((emoji) => emoji.hexcode === code);
}

/**
* Search for the nearest emoji using the `match-sorter` algorithm.
*/
search(query: string, options: BaseMojiProps = {}): FlatEmoji[] {
const { excludeTone } = { ...DEFAULT_OPTIONS, ...options };
const data = excludeTone ? this.tonelessData : this.data;

return matchSorter(data, query, {
threshold: rankings.WORD_STARTS_WITH,
keys: [
{ threshold: rankings.STARTS_WITH, key: 'shortcodes' },
'tags',
'annotation',
(item) => (item.subgroup ? subgroups[item.subgroup]?.split('-').join(' ') ?? '' : ''),
(item) => (item.group ? groups[item.group]?.split('-').join(' ') ?? '' : ''),
],
});
}

/**
* Get skins from emoji
*/
getTones(emoji: FlatEmoji): FlatEmoji[] {
const skins: FlatEmoji[] = [];

for (const skin of emoji.skins ?? []) {
const skinEmoji = this.find(skin);

if (skinEmoji) {
skins.push();
}
}

return skins;
}
}

const DEFAULT_OPTIONS: Required<BaseMojiProps> = {
excludeTone: true,
};

interface BaseMojiProps {
/**
* When true only emoji without tone data will be used.
*
* @default true;
*/
excludeTone?: boolean;
}
4 changes: 2 additions & 2 deletions packages/svgmoji__core/src/core-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FlatEmoji } from './flatten-emoji-data';
import type { MinifiedEmoji } from './minify-emoji';
import type { FlatEmoji , MinifiedEmoji } from './types';


export function isMinifiedEmoji(value: unknown): value is MinifiedEmoji {
if (typeof value !== 'object' || value == null) {
Expand Down
3 changes: 2 additions & 1 deletion packages/svgmoji__core/src/fetch-emoji.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import type { Emoji, Locale, ShortcodePreset, ShortcodesDataset } from 'emojibas

import { fetchFromCDN, FetchFromCDNOptions } from './fetch-from-cdn';
import { fetchShortcodes } from './fetch-shortcodes';
import { FlatEmoji, flattenEmojiData } from './flatten-emoji-data';
import { flattenEmojiData } from './flatten-emoji-data';
import type { FlatEmoji } from './types';

export interface FetchEmojisOptions<Type extends Locale> extends FetchFromCDNOptions {
shortcodes?: Array<EmojiShortcodeLocale<Type> | ShortcodePreset>;
Expand Down
23 changes: 8 additions & 15 deletions packages/svgmoji__core/src/fetch-from-cdn.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'isomorphic-fetch';
import { get, set } from 'idb-keyval';

export interface FetchFromCDNOptions extends RequestInit {
/**
Expand All @@ -9,22 +9,15 @@ export interface FetchFromCDNOptions extends RequestInit {
version?: string;
}

async function get<Type>(key: string): Promise<Type | undefined> {
async function runInBrowser<Type, Callback extends (...args: any[]) => Promise<Type | undefined>>(
callback: Callback,
...args: Parameters<Callback>
): Promise<Type | undefined> {
if (typeof document === 'undefined') {
return;
}

const { get: idbGet } = await import('idb-keyval');
return idbGet(key);
}

async function set<Type>(key: string, value: Type): Promise<void> {
if (typeof document === 'undefined') {
return;
}

const { set: idbSet } = await import('idb-keyval');
await idbSet(key, value);
return callback(...args);
}

export async function fetchFromCDN<T>(path: string, options: FetchFromCDNOptions = {}): Promise<T> {
Expand All @@ -41,7 +34,7 @@ export async function fetchFromCDN<T>(path: string, options: FetchFromCDNOptions
}

const cacheKey = `svgmoji/${version}/${path}`;
const cachedData: T | undefined = await get(cacheKey);
const cachedData: T | undefined = await runInBrowser(get, cacheKey);

// Check the cache first
if (cachedData) {
Expand All @@ -62,7 +55,7 @@ export async function fetchFromCDN<T>(path: string, options: FetchFromCDNOptions
const data = await response.json();

try {
await set(cacheKey, data);
await runInBrowser(set, cacheKey, data);
} catch {
// Do nothing.
}
Expand Down
34 changes: 14 additions & 20 deletions packages/svgmoji__core/src/flatten-emoji-data.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import type { Emoji, ShortcodesDataset, SkinTone } from 'emojibase';
import type { Emoji as BaseEmoji, ShortcodesDataset, SkinTone } from 'emojibase';

import { joinShortcodesToEmoji as joinShortcodesToEmoji } from './join-shortcodes-to-emoji';
import type { FlatEmoji,SkinToneTuple } from './types';

export interface FlatEmoji extends Omit<Emoji, 'skins' | 'tone'> {
/**
* The hexcodes for the skins contained.
*/
skins?: string[];
/**
* Throws an error if the tone is undefined.
*/
function getTone(tone: SkinTone | SkinTone[] | undefined): SkinToneTuple {
if (!tone) {
throw new Error('A tone is required when using `getTone`');
}

/**
* The skin tone.
*/
tone?: SkinToneTuple;
return Array.isArray(tone) ? [tone[0] as SkinTone, tone[1]] : [tone];
}

function createFlatEmoji(
base: Omit<Emoji, 'skins' | 'tone'>,
skins: Emoji[] | undefined,
base: Omit<BaseEmoji, 'skins' | 'tone'>,
skins: BaseEmoji[] | undefined,
tone: SkinTone | SkinTone[] | undefined,
) {
const flatEmoji: FlatEmoji = { ...base };

if (tone) {
flatEmoji.tone = Array.isArray(tone) ? [tone[0] as SkinTone, tone[1]] : [tone];
flatEmoji.tone = getTone(tone);
}

if (skins) {
Expand All @@ -32,14 +32,8 @@ function createFlatEmoji(
return flatEmoji;
}

/**
* The skin tone which allows a second tone for complex emoji that support multiple tones for
* different characters.
*/
export type SkinToneTuple = [primary: SkinTone, secondary?: SkinTone];

export function flattenEmojiData(
data: Emoji[],
data: BaseEmoji[],
shortcodeDatasets: ShortcodesDataset[] = [],
): FlatEmoji[] {
const emojis: FlatEmoji[] = [];
Expand Down
2 changes: 2 additions & 0 deletions packages/svgmoji__core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ export * from './flatten-emoji-data';
export * from './join-shortcodes-to-emoji';
export * from './minify-emoji';
export * from './populate-minified-emoji';
export * from './regexp';
export * from './types';
export type { Emoticon } from 'emojibase';
export { fromUnicodeToHexcode, generateEmoticonPermutations, stripHexcode } from 'emojibase';

0 comments on commit b549183

Please sign in to comment.