From 3d0121836b0a8f589139da31ffdcd7deb0127c2f Mon Sep 17 00:00:00 2001 From: Himanshu Patil Date: Mon, 8 Aug 2022 00:29:48 +0530 Subject: [PATCH 1/3] added support for font variations --- utils/fontListener.ts | 58 +++++++++++++------ utils/getFontNames.ts | 4 +- utils/parseFontInfo.ts | 124 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 19 deletions(-) create mode 100644 utils/parseFontInfo.ts diff --git a/utils/fontListener.ts b/utils/fontListener.ts index 0b7e719..64d6dc4 100644 --- a/utils/fontListener.ts +++ b/utils/fontListener.ts @@ -1,46 +1,69 @@ -import { kebabCase } from "../utils"; - -declare var document: { fonts: any }; +import { convertToFVD, parseFontInfo } from './parseFontInfo'; +export type FontInfo = { fontName: string; fontStyle: string; fontWeight: string } +const fontLoadConst = { + interval: 10, + timeout: 5000, +} 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(' ') + // refer https://stackoverflow.com/a/64192936/9740955 + return new Promise((resolve, reject) => { + const poller = setInterval(async () => { + try { + await document.fonts.load(fontFace); + } catch (err) { + clearTimeout(timeOut) + clearInterval(poller); + errorFallback(err) + reject(err) + } + if (document.fonts.check(fontFace)) { + clearTimeout(timeOut) + clearInterval(poller); + handleFontLoad(fontDetail) + resolve(true); + } + }, fontLoadConst.interval); + const timeOut = setTimeout(() => clearInterval(poller), fontLoadConst.timeout); + }) } 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) { + console.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(); + errorFallback(error); } function addClassName(fontName: string) { - document[targetElement].classList.add(`wf-${kebabCase(fontName)}`); + document[targetElement].classList.add(`wf-${fontName}`); } if (!apiAvailable) { @@ -49,6 +72,7 @@ export const fontListener = ({ fontNames, scope }) => { } if (hasFonts && apiAvailable) { + parsedFont = parseFontInfo(fontNames) loadFonts(); } }; diff --git a/utils/getFontNames.ts b/utils/getFontNames.ts index ff17e22..2e02645 100644 --- a/utils/getFontNames.ts +++ b/utils/getFontNames.ts @@ -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) ) diff --git a/utils/parseFontInfo.ts b/utils/parseFontInfo.ts new file mode 100644 index 0000000..ea3f501 --- /dev/null +++ b/utils/parseFontInfo.ts @@ -0,0 +1,124 @@ +import { kebabCase } from "../utils"; +import { FontInfo } from './fontListener'; + + +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", +} + + +const styles = { + n: "normal", + i: "italic", + o: "oblique", + normal: "n", + italic: "i", + oblique: "o", +} + +const VARIATION_MATCH = new RegExp("^(n|i)([1-9])$") + + +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[] = [] + + 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 = (fontInfo: FontInfo) => { + const weightVal = normalizeWeight(fontInfo.fontWeight) + const styleVal = normalizeStyle(fontInfo.fontStyle) + const styleWeight = styleVal + weightVal + const fontNameVal = kebabCase(fontInfo.fontName) + return styleWeight ? [fontNameVal, styleWeight].join('-') : fontNameVal + +} \ No newline at end of file From b3facd1ba8b23605ee44cc225266d18afa02b46a Mon Sep 17 00:00:00 2001 From: Himanshu Patil Date: Mon, 8 Aug 2022 10:32:49 +0530 Subject: [PATCH 2/3] better handling of font listener --- consts/defaults.ts | 36 +++++++++++++++++++++++++++ gatsby-browser.js | 28 ++++++++++++--------- utils/fontListener.ts | 55 ++++++++++++++++++++++-------------------- utils/logger.ts | 20 +++++++++++++++ utils/parseFontInfo.ts | 51 ++++++--------------------------------- 5 files changed, 108 insertions(+), 82 deletions(-) create mode 100644 utils/logger.ts diff --git a/consts/defaults.ts b/consts/defaults.ts index da455bd..fcc2f36 100644 --- a/consts/defaults.ts +++ b/consts/defaults.ts @@ -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])$") diff --git a/gatsby-browser.js b/gatsby-browser.js index 4ba46ad..011381a 100644 --- a/gatsby-browser.js +++ b/gatsby-browser.js @@ -1,12 +1,19 @@ -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; @@ -14,16 +21,13 @@ export const onClientEntry = ( 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; } diff --git a/utils/fontListener.ts b/utils/fontListener.ts index 64d6dc4..7e53a94 100644 --- a/utils/fontListener.ts +++ b/utils/fontListener.ts @@ -1,11 +1,9 @@ +import { info, warn } from './logger'; import { convertToFVD, parseFontInfo } from './parseFontInfo'; export type FontInfo = { fontName: string; fontStyle: string; fontWeight: string } -const fontLoadConst = { - interval: 10, - timeout: 5000, -} -export const fontListener = ({ fontNames, scope }) => { + +export const fontListener = ({ fontNames, scope, timeout, interval }) => { const hasFonts = fontNames && Boolean(fontNames.length); const targetElement = scope === "html" ? "documentElement" : "body"; @@ -24,26 +22,31 @@ export const fontListener = ({ fontNames, scope }) => { function fontMapper(fontDetail: FontInfo) { const fontFace = [fontDetail.fontStyle, fontDetail.fontWeight, '1rem', fontDetail.fontName].join(' ') - // refer https://stackoverflow.com/a/64192936/9740955 + const startTime = Date.now(); + return new Promise((resolve, reject) => { - const poller = setInterval(async () => { - try { - await document.fonts.load(fontFace); - } catch (err) { - clearTimeout(timeOut) - clearInterval(poller); - errorFallback(err) - reject(err) - } - if (document.fonts.check(fontFace)) { - clearTimeout(timeOut) - clearInterval(poller); - handleFontLoad(fontDetail) - resolve(true); + 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) => { + errorFallback(err); + reject(err); + }); } - }, fontLoadConst.interval); - const timeOut = setTimeout(() => clearInterval(poller), fontLoadConst.timeout); - }) + }; + recursiveFn() + }); + } function loadFonts() { @@ -52,13 +55,13 @@ export const fontListener = ({ fontNames, scope }) => { } function errorFallback(e) { - console.warn('error in omni font loader', 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.`); + info(`document.fonts API error: ${error}`); + info(`Replacing fonts instantly. FOUT handling failed.`); errorFallback(error); } diff --git a/utils/logger.ts b/utils/logger.ts new file mode 100644 index 0000000..3b0b7a7 --- /dev/null +++ b/utils/logger.ts @@ -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 } \ No newline at end of file diff --git a/utils/parseFontInfo.ts b/utils/parseFontInfo.ts index ea3f501..a34ed03 100644 --- a/utils/parseFontInfo.ts +++ b/utils/parseFontInfo.ts @@ -1,42 +1,6 @@ import { kebabCase } from "../utils"; import { FontInfo } from './fontListener'; - - -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", -} - - -const styles = { - n: "normal", - i: "italic", - o: "oblique", - normal: "n", - italic: "i", - oblique: "o", -} - -const VARIATION_MATCH = new RegExp("^(n|i)([1-9])$") - +import { VARIATION_MATCH, WEIGHTS, STYLES } from '../consts'; export const parseFontInfo = (fontFamilies: string[]) => { const length = fontFamilies.length @@ -82,14 +46,14 @@ export const normalizeStyle = (parsedStyle: string): string => { if (!parsedStyle) { return "" } - return styles[parsedStyle] + return STYLES[parsedStyle] } export const normalizeWeight = (parsedWeight: string | number): string => { if (!parsedWeight) { return "" } - return weights[parsedWeight] + return WEIGHTS[parsedWeight] } @@ -114,11 +78,10 @@ const parseVariations = (variations: string) => { } -export const convertToFVD = (fontInfo: FontInfo) => { - const weightVal = normalizeWeight(fontInfo.fontWeight) - const styleVal = normalizeStyle(fontInfo.fontStyle) +export const convertToFVD = ({fontName, fontStyle, fontWeight}: FontInfo) => { + const weightVal = normalizeWeight(fontWeight) + const styleVal = normalizeStyle(fontStyle) const styleWeight = styleVal + weightVal - const fontNameVal = kebabCase(fontInfo.fontName) + const fontNameVal = kebabCase(fontName) return styleWeight ? [fontNameVal, styleWeight].join('-') : fontNameVal - } \ No newline at end of file From 035f4c8580d4f874d7c468d15fed59ae2ce50aa7 Mon Sep 17 00:00:00 2001 From: Himanshu Patil Date: Mon, 8 Aug 2022 11:18:07 +0530 Subject: [PATCH 3/3] minor change --- utils/fontListener.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/utils/fontListener.ts b/utils/fontListener.ts index 7e53a94..091b6ec 100644 --- a/utils/fontListener.ts +++ b/utils/fontListener.ts @@ -38,8 +38,7 @@ export const fontListener = ({ fontNames, scope, timeout, interval }) => { } else { setTimeout(recursiveFn, interval); } - }).catch((err) => { - errorFallback(err); + }).catch((err) => { reject(err); }); }