Skip to content

Commit 1ca0c99

Browse files
authored
Add templates metadata (lowlighter#273)
1 parent c4af6f6 commit 1ca0c99

File tree

17 files changed

+142
-17
lines changed

17 files changed

+142
-17
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name: "🖼️ Template name"
2+
extends: classic # Fallback to "classic" template "template.mjs" if not trusted
3+
index: ~ # Leave as it (this is used to order plugins on metrics README.md)
4+
supports:
5+
- user # Support users account
6+
- organization # Support organizations account
7+
- repository # Support repositories metrics
8+
formats:
9+
- svg # Support SVG output
10+
- png # Support PNG output
11+
- jpeg # Support JPEG output
12+
- json # Support JSON output
13+
- markdown # Support markdown output
14+
- markdown-pdf # Support PDF output

CONTRIBUTING.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ This section explain how metrics is structured.
122122
* `queries/` contains plugin GraphQL queries
123123
* `source/templates/` contains templates files
124124
* `README.md` contains template documentation
125+
* `metadata.yml` contains template metadata
125126
* `image.svg` contains template image used to render metrics
126127
* `style.css` contains style used to render metrics
127128
* `fonts.css` contains additional fonts used to render metrics
@@ -201,6 +202,7 @@ npm run quickstart -- template <template_name>
201202
It will create a new folder in [`source/templates`](https://github.com/lowlighter/metrics/tree/master/source/templates) with the following files:
202203
- A `README.md` to describe your template and document it
203204
- An `image.svg` with base structure for rendering
205+
- A `metadata.yml` which list templates attributes and supported formats
204206
- A `partials/` folder where you'll be able to implement parts of your template
205207
- 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)
206208

@@ -209,6 +211,7 @@ If needed, you can also create the following optional files:
209211
- A `styles.css` with custom CSS that'll style your template
210212
- A `template.mjs` with additional data processing and formatting at template-level
211213
- 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
214+
- You can specify the default `template.mjs` fallback by filling `extends` key in your `metadata.yml` (defaults to `"classic"` template)
212215

213216
If inexistent, these will fallback to [`classic`](https://github.com/lowlighter/metrics/tree/master/source/templates/classic) template files.
214217

@@ -253,6 +256,33 @@ As you can see, we exploit the fact that SVG images are able to render HTML and
253256
254257
</details>
255258
259+
<details>
260+
<summary>💬 Filling <code>metadata.yml</code></summary>
261+
262+
`metadata.yml` is an optional file which describes what account types are allowed, which formats are supported, etc.
263+
264+
Here's an example:
265+
```yaml
266+
name: "🖼️ Template name"
267+
extends: classic # Fallback to "classic" template "template.mjs" if not trusted
268+
index: ~ # Leave as it (this is used to order plugins on metrics README.md)
269+
supports:
270+
- user # Support users account
271+
- organization # Support organizations account
272+
- repository # Support repositories metrics
273+
formats:
274+
- svg # Support SVG output
275+
- png # Support PNG output
276+
- jpeg # Support JPEG output
277+
- json # Support JSON output
278+
- markdown # Support markdown output
279+
- markdown-pdf # Support PDF output
280+
```
281+
282+
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.
283+
284+
</details>
285+
256286
<details>
257287
<summary>💬 Adding custom fonts</summary>
258288

source/app/action/index.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
...config
7676
} = metadata.plugins.core.inputs.action({core})
7777
const q = {...query, ...(_repo ? {repo:_repo} : null), template}
78-
const _output = ["jpeg", "png", "json", "markdown", "markdown-pdf"].includes(config["config.output"]) ? config["config.output"] : null
78+
const _output = ["svg", "jpeg", "png", "json", "markdown", "markdown-pdf"].includes(config["config.output"]) ? config["config.output"] : metadata.templates[template].formats[0] ?? null
7979
const filename = _filename.replace(/[*]/g, {jpeg:"jpg", markdown:"md", "markdown-pdf":"pdf"}[_output] ?? _output)
8080

8181
//Docker image

source/app/metrics/index.mjs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
throw new Error("unsupported template")
2222
const {image, style, fonts, views, partials} = conf.templates[template]
2323
const computer = Templates[template].default || Templates[template]
24+
convert = convert ?? conf.metadata.templates[template].formats[0] ?? null
25+
console.debug(`metrics/compute/${login} > output format set to ${convert}`)
2426

2527
//Initialization
2628
const pending = []
@@ -45,7 +47,7 @@
4547
//Executing base plugin and compute metrics
4648
console.debug(`metrics/compute/${login} > compute`)
4749
await Plugins.base({login, q, data, rest, graphql, plugins, queries, pending, imports}, conf)
48-
await computer({login, q}, {conf, data, rest, graphql, plugins, queries, account:data.account}, {pending, imports})
50+
await computer({login, q}, {conf, data, rest, graphql, plugins, queries, account:data.account, convert, template}, {pending, imports})
4951
const promised = await Promise.all(pending)
5052

5153
//Check plugins errors
@@ -150,7 +152,7 @@
150152
console.debug(`metrics/compute/${login} > verified SVG, no parsing errors found`)
151153
}
152154
//Resizing
153-
const {resized, mime} = await imports.svg.resize(rendered, {paddings:q["config.padding"] || conf.settings.padding, convert})
155+
const {resized, mime} = await imports.svg.resize(rendered, {paddings:q["config.padding"] || conf.settings.padding, convert:convert === "svg" ? null : convert})
154156
rendered = resized
155157

