//Imports import ejs from "ejs" import util from "util" import * as utils from "./utils.mjs" //Setup export default async function metrics({login, q}, {graphql, rest, plugins, conf, die = false, verify = false, convert = null}, {Plugins, Templates}) { //Compute rendering try { //Debug login = login.replace(/[\n\r]/g, "") console.debug(`metrics/compute/${login} > start`) console.debug(util.inspect(q, {depth:Infinity, maxStringLength:256})) //Load template const template = q.template || conf.settings.templates.default if ((!(template in Templates)) || (!(template in conf.templates)) || ((conf.settings.templates.enabled.length) && (!conf.settings.templates.enabled.includes(template)))) throw new Error("unsupported template") const {image, style, fonts, views, partials} = conf.templates[template] const computer = Templates[template].default || Templates[template] convert = convert ?? conf.metadata.templates[template].formats[0] ?? null console.debug(`metrics/compute/${login} > output format set to ${convert}`) //Initialization const pending = [] const {queries} = conf const data = {q, animated:true, large:false, base:{}, config:{}, errors:[], plugins:{}, computed:{}} const imports = { plugins:Plugins, templates:Templates, metadata:conf.metadata, ...utils, ...utils.formatters({timeZone:q["config.timezone"]}), ...(/markdown/.test(convert) ? { imgb64(url, options) { return options?.force ? utils.imgb64(...arguments) : url }, } : null), } const experimental = new Set(decodeURIComponent(q["experimental.features"] ?? "").split(" ").map(x => x.trim().toLocaleLowerCase()).filter(x => x)) if (conf.settings["debug.headless"]) imports.puppeteer.headless = false //Metrics insights if (convert === "insights") return metrics.insights.output({login, imports, conf}, {graphql, rest, Plugins, Templates}) //Partial parts { data.partials = new Set([ ...decodeURIComponent(q["config.order"] ?? "").split(",").map(x => x.trim().toLocaleLowerCase()).filter(partial => partials.includes(partial)), ...partials, ]) console.debug(`metrics/compute/${login} > content order : ${[...data.partials]}`) } //Executing base plugin and compute metrics console.debug(`metrics/compute/${login} > compute`) await Plugins.base({login, q, data, rest, graphql, plugins, queries, pending, imports}, conf) await computer({login, q}, {conf, data, rest, graphql, plugins, queries, account:data.account, convert, template}, {pending, imports}) const promised = await Promise.all(pending) //Check plugins errors const errors = [...promised.filter(({result = null}) => result?.error), ...data.errors] if (errors.length) { console.warn(`metrics/compute/${login} > ${errors.length} errors !`) if (die) throw new Error("An error occured during rendering, dying") else console.warn(util.inspect(errors, {depth:Infinity, maxStringLength:256})) } //JSON output if (convert === "json") { console.debug(`metrics/compute/${login} > json output`) const cache = new WeakSet() const rendered = JSON.parse(JSON.stringify(data, (key, value) => { if ((value instanceof Set)||(Array.isArray(value))) return [...value] if (value instanceof Map) return Object.fromEntries(value) if ((typeof value === "object")&&(value)) { if (cache.has(value)) return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, cache.has(v) ? "[Circular]" : v])) cache.add(value) } return value })) return {rendered, mime:"application/json"} } //Markdown output if (/markdown/.test(convert)) { //Retrieving template source console.debug(`metrics/compute/${login} > markdown render`) let source = image try { let template = `${q.markdown}`.replace(/\n/g, "") if (!/^https:/.test(template)) { const {data:{default_branch:branch, full_name:repo}} = await rest.repos.get({owner:login, repo:q.repo || login}) console.debug(`metrics/compute/${login} > on ${repo} with default branch ${branch}`) template = `https://raw.githubusercontent.com/${repo}/${branch}/${template}` } console.debug(`metrics/compute/${login} > fetching ${template}`) ;({data:source} = await imports.axios.get(template, {headers:{Accept:"text/plain"}})) } catch (error) { console.debug(error) } //Embed method const _q = q const embed = async (name, q = {}) => { //Check arguments console.debug(`metrics/compute/${login}/embed > ${name} >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>`) if ((!name) || (typeof q !== "object") || (q === null)) { if (die) throw new Error("An error occured during embed rendering, dying") return "

⚠️ Failed to execute embed function: invalid arguments

