Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions src/commands/devup/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ describe('devup commands', () => {
textStyleToTypographySpy = spyOn(
textStyleToTypographyModule,
'textStyleToTypography',
).mockReturnValue({
).mockResolvedValue({
fontFamily: 'Inter',
} as unknown as DevupTypography)

Expand Down Expand Up @@ -201,7 +201,7 @@ describe('devup commands', () => {
textStyleToTypographySpy = spyOn(
textStyleToTypographyModule,
'textStyleToTypography',
).mockReturnValue(typoSeg as unknown as DevupTypography)
).mockResolvedValue(typoSeg as unknown as DevupTypography)
textSegmentToTypographySpy = spyOn(
textSegmentToTypographyModule,
'textSegmentToTypography',
Expand Down Expand Up @@ -263,7 +263,7 @@ describe('devup commands', () => {
textStyleToTypographySpy = spyOn(
textStyleToTypographyModule,
'textStyleToTypography',
).mockReturnValue({ fontFamily: 'Inter' } as unknown as DevupTypography)
).mockResolvedValue({ fontFamily: 'Inter' } as unknown as DevupTypography)

const mixedSymbol = Symbol('mixed')
const mixedTextNode = {
Expand Down Expand Up @@ -314,7 +314,7 @@ describe('devup commands', () => {
textStyleToTypographySpy = spyOn(
textStyleToTypographyModule,
'textStyleToTypography',
).mockReturnValue({ fontFamily: 'Inter' } as unknown as DevupTypography)
).mockResolvedValue({ fontFamily: 'Inter' } as unknown as DevupTypography)

const currentTextNode = {
type: 'TEXT',
Expand Down Expand Up @@ -387,7 +387,7 @@ describe('devup commands', () => {
textStyleToTypographySpy = spyOn(
textStyleToTypographyModule,
'textStyleToTypography',
).mockReturnValue({ fontFamily: 'Inter' } as unknown as DevupTypography)
).mockResolvedValue({ fontFamily: 'Inter' } as unknown as DevupTypography)

const currentSectionFindAllWithCriteria = mock(() => [])
const otherTextNode = {
Expand Down Expand Up @@ -463,7 +463,8 @@ describe('devup commands', () => {
textStyleToTypographyModule,
'textStyleToTypography',
).mockImplementation(
(style: TextStyle) => ({ id: style.id }) as unknown as DevupTypography,
async (style: TextStyle) =>
({ id: style.id }) as unknown as DevupTypography,
)

const directTextNode = {
Expand Down Expand Up @@ -535,7 +536,7 @@ describe('devup commands', () => {
textStyleToTypographySpy = spyOn(
textStyleToTypographyModule,
'textStyleToTypography',
).mockReturnValue(typoSeg as unknown as DevupTypography)
).mockResolvedValue(typoSeg as unknown as DevupTypography)

const textNode = {
type: 'TEXT',
Expand Down Expand Up @@ -591,7 +592,8 @@ describe('devup commands', () => {
textStyleToTypographyModule,
'textStyleToTypography',
).mockImplementation(
(style: TextStyle) => ({ id: style.id }) as unknown as DevupTypography,
async (style: TextStyle) =>
({ id: style.id }) as unknown as DevupTypography,
)

;(globalThis as { figma?: unknown }).figma = {
Expand Down Expand Up @@ -637,7 +639,8 @@ describe('devup commands', () => {
textStyleToTypographyModule,
'textStyleToTypography',
).mockImplementation(
(style: TextStyle) => ({ id: style.id }) as unknown as DevupTypography,
async (style: TextStyle) =>
({ id: style.id }) as unknown as DevupTypography,
)

;(globalThis as { figma?: unknown }).figma = {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/devup/export-devup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ export async function buildDevupConfig(
allTypographyKeyCount += 1
}
if (!typographyValues[meta.level]) {
typographyValues[meta.level] = textStyleToTypography(style)
typographyValues[meta.level] = await textStyleToTypography(style)
}
styleMetaById[style.id] = meta
}
Expand Down
2 changes: 1 addition & 1 deletion src/commands/devup/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export interface DevupTypography {
fontFamily?: string
fontStyle?: string
fontSize?: string
fontWeight?: number
fontWeight?: number | string
lineHeight?: number | string
letterSpacing?: string
textDecoration?: string
Expand Down
62 changes: 62 additions & 0 deletions src/utils/__tests__/resolve-text-style-bound-variable.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { afterEach, describe, expect, test } from 'bun:test'
import { resolveTextStyleBoundVariable } from '../resolve-text-style-bound-variable'

describe('resolveTextStyleBoundVariable', () => {
afterEach(() => {
;(globalThis as any).figma = undefined
})

test('returns null when boundVariables is undefined', async () => {
expect(
await resolveTextStyleBoundVariable(undefined, 'fontSize'),
).toBeNull()
})

test('returns null when field is not bound', async () => {
expect(
await resolveTextStyleBoundVariable({} as any, 'fontSize'),
).toBeNull()
})

test('returns null when variable is not found', async () => {
;(globalThis as any).figma = {
variables: {
getVariableByIdAsync: async () => null,
},
}
expect(
await resolveTextStyleBoundVariable(
{ fontSize: { type: 'VARIABLE_ALIAS', id: 'var1' } } as any,
'fontSize',
),
).toBeNull()
})

test('returns $camelName when variable is found', async () => {
;(globalThis as any).figma = {
variables: {
getVariableByIdAsync: async () => ({ name: 'heading/font-size' }),
},
}
expect(
await resolveTextStyleBoundVariable(
{ fontSize: { type: 'VARIABLE_ALIAS', id: 'var1' } } as any,
'fontSize',
),
).toBe('$headingFontSize')
})

test('returns null when variable has no name', async () => {
;(globalThis as any).figma = {
variables: {
getVariableByIdAsync: async () => ({ name: '' }),
},
}
expect(
await resolveTextStyleBoundVariable(
{ fontSize: { type: 'VARIABLE_ALIAS', id: 'var1' } } as any,
'fontSize',
),
).toBeNull()
})
})
71 changes: 67 additions & 4 deletions src/utils/__tests__/text-style-to-typography.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { describe, expect, test } from 'bun:test'
import { afterEach, describe, expect, test } from 'bun:test'
import { textStyleToTypography } from '../text-style-to-typography'

function makeStyle(styleName: string): TextStyle {
function makeStyle(
styleName: string,
boundVariables?: Record<string, any>,
): TextStyle {
return {
id: 'style',
name: 'style',
Expand All @@ -19,10 +22,15 @@ function makeStyle(styleName: string): TextStyle {
textAlignVertical: 'TOP',
lineHeight: { unit: 'AUTO' },
letterSpacing: { unit: 'PIXELS', value: 0 },
boundVariables,
} as unknown as TextStyle
}

describe('textStyleToTypography', () => {
afterEach(() => {
;(globalThis as any).figma = undefined
})

test.each([
['Thin', 100],
['Extra Light', 200],
Expand All @@ -40,8 +48,63 @@ describe('textStyleToTypography', () => {
['Heavy', 900],
['750', 750],
['UnknownWeight', 400],
])('maps %s to fontWeight %d', (styleName, expected) => {
const result = textStyleToTypography(makeStyle(styleName))
])('maps %s to fontWeight %d', async (styleName, expected) => {
const result = await textStyleToTypography(makeStyle(styleName))
expect(result.fontWeight).toBe(expected)
})

test('returns base typography when no boundVariables', async () => {
const result = await textStyleToTypography(makeStyle('Regular'))
expect(result.fontFamily).toBe('Pretendard')
expect(result.fontSize).toBe('16px')
})

test('overrides fields with bound variable references', async () => {
;(globalThis as any).figma = {
variables: {
getVariableByIdAsync: async (id: string) => {
const vars: Record<string, any> = {
v1: { name: 'heading/size' },
v2: { name: 'heading/line-height' },
v3: { name: 'heading/spacing' },
v4: { name: 'heading/weight' },
v5: { name: 'heading/family' },
v6: { name: 'heading/style' },
}
return vars[id] ?? null
},
},
}
const result = await textStyleToTypography(
makeStyle('Regular', {
fontSize: { type: 'VARIABLE_ALIAS', id: 'v1' },
lineHeight: { type: 'VARIABLE_ALIAS', id: 'v2' },
letterSpacing: { type: 'VARIABLE_ALIAS', id: 'v3' },
fontWeight: { type: 'VARIABLE_ALIAS', id: 'v4' },
fontFamily: { type: 'VARIABLE_ALIAS', id: 'v5' },
fontStyle: { type: 'VARIABLE_ALIAS', id: 'v6' },
}),
)
expect(result.fontSize).toBe('$headingSize')
expect(result.lineHeight).toBe('$headingLineHeight')
expect(result.letterSpacing).toBe('$headingSpacing')
expect(result.fontWeight).toBe('$headingWeight')
expect(result.fontFamily).toBe('$headingFamily')
expect(result.fontStyle).toBe('$headingStyle')
})

test('keeps base value when variable not found', async () => {
;(globalThis as any).figma = {
variables: {
getVariableByIdAsync: async () => null,
},
}
const result = await textStyleToTypography(
makeStyle('Bold', {
fontSize: { type: 'VARIABLE_ALIAS', id: 'missing' },
}),
)
expect(result.fontSize).toBe('16px')
expect(result.fontWeight).toBe(700)
})
})
26 changes: 26 additions & 0 deletions src/utils/resolve-text-style-bound-variable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { toCamel } from './to-camel'

type VariableBindableTextField =
| 'fontFamily'
| 'fontSize'
| 'fontStyle'
| 'fontWeight'
| 'letterSpacing'
| 'lineHeight'
| 'paragraphSpacing'
| 'paragraphIndent'

type TextStyleBoundVariables = {
[field in VariableBindableTextField]?: VariableAlias
}

export async function resolveTextStyleBoundVariable(
boundVariables: TextStyleBoundVariables | undefined,
field: VariableBindableTextField,
): Promise<string | null> {
const binding = boundVariables?.[field]
if (!binding) return null
const variable = await figma.variables.getVariableByIdAsync(binding.id)
if (variable?.name) return `$${toCamel(variable.name)}`
return null
}
35 changes: 33 additions & 2 deletions src/utils/text-style-to-typography.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type { DevupTypography } from '../commands/devup/types'
import { resolveTextStyleBoundVariable } from './resolve-text-style-bound-variable'
import { textSegmentToTypography } from './text-segment-to-typography'
import { toCamel } from './to-camel'

export function textStyleToTypography(style: TextStyle): DevupTypography {
return textSegmentToTypography({
export async function textStyleToTypography(
style: TextStyle,
): Promise<DevupTypography> {
const base = textSegmentToTypography({
fontName: style.fontName,
fontWeight: getFontWeight(style.fontName.style),
fontSize: style.fontSize,
Expand All @@ -12,6 +15,34 @@ export function textStyleToTypography(style: TextStyle): DevupTypography {
lineHeight: style.lineHeight,
letterSpacing: style.letterSpacing,
})

const boundVars = style.boundVariables
if (!boundVars) return base

const [
fontFamily,
fontSize,
fontStyle,
fontWeight,
letterSpacing,
lineHeight,
] = await Promise.all([
resolveTextStyleBoundVariable(boundVars, 'fontFamily'),
resolveTextStyleBoundVariable(boundVars, 'fontSize'),
resolveTextStyleBoundVariable(boundVars, 'fontStyle'),
resolveTextStyleBoundVariable(boundVars, 'fontWeight'),
resolveTextStyleBoundVariable(boundVars, 'letterSpacing'),
resolveTextStyleBoundVariable(boundVars, 'lineHeight'),
])

if (fontFamily) base.fontFamily = fontFamily
if (fontSize) base.fontSize = fontSize
if (fontStyle) base.fontStyle = fontStyle
if (fontWeight) base.fontWeight = fontWeight
if (letterSpacing) base.letterSpacing = letterSpacing
if (lineHeight) base.lineHeight = lineHeight

return base
}

function getFontWeight(weight: string): number {
Expand Down