Compare commits

..

5 Commits

Author SHA1 Message Date
platane
fd9d7dadf6 🚀 smarter snake 2020-07-21 00:34:22 +02:00
platane
73bfce908e 🔨 fix demo 2020-07-20 23:02:23 +02:00
platane
dd23c1630e 🚿 clean up 2020-07-20 22:37:58 +02:00
platane
7377068a9a 📓 add readme 2020-07-20 10:23:32 +02:00
platane
8a06b668cd 🚀 fix action 2020-07-20 10:18:24 +02:00
52 changed files with 331 additions and 174480 deletions

View File

@@ -11,17 +11,17 @@ jobs:
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
- uses: actions/setup-node@v1.4.2
with:
node-version: 14
- uses: bahmutov/npm-install@v1
- uses: bahmutov/npm-install@v1.4.1
- run: yarn build:demo
env:
BASE_PATHNAME: "snk"
- uses: crazy-max/ghaction-github-pages@068e494
- uses: crazy-max/ghaction-github-pages@v2.1.1
with:
target_branch: gh-pages
build_dir: packages/demo/dist

View File

@@ -7,23 +7,40 @@ jobs:
runs-on: ubuntu-latest
steps:
# - run: sudo apt-get install gifsicle graphicsmagick
- run: sudo apt-get install gifsicle graphicsmagick
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
- uses: actions/setup-node@v1.4.2
with:
node-version: 14
- uses: bahmutov/npm-install@v1
- uses: bahmutov/npm-install@v1.4.1
# - run: yarn type
# - run: yarn lint
# - run: yarn test --ci
# - run: yarn build:lib
- run: yarn type
- run: yarn lint
- run: yarn test --ci
- run: yarn build:action
test-action:
runs-on: ubuntu-latest
steps:
- run: mkdir dist
- name: generate-snake-game-from-github-contribution-grid
id: snake-gif
uses: Platane/snk@master
with:
github_user_name: platane
gif_out_path: dist/github-contribution-grid-snake.gif
- run: ls
- run: ls
- name: ensure the generated file exists
run: |
ls -l ${{ steps.snake-gif.outputs.gif_out_path }}
test -f ${{ steps.snake-gif.outputs.gif_out_path }}
- uses: crazy-max/ghaction-github-pages@v2.1.1
with:
target_branch: output
build_dir: dist
env:
GITHUB_TOKEN: ${{ secrets.MY_GITHUB_TOKEN_GH_PAGES }}

3
.gitignore vendored
View File

@@ -2,4 +2,5 @@ node_modules
npm-debug.log*
yarn-error.log*
dist
build
build
out.gif

View File