156158
//Result

source/app/metrics/metadata.mjs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@
4343
Templates[name] = await metadata.template({__templates, name, plugins, logger})
4444
}
4545
//Reorder keys
46-
const {classic, repository, markdown, community, ...templates} = Templates
47-
Templates = {classic, repository, ...templates, markdown, community}
46+
const {community, ...templates} = Templates
47+
Templates = {...Object.fromEntries(Object.entries(templates).sort(([_an, a], [_bn, b]) => (a.index ?? Infinity) - (b.index ?? Infinity))), community}
4848

4949
//Packaged metadata
5050
const packaged = JSON.parse(`${await fs.promises.readFile(__package)}`)
@@ -254,7 +254,9 @@
254254
metadata.template = async function({__templates, name, plugins, logger}) {
255255
try {
256256
//Load meta descriptor
257-
const raw = `${await fs.promises.readFile(path.join(__templates, name, "README.md"), "utf-8")}`
257+
const raw = fs.existsSync(path.join(__templates, name, "metadata.yml")) ? `${await fs.promises.readFile(path.join(__templates, name, "metadata.yml"), "utf-8")}` : ""
258+
const readme = `${await fs.promises.readFile(path.join(__templates, name, "README.md"), "utf-8")}`
259+
const meta = yaml.load(raw) ?? {}
258260

259261
//Compatibility
260262
const partials = path.join(__templates, name, "partials")
@@ -269,11 +271,25 @@
269271

270272
//Result
271273
return {
272-
name:raw.match(/^### (?<name>[\s\S]+?)\n/)?.groups?.name?.trim(),
274+
name:meta.name ?? readme.match(/^### (?<name>[\s\S]+?)\n/)?.groups?.name?.trim(),
275+
index:meta.index ?? null,
276+
formats:meta.formats ?? null,
277+
supports:meta.supports ?? null,
273278
readme:{
274-
demo:raw.match(/(?<demo><table>[\s\S]*?<[/]table>)/)?.groups?.demo?.replace(/<[/]?(?:table|tr)>/g, "")?.trim() ?? (name === "community" ? '<td align="center" colspan="2">See <a href="/source/templates/community/README.md">documentation</a> 🌍</td>' : "<td></td>"),
279+
demo:readme.match(/(?<demo><table>[\s\S]*?<[/]table>)/)?.groups?.demo?.replace(/<[/]?(?:table|tr)>/g, "")?.trim() ?? (name === "community" ? '<td align="center" colspan="2">See <a href="/source/templates/community/README.md">documentation</a> 🌍</td>' : "<td></td>"),
275280
compatibility:{...compatibility, base:true},
276281
},
282+
check({q, account = "bypass", format = null}) {
283+
//Support check
284+
if (account !== "bypass") {
285+
const context = q.repo ? "repository" : account
286+
if ((Array.isArray(this.supports))&&(!this.supports.includes(context)))
287+
throw new Error(`not supported for: ${context}`)
288+
}
289+
//Format check
290+
if ((format)&&(Array.isArray(this.formats))&&(!this.formats.includes(format)))
291+
throw new Error(`not supported for: ${format}`)
292+
},
277293
}
278294
}
279295
catch (error) {

source/app/metrics/setup.mjs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import processes from "child_process"
66
import util from "util"
77
import url from "url"
8+
import yaml from "js-yaml"
89
import OctokitRest from "@octokit/rest"
910

1011
//Templates and plugins
@@ -94,6 +95,16 @@
9495
else if (fs.existsSync(path.join(__templates, `@${name}`, "template.mjs"))) {
9596
logger(`metrics/setup > removing @${name}/template.mjs`)
9697
await fs.promises.unlink(path.join(__templates, `@${name}`, "template.mjs"))
98+
const inherit = yaml.load(`${fs.promises.readFile(path.join(__templates, `@${name}`, "metadata.yml"))}`).extends ?? null
99+
if (inherit) {
100+
logger(`metrics/setup > @${name} extends from ${inherit}`)
101+
if (fs.existsSync(path.join(__templates, inherit, "template.mjs"))) {
102+
logger(`metrics/setup > @${name} extended from ${inherit}`)
103+
await fs.promises.copyFile(path.join(__templates, inherit, "template.mjs"), path.join(__templates, `@${name}`, "template.mjs"))
104+
}
105+
else
106+
logger(`metrics/setup > @${name} could not extends ${inherit} as it does not exist`)
107+
}
97108
}
98109
else
99110
logger(`metrics/setup > @${name}/template.mjs does not exist`)
@@ -194,7 +205,7 @@
194205
}
195206
}
196207

197-
//Load metadata (plugins)
208+
//Load metadata
198209
conf.metadata = await metadata({log})
199210

200211
//Store authenticated user

source/app/web/instance.mjs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@
261261
graphql, rest, plugins, conf,
262262
die:q["plugins.errors.fatal"] ?? false,
263263
verify:q.verify ?? false,
264-
convert:["jpeg", "png", "json", "markdown", "markdown-pdf"].includes(q["config.output"]) ? q["config.output"] : null,
264+
convert:["svg", "jpeg", "png", "json", "markdown", "markdown-pdf"].includes(q["config.output"]) ? q["config.output"] : null,
265265
}, {Plugins, Templates})
266266
//Cache
267267
if ((!debug)&&(cached)) {
@@ -284,6 +284,11 @@
284284
console.debug(`metrics/app/${login} > 400 (bad request)`)
285285
return res.status(400).send("Bad request: unsupported template")
286286
}
287+
//Unsupported output format or account type
288+
if ((error instanceof Error)&&(/^not supported for: [\s\S]*$/.test(error.message))) {
289+
console.debug(`metrics/app/${login} > 406 (Not Acceptable)`)
290+
return res.status(406).send("Not Acceptable: unsupported output format or account type for specified parameters")
291+
}
287292
//GitHub failed request
288293
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))) {
289294
console.debug(`metrics/app/${login} > 502 (bad gateway from GitHub)`)

source/plugins/core/index.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
*/
55

66
//Setup
7-
export default async function({login, q}, {conf, data, rest, graphql, plugins, queries, account}, {pending, imports}) {
7+
export default async function({login, q}, {conf, data, rest, graphql, plugins, queries, account, convert, template}, {pending, imports}) {
88
//Load inputs
99
const {"config.animations":animations, "config.timezone":_timezone, "debug.flags":dflags} = imports.metadata.plugins.core.inputs({data, account, q})
10+
imports.metadata.templates[template].check({q, account, format:convert})
1011

1112
//Init
1213
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}}

source/plugins/core/metadata.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,9 @@ inputs:
186186
config_output:
187187
description: Output image format
188188
type: string
189-
default: svg
189+
default: auto
190190
values:
191+
- auto # Defaults to template default
191192
- svg
192193
- png # Does not support animations
193194
- jpeg # Does not support animations and transparency

source/templates/classic/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
### 📗 Classic
1+
### 📗 Classic template
22

33
Default template, mimicking GitHub visual identity.
44

@@ -11,6 +11,8 @@ Default template, mimicking GitHub visual identity.
1111

1212
#### ℹ️ Examples workflows
1313

14+
[➡️ Supported formats and inputs](metadata.yml)
15+
1416
```yaml
1517
- uses: lowlighter/metrics@latest
1618
with:

0 commit comments

Comments
 (0)