Skip to content

Font variation support #33

New issue

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

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

Already on GitHub? # to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
36 changes: 36 additions & 0 deletions consts/defaults.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,38 @@
export const MODE_DEFAULT = "async";
export const SCOPE_DEFAULT = "body";
export const TIMEOUT_DEFAULT = 30000;
export const INTERVAL_DEFAULT = 300;

export const WEIGHTS = {
1: "100",
2: "200",
3: "300",
4: "400",
5: "500",
6: "600",
7: "700",
8: "800",
9: "900",
100: "1",
200: "2",
300: "3",
400: "4",
500: "5",
600: "6",
700: "7",
800: "8",
900: "9",
normal: "4",
bold: "7",
}

export const STYLES = {
n: "normal",
i: "italic",
o: "oblique",
normal: "n",
italic: "i",
oblique: "o",
}

export const VARIATION_MATCH = new RegExp("^(n|i)([1-9])$")
28 changes: 16 additions & 12 deletions gatsby-browser.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
import React from "react";
import { AsyncFonts } from "./components";
import { MODE_DEFAULT, SCOPE_DEFAULT } from "./consts";
import { getFontFiles, getFontNames } from "./utils";
import { fontListener } from "./utils/fontListener";
import React from 'react';
import { AsyncFonts } from './components';
import { MODE_DEFAULT, SCOPE_DEFAULT, TIMEOUT_DEFAULT, INTERVAL_DEFAULT } from './consts';
import { getFontFiles, getFontNames } from './utils';
import { fontListener } from './utils/fontListener';

export const onClientEntry = (
_,
{ custom = [], web = [], enableListener = false, scope = SCOPE_DEFAULT }
{
custom = [],
web = [],
enableListener = false,
scope = SCOPE_DEFAULT,
timeout = TIMEOUT_DEFAULT,
interval = INTERVAL_DEFAULT,
}
) => {
if (!enableListener) {
return;
}

const allFonts = [...custom, ...web];
const fontNames = getFontNames(allFonts);
const listenerProps = { fontNames, scope };
const listenerProps = { fontNames, scope, timeout, interval };

fontListener(listenerProps);
};