@@ -4,12 +4,16 @@ RUN apt-get update \
&& apt-get install -y --no-install-recommends gifsicle graphicsmagick \
&& rm -rf /var/lib/apt/lists/*
COPY tsconfig.json package.json yarn.lock ./generate-snake-game-from-github-contribution-grid/
COPY packages ./generate-snake-game-from-github-contribution-grid/packages/
COPY tsconfig.json package.json yarn.lock /github/snk/
COPY packages /github/snk/packages
RUN ( cd ./generate-snake-game-from-github-contribution-grid ; yarn install --frozen-lockfile )
RUN ( cd ./generate-snake-game-from-github-contribution-grid ; yarn build:action )
CMD ["node", "generate-snake-game-from-github-contribution-grid/packages/action/dist/index.js"]
RUN ( \
cd /github/snk \
&& find . \
&& yarn install --frozen-lockfile \
&& yarn build:action \
&& mv packages/action/dist/* . \
&& rm -rf packages tsconfig.json package.json yarn.lock node_modules \
)
CMD ["node", "/github/snk/index.js"]

View File

@@ -1,3 +1,12 @@
# snk
![type definitions](https://img.shields.io/npm/types/typescript?style=flat-square)
![code style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)
![](https://raw.githubusercontent.com/Platane/snk/output/github-contribution-grid-snake.gif)
Generates a snake game from a github user contributions grid and output a screen capture as gif
- [demo](https://platane.github.io/snk/index.html)
- [github action](https://github.com/marketplace/actions/generate-snake-game-from-github-contribution-grid)

View File

@@ -9,9 +9,6 @@ outputs:
runs:
using: "docker"
image: "Dockerfile"
args:
- ${{ inputs.github_user_name }}
- ${{ inputs.gif_out_path }}
inputs:
github_user_name:

View File

@@ -1,3 +0,0 @@
!dist
!dist/build
out.gif

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -1,84 +0,0 @@
const Canvas = require('./lib/canvas')
const Image = require('./lib/image')
const CanvasRenderingContext2D = require('./lib/context2d')
const parseFont = require('./lib/parse-font')
const packageJson = require('./package.json')
const bindings = require('./lib/bindings')
const fs = require('fs')
const PNGStream = require('./lib/pngstream')
const PDFStream = require('./lib/pdfstream')
const JPEGStream = require('./lib/jpegstream')
const DOMMatrix = require('./lib/DOMMatrix').DOMMatrix
const DOMPoint = require('./lib/DOMMatrix').DOMPoint
function createCanvas (width, height, type) {
return new Canvas(width, height, type)
}
function createImageData (array, width, height) {
return new bindings.ImageData(array, width, height)
}
function loadImage (src) {
return new Promise((resolve, reject) => {
const image = new Image()
function cleanup () {
image.onload = null
image.onerror = null
}
image.onload = () => { cleanup(); resolve(image) }
image.onerror = (err) => { cleanup(); reject(err) }
image.src = src
})
}
/**
* Resolve paths for registerFont. Must be called *before* creating a Canvas
* instance.
* @param src {string} Path to font file.
* @param fontFace {{family: string, weight?: string, style?: string}} Object
* specifying font information. `weight` and `style` default to `"normal"`.
*/
function registerFont (src, fontFace) {
// TODO this doesn't need to be on Canvas; it should just be a static method
// of `bindings`.
return Canvas._registerFont(fs.realpathSync(src), fontFace)
}
module.exports = {
Canvas,
Context2d: CanvasRenderingContext2D, // Legacy/compat export
CanvasRenderingContext2D,
CanvasGradient: bindings.CanvasGradient,
CanvasPattern: bindings.CanvasPattern,
Image,
ImageData: bindings.ImageData,
PNGStream,
PDFStream,
JPEGStream,
DOMMatrix,
DOMPoint,
registerFont,
parseFont,
createCanvas,
createImageData,
loadImage,
backends: bindings.Backends,
/** Library version. */
version: packageJson.version,
/** Cairo version. */
cairoVersion: bindings.cairoVersion,
/** jpeglib version. */
jpegVersion: bindings.jpegVersion,
/** gif_lib version. */
gifVersion: bindings.gifVersion ? bindings.gifVersion.replace(/[^.\d]/g, '') : undefined,
/** freetype version. */
freetypeVersion: bindings.freetypeVersion
}

View File

@@ -1,60 +0,0 @@
"use strict";
/* eslint-disable no-process-exit */
const util = require("util");
const { JSDOM } = require("../../../..");
const { READY_STATES } = require("./xhr-utils");
const idlUtils = require("../generated/utils");
const tough = require("tough-cookie");
const dom = new JSDOM();
const xhr = new dom.window.XMLHttpRequest();
const xhrImpl = idlUtils.implForWrapper(xhr);
const chunks = [];
process.stdin.on("data", chunk => {
chunks.push(chunk);
});
process.stdin.on("end", () => {
const buffer = Buffer.concat(chunks);
const flag = JSON.parse(buffer.toString());
if (flag.body && flag.body.type === "Buffer" && flag.body.data) {
flag.body = Buffer.from(flag.body.data);
}
if (flag.cookieJar) {
flag.cookieJar = tough.CookieJar.fromJSON(flag.cookieJar);
}
flag.synchronous = false;
Object.assign(xhrImpl.flag, flag);
const { properties } = xhrImpl;
xhrImpl.readyState = READY_STATES.OPENED;
try {
xhr.addEventListener("loadend", () => {
if (properties.error) {
properties.error = properties.error.stack || util.inspect(properties.error);
}
process.stdout.write(JSON.stringify({
responseURL: xhrImpl.responseURL,
status: xhrImpl.status,
statusText: xhrImpl.statusText,
properties
}), () => {
process.exit(0);
});
}, false);
xhr.send(flag.body);
} catch (error) {
properties.error += error.stack || util.inspect(error);
process.stdout.write(JSON.stringify({
responseURL: xhrImpl.responseURL,
status: xhrImpl.status,
statusText: xhrImpl.statusText,
properties
}), () => {
process.exit(0);
});
}
});

View File

@@ -37,7 +37,10 @@ export const generateContributionSnake = async (userName: string) => {
colorSnake: "purple",
};
const gameOptions = { maxSnakeLength: 5 };
const gameOptions = {
maxSnakeLength: 5,
colors: Array.from({ length: colorScheme.length - 1 }, (_, i) => i + 1),
};
const gifOptions = { delay: 10 };

