Bladeren bron

feat: load in changelog externally

Sv443 1 jaar geleden
bovenliggende
commit
7fcc9fa33b
10 gewijzigde bestanden met toevoegingen van 69 en 191 verwijderingen
  1. 5 1
      assets/require.json
  2. 3 1
      assets/resources.json
  3. 4 4
      changelog.md
  4. 12 167
      package-lock.json
  5. 1 1
      package.json
  6. 6 2
      rollup.config.mjs
  7. 13 4
      src/menu/menu_old.ts
  8. 12 6
      src/tools/post-build.ts
  9. 4 5
      src/tools/serve.ts
  10. 9 0
      src/utils/misc.ts

+ 5 - 1
assets/require.json

@@ -1 +1,5 @@
-[]
+[
+  {
+    "url": "https://cdn.jsdelivr.net/npm/marked/lib/marked.umd.js"
+  }
+]

+ 3 - 1
assets/resources.json

@@ -15,5 +15,7 @@
   "img-openuserjs": "external/openuserjs.png",
 
   "css-fix_spacing": "style/fixSpacing.css",
-  "css-anchor_improvements": "style/anchorImprovements.css"
+  "css-anchor_improvements": "style/anchorImprovements.css",
+
+  "doc-changelog": "/changelog.md"
 }

+ 4 - 4
changelog.md

@@ -5,7 +5,7 @@
 - **Internal Changes:**
   - Removed React JSX support
   - Small utility function refactoring
-
+  
 [See pull request for more info](https://github.com/Sv443/BetterYTM/pull/TODO)
 
 <div class="split"></div>
@@ -37,7 +37,7 @@
     - Target branch can now be specified while compiling instead of being tied to the bundler mode
   - Added support for React JSX
   - Added support for external libraries through `@require`
-
+  
 [See pull request for more info](https://github.com/Sv443/BetterYTM/pull/35)
 
 <div class="split"></div>
@@ -82,7 +82,7 @@
   - Song names with hyphens are now resolved better for lyrics lookup
   - Site switch with <kbd>F9</kbd> will now keep the video time
   - Moved lots of utility code to my new library [UserUtils](https://github.com/Sv443-Network/UserUtils)
-
+  
 [See pull request for more info](https://github.com/Sv443/BetterYTM/pull/9)
 
 <div class="split"></div>
@@ -94,7 +94,7 @@
   - Switch between YouTube and YT Music (with <kbd>F9</kbd> by default)
   - Search for song lyrics with new button in media controls
   - Remove "Upgrade to YTM Premium" tab
-
+  
 [See pull request for more info](https://github.com/Sv443/BetterYTM/pull/3)
 
 <div class="split"></div>

+ 12 - 167
package-lock.json

@@ -10,10 +10,10 @@
       "license": "AGPL-3.0-only",
       "dependencies": {
         "@sv443-network/userutils": "^4.1.0",
+        "marked": "^12.0.0",
         "nanoevents": "^9.0.0"
       },
       "devDependencies": {
-        "@jackfranklin/rollup-plugin-markdown": "^0.4.0",
         "@rollup/plugin-json": "^6.0.1",
         "@rollup/plugin-node-resolve": "^15.2.3",
         "@rollup/plugin-terser": "^0.4.4",
@@ -221,18 +221,6 @@
       "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
       "dev": true
     },
-    "node_modules/@jackfranklin/rollup-plugin-markdown": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/@jackfranklin/rollup-plugin-markdown/-/rollup-plugin-markdown-0.4.0.tgz",
-      "integrity": "sha512-9B8F/K9mzmD6cpIx1EttIVJBsHv2I1JCPYr01yxI4ibY8419wFeQ7yOykwFYNVN7wjkb+BMsct/eXnPQjt36pg==",
-      "dev": true,
-      "dependencies": {
-        "@types/showdown": "^2.0.0",
-        "gray-matter": "^4.0.2",
-        "rollup-pluginutils": "^2.8.2",
-        "showdown": "^2.1.0"
-      }
-    },
     "node_modules/@jridgewell/gen-mapping": {
       "version": "0.3.3",
       "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
@@ -766,12 +754,6 @@
         "@types/node": "*"
       }
     },
-    "node_modules/@types/showdown": {
-      "version": "2.0.6",
-      "resolved": "https://registry.npmjs.org/@types/showdown/-/showdown-2.0.6.tgz",
-      "integrity": "sha512-pTvD/0CIeqe4x23+YJWlX2gArHa8G0J0Oh6GKaVXV7TAeickpkkZiNOgFcFcmLQ5lB/K0qBJL1FtRYltBfbGCQ==",
-      "dev": true
-    },
     "node_modules/@typescript-eslint/eslint-plugin": {
       "version": "6.17.0",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.17.0.tgz",
@@ -1748,19 +1730,6 @@
         "url": "https://opencollective.com/eslint"
       }
     },
-    "node_modules/esprima": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
-      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
-      "dev": true,
-      "bin": {
-        "esparse": "bin/esparse.js",
-        "esvalidate": "bin/esvalidate.js"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/esquery": {
       "version": "1.5.0",
       "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
@@ -1875,18 +1844,6 @@
       "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
       "dev": true
     },
-    "node_modules/extend-shallow": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
-      "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
-      "dev": true,
-      "dependencies": {
-        "is-extendable": "^0.1.0"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/fast-deep-equal": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -2217,43 +2174,6 @@
       "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
       "dev": true
     },
-    "node_modules/gray-matter": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
-      "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
-      "dev": true,
-      "dependencies": {
-        "js-yaml": "^3.13.1",
-        "kind-of": "^6.0.2",
-        "section-matter": "^1.0.0",
-        "strip-bom-string": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=6.0"
-      }
-    },
-    "node_modules/gray-matter/node_modules/argparse": {
-      "version": "1.0.10",
-      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
-      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
-      "dev": true,
-      "dependencies": {
-        "sprintf-js": "~1.0.2"
-      }
-    },
-    "node_modules/gray-matter/node_modules/js-yaml": {
-      "version": "3.14.1",
-      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
-      "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
-      "dev": true,
-      "dependencies": {
-        "argparse": "^1.0.7",
-        "esprima": "^4.0.0"
-      },
-      "bin": {
-        "js-yaml": "bin/js-yaml.js"
-      }
-    },
     "node_modules/has-flag": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -2473,15 +2393,6 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/is-extendable": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
-      "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/is-extglob": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2581,15 +2492,6 @@
         "json-buffer": "3.0.1"
       }
     },
