From 3b6bbd4cfdf89aced79ce711e3e24845fb64a14a Mon Sep 17 00:00:00 2001 From: Doug Parker Date: Thu, 7 Sep 2023 21:21:12 -0700 Subject: [PATCH] Adds markdown rendering of the getting started page. Refs #16. This got a little involved, but I learned a lot in the process here. The general pipeline is: 1. Perform an `async` read of the `getting_started.md` file. 2. Parse the markdown with `gray-matter` to separate the frontmatter and markdown body. 3. Parse the markdown body with `marked` and transform it into HTML. 4. Validate the frontmatter with a `zod` schema. 5. Render the page in Preact. This relies on a "fetch-then-render" approach, where all the data is loaded up front asynchronously _and then_ rendered afterwards. I initially tried a "fetch-as-you-render" approach with `` but found it to be a bit lacking. Preact's `` implementation is still experimental, and currently intended for lazy loading components. Data loading with `` is not really a well-trodden path and does not have defined primitives, either in Preact or React. Most React frameworks seem to define their own data loading primitives which I was initially hoping to avoid, letting users structure their own rendering process. For now, the easiest option is to fetch data up front and render afterwards. This isn't great performance and I'm not totally satisfied with it, but it's good enough for now. In the future, hopefully we can revisit async Preact and come away with a better path forward. [See discussion in `preact-ssr-prepass`.](https://github.com/preactjs/preact-ssr-prepass/issues/55) --- .vscode/settings.json | 1 + docs/BUILD.bazel | 10 ++- docs/README.md | 20 ++++++ docs/components/markdown/BUILD.bazel | 46 +++++++++++++ docs/components/markdown/markdown.css | 16 +++++ docs/components/markdown/markdown.tsx | 33 +++++++++ docs/components/markdown/markdown_test.tsx | 20 ++++++ docs/markdown/BUILD.bazel | 64 ++++++++++++++++++ docs/markdown/markdown_loader.mts | 59 ++++++++++++++++ docs/markdown/markdown_loader_test.mts | 30 +++++++++ docs/markdown/markdown_page.mts | 45 +++++++++++++ docs/markdown/markdown_page_mock.mts | 26 +++++++ docs/markdown/markdown_page_test.mts | 34 ++++++++++ docs/markdown/markdown_testdata.md | 10 +++ docs/site.tsx | 31 +++++++-- docs/www/BUILD.bazel | 2 + .../getting_started/getting_started.md | 10 +++ package.json | 6 +- pnpm-lock.yaml | 67 +++++++++++++++++++ 19 files changed, 520 insertions(+), 10 deletions(-) create mode 100644 docs/README.md create mode 100644 docs/components/markdown/BUILD.bazel create mode 100644 docs/components/markdown/markdown.css create mode 100644 docs/components/markdown/markdown.tsx create mode 100644 docs/components/markdown/markdown_test.tsx create mode 100644 docs/markdown/BUILD.bazel create mode 100644 docs/markdown/markdown_loader.mts create mode 100644 docs/markdown/markdown_loader_test.mts create mode 100644 docs/markdown/markdown_page.mts create mode 100644 docs/markdown/markdown_page_mock.mts create mode 100644 docs/markdown/markdown_page_test.mts create mode 100644 docs/markdown/markdown_testdata.md create mode 100644 docs/www/tutorials/getting_started/getting_started.md diff --git a/.vscode/settings.json b/.vscode/settings.json index 691f23bc..22c183f7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,6 +18,7 @@ "effectfully", "execroot", "expando", + "frontmatter", "genfiles", "hydroactive", "inlines", diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index eafe1bc0..a6688834 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel @@ -25,11 +25,17 @@ prerender_pages( ts_project( name = "prerender", srcs = ["site.tsx"], - # Need `"type": "module"` to load `*.js` files output by `*.tsx` compilation. - data = [":package"], + data = [ + # Need `"type": "module"` to load `*.js` files output by `*.tsx` + # compilation. + ":package", + "//docs/www:tutorials/getting_started/getting_started.md", + ], deps = [ ":route", + "//docs/components/markdown:markdown_prerender", "//docs/components/under_construction:under_construction_prerender", + "//docs/markdown:markdown_loader", "//docs/www:index_prerender", "//docs/www/not_found:not_found_prerender", "//:node_modules/@rules_prerender/preact", diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..d2dae32d --- /dev/null +++ b/docs/README.md @@ -0,0 +1,20 @@ +# Documentation Site + +This is the documentation site for `@rules_prerender`. + +Hosted at https://rules-prerender.dwac.dev/. + +The site itself is generated with `@rules_prerender` as an alpha tester of new +features. + +## Markdown Conventions + +Most pages are authored in markdown and processed through the same content +pipeline. Markdown pages support frontmatter and must adhere to a specific +schema documented below. This schema is implemented in +[`markdown_page.mts`](/docs/markdown/markdown_page.mts) and should give solid +error messages when not followed correctly. + +| Option | Semantics | +| ------- | --------- | +| `title` | Defines the title of the generated page. This is used by the `` tag as well as rendered in the page body. Markdown pages should _not_ use a `# h1` header. | diff --git a/docs/components/markdown/BUILD.bazel b/docs/components/markdown/BUILD.bazel new file mode 100644 index 00000000..1cfe5ef8 --- /dev/null +++ b/docs/components/markdown/BUILD.bazel @@ -0,0 +1,46 @@ +load("//:index.bzl", "css_library", "prerender_component") +load("//tools/typescript:defs.bzl", "ts_project") +load("//tools/jasmine:defs.bzl", "jasmine_node_test") + +prerender_component( + name = "markdown", + prerender = ":prerender", + styles = ":styles", + visibility = ["//docs:__subpackages__"], +) + +ts_project( + name = "prerender", + srcs = ["markdown.tsx"], + deps = [ + "//docs/components/layout:layout_prerender", + "//docs/markdown:markdown_page", + "//:node_modules/@rules_prerender/preact", + "//:node_modules/preact", + "//:prerender_components/@rules_prerender/declarative_shadow_dom_prerender", + ], +) + +ts_project( + name = "prerender_test_lib", + srcs = ["markdown_test.tsx"], + testonly = True, + deps = [ + ":markdown_prerender", + "//docs/markdown:markdown_page_mock", + "//:node_modules/preact", + "//:node_modules/preact-render-to-string", + "//:node_modules/rules_prerender", + "//:node_modules/@types/jasmine", + ], +) + +jasmine_node_test( + name = "prerender_test", + deps = [":prerender_test_lib"], +) + +css_library( + name = "styles", + srcs = ["markdown.css"], +) diff --git a/docs/components/markdown/markdown.css b/docs/components/markdown/markdown.css new file mode 100644 index 00000000..70b73367 --- /dev/null +++ b/docs/components/markdown/markdown.css @@ -0,0 +1,16 @@ +:host { + height: 100%; +} + +#md { + height: calc(100% - (2 * 1em)); + padding: 1rem 0; +} + +#md > :first-child { + margin-block-start: 0; +} + +#md > :last-child { + margin-block-end: 0; +} diff --git a/docs/components/markdown/markdown.tsx b/docs/components/markdown/markdown.tsx new file mode 100644 index 00000000..5c508e9f --- /dev/null +++ b/docs/components/markdown/markdown.tsx @@ -0,0 +1,33 @@ +import { Template } from '@rules_prerender/declarative_shadow_dom/preact.mjs'; +import { inlineStyle } from '@rules_prerender/preact'; +import { type VNode } from 'preact'; +import { Layout } from '../layout/layout.js'; +import { Route } from '../../route.mjs'; +import { MarkdownPage } from '../../markdown/markdown_page.mjs'; + +/** + * Renders a docs page based on the given runfiles path to the markdown file. + * + * @param page The runfiles-relative path to the markdown file to render. + * @param routes Routes to render page navigation with. + */ +export function Markdown({ page, routes }: { + page: MarkdownPage, + routes?: readonly Route[], +}): VNode { + return <Layout + pageTitle={page.metadata.title} + headerTitle={page.metadata.title} + routes={routes} + > + <div> + <Template shadowrootmode="open"> + {inlineStyle('./markdown.css', import.meta)} + + <div id="md" dangerouslySetInnerHTML={{ + __html: page.html.getHtmlAsString(), + }}></div> + </Template> + </div> + </Layout>; +} diff --git a/docs/components/markdown/markdown_test.tsx b/docs/components/markdown/markdown_test.tsx new file mode 100644 index 00000000..f1c63fa2 --- /dev/null +++ b/docs/components/markdown/markdown_test.tsx @@ -0,0 +1,20 @@ +import { render } from 'preact-render-to-string'; +import { safe } from 'rules_prerender'; +import { Markdown } from './markdown.js'; +import { mockMarkdownPage } from '../../markdown/markdown_page_mock.mjs'; + +describe('markdown', () => { + describe('Markdown', () => { + it('renders the given markdown page', () => { + const md = mockMarkdownPage({ + metadata: { title: 'My title' }, + html: safe`<div>Hello, World!</div>`, + }); + const page = <Markdown page={md} routes={[]} />; + + const html = render(page); + expect(html).toContain('My title'); + expect(html).toContain('<div>Hello, World!</div>'); + }); + }); +}); diff --git a/docs/markdown/BUILD.bazel b/docs/markdown/BUILD.bazel new file mode 100644 index 00000000..d0c82577 --- /dev/null +++ b/docs/markdown/BUILD.bazel @@ -0,0 +1,64 @@ +load("//tools/jasmine:defs.bzl", "jasmine_node_test") +load("//tools/typescript:defs.bzl", "ts_project") + +ts_project( + name = "markdown_loader", + srcs = ["markdown_loader.mts"], + visibility = ["//docs:__subpackages__"], + deps = [ + "//:node_modules/@types/marked", + "//:node_modules/@types/node", + "//:node_modules/gray-matter", + "//:node_modules/marked", + "//:node_modules/rules_prerender", + ], +) + +ts_project( + name = "markdown_loader_test_lib", + srcs = ["markdown_loader_test.mts"], + data = ["markdown_testdata.md"], + testonly = True, + deps = [ + ":markdown_loader", + "//:node_modules/@types/jasmine", + ], +) + +jasmine_node_test( + name = "markdown_loader_test", + deps = [":markdown_loader_test_lib"], +) + +ts_project( + name = "markdown_page", + srcs = ["markdown_page.mts"], + visibility = ["//docs:__subpackages__"], + deps = [ + "//:node_modules/rules_prerender", + "//:node_modules/zod", + ], +) + +ts_project( + name = "markdown_page_test_lib", + srcs = ["markdown_page_test.mts"], + testonly = True, + deps = [ + ":markdown_page", + "//:node_modules/@types/jasmine", + ], +) + +jasmine_node_test( + name = "markdown_page_test", + deps = [":markdown_page_test_lib"], +) + +ts_project( + name = "markdown_page_mock", + srcs = ["markdown_page_mock.mts"], + visibility = ["//docs:__subpackages__"], + testonly = True, + deps = [":markdown_page"], +) diff --git a/docs/markdown/markdown_loader.mts b/docs/markdown/markdown_loader.mts new file mode 100644 index 00000000..9450035c --- /dev/null +++ b/docs/markdown/markdown_loader.mts @@ -0,0 +1,59 @@ +import { promises as fs } from 'fs'; +import { marked } from 'marked'; +import * as path from 'path'; +import grayMatter from 'gray-matter'; +import { SafeHtml, unsafeTreatStringAsSafeHtml } from 'rules_prerender'; + +/** + * Represents a markdown file which has been parsed into HTML. Includes + * frontmatter content without any schema or assumptions applied. + */ +export interface ParsedMarkdown { + /** Frontmatter from the markdown file. */ + frontmatter: Record<string, unknown>; + + /** HTML content of the markdown file. */ + html: SafeHtml; +} + +/** + * Reads the page given as a runfiles path and parses it as markdown, returning + * the HTML and frontmatter. + * + * @param page A runfiles-relative path to the markdown file to render. + * @returns The parsed markdown frontmatter and HTML content. + */ +export async function renderMarkdown(page: string): Promise<ParsedMarkdown> { + const runfiles = process.env['RUNFILES']; + if (!runfiles) throw new Error('`${RUNFILES}` not set.'); + + // Constrain this functionality to `*.md` files to reduce risk of misuse or + // insecure usage. + if (!page.endsWith('.md')) { + throw new Error(`Markdown files *must* use the \`.md\` file extension.`); + } + + // Read markdown from runfiles. + let md: string; + try { + md = await fs.readFile(path.join(runfiles, page), 'utf8'); + } catch (err: any) { + if (err.code === 'ENOENT') { + throw new Error(`Failed to read markdown file. Was it included as a \`data\` dependency?\n${err.message}`); + } else { + throw err; + } + } + + // Extract frontmatter from markdown files. + const { content, data } = grayMatter(md); + + // Convert markdown to HTML. The HTML content comes directly from markdown + // file in runfiles, so we can be fairly confident this is a source file or + // built from source with no user input. + const html = unsafeTreatStringAsSafeHtml(await marked(content, { + async: true, + })); + + return { frontmatter: data, html }; +} diff --git a/docs/markdown/markdown_loader_test.mts b/docs/markdown/markdown_loader_test.mts new file mode 100644 index 00000000..0b056dbc --- /dev/null +++ b/docs/markdown/markdown_loader_test.mts @@ -0,0 +1,30 @@ +import { renderMarkdown } from './markdown_loader.mjs'; + +describe('markdown_loader', () => { + describe('renderMarkdown()', () => { + it('renders a markdown file from runfiles', async () => { + const { frontmatter: metadata, html } = await renderMarkdown( + 'rules_prerender/docs/markdown/markdown_testdata.md'); + + expect(metadata).toEqual({ + key: 'value', + array: [ 1, 2, 3 ], + nested: { + foo: 'bar', + }, + }); + + expect(html.getHtmlAsString()).toContain('<h1>Hello, World!</h1>'); + }); + + it('throws an error when given a file without an `*.md` extension', async () => { + await expectAsync(renderMarkdown('rules_prerender/non/md/file.txt')) + .toBeRejectedWithError(/use the `.md` file extension/); + }); + + it('throws an error when the file is not found', async () => { + await expectAsync(renderMarkdown('rules_prerender/does/not/exist.md')) + .toBeRejectedWithError(/Was it included as a `data` dependency\?/); + }); + }); +}); diff --git a/docs/markdown/markdown_page.mts b/docs/markdown/markdown_page.mts new file mode 100644 index 00000000..f8136859 --- /dev/null +++ b/docs/markdown/markdown_page.mts @@ -0,0 +1,45 @@ +import { SafeHtml } from 'rules_prerender'; +import { z } from 'zod'; + +/** + * Represents a parsed markdown page including frontmatter metadata and raw + * HTML content. + */ +export interface MarkdownPage { + metadata: MarkdownMetadata; + html: SafeHtml; +} + +/** + * Represents the metadata of a markdown page. Most of this comes from + * frontmatter, but some may come from processing of the markdown contents. + */ +export type MarkdownMetadata = z.infer<typeof pageMetadataSchema>; + +// Validates markdown page frontmatter. +const pageMetadataSchema = z.strictObject({ + title: z.string(), +}); + +/** + * Validates the given frontmatter and asserts that it matches page metadata + * schema. + */ +export function parsePageMetadata(page: string, frontmatter: unknown): MarkdownMetadata { + const result = pageMetadataSchema.safeParse(frontmatter); + if (result.success) return result.data; + + // `formErrors` are errors on the root object (ex. parsing `null` directly). + // `fieldErrors` are errors for fields of the object and sub-objects. + const { formErrors, fieldErrors } = result.error.flatten(); + const formErrorsMessage = formErrors.join('\n'); + const fieldErrorsMessage = Object.entries(fieldErrors) + .map(([ field, message ]) => `Property \`${field}\`: ${message}`) + .join('\n'); + const errorMessage = formErrorsMessage !== '' + ? `${formErrorsMessage}\n${fieldErrorsMessage}` + : fieldErrorsMessage; + + throw new Error(`Error processing markdown frontmatter for page \`${ + page}\`:\n${errorMessage}`); +} diff --git a/docs/markdown/markdown_page_mock.mts b/docs/markdown/markdown_page_mock.mts new file mode 100644 index 00000000..9e29dbd4 --- /dev/null +++ b/docs/markdown/markdown_page_mock.mts @@ -0,0 +1,26 @@ +/** + * @fileoverview Mocks file with functions for creating mock objects for + * markdown data structures. + */ + +import { SafeHtml, safe } from 'rules_prerender'; +import { MarkdownMetadata, MarkdownPage } from './markdown_page.mjs'; + +/** Mocks a {@link MarkdownPage} object with defaults. */ +export function mockMarkdownPage({ metadata, html }: { + metadata?: MarkdownMetadata, + html?: SafeHtml, +} = {}): MarkdownPage { + return { + metadata: mockMarkdownMetadata(metadata), + html: html ?? safe`<button>Mocked HTML.</button>`, + }; +} + +/** Mocks a {@link MarkdownMetadata} object with defaults. */ +export function mockMarkdownMetadata({ title }: { title?: string } = {}): + MarkdownMetadata { + return { + title: title ?? 'An interesting post', + }; +} diff --git a/docs/markdown/markdown_page_test.mts b/docs/markdown/markdown_page_test.mts new file mode 100644 index 00000000..04aeeb53 --- /dev/null +++ b/docs/markdown/markdown_page_test.mts @@ -0,0 +1,34 @@ +import { parsePageMetadata, MarkdownMetadata } from './markdown_page.mjs'; + +describe('markdown_page', () => { + describe('parsePageMetadata()', () => { + const golden: MarkdownMetadata = { + title: 'Hello, World!', + }; + + it('returns valid data', () => { + expect(() => parsePageMetadata('page.md', golden)).not.toThrow(); + }); + + it('throws an error when given invalid data', () => { + // Bad input object. + expect(() => parsePageMetadata('page.md', false)).toThrow(); + expect(() => parsePageMetadata('page.md', null)).toThrow(); + expect(() => parsePageMetadata('page.md', undefined)).toThrow(); + expect(() => parsePageMetadata('page.md', [])).toThrow(); + + expect(() => parsePageMetadata('page.md', { + ...golden, + title: undefined, // Missing required property. + })); + expect(() => parsePageMetadata('page.md', { + ...golden, + title: true, // Wrong property type. + })).toThrow(); + expect(() => parsePageMetadata('page.md', { + ...golden, + unknown: 'value', // Extra property. + })).toThrow(); + }); + }); +}); diff --git a/docs/markdown/markdown_testdata.md b/docs/markdown/markdown_testdata.md new file mode 100644 index 00000000..a29d0d76 --- /dev/null +++ b/docs/markdown/markdown_testdata.md @@ -0,0 +1,10 @@ +--- +key: value +array: + - 1 + - 2 + - 3 +nested: + foo: bar +--- +# Hello, World! diff --git a/docs/site.tsx b/docs/site.tsx index 0741c4dc..975232c9 100644 --- a/docs/site.tsx +++ b/docs/site.tsx @@ -1,8 +1,12 @@ import { PrerenderResource, renderToHtml } from '@rules_prerender/preact'; +import { type VNode } from 'preact'; import { UnderConstruction } from './components/under_construction/under_construction.js'; import { Route } from './route.mjs'; import { IndexPage } from './www/index.js'; import { NotFound } from './www/not_found/not_found.js'; +import { Markdown } from './components/markdown/markdown.js'; +import { renderMarkdown } from './markdown/markdown_loader.mjs'; +import { type MarkdownPage, parsePageMetadata } from './markdown/markdown_page.mjs'; /** Docs site routes. */ export const routes: readonly Route[] = [ @@ -42,7 +46,8 @@ export const routes: readonly Route[] = [ }, ]; -export default function*(): Generator<PrerenderResource, void, void> { +export default async function*(): + AsyncGenerator<PrerenderResource, void, void> { yield PrerenderResource.fromHtml('/index.html', renderToHtml( <IndexPage routes={routes} /> )); @@ -64,14 +69,13 @@ export default function*(): Generator<PrerenderResource, void, void> { )); } -function* renderTutorials(): Generator<PrerenderResource, void, void> { +async function* renderTutorials(): + AsyncGenerator<PrerenderResource, void, void> { yield PrerenderResource.fromHtml( '/tutorials/getting-started/index.html', - renderToHtml(<UnderConstruction - pageTitle="Getting Started" - headerTitle="Getting Started" - routes={routes} - />), + renderToHtml(await renderMarkdownPage( + 'rules_prerender/docs/www/tutorials/getting_started/getting_started.md', + )), ); yield PrerenderResource.fromHtml( @@ -114,3 +118,16 @@ function* renderApiReference(): Generator<PrerenderResource, void, void> { />), ); } + +/** + * Renders a markdown page with a fetch-then-render architecture. Reads the + * markdown content first and then renders the full page. This avoids async and + * `<Suspense />` challenges. + */ +async function renderMarkdownPage(page: string): Promise<VNode> { + const { frontmatter, html } = await renderMarkdown(page); + const metadata = parsePageMetadata(page, frontmatter); + const markdownPage: MarkdownPage = { metadata, html }; + + return <Markdown page={markdownPage} routes={routes} />; +} diff --git a/docs/www/BUILD.bazel b/docs/www/BUILD.bazel index f50b138e..7b65c814 100644 --- a/docs/www/BUILD.bazel +++ b/docs/www/BUILD.bazel @@ -2,6 +2,8 @@ load("//:index.bzl", "css_library", "prerender_component") load("//tools/jasmine:defs.bzl", "jasmine_node_test") load("//tools/typescript:defs.bzl", "ts_project") +exports_files(["tutorials/getting_started/getting_started.md"]) + prerender_component( name = "index", prerender = ":prerender", diff --git a/docs/www/tutorials/getting_started/getting_started.md b/docs/www/tutorials/getting_started/getting_started.md new file mode 100644 index 00000000..44c4bc96 --- /dev/null +++ b/docs/www/tutorials/getting_started/getting_started.md @@ -0,0 +1,10 @@ +--- +title: Getting Started +--- + +Here's how to get started with `@rules_prerender`! + +* Read this list. +* Do the things. +* ??? +* Profit! diff --git a/package.json b/package.json index 38c98587..536ecf73 100644 --- a/package.json +++ b/package.json @@ -48,15 +48,18 @@ "@bazel/buildifier": "latest", "@bazel/ibazel": "latest", "@types/jasmine": "^4.3.1", + "@types/marked": "^5.0.1", "@types/node": "^14.14.13", "@types/yargs": "^17.0.24", "@typescript-eslint/eslint-plugin": "^5.54.0", "@typescript-eslint/parser": "^5.54.0", "eslint": "^8.35.0", + "gray-matter": "^4.0.3", "http-status-codes": "^2.1.4", "husky": "^7.0.2", "hydroactive": "^0.0.4", "jasmine": "4.3.0", + "marked": "^8.0.0", "nan": "^2.17.0", "netlify-cli": "^16.1.0", "node-fetch": "^3.2.5", @@ -65,7 +68,8 @@ "preact-render-to-string": "^5.2.6", "tree-kill": "^1.2.2", "typescript": "4.9.5", - "webdriverio": "^7.20.9" + "webdriverio": "^7.20.9", + "zod": "^3.22.2" }, "pnpm": { "packageExtensions": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13a0836e..275c3c02 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: '@types/jasmine': specifier: ^4.3.1 version: 4.3.1 + '@types/marked': + specifier: ^5.0.1 + version: 5.0.1 '@types/node': specifier: ^14.14.13 version: 14.14.13 @@ -62,6 +65,9 @@ importers: eslint: specifier: ^8.35.0 version: 8.35.0 + gray-matter: + specifier: ^4.0.3 + version: 4.0.3 http-status-codes: specifier: ^2.1.4 version: 2.1.4 @@ -74,6 +80,9 @@ importers: jasmine: specifier: 4.3.0 version: 4.3.0 + marked: + specifier: ^8.0.0 + version: 8.0.0 nan: specifier: ^2.17.0 version: 2.17.0 @@ -101,6 +110,9 @@ importers: webdriverio: specifier: ^7.20.9 version: 7.20.9(typescript@4.9.5) + zod: + specifier: ^3.22.2 + version: 3.22.2 packages/declarative_shadow_dom: dependencies: @@ -1965,6 +1977,10 @@ packages: '@types/mdurl': 1.0.2 dev: false + /@types/marked@5.0.1: + resolution: {integrity: sha512-Y3pAUzHKh605fN6fvASsz5FDSWbZcs/65Q6xYRmnIP9ZIYz27T4IOmXfH9gWJV1dpi7f1e7z7nBGUTx/a0ptpA==} + dev: true + /@types/mdurl@1.0.2: resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==} dev: false @@ -2690,6 +2706,12 @@ packages: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} dev: true + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + dev: true + /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -5361,6 +5383,16 @@ packages: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} dev: true + /gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + dependencies: + js-yaml: 3.14.1 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + dev: true + /has-ansi@2.0.0: resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} engines: {node: '>=0.10.0'} @@ -6137,6 +6169,14 @@ packages: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: true + /js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + dev: true + /js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -6857,6 +6897,12 @@ packages: uc.micro: 1.0.6 dev: false + /marked@8.0.0: + resolution: {integrity: sha512-RI/D5csFVreNrFchdKFSdV38GDHJdD7OdmbNWYzGvApPb0A9pyypgfHC/FBH4ugmRE8cr7yg/TH7tu8585eMhA==} + engines: {node: '>= 16'} + hasBin: true + dev: true + /marky@1.2.2: resolution: {integrity: sha512-k1dB2HNeaNyORco8ulVEhctyEGkKHb2YWAhDsxeFlW2nROIirsctBYzKwwS3Vza+sKTS1zO4Z+n9/+9WbGLIxQ==} dev: true @@ -8854,6 +8900,14 @@ packages: /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + /section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + dev: true + /secure-json-parse@2.7.0: resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} dev: true @@ -9232,6 +9286,10 @@ packages: through: 2.3.8 dev: false + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: true + /ssri@10.0.5: resolution: {integrity: sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -9393,6 +9451,11 @@ packages: ansi-regex: 6.0.1 dev: true + /strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + dev: true + /strip-dirs@3.0.0: resolution: {integrity: sha512-I0sdgcFTfKQlUPZyAqPJmSG3HLO9rWDFnxonnIbskYNM3DwFOeTNB5KzVq3dA1GdRAc/25b5Y7UO2TQfKWw4aQ==} dependencies: @@ -10409,3 +10472,7 @@ packages: compress-commons: 4.1.1 readable-stream: 3.6.0 dev: true + + /zod@3.22.2: + resolution: {integrity: sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==} + dev: true