svgtofont
Version:
Converts SVG to TTF/EOT/WOFF/WOFF2/SVG format fonts.
230 lines (224 loc) • 9.07 kB
JavaScript
import fs from 'fs-extra';
import path from 'path';
import { optimize } from 'svgo';
import { filterSvgFiles, toPascalCase } from './utils.js';
/**
* Generate Icon SVG Path Source
* <font-name>.json
*/
export async function generateIconsSource(options = {}) {
const ICONS_PATH = filterSvgFiles(options.src);
const data = await buildPathsObject(ICONS_PATH, options);
const outPath = path.join(options.dist, `${options.fontName}.json`);
await fs.outputFile(outPath, `{${data}\n}`);
return outPath;
}
/**
* Loads SVG file for each icon, extracts path strings `d="path-string"`,
* and constructs map of icon name to array of path strings.
* @param {array} files
*/
async function buildPathsObject(files, options = {}) {
const svgoOptions = options.svgoOptions || {};
return Promise.all(files.map(async (filepath) => {
const name = path.basename(filepath, '.svg');
const svg = fs.readFileSync(filepath, 'utf-8');
const pathStrings = optimize(svg, {
path: filepath,
...options,
plugins: [
'convertTransform',
...(svgoOptions.plugins || [])
// 'convertShapeToPath'
],
});
const str = (pathStrings.data.match(/ d="[^"]+"/g) || []).map(s => s.slice(3));
return `\n"${name}": [${str.join(',\n')}]`;
}));
}
const reactSource = (name, size, fontName, source) => `import React from 'react';
export const ${name} = props => (
<svg viewBox="0 0 20 20" ${size ? `width="${size}" height="${size}"` : ''} {...props} className={\`${fontName} \${props.className ? props.className : ''}\`}>${source}</svg>
);
`;
const reactTypeSource = (name) => `import React from 'react';
export declare const ${name}: (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
`;
/**
* Generate React Icon
* <font-name>.json
*/
export async function generateReactIcons(options = {}) {
const ICONS_PATH = filterSvgFiles(options.src);
const data = await outputReactFile(ICONS_PATH, options);
const outPath = path.join(options.dist, 'react', 'index.js');
fs.outputFileSync(outPath, data.join('\n'));
fs.outputFileSync(outPath.replace(/\.js$/, '.d.ts'), data.join('\n'));
return outPath;
}
async function outputReactFile(files, options = {}) {
const svgoOptions = options.svgoOptions || {};
const fontSizeOpt = typeof options.css !== 'boolean' && options.css.fontSize;
const fontSize = typeof fontSizeOpt === 'boolean' ? (fontSizeOpt === true ? '16px' : '') : fontSizeOpt;
const fontName = options.classNamePrefix || options.fontName;
return Promise.all(files.map(async (filepath) => {
let name = toPascalCase(path.basename(filepath, '.svg'));
if (/^[rR]eact$/.test(name)) {
name = name + toPascalCase(fontName);
}
const svg = fs.readFileSync(filepath, 'utf-8');
const pathData = optimize(svg, {
path: filepath,
...svgoOptions,
plugins: [
'removeXMLNS',
'removeEmptyAttrs',
'convertTransform',
// 'convertShapeToPath',
// 'removeViewBox'
...(svgoOptions.plugins || [])
]
});
const str = (pathData.data.match(/ d="[^"]+"/g) || []).map(s => s.slice(3));
const outDistPath = path.join(options.dist, 'react', `${name}.js`);
const pathStrings = str.map((d, i) => `<path d=${d} fillRule="evenodd" />`);
const comName = isNaN(Number(name.charAt(0))) ? name : toPascalCase(fontName) + name;
fs.outputFileSync(outDistPath, reactSource(comName, fontSize, fontName, pathStrings.join(',\n')));
fs.outputFileSync(outDistPath.replace(/\.js$/, '.d.ts'), reactTypeSource(comName));
return `export * from './${name}';`;
}));
}
const reactNativeSource = (fontName, defaultSize, iconMap) => `import { Text } from 'react-native';
const icons = ${JSON.stringify(Object.fromEntries(iconMap))};
export const ${fontName} = ({iconName, ...rest}) => {
return (<Text style={{fontFamily: '${fontName}', fontSize: ${defaultSize}, color: '#000000', ...rest}}>
{icons[iconName]}
</Text>);
};
`;
const reactNativeTypeSource = (name, iconMap) => `import { TextStyle } from 'react-native';
export type ${name}IconNames = ${[...iconMap.keys()].reduce((acc, key, index) => {
if (index === 0) {
acc = `'${key}'`;
}
else {
acc += ` | '${key}'`;
}
return acc;
}, `${'string'}`)}
export interface ${name}Props extends Omit<TextStyle, 'fontFamily' | 'fontStyle' | 'fontWeight'> {
iconName: ${name}IconNames
}
export declare const ${name}: (props: ${name}Props) => JSX.Element;
`;
/**
* Generate ReactNative Icon
* <font-name>.json
*/
export function generateReactNativeIcons(options = {}, unicodeObject) {
const ICONS_PATH = filterSvgFiles(options.src);
outputReactNativeFile(ICONS_PATH, options, unicodeObject);
}
function outputReactNativeFile(files, options = {}, unicodeObject) {
const fontSizeOpt = typeof options.css !== 'boolean' && options.css.fontSize;
const fontSize = typeof fontSizeOpt === 'boolean' ? 16 : parseInt(fontSizeOpt);
const fontName = options.classNamePrefix || options.fontName;
const iconMap = new Map();
files.map(filepath => {
const baseFileName = path.basename(filepath, '.svg');
iconMap.set(baseFileName, unicodeObject[baseFileName]);
});
const outDistPath = path.join(options.dist, 'reactNative', `${fontName}.jsx`);
const comName = isNaN(Number(fontName.charAt(0))) ? fontName : toPascalCase(fontName) + name;
fs.outputFileSync(outDistPath, reactNativeSource(comName, fontSize, iconMap));
fs.outputFileSync(outDistPath.replace(/\.jsx$/, '.d.ts'), reactNativeTypeSource(comName, iconMap));
}
/**
* Generate Vue Icon
* <font-name>.json
*/
export async function generateVueIcons(options = {}) {
const ICONS_PATH = filterSvgFiles(options.src);
const data = await outputVueFile(ICONS_PATH, options);
const outPath = path.join(options.dist, 'vue', 'index.js');
fs.outputFileSync(outPath, data.join('\n'));
fs.outputFileSync(outPath.replace(/\.js$/, '.d.ts'), data.join('\n'));
return outPath;
}
async function outputVueFile(files, options = {}) {
const svgoOptions = options.svgoOptions || {};
const fontSizeOpt = typeof options.css !== 'boolean' && options.css.fontSize;
const fontSize = typeof fontSizeOpt === 'boolean' ? (fontSizeOpt === true ? '16px' : '') : fontSizeOpt;
const fontName = options.classNamePrefix || options.fontName;
return Promise.all(files.map(async (filepath) => {
let name = toPascalCase(path.basename(filepath, '.svg'));
if (/^[vV]ue$/.test(name)) {
name = name + toPascalCase(fontName);
}
const svg = fs.readFileSync(filepath, 'utf-8');
const pathData = optimize(svg, {
path: filepath,
...svgoOptions,
plugins: [
'removeXMLNS',
'removeEmptyAttrs',
'convertTransform',
// 'convertShapeToPath',
// 'removeViewBox'
...(svgoOptions.plugins || [])
]
});
const str = (pathData.data.match(/ d="[^"]+"/g) || []).map(s => s.slice(3));
const outDistPath = path.join(options.dist, 'vue', `${name}.js`);
const pathStrings = str.map((d, i) => `<path d=${d} fillRule="evenodd" />`);
const comName = isNaN(Number(name.charAt(0))) ? name : toPascalCase(fontName) + name;
fs.outputFileSync(outDistPath, vueSource(comName, fontSize, fontName, pathStrings.join(',\n')));
fs.outputFileSync(outDistPath.replace(/\.js$/, '.d.ts'), vueTypeSource(comName));
return `export * from './${name}';`;
}));
}
const vueSource = (name, size, fontName, source) => `import { defineComponent, h } from 'vue';
export const ${name} = defineComponent({
name: '${name}',
props: {
class: {
type: String,
default: ''
}
},
setup(props, { attrs }) {
return () => h(
'svg',
{
viewBox: '0 0 20 20',
${size ? `width: '${size}', height: '${size}',` : ''}
class: \`${fontName} \${props.class}\`,
...attrs
},
[
${source
.split('\n')
.filter(Boolean)
.map(path => {
const attrPairs = [];
const attrRegex = /([a-zA-Z\-:]+)=("[^"]*"|'[^']*'|[^\s"']+)/g;
let match;
const pathContent = path.replace(/^<path\s*|\s*\/?>$/g, '');
while ((match = attrRegex.exec(pathContent)) !== null) {
const key = match[1];
const value = match[2];
attrPairs.push(`"${key}": ${value}`);
}
return `h('path', {${attrPairs.join(', ')}})`;
})
.join(',\n ')}
]
);
}
});
`;
const vueTypeSource = (name) => `import type { DefineComponent } from 'vue';
declare const ${name}: DefineComponent<Record<string, any>>;
export { ${name} };
`;
//# sourceMappingURL=generate.js.map