Compare commits

...

6 Commits

Author SHA1 Message Date
release bot
d078b2d231 📦 2.0.0 2022-04-21 18:20:09 +00:00
platane
a81c1bcc97 ♻️ drop cheerio 2022-04-21 20:16:28 +02:00
platane
40b26d0110 ♻️ refactor getGithubUserContribution 2022-04-21 20:16:28 +02:00
platane
d6e930af5b 📓 2022-04-21 20:10:36 +02:00
platane
98feaa6035 🚀 Allow to pass option as Json without ? 2022-04-21 19:04:21 +02:00
mdgw
8f1481341a fix svg-only version in readme to fix warning (#27) 2022-04-20 08:51:06 +02:00
14 changed files with 2736 additions and 22825 deletions

View File

@@ -21,7 +21,7 @@ Available as github action. Automatically generate a new image at the end of the
**github action**
```yaml
- uses: Platane/snk@v2.0.0-rc.1
- uses: Platane/snk@v2.0.0-rc.3
with:
# github user name to read the contribution graph from (**required**)
# using action context var `github.repository_owner` or specified user
@@ -29,6 +29,13 @@ Available as github action. Automatically generate a new image at the end of the
# list of files to generate.
# one file per line. Each output can be customized with options as query string.
#
# supported options:
# - palette: A preset of color, one of [github, github-dark, github-light]
# - color_snake: Color of the snake
# - color_dots: Coma separated list of dots color.
# The first one is 0 contribution, then it goes from the low contribution to the highest.
# Exactly 5 colors are expected.
outputs: |
dist/github-snake.svg
dist/github-snake.svg?palette=github-dark
@@ -37,7 +44,7 @@ Available as github action. Automatically generate a new image at the end of the
[example with cron job](https://github.com/Platane/Platane/blob/master/.github/workflows/main.yml#L24-L29)
If you are only interested in generating a svg, you can use this other faster action: `uses: Platane/snk/svg-only@v2.0.0-rc.1`
If you are only interested in generating a svg, you can use this other faster action: `uses: Platane/snk/svg-only@v2.0.0-rc.3`
**interactive demo**

View File

@@ -4,7 +4,7 @@ author: "platane"
runs:
using: docker
image: docker://platane/snk@sha256:e40bb02de6ed0f164eca8586b3f6c32109b2bcb426cd57c6882764825b40fe0d
image: docker://platane/snk@sha256:3169a2e4a5b5181c203ae18e7075d0d92be88a0cdeabce6c3221288ef6a17675
inputs:
github_user_name:
@@ -15,15 +15,15 @@ inputs:
default: null
description: |
list of files to generate.
Generates one file per line. Each output can be customized with query string.
following this pattern: path/to/file.<gif or svg>?palette=<github or github-dark>&color_snake=<color>&color_dots=<color>,<color>,<color>,<color>,<color>
one file per line. Each output can be customized with options as query string.
supported query string options:
- palette: a preset of color, one of [github, github-dark, github-light]
- color_snake: color of the snake
- color_dots: coma separated list of dots color. The first one is the empty cell color, the second one is the lightest shade, the third one is the second lightest shade ect ...
supported query string options:
- palette: A preset of color, one of [github, github-dark, github-light]
- color_snake: Color of the snake
- color_dots: Coma separated list of dots color.
The first one is 0 contribution, then it goes from the low contribution to the highest.
Exactly 5 colors are expected.
example:
outputs: |
dark.svg?palette=github-dark&color_snake=blue

View File

@@ -1,7 +1,7 @@
{
"name": "snk",
"description": "Generates a snake game from a github user contributions grid",
"version": "2.0.0-rc.3",
"version": "2.0.0",
"private": true,
"repository": "github:platane/snk",
"devDependencies": {

View File

@@ -1,5 +1,81 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should parse /out.svg {"color_snake":"yellow"} 1`] = `
Object {
"animationOptions": Object {
"frameDuration": 100,
"step": 1,
},
"drawOptions": Object {
"colorDotBorder": "#1b1f230a",
"colorDots": Array [
"#ebedf0",
"#9be9a8",
"#40c463",
"#30a14e",
"#216e39",
],
"colorEmpty": "#ebedf0",
"colorSnake": "yellow",
"dark": Object {
"colorDotBorder": "#1b1f230a",
"colorDots": Array [
"#161b22",
"#01311f",
"#034525",
"#0f6d31",
"#00c647",
],
"colorEmpty": "#161b22",
"colorSnake": "purple",
},
"sizeCell": 16,
"sizeDot": 12,
"sizeDotBorderRadius": 2,
},
"filename": "/out.svg",
"format": "svg",
}
`;
exports[`should parse /out.svg?.gif.svg?color_snake=orange 1`] = `
Object {
"animationOptions": Object {
"frameDuration": 100,
"step": 1,
},
"drawOptions": Object {
"colorDotBorder": "#1b1f230a",
"colorDots": Array [
"#ebedf0",
"#9be9a8",
"#40c463",
"#30a14e",
"#216e39",
],
"colorEmpty": "#ebedf0",
"colorSnake": "orange",
"dark": Object {
"colorDotBorder": "#1b1f230a",
"colorDots": Array [
"#161b22",
"#01311f",
"#034525",
"#0f6d31",
"#00c647",
],
"colorEmpty": "#161b22",
"colorSnake": "purple",
},
"sizeCell": 16,
"sizeDot": 12,
"sizeDotBorderRadius": 2,
},
"filename": "/out.svg?.gif.svg",
"format": "svg",
}
`;
exports[`should parse /out.svg?{"color_snake":"yellow","color_dots":["#000","#111","#222","#333","#444"]} 1`] = `
Object {
"animationOptions": Object {

View File

@@ -7,7 +7,11 @@ import { parseEntry } from "../outputsOptions";
`/out.svg?{"color_snake":"yellow","color_dots":["#000","#111","#222","#333","#444"]}`,
`/out.svg {"color_snake":"yellow"}`,
"/out.svg?color_snake=orange&color_dots=#000,#111,#222,#333,#444&dark_color_dots=#a00,#a11,#a22,#a33,#a44",
"/out.svg?.gif.svg?color_snake=orange",
].forEach((entry) =>
it(`should parse ${entry}`, () => {
expect(parseEntry(entry)).toMatchSnapshot();

View File

@@ -15,9 +15,9 @@ export const generateContributionSnake = async (
} | null)[]
) => {
console.log("🎣 fetching github user contribution");
const { cells, colorScheme } = await getGithubUserContribution(userName);
const cells = await getGithubUserContribution(userName);
const grid = userContributionToGrid(cells, colorScheme);
const grid = userContributionToGrid(cells);
const snake = snake4;
console.log("📡 computing best route");

View File

@@ -5,10 +5,13 @@ import { palettes } from "./palettes";
export const parseOutputsOption = (lines: string[]) => lines.map(parseEntry);
export const parseEntry = (entry: string) => {
const m = entry.trim().match(/^(.+\.(svg|gif))(\?(.*))?$/);
const m = entry.trim().match(/^(.+\.(svg|gif))(\?(.*)|\s*({.*}))?$/);
if (!m) return null;
const [, filename, format, , query] = m;
const [, filename, format, _, q1, q2] = m;
const query = q1 ?? q2;
let sp = new URLSearchParams(query || "");

View File

@@ -2,17 +2,13 @@ import { setColor, createEmptyGrid, setColorEmpty } from "@snk/types/grid";
import type { Cell } from "@snk/github-user-contribution";
import type { Color } from "@snk/types/grid";
export const userContributionToGrid = (
cells: Cell[],
colorScheme: string[]
) => {
export const userContributionToGrid = (cells: Cell[]) => {
const width = Math.max(0, ...cells.map((c) => c.x)) + 1;
const height = Math.max(0, ...cells.map((c) => c.y)) + 1;
const grid = createEmptyGrid(width, height);
for (const c of cells) {
const k = colorScheme.indexOf(c.color);
if (k > 0) setColor(grid, c.x, c.y, k as Color);
if (c.level > 0) setColor(grid, c.x, c.y, c.level as Color);
else setColorEmpty(grid, c.x, c.y);
}

View File

@@ -163,7 +163,7 @@ const createViewer = ({
const k = spring.x % 1;
ctx.clearRect(0, 0, 9999, 9999);
drawLerpWorld(ctx, grid, null, snake0, snake1, stack, k, drawOptions);
drawLerpWorld(ctx, grid, cells, snake0, snake1, stack, k, drawOptions);
if (!stable) animationFrame = requestAnimationFrame(loop);
};
@@ -231,19 +231,19 @@ const onSubmit = async (userName: string) => {
const res = await fetch(
process.env.GITHUB_USER_CONTRIBUTION_API_ENDPOINT + userName
);
const { cells, colorScheme } = (await res.json()) as Res;
const cells = (await res.json()) as Res;
const drawOptions: DrawOptions = {
sizeDotBorderRadius: 2,
sizeCell: 16,
sizeDot: 12,
colorDotBorder: "#1b1f230a",
colorDots: colorScheme as any,
colorEmpty: colorScheme[0],
colorDots: ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"],
colorEmpty: "#ebedf0",
colorSnake: "purple",
};
const grid = userContributionToGrid(cells, colorScheme);
const grid = userContributionToGrid(cells);
const chain = await getChain(grid);

View File

@@ -7,41 +7,22 @@ describe("getGithubUserContribution", () => {
await promise;
});
it("should get colorScheme", async () => {
const { colorScheme } = await promise;
expect(colorScheme).toEqual([
"#ebedf0",
"#9be9a8",
"#40c463",
"#30a14e",
"#216e39",
]);
});
it("should get around 365 cells", async () => {
const { cells } = await promise;
const cells = await promise;
expect(cells.length).toBeGreaterThanOrEqual(365);
expect(cells.length).toBeLessThanOrEqual(365 + 7);
});
it("cells should have x / y coords representing to a 7 x (365/7) (minus unfilled last row)", async () => {
const { cells, colorScheme } = await promise;
const cells = await promise;
expect(cells.length).toBeGreaterThan(300);
expect(colorScheme).toEqual([
"#ebedf0",
"#9be9a8",
"#40c463",
"#30a14e",
"#216e39",
]);
const undefinedDays = Array.from({ length: Math.floor(365 / 7) })
.map((x) => Array.from({ length: 7 }).map((y) => ({ x, y })))
.flat()
.filter(({ x, y }) => cells.some((c) => c.x === x && c.y === y));
.filter(({ x, y }) => cells.some((c: any) => c.x === x && c.y === y));
expect(undefinedDays).toEqual([]);
});

View File

@@ -1,5 +1,4 @@
import fetch from "node-fetch";
import * as cheerio from "cheerio";
import { formatParams, Options } from "./formatParams";
/**
@@ -39,91 +38,40 @@ export const getGithubUserContribution = async (
return parseUserPage(resText);
};
const defaultColorScheme = [
"#ebedf0",
"#9be9a8",
"#40c463",
"#30a14e",
"#216e39",
];
const parseUserPage = (content: string) => {
const $ = cheerio.load(content);
// take roughly the svg block
const block = content
.split(`class="js-calendar-graph-svg"`)[1]
.split("</svg>")[0];
//
// "parse" colorScheme
const colorScheme = [...defaultColorScheme];
let x = 0;
let lastYAttribute = 0;
//
// parse cells
const rawCells = $(".js-calendar-graph rect[data-count]")
.toArray()
.map((x) => {
const level = +x.attribs["data-level"];
const count = +x.attribs["data-count"];
const date = x.attribs["data-date"];
const rects = Array.from(block.matchAll(/<rect[^>]*>/g)).map(([m]) => {
const date = m.match(/data-date="([^"]+)"/)![1];
const count = +m.match(/data-count="([^"]+)"/)![1];
const level = +m.match(/data-level="([^"]+)"/)![1];
const yAttribute = +m.match(/y="([^"]+)"/)![1];
const color = colorScheme[level];
if (lastYAttribute > yAttribute) x++;
if (!color) throw new Error("could not determine the color of the cell");
lastYAttribute = yAttribute;
return {
svgPosition: getSvgPosition(x),
color,
count,
date,
};
});
const xMap: Record<number, true> = {};
const yMap: Record<number, true> = {};
rawCells.forEach(({ svgPosition: { x, y } }) => {
xMap[x] = true;
yMap[y] = true;
return { date, count, level, x, yAttribute };
});
const xRange = Object.keys(xMap)
.map((x) => +x)
.sort((a, b) => +a - +b);
const yRange = Object.keys(yMap)
.map((x) => +x)
.sort((a, b) => +a - +b);
const yAttributes = Array.from(
new Set(rects.map((c) => c.yAttribute)).keys()
).sort();
const cells = rawCells.map(({ svgPosition, ...c }) => ({
const cells = rects.map(({ yAttribute, ...c }) => ({
y: yAttributes.indexOf(yAttribute),
...c,
x: xRange.indexOf(svgPosition.x),
y: yRange.indexOf(svgPosition.y),
}));
return { cells, colorScheme };
};
// returns the position of the svg elements, accounting for it's transform and it's parent transform
// ( only accounts for translate transform )
const getSvgPosition = (
e: cheerio.Element | null
): { x: number; y: number } => {
if (!e || e.tagName === "svg") return { x: 0, y: 0 };
const p = getSvgPosition(e.parent as cheerio.Element);
if (e.attribs.x) p.x += +e.attribs.x;
if (e.attribs.y) p.y += +e.attribs.y;
if (e.attribs.transform) {
const m = e.attribs.transform.match(
/translate\( *([\.\d]+) *, *([\.\d]+) *\)/
);
if (m) {
p.x += +m[1];
p.y += +m[2];
}
}
return p;
return cells;
};
export type Res = Awaited<ReturnType<typeof getGithubUserContribution>>;
export type Cell = Res["cells"][number];
export type Cell = Res[number];

View File

@@ -15,14 +15,15 @@ inputs:
default: null
description: |
list of files to generate.
Generates one file per line. Each output can be customized with query string.
following this pattern: path/to/file.svg?palette=<github or github-dark>&color_snake=<color>&color_dots=<color>,<color>,<color>,<color>,<color>
one file per line. Each output can be customized with options as query string.
supported query string options:
supported query string options:
- palette: a preset of color, one of [github, github-dark, github-light]
- color_snake: color of the snake
- color_dots: coma separated list of dots color. The first one is the empty cell color, the second one is the lightest shade, the third one is the second lightest shade ect ...
- palette: A preset of color, one of [github, github-dark, github-light]
- color_snake: Color of the snake
- color_dots: Coma separated list of dots color.
The first one is 0 contribution, then it goes from the low contribution to the highest.
Exactly 5 colors are expected.
example:
outputs: |

20168
svg-only/dist/index.js vendored

File diff suppressed because one or more lines are too long