-
Notifications
You must be signed in to change notification settings - Fork 26k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor and add more tests to next/font/local (#46627)
Refactor the logic of picking the font file to use for the font fallback generation. Add additional tests that checks that the correct font file is picked. Also adds more comments explaining the logic and assumptions behind the functions. ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm build && pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
- Loading branch information
1 parent
63d81de
commit 7bf5dcb
Showing
4 changed files
with
242 additions
and
94 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
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
116 changes: 116 additions & 0 deletions
116
packages/font/src/local/pick-font-file-for-fallback-generation.test.ts
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,116 @@ | ||
import { pickFontFileForFallbackGeneration } from './pick-font-file-for-fallback-generation' | ||
|
||
describe('pickFontFileForFallbackGeneration', () => { | ||
it('should pick the weight closest to 400', () => { | ||
expect( | ||
pickFontFileForFallbackGeneration([ | ||
{ | ||
weight: '300', | ||
}, | ||
{ | ||
weight: '600', | ||
}, | ||
]) | ||
).toEqual({ | ||
weight: '300', | ||
}) | ||
|
||
expect( | ||
pickFontFileForFallbackGeneration([ | ||
{ weight: '200' }, | ||
{ | ||
weight: '500', | ||
}, | ||
]) | ||
).toEqual({ | ||
weight: '500', | ||
}) | ||
|
||
expect( | ||
pickFontFileForFallbackGeneration([ | ||
{ | ||
weight: 'normal', | ||
}, | ||
{ | ||
weight: '700', | ||
}, | ||
]) | ||
).toEqual({ | ||
weight: 'normal', | ||
}) | ||
|
||
expect( | ||
pickFontFileForFallbackGeneration([ | ||
{ | ||
weight: 'bold', | ||
}, | ||
{ | ||
weight: '900', | ||
}, | ||
]) | ||
).toEqual({ | ||
weight: 'bold', | ||
}) | ||
}) | ||
|
||
it('should pick the thinner weight if both have the same distance to 400', () => { | ||
expect( | ||
pickFontFileForFallbackGeneration([ | ||
{ | ||
weight: '300', | ||
}, | ||
{ | ||
weight: '500', | ||
}, | ||
]) | ||
).toEqual({ | ||
weight: '300', | ||
}) | ||
}) | ||
|
||
it('should pick variable range closest to 400', () => { | ||
expect( | ||
pickFontFileForFallbackGeneration([ | ||
{ | ||
weight: '100 300', | ||
}, | ||
{ | ||
weight: '600 900', | ||
}, | ||
]) | ||
).toEqual({ | ||
weight: '100 300', | ||
}) | ||
|
||
expect( | ||
pickFontFileForFallbackGeneration([ | ||
{ weight: '100 200' }, | ||
{ | ||
weight: '500 800', | ||
}, | ||
]) | ||
).toEqual({ | ||
weight: '500 800', | ||
}) | ||
|
||
expect( | ||
pickFontFileForFallbackGeneration([ | ||
{ weight: '100 900' }, | ||
{ | ||
weight: '300 399', | ||
}, | ||
]) | ||
).toEqual({ | ||
weight: '100 900', | ||
}) | ||
}) | ||
|
||
it('should prefer normal style over italic', () => { | ||
expect( | ||
pickFontFileForFallbackGeneration([ | ||
{ weight: '400', style: 'normal' }, | ||
{ weight: '400', style: 'italic' }, | ||
]) | ||
).toEqual({ weight: '400', style: 'normal' }) | ||
}) | ||
}) |
104 changes: 104 additions & 0 deletions
104
packages/font/src/local/pick-font-file-for-fallback-generation.ts
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,104 @@ | ||
import { nextFontError } from '../utils' | ||
|
||
const NORMAL_WEIGHT = 400 | ||
const BOLD_WEIGHT = 700 | ||
|
||
/** | ||
* Convert the weight string to a number so it can be used for comparison. | ||
* Weights can be defined as a number, 'normal' or 'bold'. https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-weight | ||
*/ | ||
function getWeightNumber(weight: string) { | ||
return weight === 'normal' | ||
? NORMAL_WEIGHT | ||
: weight === 'bold' | ||
? BOLD_WEIGHT | ||
: Number(weight) | ||
} | ||
|
||
/** | ||
* Get the distance from normal (400) weight for the provided weight. | ||
* If it's not a variable font we can just return the distance. | ||
* If it's a variable font we need to compare its weight range to 400. | ||
*/ | ||
function getDistanceFromNormalWeight(weight?: string) { | ||
if (!weight) return 0 | ||
|
||
// If it's a variable font the weight is defined with two numbers "100 900", rather than just one "400" | ||
const [firstWeight, secondWeight] = weight | ||
.trim() | ||
.split(/ +/) | ||
.map(getWeightNumber) | ||
|
||
if (Number.isNaN(firstWeight) || Number.isNaN(secondWeight)) { | ||
nextFontError( | ||
`Invalid weight value in src array: \`${weight}\`.\nExpected \`normal\`, \`bold\` or a number.` | ||
) | ||
} | ||
|
||
// If the weight doesn't have have a second value, it's not a variable font | ||
// If that's the case, just return the distance from normal weight | ||
if (!secondWeight) { | ||
return firstWeight - NORMAL_WEIGHT | ||
} | ||
|
||
// Normal weight is within variable font range | ||
if (firstWeight <= NORMAL_WEIGHT && secondWeight >= NORMAL_WEIGHT) { | ||
return 0 | ||
} | ||
|
||
// Normal weight is outside variable font range | ||
// Return the distance of normal weight to the variable font range | ||
const firstWeightDistance = firstWeight - NORMAL_WEIGHT | ||
const secondWeightDistance = secondWeight - NORMAL_WEIGHT | ||
if (Math.abs(firstWeightDistance) < Math.abs(secondWeightDistance)) { | ||
return firstWeightDistance | ||
} | ||
return secondWeightDistance | ||
} | ||
|
||
/** | ||
* If multiple font files are provided for a font family, we need to pick one to use for the automatic fallback generation. | ||
* This function returns the font file that is most likely to be used for the bulk of the text on a page. | ||
* | ||
* There are some assumptions here about the text on a page when picking the font file: | ||
* - Most of the text will have normal weight, use the one closest to 400 | ||
* - Most of the text will have normal style, prefer normal over italic | ||
* - If two font files have the same distance from normal weight, the thinner one will most likely be the bulk of the text | ||
*/ | ||
export function pickFontFileForFallbackGeneration< | ||
T extends { style?: string; weight?: string } | ||
>(fontFiles: T[]): T { | ||
return fontFiles.reduce((usedFontFile, currentFontFile) => { | ||
if (!usedFontFile) return currentFontFile | ||
|
||
const usedFontDistance = getDistanceFromNormalWeight(usedFontFile.weight) | ||
const currentFontDistance = getDistanceFromNormalWeight( | ||
currentFontFile.weight | ||
) | ||
|
||
// Prefer normal style if they have the same weight | ||
if ( | ||
usedFontDistance === currentFontDistance && | ||
(typeof currentFontFile.style === 'undefined' || | ||
currentFontFile.style === 'normal') | ||
) { | ||
return currentFontFile | ||
} | ||
|
||
const absUsedDistance = Math.abs(usedFontDistance) | ||
const absCurrentDistance = Math.abs(currentFontDistance) | ||
|
||
// Use closest absolute distance to normal weight | ||
if (absCurrentDistance < absUsedDistance) return currentFontFile | ||
|
||
// Prefer the thinner font if both have the same absolute distance from normal weight | ||
if ( | ||
absUsedDistance === absCurrentDistance && | ||
currentFontDistance < usedFontDistance | ||
) { | ||
return currentFontFile | ||
} | ||
|
||
return usedFontFile | ||
}) | ||
} |