Compare commits

...

5 Commits
v2.2 ... v2.3.0

Author SHA1 Message Date
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
12 changed files with 887 additions and 776 deletions

View File

@@ -14,9 +14,9 @@ jobs:
node-version: 16
- run: yarn install --frozen-lockfile
- run: yarn type
- run: yarn lint
- run: yarn test --ci
- run: npm run type
- run: npm run lint
- run: npm run test --ci
test-action:
runs-on: ubuntu-latest
@@ -63,7 +63,7 @@ jobs:
- name: build svg-only action
run: |
yarn build:action
npm run build:action
rm -r svg-only/dist
mv packages/action/dist svg-only/dist
@@ -99,7 +99,7 @@ jobs:
node-version: 16
- run: yarn install --frozen-lockfile
- run: yarn build:demo
- run: npm run build:demo
env:
GITHUB_USER_CONTRIBUTION_API_ENDPOINT: https://snk-one.vercel.app/api/github-user-contribution/

View File

@@ -53,7 +53,7 @@ jobs:
- name: build svg-only action
run: |
yarn install --frozen-lockfile
yarn build:action
npm run build:action
rm -r svg-only/dist
mv packages/action/dist svg-only/dist

View File

@@ -11,15 +11,11 @@ Generates a snake game from a github user contributions graph
<picture>
<source
media="(prefers-color-scheme: dark)"
srcset="
https://raw.githubusercontent.com/platane/snk/output/github-contribution-grid-snake-dark.svg
"
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
"
srcset="https://raw.githubusercontent.com/platane/snk/output/github-contribution-grid-snake.svg"
/>
<img
alt="github contribution grid snake animation"

View File

@@ -4,7 +4,7 @@ author: "platane"
runs:
using: docker
image: docker://platane/snk@sha256:bd0f7538482216785abbee29da431738f5ea9aff9fc3a4b8df37708a808f0968
image: docker://platane/snk@sha256:2115ffeb538e355aa155630e6e32b6d77ea2345fa8584645c41ace7f5ad667fc
inputs:
github_user_name:

View File

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

View File

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

View File

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

View File

@@ -39,40 +39,36 @@ export const getGithubUserContribution = async (
};
const parseUserPage = (content: string) => {
// take roughly the svg block
// take roughly the table block
const block = content
.split(`class="js-calendar-graph-svg"`)[1]
.split("</svg>")[0];
.split(`aria-describedby="contribution-graph-description"`)[1]
.split("<tbody>")[1]
.split("</tbody>")[0];
let x = 0;
let lastYAttribute = 0;
const cells = block.split("</tr>").flatMap((inside, y) =>
inside.split("</td>").flatMap((m) => {
const date = m.match(/data-date="([^"]+)"/)?.[1];
const rects = Array.from(block.matchAll(/<rect[^>]*>[^<]*<\/rect>/g)).map(
([m]) => {
const date = m.match(/data-date="([^"]+)"/)![1];
const level = +m.match(/data-level="([^"]+)"/)![1];
const yAttribute = +m.match(/y="([^"]+)"/)![1];
const literalLevel = m.match(/data-level="([^"]+)"/)?.[1];
const literalX = m.match(/data-ix="([^"]+)"/)?.[1];
const literalCount = m.match(/(No|\d+) contributions? on/)?.[1];
const literalCount = m.match(/(No|\d+) contributions? on/)![1];
const count = literalCount === "No" ? 0 : +literalCount;
if (date && literalLevel && literalX && literalCount)
return [
{
x: +literalX,
y,
if (lastYAttribute > yAttribute) x++;
date,
count: +literalCount,
level: +literalLevel,
},
];
lastYAttribute = yAttribute;
return { date, count, level, x, yAttribute };
}
return [];
})
);
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;
};

View File

@@ -2,7 +2,7 @@
"name": "@snk/github-user-contribution",
"version": "1.0.0",
"dependencies": {
"node-fetch": "2.6.7"
"node-fetch": "2.6.12"
},
"devDependencies": {
"@types/node-fetch": "2.6.2"

View File

@@ -1425,6 +1425,20 @@ const isDomainOrSubdomain = function isDomainOrSubdomain(destination, original)
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
*
@@ -1456,7 +1470,7 @@ function fetch(url, opts) {
let error = new AbortError('The user aborted a request.');
reject(error);
if (request.body && request.body instanceof Stream.Readable) {
request.body.destroy(error);
destroyStream(request.body, error);
}
if (!response || !response.body) return;
response.body.emit('error', error);
@@ -1497,9 +1511,43 @@ function fetch(url, opts) {
req.on('error', function (err) {
reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err));
if (response && response.body) {
destroyStream(response.body, err);
}
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) {
clearTimeout(reqTimeout);
@@ -1571,7 +1619,7 @@ function fetch(url, opts) {
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']) {
requestOpts.headers.delete(name);
}
@@ -1664,6 +1712,13 @@ function fetch(url, opts) {
response = new Response(body, response_options);
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;
}
@@ -1683,6 +1738,44 @@ function fetch(url, opts) {
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
*

View File

@@ -78,27 +78,27 @@ const getGithubUserContribution = async (userName, options = {}) => {
return parseUserPage(resText);
};
const parseUserPage = (content) => {
// take roughly the svg block
// take roughly the table block
const block = content
.split(`class="js-calendar-graph-svg"`)[1]
.split("</svg>")[0];
let x = 0;
let lastYAttribute = 0;
const rects = Array.from(block.matchAll(/<rect[^>]*>[^<]*<\/rect>/g)).map(([m]) => {
const date = m.match(/data-date="([^"]+)"/)[1];
const level = +m.match(/data-level="([^"]+)"/)[1];
const yAttribute = +m.match(/y="([^"]+)"/)[1];
const literalCount = m.match(/(No|\d+) contributions? on/)[1];
const count = literalCount === "No" ? 0 : +literalCount;
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,
.split(`aria-describedby="contribution-graph-description"`)[1]
.split("<tbody>")[1]
.split("</tbody>")[0];
const cells = block.split("</tr>").flatMap((inside, y) => inside.split("</td>").flatMap((m) => {
const date = m.match(/data-date="([^"]+)"/)?.[1];
const literalLevel = m.match(/data-level="([^"]+)"/)?.[1];
const literalX = m.match(/data-ix="([^"]+)"/)?.[1];
const literalCount = m.match(/(No|\d+) contributions? on/)?.[1];
if (date && literalLevel && literalX && literalCount)
return [
{
x: +literalX,
y,
date,
count: +literalCount,
level: +literalLevel,
},
];
return [];
}));
return cells;
};

1416
yarn.lock

File diff suppressed because it is too large Load Diff