Skip to content
This repository was archived by the owner on Mar 1, 2025. It is now read-only.

Commit 3cebf19

Browse files
authored
feat: implement emoji generation (#4)
* feat: implement emoji and variation generation commands with error handling * feat: enhance emoji data handling with version extraction and improved parsing logic * feat: add shortcode generator * feat: rename groups to metadata in MojiAdapter and update related commands * feat: enhance emoji metadata handling with version extraction and improved structure * feat: add unicode version to adapter context * chore: lint * refactor: remove errors and merge into base adapter * feat: add unicodeNames function to fetch and parse Unicode names for emojis * refactor: migrate to a single generate command * feat: add consola for improved logging throughout the application * feat: implement shortcodes functionality and update emoji data structure * feat: restructure emoji handling to include emojiData and flatten emoji groups * chore: dump * feat: update emoji version handling to correctly map unicode versions and simplify sorting logic * chore: move lockfile out of utils * chore: update test path * feat: add README files for Unicode Emoji and Character Database draft versions * refactor: rename extractVersion to extractVersionFromReadme for clarity * chore: lint * feat: add vitest-fetch-mock for enhanced testing and setup configuration * chore: lint
1 parent e538913 commit 3cebf19

26 files changed

+1624
-319
lines changed

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
},
3535
"dependencies": {
3636
"cac": "^6.7.14",
37+
"consola": "^3.4.0",
3738
"farver": "^0.4.0",
3839
"fs-extra": "^11.3.0",
3940
"semver": "^7.7.1",
@@ -53,6 +54,7 @@
5354
"tsx": "^4.19.2",
5455
"typescript": "^5.7.3",
5556
"vitest": "^3.0.5",
57+
"vitest-fetch-mock": "^0.4.3",
5658
"vitest-testdirs": "^2.1.1"
5759
},
5860
"pnpm": {

pnpm-lock.yaml

+16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/adapter/base.ts

+133-15
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,40 @@
1-
import type { EmojiGroup } from "../types";
2-
import { defineMojiAdapter } from "../adapter";
3-
import { slugify } from "../utils";
1+
import type { Emoji, EmojiGroup, EmojiMetadata, EmojiShortcode, ShortcodeProvider } from "../types";
2+
import consola from "consola";
3+
import { red, yellow } from "farver/fast";
4+
import { defineMojiAdapter, MojisNotImplemented } from "../adapter";
5+
import { extractEmojiVersion, extractUnicodeVersion, slugify } from "../utils";
46
import { fetchCache } from "../utils/cache";
57

68
function notImplemented(adapterFn: string) {
79
return async () => {
8-
throw new Error(`the adapter function ${adapterFn} is not implemented`);
10+
throw new MojisNotImplemented(`the adapter function ${red(adapterFn)} is not implemented`);
911
};
1012
}
1113

1214
export default defineMojiAdapter({
1315
name: "base",
1416
description: "base adapter",
1517
range: "*",
16-
groups: async ({ version, force }) => {
17-
if (version === "1.0" || version === "2.0" || version === "3.0") {
18-
console.warn(`version ${version} does not have group data`);
19-
return [];
18+
metadata: async (ctx) => {
19+
if (ctx.emojiVersion === "1.0" || ctx.emojiVersion === "2.0" || ctx.emojiVersion === "3.0") {
20+
consola.warn(`skipping metadata for emoji version ${yellow(ctx.emojiVersion)}, as it's not supported.`);
21+
return {
22+
groups: [],
23+
emojiMetadata: {},
24+
};
2025
}
2126

22-
const groups = await fetchCache(`https://unicode.org/Public/emoji/${version}/emoji-test.txt`, {
23-
cacheKey: `v${version}/metadata.json`,
27+
return fetchCache(`https://unicode.org/Public/emoji/${ctx.emojiVersion}/emoji-test.txt`, {
28+
cacheKey: `v${ctx.emojiVersion}/metadata.json`,
2429
parser(data) {
2530
const lines = data.split("\n");
2631
let currentGroup: EmojiGroup | undefined;
2732

2833
const groups: EmojiGroup[] = [];
2934

35+
// [group-subgroup][hexcode] = metadata
36+
const emojiMetadata: Record<string, Record<string, EmojiMetadata>> = {};
37+
3038
for (const line of lines) {
3139
if (line.trim() === "") {
3240
continue;
@@ -44,23 +52,133 @@ export default defineMojiAdapter({
4452
currentGroup = group;
4553

4654
groups.push(group);
55+
56+
continue;
4757
} else if (line.startsWith("# subgroup:")) {
4858
const subgroupName = line.slice(11).trim();
4959

5060
if (currentGroup == null) {
5161
throw new Error(`subgroup ${subgroupName} without group`);
5262
}
5363

54-
currentGroup.subgroups.push(subgroupName);
64+
currentGroup.subgroups.push(slugify(subgroupName));
65+
66+
continue;
67+
} else if (line.startsWith("#")) {
68+
continue;
5569
}
70+
71+
const [baseHexcode, trailingLine] = line.split(";");
72+
73+
if (baseHexcode == null || trailingLine == null) {
74+
throw new Error(`invalid line: ${line}`);
75+
}
76+
77+
const [baseQualifier, comment] = trailingLine.split("#");
78+
79+
if (baseQualifier == null || comment == null) {
80+
throw new Error(`invalid line: ${line}`);
81+
}
82+
83+
const hexcode = baseHexcode.trim().replace(/\s+/g, "-");
84+
const qualifier = baseQualifier.trim();
85+
86+
const emojiVersion = extractEmojiVersion(comment.trim());
87+
const [emoji, trimmedComment] = comment.trim().split(` E${emojiVersion} `);
88+
89+
const groupName = currentGroup?.slug ?? "unknown";
90+
const subgroupName = currentGroup?.subgroups[currentGroup.subgroups.length - 1] ?? "unknown";
91+
92+
const metadataGroup = `${groupName}-${subgroupName}`;
93+
94+
if (emojiMetadata[metadataGroup] == null) {
95+
emojiMetadata[metadataGroup] = {};
96+
}
97+
98+
emojiMetadata[metadataGroup][hexcode] = {
99+
group: groupName,
100+
subgroup: subgroupName,
101+
qualifier,
102+
emojiVersion: emojiVersion || null,
103+
unicodeVersion: extractUnicodeVersion(emojiVersion, ctx.unicodeVersion),
104+
description: trimmedComment || "",
105+
emoji: emoji || null,
106+
hexcodes: hexcode.split("-"),
107+
};
56108
}
57109

58-
return groups;
110+
return {
111+
groups,
112+
emojiMetadata,
113+
};
59114
},
60-
bypassCache: force,
115+
bypassCache: ctx.force,
61116
});
62-
63-
return groups;
64117
},
65118
sequences: notImplemented("sequences"),
119+
emojis: notImplemented("emojis"),
120+
variations: notImplemented("variations"),
121+
unicodeNames: async (ctx) => {
122+
return fetchCache(`https://unicode.org/Public/${ctx.emojiVersion === "13.1" ? "13.0" : ctx.emojiVersion}.0/ucd/UnicodeData.txt`, {
123+
cacheKey: `v${ctx.emojiVersion}/unicode-names.json`,
124+
parser(data) {
125+
const lines = data.split("\n");
126+
const unicodeNames: Record<string, string> = {};
127+
128+
for (const line of lines) {
129+
if (line.trim() === "" || line.startsWith("#")) {
130+
continue;
131+
}
132+
133+
const [hex, name] = line.split(";").map((col) => col.trim());
134+
135+
if (hex == null || name == null) {
136+
throw new Error(`invalid line: ${line}`);
137+
}
138+
139+
unicodeNames[hex] = name;
140+
}
141+
142+
return unicodeNames;
143+
},
144+
bypassCache: ctx.force,
145+
});
146+
},
147+
async shortcodes(ctx) {
148+
const providers = ctx.providers;
149+
150+
if (providers.length === 0) {
151+
throw new Error("no shortcode providers specified");
152+
}
153+
154+
const shortcodes: Partial<Record<ShortcodeProvider, EmojiShortcode[]>> = {};
155+
156+
if (this.emojis == null) {
157+
throw new MojisNotImplemented("emojis");
158+
}
159+
160+
const { emojis } = await this.emojis(ctx);
161+
162+
const flattenedEmojis = Object.values(emojis).reduce((acc, subgroup) => {
163+
for (const hexcodes of Object.values(subgroup)) {
164+
for (const [hexcode, emoji] of Object.entries(hexcodes)) {
165+
acc[hexcode] = emoji;
166+
}
167+
}
168+
169+
return acc;
170+
}, {} as Record<string, Emoji>);
171+
172+
if (providers.includes("github")) {
173+
const githubShortcodesFn = await import("../shortcode/github").then((m) => m.generateGitHubShortcodes);
174+
175+
shortcodes.github = await githubShortcodesFn({
176+
emojis: flattenedEmojis,
177+
force: ctx.force,
178+
version: ctx.emojiVersion,
179+
});
180+
}
181+
182+
return shortcodes;
183+
},
66184
});

src/adapter/index.ts

+29-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { EmojiGroup, EmojiSequence, EmojiVariation } from "../types";
1+
import type { Emoji, EmojiData, EmojiGroup, EmojiMetadata, EmojiSequence, EmojiShortcode, EmojiVariation, ShortcodeProvider } from "../types";
22
import semver from "semver";
33

44
export interface MojiAdapter {
@@ -22,11 +22,6 @@ export interface MojiAdapter {
2222
*/
2323
extend?: string;
2424

25-
/**
26-
* A function to generate the emoji groups for the specified version.
27-
*/
28-
groups?: GroupFn;
29-
3025
/**
3126
* A function to generate the emoji sequences for the specified version
3227
*/
@@ -41,17 +36,35 @@ export interface MojiAdapter {
4136
* A function to generate emoji variations for the specified version.
4237
*/
4338
variations?: EmojiVariationFn;
39+
40+
shortcodes?: ShortcodeFn;
41+
42+
metadata?: MetadataFn;
43+
44+
unicodeNames?: UnicodeNamesFn;
4445
}
4546

4647
export interface BaseAdapterContext {
47-
version: string;
48+
emojiVersion: string;
49+
unicodeVersion: string;
4850
force: boolean;
4951
}
5052

51-
export type GroupFn = (ctx: BaseAdapterContext) => Promise<EmojiGroup[]>;
53+
export type UnicodeNamesFn = (ctx: BaseAdapterContext) => Promise<Record<string, string>>;
5254
export type SequenceFn = (ctx: BaseAdapterContext) => Promise<{ zwj: EmojiSequence[]; sequences: EmojiSequence[] }>;
53-
export type EmojiFn = (ctx: BaseAdapterContext) => Promise<any>;
55+
export type EmojiFn = (ctx: BaseAdapterContext) => Promise<{
56+
emojiData: Record<string, EmojiData>;
57+
// group: subgroup: hexcode: emoji
58+
emojis: Record<string, Record<string, Record<string, Emoji>>>;
59+
}>;
5460
export type EmojiVariationFn = (ctx: BaseAdapterContext) => Promise<EmojiVariation[]>;
61+
export type ShortcodeFn = (ctx: BaseAdapterContext & {
62+
providers: ShortcodeProvider[];
63+
}) => Promise<Partial<Record<ShortcodeProvider, EmojiShortcode[]>>>;
64+
export type MetadataFn = (ctx: BaseAdapterContext) => Promise<{
65+
groups: EmojiGroup[];
66+
emojiMetadata: Record<string, Record<string, EmojiMetadata>>;
67+
}>;
5568

5669
export const ADAPTERS = new Map<string, MojiAdapter>();
5770

@@ -82,3 +95,10 @@ export function defineMojiAdapter(adapter: MojiAdapter): MojiAdapter {
8295

8396
return adapter;
8497
}
98+
99+
export class MojisNotImplemented extends Error {
100+
constructor(message: string) {
101+
super(message);
102+
this.name = "MojisNotImplemented";
103+
}
104+
}

0 commit comments

Comments
 (0)