-
Notifications
You must be signed in to change notification settings - Fork 818
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Split the bridges in their own objects
- Loading branch information
1 parent
2968a64
commit 0071286
Showing
5 changed files
with
326 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { PlatformBridge } from '../types'; | ||
import weakRequire from '../utils/weakRequire'; | ||
|
||
const { createStringMeasurer, findFontName } = weakRequire(module, 'node-sketch-bridge'); | ||
const fetch = weakRequire(module, 'node-fetch'); | ||
const { readFile: nodeReadFile } = weakRequire(module, 'fs'); | ||
|
||
const NodeMacOSBridge: PlatformBridge = { | ||
createStringMeasurer, | ||
findFontName, | ||
fetch, | ||
async readFile(path: string): Promise<Buffer> { | ||
return new Promise((resolve, reject) => { | ||
nodeReadFile(path, (err, data) => (err ? reject(err) : resolve(data))); | ||
}); | ||
}, | ||
}; | ||
|
||
export default NodeMacOSBridge; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import { Size, TextNode, TextStyle } from '../../types'; | ||
import { | ||
TEXT_DECORATION_UNDERLINE, | ||
TEXT_DECORATION_LINETHROUGH, | ||
TEXT_ALIGN, | ||
TEXT_TRANSFORM, | ||
} from '../../jsonUtils/textLayers'; | ||
import { findFont } from './findFontName'; | ||
import { makeColorFromCSS } from '../../jsonUtils/models'; | ||
|
||
// TODO(lmr): do something more sensible here | ||
const FLOAT_MAX = 999999; | ||
|
||
function makeParagraphStyle(textStyle) { | ||
const pStyle = NSMutableParagraphStyle.alloc().init(); | ||
if (textStyle.lineHeight !== undefined) { | ||
pStyle.minimumLineHeight = textStyle.lineHeight; | ||
pStyle.lineHeightMultiple = 1.0; | ||
pStyle.maximumLineHeight = textStyle.lineHeight; | ||
} | ||
|
||
if (textStyle.textAlign) { | ||
pStyle.alignment = TEXT_ALIGN[textStyle.textAlign]; | ||
} | ||
|
||
// TODO: check against only positive spacing values? | ||
if (textStyle.paragraphSpacing !== undefined) { | ||
pStyle.paragraphSpacing = textStyle.paragraphSpacing; | ||
} | ||
|
||
return pStyle; | ||
} | ||
|
||
// This shouldn't need to call into Sketch, but it does currently, which is bad for perf :( | ||
function createStringAttributes(textStyles: TextStyle): Object { | ||
const font = findFont(textStyles); | ||
const { textDecoration } = textStyles; | ||
|
||
const underline = textDecoration && TEXT_DECORATION_UNDERLINE[textDecoration]; | ||
const strikethrough = textDecoration && TEXT_DECORATION_LINETHROUGH[textDecoration]; | ||
|
||
const attribs: any = { | ||
MSAttributedStringFontAttribute: font.fontDescriptor(), | ||
NSFont: font, | ||
NSParagraphStyle: makeParagraphStyle(textStyles), | ||
NSUnderline: underline || 0, | ||
NSStrikethrough: strikethrough || 0, | ||
}; | ||
|
||
const color = makeColorFromCSS(textStyles.color || 'black'); | ||
attribs.MSAttributedStringColorAttribute = color; | ||
|
||
if (textStyles.letterSpacing !== undefined) { | ||
attribs.NSKern = textStyles.letterSpacing; | ||
} | ||
|
||
if (textStyles.textTransform !== undefined) { | ||
attribs.MSAttributedStringTextTransformAttribute = TEXT_TRANSFORM[textStyles.textTransform] * 1; | ||
} | ||
|
||
return attribs; | ||
} | ||
|
||
type NSAttributedString = any; | ||
|
||
function createAttributedString(textNode: TextNode): NSAttributedString { | ||
const { content, textStyles } = textNode; | ||
|
||
const attribs = createStringAttributes(textStyles); | ||
|
||
return NSAttributedString.attributedStringWithString_attributes_(content, attribs); | ||
} | ||
|
||
export default function createStringMeasurer(textNodes: TextNode[], width: number): Size { | ||
const fullStr = NSMutableAttributedString.alloc().init(); | ||
textNodes.forEach(textNode => { | ||
const newString = createAttributedString(textNode); | ||
fullStr.appendAttributedString(newString); | ||
}); | ||
const { | ||
height: measureHeight, | ||
width: measureWidth, | ||
} = fullStr.boundingRectWithSize_options_context( | ||
CGSizeMake(width, FLOAT_MAX), | ||
NSStringDrawingUsesLineFragmentOrigin, | ||
null, | ||
).size; | ||
return { width: measureWidth, height: measureHeight }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
/* eslint-disable no-bitwise */ | ||
|
||
import hashStyle from '../../utils/hashStyle'; | ||
import { TextStyle } from '../../types'; | ||
import { FONT_STYLES } from '../../jsonUtils/textLayers'; | ||
|
||
// this borrows heavily from react-native's RCTFont class | ||
// thanks y'all | ||
// https://github.com/facebook/react-native/blob/master/React/Views/RCTFont.mm | ||
|
||
const FONT_WEIGHTS = { | ||
ultralight: -0.8, | ||
'100': -0.8, | ||
thin: -0.6, | ||
'200': -0.6, | ||
light: -0.4, | ||
'300': -0.4, | ||
normal: 0, | ||
regular: 0, | ||
'400': 0, | ||
semibold: 0.23, | ||
demibold: 0.23, | ||
'500': 0.23, | ||
'600': 0.3, | ||
bold: 0.4, | ||
'700': 0.4, | ||
extrabold: 0.56, | ||
ultrabold: 0.56, | ||
heavy: 0.56, | ||
'800': 0.56, | ||
black: 0.62, | ||
'900': 0.62, | ||
}; | ||
|
||
type NSFont = any; | ||
|
||
const isItalicFont = (font: NSFont): boolean => { | ||
const traits = font.fontDescriptor().objectForKey(NSFontTraitsAttribute); | ||
const symbolicTraits = traits[NSFontSymbolicTrait].unsignedIntValue(); | ||
|
||
return (symbolicTraits & NSFontItalicTrait) !== 0; | ||
}; | ||
|
||
const isCondensedFont = (font: NSFont): boolean => { | ||
const traits = font.fontDescriptor().objectForKey(NSFontTraitsAttribute); | ||
const symbolicTraits = traits[NSFontSymbolicTrait].unsignedIntValue(); | ||
|
||
return (symbolicTraits & NSFontCondensedTrait) !== 0; | ||
}; | ||
|
||
const weightOfFont = (font: NSFont): number => { | ||
const traits = font.fontDescriptor().objectForKey(NSFontTraitsAttribute); | ||
|
||
const weight = traits[NSFontWeightTrait].doubleValue(); | ||
if (weight === 0.0) { | ||
const weights = Object.keys(FONT_WEIGHTS); | ||
const fontName = String(font.fontName()).toLowerCase(); | ||
const matchingWeight = weights.find(w => fontName.endsWith(w)); | ||
if (matchingWeight) { | ||
return FONT_WEIGHTS[matchingWeight]; | ||
} | ||
} | ||
|
||
return weight; | ||
}; | ||
|
||
const fontNamesForFamilyName = (familyName: string): Array<string> => { | ||
const manager = NSFontManager.sharedFontManager(); | ||
const members = NSArray.arrayWithArray(manager.availableMembersOfFontFamily(familyName)); | ||
|
||
const results = []; | ||
for (let i = 0; i < members.length; i += 1) { | ||
results.push(members[i][0]); | ||
} | ||
|
||
return results; | ||
}; | ||
|
||
const useCache = true; | ||
const _cache: Map<string, NSFont> = new Map(); | ||
|
||
const getCached = (key: string): NSFont => { | ||
if (!useCache) return undefined; | ||
return _cache.get(key); | ||
}; | ||
|
||
export const findFont = (style: TextStyle): NSFont => { | ||
const cacheKey = hashStyle(style); | ||
|
||
let font = getCached(cacheKey); | ||
if (font) { | ||
return font; | ||
} | ||
const defaultFontFamily = NSFont.systemFontOfSize(14).familyName(); | ||
const defaultFontWeight = NSFontWeightRegular; | ||
const defaultFontSize = 14; | ||
|
||
const fontSize = style.fontSize ? style.fontSize : defaultFontSize; | ||
let fontWeight = style.fontWeight | ||
? FONT_WEIGHTS[style.fontWeight.toLowerCase()] | ||
: defaultFontWeight; | ||
|
||
let familyName = defaultFontFamily; | ||
let isItalic = false; | ||
let isCondensed = false; | ||
|
||
if (style.fontFamily) { | ||
familyName = style.fontFamily; | ||
} | ||
|
||
if (style.fontStyle) { | ||
isItalic = FONT_STYLES[style.fontStyle] || false; | ||
} | ||
|
||
let didFindFont = false; | ||
|
||
// Handle system font as special case. This ensures that we preserve | ||
// the specific metrics of the standard system font as closely as possible. | ||
if (familyName === defaultFontFamily || familyName === 'System') { | ||
font = NSFont.systemFontOfSize_weight(fontSize, fontWeight); | ||
|
||
if (font) { | ||
didFindFont = true; | ||
|
||
if (isItalic || isCondensed) { | ||
let fontDescriptor = font.fontDescriptor(); | ||
let symbolicTraits = fontDescriptor.symbolicTraits(); | ||
if (isItalic) { | ||
symbolicTraits |= NSFontItalicTrait; | ||
} | ||
|
||
if (isCondensed) { | ||
symbolicTraits |= NSFontCondensedTrait; | ||
} | ||
|
||
fontDescriptor = fontDescriptor.fontDescriptorWithSymbolicTraits(symbolicTraits); | ||
font = NSFont.fontWithDescriptor_size(fontDescriptor, fontSize); | ||
} | ||
} | ||
} | ||
|
||
const fontNames = fontNamesForFamilyName(familyName); | ||
|
||
// Gracefully handle being given a font name rather than font family, for | ||
// example: "Helvetica Light Oblique" rather than just "Helvetica". | ||
if (!didFindFont && fontNames.length === 0) { | ||
font = NSFont.fontWithName_size(familyName, fontSize); | ||
if (font) { | ||
// It's actually a font name, not a font family name, | ||
// but we'll do what was meant, not what was said. | ||
familyName = font.familyName(); | ||
fontWeight = style.fontWeight ? fontWeight : weightOfFont(font); | ||
isItalic = style.fontStyle ? isItalic : isItalicFont(font); | ||
isCondensed = isCondensedFont(font); | ||
} else { | ||
font = NSFont.systemFontOfSize_weight(fontSize, fontWeight); | ||
} | ||
|
||
didFindFont = true; | ||
} | ||
|
||
if (!didFindFont) { | ||
// Get the closest font that matches the given weight for the fontFamily | ||
let closestWeight = Infinity; | ||
for (let i = 0; i < fontNames.length; i += 1) { | ||
const match = NSFont.fontWithName_size(fontNames[i], fontSize); | ||
|
||
if (isItalic === isItalicFont(match) && isCondensed === isCondensedFont(match)) { | ||
const testWeight = weightOfFont(match); | ||
|
||
if (Math.abs(testWeight - fontWeight) < Math.abs(closestWeight - fontWeight)) { | ||
font = match; | ||
|
||
closestWeight = testWeight; | ||
} | ||
} | ||
} | ||
} | ||
|
||
// If we still don't have a match at least return the first font in the fontFamily | ||
// This is to support built-in font Zapfino and other custom single font families like Impact | ||
if (!font) { | ||
if (fontNames.length > 0) { | ||
font = NSFont.fontWithName_size(fontNames[0], fontSize); | ||
} | ||
} | ||
|
||
// TODO(gold): support opentype features: small-caps & number types | ||
|
||
if (font) { | ||
_cache.set(cacheKey, font); | ||
} | ||
|
||
return font; | ||
}; | ||
|
||
export default function findFontName(style: TextStyle): string { | ||
const font = findFont(style); | ||
return font.fontDescriptor().postscriptName(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { PlatformBridge } from '../../types'; | ||
import createStringMeasurer from './createStringMeasurer'; | ||
import findFontName from './findFontName'; | ||
import readFile from './readFile'; | ||
|
||
const SketchBridge: PlatformBridge = { | ||
createStringMeasurer, | ||
findFontName, | ||
fetch, | ||
readFile, | ||
}; | ||
|
||
export default SketchBridge; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { readFileSync } from '@skpm/fs'; | ||
|
||
export default async function readFile(path: string): Promise<Buffer> { | ||
return Promise.resolve(readFileSync(path)); | ||
} |