react-native-nano-icons 0.1.2 → 0.1.4
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 +286 -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/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 +108 -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/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 +140 -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
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#import "NanoIconView.h"
|
|
2
|
+
#import <CoreText/CoreText.h>
|
|
3
|
+
#import <React/RCTConversions.h>
|
|
4
|
+
#import <React/RCTFabricComponentsPlugins.h>
|
|
5
|
+
#import <react/renderer/components/RNNanoIconsSpec/ComponentDescriptors.h>
|
|
6
|
+
#import <react/renderer/components/RNNanoIconsSpec/Props.h>
|
|
7
|
+
|
|
8
|
+
using namespace facebook::react;
|
|
9
|
+
|
|
10
|
+
// Drawing canvas that can be shifted outside the Yoga frame.
|
|
11
|
+
@interface NanoIconDrawingView : UIView
|
|
12
|
+
@property (nonatomic, copy) void (^drawBlock)(CGContextRef, CGRect);
|
|
13
|
+
@end
|
|
14
|
+
|
|
15
|
+
@implementation NanoIconDrawingView
|
|
16
|
+
- (instancetype)initWithFrame:(CGRect)frame {
|
|
17
|
+
if (self = [super initWithFrame:frame]) {
|
|
18
|
+
self.opaque = NO;
|
|
19
|
+
self.backgroundColor = [UIColor clearColor];
|
|
20
|
+
self.userInteractionEnabled = NO;
|
|
21
|
+
}
|
|
22
|
+
return self;
|
|
23
|
+
}
|
|
24
|
+
- (void)drawRect:(CGRect)rect {
|
|
25
|
+
if (self.drawBlock) {
|
|
26
|
+
CGContextRef ctx = UIGraphicsGetCurrentContext();
|
|
27
|
+
if (ctx) self.drawBlock(ctx, self.bounds);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
@end
|
|
31
|
+
|
|
32
|
+
@implementation NanoIconView {
|
|
33
|
+
CTFontRef _font;
|
|
34
|
+
NSString *_fontFamily;
|
|
35
|
+
CGFloat _fontSize;
|
|
36
|
+
std::vector<CGGlyph> _glyphs;
|
|
37
|
+
std::vector<uint32_t> _colors;
|
|
38
|
+
std::vector<CGColorRef> _cachedCGColors;
|
|
39
|
+
CGFloat _fitScale;
|
|
40
|
+
CGPoint _baselinePosition;
|
|
41
|
+
BOOL _metricsValid;
|
|
42
|
+
NanoIconDrawingView *_drawingView;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
- (instancetype)initWithFrame:(CGRect)frame {
|
|
46
|
+
if (self = [super initWithFrame:frame]) {
|
|
47
|
+
static const auto defaultProps = std::make_shared<const NanoIconViewProps>();
|
|
48
|
+
_props = defaultProps;
|
|
49
|
+
self.opaque = NO;
|
|
50
|
+
self.backgroundColor = [UIColor clearColor];
|
|
51
|
+
self.clipsToBounds = NO;
|
|
52
|
+
|
|
53
|
+
_fitScale = 1.0;
|
|
54
|
+
_baselinePosition = CGPointZero;
|
|
55
|
+
|
|
56
|
+
_drawingView = [[NanoIconDrawingView alloc] initWithFrame:self.bounds];
|
|
57
|
+
__weak __typeof(self) weakSelf = self;
|
|
58
|
+
_drawingView.drawBlock = ^(CGContextRef context, CGRect bounds) {
|
|
59
|
+
[weakSelf _drawIconInContext:context bounds:bounds];
|
|
60
|
+
};
|
|
61
|
+
[self addSubview:_drawingView];
|
|
62
|
+
}
|
|
63
|
+
return self;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
67
|
+
return concreteComponentDescriptorProvider<NanoIconViewComponentDescriptor>();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
- (void)updateClippedSubviewsWithClipRect:(__unused CGRect)clipRect
|
|
71
|
+
relativeToView:(__unused UIView *)clipView {}
|
|
72
|
+
|
|
73
|
+
#pragma mark - Metrics
|
|
74
|
+
|
|
75
|
+
// Scale factor to fit the icon font's em square into the view height,
|
|
76
|
+
// and the CoreText baseline origin used for all glyph draws.
|
|
77
|
+
- (void)_updateMetrics {
|
|
78
|
+
if (!_font) {
|
|
79
|
+
_metricsValid = NO;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
CGFloat ascent = CTFontGetAscent(_font);
|
|
83
|
+
CGFloat descent = CTFontGetDescent(_font);
|
|
84
|
+
CGFloat totalHeight = ascent + descent;
|
|
85
|
+
_fitScale = (totalHeight > 0) ? (self.bounds.size.height / totalHeight) : 1.0;
|
|
86
|
+
_baselinePosition = CGPointMake(0, descent);
|
|
87
|
+
_metricsValid = YES;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Distance from this view's bottom edge to the parent text baseline.
|
|
91
|
+
// Returns 0 when standalone or when the icon is taller than the text line.
|
|
92
|
+
- (CGFloat)_inlineBaselineOffset {
|
|
93
|
+
UIView *current = self.superview;
|
|
94
|
+
NSAttributedString *attrStr = nil;
|
|
95
|
+
while (current) {
|
|
96
|
+
if ([current respondsToSelector:@selector(attributedText)]) {
|
|
97
|
+
attrStr = [current performSelector:@selector(attributedText)];
|
|
98
|
+
if (attrStr.length > 0) break;
|
|
99
|
+
}
|
|
100
|
+
current = current.superview;
|
|
101
|
+
}
|
|
102
|
+
if (!attrStr) return 0;
|
|
103
|
+
|
|
104
|
+
UIFont *f = [attrStr attribute:NSFontAttributeName atIndex:0 effectiveRange:nil];
|
|
105
|
+
if (!f) return 0;
|
|
106
|
+
|
|
107
|
+
NSParagraphStyle *style = [attrStr attribute:NSParagraphStyleAttributeName
|
|
108
|
+
atIndex:0 effectiveRange:nil];
|
|
109
|
+
CGFloat lineHeight = (style && style.maximumLineHeight > 0)
|
|
110
|
+
? style.maximumLineHeight : f.lineHeight;
|
|
111
|
+
|
|
112
|
+
NSNumber *bOff = [attrStr attribute:NSBaselineOffsetAttributeName
|
|
113
|
+
atIndex:0 effectiveRange:nil];
|
|
114
|
+
CGFloat baselineFromLineTop = f.ascender - (bOff ? bOff.doubleValue : 0);
|
|
115
|
+
|
|
116
|
+
CGFloat frameBottom = self.frame.origin.y + self.frame.size.height;
|
|
117
|
+
CGFloat posInLine = fmod(frameBottom, lineHeight);
|
|
118
|
+
if (posInLine < 0.01) posInLine = lineHeight;
|
|
119
|
+
|
|
120
|
+
return MAX(0, posInLine - baselineFromLineTop);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#pragma mark - Layout
|
|
124
|
+
|
|
125
|
+
- (void)layoutSubviews {
|
|
126
|
+
[super layoutSubviews];
|
|
127
|
+
if (!_metricsValid) [self _updateMetrics];
|
|
128
|
+
|
|
129
|
+
CGFloat offset = [self _inlineBaselineOffset];
|
|
130
|
+
_drawingView.frame = CGRectMake(0, -offset,
|
|
131
|
+
self.bounds.size.width, self.bounds.size.height);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#pragma mark - Drawing
|
|
135
|
+
|
|
136
|
+
// Render multi-color icons by drawing each color layer glyph at the same
|
|
137
|
+
// position. Layers stack via painter's order to compose the final icon.
|
|
138
|
+
- (void)_drawIconInContext:(CGContextRef)context bounds:(CGRect)bounds {
|
|
139
|
+
if (!_font || _glyphs.empty()) return;
|
|
140
|
+
if (!_metricsValid) [self _updateMetrics];
|
|
141
|
+
|
|
142
|
+
CGContextSaveGState(context);
|
|
143
|
+
// Flip to CoreText coordinates (Y-up) and apply fit scale.
|
|
144
|
+
CGContextTranslateCTM(context, 0, bounds.size.height);
|
|
145
|
+
CGContextScaleCTM(context, 1.0, -1.0);
|
|
146
|
+
CGContextScaleCTM(context, _fitScale, _fitScale);
|
|
147
|
+
|
|
148
|
+
size_t i = 0;
|
|
149
|
+
while (i < _glyphs.size()) {
|
|
150
|
+
if (_glyphs[i] == 0) { i++; continue; }
|
|
151
|
+
|
|
152
|
+
CGColorRef color = (i < _cachedCGColors.size()) ? _cachedCGColors[i] : NULL;
|
|
153
|
+
if (!color) {
|
|
154
|
+
static CGColorRef sBlack = CGColorCreateSRGB(0, 0, 0, 1);
|
|
155
|
+
color = sBlack;
|
|
156
|
+
}
|
|
157
|
+
CGContextSetFillColorWithColor(context, color);
|
|
158
|
+
|
|
159
|
+
// Batch consecutive same-color glyphs.
|
|
160
|
+
size_t batchStart = i;
|
|
161
|
+
size_t batchCount = 0;
|
|
162
|
+
CGPoint posBuf[16];
|
|
163
|
+
CGGlyph glyphBuf[16];
|
|
164
|
+
|
|
165
|
+
while (i < _glyphs.size()) {
|
|
166
|
+
if (_glyphs[i] == 0) { i++; continue; }
|
|
167
|
+
CGColorRef next = (i < _cachedCGColors.size()) ? _cachedCGColors[i] : NULL;
|
|
168
|
+
if (i > batchStart && next != color) break;
|
|
169
|
+
if (batchCount < 16) {
|
|
170
|
+
posBuf[batchCount] = _baselinePosition;
|
|
171
|
+
glyphBuf[batchCount] = _glyphs[i];
|
|
172
|
+
}
|
|
173
|
+
batchCount++;
|
|
174
|
+
i++;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
CGPoint *positions = posBuf;
|
|
178
|
+
CGGlyph *glyphs = glyphBuf;
|
|
179
|
+
if (batchCount > 16) {
|
|
180
|
+
positions = (CGPoint *)malloc(batchCount * sizeof(CGPoint));
|
|
181
|
+
glyphs = (CGGlyph *)malloc(batchCount * sizeof(CGGlyph));
|
|
182
|
+
size_t idx = 0;
|
|
183
|
+
for (size_t j = batchStart; j < i; j++) {
|
|
184
|
+
if (_glyphs[j] == 0) continue;
|
|
185
|
+
positions[idx] = _baselinePosition;
|
|
186
|
+
glyphs[idx] = _glyphs[j];
|
|
187
|
+
idx++;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
CTFontDrawGlyphs(_font, glyphs, positions, batchCount, context);
|
|
192
|
+
|
|
193
|
+
if (batchCount > 16) {
|
|
194
|
+
free(positions);
|
|
195
|
+
free(glyphs);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
CGContextRestoreGState(context);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#pragma mark - Props
|
|
203
|
+
|
|
204
|
+
- (void)_releaseCachedColors {
|
|
205
|
+
for (CGColorRef c : _cachedCGColors) CGColorRelease(c);
|
|
206
|
+
_cachedCGColors.clear();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Convert ARGB uint32 color values into cached CGColorRefs.
|
|
210
|
+
- (void)_rebuildCachedColors {
|
|
211
|
+
[self _releaseCachedColors];
|
|
212
|
+
_cachedCGColors.resize(_colors.size());
|
|
213
|
+
for (size_t i = 0; i < _colors.size(); i++) {
|
|
214
|
+
uint32_t ci = _colors[i];
|
|
215
|
+
_cachedCGColors[i] = CGColorCreateSRGB(
|
|
216
|
+
((ci >> 16) & 0xFF) / 255.0,
|
|
217
|
+
((ci >> 8) & 0xFF) / 255.0,
|
|
218
|
+
( ci & 0xFF) / 255.0,
|
|
219
|
+
((ci >> 24) & 0xFF) / 255.0);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps {
|
|
224
|
+
const auto &oldViewProps = static_cast<const NanoIconViewProps &>(*_props);
|
|
225
|
+
const auto &newViewProps = static_cast<const NanoIconViewProps &>(*props);
|
|
226
|
+
|
|
227
|
+
BOOL fontChanged = NO;
|
|
228
|
+
BOOL needsRedraw = NO;
|
|
229
|
+
|
|
230
|
+
if (oldViewProps.fontFamily != newViewProps.fontFamily ||
|
|
231
|
+
oldViewProps.fontSize != newViewProps.fontSize) {
|
|
232
|
+
if (_font) { CFRelease(_font); _font = NULL; }
|
|
233
|
+
_fontFamily = [NSString stringWithUTF8String:newViewProps.fontFamily.c_str()];
|
|
234
|
+
_fontSize = newViewProps.fontSize;
|
|
235
|
+
_font = CTFontCreateWithName((__bridge CFStringRef)_fontFamily, _fontSize, NULL);
|
|
236
|
+
_metricsValid = NO;
|
|
237
|
+
fontChanged = YES;
|
|
238
|
+
needsRedraw = YES;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Map Unicode codepoints to font glyph IDs, handling surrogate pairs for codepoints > 0xFFFF.
|
|
242
|
+
if (fontChanged || oldViewProps.codepoints != newViewProps.codepoints) {
|
|
243
|
+
const auto &codepoints = newViewProps.codepoints;
|
|
244
|
+
_glyphs.resize(codepoints.size());
|
|
245
|
+
for (size_t i = 0; i < codepoints.size(); i++) {
|
|
246
|
+
int32_t cp = codepoints[i];
|
|
247
|
+
if (cp <= 0xFFFF) {
|
|
248
|
+
UniChar ch = (UniChar)cp;
|
|
249
|
+
CTFontGetGlyphsForCharacters(_font, &ch, &_glyphs[i], 1);
|
|
250
|
+
} else {
|
|
251
|
+
UniChar surr[2] = {
|
|
252
|
+
(UniChar)(0xD800 + ((cp - 0x10000) >> 10)),
|
|
253
|
+
(UniChar)(0xDC00 + ((cp - 0x10000) & 0x3FF))
|
|
254
|
+
};
|
|
255
|
+
CGGlyph pair[2] = {0, 0};
|
|
256
|
+
CTFontGetGlyphsForCharacters(_font, surr, pair, 2);
|
|
257
|
+
_glyphs[i] = pair[0];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
needsRedraw = YES;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (oldViewProps.colors != newViewProps.colors) {
|
|
264
|
+
const auto &colors = newViewProps.colors;
|
|
265
|
+
_colors.resize(colors.size());
|
|
266
|
+
for (size_t i = 0; i < colors.size(); i++) {
|
|
267
|
+
_colors[i] = (uint32_t)colors[i];
|
|
268
|
+
}
|
|
269
|
+
[self _rebuildCachedColors];
|
|
270
|
+
needsRedraw = YES;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
[super updateProps:props oldProps:oldProps];
|
|
274
|
+
if (needsRedraw) [_drawingView setNeedsDisplay];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
- (void)dealloc {
|
|
278
|
+
if (_font) CFRelease(_font);
|
|
279
|
+
[self _releaseCachedColors];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@end
|
|
283
|
+
|
|
284
|
+
Class<RCTComponentViewProtocol> NanoIconViewCls(void) {
|
|
285
|
+
return NanoIconView.class;
|
|
286
|
+
}
|
|
@@ -20,7 +20,7 @@ function shouldSkipGeneration(inputHash, outputDir, fontFamily, logger) {
|
|
|
20
20
|
return false;
|
|
21
21
|
}
|
|
22
22
|
const glyphmap = JSON.parse(fs_1.default.readFileSync(glyphmapPath, 'utf8'));
|
|
23
|
-
const storedHash = glyphmap?.
|
|
23
|
+
const storedHash = glyphmap?.m?.h;
|
|
24
24
|
if (storedHash && storedHash === inputHash) {
|
|
25
25
|
logger?.info(`${fontFamily}: SVG fingerprint unchanged, skipping build.`);
|
|
26
26
|
return true;
|
|
@@ -3,7 +3,7 @@ export type NanoIconsConfig = {
|
|
|
3
3
|
iconSets: IconSetConfig[];
|
|
4
4
|
};
|
|
5
5
|
/**
|
|
6
|
-
* Load .nanoicons.json from the
|
|
6
|
+
* Load .nanoicons.json from the given directory.
|
|
7
7
|
* Throws with a helpful message if the file is missing or malformed.
|
|
8
8
|
*/
|
|
9
|
-
export declare function loadNanoIconsConfig(
|
|
9
|
+
export declare function loadNanoIconsConfig(configRoot: string): NanoIconsConfig;
|
|
@@ -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
|
}
|
|
@@ -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
|
`);
|