@sequencemedia/make-face
Version:
Convert font files into CSS @font-face declarations with embedded Base64 data
451 lines (411 loc) • 10.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.makeFace = makeFace;
exports.readFace = readFace;
var _debug = _interopRequireDefault(require("debug"));
var _fsExtra = require("fs-extra");
var _nodePath = _interopRequireDefault(require("node:path"));
var _mime = _interopRequireDefault(require("mime"));
var _globAll = _interopRequireDefault(require("glob-all"));
var _promises = require("node:fs/promises");
var _constants = _interopRequireDefault(require("./constants.mjs"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
/**
* @typedef {Object} FileDescriptor
* @property {string} filePath
* @property {Buffer} fileData
*/
const log = (0, _debug.default)('@sequencemedia/make-face');
const SRC_GLOB = `**/?(${_constants.default.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 (0, _fsExtra.ensureFile)(filePath);
return (0, _promises.readFile)(filePath);
}
/**
* @param {string} filePath
* @param {Buffer} fileData
* @returns {Promise<undefined>}
*/
async function writeFileToFS(filePath, fileData) {
/*
* log('writeFileToFS')
*/
await (0, _fsExtra.ensureFile)(filePath);
return (0, _promises.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 (0, _promises.stat)(directory);
} catch (e) {
throw new Error(getStatError(e, directory));
}
}
/**
* @param {string} directory
* @returns {string}
*/
const getSrcFileGlob = directory => _nodePath.default.join(directory, SRC_GLOB);
/**
* @param {string} directory
* @returns {string}
*/
const getCSSFileGlob = directory => _nodePath.default.join(directory, CSS_GLOB);
/**
* @param {string} filePath
* @returns {Promise<string[]>}
*/
function getFilePathList(filePath) {
/*
* log('getFilePathList')
*/
return new Promise((resolve, reject) => {
(0, _globAll.default)(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.default.getType(filePath);
/**
* @param {string} filePath
* @returns {string}
*/
function getFontFormat(filePath) {
/*
* log('getFontFormat')
*/
const extension = _nodePath.default.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 = _nodePath.default.extname(filePath);
return filePath.replace(new RegExp(extName.concat('$')), '.css').replace(new RegExp('^'.concat(srcPath)), cssPath);
}
/**
* @param {string} filePath
* @returns {string}
*/
const transformToFontFamily = filePath => _nodePath.default.basename(filePath, _nodePath.default.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[]>}
*/
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>}
*/
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);
}
}