Skip to content

Commit 0071286

Browse files
committed
Split the bridges in their own objects
1 parent 2968a64 commit 0071286

File tree

5 files changed

+326
-0
lines changed

5 files changed

+326
-0
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { PlatformBridge } from '../types';
2+
import weakRequire from '../utils/weakRequire';
3+
4+
const { createStringMeasurer, findFontName } = weakRequire(module, 'node-sketch-bridge');
5+
const fetch = weakRequire(module, 'node-fetch');
6+
const { readFile: nodeReadFile } = weakRequire(module, 'fs');
7+
8+
const NodeMacOSBridge: PlatformBridge = {
9+
createStringMeasurer,
10+
findFontName,
11+
fetch,
12+
async readFile(path: string): Promise<Buffer> {
13+
return new Promise((resolve, reject) => {
14+
nodeReadFile(path, (err, data) => (err ? reject(err) : resolve(data)));
15+
});
16+
},
17+
};
18+
19+
export default NodeMacOSBridge;
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { Size, TextNode, TextStyle } from '../../types';
2+
import {
3+
TEXT_DECORATION_UNDERLINE,
4+
TEXT_DECORATION_LINETHROUGH,
5+
TEXT_ALIGN,
6+
TEXT_TRANSFORM,
7+
} from '../../jsonUtils/textLayers';
8+
import { findFont } from './findFontName';
9+
import { makeColorFromCSS } from '../../jsonUtils/models';
10+
11+
// TODO(lmr): do something more sensible here
12+
const FLOAT_MAX = 999999;
13+
14+
function makeParagraphStyle(textStyle) {
15+
const pStyle = NSMutableParagraphStyle.alloc().init();
16+
if (textStyle.lineHeight !== undefined) {
17+
pStyle.minimumLineHeight = textStyle.lineHeight;
18+
pStyle.lineHeightMultiple = 1.0;
19+
pStyle.maximumLineHeight = textStyle.lineHeight;
20+
}
21+
22+
if (textStyle.textAlign) {
23+
pStyle.alignment = TEXT_ALIGN[textStyle.textAlign];
24+
}
25+
26+
// TODO: check against only positive spacing values?
27+
if (textStyle.paragraphSpacing !== undefined) {
28+
pStyle.paragraphSpacing = textStyle.paragraphSpacing;
29+
}
30+
31+
return pStyle;
32+
}
33+
34+
// This shouldn't need to call into Sketch, but it does currently, which is bad for perf :(
35+
function createStringAttributes(textStyles: TextStyle): Object {
36+
const font = findFont(textStyles);
37+
const { textDecoration } = textStyles;
38+
39+
const underline = textDecoration && TEXT_DECORATION_UNDERLINE[textDecoration];
40+
const strikethrough = textDecoration && TEXT_DECORATION_LINETHROUGH[textDecoration];
41+
42+
const attribs: any = {
43+
MSAttributedStringFontAttribute: font.fontDescriptor(),
44+
NSFont: font,
45+
NSParagraphStyle: makeParagraphStyle(textStyles),
46+
NSUnderline: underline || 0,
47+
NSStrikethrough: strikethrough || 0,
48+
};
49+
50+
const color = makeColorFromCSS(textStyles.color || 'black');
51+
attribs.MSAttributedStringColorAttribute = color;
52+
53+
if (textStyles.letterSpacing !== undefined) {
54+
attribs.NSKern = textStyles.letterSpacing;
55+
}
56+
57+
if (textStyles.textTransform !== undefined) {
58+
attribs.MSAttributedStringTextTransformAttribute = TEXT_TRANSFORM[textStyles.textTransform] * 1;
59+
}
60+
61+
return attribs;
62+
}
63+
64+
type NSAttributedString = any;
65+
66+
function createAttributedString(textNode: TextNode): NSAttributedString {
67+
const { content, textStyles } = textNode;
68+
69+
const attribs = createStringAttributes(textStyles);
70+
71+
return NSAttributedString.attributedStringWithString_attributes_(content, attribs);
72+
}
73+
74+
export default function createStringMeasurer(textNodes: TextNode[], width: number): Size {
75+
const fullStr = NSMutableAttributedString.alloc().init();
76+
textNodes.forEach(textNode => {
77+
const newString = createAttributedString(textNode);
78+
fullStr.appendAttributedString(newString);
79+
});
80+
const {
81+
height: measureHeight,
82+
width: measureWidth,
83+
} = fullStr.boundingRectWithSize_options_context(
84+
CGSizeMake(width, FLOAT_MAX),
85+
NSStringDrawingUsesLineFragmentOrigin,
86+
null,
87+
).size;
88+
return { width: measureWidth, height: measureHeight };
89+
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/* eslint-disable no-bitwise */
2+
3+
import hashStyle from '../../utils/hashStyle';
4+
import { TextStyle } from '../../types';
5+
import { FONT_STYLES } from '../../jsonUtils/textLayers';
6+
7+
// this borrows heavily from react-native's RCTFont class
8+
// thanks y'all
9+
// https://github.com/facebook/react-native/blob/master/React/Views/RCTFont.mm
10+
11+
const FONT_WEIGHTS = {
12+
ultralight: -0.8,
13+
'100': -0.8,
14+
thin: -0.6,
15+
'200': -0.6,
16+
light: -0.4,
17+
'300': -0.4,
18+
normal: 0,
19+
regular: 0,
20+
'400': 0,
21+
semibold: 0.23,
22+
demibold: 0.23,
23+
'500': 0.23,
24+
'600': 0.3,
25+
bold: 0.4,
26+
'700': 0.4,
27+
extrabold: 0.56,
28+
ultrabold: 0.56,
29+
heavy: 0.56,
30+
'800': 0.56,
31+
black: 0.62,
32+
'900': 0.62,
33+
};
34+
35+
type NSFont = any;
36+
37+
const isItalicFont = (font: NSFont): boolean => {
38+
const traits = font.fontDescriptor().objectForKey(NSFontTraitsAttribute);
39+
const symbolicTraits = traits[NSFontSymbolicTrait].unsignedIntValue();
40+
41+
return (symbolicTraits & NSFontItalicTrait) !== 0;
42+
};
43+
44+
const isCondensedFont = (font: NSFont): boolean => {
45+
const traits = font.fontDescriptor().objectForKey(NSFontTraitsAttribute);
46+
const symbolicTraits = traits[NSFontSymbolicTrait].unsignedIntValue();
47+
48+
return (symbolicTraits & NSFontCondensedTrait) !== 0;
49+
};
50+
51+
const weightOfFont = (font: NSFont): number => {
52+
const traits = font.fontDescriptor().objectForKey(NSFontTraitsAttribute);
53+
54+
const weight = traits[NSFontWeightTrait].doubleValue();
55+
if (weight === 0.0) {
56+
const weights = Object.keys(FONT_WEIGHTS);
57+
const fontName = String(font.fontName()).toLowerCase();
58+
const matchingWeight = weights.find(w => fontName.endsWith(w));
59+
if (matchingWeight) {
60+
return FONT_WEIGHTS[matchingWeight];
61+
}
62+
}
63+
64+
return weight;
65+
};
66+
67+
const fontNamesForFamilyName = (familyName: string): Array<string> => {
68+
const manager = NSFontManager.sharedFontManager();
69+
const members = NSArray.arrayWithArray(manager.availableMembersOfFontFamily(familyName));
70+
71+
const results = [];
72+
for (let i = 0; i < members.length; i += 1) {
73+
results.push(members[i][0]);
74+
}
75+
76+
return results;
77+
};
78+
79+
const useCache = true;
80+
const _cache: Map<string, NSFont> = new Map();
81+
82+
const getCached = (key: string): NSFont => {
83+
if (!useCache) return undefined;
84+
return _cache.get(key);
85+
};
86+
87+
export const findFont = (style: TextStyle): NSFont => {
88+
const cacheKey = hashStyle(style);
89+
90+
let font = getCached(cacheKey);
91+
if (font) {
92+
return font;
93+
}
94+
const defaultFontFamily = NSFont.systemFontOfSize(14).familyName();
95+
const defaultFontWeight = NSFontWeightRegular;
96+
const defaultFontSize = 14;
97+
98+
const fontSize = style.fontSize ? style.fontSize : defaultFontSize;
99+
let fontWeight = style.fontWeight
100+
? FONT_WEIGHTS[style.fontWeight.toLowerCase()]
101+
: defaultFontWeight;
102+
103+
let familyName = defaultFontFamily;
104+
let isItalic = false;
105+
let isCondensed = false;
106+
107+
if (style.fontFamily) {
108+
familyName = style.fontFamily;
109+
}
110+
111+
if (style.fontStyle) {
112+
isItalic = FONT_STYLES[style.fontStyle] || false;
113+
}
114+
115+
let didFindFont = false;
116+
117+
// Handle system font as special case. This ensures that we preserve
118+
// the specific metrics of the standard system font as closely as possible.
119+
if (familyName === defaultFontFamily || familyName === 'System') {
120+
font = NSFont.systemFontOfSize_weight(fontSize, fontWeight);
121+
122+
if (font) {
123+
didFindFont = true;
124+
125+
if (isItalic || isCondensed) {
126+
let fontDescriptor = font.fontDescriptor();
127+
let symbolicTraits = fontDescriptor.symbolicTraits();
128+
if (isItalic) {
129+
symbolicTraits |= NSFontItalicTrait;
130+
}
131+
132+
if (isCondensed) {
133+
symbolicTraits |= NSFontCondensedTrait;
134+
}
135+
136+
fontDescriptor = fontDescriptor.fontDescriptorWithSymbolicTraits(symbolicTraits);
137+
font = NSFont.fontWithDescriptor_size(fontDescriptor, fontSize);
138+
}
139+
}
140+
}
141+
142+
const fontNames = fontNamesForFamilyName(familyName);
143+
144+
// Gracefully handle being given a font name rather than font family, for
145+
// example: "Helvetica Light Oblique" rather than just "Helvetica".
146+
if (!didFindFont && fontNames.length === 0) {
147+
font = NSFont.fontWithName_size(familyName, fontSize);
148+
if (font) {
149+
// It's actually a font name, not a font family name,
150+
// but we'll do what was meant, not what was said.
151+
familyName = font.familyName();
152+
fontWeight = style.fontWeight ? fontWeight : weightOfFont(font);
153+
isItalic = style.fontStyle ? isItalic : isItalicFont(font);
154+
isCondensed = isCondensedFont(font);
155+
} else {
156+
font = NSFont.systemFontOfSize_weight(fontSize, fontWeight);
157+
}
158+
159+
didFindFont = true;
160+
}
161+
162+
if (!didFindFont) {
163+
// Get the closest font that matches the given weight for the fontFamily
164+
let closestWeight = Infinity;
165+
for (let i = 0; i < fontNames.length; i += 1) {
166+
const match = NSFont.fontWithName_size(fontNames[i], fontSize);
167+
168+
if (isItalic === isItalicFont(match) && isCondensed === isCondensedFont(match)) {
169+
const testWeight = weightOfFont(match);
170+
171+
if (Math.abs(testWeight - fontWeight) < Math.abs(closestWeight - fontWeight)) {
172+
font = match;
173+
174+
closestWeight = testWeight;
175+
}
176+
}
177+
}
178+
}
179+
180+
// If we still don't have a match at least return the first font in the fontFamily
181+
// This is to support built-in font Zapfino and other custom single font families like Impact
182+
if (!font) {
183+
if (fontNames.length > 0) {
184+
font = NSFont.fontWithName_size(fontNames[0], fontSize);
185+
}
186+
}
187+
188+
// TODO(gold): support opentype features: small-caps & number types
189+
190+
if (font) {
191+
_cache.set(cacheKey, font);
192+
}
193+
194+
return font;
195+
};
196+
197+
export default function findFontName(style: TextStyle): string {
198+
const font = findFont(style);
199+
return font.fontDescriptor().postscriptName();
200+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { PlatformBridge } from '../../types';
2+
import createStringMeasurer from './createStringMeasurer';
3+
import findFontName from './findFontName';
4+
import readFile from './readFile';
5+
6+
const SketchBridge: PlatformBridge = {
7+
createStringMeasurer,
8+
findFontName,
9+
fetch,
10+
readFile,
11+
};
12+
13+
export default SketchBridge;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { readFileSync } from '@skpm/fs';
2+
3+
export default async function readFile(path: string): Promise<Buffer> {
4+
return Promise.resolve(readFileSync(path));
5+
}

0 commit comments

Comments
 (0)