diff --git a/package.json b/package.json index 9763d05..729d20f 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "start": "node dist/app.js", "build": "tsc -p tsconfig.json --pretty", + "test": "npm run build && node --test test/*.test.js", "dev": "tsc -p tsconfig.json --pretty --watch", "format": "prettier --check --ignore-path .gitignore .", "format:fix": "prettier --write --ignore-path .gitignore ." diff --git a/src/app.ts b/src/app.ts index 93baf9e..e0b6475 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,6 +5,7 @@ import { copyFluent } from "./extractors/fluent-generic.js" import { copyTwemojiTo } from "./extractors/twemoji.js" import { copyNotoTo } from "./extractors/noto.js" import { copyMutantTo } from "./extractors/mutant.js" +import { ensureVariationSelectorAliases } from "./variation-selectors.js" //#region Constants and Setup @@ -23,7 +24,9 @@ if (!fluentExists) { export const outDir = joinPath(cwd, "emoji") if (exists(outDir)) await fs.rm(outDir, { recursive: true }) -fs.mkdir(outDir) +await fs.mkdir(outDir) + +const generatedPackDirs: string[] = [] //#endregion Constants and Setup @@ -34,6 +37,7 @@ console.log("pack-twemoji: Generating pack twemoji...") const twemojiOutDir = joinPath(outDir, "twemoji") await copyTwemojiTo(twemojiOutDir) +generatedPackDirs.push(twemojiOutDir) console.timeEnd("pack-twemoji") @@ -59,6 +63,7 @@ for (const fluentType of fluentTypes) { `pack-${flavorId}: Generating twemoji placeholders for missing files...` ) await copyTwemojiTo(packOutDir) + generatedPackDirs.push(packOutDir) console.timeEnd(`pack-${flavorId}`) } @@ -75,6 +80,7 @@ await copyNotoTo(notoOutDir) console.log(`pack-noto: Generating twemoji placeholders for missing files...`) await copyTwemojiTo(notoOutDir) +generatedPackDirs.push(notoOutDir) console.timeEnd("pack-noto") @@ -90,7 +96,13 @@ await copyMutantTo(mutantOutDir) console.log(`pack-mutant: Generating twemoji placeholders for missing files...`) await copyTwemojiTo(mutantOutDir) +generatedPackDirs.push(mutantOutDir) console.timeEnd("pack-mutant") //#endregion Mutant Remix + +console.time("variation-selector-aliases") +console.log("Generating variation-selector aliases...") +await ensureVariationSelectorAliases(generatedPackDirs) +console.timeEnd("variation-selector-aliases") diff --git a/src/extractors/fluent-generic.ts b/src/extractors/fluent-generic.ts index cf2a76d..776105b 100644 --- a/src/extractors/fluent-generic.ts +++ b/src/extractors/fluent-generic.ts @@ -5,9 +5,9 @@ import { packsDir } from "../app.js" import { FLUENT_TONE_DIRS, FLUENT_TONE_DIR_TO_CODEPOINT, - VARIANT_SELECTOR_EMOJI, } from "../constants.js" import { MSFTMetadataFile } from "../types.js" +import { withAndWithoutVariationSelectors } from "../variation-selectors.js" // We embed the 3D PNGs in an SVG so that we can use the same URLs for every pack const embedPngInSvg = ( @@ -49,13 +49,24 @@ const copyWithSkinTones = async ( )[0] const inPath = joinPath(toneDir, fileName) - const outPath = joinPath(copyTo, codepointOfEmoji.join("-") + ".svg") + const outputFilenames = withAndWithoutVariationSelectors( + codepointOfEmoji.join("-") + ".svg" + ) if (fileName.endsWith(".png")) { const pngData = await readFile(inPath) - await writeFile(outPath, embedPngInSvg(pngData)) + const svg = embedPngInSvg(pngData) + await Promise.all( + outputFilenames.map((filename) => + writeFile(joinPath(copyTo, filename), svg) + ) + ) } else { - await copyFile(inPath, outPath) + await Promise.all( + outputFilenames.map((filename) => + copyFile(inPath, joinPath(copyTo, filename)) + ) + ) } } } @@ -80,17 +91,24 @@ const copySingle = async ( )[0] const inPath = joinPath(assetDir, fileName) - const outPath = joinPath( - copyTo, - codepoints.filter((x) => x !== VARIANT_SELECTOR_EMOJI).join("-") + - ".svg" + const outputFilenames = withAndWithoutVariationSelectors( + codepoints.join("-") + ".svg" ) if (fileName.endsWith(".png")) { const pngData = await readFile(inPath) - await writeFile(outPath, embedPngInSvg(pngData)) + const svg = embedPngInSvg(pngData) + await Promise.all( + outputFilenames.map((filename) => + writeFile(joinPath(copyTo, filename), svg) + ) + ) } else { - await copyFile(inPath, outPath) + await Promise.all( + outputFilenames.map((filename) => + copyFile(inPath, joinPath(copyTo, filename)) + ) + ) } } @@ -114,9 +132,14 @@ export const copyFluent = async (flavorName: string, toPath: string) => { const hasSkinTones = emojiDirContents.includes("Medium-Dark") // name unlikely to be reused if (hasSkinTones) { - copyWithSkinTones(toPath, flavorName, emojiPath, codepoints) + await copyWithSkinTones( + toPath, + flavorName, + emojiPath, + codepoints + ) } else { - copySingle(toPath, flavorName, emojiPath, codepoints) + await copySingle(toPath, flavorName, emojiPath, codepoints) } } } diff --git a/src/extractors/mutant.ts b/src/extractors/mutant.ts index 33482e7..f43e609 100644 --- a/src/extractors/mutant.ts +++ b/src/extractors/mutant.ts @@ -2,7 +2,7 @@ import { existsSync } from "fs" import { copyFile, mkdir, readdir } from "fs/promises" import { join as joinPath } from "path" import { packsDir } from "../app.js" -import { VARIANT_SELECTOR_EMOJI } from "../constants.js" +import { withAndWithoutVariationSelectors } from "../variation-selectors.js" export const copyMutantTo = async (outDir: string) => { const mutantDir = joinPath(packsDir, "mutant-remix", "emoji") @@ -14,15 +14,8 @@ export const copyMutantTo = async (outDir: string) => { for (const emoji of mutantSvgs) { const inPath = joinPath(mutantDir, emoji) - const codepoints = emoji.split("-") - const normalizedFilename = - codepoints.length === 2 - ? codepoints - .filter((x) => x !== VARIANT_SELECTOR_EMOJI) - .join("-") - : codepoints.join("-") - const outPath = joinPath(outDir, normalizedFilename) - - await copyFile(inPath, outPath) + for (const filename of withAndWithoutVariationSelectors(emoji)) { + await copyFile(inPath, joinPath(outDir, filename)) + } } } diff --git a/src/extractors/twemoji.ts b/src/extractors/twemoji.ts index 2638f3d..8b4220b 100644 --- a/src/extractors/twemoji.ts +++ b/src/extractors/twemoji.ts @@ -2,6 +2,7 @@ import { copyFile, mkdir, readdir } from "fs/promises" import { existsSync } from "fs" import { join as joinPath } from "path" import { packsDir } from "../app.js" +import { withAndWithoutVariationSelectors } from "../variation-selectors.js" export const getAllExistingTwemoji = async () => { let out: string[] = [] @@ -26,7 +27,15 @@ export const copyTwemojiTo = async (outDir: string) => { for (const emoji of twemojiSvgs) { const inPath = joinPath(twemojiDir, emoji) - const outPath = joinPath(outDir, emoji) - if (!existsSync(outPath)) await copyFile(inPath, outPath) + const outputFilenames = withAndWithoutVariationSelectors(emoji) + const existingPackAsset = outputFilenames + .map((filename) => joinPath(outDir, filename)) + .find((path) => existsSync(path)) + const source = existingPackAsset ?? inPath + + for (const filename of outputFilenames) { + const outPath = joinPath(outDir, filename) + if (!existsSync(outPath)) await copyFile(source, outPath) + } } } diff --git a/src/variation-selectors.ts b/src/variation-selectors.ts new file mode 100644 index 0000000..f2350d7 --- /dev/null +++ b/src/variation-selectors.ts @@ -0,0 +1,66 @@ +import { existsSync } from "fs" +import { copyFile, readdir } from "fs/promises" +import { basename, extname, join as joinPath } from "path" +import { VARIANT_SELECTOR_EMOJI } from "./constants.js" + +export const withAndWithoutVariationSelectors = (filename: string) => { + const extension = extname(filename) + const codepoints = basename(filename, extension).split("-") + + if (!codepoints.includes(VARIANT_SELECTOR_EMOJI)) return [filename] + + const withoutVariationSelectors = + codepoints + .filter((codepoint) => codepoint !== VARIANT_SELECTOR_EMOJI) + .join("-") + extension + + return [filename, withoutVariationSelectors] +} + +export const ensureVariationSelectorAliases = async (packDirs: string[]) => { + const filenamesByPack = await Promise.all( + packDirs.map((dir) => readdir(dir)), + ) + const aliases = new Map() + + for (const filenames of filenamesByPack) { + for (const filename of filenames) { + const [withVariationSelectors, withoutVariationSelectors] = + withAndWithoutVariationSelectors(filename) + + if (withoutVariationSelectors !== undefined) { + aliases.set(withVariationSelectors, withoutVariationSelectors) + } + } + } + + for (let index = 0; index < packDirs.length; index++) { + const packDir = packDirs[index] + const filenames = new Set(filenamesByPack[index]) + + for (const [ + withVariationSelectors, + withoutVariationSelectors, + ] of aliases) { + const hasWith = filenames.has(withVariationSelectors) + const hasWithout = filenames.has(withoutVariationSelectors) + + if (hasWith === hasWithout) continue + + const source = hasWith + ? withVariationSelectors + : withoutVariationSelectors + const destination = hasWith + ? withoutVariationSelectors + : withVariationSelectors + + if (!existsSync(joinPath(packDir, destination))) { + await copyFile( + joinPath(packDir, source), + joinPath(packDir, destination), + ) + filenames.add(destination) + } + } + } +}