From dbe95b25c1c5b80b4fbd851290be2fa2e28e9a3d Mon Sep 17 00:00:00 2001 From: Igor Klepacki Date: Tue, 7 Sep 2021 03:20:13 +0200 Subject: [PATCH] Implement draft embeddable icon links generator #5 --- packages/faviconize/src/faviconize.ts | 69 ++++++++++++++------ packages/faviconize/src/helpers.ts | 15 +++++ packages/faviconize/src/index.ts | 4 +- packages/faviconize/tests/faviconize.test.ts | 8 +++ packages/faviconize/tests/helpers.test.ts | 45 ++++++++++++- 5 files changed, 118 insertions(+), 23 deletions(-) create mode 100644 packages/faviconize/tests/faviconize.test.ts diff --git a/packages/faviconize/src/faviconize.ts b/packages/faviconize/src/faviconize.ts index 43fdc5a..cc083e9 100644 --- a/packages/faviconize/src/faviconize.ts +++ b/packages/faviconize/src/faviconize.ts @@ -1,40 +1,69 @@ import * as path from 'path' import sharp from 'sharp' -import { iconTypesAndEdgesMap } from './constants' -import { normalizeOutputTypes, resolveAndCreateOrUseOutputPath, resolveAndCheckInputFilePath } from './helpers' +import { + normalizeOutputTypes, + resolveAndCreateOrUseOutputPath, + resolveAndCheckInputFilePath, + forEachIconTypeEdgeIncludes, + isValidHexColorString, +} from './helpers' import { IconType } from './types' /** * Generate favicons in various formats from image. * @param {string} imageInput File from which icons will be generated. Can be path to input file or buffer. - * @param {IconType | IconType[]} outputTypes Icon types to be generated. Can be a single type or an array of types. null means all types. + * @param {IconType | Array} outputIconTypes Icon types to be generated. Can be a single type or an array of types. null means all types. * @param {string} outputDirectoryPath Directory where to save icons. If not specified it will be `icons/` */ export async function faviconize( imageInput: string | Buffer, - outputTypes?: IconType | Array, + outputIconTypes?: IconType | Array, outputDirectoryPath?: string, ) { const resolvedImageInput = Buffer.isBuffer(imageInput) ? imageInput : await resolveAndCheckInputFilePath(imageInput) - const normalizedOutputTypes = normalizeOutputTypes(outputTypes) + const normalizedOutputTypes = normalizeOutputTypes(outputIconTypes) const resolvedOutputPath = await resolveAndCreateOrUseOutputPath(outputDirectoryPath) - for (const [type, edges] of Object.entries(iconTypesAndEdgesMap)) { - if (normalizedOutputTypes.has(type as IconType)) { - try { - await Promise.all( - edges.map((edge) => { - const size = [edge, edge] - const outputFileAbsolutePath = path.join(resolvedOutputPath, `${type}-${size.join('x')}.png`) - return sharp(resolvedImageInput) - .resize(...size) - .toFile(outputFileAbsolutePath) - }), - ) - } catch (error) { - console.error(error) - } + await forEachIconTypeEdgeIncludes(normalizedOutputTypes, async (type, edge) => { + const size = [edge, edge] + const outputFileAbsolutePath = path.join(resolvedOutputPath, `${type}-${size.join('x')}.png`) + + await sharp(resolvedImageInput) + .resize(...size) + .toFile(outputFileAbsolutePath) + }) +} + +/** + * Generate embeddable favicons link tags. + * @param {IconType | Array} outputIconTypes Icon types for whose link tags will be generated. Can be a single type or an array of types. null means all types. + * @param {string} tileColor Optional HEX (`#rrggbb` or `#rgb`) string representing the color of the tile in Microsoft specific integrations. + */ +export async function generateIconsLinkTags(outputIconTypes?: IconType | Array, tileColor?: string) { + const normalizedOutputTypes = normalizeOutputTypes(outputIconTypes) + const linkTags: Array = [] + + if (tileColor) { + if (!isValidHexColorString(tileColor)) { + throw new Error(`Provided tile color (${tileColor}) is not valid hex color string.`) } + + linkTags.push(``) } + + await forEachIconTypeEdgeIncludes(normalizedOutputTypes, (type, edge) => { + const size = [edge, edge] + const fileName = `${type}-${size.join('x')}.png` + const filePath = path.join('icons', fileName) + + if (type === 'msapplication-TileImage') { + linkTags.push(``) + return + } + + linkTags.push(``) + }) + + return linkTags } diff --git a/packages/faviconize/src/helpers.ts b/packages/faviconize/src/helpers.ts index ec5bdf0..e94c9d7 100644 --- a/packages/faviconize/src/helpers.ts +++ b/packages/faviconize/src/helpers.ts @@ -45,3 +45,18 @@ export async function resolveAndCheckInputFilePath(inputFilePath: string) { } return resolvedInputFilePath } + +export async function forEachIconTypeEdgeIncludes( + uniqueOutputTypes: Set, + fn: (type: IconType, edge: number) => Promise | void, +) { + for (const [type, edges] of Object.entries(iconTypesAndEdgesMap)) { + if (uniqueOutputTypes.has(type as IconType)) { + await Promise.all(edges.map((edge) => fn(type as IconType, edge))) + } + } +} + +export function isValidHexColorString(color: string) { + return /^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/.test(color) +} diff --git a/packages/faviconize/src/index.ts b/packages/faviconize/src/index.ts index d9605b1..33bd636 100644 --- a/packages/faviconize/src/index.ts +++ b/packages/faviconize/src/index.ts @@ -1,2 +1,2 @@ -import { faviconize } from './faviconize' -export { faviconize } +import { faviconize, generateIconsLinkTags } from './faviconize' +export { faviconize, generateIconsLinkTags } diff --git a/packages/faviconize/tests/faviconize.test.ts b/packages/faviconize/tests/faviconize.test.ts new file mode 100644 index 0000000..4543828 --- /dev/null +++ b/packages/faviconize/tests/faviconize.test.ts @@ -0,0 +1,8 @@ +import { faviconize, generateIconsLinkTags } from '../src/faviconize' + +describe(faviconize, () => { + it('is a function', async () => { + console.log(await generateIconsLinkTags(null, '#cccccc')) + expect(typeof faviconize).toBe('function') + }) +}) diff --git a/packages/faviconize/tests/helpers.test.ts b/packages/faviconize/tests/helpers.test.ts index 5236aa0..06c95fa 100644 --- a/packages/faviconize/tests/helpers.test.ts +++ b/packages/faviconize/tests/helpers.test.ts @@ -3,7 +3,13 @@ import fs from 'fs/promises' import { vol as memoryFsVolume } from 'memfs' import { IconType, InputFileError } from '../src/types' -import { resolveAndCreateOrUseOutputPath, normalizeOutputTypes, resolveAndCheckInputFilePath } from '../src/helpers' +import { + resolveAndCreateOrUseOutputPath, + normalizeOutputTypes, + resolveAndCheckInputFilePath, + forEachIconTypeEdgeIncludes, + isValidHexColorString, +} from '../src/helpers' import { defaultOutputDirectory, iconTypesAndEdgesMap } from '../src/constants' jest.mock('fs/promises') @@ -121,3 +127,40 @@ describe(resolveAndCheckInputFilePath, () => { await expect(futureInputFilePath).rejects.toThrow(new InputFileError('is-a-directory')) }) }) + +describe(forEachIconTypeEdgeIncludes, () => { + it('should iterate over all icon types and edges', async () => { + const uniqueIconTypes = new Set(Object.keys(iconTypesAndEdgesMap) as Array) + const expectedCalls = Object.values(iconTypesAndEdgesMap).flat().length + const spyFn = jest.fn() + + await forEachIconTypeEdgeIncludes(uniqueIconTypes, spyFn) + expect(spyFn).toHaveBeenCalledTimes(expectedCalls) + }) +}) + +describe(isValidHexColorString, () => { + it('should return true for 6 digits hex color', () => { + expect(isValidHexColorString('#000000')).toBeTruthy() + }) + + it('should return true for 3 digits hex color', () => { + expect(isValidHexColorString('#000')).toBeTruthy() + }) + + it('should return false for HTML color literal', () => { + expect(isValidHexColorString('blue')).toBeFalsy() + }) + + it('should return false for hex color with too much digits', () => { + expect(isValidHexColorString('#000000000')).toBeFalsy() + }) + + it('should return false for hex color with too few digits', () => { + expect(isValidHexColorString('#00000')).toBeFalsy() + }) + + it('should return false if there is no # while rest of the color is valid', () => { + expect(isValidHexColorString('000000')).toBeFalsy() + }) +})