From ad57cbd3848979a3291337a546b2fa5a275eeb88 Mon Sep 17 00:00:00 2001 From: Alexandr Date: Fri, 11 Aug 2023 07:34:51 +0300 Subject: [PATCH 1/9] Feature: Add gist card --- api/gist.js | 80 ++++++++++++ package-lock.json | 242 ++++++++++++++++++++++++++++++++++- package.json | 1 + src/cards/gist-card.js | 171 +++++++++++++++++++++++++ src/cards/types.d.ts | 5 + src/fetchers/gist-fetcher.js | 81 ++++++++++++ src/fetchers/types.d.ts | 8 ++ 7 files changed, 581 insertions(+), 7 deletions(-) create mode 100644 api/gist.js create mode 100644 src/cards/gist-card.js create mode 100644 src/fetchers/gist-fetcher.js diff --git a/api/gist.js b/api/gist.js new file mode 100644 index 0000000000000..a1a9c35dc9e44 --- /dev/null +++ b/api/gist.js @@ -0,0 +1,80 @@ +import { blacklist } from "../src/common/blacklist.js"; +import { clampValue, CONSTANTS, renderError } from "../src/common/utils.js"; +import { isLocaleAvailable } from "../src/translations.js"; +import { renderGistCard } from "../src/cards/gist-card.js"; +import { fetchGist } from "../src/fetchers/gist-fetcher.js"; + +export default async (req, res) => { + const { + username, + id, + title_color, + icon_color, + text_color, + bg_color, + theme, + cache_seconds, + locale, + border_radius, + border_color, + } = req.query; + + res.setHeader("Content-Type", "image/svg+xml"); + + if (blacklist.includes(username)) { + return res.send(renderError("Something went wrong")); + } + + if (locale && !isLocaleAvailable(locale)) { + return res.send(renderError("Something went wrong", "Language not found")); + } + + try { + const gistData = await fetchGist(username, id); + + let cacheSeconds = clampValue( + parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10), + CONSTANTS.FOUR_HOURS, + CONSTANTS.ONE_DAY, + ); + cacheSeconds = process.env.CACHE_SECONDS + ? parseInt(process.env.CACHE_SECONDS, 10) || cacheSeconds + : cacheSeconds; + + /* + if star count & fork count is over 1k then we are kFormating the text + and if both are zero we are not showing the stats + so we can just make the cache longer, since there is no need to frequent updates + */ + const stars = gistData.starsCount; + const forks = gistData.forksCount; + const isBothOver1K = stars > 1000 && forks > 1000; + const isBothUnder1 = stars < 1 && forks < 1; + if (!cache_seconds && (isBothOver1K || isBothUnder1)) { + cacheSeconds = CONSTANTS.FOUR_HOURS; + } + + res.setHeader( + "Cache-Control", + `max-age=${ + cacheSeconds / 2 + }, s-maxage=${cacheSeconds}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, + ); + + return res.send( + renderGistCard(gistData, { + title_color, + icon_color, + text_color, + bg_color, + theme, + border_radius, + border_color, + locale: locale ? locale.toLowerCase() : null, + }), + ); + } catch (err) { + res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses. + return res.send(renderError(err.message, err.secondaryMessage)); + } +}; diff --git a/package-lock.json b/package-lock.json index 0cbd6bf567e14..37b71a8da4011 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "dotenv": "^16.3.1", "emoji-name-map": "^1.2.8", "github-username-regex": "^1.0.0", + "linkedom": "^0.15.1", "upgrade": "^1.1.0", "word-wrap": "^1.2.5" }, @@ -1930,6 +1931,11 @@ "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==", "dev": true }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2289,6 +2295,32 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -2298,8 +2330,7 @@ "node_modules/cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "dev": true + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==" }, "node_modules/cssstyle": { "version": "2.3.0", @@ -2480,6 +2511,30 @@ "integrity": "sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==", "dev": true }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, "node_modules/domexception": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", @@ -2492,6 +2547,33 @@ "node": ">=12" } }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "16.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", @@ -2552,7 +2634,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", - "dev": true, "engines": { "node": ">=0.12" }, @@ -3380,6 +3461,24 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -5019,6 +5118,23 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/linkedom": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.15.1.tgz", + "integrity": "sha512-8U+wi0VI0JW7ehxX+N/rmZyLWV9bypiCOUeklGBWslpiTpxHnVfY+yHmf6sg06q2kXYdpIJWC6LMpepgOq+JXg==", + "dependencies": { + "css-select": "^5.1.0", + "cssom": "^0.5.0", + "html-escaper": "^3.0.3", + "htmlparser2": "^8.0.1", + "uhyphen": "^0.2.0" + } + }, + "node_modules/linkedom/node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==" + }, "node_modules/lint-staged": { "version": "13.2.3", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.2.3.tgz", @@ -5545,6 +5661,17 @@ "node": ">=8" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nwsapi": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", @@ -6567,6 +6694,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/uhyphen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", + "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==" + }, "node_modules/universal-user-agent": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", @@ -8453,6 +8585,11 @@ "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==", "dev": true }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -8701,6 +8838,23 @@ "which": "^2.0.1" } }, + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==" + }, "css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -8710,8 +8864,7 @@ "cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "dev": true + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==" }, "cssstyle": { "version": "2.3.0", @@ -8849,6 +9002,21 @@ "integrity": "sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==", "dev": true }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + }, "domexception": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", @@ -8858,6 +9026,24 @@ "webidl-conversions": "^7.0.0" } }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, "dotenv": { "version": "16.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", @@ -8905,8 +9091,7 @@ "entities": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", - "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", - "dev": true + "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==" }, "error-ex": { "version": "1.3.2", @@ -9493,6 +9678,17 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -10694,6 +10890,25 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "linkedom": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.15.1.tgz", + "integrity": "sha512-8U+wi0VI0JW7ehxX+N/rmZyLWV9bypiCOUeklGBWslpiTpxHnVfY+yHmf6sg06q2kXYdpIJWC6LMpepgOq+JXg==", + "requires": { + "css-select": "^5.1.0", + "cssom": "^0.5.0", + "html-escaper": "^3.0.3", + "htmlparser2": "^8.0.1", + "uhyphen": "^0.2.0" + }, + "dependencies": { + "html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==" + } + } + }, "lint-staged": { "version": "13.2.3", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.2.3.tgz", @@ -11079,6 +11294,14 @@ "path-key": "^3.0.0" } }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "requires": { + "boolbase": "^1.0.0" + } + }, "nwsapi": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", @@ -11801,6 +12024,11 @@ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true }, + "uhyphen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", + "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==" + }, "universal-user-agent": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", diff --git a/package.json b/package.json index 5c5bf2ce3bcd4..943eb280ce9d7 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "dotenv": "^16.3.1", "emoji-name-map": "^1.2.8", "github-username-regex": "^1.0.0", + "linkedom": "^0.15.1", "upgrade": "^1.1.0", "word-wrap": "^1.2.5" }, diff --git a/src/cards/gist-card.js b/src/cards/gist-card.js new file mode 100644 index 0000000000000..c7513c6236ddd --- /dev/null +++ b/src/cards/gist-card.js @@ -0,0 +1,171 @@ +// @ts-check + +import { + getCardColors, + parseEmojis, + wrapTextMultiline, + encodeHTML, + kFormatter, + measureText, + flexLayout, +} from "../common/utils.js"; +import Card from "../common/Card.js"; +import { icons } from "../common/icons.js"; + +/** Import language colors. + * + * @description Here we use the workaround found in + * https://stackoverflow.com/questions/66726365/how-should-i-import-json-in-node + * since vercel is using v16.14.0 which does not yet support json imports without the + * --experimental-json-modules flag. + */ +import { createRequire } from "module"; +const require = createRequire(import.meta.url); +const languageColors = require("../common/languageColors.json"); // now works + +const ICON_SIZE = 16; +const CARD_DEFAULT_WIDTH = 400; + +/** + * Creates a node to display the primary programming language of the gist. + * + * @param {string} langName Language name. + * @param {string} langColor Language color. + * @returns {string} Language display SVG object. + */ +const createLanguageNode = (langName, langColor) => { + return ` + + + ${langName} + + `; +}; + +/** + * Creates an icon with label to display gist stats like forks, stars, etc. + * + * @param {string} icon The icon to display. + * @param {number|string} label The label to display. + * @param {string} testid The testid to assign to the label. + * @returns {string} Icon with label SVG object. + */ +const iconWithLabel = (icon, label, testid) => { + if (typeof label === "number" && label <= 0) return ""; + const iconSvg = ` + + ${icon} + + `; + const text = `${label}`; + return flexLayout({ items: [iconSvg, text], gap: 20 }).join(""); +}; + +/** + * @typedef {import('./types').GistCardOptions} GistCardOptions Gist card options. + * @typedef {import('../fetchers/types').GistData} GistData Gist data. + */ + +/** + * Render gist card. + * + * @param {GistData} gistData Gist data. + * @param {GistCardOptions} options Gist card options. + * @returns {string} Gist card. + */ +const renderGistCard = (gistData, options) => { + const { name, description, language, starsCount, forksCount } = gistData; + const { + title_color, + icon_color, + text_color, + bg_color, + theme, + border_radius, + border_color, + } = options; + + // returns theme based colors with proper overrides and defaults + const { titleColor, textColor, iconColor, bgColor, borderColor } = + getCardColors({ + title_color, + icon_color, + text_color, + bg_color, + border_color, + theme, + }); + + const desc = parseEmojis(description || "No description provided"); + const multiLineDescription = wrapTextMultiline(desc); + const descriptionLines = multiLineDescription.length; + const descriptionSvg = multiLineDescription + .map((line) => `${encodeHTML(line)}`) + .join(""); + + const lineHeight = 10; + const height = + (descriptionLines > 1 ? 120 : 110) + descriptionLines * lineHeight; + + const totalStars = kFormatter(starsCount); + const totalForks = kFormatter(forksCount); + const svgStars = iconWithLabel(icons.star, totalStars, "starsCount"); + const svgForks = iconWithLabel(icons.fork, totalForks, "forksCount"); + + const languageName = language || "Unspecified"; + const languageColor = languageColors[languageName] || "#858585"; + + const svgLanguage = createLanguageNode(languageName, languageColor); + + const starAndForkCount = flexLayout({ + items: [svgLanguage, svgStars, svgForks], + sizes: [ + measureText(languageName, 12), + ICON_SIZE + measureText(`${totalStars}`, 12), + ICON_SIZE + measureText(`${totalForks}`, 12), + ], + gap: 25, + }).join(""); + + const card = new Card({ + defaultTitle: name.length > 35 ? `${name.slice(0, 35)}...` : name, + width: CARD_DEFAULT_WIDTH, + height, + border_radius, + colors: { + titleColor, + textColor, + iconColor, + bgColor, + borderColor, + }, + }); + + card.setCSS(` + .description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} } + .gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} } + .icon { fill: ${iconColor} } + .badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; } + .badge rect { opacity: 0.2 } + `); + + return card.render(` + + ${descriptionSvg} + + + + ${starAndForkCount} + + `); +}; + +export { renderGistCard }; +export default renderGistCard; diff --git a/src/cards/types.d.ts b/src/cards/types.d.ts index a3f0b2b7e0cfb..447eb1a33088f 100644 --- a/src/cards/types.d.ts +++ b/src/cards/types.d.ts @@ -57,3 +57,8 @@ type WakaTimeOptions = CommonOptions & { layout: "compact" | "normal"; langs_count: number; }; + +export type GistCardOptions = CommonOptions & { + username: string; + id: string; +}; diff --git a/src/fetchers/gist-fetcher.js b/src/fetchers/gist-fetcher.js new file mode 100644 index 0000000000000..1c41e2c6b3548 --- /dev/null +++ b/src/fetchers/gist-fetcher.js @@ -0,0 +1,81 @@ +// @ts-check + +import axios from "axios"; +import { parseHTML } from "linkedom"; + +/** + * @typedef {import('./types').GistData} GistData Gist data. + */ + +/** + * Fetch GitHub gist information by given username and ID. + * + * @param {string} username Github username. + * @param {string} id Github gist ID. + * @returns {Promise} Gist data. + */ +const fetchGist = async (username, id) => { + const response = ( + await axios({ + method: "get", + url: `https://api.github.com/gists/${id}`, + headers: { + "Content-Type": "application/json", + Accept: "application/vnd.github.cloak-preview", + }, + }) + ).data; + const { starsCount, forksCount } = await fetchGistStargazers(username, id); + + return { + name: response.files[Object.keys(response.files)[0]].filename, + description: response.description, + language: response.files[Object.keys(response.files)[0]].language, + starsCount, + forksCount, + }; +}; + +/** + * Fetch GitHub gist stargazers and forks count by given username and ID. + * + * @param {string} username Github username. + * @param {string} id Github gist ID. + * @returns {Promise<{starsCount: number, forksCount: number}>} Gist stargazers and forks count. + */ +const fetchGistStargazers = async (username, id) => { + let starsCount = 0; + let forksCount = 0; + + try { + await axios({ + method: "get", + url: `https://gist.github.com/${username}/${id}/stargazers`, + headers: { + "Content-Type": "application/json", + Accept: "application/vnd.github.cloak-preview", + }, + }).then((dom) => { + const { document } = parseHTML(dom.data); + let nav = document.querySelector('[aria-label="Gist"]'); + if (!nav) throw new Error("No nav found"); + let starsBox = nav.querySelector('[data-hotkey="g s"] span.Counter'); + let forksBox = nav.querySelector('[data-hotkey="g f"] span.Counter'); + // @ts-ignore + starsCount = starsBox ? starsBox.title : 0; + // @ts-ignore + forksCount = forksBox ? forksBox.title : 0; + }); + } catch (error) { + starsCount = 0; + forksCount = 0; + } + + return { + starsCount, + forksCount, + }; +}; + +export { fetchGist }; +export default fetchGist; diff --git a/src/fetchers/types.d.ts b/src/fetchers/types.d.ts index 3a86205834c12..f281e800854fb 100644 --- a/src/fetchers/types.d.ts +++ b/src/fetchers/types.d.ts @@ -1,3 +1,11 @@ +export type GistData = { + name: string; + description: string; + language: string | null; + starsCount: number; + forksCount: number; +}; + export type RepositoryData = { name: string; nameWithOwner: string; From 3bdc691bdf85c68714586c4a0eb5dca8ac9d72d9 Mon Sep 17 00:00:00 2001 From: Alexandr Date: Fri, 11 Aug 2023 19:51:26 +0300 Subject: [PATCH 2/9] dev --- src/cards/gist-card.js | 1 + src/common/icons.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/cards/gist-card.js b/src/cards/gist-card.js index c7513c6236ddd..954c02b3318c0 100644 --- a/src/cards/gist-card.js +++ b/src/cards/gist-card.js @@ -136,6 +136,7 @@ const renderGistCard = (gistData, options) => { const card = new Card({ defaultTitle: name.length > 35 ? `${name.slice(0, 35)}...` : name, + titlePrefixIcon: icons.gist, width: CARD_DEFAULT_WIDTH, height, border_radius, diff --git a/src/common/icons.js b/src/common/icons.js index 3f91d86658c93..9ffc525eaf7e1 100644 --- a/src/common/icons.js +++ b/src/common/icons.js @@ -9,6 +9,7 @@ const icons = { reviews: ``, discussions_started: ``, discussions_answered: ``, + gist: ``, }; /** From 5f461b4b3535a96e533041afd9340b1cf2065d15 Mon Sep 17 00:00:00 2001 From: Alexandr Date: Fri, 11 Aug 2023 20:23:52 +0300 Subject: [PATCH 3/9] dev --- readme.md | 35 ++++++++++++++++++++++++++++++++--- src/cards/gist-card.js | 6 ++++-- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/readme.md b/readme.md index 2760b191c38c3..9d1ae73bc56e7 100644 --- a/readme.md +++ b/readme.md @@ -97,8 +97,11 @@ Please visit [this link](https://give.do/fundraisers/stand-beside-the-victims-of - [GitHub Extra Pins](#github-extra-pins) - [Usage](#usage) - [Demo](#demo) -- [Top Languages Card](#top-languages-card) +- [GitHub Gist Pins](#github-gist-pins) - [Usage](#usage-1) + - [Demo](#demo-1) +- [Top Languages Card](#top-languages-card) + - [Usage](#usage-2) - [Language stats algorithm](#language-stats-algorithm) - [Exclude individual repositories](#exclude-individual-repositories) - [Hide individual languages](#hide-individual-languages) @@ -108,9 +111,9 @@ Please visit [this link](https://give.do/fundraisers/stand-beside-the-victims-of - [Donut Vertical Chart Language Card Layout](#donut-vertical-chart-language-card-layout) - [Pie Chart Language Card Layout](#pie-chart-language-card-layout) - [Hide Progress Bars](#hide-progress-bars) - - [Demo](#demo-1) -- [Wakatime Stats Card](#wakatime-stats-card) - [Demo](#demo-2) +- [Wakatime Stats Card](#wakatime-stats-card) + - [Demo](#demo-3) - [All Demos](#all-demos) - [Quick Tip (Align The Cards)](#quick-tip-align-the-cards) - [Deploy on your own](#deploy-on-your-own) @@ -384,6 +387,24 @@ Use [show\_owner](#repo-card-exclusive-options) query option to include the repo ![Readme Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra\&repo=github-readme-stats\&show_owner=true) +# GitHub Gist Pins + +GitHub gist pins allow you to pin gists in your GitHub profile using a GitHub readme profile. + +### Usage + +Copy-paste this code into your readme and change the links. + +Endpoint: `api/gist?username=Yizack&id=bbfce31e0217a3689c8d961a356cb10d` + +```md +[![Gist Card](https://github-readme-stats.vercel.app/api/gist?username=Yizack&id=bbfce31e0217a3689c8d961a356cb10d)](https://gist.github.com/Yizack/bbfce31e0217a3689c8d961a356cb10d/) +``` + +### Demo + +![Gist Card](https://github-readme-stats.vercel.app/api/gist?username=Yizack\&id=bbfce31e0217a3689c8d961a356cb10d) + # Top Languages Card The top languages card shows a GitHub user's most frequently used languages. @@ -592,6 +613,14 @@ Choose from any of the [default themes](#themes) ![Customized Card](https://github-readme-stats.vercel.app/api/pin?username=anuraghazra\&repo=github-readme-stats\&title_color=fff\&icon_color=f9f9f9\&text_color=9f9f9f\&bg_color=151515) +* Gist card + +![Gist Card](https://github-readme-stats.vercel.app/api/gist?username=Yizack&id=bbfce31e0217a3689c8d961a356cb10d) + +* Customizing gist card + +![Gist Card](https://github-readme-stats.vercel.app/api/gist?username=Yizack&id=bbfce31e0217a3689c8d961a356cb10d&theme=calm) + * Top languages ![Top Langs](https://github-readme-stats.vercel.app/api/top-langs/?username=anuraghazra) diff --git a/src/cards/gist-card.js b/src/cards/gist-card.js index 954c02b3318c0..7582a24f619d3 100644 --- a/src/cards/gist-card.js +++ b/src/cards/gist-card.js @@ -103,14 +103,16 @@ const renderGistCard = (gistData, options) => { theme, }); + const lineWidth = 59; + const linesLimit = 10; const desc = parseEmojis(description || "No description provided"); - const multiLineDescription = wrapTextMultiline(desc); + const multiLineDescription = wrapTextMultiline(desc, lineWidth, linesLimit); const descriptionLines = multiLineDescription.length; const descriptionSvg = multiLineDescription .map((line) => `${encodeHTML(line)}`) .join(""); - const lineHeight = 10; + const lineHeight = descriptionLines > 3 ? 12 : 10; const height = (descriptionLines > 1 ? 120 : 110) + descriptionLines * lineHeight; From 5b3d460c6b1676c2b7e2a1c8aec5e67f15f9c764 Mon Sep 17 00:00:00 2001 From: Alexandr Date: Fri, 11 Aug 2023 20:26:01 +0300 Subject: [PATCH 4/9] dev --- readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readme.md b/readme.md index 9d1ae73bc56e7..fb8e521dc46da 100644 --- a/readme.md +++ b/readme.md @@ -331,6 +331,8 @@ You can provide multiple comma-separated values in the bg\_color option to rende * `show_owner` - Shows the repo's owner name *(boolean)*. Default: `false`. +#### Gist Card Exclusive Options + #### Language Card Exclusive Options * `hide` - Hides the languages specified from the card *(Comma-separated values)*. Default: `[] (blank array)`. From 4770b1c57ee30831e8735dcc89cf9e06a9b9d451 Mon Sep 17 00:00:00 2001 From: Alexandr Date: Fri, 11 Aug 2023 21:07:46 +0300 Subject: [PATCH 5/9] dev --- api/gist.js | 9 +++- readme.md | 2 + src/cards/gist-card.js | 20 +++++---- src/cards/types.d.ts | 3 +- src/fetchers/gist-fetcher.js | 3 ++ src/fetchers/types.d.ts | 1 + tests/renderGistCard.test.js | 79 ++++++++++++++++++++++++++++++++++++ 7 files changed, 107 insertions(+), 10 deletions(-) create mode 100644 tests/renderGistCard.test.js diff --git a/api/gist.js b/api/gist.js index a1a9c35dc9e44..1b3140c4150e0 100644 --- a/api/gist.js +++ b/api/gist.js @@ -1,5 +1,10 @@ import { blacklist } from "../src/common/blacklist.js"; -import { clampValue, CONSTANTS, renderError } from "../src/common/utils.js"; +import { + clampValue, + CONSTANTS, + renderError, + parseBoolean, +} from "../src/common/utils.js"; import { isLocaleAvailable } from "../src/translations.js"; import { renderGistCard } from "../src/cards/gist-card.js"; import { fetchGist } from "../src/fetchers/gist-fetcher.js"; @@ -17,6 +22,7 @@ export default async (req, res) => { locale, border_radius, border_color, + show_owner, } = req.query; res.setHeader("Content-Type", "image/svg+xml"); @@ -71,6 +77,7 @@ export default async (req, res) => { border_radius, border_color, locale: locale ? locale.toLowerCase() : null, + show_owner: parseBoolean(show_owner), }), ); } catch (err) { diff --git a/readme.md b/readme.md index fb8e521dc46da..412bae9d9bb87 100644 --- a/readme.md +++ b/readme.md @@ -333,6 +333,8 @@ You can provide multiple comma-separated values in the bg\_color option to rende #### Gist Card Exclusive Options +* `show_owner` - Shows the gist's owner name *(boolean)*. Default: `false`. + #### Language Card Exclusive Options * `hide` - Hides the languages specified from the card *(Comma-separated values)*. Default: `[] (blank array)`. diff --git a/src/cards/gist-card.js b/src/cards/gist-card.js index 7582a24f619d3..895616ca0d17b 100644 --- a/src/cards/gist-card.js +++ b/src/cards/gist-card.js @@ -25,6 +25,7 @@ const languageColors = require("../common/languageColors.json"); // now works const ICON_SIZE = 16; const CARD_DEFAULT_WIDTH = 400; +const HEADER_MAX_LENGTH = 35; /** * Creates a node to display the primary programming language of the gist. @@ -77,11 +78,12 @@ const iconWithLabel = (icon, label, testid) => { * Render gist card. * * @param {GistData} gistData Gist data. - * @param {GistCardOptions} options Gist card options. + * @param {Partial} options Gist card options. * @returns {string} Gist card. */ -const renderGistCard = (gistData, options) => { - const { name, description, language, starsCount, forksCount } = gistData; +const renderGistCard = (gistData, options = {}) => { + const { name, nameWithOwner, description, language, starsCount, forksCount } = + gistData; const { title_color, icon_color, @@ -90,6 +92,7 @@ const renderGistCard = (gistData, options) => { theme, border_radius, border_color, + show_owner = false, } = options; // returns theme based colors with proper overrides and defaults @@ -136,8 +139,13 @@ const renderGistCard = (gistData, options) => { gap: 25, }).join(""); + const header = show_owner ? nameWithOwner : name; + const card = new Card({ - defaultTitle: name.length > 35 ? `${name.slice(0, 35)}...` : name, + defaultTitle: + header.length > HEADER_MAX_LENGTH + ? `${header.slice(0, HEADER_MAX_LENGTH)}...` + : header, titlePrefixIcon: icons.gist, width: CARD_DEFAULT_WIDTH, height, @@ -155,8 +163,6 @@ const renderGistCard = (gistData, options) => { .description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} } .gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} } .icon { fill: ${iconColor} } - .badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; } - .badge rect { opacity: 0.2 } `); return card.render(` @@ -170,5 +176,5 @@ const renderGistCard = (gistData, options) => { `); }; -export { renderGistCard }; +export { renderGistCard, HEADER_MAX_LENGTH }; export default renderGistCard; diff --git a/src/cards/types.d.ts b/src/cards/types.d.ts index 447eb1a33088f..3274d8c291cfe 100644 --- a/src/cards/types.d.ts +++ b/src/cards/types.d.ts @@ -59,6 +59,5 @@ type WakaTimeOptions = CommonOptions & { }; export type GistCardOptions = CommonOptions & { - username: string; - id: string; + show_owner: boolean; }; diff --git a/src/fetchers/gist-fetcher.js b/src/fetchers/gist-fetcher.js index 1c41e2c6b3548..e98604011e4cf 100644 --- a/src/fetchers/gist-fetcher.js +++ b/src/fetchers/gist-fetcher.js @@ -29,6 +29,9 @@ const fetchGist = async (username, id) => { return { name: response.files[Object.keys(response.files)[0]].filename, + nameWithOwner: `${username}/${ + response.files[Object.keys(response.files)[0]].filename + }`, description: response.description, language: response.files[Object.keys(response.files)[0]].language, starsCount, diff --git a/src/fetchers/types.d.ts b/src/fetchers/types.d.ts index f281e800854fb..613fd570387e5 100644 --- a/src/fetchers/types.d.ts +++ b/src/fetchers/types.d.ts @@ -1,5 +1,6 @@ export type GistData = { name: string; + nameWithOwner: string; description: string; language: string | null; starsCount: number; diff --git a/tests/renderGistCard.test.js b/tests/renderGistCard.test.js new file mode 100644 index 0000000000000..2af6889145b25 --- /dev/null +++ b/tests/renderGistCard.test.js @@ -0,0 +1,79 @@ +import { renderGistCard } from "../src/cards/gist-card"; +import { describe, expect, it } from "@jest/globals"; +import { queryByTestId } from "@testing-library/dom"; +import "@testing-library/jest-dom"; + +/** + * @type {import("../src/fetchers/gist-fetcher").GistData} + */ +const data = { + name: "test", + nameWithOwner: "anuraghazra/test", + description: "Small test repository with different Python programs.", + language: "Python", + starsCount: 163, + forksCount: 19, +}; + +describe("test renderGistCard", () => { + it("should render correctly", () => { + document.body.innerHTML = renderGistCard(data); + + const [header] = document.getElementsByClassName("header"); + + expect(header).toHaveTextContent("test"); + expect(header).not.toHaveTextContent("anuraghazra"); + expect(document.getElementsByClassName("description")[0]).toHaveTextContent( + "Small test repository with different Python programs.", + ); + expect(queryByTestId(document.body, "starsCount")).toHaveTextContent("163"); + expect(queryByTestId(document.body, "forksCount")).toHaveTextContent("19"); + expect(queryByTestId(document.body, "lang-name")).toHaveTextContent( + "Python", + ); + expect(queryByTestId(document.body, "lang-color")).toHaveAttribute( + "fill", + "#3572A5", + ); + }); + + it("should display username in title if show_owner is true", () => { + document.body.innerHTML = renderGistCard(data, { show_owner: true }); + const [header] = document.getElementsByClassName("header"); + expect(header).toHaveTextContent("anuraghazra/test"); + }); + + it("should trim header if name is too long", () => { + document.body.innerHTML = renderGistCard({ + ...data, + name: "some-really-long-repo-name-for-test-purposes", + }); + const [header] = document.getElementsByClassName("header"); + expect(header).toHaveTextContent("some-really-long-repo-name-for-test..."); + }); + + it("should trim description if description os too long", () => { + document.body.innerHTML = renderGistCard({ + ...data, + description: + "The quick brown fox jumps over the lazy dog is an English-language pangram—a sentence that contains all of the letters of the English alphabet", + }); + expect( + document.getElementsByClassName("description")[0].children[0].textContent, + ).toBe("The quick brown fox jumps over the lazy dog is an"); + + expect( + document.getElementsByClassName("description")[0].children[1].textContent, + ).toBe("English-language pangram—a sentence that contains all"); + }); + + it("should not trim description if it is short", () => { + document.body.innerHTML = renderGistCard({ + ...data, + description: "Small text should not trim", + }); + expect(document.getElementsByClassName("description")[0]).toHaveTextContent( + "Small text should not trim", + ); + }); +}); From c6061521fcabb434c726021423d85381f5653ff6 Mon Sep 17 00:00:00 2001 From: Alexandr Date: Sun, 13 Aug 2023 10:28:30 +0300 Subject: [PATCH 6/9] dev --- api/gist.js | 8 +- package-lock.json | 242 +---------------------------------- package.json | 1 - readme.md | 10 +- src/fetchers/gist-fetcher.js | 138 +++++++++++--------- 5 files changed, 93 insertions(+), 306 deletions(-) diff --git a/api/gist.js b/api/gist.js index 1b3140c4150e0..4ef153d024ee2 100644 --- a/api/gist.js +++ b/api/gist.js @@ -1,4 +1,3 @@ -import { blacklist } from "../src/common/blacklist.js"; import { clampValue, CONSTANTS, @@ -11,7 +10,6 @@ import { fetchGist } from "../src/fetchers/gist-fetcher.js"; export default async (req, res) => { const { - username, id, title_color, icon_color, @@ -27,16 +25,12 @@ export default async (req, res) => { res.setHeader("Content-Type", "image/svg+xml"); - if (blacklist.includes(username)) { - return res.send(renderError("Something went wrong")); - } - if (locale && !isLocaleAvailable(locale)) { return res.send(renderError("Something went wrong", "Language not found")); } try { - const gistData = await fetchGist(username, id); + const gistData = await fetchGist(id); let cacheSeconds = clampValue( parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10), diff --git a/package-lock.json b/package-lock.json index 37b71a8da4011..0cbd6bf567e14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "dotenv": "^16.3.1", "emoji-name-map": "^1.2.8", "github-username-regex": "^1.0.0", - "linkedom": "^0.15.1", "upgrade": "^1.1.0", "word-wrap": "^1.2.5" }, @@ -1931,11 +1930,6 @@ "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==", "dev": true }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2295,32 +2289,6 @@ "node": ">= 8" } }, - "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -2330,7 +2298,8 @@ "node_modules/cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==" + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true }, "node_modules/cssstyle": { "version": "2.3.0", @@ -2511,30 +2480,6 @@ "integrity": "sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==", "dev": true }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, "node_modules/domexception": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", @@ -2547,33 +2492,6 @@ "node": ">=12" } }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, "node_modules/dotenv": { "version": "16.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", @@ -2634,6 +2552,7 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "dev": true, "engines": { "node": ">=0.12" }, @@ -3461,24 +3380,6 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -5118,23 +5019,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, - "node_modules/linkedom": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.15.1.tgz", - "integrity": "sha512-8U+wi0VI0JW7ehxX+N/rmZyLWV9bypiCOUeklGBWslpiTpxHnVfY+yHmf6sg06q2kXYdpIJWC6LMpepgOq+JXg==", - "dependencies": { - "css-select": "^5.1.0", - "cssom": "^0.5.0", - "html-escaper": "^3.0.3", - "htmlparser2": "^8.0.1", - "uhyphen": "^0.2.0" - } - }, - "node_modules/linkedom/node_modules/html-escaper": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", - "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==" - }, "node_modules/lint-staged": { "version": "13.2.3", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.2.3.tgz", @@ -5661,17 +5545,6 @@ "node": ">=8" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, "node_modules/nwsapi": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", @@ -6694,11 +6567,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/uhyphen": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", - "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==" - }, "node_modules/universal-user-agent": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", @@ -8585,11 +8453,6 @@ "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==", "dev": true }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" - }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -8838,23 +8701,6 @@ "which": "^2.0.1" } }, - "css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "requires": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - } - }, - "css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==" - }, "css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -8864,7 +8710,8 @@ "cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==" + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true }, "cssstyle": { "version": "2.3.0", @@ -9002,21 +8849,6 @@ "integrity": "sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==", "dev": true }, - "dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "requires": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - } - }, - "domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" - }, "domexception": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", @@ -9026,24 +8858,6 @@ "webidl-conversions": "^7.0.0" } }, - "domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "requires": { - "domelementtype": "^2.3.0" - } - }, - "domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", - "requires": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - } - }, "dotenv": { "version": "16.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", @@ -9091,7 +8905,8 @@ "entities": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", - "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==" + "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "dev": true }, "error-ex": { "version": "1.3.2", @@ -9678,17 +9493,6 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "requires": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, "http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -10890,25 +10694,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, - "linkedom": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.15.1.tgz", - "integrity": "sha512-8U+wi0VI0JW7ehxX+N/rmZyLWV9bypiCOUeklGBWslpiTpxHnVfY+yHmf6sg06q2kXYdpIJWC6LMpepgOq+JXg==", - "requires": { - "css-select": "^5.1.0", - "cssom": "^0.5.0", - "html-escaper": "^3.0.3", - "htmlparser2": "^8.0.1", - "uhyphen": "^0.2.0" - }, - "dependencies": { - "html-escaper": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", - "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==" - } - } - }, "lint-staged": { "version": "13.2.3", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.2.3.tgz", @@ -11294,14 +11079,6 @@ "path-key": "^3.0.0" } }, - "nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "requires": { - "boolbase": "^1.0.0" - } - }, "nwsapi": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", @@ -12024,11 +11801,6 @@ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true }, - "uhyphen": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", - "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==" - }, "universal-user-agent": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", diff --git a/package.json b/package.json index 943eb280ce9d7..5c5bf2ce3bcd4 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,6 @@ "dotenv": "^16.3.1", "emoji-name-map": "^1.2.8", "github-username-regex": "^1.0.0", - "linkedom": "^0.15.1", "upgrade": "^1.1.0", "word-wrap": "^1.2.5" }, diff --git a/readme.md b/readme.md index 412bae9d9bb87..5c8ee55e60900 100644 --- a/readme.md +++ b/readme.md @@ -399,15 +399,15 @@ GitHub gist pins allow you to pin gists in your GitHub profile using a GitHub re Copy-paste this code into your readme and change the links. -Endpoint: `api/gist?username=Yizack&id=bbfce31e0217a3689c8d961a356cb10d` +Endpoint: `api/gist?id=bbfce31e0217a3689c8d961a356cb10d` ```md -[![Gist Card](https://github-readme-stats.vercel.app/api/gist?username=Yizack&id=bbfce31e0217a3689c8d961a356cb10d)](https://gist.github.com/Yizack/bbfce31e0217a3689c8d961a356cb10d/) +[![Gist Card](https://github-readme-stats.vercel.app/api/gist?id=bbfce31e0217a3689c8d961a356cb10d)](https://gist.github.com/Yizack/bbfce31e0217a3689c8d961a356cb10d/) ``` ### Demo -![Gist Card](https://github-readme-stats.vercel.app/api/gist?username=Yizack\&id=bbfce31e0217a3689c8d961a356cb10d) +![Gist Card](https://github-readme-stats.vercel.app/api/gist?id=bbfce31e0217a3689c8d961a356cb10d) # Top Languages Card @@ -619,11 +619,11 @@ Choose from any of the [default themes](#themes) * Gist card -![Gist Card](https://github-readme-stats.vercel.app/api/gist?username=Yizack&id=bbfce31e0217a3689c8d961a356cb10d) +![Gist Card](https://github-readme-stats.vercel.app/api/gist?id=bbfce31e0217a3689c8d961a356cb10d) * Customizing gist card -![Gist Card](https://github-readme-stats.vercel.app/api/gist?username=Yizack&id=bbfce31e0217a3689c8d961a356cb10d&theme=calm) +![Gist Card](https://github-readme-stats.vercel.app/api/gist?id=bbfce31e0217a3689c8d961a356cb10d&theme=calm) * Top languages diff --git a/src/fetchers/gist-fetcher.js b/src/fetchers/gist-fetcher.js index e98604011e4cf..2afc62a28f58f 100644 --- a/src/fetchers/gist-fetcher.js +++ b/src/fetchers/gist-fetcher.js @@ -1,7 +1,50 @@ // @ts-check -import axios from "axios"; -import { parseHTML } from "linkedom"; +import { request } from "../common/utils.js"; +import { retryer } from "../common/retryer.js"; + +/** + * @typedef {import('axios').AxiosRequestHeaders} AxiosRequestHeaders Axios request headers. + * @typedef {import('axios').AxiosResponse} AxiosResponse Axios response. + */ + +const QUERY = ` +query gistInfo($gistName: String!) { + viewer { + gist(name: $gistName) { + description + owner { + login + } + stargazerCount + forks { + totalCount + } + files { + name + language { + name + } + size + } + } + } +} +`; + +/** + * Gist data fetcher. + * + * @param {AxiosRequestHeaders} variables Fetcher variables. + * @param {string} token GitHub token. + * @returns {Promise} The response. + */ +const fetcher = async (variables, token) => { + return await request( + { query: QUERY, variables }, + { Authorization: `token ${token}` }, + ); +}; /** * @typedef {import('./types').GistData} GistData Gist data. @@ -10,74 +53,53 @@ import { parseHTML } from "linkedom"; /** * Fetch GitHub gist information by given username and ID. * - * @param {string} username Github username. * @param {string} id Github gist ID. * @returns {Promise} Gist data. */ -const fetchGist = async (username, id) => { - const response = ( - await axios({ - method: "get", - url: `https://api.github.com/gists/${id}`, - headers: { - "Content-Type": "application/json", - Accept: "application/vnd.github.cloak-preview", - }, - }) - ).data; - const { starsCount, forksCount } = await fetchGistStargazers(username, id); - +const fetchGist = async (id) => { + const res = await retryer(fetcher, { gistName: id }); + if (res.data.errors) throw new Error(res.data.errors[0].message); + const data = res.data.data.viewer.gist; return { - name: response.files[Object.keys(response.files)[0]].filename, - nameWithOwner: `${username}/${ - response.files[Object.keys(response.files)[0]].filename + name: data.files[Object.keys(data.files)[0]].name, + nameWithOwner: `${data.owner.login}/${ + data.files[Object.keys(data.files)[0]].name }`, - description: response.description, - language: response.files[Object.keys(response.files)[0]].language, - starsCount, - forksCount, + description: data.description, + language: calculatePrimaryLanguage(data.files), + starsCount: data.stargazerCount, + forksCount: data.forks.totalCount, }; }; /** - * Fetch GitHub gist stargazers and forks count by given username and ID. - * - * @param {string} username Github username. - * @param {string} id Github gist ID. - * @returns {Promise<{starsCount: number, forksCount: number}>} Gist stargazers and forks count. + * @typedef {{ name: string; language: { name: string; }, size: number }} GistFile Gist file. */ -const fetchGistStargazers = async (username, id) => { - let starsCount = 0; - let forksCount = 0; - try { - await axios({ - method: "get", - url: `https://gist.github.com/${username}/${id}/stargazers`, - headers: { - "Content-Type": "application/json", - Accept: "application/vnd.github.cloak-preview", - }, - }).then((dom) => { - const { document } = parseHTML(dom.data); - let nav = document.querySelector('[aria-label="Gist"]'); - if (!nav) throw new Error("No nav found"); - let starsBox = nav.querySelector('[data-hotkey="g s"] span.Counter'); - let forksBox = nav.querySelector('[data-hotkey="g f"] span.Counter'); - // @ts-ignore - starsCount = starsBox ? starsBox.title : 0; - // @ts-ignore - forksCount = forksBox ? forksBox.title : 0; - }); - } catch (error) { - starsCount = 0; - forksCount = 0; +/** + * This function calculates the primary language of a gist by files size. + * + * @param {GistFile[]} files Files. + * @returns {string} Primary language. + */ +const calculatePrimaryLanguage = (files) => { + const languages = {}; + for (const file of files) { + if (file.language) { + if (languages[file.language.name]) { + languages[file.language.name] += file.size; + } else { + languages[file.language.name] = file.size; + } + } } - - return { - starsCount, - forksCount, - }; + let primaryLanguage = Object.keys(languages)[0]; + for (const language in languages) { + if (languages[language] > languages[primaryLanguage]) { + primaryLanguage = language; + } + } + return primaryLanguage; }; export { fetchGist }; From c693a2d6992ce905efe2e9b51e7c3da6c4bbfae3 Mon Sep 17 00:00:00 2001 From: Alexandr Date: Mon, 14 Aug 2023 01:18:22 +0300 Subject: [PATCH 7/9] dev --- readme.md | 4 +++ tests/fetchGist.test.js | 72 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 tests/fetchGist.test.js diff --git a/readme.md b/readme.md index 5c8ee55e60900..25b3ce81be228 100644 --- a/readme.md +++ b/readme.md @@ -409,6 +409,10 @@ Endpoint: `api/gist?id=bbfce31e0217a3689c8d961a356cb10d` ![Gist Card](https://github-readme-stats.vercel.app/api/gist?id=bbfce31e0217a3689c8d961a356cb10d) +Use [show\_owner](#gist-card-exclusive-options) query option to include the gist's owner username + +![Gist Card](https://github-readme-stats.vercel.app/api/gist?id=bbfce31e0217a3689c8d961a356cb10d\&show_owner=true) + # Top Languages Card The top languages card shows a GitHub user's most frequently used languages. diff --git a/tests/fetchGist.test.js b/tests/fetchGist.test.js new file mode 100644 index 0000000000000..2cdffcf6d4a7e --- /dev/null +++ b/tests/fetchGist.test.js @@ -0,0 +1,72 @@ +import "@testing-library/jest-dom"; +import axios from "axios"; +import MockAdapter from "axios-mock-adapter"; +import { expect, it, describe, afterEach } from "@jest/globals"; +import { fetchGist } from "../src/fetchers/gist-fetcher.js"; + +const gist_data = { + data: { + viewer: { + gist: { + description: + "List of countries and territories in English and Spanish: name, continent, capital, dial code, country codes, TLD, and area in sq km. Lista de países y territorios en Inglés y Español: nombre, continente, capital, código de teléfono, códigos de país, dominio y área en km cuadrados. Updated 2023", + owner: { + login: "Yizack", + }, + stargazerCount: 33, + forks: { + totalCount: 11, + }, + files: [ + { + name: "countries.json", + language: { + name: "JSON", + }, + size: 85858, + }, + ], + }, + }, + }, +}; + +const gist_errors_data = { + errors: [ + { + message: "Some test GraphQL error", + }, + ], +}; + +const mock = new MockAdapter(axios); + +afterEach(() => { + mock.reset(); +}); + +describe("Test fetchGist", () => { + it("should fetch gist correctly", async () => { + mock.onPost("https://api.github.com/graphql").reply(200, gist_data); + + let gist = await fetchGist("bbfce31e0217a3689c8d961a356cb10d"); + + expect(gist).toStrictEqual({ + name: "countries.json", + nameWithOwner: "Yizack/countries.json", + description: + "List of countries and territories in English and Spanish: name, continent, capital, dial code, country codes, TLD, and area in sq km. Lista de países y territorios en Inglés y Español: nombre, continente, capital, código de teléfono, códigos de país, dominio y área en km cuadrados. Updated 2023", + language: "JSON", + starsCount: 33, + forksCount: 11, + }); + }); + + it("should throw error if reaponse contains them", async () => { + mock.onPost("https://api.github.com/graphql").reply(200, gist_errors_data); + + await expect(fetchGist("bbfce31e0217a3689c8d961a356cb10d")).rejects.toThrow( + "Some test GraphQL error", + ); + }); +}); From 10d1bad3018c1e20f74f381a8dfb765357513b1a Mon Sep 17 00:00:00 2001 From: Alexandr Date: Mon, 14 Aug 2023 01:30:41 +0300 Subject: [PATCH 8/9] e2e --- tests/e2e/e2e.test.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/e2e/e2e.test.js b/tests/e2e/e2e.test.js index e220d5266fc27..0f7f4478ad91e 100644 --- a/tests/e2e/e2e.test.js +++ b/tests/e2e/e2e.test.js @@ -9,10 +9,13 @@ import { renderRepoCard } from "../../src/cards/repo-card.js"; import { renderStatsCard } from "../../src/cards/stats-card.js"; import { renderTopLanguages } from "../../src/cards/top-languages-card.js"; import { renderWakatimeCard } from "../../src/cards/wakatime-card.js"; +import { renderGistCard } from "../../src/cards/gist-card.js"; import { expect, describe, beforeAll, test } from "@jest/globals"; const REPO = "curly-fiesta"; const USER = "catelinemnemosyne"; +const GIST_ID = "372cef55fd897b31909fdeb3a7262758"; + const STATS_DATA = { name: "Cateline Mnemosyne", totalPRs: 2, @@ -81,6 +84,23 @@ const REPOSITORY_DATA = { starCount: 1, }; +/** + * @typedef {import("../../src/fetchers/types").GistData} GistData Gist data type. + */ + +/** + * @type {GistData} + */ +const GIST_DATA = { + name: "link.txt", + nameWithOwner: "qwerty541/link.txt", + description: + "Trying to access this path on Windown 10 ver. 1803+ will breaks NTFS", + language: "Text", + starsCount: 1, + forksCount: 0, +}; + const CACHE_BURST_STRING = `v=${new Date().getTime()}`; describe("Fetch Cards", () => { @@ -177,4 +197,26 @@ describe("Fetch Cards", () => { // Check if Repo card from deployment matches the local Repo card. expect(serverRepoSvg.data).toEqual(localRepoCardSVG); }, 15000); + + test("retrieve gist card", async () => { + expect(VERCEL_PREVIEW_URL).toBeDefined(); + + // Check if the Vercel preview instance Gist function is up and running. + await expect( + axios.get( + `${VERCEL_PREVIEW_URL}/api/gist?id=${GIST_ID}&${CACHE_BURST_STRING}`, + ), + ).resolves.not.toThrow(); + + // Get local gist card. + const localGistCardSVG = renderGistCard(GIST_DATA); + + // Get the Vercel preview gist card response. + const serverGistSvg = await axios.get( + `${VERCEL_PREVIEW_URL}/api/gist?id=${GIST_ID}&${CACHE_BURST_STRING}`, + ); + + // Check if Gist card from deployment matches the local Gist card. + expect(serverGistSvg.data).toEqual(localGistCardSVG); + }); }); From e25b03379bc89978763f5ad344c9ae8e2aed1b79 Mon Sep 17 00:00:00 2001 From: Alexandr Date: Mon, 14 Aug 2023 01:32:49 +0300 Subject: [PATCH 9/9] e2e test timeout --- tests/e2e/e2e.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/e2e.test.js b/tests/e2e/e2e.test.js index 0f7f4478ad91e..2ff55e71ce8e5 100644 --- a/tests/e2e/e2e.test.js +++ b/tests/e2e/e2e.test.js @@ -218,5 +218,5 @@ describe("Fetch Cards", () => { // Check if Gist card from deployment matches the local Gist card. expect(serverGistSvg.data).toEqual(localGistCardSVG); - }); + }, 15000); });