Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add a parser function to add fontStyle to typography tokens #159

Merged
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
5 changes: 5 additions & 0 deletions .changeset/tiny-poets-doubt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tokens-studio/sd-transforms': minor
---

BREAKING: add parser that extracts fontStyle from fontWeight and adds it as a separate property on Typography tokens object values.
5 changes: 5 additions & 0 deletions .changeset/wild-actors-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tokens-studio/sd-transforms': patch
---

Properly take into account fontStyle inside fontWeights values, in both the fontWeights and CSS typography shorthand transforms.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion src/checkAndEvaluateMath.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Parser } from 'expr-eval';
import { parse, reduceExpression } from 'postcss-calc-ast-parser';
import { isAlreadyQuoted } from './css/transformTypography.js';

const mathChars = ['+', '-', '*', '/'];

Expand Down Expand Up @@ -86,11 +87,19 @@ function parseAndReduce(expr: string): string {
let evaluated;
try {
evaluated = parser.evaluate(unitlessExpr);

// In CSS shorthand with white-spaced font-family that has single quotes around it
// parser ends up evaluating that and removing the single quotes. If that happens, revert that change.
if (evaluated && isAlreadyQuoted(unitlessExpr)) {
evaluated = `'${evaluated}'`;
}
} catch (ex) {
return expr;
}
// Put back the px unit if needed and if reduced doesn't come with one
return `${Number.parseFloat(evaluated.toFixed(3))}${unit ?? (hasPx ? 'px' : '')}`;
return `${typeof evaluated !== 'string' ? Number.parseFloat(evaluated.toFixed(3)) : evaluated}${
unit ?? (hasPx ? 'px' : '')
}`;
}