View File

@@ -4,23 +4,14 @@ import { generateContributionSnake } from "./generateContributionSnake";
(async () => {
try {
console.log("argv", process.argv);
const userName = core.getInput("github_user_name");
const gifOutPath = core.getInput("gif_out_path");
console.log(core.getInput("user_name"));
console.log(core.getInput("gif_out_path"));
console.log("--");
console.log("--");
console.log(process.cwd());
console.log("--");
console.log(fs.readdirSync(process.cwd()));
console.log("--");
console.log("--");
console.log(process.env.GITHUB_WORKSPACE);
console.log("--");
console.log(fs.readdirSync(process.cwd()));
const buffer = await generateContributionSnake(userName);
const buffer = await generateContributionSnake(core.getInput("user_name"));
fs.writeFileSync(core.getInput("gif_out_path"), buffer);
fs.writeFileSync(gifOutPath, buffer);
console.log(`::set-output name=gif_out_path::${gifOutPath}`);
} catch (e) {
core.setFailed(`Action failed with "${e.message}"`);
}

View File

@@ -3,9 +3,9 @@ import { Grid, Color } from "./grid";
const rand = (a: number, b: number) => Math.floor(Math.random() * (b - a)) + a;
export const generateEmptyGrid = (width: number, height: number) =>
generateGrid(width, height, { colors: [], emptyP: 1 });
generateRandomGrid(width, height, { colors: [], emptyP: 1 });
export const generateGrid = (
export const generateRandomGrid = (
width: number,
height: number,
options: { colors: Color[]; emptyP: number } = {

View File

@@ -1,44 +1,167 @@
import { Grid, Color, copyGrid, isInsideLarge } from "./grid";
import { Grid, Color, copyGrid, isInsideLarge, getColor } from "./grid";
import { Point, around4 } from "./point";
import { stepSnake, step } from "./step";
import { copySnake, snakeSelfCollide } from "./snake";
import { step } from "./step";
import { copySnake, snakeSelfCollide, Snake } from "./snake";
const isGridEmpty = (grid: Grid) => grid.data.every((x) => x === null);
export const computeBestRun = (
grid: Grid,
snake: Point[],
options: { maxSnakeLength: number }
const createComputeHeuristic = (
grid0: Grid,
_snake0: Snake,
colors: Color[]
) => {
const g = copyGrid(grid);
const s = copySnake(snake);
const q: Color[] = [];
const commands: Point[] = [];
let u = 500;
while (!isGridEmpty(g) && u-- > 0) {
let direction;
for (let k = 10; k--; ) {
direction = around4[Math.floor(Math.random() * around4.length)];
const sn = copySnake(s);
stepSnake(sn, direction, options);
if (isInsideLarge(g, 1, sn[0].x, sn[0].y) && !snakeSelfCollide(sn)) {
break;
} else {
direction = undefined;
}
const colorCount: Record<Color, number> = {};
for (let x = grid0.width; x--; )
for (let y = grid0.height; y--; ) {
const c = getColor(grid0, x, y);
if (c !== null) colorCount[c] = 1 + (colorCount[c] || 0);
}
if (direction !== undefined) {
step(g, s, q, direction, options);
commands.push(direction);
const values = colors
.map((k) => Array.from({ length: colorCount[k] }, () => k))
.flat();
return (_grid: Grid, _snake: Snake, stack: Color[]) => {
let score = 0;
for (let i = 0; i < stack.length; i++) {
const k = Math.abs(stack[i] - values[i]);
score += k === 0 ? 100 : -100 * k;
}
return score;
};
};
const computeKey = (grid: Grid, snake: Snake, stack: Color[]) =>
grid.data.map((x) => x || 0).join("") +
"|" +
snake.map((p) => p.x + "." + p.y).join(",") +
"|" +
stack.join("");
const createCell = (
key: string,
grid: Grid,
snake: Snake,
stack: Color[],
direction: Point | null,
parent: any | null,
heuristic: number
) => ({
key,
parent,
direction,
grid,
snake,
stack,
weight: 1 + (parent?.weight || 0),
f: heuristic - 0 * (1 + (parent?.weight || 0)),
});
const unwrap = (c: ReturnType<typeof createCell> | null): Point[] =>
c && c.direction ? [...unwrap(c.parent), c.direction] : [];
// c && c.parent
// ? [
// ...unwrap(c.parent),
// { x: c.snake[1].x - c.snake[0].x, y: c.snake[1].y - c.snake[0].y },
// ]
// : [];
export const computeBestRun = (
grid0: Grid,
snake0: Snake,
options: { maxSnakeLength: number; colors: Color[] }
) => {
// const grid = copyGrid(grid0);
// const snake = copySnake(snake0);
// const stack: Color[] = [];
const computeHeuristic = createComputeHeuristic(
grid0,
snake0,
options.colors
);
const closeList: any = {};
const openList = [
createCell(
computeKey(grid0, snake0, []),
grid0,
snake0,
[],
null,
null,
computeHeuristic(grid0, snake0, [])
),
];
let u = 7000;
let best = openList[0];
while (openList.length && u-- > 0) {
openList.sort((a, b) => b.f - a.f);
const c = openList.shift()!;
closeList[c.key] = true;
if (isGridEmpty(c.grid)) return unwrap(c);
if (c.f > best.f) best = c;
for (const direction of around4) {
const snake = copySnake(c.snake);
const stack = c.stack.slice();
const grid = copyGrid(c.grid);
step(grid, snake, stack, direction, options);
const key = computeKey(grid, snake, stack);
if (
!closeList[key] &&
isInsideLarge(grid, 1, snake[0].x, snake[0].y) &&
!snakeSelfCollide(snake)
) {
openList.push(
createCell(
key,
grid,
snake,
stack,
direction,
c,
computeHeuristic(grid, snake, stack)
)
);
}
}
}
return commands;
return unwrap(best);
// while (!isGridEmpty(g) && u-- > 0) {
// let direction;
// for (let k = 10; k--; ) {
// direction = around4[Math.floor(Math.random() * around4.length)];
// const sn = copySnake(s);
// stepSnake(sn, direction, options);
// if (isInsideLarge(g, 1, sn[0].x, sn[0].y) && !snakeSelfCollide(sn)) {
// break;
// } else {
// direction = undefined;
// }
// }
// if (direction !== undefined) {
// step(g, s, q, direction, options);
// commands.push(direction);
// }
// }
// return commands;
};

View File

@@ -1,7 +1,9 @@
import { Point } from "./point";
export type Snake = Point[];
export const snakeSelfCollideNext = (
snake: Point[],
snake: Snake,
direction: Point,
options: { maxSnakeLength: number }
) => {
@@ -14,11 +16,11 @@ export const snakeSelfCollideNext = (
return false;
};
export const snakeSelfCollide = (snake: Point[]) => {
export const snakeSelfCollide = (snake: Snake) => {
for (let i = 1; i < snake.length; i++)
if (snake[i].x === snake[0].x && snake[i].y === snake[0].y) return true;
return false;
};
export const copySnake = (x: Point[]) => x.map((p) => ({ ...p }));
export const copySnake = (x: Snake) => x.map((p) => ({ ...p }));

View File

@@ -1,81 +1,91 @@
// import { generateGrid } from "@snk/compute/generateGrid";
import { generateGrid } from "@snk/compute/generateGrid";
import { generateRandomGrid } from "@snk/compute/generateGrid";
import { Color, copyGrid } from "@snk/compute/grid";
import { computeBestRun } from "@snk/compute";
import { step } from "@snk/compute/step";
import { drawWorld } from "@snk/draw/drawWorld";
import { Point } from "@snk/compute/point";
import { copySnake } from "@snk/compute/snake";
const copySnake = (x: Point[]) => x.map((p) => ({ ...p }));
export const run = async () => {
const drawOptions = {
sizeBorderRadius: 2,
sizeCell: 16,
sizeDot: 12,
colorBorder: "#1b1f230a",
colorDots: { 1: "#9be9a8", 2: "#40c463", 3: "#30a14e", 4: "#216e39" },
colorEmpty: "#ebedf0",
colorSnake: "purple",
};
const gameOptions = { maxSnakeLength: 5 };
const grid0 = generateGrid(42, 7, { colors: [1, 2, 3, 4], emptyP: 3 });
const snake0 = [
{ x: 4, y: -1 },
{ x: 3, y: -1 },
{ x: 2, y: -1 },
{ x: 1, y: -1 },
{ x: 0, y: -1 },
];
const stack0: Color[] = [];
const chain = computeBestRun(grid0, snake0, gameOptions);
const canvas = document.createElement("canvas");
canvas.width = drawOptions.sizeCell * (grid0.width + 4);
canvas.height = drawOptions.sizeCell * (grid0.height + 4) + 100;
document.body.appendChild(canvas);
const ctx = canvas.getContext("2d")!;
const update = (n: number) => {
const snake = copySnake(snake0);
const stack = stack0.slice();
const grid = copyGrid(grid0);
for (let i = 0; i < n; i++) step(grid, snake, stack, chain[i], gameOptions);
ctx.clearRect(0, 0, 9999, 9999);
drawWorld(ctx, grid, snake, stack, drawOptions);
};
const input: any = document.createElement("input");
input.type = "range";
input.style.width = "100%";
input.min = 0;
input.max = chain.length;
input.step = 1;
input.value = 0;
input.addEventListener("input", () => update(+input.value));
document.addEventListener("click", () => input.focus());
document.body.appendChild(input);
update(+input.value);
// while (chain.length) {
// await wait(100);
// step(grid, snake, stack, chain.shift()!, gameOptions);
// ctx.clearRect(0, 0, 9999, 9999);
// drawWorld(ctx, grid, snake, stack, options);
// }
// const wait = (delay = 0) => new Promise((r) => setTimeout(r, delay));
const drawOptions = {
sizeBorderRadius: 2,
sizeCell: 16,
sizeDot: 12,
colorBorder: "#1b1f230a",
colorDots: { 1: "#9be9a8", 2: "#40c463", 3: "#30a14e", 4: "#216e39" },
colorEmpty: "#ebedf0",
colorSnake: "purple",
};
run();
const gameOptions = { colors: [1, 2, 3, 4], maxSnakeLength: 5 };
const grid0 = generateRandomGrid(42, 7, { ...gameOptions, emptyP: 3 });
const snake0 = [
{ x: 4, y: -1 },
{ x: 3, y: -1 },
{ x: 2, y: -1 },
{ x: 1, y: -1 },
{ x: 0, y: -1 },
];
const stack0: Color[] = [];
const chain = computeBestRun(grid0, snake0, gameOptions);
//
// draw
const canvas = document.createElement("canvas");
canvas.width = drawOptions.sizeCell * (grid0.width + 4);
canvas.height = drawOptions.sizeCell * (grid0.height + 4) + 100;
document.body.appendChild(canvas);
const ctx = canvas.getContext("2d")!;
const update = (n: number) => {
const snake = copySnake(snake0);
const stack = stack0.slice();
const grid = copyGrid(grid0);
for (let i = 0; i < n; i++) step(grid, snake, stack, chain[i], gameOptions);
ctx.clearRect(0, 0, 9999, 9999);
drawWorld(ctx, grid, snake, stack, drawOptions);
};
//
// controls
const input: any = document.createElement("input");
input.type = "range";
input.style.width = "100%";
input.min = 0;
input.max = chain.length;
input.step = 1;
input.value = 0;
input.addEventListener("input", () => {
setAutoPlay(false);
update(+input.value);
});
document.addEventListener("click", () => input.focus());
document.body.appendChild(input);
const autoplayButton = document.createElement("button");
let cancel: any;
const loop = () => {
input.value = (+input.value + 1) % +input.max;
update(+input.value);
cancelAnimationFrame(cancel);
cancel = requestAnimationFrame(loop);
};
const setAutoPlay = (a: boolean) => {
autoplayButton.innerHTML = a ? "pause" : "play";
if (a) loop();
else cancelAnimationFrame(cancel);
};
autoplayButton.addEventListener("click", () => {
debugger;
setAutoPlay(autoplayButton.innerHTML === "play");
});
document.body.appendChild(autoplayButton);
setAutoPlay(true);
update(+input.value);

View File

@@ -2,7 +2,8 @@
"name": "@snk/demo",
"version": "1.0.0",
"dependencies": {
"@snk/compute": "1.0.0"
"@snk/compute": "1.0.0",
"@snk/draw": "1.0.0"
},
"devDependencies": {
"webpack": "4.43.0",

View File

@@ -27,7 +27,6 @@ const config: Configuration = {
],
},
plugins: [
// game
new HtmlWebpackPlugin({
title: "demo",
filename: "index.html",
@@ -39,9 +38,6 @@ const config: Configuration = {
devtool: false,
stats: "errors-only",
// @ts-ignore
devServer: {},
};
export default config;

View File

@@ -45,7 +45,7 @@ export const drawWorld = (
) => {
ctx.save();
ctx.translate(2 * o.sizeCell, 2 * o.sizeCell);
ctx.translate(1 * o.sizeCell, 2 * o.sizeCell);
drawGrid(ctx, grid, o);
drawSnake(ctx, snake, o);

View File

@@ -1 +0,0 @@
out.gif

View File

@@ -1,5 +1,5 @@
import { createGif } from "..";
import { generateGrid } from "@snk/compute/generateGrid";
import { generateRandomGrid } from "@snk/compute/generateGrid";
import { computeBestRun } from "@snk/compute";
const drawOptions = {
@@ -12,12 +12,12 @@ const drawOptions = {
colorSnake: "purple",
};
const gameOptions = { maxSnakeLength: 5 };
const gameOptions = { maxSnakeLength: 5, colors: [1, 2, 3, 4] };
const gifOptions = { delay: 200 };
it("should generate gif", async () => {
const grid = generateGrid(14, 7, { colors: [1, 2, 3, 4], emptyP: 3 });
const grid = generateRandomGrid(14, 7, { ...gameOptions, emptyP: 3 });
const snake = [
{ x: 4, y: -1 },

View File

@@ -1,5 +1,5 @@
import { createGif } from "..";
import { generateGrid } from "@snk/compute/generateGrid";
import { generateRandomGrid } from "@snk/compute/generateGrid";
import { computeBestRun } from "@snk/compute";
const drawOptions = {
@@ -12,11 +12,11 @@ const drawOptions = {
colorSnake: "purple",
};
const gameOptions = { maxSnakeLength: 5 };
const gameOptions = { maxSnakeLength: 5, colors: [1, 2, 3, 4] };
const gifOptions = { delay: 20 };
const grid = generateGrid(42, 7, { colors: [1, 2, 3, 4], emptyP: 3 });
const grid = generateRandomGrid(42, 7, { ...gameOptions, emptyP: 3 });
const snake = [
{ x: 4, y: -1 },

View File

@@ -7,7 +7,6 @@ import { copySnake } from "@snk/compute/snake";
import { drawWorld } from "@snk/draw/drawWorld";
import { step } from "@snk/compute/step";
import * as tmp from "tmp";
// @ts-ignore
import * as execa from "execa";
export const createGif = async (

View File

@@ -3,7 +3,7 @@ import { getGithubUserContribution } from "..";
it("should get user contribution", async () => {
const { cells, colorScheme } = await getGithubUserContribution("platane");
expect(cells).toBeDefined();
expect(cells.length).toBeGreaterThan(300);
expect(colorScheme).toEqual([
"#ebedf0",
"#9be9a8",

View File

@@ -1,26 +1,11 @@
// import * as https from "https";
// @ts-ignore
// import * as cheerio from "cheerio";
import { JSDOM } from "jsdom";
/**
* get the contribution grid from a github user page
*
* @param userName
*/
export const getGithubUserContribution = async (userName: string) => {
// const content: string = await new Promise((resolve, reject) => {
// const req = https.request(`https://github.com/${userName}`, (res) => {
// let data = "";
// res.on("error", reject);
// res.on("data", (chunk) => (data += chunk));
// res.on("end", () => resolve(data));
// });
// req.on("error", reject);
// req.end();
// });
// const dom = new JSDOM(content);
const dom = await JSDOM.fromURL(`https://github.com/${userName}`);
const colorScheme = Array.from(
@@ -34,14 +19,14 @@ export const getGithubUserContribution = async (userName: string) => {
dom.window.document.querySelectorAll(".js-calendar-graph-svg > g > g")
)
.map((column, x) =>
Array.from(column.querySelectorAll("rect")).map((element, y) => ({
x,
y,
count: element.getAttribute("data-count"),
date: element.getAttribute("data-date"),
color: element.getAttribute("fill"),
k: colorScheme.indexOf(element.getAttribute("fill")!),
}))
Array.from(column.querySelectorAll("rect")).map((element, y) => {
const count = +element.getAttribute("data-count")!;
const date = element.getAttribute("data-date")!;
const color = element.getAttribute("fill")!;
const k = colorScheme.indexOf(color);
return { x, y, count, date, color, k };
})
)
.flat();
@@ -53,9 +38,3 @@ type ThenArg<T> = T extends PromiseLike<infer U> ? U : T;
export type Cell = ThenArg<
ReturnType<typeof getGithubUserContribution>
>["cells"][number];
// "#ebedf0";
// "#9be9a8";
// "#40c463";
// "#30a14e";
// "#216e39";