wmt-polyicon 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,65 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [0.1.5] — 2026-04-02
6
+
7
+ ### Fixed
8
+ - Fixed an issue in `stroke-linecap="round"` conversion where inward SVG arcs caused generated icons (like `close.svg`) to render redundantly or blank instead of matching their outline.
9
+
10
+ ## [0.1.4] — 2026-04-02
11
+
12
+ ### Added
13
+ - Stroke-to-fill conversion (`stroke_to_fill.js`) — stroked SVG paths (e.g. line icons from Figma/Heroicons) are now automatically converted to filled outlines for font generation, with support for round, square, and butt linecaps
14
+
15
+ ### Fixed
16
+ - Unified shape extraction regex to correctly match both self-closing and paired SVG element tags in a single pass — the previous `||` alternation silently dropped closing tags from paired elements
17
+
18
+ ## [0.1.3] — 2026-04-02
19
+
20
+ ### Fixed
21
+ - Blank icon generation for SVGs using non-path elements (`<circle>`, `<ellipse>`, `<rect>`, `<polygon>`, `<polyline>`, `<line>`) — previously only `<path>` elements were extracted, silently dropping all other shapes
22
+ - White background rect stripping no longer removes closing tags from non-white rects
23
+
24
+ ## [0.1.2] — 2026-03-18
25
+
26
+ ### Fixed
27
+ - `svgtofont` v6 is ESM-only — switched from `require()` to dynamic `import()` in `run_svgtofont.js` to resolve "svgtofont is not a function" error
28
+
29
+ ## [0.1.1] — 2026-03-18
30
+
31
+ ### Added
32
+ - `.gitignore` — excludes `node_modules/` and tmp build dirs
33
+
34
+ ### Changed
35
+ - `svgtofont` upgraded `4.x` → `6.5.1`
36
+ - `fast-xml-parser` upgraded `4.x` → `5.5.6` (fixes CVE-2026-26278)
37
+ - `CHANGELOG.md` included in published npm package via `files` field
38
+
39
+ ## [0.1.0] — 2026-03-18
40
+
41
+ ### Added
42
+ - Interactive HTML preview (`polyicon-preview.html`) generated at the root of the output folder alongside fonts and CSS
43
+ - Live search filter by icon name
44
+ - Click any icon card to copy its class name to clipboard
45
+ - Displays unicode code point for each icon
46
+ - Programmatic API — `const { buildIcons } = require('polyicon')`
47
+ - `"main"` field in `package.json` pointing to `src/index.js`
48
+ - Success log printed after `polyicon generate` completes
49
+
50
+ ### Fixed
51
+ - `write_polyicon_config.js`: replaced `charCodeAt(0)` with `codePointAt(0)` for correct unicode handling above U+FFFF
52
+
53
+ ### Changed
54
+ - `svgtofont` upgraded `4.x` → `6.5.1` — drops `del`/`node-gyp` chain, eliminating all deprecated transitive dependencies (`rimraf@3`, `glob@7`, `tar@6`, `npmlog`, `gauge`, `are-we-there-yet`, `inflight`)
55
+ - `fast-xml-parser` upgraded `4.x` → `5.5.6` — fixes high-severity CVE-2026-26278 (numeric entity expansion bypass)
56
+ - `npm audit`: 0 vulnerabilities, 0 deprecation warnings
57
+
58
+ ## [0.0.1] — initial release
59
+
60
+ - SVG → icon font pipeline (`eot`, `svg`, `ttf`, `woff`, `woff2`)
61
+ - Generated CSS with configurable font URL prefix (`importFontsPath`)
62
+ - JS/TS types file (`IconTypes`)
63
+ - Fontello-compatible `config.json` output
64
+ - `polyicon init` and `polyicon generate` CLI commands
65
+ - `.polyiconrc` config file support
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WebMob Technologies Pvt. Ltd.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # Polyicon
2
+
3
+ SVG to icon font generator with HTML preview — Fontello-style output for React and web projects.
4
+
5
+ Converts a folder of SVGs into `woff2`, `woff`, `ttf`, `eot`, `svg` font files plus a CSS file, a TypeScript/JS types file, and an interactive HTML preview page.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ # project dev dependency
11
+ npm i -D polyicon
12
+
13
+ # or global
14
+ npm i -g polyicon
15
+
16
+ # or one-off
17
+ npx polyicon init
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```bash
23
+ polyicon init # creates .polyiconrc with defaults
24
+ polyicon generate # builds fonts, CSS, types, and preview
25
+ ```
26
+
27
+ Place your SVGs in the folder specified by `svg` in `.polyiconrc`, then run `generate`. Output folders are created automatically.
28
+
29
+ ## Config (`.polyiconrc`)
30
+
31
+ ```json
32
+ {
33
+ "svg": "./src/assets/svg",
34
+ "outputFonts": "./src/assets/fontello/font",
35
+ "outputStyles": "./src/assets/fontello/css/polyicon.css",
36
+ "outputTypes": "./src/types/IconTypes.js",
37
+ "polyiconConfig": "./src/assets/fontello/config.json",
38
+ "importFontsPath": "../font/",
39
+ "formats": ["eot", "svg", "ttf", "woff", "woff2"]
40
+ }
41
+ ```
42
+
43
+ | Field | Required | Description |
44
+ |---|---|---|
45
+ | `svg` | ✓ | Folder containing your `.svg` files |
46
+ | `outputFonts` | ✓ | Where font files are written |
47
+ | `outputStyles` | | CSS file output path |
48
+ | `outputTypes` | ✓ | JS or TS types file output path |
49
+ | `polyiconConfig` | | Fontello-compatible `config.json` output path |
50
+ | `importFontsPath` | | Font URL prefix in the generated CSS (e.g. `../font/`) |
51
+ | `formats` | | Font formats to generate (default: all five) |
52
+
53
+ SVG filenames become CSS class names. Spaces and special characters are auto-sanitised to hyphens. Use clean, single-colour SVGs — icon fonts are monochrome.
54
+
55
+ ## Output
56
+
57
+ ```
58
+ src/assets/fontello/
59
+ polyicon-preview.html ← interactive icon browser
60
+ css/
61
+ polyicon.css
62
+ font/
63
+ polyicon.eot
64
+ polyicon.svg
65
+ polyicon.ttf
66
+ polyicon.woff
67
+ polyicon.woff2
68
+ config.json
69
+ src/types/
70
+ IconTypes.js
71
+ ```
72
+
73
+ Open `polyicon-preview.html` in a browser to browse all generated icons, search by name, and click any card to copy its class name to the clipboard.
74
+
75
+ ## Usage in React
76
+
77
+ Import the generated CSS once (e.g. in your root component or entry file):
78
+
79
+ ```jsx
80
+ import "./assets/fontello/css/polyicon.css";
81
+ ```
82
+
83
+ Use icons by class name:
84
+
85
+ ```jsx
86
+ <i className="polyicon polyicon-home" />
87
+ ```
88
+
89
+ Or with the generated types for autocomplete:
90
+
91
+ ```jsx
92
+ import IconTypes from "./types/IconTypes";
93
+
94
+ <i className={`polyicon polyicon-${IconTypes.HOME}`} />
95
+ ```
96
+
97
+ ## Programmatic API
98
+
99
+ ```js
100
+ const { buildIcons } = require("polyicon");
101
+
102
+ await buildIcons({
103
+ svg: "./src/assets/svg",
104
+ outputFonts: "./dist/fonts",
105
+ outputStyles: "./dist/polyicon.css",
106
+ outputTypes: "./src/types/IconTypes.js",
107
+ importFontsPath: "./fonts/",
108
+ formats: ["woff2", "woff", "ttf"]
109
+ });
110
+ ```
111
+
112
+ ## CLI
113
+
114
+ ```
115
+ polyicon init Create .polyiconrc
116
+ polyicon generate Generate fonts, CSS, types, and preview
117
+
118
+ Options:
119
+ -c, --config Path to config (default: .polyiconrc)
120
+ -f, --force Overwrite config on init
121
+ ```
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ const { run } = require("../src/cli");
3
+
4
+ run(process.argv);
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "wmt-polyicon",
3
+ "version": "0.1.5",
4
+ "description": "SVG to icon font generator with HTML preview — Fontello-style output for React and web projects.",
5
+ "license": "MIT",
6
+ "type": "commonjs",
7
+ "main": "src/index.js",
8
+ "keywords": [
9
+ "polyicon",
10
+ "icon",
11
+ "icons",
12
+ "svg",
13
+ "font",
14
+ "iconfont",
15
+ "fontello",
16
+ "react",
17
+ "cli",
18
+ "woff",
19
+ "woff2",
20
+ "ttf"
21
+ ],
22
+ "files": [
23
+ "bin",
24
+ "src",
25
+ "README.md",
26
+ "CHANGELOG.md"
27
+ ],
28
+ "bin": {
29
+ "polyicon": "bin/polyicon.js"
30
+ },
31
+ "scripts": {
32
+ "generate": "node bin/polyicon.js generate"
33
+ },
34
+ "dependencies": {
35
+ "fast-xml-parser": "^5.5.6",
36
+ "fs-extra": "^11.1.1",
37
+ "svgtofont": "^6.5.1"
38
+ }
39
+ }
@@ -0,0 +1,27 @@
1
+ const fs = require("fs");
2
+ const { loadConfig } = require("../../config/load");
3
+ const { buildIcons } = require("../../generator");
4
+ const { resolveFromCwd } = require("../../utils/paths");
5
+
6
+ async function generateCommand({ configPath }) {
7
+ const absConfig = resolveFromCwd(configPath);
8
+ if (!fs.existsSync(absConfig)) {
9
+ throw new Error(`Missing config: ${configPath}. Run \`polyicon init\`.`);
10
+ }
11
+ const conf = loadConfig(absConfig);
12
+ if (!conf.svg) {
13
+ throw new Error("Missing `svg` path in config. Set it to your SVG folder.");
14
+ }
15
+ const absSvg = resolveFromCwd(conf.svg);
16
+ if (!fs.existsSync(absSvg)) {
17
+ throw new Error(
18
+ `SVG path not found: ${conf.svg}. Update \`svg\` in ${configPath}.`
19
+ );
20
+ }
21
+ await buildIcons(conf);
22
+ console.log(`Done. Generated ${conf.outputFonts} and preview HTML.`);
23
+ }
24
+
25
+ module.exports = {
26
+ generateCommand
27
+ };
@@ -0,0 +1,22 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { DEFAULT_CONFIG } = require("../../config/defaults");
4
+ const { resolveFromCwd } = require("../../utils/paths");
5
+ const { ensureDir } = require("../../utils/fs");
6
+
7
+ async function initCommand({ configPath, force }) {
8
+ const targetPath = resolveFromCwd(configPath);
9
+ if (fs.existsSync(targetPath) && !force) {
10
+ throw new Error(
11
+ `Config already exists at ${configPath}. Use --force to overwrite.`
12
+ );
13
+ }
14
+
15
+ await ensureDir(path.dirname(targetPath));
16
+ fs.writeFileSync(targetPath, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n");
17
+
18
+ }
19
+
20
+ module.exports = {
21
+ initCommand
22
+ };
@@ -0,0 +1,59 @@
1
+ const { initCommand } = require("./commands/init");
2
+ const { generateCommand } = require("./commands/generate");
3
+
4
+ function getFlagValue(args, longName, shortName) {
5
+ const longIndex = args.indexOf(longName);
6
+ if (longIndex !== -1 && args[longIndex + 1]) return args[longIndex + 1];
7
+ const shortIndex = shortName ? args.indexOf(shortName) : -1;
8
+ if (shortIndex !== -1 && args[shortIndex + 1]) return args[shortIndex + 1];
9
+ return null;
10
+ }
11
+
12
+ function hasFlag(args, flag) {
13
+ return args.includes(flag);
14
+ }
15
+
16
+ function printHelp() {
17
+ console.log("polyicon <command>");
18
+ console.log("");
19
+ console.log("Commands:");
20
+ console.log(" init Create .polyiconrc");
21
+ console.log(" generate Generate fonts and types");
22
+ console.log("");
23
+ console.log("Options:");
24
+ console.log(" -c, --config Path to config (default: ./.polyiconrc)");
25
+ console.log(" -f, --force Overwrite config on init");
26
+ }
27
+
28
+ async function run(argv) {
29
+ const args = argv.slice(2);
30
+ const command = args[0];
31
+ const configPath = getFlagValue(args, "--config", "-c") || ".polyiconrc";
32
+ const force = hasFlag(args, "--force") || hasFlag(args, "-f");
33
+
34
+ try {
35
+ if (command === "init") {
36
+ await initCommand({ configPath, force });
37
+ console.log(`Created ${configPath}`);
38
+ console.log(
39
+ "Place SVGs in ./src/assets/svg and run `polyicon generate`."
40
+ );
41
+ return;
42
+ }
43
+
44
+ if (command === "generate") {
45
+ await generateCommand({ configPath });
46
+ return;
47
+ }
48
+
49
+ printHelp();
50
+ process.exit(command ? 1 : 0);
51
+ } catch (err) {
52
+ console.error(err && err.message ? err.message : err);
53
+ process.exit(1);
54
+ }
55
+ }
56
+
57
+ module.exports = {
58
+ run
59
+ };
@@ -0,0 +1,14 @@
1
+ const DEFAULT_CONFIG = {
2
+ svg: "./src/assets/svg",
3
+ outputFonts: "./src/assets/fontello/font",
4
+ importFontsPath: "../font/",
5
+ outputTypes: "./src/types/IconTypes.js",
6
+ outputStyles: "./src/assets/fontello/css/polyicon.css",
7
+ formats: ["eot", "svg", "ttf", "woff", "woff2"],
8
+ polyiconConfig: "./src/assets/fontello/config.json",
9
+ classPrefix: "icon"
10
+ };
11
+
12
+ module.exports = {
13
+ DEFAULT_CONFIG
14
+ };
@@ -0,0 +1,15 @@
1
+ const fs = require("fs");
2
+
3
+ function loadConfig(filePath) {
4
+ const raw = fs.readFileSync(filePath, "utf8");
5
+ try {
6
+ return JSON.parse(raw);
7
+ } catch (err) {
8
+ const message = err && err.message ? err.message : "Invalid JSON";
9
+ throw new Error(`Invalid JSON in ${filePath}: ${message}`);
10
+ }
11
+ }
12
+
13
+ module.exports = {
14
+ loadConfig
15
+ };
@@ -0,0 +1,21 @@
1
+ const WEBSITE = {
2
+ title: "polyicon",
3
+ version: 1,
4
+ logo: "logo.png",
5
+ meta: {
6
+ description: "Converts SVG icons to font files.",
7
+ keywords: "polyicon,TTF,EOT,WOFF,WOFF2,SVG"
8
+ },
9
+ links: []
10
+ };
11
+
12
+ const FONT_NAME = "polyicon";
13
+ const CLASS_PREFIX = "icon";
14
+ const TMP_DIR = ".polyicon_tmp";
15
+
16
+ module.exports = {
17
+ WEBSITE,
18
+ FONT_NAME,
19
+ CLASS_PREFIX,
20
+ TMP_DIR
21
+ };
@@ -0,0 +1,116 @@
1
+ const path = require("path");
2
+ const fse = require("fs-extra");
3
+ const { TMP_DIR, CLASS_PREFIX } = require("../constants");
4
+ const { resolveFromCwd } = require("../utils/paths");
5
+ const { runSvgToFont } = require("./run_svgtofont");
6
+ const { parseGlyphs } = require("./parse_svg");
7
+ const { writeTypes } = require("./write_types");
8
+ const { writeCss } = require("./write_css");
9
+ const { writeFonts } = require("./write_fonts");
10
+ const { writeHtml } = require("./write_html");
11
+ const { writePolyiconConfig } = require("./write_polyicon_config");
12
+ const { convertStrokesToFills } = require("./stroke_to_fill");
13
+
14
+ async function buildIcons(conf) {
15
+ const svgPath = resolveFromCwd(conf.svg);
16
+ const outputFontsPath = resolveFromCwd(conf.outputFonts);
17
+ const outputTypesPath = resolveFromCwd(conf.outputTypes);
18
+ const outputStylesPath = conf.outputStyles
19
+ ? resolveFromCwd(conf.outputStyles)
20
+ : null;
21
+ const polyiconConfigPath = conf.polyiconConfig
22
+ ? resolveFromCwd(conf.polyiconConfig)
23
+ : null;
24
+ const importFontsPath = conf.importFontsPath || "";
25
+ const formats = conf.formats || ["eot", "svg", "ttf", "woff", "woff2"];
26
+ const classPrefix = conf.classPrefix || CLASS_PREFIX;
27
+ const tmpPath = path.resolve(process.cwd(), TMP_DIR);
28
+ const tmpSvgPath = path.resolve(process.cwd(), `${TMP_DIR}_svgs`);
29
+
30
+ await fse.ensureDir(tmpSvgPath);
31
+ const svgFiles = (await fse.readdir(svgPath)).filter((file) =>
32
+ file.toLowerCase().endsWith(".svg")
33
+ );
34
+ if (!svgFiles.length) {
35
+ throw new Error(`No SVG files found in ${svgPath}.`);
36
+ }
37
+
38
+ const seen = new Map();
39
+ const sanitizeName = (name) => {
40
+ let safe = name
41
+ .replace(/[^a-zA-Z0-9]+/g, "-")
42
+ .replace(/-+/g, "-")
43
+ .replace(/^-+|-+$/g, "");
44
+ if (!safe) safe = "icon";
45
+ if (/^\d/.test(safe)) safe = `icon-${safe}`;
46
+ return safe;
47
+ };
48
+
49
+ for (const file of svgFiles) {
50
+ const inputPath = path.resolve(svgPath, file);
51
+ const baseName = path.basename(file, ".svg");
52
+ const safeBase = sanitizeName(baseName);
53
+ if (seen.has(safeBase)) {
54
+ throw new Error(
55
+ `Duplicate icon name after sanitizing: "${baseName}" conflicts with "${seen.get(
56
+ safeBase
57
+ )}". Rename one of the SVG files.`
58
+ );
59
+ }
60
+ seen.set(safeBase, baseName);
61
+ const outputPath = path.resolve(tmpSvgPath, `${safeBase}.svg`);
62
+ const raw = await fse.readFile(inputPath, "utf8");
63
+ const cleaned = raw
64
+ .replace(/<defs[\s\S]*?<\/defs>/gi, "")
65
+ .replace(/\sclip-path="[^"]*"/gi, "")
66
+ .replace(/\sclip-path='[^']*'/gi, "")
67
+ .replace(/\sfilter="[^"]*"/gi, "")
68
+ .replace(/\sfilter='[^']*'/gi, "")
69
+ .replace(/\smask="[^"]*"/gi, "")
70
+ .replace(/\smask='[^']*'/gi, "")
71
+ // Remove common white background rects (and their closing tags)
72
+ // that create square glyphs.
73
+ .replace(
74
+ /<rect\b[^>]*fill=["'](?:#fff|#ffffff|white)["'][^>]*(?:\/>|>(?:\s*<\/rect>)?)/gi,
75
+ ""
76
+ );
77
+ // Convert stroke-only paths to filled outlines — font renderers
78
+ // only understand fills, so stroked paths produce blank glyphs.
79
+ const converted = convertStrokesToFills(cleaned);
80
+ // Extract all SVG drawing elements — not just <path>.
81
+ // Covers circle, ellipse, rect, polygon, polyline, and line so icons
82
+ // built from basic shapes are no longer silently dropped.
83
+ const shapeTags = converted.match(
84
+ /<(path|circle|ellipse|rect|polygon|polyline|line)\b[^>]*>[\s\S]*?<\/\1>|<(?:path|circle|ellipse|rect|polygon|polyline|line)\b[^>]*\/>/gi
85
+ );
86
+ let outputSvg = converted;
87
+ if (shapeTags && shapeTags.length) {
88
+ const viewBoxMatch = converted.match(/viewBox="([^"]+)"/i);
89
+ const viewBoxAttr = viewBoxMatch ? ` viewBox="${viewBoxMatch[1]}"` : "";
90
+ outputSvg = `<svg xmlns="http://www.w3.org/2000/svg"${viewBoxAttr}>${shapeTags.join(
91
+ ""
92
+ )}</svg>`;
93
+ }
94
+ await fse.writeFile(outputPath, outputSvg, "utf8");
95
+ }
96
+
97
+ await runSvgToFont({ svgPath: tmpSvgPath, tmpPath, classPrefix });
98
+ const glyphs = await parseGlyphs(tmpPath);
99
+
100
+ await writeTypes({ outputTypesPath, glyphs });
101
+ await writePolyiconConfig({ outputPath: polyiconConfigPath, glyphs, classPrefix });
102
+ await writeCss({
103
+ tmpPath,
104
+ outputStylesPath,
105
+ importFontsPath
106
+ });
107
+ await writeFonts({ tmpPath, outputFontsPath, formats });
108
+ await writeHtml({ tmpPath, outputFontsPath, glyphs, classPrefix });
109
+
110
+ await fse.remove(tmpPath);
111
+ await fse.remove(tmpSvgPath);
112
+ }
113
+
114
+ module.exports = {
115
+ buildIcons
116
+ };
@@ -0,0 +1,57 @@
1
+ const fse = require("fs-extra");
2
+ const { XMLParser } = require("fast-xml-parser");
3
+ const path = require("path");
4
+ const { FONT_NAME } = require("../constants");
5
+
6
+ async function parseGlyphs(tmpPath) {
7
+ const svgPath = path.resolve(tmpPath, `${FONT_NAME}.svg`);
8
+ const svg = await fse.readFile(svgPath, "utf8");
9
+ const parser = new XMLParser({
10
+ ignoreAttributes: false,
11
+ attributeNamePrefix: "",
12
+ processEntities: true,
13
+ htmlEntities: true
14
+ });
15
+ const json = parser.parse(svg);
16
+ const defs = json && json.svg && json.svg.defs ? json.svg.defs : null;
17
+ let font = defs && defs.font ? defs.font : null;
18
+ if (Array.isArray(font)) font = font[0];
19
+ let glyphs = font && font.glyph ? font.glyph : null;
20
+ if (!glyphs) {
21
+ throw new Error(`No glyphs found in ${svgPath}.`);
22
+ }
23
+ if (!(glyphs instanceof Array)) glyphs = [glyphs];
24
+
25
+ const normalizeUnicode = (value) => {
26
+ if (typeof value !== "string") return value;
27
+ if (value.startsWith("&#x") && value.endsWith(";")) {
28
+ const code = parseInt(value.slice(3, -1), 16);
29
+ if (!Number.isNaN(code)) return String.fromCharCode(code);
30
+ }
31
+ if (value.startsWith("&#") && value.endsWith(";")) {
32
+ const code = parseInt(value.slice(2, -1), 10);
33
+ if (!Number.isNaN(code)) return String.fromCharCode(code);
34
+ }
35
+ return value;
36
+ };
37
+
38
+ glyphs = glyphs.map((g) => {
39
+ if (!g) return g;
40
+ const unicode = normalizeUnicode(g.unicode);
41
+ return { ...g, unicode };
42
+ });
43
+
44
+ const invalid = glyphs.filter(
45
+ (g) => !g || !g["glyph-name"] || !g.unicode
46
+ );
47
+ if (invalid.length) {
48
+ throw new Error(
49
+ `Invalid glyphs in ${svgPath}. Expected glyph-name and unicode attributes.`
50
+ );
51
+ }
52
+ return glyphs;
53
+ }
54
+
55
+ module.exports = {
56
+ parseGlyphs
57
+ };
@@ -0,0 +1,22 @@
1
+ const { WEBSITE, FONT_NAME } = require("../constants");
2
+
3
+ async function runSvgToFont({ svgPath, tmpPath, classPrefix }) {
4
+ const mod = await import("svgtofont");
5
+ const svgtofont = mod.default;
6
+ await svgtofont({
7
+ src: svgPath,
8
+ dist: tmpPath,
9
+ fontName: FONT_NAME,
10
+ css: true,
11
+ classNamePrefix: classPrefix,
12
+ website: WEBSITE,
13
+ svgicons2svgfont: {
14
+ fontHeight: 1000,
15
+ normalize: true
16
+ }
17
+ });
18
+ }
19
+
20
+ module.exports = {
21
+ runSvgToFont
22
+ };
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Convert stroked SVG elements to filled outlines.
3
+ *
4
+ * Font generators (svgtofont / svg2ttf) only render filled shapes.
5
+ * Stroke-only paths (e.g. line icons from Figma / Heroicons) produce
6
+ * zero-area glyphs → blank icons. This module converts the most
7
+ * common stroke patterns into equivalent filled outlines that font
8
+ * renderers can handle.
9
+ *
10
+ * Supported:
11
+ * - <path> with M/L/H/V commands (straight line segments)
12
+ * - <line> elements
13
+ * - Stroke linecaps: round, butt, square
14
+ */
15
+
16
+ /**
17
+ * Parse an SVG path `d` attribute into an array of
18
+ * { cmd, args[] } objects. Only absolute commands are
19
+ * handled here (the icons we target are already absolute).
20
+ */
21
+ function parsePathData(d) {
22
+ const commands = [];
23
+ const re = /([MLHVZmlhvz])\s*([^MLHVZmlhvz]*)/gi;
24
+ let m;
25
+ while ((m = re.exec(d)) !== null) {
26
+ const cmd = m[1];
27
+ const nums = m[2]
28
+ .trim()
29
+ .split(/[\s,]+/)
30
+ .filter(Boolean)
31
+ .map(Number);
32
+ commands.push({ cmd, args: nums });
33
+ }
34
+ return commands;
35
+ }
36
+
37
+ /**
38
+ * Given a line segment (x1,y1)→(x2,y2), stroke-width `w` and
39
+ * linecap style, return an SVG path `d` string that represents
40
+ * the filled outline of that stroke.
41
+ */
42
+ function lineToFilledPath(x1, y1, x2, y2, w, linecap) {
43
+ const hw = w / 2;
44
+ const dx = x2 - x1;
45
+ const dy = y2 - y1;
46
+ const len = Math.sqrt(dx * dx + dy * dy);
47
+ if (len === 0) {
48
+ // Degenerate: a dot → circle
49
+ return `M${x1 - hw} ${y1}A${hw} ${hw} 0 1 0 ${x1 + hw} ${y1}A${hw} ${hw} 0 1 0 ${x1 - hw} ${y1}Z`;
50
+ }
51
+
52
+ // Perpendicular offsets
53
+ const px = (-dy / len) * hw;
54
+ const py = (dx / len) * hw;
55
+
56
+ // Four corners of the stroke rectangle
57
+ const p1 = `${x1 + px} ${y1 + py}`;
58
+ const p2 = `${x2 + px} ${y2 + py}`;
59
+ const p3 = `${x2 - px} ${y2 - py}`;
60
+ const p4 = `${x1 - px} ${y1 - py}`;
61
+
62
+ if (linecap === "round") {
63
+ // Rectangle + semicircular arcs at each end
64
+ return `M${p1}L${p2}A${hw} ${hw} 0 0 0 ${p3}L${p4}A${hw} ${hw} 0 0 0 ${p1}Z`;
65
+ }
66
+
67
+ if (linecap === "square") {
68
+ // Extend rectangle by hw along the line direction at each end
69
+ const ex = (dx / len) * hw;
70
+ const ey = (dy / len) * hw;
71
+ const s1 = `${x1 - ex + px} ${y1 - ey + py}`;
72
+ const s2 = `${x2 + ex + px} ${y2 + ey + py}`;
73
+ const s3 = `${x2 + ex - px} ${y2 + ey - py}`;
74
+ const s4 = `${x1 - ex - px} ${y1 - ey - py}`;
75
+ return `M${s1}L${s2}L${s3}L${s4}Z`;
76
+ }
77
+
78
+ // butt (default)
79
+ return `M${p1}L${p2}L${p3}L${p4}Z`;
80
+ }
81
+
82
+ /**
83
+ * Detect whether a <path> is stroke-only (has stroke, no meaningful
84
+ * fill), extract the relevant attributes, and return filled
85
+ * replacement markup. Returns `null` when conversion is not needed
86
+ * or not supported.
87
+ */
88
+ function convertPathElement(tag) {
89
+ const attr = (name) => {
90
+ const re = new RegExp(`${name}\\s*=\\s*["']([^"']*)["']`, "i");
91
+ const m = tag.match(re);
92
+ return m ? m[1] : null;
93
+ };
94
+
95
+ const stroke = attr("stroke");
96
+ if (!stroke || stroke === "none") return null; // nothing to convert
97
+
98
+ const fill = attr("fill");
99
+ // If the element already has a visible fill, leave it alone
100
+ if (fill && fill !== "none") return null;
101
+
102
+ const d = attr("d");
103
+ if (!d) return null;
104
+
105
+ const strokeWidth = parseFloat(attr("stroke-width") || "1");
106
+ const linecap = attr("stroke-linecap") || "butt";
107
+
108
+ const commands = parsePathData(d);
109
+
110
+ // Collect line segments from consecutive M/L/H/V commands
111
+ const segments = [];
112
+ let cx = 0,
113
+ cy = 0,
114
+ startX = 0,
115
+ startY = 0;
116
+ let supported = true;
117
+
118
+ for (const { cmd, args } of commands) {
119
+ switch (cmd) {
120
+ case "M":
121
+ cx = args[0];
122
+ cy = args[1];
123
+ startX = cx;
124
+ startY = cy;
125
+ // M can carry implicit L args after the first pair
126
+ for (let i = 2; i + 1 < args.length; i += 2) {
127
+ segments.push({ x1: cx, y1: cy, x2: args[i], y2: args[i + 1] });
128
+ cx = args[i];
129
+ cy = args[i + 1];
130
+ }
131
+ break;
132
+ case "m":
133
+ cx += args[0];
134
+ cy += args[1];
135
+ startX = cx;
136
+ startY = cy;
137
+ for (let i = 2; i + 1 < args.length; i += 2) {
138
+ const nx = cx + args[i],
139
+ ny = cy + args[i + 1];
140
+ segments.push({ x1: cx, y1: cy, x2: nx, y2: ny });
141
+ cx = nx;
142
+ cy = ny;
143
+ }
144
+ break;
145
+ case "L":
146
+ for (let i = 0; i + 1 < args.length; i += 2) {
147
+ segments.push({ x1: cx, y1: cy, x2: args[i], y2: args[i + 1] });
148
+ cx = args[i];
149
+ cy = args[i + 1];
150
+ }
151
+ break;
152
+ case "l":
153
+ for (let i = 0; i + 1 < args.length; i += 2) {
154
+ const nx = cx + args[i],
155
+ ny = cy + args[i + 1];
156
+ segments.push({ x1: cx, y1: cy, x2: nx, y2: ny });
157
+ cx = nx;
158
+ cy = ny;
159
+ }
160
+ break;
161
+ case "H":
162
+ segments.push({ x1: cx, y1: cy, x2: args[0], y2: cy });
163
+ cx = args[0];
164
+ break;
165
+ case "h":
166
+ segments.push({ x1: cx, y1: cy, x2: cx + args[0], y2: cy });
167
+ cx += args[0];
168
+ break;
169
+ case "V":
170
+ segments.push({ x1: cx, y1: cy, x2: cx, y2: args[0] });
171
+ cy = args[0];
172
+ break;
173
+ case "v":
174
+ segments.push({ x1: cx, y1: cy, x2: cx, y2: cy + args[0] });
175
+ cy += args[0];
176
+ break;
177
+ case "Z":
178
+ case "z":
179
+ if (cx !== startX || cy !== startY) {
180
+ segments.push({ x1: cx, y1: cy, x2: startX, y2: startY });
181
+ }
182
+ cx = startX;
183
+ cy = startY;
184
+ break;
185
+ default:
186
+ // Curves, arcs, etc. — bail out and leave the element untouched
187
+ supported = false;
188
+ break;
189
+ }
190
+ if (!supported) break;
191
+ }
192
+
193
+ if (!supported || segments.length === 0) return null;
194
+
195
+ // Build filled outline for each segment
196
+ const color = stroke === "currentColor" ? "black" : stroke;
197
+ const paths = segments.map((s) => {
198
+ const outline = lineToFilledPath(
199
+ s.x1,
200
+ s.y1,
201
+ s.x2,
202
+ s.y2,
203
+ strokeWidth,
204
+ linecap
205
+ );
206
+ return `<path d="${outline}" fill="${color}"/>`;
207
+ });
208
+
209
+ return paths.join("");
210
+ }
211
+
212
+ /**
213
+ * Convert a <line> element to a filled path.
214
+ */
215
+ function convertLineElement(tag) {
216
+ const attr = (name) => {
217
+ const re = new RegExp(`${name}\\s*=\\s*["']([^"']*)["']`, "i");
218
+ const m = tag.match(re);
219
+ return m ? m[1] : null;
220
+ };
221
+
222
+ const stroke = attr("stroke");
223
+ if (!stroke || stroke === "none") return null;
224
+
225
+ const x1 = parseFloat(attr("x1") || "0");
226
+ const y1 = parseFloat(attr("y1") || "0");
227
+ const x2 = parseFloat(attr("x2") || "0");
228
+ const y2 = parseFloat(attr("y2") || "0");
229
+ const strokeWidth = parseFloat(attr("stroke-width") || "1");
230
+ const linecap = attr("stroke-linecap") || "butt";
231
+
232
+ const color = stroke === "currentColor" ? "black" : stroke;
233
+ const outline = lineToFilledPath(x1, y1, x2, y2, strokeWidth, linecap);
234
+ return `<path d="${outline}" fill="${color}"/>`;
235
+ }
236
+
237
+ /**
238
+ * Main entry point — transform an SVG string, converting stroked
239
+ * elements to filled outlines where possible.
240
+ */
241
+ function convertStrokesToFills(svgString) {
242
+ // Handle <path> elements (self-closing)
243
+ let result = svgString.replace(
244
+ /<path\b[^>]*\/?>/gi,
245
+ (match) => convertPathElement(match) || match
246
+ );
247
+
248
+ // Handle <line> elements (self-closing)
249
+ result = result.replace(
250
+ /<line\b[^>]*\/?>/gi,
251
+ (match) => convertLineElement(match) || match
252
+ );
253
+
254
+ return result;
255
+ }
256
+
257
+ module.exports = { convertStrokesToFills };
@@ -0,0 +1,34 @@
1
+ const fse = require("fs-extra");
2
+ const path = require("path");
3
+ const { ensureDir } = require("../utils/fs");
4
+ const { FONT_NAME } = require("../constants");
5
+
6
+ async function writeCss({ tmpPath, outputStylesPath, importFontsPath }) {
7
+ if (!outputStylesPath) return;
8
+
9
+ await ensureDir(path.dirname(outputStylesPath));
10
+
11
+ let css = await fse.readFile(
12
+ path.resolve(tmpPath, `${FONT_NAME}.css`),
13
+ "utf8"
14
+ );
15
+
16
+ while (css.indexOf(`url('${FONT_NAME}`) > -1) {
17
+ css = css.replace(
18
+ `url('${FONT_NAME}`,
19
+ `url('${importFontsPath}${FONT_NAME}`
20
+ );
21
+ }
22
+ while (css.indexOf(`url("${FONT_NAME}`) > -1) {
23
+ css = css.replace(
24
+ `url("${FONT_NAME}`,
25
+ `url("${importFontsPath}${FONT_NAME}`
26
+ );
27
+ }
28
+
29
+ await fse.writeFile(outputStylesPath, css, "utf8");
30
+ }
31
+
32
+ module.exports = {
33
+ writeCss
34
+ };
@@ -0,0 +1,25 @@
1
+ const fse = require("fs-extra");
2
+ const path = require("path");
3
+ const { ensureDir } = require("../utils/fs");
4
+ const { FONT_NAME } = require("../constants");
5
+
6
+ async function writeFonts({ tmpPath, outputFontsPath, formats }) {
7
+ await ensureDir(outputFontsPath);
8
+
9
+ const copyIf = async (ext) => {
10
+ await fse.copy(
11
+ path.resolve(tmpPath, `${FONT_NAME}.${ext}`),
12
+ path.resolve(outputFontsPath, `${FONT_NAME}.${ext}`)
13
+ );
14
+ };
15
+
16
+ if (formats.includes("svg")) await copyIf("svg");
17
+ if (formats.includes("ttf")) await copyIf("ttf");
18
+ if (formats.includes("woff")) await copyIf("woff");
19
+ if (formats.includes("woff2")) await copyIf("woff2");
20
+ if (formats.includes("eot")) await copyIf("eot");
21
+ }
22
+
23
+ module.exports = {
24
+ writeFonts
25
+ };
@@ -0,0 +1,302 @@
1
+ const fse = require("fs-extra");
2
+ const path = require("path");
3
+ const { ensureDir } = require("../utils/fs");
4
+ const { FONT_NAME } = require("../constants");
5
+
6
+ async function writeHtml({ tmpPath, outputFontsPath, glyphs, classPrefix }) {
7
+ const outputDir = path.dirname(outputFontsPath);
8
+ const fontsSubDir = path.basename(outputFontsPath);
9
+
10
+ let css = await fse.readFile(
11
+ path.resolve(tmpPath, `${FONT_NAME}.css`),
12
+ "utf8"
13
+ );
14
+
15
+ css = css.replace(
16
+ /url\((['"]?)([^'")\s]+\.(eot|woff2?|ttf|svg)[^'")\s]*)\1\)/gi,
17
+ (_, quote, href) => {
18
+ const q = quote || "'";
19
+ const file = href.split("?")[0];
20
+ const query = href.includes("?") ? "?" + href.split("?")[1] : "";
21
+ return `url(${q}${fontsSubDir}/${path.basename(file)}${query}${q})`;
22
+ }
23
+ );
24
+
25
+ const items = glyphs
26
+ .map((g) => {
27
+ const name = g["glyph-name"];
28
+ const className = `${classPrefix}-${name}`;
29
+ const code = g.unicode.codePointAt(0).toString(16).padStart(4, "0");
30
+ return ` <div class="icon-card" data-class="${classPrefix} ${className}" data-name="${className}">
31
+ <div class="icon-preview">
32
+ <i class="${classPrefix} ${className}"></i>
33
+ </div>
34
+ <div class="icon-info">
35
+ <span class="icon-name" title="${className}">${className}</span>
36
+ <span class="icon-code">U+${code.toUpperCase()}</span>
37
+ </div>
38
+ <button class="copy-btn" title="Copy class name">
39
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
40
+ <rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
41
+ </svg>
42
+ </button>
43
+ <div class="copied-badge">Copied!</div>
44
+ </div>`;
45
+ })
46
+ .join("\n");
47
+
48
+ const html = `<!DOCTYPE html>
49
+ <html lang="en">
50
+ <head>
51
+ <meta charset="UTF-8" />
52
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
53
+ <title>${FONT_NAME} — Icon Preview</title>
54
+ <style>
55
+ ${css}
56
+
57
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
58
+
59
+ body {
60
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
61
+ background: #f0f2f5;
62
+ color: #1a1a1a;
63
+ min-height: 100vh;
64
+ }
65
+
66
+ /* ── Header ── */
67
+ .header {
68
+ background: #fff;
69
+ border-bottom: 1px solid #e5e7eb;
70
+ padding: 28px 40px;
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: space-between;
74
+ gap: 16px;
75
+ flex-wrap: wrap;
76
+ }
77
+ .header-left h1 {
78
+ font-size: 20px;
79
+ font-weight: 700;
80
+ color: #111;
81
+ letter-spacing: -0.3px;
82
+ }
83
+ .header-left p {
84
+ font-size: 13px;
85
+ color: #6b7280;
86
+ margin-top: 2px;
87
+ }
88
+ .search-wrap {
89
+ position: relative;
90
+ flex: 0 0 260px;
91
+ }
92
+ .search-wrap svg {
93
+ position: absolute;
94
+ left: 10px;
95
+ top: 50%;
96
+ transform: translateY(-50%);
97
+ color: #9ca3af;
98
+ pointer-events: none;
99
+ }
100
+ #search {
101
+ width: 100%;
102
+ padding: 8px 12px 8px 34px;
103
+ border: 1px solid #d1d5db;
104
+ border-radius: 8px;
105
+ font-size: 13px;
106
+ outline: none;
107
+ background: #f9fafb;
108
+ transition: border-color 0.15s, background 0.15s;
109
+ }
110
+ #search:focus { border-color: #6366f1; background: #fff; }
111
+
112
+ /* ── Grid ── */
113
+ .grid-wrap { padding: 32px 40px; }
114
+ .grid {
115
+ display: grid;
116
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
117
+ gap: 12px;
118
+ }
119
+ .empty-msg {
120
+ display: none;
121
+ grid-column: 1 / -1;
122
+ text-align: center;
123
+ padding: 60px 0;
124
+ color: #9ca3af;
125
+ font-size: 14px;
126
+ }
127
+
128
+ /* ── Icon card ── */
129
+ .icon-card {
130
+ position: relative;
131
+ background: #fff;
132
+ border: 1px solid #e5e7eb;
133
+ border-radius: 10px;
134
+ padding: 20px 12px 14px;
135
+ display: flex;
136
+ flex-direction: column;
137
+ align-items: center;
138
+ gap: 10px;
139
+ cursor: pointer;
140
+ transition: border-color 0.15s, box-shadow 0.15s, transform 0.1s;
141
+ overflow: hidden;
142
+ }
143
+ .icon-card:hover {
144
+ border-color: #6366f1;
145
+ box-shadow: 0 4px 16px rgba(99,102,241,0.12);
146
+ transform: translateY(-1px);
147
+ }
148
+ .icon-preview {
149
+ display: flex;
150
+ align-items: center;
151
+ justify-content: center;
152
+ width: 48px;
153
+ height: 48px;
154
+ }
155
+ .icon-preview i { font-size: 30px; color: #374151; transition: color 0.15s; }
156
+ .icon-card:hover .icon-preview i { color: #6366f1; }
157
+ .icon-info {
158
+ display: flex;
159
+ flex-direction: column;
160
+ align-items: center;
161
+ gap: 3px;
162
+ width: 100%;
163
+ }
164
+ .icon-name {
165
+ font-size: 11px;
166
+ color: #374151;
167
+ font-weight: 500;
168
+ text-align: center;
169
+ word-break: break-all;
170
+ line-height: 1.4;
171
+ max-width: 100%;
172
+ }
173
+ .icon-code {
174
+ font-size: 10px;
175
+ color: #9ca3af;
176
+ font-family: "SF Mono", "Fira Code", monospace;
177
+ }
178
+
179
+ /* ── Copy button ── */
180
+ .copy-btn {
181
+ position: absolute;
182
+ top: 7px;
183
+ right: 7px;
184
+ display: flex;
185
+ align-items: center;
186
+ justify-content: center;
187
+ width: 24px;
188
+ height: 24px;
189
+ background: transparent;
190
+ border: none;
191
+ border-radius: 5px;
192
+ color: #9ca3af;
193
+ cursor: pointer;
194
+ opacity: 0;
195
+ transition: opacity 0.15s, background 0.15s, color 0.15s;
196
+ }
197
+ .icon-card:hover .copy-btn { opacity: 1; }
198
+ .copy-btn:hover { background: #f3f4f6; color: #6366f1; }
199
+
200
+ /* ── Copied badge ── */
201
+ .copied-badge {
202
+ position: absolute;
203
+ inset: 0;
204
+ display: flex;
205
+ align-items: center;
206
+ justify-content: center;
207
+ background: rgba(99,102,241,0.92);
208
+ color: #fff;
209
+ font-size: 12px;
210
+ font-weight: 600;
211
+ border-radius: 10px;
212
+ opacity: 0;
213
+ pointer-events: none;
214
+ transition: opacity 0.15s;
215
+ }
216
+ .icon-card.copied .copied-badge { opacity: 1; }
217
+
218
+ /* ── Footer ── */
219
+ .footer {
220
+ text-align: center;
221
+ padding: 24px;
222
+ color: #9ca3af;
223
+ font-size: 11px;
224
+ border-top: 1px solid #e5e7eb;
225
+ background: #fff;
226
+ }
227
+ </style>
228
+ </head>
229
+ <body>
230
+
231
+ <header class="header">
232
+ <div class="header-left">
233
+ <h1>${FONT_NAME}</h1>
234
+ <p id="count">${glyphs.length} icon${glyphs.length !== 1 ? "s" : ""}</p>
235
+ </div>
236
+ <div class="search-wrap">
237
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
238
+ <circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
239
+ </svg>
240
+ <input id="search" type="text" placeholder="Search icons…" autocomplete="off" />
241
+ </div>
242
+ </header>
243
+
244
+ <main class="grid-wrap">
245
+ <div class="grid" id="grid">
246
+ ${items}
247
+ <div class="empty-msg" id="empty">No icons match your search.</div>
248
+ </div>
249
+ </main>
250
+
251
+ <footer class="footer">Generated by polyicon &nbsp;·&nbsp; ${glyphs.length} icons total</footer>
252
+
253
+ <script>
254
+ const cards = Array.from(document.querySelectorAll('.icon-card'));
255
+ const search = document.getElementById('search');
256
+ const empty = document.getElementById('empty');
257
+ const count = document.getElementById('count');
258
+ const total = ${glyphs.length};
259
+
260
+ // Copy on card click or copy-btn click
261
+ function copyClass(card, e) {
262
+ e.stopPropagation();
263
+ const cls = card.dataset.name;
264
+ navigator.clipboard.writeText(cls).then(() => {
265
+ card.classList.add('copied');
266
+ setTimeout(() => card.classList.remove('copied'), 1200);
267
+ });
268
+ }
269
+
270
+ cards.forEach(card => {
271
+ card.addEventListener('click', e => copyClass(card, e));
272
+ card.querySelector('.copy-btn').addEventListener('click', e => copyClass(card, e));
273
+ });
274
+
275
+ // Search filter
276
+ search.addEventListener('input', () => {
277
+ const q = search.value.trim().toLowerCase();
278
+ let visible = 0;
279
+ cards.forEach(card => {
280
+ const match = !q || card.dataset.name.toLowerCase().includes(q);
281
+ card.style.display = match ? '' : 'none';
282
+ if (match) visible++;
283
+ });
284
+ empty.style.display = visible === 0 ? 'block' : 'none';
285
+ count.textContent = q
286
+ ? visible + ' of ' + total + ' icon' + (total !== 1 ? 's' : '')
287
+ : total + ' icon' + (total !== 1 ? 's' : '');
288
+ });
289
+ </script>
290
+
291
+ </body>
292
+ </html>`;
293
+
294
+ await ensureDir(outputDir);
295
+ await fse.writeFile(
296
+ path.resolve(outputDir, `${FONT_NAME}-preview.html`),
297
+ html,
298
+ "utf8"
299
+ );
300
+ }
301
+
302
+ module.exports = { writeHtml };
@@ -0,0 +1,28 @@
1
+ const { ensureFile, writeJson } = require("../utils/fs");
2
+ const { FONT_NAME } = require("../constants");
3
+
4
+ async function writePolyiconConfig({ outputPath, glyphs, classPrefix }) {
5
+ if (!outputPath) return;
6
+
7
+ const config = {
8
+ name: FONT_NAME,
9
+ css_prefix_text: `${classPrefix}-`,
10
+ css_use_suffix: false,
11
+ hinting: true,
12
+ units_per_em: 1000,
13
+ ascent: 850,
14
+ glyphs: glyphs.map((glyph) => {
15
+ return {
16
+ css: glyph["glyph-name"],
17
+ code: glyph.unicode.codePointAt(0)
18
+ };
19
+ })
20
+ };
21
+
22
+ await ensureFile(outputPath);
23
+ await writeJson(outputPath, config);
24
+ }
25
+
26
+ module.exports = {
27
+ writePolyiconConfig
28
+ };
@@ -0,0 +1,28 @@
1
+ const fse = require("fs-extra");
2
+ const path = require("path");
3
+ const { toConstName } = require("../utils/strings");
4
+ const { ensureFile } = require("../utils/fs");
5
+
6
+ async function writeTypes({ outputTypesPath, glyphs }) {
7
+ const isTypescript = outputTypesPath.endsWith(".ts");
8
+ let types = "";
9
+
10
+ glyphs.forEach((glyph, index) => {
11
+ const line = isTypescript
12
+ ? `\t${toConstName(glyph["glyph-name"])} = '${glyph["glyph-name"]}',`
13
+ : `\t${toConstName(glyph["glyph-name"])}: '${glyph["glyph-name"]}',`;
14
+ types += line;
15
+ if (index < glyphs.length - 1) types += "\n";
16
+ });
17
+
18
+ const content = isTypescript
19
+ ? `enum IconTypes { ${types} } export default IconTypes; `
20
+ : `const IconTypes = { ${types} }; export default IconTypes; `;
21
+
22
+ await ensureFile(outputTypesPath);
23
+ await fse.writeFile(outputTypesPath, content, "utf8");
24
+ }
25
+
26
+ module.exports = {
27
+ writeTypes
28
+ };
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ const { buildIcons } = require("./generator");
2
+
3
+ module.exports = { buildIcons };
@@ -0,0 +1,21 @@
1
+ const fse = require("fs-extra");
2
+
3
+ async function ensureDir(dirPath) {
4
+ if (!dirPath || dirPath === ".") return;
5
+ await fse.ensureDir(dirPath);
6
+ }
7
+
8
+ async function ensureFile(filePath) {
9
+ await fse.ensureFile(filePath);
10
+ }
11
+
12
+ async function writeJson(filePath, data) {
13
+ await fse.ensureFile(filePath);
14
+ await fse.writeJSON(filePath, data);
15
+ }
16
+
17
+ module.exports = {
18
+ ensureDir,
19
+ ensureFile,
20
+ writeJson
21
+ };
@@ -0,0 +1,11 @@
1
+ const path = require("path");
2
+
3
+ function resolveFromCwd(inputPath) {
4
+ if (!inputPath) return inputPath;
5
+ if (path.isAbsolute(inputPath)) return inputPath;
6
+ return path.resolve(process.cwd(), inputPath);
7
+ }
8
+
9
+ module.exports = {
10
+ resolveFromCwd
11
+ };
@@ -0,0 +1,11 @@
1
+ function toConstName(value) {
2
+ let staticName = String(value).toUpperCase();
3
+ while (staticName.indexOf("-") > -1) {
4
+ staticName = staticName.replace("-", "_");
5
+ }
6
+ return staticName;
7
+ }
8
+
9
+ module.exports = {
10
+ toConstName
11
+ };