Compare commits

...

21 Commits

Author SHA1 Message Date
release bot
aebc3a9285 📦 3.0.0 2023-07-17 20:57:38 +00:00
platane
1574f65738 📓 update readme 2023-07-17 22:55:37 +02:00
platane
ebeb59fced read contribution calendar from github api 2023-07-17 22:55:37 +02:00
release bot
4489504b7a 📦 2.3.0 2023-07-17 20:37:27 +00:00
platane
027f89563f ⬆️ bump dependencies 2023-07-17 22:34:45 +02:00
platane
7233ec9e15 update contribution parser 2023-07-17 22:20:09 +02:00
platane
54dbbbf73d ♻️ run scripts with npm run vs yarn 2023-07-17 22:13:00 +02:00
Tanmoy
3eed9ce6d6 docs: remove unnecessary whitespace
there is an inconsistency in the whitespace surrounding the URL within the `srcset` attribute, hence we always get the snake in light mode
2023-07-04 01:18:04 +02:00
release bot
3acebc09eb 📦 2.2.1 2023-02-26 09:32:32 +00:00
platane
82417bf9f5 ⬆️ bump ncc 2023-02-26 10:30:34 +01:00
platane
7b6d52d221 ⬆️ bump tooling dependencies 2023-02-26 10:30:34 +01:00
platane
fd133c88c7 remove dark theme media query on default option 2023-02-26 10:21:56 +01:00
platane
229c9a9cd6 ⬆️ bump action dependencies 2023-02-26 10:15:41 +01:00
platane
3803e1ccfa 📓 use picture element to detect dark-mode in readme 2023-02-26 10:15:17 +01:00
platane
8ca289e908 🚑 fix readme lint 2023-02-26 10:00:59 +01:00
Platane
fd7cc1f05a docs(readme): syntax-highlight the darkmode snippet as html 2023-01-18 05:01:25 +01:00
Feng Kaiyu
632fcf6cb7 docs(readme): update the description of dark mode. 2023-01-18 05:01:25 +01:00
platane
e2eb91cf8f allows to select palette in demo 2023-01-06 09:19:20 +01:00
platane
38e2ed4f23 📝 add badges 2023-01-06 08:56:13 +01:00
release bot
b7a9c1e353 📦 2.2.0 2023-01-06 07:36:57 +00:00
platane
a0e08722d9 🚑 adapt the parser to the new github page markup 2023-01-06 08:25:04 +01:00
30 changed files with 1329 additions and 3754 deletions

View File

@@ -7,21 +7,23 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-node@v2 - uses: actions/setup-node@v3
with: with:
cache: yarn cache: yarn
node-version: 16 node-version: 16
- run: yarn install --frozen-lockfile - run: yarn install --frozen-lockfile
- run: yarn type - run: npm run type
- run: yarn lint - run: npm run lint
- run: yarn test --ci - run: npm run test --ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
test-action: test-action:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: update action.yml to use image from local Dockerfile - name: update action.yml to use image from local Dockerfile
run: | run: |
@@ -36,6 +38,8 @@ jobs:
dist/github-contribution-grid-snake.svg dist/github-contribution-grid-snake.svg
dist/github-contribution-grid-snake-dark.svg?palette=github-dark dist/github-contribution-grid-snake-dark.svg?palette=github-dark
dist/github-contribution-grid-snake.gif?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9 dist/github-contribution-grid-snake.gif?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: ensure the generated file exists - name: ensure the generated file exists
run: | run: |
@@ -63,7 +67,7 @@ jobs:
- name: build svg-only action - name: build svg-only action
run: | run: |
yarn build:action npm run build:action
rm -r svg-only/dist rm -r svg-only/dist
mv packages/action/dist svg-only/dist mv packages/action/dist svg-only/dist
@@ -75,6 +79,8 @@ jobs:
outputs: | outputs: |
dist/github-contribution-grid-snake.svg dist/github-contribution-grid-snake.svg
dist/github-contribution-grid-snake-dark.svg?palette=github-dark dist/github-contribution-grid-snake-dark.svg?palette=github-dark
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: ensure the generated file exists - name: ensure the generated file exists
run: | run: |
@@ -92,18 +98,18 @@ jobs:
deploy-ghpages: deploy-ghpages:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-node@v2 - uses: actions/setup-node@v3
with: with:
cache: yarn cache: yarn
node-version: 16 node-version: 16
- run: yarn install --frozen-lockfile - run: yarn install --frozen-lockfile
- run: yarn build:demo - run: npm run build:demo
env: env:
GITHUB_USER_CONTRIBUTION_API_ENDPOINT: https://snk-one.vercel.app/api/github-user-contribution/ GITHUB_USER_CONTRIBUTION_API_ENDPOINT: https://snk-one.vercel.app/api/github-user-contribution/
- uses: crazy-max/ghaction-github-pages@v2.6.0 - uses: crazy-max/ghaction-github-pages@v3.1.0
if: success() && github.ref == 'refs/heads/main' if: success() && github.ref == 'refs/heads/main'
with: with:
target_branch: gh-pages target_branch: gh-pages

View File

@@ -21,19 +21,19 @@ jobs:
permissions: permissions:
contents: write contents: write
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: docker/setup-qemu-action@v1 - uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v1 - uses: docker/setup-buildx-action@v2
- uses: docker/login-action@v1 - uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: build and publish the docker image - name: build and publish the docker image
uses: docker/build-push-action@v2 uses: docker/build-push-action@v4
id: docker-build id: docker-build
with: with:
push: true push: true
@@ -45,7 +45,7 @@ jobs:
run: | run: |
sed -i "s/image: .*/image: docker:\/\/platane\/snk@${{ steps.docker-build.outputs.digest }}/" action.yml sed -i "s/image: .*/image: docker:\/\/platane\/snk@${{ steps.docker-build.outputs.digest }}/" action.yml
- uses: actions/setup-node@v2 - uses: actions/setup-node@v3
with: with:
cache: yarn cache: yarn
node-version: 16 node-version: 16
@@ -53,7 +53,7 @@ jobs:
- name: build svg-only action - name: build svg-only action
run: | run: |
yarn install --frozen-lockfile yarn install --frozen-lockfile
yarn build:action npm run build:action
rm -r svg-only/dist rm -r svg-only/dist
mv packages/action/dist svg-only/dist mv packages/action/dist svg-only/dist
@@ -81,7 +81,7 @@ jobs:
echo "prerelease=true" >> $GITHUB_OUTPUT echo "prerelease=true" >> $GITHUB_OUTPUT
fi fi
- uses: ncipollo/release-action@v1.11.1 - uses: ncipollo/release-action@v1.12.0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:

3
.gitignore vendored
View File

@@ -3,4 +3,5 @@ npm-debug.log*
yarn-error.log* yarn-error.log*
dist dist
!svg-only/dist !svg-only/dist
build build
.env

View File

