react-native-nano-icons 0.1.1 → 0.1.3
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/README.md +20 -164
- package/android/build.gradle +28 -0
- package/android/src/main/java/com/nanoicons/NanoIconView.kt +78 -0
- package/android/src/main/java/com/nanoicons/NanoIconViewManager.kt +84 -0
- package/android/src/main/java/com/nanoicons/NanoIconsPackage.kt +22 -0
- package/ios/NanoIconView.h +4 -0
- package/ios/NanoIconView.mm +246 -0
- package/lib/commonjs/cli/build.js +1 -1
- package/lib/commonjs/cli/config.d.ts +2 -2
- package/lib/commonjs/cli/config.js +7 -6
- package/lib/commonjs/plugin/src/buildFonts.d.ts +1 -0
- package/lib/commonjs/plugin/src/buildFonts.js +9 -0
- package/lib/commonjs/plugin/src/index.js +1 -34
- package/lib/commonjs/plugin/src/withNanoIconsFontLinking.d.ts +6 -6
- package/lib/commonjs/plugin/src/withNanoIconsFontLinking.js +11 -15
- package/lib/commonjs/scripts/cli.js +15 -5
- package/lib/commonjs/src/core/font/compile.d.ts +13 -2
- package/lib/commonjs/src/core/font/compile.js +49 -46
- package/lib/commonjs/src/core/pipeline/managers.js +19 -3
- package/lib/commonjs/src/core/pipeline/run.js +121 -32
- package/lib/commonjs/src/core/svg/layers.d.ts +16 -0
- package/lib/commonjs/src/core/svg/layers.js +27 -0
- package/lib/commonjs/src/core/svg/svg_dom.d.ts +29 -1
- package/lib/commonjs/src/core/svg/svg_dom.js +78 -2
- package/lib/commonjs/src/core/svg/svg_pathops.d.ts +11 -0
- package/lib/commonjs/src/core/svg/svg_pathops.js +209 -19
- package/lib/commonjs/src/core/types.d.ts +30 -15
- package/lib/module/core/font/compile.js +52 -41
- package/lib/module/core/font/compile.js.map +1 -1
- package/lib/module/core/pipeline/managers.js +17 -3
- package/lib/module/core/pipeline/managers.js.map +1 -1
- package/lib/module/core/pipeline/run.js +131 -44
- package/lib/module/core/pipeline/run.js.map +1 -1
- package/lib/module/core/shims/picosvg-0.22.3-py3-none-any.whl +0 -0
- package/lib/module/core/svg/layers.js +34 -0
- package/lib/module/core/svg/layers.js.map +1 -1
- package/lib/module/core/svg/svg_dom.js +91 -4
- package/lib/module/core/svg/svg_dom.js.map +1 -1
- package/lib/module/core/svg/svg_pathops.js +203 -19
- package/lib/module/core/svg/svg_pathops.js.map +1 -1
- package/lib/module/createNanoIconsSet.js +3 -79
- package/lib/module/createNanoIconsSet.js.map +1 -1
- package/lib/module/createNanoIconsSet.native.js +68 -0
- package/lib/module/createNanoIconsSet.native.js.map +1 -0
- package/lib/module/createNanoIconsSet.shared.js +91 -0
- package/lib/module/createNanoIconsSet.shared.js.map +1 -0
- package/lib/module/index.js +1 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/specs/NanoIconViewNativeComponent.ts +15 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/shallowEqualColor.js +15 -0
- package/lib/module/utils/shallowEqualColor.js.map +1 -0
- package/lib/typescript/src/core/font/compile.d.ts +13 -2
- package/lib/typescript/src/core/font/compile.d.ts.map +1 -1
- package/lib/typescript/src/core/pipeline/managers.d.ts.map +1 -1
- package/lib/typescript/src/core/pipeline/run.d.ts.map +1 -1
- package/lib/typescript/src/core/svg/layers.d.ts +16 -0
- package/lib/typescript/src/core/svg/layers.d.ts.map +1 -1
- package/lib/typescript/src/core/svg/svg_dom.d.ts +29 -1
- package/lib/typescript/src/core/svg/svg_dom.d.ts.map +1 -1
- package/lib/typescript/src/core/svg/svg_pathops.d.ts +11 -0
- package/lib/typescript/src/core/svg/svg_pathops.d.ts.map +1 -1
- package/lib/typescript/src/core/types.d.ts +30 -15
- package/lib/typescript/src/core/types.d.ts.map +1 -1
- package/lib/typescript/src/createNanoIconsSet.d.ts +5 -18
- package/lib/typescript/src/createNanoIconsSet.d.ts.map +1 -1
- package/lib/typescript/src/createNanoIconsSet.native.d.ts +7 -0
- package/lib/typescript/src/createNanoIconsSet.native.d.ts.map +1 -0
- package/lib/typescript/src/createNanoIconsSet.shared.d.ts +11 -0
- package/lib/typescript/src/createNanoIconsSet.shared.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/NanoIconViewNativeComponent.d.ts +14 -0
- package/lib/typescript/src/specs/NanoIconViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +19 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utils/shallowEqualColor.d.ts +4 -0
- package/lib/typescript/src/utils/shallowEqualColor.d.ts.map +1 -0
- package/package.json +22 -5
- package/plugin/src/buildFonts.ts +13 -0
- package/plugin/src/index.ts +3 -50
- package/plugin/src/withNanoIconsFontLinking.ts +22 -24
- package/react-native-nano-icons.podspec +18 -0
- package/scripts/cli.ts +14 -5
- package/src/core/font/compile.ts +65 -61
- package/src/core/pipeline/managers.ts +26 -3
- package/src/core/pipeline/run.ts +156 -38
- package/src/core/shims/picosvg-0.22.3-py3-none-any.whl +0 -0
- package/src/core/svg/layers.ts +44 -0
- package/src/core/svg/svg_dom.ts +96 -4
- package/src/core/svg/svg_pathops.ts +245 -27
- package/src/core/types.ts +21 -10
- package/src/createNanoIconsSet.native.tsx +108 -0
- package/src/createNanoIconsSet.shared.tsx +121 -0
- package/src/createNanoIconsSet.tsx +7 -126
- package/src/index.ts +1 -2
- package/src/specs/NanoIconViewNativeComponent.ts +15 -0
- package/src/types.ts +27 -0
- package/src/utils/shallowEqualColor.ts +17 -0
|
@@ -7,19 +7,20 @@ exports.loadNanoIconsConfig = loadNanoIconsConfig;
|
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
/**
|
|
10
|
-
* Load .nanoicons.json from the
|
|
10
|
+
* Load .nanoicons.json from the given directory.
|
|
11
11
|
* Throws with a helpful message if the file is missing or malformed.
|
|
12
12
|
*/
|
|
13
|
-
function loadNanoIconsConfig(
|
|
14
|
-
const configPath = node_path_1.default.join(
|
|
13
|
+
function loadNanoIconsConfig(configRoot) {
|
|
14
|
+
const configPath = node_path_1.default.join(configRoot, '.nanoicons.json');
|
|
15
15
|
if (!node_fs_1.default.existsSync(configPath)) {
|
|
16
|
-
throw new Error(
|
|
17
|
-
`Create one with: { "iconSets": [{ "inputDir": "assets/icons", "fontFamily": "MyIcons" }] }`
|
|
16
|
+
throw new Error(`🔬❌ [react-native-nano-icons] No .nanoicons.json found at (${configRoot}).\n` +
|
|
17
|
+
`Create one with: { "iconSets": [{ "inputDir": "assets/icons", "fontFamily": "MyIcons" }] } \n` +
|
|
18
|
+
`Or run with --path <dir> to specify a different directory.`);
|
|
18
19
|
}
|
|
19
20
|
const raw = node_fs_1.default.readFileSync(configPath, 'utf8');
|
|
20
21
|
const config = JSON.parse(raw);
|
|
21
22
|
if (!config?.iconSets?.length) {
|
|
22
|
-
throw new Error(
|
|
23
|
+
throw new Error(`🔬❌ [react-native-nano-icons] .nanoicons.json must contain an "iconSets" array with at least one entry.`);
|
|
23
24
|
}
|
|
24
25
|
return config;
|
|
25
26
|
}
|
|
@@ -5,3 +5,4 @@ import type { IconSetConfig, BuiltFont } from './types.js';
|
|
|
5
5
|
* unless EXPO_DEBUG is set, in which case the full error is re-thrown.
|
|
6
6
|
*/
|
|
7
7
|
export declare function buildAllFonts(iconSets: IconSetConfig[], projectRoot: string): Promise<BuiltFont[]>;
|
|
8
|
+
export declare function getOrBuildFonts(projectRoot: string, iconSets: IconSetConfig[]): Promise<BuiltFont[]>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildAllFonts = buildAllFonts;
|
|
4
|
+
exports.getOrBuildFonts = getOrBuildFonts;
|
|
4
5
|
const index_js_1 = require("../../cli/index.js");
|
|
5
6
|
/**
|
|
6
7
|
* Build TTF + glyphmap for all icon sets.
|
|
@@ -21,3 +22,11 @@ async function buildAllFonts(iconSets, projectRoot) {
|
|
|
21
22
|
return [];
|
|
22
23
|
}
|
|
23
24
|
}
|
|
25
|
+
// Single build run per process; reused across ios/android mods.
|
|
26
|
+
let _buildPromise = null;
|
|
27
|
+
function getOrBuildFonts(projectRoot, iconSets) {
|
|
28
|
+
if (!_buildPromise) {
|
|
29
|
+
_buildPromise = buildAllFonts(iconSets, projectRoot);
|
|
30
|
+
}
|
|
31
|
+
return _buildPromise;
|
|
32
|
+
}
|
|
@@ -1,43 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const config_plugins_1 = require("@expo/config-plugins");
|
|
4
|
-
const buildFonts_js_1 = require("./buildFonts.js");
|
|
5
3
|
const withNanoIconsFontLinking_js_1 = require("./withNanoIconsFontLinking.js");
|
|
6
|
-
const BUILT_FONTS_KEY = '_nanoIconsBuilt';
|
|
7
|
-
// Single Pyodide/PathKit run for the whole prebuild; reused across ios/android mods.
|
|
8
|
-
let _builtFontsCache = null;
|
|
9
|
-
function getOrBuildFonts(projectRoot, iconSets) {
|
|
10
|
-
if (_builtFontsCache)
|
|
11
|
-
return Promise.resolve(_builtFontsCache);
|
|
12
|
-
return (0, buildFonts_js_1.buildAllFonts)(iconSets, projectRoot).then((built) => {
|
|
13
|
-
_builtFontsCache = built;
|
|
14
|
-
return built;
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
4
|
const withNanoIcons = (config, options) => {
|
|
18
5
|
if (!options?.iconSets?.length)
|
|
19
6
|
return config;
|
|
20
|
-
|
|
21
|
-
config = (0, config_plugins_1.withDangerousMod)(config, [
|
|
22
|
-
'ios',
|
|
23
|
-
async (config) => {
|
|
24
|
-
const projectRoot = config.modRequest.projectRoot;
|
|
25
|
-
const built = await getOrBuildFonts(projectRoot, options.iconSets);
|
|
26
|
-
config[BUILT_FONTS_KEY] = built;
|
|
27
|
-
return config;
|
|
28
|
-
},
|
|
29
|
-
]);
|
|
30
|
-
config = (0, config_plugins_1.withDangerousMod)(config, [
|
|
31
|
-
'android',
|
|
32
|
-
async (config) => {
|
|
33
|
-
const projectRoot = config.modRequest.projectRoot;
|
|
34
|
-
const built = await getOrBuildFonts(projectRoot, options.iconSets);
|
|
35
|
-
config[BUILT_FONTS_KEY] = built;
|
|
36
|
-
return config;
|
|
37
|
-
},
|
|
38
|
-
]);
|
|
39
|
-
// Link built TTFs into native projects (reads _nanoIconsBuilt from config).
|
|
40
|
-
config = (0, withNanoIconsFontLinking_js_1.withNanoIconsFontLinking)(config);
|
|
7
|
+
config = (0, withNanoIconsFontLinking_js_1.withNanoIconsFontLinking)(config, options.iconSets);
|
|
41
8
|
return config;
|
|
42
9
|
};
|
|
43
10
|
exports.default = withNanoIcons;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { withXcodeProject, withDangerousMod } from '@expo/config-plugins';
|
|
2
|
+
import type { IconSetConfig } from './types.js';
|
|
2
3
|
/**
|
|
3
4
|
* Add TTFs to the iOS project (Resources group + UIAppFonts in Info.plist).
|
|
4
|
-
* Reads cached built font paths from config._nanoIconsBuilt (set by the build mod) to prevent redundant builds for iOS and Android.
|
|
5
5
|
*/
|
|
6
|
-
export declare function withNanoIconsIos(config: Parameters<typeof withXcodeProject>[0]): ReturnType<typeof withXcodeProject>;
|
|
6
|
+
export declare function withNanoIconsIos(config: Parameters<typeof withXcodeProject>[0], iconSets: IconSetConfig[]): ReturnType<typeof withXcodeProject>;
|
|
7
7
|
/**
|
|
8
|
-
* Copy TTFs to Android assets/fonts.
|
|
8
|
+
* Copy TTFs to Android assets/fonts.
|
|
9
9
|
*/
|
|
10
|
-
export declare function withNanoIconsAndroid(config: Parameters<typeof withDangerousMod>[0]): ReturnType<typeof withDangerousMod>;
|
|
10
|
+
export declare function withNanoIconsAndroid(config: Parameters<typeof withDangerousMod>[0], iconSets: IconSetConfig[]): ReturnType<typeof withDangerousMod>;
|
|
11
11
|
/**
|
|
12
|
-
* Apply iOS and Android font linking.
|
|
12
|
+
* Apply iOS and Android font linking.
|
|
13
13
|
*/
|
|
14
|
-
export declare function withNanoIconsFontLinking(config: Parameters<typeof withNanoIconsIos>[0]): ReturnType<typeof withNanoIconsAndroid>;
|
|
14
|
+
export declare function withNanoIconsFontLinking(config: Parameters<typeof withNanoIconsIos>[0], iconSets: IconSetConfig[]): ReturnType<typeof withNanoIconsAndroid>;
|
|
@@ -9,18 +9,14 @@ exports.withNanoIconsFontLinking = withNanoIconsFontLinking;
|
|
|
9
9
|
const config_plugins_1 = require("@expo/config-plugins");
|
|
10
10
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
11
11
|
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const buildFonts_js_1 = require("./buildFonts.js");
|
|
12
13
|
const ANDROID_ASSETS_FONTS_DIR = 'app/src/main/assets/fonts';
|
|
13
|
-
const BUILT_FONTS_KEY = '_nanoIconsBuilt';
|
|
14
|
-
function getBuiltFonts(config) {
|
|
15
|
-
return config[BUILT_FONTS_KEY];
|
|
16
|
-
}
|
|
17
14
|
/**
|
|
18
15
|
* Add TTFs to the iOS project (Resources group + UIAppFonts in Info.plist).
|
|
19
|
-
* Reads cached built font paths from config._nanoIconsBuilt (set by the build mod) to prevent redundant builds for iOS and Android.
|
|
20
16
|
*/
|
|
21
|
-
function withNanoIconsIos(config) {
|
|
17
|
+
function withNanoIconsIos(config, iconSets) {
|
|
22
18
|
config = (0, config_plugins_1.withXcodeProject)(config, async (config) => {
|
|
23
|
-
const built =
|
|
19
|
+
const built = await (0, buildFonts_js_1.getOrBuildFonts)(config.modRequest.projectRoot, iconSets);
|
|
24
20
|
if (!built?.length)
|
|
25
21
|
return config;
|
|
26
22
|
const ttfPaths = built.map((b) => b.ttfPath);
|
|
@@ -40,7 +36,7 @@ function withNanoIconsIos(config) {
|
|
|
40
36
|
return config;
|
|
41
37
|
});
|
|
42
38
|
config = (0, config_plugins_1.withInfoPlist)(config, async (config) => {
|
|
43
|
-
const built =
|
|
39
|
+
const built = await (0, buildFonts_js_1.getOrBuildFonts)(config.modRequest.projectRoot, iconSets);
|
|
44
40
|
if (!built?.length)
|
|
45
41
|
return config;
|
|
46
42
|
const ttfPaths = built.map((b) => b.ttfPath);
|
|
@@ -62,13 +58,13 @@ function getUIAppFonts(infoPlist) {
|
|
|
62
58
|
return [];
|
|
63
59
|
}
|
|
64
60
|
/**
|
|
65
|
-
* Copy TTFs to Android assets/fonts.
|
|
61
|
+
* Copy TTFs to Android assets/fonts.
|
|
66
62
|
*/
|
|
67
|
-
function withNanoIconsAndroid(config) {
|
|
63
|
+
function withNanoIconsAndroid(config, iconSets) {
|
|
68
64
|
return (0, config_plugins_1.withDangerousMod)(config, [
|
|
69
65
|
'android',
|
|
70
66
|
async (config) => {
|
|
71
|
-
const built =
|
|
67
|
+
const built = await (0, buildFonts_js_1.getOrBuildFonts)(config.modRequest.projectRoot, iconSets);
|
|
72
68
|
if (!built?.length)
|
|
73
69
|
return config;
|
|
74
70
|
const fontsDir = path_1.default.join(config.modRequest.platformProjectRoot, ANDROID_ASSETS_FONTS_DIR);
|
|
@@ -83,10 +79,10 @@ function withNanoIconsAndroid(config) {
|
|
|
83
79
|
]);
|
|
84
80
|
}
|
|
85
81
|
/**
|
|
86
|
-
* Apply iOS and Android font linking.
|
|
82
|
+
* Apply iOS and Android font linking.
|
|
87
83
|
*/
|
|
88
|
-
function withNanoIconsFontLinking(config) {
|
|
89
|
-
config = withNanoIconsIos(config);
|
|
90
|
-
config = withNanoIconsAndroid(config);
|
|
84
|
+
function withNanoIconsFontLinking(config, iconSets) {
|
|
85
|
+
config = withNanoIconsIos(config, iconSets);
|
|
86
|
+
config = withNanoIconsAndroid(config, iconSets);
|
|
91
87
|
return config;
|
|
92
88
|
}
|
|
@@ -1,25 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
3
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
7
|
/**
|
|
5
8
|
* Bare React Native workflow: build icon fonts and link them into the native project.
|
|
6
9
|
*
|
|
7
|
-
* Run from your app root: npx react-native-nano-icons [--verbose]
|
|
10
|
+
* Run from your app root: npx react-native-nano-icons [--verbose] [--path <dir>]
|
|
8
11
|
*
|
|
9
12
|
* Reads .nanoicons.json (same shape as Expo plugin options) so Expo and bare apps
|
|
10
13
|
* share one config format.
|
|
11
14
|
*
|
|
12
15
|
* Flags:
|
|
13
|
-
* --verbose
|
|
16
|
+
* --verbose Show per-SVG processing details and pipeline timing
|
|
17
|
+
* --path <dir> Directory containing .nanoicons.json (default: cwd)
|
|
14
18
|
*/
|
|
19
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
15
20
|
const index_js_1 = require("../cli/index.js");
|
|
16
21
|
async function main() {
|
|
17
22
|
const verbose = process.argv.includes('--verbose');
|
|
18
23
|
const level = verbose ? 'verbose' : 'normal';
|
|
24
|
+
const pathIdx = process.argv.indexOf('--path');
|
|
25
|
+
const projectRoot = process.cwd();
|
|
26
|
+
const configRoot = pathIdx !== -1 && process.argv[pathIdx + 1]
|
|
27
|
+
? node_path_1.default.resolve(projectRoot, process.argv[pathIdx + 1])
|
|
28
|
+
: projectRoot;
|
|
19
29
|
const logger = await (0, index_js_1.createOraLogger)(level);
|
|
20
|
-
const config = (0, index_js_1.loadNanoIconsConfig)(
|
|
21
|
-
const built = await (0, index_js_1.buildAllFonts)(config.iconSets,
|
|
22
|
-
await (0, index_js_1.linkBare)(
|
|
30
|
+
const config = (0, index_js_1.loadNanoIconsConfig)(configRoot);
|
|
31
|
+
const built = await (0, index_js_1.buildAllFonts)(config.iconSets, projectRoot, { logger });
|
|
32
|
+
await (0, index_js_1.linkBare)(projectRoot, built, logger);
|
|
23
33
|
}
|
|
24
34
|
main().catch((err) => {
|
|
25
35
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
1
|
+
export type FontGlyph = {
|
|
2
|
+
codepoint: number;
|
|
3
|
+
advanceWidth: number;
|
|
4
|
+
/** Path data already in font coordinates (Y-up, placement applied). */
|
|
5
|
+
d: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function parseCompileTtfFromGlyphsError(err: unknown, codepointToIcon: Map<number, string>): void;
|
|
8
|
+
/**
|
|
9
|
+
* Compile a TTF font from pre-transformed glyph data.
|
|
10
|
+
* Builds SVG font XML directly (no intermediate files), then converts via svg2ttf.
|
|
11
|
+
*/
|
|
12
|
+
export declare function compileTtfFromGlyphs(opts: {
|
|
13
|
+
glyphs: FontGlyph[];
|
|
3
14
|
outTtfPath: string;
|
|
4
15
|
fontName: string;
|
|
5
16
|
upm: number;
|
|
@@ -3,63 +3,66 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.parseCompileTtfFromGlyphsError = parseCompileTtfFromGlyphsError;
|
|
7
|
+
exports.compileTtfFromGlyphs = compileTtfFromGlyphs;
|
|
7
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
-
const node_events_1 = require("node:events");
|
|
10
10
|
const metrics_js_1 = require("./metrics.js");
|
|
11
11
|
const svg2ttf_1 = __importDefault(require("svg2ttf"));
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// Node's default MaxListeners (10). Font stream errors are handled once below.
|
|
25
|
-
fontStream.write(glyphStream);
|
|
26
|
-
glyphStream.on('end', resolve);
|
|
12
|
+
function escapeXml(s) {
|
|
13
|
+
return s.replace(/&/g, '&').replace(/"/g, '"');
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Build an SVG font XML string from pre-transformed glyph data.
|
|
17
|
+
*/
|
|
18
|
+
function buildSvgFontXml(opts) {
|
|
19
|
+
const { fontName, glyphs, upm, ascent, descent } = opts;
|
|
20
|
+
const glyphLines = glyphs.map((g) => {
|
|
21
|
+
const hex = g.codepoint.toString(16);
|
|
22
|
+
const name = `u${hex.padStart(4, '0')}`;
|
|
23
|
+
return `<glyph glyph-name="${name}" unicode="&#x${hex};" horiz-adv-x="${g.advanceWidth}" d="${escapeXml(g.d)}"/>`;
|
|
27
24
|
});
|
|
25
|
+
return `<?xml version="1.0" standalone="no"?>
|
|
26
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
27
|
+
<svg xmlns="http://www.w3.org/2000/svg">
|
|
28
|
+
<defs>
|
|
29
|
+
<font id="${escapeXml(fontName)}" horiz-adv-x="${upm}">
|
|
30
|
+
<font-face font-family="${escapeXml(fontName)}" units-per-em="${upm}" ascent="${ascent}" descent="${-Math.abs(descent)}"/>
|
|
31
|
+
<missing-glyph horiz-adv-x="0"/>
|
|
32
|
+
${glyphLines.join('\n')}
|
|
33
|
+
</font>
|
|
34
|
+
</defs>
|
|
35
|
+
</svg>`;
|
|
36
|
+
}
|
|
37
|
+
function parseCompileTtfFromGlyphsError(err, codepointToIcon) {
|
|
38
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
39
|
+
const cpMatch = msg.match(/glyph\s+"u([0-9a-fA-F]+)"/);
|
|
40
|
+
if (cpMatch) {
|
|
41
|
+
const cp = parseInt(cpMatch[1], 16);
|
|
42
|
+
const iconName = codepointToIcon.get(cp);
|
|
43
|
+
const detail = iconName
|
|
44
|
+
? `icon "${iconName}" (codepoint u${cpMatch[1]})`
|
|
45
|
+
: `codepoint u${cpMatch[1]}`;
|
|
46
|
+
throw new Error(`Font compilation failed for ${detail}: ${msg}`);
|
|
47
|
+
}
|
|
48
|
+
throw err;
|
|
28
49
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Compile a TTF font from pre-transformed glyph data.
|
|
52
|
+
* Builds SVG font XML directly (no intermediate files), then converts via svg2ttf.
|
|
53
|
+
*/
|
|
54
|
+
async function compileTtfFromGlyphs(opts) {
|
|
55
|
+
const { glyphs, outTtfPath, fontName, upm, ascent, descent } = opts;
|
|
32
56
|
const lineGap = opts.lineGap ?? 0;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.sort((a, b) => (0, parse_js_1.parseCodepointFromFilename)(a) - (0, parse_js_1.parseCodepointFromFilename)(b));
|
|
37
|
-
if (files.length === 0)
|
|
38
|
-
throw new Error(`No glyph SVGs found in: ${glyphDir}`);
|
|
39
|
-
const fontStream = new SVGIcons2SVGFontStream({
|
|
57
|
+
if (glyphs.length === 0)
|
|
58
|
+
throw new Error('No glyphs to compile');
|
|
59
|
+
const svgFontString = buildSvgFontXml({
|
|
40
60
|
fontName,
|
|
41
|
-
|
|
42
|
-
|
|
61
|
+
glyphs,
|
|
62
|
+
upm,
|
|
43
63
|
ascent,
|
|
44
64
|
descent,
|
|
45
65
|
});
|
|
46
|
-
const svgFontChunks = [];
|
|
47
|
-
fontStream.on('data', (c) => svgFontChunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c)));
|
|
48
|
-
// Single error listener for the font stream; per-glyph errors are handled in writeGlyphStreamToFont.
|
|
49
|
-
let fontStreamReject;
|
|
50
|
-
const fontStreamErrorPromise = new Promise((_, rej) => {
|
|
51
|
-
fontStreamReject = rej;
|
|
52
|
-
});
|
|
53
|
-
fontStream.on('error', (err) => fontStreamReject(err));
|
|
54
|
-
for (const f of files) {
|
|
55
|
-
await Promise.race([
|
|
56
|
-
writeGlyphStreamToFont(fontStream, node_path_1.default.join(glyphDir, f), f),
|
|
57
|
-
fontStreamErrorPromise,
|
|
58
|
-
]);
|
|
59
|
-
}
|
|
60
|
-
fontStream.end();
|
|
61
|
-
await (0, node_events_1.once)(fontStream, 'end');
|
|
62
|
-
const svgFontString = Buffer.concat(svgFontChunks).toString('utf8');
|
|
63
66
|
const ttfRaw = (0, svg2ttf_1.default)(svgFontString);
|
|
64
67
|
const rawBuf = Buffer.from(ttfRaw.buffer);
|
|
65
68
|
const fixedBuf = (0, metrics_js_1.forceTtfMetrics)(rawBuf, upm, ascent, descent, lineGap);
|
|
@@ -56,14 +56,30 @@ class PyodideManager {
|
|
|
56
56
|
const pathopsPy = await promises_1.default.readFile(pathopsPyPath, 'utf8');
|
|
57
57
|
py.FS.writeFile('/pathops.py', pathopsPy);
|
|
58
58
|
await py.loadPackage(['micropip', 'lxml'], { messageCallback: () => { } });
|
|
59
|
+
// Resolve local picosvg wheel path for offline-first installation.
|
|
60
|
+
const picosvgWhlDir = node_path_1.default.join(getPackageRoot(), 'src', 'core', 'shims');
|
|
61
|
+
const picosvgWhl = (await promises_1.default.readdir(picosvgWhlDir))
|
|
62
|
+
.find((f) => f.startsWith('picosvg-') && f.endsWith('.whl'));
|
|
63
|
+
const localWhlUrl = picosvgWhl
|
|
64
|
+
? `file://${node_path_1.default.join(picosvgWhlDir, picosvgWhl)}`
|
|
65
|
+
: null;
|
|
66
|
+
py.globals.set('_picosvg_local_whl', localWhlUrl);
|
|
59
67
|
await py.runPythonAsync(`
|
|
60
68
|
import sys
|
|
61
69
|
if "/" not in sys.path:
|
|
62
70
|
sys.path.insert(0, "/")
|
|
63
|
-
|
|
71
|
+
|
|
64
72
|
import micropip
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
|
|
74
|
+
_whl = _picosvg_local_whl
|
|
75
|
+
if _whl:
|
|
76
|
+
try:
|
|
77
|
+
await micropip.install(_whl, deps=False)
|
|
78
|
+
except Exception:
|
|
79
|
+
await micropip.install("picosvg", deps=False)
|
|
80
|
+
else:
|
|
81
|
+
await micropip.install("picosvg", deps=False)
|
|
82
|
+
|
|
67
83
|
import pathops
|
|
68
84
|
import picosvg
|
|
69
85
|
`);
|
|
@@ -4,7 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.runPipeline = runPipeline;
|
|
7
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
7
|
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
9
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
9
|
const compile_js_1 = require("../font/compile.js");
|
|
@@ -12,6 +11,64 @@ const managers_js_1 = require("./managers.js");
|
|
|
12
11
|
const config_js_1 = require("./config.js");
|
|
13
12
|
const svg_dom_js_1 = require("../svg/svg_dom.js");
|
|
14
13
|
const layers_js_1 = require("../svg/layers.js");
|
|
14
|
+
const svg_pathops_js_1 = require("../svg/svg_pathops.js");
|
|
15
|
+
/**
|
|
16
|
+
* Concatenate multiple SVG path `d` strings into a single compound path.
|
|
17
|
+
* This preserves the exact geometry of each path (no boolean operations)
|
|
18
|
+
* while combining them into one glyph. Under nonzero winding, this renders
|
|
19
|
+
* identically to drawing each path separately with the same color.
|
|
20
|
+
*/
|
|
21
|
+
function concatPathDs(ds) {
|
|
22
|
+
if (ds.length === 0)
|
|
23
|
+
return null;
|
|
24
|
+
if (ds.length === 1)
|
|
25
|
+
return ds[0];
|
|
26
|
+
return ds.join(' ');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Merge consecutive same-color paths into single compound paths via boolean UNION.
|
|
30
|
+
* Preserves z-order: only merges runs of adjacent paths with identical fill color.
|
|
31
|
+
*/
|
|
32
|
+
function mergeSameColorPaths(paths, logger) {
|
|
33
|
+
if (paths.length <= 1)
|
|
34
|
+
return paths;
|
|
35
|
+
const result = [];
|
|
36
|
+
let i = 0;
|
|
37
|
+
while (i < paths.length) {
|
|
38
|
+
const fill = paths[i].fill;
|
|
39
|
+
// Find consecutive run of same fill that are all mergeable.
|
|
40
|
+
// Paths converted from evenodd have compound hole structure and must not
|
|
41
|
+
// be merged — their CW hole contours would cancel CCW contours from
|
|
42
|
+
// adjacent paths, producing incorrect fill.
|
|
43
|
+
let j = i + 1;
|
|
44
|
+
if (!paths[i].noMerge) {
|
|
45
|
+
while (j < paths.length &&
|
|
46
|
+
paths[j].fill === fill &&
|
|
47
|
+
!paths[j].noMerge) {
|
|
48
|
+
j++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (j - i === 1) {
|
|
52
|
+
result.push(paths[i]);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const group = paths.slice(i, j);
|
|
56
|
+
const merged = concatPathDs(group.map((p) => p.d));
|
|
57
|
+
if (merged) {
|
|
58
|
+
logger?.info(` ⊕ Merged ${group.length} same-color paths (fill=${fill})`);
|
|
59
|
+
result.push({ d: merged, fill });
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
result.push(...group);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
i = j;
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Pipeline
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
15
72
|
/**
|
|
16
73
|
* Run the font pipeline with given config and paths.
|
|
17
74
|
* Uses the singleton Pyodide/PathKit instance (initialized on first call).
|
|
@@ -20,19 +77,21 @@ async function runPipeline(config, paths, options) {
|
|
|
20
77
|
const startTime = Date.now();
|
|
21
78
|
const logger = options?.logger;
|
|
22
79
|
logger?.update(`Building "${config.fontFamily}"…`);
|
|
23
|
-
(0, config_js_1.ensureEmptyDir)(paths.tempDir);
|
|
24
80
|
(0, config_js_1.ensureDir)(paths.outputDir);
|
|
25
81
|
const files = (await promises_1.default.readdir(paths.inputDir)).filter((f) => f.toLowerCase().endsWith('.svg'));
|
|
26
82
|
const glyphMap = {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
83
|
+
m: {
|
|
84
|
+
f: config.fontFamily,
|
|
85
|
+
u: config.upm,
|
|
86
|
+
z: config.safeZone,
|
|
87
|
+
s: config.startUnicode,
|
|
32
88
|
},
|
|
33
|
-
|
|
89
|
+
i: {},
|
|
34
90
|
};
|
|
35
91
|
let currentUnicode = config.startUnicode;
|
|
92
|
+
const codepointToIcon = new Map();
|
|
93
|
+
const allGlyphs = [];
|
|
94
|
+
const PathKit = await managers_js_1.PathKitManager.getInstance();
|
|
36
95
|
for (const file of files) {
|
|
37
96
|
const iconName = node_path_1.default.parse(file).name;
|
|
38
97
|
const filePath = node_path_1.default.join(paths.inputDir, file);
|
|
@@ -44,55 +103,85 @@ async function runPipeline(config, paths, options) {
|
|
|
44
103
|
continue;
|
|
45
104
|
}
|
|
46
105
|
const preprocessed = (0, svg_dom_js_1.preprocessSvg)(rawContent);
|
|
106
|
+
// Preserve original evenodd `d` strings BEFORE picosvg processes them.
|
|
107
|
+
// Picosvg's simplify (via our PathKit shim) can drop contours from
|
|
108
|
+
// multi-subpath evenodd paths — we restore the originals after.
|
|
109
|
+
const originalEvenoddDs = (0, svg_dom_js_1.extractOriginalEvenoddDs)(preprocessed);
|
|
47
110
|
const flattenedSvg = await (0, managers_js_1.picoFromFile)(filePath, preprocessed);
|
|
48
|
-
const parsed = (0, svg_dom_js_1.parseFlattenedSvg)(flattenedSvg
|
|
111
|
+
const parsed = (0, svg_dom_js_1.parseFlattenedSvg)(flattenedSvg, {
|
|
112
|
+
onSanitize: (original) => {
|
|
113
|
+
logger?.info(` ⚠ Sanitized path in "${file}": path was missing initial moveto (prepended M from endpoint)`);
|
|
114
|
+
logger?.info(` Original: ${original.slice(0, 80)}…`);
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
// Restore original evenodd path data (undamaged by picosvg's simplify),
|
|
118
|
+
// then convert to nonzero winding with our containment-based algorithm.
|
|
119
|
+
// Mark as noMerge — compound paths with holes must stay separate so their
|
|
120
|
+
// CW hole contours don't cancel adjacent paths' CCW contours.
|
|
121
|
+
if (originalEvenoddDs.length > 0) {
|
|
122
|
+
(0, svg_dom_js_1.restoreOriginalEvenoddDs)(parsed.paths, originalEvenoddDs);
|
|
123
|
+
}
|
|
124
|
+
for (const p of parsed.paths) {
|
|
125
|
+
if (p.fillRule === 'evenodd') {
|
|
126
|
+
logger?.info(` ↻ Converting evenodd path to nonzero winding in "${file}"`);
|
|
127
|
+
p.d = (0, svg_pathops_js_1.convertEvenoddToWinding)(PathKit, p.d);
|
|
128
|
+
delete p.fillRule;
|
|
129
|
+
p.noMerge = true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Merge consecutive same-color paths into single compound glyphs
|
|
133
|
+
const mergedPaths = mergeSameColorPaths(parsed.paths, logger);
|
|
49
134
|
const { vx, vy, scale, xOff, yOff, adv } = (0, layers_js_1.computePlacement)({
|
|
50
135
|
upm: config.upm,
|
|
51
136
|
safeZone: config.safeZone,
|
|
52
137
|
viewBox: parsed.viewBox,
|
|
53
138
|
});
|
|
54
|
-
const
|
|
55
|
-
for (const p of
|
|
139
|
+
const layers = [];
|
|
140
|
+
for (const p of mergedPaths) {
|
|
56
141
|
if ((0, svg_dom_js_1.shouldSkipPath)(p.d, p.fill))
|
|
57
142
|
continue;
|
|
58
143
|
const cp = currentUnicode++;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
upm: config.upm,
|
|
62
|
-
adv,
|
|
144
|
+
codepointToIcon.set(cp, iconName);
|
|
145
|
+
const fontD = (0, layers_js_1.transformPathForFont)(PathKit, p.d, {
|
|
63
146
|
vx,
|
|
64
147
|
vy,
|
|
65
148
|
scale,
|
|
66
149
|
xOff,
|
|
67
150
|
yOff,
|
|
68
|
-
|
|
151
|
+
upm: config.upm,
|
|
152
|
+
});
|
|
153
|
+
allGlyphs.push({
|
|
69
154
|
codepoint: cp,
|
|
155
|
+
advanceWidth: adv,
|
|
156
|
+
d: fontD,
|
|
70
157
|
});
|
|
71
|
-
|
|
158
|
+
layers.push([cp, p.fill || 'black']);
|
|
72
159
|
}
|
|
73
|
-
if (
|
|
74
|
-
glyphMap.
|
|
160
|
+
if (layers.length > 0) {
|
|
161
|
+
glyphMap.i[iconName] = [adv, layers];
|
|
75
162
|
}
|
|
76
163
|
}
|
|
77
164
|
const glyphmapPath = node_path_1.default.join(paths.outputDir, `${config.fontFamily}.glyphmap.json`);
|
|
78
165
|
if (options?.inputHash) {
|
|
79
|
-
glyphMap.
|
|
166
|
+
glyphMap.m.h = options.inputHash;
|
|
80
167
|
}
|
|
81
|
-
await promises_1.default.writeFile(glyphmapPath, JSON.stringify(glyphMap
|
|
168
|
+
await promises_1.default.writeFile(glyphmapPath, JSON.stringify(glyphMap), 'utf8');
|
|
82
169
|
logger?.info(`Compiling TTF…`);
|
|
83
170
|
const ttfPath = node_path_1.default.join(paths.outputDir, `${config.fontFamily}.ttf`);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
171
|
+
try {
|
|
172
|
+
await (0, compile_js_1.compileTtfFromGlyphs)({
|
|
173
|
+
glyphs: allGlyphs,
|
|
174
|
+
outTtfPath: ttfPath,
|
|
175
|
+
fontName: config.fontFamily,
|
|
176
|
+
upm: config.upm,
|
|
177
|
+
ascent: config.upm,
|
|
178
|
+
descent: 0,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
(0, compile_js_1.parseCompileTtfFromGlyphsError)(err, codepointToIcon);
|
|
94
183
|
}
|
|
95
|
-
const iconCount = Object.keys(glyphMap.
|
|
184
|
+
const iconCount = Object.keys(glyphMap.i).length;
|
|
96
185
|
const elapsed = Date.now() - startTime;
|
|
97
186
|
logger?.succeed(`Built ${config.fontFamily}.ttf [${iconCount} icon${iconCount === 1 ? '' : 's'} in ${elapsed}ms]`);
|
|
98
187
|
return { ttfPath, glyphmapPath };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PathKitModule } from '../types.js';
|
|
1
2
|
export declare function computePlacement(opts: {
|
|
2
3
|
upm: number;
|
|
3
4
|
safeZone: number;
|
|
@@ -22,3 +23,18 @@ export declare function writeLayerSvg(opts: {
|
|
|
22
23
|
d: string;
|
|
23
24
|
codepoint: number;
|
|
24
25
|
}): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Transform an SVG path `d` string from source SVG coordinates into font
|
|
28
|
+
* glyph coordinates (Y-up, with placement scaling and centering applied).
|
|
29
|
+
*
|
|
30
|
+
* Combines placement transform (translate + scale + translate) with the
|
|
31
|
+
* SVG→font Y-axis flip into a single affine transform applied via PathKit.
|
|
32
|
+
*/
|
|
33
|
+
export declare function transformPathForFont(PathKit: PathKitModule, d: string, opts: {
|
|
34
|
+
vx: number;
|
|
35
|
+
vy: number;
|
|
36
|
+
scale: number;
|
|
37
|
+
xOff: number;
|
|
38
|
+
yOff: number;
|
|
39
|
+
upm: number;
|
|
40
|
+
}): string;
|