Markdown interpretation (#237)
This commit is contained in:
123
package-lock.json
generated
123
package-lock.json
generated
@@ -30,6 +30,7 @@
|
||||
"prismjs": "^1.23.0",
|
||||
"puppeteer": "^8.0.0",
|
||||
"rss-parser": "^3.12.0",
|
||||
"sanitize-html": "^2.3.3",
|
||||
"simple-git": "^2.37.0",
|
||||
"svgo": "^2.3.0",
|
||||
"twemoji-parser": "^13.0.0",
|
||||
@@ -3282,8 +3283,7 @@
|
||||
"node_modules/colorette": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
|
||||
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w=="
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
@@ -3628,7 +3628,6 @@
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -7804,6 +7803,14 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/klona": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz",
|
||||
"integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/latest-version": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
|
||||
@@ -8221,6 +8228,17 @@
|
||||
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.1.22",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz",
|
||||
"integrity": "sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/nanomatch": {
|
||||
"version": "1.2.13",
|
||||
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
|
||||
@@ -9153,6 +9171,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-srcset": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
|
||||
"integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE="
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
|
||||
@@ -9304,6 +9327,23 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.2.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.10.tgz",
|
||||
"integrity": "sha512-b/h7CPV7QEdrqIxtAf2j31U5ef05uBDuvoXv6L51Q4rcS1jdlXAVKJv+atCFdUXYl9dyTHGyoMzIepwowRJjFw==",
|
||||
"dependencies": {
|
||||
"colorette": "^1.2.2",
|
||||
"nanoid": "^3.1.22",
|
||||
"source-map": "^0.6.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -10147,6 +10187,31 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sanitize-html": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.3.3.tgz",
|
||||
"integrity": "sha512-DCFXPt7Di0c6JUnlT90eIgrjs6TsJl/8HYU3KLdmrVclFN4O0heTcVbJiMa23OKVr6aR051XYtsgd8EWwEBwUA==",
|
||||
"dependencies": {
|
||||
"deepmerge": "^4.2.2",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"htmlparser2": "^6.0.0",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"klona": "^2.0.3",
|
||||
"parse-srcset": "^1.0.2",
|
||||
"postcss": "^8.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/sanitize-html/node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
@@ -15070,8 +15135,7 @@
|
||||
"colorette": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
|
||||
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w=="
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
@@ -15350,8 +15414,7 @@
|
||||
"deepmerge": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
|
||||
},
|
||||
"defer-to-connect": {
|
||||
"version": "2.0.1",
|
||||
@@ -18704,6 +18767,11 @@
|
||||
"integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
|
||||
"dev": true
|
||||
},
|
||||
"klona": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz",
|
||||
"integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA=="
|
||||
},
|
||||
"latest-version": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
|
||||
@@ -19044,6 +19112,11 @@
|
||||
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
|
||||
"devOptional": true
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "3.1.22",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz",
|
||||
"integrity": "sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ=="
|
||||
},
|
||||
"nanomatch": {
|
||||
"version": "1.2.13",
|
||||
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
|
||||
@@ -19838,6 +19911,11 @@
|
||||
"lines-and-columns": "^1.1.6"
|
||||
}
|
||||
},
|
||||
"parse-srcset": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
|
||||
"integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE="
|
||||
},
|
||||
"parse5": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
|
||||
@@ -19958,6 +20036,16 @@
|
||||
"integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
|
||||
"dev": true
|
||||
},
|
||||
"postcss": {
|
||||
"version": "8.2.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.10.tgz",
|
||||
"integrity": "sha512-b/h7CPV7QEdrqIxtAf2j31U5ef05uBDuvoXv6L51Q4rcS1jdlXAVKJv+atCFdUXYl9dyTHGyoMzIepwowRJjFw==",
|
||||
"requires": {
|
||||
"colorette": "^1.2.2",
|
||||
"nanoid": "^3.1.22",
|
||||
"source-map": "^0.6.1"
|
||||
}
|
||||
},
|
||||
"prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -20654,6 +20742,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sanitize-html": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.3.3.tgz",
|
||||
"integrity": "sha512-DCFXPt7Di0c6JUnlT90eIgrjs6TsJl/8HYU3KLdmrVclFN4O0heTcVbJiMa23OKVr6aR051XYtsgd8EWwEBwUA==",
|
||||
"requires": {
|
||||
"deepmerge": "^4.2.2",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"htmlparser2": "^6.0.0",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"klona": "^2.0.3",
|
||||
"parse-srcset": "^1.0.2",
|
||||
"postcss": "^8.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"prismjs": "^1.23.0",
|
||||
"puppeteer": "^8.0.0",
|
||||
"rss-parser": "^3.12.0",
|
||||
"sanitize-html": "^2.3.3",
|
||||
"simple-git": "^2.37.0",
|
||||
"svgo": "^2.3.0",
|
||||
"twemoji-parser": "^13.0.0",
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
rendered = await imports.svg.gemojis(rendered, {rest})
|
||||
//Optimize rendering
|
||||
if (!q.raw)
|
||||
rendered = xmlformat(rendered, {lineSeparator:"\n"})
|
||||
rendered = xmlformat(rendered, {lineSeparator:"\n", collapseContent:true})
|
||||
if ((conf.settings?.optimize)&&(!q.raw)) {
|
||||
console.debug(`metrics/compute/${login} > optimize`)
|
||||
if (experimental.has("--optimize")) {
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
import nodechartist from "node-chartist"
|
||||
import GIFEncoder from "gifencoder"
|
||||
import PNG from "png-js"
|
||||
import marked from "marked"
|
||||
import htmlsanitize from "sanitize-html"
|
||||
import prism from "prismjs"
|
||||
import prism_lang from "prismjs/components/index.js"
|
||||
prism_lang()
|
||||
|
||||
//Exports
|
||||
export {fs, os, paths, url, util, processes, axios, git, opengraph, jimp, rss}
|
||||
@@ -155,6 +160,26 @@
|
||||
return false
|
||||
}
|
||||
|
||||
/**Markdown-html sanitizer-interpreter */
|
||||
export async function markdown(text, {mode = "inline", codelines = Infinity} = {}) {
|
||||
//Sanitize once user text and then apply markdown. Depending on mode, reapply stricter sanitization if required
|
||||
let rendered = htmlsanitize(await marked(htmlsanitize(text), {
|
||||
highlight(code, lang) {
|
||||
return lang in prism.languages ? prism.highlight(code, prism.languages[lang]) : code
|
||||
},
|
||||
silent:true,
|
||||
xhtml:true,
|
||||
}), {
|
||||
inline:{allowedTags:["br", "code", "span"], allowedAttributes:{code:["class"], span:["class"]}},
|
||||
}[mode])
|
||||
//Trim code snippets
|
||||
rendered = rendered.replace(/(?<open><code[\s\S]*?>)(?<code>[\s\S]*?)(?<close><\/code>)/g, (m, open, code, close) => { //eslint-disable-line max-params
|
||||
const lines = code.trim().split("\n")
|
||||
return `${open}${lines.slice(0, codelines).join("\n")}${lines.length > codelines ? `\n<span class="token trimmed">(${lines.length-codelines} more ${lines.length-codelines === 1 ? "line was" : "lines were"} trimmed)</span>` : ""}${close}`
|
||||
})
|
||||
return rendered
|
||||
}
|
||||
|
||||
/**Image to base64 */
|
||||
export async function imgb64(image, {width, height, fallback = true} = {}) {
|
||||
//Undefined image
|
||||
|
||||
@@ -129,7 +129,6 @@
|
||||
app.get("/.js/prism.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/prism.js`))
|
||||
app.get("/.js/prism.yaml.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/components/prism-yaml.min.js`))
|
||||
app.get("/.js/prism.markdown.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/components/prism-markdown.min.js`))
|
||||
app.get("/.js/marked.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/marked/marked.min.js`))
|
||||
//Meta
|
||||
app.get("/.version", limiter, (req, res) => res.status(200).send(conf.package.version))
|
||||
app.get("/.requests", limiter, (req, res) => res.status(200).json(requests))
|
||||
@@ -182,7 +181,7 @@
|
||||
activity:true, "activity.limit":100, "activity.days":0,
|
||||
notable:true,
|
||||
},
|
||||
}, {graphql, rest, plugins:{achievements:{enabled:true}, isocalendar:{enabled:true}, languages:{enabled:true}, activity:{enabled:true}, notable:{enabled:true}}, conf, convert:"json"}, {Plugins, Templates})
|
||||
}, {graphql, rest, plugins:{achievements:{enabled:true}, isocalendar:{enabled:true}, languages:{enabled:true}, activity:{enabled:true, markdown:"extended"}, notable:{enabled:true}}, conf, convert:"json"}, {Plugins, Templates})
|
||||
//Cache
|
||||
if ((!debug)&&(cached)) {
|
||||
const maxage = Math.round(Number(req.query.cache))
|
||||
|
||||
@@ -190,7 +190,7 @@
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.75 2.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 01.75.75v2.19l2.72-2.72a.75.75 0 01.53-.22h4.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25H2.75zM1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0113.25 12H9.06l-2.573 2.573A1.457 1.457 0 014 13.543V12H2.75A1.75 1.75 0 011 10.25v-7.5z"></path></svg>
|
||||
<div class="content">
|
||||
Commented on <a :href="`https://github.com/${repo}/${{issue:'issues', pr:'pull', commit:'commit'}[event.on]}/${event.number}`">#{{ event.number }} {{ event.title }}</a> from <a :href="`https://github.com/${repo}`">{{ repo }}</a>
|
||||
<quote v-html="markdown(event.content)"></quote>
|
||||
<quote v-if="event.content.trim().length" v-html="event.content"></quote>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="type === 'member'">
|
||||
@@ -205,7 +205,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" 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>
|
||||
<div class="content">
|
||||
{{ event.draft ? "Drafted release" : event.prerelease ? "Pre-released" : "Released" }} of <a :href="`https://github.com/${repo}`">{{ repo }}</a>
|
||||
<quote v-html="markdown(event.content)"></quote>
|
||||
<quote v-if="event.content.trim().length" v-html="event.content"></quote>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="type === 'fork'">
|
||||
@@ -227,14 +227,14 @@
|
||||
<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 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>
|
||||
<div class="content">
|
||||
{{ event.action === "opened" ? "Opened" : event.action === "reopened" ? "Reopened" : "Closed" }} <a :href="`https://github.com/${repo}/issues/${event.number}`">#{{ event.number }} {{ event.title }}</a> in <a :href="`https://github.com/${repo}`">{{ repo }}</a>
|
||||
<quote v-html="markdown(event.content)"></quote>
|
||||
<quote v-if="event.content.trim().length" v-html="event.content"></quote>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="type === 'pr'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" 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>
|
||||
<div class="content">
|
||||
{{ event.action === "opened" ? "Opened" : event.action === "merged" ? "Merged" : "Closed" }} <a :href="`https://github.com/${repo}/pull/${event.number}`">#{{ event.number }} {{ event.title }}</a> in <a :href="`https://github.com/${repo}`">{{ repo }}</a>
|
||||
<quote v-html="markdown(event.content)"></quote>
|
||||
<quote v-if="event.content.trim().length" v-html="event.content"></quote>
|
||||
<ul>
|
||||
<li>
|
||||
{{ event.files.changed }} file{{ "s" }} changed <code>++{{ event.lines.added }} --{{ event.lines.deleted }}</code>
|
||||
@@ -295,7 +295,6 @@
|
||||
<!-- Scripts -->
|
||||
<script src="/.js/axios.min.js"></script>
|
||||
<script src="/.js/vue.min.js"></script>
|
||||
<script src="/.js/marked.min.js"></script>
|
||||
<script src="/about/.statics/script.js?v=3.7"></script>
|
||||
<script src="/about/.statics/script.js?v=3.8"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -71,11 +71,6 @@
|
||||
finally {
|
||||
this.pending = false
|
||||
}
|
||||
},
|
||||
markdown(content) {
|
||||
const escaped = document.createElement("textarea")
|
||||
escaped.textContent = content
|
||||
return marked(escaped.innerHTML)
|
||||
}
|
||||
},
|
||||
//Computed properties
|
||||
|
||||
@@ -179,7 +179,7 @@
|
||||
<script src="/.js/faker.min.js"></script>
|
||||
<script src="/.js/vue.min.js"></script>
|
||||
<script src="/.js/vue.prism.min.js"></script>
|
||||
<script src="/.js/app.placeholder.js?v=3.7"></script>
|
||||
<script src="/.js/app.placeholder.js?v=3.8"></script>
|
||||
<script src="/.js/app.js?v=3.7"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +1,5 @@
|
||||
//Setup
|
||||
export default async function({login, data, rest, q, account, imports}, {enabled = false} = {}) {
|
||||
export default async function({login, data, rest, q, account, imports}, {enabled = false, markdown = "inline"} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
@@ -18,6 +18,7 @@
|
||||
let {limit, days, filter, visibility, timestamps} = imports.metadata.plugins.activity.inputs({data, q, account})
|
||||
if (!days)
|
||||
days = Infinity
|
||||
const codelines = 2
|
||||
|
||||
//Get user recent activity
|
||||
console.debug(`metrics/compute/${login}/plugins > activity > querying api`)
|
||||
@@ -25,11 +26,11 @@
|
||||
console.debug(`metrics/compute/${login}/plugins > activity > ${events.length} events loaded`)
|
||||
|
||||
//Extract activity events
|
||||
const activity = events
|
||||
const activity = (await Promise.all(events
|
||||
.filter(({actor}) => account === "organization" ? true : actor.login === login)
|
||||
.filter(({created_at}) => Number.isFinite(days) ? new Date(created_at) > new Date(Date.now()-days*24*60*60*1000) : true)
|
||||
.filter(event => visibility === "public" ? event.public : true)
|
||||
.map(({type, payload, actor:{login:actor}, repo:{name:repo}, created_at}) => {
|
||||
.map(async({type, payload, actor:{login:actor}, repo:{name:repo}, created_at}) => {
|
||||
//See https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/github-event-types
|
||||
const timestamp = new Date(created_at)
|
||||
switch (type) {
|
||||
@@ -38,7 +39,7 @@
|
||||
if (!["created"].includes(payload.action))
|
||||
return null
|
||||
const {comment:{user:{login:user}, commit_id:sha, body:content}} = payload
|
||||
return {type:"comment", on:"commit", actor, timestamp, repo, content, user, mobile:null, number:sha.substring(0, 7), title:""}
|
||||
return {type:"comment", on:"commit", actor, timestamp, repo, content:await imports.markdown(content, {mode:markdown, codelines}), user, mobile:null, number:sha.substring(0, 7), title:""}
|
||||
}
|
||||
//Created a git branch or tag
|
||||
case "CreateEvent":{
|
||||
@@ -64,14 +65,14 @@
|
||||
if (!["created"].includes(payload.action))
|
||||
return null
|
||||
const {issue:{user:{login:user}, title, number}, comment:{body:content, performed_via_github_app:mobile}} = payload
|
||||
return {type:"comment", on:"issue", actor, timestamp, repo, content, user, mobile, number, title}
|
||||
return {type:"comment", on:"issue", actor, timestamp, repo, content:await imports.markdown(content, {mode:markdown, codelines}), user, mobile, number, title}
|
||||
}
|
||||
//Issue event
|
||||
case "IssuesEvent":{
|
||||
if (!["opened", "closed", "reopened"].includes(payload.action))
|
||||
return null
|
||||
const {action, issue:{user:{login:user}, title, number, body:content}} = payload
|
||||
return {type:"issue", actor, timestamp, repo, action, user, number, title, content}
|
||||
return {type:"issue", actor, timestamp, repo, action, user, number, title, content:await imports.markdown(content, {mode:markdown, codelines})}
|
||||
}
|
||||
//Activity from repository collaborators
|
||||
case "MemberEvent":{
|
||||
@@ -89,7 +90,7 @@
|
||||
if (!["opened", "closed"].includes(payload.action))
|
||||
return null
|
||||
const {action, pull_request:{user:{login:user}, title, number, body:content, additions:added, deletions:deleted, changed_files:changed, merged}} = payload
|
||||
return {type:"pr", actor, timestamp, repo, action:(action === "closed")&&(merged) ? "merged" : action, user, title, number, content, lines:{added, deleted}, files:{changed}}
|
||||
return {type:"pr", actor, timestamp, repo, action:(action === "closed")&&(merged) ? "merged" : action, user, title, number, content:await imports.markdown(content, {mode:markdown, codelines}), lines:{added, deleted}, files:{changed}}
|
||||
}
|
||||
//Reviewed a pull request
|
||||
case "PullRequestReviewEvent":{
|
||||
@@ -101,7 +102,7 @@
|
||||
if (!["created"].includes(payload.action))
|
||||
return null
|
||||
const {pull_request:{user:{login:user}, title, number}, comment:{body:content, performed_via_github_app:mobile}} = payload
|
||||
return {type:"comment", on:"pr", actor, timestamp, repo, content, user, mobile, number, title}
|
||||
return {type:"comment", on:"pr", actor, timestamp, repo, content:await imports.markdown(content, {mode:markdown, codelines}), user, mobile, number, title}
|
||||
}
|
||||
//Pushed commits
|
||||
case "PushEvent":{
|
||||
@@ -113,7 +114,7 @@
|
||||
if (!["published"].includes(payload.action))
|
||||
return null
|
||||
const {action, release:{name, prerelease, draft, body:content}} = payload
|
||||
return {type:"release", actor, timestamp, repo, action, name, prerelease, draft, content}
|
||||
return {type:"release", actor, timestamp, repo, action, name, prerelease, draft, content:await imports.markdown(content, {mode:markdown, codelines})}
|
||||
}
|
||||
//Starred a repository
|
||||
case "WatchEvent":{
|
||||
@@ -127,7 +128,7 @@
|
||||
return null
|
||||
}
|
||||
}
|
||||
})
|
||||
})))
|
||||
.filter(event => event)
|
||||
.filter(event => filter.includes("all") || filter.includes(event.type))
|
||||
.slice(0, limit)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {sections, user, limit, lines} = imports.metadata.plugins.stackoverflow.inputs({data, account, q})
|
||||
let {sections, user, limit, lines, "lines.snippet":codelines} = imports.metadata.plugins.stackoverflow.inputs({data, account, q})
|
||||
if (!user)
|
||||
throw {error:{message:"You must provide a stackoverflow user id"}}
|
||||
|
||||
@@ -32,14 +32,14 @@
|
||||
//Load and format answers
|
||||
console.debug(`metrics/compute/${login}/plugins > stackoverflow > querying api for ${key}`)
|
||||
const {data:{items}} = await imports.axios.get(`${api.user}/answers?site=stackoverflow&pagesize=${limit}&filter=${filters.answer}&${sort}`)
|
||||
result[key] = items.map(item => format.answer(item, {imports, data}))
|
||||
result[key] = await Promise.all(items.map(item => format.answer(item, {imports, data, codelines})))
|
||||
console.debug(`metrics/compute/${login}/plugins > stackoverflow > loaded ${result[key].length} items`)
|
||||
//Load related questions
|
||||
const ids = result[key].map(({question_id}) => question_id).filter(id => id)
|
||||
if (ids) {
|
||||
console.debug(`metrics/compute/${login}/plugins > stackoverflow > loading ${ids.length} related items`)
|
||||
const {data:{items}} = await imports.axios.get(`${api.base}/questions/${ids.join(";")}?site=stackoverflow&filter=${filters.question}`)
|
||||
items.map(item => format.question(item, {imports, data}))
|
||||
await Promise.all(items.map(item => format.question(item, {imports, data, codelines})))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,14 +48,14 @@
|
||||
//Load and format questions
|
||||
console.debug(`metrics/compute/${login}/plugins > stackoverflow > querying api for ${key}`)
|
||||
const {data:{items}} = await imports.axios.get(`${api.user}/questions?site=stackoverflow&pagesize=${limit}&filter=${filters.question}&${sort}`)
|
||||
result[key] = items.map(item => format.question(item, {imports, data}))
|
||||
result[key] = await Promise.all(items.map(item => format.question(item, {imports, data, codelines})))
|
||||
console.debug(`metrics/compute/${login}/plugins > stackoverflow > loaded ${result[key].length} items`)
|
||||
//Load related answers
|
||||
const ids = result[key].map(({accepted_answer_id}) => accepted_answer_id).filter(id => id)
|
||||
if (ids) {
|
||||
console.debug(`metrics/compute/${login}/plugins > stackoverflow > loading ${ids.length} related items`)
|
||||
const {data:{items}} = await imports.axios.get(`${api.base}/answers/${ids.join(";")}?site=stackoverflow&filter=${filters.answer}`)
|
||||
items.map(item => format.answer(item, {imports, data}))
|
||||
await Promise.all(items.map(item => format.answer(item, {imports, data, codelines})))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,9 +74,13 @@
|
||||
const format = {
|
||||
/**Cached */
|
||||
cached:new Map(),
|
||||
/**Format stackoverflow code snippets */
|
||||
code(text) {
|
||||
return text.replace(/<!-- language: lang-(?<lang>\w+) -->\s*(?<snippet> {4}[\s\S]+?)(?=(?:<!-- end snippet -->)|(?:<!-- language: lang-))/g, "```$<lang>\n$<snippet>```")
|
||||
},
|
||||
/**Format answers */
|
||||
answer({body_markdown:body, score, up_vote_count:upvotes, down_vote_count:downvotes, is_accepted:accepted, comment_count:comments = 0, creation_date, owner:{display_name:author}, link, answer_id:id, question_id}, {imports, data}) {
|
||||
const formatted = {type:"answer", body:imports.htmlunescape(body), score, upvotes, downvotes, accepted, comments, author, created:imports.date(creation_date*1000, {dateStyle:"short", timeZone:data.config.timezone?.name}), link, id, question_id,
|
||||
async answer({body_markdown:body, score, up_vote_count:upvotes, down_vote_count:downvotes, is_accepted:accepted, comment_count:comments = 0, creation_date, owner:{display_name:author}, link, answer_id:id, question_id}, {imports, data, codelines}) {
|
||||
const formatted = {type:"answer", body:await imports.markdown(format.code(imports.htmlunescape(body)), {codelines}), score, upvotes, downvotes, accepted, comments, author, created:imports.date(creation_date*1000, {dateStyle:"short", timeZone:data.config.timezone?.name}), link, id, question_id,
|
||||
get question() {
|
||||
return format.cached.get(`q${this.question_id}`) ?? null
|
||||
},
|
||||
@@ -85,8 +89,8 @@
|
||||
return formatted
|
||||
},
|
||||
/**Format questions */
|
||||
question({title, body_markdown:body, score, up_vote_count:upvotes, down_vote_count:downvotes, favorite_count:favorites, tags, is_answered:answered, answer_count:answers, comment_count:comments, view_count:views, creation_date, owner:{display_name:author}, link, question_id:id, accepted_answer_id = null}, {imports, data}) {
|
||||
const formatted = {type:"question", title:imports.htmlunescape(title), body:imports.htmlunescape(body), score, upvotes, downvotes, favorites, tags, answered, answers, comments, views, author, created:imports.date(creation_date*1000, {dateStyle:"short", timeZone:data.config.timezone?.name}), link, id, accepted_answer_id,
|
||||
async question({title, body_markdown:body, score, up_vote_count:upvotes, down_vote_count:downvotes, favorite_count:favorites, tags, is_answered:answered, answer_count:answers, comment_count:comments, view_count:views, creation_date, owner:{display_name:author}, link, question_id:id, accepted_answer_id = null}, {imports, data, codelines}) {
|
||||
const formatted = {type:"question", title:await imports.markdown(title), body:await imports.markdown(format.code(imports.htmlunescape(body)), {codelines}), score, upvotes, downvotes, favorites, tags, answered, answers, comments, views, author, created:imports.date(creation_date*1000, {dateStyle:"short", timeZone:data.config.timezone?.name}), link, id, accepted_answer_id,
|
||||
get answer() {
|
||||
return format.cached.get(`a${this.accepted_answer_id}`) ?? null
|
||||
},
|
||||
|
||||
@@ -41,9 +41,18 @@ inputs:
|
||||
max: 30
|
||||
|
||||
# Number of lines to display per question or answer
|
||||
# Code snippets will take one line slot and should be configured with "plugin_stackoverflow_lines_snippet" instead
|
||||
# Set to 0 to disable limitations
|
||||
plugin_stackoverflow_lines:
|
||||
description: Maximum number of lines to display per question or answer
|
||||
type: number
|
||||
default: 4
|
||||
min: 0
|
||||
|
||||
# Number of lines to display per code snippet
|
||||
# Set to 0 to disable limitations
|
||||
plugin_stackoverflow_lines_snippet:
|
||||
description: Maximum number of lines to display per code snippet
|
||||
type: number
|
||||
default: 2
|
||||
min: 0
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
</div>
|
||||
<div class="details">
|
||||
<div><%= event.on === "commit" ? "committed" : "opened" %> by <%= event.user %> in <span class="repo"><%= repo %></span></div>
|
||||
<div class="comment"><%= event.content %></div>
|
||||
<div class="comment"><%- event.content %></div>
|
||||
</div>
|
||||
<% } %>
|
||||
<% if (type === "wiki") { %>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<% if (type === "question") { %>
|
||||
<div class="field title">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.75 2.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 01.75.75v2.19l2.72-2.72a.75.75 0 01.53-.22h4.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25H2.75zM1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0113.25 12H9.06l-2.573 2.573A1.457 1.457 0 014 13.543V12H2.75A1.75 1.75 0 011 10.25v-7.5z"></path></svg>
|
||||
<%= entry.title %>
|
||||
<%- entry.title %>
|
||||
</div>
|
||||
<div class="infos">
|
||||
<div>
|
||||
@@ -77,7 +77,7 @@
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="body">
|
||||
<%= entry.body %>
|
||||
<%- entry.body %>
|
||||
</div>
|
||||
<div class="infos">
|
||||
<div>
|
||||
@@ -108,7 +108,7 @@
|
||||
<% } else if (type === "answer") { %>
|
||||
<div class="field title">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M16 1.25v4.146a.25.25 0 01-.427.177L14.03 4.03l-3.75 3.75a.75.75 0 11-1.06-1.06l3.75-3.75-1.543-1.543A.25.25 0 0111.604 1h4.146a.25.25 0 01.25.25zM2.75 3.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 01.75.75v2.19l2.72-2.72a.75.75 0 01.53-.22h4.5a.25.25 0 00.25-.25v-2.5a.75.75 0 111.5 0v2.5A1.75 1.75 0 0113.25 13H9.06l-2.573 2.573A1.457 1.457 0 014 14.543V13H2.75A1.75 1.75 0 011 11.25v-7.5C1 2.784 1.784 2 2.75 2h5.5a.75.75 0 010 1.5h-5.5z"></path></svg>
|
||||
<%= entry.question?.title %>
|
||||
<%- entry.question?.title %>
|
||||
</div>
|
||||
<div class="infos">
|
||||
<div>
|
||||
@@ -123,7 +123,7 @@
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="body">
|
||||
<%= entry.body %>
|
||||
<%- entry.body %>
|
||||
</div>
|
||||
<div class="infos">
|
||||
<div>
|
||||
|
||||
@@ -610,7 +610,7 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.activity .details .comment {
|
||||
.activity .details > .comment {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
@@ -618,7 +618,6 @@
|
||||
margin-top: 6px;
|
||||
border-left: 3px solid #777777B2;
|
||||
padding-left: 6px;
|
||||
max-height: 38px;
|
||||
/* May not work in all browsers */
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
@@ -940,6 +939,56 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Syntax highlighting */
|
||||
code {
|
||||
background-color: #7777771F;
|
||||
display: inline-block;
|
||||
border-radius: 6px;
|
||||
color: #777777;
|
||||
padding: 1px 5px;
|
||||
font-size: 80%;
|
||||
font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;
|
||||
}
|
||||
code[class^=language-] {
|
||||
white-space: pre-wrap;
|
||||
width: 97%;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.token.comment, .token.prolog, .token.doctype, .token.cdata {
|
||||
color: #6a737d;
|
||||
}
|
||||
.token.punctuation {
|
||||
color: #24292e;
|
||||
}
|
||||
.token.namespace, .token.property, .token.tag, .token.boolean, .token.number, .token.constant, .token.symbol {
|
||||
color: #d73a49;
|
||||
}
|
||||
.token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted {
|
||||
color: #032f62;
|
||||
}
|
||||
.token.operator, .token.entity, .token.url, .language-css .token.string, .style .token.string, .token.variable {
|
||||
color: #005cc5;
|
||||
}
|
||||
.token.atrule, .token.attr-value, .token.keyword {
|
||||
color: #6f42c1
|
||||
}
|
||||
.token.regex, .token.important {
|
||||
color: #e90;
|
||||
}
|
||||
.token.important, .token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
.token.deleted {
|
||||
color: red;
|
||||
}
|
||||
.token.trimmed {
|
||||
font-style: italic;
|
||||
color: #77777760
|
||||
}
|
||||
|
||||
/* Charts */
|
||||
.ct-line {
|
||||
stroke-width: 2px !important;
|
||||
|
||||
Reference in New Issue
Block a user