diff --git a/.github/quickstart/template/metadata.yml b/.github/quickstart/template/metadata.yml new file mode 100644 index 00000000..1f59c6a6 --- /dev/null +++ b/.github/quickstart/template/metadata.yml @@ -0,0 +1,14 @@ +name: "đŸ–ŧī¸ Template name" +extends: classic # Fallback to "classic" template "template.mjs" if not trusted +index: ~ # Leave as it (this is used to order plugins on metrics README.md) +supports: + - user # Support users account + - organization # Support organizations account + - repository # Support repositories metrics +formats: + - svg # Support SVG output + - png # Support PNG output + - jpeg # Support JPEG output + - json # Support JSON output + - markdown # Support markdown output + - markdown-pdf # Support PDF output \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 52b454bd..754519c1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -122,6 +122,7 @@ This section explain how metrics is structured. * `queries/` contains plugin GraphQL queries * `source/templates/` contains templates files * `README.md` contains template documentation + * `metadata.yml` contains template metadata * `image.svg` contains template image used to render metrics * `style.css` contains style used to render metrics * `fonts.css` contains additional fonts used to render metrics @@ -201,6 +202,7 @@ npm run quickstart -- template It will create a new folder in [`source/templates`](https://github.com/lowlighter/metrics/tree/master/source/templates) with the following files: - A `README.md` to describe your template and document it - An `image.svg` with base structure for rendering +- A `metadata.yml` which list templates attributes and supported formats - A `partials/` folder where you'll be able to implement parts of your template - A `partials/_.json` with a JSON array listing these parts in the order you want them displayed (unless overridden by user with `config_order` option) @@ -209,6 +211,7 @@ If needed, you can also create the following optional files: - A `styles.css` with custom CSS that'll style your template - A `template.mjs` with additional data processing and formatting at template-level - When your template is used through `setup_community_templates` on official releases, this is disabled by default unless user trusts it by appending `+trust` at the end of source + - You can specify the default `template.mjs` fallback by filling `extends` key in your `metadata.yml` (defaults to `"classic"` template) If inexistent, these will fallback to [`classic`](https://github.com/lowlighter/metrics/tree/master/source/templates/classic) template files. @@ -253,6 +256,33 @@ As you can see, we exploit the fact that SVG images are able to render HTML and +
+đŸ’Ŧ Filling metadata.yml + +`metadata.yml` is an optional file which describes what account types are allowed, which formats are supported, etc. + +Here's an example: +```yaml +name: "đŸ–ŧī¸ Template name" +extends: classic # Fallback to "classic" template "template.mjs" if not trusted +index: ~ # Leave as it (this is used to order plugins on metrics README.md) +supports: + - user # Support users account + - organization # Support organizations account + - repository # Support repositories metrics +formats: + - svg # Support SVG output + - png # Support PNG output + - jpeg # Support JPEG output + - json # Support JSON output + - markdown # Support markdown output + - markdown-pdf # Support PDF output +``` + +Core plugin will automatically check whether template supports given account or repository and output format and will throw an error in case they aren't compatible. + +
+
đŸ’Ŧ Adding custom fonts diff --git a/source/app/action/index.mjs b/source/app/action/index.mjs index 4f8d6eb3..12ada790 100644 --- a/source/app/action/index.mjs +++ b/source/app/action/index.mjs @@ -75,7 +75,7 @@ ...config } = metadata.plugins.core.inputs.action({core}) const q = {...query, ...(_repo ? {repo:_repo} : null), template} - const _output = ["jpeg", "png", "json", "markdown", "markdown-pdf"].includes(config["config.output"]) ? config["config.output"] : null + const _output = ["svg", "jpeg", "png", "json", "markdown", "markdown-pdf"].includes(config["config.output"]) ? config["config.output"] : metadata.templates[template].formats[0] ?? null const filename = _filename.replace(/[*]/g, {jpeg:"jpg", markdown:"md", "markdown-pdf":"pdf"}[_output] ?? _output) //Docker image diff --git a/source/app/metrics/index.mjs b/source/app/metrics/index.mjs index 2e236a54..824cc5d1 100644 --- a/source/app/metrics/index.mjs +++ b/source/app/metrics/index.mjs @@ -21,6 +21,8 @@ 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 = [] @@ -45,7 +47,7 @@ //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}, {pending, imports}) + 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 @@ -150,7 +152,7 @@ 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}) + const {resized, mime} = await imports.svg.resize(rendered, {paddings:q["config.padding"] || conf.settings.padding, convert:convert === "svg" ? null : convert}) rendered = resized //Result diff --git a/source/app/metrics/metadata.mjs b/source/app/metrics/metadata.mjs index 762cac82..bc761b6d 100644 --- a/source/app/metrics/metadata.mjs +++ b/source/app/metrics/metadata.mjs @@ -43,8 +43,8 @@ Templates[name] = await metadata.template({__templates, name, plugins, logger}) } //Reorder keys - const {classic, repository, markdown, community, ...templates} = Templates - Templates = {classic, repository, ...templates, markdown, community} + const {community, ...templates} = Templates + Templates = {...Object.fromEntries(Object.entries(templates).sort(([_an, a], [_bn, b]) => (a.index ?? Infinity) - (b.index ?? Infinity))), community} //Packaged metadata const packaged = JSON.parse(`${await fs.promises.readFile(__package)}`) @@ -254,7 +254,9 @@ metadata.template = async function({__templates, name, plugins, logger}) { try { //Load meta descriptor - const raw = `${await fs.promises.readFile(path.join(__templates, name, "README.md"), "utf-8")}` + const raw = fs.existsSync(path.join(__templates, name, "metadata.yml")) ? `${await fs.promises.readFile(path.join(__templates, name, "metadata.yml"), "utf-8")}` : "" + const readme = `${await fs.promises.readFile(path.join(__templates, name, "README.md"), "utf-8")}` + const meta = yaml.load(raw) ?? {} //Compatibility const partials = path.join(__templates, name, "partials") @@ -269,11 +271,25 @@ //Result return { - name:raw.match(/^### (?[\s\S]+?)\n/)?.groups?.name?.trim(), + name:meta.name ?? readme.match(/^### (?[\s\S]+?)\n/)?.groups?.name?.trim(), + index:meta.index ?? null, + formats:meta.formats ?? null, + supports:meta.supports ?? null, readme:{ - demo:raw.match(/(?[\s\S]*?<[/]table>)/)?.groups?.demo?.replace(/<[/]?(?:table|tr)>/g, "")?.trim() ?? (name === "community" ? '' : ""), + demo:readme.match(/(?
See documentation 🌍
[\s\S]*?<[/]table>)/)?.groups?.demo?.replace(/<[/]?(?:table|tr)>/g, "")?.trim() ?? (name === "community" ? '' : ""), compatibility:{...compatibility, base:true}, }, + check({q, account = "bypass", format = null}) { + //Support check + if (account !== "bypass") { + const context = q.repo ? "repository" : account + if ((Array.isArray(this.supports))&&(!this.supports.includes(context))) + throw new Error(`not supported for: ${context}`) + } + //Format check + if ((format)&&(Array.isArray(this.formats))&&(!this.formats.includes(format))) + throw new Error(`not supported for: ${format}`) + }, } } catch (error) { diff --git a/source/app/metrics/setup.mjs b/source/app/metrics/setup.mjs index 95931bf6..38966844 100644 --- a/source/app/metrics/setup.mjs +++ b/source/app/metrics/setup.mjs @@ -5,6 +5,7 @@ import processes from "child_process" import util from "util" import url from "url" + import yaml from "js-yaml" import OctokitRest from "@octokit/rest" //Templates and plugins @@ -94,6 +95,16 @@ else if (fs.existsSync(path.join(__templates, `@${name}`, "template.mjs"))) { logger(`metrics/setup > removing @${name}/template.mjs`) await fs.promises.unlink(path.join(__templates, `@${name}`, "template.mjs")) + const inherit = yaml.load(`${fs.promises.readFile(path.join(__templates, `@${name}`, "metadata.yml"))}`).extends ?? null + if (inherit) { + logger(`metrics/setup > @${name} extends from ${inherit}`) + if (fs.existsSync(path.join(__templates, inherit, "template.mjs"))) { + logger(`metrics/setup > @${name} extended from ${inherit}`) + await fs.promises.copyFile(path.join(__templates, inherit, "template.mjs"), path.join(__templates, `@${name}`, "template.mjs")) + } + else + logger(`metrics/setup > @${name} could not extends ${inherit} as it does not exist`) + } } else logger(`metrics/setup > @${name}/template.mjs does not exist`) @@ -194,7 +205,7 @@ } } - //Load metadata (plugins) + //Load metadata conf.metadata = await metadata({log}) //Store authenticated user diff --git a/source/app/web/instance.mjs b/source/app/web/instance.mjs index b549026e..ed740550 100644 --- a/source/app/web/instance.mjs +++ b/source/app/web/instance.mjs @@ -261,7 +261,7 @@ graphql, rest, plugins, conf, die:q["plugins.errors.fatal"] ?? false, verify:q.verify ?? false, - convert:["jpeg", "png", "json", "markdown", "markdown-pdf"].includes(q["config.output"]) ? q["config.output"] : null, + convert:["svg", "jpeg", "png", "json", "markdown", "markdown-pdf"].includes(q["config.output"]) ? q["config.output"] : null, }, {Plugins, Templates}) //Cache if ((!debug)&&(cached)) { @@ -284,6 +284,11 @@ console.debug(`metrics/app/${login} > 400 (bad request)`) return res.status(400).send("Bad request: unsupported template") } + //Unsupported output format or account type + if ((error instanceof Error)&&(/^not supported for: [\s\S]*$/.test(error.message))) { + console.debug(`metrics/app/${login} > 406 (Not Acceptable)`) + return res.status(406).send("Not Acceptable: unsupported output format or account type for specified parameters") + } //GitHub failed request if ((error instanceof Error)&&(/this may be the result of a timeout, or it could be a GitHub bug/i.test(error.errors?.[0]?.message))) { console.debug(`metrics/app/${login} > 502 (bad gateway from GitHub)`) diff --git a/source/plugins/core/index.mjs b/source/plugins/core/index.mjs index 1a2f3e34..cc03526e 100644 --- a/source/plugins/core/index.mjs +++ b/source/plugins/core/index.mjs @@ -4,9 +4,10 @@ */ //Setup - export default async function({login, q}, {conf, data, rest, graphql, plugins, queries, account}, {pending, imports}) { + export default async function({login, q}, {conf, data, rest, graphql, plugins, queries, account, convert, template}, {pending, imports}) { //Load inputs const {"config.animations":animations, "config.timezone":_timezone, "debug.flags":dflags} = imports.metadata.plugins.core.inputs({data, account, q}) + imports.metadata.templates[template].check({q, account, format:convert}) //Init const computed = {commits:0, sponsorships:0, licenses:{favorite:"", used:{}}, token:{}, repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_closed:0, pr_merged:0, forks:0, forked:0, releases:0}} diff --git a/source/plugins/core/metadata.yml b/source/plugins/core/metadata.yml index 227689bf..04b0e7a8 100644 --- a/source/plugins/core/metadata.yml +++ b/source/plugins/core/metadata.yml @@ -186,8 +186,9 @@ inputs: config_output: description: Output image format type: string - default: svg + default: auto values: + - auto # Defaults to template default - svg - png # Does not support animations - jpeg # Does not support animations and transparency diff --git a/source/templates/classic/README.md b/source/templates/classic/README.md index 80759f2d..b838d72d 100644 --- a/source/templates/classic/README.md +++ b/source/templates/classic/README.md @@ -1,4 +1,4 @@ -### 📗 Classic +### 📗 Classic template Default template, mimicking GitHub visual identity. @@ -11,6 +11,8 @@ Default template, mimicking GitHub visual identity. #### â„šī¸ Examples workflows +[âžĄī¸ Supported formats and inputs](metadata.yml) + ```yaml - uses: lowlighter/metrics@latest with: diff --git a/source/templates/classic/metadata.yml b/source/templates/classic/metadata.yml new file mode 100644 index 00000000..8487c448 --- /dev/null +++ b/source/templates/classic/metadata.yml @@ -0,0 +1,10 @@ +name: "📗 Classic template" +index: 0 +supports: + - user + - organization +formats: + - svg + - png + - jpeg + - json diff --git a/source/templates/markdown/README.md b/source/templates/markdown/README.md index c3f92415..1d63a0b9 100644 --- a/source/templates/markdown/README.md +++ b/source/templates/markdown/README.md @@ -1,4 +1,4 @@ -### 📒 Markdown +### 📒 Markdown template Markdown template can render a **markdown template** by interpreting **templating brackets** `{{` and `}}`. @@ -24,6 +24,8 @@ For convenience, several useful properties are aliased in [/source/templates/mar #### â„šī¸ Examples workflows +[âžĄī¸ Supported formats and inputs](metadata.yml) + ```yaml # Markdown output - uses: lowlighter/metrics@latest @@ -33,7 +35,6 @@ For convenience, several useful properties are aliased in [/source/templates/mar filename: README.md # Output file markdown: TEMPLATE.md # Template file markdown_cache: .cache # Cache folder - config_output: markdown # Output as markdown file ``` ```yaml diff --git a/source/templates/markdown/metadata.yml b/source/templates/markdown/metadata.yml new file mode 100644 index 00000000..973d4440 --- /dev/null +++ b/source/templates/markdown/metadata.yml @@ -0,0 +1,9 @@ +name: "📒 Markdown template" +index: 3 +supports: + - user + - organization +formats: + - markdown + - markdown-pdf + - json diff --git a/source/templates/repository/README.md b/source/templates/repository/README.md index e8bf2b3d..3a082230 100644 --- a/source/templates/repository/README.md +++ b/source/templates/repository/README.md @@ -1,4 +1,4 @@ -### 📘 Repository +### 📘 Repository template Template crafted for repositories, mimicking GitHub visual identity. @@ -11,6 +11,8 @@ Template crafted for repositories, mimicking GitHub visual identity. #### â„šī¸ Examples workflows +[âžĄī¸ Supported formats and inputs](metadata.yml) + ```yaml - uses: lowlighter/metrics@latest with: diff --git a/source/templates/repository/metadata.yml b/source/templates/repository/metadata.yml new file mode 100644 index 00000000..45f9e933 --- /dev/null +++ b/source/templates/repository/metadata.yml @@ -0,0 +1,9 @@ +name: "📘 Repository template" +index: 1 +supports: + - repository +formats: + - svg + - png + - jpeg + - json diff --git a/source/templates/terminal/README.md b/source/templates/terminal/README.md index 7b25ec42..1aff2d57 100644 --- a/source/templates/terminal/README.md +++ b/source/templates/terminal/README.md @@ -1,4 +1,4 @@ -### 📙 Terminal +### 📙 Terminal template Terminal template, mimicking a SSH session. @@ -11,6 +11,8 @@ Terminal template, mimicking a SSH session. #### â„šī¸ Examples workflows +[âžĄī¸ Supported formats and inputs](metadata.yml) + ```yaml - uses: lowlighter/metrics@latest with: diff --git a/source/templates/terminal/metadata.yml b/source/templates/terminal/metadata.yml new file mode 100644 index 00000000..a6816a77 --- /dev/null +++ b/source/templates/terminal/metadata.yml @@ -0,0 +1,10 @@ +name: "📙 Terminal template" +index: 2 +supports: + - user + - organization +formats: + - svg + - png + - jpeg + - json
See documentation 🌍