Skip to content

Commit 842aee7

Browse files
authored
feat(plugins/code): add new code plugin (lowlighter#526)
1 parent 47a0689 commit 842aee7

File tree

11 files changed

+256
-10
lines changed

11 files changed

+256
-10
lines changed

source/app/metrics/utils.mjs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -208,16 +208,15 @@ export async function which(command) {
208208
return false
209209
}
210210

211+
/**Code hightlighter */
212+
export async function highlight(code, lang) {
213+
return lang in prism.languages ? prism.highlight(code, prism.languages[lang]) : code
214+
}
215+
211216
/**Markdown-html sanitizer-interpreter */
212217
export async function markdown(text, {mode = "inline", codelines = Infinity} = {}) {
213218
//Sanitize user input once to prevent injections and parse into markdown
214-
let rendered = await marked(htmlunescape(htmlsanitize(text)), {
215-
highlight(code, lang) {
216-
return lang in prism.languages ? prism.highlight(code, prism.languages[lang]) : code
217-
},
218-
silent:true,
219-
xhtml:true,
220-
})
219+
let rendered = await marked(htmlunescape(htmlsanitize(text)), {highlight, silent:true, xhtml:true})
221220
//Markdown mode
222221
switch (mode) {
223222
case "inline": {

source/app/mocks/api/github/rest/request.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export default function({faker}, target, that, args) {
3535
email:faker.internet.email(),
3636
date:`${faker.date.recent(7)}`,
3737
},
38+
url:"https://api.github.com/repos/lowlighter/metrics/commits/MOCKED_SHA",
3839
},
3940
author:{
4041
login:faker.internet.userName(),

source/app/web/statics/app.placeholder.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,23 @@
336336
},
337337
})
338338
: null),
339+
//Code snippet
340+
...(set.plugins.enabled.code
341+
? ({
342+
code: {
343+
snippet: {
344+
sha: faker.git.shortSha(),
345+
message: faker.lorem.sentence(),
346+
filename: 'docs/specifications.html',
347+
status: "modified",
348+
additions: faker.datatype.number(50),
349+
deletions: faker.datatype.number(50),
350+
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`,
351+
repo: `${faker.random.word()}/${faker.random.word()}`,
352+
},
353+
}
354+
})
355+
: null),
339356
//Languages
340357
...(set.plugins.enabled.languages
341358
? ({

source/plugins/code/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
### ♐ Code snippet of the day
2+
3+
> ⚠️ 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.
4+
5+
Display a random code snippet from your recent activity history.
6+
7+
<table>
8+
<td align="center">
9+
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.code.svg">
10+
<img width="900" height="1" alt="">
11+
</td>
12+
</table>
13+
14+
#### ℹ️ Examples workflows
15+
16+
[➡️ Available options for this plugin](metadata.yml)
17+
18+
```yaml
19+
- uses: lowlighter/metrics@latest
20+
with:
21+
# ... other options
22+
plugin_code: yes
23+
plugin_code_lines: 12 # Only display snippets with less than 12 lines
24+
plugin_code_load: 100 # Fetch 100 events from activity
25+
plugin_code_visibility: public # Only display snippets from public activity
26+
plugin_code_skipped: github/octocat # Skip github/octocat repository
27+
```

source/plugins/code/index.mjs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//Setup
2+
export default async function({login, q, imports, data, rest, account}, {enabled = false} = {}) {
3+
//Plugin execution
4+
try {
5+
//Check if plugin is enabled and requirements are met
6+
if ((!enabled)||(!q.code))
7+
return null
8+
9+
//Context
10+
let context = {mode:"user"}
11+
if (q.repo) {
12+
console.debug(`metrics/compute/${login}/plugins > code > switched to repository mode`)
13+
const {owner, repo} = data.user.repositories.nodes.map(({name:repo, owner:{login:owner}}) => ({repo, owner})).shift()
14+
context = {...context, mode:"repository", owner, repo}
15+
}
16+
17+
//Load inputs
18+
let {load, lines, visibility, skipped} = imports.metadata.plugins.code.inputs({data, q, account})
19+
skipped.push(...data.shared["repositories.skipped"])
20+
const pages = Math.ceil(load / 100)
21+
22+
//Get user recent code
23+
console.debug(`metrics/compute/${login}/plugins > code > querying api`)
24+
const events = []
25+
try {
26+
for (let page = 1; page <= pages; page++) {
27+
console.debug(`metrics/compute/${login}/plugins > code > loading page ${page}/${pages}`)
28+
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
29+
.filter(({type}) => type === "PushEvent")
30+
.filter(({actor}) => account === "organization" ? true : actor.login?.toLocaleLowerCase() === login.toLocaleLowerCase())
31+
.filter(({repo:{name:repo}}) => !((skipped.includes(repo.split("/").pop())) || (skipped.includes(repo))))
32+
.filter(event => visibility === "public" ? event.public : true)
33+
.flatMap(({payload}) => Promise.all(payload.commits.map(async commit => (await rest.request(commit.url)).data)))])]
34+
.flat()
35+
.filter(({author}) => data.shared["commits.authoring"].filter(authoring => author?.email?.toLocaleLowerCase().includes(authoring)||author?.name?.toLocaleLowerCase().includes(authoring)))
36+
)
37+
}
38+
}
39+
catch {
40+
console.debug(`metrics/compute/${login}/plugins > code > no more page to load`)
41+
}
42+
console.debug(`metrics/compute/${login}/plugins > code > ${events.length} events loaded`)
43+
44+
//Search for a random snippet
45+
const files = events
46+
.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})))
47+
.filter(({patch}) => (patch ? (patch.match(/\n/mg)?.length ?? 1) : Infinity) < lines)
48+
const snippet = files[Math.floor(Math.random()*files.length)]
49+
50+
//Trim common indent from content and change line feed
51+
if (!snippet.patch.split("\n").shift().endsWith("@@"))
52+
snippet.patch = snippet.patch.replace(/^(?<coord>@@.*?@@)/, "$<coord>\n")
53+
const indent = Math.min(...(snippet.patch.match(/^[+-]? +/mg)?.map(indent => (indent.length ?? Infinity) - indent.startsWith("+") - indent.startsWith("-")) ?? [])) || 0
54+
const content = imports.htmlescape(snippet.patch.replace(/\r\n/mg, "\n").replace(new RegExp(`^([+-]?)${" ".repeat(indent)}`, "mg"), "$1"))
55+
56+
//Format patch
57+
snippet.patch = imports.htmlunescape((await imports.highlight(content, "diff")).trim())
58+
59+
//Results
60+
return {snippet}
61+
}
62+
//Handle errors
63+
catch (error) {
64+
throw {error:{message:"An error occured", instance:error}}
65+
}
66+
}

source/plugins/code/metadata.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: "♐ Code snippet of the day"
2+
cost: 1 REST request per 100 events fetched
3+
category: github
4+
supports:
5+
- user
6+
- organization
7+
- repository
8+
inputs:
9+
10+
# Enable or disable plugin
11+
plugin_code:
12+
description: Display a random code snippet from recent activity
13+
type: boolean
14+
default: no
15+
16+
# Maximum number of lines that a code snippet can contain
17+
plugin_code_lines:
18+
description: Maximum number of line that a code snippet can contain
19+
type: number
20+
default: 12
21+
22+
# Number of activity events to load
23+
# A high number will consume more requests
24+
plugin_code_load:
25+
description: Number of events to load
26+
type: number
27+
default: 100
28+
min: 100
29+
max: 1000
30+
31+
# Set events visibility (use this to restrict events when using a "repo" token)
32+
plugin_code_visibility:
33+
description: Set events visibility
34+
type: string
35+
default: public
36+
values:
37+
- public
38+
- all
39+
40+
# List of repositories that will be skipped
41+
plugin_code_skipped:
42+
description: Repositories to skip
43+
type: array
44+
format: comma-separated
45+
default: ""
46+
example: my-repo-1, my-repo-2, owner/repo-3 ...

source/plugins/code/tests.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
- name: Code plugin (default)
2+
uses: lowlighter/metrics@latest
3+
with:
4+
token: MOCKED_TOKEN
5+
plugin_code: yes

source/plugins/languages/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ If you work a lot with other people, these numbers may be less representative of
3030

3131
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.
3232

33-
> ⚠️ 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.
33+
> ⚠️ 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.
3434
> Source code is available for auditing at [analyzers.mjs](/source/plugins/languages/analyzers.mjs)
3535
3636
> 🔣 On web instances, `indepth` is an extra feature and must be enabled globally in `settings.json`

source/templates/classic/partials/_.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,6 @@
3131
"stackoverflow",
3232
"stock",
3333
"achievements",
34-
"screenshot"
34+
"screenshot",
35+
"code"
3536
]
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<% if (plugins.code) { %>
2+
<section>
3+
<h2 class="field">
4+
<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>
5+
Code snippet of the day
6+
</h2>
7+
<% if (plugins.code.error) { %>
8+
<div class="row">
9+
<section>
10+
<div class="field error">
11+
<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>
12+
<%= plugins.code.error.message %>
13+
</div>
14+
</section>
15+
</div>
16+
<% } else { %>
17+
<div class="row">
18+
<section class="largeable-column-fields">
19+
<div class="field">
20+
<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>
21+
From <span class="blue space"><%= plugins.code.snippet.repo %></span>
22+
</div>
23+
<div class="field">
24+
<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>
25+
<%= plugins.code.snippet.message %>
26+
</div>
27+
<div class="field">
28+
<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>
29+
<span class="code"><%= plugins.code.snippet.sha.substring(0, 8) %></span>
30+
<span class="code"><%= plugins.code.snippet.filename %></span>
31+
<span class="code"><span class="snippet additions">++<%= plugins.code.snippet.additions %></span> <span class="snippet deletions">--<%= plugins.code.snippet.deletions %></span></span>
32+
</div>
33+
</section>
34+
</div>
35+
<div class="row">
36+
<section class="snippet">
37+
<div class="body markdown">
38+
<code class="language-diff" xml:space="preserve"><%- plugins.code.snippet.patch %></code>
39+
</div>
40+
</section>
41+
</div>
42+
<% } %>
43+
</section>
44+
<% } %>

0 commit comments

Comments
 (0)