Initial commit

This commit is contained in:
lowlighter
2020-09-08 18:01:22 +02:00
commit 22d732c3f8
21 changed files with 8567 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

107
.gitignore vendored Normal file
View File

@@ -0,0 +1,107 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# User settings
settings.json

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 lowlighter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

297
README.md Normal file
View File

@@ -0,0 +1,297 @@
# 📊 GitHub metrics
Generates your own GitHub metrics as an SVG image to put them on your profile page or elsewhere !
Below is what it looks like :
![GitHub metrics](https://github.com/lowlighter/lowlighter/blob/master/github-metrics.svg)
## 📜 How to use ?
### ⚙️ Using GitHub Action on your profile repo (~5 min setup)
A GitHub Action which is run periodically at your convenience which generates and push an SVG image on your personal repository.
Assuming your username is `my-github-user`, you can embed your metrics like below :
```markdown
![GitHub metrics](https://github.com/my-github-user/my-github-user/blob/master/github-metrics.svg)
```
```html
<img src="https://github.com/my-github-user/my-github-user/blob/master/github-metrics.svg" alt="My GitHub metrics">
```
<details>
<summary>💬 How to setup in 3 steps</summary>
### 1. Create a GitHub token
In your account settings, go to `Developer settings` and select `Personal access tokens` to create a new token.
You'll need to create a token with the `public_repo` right so this GitHub Action has enough permissions to push the updated SVG metrics on your personal repository.
![Create a GitHub token](https://github.com/lowlighter/metrics/blob/master/docs/imgs/personal_token.png)
### 2. Put your GitHub token in your personal repository secrets
Go to the `Settings` of your personal repository to create a new secret and paste your GitHub token here with the name `METRICS_TOKEN`.
![Setup secret](https://github.com/lowlighter/metrics/blob/master/docs/imgs/repo_secrets.png)
### 3. Create a new GitHub Action workflow on your personal repo
Go to the `Actions` of your personal repository and create a new workflow.
Paste the following and don't forget to put your GitHub username.
```yaml
name: GitHub metrics as SVG image
on:
# Update metrics each 15 minutes. Edit this if you want to increase/decrease frequency
schedule: [{cron: "*/15 * * * *"}]
# Add this if you want to force update each time you commit on master branch
push: {branches: "master"}
jobs:
github-metrics:
runs-on: ubuntu-latest
steps:
- uses: lowlighter/metrics@latest
# This line will prevent this GitHub action from running when it is updated by itself if you enabled trigger on master branch
if: "!contains(github.event.head_commit.message, '[Skip GitHub Action]')"
with:
# Your GitHub token ("public_repo" is required to allow this action to update the metrics SVG image)
token: ${{ secrets.METRICS_TOKEN }}
# Your GitHub user name
user: my-github-user
```
On each run, a new SVG image will be generated and committed to your repository.
Note that this will virtually increase your commits stats, so you could use a bot account instead.
![Action update](https://github.com/lowlighter/metrics/blob/master/docs/imgs/action_update.png)
</details>
### 💕 Using the shared instance (~2 min setup, but with limitations)
For conveniency, you can use the shared instance available at [metrics.lecoq.io](https://metrics.lecoq.io).
Assuming your username is `my-github-user`, you can embed your metrics like below :
```markdown
![GitHub metrics](https://metrics.lecoq.io/my-github-user)
```
```html
<img src="https://metrics.lecoq.io/my-github-user" alt="My GitHub metrics">
```
<details>
<summary>💬 Restrictions and fair use</summary>
Since GitHub API has rate limitations and to avoid abuse, the shared instance has the following limitations :
* Images are cached for 1 day (meaning that your metrics won't be updated until the next day)
* A maximum of 1000 users can use this service
* You're limited to 3 requests per hour (cached metrics are not counted)
You should consider deploying your own instance or use GitHub Action if you're planning using this service.
</details>
### 🏗️ Deploying your own instance (~15 min setup, depending on your sysadmin knowledge)
Using your own instance is useful if you do not want to use GitHub Action or allow others users to use your instance.
A GitHub token is required to setup your instance, however since metrics images are not stored on your repositories you do not need to grant any additional permissions to your token, which reduce security issues.
You can restrict which users can generate metrics on your server and apply rate limiting (which is advised or else you'll hit the GitHub API rate limiter).
It is also easier to change `query.graphql`, `style.css` and `template.svg` if you want to gather additional stats, perform esthetical changes or edit the structure of the SVG image.
<details>
<summary>💬 How to setup in 5 steps</summary>
### 0. Prepare your server
You'll need to have a server at your disposal where you can install and configure stuff.
### 1. Create a GitHub token
In your account settings, go to `Developer settings` and select `Personal access tokens` to create a new token.
As explained above, you do not need to grant additional permissions to the token.
![Create a GitHub token](https://github.com/lowlighter/metrics/blob/master/docs/imgs/personal_token_alt.png)
### 2. Install the dependancies
Connect to your server.
You'll need [NodeJS](https://nodejs.org/en/) (the latter version is better, for reference this was tested on v14.9.0).
Clone the repository
```shell
git clone https://github.com/lowlighter/metrics.git
```
Go inside project and install dependancies :
```shell
cd metrics/
npm install
```
Copy `settings.example.json` to `settings.json`
```shell
cp settings.example.json settings.json
```
### 3. Configure your instance
Open and edit `settings.json` to configure your instance.
```javascript
{
//Your GitHub API token
"token":"****************************************",
//The optionals parameters below allows you to avoid reaching the GitHub API rate limitation
//A set of whitelisted users which can generate metrics on your instance
//Leave empty or undefined to disable
//Defaults to unrestricted
"restricted":["my-github-user"],
//Lifetime of each generated metrics
//If an user's metrics are requested while lifetime is still up, a cached version will be served
//Defaults to 60 minutes
"cached":3600000,
//Maximum simultaneous number of user which can be cached
//When this limit is reached, new users will receive a 503 error
//Defaults to 0 (unlimited)
"maxusers":0,
//Rate limiter
//See https://www.npmjs.com/package/express-rate-limit
//Disabled by default
"ratelimiter":{
"windowMs":60000,
"max":100
},
//Port on which your instance listen
//Defaults to 3000
"port":3000,
//Debug mode
//When enabled, "query.graphql", "style.css" and "template.svg" will be reloaded at each request
//Cache will be disabled
//This is intendend for easier development which allows to see your changes quickly
//Defaults to false
"debug":false,
}
```
### 4. Start your instance
Run the following command to start your instance :
```shell
npm start
```
Open your browser and test your instance :
```shell
http://localhost:3000/my-github-user
```
### 5. Setup as service on your instance (optional)
You should consider using a service to run your instance.
It will allow to restart automatically on crash and on boot.
Create a new file in `/etc/systemd/system` :
```shell
vi /etc/systemd/system/github_metrics.service
```
Paste the following and edit it with the correct paths :
```
[Unit]
Description=GitHub metrics
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/path/to/metrics
ExecStart=/usr/bin/node /path/to/metrics/index.mjs
[Install]
WantedBy=multi-user.target
```
Reload services, enable it and start it :
```shell
systemctl daemon-reload
systemctl enable github_metrics
systemctl start github_metrics
```
Check if your service is up and running :
```shell
systemctl status github_metrics
```
</details>
## 🗂️ Project structure
* `index.mjs` contains the entry points and the settings instance
* `src/app.mjs` contains the server code which serves renders and apply rate limiting, restrictions, etc.
* `src/metrics.mjs` contains metrics renderer
* `src/query.graphql` is the GraphQL query which is sent to GitHub API
* `src/style.css` contains the style for the generated svg image metrics
* `src/template.svg` contains the structure of the generated svg image metrics
* `action/index.mjs` contains the GitHub action code
* `action/dist/index.js` contains compiled the GitHub action code
* `utils/*` contains various utilitaries for build
## ⚠️ HTTP errors code
The following errors code can be encountered if your using a server instance :
* 403 Forbidden : User is not whitelisted in `restricted` users list
* 404 Not found : GitHub API did not found the requested user
* 429 Too many requests : Thrown when rate limiter is trigerred
* 500 Internal error : An error ocurred while generating metrics images (logs can be seen if you're the owner of the instance)
* 503 Service unavailable : Maximum user capacity reached, only already cached images can be accessed for now
## 📚 Documentations
Below is a list of useful documentations links :
* [GitHub GraphQL API](https://docs.github.com/en/graphql)
* [GitHub GraphQL Explorer](https://developer.github.com/v4/explorer/)
## 📦 Used packages
Below is a list of primary dependencies :
* [express/express.js](https://github.com/expressjs/express)
* To serve, compute and render a GitHub user's metrics
* [nfriedly/express-rate-limit](https://github.com/nfriedly/express-rate-limit)
* To apply rate limiting on server and avoid spams and hitting GitHub API's own rate limit
* [octokit/graphql.js](https://github.com/octokit/graphql.js/)
* To perform request to GitHub GraphQL API
* [ptarjan/node-cache](https://github.com/ptarjan/node-cache)
* To cache generated content and reduce
* [renanbastos93/image-to-base64](https://github.com/renanbastos93/image-to-base64)
* To generate base64 representation of users' avatars
All icons were ripped across GitHub's site, but still remains the intellectual property of GitHub.
See [GitHub Logos and Usage](https://github.com/logos) for more information.
## ✨ Inspirations
This project was inspired by the following projects :
* [anuraghazra/github-readme-stats](https://github.com/anuraghazra/github-readme-stats)
* [jstrieb/github-stats](https://github.com/jstrieb/github-stats)

19
action.yml Normal file
View File

@@ -0,0 +1,19 @@
name: GitHub metrics as SVG image
author: lowlighter
description: Generate an user's GitHub metrics as SVG image format to embed somewhere else
branding:
icon: user-check
color: gray-dark
inputs:
token:
description: GitHub Personal Token (require "public_repo" permissions)
required: true
user:
description: Target GitHub user
required: true
filename:
description: Name of SVG image output
default: github-metrics.svg
runs:
using: node12
main: action/dist/index.js

6417
action/dist/index.js vendored Normal file

File diff suppressed because it is too large Load Diff

61
action/index.mjs Normal file
View File

@@ -0,0 +1,61 @@
//Imports
import path from "path"
import * as _metrics from "./../src/metrics.mjs"
import * as _octokit from "@octokit/graphql"
import * as _core from "@actions/core"
import * as _github from "@actions/github"
;((async function () {
//Hack because ES modules are not correctly transpiled with ncc
const [core, github, octokit, metrics] = [_core, _github, _octokit, _metrics].map(m => (m && m.default) ? m.default : m)
//Runner
try {
//Initialization
console.log(`GitHub metrics as SVG image`)
console.log(`========================================================`)
//Load svg template, style and query
const template = `<#include template.svg>`, style = `<#include style.css>`, query = `<#include query.graphql>`
console.log(`Templates | loaded`)
//Initialization
const [token, user, filename] = [core.getInput("token"), core.getInput("user"), core.getInput("filename", {default:"github-metrics.svg"})]
const output = path.join(filename)
console.log(`GitHub user | ${user}`)
console.log(`Output file | ${output}`)
console.log(`Github token | ${token ? "provided" : "missing"}`)
if (!token)
throw new Error("You must provide a valid GitHub token")
const graphql = octokit.graphql.defaults({headers:{authorization: `token ${token}`}})
const rest = github.getOctokit(token)
//Render metrics
const rendered = await metrics({login:user}, {template, style, query, graphql})
console.log(`Render | complete`)
//Commit to repository
let sha = undefined
try {
const {data} = await rest.repos.getContent({
owner:user,
repo:user,
path:filename,
})
sha = data.sha
} catch (error) { }
console.log(`Previous render sha | ${sha || "none"}`)
await rest.repos.createOrUpdateFileContents({
owner:user, repo:user, path:filename, sha, message:`Update ${filename} - [Skip GitHub Action]`,
content:Buffer.from(rendered).toString("base64"),
})
console.log(`Commit to repo | ok`)
//Success
console.log(`Success !`)
//Errors
} catch (error) {
console.error(error)
core.setFailed(error.message)
process.exit(1)
}
})()).catch(error => process.exit(1))

BIN
docs/imgs/action_update.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/imgs/repo_secrets.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

5
index.mjs Normal file
View File

@@ -0,0 +1,5 @@
//Imports
import app from "./src/app.mjs"
//Start app
await app()

1044
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "metrics",
"version": "1.0.0",
"description": "Generate an user's GitHub metrics as SVG image format to embed somewhere else",
"main": "index.mjs",
"scripts": {
"start": "node index.mjs",
"build-ncc": "npx ncc build action/index.mjs --out action/dist",
"build-post": "node utils/post_build.mjs",
"build": "npx run-s build-ncc build-post",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/lowlighter/metrics.git"
},
"author": "lowlighter",
"license": "MIT",
"bugs": {
"url": "https://github.com/lowlighter/metrics/issues"
},
"homepage": "https://github.com/lowlighter/metrics#readme",
"dependencies": {
"@actions/core": "^1.2.5",
"@actions/github": "^4.0.0",
"@octokit/graphql": "^4.5.4",
"express": "^4.17.1",
"express-rate-limit": "^5.1.3",
"image-to-base64": "^2.1.1",
"memory-cache": "^0.2.0"
},
"devDependencies": {
"@vercel/ncc": "^0.24.0",
"npm-run-all": "^4.1.5"
}
}

9
settings.example.json Normal file
View File

@@ -0,0 +1,9 @@
{
"token":"MY GITHUB API TOKEN", "//":"Your own GitHub API token (required)",
"restricted":[], "//":"List of authorized users, leave empty for unrestricted",
"cached":3600000, "//":"Cached time for generated images, 0 to disable",
"maxusers":0, "//":"Maximum number of users, 0 for unlimited",
"ratelimiter":null, "//":"Rate limiter (see express-rate-limit documentation for options)",
"port":3000, "//":"Listening port",
"debug":false, "//":"Debug mode"
}

99
src/app.mjs Normal file
View File

@@ -0,0 +1,99 @@
//Imports
import express from "express"
import fs from "fs"
import path from "path"
import octokit from "@octokit/graphql"
import cache from "memory-cache"
import ratelimit from "express-rate-limit"
import metrics from "./metrics.mjs"
//Load svg template, style and query
async function load() {
return await Promise.all(["template.svg", "style.css", "query.graphql"].map(async file => `${await fs.promises.readFile(path.join("src", file))}`))
}
//Setup
export default async function setup() {
//Load settings
const settings = JSON.parse((await fs.promises.readFile(path.join("settings.json"))).toString())
console.log(settings)
const {token, maxusers = 0, restricted = [], debug = false, cached = 30*60*1000, port = 3000, ratelimiter = null} = settings
//Load svg template, style and query
let [template, style, query] = await load()
const graphql = octokit.graphql.defaults({headers:{authorization: `token ${token}`}})
//Setup server
const app = express()
const middlewares = []
//Rate limiter middleware
if (ratelimiter) {
app.set("trust proxy", 1)
middlewares.push(ratelimit({
skip(req, res) { return !!cache.get(req.params.login) },
message:"Too many requests",
...ratelimiter
}))
}
//Cache headers middleware
middlewares.push((req, res, next) => {
res.header("Cache-Control", cached ? `public, max-age=${cached}` : "no-store, no-cache")
next()
})
//Base routes
app.get("/", (req, res) => res.redirect("https://github.com/lowlighter/metrics"))
app.get("/favicon.ico", (req, res) => res.sendStatus(204))
//Metrics
app.get("/:login", ...middlewares, async (req, res) => {
//Request params
const {login} = req.params
if ((restricted.length)&&(!restricted.includes(login)))
return res.sendStatus(403)
//Read cached data if possible
if ((!debug)&&(cached)&&(cache.get(login))) {
res.header("Content-Type", "image/svg+xml")
res.send(cache.get(login))
return
}
//Maximum simultaneous users
if ((maxusers)&&(cache.size()+1 > maxusers))
return res.sendStatus(503)
//Compute rendering
try {
//Render
if (debug)
[template, style, query] = await load()
const rendered = await metrics({login}, {template, style, query, graphql})
//Cache
if ((!debug)&&(cached))
cache.put(login, rendered, cached)
//Send response
res.header("Content-Type", "image/svg+xml")
res.send(rendered)
}
//Internal error
catch (error) {
//Not found user
if ((error instanceof Error)&&(/^user not found$/.test(error.message)))
return res.sendStatus(404)
//General error
console.error(error)
res.sendStatus(500)
}
})
//Listen
app.listen(port, () => console.log([
`Listening on port | ${port}`,
`Debug mode | ${debug}`,
`Restricted to users | ${restricted.size ? [...restricted].join(", ") : "(unrestricted)"}`,
`Cached time | ${cached} seconds`,
`Rate limiter | ${ratelimiter ? JSON.stringify(ratelimiter) : "(enabled)"}`,
`Max simultaneous users | ${maxusers ? `${maxusers} users` : "(unrestricted)"}`
].join("\n")))
}

67
src/metrics.mjs Normal file
View File

@@ -0,0 +1,67 @@
//Imports
import imgb64 from "image-to-base64"
//Setup
export default async function metrics({login}, {template, style, query, graphql}) {
//Compute rendering
try {
//Query data from GitHub API
const data = await graphql(query
.replace(/[$]login/, `"${login}"`)
.replace(/[$]calendar.to/, `"${(new Date()).toISOString()}"`)
.replace(/[$]calendar.from/, `"${(new Date(Date.now()-14*24*60*60*1000)).toISOString()}"`)
)
//Init
const languages = {colors:{}, total:0, stats:{}}
const computed = data.computed = {commits:0, languages, repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_merged:0, forks:0}}
const avatar = imgb64(data.user.avatarUrl)
//Iterate through user's repositories
for (const repository of data.user.repositories.nodes) {
//Simple properties with totalCount
for (const property of ["watchers", "stargazers", "issues_open", "issues_closed", "pr_open", "pr_merged"])
computed.repositories[property] += repository[property].totalCount
//Forks
computed.repositories.forks += repository.forkCount
//Languages
for (const {size, node:{color, name}} of Object.values(repository.languages.edges)) {
languages.stats[name] = (languages.stats[name] || 0) + size
languages.colors[name] = color || "#ededed"
languages.total += size
}
}
//Compute count for issues and pull requests
for (const property of ["issues", "pr"])
computed.repositories[`${property}_count`] = computed.repositories[`${property}_open`] + computed.repositories[`${property}_${property === "pr" ? "merged" : "closed"}`]
//Compute total commits and sponsorships
computed.commits = data.user.contributionsCollection.totalCommitContributions + data.user.contributionsCollection.restrictedContributionsCount
computed.sponsorships = data.user.sponsorshipsAsSponsor.totalCount + data.user.sponsorshipsAsMaintainer.totalCount
//Compute registration date
const diff = (Date.now()-(new Date(data.user.createdAt)).getTime())/(365*24*60*60*1000)
const years = Math.floor(diff)
const months = Math.ceil((diff-years)*12)
computed.registration = years ? `${years} year${years > 1 ? "s" : ""} ago` : `${months} month${months > 1 ? "s" : ""} ago`
//Compute languages stats
Object.keys(languages.stats).map(name => languages.stats[name] /= languages.total)
languages.favorites = Object.entries(languages.stats).sort(([an, a], [bn, b]) => b - a).slice(0, 8).map(([name, value]) => ({name, value, color:languages.colors[name], x:0}))
for (let i = 1; i < languages.favorites.length; i++)
languages.favorites[i].x = languages.favorites[i-1].x + languages.favorites[i-1].value
//Compute calendar
computed.calendar = data.user.calendar.contributionCalendar.weeks.flatMap(({contributionDays}) => contributionDays).slice(0, 14).reverse()
//Avatar (base64)
computed.avatar = await avatar || "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
//Eval rendering and return
return eval(`\`${template}\``)
}
//Internal error
catch (error) { throw (((Array.isArray(error.errors))&&(error.errors[0].type === "NOT_FOUND")) ? new Error("user not found") : error) }
}

82
src/query.graphql Normal file
View File

@@ -0,0 +1,82 @@
query Metrics {
user(login: $login) {
name
login
createdAt
avatarUrl
repositories(last: 100, isFork: false, ownerAffiliations: OWNER) {
totalCount
nodes {
watchers {
totalCount
}
stargazers {
totalCount
}
languages(first: 4) {
edges {
size
node {
color
name
}
}
}
issues_open: issues(states: OPEN) {
totalCount
}
issues_closed: issues(states: CLOSED) {
totalCount
}
pr_open: pullRequests(states: OPEN) {
totalCount
}
pr_merged: pullRequests(states: MERGED) {
totalCount
}
forkCount
}
}
packages {
totalCount
}
starredRepositories {
totalCount
}
watching {
totalCount
}
sponsorshipsAsSponsor {
totalCount
}
sponsorshipsAsMaintainer {
totalCount
}
contributionsCollection {
totalRepositoriesWithContributedCommits
totalCommitContributions
restrictedContributionsCount
totalIssueContributions
totalPullRequestContributions
totalPullRequestReviewContributions
}
calendar:contributionsCollection(from: $calendar.from, to: $calendar.to) {
contributionCalendar {
weeks {
contributionDays {
color
}
}
}
}
repositoriesContributedTo {
totalCount
}
followers {
totalCount
}
following {
totalCount
}
}
}

86
src/style.css Normal file
View File

@@ -0,0 +1,86 @@
svg {
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
font-size: 14px;
color: #777777;
}
svg.bar {
margin: 4px 0;
}
h1, h2, h3 {
margin: 8px 0 2px;
padding: 0;
color: #0366d6;
font-weight: normal;
}
h1 {
font-size: 20px;
font-weight: bold;
}
h2 {
font-size: 16px;
}
h2 svg {
fill: #0366d6;
}
h3 {
font-size: 14px;
}
.field {
display: flex;
align-items: center;
margin-bottom: 2px;
}
section > .field {
margin-left: 5px;
margin-right: 5px;
}
.field svg {
margin: 0 8px;
fill: #959da5;
}
.column {
display: flex;
flex-direction: column;
align-items: center;
}
.center {
justify-content: center;
}
.horizontal {
justify-content: space-around;
}
.horizontal-wrap {
flex-wrap: wrap;
}
.horizontal .field {
flex: 1 1 0;
}
.row {
display: flex;
}
.row section {
flex: 1 1 0;
}
.no-wrap {
white-space: nowrap;
}
.avatar {
background-color: #000000;
border-radius: 50%;
margin: 0 6px;
}
.calendar.field {
margin: 4px 0;
margin-left: 7px;
}
.calendar .day {
outline: 1px solid rgba(27,31,35,.04);
outline-offset: -1px;
}
footer {
margin-top: 8px;
text-align: right;
font-size: 8px;
font-style: italic;
opacity: 0.5;
}

203
src/template.svg Normal file
View File

@@ -0,0 +1,203 @@
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="480">
<style>
${style}
</style>
<foreignObject x="0" y="0" width="100%" height="100%">
<div xmlns="http://www.w3.org/1999/xhtml">
<section>
<h1 class="field">
<img class="avatar" src="data:image/png;base64,${data.computed.avatar}" width="20" height="20" />
<span>${data.user.name || data.user.login}</span>
</h1>
<div class="row">
<section>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zm.5 4.75a.75.75 0 00-1.5 0v3.5a.75.75 0 00.471.696l2.5 1a.75.75 0 00.557-1.392L8.5 7.742V4.75z"></path></svg>
Joined GitHub ${data.computed.registration}
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M5.5 3.5a2 2 0 100 4 2 2 0 000-4zM2 5.5a3.5 3.5 0 115.898 2.549 5.507 5.507 0 013.034 4.084.75.75 0 11-1.482.235 4.001 4.001 0 00-7.9 0 .75.75 0 01-1.482-.236A5.507 5.507 0 013.102 8.05 3.49 3.49 0 012 5.5zM11 4a.75.75 0 100 1.5 1.5 1.5 0 01.666 2.844.75.75 0 00-.416.672v.352a.75.75 0 00.574.73c1.2.289 2.162 1.2 2.522 2.372a.75.75 0 101.434-.44 5.01 5.01 0 00-2.56-3.012A3 3 0 0011 4z"></path></svg>
Followed by ${data.user.followers.totalCount} user${data.user.followers.totalCount > 1 ? "s" : ""}
</div>
</section>
<section>
<div class="field calendar">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${data.computed.calendar.length*15} 11" width="${data.computed.calendar.length*15}" height="16">
<g>
${data.computed.calendar.map(({color}, x) => `
<rect class="day" x="${x*15}" y="0" width="11" height="11" fill="${color}" rx="2" ry="2" />
`).join("")}
</g>
</svg>
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1 2.5A2.5 2.5 0 013.5 0h8.75a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0V1.5h-8a1 1 0 00-1 1v6.708A2.492 2.492 0 013.5 9h3.25a.75.75 0 010 1.5H3.5a1 1 0 100 2h5.75a.75.75 0 010 1.5H3.5A2.5 2.5 0 011 11.5v-9zm13.23 7.79a.75.75 0 001.06-1.06l-2.505-2.505a.75.75 0 00-1.06 0L9.22 9.229a.75.75 0 001.06 1.061l1.225-1.224v6.184a.75.75 0 001.5 0V9.066l1.224 1.224z"></path></svg>
Contributed to ${data.user.repositoriesContributedTo.totalCount} repositor${data.user.repositoriesContributedTo.totalCount > 1 ? "ies" : "y"}
</div>
</section>
</div>
</section>
<div class="row">
<section>
<h2 class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M1.5 1.75a.75.75 0 00-1.5 0v12.5c0 .414.336.75.75.75h14.5a.75.75 0 000-1.5H1.5V1.75zm14.28 2.53a.75.75 0 00-1.06-1.06L10 7.94 7.53 5.47a.75.75 0 00-1.06 0L3.22 8.72a.75.75 0 001.06 1.06L7 7.06l2.47 2.47a.75.75 0 001.06 0l5.25-5.25z"></path></svg>
Activity
</h2>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M10.5 7.75a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0zm1.43.75a4.002 4.002 0 01-7.86 0H.75a.75.75 0 110-1.5h3.32a4.001 4.001 0 017.86 0h3.32a.75.75 0 110 1.5h-3.32z"></path></svg>
${data.computed.commits} Commit${data.computed.commits > 1 ? "s" : ""}
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M2.5 1.75a.25.25 0 01.25-.25h8.5a.25.25 0 01.25.25v7.736a.75.75 0 101.5 0V1.75A1.75 1.75 0 0011.25 0h-8.5A1.75 1.75 0 001 1.75v11.5c0 .966.784 1.75 1.75 1.75h3.17a.75.75 0 000-1.5H2.75a.25.25 0 01-.25-.25V1.75zM4.75 4a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-4.5zM4 7.75A.75.75 0 014.75 7h2a.75.75 0 010 1.5h-2A.75.75 0 014 7.75zm11.774 3.537a.75.75 0 00-1.048-1.074L10.7 14.145 9.281 12.72a.75.75 0 00-1.062 1.058l1.943 1.95a.75.75 0 001.055.008l4.557-4.45z"></path></svg>
${data.user.contributionsCollection.totalPullRequestReviewContributions} Pull request${data.user.contributionsCollection.totalPullRequestReviewContributions > 1 ? "s" : ""} reviewed
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"></path></svg>
${data.user.contributionsCollection.totalPullRequestContributions} Pull request${data.user.contributionsCollection.totalPullRequestContributions > 1 ? "s" : ""} opened
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"></path></svg>
${data.user.contributionsCollection.totalIssueContributions} Issue${data.user.contributionsCollection.totalIssueContributions > 1 ? "s" : ""} opened
</div>
</section>
<section>
<h2 class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M1.75 0A1.75 1.75 0 000 1.75v12.5C0 15.216.784 16 1.75 16h12.5A1.75 1.75 0 0016 14.25V1.75A1.75 1.75 0 0014.25 0H1.75zM1.5 1.75a.25.25 0 01.25-.25h12.5a.25.25 0 01.25.25v12.5a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25V1.75zM11.75 3a.75.75 0 00-.75.75v7.5a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75zm-8.25.75a.75.75 0 011.5 0v5.5a.75.75 0 01-1.5 0v-5.5zM8 3a.75.75 0 00-.75.75v3.5a.75.75 0 001.5 0v-3.5A.75.75 0 008 3z"></path></svg>
Community stats
</h2>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M5.5 3.5a2 2 0 100 4 2 2 0 000-4zM2 5.5a3.5 3.5 0 115.898 2.549 5.507 5.507 0 013.034 4.084.75.75 0 11-1.482.235 4.001 4.001 0 00-7.9 0 .75.75 0 01-1.482-.236A5.507 5.507 0 013.102 8.05 3.49 3.49 0 012 5.5zM11 4a.75.75 0 100 1.5 1.5 1.5 0 01.666 2.844.75.75 0 00-.416.672v.352a.75.75 0 00.574.73c1.2.289 2.162 1.2 2.522 2.372a.75.75 0 101.434-.44 5.01 5.01 0 00-2.56-3.012A3 3 0 0011 4z"></path></svg>
Following ${data.user.following.totalCount} user${data.user.followers.totalCount > 1 ? "s" : ""}
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.565 20.565 0 008 13.393a20.561 20.561 0 003.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.75.75 0 01-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5zM8 14.25l-.345.666-.002-.001-.006-.003-.018-.01a7.643 7.643 0 01-.31-.17 22.075 22.075 0 01-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.08 22.08 0 01-3.744 2.584l-.018.01-.006.003h-.002L8 14.25zm0 0l.345.666a.752.752 0 01-.69 0L8 14.25z"></path></svg>
Sponsoring ${data.computed.sponsorships} repositor${data.computed.sponsorships > 1 ? "ies" : "y"}
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"></path></svg>
Starred ${data.user.starredRepositories.totalCount} repositor${data.user.starredRepositories.totalCount > 1 ? "ies" : "y"}
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M1.679 7.932c.412-.621 1.242-1.75 2.366-2.717C5.175 4.242 6.527 3.5 8 3.5c1.473 0 2.824.742 3.955 1.715 1.124.967 1.954 2.096 2.366 2.717a.119.119 0 010 .136c-.412.621-1.242 1.75-2.366 2.717C10.825 11.758 9.473 12.5 8 12.5c-1.473 0-2.824-.742-3.955-1.715C2.92 9.818 2.09 8.69 1.679 8.068a.119.119 0 010-.136zM8 2c-1.981 0-3.67.992-4.933 2.078C1.797 5.169.88 6.423.43 7.1a1.619 1.619 0 000 1.798c.45.678 1.367 1.932 2.637 3.024C4.329 13.008 6.019 14 8 14c1.981 0 3.67-.992 4.933-2.078 1.27-1.091 2.187-2.345 2.637-3.023a1.619 1.619 0 000-1.798c-.45-.678-1.367-1.932-2.637-3.023C11.671 2.992 9.981 2 8 2zm0 8a2 2 0 100-4 2 2 0 000 4z"></path></svg>
Watching ${data.user.watching.totalCount} repositor${data.user.watching.totalCount > 1 ? "ies" : "y"}
</div>
</section>
</div>
<section>
<h2 class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"></path></svg>
${data.user.repositories.totalCount} Repositor${data.user.repositories.totalCount > 1 ? "ies" : "y"}
</h2>
<div class="row">
<section>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"></path></svg>
${data.computed.repositories.stargazers} Stargazer${data.computed.repositories.stargazers > 1 ? "s" : ""}
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M8.878.392a1.75 1.75 0 00-1.756 0l-5.25 3.045A1.75 1.75 0 001 4.951v6.098c0 .624.332 1.2.872 1.514l5.25 3.045a1.75 1.75 0 001.756 0l5.25-3.045c.54-.313.872-.89.872-1.514V4.951c0-.624-.332-1.2-.872-1.514L8.878.392zM7.875 1.69a.25.25 0 01.25 0l4.63 2.685L8 7.133 3.245 4.375l4.63-2.685zM2.5 5.677v5.372c0 .09.047.171.125.216l4.625 2.683V8.432L2.5 5.677zm6.25 8.271l4.625-2.683a.25.25 0 00.125-.216V5.677L8.75 8.432v5.516z"></path></svg>
${data.user.packages.totalCount} Package${data.user.packages.totalCount > 1 ? "s" : ""}
</div>
</section>
<section>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path></svg>
${data.computed.repositories.forks} Fork${data.computed.repositories.forks > 1 ? "s" : ""}
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M1.679 7.932c.412-.621 1.242-1.75 2.366-2.717C5.175 4.242 6.527 3.5 8 3.5c1.473 0 2.824.742 3.955 1.715 1.124.967 1.954 2.096 2.366 2.717a.119.119 0 010 .136c-.412.621-1.242 1.75-2.366 2.717C10.825 11.758 9.473 12.5 8 12.5c-1.473 0-2.824-.742-3.955-1.715C2.92 9.818 2.09 8.69 1.679 8.068a.119.119 0 010-.136zM8 2c-1.981 0-3.67.992-4.933 2.078C1.797 5.169.88 6.423.43 7.1a1.619 1.619 0 000 1.798c.45.678 1.367 1.932 2.637 3.024C4.329 13.008 6.019 14 8 14c1.981 0 3.67-.992 4.933-2.078 1.27-1.091 2.187-2.345 2.637-3.023a1.619 1.619 0 000-1.798c-.45-.678-1.367-1.932-2.637-3.023C11.671 2.992 9.981 2 8 2zm0 8a2 2 0 100-4 2 2 0 000 4z"></path></svg>
${data.computed.repositories.watchers} Watcher${data.computed.repositories.watchers > 1 ? "s" : ""}
</div>
</section>
</div>
</section>
<div class="row">
<section class="column">
<h3>Issues</h3>
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="220" height="8">
<mask id="issues-bar">
<rect x="0" y="0" width="220" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#issues-bar)" x="0" y="0" width="${data.computed.repositories.issues_count ? 0 : 220}" height="8" fill="#d1d5da"/>
<rect mask="url(#issues-bar)" x="0" y="0" width="${(data.computed.repositories.issues_closed/data.computed.repositories.issues_count)*220 || 0}" height="8" fill="#d73a49"/>
<rect mask="url(#issues-bar)" x="${(data.computed.repositories.issues_closed/data.computed.repositories.issues_count)*220 || 0}" y="0" width="${(1-data.computed.repositories.issues_closed/data.computed.repositories.issues_count)*220 || 0}" height="8" fill="#28a745"/>
</svg>
<div class="field horizontal" style="width:220">
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill="#d73a49" fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 0110.65-5.003.75.75 0 00.959-1.153 8 8 0 102.592 8.33.75.75 0 10-1.444-.407A6.5 6.5 0 011.5 8zM8 12a1 1 0 100-2 1 1 0 000 2zm0-8a.75.75 0 01.75.75v3.5a.75.75 0 11-1.5 0v-3.5A.75.75 0 018 4zm4.78 4.28l3-3a.75.75 0 00-1.06-1.06l-2.47 2.47-.97-.97a.749.749 0 10-1.06 1.06l1.5 1.5a.75.75 0 001.06 0z"></path></svg>
${data.computed.repositories.issues_closed} Closed
</div>
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill="#28a745" fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"></path></svg>
${data.computed.repositories.issues_open} Open
</div>
</div>
</section>
<section class="column">
<h3>Pull requests</h3>
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="220" height="8">
<mask id="pr-bar">
<rect x="0" y="0" width="220" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#pr-bar)" x="0" y="0" width="${data.computed.repositories.pr_count ? 0 : 220}" height="8" fill="#d1d5da"/>
<rect mask="url(#pr-bar)" x="0" y="0" width="${(data.computed.repositories.pr_merged/data.computed.repositories.pr_count)*220 || 0}" height="8" fill="#6f42c1"/>
<rect mask="url(#pr-bar)" x="${(data.computed.repositories.pr_merged/data.computed.repositories.pr_count)*220 || 0}" y="0" width="${(1-data.computed.repositories.pr_merged/data.computed.repositories.pr_count)*220 || 0}" height="8" fill="#28a745"/>
</svg>
<div class="field horizontal" style="width:220">
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill="#6f42c1" fill-rule="evenodd" d="M5 3.254V3.25v.005a.75.75 0 110-.005v.004zm.45 1.9a2.25 2.25 0 10-1.95.218v5.256a2.25 2.25 0 101.5 0V7.123A5.735 5.735 0 009.25 9h1.378a2.251 2.251 0 100-1.5H9.25a4.25 4.25 0 01-3.8-2.346zM12.75 9a.75.75 0 100-1.5.75.75 0 000 1.5zm-8.5 4.5a.75.75 0 100-1.5.75.75 0 000 1.5z"></path></svg>
${data.computed.repositories.pr_merged} Merged
</div>
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill="#28a745" fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"></path></svg>
${data.computed.repositories.pr_open} Open
</div>
</div>
</section>
</div>
<section class="column">
<h3>Most used languages</h3>
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="460" height="8">
<mask id="languages-bar">
<rect x="0" y="0" width="460" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#languages-bar)" x="0" y="0" width="${data.computed.languages.favorites.length ? 0 : 460}" height="8" fill="#d1d5da"/>
${data.computed.languages.favorites.map(({name, value, color, x}) => `
<rect mask="url(#languages-bar)" x="${x*460}" y="0" width="${value*460}" height="8" fill="${color}"/>
`).join("")}
</svg>
<div class="field horizontal horizontal-wrap" style="width:460">
${data.computed.languages.favorites.map(({name, color}) => `
<div class="field center no-wrap">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill="${color}" fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"></path></svg>
${name}
</div>
`).join("")}
</div>
</section>
<footer>
Last updated ${new Date()}
</footer>
</div>
</foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 19 KiB

12
utils/post_build.mjs Normal file
View File

@@ -0,0 +1,12 @@
//Imports
import fs from "fs"
import path from "path"
//Perform static includes
let generated = `${await fs.promises.readFile(path.join("action/dist", "index.js"))}`
for (const match of [...generated.match(/(?<=`)<#include (.+?)>(?=`)/g)]) {
const file = match.match(/<#include (.+?)>/)[1]
generated = generated.replace(`<#include ${file}>`, `${await fs.promises.readFile(path.join("src", file))}`.replace(/([$`])/g, "\\$1"))
console.log(`Included ${file}`)
}
await fs.promises.writeFile(path.join("action/dist", "index.js"), generated)