Skip to content

Commit

Permalink
Implement draft embeddable icon links generator #5
Browse files Browse the repository at this point in the history
  • Loading branch information
neg4n committed Sep 7, 2021
1 parent 722f7bd commit dbe95b2
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 23 deletions.
69 changes: 49 additions & 20 deletions packages/faviconize/src/faviconize.ts
Original file line number Diff line number Diff line change
@@ -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<IconType>} 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<IconType>,
outputIconTypes?: IconType | Array<IconType>,
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<IconType>} 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<IconType>, tileColor?: string) {
const normalizedOutputTypes = normalizeOutputTypes(outputIconTypes)
const linkTags: Array<string> = []

if (tileColor) {
if (!isValidHexColorString(tileColor)) {
throw new Error(`Provided tile color (${tileColor}) is not valid hex color string.`)
}

linkTags.push(`<meta name="msapplication-TileColor" content="${tileColor}">`)
}

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(`<meta name="msapplication-TileImage" content="${filePath}">`)
return
}

linkTags.push(`<link rel="${type}" type="image/png" href="${filePath}" sizes="${size.join('x')}">`)
})

return linkTags
}
15 changes: 15 additions & 0 deletions packages/faviconize/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,18 @@ export async function resolveAndCheckInputFilePath(inputFilePath: string) {
}
return resolvedInputFilePath
}

export async function forEachIconTypeEdgeIncludes(
uniqueOutputTypes: Set<IconType>,
fn: (type: IconType, edge: number) => Promise<void> | 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)
}
4 changes: 2 additions & 2 deletions packages/faviconize/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
import { faviconize } from './faviconize'
export { faviconize }
import { faviconize, generateIconsLinkTags } from './faviconize'
export { faviconize, generateIconsLinkTags }
8 changes: 8 additions & 0 deletions packages/faviconize/tests/faviconize.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
45 changes: 44 additions & 1 deletion packages/faviconize/tests/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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<IconType>)
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()
})
})

0 comments on commit dbe95b2

Please sign in to comment.