" } console.debug(`metrics/compute/${login} > embed called with`) console.debug(q) let {base} = q q = {..._q, ...Object.fromEntries(Object.keys(Plugins).map(key => [key, false])), ...Object.fromEntries(conf.settings.plugins.base.parts.map(part => [`base.${part}`, false])), template:"classic", ...q} //Translate action syntax to web syntax let parts = [] if (base === true) q = {...q, ...Object.fromEntries(Object.entries(_q).filter(([key]) => /^base[.]?/.test(key)))} if (typeof base === "string") parts = base.split(",").map(x => x.trim()) if (Array.isArray(base)) parts = base for (const part of parts) q[`base.${part}`] = true if (convert === "markdown-pdf") { q["config.animations"] = false q.config_animations = false } q = Object.fromEntries([...Object.entries(q).map(([key, value]) => [key.replace(/^plugin_/, "").replace(/_/g, "."), value]), ["base", false]]) //Compute rendering const {rendered} = await metrics({login, q}, {...arguments[1], convert:["svg", "png", "jpeg"].includes(q["config.output"]) ? q["config.output"] : null}, arguments[2]) console.debug(`metrics/compute/${login}/embed > ${name} > success >>>>>>>>>>>>>>>>>>>>>>`) return `` } //Rendering template source let rendered = source.replace(/\{\{ (?[\s\S]*?) \}\}/g, "{%= $ %}") console.debug(rendered) for (const delimiters of [{openDelimiter:"<", closeDelimiter:">"}, {openDelimiter:"{", closeDelimiter:"}"}]) rendered = await ejs.render(rendered, {...data, s:imports.s, f:imports.format, embed}, {views, async:true, ...delimiters}) console.debug(`metrics/compute/${login} > success`) //Output if (convert === "markdown-pdf") { return imports.svg.pdf(rendered, { paddings:q["config.padding"] || conf.settings.padding, style:(conf.settings.extras?.css ?? conf.settings.extras?.default ? q["extras.css"] ?? "" : ""), twemojis:q["config.twemoji"], gemojis:q["config.gemoji"], rest, }) } return {rendered, mime:"text/html"} } //Rendering console.debug(`metrics/compute/${login} > render`) let rendered = await ejs.render(image, {...data, s:imports.s, f:imports.format, style:style + (conf.settings.extras?.css ?? conf.settings.extras?.default ? q["extras.css"] ?? "" : ""), fonts}, {views, async:true}) //Additional transformations if (q["config.twemoji"]) rendered = await imports.svg.twemojis(rendered) if (q["config.gemoji"]) rendered = await imports.svg.gemojis(rendered, {rest}) //Optimize rendering if ((conf.settings?.optimize === true) || (conf.settings?.optimize?.includes?.("css"))) rendered = await imports.svg.optimize.css(rendered) if ((conf.settings?.optimize === true) || (conf.settings?.optimize?.includes?.("xml"))) rendered = await imports.svg.optimize.xml(rendered, q) if ((conf.settings?.optimize === true) || (conf.settings?.optimize?.includes?.("svg"))) rendered = await imports.svg.optimize.svg(rendered, q, experimental) //Verify svg if (verify) { console.debug(`metrics/compute/${login} > verify SVG`) const libxmljs = (await import("libxmljs2")).default const parsed = libxmljs.parseXml(rendered) if (parsed.errors.length) throw new Error(`Malformed SVG : \n${parsed.errors.join("\n")}`) console.debug(`metrics/compute/${login} > verified SVG, no parsing errors found`) } //Resizing const {resized, mime} = await imports.svg.resize(rendered, {paddings:q["config.padding"] || conf.settings.padding, convert:convert === "svg" ? null : convert}) rendered = resized //Result console.debug(`metrics/compute/${login} > success`) return {rendered, mime} } //Internal error catch (error) { //User not found if (((Array.isArray(error.errors)) && (error.errors[0].type === "NOT_FOUND"))) throw new Error("user not found") //Generic error throw error } } //Metrics insights metrics.insights = async function({login}, {graphql, rest, conf}, {Plugins, Templates}) { const q = { template:"classic", achievements:true, "achievements.threshold":"X", isocalendar:true, "isocalendar.duration":"full-year", languages:true, "languages.limit":0, activity:true, "activity.limit":100, "activity.days":0, notable:true, followup:true, "followup.sections":"repositories, user", habits:true, "habits.from":100, "habits.days":7, "habits.facts":false, "habits.charts":true, introduction:true } const plugins = {achievements:{enabled:true}, isocalendar:{enabled:true}, languages:{enabled:true, extras:false}, activity:{enabled:true, markdown:"extended"}, notable:{enabled:true}, followup:{enabled:true}, habits:{enabled:true, extras:false}, introduction:{enabled:true}} return metrics({login, q}, {graphql, rest, plugins, conf, convert:"json"}, {Plugins, Templates}) } //Metrics insights static render metrics.insights.output = async function ({login, imports, conf}, {graphql, rest, Plugins, Templates}) { //Server console.debug(`metrics/compute/${login} > insights`) const server = `http://localhost:${conf.settings.port}` console.debug(`metrics/compute/${login} > insights > server on port ${conf.settings.port}`) //Data processing const browser = await imports.puppeteer.launch() const page = await browser.newPage() console.debug(`metrics/compute/${login} > insights > generating data`) const json = JSON.stringify(await metrics.insights({login}, {graphql, rest, conf}, {Plugins, Templates})) await page.goto(`${server}/about/${login}?embed=1&localstorage=1`) await page.evaluate(async json => localStorage.setItem("local.metrics", json), json) //eslint-disable-line no-undef await page.goto(`${server}/about/${login}?embed=1&localstorage=1`) await page.waitForSelector(".container .user", {timeout:10*60*1000}) //Rendering console.debug(`metrics/compute/${login} > insights > rendering data`) const rendered = ` Metrics insights: ${login} ${await page.evaluate(() => document.querySelector("main").outerHTML)} ${(await Promise.all([".css/style.vars.css", ".css/style.css", "about/.statics/style.css"].map(path => utils.axios.get(`${server}/${path}`)))).map(({data:style}) => ``).join("\n")} ` await browser.close() return {mime:"text/html", rendered} }