diff --git a/api/gist.js b/api/gist.js index d6713eafcef43..02d43d079d510 100644 --- a/api/gist.js +++ b/api/gist.js @@ -11,7 +11,9 @@ import { setErrorCacheHeaders, } from "../src/common/cache.js"; import { guardAccess } from "../src/common/access.js"; +import { retrieveSecondaryMessage } from "../src/common/error.js"; +// @ts-ignore export default async (req, res) => { const { id, @@ -89,10 +91,24 @@ export default async (req, res) => { ); } catch (err) { setErrorCacheHeaders(res); + if (err instanceof Error) { + return res.send( + renderError({ + message: err.message, + secondaryMessage: retrieveSecondaryMessage(err), + renderOptions: { + title_color, + text_color, + bg_color, + border_color, + theme, + }, + }), + ); + } return res.send( renderError({ - message: err.message, - secondaryMessage: err.secondaryMessage, + message: "An unknown error occurred", renderOptions: { title_color, text_color, diff --git a/api/index.js b/api/index.js index d5e55fbbfcd60..c8c5de63bc703 100644 --- a/api/index.js +++ b/api/index.js @@ -8,10 +8,12 @@ import { setCacheHeaders, setErrorCacheHeaders, } from "../src/common/cache.js"; +import { retrieveSecondaryMessage } from "../src/common/error.js"; import { parseArray, parseBoolean, renderError } from "../src/common/utils.js"; import { fetchStats } from "../src/fetchers/stats.js"; import { isLocaleAvailable } from "../src/translations.js"; +// @ts-ignore export default async (req, res) => { const { username, @@ -127,10 +129,24 @@ export default async (req, res) => { ); } catch (err) { setErrorCacheHeaders(res); + if (err instanceof Error) { + return res.send( + renderError({ + message: err.message, + secondaryMessage: retrieveSecondaryMessage(err), + renderOptions: { + title_color, + text_color, + bg_color, + border_color, + theme, + }, + }), + ); + } return res.send( renderError({ - message: err.message, - secondaryMessage: err.secondaryMessage, + message: "An unknown error occurred", renderOptions: { title_color, text_color, diff --git a/api/pin.js b/api/pin.js index e1f46b32c34b9..c8fa592468fcd 100644 --- a/api/pin.js +++ b/api/pin.js @@ -8,10 +8,12 @@ import { setCacheHeaders, setErrorCacheHeaders, } from "../src/common/cache.js"; +import { retrieveSecondaryMessage } from "../src/common/error.js"; import { parseBoolean, renderError } from "../src/common/utils.js"; import { fetchRepo } from "../src/fetchers/repo.js"; import { isLocaleAvailable } from "../src/translations.js"; +// @ts-ignore export default async (req, res) => { const { username, @@ -92,10 +94,24 @@ export default async (req, res) => { ); } catch (err) { setErrorCacheHeaders(res); + if (err instanceof Error) { + return res.send( + renderError({ + message: err.message, + secondaryMessage: retrieveSecondaryMessage(err), + renderOptions: { + title_color, + text_color, + bg_color, + border_color, + theme, + }, + }), + ); + } return res.send( renderError({ - message: err.message, - secondaryMessage: err.secondaryMessage, + message: "An unknown error occurred", renderOptions: { title_color, text_color, diff --git a/api/top-langs.js b/api/top-langs.js index df286b483d929..47c3c29d3713b 100644 --- a/api/top-langs.js +++ b/api/top-langs.js @@ -8,10 +8,12 @@ import { setCacheHeaders, setErrorCacheHeaders, } from "../src/common/cache.js"; +import { retrieveSecondaryMessage } from "../src/common/error.js"; import { parseArray, parseBoolean, renderError } from "../src/common/utils.js"; import { fetchTopLanguages } from "../src/fetchers/top-languages.js"; import { isLocaleAvailable } from "../src/translations.js"; +// @ts-ignore export default async (req, res) => { const { username, @@ -150,10 +152,24 @@ export default async (req, res) => { ); } catch (err) { setErrorCacheHeaders(res); + if (err instanceof Error) { + return res.send( + renderError({ + message: err.message, + secondaryMessage: retrieveSecondaryMessage(err), + renderOptions: { + title_color, + text_color, + bg_color, + border_color, + theme, + }, + }), + ); + } return res.send( renderError({ - message: err.message, - secondaryMessage: err.secondaryMessage, + message: "An unknown error occurred", renderOptions: { title_color, text_color, diff --git a/api/wakatime.js b/api/wakatime.js index b8642649be219..9cb995e2bbcbb 100644 --- a/api/wakatime.js +++ b/api/wakatime.js @@ -11,7 +11,9 @@ import { setErrorCacheHeaders, } from "../src/common/cache.js"; import { guardAccess } from "../src/common/access.js"; +import { retrieveSecondaryMessage } from "../src/common/error.js"; +// @ts-ignore export default async (req, res) => { const { username, @@ -108,10 +110,24 @@ export default async (req, res) => { ); } catch (err) { setErrorCacheHeaders(res); + if (err instanceof Error) { + return res.send( + renderError({ + message: err.message, + secondaryMessage: retrieveSecondaryMessage(err), + renderOptions: { + title_color, + text_color, + bg_color, + border_color, + theme, + }, + }), + ); + } return res.send( renderError({ - message: err.message, - secondaryMessage: err.secondaryMessage, + message: "An unknown error occurred", renderOptions: { title_color, text_color, diff --git a/scripts/preview-theme.js b/scripts/preview-theme.js index 0f6de3729daf9..b9e53ffe97aad 100644 --- a/scripts/preview-theme.js +++ b/scripts/preview-theme.js @@ -12,7 +12,7 @@ import Hjson from "hjson"; import snakeCase from "lodash.snakecase"; import parse from "parse-diff"; import { inspect } from "util"; -import { isValidHexColor, isValidGradient } from "../src/common/utils.js"; +import { isValidHexColor, isValidGradient } from "../src/common/color.js"; import { themes } from "../themes/index.js"; import { getGithubToken, getRepoInfo } from "./helpers.js"; diff --git a/src/cards/gist.js b/src/cards/gist.js index 9e889e74424cd..ecace8381495f 100644 --- a/src/cards/gist.js +++ b/src/cards/gist.js @@ -1,7 +1,6 @@ // @ts-check import { - getCardColors, parseEmojis, wrapTextMultiline, encodeHTML, @@ -12,6 +11,7 @@ import { createLanguageNode, } from "../common/utils.js"; import Card from "../common/Card.js"; +import { getCardColors } from "../common/color.js"; import { icons } from "../common/icons.js"; /** Import language colors. @@ -96,6 +96,7 @@ const renderGistCard = (gistData, options = {}) => { ); const languageName = language || "Unspecified"; + // @ts-ignore const languageColor = languageColors[languageName] || "#858585"; const svgLanguage = createLanguageNode(languageName, languageColor); diff --git a/src/cards/repo.js b/src/cards/repo.js index bbfda52d47778..9f28c7a5c2555 100644 --- a/src/cards/repo.js +++ b/src/cards/repo.js @@ -1,11 +1,12 @@ // @ts-check + import { Card } from "../common/Card.js"; +import { getCardColors } from "../common/color.js"; import { I18n } from "../common/I18n.js"; import { icons } from "../common/icons.js"; import { encodeHTML, flexLayout, - getCardColors, kFormatter, measureText, parseEmojis, diff --git a/src/cards/stats.js b/src/cards/stats.js index 6e5bdd5186a74..a0b12096e3aa5 100644 --- a/src/cards/stats.js +++ b/src/cards/stats.js @@ -1,13 +1,13 @@ // @ts-check import { Card } from "../common/Card.js"; +import { getCardColors } from "../common/color.js"; import { CustomError } from "../common/error.js"; import { I18n } from "../common/I18n.js"; import { icons, rankIcon } from "../common/icons.js"; import { clampValue, flexLayout, - getCardColors, kFormatter, measureText, } from "../common/utils.js"; @@ -392,21 +392,24 @@ const renderStatsCard = (stats, options = {}) => { // filter out hidden stats defined by user & create the text nodes const statItems = Object.keys(STATS) .filter((key) => !hide.includes(key)) - .map((key, index) => + .map((key, index) => { + // @ts-ignore + const stats = STATS[key]; + // create the text nodes, and pass index so that we can calculate the line spacing - createTextNode({ - icon: STATS[key].icon, - label: STATS[key].label, - value: STATS[key].value, - id: STATS[key].id, - unitSymbol: STATS[key].unitSymbol, + return createTextNode({ + icon: stats.icon, + label: stats.label, + value: stats.value, + id: stats.id, + unitSymbol: stats.unitSymbol, index, showIcons: show_icons, shiftValuePos: 79.01 + (isLongLocale ? 50 : 0), bold: text_bold, number_format, - }), - ); + }); + }); if (statItems.length === 0 && hide_rank) { throw new CustomError( @@ -541,14 +544,16 @@ const renderStatsCard = (stats, options = {}) => { const labels = Object.keys(STATS) .filter((key) => !hide.includes(key)) .map((key) => { + // @ts-ignore + const stats = STATS[key]; if (key === "commits") { return `${i18n.t("statcard.commits")} ${getTotalCommitsYearLabel( include_all_commits, commits_year, i18n, - )} : ${STATS[key].value}`; + )} : ${stats.value}`; } - return `${STATS[key].label}: ${STATS[key].value}`; + return `${stats.label}: ${stats.value}`; }) .join(", "); diff --git a/src/cards/top-languages.js b/src/cards/top-languages.js index fec72c7c391c8..76c02fad2156a 100644 --- a/src/cards/top-languages.js +++ b/src/cards/top-languages.js @@ -1,12 +1,13 @@ // @ts-check + import { Card } from "../common/Card.js"; +import { getCardColors } from "../common/color.js"; import { createProgressNode } from "../common/createProgressNode.js"; import { I18n } from "../common/I18n.js"; import { chunkArray, clampValue, flexLayout, - getCardColors, lowercaseTrim, measureText, formatBytes, @@ -180,6 +181,7 @@ const trimTopLanguages = (topLangs, langs_count, hide) => { // while filtering out if (hide) { hide.forEach((langName) => { + // @ts-ignore langsToHide[lowercaseTrim(langName)] = true; }); } @@ -188,6 +190,7 @@ const trimTopLanguages = (topLangs, langs_count, hide) => { langs = langs .sort((a, b) => b.size - a.size) .filter((lang) => { + // @ts-ignore return !langsToHide[lowercaseTrim(lang.name)]; }) .slice(0, langsCount); diff --git a/src/cards/wakatime.js b/src/cards/wakatime.js index 64ded83d41692..5dadcdab905bf 100644 --- a/src/cards/wakatime.js +++ b/src/cards/wakatime.js @@ -1,14 +1,10 @@ // @ts-check import { Card } from "../common/Card.js"; +import { getCardColors } from "../common/color.js"; import { createProgressNode } from "../common/createProgressNode.js"; import { I18n } from "../common/I18n.js"; -import { - clampValue, - flexLayout, - getCardColors, - lowercaseTrim, -} from "../common/utils.js"; +import { clampValue, flexLayout, lowercaseTrim } from "../common/utils.js"; import { wakatimeCardLocales } from "../translations.js"; /** Import language colors. @@ -69,6 +65,7 @@ const formatLanguageValue = ({ display_format, lang }) => { * @returns {string} The compact layout language SVG node. */ const createCompactLangNode = ({ lang, x, y, display_format }) => { + // @ts-ignore const color = languageColors[lang.name] || "#858585"; const value = formatLanguageValue({ display_format, lang }); @@ -329,6 +326,7 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { // const progress = (width * lang.percent) / 100; const progress = ((width - 25) * language.percent) / 100; + // @ts-ignore const languageColor = languageColors[language.name] || "#858585"; const output = ` diff --git a/src/common/cache.js b/src/common/cache.js index 3e88e3c9c4fae..596aa5e3db87f 100644 --- a/src/common/cache.js +++ b/src/common/cache.js @@ -86,7 +86,7 @@ const resolveCacheSeconds = ({ requested, def, min, max }) => { /** * Disables caching by setting appropriate headers on the response object. * - * @param {Object} res The response object. + * @param {any} res The response object. */ const disableCaching = (res) => { // Disable caching for browsers, shared caches/CDNs, and GitHub Camo. @@ -101,7 +101,7 @@ const disableCaching = (res) => { /** * Sets the Cache-Control headers on the response object. * - * @param {Object} res The response object. + * @param {any} res The response object. * @param {number} cacheSeconds The cache seconds to set in the headers. */ const setCacheHeaders = (res, cacheSeconds) => { @@ -121,7 +121,7 @@ const setCacheHeaders = (res, cacheSeconds) => { /** * Sets the Cache-Control headers for error responses on the response object. * - * @param {Object} res The response object. + * @param {any} res The response object. */ const setErrorCacheHeaders = (res) => { const envCacheSeconds = process.env.CACHE_SECONDS diff --git a/src/common/color.js b/src/common/color.js new file mode 100644 index 0000000000000..759a07ca25c7b --- /dev/null +++ b/src/common/color.js @@ -0,0 +1,141 @@ +import { themes } from "../../themes/index.js"; + +/** + * Checks if a string is a valid hex color. + * + * @param {string} hexColor String to check. + * @returns {boolean} True if the given string is a valid hex color. + */ +const isValidHexColor = (hexColor) => { + return new RegExp( + /^([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{4})$/, + ).test(hexColor); +}; + +/** + * Check if the given string is a valid gradient. + * + * @param {string[]} colors Array of colors. + * @returns {boolean} True if the given string is a valid gradient. + */ +const isValidGradient = (colors) => { + return ( + colors.length > 2 && + colors.slice(1).every((color) => isValidHexColor(color)) + ); +}; + +/** + * Retrieves a gradient if color has more than one valid hex codes else a single color. + * + * @param {string} color The color to parse. + * @param {string | string[]} fallbackColor The fallback color. + * @returns {string | string[]} The gradient or color. + */ +const fallbackColor = (color, fallbackColor) => { + let gradient = null; + + let colors = color ? color.split(",") : []; + if (colors.length > 1 && isValidGradient(colors)) { + gradient = colors; + } + + return ( + (gradient ? gradient : isValidHexColor(color) && `#${color}`) || + fallbackColor + ); +}; + +/** + * Object containing card colors. + * @typedef {{ + * titleColor: string; + * iconColor: string; + * textColor: string; + * bgColor: string | string[]; + * borderColor: string; + * ringColor: string; + * }} CardColors + */ + +/** + * Returns theme based colors with proper overrides and defaults. + * + * @param {Object} args Function arguments. + * @param {string=} args.title_color Card title color. + * @param {string=} args.text_color Card text color. + * @param {string=} args.icon_color Card icon color. + * @param {string=} args.bg_color Card background color. + * @param {string=} args.border_color Card border color. + * @param {string=} args.ring_color Card ring color. + * @param {string=} args.theme Card theme. + * @param {string=} args.fallbackTheme Fallback theme. + * @returns {CardColors} Card colors. + */ +const getCardColors = ({ + title_color, + text_color, + icon_color, + bg_color, + border_color, + ring_color, + theme, + fallbackTheme = "default", +}) => { + const defaultTheme = themes[fallbackTheme]; + const isThemeProvided = theme !== null && theme !== undefined; + const selectedTheme = isThemeProvided ? themes[theme] : defaultTheme; + const defaultBorderColor = + "border_color" in selectedTheme + ? selectedTheme.border_color + : // @ts-ignore + defaultTheme.border_color; + + // get the color provided by the user else the theme color + // finally if both colors are invalid fallback to default theme + const titleColor = fallbackColor( + title_color || selectedTheme.title_color, + "#" + defaultTheme.title_color, + ); + + // get the color provided by the user else the theme color + // finally if both colors are invalid we use the titleColor + const ringColor = fallbackColor( + // @ts-ignore + ring_color || selectedTheme.ring_color, + titleColor, + ); + const iconColor = fallbackColor( + icon_color || selectedTheme.icon_color, + "#" + defaultTheme.icon_color, + ); + const textColor = fallbackColor( + text_color || selectedTheme.text_color, + "#" + defaultTheme.text_color, + ); + const bgColor = fallbackColor( + bg_color || selectedTheme.bg_color, + "#" + defaultTheme.bg_color, + ); + + const borderColor = fallbackColor( + border_color || defaultBorderColor, + "#" + defaultBorderColor, + ); + + if ( + typeof titleColor !== "string" || + typeof textColor !== "string" || + typeof ringColor !== "string" || + typeof iconColor !== "string" || + typeof borderColor !== "string" + ) { + throw new Error( + "Unexpected behavior, all colors except background should be string.", + ); + } + + return { titleColor, iconColor, textColor, bgColor, borderColor, ringColor }; +}; + +export { isValidHexColor, isValidGradient, getCardColors }; diff --git a/src/common/error.js b/src/common/error.js index c76a198d75d6a..d3547bab4df45 100644 --- a/src/common/error.js +++ b/src/common/error.js @@ -1,3 +1,5 @@ +// @ts-check + /** * @type {string} A general message to ask user to try again later. */ @@ -61,9 +63,22 @@ class MissingParamError extends Error { } } +/** + * Retrieve secondary message from an error object. + * + * @param {Error} err The error object. + * @returns {string|undefined} The secondary message if available, otherwise undefined. + */ +const retrieveSecondaryMessage = (err) => { + return "secondaryMessage" in err && typeof err.secondaryMessage === "string" + ? err.secondaryMessage + : undefined; +}; + export { CustomError, MissingParamError, SECONDARY_ERROR_MESSAGES, TRY_AGAIN_LATER, + retrieveSecondaryMessage, }; diff --git a/src/common/index.js b/src/common/index.js index aa5ec0828b41a..5dfb0e1209986 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -11,15 +11,11 @@ export { renderError, encodeHTML, kFormatter, - isValidHexColor, parseBoolean, parseArray, clampValue, - isValidGradient, - fallbackColor, request, flexLayout, - getCardColors, wrapTextMultiline, logger, measureText, diff --git a/src/common/utils.js b/src/common/utils.js index c9ceb24a568d6..054788039b384 100644 --- a/src/common/utils.js +++ b/src/common/utils.js @@ -3,8 +3,8 @@ import axios from "axios"; import toEmoji from "emoji-name-map"; import wrap from "word-wrap"; -import { themes } from "../../themes/index.js"; import { SECONDARY_ERROR_MESSAGES, TRY_AGAIN_LATER } from "./error.js"; +import { getCardColors } from "./color.js"; /** * Auto layout utility, allows us to layout things vertically or horizontally with @@ -88,18 +88,6 @@ const kFormatter = (num) => { : Math.sign(num) * Math.abs(num); }; -/** - * Checks if a string is a valid hex color. - * - * @param {string} hexColor String to check. - * @returns {boolean} True if the given string is a valid hex color. - */ -const isValidHexColor = (hexColor) => { - return new RegExp( - /^([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{4})$/, - ).test(hexColor); -}; - /** * Returns boolean if value is either "true" or "false" else the value as it is. * @@ -150,40 +138,6 @@ const clampValue = (number, min, max) => { return Math.max(min, Math.min(number, max)); }; -/** - * Check if the given string is a valid gradient. - * - * @param {string[]} colors Array of colors. - * @returns {boolean} True if the given string is a valid gradient. - */ -const isValidGradient = (colors) => { - return ( - colors.length > 2 && - colors.slice(1).every((color) => isValidHexColor(color)) - ); -}; - -/** - * Retrieves a gradient if color has more than one valid hex codes else a single color. - * - * @param {string} color The color to parse. - * @param {string | string[]} fallbackColor The fallback color. - * @returns {string | string[]} The gradient or color. - */ -const fallbackColor = (color, fallbackColor) => { - let gradient = null; - - let colors = color ? color.split(",") : []; - if (colors.length > 1 && isValidGradient(colors)) { - gradient = colors; - } - - return ( - (gradient ? gradient : isValidHexColor(color) && `#${color}`) || - fallbackColor - ); -}; - /** * @typedef {import('axios').AxiosRequestConfig['data']} AxiosRequestConfigData Axios request data. * @typedef {import('axios').AxiosRequestConfig['headers']} AxiosRequestConfigHeaders Axios request headers. @@ -205,93 +159,6 @@ const request = (data, headers) => { }); }; -/** - * Object containing card colors. - * @typedef {{ - * titleColor: string; - * iconColor: string; - * textColor: string; - * bgColor: string | string[]; - * borderColor: string; - * ringColor: string; - * }} CardColors - */ - -/** - * Returns theme based colors with proper overrides and defaults. - * - * @param {Object} args Function arguments. - * @param {string=} args.title_color Card title color. - * @param {string=} args.text_color Card text color. - * @param {string=} args.icon_color Card icon color. - * @param {string=} args.bg_color Card background color. - * @param {string=} args.border_color Card border color. - * @param {string=} args.ring_color Card ring color. - * @param {string=} args.theme Card theme. - * @param {string=} args.fallbackTheme Fallback theme. - * @returns {CardColors} Card colors. - */ -const getCardColors = ({ - title_color, - text_color, - icon_color, - bg_color, - border_color, - ring_color, - theme, - fallbackTheme = "default", -}) => { - const defaultTheme = themes[fallbackTheme]; - const selectedTheme = themes[theme] || defaultTheme; - const defaultBorderColor = - selectedTheme.border_color || defaultTheme.border_color; - - // get the color provided by the user else the theme color - // finally if both colors are invalid fallback to default theme - const titleColor = fallbackColor( - title_color || selectedTheme.title_color, - "#" + defaultTheme.title_color, - ); - - // get the color provided by the user else the theme color - // finally if both colors are invalid we use the titleColor - const ringColor = fallbackColor( - ring_color || selectedTheme.ring_color, - titleColor, - ); - const iconColor = fallbackColor( - icon_color || selectedTheme.icon_color, - "#" + defaultTheme.icon_color, - ); - const textColor = fallbackColor( - text_color || selectedTheme.text_color, - "#" + defaultTheme.text_color, - ); - const bgColor = fallbackColor( - bg_color || selectedTheme.bg_color, - "#" + defaultTheme.bg_color, - ); - - const borderColor = fallbackColor( - border_color || defaultBorderColor, - "#" + defaultBorderColor, - ); - - if ( - typeof titleColor !== "string" || - typeof textColor !== "string" || - typeof ringColor !== "string" || - typeof iconColor !== "string" || - typeof borderColor !== "string" - ) { - throw new Error( - "Unexpected behavior, all colors except background should be string.", - ); - } - - return { titleColor, iconColor, textColor, bgColor, borderColor, ringColor }; -}; - // Script parameters. const ERROR_CARD_LENGTH = 576.5; @@ -327,7 +194,7 @@ const UPSTREAM_API_ERRORS = [ * @param {string=} args.renderOptions.text_color Card text color. * @param {string=} args.renderOptions.bg_color Card background color. * @param {string=} args.renderOptions.border_color Card border color. - * @param {string=} args.renderOptions.theme Card theme. + * @param {Parameters[0]["theme"]=} args.renderOptions.theme Card theme. * @param {boolean=} args.renderOptions.show_repo_link Whether to show repo link or not. * @returns {string} The SVG markup. */ @@ -556,15 +423,11 @@ export { iconWithLabel, encodeHTML, kFormatter, - isValidHexColor, parseBoolean, parseArray, clampValue, - isValidGradient, - fallbackColor, request, flexLayout, - getCardColors, wrapTextMultiline, logger, measureText, diff --git a/tests/card.test.js b/tests/card.test.js index 02e476c3721db..21303603652c9 100644 --- a/tests/card.test.js +++ b/tests/card.test.js @@ -4,7 +4,7 @@ import "@testing-library/jest-dom"; import { cssToObject } from "@uppercod/css-to-object"; import { Card } from "../src/common/Card.js"; import { icons } from "../src/common/icons.js"; -import { getCardColors } from "../src/common/utils.js"; +import { getCardColors } from "../src/common/color.js"; describe("Card", () => { it("should hide border", () => { diff --git a/tests/utils.test.js b/tests/utils.test.js index 21a78e4730b32..f7ce665cc7624 100644 --- a/tests/utils.test.js +++ b/tests/utils.test.js @@ -4,12 +4,12 @@ import "@testing-library/jest-dom"; import { encodeHTML, formatBytes, - getCardColors, kFormatter, parseBoolean, renderError, wrapTextMultiline, } from "../src/common/utils.js"; +import { getCardColors } from "../src/common/color.js"; describe("Test utils.js", () => { it("should test kFormatter", () => {