Skip to content

Commit

Permalink
Split the bridges in their own objects
Browse files Browse the repository at this point in the history
  • Loading branch information
lordofthelake committed Jan 21, 2020
1 parent 2968a64 commit 0071286
Show file tree
Hide file tree
Showing 5 changed files with 326 additions and 0 deletions.
19 changes: 19 additions & 0 deletions src/platformBridges/NodeMacOSBridge.ts
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;
89 changes: 89 additions & 0 deletions src/platformBridges/SketchBridge/createStringMeasurer.ts
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 };
}
200 changes: 200 additions & 0 deletions src/platformBridges/SketchBridge/findFontName.ts
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();
}
13 changes: 13 additions & 0 deletions src/platformBridges/SketchBridge/index.ts
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;
5 changes: 5 additions & 0 deletions src/platformBridges/SketchBridge/readFile.ts
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));
}

0 comments on commit 0071286

Please sign in to comment.