Skip to content

Commit dbe95b2

Browse files
committed
Implement draft embeddable icon links generator #5
1 parent 722f7bd commit dbe95b2

File tree

5 files changed

+118
-23
lines changed

5 files changed

+118
-23
lines changed

packages/faviconize/src/faviconize.ts

+49-20
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,69 @@
11
import * as path from 'path'
22
import sharp from 'sharp'
33

4-
import { iconTypesAndEdgesMap } from './constants'
5-
import { normalizeOutputTypes, resolveAndCreateOrUseOutputPath, resolveAndCheckInputFilePath } from './helpers'
4+
import {
5+
normalizeOutputTypes,
6+
resolveAndCreateOrUseOutputPath,
7+
resolveAndCheckInputFilePath,
8+
forEachIconTypeEdgeIncludes,
9+
isValidHexColorString,
10+
} from './helpers'
611
import { IconType } from './types'
712

813
/**
914
* Generate favicons in various formats from image.
1015
* @param {string} imageInput File from which icons will be generated. Can be path to input file or buffer.
11-
* @param {IconType | IconType[]} outputTypes Icon types to be generated. Can be a single type or an array of types. null means all types.
16+
* @param {IconType | Array<IconType>} outputIconTypes Icon types to be generated. Can be a single type or an array of types. null means all types.
1217
* @param {string} outputDirectoryPath Directory where to save icons. If not specified it will be `icons/`
1318
*/
1419
export async function faviconize(
1520
imageInput: string | Buffer,
16-
outputTypes?: IconType | Array<IconType>,
21+
outputIconTypes?: IconType | Array<IconType>,
1722
outputDirectoryPath?: string,
1823
) {
1924
const resolvedImageInput = Buffer.isBuffer(imageInput) ? imageInput : await resolveAndCheckInputFilePath(imageInput)
20-
const normalizedOutputTypes = normalizeOutputTypes(outputTypes)
25+
const normalizedOutputTypes = normalizeOutputTypes(outputIconTypes)
2126
const resolvedOutputPath = await resolveAndCreateOrUseOutputPath(outputDirectoryPath)
2227

23-
for (const [type, edges] of Object.entries(iconTypesAndEdgesMap)) {
24-
if (normalizedOutputTypes.has(type as IconType)) {
25-
try {
26-
await Promise.all(
27-
edges.map((edge) => {
28-
const size = [edge, edge]
29-
const outputFileAbsolutePath = path.join(resolvedOutputPath, `${type}-${size.join('x')}.png`)
30-
return sharp(resolvedImageInput)
31-
.resize(...size)
32-
.toFile(outputFileAbsolutePath)
33-
}),
34-
)
35-
} catch (error) {
36-
console.error(error)
37-
}
28+
await forEachIconTypeEdgeIncludes(normalizedOutputTypes, async (type, edge) => {
29+
const size = [edge, edge]
30+
const outputFileAbsolutePath = path.join(resolvedOutputPath, `${type}-${size.join('x')}.png`)
31+
32+
await sharp(resolvedImageInput)
33+
.resize(...size)
34+
.toFile(outputFileAbsolutePath)
35+
})
36+
}
37+
38+
/**
39+
* Generate embeddable favicons link tags.
40+
* @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.
41+
* @param {string} tileColor Optional HEX (`#rrggbb` or `#rgb`) string representing the color of the tile in Microsoft specific integrations.
42+
*/
43+
export async function generateIconsLinkTags(outputIconTypes?: IconType | Array<IconType>, tileColor?: string) {
44+
const normalizedOutputTypes = normalizeOutputTypes(outputIconTypes)
45+
const linkTags: Array<string> = []
46+
47+
if (tileColor) {
48+
if (!isValidHexColorString(tileColor)) {
49+
throw new Error(`Provided tile color (${tileColor}) is not valid hex color string.`)
3850
}
51+
52+
linkTags.push(`<meta name="msapplication-TileColor" content="${tileColor}">`)
3953
}
54+
55+
await forEachIconTypeEdgeIncludes(normalizedOutputTypes, (type, edge) => {
56+
const size = [edge, edge]
57+
const fileName = `${type}-${size.join('x')}.png`
58+
const filePath = path.join('icons', fileName)
59+
60+
if (type === 'msapplication-TileImage') {
61+
linkTags.push(`<meta name="msapplication-TileImage" content="${filePath}">`)
62+
return
63+
}
64+
65+
linkTags.push(`<link rel="${type}" type="image/png" href="${filePath}" sizes="${size.join('x')}">`)
66+
})
67+
68+
return linkTags
4069
}

packages/faviconize/src/helpers.ts

+15
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,18 @@ export async function resolveAndCheckInputFilePath(inputFilePath: string) {
4545
}
4646
return resolvedInputFilePath
4747
}
48+
49+
export async function forEachIconTypeEdgeIncludes(
50+
uniqueOutputTypes: Set<IconType>,
51+
fn: (type: IconType, edge: number) => Promise<void> | void,
52+
) {
53+
for (const [type, edges] of Object.entries(iconTypesAndEdgesMap)) {
54+
if (uniqueOutputTypes.has(type as IconType)) {
55+
await Promise.all(edges.map((edge) => fn(type as IconType, edge)))
56+
}
57+
}
58+
}
59+
60+
export function isValidHexColorString(color: string) {
61+
return /^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/.test(color)
62+
}

packages/faviconize/src/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
import { faviconize } from './faviconize'
2-
export { faviconize }
1+
import { faviconize, generateIconsLinkTags } from './faviconize'
2+
export { faviconize, generateIconsLinkTags }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { faviconize, generateIconsLinkTags } from '../src/faviconize'
2+
3+
describe(faviconize, () => {
4+
it('is a function', async () => {
5+
console.log(await generateIconsLinkTags(null, '#cccccc'))
6+
expect(typeof faviconize).toBe('function')
7+
})
8+
})

packages/faviconize/tests/helpers.test.ts

+44-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import fs from 'fs/promises'
33
import { vol as memoryFsVolume } from 'memfs'
44

55
import { IconType, InputFileError } from '../src/types'
6-
import { resolveAndCreateOrUseOutputPath, normalizeOutputTypes, resolveAndCheckInputFilePath } from '../src/helpers'
6+
import {
7+
resolveAndCreateOrUseOutputPath,
8+
normalizeOutputTypes,
9+
resolveAndCheckInputFilePath,
10+
forEachIconTypeEdgeIncludes,
11+
isValidHexColorString,
12+
} from '../src/helpers'
713
import { defaultOutputDirectory, iconTypesAndEdgesMap } from '../src/constants'
814

915
jest.mock('fs/promises')
@@ -121,3 +127,40 @@ describe(resolveAndCheckInputFilePath, () => {
121127
await expect(futureInputFilePath).rejects.toThrow(new InputFileError('is-a-directory'))
122128
})
123129
})
130+
131+
describe(forEachIconTypeEdgeIncludes, () => {
132+
it('should iterate over all icon types and edges', async () => {
133+
const uniqueIconTypes = new Set(Object.keys(iconTypesAndEdgesMap) as Array<IconType>)
134+
const expectedCalls = Object.values(iconTypesAndEdgesMap).flat().length
135+
const spyFn = jest.fn()
136+
137+
await forEachIconTypeEdgeIncludes(uniqueIconTypes, spyFn)
138+
expect(spyFn).toHaveBeenCalledTimes(expectedCalls)
139+
})
140+
})
141+
142+
describe(isValidHexColorString, () => {
143+
it('should return true for 6 digits hex color', () => {
144+
expect(isValidHexColorString('#000000')).toBeTruthy()
145+
})
146+
147+
it('should return true for 3 digits hex color', () => {
148+
expect(isValidHexColorString('#000')).toBeTruthy()
149+
})
150+
151+
it('should return false for HTML color literal', () => {
152+
expect(isValidHexColorString('blue')).toBeFalsy()
153+
})
154+
155+
it('should return false for hex color with too much digits', () => {
156+
expect(isValidHexColorString('#000000000')).toBeFalsy()
157+
})
158+
159+
it('should return false for hex color with too few digits', () => {
160+
expect(isValidHexColorString('#00000')).toBeFalsy()
161+
})
162+
163+
it('should return false if there is no # while rest of the color is valid', () => {
164+
expect(isValidHexColorString('000000')).toBeFalsy()
165+
})
166+
})

0 commit comments

Comments
 (0)