@@ -1,5 +1,6 @@
# snk # snk
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/platane/platane/main.yml?label=action&style=flat-square)](https://github.com/Platane/Platane/actions/workflows/main.yml)
[![GitHub release](https://img.shields.io/github/release/platane/snk.svg?style=flat-square)](https://github.com/platane/snk/releases/latest) [![GitHub release](https://img.shields.io/github/release/platane/snk.svg?style=flat-square)](https://github.com/platane/snk/releases/latest)
[![GitHub marketplace](https://img.shields.io/badge/marketplace-snake-blue?logo=github&style=flat-square)](https://github.com/marketplace/actions/generate-snake-game-from-github-contribution-grid) [![GitHub marketplace](https://img.shields.io/badge/marketplace-snake-blue?logo=github&style=flat-square)](https://github.com/marketplace/actions/generate-snake-game-from-github-contribution-grid)
![type definitions](https://img.shields.io/npm/types/typescript?style=flat-square) ![type definitions](https://img.shields.io/npm/types/typescript?style=flat-square)
@@ -7,7 +8,20 @@
Generates a snake game from a github user contributions graph Generates a snake game from a github user contributions graph
![](https://github.com/Platane/snk/raw/output/github-contribution-grid-snake.svg) <picture>
<source
media="(prefers-color-scheme: dark)"
srcset="https://raw.githubusercontent.com/platane/snk/output/github-contribution-grid-snake-dark.svg"
/>
<source
media="(prefers-color-scheme: light)"
srcset="https://raw.githubusercontent.com/platane/snk/output/github-contribution-grid-snake.svg"
/>
<img
alt="github contribution grid snake animation"
src="https://raw.githubusercontent.com/platane/snk/output/github-contribution-grid-snake.svg"
/>
</picture>
Pull a github user's contribution graph. Pull a github user's contribution graph.
Make it a snake Game, generate a snake path where the cells get eaten in an orderly fashion. Make it a snake Game, generate a snake path where the cells get eaten in an orderly fashion.
@@ -21,7 +35,7 @@ Available as github action. It can automatically generate a new image each day.
**github action** **github action**
```yaml ```yaml
- uses: Platane/snk@v2 - uses: Platane/snk@v3
with: with:
# github user name to read the contribution graph from (**required**) # github user name to read the contribution graph from (**required**)
# using action context var `github.repository_owner` or specified user # using action context var `github.repository_owner` or specified user
@@ -40,6 +54,10 @@ Available as github action. It can automatically generate a new image each day.
dist/github-snake.svg dist/github-snake.svg
dist/github-snake-dark.svg?palette=github-dark dist/github-snake-dark.svg?palette=github-dark
dist/ocean.gif?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9 dist/ocean.gif?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9
env:
# a github token is required to fetch the contribution calendar from github API
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
``` ```
[example with cron job](https://github.com/Platane/Platane/blob/master/.github/workflows/main.yml#L24-L29) [example with cron job](https://github.com/Platane/Platane/blob/master/.github/workflows/main.yml#L24-L29)
@@ -48,11 +66,14 @@ If you are only interested in generating a svg, consider using this faster actio
**dark mode** **dark mode**
For **dark mode** support on github, use this [special syntax](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#specifying-the-theme-an-image-is-shown-to=) in your readme. For **dark mode** support on github, use this [special syntax](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#specifying-the-theme-an-image-is-shown-to) in your readme.
```md ```html
![GitHub Snake Light](github-snake.svg#gh-light-mode-only) <picture>
![GitHub Snake dark](github-snake-dark.svg#gh-dark-mode-only) <source media="(prefers-color-scheme: dark)" srcset="github-snake-dark.svg" />
<source media="(prefers-color-scheme: light)" srcset="github-snake.svg" />
<img alt="github-snake" src="github-snake.svg" />
</picture>
``` ```
**interactive demo** **interactive demo**

View File

@@ -4,7 +4,7 @@ author: "platane"
runs: runs:
using: docker using: docker
image: docker://platane/snk@sha256:89466e404c3d3ba2384e24aabad0542a643eacdc53d0c6320ce369cc1af19d56 image: docker://platane/snk@sha256:753878055e52fbbaf3148fdac4590e396f97581f1dc4c1f861701add7a1dc1b5
inputs: inputs:
github_user_name: github_user_name:

View File

@@ -1,17 +1,17 @@
{ {
"name": "snk", "name": "snk",
"description": "Generates a snake game from a github user contributions grid", "description": "Generates a snake game from a github user contributions grid",
"version": "2.1.0", "version": "3.0.0",
"private": true, "private": true,
"repository": "github:platane/snk", "repository": "github:platane/snk",
"devDependencies": { "devDependencies": {
"@sucrase/jest-plugin": "3.0.0", "@sucrase/jest-plugin": "3.0.0",
"@types/jest": "29.2.1", "@types/jest": "29.5.3",
"@types/node": "16.11.7", "@types/node": "16.18.38",
"jest": "29.2.2", "jest": "29.6.1",
"prettier": "2.7.1", "prettier": "2.8.8",
"sucrase": "3.28.0", "sucrase": "3.33.0",
"typescript": "4.8.4" "typescript": "5.1.6"
}, },
"workspaces": [ "workspaces": [
"packages/**" "packages/**"
@@ -27,10 +27,10 @@
}, },
"scripts": { "scripts": {
"type": "tsc --noEmit", "type": "tsc --noEmit",
"lint": "yarn prettier -c '**/*.{ts,js,json,md,yml,yaml}' '!packages/*/dist/**' '!svg-only/dist/**'", "lint": "prettier -c '**/*.{ts,js,json,md,yml,yaml}' '!packages/*/dist/**' '!svg-only/dist/**'",
"test": "jest --verbose --passWithNoTests --no-cache", "test": "jest --verbose --no-cache",
"dev:demo": "( cd packages/demo ; yarn dev )", "dev:demo": "( cd packages/demo ; npm run dev )",
"build:demo": "( cd packages/demo ; yarn build )", "build:demo": "( cd packages/demo ; npm run build )",
"build:action": "( cd packages/action ; yarn build )" "build:action": "( cd packages/action ; npm run build )"
} }
} }

View File

@@ -1,81 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should parse /out.svg {"color_snake":"yellow"} 1`] = `
{
"animationOptions": {
"frameDuration": 100,
"step": 1,
},
"drawOptions": {
"colorDotBorder": "#1b1f230a",
"colorDots": [
"#ebedf0",
"#9be9a8",
"#40c463",
"#30a14e",
"#216e39",
],
"colorEmpty": "#ebedf0",
"colorSnake": "yellow",
"dark": {
"colorDotBorder": "#1b1f230a",
"colorDots": [
"#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`] = `
{
"animationOptions": {
"frameDuration": 100,
"step": 1,
},
"drawOptions": {
"colorDotBorder": "#1b1f230a",
"colorDots": [
"#ebedf0",
"#9be9a8",
"#40c463",
"#30a14e",
"#216e39",
],
"colorEmpty": "#ebedf0",
"colorSnake": "orange",
"dark": {
"colorDotBorder": "#1b1f230a",
"colorDots": [
"#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`] = ` exports[`should parse /out.svg?{"color_snake":"yellow","color_dots":["#000","#111","#222","#333","#444"]} 1`] = `
{ {
"animationOptions": { "animationOptions": {
@@ -148,6 +72,7 @@ exports[`should parse /out.svg?color_snake=orange&color_dots=#000,#111,#222,#333
"colorEmpty": "#000", "colorEmpty": "#000",
"colorSnake": "orange", "colorSnake": "orange",
"dark": { "dark": {
"colorDotBorder": "#1b1f230a",
"colorDots": [ "colorDots": [
"#a00", "#a00",
"#a11", "#a11",
@@ -156,6 +81,7 @@ exports[`should parse /out.svg?color_snake=orange&color_dots=#000,#111,#222,#333
"#a44", "#a44",
], ],
"colorEmpty": "#a00", "colorEmpty": "#a00",
"colorSnake": "orange",
}, },
"sizeCell": 16, "sizeCell": 16,
"sizeDot": 12, "sizeDot": 12,
@@ -183,18 +109,7 @@ exports[`should parse path/to/out.gif 1`] = `
], ],
"colorEmpty": "#ebedf0", "colorEmpty": "#ebedf0",
"colorSnake": "purple", "colorSnake": "purple",
"dark": { "dark": undefined,
"colorDotBorder": "#1b1f230a",
"colorDots": [
"#161b22",
"#01311f",
"#034525",
"#0f6d31",
"#00c647",
],
"colorEmpty": "#161b22",
"colorSnake": "purple",
},
"sizeCell": 16, "sizeCell": 16,
"sizeDot": 12, "sizeDot": 12,
"sizeDotBorderRadius": 2, "sizeDotBorderRadius": 2,

View File

@@ -2,6 +2,8 @@ import * as fs from "fs";
import * as path from "path"; import * as path from "path";
import { generateContributionSnake } from "../generateContributionSnake"; import { generateContributionSnake } from "../generateContributionSnake";
import { parseOutputsOption } from "../outputsOptions"; import { parseOutputsOption } from "../outputsOptions";
import { config } from "dotenv";
config({ path: __dirname + "/../../../.env" });
jest.setTimeout(2 * 60 * 1000); jest.setTimeout(2 * 60 * 1000);
@@ -30,7 +32,9 @@ it(
const outputs = parseOutputsOption(entries); const outputs = parseOutputsOption(entries);
const results = await generateContributionSnake("platane", outputs); const results = await generateContributionSnake("platane", outputs, {
githubToken: process.env.GITHUB_TOKEN!,
});
expect(results[0]).toBeDefined(); expect(results[0]).toBeDefined();
expect(results[1]).toBeDefined(); expect(results[1]).toBeDefined();

View File

@@ -1,17 +1,58 @@
import { parseEntry } from "../outputsOptions"; import { parseEntry } from "../outputsOptions";
it("should parse options as json", () => {
expect(
parseEntry(`/out.svg {"color_snake":"yellow"}`)?.drawOptions
).toHaveProperty("colorSnake", "yellow");
expect(
parseEntry(`/out.svg?{"color_snake":"yellow"}`)?.drawOptions
).toHaveProperty("colorSnake", "yellow");
expect(
parseEntry(`/out.svg?{"color_dots":["#000","#111","#222","#333","#444"]}`)
?.drawOptions.colorDots
).toEqual(["#000", "#111", "#222", "#333", "#444"]);
});
it("should parse options as searchparams", () => {
expect(parseEntry(`/out.svg?color_snake=yellow`)?.drawOptions).toHaveProperty(
"colorSnake",
"yellow"
);
expect(
parseEntry(`/out.svg?color_dots=#000,#111,#222,#333,#444`)?.drawOptions
.colorDots
).toEqual(["#000", "#111", "#222", "#333", "#444"]);
});
it("should parse filename", () => {
expect(parseEntry(`/a/b/c.svg?{"color_snake":"yellow"}`)).toHaveProperty(
"filename",
"/a/b/c.svg"
);
expect(
parseEntry(`/a/b/out.svg?.gif.svg?{"color_snake":"yellow"}`)
).toHaveProperty("filename", "/a/b/out.svg?.gif.svg");
expect(
parseEntry(`/a/b/{[-1].svg?.gif.svg?{"color_snake":"yellow"}`)
).toHaveProperty("filename", "/a/b/{[-1].svg?.gif.svg");
});
[ [
// default
"path/to/out.gif", "path/to/out.gif",
// overwrite colors (search params)
"/out.svg?color_snake=orange&color_dots=#000,#111,#222,#333,#444", "/out.svg?color_snake=orange&color_dots=#000,#111,#222,#333,#444",
// overwrite colors (json)
`/out.svg?{"color_snake":"yellow","color_dots":["#000","#111","#222","#333","#444"]}`, `/out.svg?{"color_snake":"yellow","color_dots":["#000","#111","#222","#333","#444"]}`,
`/out.svg {"color_snake":"yellow"}`, // overwrite dark colors
"/out.svg?color_snake=orange&color_dots=#000,#111,#222,#333,#444&dark_color_dots=#a00,#a11,#a22,#a33,#a44", "/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) => ].forEach((entry) =>
it(`should parse ${entry}`, () => { it(`should parse ${entry}`, () => {
expect(parseEntry(entry)).toMatchSnapshot(); expect(parseEntry(entry)).toMatchSnapshot();

View File

@@ -12,10 +12,11 @@ export const generateContributionSnake = async (
format: "svg" | "gif"; format: "svg" | "gif";
drawOptions: DrawOptions; drawOptions: DrawOptions;
animationOptions: AnimationOptions; animationOptions: AnimationOptions;
} | null)[] } | null)[],
options: { githubToken: string }
) => { ) => {
console.log("🎣 fetching github user contribution"); console.log("🎣 fetching github user contribution");
const cells = await getGithubUserContribution(userName); const cells = await getGithubUserContribution(userName, options);
const grid = userContributionToGrid(cells); const grid = userContributionToGrid(cells);
const snake = snake4; const snake = snake4;

View File

@@ -12,11 +12,14 @@ import { parseOutputsOption } from "./outputsOptions";
core.getInput("svg_out_path"), core.getInput("svg_out_path"),
] ]
); );
const githubToken = process.env.GITHUB_TOKEN!;
const { generateContributionSnake } = await import( const { generateContributionSnake } = await import(
"./generateContributionSnake" "./generateContributionSnake"
); );
const results = await generateContributionSnake(userName, outputs); const results = await generateContributionSnake(userName, outputs, {
githubToken,
});
outputs.forEach((out, i) => { outputs.forEach((out, i) => {
const result = results[i]; const result = results[i];

View File

@@ -32,6 +32,7 @@ export const parseEntry = (entry: string) => {
sizeCell: 16, sizeCell: 16,
sizeDot: 12, sizeDot: 12,
...palettes["default"], ...palettes["default"],
dark: palettes["default"].dark && { ...palettes["default"].dark },
}; };
const animationOptions: AnimationOptions = { step: 1, frameDuration: 100 }; const animationOptions: AnimationOptions = { step: 1, frameDuration: 100 };
@@ -43,6 +44,14 @@ export const parseEntry = (entry: string) => {
} }
} }
{
const dark_palette = palettes[sp.get("dark_palette")!];
if (dark_palette) {
const clone = { ...dark_palette, dark: undefined };
drawOptions.dark = clone;
}
}
if (sp.has("color_snake")) drawOptions.colorSnake = sp.get("color_snake")!; if (sp.has("color_snake")) drawOptions.colorSnake = sp.get("color_snake")!;
if (sp.has("color_dots")) { if (sp.has("color_dots")) {
const colors = sp.get("color_dots")!.split(/[,;]/); const colors = sp.get("color_dots")!.split(/[,;]/);
@@ -56,6 +65,8 @@ export const parseEntry = (entry: string) => {
if (sp.has("dark_color_dots")) { if (sp.has("dark_color_dots")) {
const colors = sp.get("dark_color_dots")!.split(/[,;]/); const colors = sp.get("dark_color_dots")!.split(/[,;]/);
drawOptions.dark = { drawOptions.dark = {
colorDotBorder: drawOptions.colorDotBorder,
colorSnake: drawOptions.colorSnake,
...drawOptions.dark, ...drawOptions.dark,
colorDots: colors, colorDots: colors,
colorEmpty: colors[0], colorEmpty: colors[0],

View File

@@ -10,7 +10,8 @@
"@snk/types": "1.0.0" "@snk/types": "1.0.0"
}, },
"devDependencies": { "devDependencies": {
"@vercel/ncc": "0.34.0" "@vercel/ncc": "0.36.1",
"dotenv": "16.3.1"
}, },
"scripts": { "scripts": {
"build": "ncc build --external canvas --external gifsicle --out dist ./index.ts", "build": "ncc build --external canvas --external gifsicle --out dist ./index.ts",

View File

@@ -1,6 +1,6 @@
import { DrawOptions as DrawOptions } from "@snk/svg-creator"; import { DrawOptions as DrawOptions } from "@snk/svg-creator";
export const palettes: Record< export const basePalettes: Record<
string, string,
Pick< Pick<
DrawOptions, DrawOptions,
@@ -22,8 +22,6 @@ export const palettes: Record<
}; };
// aliases // aliases
palettes["github"] = { export const palettes = { ...basePalettes };
...palettes["github-light"], palettes["github"] = palettes["github-light"];
dark: { ...palettes["github-dark"] },
};
palettes["default"] = palettes["github"]; palettes["default"] = palettes["github"];

View File

@@ -14,6 +14,7 @@ import { createSvg } from "@snk/svg-creator";
import { createRpcClient } from "./worker-utils"; import { createRpcClient } from "./worker-utils";
import type { API as WorkerAPI } from "./demo.interactive.worker"; import type { API as WorkerAPI } from "./demo.interactive.worker";
import { AnimationOptions } from "@snk/gif-creator"; import { AnimationOptions } from "@snk/gif-creator";
import { basePalettes } from "@snk/action/palettes";
const createForm = ({ const createForm = ({
onSubmit, onSubmit,
@@ -119,13 +120,18 @@ const createViewer = ({
grid0, grid0,
chain, chain,
cells, cells,
drawOptions,
}: { }: {
grid0: Grid; grid0: Grid;
chain: Snake[]; chain: Snake[];
cells: Point[]; cells: Point[];
drawOptions: DrawOptions;
}) => { }) => {
const drawOptions: DrawOptions = {
sizeDotBorderRadius: 2,
sizeCell: 16,
sizeDot: 12,
...basePalettes["github-light"],
};
// //
// canvas // canvas
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
@@ -171,12 +177,12 @@ const createViewer = ({
// //
// controls // controls
const input = document.createElement("input") as any; const input = document.createElement("input");
input.type = "range"; input.type = "range";
input.value = 0; input.value = "0";
input.step = 1; input.step = "1";
input.min = 0; input.min = "0";
input.max = chain.length; input.max = "" + chain.length;
input.style.width = "calc( 100% - 20px )"; input.style.width = "calc( 100% - 20px )";
input.addEventListener("input", () => { input.addEventListener("input", () => {
spring.target = +input.value; spring.target = +input.value;
@@ -190,10 +196,49 @@ const createViewer = ({
window.addEventListener("click", onClickBackground); window.addEventListener("click", onClickBackground);
document.body.append(input); document.body.append(input);
//
const schemaSelect = document.createElement("select");
schemaSelect.style.margin = "10px";
schemaSelect.style.alignSelf = "flex-start";
schemaSelect.value = "github-light";
schemaSelect.addEventListener("change", () => {
Object.assign(drawOptions, basePalettes[schemaSelect.value]);
svgString = createSvg(grid0, cells, chain, drawOptions, {
frameDuration: 100,
} as AnimationOptions);
const svgImageUri = `data:image/*;charset=utf-8;base64,${btoa(svgString)}`;
svgLink.href = svgImageUri;
if (schemaSelect.value.includes("dark"))
document.body.parentElement?.classList.add("dark-mode");
else document.body.parentElement?.classList.remove("dark-mode");
loop();
});
for (const name of Object.keys(basePalettes)) {
const option = document.createElement("option");
option.value = name;
option.innerText = name;
schemaSelect.appendChild(option);
}
document.body.append(schemaSelect);
//
// dark mode
const style = document.createElement("style");
style.innerText = `
html { transition:background-color 180ms }
a { transition:color 180ms }
html.dark-mode{ background-color:#0d1117 }
html.dark-mode a{ color:rgb(201, 209, 217) }
`;
document.head.append(style);
// //
// svg // svg
const svgLink = document.createElement("a"); const svgLink = document.createElement("a");
const svgString = createSvg(grid0, cells, chain, drawOptions, { let svgString = createSvg(grid0, cells, chain, drawOptions, {
frameDuration: 100, frameDuration: 100,
} as AnimationOptions); } as AnimationOptions);
const svgImageUri = `data:image/*;charset=utf-8;base64,${btoa(svgString)}`; const svgImageUri = `data:image/*;charset=utf-8;base64,${btoa(svgString)}`;
@@ -203,7 +248,10 @@ const createViewer = ({
svgLink.addEventListener("click", (e) => { svgLink.addEventListener("click", (e) => {
const w = window.open("")!; const w = window.open("")!;
w.document.write( w.document.write(
`<a href="${svgImageUri}" download="github-user-contribution.svg">` + (document.body.parentElement?.classList.contains("dark-mode")
? "<style>html{ background-color:#0d1117 }</style>"
: "") +
`<a href="${svgLink.href}" download="github-user-contribution.svg">` +
svgString + svgString +
"<a/>" "<a/>"
); );
@@ -233,23 +281,13 @@ const onSubmit = async (userName: string) => {
); );
const cells = (await res.json()) as Res; const cells = (await res.json()) as Res;
const drawOptions: DrawOptions = {
sizeDotBorderRadius: 2,
sizeCell: 16,
sizeDot: 12,
colorDotBorder: "#1b1f230a",
colorDots: ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"],
colorEmpty: "#ebedf0",
colorSnake: "purple",
};
const grid = userContributionToGrid(cells); const grid = userContributionToGrid(cells);
const chain = await getChain(grid); const chain = await getChain(grid);
dispose(); dispose();
createViewer({ grid0: grid, chain, cells, drawOptions }); createViewer({ grid0: grid, chain, cells });
}; };
const worker = new Worker( const worker = new Worker(

View File

@@ -10,14 +10,14 @@
"@snk/types": "1.0.0" "@snk/types": "1.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/dat.gui": "0.7.7", "@types/dat.gui": "0.7.10",
"dat.gui": "0.7.9", "dat.gui": "0.7.9",
"html-webpack-plugin": "5.5.0", "html-webpack-plugin": "5.5.3",
"ts-loader": "9.4.1", "ts-loader": "9.4.4",
"ts-node": "10.9.1", "ts-node": "10.9.1",
"webpack": "5.74.0", "webpack": "5.88.1",
"webpack-cli": "4.10.0", "webpack-cli": "5.1.4",
"webpack-dev-server": "4.11.1" "webpack-dev-server": "4.15.1"
}, },
"scripts": { "scripts": {
"build": "webpack", "build": "webpack",

View File

@@ -1,10 +1,11 @@
import path from "path"; import path from "path";
import HtmlWebpackPlugin from "html-webpack-plugin"; import HtmlWebpackPlugin from "html-webpack-plugin";
import type { Configuration as WebpackConfiguration } from "webpack";
import type { Configuration as WebpackDevServerConfiguration } from "webpack-dev-server";
import webpack from "webpack"; import webpack from "webpack";
import { getGithubUserContribution } from "@snk/github-user-contribution"; import { getGithubUserContribution } from "@snk/github-user-contribution";
import { config } from "dotenv";
import type { Configuration as WebpackConfiguration } from "webpack";
import type { Configuration as WebpackDevServerConfiguration } from "webpack-dev-server";
config({ path: __dirname + "/../../.env" });
const demos: string[] = require("./demo.json"); const demos: string[] = require("./demo.json");
@@ -13,7 +14,11 @@ const webpackDevServerConfiguration: WebpackDevServerConfiguration = {
onAfterSetupMiddleware: ({ app }) => { onAfterSetupMiddleware: ({ app }) => {
app!.get("/api/github-user-contribution/:userName", async (req, res) => { app!.get("/api/github-user-contribution/:userName", async (req, res) => {
const userName: string = req.params.userName; const userName: string = req.params.userName;
res.send(await getGithubUserContribution(userName)); res.send(
await getGithubUserContribution(userName, {
githubToken: process.env.GITHUB_TOKEN!,
})
);
}); });
}, },
}; };

View File

@@ -12,7 +12,7 @@
"devDependencies": { "devDependencies": {
"@types/gifsicle": "5.2.0", "@types/gifsicle": "5.2.0",
"@types/tmp": "0.2.3", "@types/tmp": "0.2.3",
"@vercel/ncc": "0.34.0" "@vercel/ncc": "0.36.1"
}, },
"scripts": { "scripts": {
"benchmark": "ncc run __tests__/benchmark.ts --quiet" "benchmark": "ncc run __tests__/benchmark.ts --quiet"

View File

@@ -7,7 +7,11 @@ export default async (req: VercelRequest, res: VercelResponse) => {
try { try {
res.setHeader("Access-Control-Allow-Origin", "https://platane.github.io"); res.setHeader("Access-Control-Allow-Origin", "https://platane.github.io");
res.statusCode = 200; res.statusCode = 200;
res.json(await getGithubUserContribution(userName as string)); res.json(
await getGithubUserContribution(userName as string, {
githubToken: process.env.GITHUB!,
})
);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
res.statusCode = 500; res.statusCode = 500;

View File

@@ -3,6 +3,6 @@
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@snk/github-user-contribution": "1.0.0", "@snk/github-user-contribution": "1.0.0",
"@vercel/node": "2.6.1" "@vercel/node": "2.15.5"
} }
} }

View File

@@ -1,19 +0,0 @@
import { formatParams } from "../formatParams";
const params = [
//
[{}, ""],
[{ year: 2017 }, "from=2017-01-01&to=2017-12-31"],
[{ from: "2017-12-03" }, "from=2017-12-03"],
[{ to: "2017-12-03" }, "to=2017-12-03"],
] as const;
params.forEach(([params, res]) =>
it(`should format ${JSON.stringify(params)}`, () => {
expect(formatParams(params)).toBe(res);
})
);
it("should fail if the date is in the future", () => {
expect(() => formatParams({ to: "9999-01-01" })).toThrow(Error);
});

View File

@@ -1,9 +1,18 @@
import { getGithubUserContribution } from ".."; import { getGithubUserContribution } from "..";
import { config } from "dotenv";
config({ path: __dirname + "/../../../.env" });
describe("getGithubUserContribution", () => { describe("getGithubUserContribution", () => {
const promise = getGithubUserContribution("platane"); const promise = getGithubUserContribution("platane", {
githubToken: process.env.GITHUB_TOKEN!,
});
it("should resolve", async () => { it("should resolve", async () => {
console.log(
"process.env.GITHUB_TOKEN",
process.env.GITHUB_TOKEN?.replace(/\d/g, "x")
);
await promise; await promise;
}); });
@@ -27,9 +36,3 @@ describe("getGithubUserContribution", () => {
expect(undefinedDays).toEqual([]); expect(undefinedDays).toEqual([]);
}); });
}); });
xit("should match snapshot for year=2019", async () => {
expect(
await getGithubUserContribution("platane", { year: 2019 })
).toMatchSnapshot();
});

View File

@@ -1,38 +0,0 @@
export type Options = { from?: string; to?: string } | { year: number };
export const formatParams = (options: Options = {}) => {
const sp = new URLSearchParams();
const o: any = { ...options };
if ("year" in options) {
o.from = `${options.year}-01-01`;
o.to = `${options.year}-12-31`;
}
for (const s of ["from", "to"])
if (o[s]) {
const value = o[s];
if (value >= formatDate(new Date()))
throw new Error(
"Cannot get contribution for a date in the future.\nPlease limit your range to the current UTC day."
);
sp.set(s, value);
}
return sp.toString();
};
const formatDate = (d: Date) => {
const year = d.getUTCFullYear();
const month = d.getUTCMonth() + 1;
const date = d.getUTCDate();
return [
year,
month.toString().padStart(2, "0"),
date.toString().padStart(2, "0"),
].join("-");
};

View File

@@ -1,5 +1,4 @@
import fetch from "node-fetch"; import fetch from "node-fetch";
import { formatParams, Options } from "./formatParams";
/** /**
* get the contribution grid from a github user page * get the contribution grid from a github user page
@@ -19,57 +18,83 @@ import { formatParams, Options } from "./formatParams";
*/ */
export const getGithubUserContribution = async ( export const getGithubUserContribution = async (
userName: string, userName: string,
options: Options = {} o: { githubToken: string }
) => { ) => {
// either use github.com/users/xxxx/contributions for previous years const query = /* GraphQL */ `
// or github.com/xxxx ( which gives the latest update to today result ) query ($login: String!) {
const url = user(login: $login) {
"year" in options || "from" in options || "to" in options contributionsCollection {
? `https://github.com/users/${userName}/contributions?` + contributionCalendar {
formatParams(options) weeks {
: `https://github.com/${userName}`; contributionDays {
contributionCount
contributionLevel
weekday
date
}
}
}
}
}
}
`;
const variables = { login: userName };
const res = await fetch(url); const res = await fetch("https://api.github.com/graphql", {
headers: {
Authorization: `bearer ${o.githubToken}`,
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify({ variables, query }),
});
if (!res.ok) throw new Error(res.statusText); if (!res.ok) throw new Error(res.statusText);
const resText = await res.text(); const { data, errors } = (await res.json()) as {
data: GraphQLRes;
errors?: { message: string }[];
};
return parseUserPage(resText); if (errors?.[0]) throw errors[0];
return data.user.contributionsCollection.contributionCalendar.weeks.flatMap(
({ contributionDays }, x) =>
contributionDays.map((d) => ({
x,
y: d.weekday,
date: d.date,
count: d.contributionCount,
level:
(d.contributionLevel === "FOURTH_QUARTILE" && 4) ||
(d.contributionLevel === "THIRD_QUARTILE" && 3) ||
(d.contributionLevel === "SECOND_QUARTILE" && 2) ||
(d.contributionLevel === "FIRST_QUARTILE" && 1) ||
0,
}))
);
}; };
const parseUserPage = (content: string) => { type GraphQLRes = {
// take roughly the svg block user: {
const block = content contributionsCollection: {
.split(`class="js-calendar-graph-svg"`)[1] contributionCalendar: {
.split("</svg>")[0]; weeks: {
contributionDays: {
let x = 0; contributionCount: number;
let lastYAttribute = 0; contributionLevel:
| "FOURTH_QUARTILE"
const rects = Array.from(block.matchAll(/<rect[^>]*>/g)).map(([m]) => { | "THIRD_QUARTILE"
const date = m.match(/data-date="([^"]+)"/)![1]; | "SECOND_QUARTILE"
const count = +m.match(/data-count="([^"]+)"/)![1]; | "FIRST_QUARTILE"
const level = +m.match(/data-level="([^"]+)"/)![1]; | "NONE";
const yAttribute = +m.match(/y="([^"]+)"/)![1]; date: string;
weekday: number;
if (lastYAttribute > yAttribute) x++; }[];
}[];
lastYAttribute = yAttribute; };
};
return { date, count, level, x, yAttribute }; };
});
const yAttributes = Array.from(
new Set(rects.map((c) => c.yAttribute)).keys()
).sort();
const cells = rects.map(({ yAttribute, ...c }) => ({
y: yAttributes.indexOf(yAttribute),
...c,
}));
return cells;
}; };
export type Res = Awaited<ReturnType<typeof getGithubUserContribution>>; export type Res = Awaited<ReturnType<typeof getGithubUserContribution>>;

View File

@@ -2,10 +2,10 @@
"name": "@snk/github-user-contribution", "name": "@snk/github-user-contribution",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"cheerio": "1.0.0-rc.10", "node-fetch": "2.6.12"
"node-fetch": "2.6.7"
}, },
"devDependencies": { "devDependencies": {
"@types/node-fetch": "2.6.1" "@types/node-fetch": "2.6.4",
"dotenv": "16.3.1"
} }
} }

View File

@@ -1425,6 +1425,20 @@ const isDomainOrSubdomain = function isDomainOrSubdomain(destination, original)
return orig === dest || orig[orig.length - dest.length - 1] === '.' && orig.endsWith(dest); return orig === dest || orig[orig.length - dest.length - 1] === '.' && orig.endsWith(dest);
}; };
/**
* isSameProtocol reports whether the two provided URLs use the same protocol.
*
* Both domains must already be in canonical form.
* @param {string|URL} original
* @param {string|URL} destination
*/
const isSameProtocol = function isSameProtocol(destination, original) {
const orig = new URL$1(original).protocol;
const dest = new URL$1(destination).protocol;
return orig === dest;
};
/** /**
* Fetch function * Fetch function
* *
@@ -1456,7 +1470,7 @@ function fetch(url, opts) {
let error = new AbortError('The user aborted a request.'); let error = new AbortError('The user aborted a request.');
reject(error); reject(error);
if (request.body && request.body instanceof Stream.Readable) { if (request.body && request.body instanceof Stream.Readable) {
request.body.destroy(error); destroyStream(request.body, error);
} }
if (!response || !response.body) return; if (!response || !response.body) return;
response.body.emit('error', error); response.body.emit('error', error);
@@ -1497,9 +1511,43 @@ function fetch(url, opts) {
req.on('error', function (err) { req.on('error', function (err) {
reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err)); reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err));
if (response && response.body) {
destroyStream(response.body, err);
}
finalize(); finalize();
}); });
fixResponseChunkedTransferBadEnding(req, function (err) {
if (signal && signal.aborted) {
return;
}
if (response && response.body) {
destroyStream(response.body, err);
}
});
/* c8 ignore next 18 */
if (parseInt(process.version.substring(1)) < 14) {
// Before Node.js 14, pipeline() does not fully support async iterators and does not always
// properly handle when the socket close/end events are out of order.
req.on('socket', function (s) {
s.addListener('close', function (hadError) {
// if a data listener is still present we didn't end cleanly
const hasDataListener = s.listenerCount('data') > 0;
// if end happened before close but the socket didn't emit an error, do it now
if (response && hasDataListener && !hadError && !(signal && signal.aborted)) {
const err = new Error('Premature close');
err.code = 'ERR_STREAM_PREMATURE_CLOSE';
response.body.emit('error', err);
}
});
});
}
req.on('response', function (res) { req.on('response', function (res) {
clearTimeout(reqTimeout); clearTimeout(reqTimeout);
@@ -1571,7 +1619,7 @@ function fetch(url, opts) {
size: request.size size: request.size
}; };
if (!isDomainOrSubdomain(request.url, locationURL)) { if (!isDomainOrSubdomain(request.url, locationURL) || !isSameProtocol(request.url, locationURL)) {
for (const name of ['authorization', 'www-authenticate', 'cookie', 'cookie2']) { for (const name of ['authorization', 'www-authenticate', 'cookie', 'cookie2']) {
requestOpts.headers.delete(name); requestOpts.headers.delete(name);
} }
@@ -1664,6 +1712,13 @@ function fetch(url, opts) {
response = new Response(body, response_options); response = new Response(body, response_options);
resolve(response); resolve(response);
}); });
raw.on('end', function () {
// some old IIS servers return zero-length OK deflate responses, so 'data' is never emitted.
if (!response) {
response = new Response(body, response_options);
resolve(response);
}
});
return; return;
} }
@@ -1683,6 +1738,44 @@ function fetch(url, opts) {
writeToStream(req, request); writeToStream(req, request);
}); });
} }
function fixResponseChunkedTransferBadEnding(request, errorCallback) {
let socket;
request.on('socket', function (s) {
socket = s;
});
request.on('response', function (response) {
const headers = response.headers;
if (headers['transfer-encoding'] === 'chunked' && !headers['content-length']) {
response.once('close', function (hadError) {
// tests for socket presence, as in some situations the
// the 'socket' event is not triggered for the request
// (happens in deno), avoids `TypeError`
// if a data listener is still present we didn't end cleanly
const hasDataListener = socket && socket.listenerCount('data') > 0;
if (hasDataListener && !hadError) {
const err = new Error('Premature close');
err.code = 'ERR_STREAM_PREMATURE_CLOSE';
errorCallback(err);
}
});
}
});
}
function destroyStream(stream, err) {
if (stream.destroy) {
stream.destroy(err);
} else {
// node < 8
stream.emit('error', err);
stream.end();
}
}
/** /**
* Redirect code matching * Redirect code matching
* *

View File

@@ -1,9 +1,9 @@
"use strict"; "use strict";
exports.id = 317; exports.id = 407;
exports.ids = [317]; exports.ids = [407];
exports.modules = { exports.modules = {
/***/ 5317: /***/ 407:
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
// ESM COMPAT FLAG // ESM COMPAT FLAG
@@ -17,37 +17,8 @@ __webpack_require__.d(__webpack_exports__, {
// EXTERNAL MODULE: ../../node_modules/node-fetch/lib/index.js // EXTERNAL MODULE: ../../node_modules/node-fetch/lib/index.js
var lib = __webpack_require__(2197); var lib = __webpack_require__(2197);
var lib_default = /*#__PURE__*/__webpack_require__.n(lib); var lib_default = /*#__PURE__*/__webpack_require__.n(lib);
;// CONCATENATED MODULE: ../github-user-contribution/formatParams.ts
const formatParams = (options = {}) => {
const sp = new URLSearchParams();
const o = { ...options };
if ("year" in options) {
o.from = `${options.year}-01-01`;
o.to = `${options.year}-12-31`;
}
for (const s of ["from", "to"])
if (o[s]) {
const value = o[s];
if (value >= formatDate(new Date()))
throw new Error("Cannot get contribution for a date in the future.\nPlease limit your range to the current UTC day.");
sp.set(s, value);
}
return sp.toString();
};
const formatDate = (d) => {
const year = d.getUTCFullYear();
const month = d.getUTCMonth() + 1;
const date = d.getUTCDate();
return [
year,
month.toString().padStart(2, "0"),
date.toString().padStart(2, "0"),
].join("-");
};
;// CONCATENATED MODULE: ../github-user-contribution/index.ts ;// CONCATENATED MODULE: ../github-user-contribution/index.ts
/** /**
* get the contribution grid from a github user page * get the contribution grid from a github user page
* *
@@ -64,42 +35,50 @@ const formatDate = (d) => {
* getGithubUserContribution("platane", { year: 2019 }) * getGithubUserContribution("platane", { year: 2019 })
* *
*/ */
const getGithubUserContribution = async (userName, options = {}) => { const getGithubUserContribution = async (userName, o) => {
// either use github.com/users/xxxx/contributions for previous years const query = /* GraphQL */ `
// or github.com/xxxx ( which gives the latest update to today result ) query ($login: String!) {
const url = "year" in options || "from" in options || "to" in options user(login: $login) {
? `https://github.com/users/${userName}/contributions?` + contributionsCollection {
formatParams(options) contributionCalendar {
: `https://github.com/${userName}`; weeks {
const res = await lib_default()(url); contributionDays {
contributionCount
contributionLevel
weekday
date
}
}
}
}
}
}
`;
const variables = { login: userName };
const res = await lib_default()("https://api.github.com/graphql", {
headers: {
Authorization: `bearer ${o.githubToken}`,
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify({ variables, query }),
});
if (!res.ok) if (!res.ok)
throw new Error(res.statusText); throw new Error(res.statusText);
const resText = await res.text(); const { data, errors } = (await res.json());
return parseUserPage(resText); if (errors?.[0])
}; throw errors[0];
const parseUserPage = (content) => { return data.user.contributionsCollection.contributionCalendar.weeks.flatMap(({ contributionDays }, x) => contributionDays.map((d) => ({
// take roughly the svg block x,
const block = content y: d.weekday,
.split(`class="js-calendar-graph-svg"`)[1] date: d.date,
.split("</svg>")[0]; count: d.contributionCount,
let x = 0; level: (d.contributionLevel === "FOURTH_QUARTILE" && 4) ||
let lastYAttribute = 0; (d.contributionLevel === "THIRD_QUARTILE" && 3) ||
const rects = Array.from(block.matchAll(/<rect[^>]*>/g)).map(([m]) => { (d.contributionLevel === "SECOND_QUARTILE" && 2) ||
const date = m.match(/data-date="([^"]+)"/)[1]; (d.contributionLevel === "FIRST_QUARTILE" && 1) ||
const count = +m.match(/data-count="([^"]+)"/)[1]; 0,
const level = +m.match(/data-level="([^"]+)"/)[1]; })));
const yAttribute = +m.match(/y="([^"]+)"/)[1];
if (lastYAttribute > yAttribute)
x++;
lastYAttribute = yAttribute;
return { date, count, level, x, yAttribute };
});
const yAttributes = Array.from(new Set(rects.map((c) => c.yAttribute)).keys()).sort();
const cells = rects.map(({ yAttribute, ...c }) => ({
y: yAttributes.indexOf(yAttribute),
...c,
}));
return cells;
}; };
// EXTERNAL MODULE: ../types/grid.ts // EXTERNAL MODULE: ../types/grid.ts
@@ -654,9 +633,9 @@ const getPathToPose = (snake0, target, grid) => {
const generateContributionSnake = async (userName, outputs) => { const generateContributionSnake = async (userName, outputs, options) => {
console.log("🎣 fetching github user contribution"); console.log("🎣 fetching github user contribution");
const cells = await getGithubUserContribution(userName); const cells = await getGithubUserContribution(userName, options);
const grid = userContributionToGrid(cells); const grid = userContributionToGrid(cells);
const snake = snake4; const snake = snake4;
console.log("📡 computing best route"); console.log("📡 computing best route");
@@ -688,14 +667,14 @@ const generateContributionSnake = async (userName, outputs) => {
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "V0": () => (/* binding */ isInside),
/* harmony export */ "HJ": () => (/* binding */ isInsideLarge),
/* harmony export */ "VJ": () => (/* binding */ copyGrid),
/* harmony export */ "Lq": () => (/* binding */ getColor),
/* harmony export */ "xb": () => (/* binding */ isEmpty),
/* harmony export */ "vk": () => (/* binding */ setColor),
/* harmony export */ "Dy": () => (/* binding */ setColorEmpty), /* harmony export */ "Dy": () => (/* binding */ setColorEmpty),
/* harmony export */ "u1": () => (/* binding */ createEmptyGrid) /* harmony export */ "HJ": () => (/* binding */ isInsideLarge),
/* harmony export */ "Lq": () => (/* binding */ getColor),
/* harmony export */ "V0": () => (/* binding */ isInside),
/* harmony export */ "VJ": () => (/* binding */ copyGrid),
/* harmony export */ "u1": () => (/* binding */ createEmptyGrid),
/* harmony export */ "vk": () => (/* binding */ setColor),
/* harmony export */ "xb": () => (/* binding */ isEmpty)
/* harmony export */ }); /* harmony export */ });
/* unused harmony exports isGridEmpty, gridEquals */ /* unused harmony exports isGridEmpty, gridEquals */
const isInside = (grid, x, y) => x >= 0 && y >= 0 && x < grid.width && y < grid.height; const isInside = (grid, x, y) => x >= 0 && y >= 0 && x < grid.width && y < grid.height;
@@ -732,13 +711,13 @@ const createEmptyGrid = (width, height) => ({
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "If": () => (/* binding */ getHeadX),
/* harmony export */ "IP": () => (/* binding */ getHeadY), /* harmony export */ "IP": () => (/* binding */ getHeadY),
/* harmony export */ "If": () => (/* binding */ getHeadX),
/* harmony export */ "JJ": () => (/* binding */ getSnakeLength), /* harmony export */ "JJ": () => (/* binding */ getSnakeLength),
/* harmony export */ "Ks": () => (/* binding */ snakeToCells),
/* harmony export */ "kE": () => (/* binding */ snakeEquals), /* harmony export */ "kE": () => (/* binding */ snakeEquals),
/* harmony export */ "kv": () => (/* binding */ nextSnake), /* harmony export */ "kv": () => (/* binding */ nextSnake),
/* harmony export */ "nJ": () => (/* binding */ snakeWillSelfCollide), /* harmony export */ "nJ": () => (/* binding */ snakeWillSelfCollide),
/* harmony export */ "Ks": () => (/* binding */ snakeToCells),
/* harmony export */ "xG": () => (/* binding */ createSnakeFromCells) /* harmony export */ "xG": () => (/* binding */ createSnakeFromCells)
/* harmony export */ }); /* harmony export */ });
/* unused harmony export copySnake */ /* unused harmony export copySnake */

View File

@@ -2991,7 +2991,7 @@ var external_path_ = __nccwpck_require__(1017);
// EXTERNAL MODULE: ../../node_modules/@actions/core/lib/core.js // EXTERNAL MODULE: ../../node_modules/@actions/core/lib/core.js
var core = __nccwpck_require__(7117); var core = __nccwpck_require__(7117);
;// CONCATENATED MODULE: ./palettes.ts ;// CONCATENATED MODULE: ./palettes.ts
const palettes = { const basePalettes = {
"github-light": { "github-light": {
colorDotBorder: "#1b1f230a", colorDotBorder: "#1b1f230a",
colorDots: ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"], colorDots: ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"],
@@ -3006,10 +3006,8 @@ const palettes = {
}, },
}; };
// aliases // aliases
palettes["github"] = { const palettes = { ...basePalettes };
...palettes["github-light"], palettes["github"] = palettes["github-light"];
dark: { ...palettes["github-dark"] },
};
palettes["default"] = palettes["github"]; palettes["default"] = palettes["github"];
;// CONCATENATED MODULE: ./outputsOptions.ts ;// CONCATENATED MODULE: ./outputsOptions.ts
@@ -3039,6 +3037,7 @@ const parseEntry = (entry) => {
sizeCell: 16, sizeCell: 16,
sizeDot: 12, sizeDot: 12,
...palettes["default"], ...palettes["default"],
dark: palettes["default"].dark && { ...palettes["default"].dark },
}; };
const animationOptions = { step: 1, frameDuration: 100 }; const animationOptions = { step: 1, frameDuration: 100 };
{ {
@@ -3048,6 +3047,13 @@ const parseEntry = (entry) => {
drawOptions.dark = palette.dark && { ...palette.dark }; drawOptions.dark = palette.dark && { ...palette.dark };
} }
} }
{
const dark_palette = palettes[sp.get("dark_palette")];
if (dark_palette) {
const clone = { ...dark_palette, dark: undefined };
drawOptions.dark = clone;
}
}
if (sp.has("color_snake")) if (sp.has("color_snake"))
drawOptions.colorSnake = sp.get("color_snake"); drawOptions.colorSnake = sp.get("color_snake");
if (sp.has("color_dots")) { if (sp.has("color_dots")) {
@@ -3061,6 +3067,8 @@ const parseEntry = (entry) => {
if (sp.has("dark_color_dots")) { if (sp.has("dark_color_dots")) {
const colors = sp.get("dark_color_dots").split(/[,;]/); const colors = sp.get("dark_color_dots").split(/[,;]/);
drawOptions.dark = { drawOptions.dark = {
colorDotBorder: drawOptions.colorDotBorder,
colorSnake: drawOptions.colorSnake,
...drawOptions.dark, ...drawOptions.dark,
colorDots: colors, colorDots: colors,
colorEmpty: colors[0], colorEmpty: colors[0],
@@ -3090,8 +3098,11 @@ const parseEntry = (entry) => {
core.getInput("gif_out_path"), core.getInput("gif_out_path"),
core.getInput("svg_out_path"), core.getInput("svg_out_path"),
]); ]);
const { generateContributionSnake } = await Promise.all(/* import() */[__nccwpck_require__.e(197), __nccwpck_require__.e(317)]).then(__nccwpck_require__.bind(__nccwpck_require__, 5317)); const githubToken = process.env.GITHUB_TOKEN;
const results = await generateContributionSnake(userName, outputs); const { generateContributionSnake } = await Promise.all(/* import() */[__nccwpck_require__.e(197), __nccwpck_require__.e(407)]).then(__nccwpck_require__.bind(__nccwpck_require__, 407));
const results = await generateContributionSnake(userName, outputs, {
githubToken,
});
outputs.forEach((out, i) => { outputs.forEach((out, i) => {
const result = results[i]; const result = results[i];
if (out?.filename && result) { if (out?.filename && result) {

1677
yarn.lock

File diff suppressed because it is too large Load Diff