UNPKG

svgtofont

Version:

Converts SVG to TTF/EOT/WOFF/WOFF2/SVG format fonts.

251 lines (230 loc) 9.36 kB
import fs from 'fs-extra'; import path from 'path'; import { optimize } from 'svgo'; import { filterSvgFiles, toPascalCase } from './utils.js'; import { type SvgToFontOptions } from './'; /** * Generate Icon SVG Path Source * <font-name>.json */ export async function generateIconsSource(options: SvgToFontOptions = {}){ 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: string[], options: SvgToFontOptions = {}) { 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: string[] = (pathStrings.data.match(/ d="[^"]+"/g) || []).map(s => s.slice(3)); return `\n"${name}": [${str.join(',\n')}]`; }), ); } const reactSource = (name: string, size: string, fontName: string, source: string) => `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: string) => `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: SvgToFontOptions = {}) { 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: string[], options: SvgToFontOptions = {}) { 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: string[] = (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: string, defaultSize: number, iconMap: Map<string,string>) => `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: string, iconMap: Map<string, string>) => `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: SvgToFontOptions = {}, unicodeObject: Record<string, string>) { const ICONS_PATH = filterSvgFiles(options.src); outputReactNativeFile(ICONS_PATH, options, unicodeObject); } function outputReactNativeFile(files: string[], options: SvgToFontOptions = {}, unicodeObject: Record<string, string>) { 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<string, string>(); 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: SvgToFontOptions = {}) { 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: string[], options: SvgToFontOptions = {}) { 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: string[] = (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: string, size: string, fontName: string, source: string) => `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: string) => `import type { DefineComponent } from 'vue'; declare const ${name}: DefineComponent<Record<string, any>>; export { ${name} }; `;