export const wrapRootElement = (
{ element },
{ custom = [], web = [], mode = MODE_DEFAULT }
) => {
if (mode !== "async") {
export const wrapRootElement = ({ element }, { custom = [], web = [], mode = MODE_DEFAULT }) => {
if (mode !== 'async') {
return element;
}

Expand Down
64 changes: 45 additions & 19 deletions utils/fontListener.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,71 @@
import { kebabCase } from "../utils";
import { info, warn } from './logger';
import { convertToFVD, parseFontInfo } from './parseFontInfo';

declare var document: { fonts: any };
export type FontInfo = { fontName: string; fontStyle: string; fontWeight: string }

export const fontListener = ({ fontNames, scope, timeout, interval }) => {

export const fontListener = ({ fontNames, scope }) => {
const hasFonts = fontNames && Boolean(fontNames.length);
const targetElement = scope === "html" ? "documentElement" : "body";
const apiAvailable = "fonts" in document;

let parsedFont: FontInfo[] = [];

function handleLoadComplete() {
addClassName("all");
}

function handleFontLoad(fontFaces: FontFace[]) {
fontFaces.forEach((fontFace) => {
addClassName(fontFace.family);
})
function handleFontLoad(fontInfo: FontInfo) {
const fvd = convertToFVD(fontInfo)
addClassName(fvd);
}

function fontMapper(fontName) {
return document.fonts
.load(`1rem ${fontName}`)
.then(handleFontLoad)
.catch(errorFallback);
function fontMapper(fontDetail: FontInfo) {
const fontFace = [fontDetail.fontStyle, fontDetail.fontWeight, '1rem', fontDetail.fontName].join(' ')
const startTime = Date.now();

return new Promise((resolve, reject) => {
const recursiveFn = () => {
const currTime = Date.now();

if ((currTime - startTime) >= timeout) {
reject('font listener timeout ' + fontFace);
} else {
document.fonts.load(fontFace).then((fonts) => {
if (fonts.length >= 1) {
handleFontLoad(fontDetail);
resolve(true);
} else {
setTimeout(recursiveFn, interval);
}
}).catch((err) => {
reject(err);
});
}
};
recursiveFn()
});

}

function loadFonts() {
const fonts = fontNames.map(fontMapper);
const fonts = parsedFont.map(fontMapper);
Promise.all(fonts).then(handleLoadComplete).catch(errorFallback);
}

function errorFallback() {
fontNames.forEach(addClassName);
function errorFallback(e) {
warn('error in omni font loader', e)
parsedFont.forEach((fontInfo) => addClassName(convertToFVD(fontInfo)));
}

function handleApiError(error) {
console.info(`document.fonts API error: ${error}`);
console.info(`Replacing fonts instantly. FOUT handling failed.`);
errorFallback();
info(`document.fonts API error: ${error}`);
info(`Replacing fonts instantly. FOUT handling failed.`);
errorFallback(error);
}

function addClassName(fontName: string) {
document[targetElement].classList.add(`wf-${kebabCase(fontName)}`);
document[targetElement].classList.add(`wf-${fontName}`);
}

if (!apiAvailable) {
Expand All @@ -49,6 +74,7 @@ export const fontListener = ({ fontNames, scope }) => {
}

if (hasFonts && apiAvailable) {
parsedFont = parseFontInfo(fontNames)
loadFonts();
}
};
4 changes: 2 additions & 2 deletions utils/getFontNames.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const getFontNames = (allFonts: { name: string }[]) => {
const fontNames = []
export const getFontNames = (allFonts: { name: string | string[] }[]) => {
const fontNames: string[] = []
allFonts.forEach(({ name }) =>
Array.isArray(name) ? fontNames.push(...name) : fontNames.push(name)
)
Expand Down
20 changes: 20 additions & 0 deletions utils/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

const warn = (function (environment) {
if (environment === "production") {
return () => { }
}
return (...args) => {
console.warn(...args)
}
})(process.env.NODE_ENV);

const info = (function (environment) {
if (environment === "production") {
return () => { }
}
return (...args) => {
console.info(...args)
}
})(process.env.NODE_ENV);

export { warn, info }
87 changes: 87 additions & 0 deletions utils/parseFontInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { kebabCase } from "../utils";
import { FontInfo } from './fontListener';
import { VARIATION_MATCH, WEIGHTS, STYLES } from '../consts';

export const parseFontInfo = (fontFamilies: string[]) => {
const length = fontFamilies.length

const parsedFonts: FontInfo[] = []
for (let i = 0; i < length; i++) {
const elements = fontFamilies[i].split(":")
const fontFamily = elements[0].replace(/\+/g, " ")
let variations = [{ fontStyle: '', fontWeight: '' }]

if (elements.length >= 2) {
const fvds = parseVariations(elements[1])

if (fvds.length > 0) {
variations = fvds
}
}

for (let j = 0; j < variations.length; j += 1) {
parsedFonts.push({ fontName: fontFamily, ...variations[j] })
}
}
return parsedFonts
}

const generateFontVariationDescription = (variation: string) => {
const normalizedVariation = variation.toLowerCase()
const groups = VARIATION_MATCH.exec(normalizedVariation)
if (groups == null) {
return ""
}
const styleMatch = normalizeStyle(groups[1])
const weightMatch = normalizeWeight(groups[2])
return (
{
fontStyle: styleMatch,
fontWeight: weightMatch
}
)
}

export const normalizeStyle = (parsedStyle: string): string => {
if (!parsedStyle) {
return ""
}
return STYLES[parsedStyle]
}

export const normalizeWeight = (parsedWeight: string | number): string => {
if (!parsedWeight) {
return ""
}
return WEIGHTS[parsedWeight]

}

const parseVariations = (variations: string) => {
let finalVariations: Omit<FontInfo, 'fontName'>[] = []

if (!variations) {
return finalVariations
}
const providedVariations = variations.split(",")
const length = providedVariations.length

for (let i = 0; i < length; i++) {
let variation = providedVariations[i]
const fvd = generateFontVariationDescription(variation)

if (fvd) {
finalVariations.push(fvd)
}
}
return finalVariations
}


export const convertToFVD = ({fontName, fontStyle, fontWeight}: FontInfo) => {
const weightVal = normalizeWeight(fontWeight)
const styleVal = normalizeStyle(fontStyle)
const styleWeight = styleVal + weightVal
const fontNameVal = kebabCase(fontName)
return styleWeight ? [fontNameVal, styleWeight].join('-') : fontNameVal
}