UNPKG

@sequencemedia/make-face

Version:

Convert font files into CSS @font-face declarations with embedded Base64 data

490 lines (429 loc) 10.5 kB
/** * @typedef {Object} FileDescriptor * @property {string} filePath * @property {Buffer} fileData */ import debug from 'debug' import { ensureFile } from 'fs-extra' import path from 'node:path' import mime from 'mime' import glob from 'glob-all' import { stat, writeFile, readFile } from 'node:fs/promises' import FORMATS from './constants.mjs' const log = debug('@sequencemedia/make-face') const SRC_GLOB = `**/?(${FORMATS.sort().map((format) => '*.'.concat(format)).join('|')})` const CSS_GLOB = '**/*.css' /** * @param {Error} e - Error * @param {string|undefined} p - Path * @returns {string} */ const getStatError = (e, p) => ( (e.code === 'ENOENT') ? `Path "${p}" does not exist.` : (p) ? `An error occurred on path "${p}": ${e.message}` : 'Path is not defined.' ) /** * @param {string} filePath * @returns {Promise<Buffer>} */ async function readFileFromFS (filePath) { /* * log('readFileFromFS') */ await ensureFile(filePath) return ( readFile(filePath) ) } /** * @param {string} filePath * @param {Buffer} fileData * @returns {Promise<undefined>} */ async function writeFileToFS (filePath, fileData) { /* * log('writeFileToFS') */ await ensureFile(filePath) return ( writeFile(filePath, fileData) ) } /** * @param {FileDescriptor} * @returns {Promise<undefined>} */ function writeFileDataToFS ({ filePath, fileData }) { /* * log('writeFileDataToFS') */ return ( writeFileToFS(filePath, fileData) ) } /** * @param {FileDescriptor[]} fileDataList * @returns {Promise<undefined[]>} */ function writeFileDataListToFS (fileDataList) { /* * log('writeFileDataListToFS') */ return ( Promise.all(fileDataList.map(writeFileDataToFS)) ) } /** * @param {string} filePath * @returns {Promise<FileDescriptor>} */ async function readFileDataFromFS (filePath) { /* * log('readFileDataFromFS') */ const fileData = await readFileFromFS(filePath) return { filePath, fileData } } /** * @param {string[]} filePathList * @returns {Promise<FileDescriptor[]>} */ function readFileDataListFromFS (filePathList) { /* * log('readFileDataListFromFS') */ return ( Promise.all(filePathList.map(readFileDataFromFS)) ) } /** * @param {string} directory * @returns {Promise<undefined>} */ async function statPath (directory) { /* * log('statPath') */ try { await stat(directory) } catch (e) { throw new Error(getStatError(e, directory)) } } /** * @param {string} directory * @returns {string} */ const getSrcFileGlob = (directory) => path.join(directory, SRC_GLOB) /** * @param {string} directory * @returns {string} */ const getCSSFileGlob = (directory) => path.join(directory, CSS_GLOB) /** * @param {string} filePath * @returns {Promise<string[]>} */ function getFilePathList (filePath) { /* * log('getFilePathList') */ return new Promise((resolve, reject) => { glob(filePath, (e, filePathList) => (!e) ? resolve(filePathList) : reject(e)) }) } /** * @param {string} directory * @returns {Promise<string[]>} */ function getSrcFilePathList (directory) { /* * log('getSrcFilePathList') */ return ( getFilePathList(getSrcFileGlob(directory)) ) } /** * @param {string} directory * @returns {Promise<string[]>} */ function getCSSFilePathList (directory) { /* * log('getCSSFilePathList') */ return ( getFilePathList(getCSSFileGlob(directory)) ) } /** * @param {string} cssFilePath * @param {string} srcPath * @param {string} cssPath * @returns {Function} */ function findCSSFilePathFactory (cssFilePath, srcPath, cssPath) { /* * log('findCSSFilePathFactory') */ return function findCSSFilePath ({ filePath }) { /* * log('findCSSFilePath') */ return cssFilePath === getCSSFilePathFromSrcFilePath(filePath, srcPath, cssPath) } } /** * @param {string} filePath * @returns {string} */ const getFontMimeType = (filePath) => mime.getType(filePath) /** * @param {string} filePath * @returns {string} */ function getFontFormat (filePath) { /* * log('getFontFormat') */ const extension = path.extname(filePath).slice(1).toLowerCase() switch (extension) { case 'ttf': return 'truetype' case 'otf': return 'opentype' case 'eot': return 'embedded-opentype' default: return extension } } /** * @param {string} filePath * @param {string} srcPath * @param {string} cssPath * @returns {string} */ function getCSSFilePathFromSrcFilePath (filePath, srcPath, cssPath) { /* * log('getCSSFilePathFromSrcFilePath') */ const extName = path.extname(filePath) return ( filePath .replace(new RegExp(extName.concat('$')), '.css') .replace(new RegExp('^'.concat(srcPath)), cssPath) ) } /** * @param {string} filePath * @returns {string} */ const transformToFontFamily = (filePath) => path.basename(filePath, path.extname(filePath)) /** * @param {string} filePath * @param {Buffer} fileData * @returns {string} */ const transformToUrl = (filePath, fileData) => `url(data:${getFontMimeType(filePath)};base64,${fileData.toString('base64')}) format('${getFontFormat(filePath)}')` /** * @param {FileDescriptor[]} filePathList * @returns {string} */ const transformToSrc = (filePathList) => filePathList.map(({ filePath, fileData }) => transformToUrl(filePath, fileData)).join(', ') /** * @param {string} filePath * @param {Buffer} fileData * @returns {string} */ const transformCSSFileDataListLine = (filePath, fileData) => { /* * log('transformCSSFileDataListLine') */ return ` /** * "${filePath}" */ ${fileData.toString('utf8').replace(/^.*\/\*\*.*\n(?: \* +".*"\n)+ \*\/\n/gm, '').replace(/^\n+|\n+$/g, '')} ` } /** * @param {FileDescriptor[]} filePathList * @returns {string} */ const transformCSSFilePathList = (filePathList) => { /* * log('transformCSSFilePathList') */ const [ { filePath } ] = filePathList return ` /** ${filePathList.map(({ filePath }) => ` * "${filePath}"`).join('\n')} */ @font-face { font-family: '${transformToFontFamily(filePath)}'; src: ${transformToSrc(filePathList)}; } ` } /** * @param {FileDescriptor[]} fileDataList * @returns {string} */ const transformCSSFileDataList = (fileDataList) => { /* * log('transformCSSFileDataList') */ return ( fileDataList .reduce((accumulator, { filePath, fileData }) => accumulator.concat(transformCSSFileDataListLine(filePath, fileData)), '') ) } /** * @param {FileDescriptor[]} filePathList * @returns {Buffer} */ const createCSSFileDataFromCSSFilePathList = (filePathList) => Buffer.from(transformCSSFilePathList(filePathList)) /** * @param {FileDescriptor[]} fileDataList * @returns {Buffer} */ const createCSSFileDataFromCSSFileDataList = (fileDataList) => Buffer.from(transformCSSFileDataList(fileDataList)) /** * @param {FileDescriptor[]} cssFileDataList * @param {string} cssFilePath * @returns {FileDescriptor} */ function transformToCSSFileDataFromCSSFileDataList (cssFileDataList, cssFilePath) { /* * log('transformToCSSFileDataFromCSSFileDataList') */ const cssFileData = createCSSFileDataFromCSSFileDataList(cssFileDataList) return { filePath: cssFilePath, fileData: cssFileData } } /** * @param {FileDescriptor[]} srcFilePathList * @param {string} srcPath * @param {string} cssPath * @returns {FileDescriptor[]} */ function transformToCSSFileDataListFromSrcFilePathList (srcFilePathList, srcPath, cssPath) { /* * log('transformToCSSFileDataListFromSrcFilePathList') */ return ( srcFilePathList.reduce((accumulator, { filePath: srcFilePath }) => { // , index, array) => { /* * Create the css file path */ const cssFilePath = getCSSFilePathFromSrcFilePath(srcFilePath, srcPath, cssPath) /* * Create a filter function using the css file path */ const findCSSFilePath = findCSSFilePathFactory(cssFilePath, srcPath, cssPath) /* * Use the filter function to determine whether we have processed this css file path */ if (accumulator.some(findCSSFilePath)) return accumulator /* * Process this css file path by using the filter function to generate a list of file paths */ const cssFilePathList = srcFilePathList.filter(findCSSFilePath) /* * Create the css file data */ const cssFileData = createCSSFileDataFromCSSFilePathList(cssFilePathList) return accumulator.concat({ filePath: cssFilePath, fileData: cssFileData }) }, []) ) } /** * @param {string} origin * @param {string} destination * @returns {Promise<FileDescriptor[]>} */ export async function makeFace (origin, destination) { log('Starting ...') try { /* * Does `origin` exist? */ await statPath(origin) /* * Does `destination` exist? */ await statPath(destination) /* * Read files at `origin` to file path list array */ log(`Reading faces from "${origin}"`) const srcFileDataList = await readFileDataListFromFS(await getSrcFilePathList(origin)) const cssFileDataList = transformToCSSFileDataListFromSrcFilePathList(srcFileDataList, origin, destination) /* * Transform and write to `destination` */ log(`Writing faces to "${destination}"`) await writeFileDataListToFS(cssFileDataList) log('Done.') return ( cssFileDataList ) } catch ({ message }) { log(message) } } /** * @returns {Promise<FileDescriptor>} */ export async function readFace (origin, destination) { log('Starting ...') try { /* * Does `origin` exist? */ await statPath(origin) /* * Read files at `origin` to file path list array */ log(`Reading faces from "${origin}"`) const cssFileDataList = await readFileDataListFromFS(await getCSSFilePathList(origin)) const cssFileData = transformToCSSFileDataFromCSSFileDataList(cssFileDataList, destination) /* * Transform and write to `destination` */ log(`Writing faces to "${destination}"`) await writeFileDataToFS(cssFileData) log('Done.') return ( cssFileData ) } catch ({ message }) { log(message) } }