export function checkAndEvaluateMath(expr: string | number | undefined): string | undefined {
Expand Down
7 changes: 5 additions & 2 deletions src/css/transformTypography.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function hasWhiteSpace(value: string): boolean {
return whiteSpaceRegex.test(value);
}

function isAlreadyQuoted(value: string): boolean {
export function isAlreadyQuoted(value: string): boolean {
return value.startsWith("'") && value.endsWith("'");
}

Expand Down Expand Up @@ -48,12 +48,15 @@ export function transformTypographyForCSS(
}

let { fontFamily, fontWeight, fontSize, lineHeight } = value;
const { fontStyle } = value;
fontSize = transformDimension(checkAndEvaluateMath(fontSize));
lineHeight = checkAndEvaluateMath(lineHeight);
fontWeight = transformFontWeights(fontWeight);
fontFamily = processFontFamily(fontFamily as string | undefined);

return `${isNothing(fontWeight) ? 400 : fontWeight} ${isNothing(fontSize) ? '16px' : fontSize}/${
return `${isNothing(fontWeight) ? 400 : fontWeight}${
isNothing(fontStyle) ? '' : ` ${fontStyle}`
} ${isNothing(fontSize) ? '16px' : fontSize}/${
isNothing(lineHeight) ? 1 : lineHeight
} ${fontFamily}`;
}
58 changes: 58 additions & 0 deletions src/parsers/add-font-styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { DeepKeyTokenMap, SingleToken, TokenTypographyValue } from '@tokens-studio/types';
// @ts-expect-error no type exported for this function
import getReferences from 'style-dictionary/lib/utils/references/getReferences.js';
// @ts-expect-error no type exported for this function
import usesReference from 'style-dictionary/lib/utils/references/usesReference.js';
import { fontWeightReg } from '../transformFontWeights.js';

function recurse(
slice: DeepKeyTokenMap<false>,
boundGetRef: (ref: string) => Array<SingleToken<false>>,
) {
for (const key in slice) {
const token = slice[key];
const { type, value } = token;
if (type === 'typography') {
if (typeof value !== 'object') {
continue;
}
let fontWeight = value.fontWeight;

if (usesReference(fontWeight)) {
let ref = { value: fontWeight } as SingleToken<false>;
while (ref && ref.value && typeof ref.value === 'string' && usesReference(ref.value)) {
try {
ref = Object.fromEntries(
Object.entries(boundGetRef(ref.value)[0]).map(([k, v]) => [k, v]),
) as SingleToken<false>;
} catch (e) {
console.warn(`Warning: could not resolve reference ${ref.value}`);
return;
}
}
fontWeight = ref.value as string;
}

// cast it to TokenTypographyValue now that we've resolved references all the way, we know it cannot be a string anymore.
const tokenValue = value as TokenTypographyValue;

if (fontWeight) {
const fontStyleMatch = fontWeight.match(fontWeightReg);
if (fontStyleMatch?.groups?.weight && fontStyleMatch.groups.style) {
// @ts-expect-error fontStyle is not a property that exists on Typography Tokens, we just add it ourselves
tokenValue.fontStyle = fontStyleMatch.groups.style.toLowerCase();
tokenValue.fontWeight = fontStyleMatch?.groups?.weight;
}
}
} else if (typeof token === 'object') {
recurse(token as unknown as DeepKeyTokenMap<false>, boundGetRef);
}
}
}

export function addFontStyles(dictionary: DeepKeyTokenMap<false>): DeepKeyTokenMap<false> {
const copy = { ...dictionary };
const boundGetRef = getReferences.bind({ properties: copy });
recurse(copy, boundGetRef);
return copy;
}
1 change: 1 addition & 0 deletions src/parsers/expand-composites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const typeMaps = {
fontWeight: 'fontWeights',
lineHeight: 'lineHeights',
fontSize: 'fontSizes',
fontStyle: 'fontStyles',
},
};

Expand Down
4 changes: 3 additions & 1 deletion src/registerTransforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { TransformOptions } from './TransformOptions.js';
import { expandComposites } from './parsers/expand-composites.js';
import { excludeParentKeys } from './parsers/exclude-parent-keys.js';
import { transformOpacity } from './transformOpacity.js';
import { addFontStyles } from './parsers/add-font-styles.js';

const isBrowser = typeof window === 'object';

Expand Down Expand Up @@ -59,7 +60,8 @@ export async function registerTransforms(sd: Core, transformOpts?: TransformOpti
parse: ({ filePath, contents }) => {
const obj = JSON.parse(contents);
const excluded = excludeParentKeys(obj, transformOpts);
const expanded = expandComposites(excluded, filePath, transformOpts);
const withFontStyles = addFontStyles(excluded);
const expanded = expandComposites(withFontStyles, filePath, transformOpts);
return expanded;
},
});
Expand Down
12 changes: 11 additions & 1 deletion src/transformFontWeights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export const fontWeightMap = {
extrafett: 900,
};

export const fontWeightReg = /(?<weight>.+?)\s?(?<style>italic|oblique|normal)?$/i;

/**
* Helper: Transforms fontweight keynames to fontweight numbers (100, 200, 300 ... 900)
*/
Expand All @@ -34,7 +36,15 @@ export function transformFontWeights(
if (value === undefined) {
return value;
}
const mapped = fontWeightMap[`${value}`.toLowerCase()];
const match = `${value}`.match(fontWeightReg);

let mapped;
if (match?.groups?.weight) {
mapped = fontWeightMap[match?.groups?.weight.toLowerCase()];
if (match.groups.style) {
mapped = `${mapped} ${match.groups.style.toLowerCase()}`;
}
}

return mapped ?? value;
}
18 changes: 12 additions & 6 deletions test/integration/expand-composition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ describe('expand composition tokens', () => {

it('only expands composition tokens by default', async () => {
const file = await promises.readFile(outputFilePath, 'utf-8');

expect(file).to.include(
`
--sdCompositionSize: 24px;
Expand All @@ -61,11 +62,12 @@ describe('expand composition tokens', () => {
--sdCompositionHeaderFontFamilies: Roboto;
--sdCompositionHeaderFontSizes: 96px;
--sdCompositionHeaderFontWeights: 700;
--sdTypography: 500 26px/1.25 Arial;
--sdTypography: 800 italic 26px/1.25 Arial;
--sdFontWeightRef: 800 italic;
--sdBorder: 4px solid #FFFF00;
--sdShadowSingle: inset 0 4px 10px 0 rgba(0,0,0,0.4);
--sdShadowDouble: inset 0 4px 10px 0 rgba(0,0,0,0.4), 0 8px 12px 5px rgba(0,0,0,0.4);
--sdRef: 500 26px/1.25 Arial;`,
--sdRef: 800 italic 26px/1.25 Arial;`,
);
});

Expand All @@ -91,14 +93,16 @@ describe('expand composition tokens', () => {
--sdCompositionHeaderFontSizes: 96px;
--sdCompositionHeaderFontWeights: 700;
--sdTypographyFontFamily: Arial;
--sdTypographyFontWeight: 500;
--sdTypographyFontWeight: 800;
--sdTypographyLineHeight: 1.25;
--sdTypographyFontSize: 26px;
--sdTypographyLetterSpacing: 0;
--sdTypographyParagraphSpacing: 0;
--sdTypographyParagraphIndent: 0;
--sdTypographyTextDecoration: none;
--sdTypographyTextCase: none;
--sdTypographyFontStyle: italic;
--sdFontWeightRef: 800 italic;
--sdBorderColor: #FFFF00;
--sdBorderWidth: 4px;
--sdBorderStyle: solid;
Expand Down Expand Up @@ -136,23 +140,25 @@ describe('expand composition tokens', () => {
expect(file).to.include(
`
--sdRefFontFamily: Arial;
--sdRefFontWeight: 500;
--sdRefFontWeight: 800;
--sdRefLineHeight: 1.25;
--sdRefFontSize: 26px;
--sdRefLetterSpacing: 0;
--sdRefParagraphSpacing: 0;
--sdRefParagraphIndent: 0;
--sdRefTextDecoration: none;
--sdRefTextCase: none;
--sdRefFontStyle: italic;
--sdDeepRefFontFamily: Arial;
--sdDeepRefFontWeight: 500;
--sdDeepRefFontWeight: 800;
--sdDeepRefLineHeight: 1.25;
--sdDeepRefFontSize: 26px;
--sdDeepRefLetterSpacing: 0;
--sdDeepRefParagraphSpacing: 0;
--sdDeepRefParagraphIndent: 0;
--sdDeepRefTextDecoration: none;
--sdDeepRefTextCase: none;`,
--sdDeepRefTextCase: none;
--sdDeepRefFontStyle: italic;`,
);
});
});
6 changes: 3 additions & 3 deletions test/integration/object-value-references.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ describe('typography references', () => {
const file = await promises.readFile(outputFilePath, 'utf-8');
expect(file).to.include(
`
--sdBefore: 700 36px/1 'Aria Sans';
--sdFontHeadingXxl: 700 36px/1 'Aria Sans';
--sdAfter: 700 36px/1 'Aria Sans';`,
--sdBefore: 400 italic 36px/1 'Aria Sans';
--sdFontHeadingXxl: 400 italic 36px/1 'Aria Sans';
--sdAfter: 400 italic 36px/1 'Aria Sans';`,
);
});

Expand Down
6 changes: 5 additions & 1 deletion test/integration/tokens/expand-composition.tokens.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"typography": {
"value": {
"fontFamily": "Arial",
"fontWeight": "500",
"fontWeight": "{fontWeightRef}",
"lineHeight": "1.25",
"fontSize": "26",
"letterSpacing": "0",
Expand All @@ -43,6 +43,10 @@
},
"type": "typography"
},
"fontWeightRef": {
"value": "ExtraBold italic",
"type": "fontWeights"
},
"border": {
"value": {
"color": "#FFFF00",
Expand Down
6 changes: 5 additions & 1 deletion test/integration/tokens/object-value-references.tokens.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"xxl": {
"value": {
"fontFamily": "Aria Sans",
"fontWeight": "bold",
"fontWeight": "{fontWeightRef}",
"lineHeight": "1",
"fontSize": "36",
"letterSpacing": "1",
Expand Down Expand Up @@ -39,6 +39,10 @@
"shadowRef": {
"value": "{shadow}"
},
"fontWeightRef": {
"value": "Regular Italic",
"type": "fontWeights"
},
"border": {
"value": {
"color": "#FFFF00",
Expand Down
6 changes: 6 additions & 0 deletions test/spec/checkAndEvaluateMath.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,10 @@ describe('check and evaluate math', () => {
expect(checkAndEvaluateMath('min(10, 24, 5, 12, 6) 8 * 14px')).to.equal('5 112px');
expect(checkAndEvaluateMath('ceil(roundTo(16/1.2,0)/2)*2')).to.equal('14');
});

it('does not unnecessarily remove wrapped quotes around font-family values', () => {
expect(checkAndEvaluateMath(`800 italic 16px/1 'Arial Black'`)).to.equal(
`800 italic 16px/1 'Arial Black'`,
);
});
});
20 changes: 19 additions & 1 deletion test/spec/css/transformTypographyForCSS.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { expect } from '@esm-bundle/chai';
import {
transformTypographyForCSS,
hasWhiteSpace,
isCommaSeparated,
} from '../../../src/css/transformTypography.js';
import { runTransformSuite } from '../../suites/transform-suite.spec.js';
Expand Down Expand Up @@ -69,4 +68,23 @@ describe('transform typography', () => {
}),
).to.equal('300 20px/1.5 sans-serif');
});

it('includes fontStyle if included in the fontWeight', () => {
expect(
transformTypographyForCSS({
fontWeight: 'light Italic',
fontSize: '20',
lineHeight: '1.5',
}),
).to.equal('300 italic 20px/1.5 sans-serif');

expect(
transformTypographyForCSS({
fontWeight: 'light',
fontSize: '20',
lineHeight: '1.5',
fontStyle: 'italic',
}),
).to.equal('300 italic 20px/1.5 sans-serif');
});
});
Loading