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 0b7e719..091b6ec 100644 --- a/utils/fontListener.ts +++ b/utils/fontListener.ts @@ -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) { @@ -49,6 +74,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/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 new file mode 100644 index 0000000..a34ed03 --- /dev/null +++ b/utils/parseFontInfo.ts @@ -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[] = [] + + 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 +} \ No newline at end of file