new changes

This commit is contained in:
Niranjan
2026-04-07 05:05:28 +05:30
parent 7c070224bd
commit a18bba15f2
29975 changed files with 3247495 additions and 2761 deletions

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 小弟调调™
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,941 @@
<div markdown="1">
<sup>Using <a href="https://wangchujiang.com/#/app" target="_blank">my app</a> is also a way to <a href="https://wangchujiang.com/#/sponsor" target="_blank">support</a> me:</sup>
<br>
<a target="_blank" href="https://apps.apple.com/app/Deskmark/6755948110" title="Deskmark for macOS"><img alt="Deskmark" height="52" width="52" src="https://wangchujiang.com/appicon/deskmark.png"></a>
<a target="_blank" href="https://apps.apple.com/app/Keyzer/6500434773" title="Keyzer for macOS"><img alt="Keyzer" height="52" width="52" src="https://wangchujiang.com/appicon/keyzer.png"></a>
<a target="_blank" href="https://github.com/jaywcjlove/vidwall-hub" title="Vidwall Hub for macOS"><img alt="Vidwall Hub" height="52" width="52" src="https://wangchujiang.com/appicon/vidwall-hub.png"></a>
<a target="_blank" href="https://apps.apple.com/app/VidCrop/6752624705" title="VidCrop for macOS"><img alt="VidCrop" height="52" width="52" src="https://wangchujiang.com/appicon/vidcrop.png"></a>
<a target="_blank" href="https://apps.apple.com/app/Vidwall/6747587746" title="Vidwall for macOS"><img alt="Vidwall" height="52" width="52" src="https://wangchujiang.com/appicon/vidwall.png"></a>
<a target="_blank" href="https://wangchujiang.com/mousio-hint/" title="Mousio Hint for macOS"><img alt="Mousio Hint" height="52" width="52" src="https://wangchujiang.com/appicon/mousio-hint.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6746747327" title="Mousio for macOS"><img alt="Mousio" height="52" width="52" src="https://wangchujiang.com/appicon/mousio.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6745227444" title="Musicer for macOS"><img alt="Musicer" height="52" width="52" src="https://wangchujiang.com/appicon/musicer.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6743841447" title="Audioer for macOS"><img alt="Audioer" height="52" width="52" src="https://wangchujiang.com/appicon/audioer.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6744690194" title="FileSentinel for macOS"><img alt="FileSentinel" height="52" width="52" src="https://wangchujiang.com/appicon/file-sentinel.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6743495172" title="FocusCursor for macOS"><img alt="FocusCursor" height="52" width="52" src="https://wangchujiang.com/appicon/focus-cursor.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6742680573" title="Videoer for macOS"><img alt="Videoer" height="52" width="52" src="https://wangchujiang.com/appicon/videoer.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6740425504" title="KeyClicker for macOS"><img alt="KeyClicker" height="52" width="52" src="https://wangchujiang.com/appicon/key-clicker.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6739052447" title="DayBar for macOS"><img alt="DayBar" height="52" width="52" src="https://wangchujiang.com/appicon/daybar.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6739444407" title="Iconed for macOS"><img alt="Iconed" height="52" width="52" src="https://wangchujiang.com/appicon/iconed.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6737160756" title="Mousio for macOS"><img alt="Mousio" height="52" width="52" src="https://wangchujiang.com/appicon/rightmenu-master.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6723903021" title="Paste Quick for macOS"><img alt="Quick RSS" height="52" width="52" src="https://wangchujiang.com/appicon/paste-quick.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6670696072" title="Quick RSS for macOS/iOS"><img alt="Quick RSS" height="52" width="52" src="https://wangchujiang.com/appicon/quick-rss.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6670167443" title="Web Serve for macOS"><img alt="Web Serve" height="52" width="52" src="https://wangchujiang.com/appicon/web-serve.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6503953628" title="Copybook Generator for macOS/iOS"><img alt="Copybook Generator" height="52" width="52" src="https://wangchujiang.com/appicon/copybook-generator.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6471227008" title="DevTutor for macOS/iOS"><img alt="DevTutor for SwiftUI" height="52" width="52" src="https://wangchujiang.com/appicon/devtutor.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6479819388" title="RegexMate for macOS/iOS"><img alt="RegexMate" height="52" width="52" src="https://wangchujiang.com/appicon/regex-mate.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6479194014" title="Time Passage for macOS/iOS"><img alt="Time Passage" height="52" width="52" src="https://wangchujiang.com/appicon/time-passage.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6478772538" title="IconizeFolder for macOS"><img alt="Iconize Folder" height="52" width="52" src="https://wangchujiang.com/appicon/iconize-folder.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6478511402" title="Textsound Saver for macOS/iOS"><img alt="Textsound Saver" height="52" width="52" src="https://wangchujiang.com/appicon/textsound-saver.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6476924627" title="Create Custom Symbols for macOS"><img alt="Create Custom Symbols" height="52" width="52" src="https://wangchujiang.com/appicon/create-custom-symbols.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6476452351" title="DevHub for macOS"><img alt="DevHub" height="52" width="52" src="https://wangchujiang.com/appicon/devhub.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6476400184" title="Resume Revise for macOS"><img alt="Resume Revise" height="52" width="52" src="https://wangchujiang.com/appicon/resume-revise.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6472593276" title="Palette Genius for macOS"><img alt="Palette Genius" height="52" width="52" src="https://wangchujiang.com/appicon/palette-genius.png"></a>
<a target="_blank" href="https://apps.apple.com/app/6470879005" title="Symbol Scribe for macOS"><img alt="Symbol Scribe" height="52" width="52" src="https://wangchujiang.com/appicon/symbol-scribe.png"></a>
</div>
<hr>
[Free Font](https://github.com/jaywcjlove/free-font)
<p align="center">
<a href="https://github.com/jaywcjlove/svgtofont/">
<img src="https://github.com/jaywcjlove/svgtofont/assets/1680273/bd3aeeea-eac5-43ae-9bd0-c978d81c43a5" alt="SVG To Font" />
</a>
</p>
<p align="center">
<a href="https://jaywcjlove.github.io/#/sponsor" target="_blank">
<img src="https://img.shields.io/badge/Buy%20me%20a%20coffee-048754?logo=buymeacoffee" alt="Buy me a coffee">
</a>
<a href="https://github.com/jaywcjlove/svgtofont/actions">
<img src="https://github.com/jaywcjlove/svgtofont/workflows/Build/badge.svg" alt="Build & Deploy">
</a>
<a href="https://gitee.com/jaywcjlove/svgtofont">
<img src="https://jaywcjlove.github.io/sb/ico/gitee.svg" alt="Gitee Repo">
</a>
<a href="https://uiwjs.github.io/npm-unpkg/#/pkg/svgtofont/file/README.md">
<img src="https://img.shields.io/badge/Open%20in-unpkg-blue" alt="Open in unpkg">
</a>
<a href="https://www.npmjs.com/package/svgtofont">
<img src="https://img.shields.io/npm/dm/svgtofont.svg?style=flat" alt="NPM Download">
</a>
<a href="https://www.npmjs.com/package/svgtofont">
<img src="https://img.shields.io/npm/v/svgtofont.svg" alt="npm version">
</a>
</p>
Read a set of SVG icons and ouput a TTF/EOT/WOFF/WOFF2/SVG font, Generator of fonts from SVG icons.
[Install](#install) · [Usage](#using-with-nodejs) · [Command](#using-with-command) · [Font Usage](#font-usage) · [API](#api) · [options](#options) · [npm](http://npm.im/svgtofont) · [License](#license)
**Features:**
- Supported font formats: `WOFF2`, `WOFF`, `EOT`, `TTF` and `SVG`.
- Support SVG Symbol file.
- Support [`React`](https://github.com/facebook/react), [`ReactNative`](https://github.com/facebook/react-native), [`Vue`](https://github.com/vuejs/core) & [`TypeScript`](https://github.com/microsoft/TypeScript).
- Support [`Less`](https://github.com/less/less.js)/[`Sass`](https://github.com/sass/sass)/[`Stylus`](https://github.com/stylus/stylus).
- Allows to use custom templates (example `css`, `less` and etc).
- Automatically generate a preview site.
```bash
╭┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈╮
┆ Project ┆
┆ ╭┈┈┈┈┈┈┈┈┈┈┈╮ ┆
╭┈┈┈┈┈┈┈┈╮ ┆ ┆ svg ┆┈┈╮ ┆
┆iconfont┆┈┈╮ ┆ ╰┈┈┈┈┈┈┈┈┈┈┈╯ ┆ ┆
╰┈┈┈┈┈┈┈┈╯ ┆ ╭┈┈┈┈┈┈┈┈┈┈┈┈╮ ┆ ╭┈┈┈┈┈┈┈┈┈┈┈╮ ┆ ┆
├┈▶┆download svg┆┈┈▶┆ ┆┈svgtofont┈┆ ┆ ┆
╭┈┈┈┈┈┈┈┈╮ ┆ ╰┈┈┈┈┈┈┈┈┈┈┈┈╯ ┆╭┈┈┆create font┆◀┈╯ ┆
┆icomoon ┆┈┈╯ ┆┆ ╰┈┈┈┈┈┈┈┈┈┈┈╯ ┆
╰┈┈┈┈┈┈┈┈╯ ┆┆ ╭┈┈┈┈┈┈┈┈┈┈┈╮ ┆
┆╰┈▶┆ use font ┆ ┆
┆ ╰┈┈┈┈┈┈┈┈┈┈┈╯ ┆
╰┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈╯
```
```mermaid
graph LR;
A[iconfont]-->C[Download SVG];
B[icomoon]-->C;
D[icongo]-->C;
E[yesicon]-->C;
click A "https://www.iconfont.cn" "阿里巴巴矢量图标库" _blank
click B "https://icomoon.io" "Pixel Perfect Icon Solutions" _blank
click D "https://icongo.github.io" "Include popular icons in your React projects easily icons." _blank
click E "https://yesicon.app/" "216,162 High-Quality Vector Icons from Top Design Teams." _blank
C .-> ide1
subgraph ide1 [Project]
svg -->a2[svgtofont create font]
a2 .-> b3[use font]
end
```
**Icon Font Created By svgtofont**
- [file-icons](https://uiwjs.github.io/file-icons/) File icons in the file tree.
- [uiw-iconfont](https://github.com/uiwjs/icons) The premium icon font for [@uiwjs](https://github.com/uiwjs) Component Library. Support [`React`](https://github.com/facebook/react) & [`TypeScript`](https://github.com/microsoft/TypeScript).
- [Bootstrap Icons Font](https://github.com/uiwjs/bootstrap-icons) Official open source SVG icon library for Bootstrap.
- [test example](./test) For a simple test example, run `npm run test` in the root directory to see the results.
## Install
```bash
npm i svgtofont
```
> [!NOTE]
> This package `v5+` is [ESM only](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c): Node 18+ is needed to use it and it must be `import` instead of `require`.
> ```js
> import svgtofont from 'svgtofont';
> ```
#### Using With Command
```json
{
"scripts": {
"font": "svgtofont --sources ./svg --output ./font --fontName uiw-font"
},
"svgtofont": {
"css": {
"fontSize": "12px"
}
}
}
```
You can add configuration to package.json. [#48](https://github.com/jaywcjlove/svgtofont/issues/48)
Support for `.svgtofontrc` and [more](https://github.com/jaywcjlove/auto-config-loader/blob/add7ae012f5c3903296fbf0ef06e3631e379c2cc/core/README.md?plain=1#L106-L135) configuration files.
```js
{
"fontName": "svgtofont",
"css": true
}
```
```js
/**
* @type {import('svgtofont').SvgToFontOptions}
*/
export default {
fontName: "iconfont",
}
```
#### Using With Nodejs
> [!NOTE]
> This package `v5+` is now pure ESM. Please [read this](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c).
```js
import svgtofont from 'svgtofont';
import path from 'node:path';
svgtofont({
src: path.resolve(process.cwd(), 'icon'), // svg path, only searches one level, not recursive
dist: path.resolve(process.cwd(), 'fonts'), // output path
fontName: 'svgtofont', // font name
css: true, // Create CSS files.
}).then(() => {
console.log('done!');
});
```
Or
```js
import svgtofont from 'svgtofont';
import path from 'node:path';
svgtofont({
src: path.resolve(process.cwd(), "icon"), // svg path, only searches one level, not recursive
dist: path.resolve(process.cwd(), "fonts"), // output path
styleTemplates: path.resolve(rootPath, "styles"), // file templates path (optional)
fontName: "svgtofont", // font name
css: true, // Create CSS files.
startUnicode: 0xea01, // unicode start number
svgicons2svgfont: {
fontHeight: 1000,
normalize: true
},
// website = null, no demo html files
website: {
title: "svgtofont",
// Must be a .svg format image.
logo: path.resolve(process.cwd(), "svg", "git.svg"),
version: pkg.version,
meta: {
description: "Converts SVG fonts to TTF/EOT/WOFF/WOFF2/SVG format.",
keywords: "svgtofont,TTF,EOT,WOFF,WOFF2,SVG"
},
description: ``,
// Add a Github corner to your website
// Like: https://github.com/uiwjs/react-github-corners
corners: {
url: 'https://github.com/jaywcjlove/svgtofont',
width: 62, // default: 60
height: 62, // default: 60
bgColor: '#dc3545' // default: '#151513'
},
links: [
{
title: "GitHub",
url: "https://github.com/jaywcjlove/svgtofont"
},
{
title: "Feedback",
url: "https://github.com/jaywcjlove/svgtofont/issues"
},
{
title: "Font Class",
url: "index.html"
},
{
title: "Unicode",
url: "unicode.html"
}
],
footerInfo: `Licensed under MIT. (Yes it's free and <a href="https://github.com/jaywcjlove/svgtofont">open-sourced</a>`
}
}).then(() => {
console.log('done!');
});;
```
## API
```js
import { createSVG, createTTF, createEOT, createWOFF, createWOFF2, createSvgSymbol, copyTemplate, createHTML } from 'svgtofont/lib/utils';
const options = { ... };
async function createFont() {
const unicodeObject = await createSVG(options);
const ttf = await createTTF(options); // SVG Font => TTF
await createEOT(options, ttf); // TTF => EOT
await createWOFF(options, ttf); // TTF => WOFF
await createWOFF2(options, ttf); // TTF => WOFF2
await createSvgSymbol(options); // SVG Files => SVG Symbol
}
```
## options
> svgtofont(options)
### config
> Type: `config?: AutoConfOption<SvgToFontOptions>`
By default, settings are automatically loaded from `.svgtofontrc` and `package.json`. You can add configuration to `package.json`. [#48](https://github.com/jaywcjlove/svgtofont/issues/48)
Support for `.svgtofontrc` and [more](https://github.com/jaywcjlove/auto-config-loader/blob/add7ae012f5c3903296fbf0ef06e3631e379c2cc/core/README.md?plain=1#L106-L135) configuration files.
### log
> Type: `Boolean`
A value of `false` disables logging
### logger
> Type: `(msg) => void`
log callback function
### dist
> Type: `String`
> Default value: ~~`dist`~~ => `fonts`
The output directory.
### outSVGReact
> Type: `Boolean`
> Default value: `false`
Output `./dist/react/`, SVG generates `react` components.
```js
git/git.svg
// ↓↓↓↓↓↓↓↓↓↓
import React from 'react';
export const Git = props => (
<svg viewBox="0 0 20 20" {...props}><path d="M2.6 10.59L8.38 4.8l1.69 -." fillRule="evenodd" /></svg>
);
```
### outSVGReactNative
> Type: `Boolean`
> Default value: `false`
Output `./dist/reactNative/`, SVG generates `reactNative` components.
```js
import { Text } from 'react-native';
const icons = { "Git": "__GitUnicodeChar__", "Adobe": "__AdobeUnicodeChar__" };
export const RangeIconFont = props => {
const { name, ...rest } = props;
return (<Text style={{ fontFamily: 'svgtofont', fontSize: 16, color: '#000000', ...rest }}>
{icons[name]}
</Text>);
};
```
### outSVGVue
> Type: `Boolean`
> Default value: `false`
Output `./dist/vue/`, SVG generates `vue` components.
```js
git/git.svg
// ↓↓↓↓↓↓↓↓↓↓
import { defineComponent, h } from 'vue';
export const Git = defineComponent({
name: 'Git',
props: {
class: {
type: String,
default: ''
}
},
setup(props, { attrs }) {
return () => h(
'svg',
{
viewBox: '0 0 20 20',
width: undefined,
height: undefined,
class: `svgtofont ${props.class}`,
...attrs
},
[<path d="m2.6 10.59l5.78-5.79 1.69 1.7c-0.24 0.85 0.15 1.78 0.93 2.23v5.54c-0.6 0.34-1 0.99-1 1.73a2 2 0 0 0 2 2 2 2 0 0 0 2 -2c0-0.74-0.4-1.39-1-1.73v-4.86l2.07 2.09c-0.07 0.15-0.07 0.32-0.07 0.5a2 2 0 0 0 2 2 2 2 0 0 0 2 -2 2 2 0 0 0 -2 -2c-0.18 0-0.35 0-0.5 0.07l-2.57-2.57c0.26-0.93-0.22-1.95-1.15-2.34-0.43-0.16-0.88-0.2-1.28-0.09l-1.7-1.69 0.79-0.78c0.78-0.79 2.04-0.79 2.82 0l7.99 7.99c0.79 0.78 0.79 2.04 0 2.82l-7.99 7.99c-0.78 0.79-2.04 0.79-2.82 0l-7.99-7.99c-0.79-0.78-0.79-2.04 0-2.82z" fillRule="evenodd" />]
);
}
});
```
### outSVGPath
> Type: `Boolean`
> Default value: `false`
Output `./dist/svgtofont.json`, The content is as follows:
```js
{
"adobe": ["M14.868 3H23v19L14.868 3zM1 3h8.138L1 22V3zm.182 11.997H13.79l-1.551-3.82H8.447z...."],
"git": ["M2.6 10.59L8.38 4.8l1.69 1.7c-.24.85.15 1.78.93 2.23v5.54c-.6.34-1 .99-1..."],
"stylelint": ["M129.74 243.648c28-100.109 27.188-100.5.816c2.65..."]
}
```
Or you can generate the file separately:
```js
const { generateIconsSource } = require('svgtofont/src/generate');
const path = require('path');
async function generate () {
const outPath = await generateIconsSource({
src: path.resolve(process.cwd(), 'svg'),
dist: path.resolve(process.cwd(), 'dist'),
fontName: 'svgtofont',
});
}
generate();
```
### generateInfoData
> Type: `Boolean`
> Default value: `false`
Output `./dist/info.json`, The content is as follows:
```js
{
"adobe": {
"encodedCode": "\\ea01",
"prefix": "svgtofont",
"className": "svgtofont-adobe",
"unicode": "&#59905;"
},
...
}
```
### src
> Type: `String`
> Default value: `svg`
output path
### emptyDist
> Type: `String`
> Default value: `false`
Clear output directory contents
### fontName
> Type: `String`
> Default value: `iconfont`
The font family name you want.
### styleTemplates
> Type: `String`
> Default value: `undefined`
The path of the templates, see `src/styles` or `test/templates/styles` to get reference about
how to create a template, file names can have the extension .template, like a `filename.scss.template`
### startUnicode
> Type: `Number`
> Default value: `0xea01`
unicode start number
### getIconUnicode
Get Icon Unicode
```ts
getIconUnicode?: (name: string, unicode: string, startUnicode: number)
=> [string, number];
```
### useNameAsUnicode
> Type: `Boolean`
> Default value: `false`
should the name(file name) be used as unicode? this switch allows for the support of ligatures.
let's say you have an svg with a file name of `add` and you want to use ligatures for it. you would set up your processing as mentioned above and turn on this switch.
```js
{
...
useNameAsUnicode: true
}
```
while processing, instead of using a single sequential char for the unicode, it uses the file name. using the file name as the unicode allows the following code to work as expected.
```css
.icons {
font-family: 'your-font-icon-name' !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
```
```html
<i class="icons">add</i>
```
as you add more svgs and process them into your font you would just use the same pattern.
```html
<i class="icons">add</i>
<i class="icons">remove</i>
<i class="icons">edit</i>
```
### addLigatures
> Type: `Boolean`
> Default value: `false`
adds possibility to use name (file name) in addition to codepoints. adds support of ligatures.
let's say you have some svgs and you want to use codepoints but for some of them for example with a file name of `add` you want to use ligatures for it. this option only adds ligatures and still allows for using codepoints as usual. this is in contrary to useNameAsUnicode which basically removes support for codepoints in favour of ligatures.
```js
{
...
addLigatures: true
}
```
### useCSSVars
> Type: `Boolean`
> Default value: `false`
consoles whenever {{ cssString }} template outputs unicode characters or css vars
### classNamePrefix
> Type: `String`
> Default value: font name
Create font class name prefix, default value font name.
### css
> Type: `Boolean|CSSOptions`
> Default value: `false`
Create CSS/LESS files, default `true`.
```ts
type CSSOptions = {
/**
* Output the css file to the specified directory
*/
output?: string;
/**
* Which files are exported.
*/
include?: RegExp;
/**
* Setting font size.
*/
fontSize?: string | boolean;
/**
* Set the path in the css file
* https://github.com/jaywcjlove/svgtofont/issues/48#issuecomment-739547189
*/
cssPath?: string;
/**
* Set file name
* https://github.com/jaywcjlove/svgtofont/issues/48#issuecomment-739547189
*/
fileName?: string;
/**
* Ad hoc template variables.
*/
templateVars?: Record<string, any>;
/**
* When including CSS files in a CSS file,
* you can add a timestamp parameter or custom text to the file path to prevent browser caching issues and ensure style updates are applied. @default true
* @example `path/to/iconfont.css?t=1612345678`
*/
hasTimestamp?: boolean | string;
}
```
### svgicons2svgfont
This is the setting for [svgicons2svgfont](https://github.com/nfroidure/svgicons2svgfont/tree/dd713bea4f97afa59f7dba6a21ff7f22db565bcf#api)
#### svgicons2svgfont.fontName
> Type: `String`
> Default value: `'iconfont'`
The font family name you want.
#### svgicons2svgfont.fontId
> Type: `String`
> Default value: the options.fontName value
The font id you want.
#### svgicons2svgfont.fontStyle
> Type: `String`
> Default value: `''`
The font style you want.
#### svgicons2svgfont.fontWeight
> Type: `String`
> Default value: `''`
The font weight you want.
#### svgicons2svgfont.fixedWidth
> Type: `Boolean`
> Default value: `false`
Creates a monospace font of the width of the largest input icon.
#### svgicons2svgfont.centerHorizontally
> Type: `Boolean`
> Default value: `false`
Calculate the bounds of a glyph and center it horizontally.
#### svgicons2svgfont.normalize
> Type: `Boolean`
> Default value: `false`
Normalize icons by scaling them to the height of the highest icon.
#### svgicons2svgfont.fontHeight
> Type: `Number`
> Default value: `MAX(icons.height)`
The outputted font height (defaults to the height of the highest input icon).
#### svgicons2svgfont.round
> Type: `Number`
> Default value: `10e12`
Setup SVG path rounding.
#### svgicons2svgfont.descent
> Type: `Number`
> Default value: `0`
The font descent. It is useful to fix the font baseline yourself.
**Warning:** The descent is a positive value!
#### svgicons2svgfont.ascent
> Type: `Number`
> Default value: `fontHeight - descent`
The font ascent. Use this options only if you know what you're doing. A suitable
value for this is computed for you.
#### svgicons2svgfont.metadata
> Type: `String`
> Default value: `undefined`
The font [metadata](http://www.w3.org/TR/SVG/metadata.html). You can set any
character data in but it is the be suited place for a copyright mention.
#### svgicons2svgfont.log
> Type: `Function`
> Default value: `console.log`
Allows you to provide your own logging function. Set to `function(){}` to
disable logging.
### svgoOptions
> Type: `OptimizeOptions`
> Default value: `undefined`
Some options can be configured with `svgoOptions` though it. [svgo](https://github.com/svg/svgo#configuration)
### svg2ttf
This is the setting for [svg2ttf](https://github.com/fontello/svg2ttf/tree/c33a126920f46b030e8ce960cc7a0e38a6946bbc#svg2ttfsvgfontstring-options---buf)
#### svg2ttf.copyright
> Type: `String`
copyright string
#### svg2ttf.ts
> Type: `String`
Unix timestamp (in seconds) to override creation time
#### svg2ttf.version
> Type: `Number`
font version string, can be Version `x.y` or `x.y`.
### website
Define preview web content. Example:
```js
{
...
// website = null, no demo html files
website: {
title: "svgtofont",
logo: path.resolve(process.cwd(), "svg", "git.svg"),
version: pkg.version,
meta: {
description: "Converts SVG fonts to TTF/EOT/WOFF/WOFF2/SVG format.",
keywords: "svgtofont,TTF,EOT,WOFF,WOFF2,SVG",
favicon: "./favicon.png"
},
// Add a Github corner to your website
// Like: https://github.com/uiwjs/react-github-corners
corners: {
url: 'https://github.com/jaywcjlove/svgtofont',
width: 62, // default: 60
height: 62, // default: 60
bgColor: '#dc3545' // default: '#151513'
},
links: [
{
title: "GitHub",
url: "https://github.com/jaywcjlove/svgtofont"
},
{
title: "Feedback",
url: "https://github.com/jaywcjlove/svgtofont/issues"
},
{
title: "Font Class",
url: "index.html"
},
{
title: "Unicode",
url: "unicode.html"
}
]
}
}
```
#### website.template
> Type: `String`
> Default value: [index.njk](src/website/index.njk)
Custom template can customize parameters. You can define your own template based on the [default template](src/website/index.njk).
```js
{
website: {
template: path.join(process.cwd(), "my-template.njk")
}
}
```
#### website.index
> Type: `String`
> Default value: `font-class`, Enum{`font-class`, `unicode`, `symbol`}
Set default home page.
## Font Usage
Suppose the font name is defined as `svgtofont`, The default home page is `unicode`, Will generate:
```bash
font-class.html
index.html
svgtofont.css
svgtofont.eot
svgtofont.json
svgtofont.less
svgtofont.module.less
svgtofont.scss
svgtofont.styl
svgtofont.svg
svgtofont.symbol.svg
svgtofont.ttf
svgtofont.woff
svgtofont.woff2
symbol.html
```
Preview demo `font-class.html`, `symbol.html` and `index.html`. Automatically generated style `svgtofont.css` and `svgtofont.less`.
### symbol svg
```xml
<svg class="icon" aria-hidden="true">
<use xlink:href="svgtofont.symbol.svg#svgtofont-git"></use>
</svg>
```
### Unicode
```html
<style>
.iconfont {
font-family: "svgtofont-iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-webkit-text-stroke-width: 0.2px;
-moz-osx-font-smoothing: grayscale;
}
</style>
<span class="iconfont">&#59907;</span>
```
### Class Name
Support for `.less` and `.css` styles references.
```html
<link rel="stylesheet" type="text/css" href="node_modules/fonts/svgtofont.css">
<i class="svgtofont-apple"></i>
```
### Using With React
Icons are used as components. `v3.16.7+` support.
```jsx
import { Adobe, Alipay } from '@uiw/icons';
<Adobe style={{ fill: 'red' }} />
<Alipay height="36" />
```
#### In the project created by [create-react-app](https://github.com/facebook/create-react-app)
```jsx
import logo from './logo.svg';
<img src={logo} />
```
```jsx
import { ReactComponent as ComLogo } from './logo.svg';
<ComLogo />
```
#### In the project created by [webpack](https://github.com/webpack/webpack)
```bash
yarn add babel-plugin-named-asset-import
yarn add @svgr/webpack
```
```js
// webpack.config.js
[
require.resolve('babel-plugin-named-asset-import'),
{
loaderMap: {
svg: {
ReactComponent: '@svgr/webpack?-svgo,+ref![path]',
},
},
},
],
```
```jsx
import { ReactComponent as ComLogo } from './logo.svg';
<ComLogo />
```
### Using With ReactNative
A unique component named after the font name is generated.
Props are TextProps and are used as inline style.
In addition, the iconName prop is mandatory and refers to svg names written in camelCase
```jsx
SvgToFont.jsx
// ↓↓↓↓↓↓↓↓↓↓
import { SvgToFont } from './SvgToFont';
<SvgToFont fontSize={32} color="#fefefe" iconName={"git"} />
```
```ts
SvgToFont.d.ts
// ↓↓↓↓↓↓↓↓↓↓
import { TextStyle } from 'react-native';
export type SvgToFontIconNames = 'git'| 'adobe'| 'demo' | 'left' | 'styleInline'
export interface SvgToFontProps extends Omit<TextStyle, 'fontFamily' | 'fontStyle' | 'fontWeight'> {
iconName: SvgToFontIconNames
}
export declare const SvgToFont: (props: SvgToFontProps) => JSX.Element;
```
### Using with Vue
Icons are used as components. `v3+` support.
```vue
<script setup lang="ts">
import { Adobe, Alipay } from '@uiw/icons';
</script>
<template>
<Abobe :style="{fill: red}" />
<Alipay :height="36" />
</template>
```
## Contributors
As always, thanks to our amazing contributors!
<a href="https://github.com/jaywcjlove/svgtofont/graphs/contributors">
<img src="https://jaywcjlove.github.io/svgtofont/CONTRIBUTORS.svg" />
</a>
Made with [contributors](https://github.com/jaywcjlove/github-action-contributors).
## License
Licensed under the [MIT License](https://opensource.org/licenses/MIT).

View File

@@ -0,0 +1,2 @@
#!/usr/bin/env node
export {};

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env node
import FS from 'fs-extra';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import path from 'path';
import svgtofont from './index.js';
import { log } from './log.js';
const argv = yargs(hideBin(process.argv))
.alias('s', 'sources')
.describe('s', 'The root from which all sources are relative.')
.alias('o', 'output')
.describe('o', 'Output directory.')
.alias('f', 'fontName')
.describe('f', 'Font Name.')
.demandOption(['output', 'sources'])
.help('h')
.alias('h', 'help')
.epilog('copyright 2019')
.argv;
const sourcesPath = path.resolve(process.cwd(), argv.sources);
const outputPath = path.resolve(process.cwd(), argv.output);
if (!FS.pathExistsSync(sourcesPath)) {
log.error('The directory does not exist!', sourcesPath);
process.exit();
}
if (!FS.pathExistsSync(outputPath)) {
FS.mkdirpSync(outputPath);
}
svgtofont({
src: sourcesPath, // svg path
dist: outputPath, // output path
// emptyDist: true, // Clear output directory contents
fontName: (argv.fontName) || "svgfont", // font name
css: true, // Create CSS files.
outSVGReact: true,
outSVGReactNative: false,
outSVGVue: true,
outSVGPath: true,
svgicons2svgfont: {
fontHeight: 1000,
normalize: true,
},
})
.then(() => {
log.log('done!');
}).catch((err) => {
log.log('SvgToFont:ERR:', err);
});
//# sourceMappingURL=cli.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,MAAM,UAAU,CAAC;AAE1B,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAQ/B,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;KACtC,KAAK,CAAC,GAAG,EAAE,SAAS,CAAC;KACrB,QAAQ,CAAC,GAAG,EAAE,+CAA+C,CAAC;KAC9D,KAAK,CAAC,GAAG,EAAE,QAAQ,CAAC;KACpB,QAAQ,CAAC,GAAG,EAAE,mBAAmB,CAAC;KAClC,KAAK,CAAC,GAAG,EAAE,UAAU,CAAC;KACtB,QAAQ,CAAC,GAAG,EAAE,YAAY,CAAC;KAC3B,YAAY,CAAC,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;KACnC,IAAI,CAAC,GAAG,CAAC;KACT,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC;KAClB,MAAM,CAAC,gBAAgB,CAAC;KACxB,IAAkB,CAAC;AAEtB,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;AAC9D,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;AAE5D,IAAI,CAAC,EAAE,CAAC,cAAc,CAAC,WAAW,CAAC,EAAE,CAAC;IACpC,GAAG,CAAC,KAAK,CAAC,+BAA+B,EAAE,WAAW,CAAC,CAAC;IACxD,OAAO,CAAC,IAAI,EAAE,CAAC;AACjB,CAAC;AAED,IAAI,CAAC,EAAE,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,CAAC;IACnC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;AAC5B,CAAC;AAED,SAAS,CAAC;IACR,GAAG,EAAE,WAAW,EAAE,WAAW;IAC7B,IAAI,EAAE,UAAU,EAAE,cAAc;IAChC,sDAAsD;IACtD,QAAQ,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,SAAS,EAAE,YAAY;IACpD,GAAG,EAAE,IAAI,EAAE,oBAAoB;IAC/B,WAAW,EAAE,IAAI;IACjB,iBAAiB,EAAE,KAAK;IACxB,SAAS,EAAE,IAAI;IACf,UAAU,EAAE,IAAI;IAChB,gBAAgB,EAAE;QAChB,UAAU,EAAE,IAAI;QAChB,SAAS,EAAE,IAAI;KAChB;CACF,CAAC;KACD,IAAI,CAAC,GAAG,EAAE;IACT,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;AACnB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACf,GAAG,CAAC,GAAG,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAC;AACjC,CAAC,CAAC,CAAC"}

View File

@@ -0,0 +1,21 @@
import { type SvgToFontOptions } from './';
/**
* Generate Icon SVG Path Source
* <font-name>.json
*/
export declare function generateIconsSource(options?: SvgToFontOptions): Promise<string>;
/**
* Generate React Icon
* <font-name>.json
*/
export declare function generateReactIcons(options?: SvgToFontOptions): Promise<string>;
/**
* Generate ReactNative Icon
* <font-name>.json
*/
export declare function generateReactNativeIcons(options: SvgToFontOptions, unicodeObject: Record<string, string>): void;
/**
* Generate Vue Icon
* <font-name>.json
*/
export declare function generateVueIcons(options?: SvgToFontOptions): Promise<string>;

View File

@@ -0,0 +1,230 @@
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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,209 @@
import { type SVGIcons2SVGFontStreamOptions } from 'svgicons2svgfont';
import { type AutoConfOption } from 'auto-config-loader';
import type { FontOptions } from 'svg2ttf';
import type { Config } from 'svgo';
import { type CSSOptions, type TypescriptOptions } from './utils.js';
export type SvgToFontOptions = {
/** Support for .svgtofontrc and more configuration files. */
config?: AutoConfOption<SvgToFontOptions>;
/** A value of `false` disables logging */
log?: boolean;
/** log callback function */
logger?: (message: string) => void;
/**
* The output directory.
* @default fonts
* @example
* ```
* path.join(process.cwd(), 'fonts')
* ```
*/
dist?: string;
/**
* svg path
* @default svg
* @example
* ```
* path.join(process.cwd(), 'svg')
* ```
*/
src?: string;
/**
* The font family name you want.
* @default iconfont
*/
fontName?: string;
/**
* Create CSS/LESS/Scss/Styl files, default `true`.
*/
css?: boolean | CSSOptions;
/**
* Output `./dist/react/`, SVG generates `react` components.
*/
outSVGReact?: boolean;
/**
* Output `./dist/reactNative/`, SVG generates `reactNative` component.
*/
outSVGReactNative?: boolean;
/**
* Output `./dist/vue/`, SVG generates `vue` components.
*/
outSVGVue?: boolean;
/**
* Output `./dist/svgtofont.json`, The content is as follows:
* @example
* ```js
* {
* "adobe": ["M14.868 3H23v19L14.868 3zM1 3h8.8.447z...."],
* "git": ["M2.6 10.59L8.38 4.8l1.69 1.7c-.24c-.6.34-1 .99-1..."],
* "stylelint": ["M129.74 243.648c28-100.5.816c2.65..."]
* }
* ```
*/
outSVGPath?: boolean;
/**
* Output `./dist/info.json`, The content is as follows:
* @example
* ```js
* {
* "adobe": {
* "encodedCode": "\\ea01",
* "prefix": "svgtofont",
* "className": "svgtofont-adobe",
* "unicode": "&#59905;"
* },
* .....
* }
* ```
*/
generateInfoData?: boolean;
/**
* This is the setting for [svgicons2svgfont](https://github.com/nfroidure/svgicons2svgfont/tree/dd713bea4f97afa59f7dba6a21ff7f22db565bcf#api)
*/
svgicons2svgfont?: Partial<SVGIcons2SVGFontStreamOptions>;
/** Some options can be configured with svgoOptions though it. [svgo](https://github.com/svg/svgo#configuration) */
svgoOptions?: Config;
/**
* Create font class name prefix, default value font name.
* @default fontName
*/
classNamePrefix?: SvgToFontOptions['fontName'];
/**
* Symbol Name Delimiter, @default `-`
*/
symbolNameDelimiter?: string;
/**
* Directory of custom templates.
*/
styleTemplates?: string;
/**
* unicode start number
* @default 10000
*/
startUnicode?: number;
/** Get Icon Unicode */
getIconUnicode?: (name: string, unicode: string, startUnicode: number) => [string, number];
/**
* should the name(file name) be used as unicode? this switch allows for the support of ligatures.
* @default false
*/
useNameAsUnicode?: boolean;
/**
* adds possibility to use name (file name) in addition to codepoints. adds support of ligatures.
* @default false
*/
addLigatures?: boolean;
/**
* consoles whenever {{ cssString }} template outputs unicode characters or css vars
* @default false
*/
useCSSVars?: boolean;
/**
* Clear output directory contents
* @default false
*/
emptyDist?: boolean;
/**
* This is the setting for [svg2ttf](https://github.com/fontello/svg2ttf/tree/c33a126920f46b030e8ce960cc7a0e38a6946bbc#svg2ttfsvgfontstring-options---buf)
*/
svg2ttf?: FontOptions;
/**
* You can configure which font files to exclude from generation. By default, all font files will be generated.
* https://github.com/jaywcjlove/svgtofont/issues/238
*/
excludeFormat?: Array<"eot" | "woff" | "woff2" | "ttf" | "svg" | "symbol.svg">;
website?: {
/**
* Add a Github corner to your website
* @like https://github.com/uiwjs/react-github-corners
*/
corners?: {
/**
* @example `https://github.com/jaywcjlove/svgtofont`
*/
url?: string;
/**
* @default 60
*/
width?: number;
/**
* @default 60
*/
height?: number;
/**
* @default #151513
*/
bgColor?: '#dc3545';
};
/**
* @default unicode
*/
index?: 'font-class' | 'unicode' | 'symbol';
/**
* website title
*/
title?: string;
/**
* @example
* ```js
* path.resolve(rootPath, "favicon.png")
* ```
*/
favicon?: string;
/**
* Must be a .svg format image.
* @example
* ```js
* path.resolve(rootPath, "svg", "git.svg")
* ```
*/
logo?: string;
version?: string;
meta?: {
description?: string;
keywords?: string;
};
description?: string;
template?: string;
footerInfo?: string;
links: Array<{
title: string;
url: string;
}>;
};
/**
* Create typescript file with declarations for icon classnames
* @default false
*/
typescript?: boolean | TypescriptOptions;
};
export type IconInfo = {
prefix: string;
symbol: string;
unicode: string;
className: string;
encodedCode: string | number;
};
export type InfoData = Record<string, Partial<IconInfo>>;
declare const _default: (options?: SvgToFontOptions) => Promise<InfoData>;
export default _default;

View File

@@ -0,0 +1,235 @@
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs-extra';
import image2uri from 'image2uri';
import color from 'colors-cli';
import { autoConf, merge } from 'auto-config-loader';
import { log } from './log.js';
import { generateIconsSource, generateReactIcons, generateReactNativeIcons, generateVueIcons } from './generate.js';
import { createSVG, createTTF, createEOT, createWOFF, createWOFF2, createSvgSymbol, copyTemplate, createHTML, createTypescript } from './utils.js';
import { generateFontFaceCSS, getDefaultOptions } from './utils.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const loadConfig = async (options) => {
const defaultOptions = getDefaultOptions(options);
const data = await autoConf('svgtofont', {
mustExist: true,
default: defaultOptions,
...options.config,
});
return merge(defaultOptions, data);
};
const handlePkgConfig = (options) => {
const pkgPath = path.join(process.cwd(), 'package.json');
if (fs.pathExistsSync(pkgPath)) {
const pkg = fs.readJSONSync(pkgPath);
if (pkg.svgtofont) {
const cssOptions = options.css;
options = merge(options, pkg.svgtofont);
if (pkg.svgtofont.css && cssOptions && typeof cssOptions === 'object') {
options.css = merge(cssOptions, pkg.svgtofont.css);
}
}
if (options.website && pkg.version) {
options.website.version = options.website.version ?? pkg.version;
}
}
return options;
};
export default async (options = {}) => {
options = await loadConfig(options);
options = handlePkgConfig(options);
if (options.log === undefined)
options.log = true;
log.disabled = !options.log;
if (options.logger && typeof options.logger === 'function')
log.logger = options.logger;
options.svgicons2svgfont.fontName = options.fontName;
options.classNamePrefix = options.classNamePrefix || options.fontName;
const excludeFormat = options.excludeFormat || [];
const fontSizeOpt = typeof options.css !== 'boolean' && options.css.fontSize;
const fontSize = typeof fontSizeOpt === 'boolean' ? (fontSizeOpt === true ? 'font-size: 16px;' : '') : `font-size: ${fontSizeOpt};`;
// If you generate a font you need to generate a style.
if (options.website && !options.css)
options.css = true;
const infoDataPath = path.resolve(options.dist, 'info.json');
try {
if (options.emptyDist) {
await fs.emptyDir(options.dist);
}
// Ensures that the directory exists.
await fs.ensureDir(options.dist);
const unicodeObject = await createSVG(options);
/** @deprecated */
let cssToVars = [];
let cssString = [];
let cssRootVars = [];
let cssIconHtml = [];
let unicodeHtml = [];
let symbolHtml = [];
const prefix = options.classNamePrefix || options.fontName;
const infoData = {};
Object.keys(unicodeObject).forEach((name, index, self) => {
if (!infoData[name])
infoData[name] = {};
const _code = unicodeObject[name];
let symbolName = options.classNamePrefix + options.symbolNameDelimiter + name;
let iconPart = symbolName + '">';
let encodedCodes = _code.codePointAt(0);
if (options.useNameAsUnicode) {
symbolName = name;
iconPart = prefix + '">' + name;
encodedCodes = [..._code].map(x => x.codePointAt(0)).join(';&amp;#');
}
else {
cssToVars.push(`$${symbolName}: "\\${encodedCodes.toString(16)}";\n`);
if (options.useCSSVars) {
if (index === 0)
cssRootVars.push(`:root {\n`);
cssRootVars.push(`--${symbolName}: "\\${encodedCodes.toString(16)}";\n`);
cssString.push(`.${symbolName}::before { content: var(--${symbolName}); }\n`);
if (index === self.length - 1)
cssRootVars.push(`}\n`);
}
else {
cssString.push(`.${symbolName}::before { content: "\\${encodedCodes.toString(16)}"; }\n`);
}
}
infoData[name].encodedCode = `\\${encodedCodes.toString(16)}`;
infoData[name].prefix = prefix;
infoData[name].className = symbolName;
infoData[name].unicode = `&#${encodedCodes};`;
cssIconHtml.push(`<li class="class-icon"><i class="${iconPart}</i><p class="name">${name}</p></li>`);
unicodeHtml.push(`<li class="unicode-icon"><span class="iconfont">${_code}</span><h4>${name}</h4><span class="unicode">&amp;#${encodedCodes};</span></li>`);
symbolHtml.push(`
<li class="symbol">
<svg class="icon" aria-hidden="true">
<use xlink:href="${options.fontName}.symbol.svg#${symbolName}"></use>
</svg>
<h4>${symbolName}</h4>
</li>
`);
});
if (options.useCSSVars) {
cssString = [...cssRootVars, ...cssString];
}
if (options.generateInfoData) {
await fs.writeJSON(infoDataPath, infoData, { spaces: 2 });
log.log(`${color.green('SUCCESS')} Created ${infoDataPath} `);
}
const ttf = await createTTF(options);
if (!excludeFormat.includes('eot'))
await createEOT(options, ttf);
if (!excludeFormat.includes('woff'))
await createWOFF(options, ttf);
if (!excludeFormat.includes('woff2'))
await createWOFF2(options, ttf);
if (!excludeFormat.includes('symbol.svg'))
await createSvgSymbol(options);
const ttfPath = path.join(options.dist, options.fontName + ".ttf");
if (excludeFormat.includes('ttf')) {
fs.removeSync(ttfPath);
}
const svgPath = path.join(options.dist, options.fontName + ".svg");
if (excludeFormat.includes('svg')) {
fs.removeSync(svgPath);
}
if (options.css) {
const styleTemplatePath = options.styleTemplates || path.resolve(__dirname, 'styles');
const outDir = typeof options.css === 'object' ? options.css.output || options.dist : options.dist;
const hasTimestamp = typeof options.css === 'object' ? options.css.hasTimestamp : true;
const cssOptions = typeof options.css === 'object' ? options.css : {};
const fontFamilyString = generateFontFaceCSS(options.fontName, cssOptions.cssPath || "", Date.now(), excludeFormat, hasTimestamp);
await copyTemplate(styleTemplatePath, outDir, {
fontname: options.fontName,
cssString: cssString.join(''),
cssToVars: cssToVars.join(''),
infoData,
fontSize: fontSize,
timestamp: new Date().getTime(),
prefix,
fontFamily: fontFamilyString,
nameAsUnicode: options.useNameAsUnicode,
_opts: cssOptions
});
}
if (options.typescript) {
await createTypescript({ ...options, typescript: options.typescript });
}
if (options.website) {
const pageNames = ['font-class', 'unicode', 'symbol'];
const htmlPaths = {};
// setting default home page.
const indexName = pageNames.includes(options.website.index) ? options.website.index : 'font-class';
pageNames.forEach(name => {
const fileName = name === indexName ? 'index.html' : `${name}.html`;
htmlPaths[name] = path.join(options.dist, fileName);
});
const fontClassPath = htmlPaths['font-class'];
const unicodePath = htmlPaths['unicode'];
const symbolPath = htmlPaths['symbol'];
// default template
options.website.template = options.website.template || path.join(__dirname, 'website', 'index.njk');
// template data
const tempData = {
meta: null,
links: null,
corners: null,
description: null,
footerInfo: null,
...options.website,
fontname: options.fontName,
classNamePrefix: options.classNamePrefix,
_type: 'font-class',
_link: `${(options.css && typeof options.css !== 'boolean' && options.css.fileName) || options.fontName}.css`,
_IconHtml: cssIconHtml.join(''),
_title: options.website.title || options.fontName
};
// website logo
if (options.website.logo && fs.pathExistsSync(options.website.logo) && path.extname(options.website.logo) === '.svg') {
tempData.logo = fs.readFileSync(options.website.logo).toString();
}
// website favicon
if (options.website.favicon && fs.pathExistsSync(options.website.favicon)) {
tempData.favicon = await image2uri(options.website.favicon);
}
else {
tempData.favicon = '';
}
const classHtmlStr = await createHTML(options.website.template, tempData);
fs.outputFileSync(fontClassPath, classHtmlStr);
log.log(`${color.green('SUCCESS')} Created ${fontClassPath} `);
tempData._IconHtml = unicodeHtml.join('');
tempData._type = 'unicode';
const unicodeHtmlStr = await createHTML(options.website.template, tempData);
fs.outputFileSync(unicodePath, unicodeHtmlStr);
log.log(`${color.green('SUCCESS')} Created ${unicodePath} `);
tempData._IconHtml = symbolHtml.join('');
tempData._type = 'symbol';
const symbolHtmlStr = await createHTML(options.website.template, tempData);
fs.outputFileSync(symbolPath, symbolHtmlStr);
log.log(`${color.green('SUCCESS')} Created ${symbolPath} `);
}
if (options.outSVGPath) {
const outPath = await generateIconsSource(options);
log.log(`${color.green('SUCCESS')} Created ${outPath} `);
}
if (options.outSVGReact) {
const outPath = await generateReactIcons(options);
log.log(`${color.green('SUCCESS')} Created React Components. `);
}
if (options.outSVGReactNative) {
generateReactNativeIcons(options, unicodeObject);
log.log(`${color.green('SUCCESS')} Created React Native Components. `);
}
if (options.outSVGVue) {
const outPath = await generateVueIcons(options);
log.log(`${color.green('SUCCESS')} Created Vue Components. `);
}
return infoData;
}
catch (error) {
log.log('SvgToFont:CLI:ERR:', error);
}
};
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,10 @@
export declare class Log {
_disabled?: boolean;
constructor(disabled?: boolean);
get disabled(): boolean;
set disabled(val: boolean);
log: (message?: any, ...optionalParams: any[]) => void | (() => void);
error: (message?: any, ...optionalParams: any[]) => void | (() => void);
logger: (message?: string) => void;
}
export declare const log: Log;

View File

@@ -0,0 +1,29 @@
export class Log {
_disabled;
constructor(disabled) {
this.disabled = disabled || false;
}
get disabled() {
return this._disabled;
}
set disabled(val) {
this._disabled = val;
}
log = (message, ...optionalParams) => {
if (this.logger)
this.logger(message);
if (this.disabled)
return () => { };
return console.log(message, ...optionalParams);
};
error = (message, ...optionalParams) => {
if (this.logger)
this.logger(message);
if (this.disabled)
return () => { };
return console.error(message, ...optionalParams);
};
logger = (message) => { };
}
export const log = new Log();
//# sourceMappingURL=log.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,GAAG;IACd,SAAS,CAAU;IACnB,YAAY,QAAkB;QAC5B,IAAI,CAAC,QAAQ,GAAG,QAAQ,IAAI,KAAK,CAAA;IACnC,CAAC;IACD,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;IACD,IAAI,QAAQ,CAAC,GAAY;QACvB,IAAI,CAAC,SAAS,GAAG,GAAG,CAAC;IACvB,CAAC;IACD,GAAG,GAAG,CAAC,OAAa,EAAE,GAAG,cAAqB,EAAE,EAAE;QAChD,IAAI,IAAI,CAAC,MAAM;YAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACtC,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO,GAAG,EAAE,GAAE,CAAC,CAAA;QAClC,OAAO,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,cAAc,CAAC,CAAA;IAChD,CAAC,CAAA;IACD,KAAK,GAAG,CAAC,OAAa,EAAE,GAAG,cAAqB,EAAE,EAAE;QAClD,IAAI,IAAI,CAAC,MAAM;YAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACtC,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO,GAAG,EAAE,GAAE,CAAC,CAAA;QAClC,OAAO,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,cAAc,CAAC,CAAA;IAClD,CAAC,CAAA;IACD,MAAM,GAAG,CAAC,OAAgB,EAAE,EAAE,GAAE,CAAC,CAAA;CAClC;AAED,MAAM,CAAC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC"}

View File

@@ -0,0 +1,14 @@
@font-face {
{{fontFamily}}
}
{% if nameAsUnicode %}.{{prefix}}{% else %}[class^="{{prefix}}-"], [class*=" {{prefix}}-"]{% endif %} {
font-family: '{{fontname}}' !important;{{fontSize}}
font-style:normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
{% if not nameAsUnicode %}
{{cssString}}
{% endif %}

View File

@@ -0,0 +1,14 @@
@font-face {
{{fontFamily}}
}
{% if nameAsUnicode %}.{{prefix}}{% else %}[class^="{{prefix}}-"], [class*=" {{prefix}}-"]{% endif %} {
font-family: '{{fontname}}' !important;{{fontSize}}
font-style:normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
{% if not nameAsUnicode %}
{{cssString}}
{% endif %}

View File

@@ -0,0 +1,16 @@
@font-face {
{{fontFamily}}
}
{% if nameAsUnicode %}.{{prefix}}{% else %}[class^="{{prefix}}-"], [class*=" {{prefix}}-"]{% endif %} {
font-family: '{{fontname}}' !important;{{fontSize}}
font-style:normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
{% if not nameAsUnicode %}
:global {
{{cssString}}
}
{% endif %}

View File

@@ -0,0 +1,17 @@
@font-face {
{{fontFamily}}
}
{% if nameAsUnicode %}.{{prefix}}{% else %}[class^="{{prefix}}-"], [class*=" {{prefix}}-"]{% endif %} {
font-family: '{{fontname}}' !important;{{fontSize}}
font-style:normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
{% if not nameAsUnicode %}
{{ cssString }}
{% for name, value in infoData %}
${{prefix}}-{{ name }}: '{{ value.encodedCode }}';
{%- endfor %}
{% endif %}

View File

@@ -0,0 +1,17 @@
@font-face {
{{fontFamily}}
}
{% if nameAsUnicode %}.{{prefix}}{% else %}[class^="{{prefix}}-"], [class*=" {{prefix}}-"]{% endif %} {
font-family: '{{fontname}}' !important;{{fontSize}}
font-style:normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
{% if not nameAsUnicode %}
{{ cssString }}
{% for name, value in infoData %}
${{prefix}}-{{ name }} = '{{ value.encodedCode }}'
{%- endfor %}
{% endif %}

View File

@@ -0,0 +1,98 @@
import { type SvgToFontOptions } from './';
/**
* SVG to SVG font
*/
export declare function createSVG(options?: SvgToFontOptions): Promise<Record<string, string>>;
/**
* Converts a string to pascal case.
*
* @example
*
* ```js
* toPascalCase('some_database_field_name'); // 'SomeDatabaseFieldName'
* toPascalCase('Some label that needs to be pascalized');
* // 'SomeLabelThatNeedsToBePascalized'
* toPascalCase('some-javascript-property'); // 'SomeJavascriptProperty'
* toPascalCase('some-mixed_string with spaces_underscores-and-hyphens');
* // 'SomeMixedStringWithSpacesUnderscoresAndHyphens'
* ```
*/
export declare const toPascalCase: (str: string) => string;
export declare function filterSvgFiles(svgFolderPath: string): string[];
export declare function snakeToUppercase(str: string): string;
export type TypescriptOptions = {
extension?: 'd.ts' | 'ts' | 'tsx';
enumName?: string;
};
/**
* Create typescript declarations for icon classnames
*/
export declare function createTypescript(options: Omit<SvgToFontOptions, 'typescript'> & {
typescript: TypescriptOptions | true;
}): Promise<void>;
/**
* SVG font to TTF
*/
export declare function createTTF(options?: SvgToFontOptions): Promise<Buffer>;
/**
* TTF font to EOT
*/
export declare function createEOT(options: SvgToFontOptions, ttf: Buffer): Promise<unknown>;
/**
* TTF font to WOFF
*/
export declare function createWOFF(options: SvgToFontOptions, ttf: Buffer): Promise<unknown>;
/**
* TTF font to WOFF2
*/
export declare function createWOFF2(options: SvgToFontOptions, ttf: Buffer): Promise<unknown>;
/**
* Create SVG Symbol
*/
export declare function createSvgSymbol(options?: SvgToFontOptions): Promise<unknown>;
export type CSSOptions = {
/**
* Output the css file to the specified directory
*/
output?: string;
/**
* Which files are exported.
*/
include?: RegExp;
/**
* Setting font size.
*/
fontSize?: string | boolean;
/**
* Set the path in the css file
* https://github.com/jaywcjlove/svgtofont/issues/48#issuecomment-739547189
*/
cssPath?: string;
/**
* Set file name
* https://github.com/jaywcjlove/svgtofont/issues/48#issuecomment-739547189
*/
fileName?: string;
/**
* Ad hoc template variables.
*/
templateVars?: Record<string, any>;
/**
* When including CSS files in a CSS file,
* you can add a timestamp parameter or custom text to the file path to prevent browser caching issues and ensure style updates are applied. @default true
* @example `path/to/iconfont.css?t=1612345678`
*/
hasTimestamp?: boolean | string;
};
/**
* Copy template files
*/
export declare function copyTemplate(inDir: string, outDir: string, { _opts, ...vars }: Record<string, any> & {
_opts: CSSOptions;
}): Promise<void>;
/**
* Create HTML
*/
export declare function createHTML(templatePath: string, data: Record<string, any>): string;
export declare function generateFontFaceCSS(fontName: string, cssPath: string, timestamp: number, excludeFormat: string[], hasTimestamp?: boolean | string): string;
export declare const getDefaultOptions: (options: SvgToFontOptions) => SvgToFontOptions;

View File

@@ -0,0 +1,323 @@
import { SVGIcons2SVGFontStream } from 'svgicons2svgfont';
import fs from 'fs-extra';
import path from 'path';
import color from 'colors-cli';
import { load } from 'cheerio';
import svg2ttf from 'svg2ttf';
import ttf2eot from 'ttf2eot';
import ttf2woff from 'ttf2woff';
import ttf2woff2 from 'ttf2woff2';
import nunjucks from 'nunjucks';
import { merge } from 'auto-config-loader';
import { log } from './log.js';
let UnicodeObj = {};
/**
* Unicode Private Use Area start.
* https://en.wikipedia.org/wiki/Private_Use_Areas
*/
let startUnicode = 0xea01;
/**
* SVG to SVG font
*/
export function createSVG(options = {}) {
startUnicode = options.startUnicode;
UnicodeObj = {};
return new Promise(async (resolve, reject) => {
const fontStream = new SVGIcons2SVGFontStream({
...options.svgicons2svgfont
});
function writeFontStream(svgPath) {
// file name
let _name = path.basename(svgPath, ".svg");
const glyph = fs.createReadStream(svgPath);
const curUnicode = String.fromCodePoint(startUnicode);
const [_curUnicode, _startUnicode] = options.getIconUnicode
? (options.getIconUnicode(_name, curUnicode, startUnicode) || [curUnicode]) : [curUnicode];
if (_startUnicode)
startUnicode = _startUnicode;
const unicode = [_curUnicode];
if (curUnicode === _curUnicode && (!_startUnicode || startUnicode === _startUnicode))
startUnicode++;
UnicodeObj[_name] = unicode[0];
if (!!options.useNameAsUnicode) {
unicode[0] = _name;
UnicodeObj[_name] = _name;
}
if (!!options.addLigatures) {
unicode.push(_name);
}
glyph.metadata = { unicode, name: _name };
fontStream.write(glyph);
}
const DIST_PATH = path.join(options.dist, options.fontName + ".svg");
// Setting the font destination
fontStream.pipe(fs.createWriteStream(DIST_PATH))
.on("finish", () => {
log.log(`${color.green('SUCCESS')} ${color.blue_bt('SVG')} font successfully created!\n ╰┈▶ ${DIST_PATH}`);
resolve(UnicodeObj);
})
.on("error", (err) => {
if (err) {
reject(err);
}
});
filterSvgFiles(options.src).forEach((svg) => {
if (typeof svg !== 'string')
return false;
writeFontStream(svg);
});
// Do not forget to end the stream
fontStream.end();
});
}
/**
* Converts a string to pascal case.
*
* @example
*
* ```js
* toPascalCase('some_database_field_name'); // 'SomeDatabaseFieldName'
* toPascalCase('Some label that needs to be pascalized');
* // 'SomeLabelThatNeedsToBePascalized'
* toPascalCase('some-javascript-property'); // 'SomeJavascriptProperty'
* toPascalCase('some-mixed_string with spaces_underscores-and-hyphens');
* // 'SomeMixedStringWithSpacesUnderscoresAndHyphens'
* ```
*/
export const toPascalCase = (str) => str
.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
.map(x => x.charAt(0).toUpperCase() + x.slice(1).toLowerCase())
.join('');
/*
* Filter svg files
* @return {Array} svg files
*/
export function filterSvgFiles(svgFolderPath) {
let files = fs.readdirSync(svgFolderPath, 'utf-8');
let svgArr = [];
if (!files) {
throw new Error(`Error! Svg folder is empty.${svgFolderPath}`);
}
for (let i in files) {
if (typeof files[i] !== 'string' || path.extname(files[i]) !== '.svg')
continue;
if (!~svgArr.indexOf(files[i])) {
svgArr.push(path.join(svgFolderPath, files[i]));
}
}
return svgArr;
}
export function snakeToUppercase(str) {
return str.split(/[-_]/)
.map(partial => partial.charAt(0).toUpperCase() + partial.slice(1))
.join('');
}
/**
* Create typescript declarations for icon classnames
*/
export async function createTypescript(options) {
const tsOptions = options.typescript === true ? {} : options.typescript;
const uppercaseFontName = snakeToUppercase(options.fontName);
const { extension = 'd.ts', enumName = uppercaseFontName } = tsOptions;
const DIST_PATH = path.join(options.dist, `${options.fontName}.${extension}`);
const fileNames = filterSvgFiles(options.src).map(svgPath => path.basename(svgPath, path.extname(svgPath)));
await fs.writeFile(DIST_PATH, [
`export enum ${enumName} {`,
...fileNames.map(name => ` ${snakeToUppercase(name)} = "${options.classNamePrefix}-${name}",`),
'}',
`export type ${enumName}Classname = ${fileNames.map(name => `"${options.classNamePrefix}-${name}"`).join(' | ')}`,
`export type ${enumName}Icon = ${fileNames.map(name => `"${name}"`).join(' | ')}`,
`export const ${enumName}Prefix = "${options.classNamePrefix}-"`,
].join('\n'));
log.log(`${color.green('SUCCESS')} Created ${DIST_PATH}`);
}
/**
* SVG font to TTF
*/
export function createTTF(options = {}) {
return new Promise((resolve, reject) => {
options.svg2ttf = options.svg2ttf || {};
const DIST_PATH = path.join(options.dist, options.fontName + ".ttf");
let ttf = svg2ttf(fs.readFileSync(path.join(options.dist, options.fontName + ".svg"), "utf8"), options.svg2ttf);
const ttfBuf = Buffer.from(ttf.buffer);
fs.writeFile(DIST_PATH, ttfBuf, (err) => {
if (err) {
return reject(err);
}
log.log(`${color.green('SUCCESS')} ${color.blue_bt('TTF')} font successfully created!\n ╰┈▶ ${DIST_PATH}`);
resolve(ttfBuf);
});
});
}
;
/**
* TTF font to EOT
*/
export function createEOT(options = {}, ttf) {
return new Promise((resolve, reject) => {
const DIST_PATH = path.join(options.dist, options.fontName + '.eot');
const eot = Buffer.from(ttf2eot(ttf).buffer);
fs.writeFile(DIST_PATH, eot, (err) => {
if (err) {
return reject(err);
}
log.log(`${color.green('SUCCESS')} ${color.blue_bt('EOT')} font successfully created!\n ╰┈▶ ${DIST_PATH}`);
resolve(eot);
});
});
}
;
/**
* TTF font to WOFF
*/
export function createWOFF(options = {}, ttf) {
return new Promise((resolve, reject) => {
const DIST_PATH = path.join(options.dist, options.fontName + ".woff");
const woff = Buffer.from(ttf2woff(ttf).buffer);
fs.writeFile(DIST_PATH, woff, (err) => {
if (err) {
return reject(err);
}
log.log(`${color.green('SUCCESS')} ${color.blue_bt('WOFF')} font successfully created!\n ╰┈▶ ${DIST_PATH}`);
resolve(woff);
});
});
}
;
/**
* TTF font to WOFF2
*/
export function createWOFF2(options = {}, ttf) {
return new Promise((resolve, reject) => {
const DIST_PATH = path.join(options.dist, options.fontName + ".woff2");
const woff2 = Buffer.from(ttf2woff2(ttf).buffer);
fs.writeFile(DIST_PATH, woff2, (err) => {
if (err) {
return reject(err);
}
log.log(`${color.green('SUCCESS')} ${color.blue_bt('WOFF2')} font successfully created!\n ╰┈▶ ${DIST_PATH}`);
resolve({
path: DIST_PATH
});
});
});
}
;
/**
* Create SVG Symbol
*/
export function createSvgSymbol(options = {}) {
const DIST_PATH = path.join(options.dist, `${options.fontName}.symbol.svg`);
const $ = load('<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="0" height="0" style="display:none;"></svg>');
return new Promise((resolve, reject) => {
filterSvgFiles(options.src).forEach(svgPath => {
const fileName = path.basename(svgPath, path.extname(svgPath));
let file = fs.readFileSync(svgPath, "utf8");
// trim xml declaration
file = file.replace(/<\?xml.*?\?>\s*/g, '').trim();
const svgNode = $(file);
const symbolNode = $("<symbol></symbol>");
symbolNode.attr("viewBox", svgNode.attr("viewBox"));
symbolNode.attr("id", `${options.classNamePrefix}-${fileName}`);
symbolNode.append(svgNode.html());
$('svg').append(symbolNode);
});
fs.writeFile(DIST_PATH, $.html("svg"), (err) => {
if (err) {
return reject(err);
}
log.log(`${color.green('SUCCESS')} ${color.blue_bt('Svg Symbol')} font successfully created!\n ╰┈▶ ${DIST_PATH}`);
resolve({
path: DIST_PATH,
svg: $.html("svg")
});
});
});
}
;
// As we are processing css files, we need to eacape HTML entities.
const safeNunjucks = nunjucks.configure({ autoescape: false });
/**
* Copy template files
*/
export async function copyTemplate(inDir, outDir, { _opts, ...vars }) {
const files = await fs.readdir(inDir, { withFileTypes: true });
const context = {
...(_opts.templateVars || {}),
...vars,
cssPath: _opts.cssPath || '',
filename: _opts.fileName || vars.fontname,
};
await fs.ensureDir(outDir);
for (const file of files) {
if (!file.isFile())
continue;
if (_opts.include && !(new RegExp(_opts.include)).test(file.name))
continue;
let newFileName = file.name.replace(/\.template$/, '').replace(/^_/, '');
for (const key in context)
newFileName = newFileName.replace(`{{${key}}}`, `${context[key]}`);
const template = await fs.readFile(path.join(inDir, file.name), 'utf8');
const content = safeNunjucks.renderString(template, context);
const filePath = path.join(outDir, newFileName);
await fs.writeFile(filePath, content);
log.log(`${color.green('SUCCESS')} Created ${filePath} `);
}
}
;
/**
* Create HTML
*/
export function createHTML(templatePath, data) {
return nunjucks.renderString(fs.readFileSync(templatePath, 'utf8'), {
...data,
Date: Date,
JSON: JSON,
Math: Math,
Number: Number,
Object: Object,
RegExp: RegExp,
String: String,
typeof: (v) => typeof v,
});
}
;
export function generateFontFaceCSS(fontName, cssPath, timestamp, excludeFormat, hasTimestamp = true) {
const timestamString = hasTimestamp === true ? `?t=${timestamp}` : (typeof hasTimestamp == 'string' ? `?t=${hasTimestamp}` : undefined);
const formats = [
{ ext: 'eot', format: 'embedded-opentype', ieFix: true },
{ ext: 'woff2', format: 'woff2' },
{ ext: 'woff', format: 'woff' },
{ ext: 'ttf', format: 'truetype' },
{ ext: 'svg', format: 'svg' }
];
let cssString = ` font-family: "${fontName}";\n`;
if (!excludeFormat.includes('eot')) {
cssString += ` src: url('${cssPath}${fontName}.eot${timestamString || ''}'); /* IE9*/\n`;
}
cssString += ' src: ';
const srcParts = formats
.filter(format => !excludeFormat.includes(format.ext))
.map(format => {
if (format.ext === 'eot') {
return `url('${cssPath}${fontName}.eot${timestamString || '?'}#iefix') format('${format.format}') /* IE6-IE8 */`;
}
return `url('${cssPath}${fontName}.${format.ext}${timestamString || ''}') format('${format.format}')`;
});
cssString += srcParts.join(',\n ') + ';';
return cssString;
}
export const getDefaultOptions = (options) => {
return merge({
dist: path.resolve(process.cwd(), 'fonts'),
src: path.resolve(process.cwd(), 'svg'),
startUnicode: 0xea01,
svg2ttf: {},
svgicons2svgfont: {
fontName: 'iconfont',
},
fontName: 'iconfont',
symbolNameDelimiter: '-',
}, options);
};
//# sourceMappingURL=utils.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,159 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ _title }}</title>
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
{% for k, v in meta|default({}) %}
<meta name="{{ k }}" content="{{ v }}">
{% endfor %}
{% if favicon %}
<link rel="icon" type="image/x-icon" href="{{ favicon }}">
{% endif %}
{% if _type === 'font-class' and _link %}
<link rel="stylesheet" href="{{ _link }}" />
{% endif %}
<style>
*{margin: 0;padding: 0;list-style: none;}
body { color: #696969; font: 12px/1.5 tahoma, arial, \5b8b\4f53, sans-serif; }
a { color: #333; text-decoration: underline; }
a:hover { color: rgb(9, 73, 209); }
.header { color: #333; text-align: center; padding: 80px 0 60px 0; min-height: 153px; font-size: 14px; }
.header .logo svg { height: 120px; width: 120px; }
.header h1 { font-size: 42px; padding: 26px 0 10px 0; }
.header sup {font-size: 14px; margin: 8px 0 0 8px; position: absolute; color: #7b7b7b; }
.info {
color: #999;
font-weight: normal;
max-width: 346px;
margin: 0 auto;
padding: 20px 0;
font-size: 14px;
}
.icons { max-width: 1190px; margin: 0 auto; }
.icons ul { text-align: center; }
.icons ul li {
vertical-align: top;
width: 120px;
display: inline-block;
text-align: center;
background-color: rgba(0,0,0,.02);
border-radius: 3px;
padding: 29px 0 10px 0;
margin-right: 10px;
margin-top: 10px;
transition: all 0.6s ease;
}
.icons ul li:hover { background-color: rgba(0,0,0,.06); }
.icons ul li:hover span { color: #3c75e4; opacity: 1; }
.icons ul li .unicode { color: #8c8c8c; opacity: 0.3; }
.icons ul li h4 {
font-weight: normal;
padding: 10px 0 5px 0;
display: block;
color: #8c8c8c;
font-size: 14px;
line-height: 12px;
opacity: 0.8;
}
.icons ul li:hover h4 { opacity: 1; }
.icons ul li svg { width: 24px; height: 24px; }
.icons ul li:hover { color: #3c75e4; }
.footer { text-align: center; padding: 10px 0 90px 0; }
.footer a { text-align: center; padding: 10px 0 90px 0; color: #696969;}
.footer a:hover { color: #0949d1; }
.links { text-align: center; padding: 50px 0 0 0; font-size: 14px; }
{% if _type === 'font-class' %}
.icons ul li.class-icon { font-size: 21px; line-height: 21px; padding-bottom: 20px; }
.icons ul li.class-icon p{ font-size: 12px; }
.icons ul li.class-icon [class^="{{ classNamePrefix }}-"]{ font-size: 26px; }
{% elif _type === 'unicode' %}
.icons ul .unicode-icon span { display: block; }
.icons ul .unicode-icon h4 { font-size: 12px; }
@font-face {
font-family: "{{ fontname }}";
src: url("{{ fontname }}.eot");
/* IE9*/
src: url("{{ fontname }}.eot#iefix") format("embedded-opentype"),
/* IE6-IE8 */
url("{{ fontname }}.woff2?{{ Date.now() }}") format("woff2"),
url("{{ fontname }}.woff?{{ Date.now() }}") format("woff"),
/* chrome, firefox */
url("{{ fontname }}.ttf?{{ Date.now() }}") format("truetype"),
/* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
url("{{ fontname }}.svg#{{ fontname }}?{{ Date.now() }}") format("svg");
/* iOS 4.1- */
}
.iconfont {
font-family: "{{ fontname }}" !important;
font-size: 26px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-webkit-text-stroke-width: 0.2px;
-moz-osx-font-smoothing: grayscale;
}
{% elif _type === 'symbol' %}
.icons ul li.symbol {padding: 28px 10px 16px 10px; width: 100px;}
.icons ul li.symbol svg {width: 34px; height: 34px;}
.icons ul li.symbol h4 {font-size: 12px;}
{% endif %}
{% if corners and corners.url %}
.github-corner:hover .octo-arm { animation: octocat-wave 560ms ease-in-out; }
.github-corner svg { position: fixed; z-index: 999; border: 0px; top: 0px; right: 0px;}
@keyframes octocat-wave{
0%, 100% { transform: rotate(0); }
20%, 60% { transform: rotate(-25deg); }
40%, 80% { transform: rotate(10deg); }
}
@media (max-width:500px){
.github-corner:hover .octo-arm { animation: none; }
.github-corner .octo-arm { animation: octocat-wave 560ms ease-in-out; }
}
{% endif %}
</style>
</head>
<body>
{% if corners and corners.url %}
<a href="{{ corners.url|default('#') }}" target="__blank" class="github-corner">
<svg width="{{ corners.width|default(60) }}" height="{{ corners.width|default(60) }}" viewBox="0 0 250 250" aria-hidden="true" style="fill: {{ corners.bgColor|default('#151513') }}; color: rgb(255, 255, 255); ">
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" class="octo-arm" style="transform-origin: 130px 106px;"></path>
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path>
</svg>
</a>
{% endif %}
<div class="header">
{% if typeof(logo) === 'string' %}
<div class="logo">
<a href="./">{{ logo }}</a>
</div>
{% endif %}
<h1>{{ _title }}<sup>{{ version }}</sup></h1>
<div class="info">
{{ description|default(meta.description) }}
</div>
<p>
{% for linkItem in links|default([]) %}
<a href="{{ linkItem.url }}">{{ linkItem.title }}</a>{% if not loop.last %} · {% endif %}
{% endfor %}
</p>
</div>
<div class="icons">
<ul>
{{ _IconHtml|safe }}
</ul>
</div>
<p class="links">
{% for linkItem in links|default([]) %}
<a href="{{ linkItem.url }}">{{ linkItem.title }}</a>{% if not loop.last %} · {% endif %}
{% endfor %}
</p>
<div class="footer">
{{ footerInfo|safe }}
<div><a target="_blank" href="https://github.com/jaywcjlove/svgtofont">Created By svgtofont</a></div>
</div>
</body>
</html>

View File

@@ -0,0 +1,114 @@
{
"name": "svgtofont",
"version": "6.5.1",
"description": "Converts SVG to TTF/EOT/WOFF/WOFF2/SVG format fonts.",
"homepage": "https://jaywcjlove.github.io/svgtofont/",
"funding": "https://jaywcjlove.github.io/#/sponsor",
"main": "./lib/index.js",
"typings": "./lib/index.d.ts",
"exports": {
".": {
"default": "./lib/index.js",
"types": "./lib/index.d.ts"
},
"./lib/utils": {
"default": "./lib/utils.js",
"types": "./lib/utils.d.ts"
}
},
"type": "module",
"bin": {
"svgtofont": "lib/cli.js"
},
"scripts": {
"prepare": "npm run build",
"start": "node lib/index.js",
"watch": "tsbb watch src/*.ts",
"build": "tsbb build src/*.ts",
"example": "node examples/example/index.mjs",
"example:cli": "node lib/cli.js --sources ./examples/example/svg --output ./dist --fontName uiw-font",
"example:simple": "node examples/example/simple.mjs",
"example:templates": "node examples/templates/index.mjs",
"pretest": "npm run example && npm run example:simple && npm run example:templates",
"checked": "tsc --noEmit",
"test": "tsbb test",
"coverage": "tsbb test --coverage"
},
"author": "Kenny <wowohoo@qq.com>",
"repository": {
"type": "git",
"url": "git+https://github.com/jaywcjlove/svgtofont.git"
},
"svgtofont": {
"css": {
"fontSize": false
}
},
"keywords": [
"webfont",
"font",
"icon",
"iconfont",
"font-face",
"compress",
"minify",
"font-cli",
"ttf",
"woff",
"eot",
"svg",
"ttf2eot",
"ttf2woff",
"ttf2svg",
"svg2ttf",
"css",
"base64"
],
"license": "MIT",
"files": [
"lib",
"src"
],
"engines": {
"node": ">=18.0.0"
},
"jest": {
"transformIgnorePatterns": [
"<rootDir>/node_modules/?!(.*)"
],
"moduleNameMapper": {
"^(\\.{1,2}/.*)\\.js$": "$1"
}
},
"dependencies": {
"auto-config-loader": "^2.0.0",
"cheerio": "~1.0.0",
"colors-cli": "~1.0.28",
"fs-extra": "~11.2.0",
"image2uri": "^2.1.2",
"nunjucks": "^3.2.4",
"svg2ttf": "~6.0.3",
"svgicons2svgfont": "~15.0.0",
"svgo": "~3.3.0",
"ttf2eot": "~3.1.0",
"ttf2woff": "~3.0.0",
"ttf2woff2": "~8.0.0",
"yargs": "^17.7.2"
},
"devDependencies": {
"@types/fs-extra": "^11.0.1",
"@types/nunjucks": "^3.2.6",
"@types/svg2ttf": "~5.0.1",
"@types/ttf2eot": "~2.0.0",
"@types/ttf2woff": "~2.0.2",
"tsbb": "^4.4.0"
},
"peerDependencies": {
"@types/svg2ttf": "~5.0.1"
},
"peerDependenciesMeta": {
"@types/svg2ttf": {
"optional": true
}
}
}

View File

@@ -0,0 +1,61 @@
#!/usr/bin/env node
import FS from 'fs-extra';
import { Arguments } from 'yargs';
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'
import path from 'path';
import svgtofont from './index.js';
import { log } from './log.js';
type ArgvResult = Arguments<{
sources: string;
output: string;
fontName: string;
}>
const argv = yargs(hideBin(process.argv))
.alias('s', 'sources')
.describe('s', 'The root from which all sources are relative.')
.alias('o', 'output')
.describe('o', 'Output directory.')
.alias('f', 'fontName')
.describe('f', 'Font Name.')
.demandOption(['output', 'sources'])
.help('h')
.alias('h', 'help')
.epilog('copyright 2019')
.argv as ArgvResult;
const sourcesPath = path.resolve(process.cwd(), argv.sources);
const outputPath = path.resolve(process.cwd(), argv.output);
if (!FS.pathExistsSync(sourcesPath)) {
log.error('The directory does not exist!', sourcesPath);
process.exit();
}
if (!FS.pathExistsSync(outputPath)) {
FS.mkdirpSync(outputPath);
}
svgtofont({
src: sourcesPath, // svg path
dist: outputPath, // output path
// emptyDist: true, // Clear output directory contents
fontName: (argv.fontName) || "svgfont", // font name
css: true, // Create CSS files.
outSVGReact: true,
outSVGReactNative: false,
outSVGVue: true,
outSVGPath: true,
svgicons2svgfont: {
fontHeight: 1000,
normalize: true,
},
})
.then(() => {
log.log('done!');
}).catch((err) => {
log.log('SvgToFont:ERR:', err);
});

View File

@@ -0,0 +1,250 @@
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} };
`;

View File

@@ -0,0 +1,460 @@
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs-extra';
import image2uri from 'image2uri';
import { type SVGIcons2SVGFontStreamOptions } from 'svgicons2svgfont';
import color from 'colors-cli';
import { autoConf, merge, type AutoConfOption } from 'auto-config-loader';
import type { FontOptions } from 'svg2ttf';
import type { Config } from 'svgo';
import { log } from './log.js';
import { generateIconsSource, generateReactIcons, generateReactNativeIcons, generateVueIcons } from './generate.js';
import { createSVG, createTTF, createEOT, createWOFF, createWOFF2, createSvgSymbol, copyTemplate, type CSSOptions, createHTML, createTypescript, type TypescriptOptions } from './utils.js';
import { generateFontFaceCSS, getDefaultOptions } from './utils.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export type SvgToFontOptions = {
/** Support for .svgtofontrc and more configuration files. */
config?: AutoConfOption<SvgToFontOptions>
/** A value of `false` disables logging */
log?: boolean;
/** log callback function */
logger?: (message: string) => void;
/**
* The output directory.
* @default fonts
* @example
* ```
* path.join(process.cwd(), 'fonts')
* ```
*/
dist?: string;
/**
* svg path
* @default svg
* @example
* ```
* path.join(process.cwd(), 'svg')
* ```
*/
src?: string;
/**
* The font family name you want.
* @default iconfont
*/
fontName?: string;
/**
* Create CSS/LESS/Scss/Styl files, default `true`.
*/
css?: boolean | CSSOptions;
/**
* Output `./dist/react/`, SVG generates `react` components.
*/
outSVGReact?: boolean;
/**
* Output `./dist/reactNative/`, SVG generates `reactNative` component.
*/
outSVGReactNative?: boolean;
/**
* Output `./dist/vue/`, SVG generates `vue` components.
*/
outSVGVue?: boolean;
/**
* Output `./dist/svgtofont.json`, The content is as follows:
* @example
* ```js
* {
* "adobe": ["M14.868 3H23v19L14.868 3zM1 3h8.8.447z...."],
* "git": ["M2.6 10.59L8.38 4.8l1.69 1.7c-.24c-.6.34-1 .99-1..."],
* "stylelint": ["M129.74 243.648c28-100.5.816c2.65..."]
* }
* ```
*/
outSVGPath?: boolean;
/**
* Output `./dist/info.json`, The content is as follows:
* @example
* ```js
* {
* "adobe": {
* "encodedCode": "\\ea01",
* "prefix": "svgtofont",
* "className": "svgtofont-adobe",
* "unicode": "&#59905;"
* },
* .....
* }
* ```
*/
generateInfoData?: boolean;
/**
* This is the setting for [svgicons2svgfont](https://github.com/nfroidure/svgicons2svgfont/tree/dd713bea4f97afa59f7dba6a21ff7f22db565bcf#api)
*/
svgicons2svgfont?: Partial<SVGIcons2SVGFontStreamOptions>;
/** Some options can be configured with svgoOptions though it. [svgo](https://github.com/svg/svgo#configuration) */
svgoOptions?: Config;
/**
* Create font class name prefix, default value font name.
* @default fontName
*/
classNamePrefix?: SvgToFontOptions['fontName'];
/**
* Symbol Name Delimiter, @default `-`
*/
symbolNameDelimiter?: string;
/**
* Directory of custom templates.
*/
styleTemplates?: string;
/**
* unicode start number
* @default 10000
*/
startUnicode?: number;
/** Get Icon Unicode */
getIconUnicode?: (name: string, unicode: string, startUnicode: number) => [string, number];
/**
* should the name(file name) be used as unicode? this switch allows for the support of ligatures.
* @default false
*/
useNameAsUnicode?: boolean;
/**
* adds possibility to use name (file name) in addition to codepoints. adds support of ligatures.
* @default false
*/
addLigatures?: boolean
/**
* consoles whenever {{ cssString }} template outputs unicode characters or css vars
* @default false
*/
useCSSVars?: boolean;
/**
* Clear output directory contents
* @default false
*/
emptyDist?: boolean;
/**
* This is the setting for [svg2ttf](https://github.com/fontello/svg2ttf/tree/c33a126920f46b030e8ce960cc7a0e38a6946bbc#svg2ttfsvgfontstring-options---buf)
*/
svg2ttf?: FontOptions;
/**
* You can configure which font files to exclude from generation. By default, all font files will be generated.
* https://github.com/jaywcjlove/svgtofont/issues/238
*/
excludeFormat?: Array<"eot" | "woff" | "woff2" | "ttf" | "svg" | "symbol.svg">;
website?: {
/**
* Add a Github corner to your website
* @like https://github.com/uiwjs/react-github-corners
*/
corners?: {
/**
* @example `https://github.com/jaywcjlove/svgtofont`
*/
url?: string,
/**
* @default 60
*/
width?: number,
/**
* @default 60
*/
height?: number,
/**
* @default #151513
*/
bgColor?: '#dc3545'
},
/**
* @default unicode
*/
index?: 'font-class' | 'unicode' | 'symbol';
/**
* website title
*/
title?: string;
/**
* @example
* ```js
* path.resolve(rootPath, "favicon.png")
* ```
*/
favicon?: string;
/**
* Must be a .svg format image.
* @example
* ```js
* path.resolve(rootPath, "svg", "git.svg")
* ```
*/
logo?: string,
version?: string,
meta?: {
description?: string;
keywords?: string;
},
description?: string;
template?: string;
footerInfo?: string;
links: Array<{
title: string;
url: string;
}>;
};
/**
* Create typescript file with declarations for icon classnames
* @default false
*/
typescript?: boolean | TypescriptOptions
}
export type IconInfo = {
prefix: string;
symbol: string;
unicode: string;
className: string;
encodedCode: string | number;
}
export type InfoData = Record<string, Partial<IconInfo>>
const loadConfig = async (options: SvgToFontOptions): Promise<SvgToFontOptions> => {
const defaultOptions = getDefaultOptions(options);
const data = await autoConf<SvgToFontOptions>('svgtofont', {
mustExist: true,
default: defaultOptions,
...options.config,
});
return merge(defaultOptions, data);
};
const handlePkgConfig = (options: SvgToFontOptions): SvgToFontOptions => {
const pkgPath = path.join(process.cwd(), 'package.json');
if (fs.pathExistsSync(pkgPath)) {
const pkg = fs.readJSONSync(pkgPath);
if (pkg.svgtofont) {
const cssOptions = options.css;
options = merge(options, pkg.svgtofont);
if (pkg.svgtofont.css && cssOptions && typeof cssOptions === 'object') {
options.css = merge(cssOptions, pkg.svgtofont.css);
}
}
if (options.website && pkg.version) {
options.website.version = options.website.version ?? pkg.version;
}
}
return options;
};
export default async (options: SvgToFontOptions = {}) => {
options = await loadConfig(options);
options = handlePkgConfig(options);
if (options.log === undefined) options.log = true;
log.disabled = !options.log;
if (options.logger && typeof options.logger === 'function') log.logger = options.logger;
options.svgicons2svgfont.fontName = options.fontName;
options.classNamePrefix = options.classNamePrefix || options.fontName;
const excludeFormat = options.excludeFormat || [];
const fontSizeOpt = typeof options.css !== 'boolean' && options.css.fontSize;
const fontSize = typeof fontSizeOpt === 'boolean' ? (fontSizeOpt === true ? 'font-size: 16px;' : '') : `font-size: ${fontSizeOpt};`;
// If you generate a font you need to generate a style.
if (options.website && !options.css) options.css = true;
const infoDataPath = path.resolve(options.dist, 'info.json');
try {
if (options.emptyDist) {
await fs.emptyDir(options.dist);
}
// Ensures that the directory exists.
await fs.ensureDir(options.dist);
const unicodeObject = await createSVG(options);
/** @deprecated */
let cssToVars: string[] = [];
let cssString: string[] = [];
let cssRootVars: string[] = [];
let cssIconHtml: string[] = [];
let unicodeHtml: string[] = [];
let symbolHtml: string[] = [];
const prefix = options.classNamePrefix || options.fontName;
const infoData: InfoData = {}
Object.keys(unicodeObject).forEach((name, index, self) => {
if (!infoData[name]) infoData[name] = {};
const _code = unicodeObject[name];
let symbolName = options.classNamePrefix + options.symbolNameDelimiter + name
let iconPart = symbolName + '">';
let encodedCodes: string | number = _code.codePointAt(0);
if (options.useNameAsUnicode) {
symbolName = name;
iconPart = prefix + '">' + name;
encodedCodes = [..._code].map(x => x.codePointAt(0)).join(';&amp;#');
} else {
cssToVars.push(`$${symbolName}: "\\${encodedCodes.toString(16)}";\n`);
if (options.useCSSVars) {
if (index === 0) cssRootVars.push(`:root {\n`)
cssRootVars.push(`--${symbolName}: "\\${encodedCodes.toString(16)}";\n`);
cssString.push(`.${symbolName}::before { content: var(--${symbolName}); }\n`);
if (index === self.length - 1) cssRootVars.push(`}\n`)
} else {
cssString.push(`.${symbolName}::before { content: "\\${encodedCodes.toString(16)}"; }\n`);
}
}
infoData[name].encodedCode = `\\${encodedCodes.toString(16)}`;
infoData[name].prefix = prefix;
infoData[name].className = symbolName;
infoData[name].unicode = `&#${encodedCodes};`;
cssIconHtml.push(`<li class="class-icon"><i class="${iconPart}</i><p class="name">${name}</p></li>`);
unicodeHtml.push(`<li class="unicode-icon"><span class="iconfont">${_code}</span><h4>${name}</h4><span class="unicode">&amp;#${encodedCodes};</span></li>`);
symbolHtml.push(`
<li class="symbol">
<svg class="icon" aria-hidden="true">
<use xlink:href="${options.fontName}.symbol.svg#${symbolName}"></use>
</svg>
<h4>${symbolName}</h4>
</li>
`);
});
if (options.useCSSVars) {
cssString = [...cssRootVars, ...cssString]
}
if (options.generateInfoData) {
await fs.writeJSON(infoDataPath, infoData, { spaces: 2 });
log.log(`${color.green('SUCCESS')} Created ${infoDataPath} `);
}
const ttf = await createTTF(options);
if (!excludeFormat.includes('eot')) await createEOT(options, ttf);
if (!excludeFormat.includes('woff')) await createWOFF(options, ttf);
if (!excludeFormat.includes('woff2')) await createWOFF2(options, ttf);
if (!excludeFormat.includes('symbol.svg')) await createSvgSymbol(options);
const ttfPath = path.join(options.dist, options.fontName + ".ttf");
if (excludeFormat.includes('ttf')) {
fs.removeSync(ttfPath);
}
const svgPath = path.join(options.dist, options.fontName + ".svg");
if (excludeFormat.includes('svg')) {
fs.removeSync(svgPath)
}
if (options.css) {
const styleTemplatePath = options.styleTemplates || path.resolve(__dirname, 'styles')
const outDir = typeof options.css === 'object' ? options.css.output || options.dist : options.dist;
const hasTimestamp = typeof options.css === 'object' ? options.css.hasTimestamp : true;
const cssOptions = typeof options.css === 'object' ? options.css : {};
const fontFamilyString = generateFontFaceCSS(options.fontName, cssOptions.cssPath || "", Date.now(), excludeFormat, hasTimestamp);
await copyTemplate(styleTemplatePath, outDir, {
fontname: options.fontName,
cssString: cssString.join(''),
cssToVars: cssToVars.join(''),
infoData,
fontSize: fontSize,
timestamp: new Date().getTime(),
prefix,
fontFamily: fontFamilyString,
nameAsUnicode: options.useNameAsUnicode,
_opts: cssOptions
});
}
if (options.typescript) {
await createTypescript({ ...options, typescript: options.typescript })
}
if (options.website) {
const pageNames = ['font-class', 'unicode', 'symbol'];
const htmlPaths: Record<string, string> = {};
// setting default home page.
const indexName = pageNames.includes(options.website.index) ? options.website.index : 'font-class';
pageNames.forEach(name => {
const fileName = name === indexName ? 'index.html' : `${name}.html`;
htmlPaths[name] = path.join(options.dist, fileName);
});
const fontClassPath = htmlPaths['font-class'];
const unicodePath = htmlPaths['unicode'];
const symbolPath = htmlPaths['symbol'];
// default template
options.website.template = options.website.template || path.join(__dirname, 'website', 'index.njk');
// template data
const tempData: SvgToFontOptions['website'] & {
fontname: string;
classNamePrefix: string;
_type: string;
_link: string;
_IconHtml: string;
_title: string;
} = {
meta: null,
links: null,
corners: null,
description: null,
footerInfo: null,
...options.website,
fontname: options.fontName,
classNamePrefix: options.classNamePrefix,
_type: 'font-class',
_link: `${(options.css && typeof options.css !== 'boolean' && options.css.fileName) || options.fontName}.css`,
_IconHtml: cssIconHtml.join(''),
_title: options.website.title || options.fontName
};
// website logo
if (options.website.logo && fs.pathExistsSync(options.website.logo) && path.extname(options.website.logo) === '.svg') {
tempData.logo = fs.readFileSync(options.website.logo).toString();
}
// website favicon
if (options.website.favicon && fs.pathExistsSync(options.website.favicon)) {
tempData.favicon = await image2uri(options.website.favicon);
} else {
tempData.favicon = '';
}
const classHtmlStr = await createHTML(options.website.template, tempData);
fs.outputFileSync(fontClassPath, classHtmlStr);
log.log(`${color.green('SUCCESS')} Created ${fontClassPath} `);
tempData._IconHtml = unicodeHtml.join('');
tempData._type = 'unicode';
const unicodeHtmlStr = await createHTML(options.website.template, tempData);
fs.outputFileSync(unicodePath, unicodeHtmlStr);
log.log(`${color.green('SUCCESS')} Created ${unicodePath} `);
tempData._IconHtml = symbolHtml.join('');
tempData._type = 'symbol';
const symbolHtmlStr = await createHTML(options.website.template, tempData);
fs.outputFileSync(symbolPath, symbolHtmlStr);
log.log(`${color.green('SUCCESS')} Created ${symbolPath} `);
}
if (options.outSVGPath) {
const outPath = await generateIconsSource(options);
log.log(`${color.green('SUCCESS')} Created ${outPath} `);
}
if (options.outSVGReact) {
const outPath = await generateReactIcons(options);
log.log(`${color.green('SUCCESS')} Created React Components. `);
}
if (options.outSVGReactNative) {
generateReactNativeIcons(options, unicodeObject);
log.log(`${color.green('SUCCESS')} Created React Native Components. `);
}
if (options.outSVGVue) {
const outPath = await generateVueIcons(options);
log.log(`${color.green('SUCCESS')} Created Vue Components. `);
}
return infoData;
} catch (error) {
log.log('SvgToFont:CLI:ERR:', error);
}
}

View File

@@ -0,0 +1,25 @@
export class Log {
_disabled?:boolean;
constructor(disabled?: boolean) {
this.disabled = disabled || false
}
get disabled () {
return this._disabled;
}
set disabled(val: boolean) {
this._disabled = val;
}
log = (message?: any, ...optionalParams: any[]) => {
if (this.logger) this.logger(message);
if (this.disabled) return () => {}
return console.log(message, ...optionalParams)
}
error = (message?: any, ...optionalParams: any[]) => {
if (this.logger) this.logger(message);
if (this.disabled) return () => {}
return console.error(message, ...optionalParams)
}
logger = (message?: string) => {}
}
export const log = new Log();

View File

@@ -0,0 +1,14 @@
@font-face {
{{fontFamily}}
}
{% if nameAsUnicode %}.{{prefix}}{% else %}[class^="{{prefix}}-"], [class*=" {{prefix}}-"]{% endif %} {
font-family: '{{fontname}}' !important;{{fontSize}}
font-style:normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
{% if not nameAsUnicode %}
{{cssString}}
{% endif %}

View File

@@ -0,0 +1,14 @@
@font-face {
{{fontFamily}}
}
{% if nameAsUnicode %}.{{prefix}}{% else %}[class^="{{prefix}}-"], [class*=" {{prefix}}-"]{% endif %} {
font-family: '{{fontname}}' !important;{{fontSize}}
font-style:normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
{% if not nameAsUnicode %}
{{cssString}}
{% endif %}

View File

@@ -0,0 +1,16 @@
@font-face {
{{fontFamily}}
}
{% if nameAsUnicode %}.{{prefix}}{% else %}[class^="{{prefix}}-"], [class*=" {{prefix}}-"]{% endif %} {
font-family: '{{fontname}}' !important;{{fontSize}}
font-style:normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
{% if not nameAsUnicode %}
:global {
{{cssString}}
}
{% endif %}

View File

@@ -0,0 +1,17 @@
@font-face {
{{fontFamily}}
}
{% if nameAsUnicode %}.{{prefix}}{% else %}[class^="{{prefix}}-"], [class*=" {{prefix}}-"]{% endif %} {
font-family: '{{fontname}}' !important;{{fontSize}}
font-style:normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
{% if not nameAsUnicode %}
{{ cssString }}
{% for name, value in infoData %}
${{prefix}}-{{ name }}: '{{ value.encodedCode }}';
{%- endfor %}
{% endif %}

View File

@@ -0,0 +1,17 @@
@font-face {
{{fontFamily}}
}
{% if nameAsUnicode %}.{{prefix}}{% else %}[class^="{{prefix}}-"], [class*=" {{prefix}}-"]{% endif %} {
font-family: '{{fontname}}' !important;{{fontSize}}
font-style:normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
{% if not nameAsUnicode %}
{{ cssString }}
{% for name, value in infoData %}
${{prefix}}-{{ name }} = '{{ value.encodedCode }}'
{%- endfor %}
{% endif %}

View File

@@ -0,0 +1,381 @@
import { SVGIcons2SVGFontStream } from 'svgicons2svgfont';
import fs, { ReadStream } from 'fs-extra';
import path from 'path';
import color from 'colors-cli';
import { load } from 'cheerio';
import svg2ttf from 'svg2ttf';
import ttf2eot from 'ttf2eot';
import ttf2woff from 'ttf2woff';
import ttf2woff2 from 'ttf2woff2';
import nunjucks from 'nunjucks';
import { merge } from 'auto-config-loader';
import { type SvgToFontOptions } from './';
import { log } from './log.js';
let UnicodeObj: Record<string, string> = {};
/**
* Unicode Private Use Area start.
* https://en.wikipedia.org/wiki/Private_Use_Areas
*/
let startUnicode = 0xea01;
/**
* SVG to SVG font
*/
export function createSVG(options: SvgToFontOptions = {}): Promise<Record<string, string>> {
startUnicode = options.startUnicode
UnicodeObj = {}
return new Promise(async (resolve, reject) => {
const fontStream = new SVGIcons2SVGFontStream({
...options.svgicons2svgfont
});
function writeFontStream(svgPath: string) {
// file name
let _name = path.basename(svgPath, ".svg");
const glyph = fs.createReadStream(svgPath) as ReadStream & { metadata: { unicode: string[], name: string } };
const curUnicode = String.fromCodePoint(startUnicode);
const [_curUnicode, _startUnicode] = options.getIconUnicode
? (options.getIconUnicode(_name, curUnicode, startUnicode) || [curUnicode]) : [curUnicode];
if (_startUnicode) startUnicode = _startUnicode;
const unicode: string[] = [_curUnicode];
if (curUnicode === _curUnicode && (!_startUnicode || startUnicode === _startUnicode)) startUnicode++;
UnicodeObj[_name] = unicode[0];
if (!!options.useNameAsUnicode) {
unicode[0] = _name;
UnicodeObj[_name] = _name;
}
if (!!options.addLigatures) {
unicode.push(_name)
}
glyph.metadata = { unicode, name: _name };
fontStream.write(glyph);
}
const DIST_PATH = path.join(options.dist, options.fontName + ".svg");
// Setting the font destination
fontStream.pipe(fs.createWriteStream(DIST_PATH))
.on("finish", () => {
log.log(`${color.green('SUCCESS')} ${color.blue_bt('SVG')} font successfully created!\n ╰┈▶ ${DIST_PATH}`);
resolve(UnicodeObj);
})
.on("error", (err) => {
if (err) {
reject(err);
}
});
filterSvgFiles(options.src).forEach((svg: string) => {
if (typeof svg !== 'string') return false;
writeFontStream(svg);
});
// Do not forget to end the stream
fontStream.end();
});
}
/**
* Converts a string to pascal case.
*
* @example
*
* ```js
* toPascalCase('some_database_field_name'); // 'SomeDatabaseFieldName'
* toPascalCase('Some label that needs to be pascalized');
* // 'SomeLabelThatNeedsToBePascalized'
* toPascalCase('some-javascript-property'); // 'SomeJavascriptProperty'
* toPascalCase('some-mixed_string with spaces_underscores-and-hyphens');
* // 'SomeMixedStringWithSpacesUnderscoresAndHyphens'
* ```
*/
export const toPascalCase = (str: string) =>
str
.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
.map(x => x.charAt(0).toUpperCase() + x.slice(1).toLowerCase())
.join('');
/*
* Filter svg files
* @return {Array} svg files
*/
export function filterSvgFiles(svgFolderPath: string): string[] {
let files = fs.readdirSync(svgFolderPath, 'utf-8');
let svgArr = [];
if (!files) {
throw new Error(`Error! Svg folder is empty.${svgFolderPath}`);
}
for (let i in files) {
if (typeof files[i] !== 'string' || path.extname(files[i]) !== '.svg') continue;
if (!~svgArr.indexOf(files[i])) {
svgArr.push(path.join(svgFolderPath, files[i]));
}
}
return svgArr;
}
export function snakeToUppercase(str: string) {
return str.split(/[-_]/)
.map(partial => partial.charAt(0).toUpperCase() + partial.slice(1))
.join('')
}
export type TypescriptOptions = {
extension?: 'd.ts' | 'ts' | 'tsx',
enumName?: string
}
/**
* Create typescript declarations for icon classnames
*/
export async function createTypescript(options: Omit<SvgToFontOptions, 'typescript'> & { typescript: TypescriptOptions | true }) {
const tsOptions = options.typescript === true ? {} : options.typescript;
const uppercaseFontName = snakeToUppercase(options.fontName);
const { extension = 'd.ts', enumName = uppercaseFontName } = tsOptions;
const DIST_PATH = path.join(options.dist, `${options.fontName}.${extension}`);
const fileNames = filterSvgFiles(options.src).map(svgPath => path.basename(svgPath, path.extname(svgPath)));
await fs.writeFile(
DIST_PATH,
[
`export enum ${enumName} {`,
...fileNames.map(name => ` ${snakeToUppercase(name)} = "${options.classNamePrefix}-${name}",`),
'}',
`export type ${enumName}Classname = ${fileNames.map(name => `"${options.classNamePrefix}-${name}"`).join(' | ')}`,
`export type ${enumName}Icon = ${fileNames.map(name => `"${name}"`).join(' | ')}`,
`export const ${enumName}Prefix = "${options.classNamePrefix}-"`,
].join('\n'),
);
log.log(`${color.green('SUCCESS')} Created ${DIST_PATH}`);
}
/**
* SVG font to TTF
*/
export function createTTF(options: SvgToFontOptions = {}): Promise<Buffer> {
return new Promise((resolve, reject) => {
options.svg2ttf = options.svg2ttf || {};
const DIST_PATH = path.join(options.dist, options.fontName + ".ttf");
let ttf = svg2ttf(fs.readFileSync(path.join(options.dist, options.fontName + ".svg"), "utf8"), options.svg2ttf);
const ttfBuf = Buffer.from(ttf.buffer);
fs.writeFile(DIST_PATH, ttfBuf, (err: NodeJS.ErrnoException) => {
if (err) {
return reject(err);
}
log.log(`${color.green('SUCCESS')} ${color.blue_bt('TTF')} font successfully created!\n ╰┈▶ ${DIST_PATH}`);
resolve(ttfBuf);
});
});
};
/**
* TTF font to EOT
*/
export function createEOT(options: SvgToFontOptions = {}, ttf: Buffer) {
return new Promise((resolve, reject) => {
const DIST_PATH = path.join(options.dist, options.fontName + '.eot');
const eot = Buffer.from(ttf2eot(ttf).buffer);
fs.writeFile(DIST_PATH, eot, (err: NodeJS.ErrnoException) => {
if (err) {
return reject(err);
}
log.log(`${color.green('SUCCESS')} ${color.blue_bt('EOT')} font successfully created!\n ╰┈▶ ${DIST_PATH}`);
resolve(eot);
});
});
};
/**
* TTF font to WOFF
*/
export function createWOFF(options: SvgToFontOptions = {}, ttf: Buffer) {
return new Promise((resolve, reject) => {
const DIST_PATH = path.join(options.dist, options.fontName + ".woff");
const woff = Buffer.from(ttf2woff(ttf).buffer);
fs.writeFile(DIST_PATH, woff, (err) => {
if (err) {
return reject(err);
}
log.log(`${color.green('SUCCESS')} ${color.blue_bt('WOFF')} font successfully created!\n ╰┈▶ ${DIST_PATH}`);
resolve(woff);
});
});
};
/**
* TTF font to WOFF2
*/
export function createWOFF2(options: SvgToFontOptions = {}, ttf: Buffer) {
return new Promise((resolve, reject) => {
const DIST_PATH = path.join(options.dist, options.fontName + ".woff2");
const woff2 = Buffer.from(ttf2woff2(ttf).buffer);
fs.writeFile(DIST_PATH, woff2, (err) => {
if (err) {
return reject(err);
}
log.log(`${color.green('SUCCESS')} ${color.blue_bt('WOFF2')} font successfully created!\n ╰┈▶ ${DIST_PATH}`);
resolve({
path: DIST_PATH
});
});
});
};
/**
* Create SVG Symbol
*/
export function createSvgSymbol(options: SvgToFontOptions = {}) {
const DIST_PATH = path.join(options.dist, `${options.fontName}.symbol.svg`);
const $ = load('<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="0" height="0" style="display:none;"></svg>');
return new Promise((resolve, reject) => {
filterSvgFiles(options.src).forEach(svgPath => {
const fileName = path.basename(svgPath, path.extname(svgPath));
let file = fs.readFileSync(svgPath, "utf8");
// trim xml declaration
file = file.replace(/<\?xml.*?\?>\s*/g, '').trim();
const svgNode = $(file);
const symbolNode = $("<symbol></symbol>");
symbolNode.attr("viewBox", svgNode.attr("viewBox"));
symbolNode.attr("id", `${options.classNamePrefix}-${fileName}`);
symbolNode.append(svgNode.html());
$('svg').append(symbolNode);
});
fs.writeFile(DIST_PATH, $.html("svg"), (err) => {
if (err) {
return reject(err);
}
log.log(`${color.green('SUCCESS')} ${color.blue_bt('Svg Symbol')} font successfully created!\n ╰┈▶ ${DIST_PATH}`);
resolve({
path: DIST_PATH,
svg: $.html("svg")
});
});
});
};
export type CSSOptions = {
/**
* Output the css file to the specified directory
*/
output?: string;
/**
* Which files are exported.
*/
include?: RegExp;
/**
* Setting font size.
*/
fontSize?: string | boolean;
/**
* Set the path in the css file
* https://github.com/jaywcjlove/svgtofont/issues/48#issuecomment-739547189
*/
cssPath?: string;
/**
* Set file name
* https://github.com/jaywcjlove/svgtofont/issues/48#issuecomment-739547189
*/
fileName?: string;
/**
* Ad hoc template variables.
*/
templateVars?: Record<string, any>;
/**
* When including CSS files in a CSS file,
* you can add a timestamp parameter or custom text to the file path to prevent browser caching issues and ensure style updates are applied. @default true
* @example `path/to/iconfont.css?t=1612345678`
*/
hasTimestamp?: boolean | string;
}
// As we are processing css files, we need to eacape HTML entities.
const safeNunjucks = nunjucks.configure({ autoescape: false });
/**
* Copy template files
*/
export async function copyTemplate(inDir: string, outDir: string, { _opts, ...vars }: Record<string, any> & { _opts: CSSOptions }) {
const files = await fs.readdir(inDir, { withFileTypes: true });
const context = {
...(_opts.templateVars || {}),
...vars,
cssPath: _opts.cssPath || '',
filename: _opts.fileName || vars.fontname,
}
await fs.ensureDir(outDir);
for (const file of files) {
if (!file.isFile()) continue;
if (_opts.include && !(new RegExp(_opts.include)).test(file.name)) continue;
let newFileName = file.name.replace(/\.template$/, '').replace(/^_/, '');
for (const key in context) newFileName = newFileName.replace(`{{${key}}}`, `${context[key]}`);
const template = await fs.readFile(path.join(inDir, file.name), 'utf8');
const content = safeNunjucks.renderString(template, context);
const filePath = path.join(outDir, newFileName)
await fs.writeFile(filePath, content);
log.log(`${color.green('SUCCESS')} Created ${filePath} `);
}
};
/**
* Create HTML
*/
export function createHTML(templatePath: string, data: Record<string, any>): string {
return nunjucks.renderString(fs.readFileSync(templatePath, 'utf8'), {
...data,
Date: Date,
JSON: JSON,
Math: Math,
Number: Number,
Object: Object,
RegExp: RegExp,
String: String,
typeof: (v: any) => typeof v,
});
};
export function generateFontFaceCSS(fontName: string, cssPath: string, timestamp: number, excludeFormat: string[], hasTimestamp: boolean | string = true): string {
const timestamString = hasTimestamp === true ? `?t=${timestamp}` : (typeof hasTimestamp == 'string' ? `?t=${hasTimestamp}` : undefined);
const formats = [
{ ext: 'eot', format: 'embedded-opentype', ieFix: true },
{ ext: 'woff2', format: 'woff2' },
{ ext: 'woff', format: 'woff' },
{ ext: 'ttf', format: 'truetype' },
{ ext: 'svg', format: 'svg' }
];
let cssString = ` font-family: "${fontName}";\n`;
if (!excludeFormat.includes('eot')) {
cssString += ` src: url('${cssPath}${fontName}.eot${timestamString || ''}'); /* IE9*/\n`;
}
cssString += ' src: ';
const srcParts = formats
.filter(format => !excludeFormat.includes(format.ext))
.map(format => {
if (format.ext === 'eot') {
return `url('${cssPath}${fontName}.eot${timestamString || '?'}#iefix') format('${format.format}') /* IE6-IE8 */`;
}
return `url('${cssPath}${fontName}.${format.ext}${timestamString || ''}') format('${format.format}')`;
});
cssString += srcParts.join(',\n ') + ';';
return cssString;
}
export const getDefaultOptions = (options: SvgToFontOptions): SvgToFontOptions => {
return merge({
dist: path.resolve(process.cwd(), 'fonts'),
src: path.resolve(process.cwd(), 'svg'),
startUnicode: 0xea01,
svg2ttf: {},
svgicons2svgfont: {
fontName: 'iconfont',
},
fontName: 'iconfont',
symbolNameDelimiter: '-',
}, options);
};

View File

@@ -0,0 +1,159 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ _title }}</title>
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
{% for k, v in meta|default({}) %}
<meta name="{{ k }}" content="{{ v }}">
{% endfor %}
{% if favicon %}
<link rel="icon" type="image/x-icon" href="{{ favicon }}">
{% endif %}
{% if _type === 'font-class' and _link %}
<link rel="stylesheet" href="{{ _link }}" />
{% endif %}
<style>
*{margin: 0;padding: 0;list-style: none;}
body { color: #696969; font: 12px/1.5 tahoma, arial, \5b8b\4f53, sans-serif; }
a { color: #333; text-decoration: underline; }
a:hover { color: rgb(9, 73, 209); }
.header { color: #333; text-align: center; padding: 80px 0 60px 0; min-height: 153px; font-size: 14px; }
.header .logo svg { height: 120px; width: 120px; }
.header h1 { font-size: 42px; padding: 26px 0 10px 0; }
.header sup {font-size: 14px; margin: 8px 0 0 8px; position: absolute; color: #7b7b7b; }
.info {
color: #999;
font-weight: normal;
max-width: 346px;
margin: 0 auto;
padding: 20px 0;
font-size: 14px;
}
.icons { max-width: 1190px; margin: 0 auto; }
.icons ul { text-align: center; }
.icons ul li {
vertical-align: top;
width: 120px;
display: inline-block;
text-align: center;
background-color: rgba(0,0,0,.02);
border-radius: 3px;
padding: 29px 0 10px 0;
margin-right: 10px;
margin-top: 10px;
transition: all 0.6s ease;
}
.icons ul li:hover { background-color: rgba(0,0,0,.06); }
.icons ul li:hover span { color: #3c75e4; opacity: 1; }
.icons ul li .unicode { color: #8c8c8c; opacity: 0.3; }
.icons ul li h4 {
font-weight: normal;
padding: 10px 0 5px 0;
display: block;
color: #8c8c8c;
font-size: 14px;
line-height: 12px;
opacity: 0.8;
}
.icons ul li:hover h4 { opacity: 1; }
.icons ul li svg { width: 24px; height: 24px; }
.icons ul li:hover { color: #3c75e4; }
.footer { text-align: center; padding: 10px 0 90px 0; }
.footer a { text-align: center; padding: 10px 0 90px 0; color: #696969;}
.footer a:hover { color: #0949d1; }
.links { text-align: center; padding: 50px 0 0 0; font-size: 14px; }
{% if _type === 'font-class' %}
.icons ul li.class-icon { font-size: 21px; line-height: 21px; padding-bottom: 20px; }
.icons ul li.class-icon p{ font-size: 12px; }
.icons ul li.class-icon [class^="{{ classNamePrefix }}-"]{ font-size: 26px; }
{% elif _type === 'unicode' %}
.icons ul .unicode-icon span { display: block; }
.icons ul .unicode-icon h4 { font-size: 12px; }
@font-face {
font-family: "{{ fontname }}";
src: url("{{ fontname }}.eot");
/* IE9*/
src: url("{{ fontname }}.eot#iefix") format("embedded-opentype"),
/* IE6-IE8 */
url("{{ fontname }}.woff2?{{ Date.now() }}") format("woff2"),
url("{{ fontname }}.woff?{{ Date.now() }}") format("woff"),
/* chrome, firefox */
url("{{ fontname }}.ttf?{{ Date.now() }}") format("truetype"),
/* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
url("{{ fontname }}.svg#{{ fontname }}?{{ Date.now() }}") format("svg");
/* iOS 4.1- */
}
.iconfont {
font-family: "{{ fontname }}" !important;
font-size: 26px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-webkit-text-stroke-width: 0.2px;
-moz-osx-font-smoothing: grayscale;
}
{% elif _type === 'symbol' %}
.icons ul li.symbol {padding: 28px 10px 16px 10px; width: 100px;}
.icons ul li.symbol svg {width: 34px; height: 34px;}
.icons ul li.symbol h4 {font-size: 12px;}
{% endif %}
{% if corners and corners.url %}
.github-corner:hover .octo-arm { animation: octocat-wave 560ms ease-in-out; }
.github-corner svg { position: fixed; z-index: 999; border: 0px; top: 0px; right: 0px;}
@keyframes octocat-wave{
0%, 100% { transform: rotate(0); }
20%, 60% { transform: rotate(-25deg); }
40%, 80% { transform: rotate(10deg); }
}
@media (max-width:500px){
.github-corner:hover .octo-arm { animation: none; }
.github-corner .octo-arm { animation: octocat-wave 560ms ease-in-out; }
}
{% endif %}
</style>
</head>
<body>
{% if corners and corners.url %}
<a href="{{ corners.url|default('#') }}" target="__blank" class="github-corner">
<svg width="{{ corners.width|default(60) }}" height="{{ corners.width|default(60) }}" viewBox="0 0 250 250" aria-hidden="true" style="fill: {{ corners.bgColor|default('#151513') }}; color: rgb(255, 255, 255); ">
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" class="octo-arm" style="transform-origin: 130px 106px;"></path>
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path>
</svg>
</a>
{% endif %}
<div class="header">
{% if typeof(logo) === 'string' %}
<div class="logo">
<a href="./">{{ logo }}</a>
</div>
{% endif %}
<h1>{{ _title }}<sup>{{ version }}</sup></h1>
<div class="info">
{{ description|default(meta.description) }}
</div>
<p>
{% for linkItem in links|default([]) %}
<a href="{{ linkItem.url }}">{{ linkItem.title }}</a>{% if not loop.last %} · {% endif %}
{% endfor %}
</p>
</div>
<div class="icons">
<ul>
{{ _IconHtml|safe }}
</ul>
</div>
<p class="links">
{% for linkItem in links|default([]) %}
<a href="{{ linkItem.url }}">{{ linkItem.title }}</a>{% if not loop.last %} · {% endif %}
{% endfor %}
</p>
<div class="footer">
{{ footerInfo|safe }}
<div><a target="_blank" href="https://github.com/jaywcjlove/svgtofont">Created By svgtofont</a></div>
</div>
</body>
</html>