├── .github └── workflows │ ├── ci.yml │ ├── healthcheck.yml │ └── udd.yml ├── .gitignore ├── LICENSE ├── README.md ├── color_scheme.ts ├── color_scheme_test.ts ├── contributions.ts ├── contributions_test.ts ├── deps.ts ├── health_check.ts ├── main.ts ├── mod.ts ├── resources ├── t-rec.gif ├── tests │ ├── example_contributions.json │ ├── to_svg.svg │ ├── to_svg_bg_font_frame_scheme.svg │ ├── to_term_github.text │ ├── to_term_invert.text │ ├── to_term_no_legend.text │ ├── to_term_no_total.text │ ├── to_term_pixel_x.text │ ├── to_term_unicorn.text │ ├── to_text.text │ └── to_text_no_total.text └── tweet.webp ├── server.ts ├── utils.ts ├── utils_test.ts └── velociraptor.yml /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ci: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@master 10 | - uses: denoland/setup-deno@v1 11 | - uses: jurassiscripts/setup-velociraptor@v1 12 | - run: VR_HOOKS=false vr ci 13 | -------------------------------------------------------------------------------- /.github/workflows/healthcheck.yml: -------------------------------------------------------------------------------- 1 | name: Health check 2 | 3 | on: 4 | schedule: 5 | - cron: 0 0/6 * * * 6 | repository_dispatch: 7 | types: [health_check] 8 | 9 | jobs: 10 | health_check: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@master 14 | - uses: denoland/setup-deno@v1 15 | - uses: jurassiscripts/setup-velociraptor@v1 16 | - run: VR_HOOKS=false vr health_check 17 | -------------------------------------------------------------------------------- /.github/workflows/udd.yml: -------------------------------------------------------------------------------- 1 | name: update-deno-dependencies 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | udd: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - uses: denoland/setup-deno@v1 13 | - name: Update dependencies 14 | run: > 15 | deno run -A https://deno.land/x/udd/main.ts 16 | $(find . -name "*.ts") --test="deno test -Ar" 17 | - name: Create Pull Request 18 | uses: peter-evans/create-pull-request@v3 19 | with: 20 | commit-message: "chore(deps): update deno dependencies" 21 | title: Update Deno Dependencies 22 | body: > 23 | Automated updates by [deno-udd](https://github.com/hayd/deno-udd) 24 | and [create-pull-request](https://github.com/peter-evans/create-pull-request) 25 | GitHub action 26 | branch: update-deno-dependencies 27 | author: GitHub 28 | delete-branch: true 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !.vim 2 | !.env.example 3 | *.log 4 | *.vscode/ 5 | cov_profile 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 カワリミ人形 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deno-github-contributions-api 2 | 3 | [![ci](https://github.com/kawarimidoll/deno-github-contributions-api/workflows/ci/badge.svg)](.github/workflows/ci.yml) 4 | [![deno deploy](https://img.shields.io/badge/deno-deploy-blue?logo=deno)](https://github-contributions-api.deno.dev) 5 | [![deno.land](https://img.shields.io/badge/deno-%5E1.13.0-green?logo=deno)](https://deno.land) 6 | [![vr scripts](https://badges.velociraptor.run/flat.svg)](https://velociraptor.run) 7 | [![LICENSE](https://img.shields.io/badge/license-MIT-brightgreen)](LICENSE) 8 | 9 | Get your GitHub contributions data powered by Deno! 10 | 11 | ![gif](resources/t-rec.gif) 12 | 13 | ## Usage 14 | 15 | ### as API 16 | 17 | In your terminal: 18 | 19 | ``` 20 | $ curl https://github-contributions-api.deno.dev 21 | # Then follow the messages... 22 | ``` 23 | 24 | Of course, you can access the endpoint from the web browser: 25 | https://github-contributions-api.deno.dev 26 | 27 | ### as deno module 28 | 29 | In your deno script file: 30 | 31 | ```ts 32 | import { getContributions } from "https://github.com/kawarimidoll/deno-github-contributions-api/raw/main/mod.ts"; 33 | 34 | const username = "your-github-username"; 35 | const token = "xxxxxxxxxxxxxxxxxxxxxxx"; 36 | 37 | const contributions = await getContributions(username, token); 38 | 39 | console.log(contributions.toTerm({ scheme: "random" })); 40 | ``` 41 | 42 | You can see an example in 43 | [main.ts](https://github.com/kawarimidoll/deno-github-contributions-api/blob/main/main.ts) 44 | 45 | The personal access token which has a "read:user" scope is required. 46 | 47 | Generate your token from this page: https://github.com/settings/tokens/new 48 | 49 | ## Extra 50 | 51 | If you are using [GitHub CLI](https://github.com/cli/cli), you can call this API 52 | from [gh-graph](https://github.com/kawarimidoll/gh-graph). 53 | 54 |
55 | Acknowledgements 56 | 57 | tweet 58 | 59 |
60 | 61 | 62 | 63 | 64 | 65 | --- 66 | 67 | ```ts 68 | if (this.repo.isAwesome || this.repo.isHelpful) { 69 | star(this.repo); 70 | } 71 | ``` 72 | 73 | 74 | -------------------------------------------------------------------------------- /color_scheme.ts: -------------------------------------------------------------------------------- 1 | import { hexStrToHexNum } from "./utils.ts"; 2 | import { 3 | CONTRIBUTION_LEVELS, 4 | ContributionLevelName, 5 | isValidContributionLevelName, 6 | } from "./contributions.ts"; 7 | 8 | const COLOR_SCHEMES = { 9 | // by [williambelle/github-contribution-color-graph](https://github.com/williambelle/github-contribution-color-graph) 10 | github: ["#eeeeee", "#9be9a8", "#40c463", "#30a14e", "#216e39"], 11 | halloween: ["#eeeeee", "#fdf156", "#ffc722", "#ff9711", "#04001b"], 12 | amber: ["#eeeeee", "#ffecb3", "#ffd54f", "#ffb300", "#ff6f00"], 13 | blue: ["#eeeeee", "#bbdefb", "#64b5f6", "#1e88e5", "#0d47a1"], 14 | bluegrey: ["#eeeeee", "#cfd8dc", "#90a4ae", "#546e7a", "#263238"], 15 | brown: ["#eeeeee", "#d7ccc8", "#a1887f", "#6d4c41", "#3e2723"], 16 | cyan: ["#eeeeee", "#b2ebf2", "#4dd0e1", "#00acc1", "#006064"], 17 | deeporange: ["#eeeeee", "#ffccbc", "#ff8a65", "#f4511e", "#bf360c"], 18 | deeppurple: ["#eeeeee", "#d1c4e9", "#9575cd", "#5e35b1", "#311b92"], 19 | green: ["#eeeeee", "#c8e6c9", "#81c784", "#43a047", "#1b5e20"], 20 | grey: ["#eeeeee", "#e0e0e0", "#9e9e9e", "#616161", "#212121"], 21 | indigo: ["#eeeeee", "#c5cae9", "#7986cb", "#3949ab", "#1a237e"], 22 | lightblue: ["#eeeeee", "#b3e5fc", "#4fc3f7", "#039be5", "#01579b"], 23 | lightgreen: ["#eeeeee", "#dcedc8", "#aed581", "#7cb342", "#33691e"], 24 | lime: ["#eeeeee", "#f0f4c3", "#dce775", "#c0ca33", "#827717"], 25 | orange: ["#eeeeee", "#ffe0b2", "#ffb74d", "#fb8c00", "#e65100"], 26 | pink: ["#eeeeee", "#f8bbd0", "#f06292", "#e91e63", "#880e4f"], 27 | purple: ["#eeeeee", "#e1bee7", "#ba68c8", "#8e24aa", "#4a148c"], 28 | red: ["#eeeeee", "#ffcdd2", "#e57373", "#e53935", "#b71c1c"], 29 | teal: ["#eeeeee", "#b2dfdb", "#4db6ac", "#00897b", "#004d40"], 30 | yellowMd: ["#eeeeee", "#fff9c4", "#fff176", "#ffd835", "#f57f17"], 31 | unicorn: ["#eeeeee", "#6dc5fb", "#f6f68c", "#8affa4", "#f283d1"], 32 | summer: ["#eeeeee", "#eae374", "#f9d62e", "#fc913a", "#ff4e50"], 33 | sunset: ["#eeeeee", "#fed800", "#ff6f01", "#fd2f24", "#811d5e"], 34 | moon: ["#eeeeee", "#6bcdff", "#00a1f3", "#48009a", "#4f2266"], 35 | psychedelic: ["#eeeeee", "#faafe1", "#fb6dcc", "#fa3fbc", "#ff00ab"], 36 | yellow: ["#eeeeee", "#d7d7a2", "#d4d462", "#e0e03f", "#ffff00"], 37 | 38 | // by kawarimidoll 39 | gameboy: ["#eeeeee", "#ccdc5f", "#91a633", "#606520", "#2c370b"], 40 | }; 41 | type ColorSchemeName = keyof typeof COLOR_SCHEMES; 42 | 43 | const isValidColorSchemeName = (name?: string): name is ColorSchemeName => 44 | !!name && Object.hasOwn(COLOR_SCHEMES, name); 45 | 46 | const randomColorScheme = () => { 47 | const values = Object.values(COLOR_SCHEMES); 48 | return values[(Math.random() * values.length) << 0]; 49 | }; 50 | 51 | const getColorScheme = (name = "github") => { 52 | if (name != "random" && !isValidColorSchemeName(name)) { 53 | throw new Error( 54 | `'${name}' is invalid color scheme name! Choose from: ${ 55 | Object.keys(COLOR_SCHEMES) 56 | },random`, 57 | ); 58 | } 59 | 60 | const hexStrColors = name === "random" 61 | ? randomColorScheme() 62 | : COLOR_SCHEMES[name]; 63 | 64 | const hexNumColors = hexStrColors.map((color) => hexStrToHexNum(color)); 65 | 66 | const getByLevel = (levelName?: ContributionLevelName) => 67 | hexNumColors[ 68 | isValidContributionLevelName(levelName) 69 | ? CONTRIBUTION_LEVELS[levelName] 70 | : 0 71 | ]; 72 | 73 | return { hexStrColors, hexNumColors, getByLevel }; 74 | }; 75 | 76 | export { COLOR_SCHEMES, getColorScheme, isValidColorSchemeName }; 77 | export type { ColorSchemeName }; 78 | -------------------------------------------------------------------------------- /color_scheme_test.ts: -------------------------------------------------------------------------------- 1 | import { assert, assertEquals, assertThrows } from "./deps.ts"; 2 | import { 3 | COLOR_SCHEMES, 4 | getColorScheme, 5 | isValidColorSchemeName, 6 | } from "./color_scheme.ts"; 7 | 8 | Deno.test("getColorScheme", () => { 9 | const correctScheme = ["#eeeeee", "#9be9a8", "#40c463", "#30a14e", "#216e39"]; 10 | 11 | const scheme1 = getColorScheme("github"); 12 | assertEquals(scheme1.hexStrColors, correctScheme); 13 | 14 | const scheme2 = getColorScheme(); 15 | assertEquals(scheme2.hexStrColors, correctScheme); 16 | 17 | assertEquals(scheme2.getByLevel("NONE"), 0xeeeeee); 18 | assertEquals(scheme2.getByLevel("FIRST_QUARTILE"), 0x9be9a8); 19 | assertEquals(scheme2.getByLevel(), 0xeeeeee); 20 | 21 | const scheme3 = getColorScheme("random"); 22 | assert(Object.values(COLOR_SCHEMES).includes(scheme3.hexStrColors)); 23 | 24 | assertThrows(() => { 25 | getColorScheme("123456"); 26 | }); 27 | }); 28 | 29 | Deno.test("isValidColorSchemeName", () => { 30 | assert(isValidColorSchemeName("github")); 31 | assert(isValidColorSchemeName("unicorn")); 32 | assert(!isValidColorSchemeName("")); 33 | assert(!isValidColorSchemeName("nothub")); 34 | }); 35 | -------------------------------------------------------------------------------- /contributions.ts: -------------------------------------------------------------------------------- 1 | import { getColorScheme } from "./color_scheme.ts"; 2 | import { bgRgb24, h, ky, rgb24, stringWidth } from "./deps.ts"; 3 | import { confirmHex, convertToSixChars } from "./utils.ts"; 4 | 5 | type ContributionDay = { 6 | contributionCount: number; 7 | contributionLevel: ContributionLevelName; 8 | date: string; 9 | color: string; 10 | }; 11 | 12 | const CONTRIBUTION_LEVELS = { 13 | NONE: 0, 14 | FIRST_QUARTILE: 1, 15 | SECOND_QUARTILE: 2, 16 | THIRD_QUARTILE: 3, 17 | FOURTH_QUARTILE: 4, 18 | }; 19 | type ContributionLevelName = keyof typeof CONTRIBUTION_LEVELS; 20 | 21 | const isValidContributionLevelName = ( 22 | name?: string, 23 | ): name is ContributionLevelName => 24 | !!name && Object.hasOwn(CONTRIBUTION_LEVELS, name); 25 | 26 | type ContributionOptions = { 27 | from?: string; 28 | to?: string; 29 | }; 30 | 31 | type ContributionResponse = { 32 | data: { 33 | user: { 34 | contributionsCollection: { 35 | contributionCalendar: { 36 | totalContributions: number; 37 | weeks: { 38 | contributionDays: ContributionDay[]; 39 | }[]; 40 | }; 41 | }; 42 | }; 43 | }; 44 | }; 45 | 46 | const getContributionCalendar = async ( 47 | userName: string, 48 | token: string, 49 | contributionOptions: ContributionOptions = {}, 50 | ) => { 51 | if (!userName || !token) { 52 | throw new Error("Missing required arguments"); 53 | } 54 | const { from, to } = contributionOptions; 55 | 56 | const query = ` 57 | query($userName:String! $from:DateTime $to:DateTime) { 58 | user(login: $userName){ 59 | contributionsCollection(from: $from, to: $to) { 60 | contributionCalendar { 61 | totalContributions 62 | weeks { 63 | contributionDays { 64 | color 65 | contributionCount 66 | contributionLevel 67 | date 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | `; 75 | const variables = JSON.stringify({ userName, from, to }); 76 | 77 | const json = { query, variables }; 78 | const url = "https://api.github.com/graphql"; 79 | const { data } = await ky.post(url, { 80 | headers: { Authorization: `Bearer ${token}` }, 81 | json, 82 | }).json() as ContributionResponse; 83 | 84 | const contributionCalendar = data?.user?.contributionsCollection 85 | ?.contributionCalendar; 86 | 87 | if ( 88 | !contributionCalendar || !Object.hasOwn(contributionCalendar, "weeks") || 89 | !Object.hasOwn(contributionCalendar, "totalContributions") 90 | ) { 91 | throw new Error("Could not get contributions data"); 92 | } 93 | 94 | const { weeks, totalContributions }: { 95 | weeks: { contributionDays: ContributionDay[] }[]; 96 | totalContributions: number; 97 | } = contributionCalendar; 98 | 99 | const contributions = weeks.map((week) => week.contributionDays); 100 | 101 | return { contributions, totalContributions }; 102 | }; 103 | 104 | const totalMsg = (totalNum: number): string => 105 | `${totalNum} contributions in the last year`; 106 | 107 | const moreContributionDay = (a: ContributionDay, b: ContributionDay) => 108 | a.contributionCount > b.contributionCount ? a : b; 109 | 110 | const getMaxContributionDay = ( 111 | contributions: ContributionDay[][], 112 | ): ContributionDay => 113 | contributions.reduce( 114 | (max, week) => 115 | moreContributionDay( 116 | max, 117 | week.reduce( 118 | (maxInWeek, current) => moreContributionDay(maxInWeek, current), 119 | week[0], 120 | ), 121 | ), 122 | contributions[0][0], 123 | ); 124 | 125 | const contributionsToJson = ( 126 | contributions: ContributionDay[][], 127 | totalContributions: number, 128 | { 129 | flat = false, 130 | } = {}, 131 | ) => 132 | JSON.stringify({ 133 | contributions: flat ? contributions.flat() : contributions, 134 | totalContributions, 135 | }); 136 | 137 | const contributionsToTerm = ( 138 | contributions: ContributionDay[][], 139 | totalContributions: number, 140 | { 141 | noTotal = false, 142 | noLegend = false, 143 | scheme = "github", 144 | pixel = "■", 145 | invert = false, 146 | } = {}, 147 | ) => { 148 | const pixelWidth = stringWidth(pixel); 149 | if (pixelWidth > 2) { 150 | // width == 2 is ok 151 | // like as "[]", "草", " " 152 | throw new Error(`Pixel '${pixel}' is too long. Max width of pixel is 2.`); 153 | } 154 | 155 | const colorScheme = getColorScheme(scheme); 156 | 157 | const total = !noTotal ? `${totalMsg(totalContributions)}\n` : ""; 158 | 159 | // 10 is length of 'Less More' 160 | // 5 is count of colored pixels as legend 161 | const legendOffset = " ".repeat( 162 | (contributions.length - 5) * pixelWidth - 10, 163 | ); 164 | 165 | const legend = !noLegend 166 | ? legendOffset + 167 | "Less " + colorScheme.hexNumColors.map((color) => 168 | invert ? bgRgb24(pixel, color) : rgb24(pixel, color) 169 | ).join("") + " More\n" 170 | : ""; 171 | 172 | const grass = (day?: ContributionDay) => 173 | day?.contributionLevel 174 | ? invert 175 | ? bgRgb24(pixel, colorScheme.getByLevel(day?.contributionLevel)) 176 | : rgb24(pixel, colorScheme.getByLevel(day?.contributionLevel)) 177 | : ""; 178 | 179 | return total + 180 | contributions[0].reduce( 181 | (acc, _, i) => 182 | acc + contributions.map((row) => grass(row[i])).join("") + "\n", 183 | "", 184 | ) + legend; 185 | }; 186 | 187 | const contributionsToText = ( 188 | contributions: ContributionDay[][], 189 | totalContributions: number, 190 | maxContributionDay: ContributionDay, 191 | { 192 | noTotal = false, 193 | } = {}, 194 | ) => { 195 | const total = !noTotal ? `${totalMsg(totalContributions)}\n` : ""; 196 | 197 | const pad = String(maxContributionDay.contributionCount).length; 198 | 199 | return total + 200 | contributions[0].reduce( 201 | (acc, _, i) => 202 | acc + contributions.map((row) => 203 | `${row[i]?.contributionCount ?? ""}`.padStart(pad) 204 | ).join(",") + 205 | "\n", 206 | "", 207 | ); 208 | }; 209 | 210 | const contributionsToSvg = ( 211 | contributions: ContributionDay[][], 212 | totalContributions: number, 213 | { 214 | noTotal = false, 215 | noLegend = false, 216 | scheme = "github", 217 | fontColor = "000", 218 | frame = "none", 219 | bg = "none", 220 | } = {}, 221 | ): string => { 222 | const svgID = "deno-github-contributions-graph"; 223 | const rectSize = 10; 224 | const rectSpan = 3; 225 | const rectRadius = 2; 226 | const rectStep = rectSize + rectSpan; 227 | 228 | const weekCounts = 53; 229 | const dayCounts = 7; 230 | 231 | const topPadding = noTotal ? 0 : 1; 232 | const bottomPadding = noLegend ? 0 : 1; 233 | 234 | const width = rectStep * (weekCounts + 2) - rectSpan; 235 | const height = rectStep * (dayCounts + 2 + topPadding + bottomPadding) - 236 | rectSpan; 237 | 238 | const offset = { x: rectStep, y: rectStep * (topPadding + 1) }; 239 | 240 | // the left top position of the 5 pixels of legend 241 | const legendPos = { 242 | x: width - rectStep * 10 + rectSpan, 243 | y: offset.y + rectStep * dayCounts + rectSpan, 244 | }; 245 | 246 | const styles = `#${svgID} .pixel { 247 | width: ${rectSize}px; 248 | height: ${rectSize}px; 249 | rx: ${rectRadius}px; 250 | ry: ${rectRadius}px; 251 | stroke: rgba(27,31,35,0.06); 252 | stroke-width: 2px; 253 | } 254 | #${svgID} text { 255 | font-family: monospace; 256 | font-size: ${rectSize * 1.5}px; 257 | fill: #${convertToSixChars(fontColor, "000")}; 258 | } 259 | `; 260 | 261 | try { 262 | const colorScheme = getColorScheme(scheme); 263 | 264 | const rect = (x: number, y: number, { 265 | contributionLevel = "", 266 | date = "", 267 | contributionCount = 0, 268 | }): string => 269 | contributionLevel == null ? "" : h("rect", { 270 | class: `pixel ${contributionLevel}`, 271 | x: x * rectStep, 272 | y: y * rectStep, 273 | "data-date": date, 274 | "data-count": contributionCount, 275 | }, h("title", `${date}: ${contributionCount}`)); 276 | 277 | frame = confirmHex(frame, "none"); 278 | const stroke = frame === "none" ? frame : "#" + convertToSixChars(frame); 279 | bg = confirmHex(bg, "none"); 280 | const fill = bg === "none" ? bg : "#" + convertToSixChars(bg); 281 | 282 | return h( 283 | "svg", 284 | { width, height, xmlns: "http://www.w3.org/2000/svg", id: svgID }, 285 | h( 286 | "style", 287 | styles, 288 | ...Object.entries(CONTRIBUTION_LEVELS).map(([k, v]) => 289 | `#${svgID} .${k} { fill: ${colorScheme.hexStrColors[v]}; }` 290 | ), 291 | ), 292 | h("rect", { width, height, stroke, "stroke-width": "2px", fill }), 293 | noTotal ? "" : h( 294 | "g", 295 | h( 296 | "text", 297 | { transform: `translate(${offset.x}, ${offset.y - rectSpan * 2})` }, 298 | totalMsg(totalContributions), 299 | ), 300 | ), 301 | h( 302 | "g", 303 | { transform: `translate(${offset.x}, ${offset.y})` }, 304 | contributions.map((column, i) => 305 | column.map((pixel, j) => rect(i, j, pixel)).join("") 306 | ).join(""), 307 | ), 308 | noLegend ? "" : h( 309 | "g", 310 | { transform: `translate(${legendPos.x}, ${legendPos.y})` }, 311 | h( 312 | "text", 313 | { 314 | transform: `translate(-${rectStep * 1}, ${rectSize * 1})`, 315 | "text-anchor": "end", 316 | }, 317 | "Less", 318 | ), 319 | Object.keys(CONTRIBUTION_LEVELS).map((levelName, idx) => 320 | rect(idx, 0, { contributionLevel: levelName }) 321 | ).join(""), 322 | h( 323 | "text", 324 | { 325 | transform: `translate(${rectStep * 5 + rectSize}, ${rectSize * 1})`, 326 | }, 327 | "More", 328 | ), 329 | ), 330 | ); 331 | } catch (error) { 332 | return h( 333 | "svg", 334 | { width, height, xmlns: "http://www.w3.org/2000/svg", id: svgID }, 335 | h( 336 | "text", 337 | { y: height }, 338 | `${error}`, 339 | ), 340 | ); 341 | } 342 | }; 343 | 344 | const getContributions = async ( 345 | userName: string, 346 | token: string, 347 | contributionOptions: ContributionOptions = {}, 348 | ) => { 349 | const { from, to } = contributionOptions; 350 | const { contributions, totalContributions } = await getContributionCalendar( 351 | userName, 352 | token, 353 | { from, to }, 354 | ); 355 | 356 | const maxContributionDay = getMaxContributionDay(contributions); 357 | 358 | const toJson = ({ flat = false } = {}) => 359 | contributionsToJson(contributions, totalContributions, { flat }); 360 | 361 | const toTerm = ( 362 | { 363 | noTotal = false, 364 | noLegend = false, 365 | scheme = "github", 366 | pixel = "■", 367 | invert = false, 368 | } = {}, 369 | ) => 370 | contributionsToTerm(contributions, totalContributions, { 371 | noTotal, 372 | noLegend, 373 | scheme, 374 | pixel, 375 | invert, 376 | }); 377 | 378 | const toText = ( 379 | { 380 | noTotal = false, 381 | } = {}, 382 | ) => 383 | contributionsToText(contributions, totalContributions, maxContributionDay, { 384 | noTotal, 385 | }); 386 | const toSvg = ( 387 | { 388 | noTotal = false, 389 | noLegend = false, 390 | scheme = "github", 391 | fontColor = "000", 392 | frame = "none", 393 | bg = "none", 394 | } = {}, 395 | ) => 396 | contributionsToSvg(contributions, totalContributions, { 397 | noTotal, 398 | noLegend, 399 | scheme, 400 | fontColor, 401 | frame, 402 | bg, 403 | }); 404 | 405 | return { 406 | contributions, 407 | totalContributions, 408 | maxContributionDay, 409 | toJson, 410 | toTerm, 411 | toText, 412 | toSvg, 413 | }; 414 | }; 415 | 416 | export { 417 | CONTRIBUTION_LEVELS, 418 | contributionsToJson, 419 | contributionsToSvg, 420 | contributionsToTerm, 421 | contributionsToText, 422 | getContributionCalendar, 423 | getContributions, 424 | getMaxContributionDay, 425 | isValidContributionLevelName, 426 | moreContributionDay, 427 | totalMsg, 428 | }; 429 | 430 | export type { 431 | ContributionDay, 432 | ContributionLevelName, 433 | ContributionOptions, 434 | ContributionResponse, 435 | }; 436 | -------------------------------------------------------------------------------- /contributions_test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | assertEquals, 4 | assertRejects, 5 | assertThrows, 6 | ky, 7 | testdouble, 8 | } from "./deps.ts"; 9 | import { 10 | ContributionDay, 11 | contributionsToJson, 12 | contributionsToSvg, 13 | contributionsToTerm, 14 | contributionsToText, 15 | getContributionCalendar, 16 | getContributions, 17 | getMaxContributionDay, 18 | isValidContributionLevelName, 19 | moreContributionDay, 20 | totalMsg, 21 | } from "./contributions.ts"; 22 | 23 | const { 24 | contributions, 25 | totalContributions, 26 | }: { 27 | contributions: ContributionDay[][]; 28 | totalContributions: number; 29 | } = JSON.parse( 30 | await Deno.readTextFile("./resources/tests/example_contributions.json"), 31 | ); 32 | 33 | const weeks = contributions.map((week) => ({ contributionDays: week })); 34 | 35 | const max: ContributionDay = { 36 | contributionCount: 32, 37 | contributionLevel: "FOURTH_QUARTILE", 38 | date: "2021-03-22", 39 | color: "#216e39", 40 | }; 41 | 42 | Deno.test("contributionsToJson", () => { 43 | assertEquals( 44 | contributionsToJson(contributions, totalContributions), 45 | JSON.stringify({ contributions, totalContributions }), 46 | ); 47 | 48 | assertEquals( 49 | contributionsToJson(contributions, totalContributions, { flat: true }), 50 | JSON.stringify({ contributions: contributions.flat(), totalContributions }), 51 | ); 52 | }); 53 | 54 | Deno.test("contributionsToSvg", async () => { 55 | const resultToSvg = await Deno.readTextFile( 56 | "./resources/tests/to_svg.svg", 57 | ); 58 | const resultToSvgWithParams = await Deno.readTextFile( 59 | "./resources/tests/to_svg_bg_font_frame_scheme.svg", 60 | ); 61 | assertEquals( 62 | contributionsToSvg(contributions, totalContributions), 63 | resultToSvg, 64 | ); 65 | assertEquals( 66 | contributionsToSvg(contributions, totalContributions, { 67 | bg: "786688", 68 | fontColor: "#d7f07b", 69 | frame: "#f03153", 70 | scheme: "amber", 71 | }), 72 | resultToSvgWithParams, 73 | ); 74 | }); 75 | 76 | Deno.test("contributionsToTerm", async () => { 77 | const resultToTerm = await Deno.readTextFile( 78 | "./resources/tests/to_term_github.text", 79 | ); 80 | const resultToTermUnicorn = await Deno.readTextFile( 81 | "./resources/tests/to_term_unicorn.text", 82 | ); 83 | const resultToTermNoTotal = await Deno.readTextFile( 84 | "./resources/tests/to_term_no_total.text", 85 | ); 86 | const resultToTermNoLegend = await Deno.readTextFile( 87 | "./resources/tests/to_term_no_legend.text", 88 | ); 89 | const resultToTermPixelX = await Deno.readTextFile( 90 | "./resources/tests/to_term_pixel_x.text", 91 | ); 92 | const resultToTermInvert = await Deno.readTextFile( 93 | "./resources/tests/to_term_invert.text", 94 | ); 95 | assertEquals( 96 | contributionsToTerm(contributions, totalContributions), 97 | resultToTerm, 98 | ); 99 | assertEquals( 100 | contributionsToTerm(contributions, totalContributions, { 101 | noTotal: false, 102 | noLegend: false, 103 | scheme: "github", 104 | pixel: "■", 105 | invert: false, 106 | }), 107 | resultToTerm, 108 | ); 109 | assertEquals( 110 | contributionsToTerm(contributions, totalContributions, { 111 | scheme: "unicorn", 112 | }), 113 | resultToTermUnicorn, 114 | ); 115 | assertEquals( 116 | contributionsToTerm(contributions, totalContributions, { noTotal: true }), 117 | resultToTermNoTotal, 118 | ); 119 | assertEquals( 120 | contributionsToTerm(contributions, totalContributions, { noLegend: true }), 121 | resultToTermNoLegend, 122 | ); 123 | assertEquals( 124 | contributionsToTerm(contributions, totalContributions, { pixel: "x" }), 125 | resultToTermPixelX, 126 | ); 127 | assertThrows( 128 | () => { 129 | contributionsToTerm(contributions, totalContributions, { pixel: "xxx" }); 130 | }, 131 | Error, 132 | ); 133 | assertEquals( 134 | contributionsToTerm(contributions, totalContributions, { invert: true }), 135 | resultToTermInvert, 136 | ); 137 | }); 138 | 139 | Deno.test("contributionsToText", async () => { 140 | const resultToText = await Deno.readTextFile( 141 | "./resources/tests/to_text.text", 142 | ); 143 | const resultToTextNoTotal = await Deno.readTextFile( 144 | "./resources/tests/to_text_no_total.text", 145 | ); 146 | assertEquals( 147 | contributionsToText(contributions, totalContributions, max), 148 | resultToText, 149 | ); 150 | assertEquals( 151 | contributionsToText(contributions, totalContributions, max, { 152 | noTotal: true, 153 | }), 154 | resultToTextNoTotal, 155 | ); 156 | }); 157 | 158 | Deno.test("getContributions", async () => { 159 | testdouble.replace( 160 | ky, 161 | "post", 162 | (_: string) => ({ 163 | json: () => ({ data: null }), 164 | }), 165 | ); 166 | 167 | assertRejects( 168 | () => { 169 | return getContributions("a", "a"); 170 | }, 171 | Error, 172 | "Could not get contributions data", 173 | ); 174 | 175 | testdouble.replace( 176 | ky, 177 | "post", 178 | () => ({ 179 | json: () => ({ 180 | data: { 181 | user: { 182 | contributionsCollection: { 183 | contributionCalendar: { 184 | weeks, 185 | totalContributions, 186 | }, 187 | }, 188 | }, 189 | }, 190 | }), 191 | }), 192 | ); 193 | 194 | const obj = await getContributions("a", "a"); 195 | assert(obj); 196 | assertEquals(obj.contributions, contributions); 197 | assertEquals(obj.totalContributions, totalContributions); 198 | assertEquals(obj.maxContributionDay, max); 199 | assert(obj.toJson()); 200 | assert(obj.toTerm()); 201 | assert(obj.toText()); 202 | }); 203 | 204 | Deno.test("getContributionCalendar", async () => { 205 | assertRejects( 206 | () => { 207 | return getContributionCalendar("userName", ""); 208 | }, 209 | Error, 210 | "Missing required arguments", 211 | ); 212 | assertRejects( 213 | () => { 214 | return getContributionCalendar("", "token"); 215 | }, 216 | Error, 217 | "Missing required arguments", 218 | ); 219 | 220 | testdouble.replace( 221 | ky, 222 | "post", 223 | (_: string) => ({ 224 | json: () => ({ data: null }), 225 | }), 226 | ); 227 | 228 | assertRejects( 229 | () => { 230 | return getContributionCalendar("a", "a"); 231 | }, 232 | Error, 233 | "Could not get contributions data", 234 | ); 235 | 236 | testdouble.replace( 237 | ky, 238 | "post", 239 | () => ({ 240 | json: () => ({ 241 | data: { 242 | user: { 243 | contributionsCollection: { 244 | contributionCalendar: { 245 | // weeks: [{ contributionDays: [max] }], 246 | weeks, 247 | totalContributions, 248 | }, 249 | }, 250 | }, 251 | }, 252 | }), 253 | }), 254 | ); 255 | 256 | assertEquals( 257 | await getContributionCalendar("a", "a"), 258 | { contributions, totalContributions }, 259 | ); 260 | }); 261 | 262 | Deno.test("getMaxContributionDay", () => { 263 | assertEquals(getMaxContributionDay(contributions), max); 264 | }); 265 | 266 | Deno.test("isValidContributionLevelName", () => { 267 | assert(isValidContributionLevelName("NONE")); 268 | assert(isValidContributionLevelName("FIRST_QUARTILE")); 269 | assert(isValidContributionLevelName("SECOND_QUARTILE")); 270 | assert(isValidContributionLevelName("THIRD_QUARTILE")); 271 | assert(isValidContributionLevelName("FOURTH_QUARTILE")); 272 | assert(!isValidContributionLevelName("")); 273 | assert(!isValidContributionLevelName("none")); 274 | }); 275 | 276 | Deno.test("moreContributionDay", () => { 277 | const a: ContributionDay = { 278 | contributionCount: 10, 279 | contributionLevel: "FIRST_QUARTILE", 280 | date: "2000-01-01", 281 | color: "#eeeeee", 282 | }; 283 | const b: ContributionDay = { 284 | contributionCount: 3, 285 | contributionLevel: "FIRST_QUARTILE", 286 | date: "2000-01-01", 287 | color: "#eeeeee", 288 | }; 289 | 290 | assertEquals(moreContributionDay(a, b), a); 291 | }); 292 | 293 | Deno.test("totalMsg", () => { 294 | assertEquals(totalMsg(10), "10 contributions in the last year"); 295 | }); 296 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | import ky from "https://cdn.skypack.dev/ky@0.28.5?dts"; 2 | 3 | import testdouble from "https://esm.sh/testdouble@3.18.0/dist/testdouble.js"; 4 | 5 | import stringWidth from "https://cdn.skypack.dev/string-width@5.0.0?dts"; 6 | 7 | import { bgRgb24, rgb24 } from "https://deno.land/std@0.201.0/fmt/colors.ts"; 8 | 9 | import { 10 | assert, 11 | assertEquals, 12 | assertRejects, 13 | assertThrows, 14 | } from "https://deno.land/std@0.201.0/testing/asserts.ts"; 15 | 16 | import { loadSync } from "https://deno.land/std@0.201.0/dotenv/mod.ts"; 17 | loadSync({ 18 | export: true, 19 | examplePath: null, 20 | defaultsPath: null, 21 | restrictEnvAccessTo: ["GH_READ_USER_TOKEN"], 22 | }); 23 | 24 | import { outdent } from "https://deno.land/x/outdent@v0.8.0/mod.ts"; 25 | 26 | import { tag as h } from "https://deno.land/x/markup_tag@0.4.0/mod.ts"; 27 | 28 | export { 29 | assert, 30 | assertEquals, 31 | assertRejects, 32 | assertThrows, 33 | bgRgb24, 34 | h, 35 | ky, 36 | outdent, 37 | rgb24, 38 | stringWidth, 39 | testdouble, 40 | }; 41 | -------------------------------------------------------------------------------- /health_check.ts: -------------------------------------------------------------------------------- 1 | import { ky } from "./deps.ts"; 2 | 3 | const prefixUrl = "https://github-contributions-api.deno.dev"; 4 | const k = ky.create({ prefixUrl }); 5 | try { 6 | console.log("Root"); 7 | await k("").text(); 8 | 9 | console.log("User"); 10 | await k("kawarimidoll").text(); 11 | 12 | console.log("Text"); 13 | await k("kawarimidoll.text").text(); 14 | 15 | console.log("Json"); 16 | await k("kawarimidoll.json").text(); 17 | 18 | console.log("Term"); 19 | await k("kawarimidoll.term").text(); 20 | 21 | console.log("Svg"); 22 | await k("kawarimidoll.svg").text(); 23 | 24 | console.log("Parameters"); 25 | await k("kawarimidoll.svg", { 26 | searchParams: { 27 | scheme: "random", 28 | "no-total": true, 29 | bg: "123abc", 30 | }, 31 | }).text(); 32 | 33 | console.log("System all green!"); 34 | } catch (error) { 35 | console.error(`${error}`); 36 | Deno.exit(1); 37 | } 38 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { getContributions } from "./contributions.ts"; 2 | 3 | const username = "kawarimidoll"; 4 | const token = Deno.env.get("GH_READ_USER_TOKEN") || ""; 5 | 6 | const contributions = await getContributions(username, token); 7 | 8 | // console.log(contributions.toJson()); 9 | console.log(contributions.toTerm({ scheme: "random" })); 10 | // console.log(contributions.toText()); 11 | // console.log(contributions.toTerm({ invert: true, pixel: " " })); 12 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | // constants and functions 2 | export { 3 | CONTRIBUTION_LEVELS, 4 | getContributions, 5 | isValidContributionLevelName, 6 | } from "./contributions.ts"; 7 | 8 | export { COLOR_SCHEMES, getColorScheme } from "./color_scheme.ts"; 9 | 10 | export { 11 | confirmHex, 12 | convertToSixChars, 13 | hexStrToHexNum, 14 | hexStrToRgbObj, 15 | } from "./utils.ts"; 16 | 17 | // types and interfaces 18 | export type { 19 | ContributionDay, 20 | ContributionLevelName, 21 | } from "./contributions.ts"; 22 | 23 | export type { ColorSchemeName } from "./color_scheme.ts"; 24 | -------------------------------------------------------------------------------- /resources/t-rec.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kawarimidoll/deno-github-contributions-api/6898dfb4baac1a6ef428519aaa92a3269bab799f/resources/t-rec.gif -------------------------------------------------------------------------------- /resources/tests/to_svg.svg: -------------------------------------------------------------------------------- 1 | 2309 contributions in the last year2020-07-12: 52020-07-13: 22020-07-14: 22020-07-15: 32020-07-16: 02020-07-17: 92020-07-18: 02020-07-19: 12020-07-20: 12020-07-21: 42020-07-22: 42020-07-23: 132020-07-24: 102020-07-25: 52020-07-26: 132020-07-27: 42020-07-28: 02020-07-29: 82020-07-30: 62020-07-31: 22020-08-01: 72020-08-02: 52020-08-03: 02020-08-04: 52020-08-05: 52020-08-06: 12020-08-07: 32020-08-08: 32020-08-09: 22020-08-10: 12020-08-11: 02020-08-12: 12020-08-13: 02020-08-14: 32020-08-15: 32020-08-16: 42020-08-17: 52020-08-18: 72020-08-19: 42020-08-20: 02020-08-21: 22020-08-22: 102020-08-23: 52020-08-24: 72020-08-25: 82020-08-26: 22020-08-27: 52020-08-28: 32020-08-29: 42020-08-30: 92020-08-31: 12020-09-01: 32020-09-02: 52020-09-03: 12020-09-04: 12020-09-05: 52020-09-06: 12020-09-07: 12020-09-08: 32020-09-09: 22020-09-10: 42020-09-11: 32020-09-12: 42020-09-13: 22020-09-14: 32020-09-15: 32020-09-16: 42020-09-17: 32020-09-18: 12020-09-19: 32020-09-20: 32020-09-21: 92020-09-22: 22020-09-23: 12020-09-24: 12020-09-25: 92020-09-26: 22020-09-27: 32020-09-28: 12020-09-29: 22020-09-30: 52020-10-01: 62020-10-02: 52020-10-03: 42020-10-04: 32020-10-05: 52020-10-06: 52020-10-07: 22020-10-08: 32020-10-09: 62020-10-10: 12020-10-11: 82020-10-12: 82020-10-13: 12020-10-14: 32020-10-15: 22020-10-16: 12020-10-17: 62020-10-18: 32020-10-19: 32020-10-20: 12020-10-21: 12020-10-22: 12020-10-23: 12020-10-24: 42020-10-25: 102020-10-26: 12020-10-27: 42020-10-28: 42020-10-29: 12020-10-30: 12020-10-31: 12020-11-01: 12020-11-02: 42020-11-03: 12020-11-04: 12020-11-05: 22020-11-06: 32020-11-07: 42020-11-08: 42020-11-09: 52020-11-10: 32020-11-11: 12020-11-12: 102020-11-13: 12020-11-14: 22020-11-15: 12020-11-16: 32020-11-17: 12020-11-18: 12020-11-19: 12020-11-20: 12020-11-21: 22020-11-22: 12020-11-23: 12020-11-24: 12020-11-25: 12020-11-26: 12020-11-27: 12020-11-28: 32020-11-29: 62020-11-30: 12020-12-01: 22020-12-02: 12020-12-03: 12020-12-04: 12020-12-05: 12020-12-06: 22020-12-07: 12020-12-08: 12020-12-09: 22020-12-10: 22020-12-11: 22020-12-12: 22020-12-13: 42020-12-14: 12020-12-15: 12020-12-16: 22020-12-17: 22020-12-18: 12020-12-19: 12020-12-20: 22020-12-21: 62020-12-22: 72020-12-23: 42020-12-24: 112020-12-25: 92020-12-26: 102020-12-27: 102020-12-28: 72020-12-29: 52020-12-30: 102020-12-31: 42021-01-01: 72021-01-02: 72021-01-03: 102021-01-04: 122021-01-05: 92021-01-06: 52021-01-07: 182021-01-08: 232021-01-09: 282021-01-10: 102021-01-11: 52021-01-12: 42021-01-13: 62021-01-14: 42021-01-15: 62021-01-16: 62021-01-17: 92021-01-18: 72021-01-19: 102021-01-20: 42021-01-21: 152021-01-22: 132021-01-23: 122021-01-24: 82021-01-25: 52021-01-26: 22021-01-27: 22021-01-28: 42021-01-29: 62021-01-30: 82021-01-31: 62021-02-01: 52021-02-02: 142021-02-03: 62021-02-04: 122021-02-05: 12021-02-06: 172021-02-07: 132021-02-08: 192021-02-09: 122021-02-10: 182021-02-11: 242021-02-12: 142021-02-13: 72021-02-14: 42021-02-15: 22021-02-16: 182021-02-17: 162021-02-18: 42021-02-19: 62021-02-20: 62021-02-21: 72021-02-22: 202021-02-23: 112021-02-24: 62021-02-25: 232021-02-26: 32021-02-27: 42021-02-28: 72021-03-01: 112021-03-02: 222021-03-03: 62021-03-04: 152021-03-05: 122021-03-06: 142021-03-07: 62021-03-08: 82021-03-09: 62021-03-10: 22021-03-11: 42021-03-12: 32021-03-13: 132021-03-14: 62021-03-15: 102021-03-16: 72021-03-17: 22021-03-18: 172021-03-19: 42021-03-20: 32021-03-21: 32021-03-22: 322021-03-23: 72021-03-24: 72021-03-25: 82021-03-26: 62021-03-27: 52021-03-28: 12021-03-29: 42021-03-30: 142021-03-31: 92021-04-01: 52021-04-02: 22021-04-03: 152021-04-04: 112021-04-05: 62021-04-06: 12021-04-07: 12021-04-08: 42021-04-09: 12021-04-10: 22021-04-11: 12021-04-12: 22021-04-13: 32021-04-14: 42021-04-15: 22021-04-16: 22021-04-17: 52021-04-18: 52021-04-19: 52021-04-20: 22021-04-21: 12021-04-22: 22021-04-23: 62021-04-24: 42021-04-25: 22021-04-26: 42021-04-27: 52021-04-28: 42021-04-29: 142021-04-30: 142021-05-01: 112021-05-02: 62021-05-03: 32021-05-04: 12021-05-05: 42021-05-06: 22021-05-07: 62021-05-08: 62021-05-09: 62021-05-10: 32021-05-11: 182021-05-12: 92021-05-13: 122021-05-14: 122021-05-15: 152021-05-16: 62021-05-17: 72021-05-18: 52021-05-19: 42021-05-20: 62021-05-21: 62021-05-22: 242021-05-23: 212021-05-24: 202021-05-25: 202021-05-26: 112021-05-27: 92021-05-28: 152021-05-29: 202021-05-30: 112021-05-31: 52021-06-01: 72021-06-02: 52021-06-03: 12021-06-04: 22021-06-05: 82021-06-06: 172021-06-07: 172021-06-08: 42021-06-09: 202021-06-10: 92021-06-11: 102021-06-12: 192021-06-13: 132021-06-14: 62021-06-15: 62021-06-16: 62021-06-17: 42021-06-18: 42021-06-19: 92021-06-20: 162021-06-21: 52021-06-22: 52021-06-23: 142021-06-24: 22021-06-25: 182021-06-26: 92021-06-27: 22021-06-28: 132021-06-29: 162021-06-30: 72021-07-01: 112021-07-02: 72021-07-03: 232021-07-04: 72021-07-05: 142021-07-06: 142021-07-07: 162021-07-08: 172021-07-09: 202021-07-10: 42021-07-11: 62021-07-12: 222021-07-13: 42021-07-14: 9Less: 0: 0: 0: 0: 0More -------------------------------------------------------------------------------- /resources/tests/to_svg_bg_font_frame_scheme.svg: -------------------------------------------------------------------------------- 1 | 2309 contributions in the last year2020-07-12: 52020-07-13: 22020-07-14: 22020-07-15: 32020-07-16: 02020-07-17: 92020-07-18: 02020-07-19: 12020-07-20: 12020-07-21: 42020-07-22: 42020-07-23: 132020-07-24: 102020-07-25: 52020-07-26: 132020-07-27: 42020-07-28: 02020-07-29: 82020-07-30: 62020-07-31: 22020-08-01: 72020-08-02: 52020-08-03: 02020-08-04: 52020-08-05: 52020-08-06: 12020-08-07: 32020-08-08: 32020-08-09: 22020-08-10: 12020-08-11: 02020-08-12: 12020-08-13: 02020-08-14: 32020-08-15: 32020-08-16: 42020-08-17: 52020-08-18: 72020-08-19: 42020-08-20: 02020-08-21: 22020-08-22: 102020-08-23: 52020-08-24: 72020-08-25: 82020-08-26: 22020-08-27: 52020-08-28: 32020-08-29: 42020-08-30: 92020-08-31: 12020-09-01: 32020-09-02: 52020-09-03: 12020-09-04: 12020-09-05: 52020-09-06: 12020-09-07: 12020-09-08: 32020-09-09: 22020-09-10: 42020-09-11: 32020-09-12: 42020-09-13: 22020-09-14: 32020-09-15: 32020-09-16: 42020-09-17: 32020-09-18: 12020-09-19: 32020-09-20: 32020-09-21: 92020-09-22: 22020-09-23: 12020-09-24: 12020-09-25: 92020-09-26: 22020-09-27: 32020-09-28: 12020-09-29: 22020-09-30: 52020-10-01: 62020-10-02: 52020-10-03: 42020-10-04: 32020-10-05: 52020-10-06: 52020-10-07: 22020-10-08: 32020-10-09: 62020-10-10: 12020-10-11: 82020-10-12: 82020-10-13: 12020-10-14: 32020-10-15: 22020-10-16: 12020-10-17: 62020-10-18: 32020-10-19: 32020-10-20: 12020-10-21: 12020-10-22: 12020-10-23: 12020-10-24: 42020-10-25: 102020-10-26: 12020-10-27: 42020-10-28: 42020-10-29: 12020-10-30: 12020-10-31: 12020-11-01: 12020-11-02: 42020-11-03: 12020-11-04: 12020-11-05: 22020-11-06: 32020-11-07: 42020-11-08: 42020-11-09: 52020-11-10: 32020-11-11: 12020-11-12: 102020-11-13: 12020-11-14: 22020-11-15: 12020-11-16: 32020-11-17: 12020-11-18: 12020-11-19: 12020-11-20: 12020-11-21: 22020-11-22: 12020-11-23: 12020-11-24: 12020-11-25: 12020-11-26: 12020-11-27: 12020-11-28: 32020-11-29: 62020-11-30: 12020-12-01: 22020-12-02: 12020-12-03: 12020-12-04: 12020-12-05: 12020-12-06: 22020-12-07: 12020-12-08: 12020-12-09: 22020-12-10: 22020-12-11: 22020-12-12: 22020-12-13: 42020-12-14: 12020-12-15: 12020-12-16: 22020-12-17: 22020-12-18: 12020-12-19: 12020-12-20: 22020-12-21: 62020-12-22: 72020-12-23: 42020-12-24: 112020-12-25: 92020-12-26: 102020-12-27: 102020-12-28: 72020-12-29: 52020-12-30: 102020-12-31: 42021-01-01: 72021-01-02: 72021-01-03: 102021-01-04: 122021-01-05: 92021-01-06: 52021-01-07: 182021-01-08: 232021-01-09: 282021-01-10: 102021-01-11: 52021-01-12: 42021-01-13: 62021-01-14: 42021-01-15: 62021-01-16: 62021-01-17: 92021-01-18: 72021-01-19: 102021-01-20: 42021-01-21: 152021-01-22: 132021-01-23: 122021-01-24: 82021-01-25: 52021-01-26: 22021-01-27: 22021-01-28: 42021-01-29: 62021-01-30: 82021-01-31: 62021-02-01: 52021-02-02: 142021-02-03: 62021-02-04: 122021-02-05: 12021-02-06: 172021-02-07: 132021-02-08: 192021-02-09: 122021-02-10: 182021-02-11: 242021-02-12: 142021-02-13: 72021-02-14: 42021-02-15: 22021-02-16: 182021-02-17: 162021-02-18: 42021-02-19: 62021-02-20: 62021-02-21: 72021-02-22: 202021-02-23: 112021-02-24: 62021-02-25: 232021-02-26: 32021-02-27: 42021-02-28: 72021-03-01: 112021-03-02: 222021-03-03: 62021-03-04: 152021-03-05: 122021-03-06: 142021-03-07: 62021-03-08: 82021-03-09: 62021-03-10: 22021-03-11: 42021-03-12: 32021-03-13: 132021-03-14: 62021-03-15: 102021-03-16: 72021-03-17: 22021-03-18: 172021-03-19: 42021-03-20: 32021-03-21: 32021-03-22: 322021-03-23: 72021-03-24: 72021-03-25: 82021-03-26: 62021-03-27: 52021-03-28: 12021-03-29: 42021-03-30: 142021-03-31: 92021-04-01: 52021-04-02: 22021-04-03: 152021-04-04: 112021-04-05: 62021-04-06: 12021-04-07: 12021-04-08: 42021-04-09: 12021-04-10: 22021-04-11: 12021-04-12: 22021-04-13: 32021-04-14: 42021-04-15: 22021-04-16: 22021-04-17: 52021-04-18: 52021-04-19: 52021-04-20: 22021-04-21: 12021-04-22: 22021-04-23: 62021-04-24: 42021-04-25: 22021-04-26: 42021-04-27: 52021-04-28: 42021-04-29: 142021-04-30: 142021-05-01: 112021-05-02: 62021-05-03: 32021-05-04: 12021-05-05: 42021-05-06: 22021-05-07: 62021-05-08: 62021-05-09: 62021-05-10: 32021-05-11: 182021-05-12: 92021-05-13: 122021-05-14: 122021-05-15: 152021-05-16: 62021-05-17: 72021-05-18: 52021-05-19: 42021-05-20: 62021-05-21: 62021-05-22: 242021-05-23: 212021-05-24: 202021-05-25: 202021-05-26: 112021-05-27: 92021-05-28: 152021-05-29: 202021-05-30: 112021-05-31: 52021-06-01: 72021-06-02: 52021-06-03: 12021-06-04: 22021-06-05: 82021-06-06: 172021-06-07: 172021-06-08: 42021-06-09: 202021-06-10: 92021-06-11: 102021-06-12: 192021-06-13: 132021-06-14: 62021-06-15: 62021-06-16: 62021-06-17: 42021-06-18: 42021-06-19: 92021-06-20: 162021-06-21: 52021-06-22: 52021-06-23: 142021-06-24: 22021-06-25: 182021-06-26: 92021-06-27: 22021-06-28: 132021-06-29: 162021-06-30: 72021-07-01: 112021-07-02: 72021-07-03: 232021-07-04: 72021-07-05: 142021-07-06: 142021-07-07: 162021-07-08: 172021-07-09: 202021-07-10: 42021-07-11: 62021-07-12: 222021-07-13: 42021-07-14: 9Less: 0: 0: 0: 0: 0More -------------------------------------------------------------------------------- /resources/tests/to_term_github.text: -------------------------------------------------------------------------------- 1 | 2309 contributions in the last year 2 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 3 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 4 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 5 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 6 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 7 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 8 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 9 | Less ■■■■■ More 10 | -------------------------------------------------------------------------------- /resources/tests/to_term_invert.text: -------------------------------------------------------------------------------- 1 | 2309 contributions in the last year 2 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 3 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 4 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 5 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 6 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 7 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 8 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 9 | Less ■■■■■ More 10 | -------------------------------------------------------------------------------- /resources/tests/to_term_no_legend.text: -------------------------------------------------------------------------------- 1 | 2309 contributions in the last year 2 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 3 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 4 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 5 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 6 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 7 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 8 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 9 | -------------------------------------------------------------------------------- /resources/tests/to_term_no_total.text: -------------------------------------------------------------------------------- 1 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 2 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 3 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 4 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 5 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 6 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 7 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 8 | Less ■■■■■ More 9 | -------------------------------------------------------------------------------- /resources/tests/to_term_pixel_x.text: -------------------------------------------------------------------------------- 1 | 2309 contributions in the last year 2 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 3 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 4 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 5 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 6 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 7 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 8 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 9 | Less xxxxx More 10 | -------------------------------------------------------------------------------- /resources/tests/to_term_unicorn.text: -------------------------------------------------------------------------------- 1 | 2309 contributions in the last year 2 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 3 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 4 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 5 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 6 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 7 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 8 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 9 | Less ■■■■■ More 10 | -------------------------------------------------------------------------------- /resources/tests/to_text.text: -------------------------------------------------------------------------------- 1 | 2309 contributions in the last year 2 | 5, 1,13, 5, 2, 4, 5, 9, 1, 2, 3, 3, 3, 8, 3,10, 1, 4, 1, 1, 6, 2, 4, 2,10,10,10, 9, 8, 6,13, 4, 7, 7, 6, 6, 3, 1,11, 1, 5, 2, 6, 6, 6,21,11,17,13,16, 2, 7, 6 3 | 2, 1, 4, 0, 1, 5, 7, 1, 1, 3, 9, 1, 5, 8, 3, 1, 4, 5, 3, 1, 1, 1, 1, 6, 7,12, 5, 7, 5, 5,19, 2,20,11, 8,10,32, 4, 6, 2, 5, 4, 3, 3, 7,20, 5,17, 6, 5,13,14,22 4 | 2, 4, 0, 5, 0, 7, 8, 3, 3, 3, 2, 2, 5, 1, 1, 4, 1, 3, 1, 1, 2, 1, 1, 7, 5, 9, 4,10, 2,14,12,18,11,22, 6, 7, 7,14, 1, 3, 2, 5, 1,18, 5,20, 7, 4, 6, 5,16,14, 4 5 | 3, 4, 8, 5, 1, 4, 2, 5, 2, 4, 1, 5, 2, 3, 1, 4, 1, 1, 1, 1, 1, 2, 2, 4,10, 5, 6, 4, 2, 6,18,16, 6, 6, 2, 2, 7, 9, 1, 4, 1, 4, 4, 9, 4,11, 5,20, 6,14, 7,16, 9 6 | 0,13, 6, 1, 0, 0, 5, 1, 4, 3, 1, 6, 3, 2, 1, 1, 2,10, 1, 1, 1, 2, 2,11, 4,18, 4,15, 4,12,24, 4,23,15, 4,17, 8, 5, 4, 2, 2,14, 2,12, 6, 9, 1, 9, 4, 2,11,17, 7 | 9,10, 2, 3, 3, 2, 3, 1, 3, 1, 9, 5, 6, 1, 1, 1, 3, 1, 1, 1, 1, 2, 1, 9, 7,23, 6,13, 6, 1,14, 6, 3,12, 3, 4, 6, 2, 1, 2, 6,14, 6,12, 6,15, 2,10, 4,18, 7,20, 8 | 0, 5, 7, 3, 3,10, 4, 5, 4, 3, 2, 4, 1, 6, 4, 1, 4, 2, 2, 3, 1, 2, 1,10, 7,28, 6,12, 8,17, 7, 6, 4,14,13, 3, 5,15, 2, 5, 4,11, 6,15,24,20, 8,19, 9, 9,23, 4, 9 | -------------------------------------------------------------------------------- /resources/tests/to_text_no_total.text: -------------------------------------------------------------------------------- 1 | 5, 1,13, 5, 2, 4, 5, 9, 1, 2, 3, 3, 3, 8, 3,10, 1, 4, 1, 1, 6, 2, 4, 2,10,10,10, 9, 8, 6,13, 4, 7, 7, 6, 6, 3, 1,11, 1, 5, 2, 6, 6, 6,21,11,17,13,16, 2, 7, 6 2 | 2, 1, 4, 0, 1, 5, 7, 1, 1, 3, 9, 1, 5, 8, 3, 1, 4, 5, 3, 1, 1, 1, 1, 6, 7,12, 5, 7, 5, 5,19, 2,20,11, 8,10,32, 4, 6, 2, 5, 4, 3, 3, 7,20, 5,17, 6, 5,13,14,22 3 | 2, 4, 0, 5, 0, 7, 8, 3, 3, 3, 2, 2, 5, 1, 1, 4, 1, 3, 1, 1, 2, 1, 1, 7, 5, 9, 4,10, 2,14,12,18,11,22, 6, 7, 7,14, 1, 3, 2, 5, 1,18, 5,20, 7, 4, 6, 5,16,14, 4 4 | 3, 4, 8, 5, 1, 4, 2, 5, 2, 4, 1, 5, 2, 3, 1, 4, 1, 1, 1, 1, 1, 2, 2, 4,10, 5, 6, 4, 2, 6,18,16, 6, 6, 2, 2, 7, 9, 1, 4, 1, 4, 4, 9, 4,11, 5,20, 6,14, 7,16, 9 5 | 0,13, 6, 1, 0, 0, 5, 1, 4, 3, 1, 6, 3, 2, 1, 1, 2,10, 1, 1, 1, 2, 2,11, 4,18, 4,15, 4,12,24, 4,23,15, 4,17, 8, 5, 4, 2, 2,14, 2,12, 6, 9, 1, 9, 4, 2,11,17, 6 | 9,10, 2, 3, 3, 2, 3, 1, 3, 1, 9, 5, 6, 1, 1, 1, 3, 1, 1, 1, 1, 2, 1, 9, 7,23, 6,13, 6, 1,14, 6, 3,12, 3, 4, 6, 2, 1, 2, 6,14, 6,12, 6,15, 2,10, 4,18, 7,20, 7 | 0, 5, 7, 3, 3,10, 4, 5, 4, 3, 2, 4, 1, 6, 4, 1, 4, 2, 2, 3, 1, 2, 1,10, 7,28, 6,12, 8,17, 7, 6, 4,14,13, 3, 5,15, 2, 5, 4,11, 6,15,24,20, 8,19, 9, 9,23, 4, 8 | -------------------------------------------------------------------------------- /resources/tweet.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kawarimidoll/deno-github-contributions-api/6898dfb4baac1a6ef428519aaa92a3269bab799f/resources/tweet.webp -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import { getContributions, totalMsg } from "./contributions.ts"; 2 | import { outdent } from "./deps.ts"; 3 | 4 | // cache one hour 5 | const CACHE_MAX_AGE = 3600; 6 | 7 | function getPathExtension(request: Request): string { 8 | const { pathname } = new URL(request.url); 9 | const split = pathname.split("."); 10 | return split.length > 1 ? split[split.length - 1] : ""; 11 | } 12 | 13 | async function handleRequest(request: Request) { 14 | const { pathname, searchParams, host } = new URL(request.url); 15 | 16 | if (pathname === "/") { 17 | return [ 18 | "Welcome to deno-github-contributions-api!", 19 | `Access to ${host}/[username] to get your contributions data.`, 20 | ].join("\n"); 21 | } 22 | 23 | const paths = pathname.split("/"); 24 | if (paths.length > 2) { 25 | throw new Error( 26 | `'${request.url}' is invalid path. Access to ${host}/[username].`, 27 | ); 28 | } 29 | const username = paths[1].replace(/\..*$/, ""); 30 | const ext = getPathExtension(request); 31 | 32 | const toYmd = searchParams.get("to") ?? null; 33 | const fromYmd = searchParams.get("from") ?? null; 34 | const from = fromYmd ? new Date(fromYmd).toISOString() : undefined; 35 | const to = toYmd ? new Date(toYmd).toISOString() : undefined; 36 | 37 | const contributions = await getContributions( 38 | username, 39 | Deno.env.get("GH_READ_USER_TOKEN") || "", 40 | { from, to }, 41 | ); 42 | 43 | const scheme = searchParams.get("scheme") ?? "github"; 44 | const pixel = searchParams.get("pixel") ?? undefined; 45 | const noTotal = searchParams.get("no-total") == "true"; 46 | const noLegend = searchParams.get("no-legend") == "true"; 47 | const flat = searchParams.get("flat") == "true"; 48 | const invert = searchParams.get("invert") == "true"; 49 | const fontColor = searchParams.get("font-color") ?? "#000"; 50 | const frame = searchParams.get("frame") ?? "none"; 51 | const bg = searchParams.get("bg") ?? "none"; 52 | 53 | if (ext === "json") { 54 | return contributions.toJson({ flat }); 55 | } 56 | 57 | if (ext === "term") { 58 | return contributions.toTerm({ scheme, pixel, noTotal, noLegend, invert }); 59 | } 60 | 61 | if (ext === "text") { 62 | return contributions.toText({ noTotal }); 63 | } 64 | 65 | if (ext === "svg") { 66 | return contributions.toSvg({ 67 | scheme, 68 | noTotal, 69 | noLegend, 70 | frame, 71 | bg, 72 | fontColor, 73 | }); 74 | } 75 | 76 | return outdent` 77 | ${totalMsg(contributions.totalContributions)}. 78 | 79 | Use extensions like as '${host}/${username}.text'. 80 | - .json : return data as a json 81 | - .term : return data as a colored pixels graph (works in the terminal with true color) 82 | - .text : return data as a table-styled text 83 | - .svg : return data as a svg image 84 | 85 | You can use other parameters. Each of them works on specific extensions. 86 | - no-total=true : remove total contributions count (term/text/svg) 87 | - no-legend=true : remove legend (term/svg) 88 | - invert=true : change the background colors instead of the foreground colors (term) 89 | - flat=true : return contributions as one-dimensional array (json) 90 | - scheme=[name] : use specific color scheme (term/svg) 91 | - pixel=[char] : use the character as pixels, URL encoding is required (term) 92 | - frame=[color] : use the color as a frame of image (svg) 93 | - bg=[color] : use the color as a background of image (svg) 94 | - font-color=[color] : use the color as a font color (svg) 95 | - from=[yyyy-mm-dd] : get contributions from the date (term/text/svg/json) 96 | - to=[yyyy-mm-dd] : get contributions to the date (term/text/svg/json) 97 | 98 | Color parameters allows hex color string without # like '123abc'. 99 | `; 100 | } 101 | 102 | Deno.serve(async (request: Request) => { 103 | const ext = getPathExtension(request); 104 | const type = { 105 | json: "application/json", 106 | svg: "image/svg+xml", 107 | }[ext] || "text/plain"; 108 | const headers = { 109 | "Content-Type": `${type}; charset=utf-8`, 110 | "Cache-Control": `public, max-age=${CACHE_MAX_AGE}`, 111 | "Access-Control-Allow-Origin": "*", 112 | }; 113 | 114 | try { 115 | const body = await handleRequest(request); 116 | return (new Response(body, { headers })); 117 | } catch (error) { 118 | console.error(error); 119 | 120 | const body = ext == "json" 121 | ? JSON.stringify({ error: `${error}` }) 122 | : `${error}`; 123 | return (new Response(body, { 124 | status: 400, 125 | headers, 126 | })); 127 | } 128 | }); 129 | -------------------------------------------------------------------------------- /utils.ts: -------------------------------------------------------------------------------- 1 | const defaultPixelColor = "eee"; 2 | 3 | const confirmHex = (str: string, defaultColor = defaultPixelColor) => 4 | /^#?([0-9a-f]{3}){1,2}$/i.test(str) ? str : defaultColor; 5 | 6 | const convertToSixChars = (str: string, defaultColor = defaultPixelColor) => 7 | confirmHex(str, defaultColor).replace( 8 | /^#?(.*)$/, 9 | (_, hex) => (hex.length == 3) ? hex.replace(/./g, "$&$&") : hex, 10 | ); 11 | 12 | const hexStrToRgbObj = (color: string, defaultColor = defaultPixelColor) => 13 | Object.fromEntries( 14 | (convertToSixChars(color || defaultColor).match(/../g) ?? []).map(( 15 | c, 16 | i, 17 | ) => ["rgb".charAt(i), parseInt("0x" + c)]), 18 | ); 19 | 20 | const hexStrToHexNum = (color: string, defaultColor = defaultPixelColor) => 21 | parseInt("0x" + convertToSixChars(color || defaultColor)); 22 | 23 | export { confirmHex, convertToSixChars, hexStrToHexNum, hexStrToRgbObj }; 24 | -------------------------------------------------------------------------------- /utils_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "./deps.ts"; 2 | import { 3 | confirmHex, 4 | convertToSixChars, 5 | hexStrToHexNum, 6 | hexStrToRgbObj, 7 | } from "./utils.ts"; 8 | 9 | Deno.test("confirmHex", () => { 10 | assertEquals(confirmHex("#123456"), "#123456"); 11 | assertEquals(confirmHex("123456"), "123456"); 12 | assertEquals(confirmHex("#12A"), "#12A"); 13 | assertEquals(confirmHex("12A"), "12A"); 14 | assertEquals(confirmHex("#12345"), "eee"); 15 | assertEquals(confirmHex("12345"), "eee"); 16 | assertEquals(confirmHex("#12"), "eee"); 17 | assertEquals(confirmHex("12"), "eee"); 18 | assertEquals(confirmHex("hex"), "eee"); 19 | }); 20 | 21 | Deno.test("convertToSixChars", () => { 22 | assertEquals(convertToSixChars("#123456"), "123456"); 23 | assertEquals(convertToSixChars("123456"), "123456"); 24 | assertEquals(convertToSixChars("#12A"), "1122AA"); 25 | assertEquals(convertToSixChars("12A"), "1122AA"); 26 | assertEquals(convertToSixChars("12"), "eeeeee"); 27 | assertEquals(convertToSixChars("hex"), "eeeeee"); 28 | }); 29 | 30 | Deno.test("hexStrToRgbObj", () => { 31 | assertEquals(hexStrToRgbObj("#123456"), { r: 18, g: 52, b: 86 }); 32 | assertEquals(hexStrToRgbObj("123456"), { r: 18, g: 52, b: 86 }); 33 | assertEquals(hexStrToRgbObj("#12A"), { r: 17, g: 34, b: 170 }); 34 | assertEquals(hexStrToRgbObj("12A"), { r: 17, g: 34, b: 170 }); 35 | assertEquals(hexStrToRgbObj("12"), { r: 238, g: 238, b: 238 }); 36 | assertEquals(hexStrToRgbObj("hex"), { r: 238, g: 238, b: 238 }); 37 | }); 38 | 39 | Deno.test("hexStrToHexNum", () => { 40 | assertEquals(hexStrToHexNum("#123456"), 0x123456); 41 | assertEquals(hexStrToHexNum("123456"), 0x123456); 42 | assertEquals(hexStrToHexNum("#12A"), 0x1122aa); 43 | assertEquals(hexStrToHexNum("12A"), 0x1122aa); 44 | }); 45 | -------------------------------------------------------------------------------- /velociraptor.yml: -------------------------------------------------------------------------------- 1 | allow: 2 | # - write 3 | - read 4 | - env=GH_READ_USER_TOKEN 5 | - net 6 | 7 | scripts: 8 | main: 9 | desc: Runs main script 10 | cmd: main.ts 11 | 12 | server: 13 | desc: Starts local server 14 | watch: true 15 | cmd: server.ts 16 | 17 | health_check: 18 | desc: Runs health_check script 19 | cmd: health_check.ts 20 | 21 | deps: 22 | desc: Update dependencies with ensuring pass tests 23 | cmd: udd deps.ts --test="vr test" 24 | 25 | lint: 26 | desc: Runs lint 27 | cmd: deno lint --ignore=cov_profile,resources 28 | 29 | fmt: 30 | desc: Runs format 31 | cmd: deno fmt --ignore=cov_profile,resources 32 | 33 | pre-commit: 34 | cmd: | 35 | FILES=$(git diff --staged --name-only --diff-filter=ACMR "*.ts") 36 | [ -z "$FILES" ] && exit 0 37 | echo "$FILES" | xargs deno lint 38 | echo "$FILES" | xargs deno fmt 39 | # echo "$FILES" | xargs git add 40 | desc: Lints and formats staged files 41 | gitHook: pre-commit 42 | 43 | test: 44 | # allow: 45 | # - read 46 | desc: Runs the tests 47 | cmd: deno test --reload 48 | gitHook: pre-push 49 | 50 | cov: 51 | desc: Shows uncovered lists 52 | cmd: 53 | - vr test --coverage=cov_profile 54 | - deno coverage cov_profile 55 | 56 | ci: 57 | desc: Runs lint, check format and test 58 | cmd: 59 | - vr lint 60 | - vr fmt --check 61 | - vr test 62 | 63 | commitlint: 64 | # dependencies: commitlint and @commitlint/config-conventional 65 | # yarn global add commitlint @commitlint/config-conventional 66 | desc: Checks commit messages format with commitlint 67 | cmd: commitlint -x @commitlint/config-conventional -e ${GIT_ARGS[1]} 68 | gitHook: commit-msg 69 | --------------------------------------------------------------------------------