-    "node_modules/kind-of": {
-      "version": "6.0.3",
-      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
-      "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/levn": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -2654,6 +2556,17 @@
       "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
       "dev": true
     },
+    "node_modules/marked": {
+      "version": "12.0.0",
+      "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.0.tgz",
+      "integrity": "sha512-Vkwtq9rLqXryZnWaQc86+FHLC6tr/fycMfYAhiOIXkrNmeGAyhSxjqu0Rs1i0bBqw5u0S7+lV9fdH2ZSVaoa0w==",
+      "bin": {
+        "marked": "bin/marked.js"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
     "node_modules/media-typer": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -3338,21 +3251,6 @@
         "rollup": "^2.x.x || ^3.x.x || ^4.x.x"
       }
     },
-    "node_modules/rollup-pluginutils": {
-      "version": "2.8.2",
-      "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
-      "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
-      "dev": true,
-      "dependencies": {
-        "estree-walker": "^0.6.1"
-      }
-    },
-    "node_modules/rollup-pluginutils/node_modules/estree-walker": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
-      "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
-      "dev": true
-    },
     "node_modules/run-parallel": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -3411,19 +3309,6 @@
       "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
       "dev": true
     },
-    "node_modules/section-matter": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
-      "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
-      "dev": true,
-      "dependencies": {
-        "extend-shallow": "^2.0.1",
-        "kind-of": "^6.0.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/semver": {
       "version": "7.5.4",
       "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
@@ -3561,31 +3446,6 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/showdown": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/showdown/-/showdown-2.1.0.tgz",
-      "integrity": "sha512-/6NVYu4U819R2pUIk79n67SYgJHWCce0a5xTP979WbNp0FL9MN1I1QK662IDU1b6JzKTvmhgI7T7JYIxBi3kMQ==",
-      "dev": true,
-      "dependencies": {
-        "commander": "^9.0.0"
-      },
-      "bin": {
-        "showdown": "bin/showdown.js"
-      },
-      "funding": {
-        "type": "individual",
-        "url": "https://www.paypal.me/tiviesantos"
-      }
-    },
-    "node_modules/showdown/node_modules/commander": {
-      "version": "9.5.0",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
-      "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
-      "dev": true,
-      "engines": {
-        "node": "^12.20.0 || >=14"
-      }
-    },
     "node_modules/side-channel": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
@@ -3652,12 +3512,6 @@
       "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==",
       "dev": true
     },
