feat(plugins/code): add new code plugin (#526)

This commit is contained in:
Simon Lecoq
2021-09-13 13:31:36 +02:00
committed by GitHub
parent 47a0689d4c
commit 842aee763f
11 changed files with 256 additions and 10 deletions

View File

@@ -208,16 +208,15 @@ export async function which(command) {
return false
}
/**Code hightlighter */
export async function highlight(code, lang) {
return lang in prism.languages ? prism.highlight(code, prism.languages[lang]) : code
}
/**Markdown-html sanitizer-interpreter */
export async function markdown(text, {mode = "inline", codelines = Infinity} = {}) {
//Sanitize user input once to prevent injections and parse into markdown
let rendered = await marked(htmlunescape(htmlsanitize(text)), {
highlight(code, lang) {
return lang in prism.languages ? prism.highlight(code, prism.languages[lang]) : code
},
silent:true,
xhtml:true,
})
let rendered = await marked(htmlunescape(htmlsanitize(text)), {highlight, silent:true, xhtml:true})
//Markdown mode
switch (mode) {
case "inline": {

View File

@@ -35,6 +35,7 @@ export default function({faker}, target, that, args) {
email:faker.internet.email(),
date:`${faker.date.recent(7)}`,
},
url:"https://api.github.com/repos/lowlighter/metrics/commits/MOCKED_SHA",
},
author:{
login:faker.internet.userName(),

View File

@@ -336,6 +336,23 @@
},
})
: null),
//Code snippet
...(set.plugins.enabled.code
? ({
code: {
snippet: {
sha: faker.git.shortSha(),
message: faker.lorem.sentence(),
filename: 'docs/specifications.html',
status: "modified",
additions: faker.datatype.number(50),
deletions: faker.datatype.number(50),
patch: `<span class="token coord">@@ -0,0 +1,5 @@</span><br> //Imports<br><span class="token inserted">+ import app from "./src/app.mjs"</span><br><span class="token deleted">- import app from "./src/app.js"</span><br> //Start app<br> await app()<br>\\ No newline at end of file`,
repo: `${faker.random.word()}/${faker.random.word()}`,
},
}
})
: null),
//Languages
...(set.plugins.enabled.languages
? ({

View File

@@ -0,0 +1,27 @@
### ♐ Code snippet of the day
> ⚠️ When improperly configured, this plugin could display private code. If you work with sensitive data or company code, it is advised to keep this plugin disabled. *Metrics* and its authors cannot be held responsible for any resulting code leaks, use at your own risk.
Display a random code snippet from your recent activity history.
<table>
<td align="center">
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.code.svg">
<img width="900" height="1" alt="">
</td>
</table>
#### Examples workflows
[➡️ Available options for this plugin](metadata.yml)
```yaml
- uses: lowlighter/metrics@latest
with:
# ... other options
plugin_code: yes
plugin_code_lines: 12 # Only display snippets with less than 12 lines
plugin_code_load: 100 # Fetch 100 events from activity
plugin_code_visibility: public # Only display snippets from public activity
plugin_code_skipped: github/octocat # Skip github/octocat repository
```

View File

@@ -0,0 +1,66 @@
//Setup
export default async function({login, q, imports, data, rest, account}, {enabled = false} = {}) {
//Plugin execution
try {
//Check if plugin is enabled and requirements are met
if ((!enabled)||(!q.code))
return null
//Context
let context = {mode:"user"}
if (q.repo) {
console.debug(`metrics/compute/${login}/plugins > code > switched to repository mode`)
const {owner, repo} = data.user.repositories.nodes.map(({name:repo, owner:{login:owner}}) => ({repo, owner})).shift()
context = {...context, mode:"repository", owner, repo}
}
//Load inputs
let {load, lines, visibility, skipped} = imports.metadata.plugins.code.inputs({data, q, account})
skipped.push(...data.shared["repositories.skipped"])
const pages = Math.ceil(load / 100)
//Get user recent code
console.debug(`metrics/compute/${login}/plugins > code > querying api`)
const events = []
try {
for (let page = 1; page <= pages; page++) {
console.debug(`metrics/compute/${login}/plugins > code > loading page ${page}/${pages}`)
events.push(...[...await Promise.all([...(context.mode === "repository" ? await rest.activity.listRepoEvents({owner:context.owner, repo:context.repo}) : await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100, page})).data
.filter(({type}) => type === "PushEvent")
.filter(({actor}) => account === "organization" ? true : actor.login?.toLocaleLowerCase() === login.toLocaleLowerCase())
.filter(({repo:{name:repo}}) => !((skipped.includes(repo.split("/").pop())) || (skipped.includes(repo))))
.filter(event => visibility === "public" ? event.public : true)
.flatMap(({payload}) => Promise.all(payload.commits.map(async commit => (await rest.request(commit.url)).data)))])]
.flat()
.filter(({author}) => data.shared["commits.authoring"].filter(authoring => author?.email?.toLocaleLowerCase().includes(authoring)||author?.name?.toLocaleLowerCase().includes(authoring)))
)
}
}
catch {
console.debug(`metrics/compute/${login}/plugins > code > no more page to load`)
}
console.debug(`metrics/compute/${login}/plugins > code > ${events.length} events loaded`)
//Search for a random snippet
const files = events
.flatMap(({sha, commit:{message, url}, files}) => files.map(({filename, status, additions, deletions, patch}) => ({sha, message, filename, status, additions, deletions, patch, repo:url.match(/repos[/](?<repo>[\s\S]+)[/]git[/]commits/)?.groups?.repo})))
.filter(({patch}) => (patch ? (patch.match(/\n/mg)?.length ?? 1) : Infinity) < lines)
const snippet = files[Math.floor(Math.random()*files.length)]
//Trim common indent from content and change line feed
if (!snippet.patch.split("\n").shift().endsWith("@@"))
snippet.patch = snippet.patch.replace(/^(?<coord>@@.*?@@)/, "$<coord>\n")
const indent = Math.min(...(snippet.patch.match(/^[+-]? +/mg)?.map(indent => (indent.length ?? Infinity) - indent.startsWith("+") - indent.startsWith("-")) ?? [])) || 0
const content = imports.htmlescape(snippet.patch.replace(/\r\n/mg, "\n").replace(new RegExp(`^([+-]?)${" ".repeat(indent)}`, "mg"), "$1"))
//Format patch
snippet.patch = imports.htmlunescape((await imports.highlight(content, "diff")).trim())
//Results
return {snippet}
}
//Handle errors
catch (error) {
throw {error:{message:"An error occured", instance:error}}
}
}

View File

@@ -0,0 +1,46 @@
name: "♐ Code snippet of the day"
cost: 1 REST request per 100 events fetched
category: github
supports:
- user
- organization
- repository
inputs:
# Enable or disable plugin
plugin_code:
description: Display a random code snippet from recent activity
type: boolean
default: no
# Maximum number of lines that a code snippet can contain
plugin_code_lines:
description: Maximum number of line that a code snippet can contain
type: number
default: 12
# Number of activity events to load
# A high number will consume more requests
plugin_code_load:
description: Number of events to load
type: number
default: 100
min: 100
max: 1000
# Set events visibility (use this to restrict events when using a "repo" token)
plugin_code_visibility:
description: Set events visibility
type: string
default: public
values:
- public
- all
# List of repositories that will be skipped
plugin_code_skipped:
description: Repositories to skip
type: array
format: comma-separated
default: ""
example: my-repo-1, my-repo-2, owner/repo-3 ...

View File

@@ -0,0 +1,5 @@
- name: Code plugin (default)
uses: lowlighter/metrics@latest
with:
token: MOCKED_TOKEN
plugin_code: yes

View File

@@ -30,7 +30,7 @@ If you work a lot with other people, these numbers may be less representative of
The `plugin_languages_indepth` option lets you get more accurate metrics by cloning each repository you contributed to, running [github/linguist](https://github.com/github/linguist) on it and then iterating over patches matching your username from `git log`. This method is slower than the first one.
> ⚠️ Although *metrics* does not send any code to external sources, you must understand that when using this option repositories are cloned locally temporarly on the GitHub Action runner. If you work with sensitive data or company code, it is advised to keep this option disabled. *Metrics* and its authors cannot be held responsible for any eventual code leaks, use at your own risk.
> ⚠️ Although *metrics* does not send any code to external sources, you must understand that when using this option repositories are cloned locally temporarly on the GitHub Action runner. If you work with sensitive data or company code, it is advised to keep this option disabled. *Metrics* and its authors cannot be held responsible for any resulting code leaks, use at your own risk.
> Source code is available for auditing at [analyzers.mjs](/source/plugins/languages/analyzers.mjs)
> 🔣 On web instances, `indepth` is an extra feature and must be enabled globally in `settings.json`

View File

@@ -31,5 +31,6 @@
"stackoverflow",
"stock",
"achievements",
"screenshot"
"screenshot",
"code"
]

View File

@@ -0,0 +1,44 @@
<% if (plugins.code) { %>
<section>
<h2 class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4 1.75C4 .784 4.784 0 5.75 0h5.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v8.586A1.75 1.75 0 0114.25 15h-9a.75.75 0 010-1.5h9a.25.25 0 00.25-.25V6h-2.75A1.75 1.75 0 0110 4.25V1.5H5.75a.25.25 0 00-.25.25v2.5a.75.75 0 01-1.5 0v-2.5zm7.5-.188V4.25c0 .138.112.25.25.25h2.688a.252.252 0 00-.011-.013l-2.914-2.914a.272.272 0 00-.013-.011zM5.72 6.72a.75.75 0 000 1.06l1.47 1.47-1.47 1.47a.75.75 0 101.06 1.06l2-2a.75.75 0 000-1.06l-2-2a.75.75 0 00-1.06 0zM3.28 7.78a.75.75 0 00-1.06-1.06l-2 2a.75.75 0 000 1.06l2 2a.75.75 0 001.06-1.06L1.81 9.25l1.47-1.47z"></path></svg>
Code snippet of the day
</h2>
<% if (plugins.code.error) { %>
<div class="row">
<section>
<div class="field error">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
<%= plugins.code.error.message %>
</div>
</section>
</div>
<% } else { %>
<div class="row">
<section class="largeable-column-fields">
<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="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>
From <span class="blue space"><%= plugins.code.snippet.repo %></span>
</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="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>
<%= plugins.code.snippet.message %>
</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="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm6.5-.25A.75.75 0 017.25 7h1a.75.75 0 01.75.75v2.75h.25a.75.75 0 010 1.5h-2a.75.75 0 010-1.5h.25v-2h-.25a.75.75 0 01-.75-.75zM8 6a1 1 0 100-2 1 1 0 000 2z"></path></svg>
<span class="code"><%= plugins.code.snippet.sha.substring(0, 8) %></span>
<span class="code"><%= plugins.code.snippet.filename %></span>
<span class="code"><span class="snippet additions">++<%= plugins.code.snippet.additions %></span> <span class="snippet deletions">--<%= plugins.code.snippet.deletions %></span></span>
</div>
</section>
</div>
<div class="row">
<section class="snippet">
<div class="body markdown">
<code class="language-diff" xml:space="preserve"><%- plugins.code.snippet.patch %></code>
</div>
</section>
</div>
<% } %>
</section>
<% } %>

View File

@@ -1121,6 +1121,17 @@
overflow: hidden;
}
/* Code snippet */
.snippet .body {
padding-left: 12px;
}
.snippet.additions {
color: #336543;
}
.snippet.deletions {
color: #9A5256;
}
/* Markdown and syntax highlighting */
.markdown b, .markdown i {
display: inline-block;
@@ -1140,6 +1151,15 @@
width: 97%;
margin-top: 4px;
}
span.code {
background-color: #7777771F;
padding: 1px 5px;
font-size: 80%;
border-radius: 6px;
color: #777777;
font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;
margin: 0 4px -3px;
}
.token.comment {
color: #669900;
}
@@ -1160,7 +1180,27 @@
}
.token.trimmed {
font-style: italic;
color: #77777760
color: #77777760;
}
.token.coord {
color: #D2A8FF;
font-weight: bold;
}
.token.inserted:not(.prefix) {
color: #AAD0B4DC;
background-color: #336543DC;
}
.token.deleted:not(.prefix) {
color: #EED2D0DC;
background-color: #9A5256DC;
}
/* Typography */
.space {
margin-left: 7px;
}
.blue {
color: #58a6ff;
}
/* Charts */