From 94f1deb37db821e720b696f793e9559439f11728 Mon Sep 17 00:00:00 2001 From: Jo Franchetti Date: Mon, 25 Nov 2024 17:40:56 +0000 Subject: [PATCH] fmt --- _includes/doc.tsx | 1 - _includes/renderCommand.tsx | 336 +++++++++++++------------ learn/_components/CopyButton.tsx | 37 ++- learn/_components/SnippetComponent.tsx | 97 ++++--- learn/_pages/ExamplePage.tsx | 224 ++++++++--------- learn/index.tsx | 6 +- learn/types.ts | 83 +++--- learn/utils/parseExample.ts | 289 +++++++++++---------- 8 files changed, 539 insertions(+), 534 deletions(-) diff --git a/_includes/doc.tsx b/_includes/doc.tsx index 9243a231..1e3f0029 100644 --- a/_includes/doc.tsx +++ b/_includes/doc.tsx @@ -199,4 +199,3 @@ export default function Page(props: Lume.Data, helpers: Lume.Helpers) { ); } - diff --git a/_includes/renderCommand.tsx b/_includes/renderCommand.tsx index b3f9e197..81ae5443 100644 --- a/_includes/renderCommand.tsx +++ b/_includes/renderCommand.tsx @@ -6,8 +6,20 @@ import CLI_REFERENCE from "../runtime/reference/cli/_commands_reference.json" wi }; import { VNode } from "npm:preact"; -type ArgType = { name: string; help: string; short: string | number | bigint | boolean | object | VNode | null | undefined; }; - +type ArgType = { + name: string; + help: string; + short: + | string + | number + | bigint + | boolean + | object + | VNode + | null + | undefined; +}; + const ANSI_RE = ansiRegex(); const SUBSEQUENT_ANSI_RE = new RegExp( `(?:${ANSI_RE.source})(?:${ANSI_RE.source})`, @@ -31,172 +43,172 @@ function flagsToInlineCode(text: string): string { } export default function renderCommand( - commandName: string, - helpers: Lume.Helpers, - ): { rendered: any; toc: TableOfContentsItem_[] } { - const command = CLI_REFERENCE.subcommands.find((command) => - command.name === commandName - )!; - - const toc: TableOfContentsItem_[] = []; - - let about = command.about!.replaceAll( - SUBSEQUENT_ENCAPSULATED_ANSI_RE, - function ( - _, - _opening1, - text1, - _closing1, - space, - opening2, - text2, - closing2, - ) { - return `${opening2}${text1}${space}${text2}${closing2}`; - }, - ).replaceAll(SUBSEQUENT_ANSI_RE, ""); - let aboutLines = about.split("\n"); - const aboutLinesReadMoreIndex = aboutLines.findLastIndex((line) => - line.toLowerCase().replaceAll(ANSI_RE, "").trim().startsWith("read more:") - ); - if (aboutLinesReadMoreIndex !== -1) { - aboutLines = aboutLines.slice(0, aboutLinesReadMoreIndex); - } - - about = aboutLines.join("\n").replaceAll( - ENCAPSULATED_ANSI_RE, - (_, opening, text, _closing, offset, string) => { - if (opening === "\u001b[32m") { // green, used as heading - return `### ${text}`; - } else if ( - opening === "\u001b[38;5;245m" || opening === "\u001b[36m" || - opening === "\u001b[1m" || opening === "\u001b[22m" - ) { // gray and cyan used for code and snippets, and we treat yellow and bold as well as such - const lines = string.split("\n"); - let line = ""; - - while (offset > 0) { - line = lines.shift(); - offset -= line.length; - } - - if (START_AND_END_ANSI_RE.test(line.trim())) { - return "\n```\n" + text + "\n```\n\n"; - } else { - return "`" + text + "`"; - } + commandName: string, + helpers: Lume.Helpers, +): { rendered: any; toc: TableOfContentsItem_[] } { + const command = CLI_REFERENCE.subcommands.find((command) => + command.name === commandName + )!; + + const toc: TableOfContentsItem_[] = []; + + let about = command.about!.replaceAll( + SUBSEQUENT_ENCAPSULATED_ANSI_RE, + function ( + _, + _opening1, + text1, + _closing1, + space, + opening2, + text2, + closing2, + ) { + return `${opening2}${text1}${space}${text2}${closing2}`; + }, + ).replaceAll(SUBSEQUENT_ANSI_RE, ""); + let aboutLines = about.split("\n"); + const aboutLinesReadMoreIndex = aboutLines.findLastIndex((line) => + line.toLowerCase().replaceAll(ANSI_RE, "").trim().startsWith("read more:") + ); + if (aboutLinesReadMoreIndex !== -1) { + aboutLines = aboutLines.slice(0, aboutLinesReadMoreIndex); + } + + about = aboutLines.join("\n").replaceAll( + ENCAPSULATED_ANSI_RE, + (_, opening, text, _closing, offset, string) => { + if (opening === "\u001b[32m") { // green, used as heading + return `### ${text}`; + } else if ( + opening === "\u001b[38;5;245m" || opening === "\u001b[36m" || + opening === "\u001b[1m" || opening === "\u001b[22m" + ) { // gray and cyan used for code and snippets, and we treat yellow and bold as well as such + const lines = string.split("\n"); + let line = ""; + + while (offset > 0) { + line = lines.shift(); + offset -= line.length; + } + + if (START_AND_END_ANSI_RE.test(line.trim())) { + return "\n```\n" + text + "\n```\n\n"; } else { - return text; + return "`" + text + "`"; } - }, - ); - - const args = []; - const options: Record = {}; - - for (const arg of command.args) { - if (arg.help_heading === "Unstable options") { - continue; - } - - if (arg.long) { - const key = arg.help_heading ?? "Options"; - options[key] ??= []; - options[key].push(arg); } else { - args.push(arg); + return text; } + }, + ); + + const args = []; + const options: Record = {}; + + for (const arg of command.args) { + if (arg.help_heading === "Unstable options") { + continue; + } + + if (arg.long) { + const key = arg.help_heading ?? "Options"; + options[key] ??= []; + options[key].push(arg); + } else { + args.push(arg); } - - const rendered = ( -
-
-

- Command line usage -

-
-
+  }
+
+  const rendered = (
+    
+
+

+ Command line usage +

+
+
               {command.usage.replaceAll(ANSI_RE, "").slice("usage: ".length)}
-            
-
+
- -
-
- - {Object.entries(options).map(([heading, flags]) => { - const id = heading.toLowerCase().replace(/\s/g, "-"); - - const renderedFlags = flags.toSorted((a: ArgType, b: ArgType) => - a.name.localeCompare(b.name) - ).map((flag: ArgType) => renderOption(id, flag, helpers)); - - toc.push({ - text: heading, - slug: id, - children: [], - }); - - return ( - <> -

- {heading} -

- {renderedFlags} - - ); - })}
- ); - - return { - rendered, - toc, - }; - } - - function renderOption(group: string, arg: ArgType, helpers: Lume.Helpers) { - const id = `${group}-${arg.name}`; - - let docsLink = null; - let help = arg.help.replaceAll(ANSI_RE, ""); - const helpLines = help.split("\n"); - const helpLinesDocsIndex = helpLines.findLastIndex((line) => - line.toLowerCase() - .trim() - .startsWith("docs:") - ); - if (helpLinesDocsIndex !== -1) { - help = helpLines.slice(0, helpLinesDocsIndex).join("\n"); - docsLink = helpLines[helpLinesDocsIndex].trim().slice("docs:".length); - } - - return ( - <> -

- - {docsLink - ? {"--" + arg.name} - : ("--" + arg.name)} - {" "} - -

- {arg.short && ( -

- Short flag: -{arg.short} -

- )} - {arg.help && ( -

- )} - - ); + +

+
+ + {Object.entries(options).map(([heading, flags]) => { + const id = heading.toLowerCase().replace(/\s/g, "-"); + + const renderedFlags = flags.toSorted((a: ArgType, b: ArgType) => + a.name.localeCompare(b.name) + ).map((flag: ArgType) => renderOption(id, flag, helpers)); + + toc.push({ + text: heading, + slug: id, + children: [], + }); + + return ( + <> +

+ {heading} +

+ {renderedFlags} + + ); + })} +
+ ); + + return { + rendered, + toc, + }; +} + +function renderOption(group: string, arg: ArgType, helpers: Lume.Helpers) { + const id = `${group}-${arg.name}`; + + let docsLink = null; + let help = arg.help.replaceAll(ANSI_RE, ""); + const helpLines = help.split("\n"); + const helpLinesDocsIndex = helpLines.findLastIndex((line) => + line.toLowerCase() + .trim() + .startsWith("docs:") + ); + if (helpLinesDocsIndex !== -1) { + help = helpLines.slice(0, helpLinesDocsIndex).join("\n"); + docsLink = helpLines[helpLinesDocsIndex].trim().slice("docs:".length); } + + return ( + <> +

+ + {docsLink + ? {"--" + arg.name} + : ("--" + arg.name)} + {" "} + +

+ {arg.short && ( +

+ Short flag: -{arg.short} +

+ )} + {arg.help && ( +

+ )} + + ); +} diff --git a/learn/_components/CopyButton.tsx b/learn/_components/CopyButton.tsx index e02eedfa..f5d06c6b 100644 --- a/learn/_components/CopyButton.tsx +++ b/learn/_components/CopyButton.tsx @@ -1,20 +1,19 @@ export function CopyButton(props: { text: string }) { - return ( - - ); - } - \ No newline at end of file + return ( + + ); +} diff --git a/learn/_components/SnippetComponent.tsx b/learn/_components/SnippetComponent.tsx index 549aef94..c90fa080 100644 --- a/learn/_components/SnippetComponent.tsx +++ b/learn/_components/SnippetComponent.tsx @@ -1,59 +1,58 @@ import { ExampleSnippet } from "../types.ts"; export default function SnippetComponent(props: { - filename: string; - firstOfFile: boolean; - lastOfFile: boolean; - snippet: ExampleSnippet; - }) { - return ( -

-
- {props.snippet.text} -
-
- {props.filename && ( - +
+ {props.snippet.text} +
+
+ {props.filename && ( + + {props.filename} + + )} +
+ {props.snippet.code && ( +
- {props.filename} - - )} -
- {props.snippet.code && ( -
-
                   
-                
-
- )} -
+ +
+ )}
- ); - } - \ No newline at end of file +
+ ); +} diff --git a/learn/_pages/ExamplePage.tsx b/learn/_pages/ExamplePage.tsx index 3795ba3c..903aa28a 100644 --- a/learn/_pages/ExamplePage.tsx +++ b/learn/_pages/ExamplePage.tsx @@ -2,135 +2,133 @@ import { CopyButton } from "../_components/CopyButton.tsx"; import SnippetComponent from "../_components/SnippetComponent.tsx"; import { ExampleFromFileSystem } from "../types.ts"; -type Props = { example: ExampleFromFileSystem }; +type Props = { example: ExampleFromFileSystem }; export default function ExamplePage({ example }: Props) { + const contentNoCommentary = example.parsed.files.map((file) => + file.snippets.map((snippet) => snippet.code).join("\n") + ).join("\n"); + const url = + `https://github.com/denoland/deno-docs/blob/main/examples/${example.name}${ + example.parsed.files.length > 1 ? "/main" : "" + }`; + const rawUrl = `https://docs.deno.com/learn/examples/${example.name}${ + example.parsed.files.length > 1 ? "/main" : "" + }`; - const contentNoCommentary = example.parsed.files.map((file) => - file.snippets.map((snippet) => snippet.code).join("\n") - ).join("\n"); - const url = - `https://github.com/denoland/deno-docs/blob/main/examples/${example.name}${ - example.parsed.files.length > 1 ? "/main" : "" - }`; - const rawUrl = `https://docs.deno.com/learn/examples/${example.name}${ - example.parsed.files.length > 1 ? "/main" : "" - }`; + return ( +
+
+
+
+

+ {example.parsed.title} +

+ {example.parsed.description && ( +

+ )} +

- - return ( + + Edit on Github + +
+
+ +
+ {example.parsed.files.map((file) => ( +
+ {file.snippets.map((snippet, i) => ( + + ))} +
+ ))}
-
-
-
-

- {example.parsed.title} -

- {example.parsed.description && ( -

- )} -

- + {example.parsed.run && ( + <> +

+ Run{" "} - Edit on Github - -

-
- -
- {example.parsed.files.map((file) => ( -
- {file.snippets.map((snippet, i) => ( - - ))} -
- ))} -
- {example.parsed.run && ( - <> -

- Run{" "} - - this example - {" "} - locally using the Deno CLI: -

-
-
+                  this example
+                {" "}
+                locally using the Deno CLI:
+              

+
+
                         
                         {example.parsed.run.startsWith("deno")
                             ? example.parsed.run.replace("", url)
                             : "deno run " +
                             example.parsed.run.replace("", rawUrl)}
                         
-                    
-
- - )} - {example.parsed.playground && ( -
-

- Try this example in a Deno Deploy playground: -

-

+

+
+ + )} + {example.parsed.playground && ( +
+

+ Try this example in a Deno Deploy playground: +

+

+ + Deploy + +

+
+ )} + {example.parsed.additionalResources.length > 0 && ( +
+

Additional resources

+
- )} - {example.parsed.additionalResources.length > 0 && ( -
-

Additional resources

-
    - {example.parsed.additionalResources.map(([link, title]) => ( -
  • - - {title} - -
  • - ))} -
-
- )} + + ))} +
-
+ )}
- ) +
+
+ ); } diff --git a/learn/index.tsx b/learn/index.tsx index 4c7e3416..8574c94c 100644 --- a/learn/index.tsx +++ b/learn/index.tsx @@ -4,7 +4,7 @@ import ExamplePage from "./_pages/ExamplePage.tsx"; import ExamplesPage from "./_pages/ExamplesPage.tsx"; import TutorialPage from "./_pages/TutorialsPage.tsx"; import VideoPage from "./_pages/VideosPage.tsx"; -import {ExampleFromFileSystem } from "./types.ts"; +import { ExampleFromFileSystem } from "./types.ts"; import { parseExample } from "./utils/parseExample.ts"; export const layout = "raw.tsx"; @@ -41,8 +41,8 @@ export const sidebar = [ ]; export default function* (_data: Lume.Data, helpers: Lume.Helpers) { - const files = [...walkSync("./learn/examples/", { exts: [".ts"] }) ]; - + const files = [...walkSync("./learn/examples/", { exts: [".ts"] })]; + const examples = files.map((file) => { const content = Deno.readTextFileSync(file.path); diff --git a/learn/types.ts b/learn/types.ts index 3220e4c3..3021ba29 100644 --- a/learn/types.ts +++ b/learn/types.ts @@ -1,57 +1,56 @@ - export const TAGS = { - cli: { - title: "cli", - description: "Works in Deno CLI", - }, - deploy: { - title: "deploy", - description: "Works on Deno Deploy", - }, - web: { - title: "web", - description: "Works in on the Web", - }, - }; + cli: { + title: "cli", + description: "Works in Deno CLI", + }, + deploy: { + title: "deploy", + description: "Works on Deno Deploy", + }, + web: { + title: "web", + description: "Works in on the Web", + }, +}; export const DIFFICULTIES = { - beginner: { - title: "Beginner", - description: "No significant prior knowledge is required for this example.", - }, - intermediate: { - title: "Intermediate", - description: "Some prior knowledge is needed for this example.", - }, + beginner: { + title: "Beginner", + description: "No significant prior knowledge is required for this example.", + }, + intermediate: { + title: "Intermediate", + description: "Some prior knowledge is needed for this example.", + }, }; export type ExampleFromFileSystem = { - name: string; - content: string; - label: string; - parsed: Example; - }; + name: string; + content: string; + label: string; + parsed: Example; +}; export interface Example { - id: string; - title: string; - description: string; - difficulty: keyof typeof DIFFICULTIES; - tags: (keyof typeof TAGS)[]; - additionalResources: [string, string][]; - run?: string; - playground?: string; - files: ExampleFile[]; - group: string; - sortOrder: any; + id: string; + title: string; + description: string; + difficulty: keyof typeof DIFFICULTIES; + tags: (keyof typeof TAGS)[]; + additionalResources: [string, string][]; + run?: string; + playground?: string; + files: ExampleFile[]; + group: string; + sortOrder: any; } export interface ExampleFile { - name: string; - snippets: ExampleSnippet[]; + name: string; + snippets: ExampleSnippet[]; } export interface ExampleSnippet { - text: string; - code: string; + text: string; + code: string; } diff --git a/learn/utils/parseExample.ts b/learn/utils/parseExample.ts index 314de3c8..ec32ccaf 100644 --- a/learn/utils/parseExample.ts +++ b/learn/utils/parseExample.ts @@ -1,163 +1,162 @@ import { DIFFICULTIES, Example, ExampleFile, TAGS } from "../types.ts"; export function parseExample(id: string, file: string): Example { - // Extract the multi line JS doc comment at the top of the file - const [, jsdoc, rest] = file.match(/^\s*\/\*\*(.*?)\*\/\s*(.*)/s) || []; - - // Extract the @key value pairs from the JS doc comment - let description = ""; - const kvs: Record = {}; - const resources = []; - for (let line of jsdoc.split("\n")) { - line = line.trim().replace(/^\*/, "").trim(); - const [, key, value] = line.match(/^\s*@(\w+)\s+(.*)/) || []; - if (key) { - if (key === "resource") { - resources.push(value); - } else { - kvs[key] = value.trim(); - } + // Extract the multi line JS doc comment at the top of the file + const [, jsdoc, rest] = file.match(/^\s*\/\*\*(.*?)\*\/\s*(.*)/s) || []; + + // Extract the @key value pairs from the JS doc comment + let description = ""; + const kvs: Record = {}; + const resources = []; + for (let line of jsdoc.split("\n")) { + line = line.trim().replace(/^\*/, "").trim(); + const [, key, value] = line.match(/^\s*@(\w+)\s+(.*)/) || []; + if (key) { + if (key === "resource") { + resources.push(value); } else { - description += " " + line; + kvs[key] = value.trim(); } + } else { + description += " " + line; } - description = description.trim(); - - // Separate the code into snippets. - const files: ExampleFile[] = [ - { - name: "", - snippets: [], - }, - ]; - let parseMode = "code"; - let currentFile = files[0]; - let text = ""; - let code = ""; - - for (const line of rest.split("\n")) { - const trimmedLine = line.trim(); - if (parseMode == "code") { - if (line.startsWith("// File:")) { - if (text || code.trimEnd()) { - code = code.trimEnd(); - currentFile.snippets.push({ text, code }); - text = ""; - code = ""; - } - const name = line.slice(8).trim(); - if (currentFile.snippets.length == 0) { - currentFile.name = name; - } else { - currentFile = { - name, - snippets: [], - }; - files.push(currentFile); - } - } else if (line.startsWith("/* File:")) { - if (text || code.trimEnd()) { - code = code.trimEnd(); - currentFile.snippets.push({ text, code }); - text = ""; - code = ""; - } - const name = line.slice(8).trim(); - if (currentFile.snippets.length == 0) { - currentFile.name = name; - } else { - currentFile = { - name, - snippets: [], - }; - files.push(currentFile); - } - parseMode = "file"; - } else if ( - trimmedLine.startsWith("// deno-lint-ignore") || - trimmedLine.startsWith("//deno-lint-ignore") || - trimmedLine.startsWith("// deno-fmt-ignore") || - trimmedLine.startsWith("//deno-fmt-ignore") - ) { - // skip deno directives - } else if (trimmedLine.startsWith("//-")) { - code += line.replace("//-", "//") + "\n"; - } else if (trimmedLine.startsWith("//")) { - if (text || code.trimEnd()) { - code = code.trimEnd(); - currentFile.snippets.push({ text, code }); - } - text = trimmedLine.slice(2).trim(); + } + description = description.trim(); + + // Separate the code into snippets. + const files: ExampleFile[] = [ + { + name: "", + snippets: [], + }, + ]; + let parseMode = "code"; + let currentFile = files[0]; + let text = ""; + let code = ""; + + for (const line of rest.split("\n")) { + const trimmedLine = line.trim(); + if (parseMode == "code") { + if (line.startsWith("// File:")) { + if (text || code.trimEnd()) { + code = code.trimEnd(); + currentFile.snippets.push({ text, code }); + text = ""; code = ""; - parseMode = "comment"; - } else { - code += line + "\n"; } - } else if (parseMode == "comment") { - if ( - trimmedLine.startsWith("// deno-lint-ignore") || - trimmedLine.startsWith("//deno-lint-ignore") || - trimmedLine.startsWith("// deno-fmt-ignore") || - trimmedLine.startsWith("//deno-fmt-ignore") - ) { - // skip deno directives - } else if (trimmedLine.startsWith("//")) { - text += " " + trimmedLine.slice(2).trim(); + const name = line.slice(8).trim(); + if (currentFile.snippets.length == 0) { + currentFile.name = name; } else { - code += line + "\n"; - parseMode = "code"; + currentFile = { + name, + snippets: [], + }; + files.push(currentFile); } - } else if (parseMode == "file") { - if (line == "*/") { - parseMode = "code"; + } else if (line.startsWith("/* File:")) { + if (text || code.trimEnd()) { + code = code.trimEnd(); + currentFile.snippets.push({ text, code }); + text = ""; + code = ""; + } + const name = line.slice(8).trim(); + if (currentFile.snippets.length == 0) { + currentFile.name = name; } else { - code += line + "\n"; + currentFile = { + name, + snippets: [], + }; + files.push(currentFile); } + parseMode = "file"; + } else if ( + trimmedLine.startsWith("// deno-lint-ignore") || + trimmedLine.startsWith("//deno-lint-ignore") || + trimmedLine.startsWith("// deno-fmt-ignore") || + trimmedLine.startsWith("//deno-fmt-ignore") + ) { + // skip deno directives + } else if (trimmedLine.startsWith("//-")) { + code += line.replace("//-", "//") + "\n"; + } else if (trimmedLine.startsWith("//")) { + if (text || code.trimEnd()) { + code = code.trimEnd(); + currentFile.snippets.push({ text, code }); + } + text = trimmedLine.slice(2).trim(); + code = ""; + parseMode = "comment"; + } else { + code += line + "\n"; } - } - if (text || code.trimEnd()) { - code = code.trimEnd(); - currentFile.snippets.push({ text, code }); - } - - if (!kvs.title) { - throw new Error("Missing title in JS doc comment."); - } - - const tags = kvs.tags.split(",").map((s) => s.trim() as keyof typeof TAGS); - for (const tag of tags) { - if (!TAGS[tag]) { - throw new Error(`Unknown tag '${tag}'`); + } else if (parseMode == "comment") { + if ( + trimmedLine.startsWith("// deno-lint-ignore") || + trimmedLine.startsWith("//deno-lint-ignore") || + trimmedLine.startsWith("// deno-fmt-ignore") || + trimmedLine.startsWith("//deno-fmt-ignore") + ) { + // skip deno directives + } else if (trimmedLine.startsWith("//")) { + text += " " + trimmedLine.slice(2).trim(); + } else { + code += line + "\n"; + parseMode = "code"; + } + } else if (parseMode == "file") { + if (line == "*/") { + parseMode = "code"; + } else { + code += line + "\n"; } } - - const difficulty = kvs.difficulty as keyof typeof DIFFICULTIES; - if (!DIFFICULTIES[difficulty]) { - throw new Error(`Unknown difficulty '${difficulty}'`); + } + if (text || code.trimEnd()) { + code = code.trimEnd(); + currentFile.snippets.push({ text, code }); + } + + if (!kvs.title) { + throw new Error("Missing title in JS doc comment."); + } + + const tags = kvs.tags.split(",").map((s) => s.trim() as keyof typeof TAGS); + for (const tag of tags) { + if (!TAGS[tag]) { + throw new Error(`Unknown tag '${tag}'`); } - - const additionalResources: [string, string][] = []; - for (const resource of resources) { - // @resource {https://jsr.io/@std/http/server/~/} std/http/server - const [_, url, title] = resource.match(/^\{(.*?)\}\s(.*)/) || []; - if (!url || !title) { - throw new Error(`Invalid resource: ${resource}`); - } - additionalResources.push([url, title]); + } + + const difficulty = kvs.difficulty as keyof typeof DIFFICULTIES; + if (!DIFFICULTIES[difficulty]) { + throw new Error(`Unknown difficulty '${difficulty}'`); + } + + const additionalResources: [string, string][] = []; + for (const resource of resources) { + // @resource {https://jsr.io/@std/http/server/~/} std/http/server + const [_, url, title] = resource.match(/^\{(.*?)\}\s(.*)/) || []; + if (!url || !title) { + throw new Error(`Invalid resource: ${resource}`); } - - return { - id, - title: kvs.title, - description, - difficulty, - tags, - additionalResources, - run: kvs.run, - playground: kvs.playground, - files, - group: kvs.group || "Misc", - sortOrder: kvs.sortOrder || 999999, - }; + additionalResources.push([url, title]); } - \ No newline at end of file + + return { + id, + title: kvs.title, + description, + difficulty, + tags, + additionalResources, + run: kvs.run, + playground: kvs.playground, + files, + group: kvs.group || "Misc", + sortOrder: kvs.sortOrder || 999999, + }; +}