-    "node_modules/sprintf-js": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
-      "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
-      "dev": true
-    },
     "node_modules/statuses": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@@ -3693,15 +3547,6 @@
         "node": ">=8"
       }
     },
-    "node_modules/strip-bom-string": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
-      "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/strip-json-comments": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",

+ 1 - 1
package.json

@@ -57,10 +57,10 @@
   },
   "dependencies": {
     "@sv443-network/userutils": "^4.1.0",
+    "marked": "^12.0.0",
     "nanoevents": "^9.0.0"
   },
   "devDependencies": {
-    "@jackfranklin/rollup-plugin-markdown": "^0.4.0",
     "@rollup/plugin-json": "^6.0.1",
     "@rollup/plugin-node-resolve": "^15.2.3",
     "@rollup/plugin-terser": "^0.4.4",

+ 6 - 2
rollup.config.mjs

@@ -3,7 +3,6 @@ import pluginNodeResolve from "@rollup/plugin-node-resolve";
 import pluginJson from "@rollup/plugin-json";
 import pluginHtml from "rollup-plugin-html";
 import pluginCss from "rollup-plugin-import-css";
-import pluginMarkdown from "@jackfranklin/rollup-plugin-markdown";
 import pluginExecute from "rollup-plugin-execute";
 import typescript from "typescript";
 
@@ -37,7 +36,6 @@ export default (/**@type {import("./src/types").RollupArgs}*/ args) => (async ()
       pluginCss({
         output: "global.css",
       }),
-      pluginMarkdown(),
       pluginExecute([
         `npm run --silent post-build -- --mode=${mode} --branch=${branch} --host=${host} --suffix=${suffix}`,
         ...(mode === "development" ? ["npm run --silent invisible -- \"npm run tr-progress\""] : []),
@@ -48,6 +46,9 @@ export default (/**@type {import("./src/types").RollupArgs}*/ args) => (async ()
       format: "iife",
       sourcemap: mode === "development",
       compact: mode === "development",
+      globals: {
+        "marked": "marked",
+      },
     },
     onwarn(warning) {
       // ignore circular dependency warnings
@@ -56,6 +57,9 @@ export default (/**@type {import("./src/types").RollupArgs}*/ args) => (async ()
         console.error(`\x1b[33m(!)\x1b[0m ${message}\n`, rest);
       }
     },
+    external: [
+      "marked",
+    ],
   };
 
   return config;

+ 13 - 4
src/menu/menu_old.ts

@@ -1,12 +1,11 @@
-import { compress, decompress, debounce, isScrollable } from "@sv443-network/userutils";
+import { compress, decompress, debounce, isScrollable, fetchAdvanced } from "@sv443-network/userutils";
 import { defaultConfig, getFeatures, migrations, saveFeatures, setDefaultFeatures } from "../config";
 import { host, scriptInfo } from "../constants";
 import { featInfo, disableBeforeUnload } from "../features/index";
-import { error, getResourceUrl, info, log, resourceToHTMLString, warn, getLocale, hasKey, initTranslations, setLocale, t } from "../utils";
+import { error, getResourceUrl, info, log, resourceToHTMLString, warn, getLocale, hasKey, initTranslations, setLocale, t, parseMarkdown } from "../utils";
 import { formatVersion } from "../config";
 import { emitSiteEvent, siteEvents } from "../siteEvents";
 import type { FeatureCategory, FeatureKey, FeatureConfig, HotkeyObj, FeatureInfo } from "../types";
-import changelog from "../../changelog.md";
 import "./menu_old.css";
 import { createHotkeyInput } from "./hotkeyInput";
 import pkg from "../../package.json" assert { type: "json" };
@@ -1224,6 +1223,16 @@ async function addChangelogMenu() {
 
   //#SECTION body
 
+  const getChangelogHtml = (async () => {
+    try {
+      const changelogHtmlFull = await (await fetchAdvanced(await getResourceUrl("doc-changelog"))).text();
+      return await parseMarkdown(changelogHtmlFull);
+    }
+    catch(err) {
+      return `Error: ${err}`;
+    }
+  });
+
   const menuBodyElem = document.createElement("div");
   menuBodyElem.id = "bytm-changelog-menu-body";
   menuBodyElem.classList.add("bytm-menu-body");
@@ -1231,7 +1240,7 @@ async function addChangelogMenu() {
   const textElem = document.createElement("div");
   textElem.id = "bytm-changelog-menu-text";
   textElem.classList.add("bytm-markdown-container");
-  textElem.innerHTML = changelog.html;
+  textElem.innerHTML = await getChangelogHtml();
 
   //#SECTION finalize
 

+ 12 - 6
src/tools/post-build.ts

@@ -85,7 +85,7 @@ ${localizedDescriptions ? "\n" + localizedDescriptions : ""}\
 // @license           ${pkg.license}
 // @author            ${pkg.author.name}
 // @copyright         ${pkg.author.name} (${pkg.author.url})
-// @icon              ${getAssetUrl("logo/logo_48.png")}
+// @icon              ${getResourceUrl("logo/logo_48.png")}
 // @match             https://music.youtube.com/*
 // @match             https://www.youtube.com/*
 // @run-at            document-start
@@ -256,7 +256,7 @@ async function getResourceDirectives() {
       directives.push(`// @resource          ${name}${bufferSpace} ${
         path.match(/^https?:\/\//)
           ? path
-          : getAssetUrl(path)
+          : getResourceUrl(path)
       }`);
     }
 
@@ -297,11 +297,17 @@ function getLocalizedDescriptions() {
   }
 }
 
-/** Returns the full URL for a given relative asset path, based on the current mode */
-function getAssetUrl(relativePath: string) {
+/**
+ * Returns the full URL for a given resource path, based on the current mode and branch
+ * @path If the path starts with a /, it is treated as an absolute path, starting at project root. Otherwise it will be relative to the assets folder.
+ */
+function getResourceUrl(path: string) {
+  let assetPath = "/assets/";
+  if(path.startsWith("/"))
+    assetPath = "";
   return mode === "development"
-    ? `http://localhost:${devServerPort}/assets/${relativePath}?t=${buildUuid}`
-    : `https://raw.githubusercontent.com/${repo}/${branch}/assets/${relativePath}`;
+    ? `http://localhost:${devServerPort}${assetPath}${path}?t=${buildUuid}`
+    : `https://raw.githubusercontent.com/${repo}/${branch}${assetPath}${path}`;
 }
 
 /** Returns the value of a CLI argument (in the format `--arg=<value>`) or the value of `defaultVal` if it doesn't exist */

+ 4 - 5
src/tools/serve.ts

@@ -24,15 +24,14 @@ app.use((err: unknown, _req: Request, _res: Response, _next: NextFunction) => {
     console.error("\x1b[31mError in dev server:\x1b[0m\n", err);
 });
 
-// app.use((_req, res, next) => {
-//   res.setHeader("Cache-Control", "no-store");
-//   next();
-// });
-// serves everything from `rollupConfig.output.path` (`dist/` by default)
 app.use("/", express.static(
   resolve(fileURLToPath(import.meta.url), `../../../${outputDir}`)
 ));
 
+app.use("/", express.static(
+  resolve(fileURLToPath(import.meta.url), "../../../")
+));
+
 app.use("/assets", express.static(
   resolve(fileURLToPath(import.meta.url), "../../../assets/")
 ));

+ 9 - 0
src/utils/misc.ts

@@ -1,4 +1,5 @@
 import { fetchAdvanced, randomId } from "@sv443-network/userutils";
+import { marked } from "marked";
 import { branch, repo } from "../constants";
 import { type Domain, type ResourceKey } from "../types";
 import { error, type TrLocale, warn } from ".";
@@ -95,3 +96,11 @@ export async function resourceToHTMLString(resource: ResourceKey) {
     return null;
   }
 }
+
+/** Parses a markdown string and turns it into an HTML string - doesn't sanitize against XSS! */
+export function parseMarkdown(md: string) {
+  return marked.parse(md, {
+    async: true,
+    gfm: true,
+  });
+}