diff --git a/.gitignore b/.gitignore index d1cc44e..76911db 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ node_modules/ # Output dist/ +# Content Collections (generated) +.content-collections/ + # Deployment .alchemy/ .wrangler/ diff --git a/content-collections.ts b/content-collections.ts new file mode 100644 index 0000000..38e5cb3 --- /dev/null +++ b/content-collections.ts @@ -0,0 +1,149 @@ +import { existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; + +import type { WriterHook } from '@content-collections/core'; +import { createDefaultImport, defineCollection, defineConfig } from '@content-collections/core'; +import type { MDXContent } from 'mdx/types'; + +import { + extractTableOfContents, + getLastModification, + resolveAsset, +} from '#/modules/markdown/markdown.utils'; +import { postFrontmatterSchema } from '#/modules/post/post.schema'; +import { + seriesFrontmatterSchema, + seriesPostFrontmatterSchema, +} from '#/modules/series/series.schema'; + +const CONTENT_DIRECTORY = 'src/content'; + +const posts = defineCollection({ + name: 'posts', + directory: `${CONTENT_DIRECTORY}/posts`, + include: '*.mdx', + parser: 'frontmatter-only', + schema: postFrontmatterSchema, + transform: async (document, ctx) => { + const slug = document._meta.path; + const filePath = document._meta.filePath; + const contentPath = path.join(CONTENT_DIRECTORY, '/posts', filePath); + const lastModification = await ctx.cache(contentPath, getLastModification); + const raw = await readFile(contentPath, 'utf-8'); + // Cache the parse on the file's content (a distinct `key` from the + // path-keyed `getLastModification` above), so it recomputes when the body's + // headings change and never collides with that sibling cache entry. + const toc = await ctx.cache(raw, extractTableOfContents, { key: 'toc' }); + const mdx = createDefaultImport(`#/content/posts/${filePath}`); + const contentSubDir = path.posix.join('posts', path.posix.dirname(filePath)); + const thumbnail = document.thumbnail + ? { ...document.thumbnail, src: resolveAsset(document.thumbnail.src, contentSubDir) } + : null; + return { ...document, slug, mdx, lastModification, thumbnail, toc }; + }, +}); + +const series = defineCollection({ + name: 'series', + directory: `${CONTENT_DIRECTORY}/series`, + include: '*/_index.mdx', + parser: 'frontmatter-only', + schema: seriesFrontmatterSchema, + transform: async (document, ctx) => { + const filePath = document._meta.filePath; + const slug = document._meta.directory; + const contentPath = path.join(CONTENT_DIRECTORY, '/series', filePath); + const lastModification = await ctx.cache(contentPath, getLastModification); + const raw = await readFile(contentPath, 'utf-8'); + // Cache the parse on the file's content (a distinct `key` from the + // path-keyed `getLastModification` above), so it recomputes when the body's + // headings change and never collides with that sibling cache entry. + const toc = await ctx.cache(raw, extractTableOfContents, { key: 'toc' }); + const mdx = createDefaultImport(`#/content/series/${filePath}`); + const contentSubDir = path.posix.join('series', path.posix.dirname(filePath)); + const thumbnail = document.thumbnail + ? { ...document.thumbnail, src: resolveAsset(document.thumbnail.src, contentSubDir) } + : null; + return { ...document, slug, mdx, lastModification, thumbnail, toc }; + }, +}); + +const seriesPost = defineCollection({ + name: 'seriesPost', + directory: `${CONTENT_DIRECTORY}/series`, + include: ['*/*.mdx', '!*/_index.mdx'], + parser: 'frontmatter-only', + schema: seriesPostFrontmatterSchema, + transform: async (document, ctx) => { + const filePath = document._meta.filePath; + const seriesSlug = document._meta.directory; + const fileName = document._meta.fileName.replace(`.${document._meta.extension}`, ''); + const match = fileName.match(/^(\d+)_(.+)$/); + if (!match) { + throw new Error( + `Series post "${filePath}" must be prefixed with an order number, e.g. "00_${fileName}.mdx".`, + ); + } + const [_, orderPrefix, slug] = match; + const order = Number(orderPrefix); + const contentPath = path.join(CONTENT_DIRECTORY, '/series', filePath); + const lastModification = await ctx.cache(contentPath, getLastModification); + const raw = await readFile(contentPath, 'utf-8'); + // Cache the parse on the file's content (a distinct `key` from the + // path-keyed `getLastModification` above), so it recomputes when the body's + // headings change and never collides with that sibling cache entry. + const toc = await ctx.cache(raw, extractTableOfContents, { key: 'toc' }); + const mdx = createDefaultImport(`#/content/series/${filePath}`); + const contentSubDir = path.posix.join('series', path.posix.dirname(filePath)); + const thumbnail = document.thumbnail + ? { ...document.thumbnail, src: resolveAsset(document.thumbnail.src, contentSubDir) } + : null; + return { ...document, slug, seriesSlug, order, mdx, lastModification, thumbnail, toc }; + }, + onSuccess: (documents) => { + const ordersBySeries = new Map>(); + for (const document of documents) { + const seen = ordersBySeries.get(document.seriesSlug) ?? new Set(); + if (seen.has(document.order)) { + throw new Error( + `Series "${document.seriesSlug}" has two posts with order ${document.order}. Each post needs a unique numeric prefix.`, + ); + } + seen.add(document.order); + ordersBySeries.set(document.seriesSlug, seen); + } + + // Every series directory with posts must also ship an `_index.mdx`; without + // it the series is absent from `allSeries`, so its detail page 404s while + // the posts beneath it stay reachable — an easy-to-miss orphan state. + for (const seriesSlug of ordersBySeries.keys()) { + const indexPath = path.join(CONTENT_DIRECTORY, 'series', seriesSlug, '_index.mdx'); + if (!existsSync(indexPath)) { + throw new Error( + `Series "${seriesSlug}" has posts but no "_index.mdx". Add ${indexPath} so the series is listed and its detail page resolves.`, + ); + } + } + }, +}); + +// Keep the generated collection (and the compiled MDX it imports) out of the +// client bundle: prepend a `server-only` marker so TanStack Start's import +// protection fails the build if the collection is ever pulled client-side. +// See https://tanstack.com/start/latest/docs/framework/react/guide/import-protection +const serverOnlyHook: WriterHook = async ({ fileType, content }) => { + if (fileType === 'typeDefinition') { + return { content }; + } + return { + content: `import '@tanstack/react-start/server-only';\n\n${content}`, + }; +}; + +export default defineConfig({ + content: [posts, series, seriesPost], + hooks: { + writer: [serverOnlyHook], + }, +}); diff --git a/package.json b/package.json index e39a541..6e77307 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,8 @@ "react-dom": "19.2.7", "react-resizable-panels": "4.12.1", "recharts": "3.9.2", + "rehype-autolink-headings": "7.1.0", + "rehype-slug": "6.0.0", "sonner": "2.0.7", "tailwind-merge": "3.6.0", "tw-animate-css": "1.4.0", @@ -46,22 +48,49 @@ "devDependencies": { "@cloudflare/vite-plugin": "1.43.0", "@cloudflare/workers-types": "5.20260705.1", + "@content-collections/core": "0.15.2", + "@content-collections/vite": "0.3.0", "@inlang/paraglide-js": "^2.20.2", + "@mdx-js/rollup": "3.1.1", "@rolldown/plugin-babel": "0.2.3", + "@shikijs/colorized-brackets": "4.3.1", + "@shikijs/transformers": "4.3.1", + "@tailwindcss/typography": "0.5.20", "@tailwindcss/vite": "4.3.2", "@tanstack/devtools-vite": "0.8.1", "@tanstack/react-devtools": "0.10.8", "@tanstack/react-router-devtools": "1.167.0", + "@types/hast": "3.0.5", + "@types/mdast": "4.0.4", + "@types/mdx": "2.0.13", "@types/node": "26.1.0", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", "@vitejs/plugin-react": "6.0.3", + "@vitejs/plugin-rsc": "0.5.27", "alchemy": "0.93.12", "babel-plugin-react-compiler": "1.0.0", + "hast-util-raw": "9.1.0", + "hast-util-to-html": "9.0.5", + "hast-util-to-jsx-runtime": "2.3.6", + "image-size": "2.0.2", + "mdast-util-directive": "3.1.0", + "mdast-util-from-markdown": "2.0.3", + "mdast-util-mdx-jsx": "3.2.0", + "mdast-util-to-hast": "13.2.1", + "rehype-mdx-import-media": "1.4.0", + "rehype-pretty-code": "0.14.4", + "remark-directive": "4.0.0", + "remark-frontmatter": "5.0.0", + "remark-gfm": "4.0.1", + "remark-mdx-frontmatter": "4.0.0", "shadcn": "4.13.0", + "shiki": "4.3.1", "tailwindcss": "4.3.2", "tldts": "7.4.6", "typescript": "6.0.3", + "unist-util-visit": "5.1.0", + "vfile": "6.0.3", "vite": "catalog:", "vite-plus": "catalog:" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99e5f3e..a7b9c00 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -211,6 +211,7 @@ catalogs: overrides: vite: npm:@voidzero-dev/vite-plus-core@0.2.2 + '@types/hast': 3.0.5 importers: @@ -236,7 +237,7 @@ importers: version: 1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@tanstack/react-start': specifier: 1.168.27 - version: 1.168.27(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.4) + version: 1.168.27(@vitejs/plugin-rsc@0.5.27(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.4)(rollup@4.62.2) class-variance-authority: specifier: 0.7.1 version: 0.7.1 @@ -273,6 +274,12 @@ importers: recharts: specifier: 3.9.2 version: 3.9.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@17.0.2)(react@19.2.7)(redux@5.0.1) + rehype-autolink-headings: + specifier: 7.1.0 + version: 7.1.0 + rehype-slug: + specifier: 6.0.0 + version: 6.0.0 sonner: specifier: 2.0.7 version: 2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -292,12 +299,30 @@ importers: '@cloudflare/workers-types': specifier: 5.20260705.1 version: 5.20260705.1 + '@content-collections/core': + specifier: 0.15.2 + version: 0.15.2(typescript@6.0.3) + '@content-collections/vite': + specifier: 0.3.0 + version: 0.3.0(@content-collections/core@0.15.2(typescript@6.0.3))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0)) '@inlang/paraglide-js': specifier: ^2.20.2 version: 2.20.2(typescript@6.0.3) + '@mdx-js/rollup': + specifier: 3.1.1 + version: 3.1.1(rollup@4.62.2) '@rolldown/plugin-babel': specifier: 0.2.3 version: 0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(rolldown@1.1.4) + '@shikijs/colorized-brackets': + specifier: 4.3.1 + version: 4.3.1 + '@shikijs/transformers': + specifier: 4.3.1 + version: 4.3.1 + '@tailwindcss/typography': + specifier: 0.5.20 + version: 0.5.20(tailwindcss@4.3.2) '@tailwindcss/vite': specifier: 4.3.2 version: 4.3.2(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0)) @@ -310,6 +335,15 @@ importers: '@tanstack/react-router-devtools': specifier: 1.167.0 version: 1.167.0(@tanstack/react-router@1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@tanstack/router-core@1.171.14)(csstype@3.2.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@types/hast': + specifier: 3.0.5 + version: 3.0.5 + '@types/mdast': + specifier: 4.0.4 + version: 4.0.4 + '@types/mdx': + specifier: 2.0.13 + version: 2.0.13 '@types/node': specifier: 26.1.0 version: 26.1.0 @@ -322,15 +356,63 @@ importers: '@vitejs/plugin-react': specifier: 6.0.3 version: 6.0.3(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(rolldown@1.1.4))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(babel-plugin-react-compiler@1.0.0) + '@vitejs/plugin-rsc': + specifier: 0.5.27 + version: 0.5.27(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) alchemy: specifier: 0.93.12 version: 0.93.12(@cloudflare/vite-plugin@1.43.0(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(workerd@1.20260701.1)(wrangler@4.107.0(@cloudflare/workers-types@5.20260705.1)))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(kysely@0.28.17)(workerd@1.20260701.1) babel-plugin-react-compiler: specifier: 1.0.0 version: 1.0.0 + hast-util-raw: + specifier: 9.1.0 + version: 9.1.0 + hast-util-to-html: + specifier: 9.0.5 + version: 9.0.5 + hast-util-to-jsx-runtime: + specifier: 2.3.6 + version: 2.3.6 + image-size: + specifier: 2.0.2 + version: 2.0.2 + mdast-util-directive: + specifier: 3.1.0 + version: 3.1.0 + mdast-util-from-markdown: + specifier: 2.0.3 + version: 2.0.3 + mdast-util-mdx-jsx: + specifier: 3.2.0 + version: 3.2.0 + mdast-util-to-hast: + specifier: 13.2.1 + version: 13.2.1 + rehype-mdx-import-media: + specifier: 1.4.0 + version: 1.4.0 + rehype-pretty-code: + specifier: 0.14.4 + version: 0.14.4(shiki@4.3.1) + remark-directive: + specifier: 4.0.0 + version: 4.0.0 + remark-frontmatter: + specifier: 5.0.0 + version: 5.0.0 + remark-gfm: + specifier: 4.0.1 + version: 4.0.1 + remark-mdx-frontmatter: + specifier: 4.0.0 + version: 4.0.0 shadcn: specifier: 4.13.0 version: 4.13.0(typescript@6.0.3) + shiki: + specifier: 4.3.1 + version: 4.3.1 tailwindcss: specifier: 4.3.2 version: 4.3.2 @@ -340,6 +422,12 @@ importers: typescript: specifier: 6.0.3 version: 6.0.3 + unist-util-visit: + specifier: 5.1.0 + version: 5.1.0 + vfile: + specifier: 6.0.3 + version: 6.0.3 vite: specifier: npm:@voidzero-dev/vite-plus-core@0.2.2 version: '@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0)' @@ -683,6 +771,22 @@ packages: '@cloudflare/workers-types@5.20260705.1': resolution: {integrity: sha512-My4wQdN7ZkM9PzPC92mdCRbdJivtcEiyCr5Qi5cgKxMk3hrULkmiIoI7ZrhGei+QHDlok+g5HSPMuDm62sdaMQ==} + '@content-collections/core@0.15.2': + resolution: {integrity: sha512-Oaz3J3lw/8NXxmV/YSsTl8tTmxscTiotNp1wUNDHvRqW0MtNU8a0h843XYCFWSrlW5q6dcVoM0rGb2UF8kX5Ig==} + peerDependencies: + typescript: ^5.0.2 || ^6.0.0 || ^7.0.0 + + '@content-collections/integrations@0.5.0': + resolution: {integrity: sha512-1en7r518sct0Y8CQ5IsuuBN4uAmtNLaWuxmseW43OxeXyj43Uu2aPBfbopjL4b5xH8WZBdDrrPmikgOl42U46A==} + peerDependencies: + '@content-collections/core': 0.x + + '@content-collections/vite@0.3.0': + resolution: {integrity: sha512-1YDIQNXRKfU4diHQswX8xxhBk+0cJTfC54RRQMNMGOWPnkREVcCySt8ZbPZ9/osx3ACT0+kiWvHRzOKMKPubYA==} + peerDependencies: + '@content-collections/core': ^0.x + vite: ^6 || ^7 || ^8 + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -1253,6 +1357,14 @@ packages: '@lix-js/server-protocol-schema@0.1.1': resolution: {integrity: sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ==} + '@mdx-js/mdx@3.1.1': + resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} + + '@mdx-js/rollup@3.1.1': + resolution: {integrity: sha512-v8satFmBB+DqDzYohnm1u2JOvxx6Hl3pUvqzJvfs2Zk/ngZ1aRUhsWpXvwPkNeGN9c2NCm/38H29ZqXQUjf8dw==} + peerDependencies: + rollup: '>=2' + '@modelcontextprotocol/sdk@1.29.0': resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} engines: {node: '>=18'} @@ -2077,12 +2189,153 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + '@rollup/rollup-linux-x64-gnu@4.62.2': resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} cpu: [x64] os: [linux] libc: [glibc] + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -2097,6 +2350,45 @@ packages: react: optional: true + '@shikijs/colorized-brackets@4.3.1': + resolution: {integrity: sha512-hyp9iRV7iEnU/sT1DL3eLevY0wOwIeFMp6ss8U8FfWdwT5LLhv4sO5Xt6r0dD3eteeswDngBiuEv6nQzQEwv2w==} + engines: {node: '>=20'} + + '@shikijs/core@4.3.1': + resolution: {integrity: sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==} + engines: {node: '>=20'} + + '@shikijs/engine-javascript@4.3.1': + resolution: {integrity: sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==} + engines: {node: '>=20'} + + '@shikijs/engine-oniguruma@4.3.1': + resolution: {integrity: sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==} + engines: {node: '>=20'} + + '@shikijs/langs@4.3.1': + resolution: {integrity: sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==} + engines: {node: '>=20'} + + '@shikijs/primitive@4.3.1': + resolution: {integrity: sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==} + engines: {node: '>=20'} + + '@shikijs/themes@4.3.1': + resolution: {integrity: sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==} + engines: {node: '>=20'} + + '@shikijs/transformers@4.3.1': + resolution: {integrity: sha512-z6ir0bGDgWcF2FduktEfPgIsdOtIlDiLAjFBgBzE42Q9xHbkkIXZtORHzlLVB71iZP9elEcqKg6keajvOUwE2A==} + engines: {node: '>=20'} + + '@shikijs/types@4.3.1': + resolution: {integrity: sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==} + engines: {node: '>=20'} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sinclair/typebox@0.31.28': resolution: {integrity: sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==} @@ -2285,6 +2577,11 @@ packages: resolution: {integrity: sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==} engines: {node: '>= 20'} + '@tailwindcss/typography@0.5.20': + resolution: {integrity: sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw==} + peerDependencies: + tailwindcss: '>=3.0.0 || >=4.0.0 || insiders' + '@tailwindcss/vite@4.3.2': resolution: {integrity: sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==} peerDependencies: @@ -2536,12 +2833,33 @@ packages: '@types/d3-timer@3.0.2': resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdx@2.0.13': + resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + + '@types/mdx@2.0.14': + resolution: {integrity: sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@26.1.0': resolution: {integrity: sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==} @@ -2553,12 +2871,21 @@ packages: '@types/react@19.2.17': resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/use-sync-external-store@0.0.6': resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} '@types/validate-npm-package-name@4.0.2': resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} + '@ungap/structured-clone@1.3.2': + resolution: {integrity: sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==} + '@vitejs/plugin-react@6.0.3': resolution: {integrity: sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2572,6 +2899,17 @@ packages: babel-plugin-react-compiler: optional: true + '@vitejs/plugin-rsc@0.5.27': + resolution: {integrity: sha512-s1fd5DUkPXk86DDHPM/kP93WrvI0MoA8klxdDZmD1fMSaA9xujfgunsm8ZoUH0FemR+63vNalFsIDR0AJH4ktg==} + peerDependencies: + react: '*' + react-dom: '*' + react-server-dom-webpack: '*' + vite: '*' + peerDependenciesMeta: + react-server-dom-webpack: + optional: true + '@vitest/browser-preview@4.1.9': resolution: {integrity: sha512-a4/OrkMDb/WUnE4OOB/4FJbK3rYVO7YykqtUgcTKG4p2a0R3XcjPVu7SLRHFBs2+NIYhv5yxp1Lz3dbdGBjIow==} peerDependencies: @@ -2724,6 +3062,11 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn@8.17.0: resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} engines: {node: '>=0.4.0'} @@ -2836,6 +3179,9 @@ packages: anynum@1.0.1: resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==} + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -2857,6 +3203,10 @@ packages: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} + astring@1.9.0: + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} + hasBin: true + atomically@1.7.0: resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==} engines: {node: '>=10.12.0'} @@ -2870,6 +3220,9 @@ packages: babel-plugin-react-compiler@1.0.0: resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -2931,9 +3284,16 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + caniuse-lite@1.0.30001800: resolution: {integrity: sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -2946,6 +3306,18 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chokidar@5.0.0: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} @@ -2974,6 +3346,9 @@ packages: code-block-writer@13.0.3: resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + collapse-white-space@2.1.0: + resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -2981,6 +3356,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@11.1.0: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} @@ -3125,6 +3503,9 @@ packages: decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + dedent@1.5.1: resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} peerDependencies: @@ -3179,6 +3560,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + diff@8.0.4: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} engines: {node: '>=0.3.1'} @@ -3333,6 +3717,10 @@ packages: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -3365,6 +3753,12 @@ packages: es-toolkit@1.49.0: resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==} + esast-util-from-estree@2.0.0: + resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} + + esast-util-from-js@2.0.1: + resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} + esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -3382,11 +3776,39 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + estree-util-attach-comments@3.0.0: + resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} + + estree-util-build-jsx@3.0.1: + resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-util-scope@1.0.0: + resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==} + + estree-util-to-js@2.0.0: + resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} + + estree-util-value-to-estree@3.5.0: + resolution: {integrity: sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ==} + + estree-util-visit@2.0.0: + resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -3430,6 +3852,13 @@ packages: exsolve@1.1.0: resolution: {integrity: sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==} + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-content-type-parse@2.0.1: resolution: {integrity: sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==} @@ -3456,6 +3885,9 @@ packages: fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fault@2.0.1: + resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -3492,6 +3924,10 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -3547,6 +3983,9 @@ packages: resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} engines: {node: '>=18'} + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -3568,6 +4007,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + h3@2.0.1-rc.20: resolution: {integrity: sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg==} engines: {node: '>=20.11.1'} @@ -3590,10 +4033,55 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-heading-rank@3.0.0: + resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==} + + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-properties-to-mdx-jsx-attributes@1.1.1: + resolution: {integrity: sha512-MMrAoGgvhYULEqMB/r6AlcVz1D3Cyml/9cMB2NIqZsIsEJ+XEXPMqH0gjba8dVs9AnQUYvPReAS+OIYx4ip+Ug==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-to-estree@3.1.3: + resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-to-parse5@8.0.1: + resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==} + + hast-util-to-string@3.0.1: + resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + hono@4.12.27: resolution: {integrity: sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==} engines: {node: '>=16.9.0'} + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -3618,6 +4106,11 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + image-size@2.0.2: + resolution: {integrity: sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==} + engines: {node: '>=16.x'} + hasBin: true + immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} @@ -3631,6 +4124,9 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + input-otp@1.4.2: resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==} peerDependencies: @@ -3649,9 +4145,18 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} @@ -3662,6 +4167,10 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3674,6 +4183,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-in-ssh@1.0.0: resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} engines: {node: '>=20'} @@ -3767,6 +4279,13 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@3.15.0: + resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==} + hasBin: true + js-yaml@4.3.0: resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true @@ -3799,6 +4318,10 @@ packages: jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -3912,6 +4435,9 @@ packages: resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} engines: {node: '>= 0.6.0'} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -3930,10 +4456,71 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + markdown-extensions@2.0.0: + resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} + engines: {node: '>=16'} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-directive@3.1.0: + resolution: {integrity: sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-frontmatter@2.0.1: + resolution: {integrity: sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdx@3.0.0: + resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -3949,41 +4536,152 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} - mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} + micromark-extension-directive@4.0.0: + resolution: {integrity: sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==} - mime-types@3.0.2: - resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} - engines: {node: '>=18'} + micromark-extension-frontmatter@2.0.0: + resolution: {integrity: sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==} - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} - mimic-fn@3.1.0: - resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==} - engines: {node: '>=8'} + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} - mimic-function@5.0.1: - resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} - engines: {node: '>=18'} + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} - miniflare@4.20260424.0: - resolution: {integrity: sha512-B6MKBBd5TJ19daUc3Ae9rWctn1nDA/VCXykXfCsp9fTxyfGxnZY27tJs1caxgE9MWEMMKGbGHouqVtgKbKGxmw==} - engines: {node: '>=18.0.0'} - hasBin: true + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} - miniflare@4.20260701.0: - resolution: {integrity: sha512-L6eAAi6IKtyb/7J6L+YsH2vb1yBrJWKRXI293JYDiMl70+6nncdAgigex58w6WBd+CwvdMsqOyNyGs95Op5gWQ==} - engines: {node: '>=22.0.0'} - hasBin: true + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} - minimatch@10.2.5: + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-extension-mdx-expression@3.0.1: + resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==} + + micromark-extension-mdx-jsx@3.0.2: + resolution: {integrity: sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==} + + micromark-extension-mdx-md@2.0.0: + resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==} + + micromark-extension-mdxjs-esm@3.0.0: + resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==} + + micromark-extension-mdxjs@3.0.0: + resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-mdx-expression@2.0.3: + resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-events-to-acorn@2.0.3: + resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-fn@3.1.0: + resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==} + engines: {node: '>=8'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + miniflare@4.20260424.0: + resolution: {integrity: sha512-B6MKBBd5TJ19daUc3Ae9rWctn1nDA/VCXykXfCsp9fTxyfGxnZY27tJs1caxgE9MWEMMKGbGHouqVtgKbKGxmw==} + engines: {node: '>=18.0.0'} + hasBin: true + + miniflare@4.20260701.0: + resolution: {integrity: sha512-L6eAAi6IKtyb/7J6L+YsH2vb1yBrJWKRXI293JYDiMl70+6nncdAgigex58w6WBd+CwvdMsqOyNyGs95Op5gWQ==} + engines: {node: '>=22.0.0'} + hasBin: true + + minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -4064,6 +4762,12 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + oniguruma-parser@0.12.2: + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} + + oniguruma-to-es@4.3.6: + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + open@10.2.0: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} @@ -4121,6 +4825,10 @@ packages: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} + p-limit@6.2.0: + resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==} + engines: {node: '>=18'} + p-locate@3.0.0: resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} engines: {node: '>=6'} @@ -4139,6 +4847,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -4147,6 +4858,15 @@ packages: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} + parse-numeric-range@1.3.0: + resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==} + + parse-srcset@1.0.2: + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -4202,10 +4922,18 @@ packages: resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} engines: {node: '>=8'} + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + pngjs@7.0.0: resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} engines: {node: '>=14.19.0'} + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + postcss-selector-parser@7.1.4: resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==} engines: {node: '>=4'} @@ -4241,6 +4969,9 @@ packages: proper-lockfile@4.1.2: resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -4349,6 +5080,20 @@ packages: react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + recma-build-jsx@1.0.0: + resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} + + recma-jsx@1.0.1: + resolution: {integrity: sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + recma-parse@1.0.0: + resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==} + + recma-stringify@1.0.0: + resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} + redux-thunk@3.1.0: resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} peerDependencies: @@ -4357,6 +5102,60 @@ packages: redux@5.0.1: resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + + rehype-autolink-headings@7.1.0: + resolution: {integrity: sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==} + + rehype-mdx-import-media@1.4.0: + resolution: {integrity: sha512-O2Q/yFj8lj0IXK7iwfHFowYrn0c5wA4/RCoS8J30HVioMmMzr+fF82PbxV3YPf6IpmbaRrVPvRfae1LhM8hXmQ==} + + rehype-parse@9.0.1: + resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} + + rehype-pretty-code@0.14.4: + resolution: {integrity: sha512-FKPkiSnTqtJHKjCwfqfQs0tXSkmcq7K3jsryZI0rpazo2kWgSYw+sk18GAhxDzKdPk004WWWZ5yiZFEUQhN4Bg==} + engines: {node: '>=18'} + peerDependencies: + shiki: ^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 + + rehype-recma@1.0.0: + resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + + rehype-slug@6.0.0: + resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==} + + remark-directive@4.0.0: + resolution: {integrity: sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==} + + remark-frontmatter@5.0.0: + resolution: {integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-mdx-frontmatter@4.0.0: + resolution: {integrity: sha512-PZzAiDGOEfv1Ua7exQ8S5kKxkD8CDaSb4nM+1Mprs6u8dyvQifakh+kCj6NovfGXW+bTvrhjaR3srzjS2qJHKg==} + + remark-mdx@3.1.1: + resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -4385,6 +5184,11 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + rou3@0.8.1: resolution: {integrity: sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA==} @@ -4408,6 +5212,10 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -4421,6 +5229,10 @@ packages: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} + serialize-javascript@7.0.7: + resolution: {integrity: sha512-YAy8Od6KV+uuwUuU50np8fGB/Aues6Y0nAhA9y/hId74PlKUcme4pXcBD46NWKr1Q4osN/iseZ17YqO1XfmI8g==} + engines: {node: '>=20.0.0'} + seroval-plugins@1.5.4: resolution: {integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==} engines: {node: '>=10'} @@ -4462,6 +5274,10 @@ packages: resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==} engines: {node: '>= 0.4'} + shiki@4.3.1: + resolution: {integrity: sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==} + engines: {node: '>=20'} + side-channel-list@1.0.1: resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} engines: {node: '>= 0.4'} @@ -4516,6 +5332,12 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sqlite-wasm-kysely@0.3.0: resolution: {integrity: sha512-TzjBNv7KwRw6E3pdKdlRyZiTmUIE0UttT/Sl56MVwVARl/u5gp978KepazCJZewFUnlWHz9i3NQd4kOtP/Afdg==} peerDependencies: @@ -4555,6 +5377,9 @@ packages: string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + stringify-object@5.0.0: resolution: {integrity: sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==} engines: {node: '>=14.16'} @@ -4567,6 +5392,10 @@ packages: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -4579,9 +5408,18 @@ packages: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + strnum@2.4.1: resolution: {integrity: sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} @@ -4643,10 +5481,19 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-morph@26.0.0: resolution: {integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==} @@ -4657,6 +5504,9 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + turbo-stream@3.2.0: + resolution: {integrity: sha512-EK+bZ9UVrVh7JLslVFOV0GEMsociOqVOvEMTAd4ixMyffN5YNIEdLZWXUx5PJqDbTxSIBWw04HS9gCY4frYQDQ==} + tw-animate-css@1.4.0: resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} @@ -4693,6 +5543,27 @@ packages: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position-from-estree@2.0.0: + resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + universal-user-agent@7.0.3: resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} @@ -4790,6 +5661,15 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + victory-vendor@37.3.6: resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} @@ -4855,6 +5735,9 @@ packages: jsdom: optional: true + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} @@ -4952,6 +5835,10 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + yocto-spinner@1.2.0: resolution: {integrity: sha512-Yw0hUB6UA3o4YUgKy3oSe9a4cxoaZ9sBfYDw+JSxo6Id0KoJGoxzPA24qqUXYKBWABs/zDSGTz9kww7t3F0XGw==} engines: {node: '>=18.19'} @@ -4977,6 +5864,9 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@aws-sdk/client-cognito-identity@3.1079.0': @@ -5434,6 +6324,31 @@ snapshots: '@cloudflare/workers-types@5.20260705.1': {} + '@content-collections/core@0.15.2(typescript@6.0.3)': + dependencies: + '@standard-schema/spec': 1.1.0 + camelcase: 8.0.0 + chokidar: 5.0.0 + esbuild: 0.25.12 + gray-matter: 4.0.3 + p-limit: 6.2.0 + picomatch: 4.0.5 + pluralize: 8.0.0 + serialize-javascript: 7.0.7 + tinyglobby: 0.2.17 + typescript: 6.0.3 + yaml: 2.9.0 + + '@content-collections/integrations@0.5.0(@content-collections/core@0.15.2(typescript@6.0.3))': + dependencies: + '@content-collections/core': 0.15.2(typescript@6.0.3) + + '@content-collections/vite@0.3.0(@content-collections/core@0.15.2(typescript@6.0.3))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))': + dependencies: + '@content-collections/core': 0.15.2(typescript@6.0.3) + '@content-collections/integrations': 0.5.0(@content-collections/core@0.15.2(typescript@6.0.3)) + vite: '@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0)' + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -5842,6 +6757,46 @@ snapshots: '@lix-js/server-protocol-schema@0.1.1': {} + '@mdx-js/mdx@3.1.1': + dependencies: + '@types/estree': 1.0.9 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdx': 2.0.14 + acorn: 8.17.0 + collapse-white-space: 2.1.0 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-util-scope: 1.0.0 + estree-walker: 3.0.3 + hast-util-to-jsx-runtime: 2.3.6 + markdown-extensions: 2.0.0 + recma-build-jsx: 1.0.0 + recma-jsx: 1.0.1(acorn@8.17.0) + recma-stringify: 1.0.0 + rehype-recma: 1.0.0 + remark-mdx: 3.1.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + source-map: 0.7.6 + unified: 11.0.5 + unist-util-position-from-estree: 2.0.0 + unist-util-stringify-position: 4.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@mdx-js/rollup@3.1.1(rollup@4.62.2)': + dependencies: + '@mdx-js/mdx': 3.1.1 + '@rollup/pluginutils': 5.4.0(rollup@4.62.2) + rollup: 4.62.2 + source-map: 0.7.6 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': dependencies: '@hono/node-server': 1.19.14(hono@4.12.27) @@ -6406,9 +7361,89 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@rollup/pluginutils@5.4.0(rollup@4.62.2)': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.5 + optionalDependencies: + rollup: 4.62.2 + + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + + '@rollup/rollup-android-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + '@rollup/rollup-linux-x64-gnu@4.62.2': optional: true + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + '@sec-ant/readable-stream@0.4.1': {} '@shadcn/react@0.2.0(@types/react@19.2.17)(react@19.2.7)': @@ -6416,6 +7451,55 @@ snapshots: '@types/react': 19.2.17 react: 19.2.7 + '@shikijs/colorized-brackets@4.3.1': + dependencies: + shiki: 4.3.1 + + '@shikijs/core@4.3.1': + dependencies: + '@shikijs/primitive': 4.3.1 + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.6 + + '@shikijs/engine-oniguruma@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + + '@shikijs/primitive@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + + '@shikijs/themes@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + + '@shikijs/transformers@4.3.1': + dependencies: + '@shikijs/core': 4.3.1 + '@shikijs/types': 4.3.1 + + '@shikijs/types@4.3.1': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + + '@shikijs/vscode-textmate@10.0.2': {} + '@sinclair/typebox@0.31.28': {} '@sindresorhus/is@7.2.0': {} @@ -6568,6 +7652,11 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.2 '@tailwindcss/oxide-win32-x64-msvc': 4.3.2 + '@tailwindcss/typography@0.5.20(tailwindcss@4.3.2)': + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 4.3.2 + '@tailwindcss/vite@4.3.2(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.2 @@ -6672,19 +7761,21 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@tanstack/react-start-rsc@0.1.26(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.4)': + '@tanstack/react-start-rsc@0.1.26(@vitejs/plugin-rsc@0.5.27(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.4)(rollup@4.62.2)': dependencies: '@tanstack/react-router': 1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@tanstack/router-core': 1.171.14 '@tanstack/router-utils': 1.162.2 '@tanstack/start-client-core': 1.170.13 '@tanstack/start-fn-stubs': 1.162.0 - '@tanstack/start-plugin-core': 1.171.19(@tanstack/react-router@1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(rolldown@1.1.4) + '@tanstack/start-plugin-core': 1.171.19(@tanstack/react-router@1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(rolldown@1.1.4)(rollup@4.62.2) '@tanstack/start-server-core': 1.169.16 '@tanstack/start-storage-context': 1.167.16 pathe: 2.0.3 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@vitejs/plugin-rsc': 0.5.27(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) transitivePeerDependencies: - '@farmfe/core' - '@rsbuild/core' @@ -6709,20 +7800,21 @@ snapshots: transitivePeerDependencies: - crossws - '@tanstack/react-start@1.168.27(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.4)': + '@tanstack/react-start@1.168.27(@vitejs/plugin-rsc@0.5.27(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.4)(rollup@4.62.2)': dependencies: '@tanstack/react-router': 1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@tanstack/react-start-client': 1.168.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@tanstack/react-start-rsc': 0.1.26(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.4) + '@tanstack/react-start-rsc': 0.1.26(@vitejs/plugin-rsc@0.5.27(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.4)(rollup@4.62.2) '@tanstack/react-start-server': 1.167.21(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@tanstack/router-utils': 1.162.2 '@tanstack/start-client-core': 1.170.13 - '@tanstack/start-plugin-core': 1.171.19(@tanstack/react-router@1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(rolldown@1.1.4) + '@tanstack/start-plugin-core': 1.171.19(@tanstack/react-router@1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(rolldown@1.1.4)(rollup@4.62.2) '@tanstack/start-server-core': 1.169.16 pathe: 2.0.3 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: + '@vitejs/plugin-rsc': 0.5.27(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) vite: '@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0)' transitivePeerDependencies: - '@farmfe/core' @@ -6773,7 +7865,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.168.19(@tanstack/react-router@1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(rolldown@1.1.4)': + '@tanstack/router-plugin@1.168.19(@tanstack/react-router@1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(rolldown@1.1.4)(rollup@4.62.2)': dependencies: '@babel/core': 7.29.7 '@babel/template': 7.29.7 @@ -6782,7 +7874,7 @@ snapshots: '@tanstack/router-generator': 1.167.18 '@tanstack/router-utils': 1.162.2 chokidar: 5.0.0 - unplugin: 3.3.0(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(rolldown@1.1.4) + unplugin: 3.3.0(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(rolldown@1.1.4)(rollup@4.62.2) zod: 4.4.3 optionalDependencies: '@tanstack/react-router': 1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -6819,14 +7911,14 @@ snapshots: '@tanstack/start-fn-stubs@1.162.0': {} - '@tanstack/start-plugin-core@1.171.19(@tanstack/react-router@1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(rolldown@1.1.4)': + '@tanstack/start-plugin-core@1.171.19(@tanstack/react-router@1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(rolldown@1.1.4)(rollup@4.62.2)': dependencies: '@babel/code-frame': 7.27.1 '@babel/core': 7.29.7 '@babel/types': 7.29.7 '@tanstack/router-core': 1.171.14 '@tanstack/router-generator': 1.167.18 - '@tanstack/router-plugin': 1.168.19(@tanstack/react-router@1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(rolldown@1.1.4) + '@tanstack/router-plugin': 1.168.19(@tanstack/react-router@1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(rolldown@1.1.4)(rollup@4.62.2) '@tanstack/router-utils': 1.162.2 '@tanstack/start-server-core': 1.169.16 exsolve: 1.1.0 @@ -6934,10 +8026,32 @@ snapshots: '@types/d3-timer@3.0.2': {} + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.9 + '@types/estree@1.0.9': {} + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdx@2.0.13': {} + + '@types/mdx@2.0.14': {} + + '@types/ms@2.1.0': {} + '@types/node@26.1.0': dependencies: undici-types: 8.3.0 @@ -6950,10 +8064,16 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + '@types/use-sync-external-store@0.0.6': {} '@types/validate-npm-package-name@4.0.2': {} + '@ungap/structured-clone@1.3.2': {} + '@vitejs/plugin-react@6.0.3(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(rolldown@1.1.4))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(babel-plugin-react-compiler@1.0.0)': dependencies: '@rolldown/pluginutils': 1.0.1 @@ -6962,11 +8082,25 @@ snapshots: '@rolldown/plugin-babel': 0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(rolldown@1.1.4) babel-plugin-react-compiler: 1.0.0 - '@vitest/browser-preview@4.1.9(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(vitest@4.1.9)': + '@vitejs/plugin-rsc@0.5.27(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@testing-library/dom': 10.4.1 - '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/browser': 4.1.9(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(vitest@4.1.9) + '@rolldown/pluginutils': 1.0.1 + es-module-lexer: 2.3.0 + estree-walker: 3.0.3 + magic-string: 0.30.21 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + srvx: 0.11.21 + strip-literal: 3.1.0 + turbo-stream: 3.2.0 + vite: '@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0)' + vitefu: 1.1.3(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0)) + + '@vitest/browser-preview@4.1.9(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(vitest@4.1.9)': + dependencies: + '@testing-library/dom': 10.4.1 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) + '@vitest/browser': 4.1.9(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(vitest@4.1.9) vitest: 4.1.9(@types/node@26.1.0)(@vitest/browser-preview@4.1.9)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0)) transitivePeerDependencies: - bufferutil @@ -7075,6 +8209,10 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + acorn@8.17.0: {} ajv-formats@2.1.1(ajv@8.20.0): @@ -7177,6 +8315,10 @@ snapshots: anynum@1.0.1: {} + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} aria-hidden@1.2.6: @@ -7195,6 +8337,8 @@ snapshots: dependencies: tslib: 2.8.1 + astring@1.9.0: {} + atomically@1.7.0: {} aws4fetch@1.0.20: {} @@ -7212,6 +8356,8 @@ snapshots: dependencies: '@babel/types': 7.29.7 + bail@2.0.2: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -7276,8 +8422,12 @@ snapshots: callsites@3.1.0: {} + camelcase@8.0.0: {} + caniuse-lite@1.0.30001800: {} + ccount@2.0.1: {} + chai@6.2.2: {} chalk@4.1.2: @@ -7287,6 +8437,14 @@ snapshots: chalk@5.6.2: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + chokidar@5.0.0: dependencies: readdirp: 5.0.0 @@ -7317,12 +8475,16 @@ snapshots: code-block-writer@13.0.3: {} + collapse-white-space@2.1.0: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 color-name@1.1.4: {} + comma-separated-tokens@2.0.3: {} + commander@11.1.0: {} commander@14.0.3: {} @@ -7441,6 +8603,10 @@ snapshots: decimal.js-light@2.5.1: {} + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + dedent@1.5.1: {} dedent@1.7.2: {} @@ -7468,6 +8634,10 @@ snapshots: detect-node-es@1.1.0: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + diff@8.0.4: {} dom-accessibility-api@0.5.16: {} @@ -7525,6 +8695,8 @@ snapshots: ansi-colors: 4.1.3 strip-ansi: 6.0.1 + entities@6.0.1: {} + env-paths@2.2.1: {} env-paths@3.0.0: {} @@ -7547,6 +8719,20 @@ snapshots: es-toolkit@1.49.0: {} + esast-util-from-estree@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + unist-util-position-from-estree: 2.0.0 + + esast-util-from-js@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + acorn: 8.17.0 + esast-util-from-estree: 2.0.0 + vfile-message: 4.0.3 + esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -7609,8 +8795,45 @@ snapshots: escape-html@1.0.3: {} + escape-string-regexp@5.0.0: {} + esprima@4.0.1: {} + estree-util-attach-comments@3.0.0: + dependencies: + '@types/estree': 1.0.9 + + estree-util-build-jsx@3.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-walker: 3.0.3 + + estree-util-is-identifier-name@3.0.0: {} + + estree-util-scope@1.0.0: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + + estree-util-to-js@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + astring: 1.9.0 + source-map: 0.7.6 + + estree-util-value-to-estree@3.5.0: + dependencies: + '@types/estree': 1.0.9 + + estree-util-visit@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/unist': 3.0.3 + + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 @@ -7694,6 +8917,12 @@ snapshots: exsolve@1.1.0: {} + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend@3.0.2: {} + fast-content-type-parse@2.0.1: {} fast-deep-equal@3.1.3: {} @@ -7728,6 +8957,10 @@ snapshots: dependencies: reusify: 1.1.0 + fault@2.0.1: + dependencies: + format: 0.2.2 + fdir@6.5.0(picomatch@4.0.5): optionalDependencies: picomatch: 4.0.5 @@ -7768,6 +9001,8 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + format@0.2.2: {} + forwarded@0.2.0: {} fresh@2.0.0: {} @@ -7818,6 +9053,8 @@ snapshots: '@sec-ant/readable-stream': 0.4.1 is-stream: 4.0.1 + github-slugger@2.0.0: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -7839,6 +9076,13 @@ snapshots: graceful-fs@4.2.11: {} + gray-matter@4.0.3: + dependencies: + js-yaml: 3.15.0 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + h3@2.0.1-rc.20: dependencies: rou3: 0.8.1 @@ -7852,8 +9096,150 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.5 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.3 + parse5: 7.3.0 + vfile: 6.0.3 + vfile-message: 4.0.3 + + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.2.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-heading-rank@3.0.0: + dependencies: + '@types/hast': 3.0.5 + + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.5 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.5 + + hast-util-properties-to-mdx-jsx-attributes@1.1.1: + dependencies: + '@types/estree': 1.0.9 + '@types/hast': 3.0.5 + estree-util-value-to-estree: 3.5.0 + mdast-util-mdx-jsx: 3.2.0 + property-information: 7.2.0 + style-to-js: 1.1.21 + transitivePeerDependencies: + - supports-color + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.2 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.1 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-estree@3.1.3: + dependencies: + '@types/estree': 1.0.9 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-attach-comments: 3.0.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + zwitch: 2.0.4 + transitivePeerDependencies: + - supports-color + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.9 + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-to-parse5@8.0.1: + dependencies: + '@types/hast': 3.0.5 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-string@3.0.1: + dependencies: + '@types/hast': 3.0.5 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.5 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.5 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + hono@4.12.27: {} + html-void-elements@3.0.0: {} + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -7874,6 +9260,8 @@ snapshots: ignore@5.3.2: {} + image-size@2.0.2: {} + immediate@3.0.6: {} immer@11.1.11: {} @@ -7885,6 +9273,8 @@ snapshots: inherits@2.0.4: {} + inline-style-parser@0.2.7: {} + input-otp@1.4.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: react: 19.2.7 @@ -7896,12 +9286,23 @@ snapshots: ipaddr.js@1.9.1: {} + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-arrayish@0.2.1: {} + is-decimal@2.0.1: {} + is-docker@2.2.1: {} is-docker@3.0.0: {} + is-extendable@0.1.1: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -7910,6 +9311,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-in-ssh@1.0.0: {} is-inside-container@1.0.0: @@ -7970,6 +9373,13 @@ snapshots: js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + + js-yaml@3.15.0: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + js-yaml@4.3.0: dependencies: argparse: 2.0.1 @@ -7999,6 +9409,8 @@ snapshots: readable-stream: 2.3.8 setimmediate: 1.0.5 + kind-of@6.0.3: {} + kleur@3.0.3: {} kleur@4.1.5: {} @@ -8083,6 +9495,8 @@ snapshots: loglevel@1.9.2: {} + longest-streak@3.1.0: {} + lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -8099,8 +9513,200 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + markdown-extensions@2.0.0: {} + + markdown-table@3.0.4: {} + math-intrinsics@1.1.0: {} + mdast-util-directive@3.1.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-visit-parents: 6.0.2 + transitivePeerDependencies: + - supports-color + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-frontmatter@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + escape-string-regexp: 5.0.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-extension-frontmatter: 2.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.2 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + media-typer@1.1.0: {} merge-descriptors@2.0.0: {} @@ -8109,6 +9715,287 @@ snapshots: merge2@1.4.1: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-directive@4.0.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + parse-entities: 4.0.2 + + micromark-extension-frontmatter@2.0.0: + dependencies: + fault: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-expression@3.0.1: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-jsx@3.0.2: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.3 + + micromark-extension-mdx-md@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-mdxjs-esm@3.0.0: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.3 + + micromark-extension-mdxjs@3.0.0: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + micromark-extension-mdx-expression: 3.0.1 + micromark-extension-mdx-jsx: 3.0.2 + micromark-extension-mdx-md: 2.0.0 + micromark-extension-mdxjs-esm: 3.0.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-mdx-expression@2.0.3: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.3 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-events-to-acorn@2.0.3: + dependencies: + '@types/estree': 1.0.9 + '@types/unist': 3.0.3 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.3 + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -8211,6 +10098,14 @@ snapshots: dependencies: mimic-function: 5.0.1 + oniguruma-parser@0.12.2: {} + + oniguruma-to-es@4.3.6: + dependencies: + oniguruma-parser: 0.12.2 + regex: 6.1.0 + regex-recursion: 6.0.2 + open@10.2.0: dependencies: default-browser: 5.5.0 @@ -8337,6 +10232,10 @@ snapshots: dependencies: p-try: 2.2.0 + p-limit@6.2.0: + dependencies: + yocto-queue: 1.2.2 + p-locate@3.0.0: dependencies: p-limit: 2.3.0 @@ -8351,6 +10250,16 @@ snapshots: dependencies: callsites: 3.1.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.29.7 @@ -8360,6 +10269,14 @@ snapshots: parse-ms@4.0.0: {} + parse-numeric-range@1.3.0: {} + + parse-srcset@1.0.2: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + parseurl@1.3.3: {} path-browserify@1.0.1: {} @@ -8395,8 +10312,15 @@ snapshots: dependencies: find-up: 3.0.0 + pluralize@8.0.0: {} + pngjs@7.0.0: {} + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss-selector-parser@7.1.4: dependencies: cssesc: 3.0.0 @@ -8435,6 +10359,8 @@ snapshots: retry: 0.12.0 signal-exit: 3.0.7 + property-information@7.2.0: {} + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -8554,12 +10480,170 @@ snapshots: - '@types/react' - redux + recma-build-jsx@1.0.0: + dependencies: + '@types/estree': 1.0.9 + estree-util-build-jsx: 3.0.1 + vfile: 6.0.3 + + recma-jsx@1.0.1(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + estree-util-to-js: 2.0.0 + recma-parse: 1.0.0 + recma-stringify: 1.0.0 + unified: 11.0.5 + + recma-parse@1.0.0: + dependencies: + '@types/estree': 1.0.9 + esast-util-from-js: 2.0.1 + unified: 11.0.5 + vfile: 6.0.3 + + recma-stringify@1.0.0: + dependencies: + '@types/estree': 1.0.9 + estree-util-to-js: 2.0.0 + unified: 11.0.5 + vfile: 6.0.3 + redux-thunk@3.1.0(redux@5.0.1): dependencies: redux: 5.0.1 redux@5.0.1: {} + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + rehype-autolink-headings@7.1.0: + dependencies: + '@types/hast': 3.0.5 + '@ungap/structured-clone': 1.3.2 + hast-util-heading-rank: 3.0.0 + hast-util-is-element: 3.0.0 + unified: 11.0.5 + unist-util-visit: 5.1.0 + + rehype-mdx-import-media@1.4.0: + dependencies: + '@types/hast': 3.0.5 + hast-util-properties-to-mdx-jsx-attributes: 1.1.1 + parse-srcset: 1.0.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + transitivePeerDependencies: + - supports-color + + rehype-parse@9.0.1: + dependencies: + '@types/hast': 3.0.5 + hast-util-from-html: 2.0.3 + unified: 11.0.5 + + rehype-pretty-code@0.14.4(shiki@4.3.1): + dependencies: + '@types/hast': 3.0.5 + hast-util-to-string: 3.0.1 + parse-numeric-range: 1.3.0 + rehype-parse: 9.0.1 + shiki: 4.3.1 + unified: 11.0.5 + unist-util-visit: 5.1.0 + + rehype-recma@1.0.0: + dependencies: + '@types/estree': 1.0.9 + '@types/hast': 3.0.5 + hast-util-to-estree: 3.1.3 + transitivePeerDependencies: + - supports-color + + rehype-slug@6.0.0: + dependencies: + '@types/hast': 3.0.5 + github-slugger: 2.0.0 + hast-util-heading-rank: 3.0.0 + hast-util-to-string: 3.0.1 + unist-util-visit: 5.1.0 + + remark-directive@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-directive: 3.1.0 + micromark-extension-directive: 4.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-frontmatter@5.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-frontmatter: 2.0.1 + micromark-extension-frontmatter: 2.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-mdx-frontmatter@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + estree-util-is-identifier-name: 3.0.0 + estree-util-value-to-estree: 3.5.0 + toml: 3.0.0 + unified: 11.0.5 + yaml: 2.9.0 + + remark-mdx@3.1.1: + dependencies: + mdast-util-mdx: 3.0.0 + micromark-extension-mdxjs: 3.0.0 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + require-from-string@2.0.2: {} reselect@5.2.0: {} @@ -8596,6 +10680,37 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.1.4 '@rolldown/binding-win32-x64-msvc': 1.1.4 + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + rou3@0.8.1: {} router@2.2.0: @@ -8620,6 +10735,11 @@ snapshots: scheduler@0.27.0: {} + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + semver@6.3.1: {} semver@7.8.5: {} @@ -8640,6 +10760,8 @@ snapshots: transitivePeerDependencies: - supports-color + serialize-javascript@7.0.7: {} + seroval-plugins@1.5.4(seroval@1.5.4): dependencies: seroval: 1.5.4 @@ -8738,6 +10860,17 @@ snapshots: shell-quote@1.9.0: {} + shiki@4.3.1: + dependencies: + '@shikijs/core': 4.3.1 + '@shikijs/engine-javascript': 4.3.1 + '@shikijs/engine-oniguruma': 4.3.1 + '@shikijs/langs': 4.3.1 + '@shikijs/themes': 4.3.1 + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + side-channel-list@1.0.1: dependencies: es-errors: 1.3.0 @@ -8797,6 +10930,10 @@ snapshots: source-map@0.7.6: {} + space-separated-tokens@2.0.2: {} + + sprintf-js@1.0.3: {} + sqlite-wasm-kysely@0.3.0(kysely@0.28.17): dependencies: '@sqlite.org/sqlite-wasm': 3.48.0-build4 @@ -8834,6 +10971,11 @@ snapshots: dependencies: safe-buffer: 5.1.2 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + stringify-object@5.0.0: dependencies: get-own-enumerable-keys: 1.0.0 @@ -8848,16 +10990,30 @@ snapshots: dependencies: ansi-regex: 6.2.2 + strip-bom-string@1.0.0: {} + strip-bom@3.0.0: {} strip-final-newline@2.0.0: {} strip-final-newline@4.0.0: {} + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + strnum@2.4.1: dependencies: anynum: 1.0.1 + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + supports-color@10.2.2: {} supports-color@7.2.0: @@ -8899,8 +11055,14 @@ snapshots: toidentifier@1.0.1: {} + toml@3.0.0: {} + totalist@3.0.1: {} + trim-lines@3.0.1: {} + + trough@2.2.0: {} + ts-morph@26.0.0: dependencies: '@ts-morph/common': 0.27.0 @@ -8914,6 +11076,8 @@ snapshots: tslib@2.8.1: {} + turbo-stream@3.2.0: {} + tw-animate-css@1.4.0: {} type-is@2.1.0: @@ -8946,6 +11110,43 @@ snapshots: unicorn-magic@0.3.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position-from-estree@2.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + universal-user-agent@7.0.3: {} universalify@2.0.1: {} @@ -8959,7 +11160,7 @@ snapshots: picomatch: 4.0.5 webpack-virtual-modules: 0.6.2 - unplugin@3.3.0(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(rolldown@1.1.4): + unplugin@3.3.0(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(rolldown@1.1.4)(rollup@4.62.2): dependencies: '@jridgewell/remapping': 2.3.5 picomatch: 4.0.5 @@ -8967,6 +11168,7 @@ snapshots: optionalDependencies: esbuild: 0.28.1 rolldown: 1.1.4 + rollup: 4.62.2 vite: '@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0)' update-browserslist-db@1.2.3(browserslist@4.28.4): @@ -9004,6 +11206,21 @@ snapshots: vary@1.1.2: {} + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + victory-vendor@37.3.6: dependencies: '@types/d3-array': 3.2.2 @@ -9111,6 +11328,8 @@ snapshots: transitivePeerDependencies: - msw + web-namespaces@2.0.1: {} + webpack-virtual-modules@0.6.2: {} which@2.0.2: @@ -9216,6 +11435,8 @@ snapshots: yaml@2.9.0: {} + yocto-queue@1.2.2: {} + yocto-spinner@1.2.0: dependencies: yoctocolors: 2.1.2 @@ -9242,3 +11463,5 @@ snapshots: zod@3.25.76: {} zod@4.4.3: {} + + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2cc70d2..1b06192 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,6 +9,12 @@ catalog: vite-plus: 0.2.2 overrides: vite: 'catalog:' + # Dedupe @types/hast to a single copy. Two versions (3.0.4 + 3.0.5) otherwise + # coexist in the store, and mdast-util-to-hast vs hast-util-raw can resolve + # different ones — making `toHast()`'s return type unassignable to `raw()`'s + # parameter under per-package type resolution (the editor sees it; a root + # `tsc` dedupes and doesn't). See src/modules/markdown/plugins/rehype-image.ts. + '@types/hast': 3.0.5 peerDependencyRules: allowAny: - vite diff --git a/src/content/series/markdown/01_markdown-style-guide.mdx b/src/content/series/markdown/01_markdown-style-guide.mdx new file mode 100644 index 0000000..9cc9d05 --- /dev/null +++ b/src/content/series/markdown/01_markdown-style-guide.mdx @@ -0,0 +1,687 @@ +--- +title: Markdown Style Guide +description: The complete reference for every formatting feature this blog renders — frontmatter, GFM prose, syntax-highlighted code, tabs, footnotes, and the edge cases around each. +date: 2026-07-09 +thumbnail: null +author: Devsantara Team +tags: + - meta + - markdown +--- + +This is the reference for everything you can write in a post. Formatting comes +from four layers, all resolved at build time so the browser only ever receives +finished HTML: + +- **CommonMark + GFM** (`remark-gfm`) — headings, emphasis, lists, tables, + task lists, strikethrough, autolinks, and footnotes. +- **The `:::tabs` directive** (`remark-directive`) — tabbed content. +- **Shiki** (`rehype-pretty-code`) — syntax highlighting with diffs, focus, + highlights, and line numbers. +- **App components** — links, images, and code blocks are swapped for + app-aware React components. + +Each section below states the rule, shows the syntax, and renders the result so +you can see exactly what produces what. Where a feature has a sharp edge, it's +called out with **Edge case**. + +## Frontmatter + +Every post opens with a YAML block between `---` fences. It's parsed separately +and never renders into the body. All fields are required except `thumbnail`, +which may be `null`. + +```yaml +--- +title: Markdown Style Guide +description: One-line summary used in listings and metadata. +date: 2026-07-09 +author: Devsantara Team +tags: + - meta + - markdown +thumbnail: null +--- +``` + +`title` is the page's `h1` — don't write another `#` heading in the body. +`date` is an ISO date (`YYYY-MM-DD`). `tags` is a list, even for a single tag. + +A `thumbnail` is either `null` or an object. Only `src` is required; `alt` +defaults to empty and `caption` to none. `src` is a remote URL or a path +relative to the document, and `caption` renders **inline Markdown** — including +links, which flow through the same app-aware anchor as the body: + +```yaml +thumbnail: + src: './assets/cover.jpg' + alt: 'What a screen reader announces for the cover image' + caption: 'Credit: **bold**, _italic_, `code`, and [links](https://viteplus.dev) all work.' +``` + +## Headings + +Section titles run from `##` to `####`. The post title owns the only `h1`, so +body headings start at level two. + +```md +## Second level — a top-level section + +### Third level — a subsection + +#### Fourth level — one more step down +``` + +Renders as: + +### Third level + +A subsection within a topic — like this one. + +#### Fourth level + +Rarely needed, but supported for one more level of depth. + +**Don't skip levels** on the way down (`##` → `####`). It breaks the document +outline that assistive tech relies on. + +**Edge case — a space is required.** `## Heading` is a heading; `##Heading` +(no space) is literal text. A closing run of `#` is optional and ignored, so +`## Heading ##` renders the same as `## Heading`. + +**Edge case — underline headings.** CommonMark also reads a line of text +underlined with `===` as an `h1` and `---` as an `h2`. Both are easy to trigger +by accident, so prefer the `#` form everywhere. + +## Paragraphs and line breaks + +A blank line separates paragraphs. A single newline _inside_ a paragraph +collapses to a space, so you can hard-wrap the source however you like — it +reflows into one paragraph. Extra blank lines collapse to a single break. + +To force a line break _within_ a paragraph, end the line with a backslash (a +trailing double-space works too, but it's invisible in review — prefer the +backslash): + +```md +Roses are red,\ +violets are blue,\ +this line broke early because a backslash told it to. +``` + +Renders as: + +Roses are red,\ +violets are blue,\ +this line broke early because a backslash told it to. + +## Emphasis and inline formatting + +| Syntax | Result | +| :----------------------- | :----------------- | +| `**bold**` or `__bold__` | **bold** | +| `_italic_` or `*italic*` | _italic_ | +| `**_both at once_**` | **_both at once_** | +| `~~strikethrough~~` | ~~strikethrough~~ | +| `` `inline code` `` | `inline code` | + +Marks combine freely — **bold with `code` inside it** or a +~~_struck-through italic_~~ aside — though restraint reads better. Inline code +is also where keyboard hints like `Ctrl + C` and literal punctuation live, +since nothing inside it is interpreted. + +**Edge case — intra-word emphasis.** Asterisks work mid-word but underscores +don't. So `un*bel*ievable` gives un*bel*ievable, while a `snake_case_name` stays +literal — the underscores in snake_case_name never turn into emphasis. + +**Edge case — a literal backtick in code.** Wrap the span in a longer run of +backticks and pad with spaces: ` `here's a `backtick`` `` renders as +``here's a` backtick``. + +## Links + +```md +- an [internal link](/posts) to another page on this site +- an [external link](https://viteplus.dev/guide/) to somewhere else +- a [titled link](https://mdxjs.com 'The MDX docs') — hover to see the title +- a bare URL like https://tanstack.com that GFM autolinks on its own +``` + +Renders as: + +- an [internal link](/posts) to another page on this site, +- an [external link](https://viteplus.dev/guide/) to somewhere else, +- a [titled link](https://mdxjs.com 'The MDX docs') you can hover, and +- a bare URL like https://tanstack.com that GFM autolinks on its own. + +Internal links (paths starting with `/`) navigate client-side with no full +reload. Everything else — external URLs, `mailto:`, `tel:`, and hash links — +opens in a new, isolated tab. GFM also autolinks bare `www.` hosts and email +addresses. + +**Reference-style links** move the URL to a definition elsewhere in the file — +handy when one source is cited more than once, like the [MDX docs][mdx] and the +[Vite+ guide][vite]: + +```md +See the [MDX docs][mdx] and the [Vite+ guide][vite]. + +[mdx]: https://mdxjs.com +[vite]: https://viteplus.dev/guide/ +``` + +[mdx]: https://mdxjs.com +[vite]: https://viteplus.dev/guide/ + +The definitions render nothing; only the labels above become links. + +## Images + +Reference a local image with a relative path. It's fingerprinted at build time +and its real pixel dimensions are baked in, so the page reserves space and never +reflows as images load: + +```md +![Rocky mountain peaks lit by a golden sunset](./assets/picture-1.jpg) +``` + +![Rocky mountain peaks illuminated by golden sunset light](./assets/picture-1.jpg) + +The alt text in square brackets describes the image for screen readers. Leave it +empty (`![]`) _only_ for purely decorative images. + +### Captions + +Add a **title in quotes** after the path and a lone image becomes a `
` +with a `
`. The caption accepts inline Markdown _and_ raw HTML — ideal +for image credits, which is often what a stock library hands you: + +```md +![Jagged peaks under a cloudy sky](./assets/picture-2.jpg 'Photo by [Intricate Explorer](https://unsplash.com) on Unsplash') +``` + +![Jagged mountain peaks under a cloudy sky](./assets/picture-2.jpg 'Photo by [Intricate Explorer](https://unsplash.com/@intricateexplorer) on [Unsplash](https://unsplash.com/photos/Xwh7RclsOUA)') + +### Remote images + +Remote images work, but can't be measured at build time, so they don't get +intrinsic dimensions (the layout may shift as they arrive): + +![A glowing red nebula scattered with stars in deep space](https://images.unsplash.com/photo-1783716549682-5f945bb5f47e?q=80&w=1329&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D) + +**Edge case — when an image stays inline.** The figure/caption treatment only +applies to an image that is _alone_ in its paragraph _and_ has a title. An image +with no title, one wrapped in a link, or one sharing its paragraph with text +stays inline — and its title, if any, becomes a hover tooltip instead of a +caption. + +**Edge case — unmeasurable files.** Remote, root-absolute (`/logo.png`), and +`public/` sources skip build-time sizing, as do files with no pixel dimensions +(e.g. a `viewBox`-only SVG). They still render; they just don't reserve space. + +## Blockquotes + +```md +> The best way to predict the future is to invent it. +``` + +> The best way to predict the future is to invent it. + +Blockquotes can span multiple paragraphs, hold other blocks, and nest inside one +another — prefix each nested level with another `>`: + +> **Worth remembering** +> +> A quote can carry more than a single line — it can contain its own lists: +> +> - a first point, +> - a second point with `inline code`, +> +> and it can nest another quote inside it: +> +> > Like this one, tucked one level deeper. + +## Alerts + +Alerts (also called callouts or admonitions) emphasize information that's easy to +miss. They use GitHub's syntax: a blockquote whose **first line** is a bracketed +type marker, followed by the content on the lines below. Five types are +available, each with its own color and icon: + +```md +> [!NOTE] +> Useful information that users should know, even when skimming. +``` + +> [!NOTE] +> Useful information that users should know, even when skimming. + +> [!TIP] +> Helpful advice for doing things better or more easily. + +> [!IMPORTANT] +> Key information users need to know to achieve their goal. + +> [!WARNING] +> Urgent info that needs immediate attention to avoid problems. + +> [!CAUTION] +> Advises about risks or negative outcomes of certain actions. + +An alert body is a normal blockquote, so it can hold more than one line — +including other blocks like lists and `inline code`: + +> [!TIP] +> Reach for an alert sparingly: +> +> - one or two per post, at most, +> - never two back to back, +> - and only when the reader really needs to stop and read it. + +**Use them sparingly.** Limit yourself to one or two per post, don't stack them +back to back, and reserve them for information that genuinely warrants the +interruption — overusing alerts trains readers to skip them. + +**Edge case — the marker must be exact.** The type is case-sensitive and must be +one of the five names in uppercase; `[!note]` or `[!HINT]` renders as an ordinary +blockquote. The marker also has to sit alone on the first line — `> [!NOTE] text` +all on one line is not an alert. Alerts don't nest, either: a marker inside an +alert's body is just text. + +## Lists + +**Unordered** lists use `-`, `*`, or `+` (pick one and keep it consistent). +Indent to nest: + +```md +- Compile MDX with the bundler +- Stream it from the server + - No `eval` on the edge runtime + - No client-side MDX payload +- Drop the finished HTML into the page +``` + +- Compile MDX with the bundler +- Stream it from the server + - No `eval` on the edge runtime + - No client-side MDX payload +- Drop the finished HTML into the page + +**Ordered** lists count for you — the source numbers don't have to be right: + +```md +1. Write the post as `.mdx` +2. Let Content Collections validate the frontmatter +3. Ship it +``` + +1. Write the post as `.mdx` +2. Let Content Collections validate the frontmatter +3. Ship it + +An ordered list can **start from a specific number** (the first item's number +wins) and mix in other list types as it nests: + +3. This item is numbered three, +4. and this one four — + - with an unordered branch, + - hanging off of it. + +**Task lists** (GFM) render as checkboxes with `- [ ]` and `- [x]`: + +```md +- [x] Headings, emphasis, and lists +- [ ] Anything we haven't built yet +``` + +- [x] Headings, emphasis, and lists +- [x] Tables and footnotes +- [x] Syntax highlighting +- [ ] Anything we haven't built yet + +**Edge case — tight vs. loose.** Items with no blank line between them render +tightly (no `

` wrapper, less spacing). Put a blank line between items and the +whole list becomes "loose," wrapping each item in a paragraph with more room +around it. + +## Horizontal rules + +Three or more `-`, `*`, or `_` on their own line draw a divider. **Keep a blank +line above it:** + +```md +Content above the break. + +--- + +Content below the break. +``` + +Content above the break. + +--- + +Content below the break. + +**Edge case — the accidental heading.** A `---` line placed _directly_ under a +paragraph, with no blank line, is not a rule — CommonMark reads it as an +underlined `h2` and turns the paragraph above into a heading. The blank line is +what keeps it a divider. + +## Tables + +A header row, a delimiter row, and any number of body rows. Colons in the +delimiter set column alignment — left (`:--`), center (`:-:`), or right (`--:`): + +```md +| Feature | Source | Alignment | +| :------------ | :--------: | ----------------: | +| Tables | remark-gfm | left / default | +| Strikethrough | remark-gfm | centered ~~text~~ | +| Footnotes | remark-gfm | right, at the end | +``` + +| Feature | Source | Alignment | +| :------------ | :--------: | ----------------: | +| Tables | remark-gfm | left / default | +| Strikethrough | remark-gfm | centered ~~text~~ | +| Footnotes | remark-gfm | right, at the end | +| Tabs | MDX | `:::tabs` here | + +Cells take inline formatting — emphasis, `code`, and links all work — but **not** +block content like lists or multiple paragraphs. To put a literal pipe inside a +cell, escape it as `\|`. The outer border pipes are optional but keep the source +readable. + +## Footnotes + +Footnotes (GFM) attach an aside without breaking the sentence's flow[^1]. Mark a +reference with `[^label]` and define it anywhere in the file; definitions are +collected at the bottom no matter where you write them. A footnote can be +referenced more than once[^2], and each gets a link back to where it was cited. + +```md +Attach an aside without breaking flow[^note]. + +[^note]: The definition can live anywhere — it renders at the bottom. +``` + +## Code blocks + +Code is syntax-highlighted at build time by Shiki, so the browser receives only +finished HTML. Every block gets a copy button: it sits in the header bar on +titled blocks and floats over the code (on hover) otherwise, and it copies the +block's _end state_ — markers stripped, diff-removed lines skipped. + +### Language tags + +Name the language after the opening fence for real token colors: + +````md +```ts +export function greet(name: string): string { + return `Hello, ${name}!`; +} +``` +```` + +```ts +export function greet(name: string): string { + return `Hello, ${name}!`; +} +``` + +```css +.button:hover { + background: var(--color-primary); + transform: translateY(-1px); +} +``` + +```json +{ "name": "devsantara", "private": true, "type": "module" } +``` + +A fence with **no language** falls back to plain text — good for terminal +output: + +``` +$ vp build +✓ built in 1.02s +``` + +### Titles + +The fence meta `title="..."` adds a header bar with the filename and a permanent +copy button: + +````md +```ts title="src/modules/markdown/greet.ts" +export const greet = (name: string) => `Hello, ${name}!`; +``` +```` + +```ts title="src/modules/markdown/greet.ts" +export const greet = (name: string) => `Hello, ${name}!`; +``` + +### Line and word highlights + +`{2}` highlights a line by number and takes ranges like `{1,3-4}`. `/word/` +highlights every occurrence of that word: + +````md +```ts {2} /answer/ +const question = 'life, the universe, everything'; +const answer = 6 * 7; +console.log(`${question} = ${answer}`); +``` +```` + +```ts {2} /answer/ +const question = 'life, the universe, everything'; +const answer = 6 * 7; +console.log(`${question} = ${answer}`); +``` + +### Line numbers + +Line numbers are opt-in per fence with `showLineNumbers`. Add a count in braces — +`showLineNumbers{40}` — to start from a different number: + +````md +```ts showLineNumbers{40} +export function fibonacci(n: number): number { + if (n <= 1) return n; + return fibonacci(n - 1) + fibonacci(n - 2); +} +``` +```` + +```ts showLineNumbers{40} +export function fibonacci(n: number): number { + if (n <= 1) return n; + return fibonacci(n - 1) + fibonacci(n - 2); +} +``` + +### Diffs + +A trailing `[!code ++]` comment marks an added line and `[!code --]` a removed +one. The markers never render, and the copy button copies the result without the +removed lines: + +```ts title="src/modules/post/post.fn.tsx" +export function getPost(slug: string) { + const post = legacyLookup(slug); // [!code --] + const post = allPosts.find((p) => p.slug === slug); // [!code ++] + return post; +} +``` + +### Focus + +A `[!code focus]` comment dims everything else until you hover the block: + +```ts +export function getPost(slug: string) { + const post = allPosts.find((p) => p.slug === slug); // [!code focus] + return post; +} +``` + +### Errors and warnings + +`[!code error]` and `[!code warning]` tint a line the way an editor's +diagnostics would: + +```ts +const post = allPosts.find((p) => p.slug === slug); +post.title; // [!code error] +console.log('debug output'); // [!code warning] +``` + +### Indent guides + +Nested code gets faint vertical guides at each indent level, automatically — no +meta needed: + +```ts +export function walk(node: Node) { + if (node.children) { + for (const child of node.children) { + if (child.type === 'element') { + visit(child); + } + } + } +} +``` + +### Combining meta + +Meta stacks. A single fence can carry a title, line numbers, line highlights, +word highlights, and `[!code]` comments at once: + +```ts title="src/config.ts" showLineNumbers {3} /timeout/ +export const config = { + retries: 3, + timeout: 5_000, // [!code warning] + baseUrl: 'https://api.example.com', +}; +``` + +### Inline code highlighting + +Inline code opts into highlighting with a trailing `{:lang}` marker, so a token +gets real colors mid-sentence: + +```md +Inline code highlights with `const answer = 42{:ts}`. +``` + +Inline code highlights with `const answer = 42{:ts}`. Without the marker, +`const answer = 42` stays plain. + +## Tabs + +`:::tabs` groups content into a tabbed panel. Each `::tab[Label]` marker starts a +tab, and **anything** can follow it — a fence, prose, a list, or a mix. Note +there's no space after the `::` colons: + +````md +:::tabs + +::tab[pnpm] + +```bash +pnpm add shiki +``` + +::tab[npm] + +```bash +npm install shiki +``` + +::: +```` + +Renders as: + +:::tabs + +::tab[pnpm] + +```bash title="terminal" +pnpm add shiki +``` + +::tab[npm] + +```bash +npm install shiki +``` + +::tab[bun] + +```bash +bun add shiki +``` + +::: + +Tabs aren't limited to code — here prose and a list share a group: + +:::tabs + +::tab[Install] + +Install the dependency with your package manager of choice: + +```bash +pnpm add shiki +``` + +::tab[Configure] + +Then wire it into the config: + +- open `vite.config.ts`, +- add the plugin to the `plugins` array, +- restart the dev server. + +::: + +**Edge cases — the build fails loudly** if a tab group is malformed, so you find +out at compile time rather than shipping something broken: + +- `:::tabs` must contain at least one `::tab[Label]` marker. +- Every `::tab[...]` needs a non-empty label _and_ at least one block of content. +- Content can't appear before the first `::tab` marker. +- `::tab` outside a `:::tabs` container is an error. +- Any other directive name (e.g. `:::note`) is rejected — `tabs` is the only one. + +## Escapes and literal characters + +A backslash makes the next character literal, so Markdown punctuation shows up as +plain text instead of doing its usual job. Writing `\*asterisks\*` gives +\*asterisks\* rather than emphasis. + +Because this site also parses `:::` directives, a stray colon-word is turned back +into plain text for you — no escaping needed. CSS pseudo-classes like :hover and +selectors like :root render literally in prose. + +To show a literal backtick inside inline code, wrap the span in a longer run of +backticks, padding with spaces. To show a literal fenced code block inside another +one — as this guide does throughout — open the outer fence with **four** +backticks so the inner three-backtick fence is treated as content. + +[^1]: + Like this — a short note that lives at the bottom of the post but reads as if + it were right here. A footnote definition can even span multiple paragraphs + if you indent its continuation lines. + +[^2]: + Every footnote gets a return arrow that jumps back to its reference, so + readers never lose their place. diff --git a/src/content/series/markdown/_index.mdx b/src/content/series/markdown/_index.mdx new file mode 100644 index 0000000..1abdf81 --- /dev/null +++ b/src/content/series/markdown/_index.mdx @@ -0,0 +1,13 @@ +--- +title: Markdown & Formatting +description: A living reference for how content renders on the site — every heading, list, table, code block, and custom MDX component we support, in one place. +date: 2026-07-09 +thumbnail: + src: './assets/series-markdown-thumbnail.jpg' + alt: '' + caption: 'Photo by [James Harrison](https://unsplash.com/@jstrippa?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/photos/black-laptop-computer-turned-on-on-table-vpOeXr5wmR4?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)' +--- + +This series is the source of truth for how writing looks once it's published here. Rather than describe the formatting in the abstract, each post exercises the real components so you can see exactly what renders — and catch it here first if something ever looks off elsewhere on the site. + +Start with the style guide for a top-to-bottom tour of every supported feature. diff --git a/src/content/series/markdown/assets/picture-1.jpg b/src/content/series/markdown/assets/picture-1.jpg new file mode 100644 index 0000000..1a5432b Binary files /dev/null and b/src/content/series/markdown/assets/picture-1.jpg differ diff --git a/src/content/series/markdown/assets/picture-2.jpg b/src/content/series/markdown/assets/picture-2.jpg new file mode 100644 index 0000000..83d8724 Binary files /dev/null and b/src/content/series/markdown/assets/picture-2.jpg differ diff --git a/src/content/series/markdown/assets/series-markdown-thumbnail.jpg b/src/content/series/markdown/assets/series-markdown-thumbnail.jpg new file mode 100644 index 0000000..4a09ebf Binary files /dev/null and b/src/content/series/markdown/assets/series-markdown-thumbnail.jpg differ diff --git a/src/entry.start.ts b/src/entry.start.ts index 8230ff2..cec4477 100644 --- a/src/entry.start.ts +++ b/src/entry.start.ts @@ -1,11 +1,15 @@ -import { createStart } from '@tanstack/react-start'; +import { createCsrfMiddleware, createStart } from '@tanstack/react-start'; import { cacheRequestMiddleware } from '#/lib/cache/middleware'; import { CachePreset } from '#/lib/cache/presets'; +const csrfMiddleware = createCsrfMiddleware({ + filter: (ctx) => ctx.handlerType === 'serverFn', +}); + export const startInstance = createStart(() => { return { - requestMiddleware: [cacheRequestMiddleware(CachePreset.standard())], + requestMiddleware: [cacheRequestMiddleware(CachePreset.standard()), csrfMiddleware], functionMiddleware: [], }; }); diff --git a/src/lib/i18n/config.ts b/src/lib/i18n/config.ts deleted file mode 100644 index 109bd6d..0000000 --- a/src/lib/i18n/config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { createTranslatedPathnames, createTranslatedPrerender } from '#/lib/i18n/utils'; - -export const translatedPathnames = createTranslatedPathnames({ - '/': { en: '/', id: '/' }, -}); - -export const translatedPrerender = createTranslatedPrerender( - { en: ['/'], id: ['/'] }, - { crawlLinks: false }, -); diff --git a/src/lib/i18n/utils.ts b/src/lib/i18n/utils.ts index 11fa21d..d6c0b4d 100644 --- a/src/lib/i18n/utils.ts +++ b/src/lib/i18n/utils.ts @@ -1,62 +1,37 @@ import { type Locale } from '#/lib/i18n/paraglide/runtime'; -import type { FileRoutesByTo } from '#/routeTree.gen'; import inlangConfig from '../../../project.inlang/settings.json' with { type: 'json' }; const availableLocales = inlangConfig.locales as Locale[]; -type RoutePath = keyof FileRoutesByTo; - -const excludedPaths = ['health', 'api', 'rpc'] as const; - -type PublicRoutePath = Exclude; - -type TranslatedPathname = { +// Locale-prefixed URL patterns for Paraglide route matching. Each base pattern +// maps to `[locale, localizedPath]` tuples, e.g. `/` -> `/en`, `/id`. +export function createLocaleUrlPatterns(): { pattern: string; - localized: Array<[Locale, string]>; -}; - -function toUrlPattern(path: string) { - return ( - path - // catch-all - .replace(/\/\$$/, '/:path(.*)?') - // optional parameters: {-$param} - .replace(/\{-\$([a-zA-Z0-9_]+)\}/g, ':$1?') - // named parameters: $param - .replace(/\$([a-zA-Z0-9_]+)/g, ':$1') - // remove trailing slash - .replace(/\/+$/, '') - ); -} - -export function createTranslatedPathnames( - input: Record>, -): TranslatedPathname[] { - return Object.entries(input).map(([pattern, locales]) => ({ - pattern: toUrlPattern(pattern), - localized: Object.entries(locales).map( - ([locale, path]) => - [locale as Locale, `/${locale}${toUrlPattern(path)}`] satisfies [Locale, string], + localized: [string, string][]; +}[] { + const basePatterns = ['/', '/:path(.*)?']; + return basePatterns.map((pattern) => ({ + pattern, + localized: availableLocales.map( + (locale) => [locale, `/${locale}${pattern === '/' ? '' : pattern}`] as const, ), })); } -export function createTranslatedPrerender( - routes: Record, +// Expands each route into a locale-prefixed prerender entry, e.g. +// `/posts` -> `/en/posts`, `/id/posts`. +export function createLocalePrerenderPages( + routes: string[], options: { headers?: Record; crawlLinks?: boolean } = {}, ) { - const prerenderRoutes = []; - for (const locale of availableLocales) { - for (const route of routes[locale]) { - if (!route.startsWith('/')) { - throw new Error(`[PRERENDER] Route must start with a leading slash: ${route}`); - } - prerenderRoutes.push({ - path: `/${locale}${route === '/' ? '' : route}`, - prerender: { enabled: true, ...options }, - }); + return routes.flatMap((route) => { + if (!route.startsWith('/')) { + throw new Error(`[PRERENDER] Route must start with a leading slash: ${route}`); } - } - return prerenderRoutes; + return availableLocales.map((locale) => ({ + path: `/${locale}${route === '/' ? '' : route}`, + prerender: { enabled: true, ...options }, + })); + }); } diff --git a/src/modules/markdown/components/anchor.tsx b/src/modules/markdown/components/anchor.tsx new file mode 100644 index 0000000..65cd801 --- /dev/null +++ b/src/modules/markdown/components/anchor.tsx @@ -0,0 +1,45 @@ +import { Link } from '@tanstack/react-router'; +import * as React from 'react'; + +/** + * Whether `href` points inside this app and should use client-side routing. + * + * Absolute in-app paths (`/posts/...`) qualify; everything else — external + * URLs, protocol-relative (`//host`), `mailto:`/`tel:`, and hash-only links — + * does not, so the browser handles it natively. + */ +function isInternalLink(href: string) { + return href.startsWith('/') && !href.startsWith('//'); +} + +/** + * Whether `href` is a same-page fragment (`#section`). Heading permalinks + * (rehype-autolink-headings) and author-written anchors both produce these. + */ +function isHashLink(href: string) { + return href.startsWith('#'); +} + +/** + * Renders markdown links (`a`) with app-aware behavior: internal paths + * ({@link isInternalLink}) navigate through TanStack Router's client-side + * `Link`, same-page fragments ({@link isHashLink}) use a plain native anchor so + * the browser scrolls in place, and everything else falls back to a native + * anchor that opens in a new, isolated tab. + */ +export function Anchor({ href = '', ...props }: React.ComponentProps<'a'>) { + if (isInternalLink(href)) { + return ; + } + + // Hash links must stay on the page: without this they'd fall through to the + // external branch below and open the fragment in a blank new tab. + if (isHashLink(href)) { + return ; + } + + // External links open in a new tab; `rel` drops the `window.opener` reference + // (noopener) and withholds the referrer (noreferrer) so the target page can't + // reach back into this one. + return ; +} diff --git a/src/modules/markdown/components/callout.tsx b/src/modules/markdown/components/callout.tsx new file mode 100644 index 0000000..6907d85 --- /dev/null +++ b/src/modules/markdown/components/callout.tsx @@ -0,0 +1,45 @@ +import { + InfoIcon, + LightbulbIcon, + MessageSquareWarningIcon, + OctagonAlertIcon, + TriangleAlertIcon, +} from 'lucide-react'; +import type { ReactNode } from 'react'; + +/** + * The five GitHub alert types, each mapped to its icon and visible label. + * `remark-alert` lowercases the `[!TYPE]` marker into the `type` prop, and + * `markdown.css` keys each type's accent color off the matching `[data-callout]` + * attribute value. + */ +const ALERTS = { + note: { icon: InfoIcon, label: 'Note' }, + tip: { icon: LightbulbIcon, label: 'Tip' }, + important: { icon: MessageSquareWarningIcon, label: 'Important' }, + warning: { icon: TriangleAlertIcon, label: 'Warning' }, + caution: { icon: OctagonAlertIcon, label: 'Caution' }, +} as const; + +type AlertType = keyof typeof ALERTS; + +/** + * Renders a GitHub-style alert (`> [!NOTE]`, `> [!WARNING]`, …) emitted by the + * `remark-alert` plugin. The icon is decorative — the visible label already + * names the alert — so it's hidden from assistive tech. + * + * @param props.type - One of the five alert types; supplied by `remark-alert`. + * @param props.children - The alert body, already stripped of its `[!TYPE]` marker. + */ +export function Callout({ type, children }: { type: AlertType; children: ReactNode }) { + const { icon: Icon, label } = ALERTS[type]; + return ( +

+ ); +} diff --git a/src/modules/markdown/components/code-block.tsx b/src/modules/markdown/components/code-block.tsx new file mode 100644 index 0000000..b8ddb91 --- /dev/null +++ b/src/modules/markdown/components/code-block.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; + +import { CodeCopyButton } from '#/modules/markdown/components/code-copy-button'; + +/** + * Renders a fenced code block (`pre`) with a {@link CodeCopyButton}. + * + * rehype-pretty-code wraps every fence in a `figure[data-rehype-pretty-code-figure]` + * (plus a `figcaption` when the fence has a title). Rendering the copy button + * as a sibling of `pre` puts it inside that figure, which app.css makes the + * positioning context — so the same button lands in the header bar of titled + * blocks and floats over the code of untitled ones. + */ +export function CodeBlock(props: React.ComponentProps<'pre'>) { + return ( + <> +
+      
+    
+  );
+}
diff --git a/src/modules/markdown/components/code-copy-button.tsx b/src/modules/markdown/components/code-copy-button.tsx
new file mode 100644
index 0000000..407588b
--- /dev/null
+++ b/src/modules/markdown/components/code-copy-button.tsx
@@ -0,0 +1,57 @@
+'use client';
+
+import { CheckIcon, CopyIcon } from 'lucide-react';
+import type { MouseEvent } from 'react';
+import { useRef, useState } from 'react';
+
+import { Button } from '#/ui/components/core/button';
+
+/** How long the copied-state checkmark stays visible before reverting to the copy icon. */
+const COPY_FEEDBACK_DURATION_MS = 2000;
+
+/**
+ * Copies the rendered code of the enclosing rehype-pretty-code figure.
+ * Reading the DOM (instead of receiving the source as a prop) keeps the code
+ * out of the RSC payload twice, and lets us copy the block's end state:
+ * `[!code]` markers are already stripped at render time and diff-removed
+ * lines are skipped here.
+ */
+export function CodeCopyButton() {
+  const [copied, setCopied] = useState(false);
+  const timeoutRef = useRef>(undefined);
+
+  function handleCopy(event: MouseEvent) {
+    const figure = event.currentTarget.closest('figure');
+    if (!figure) return;
+
+    const lines = Array.from(figure.querySelectorAll('pre code [data-line]'));
+    const text =
+      lines.length > 0
+        ? lines
+            .filter(
+              (line) => !(line.classList.contains('diff') && line.classList.contains('remove')),
+            )
+            .map((line) => line.textContent ?? '')
+            .join('\n')
+        : (figure.querySelector('pre code')?.textContent ?? '');
+
+    void navigator.clipboard.writeText(text);
+    setCopied(true);
+    clearTimeout(timeoutRef.current);
+    timeoutRef.current = setTimeout(() => setCopied(false), COPY_FEEDBACK_DURATION_MS);
+  }
+
+  return (
+    
+  );
+}
diff --git a/src/modules/markdown/components/image.tsx b/src/modules/markdown/components/image.tsx
new file mode 100644
index 0000000..d634bb1
--- /dev/null
+++ b/src/modules/markdown/components/image.tsx
@@ -0,0 +1,16 @@
+import * as React from 'react';
+
+/**
+ * Renders markdown images (`img`) with content-page defaults.
+ *
+ * `loading="lazy"` and `decoding="async"` keep below-the-fold images off the
+ * critical path. Local images arrive with intrinsic `width`/`height` from the
+ * `rehypeImage` build pass (via `src` and dimension props spread here), so the
+ * browser reserves space and the article doesn't reflow as they load; markdown
+ * CSS pins `height:auto`/`max-width:100%` so those attributes never distort the
+ * rendered size. `alt` defaults to `""` — a valid, decorative-image default —
+ * when the author omits it.
+ */
+export function Image({ alt = '', ...props }: React.ComponentProps<'img'>) {
+  return {alt};
+}
diff --git a/src/modules/markdown/components/index.tsx b/src/modules/markdown/components/index.tsx
new file mode 100644
index 0000000..cf9143d
--- /dev/null
+++ b/src/modules/markdown/components/index.tsx
@@ -0,0 +1,23 @@
+import type { MDXComponents } from 'mdx/types';
+
+import { Anchor } from '#/modules/markdown/components/anchor';
+import { Callout } from '#/modules/markdown/components/callout';
+import { CodeBlock } from '#/modules/markdown/components/code-block';
+import { Image } from '#/modules/markdown/components/image';
+import { Table } from '#/modules/markdown/components/table';
+import { Tabs } from '#/modules/markdown/components/tabs';
+
+/**
+ * Component overrides handed to the MDX runtime. Markdown-generated HTML tags
+ * (`a`, `pre`, `img`, `table`) are swapped for app-aware components, and custom
+ * elements (`Tabs` from the `:::tabs` directive, `Callout` from GitHub-style
+ * `> [!NOTE]` alerts) are exposed to MDX output.
+ */
+export const mdxComponents: MDXComponents = {
+  a: Anchor,
+  pre: CodeBlock,
+  img: Image,
+  table: Table,
+  Tabs,
+  Callout,
+};
diff --git a/src/modules/markdown/components/table.tsx b/src/modules/markdown/components/table.tsx
new file mode 100644
index 0000000..25ad1bc
--- /dev/null
+++ b/src/modules/markdown/components/table.tsx
@@ -0,0 +1,18 @@
+import * as React from 'react';
+
+/**
+ * Renders markdown tables (`table`) inside a horizontal scroll container.
+ *
+ * @tailwindcss/typography pins tables to `width: 100%`, so a wide table would
+ * otherwise squeeze its columns unreadably narrow on small screens rather than
+ * scroll. The `[data-table-wrapper]` div is the scroll boundary; markdown CSS
+ * floors the table with a `min-width` so it overflows (and the wrapper scrolls)
+ * once the viewport gets narrow, and moves the block margin onto the wrapper.
+ */
+export function Table(props: React.ComponentProps<'table'>) {
+  return (
+    
+ + + ); +} diff --git a/src/modules/markdown/components/tabs.tsx b/src/modules/markdown/components/tabs.tsx new file mode 100644 index 0000000..e77e3fb --- /dev/null +++ b/src/modules/markdown/components/tabs.tsx @@ -0,0 +1,34 @@ +'use client'; + +import type { ReactNode } from 'react'; +import { Children, isValidElement } from 'react'; + +import { Tabs as TabsRoot, TabsContent, TabsList, TabsTrigger } from '#/ui/components/core/tabs'; + +/** + * Tab group produced by the `:::tabs` directive (see `remark-tabs.ts`). + * `labels` arrives JSON-encoded because MDX JSX attributes created by the + * remark plugin are plain strings; children hold one wrapper div per + * `::tab[Label]` section, in label order. + */ +export function Tabs({ labels, children }: { labels: string; children: ReactNode }) { + const tabLabels = JSON.parse(labels) as string[]; + const panels = Children.toArray(children).filter((child) => isValidElement(child)); + + return ( + + + {tabLabels.map((label, index) => ( + + {label} + + ))} + + {panels.map((panel, index) => ( + + {panel} + + ))} + + ); +} diff --git a/src/modules/markdown/index.tsx b/src/modules/markdown/index.tsx new file mode 100644 index 0000000..a928eb8 --- /dev/null +++ b/src/modules/markdown/index.tsx @@ -0,0 +1,16 @@ +import type { MDXContent } from 'mdx/types'; + +import { mdxComponents } from '#/modules/markdown/components'; + +/** + * Renders compiled MDX content with the app's component overrides applied. + * + * Takes an `MDXContent` component produced by the MDX compilation step (e.g. + * `series.mdx`, `post.mdx`) and invokes it with {@link mdxComponents}, so that + * markdown-generated tags and custom directives resolve to app-aware components. + * + * @param props.content - The compiled MDX component to render. + */ +export function MarkdownRender({ content: Content }: { content: MDXContent }) { + return ; +} diff --git a/src/modules/markdown/markdown.types.ts b/src/modules/markdown/markdown.types.ts new file mode 100644 index 0000000..5ded52b --- /dev/null +++ b/src/modules/markdown/markdown.types.ts @@ -0,0 +1,20 @@ +/** + * One entry in a document's table of contents, derived from a Markdown heading. + * `depth` is the heading level (1–6), `title` the heading's plain text, and + * `id` the slug `rehype-slug` stamps on the rendered heading — deep-link + * to it with `#${id}`. + * + * It is plain data, not a rendered node, so a page renders it however it likes + * (`post.toc[0].title`, `post.toc[0].id`). + */ +// A `type` (not an `interface`) so it keeps an implicit index signature and +// stays assignable to Content Collections' JSON-serializable document +// constraint — the value is derived in a collection `transform`. +export type TableOfContentsEntry = { + depth: number; + title: string; + id: string; +}; + +/** A document's headings, in document order. */ +export type TableOfContents = TableOfContentsEntry[]; diff --git a/src/modules/markdown/markdown.utils.ts b/src/modules/markdown/markdown.utils.ts new file mode 100644 index 0000000..1baaa5a --- /dev/null +++ b/src/modules/markdown/markdown.utils.ts @@ -0,0 +1,101 @@ +import { execFile as syncExecFile } from 'node:child_process'; +import path from 'node:path'; +import { promisify } from 'node:util'; + +import { createDefaultImport } from '@content-collections/core'; +import type { Nodes } from 'hast'; +import { fromMarkdown } from 'mdast-util-from-markdown'; +import { toHast } from 'mdast-util-to-hast'; +import rehypeSlug from 'rehype-slug'; +import { visit } from 'unist-util-visit'; + +import type { TableOfContents } from '#/modules/markdown/markdown.types'; + +const execFile = promisify(syncExecFile); + +// Resolve the last-modified timestamp from git history instead of fs mtime, +// which is unreliable in CI (fresh checkouts stamp every file with clone time). +// Falls back to the current time for files with no commits yet (new/uncommitted +// posts). Memoized via `ctx.cache`, so it only re-runs when the document changes. +export async function getLastModification(filePath: string) { + const { stdout } = await execFile('git', ['log', '-1', '--format=%aI', '--', filePath]); + if (stdout) { + return new Date(stdout.trim()).toISOString(); + } + return new Date().toISOString(); +} + +/** + * Resolves a content-relative asset reference to a build-time import, so any + * frontmatter field that points at a local file (a thumbnail `src`, an OG image, + * a gallery entry) is served as a fingerprinted, hashed asset URL. + * + * A remote or absolute `src` — anything not starting with `./` or `../` — passes + * through untouched. A path relative to the document, `./cover.jpg` (the same + * style MDX-body images use), becomes an import via `createDefaultImport`, which + * content-collections rewrites into a real Vite import at build time; its + * `ResolveImports` then collapses the marker back to the resolved URL string on + * the collection type. Frontmatter is parsed as plain data and never reaches the + * MDX image pipeline, so without this a relative path would ship verbatim and + * 404 in the browser. + * + * `contentSubDir` is the referencing document's directory beneath `src/content` + * (e.g. `series/markdown`), used to rebase the relative path onto the `#` alias. + * + * Build-only: this is the sole consumer of `@content-collections/core`, whose + * bundle drags in gray-matter (direct `eval`) and esbuild. It lives here beside + * the other content-collections build helpers — reached only from the config's + * `transform`, which runs in Node — so those heavy transitive deps never pull + * into the RSC/SSR Worker bundle via a runtime-reachable module. + */ +export function resolveAsset(src: string, contentSubDir: string) { + if (!src.startsWith('.')) return src; + const assetPath = path.posix.join(contentSubDir, src); + return createDefaultImport(`#/content/${assetPath}`); +} + +/** Concatenates a hast subtree's text — the same content `rehype-slug` slugs. */ +function textContent(node: Nodes): string { + if (node.type === 'text') return node.value; + if ('children' in node) return node.children.map(textContent).join(''); + return ''; +} + +/** + * Builds a document's table of contents from its Markdown headings, as plain + * data (`{ depth, title, id }`) rather than rendered nodes — so a page can + * lay the entries out however it likes (`post.toc[0].title`, + * `post.toc[0].id`), in a sidebar, inline, or nested list. + * + * `id` is the heading's slug: we run the very same `rehype-slug` the + * render pipeline uses (see `markdown.vite.ts`) over the same headings, then read + * the stamped `id` back off each one — so a `#id` link always lands on its + * heading, duplicate titles and all, with no second slug implementation to drift. + * + * Takes the raw `.mdx` source (the caller reads it off disk — the + * `frontmatter-only` Content Collections parser never exposes the body — and + * memoizes this on that content). Build-only: it lives beside the other build + * helpers so its Markdown-parsing dependencies stay out of the runtime bundle + * (this module is reached only from the config's Node-side `transform`). + */ +export async function extractTableOfContents(content: string): Promise { + // Content Collections strips the frontmatter through its own parser, but the + // caller hands us the raw file, so drop the leading `---` … `---` block first — + // otherwise its closing fence parses as a setext underline and turns the last + // YAML line into a phantom heading. + const body = content.replace(/^\uFEFF?---\r?\n[\s\S]*?\r?\n---\r?\n/, ''); + const tree = toHast(fromMarkdown(body)) as Nodes; + // Stamp heading `id`s in place, exactly as the render pipeline does. + rehypeSlug()(tree as never); + + const toc: TableOfContents = []; + visit(tree, 'element', (node) => { + const match = /^h([1-6])$/.exec(node.tagName); + if (!match) return; + const title = textContent(node).trim(); + const id = typeof node.properties.id === 'string' ? node.properties.id : ''; + if (!title || !id) return; + toc.push({ depth: Number(match[1]), title, id }); + }); + return toc; +} diff --git a/src/modules/markdown/markdown.vite.ts b/src/modules/markdown/markdown.vite.ts new file mode 100644 index 0000000..949f66f --- /dev/null +++ b/src/modules/markdown/markdown.vite.ts @@ -0,0 +1,108 @@ +import mdx from '@mdx-js/rollup'; +import { transformerColorizedBrackets } from '@shikijs/colorized-brackets'; +import { + transformerNotationDiff, + transformerNotationErrorLevel, + transformerNotationFocus, + transformerRenderIndentGuides, +} from '@shikijs/transformers'; +import rehypeAutolinkHeadings, { + type Options as RehypeAutolinkHeadingsOptions, +} from 'rehype-autolink-headings'; +import rehypeMdxImportMedia from 'rehype-mdx-import-media'; +import type { Options as RehypePrettyCodeOptions } from 'rehype-pretty-code'; +import rehypePrettyCode from 'rehype-pretty-code'; +import rehypeSlug from 'rehype-slug'; +import remarkDirective from 'remark-directive'; +import remarkFrontmatter from 'remark-frontmatter'; +import remarkGfm from 'remark-gfm'; +import remarkMdxFrontmatter from 'remark-mdx-frontmatter'; +import type { Plugin } from 'vite'; + +import { rehypeImage } from '#/modules/markdown/plugins/rehype-image'; +import { remarkAlert } from '#/modules/markdown/plugins/remark-alert'; +import { remarkTabs } from '#/modules/markdown/plugins/remark-tabs'; + +/** + * rehype-pretty-code configuration for compile-time syntax highlighting. + * + * Highlighting happens entirely at MDX compile time: the Worker and the + * browser only ever see the finished HTML. Fence meta owns block-level config + * (title="...", {1,3}, /word/, showLineNumbers) via rehype-pretty-code; + * `[!code ...]` comments own per-line semantics (diff/focus/error/warning) via + * the Shiki transformers. Dual themes are emitted as CSS variables and toggled + * by the `.dark` class in app.css. + */ +const rehypePrettyCodeOptions: RehypePrettyCodeOptions = { + theme: { + light: 'github-light-default', + dark: 'github-dark-default', + }, + keepBackground: false, + // Block-scoped on purpose: a bare string would also apply to inline code, + // pulling every unmarked `code span` through Shiki — where the notation + // transformers strip `[!code ...]` mentions out of prose. + defaultLang: { block: 'plaintext' }, + transformers: [ + transformerNotationDiff(), + transformerNotationFocus(), + transformerNotationErrorLevel(), + transformerRenderIndentGuides(), + transformerColorizedBrackets(), + ], +}; + +/** + * rehype-autolink-headings configuration. `rehypeSlug` stamps a stable `id` on + * every heading (derived from its text); this then appends a hover-revealed `#` + * permalink inside each heading that deep-links to that `id`. The generated + * `` renders through the `Anchor` override, which routes hash + * links to a plain same-page anchor (not a new tab). `markdown.css` owns the + * reveal-on-hover styling and the `scroll-margin-top` landing offset. + */ +const rehypeAutolinkHeadingsOptions: RehypeAutolinkHeadingsOptions = { + behavior: 'append', + properties: { className: ['heading-anchor'], ariaLabel: 'Permalink to this heading' }, + content: { type: 'text', value: '#' }, +}; + +/** + * Vite plugin that compiles `.mdx` files through the project's remark/rehype + * pipeline: frontmatter handling, GFM, GitHub-style alerts (`remarkAlert`), the + * `:::tabs` directive (`remarkTabs`), and Shiki syntax highlighting + * (`rehypePrettyCodeOptions`). + */ +export function viteMdx(): Plugin { + return { + // MDX must run before the React plugin, hence `enforce: 'pre'`. The remark + // plugins strip the YAML frontmatter so it never leaks into the rendered + // output (Content Collections parses it separately, `frontmatter-only`). + enforce: 'pre', + ...mdx({ + remarkPlugins: [ + remarkFrontmatter, + remarkMdxFrontmatter, + remarkGfm, + // `remarkAlert` reads the blockquotes GFM produced; it must run after + // `remarkGfm` and is independent of the `:::` directive passes below. + remarkAlert, + remarkDirective, + remarkTabs, + ], + // `rehypeImage` runs before `rehypeMdxImportMedia` so it still sees the + // raw string `src` it needs to read pixel dimensions from disk and to + // detect a captioned image; import-media then turns every local `src` + // into a fingerprinted asset import. + rehypePlugins: [ + [rehypePrettyCode, rehypePrettyCodeOptions], + // `rehypeSlug` must run before `rehypeAutolinkHeadings`, which links to + // the `id` it stamps. Both only touch headings, so their position + // relative to the code/image passes is immaterial. + rehypeSlug, + [rehypeAutolinkHeadings, rehypeAutolinkHeadingsOptions], + rehypeImage, + rehypeMdxImportMedia, + ], + }), + }; +} diff --git a/src/modules/markdown/plugins/rehype-image.ts b/src/modules/markdown/plugins/rehype-image.ts new file mode 100644 index 0000000..e92d7de --- /dev/null +++ b/src/modules/markdown/plugins/rehype-image.ts @@ -0,0 +1,108 @@ +import { readFileSync } from 'node:fs'; +import path from 'node:path'; + +import type { Element, Root } from 'hast'; +import { imageSize } from 'image-size'; +import { SKIP, visit } from 'unist-util-visit'; +import type { VFile } from 'vfile'; + +import { parseCaption } from '#/modules/thumbnail/thumbnail.utils'; + +/** + * Matches sources `rehype-mdx-import-media` leaves untouched: `http:`/`https:`, + * protocol-relative (`//host`), and root-absolute (`/logo.png`, a `public/` + * asset). We can't measure any of these at build time, so they render without + * intrinsic dimensions — the accepted trade-off for the pass-through case. + */ +const REMOTE = /^(https?:)?\//; +/** `data:`, `mailto:`, and any other URI scheme — also unmeasurable on disk. */ +const SCHEME = /^[a-z][a-z0-9+.-]*:/i; + +/** Whether `src` points at a colocated file we can read relative to the MDX. */ +function isLocal(src: string): boolean { + return !REMOTE.test(src) && !SCHEME.test(src); +} + +/** The whitespace-only text nodes MDX leaves around an image in a paragraph. */ +function isBlank(node: { type: string; value?: string }): boolean { + return node.type === 'text' && (node.value ?? '').trim() === ''; +} + +/** + * Build-time rehype pass for local images, run *before* + * `rehype-mdx-import-media` rewrites `src` into an import — so it still sees the + * raw string path. It does two things: + * + * 1. **Intrinsic dimensions.** For every colocated image (`isLocal`) it + * reads the file's real pixel size and stamps `width`/`height` on the node. + * The browser then reserves space before the bytes arrive, eliminating the + * layout shift a content page would otherwise suffer. Remote/absolute srcs + * and unreadable/unsupported files (e.g. a viewBox-only SVG) are skipped — + * dimensions are best-effort; resolving the import is import-media's job. + * 2. **Captions.** A paragraph whose only content is a titled image + * (`![alt](./x.png "caption")`) becomes `
…`, the + * caption parsed as Markdown/HTML (`parseCaption`). The `

` unwrap is + * mandatory: `

` is not valid inside `

`. An image without a title, + * or one sharing its paragraph with other content (e.g. wrapped in a link), + * stays inline. + */ +export function rehypeImage() { + return (tree: Root, file: VFile) => { + // Pass 1: stamp intrinsic dimensions onto every measurable image. Done + // first so the node already carries width/height when pass 2 moves it into + // a figure. + visit(tree, 'element', (node) => { + if (node.tagName !== 'img') return; + const src = node.properties.src; + if (typeof src !== 'string' || !isLocal(src)) return; + if (file.dirname == null) return; + + try { + const buffer = readFileSync(path.resolve(file.dirname, src)); + const { width, height } = imageSize(buffer); + if (width && height) { + node.properties.width = width; + node.properties.height = height; + } + } catch { + // Unreadable or unsupported: leave dimensions off. If the file is + // genuinely missing, import-media raises the canonical resolve error. + } + }); + + // Pass 2: promote `

` wrappers around a single titled image into a figure + // with a caption. + visit(tree, 'element', (node, index, parent) => { + if (node.tagName !== 'p' || parent == null || index == null) return; + + const content = node.children.filter((child) => !isBlank(child)); + const [only] = content; + if (content.length !== 1 || only.type !== 'element' || only.tagName !== 'img') return; + + const img = only; + const title = img.properties.title; + if (typeof title !== 'string' || title.trim() === '') return; + + // Move the caption out of the tooltip and into the figcaption so it isn't + // announced twice. + delete img.properties.title; + const figure: Element = { + type: 'element', + tagName: 'figure', + properties: { dataImageFigure: '' }, + children: [ + img, + { + type: 'element', + tagName: 'figcaption', + properties: {}, + children: parseCaption(title), + }, + ], + }; + parent.children[index] = figure; + // Dimensions are already set and the figure needs no further visiting. + return SKIP; + }); + }; +} diff --git a/src/modules/markdown/plugins/remark-alert.ts b/src/modules/markdown/plugins/remark-alert.ts new file mode 100644 index 0000000..068791d --- /dev/null +++ b/src/modules/markdown/plugins/remark-alert.ts @@ -0,0 +1,73 @@ +import type { Blockquote, Root, RootContent } from 'mdast'; +import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx'; +import { SKIP, visit } from 'unist-util-visit'; + +/** The five GitHub alert types, uppercase as authored in the `[!TYPE]` marker. */ +const ALERT_TYPES = ['NOTE', 'TIP', 'IMPORTANT', 'WARNING', 'CAUTION'] as const; + +/** + * A leading alert marker occupying the blockquote's first line: `[!NOTE]` (one + * of the five types, uppercase) followed by optional trailing spaces and then + * either a soft break — the body continues in the same paragraph — or the end of + * the text node — the body lives in later blocks. A marker that shares its line + * with other text does not match, mirroring GitHub, which needs it alone on + * line one. + */ +const ALERT_MARKER = new RegExp(String.raw`^\[!(${ALERT_TYPES.join('|')})\][^\S\n]*(?:\n|$)`); + +/** + * Rewrites GitHub-style alert blockquotes into `` MDX elements. + * + * A blockquote whose first line is an alert marker — + * + * ```md + * > [!WARNING] + * > This needs your attention. + * ``` + * + * — becomes `` with the marker stripped and + * the rest of the blockquote preserved (prose, lists, code, and links all + * survive). `Callout` must be provided through the MDX components mapping. A + * blockquote without a recognized marker is left untouched as an ordinary quote, + * and a lowercase or mistyped marker (`[!note]`, `[!HINT]`) renders literally — + * matching GitHub, which is strict about the five uppercase names. + */ +export function remarkAlert() { + return (tree: Root) => { + visit(tree, (node, index, parent) => { + if (node.type !== 'blockquote' || parent === undefined || index === undefined) return; + const blockquote = node as Blockquote; + + const opening = blockquote.children[0]; + if (opening?.type !== 'paragraph') return; + const marker = opening.children[0]; + if (marker?.type !== 'text') return; + + const match = ALERT_MARKER.exec(marker.value); + if (match === null) return; + + // Strip the marker from the opening paragraph. If it left an empty text + // node, drop that; if the paragraph then holds nothing, drop it too so the + // callout body starts cleanly at the next block. + const remainder = marker.value.slice(match[0].length); + if (remainder === '') { + opening.children.shift(); + if (opening.children.length === 0) blockquote.children.shift(); + } else { + marker.value = remainder; + } + + const callout: MdxJsxFlowElement = { + type: 'mdxJsxFlowElement', + name: 'Callout', + attributes: [{ type: 'mdxJsxAttribute', name: 'type', value: match[1].toLowerCase() }], + children: blockquote.children as MdxJsxFlowElement['children'], + }; + (parent.children as RootContent[])[index] = callout; + + // Don't descend into the moved children: nothing inside is another alert + // to rewrite, and GitHub alerts can't nest. + return SKIP; + }); + }; +} diff --git a/src/modules/markdown/plugins/remark-tabs.ts b/src/modules/markdown/plugins/remark-tabs.ts new file mode 100644 index 0000000..e7f6ae9 --- /dev/null +++ b/src/modules/markdown/plugins/remark-tabs.ts @@ -0,0 +1,147 @@ +import type { PhrasingContent, Root, RootContent } from 'mdast'; +import type { ContainerDirective } from 'mdast-util-directive'; +import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx'; +import { SKIP, visit } from 'unist-util-visit'; +import type { VFile } from 'vfile'; + +/** Container directive authors open with `:::tabs`. */ +export const TABS_DIRECTIVE_NAME = 'tabs'; +/** Leaf directive authors write as `::tab[Label]` to start a tab section. */ +const TAB_DIRECTIVE_NAME = 'tab'; + +/** Flattens a phrasing-content subtree (e.g. a `::tab[...]` label) to plain text. */ +function toText(nodes: PhrasingContent[]): string { + return nodes + .map((node) => { + if ('value' in node) return node.value; + if ('children' in node) return toText(node.children); + return ''; + }) + .join(''); +} + +/** A single tab: its label and the blocks that render as its panel. */ +type TabSection = { label: string; children: MdxJsxFlowElement['children'] }; + +/** + * Rewrites `:::tabs` containers (parsed by `remark-directive`) into a + * `` MDX element. `Tabs` must be provided through the MDX + * components mapping. Each `::tab[Label]` marker starts a tab, and anything + * may follow it — prose, lists, fences (with all their meta): + * + * ```md + * :::tabs + * ::tab[pnpm] + * ... + * ::tab[npm] + * ... + * ::: + * ``` + * + * Inline and leaf directives (`:hover`, `::name`) found elsewhere are + * unwrapped back to literal text: authors writing a bare colon-word almost + * always mean the text itself, and leaving directive nodes in the tree + * crashes mdast-to-hast. + */ +export function remarkTabs() { + return (tree: Root, file: VFile) => { + visit(tree, (node, index, parent) => { + if (parent === undefined || index === undefined) return; + const siblings = parent.children as RootContent[]; + + if (node.type === 'textDirective') { + siblings.splice(index, 1, { type: 'text', value: `:${node.name}` }, ...node.children); + return index; + } + if (node.type === 'leafDirective') { + if (node.name === TAB_DIRECTIVE_NAME) { + file.fail( + `"::${TAB_DIRECTIVE_NAME}" can only be used inside ":::${TABS_DIRECTIVE_NAME}".`, + node, + ); + } + siblings.splice(index, 1, { + type: 'paragraph', + children: [{ type: 'text', value: `::${node.name}` }, ...node.children], + }); + return index; + } + + if (node.type !== 'containerDirective') return; + const directive = node as ContainerDirective; + if (directive.name !== TABS_DIRECTIVE_NAME) { + file.fail( + `Unknown directive ":::${directive.name}". Only ":::${TABS_DIRECTIVE_NAME}" is supported.`, + directive, + ); + } + + const sections = collectTabSections(directive, file); + const tabs: MdxJsxFlowElement = { + type: 'mdxJsxFlowElement', + name: 'Tabs', + attributes: [ + { + type: 'mdxJsxAttribute', + name: 'labels', + value: JSON.stringify(sections.map((section) => section.label)), + }, + ], + // One wrapper element per tab so multi-block content stays 1:1 with + // its label. + children: sections.map((section) => ({ + type: 'mdxJsxFlowElement', + name: 'div', + attributes: [], + children: section.children, + })), + }; + siblings[index] = tabs; + // SKIP, or traversal would descend into the replaced (stale) directive + // node and hit its already-consumed `::tab` leaves; resuming at the + // same index visits the new Tabs element instead, so section content + // still gets accidental inline directives unwrapped. + return [SKIP, index]; + }); + }; +} + +/** `::tab[Label]` markers partition the container into sections. */ +function collectTabSections(directive: ContainerDirective, file: VFile): TabSection[] { + const sections: TabSection[] = []; + for (const child of directive.children) { + if (child.type === 'leafDirective' && child.name === TAB_DIRECTIVE_NAME) { + const label = toText(child.children).trim(); + if (!label) { + file.fail( + `"::${TAB_DIRECTIVE_NAME}" needs a label, e.g. ::${TAB_DIRECTIVE_NAME}[First tab].`, + child, + ); + } + sections.push({ label, children: [] }); + continue; + } + const current = sections.at(-1); + if (!current) { + file.fail( + `Content inside ":::${TABS_DIRECTIVE_NAME}" must come after a "::${TAB_DIRECTIVE_NAME}[...]" marker.`, + child, + ); + } + current.children.push(child as MdxJsxFlowElement['children'][number]); + } + if (sections.length === 0) { + file.fail( + `":::${TABS_DIRECTIVE_NAME}" must contain at least one "::${TAB_DIRECTIVE_NAME}[Label]" marker.`, + directive, + ); + } + const empty = sections.find((section) => section.children.length === 0); + if (empty) { + file.fail( + `"::${TAB_DIRECTIVE_NAME}[${empty.label}]" has no content — every tab needs at least one block.`, + directive, + ); + } + return sections; +} diff --git a/src/modules/post/post.fn.tsx b/src/modules/post/post.fn.tsx new file mode 100644 index 0000000..4ec08dd --- /dev/null +++ b/src/modules/post/post.fn.tsx @@ -0,0 +1,46 @@ +import { notFound } from '@tanstack/react-router'; +import { createServerFn } from '@tanstack/react-start'; +import { renderServerComponent } from '@tanstack/react-start/rsc'; +import { allPosts } from 'content-collections'; +import * as z from 'zod/v4'; + +import { MarkdownRender } from '#/modules/markdown'; +import type { PostContent, PostItem } from '#/modules/post/post.types'; +import { parseThumbnail } from '#/modules/thumbnail/thumbnail.utils'; + +export const getAllPostsFn = createServerFn({ method: 'GET' }).handler((): PostItem[] => { + return [...allPosts] + .sort((a, b) => b.date.localeCompare(a.date)) + .map((post) => { + return { + slug: post.slug, + title: post.title, + description: post.description, + date: post.date, + author: post.author, + tags: post.tags, + thumbnail: post.thumbnail, + lastModification: post.lastModification, + }; + }); +}); + +export const getPostBySlugFn = createServerFn({ method: 'GET' }) + .validator(z.object({ slug: z.string() })) + .handler(async ({ data }): Promise => { + const post = allPosts.find((post) => post.slug === data.slug); + if (!post) throw notFound(); + + return { + slug: post.slug, + title: post.title, + description: post.description, + date: post.date, + author: post.author, + tags: post.tags, + thumbnail: parseThumbnail(post.thumbnail), + lastModification: post.lastModification, + toc: post.toc, + mdx: await renderServerComponent(), + }; + }); diff --git a/src/modules/post/post.schema.ts b/src/modules/post/post.schema.ts new file mode 100644 index 0000000..5a015dd --- /dev/null +++ b/src/modules/post/post.schema.ts @@ -0,0 +1,12 @@ +import * as z from 'zod/v4'; + +import { thumbnailSchema } from '#/modules/thumbnail/thumbnail.schema'; + +export const postFrontmatterSchema = z.object({ + title: z.string(), + description: z.string(), + date: z.iso.date(), + author: z.string(), + tags: z.array(z.string()), + thumbnail: thumbnailSchema, +}); diff --git a/src/modules/post/post.types.ts b/src/modules/post/post.types.ts new file mode 100644 index 0000000..7721607 --- /dev/null +++ b/src/modules/post/post.types.ts @@ -0,0 +1,18 @@ +import type { RenderableServerComponent } from '@tanstack/react-start/rsc'; +import type { JSX } from 'react/jsx-runtime'; +import * as z from 'zod/v4'; + +import type { TableOfContents } from '#/modules/markdown/markdown.types'; +import type { postFrontmatterSchema } from '#/modules/post/post.schema'; + +export type PostFrontmatter = z.infer; + +export interface PostItem extends PostFrontmatter { + slug: string; + lastModification: string; +} + +export interface PostContent extends PostItem { + toc: TableOfContents; + mdx: RenderableServerComponent; +} diff --git a/src/modules/series/series.fn.tsx b/src/modules/series/series.fn.tsx new file mode 100644 index 0000000..f56bc3f --- /dev/null +++ b/src/modules/series/series.fn.tsx @@ -0,0 +1,94 @@ +import { notFound } from '@tanstack/react-router'; +import { createServerFn } from '@tanstack/react-start'; +import { renderServerComponent } from '@tanstack/react-start/rsc'; +import { allSeries, allSeriesPosts } from 'content-collections'; +import * as z from 'zod/v4'; + +import { MarkdownRender } from '#/modules/markdown'; +import type { + SeriesContent, + SeriesItem, + SeriesPostContent, + SeriesPostItem, +} from '#/modules/series/series.types'; +import { parseThumbnail } from '#/modules/thumbnail/thumbnail.utils'; + +export const getAllSeriesFn = createServerFn({ method: 'GET' }).handler((): SeriesItem[] => { + return [...allSeries] + .sort((a, b) => b.date.localeCompare(a.date)) + .map((series) => { + return { + slug: series.slug, + title: series.title, + description: series.description, + date: series.date, + thumbnail: series.thumbnail, + lastModification: series.lastModification, + }; + }); +}); + +export const getSeriesBySlugFn = createServerFn({ method: 'GET' }) + .validator(z.object({ slug: z.string() })) + .handler(async ({ data }): Promise => { + const series = allSeries.find((series) => series.slug === data.slug); + if (!series) throw notFound(); + + const posts: SeriesPostItem[] = allSeriesPosts + .filter((post) => post.seriesSlug === data.slug) + .sort((a, b) => a.order - b.order) + .map((post) => { + return { + slug: post.slug, + order: post.order, + title: post.title, + description: post.description, + date: post.date, + author: post.author, + tags: post.tags, + // A post without its own thumbnail inherits the series' one. + thumbnail: post.thumbnail ?? series.thumbnail, + lastModification: post.lastModification, + series: { slug: post.seriesSlug }, + }; + }); + + return { + slug: series.slug, + title: series.title, + description: series.description, + date: series.date, + thumbnail: parseThumbnail(series.thumbnail), + lastModification: series.lastModification, + posts, + toc: series.toc, + mdx: await renderServerComponent(), + }; + }); + +export const getSeriesPostFn = createServerFn({ method: 'GET' }) + .validator(z.object({ slug: z.string(), postSlug: z.string() })) + .handler(async ({ data }): Promise => { + const post = allSeriesPosts.find( + (post) => post.seriesSlug === data.slug && post.slug === data.postSlug, + ); + if (!post) throw notFound(); + + // A post without its own thumbnail inherits the series' one. + const series = allSeries.find((series) => series.slug === post.seriesSlug); + + return { + slug: post.slug, + order: post.order, + title: post.title, + description: post.description, + date: post.date, + author: post.author, + tags: post.tags, + thumbnail: parseThumbnail(post.thumbnail ?? series?.thumbnail ?? null), + lastModification: post.lastModification, + series: { slug: post.seriesSlug }, + toc: post.toc, + mdx: await renderServerComponent(), + }; + }); diff --git a/src/modules/series/series.schema.ts b/src/modules/series/series.schema.ts new file mode 100644 index 0000000..06051e8 --- /dev/null +++ b/src/modules/series/series.schema.ts @@ -0,0 +1,19 @@ +import * as z from 'zod/v4'; + +import { thumbnailSchema } from '#/modules/thumbnail/thumbnail.schema'; + +export const seriesFrontmatterSchema = z.object({ + title: z.string(), + description: z.string(), + date: z.iso.date(), + thumbnail: thumbnailSchema, +}); + +export const seriesPostFrontmatterSchema = z.object({ + title: z.string(), + description: z.string(), + date: z.iso.date(), + author: z.string(), + tags: z.array(z.string()), + thumbnail: thumbnailSchema, +}); diff --git a/src/modules/series/series.types.ts b/src/modules/series/series.types.ts new file mode 100644 index 0000000..0bd3155 --- /dev/null +++ b/src/modules/series/series.types.ts @@ -0,0 +1,37 @@ +import type { RenderableServerComponent } from '@tanstack/react-start/rsc'; +import type { JSX } from 'react/jsx-runtime'; +import * as z from 'zod/v4'; + +import type { TableOfContents } from '#/modules/markdown/markdown.types'; +import type { + seriesFrontmatterSchema, + seriesPostFrontmatterSchema, +} from '#/modules/series/series.schema'; + +export type SeriesFrontmatter = z.infer; +export type SeriesPostFrontmatter = z.infer; + +export interface SeriesItem extends SeriesFrontmatter { + slug: string; + lastModification: string; +} + +export interface SeriesContent extends SeriesItem { + posts: SeriesPostItem[]; + toc: TableOfContents; + mdx: RenderableServerComponent; +} + +export interface SeriesPostItem extends SeriesPostFrontmatter { + slug: string; + series: { + slug: SeriesItem['slug']; + }; + order: number; + lastModification: string; +} + +export interface SeriesPostContent extends SeriesPostItem { + toc: TableOfContents; + mdx: RenderableServerComponent; +} diff --git a/src/modules/thumbnail/components/thumbnail-figure.tsx b/src/modules/thumbnail/components/thumbnail-figure.tsx new file mode 100644 index 0000000..ec0fba7 --- /dev/null +++ b/src/modules/thumbnail/components/thumbnail-figure.tsx @@ -0,0 +1,46 @@ +import type { Thumbnail } from '#/modules/thumbnail/thumbnail.schema'; +import { cn } from '#/ui/utils'; + +const IMAGE_CLASS = 'aspect-video w-full rounded-lg object-cover'; + +/** + * Renders a detail-page thumbnail. With a caption it's a `figure` + the + * `figcaption`, whose HTML `parseThumbnail` rendered from Markdown on the + * server and this client component injects; without one it's a bare `img` — an + * empty `figure` would be meaningless markup. Renders `null` for a missing + * thumbnail. `className` styles the outer element, so outer spacing (the caller's + * margin) lives with the layout rather than being baked in here. Caption links + * are underlined here since this figure lives outside the `.prose` body. + */ +export function ThumbnailFigure({ + thumbnail, + className, +}: { + thumbnail: Thumbnail; + className?: string; +}) { + if (thumbnail == null) return null; + + if (!thumbnail.caption) { + return ( + {thumbnail.alt + ); + } + + return ( +

+ {thumbnail.alt +
+
+ ); +} diff --git a/src/modules/thumbnail/thumbnail.schema.ts b/src/modules/thumbnail/thumbnail.schema.ts new file mode 100644 index 0000000..3638ebe --- /dev/null +++ b/src/modules/thumbnail/thumbnail.schema.ts @@ -0,0 +1,24 @@ +import * as z from 'zod/v4'; + +/** + * A content thumbnail. `src` is a remote URL, or a path relative to the + * document (`./cover.jpg`) that the build resolves to a fingerprinted asset. + * `alt` is the image's alternative text (omit for a decorative image), and + * `caption` is optional inline Markdown rendered beneath the image (e.g. an + * image-credit link). Both `alt` and `caption` are optional, so authors may set + * only `src`. The whole thumbnail is optional (`null`). + */ +export const thumbnailSchema = z + .object({ + src: z.union([z.url(), z.string().regex(/^\.\.?\//)]), + alt: z.string().optional(), + caption: z.string().optional(), + }) + .nullable(); + +/** + * A thumbnail. As authored, `caption` is a raw inline-Markdown string; once + * passed through `parseThumbnail` it holds the rendered HTML (or is + * absent). `null` when the document has no thumbnail. + */ +export type Thumbnail = z.infer; diff --git a/src/modules/thumbnail/thumbnail.utils.ts b/src/modules/thumbnail/thumbnail.utils.ts new file mode 100644 index 0000000..fa03e07 --- /dev/null +++ b/src/modules/thumbnail/thumbnail.utils.ts @@ -0,0 +1,59 @@ +import type { ElementContent, Root } from 'hast'; +import { raw } from 'hast-util-raw'; +import { toHtml } from 'hast-util-to-html'; +import { fromMarkdown } from 'mdast-util-from-markdown'; +import { toHast } from 'mdast-util-to-hast'; + +import type { Thumbnail } from '#/modules/thumbnail/thumbnail.schema'; + +/** + * Parses a caption as inline Markdown that may also contain raw HTML, returning + * hast children ready to drop into a `figcaption`. Both `[text](url)` and + * `
` render — the latter is what image credit tools (e.g. Unsplash) + * hand you. Anchors flow on through the MDX component map, so caption links pick + * up the app's `` behavior. This renders author-supplied HTML, but at + * the same trust level MDX already grants post bodies, so it adds no exposure. + * + * Shared by the build-time image pass (`rehypeImage`) and the runtime + * thumbnail caption resolver (`parseThumbnail`) so both parse identically. + */ +export function parseCaption(caption: string): ElementContent[] { + // `mdast-util-to-hast` and `hast-util-raw` can each resolve their own copy of + // @types/hast under pnpm's isolated store, so their `Nodes` types aren't + // nominally identical even when structurally equal — passing `toHast`'s result + // straight into `raw` errors under that resolution. Bridge the boundary + // through the project's own `hast` types; the runtime values are ordinary + // hast nodes, so only the compile-time identity is being reconciled. + const hast = toHast(fromMarkdown(caption), { allowDangerousHtml: true }); + const tree = raw(hast as never) as unknown as Root; + if (tree.type !== 'root') return []; + + // Inline Markdown comes back wrapped in a single paragraph; unwrap it so the + // figcaption holds inline nodes rather than a nested block element. + const [first] = tree.children; + const nodes = + tree.children.length === 1 && first?.type === 'element' && first.tagName === 'p' + ? first.children + : tree.children; + return nodes.filter((node): node is ElementContent => node.type !== 'doctype'); +} + +/** + * Parses an authored `Thumbnail` for a detail page: `src`/`alt` pass + * through, and `caption` is rendered from inline Markdown to a static HTML + * string — or dropped when the author gave no caption. Runs on the server, so + * the Markdown parser never reaches the client bundle; the client only injects + * the finished HTML (`ThumbnailFigure`). Returns `null` for a missing + * thumbnail so callers can guard on the whole object. + */ +export function parseThumbnail(thumbnail: Thumbnail): Thumbnail { + if (thumbnail == null) return null; + // `parseCaption` returns hast typed against the project's `@types/hast`, while + // `hast-util-to-html` resolves its own copy under pnpm's isolated store; the + // runtime value is an ordinary hast root, so only compile-time identity is + // being bridged here (same seam as `parseCaption` itself). + const caption = thumbnail.caption?.trim() + ? toHtml({ type: 'root', children: parseCaption(thumbnail.caption) } as never) + : undefined; + return { ...thumbnail, caption }; +} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index aea6306..5b58992 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -10,33 +10,102 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as IndexRouteImport } from './routes/index' +import { Route as SeriesIndexRouteImport } from './routes/series/index' +import { Route as PostsIndexRouteImport } from './routes/posts/index' +import { Route as PostsSlugRouteImport } from './routes/posts/$slug' +import { Route as SeriesSlugIndexRouteImport } from './routes/series/$slug/index' +import { Route as SeriesSlugPostSlugRouteImport } from './routes/series/$slug/$postSlug' const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', getParentRoute: () => rootRouteImport, } as any) +const SeriesIndexRoute = SeriesIndexRouteImport.update({ + id: '/series/', + path: '/series/', + getParentRoute: () => rootRouteImport, +} as any) +const PostsIndexRoute = PostsIndexRouteImport.update({ + id: '/posts/', + path: '/posts/', + getParentRoute: () => rootRouteImport, +} as any) +const PostsSlugRoute = PostsSlugRouteImport.update({ + id: '/posts/$slug', + path: '/posts/$slug', + getParentRoute: () => rootRouteImport, +} as any) +const SeriesSlugIndexRoute = SeriesSlugIndexRouteImport.update({ + id: '/series/$slug/', + path: '/series/$slug/', + getParentRoute: () => rootRouteImport, +} as any) +const SeriesSlugPostSlugRoute = SeriesSlugPostSlugRouteImport.update({ + id: '/series/$slug/$postSlug', + path: '/series/$slug/$postSlug', + getParentRoute: () => rootRouteImport, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute + '/posts/$slug': typeof PostsSlugRoute + '/posts/': typeof PostsIndexRoute + '/series/': typeof SeriesIndexRoute + '/series/$slug/$postSlug': typeof SeriesSlugPostSlugRoute + '/series/$slug/': typeof SeriesSlugIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute + '/posts/$slug': typeof PostsSlugRoute + '/posts': typeof PostsIndexRoute + '/series': typeof SeriesIndexRoute + '/series/$slug/$postSlug': typeof SeriesSlugPostSlugRoute + '/series/$slug': typeof SeriesSlugIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute + '/posts/$slug': typeof PostsSlugRoute + '/posts/': typeof PostsIndexRoute + '/series/': typeof SeriesIndexRoute + '/series/$slug/$postSlug': typeof SeriesSlugPostSlugRoute + '/series/$slug/': typeof SeriesSlugIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' + fullPaths: + | '/' + | '/posts/$slug' + | '/posts/' + | '/series/' + | '/series/$slug/$postSlug' + | '/series/$slug/' fileRoutesByTo: FileRoutesByTo - to: '/' - id: '__root__' | '/' + to: + | '/' + | '/posts/$slug' + | '/posts' + | '/series' + | '/series/$slug/$postSlug' + | '/series/$slug' + id: + | '__root__' + | '/' + | '/posts/$slug' + | '/posts/' + | '/series/' + | '/series/$slug/$postSlug' + | '/series/$slug/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + PostsSlugRoute: typeof PostsSlugRoute + PostsIndexRoute: typeof PostsIndexRoute + SeriesIndexRoute: typeof SeriesIndexRoute + SeriesSlugPostSlugRoute: typeof SeriesSlugPostSlugRoute + SeriesSlugIndexRoute: typeof SeriesSlugIndexRoute } declare module '@tanstack/react-router' { @@ -48,11 +117,51 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } + '/series/': { + id: '/series/' + path: '/series' + fullPath: '/series/' + preLoaderRoute: typeof SeriesIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/posts/': { + id: '/posts/' + path: '/posts' + fullPath: '/posts/' + preLoaderRoute: typeof PostsIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/posts/$slug': { + id: '/posts/$slug' + path: '/posts/$slug' + fullPath: '/posts/$slug' + preLoaderRoute: typeof PostsSlugRouteImport + parentRoute: typeof rootRouteImport + } + '/series/$slug/': { + id: '/series/$slug/' + path: '/series/$slug' + fullPath: '/series/$slug/' + preLoaderRoute: typeof SeriesSlugIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/series/$slug/$postSlug': { + id: '/series/$slug/$postSlug' + path: '/series/$slug/$postSlug' + fullPath: '/series/$slug/$postSlug' + preLoaderRoute: typeof SeriesSlugPostSlugRouteImport + parentRoute: typeof rootRouteImport + } } } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + PostsSlugRoute: PostsSlugRoute, + PostsIndexRoute: PostsIndexRoute, + SeriesIndexRoute: SeriesIndexRoute, + SeriesSlugPostSlugRoute: SeriesSlugPostSlugRoute, + SeriesSlugIndexRoute: SeriesSlugIndexRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 7a4d517..50fb910 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,5 +1,8 @@ +import geistMonoFont from '@fontsource-variable/geist-mono/files/geist-mono-latin-wght-normal.woff2?url'; +import geistSansFont from '@fontsource-variable/geist/files/geist-latin-wght-normal.woff2?url'; import { TanStackDevtools } from '@tanstack/react-devtools'; import { createRootRoute, HeadContent, Scripts } from '@tanstack/react-router'; +import { preload } from 'react-dom'; import { tanstackRouterDevtools } from '#/devtools/router-devtools'; import { getLocale, getTextDirection } from '#/lib/i18n/paraglide/runtime'; @@ -8,6 +11,7 @@ import { TooltipProvider } from '#/ui/components/core/tooltip'; import { ThemeProvider } from '#/ui/theme'; import appStylesheet from '#/ui/styles/app.css?url'; +import fontsStylesheet from '#/ui/styles/fonts.css?url'; export const Route = createRootRoute({ head: () => ({ @@ -16,7 +20,10 @@ export const Route = createRootRoute({ { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { title: '@devsantara/website' }, ], - links: [{ rel: 'stylesheet', href: appStylesheet }], + links: [ + { rel: 'stylesheet', href: fontsStylesheet }, + { rel: 'stylesheet', href: appStylesheet }, + ], }), shellComponent: RootDocument, }); @@ -25,6 +32,9 @@ function RootDocument({ children }: { children: React.ReactNode }) { const locale = getLocale(); const textDirection = getTextDirection(); + preload(geistSansFont, { as: 'font', type: 'font/woff2' }); + preload(geistMonoFont, { as: 'font', type: 'font/woff2' }); + return ( diff --git a/src/routes/index.tsx b/src/routes/index.tsx index a556c12..a240174 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,4 +1,4 @@ -import { createFileRoute } from '@tanstack/react-router'; +import { createFileRoute, Link } from '@tanstack/react-router'; export const Route = createFileRoute('/')({ component: HomePage, @@ -6,8 +6,16 @@ export const Route = createFileRoute('/')({ function HomePage() { return ( -
+

@devsantara/website

+
); } diff --git a/src/routes/posts/$slug.tsx b/src/routes/posts/$slug.tsx new file mode 100644 index 0000000..7d6c18e --- /dev/null +++ b/src/routes/posts/$slug.tsx @@ -0,0 +1,41 @@ +import { createFileRoute } from '@tanstack/react-router'; + +import { getPostBySlugFn } from '#/modules/post/post.fn'; +import { ThumbnailFigure } from '#/modules/thumbnail/components/thumbnail-figure'; +import { Badge } from '#/ui/components/core/badge'; +import { Separator } from '#/ui/components/core/separator'; + +export const Route = createFileRoute('/posts/$slug')({ + loader: ({ params: { slug } }) => getPostBySlugFn({ data: { slug } }), + component: PostPage, +}); + +function PostPage() { + const post = Route.useLoaderData(); + + return ( +
+
+ {post.thumbnail && } +
+

{post.title}

+

+ By {post.author} · +

+

{post.description}

+ {post.tags.length > 0 && ( +
    + {post.tags.map((tag) => ( +
  • + {tag} +
  • + ))} +
+ )} +
+ +
{post.mdx}
+
+
+ ); +} diff --git a/src/routes/posts/index.tsx b/src/routes/posts/index.tsx new file mode 100644 index 0000000..e3f94eb --- /dev/null +++ b/src/routes/posts/index.tsx @@ -0,0 +1,36 @@ +import { createFileRoute, Link } from '@tanstack/react-router'; + +import { getAllPostsFn } from '#/modules/post/post.fn'; + +export const Route = createFileRoute('/posts/')({ + component: PostsPage, + loader: () => getAllPostsFn(), +}); + +function PostsPage() { + const posts = Route.useLoaderData(); + + return ( +
+

Posts

+
    + {posts.map((post) => ( +
  • +
    +

    + + {post.title} + +

    +

    {post.description}

    +
    +
  • + ))} +
+
+ ); +} diff --git a/src/routes/series/$slug/$postSlug.tsx b/src/routes/series/$slug/$postSlug.tsx new file mode 100644 index 0000000..12cf8a3 --- /dev/null +++ b/src/routes/series/$slug/$postSlug.tsx @@ -0,0 +1,49 @@ +import { createFileRoute, Link } from '@tanstack/react-router'; + +import { getSeriesPostFn } from '#/modules/series/series.fn'; +import { ThumbnailFigure } from '#/modules/thumbnail/components/thumbnail-figure'; +import { Badge } from '#/ui/components/core/badge'; +import { Separator } from '#/ui/components/core/separator'; + +export const Route = createFileRoute('/series/$slug/$postSlug')({ + loader: ({ params: { slug, postSlug } }) => getSeriesPostFn({ data: { slug, postSlug } }), + component: SeriesPostPage, +}); + +function SeriesPostPage() { + const post = Route.useLoaderData(); + const { slug } = Route.useParams(); + + return ( +
+
+
+ + ← Back to series + + {post.thumbnail && } +

{post.title}

+

+ By {post.author} · +

+

{post.description}

+ {post.tags.length > 0 && ( +
    + {post.tags.map((tag) => ( +
  • + {tag} +
  • + ))} +
+ )} +
+ +
{post.mdx}
+
+
+ ); +} diff --git a/src/routes/series/$slug/index.tsx b/src/routes/series/$slug/index.tsx new file mode 100644 index 0000000..10a9dfb --- /dev/null +++ b/src/routes/series/$slug/index.tsx @@ -0,0 +1,54 @@ +import { createFileRoute, Link } from '@tanstack/react-router'; + +import { getSeriesBySlugFn } from '#/modules/series/series.fn'; +import { ThumbnailFigure } from '#/modules/thumbnail/components/thumbnail-figure'; +import { Separator } from '#/ui/components/core/separator'; + +export const Route = createFileRoute('/series/$slug/')({ + loader: ({ params: { slug } }) => getSeriesBySlugFn({ data: { slug } }), + component: SeriesDetailPage, +}); + +function SeriesDetailPage() { + const series = Route.useLoaderData(); + + return ( +
+
+ {series.thumbnail && } +
+

{series.title}

+

{series.description}

+
+ +
{series.mdx}
+
+ +
+

In this series

+ {series.posts.length > 0 ? ( +
    + {series.posts.map((post) => ( +
  1. +
    +

    + + {post.title} + +

    +

    {post.description}

    +
    +
  2. + ))} +
+ ) : ( +

No posts in this series yet.

+ )} +
+
+ ); +} diff --git a/src/routes/series/index.tsx b/src/routes/series/index.tsx new file mode 100644 index 0000000..22b1fd4 --- /dev/null +++ b/src/routes/series/index.tsx @@ -0,0 +1,36 @@ +import { createFileRoute, Link } from '@tanstack/react-router'; + +import { getAllSeriesFn } from '#/modules/series/series.fn'; + +export const Route = createFileRoute('/series/')({ + component: SeriesPage, + loader: () => getAllSeriesFn(), +}); + +function SeriesPage() { + const series = Route.useLoaderData(); + + return ( +
+

Series

+
    + {series.map((item) => ( +
  • +
    +

    + + {item.title} + +

    +

    {item.description}

    +
    +
  • + ))} +
+
+ ); +} diff --git a/src/ui/styles/app.css b/src/ui/styles/app.css index 2afcdb4..a135d9f 100644 --- a/src/ui/styles/app.css +++ b/src/ui/styles/app.css @@ -1,8 +1,9 @@ @import 'tailwindcss'; -@import 'tw-animate-css'; @import 'shadcn/tailwind.css'; -@import '@fontsource-variable/geist'; -@import '@fontsource-variable/geist-mono'; +@import 'tw-animate-css'; +@import './markdown.css'; + +@plugin "@tailwindcss/typography"; @custom-variant dark (&:is(.dark *)); @@ -122,6 +123,7 @@ @layer base { * { @apply border-border outline-ring/50; + font-variant-ligatures: none; } body { @apply bg-background text-foreground; diff --git a/src/ui/styles/fonts.css b/src/ui/styles/fonts.css new file mode 100644 index 0000000..f19db43 --- /dev/null +++ b/src/ui/styles/fonts.css @@ -0,0 +1,2 @@ +@import '@fontsource-variable/geist'; +@import '@fontsource-variable/geist-mono'; diff --git a/src/ui/styles/markdown.css b/src/ui/styles/markdown.css new file mode 100644 index 0000000..5490c51 --- /dev/null +++ b/src/ui/styles/markdown.css @@ -0,0 +1,392 @@ +/* + * Code blocks — rehype-pretty-code (Shiki) output. + * + * Highlighting is baked into the HTML at MDX compile time as dual-theme CSS + * variables; these rules do the light/dark swap (keyed to `.dark`, matching + * app.css) and all block chrome. @tailwindcss/typography wraps its rules in + * `:where()`, so plain selectors here always win inside `.prose`. + */ + +[data-rehype-pretty-code-figure] { + position: relative; + + --code-add: oklch(0.627 0.194 149.214); + --code-remove: oklch(0.577 0.245 27.325); + --code-warning: oklch(0.769 0.188 70.08); +} + +/* Dual themes: rehype-pretty-code ships BOTH palettes as custom properties + (--shiki-light/--shiki-dark) and no direct color, so each mode must opt in. */ +code[data-theme] span { + color: var(--shiki-light); + font-style: var(--shiki-light-font-style, inherit); + font-weight: var(--shiki-light-font-weight, inherit); + text-decoration: var(--shiki-light-text-decoration, inherit); +} + +.dark code[data-theme] span { + color: var(--shiki-dark); + font-style: var(--shiki-dark-font-style, inherit); + font-weight: var(--shiki-dark-font-weight, inherit); + text-decoration: var(--shiki-dark-text-decoration, inherit); +} + +[data-rehype-pretty-code-figure] pre { + overflow-x: auto; + color: var(--foreground); + padding-block: 0.875rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + background-color: color-mix(in oklab, var(--color-muted) 50%, transparent); +} + +[data-rehype-pretty-code-figure] pre code { + display: grid; + min-width: 100%; + font-family: var(--font-mono); + font-size: 0.8125rem; + line-height: 1.7; +} + +/* Thin horizontal scrollbar shared by overflowing code blocks and tables on + desktop (fine) pointers; touch devices keep their native overlay scrollbars + (see `@media (hover: none)`). */ +@media (hover: hover) { + [data-rehype-pretty-code-figure] pre, + [data-table-wrapper] { + scrollbar-width: thin; + scrollbar-color: color-mix(in oklab, var(--color-foreground) 20%, transparent) transparent; + } + + [data-rehype-pretty-code-figure] pre::-webkit-scrollbar, + [data-table-wrapper]::-webkit-scrollbar { + height: 0.375rem; + } + + [data-rehype-pretty-code-figure] pre::-webkit-scrollbar-track, + [data-table-wrapper]::-webkit-scrollbar-track { + background: transparent; + } + + [data-rehype-pretty-code-figure] pre::-webkit-scrollbar-thumb, + [data-table-wrapper]::-webkit-scrollbar-thumb { + border-radius: var(--radius-lg); + background-color: color-mix(in oklab, var(--color-foreground) 20%, transparent); + } + + [data-rehype-pretty-code-figure] pre::-webkit-scrollbar-thumb:hover, + [data-table-wrapper]::-webkit-scrollbar-thumb:hover { + background-color: color-mix(in oklab, var(--color-foreground) 35%, transparent); + } +} + +[data-rehype-pretty-code-figure] code [data-line] { + padding-inline: 0.05rem; + border-inline-start: 2px solid transparent; +} + +/* Title header (fence meta: title="..."). + * A block box (not flex) so text-overflow can truncate a long filename with an + * ellipsis on narrow screens; line-height centers the single line, and the + * right padding keeps the ellipsis clear of the copy button. */ +[data-rehype-pretty-code-title] { + height: 2.5rem; + padding-inline: 1rem 2.75rem; + line-height: 2.375rem; + border: 1px solid var(--color-border); + border-bottom: none; + border-radius: var(--radius-lg) var(--radius-lg) 0 0; + background-color: var(--color-muted); + color: var(--color-muted-foreground); + font-family: var(--font-mono); + font-size: 0.75rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +[data-rehype-pretty-code-title] ~ pre { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +/* Copy button: floats on untitled blocks, sits in the header of titled ones. */ +[data-rehype-pretty-code-figure]:not(:has([data-rehype-pretty-code-title])) [data-copy-button] { + opacity: 0; + transition: opacity 150ms; +} + +[data-rehype-pretty-code-figure]:not(:has([data-rehype-pretty-code-title])):hover + [data-copy-button], +[data-rehype-pretty-code-figure] [data-copy-button]:focus-visible { + opacity: 1; +} + +@media (hover: none) { + [data-rehype-pretty-code-figure] [data-copy-button] { + opacity: 1; + } +} + +/* Line highlight (fence meta: {1,3-5}) */ +[data-rehype-pretty-code-figure] code [data-line][data-highlighted-line] { + background-color: color-mix(in oklab, var(--color-foreground) 7%, transparent); + border-inline-start-color: var(--color-foreground); +} + +/* Word highlight (fence meta: /word/) */ +[data-rehype-pretty-code-figure] code mark[data-highlighted-chars] { + border-radius: var(--radius-sm); + background-color: color-mix(in oklab, var(--color-foreground) 10%, transparent); + box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-foreground) 10%, transparent); + color: inherit; +} + +/* Line numbers (fence meta: showLineNumbers) */ +[data-rehype-pretty-code-figure] code[data-line-numbers] { + counter-reset: line; +} + +[data-rehype-pretty-code-figure] code[data-line-numbers] > [data-line]::before { + counter-increment: line; + content: counter(line); + display: inline-block; + width: 1rem; + margin-inline-end: 1.25rem; + color: var(--color-muted-foreground); + text-align: right; + opacity: 0.6; +} + +[data-rehype-pretty-code-figure] code[data-line-numbers-max-digits='2'] > [data-line]::before { + width: 1.5rem; +} + +[data-rehype-pretty-code-figure] code[data-line-numbers-max-digits='3'] > [data-line]::before { + width: 2rem; +} + +/* Diff ([!code ++] / [!code --]) */ +[data-rehype-pretty-code-figure] code [data-line].diff.add { + background-color: color-mix(in oklab, var(--code-add) 14%, transparent); + border-inline-start-color: var(--code-add); +} + +[data-rehype-pretty-code-figure] code [data-line].diff.remove { + background-color: color-mix(in oklab, var(--code-remove) 12%, transparent); + border-inline-start-color: var(--code-remove); + opacity: 0.75; +} + +/* Error/warning ([!code error] / [!code warning]) */ +[data-rehype-pretty-code-figure] code [data-line].highlighted.error { + background-color: color-mix(in oklab, var(--code-remove) 14%, transparent); + border-inline-start-color: var(--code-remove); +} + +[data-rehype-pretty-code-figure] code [data-line].highlighted.warning { + background-color: color-mix(in oklab, var(--code-warning) 16%, transparent); + border-inline-start-color: var(--code-warning); +} + +/* Focus ([!code focus]): dim the rest, reveal on hover. */ +[data-rehype-pretty-code-figure] pre.has-focused [data-line]:not(.focused) { + opacity: 0.4; + filter: blur(0.095rem); + transition: + opacity 300ms, + filter 300ms; +} + +[data-rehype-pretty-code-figure] pre.has-focused:hover [data-line]:not(.focused) { + opacity: 1; + filter: none; +} + +/* Indent guides (transformerRenderIndentGuides wraps indents in .indent) */ +[data-rehype-pretty-code-figure] code .indent { + box-shadow: inset 1px 0 0 color-mix(in oklab, var(--color-foreground) 8%, transparent); +} + +/* Code tabs (:::tabs) */ +.prose [data-tabs] { + margin-block: 1.7142857em; +} + +/* Each `::tab` section is a wrapper div holding arbitrary content. */ +.prose [data-tabs] [data-slot='tabs-content'] > div > :first-child { + margin-top: 0.75em; +} + +.prose [data-tabs] [data-slot='tabs-content'] > div > :last-child { + margin-bottom: 0; +} + +/* + * Local images (rehype-image) and their optional captions. + * + * `rehypeImage` stamps intrinsic width/height for zero layout shift; these + * rules keep the rendered box responsive (`max-width`/`height:auto`) so those + * attributes drive aspect ratio without freezing the display size. Captioned + * images are wrapped in `figure[data-image-figure]` — distinct from the code + * block figure ([data-rehype-pretty-code-figure]) so the two never collide. + */ +.prose img { + max-width: 100%; + height: auto; + border-radius: var(--radius-lg); + border: 1px solid var(--color-border); +} + +.prose figure[data-image-figure] { + margin-block: 1.7142857em; +} + +.prose figure[data-image-figure] img { + margin-block: 0; +} + +.prose figure[data-image-figure] figcaption { + margin-top: 0.75em; + text-align: center; + font-size: 0.875em; + color: var(--color-muted-foreground); +} + +/* + * Heading permalinks (rehype-slug + rehype-autolink-headings). + * + * Every heading carries an `id` and an appended `.heading-anchor` link. The `#` + * marker stays hidden until its heading is hovered (or the link itself is + * focused), so it never competes with the heading text; touch devices, which + * can't hover, keep it visible — mirroring the copy button. `scroll-margin-top` + * leaves a gap above a heading the browser jumps to via its fragment so it + * doesn't land flush against the viewport edge. + */ +.prose :is(h1, h2, h3, h4, h5, h6) { + scroll-margin-top: 2rem; +} + +.prose .heading-anchor { + margin-left: 0.35em; + color: var(--color-muted-foreground); + font-weight: 400; + text-decoration: none; + opacity: 0; + transition: opacity 150ms; +} + +.prose :is(h1, h2, h3, h4, h5, h6):hover .heading-anchor, +.prose .heading-anchor:focus-visible { + opacity: 1; +} + +@media (hover: none) { + .prose .heading-anchor { + opacity: 1; + } +} + +/* + * Footnote jump targets (remark-gfm). Mirror the heading `scroll-margin-top` so + * following a footnote reference (to its definition) or a back-reference arrow + * (`data-footnote-ref`, back to the reference) lands with a gap above it rather + * than flush against the viewport edge. + */ +.prose [data-footnotes] li, +.prose [data-footnote-ref] { + scroll-margin-top: 2rem; +} + +/* + * Tables (GFM), wrapped in `[data-table-wrapper]` by the `Table` override. + * + * The wrapper is the horizontal scroll boundary and carries the block margin + * typography would otherwise put on the table. The table keeps `width: 100%` + * (filling wide columns) but a `min-width` floors it, so on a viewport narrower + * than that floor it overflows the wrapper and scrolls instead of crushing + * every column down to an unreadable width. + */ +.prose [data-table-wrapper] { + overflow-x: auto; + margin-block: 2em; +} + +.prose [data-table-wrapper] > table { + margin-block: 0; + min-width: 32rem; +} + +/* + * Alerts / callouts (remark-alert) — GitHub-style blockquote alerts. + * + * `remark-alert` rewrites a `> [!NOTE]`-style blockquote into a ``, + * which renders a plain `div[data-callout]` — so @tailwindcss/typography's + * blockquote rules never touch it. Following GitHub, there's no filled box: just + * an accent left border and an accent title. Each type sets its own + * `--callout-accent`; the `.dark` overrides lift it for contrast on the dark + * surface. + */ +.prose [data-callout] { + margin-block: 1.7142857em; + padding-block: 0.25rem; + padding-inline-start: 1em; + border-inline-start: 3px solid var(--callout-accent); +} + +.prose [data-callout-title] { + display: flex; + align-items: center; + gap: 0.5em; + margin-block: 0 0.5em; + color: var(--callout-accent); + font-weight: 600; +} + +.prose [data-callout-title] svg { + width: 1.15em; + height: 1.15em; + flex-shrink: 0; +} + +/* Keep the body flush with the padding box: no gap above the first block after + the title, none below the last. */ +.prose [data-callout] > [data-callout-title] + * { + margin-top: 0; +} + +.prose [data-callout] > :last-child { + margin-bottom: 0; +} + +.prose [data-callout='note'] { + --callout-accent: oklch(0.55 0.2 255); +} +.prose [data-callout='tip'] { + --callout-accent: oklch(0.58 0.17 149.214); +} +.prose [data-callout='important'] { + --callout-accent: oklch(0.55 0.22 300); +} +.prose [data-callout='warning'] { + --callout-accent: oklch(0.62 0.14 75); +} +.prose [data-callout='caution'] { + --callout-accent: oklch(0.577 0.245 27.325); +} + +.dark .prose [data-callout='note'] { + --callout-accent: oklch(0.72 0.16 255); +} +.dark .prose [data-callout='tip'] { + --callout-accent: oklch(0.72 0.17 149.214); +} +.dark .prose [data-callout='important'] { + --callout-accent: oklch(0.75 0.16 300); +} +.dark .prose [data-callout='warning'] { + --callout-accent: oklch(0.8 0.14 75); +} +.dark .prose [data-callout='caution'] { + --callout-accent: oklch(0.704 0.191 22.216); +} diff --git a/tsconfig.json b/tsconfig.json index ec9cbb9..c54415a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,8 @@ "jsx": "react-jsx", "module": "ESNext", "paths": { - "#/*": ["./src/*"] + "#/*": ["./src/*"], + "content-collections": ["./.content-collections/generated"] }, "lib": ["ES2022", "DOM", "DOM.Iterable"], "types": ["vite/client", "@cloudflare/workers-types"], diff --git a/vite.config.ts b/vite.config.ts index 32cf7de..e658da0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,13 +1,16 @@ +import contentCollections from '@content-collections/vite'; import { paraglideVitePlugin } from '@inlang/paraglide-js'; import babel from '@rolldown/plugin-babel'; import tailwindcss from '@tailwindcss/vite'; import { devtools as tanstackDevtools } from '@tanstack/devtools-vite'; import { tanstackStart } from '@tanstack/react-start/plugin/vite'; import viteReact, { reactCompilerPreset } from '@vitejs/plugin-react'; +import rsc from '@vitejs/plugin-rsc'; import alchemy from 'alchemy/cloudflare/tanstack-start'; import { defineConfig, lazyPlugins } from 'vite-plus'; -import { translatedPathnames, translatedPrerender } from '#/lib/i18n/config'; +import { createLocalePrerenderPages, createLocaleUrlPatterns } from '#/lib/i18n/utils'; +import { viteMdx } from '#/modules/markdown/markdown.vite'; const config = defineConfig({ resolve: { tsconfigPaths: true }, @@ -40,15 +43,22 @@ const config = defineConfig({ rules: { 'vite-plus/prefer-vite-plus-imports': 'error' }, }, plugins: lazyPlugins(() => [ + viteMdx(), + // `environment: 'ssr'` builds the collection only in the SSR/RSC graph, so + // the compiled MDX is never emitted into the client bundle. + contentCollections({ environment: 'ssr' }), tanstackDevtools(), - alchemy(), tailwindcss(), tanstackStart({ srcDirectory: 'src', start: { entry: 'entry.start.ts' }, server: { entry: 'entry.server.ts' }, client: { entry: 'entry.client.tsx' }, - pages: translatedPrerender, + prerender: { autoSubfolderIndex: false }, + pages: createLocalePrerenderPages(['/', '/posts', '/series']), + // RSC lets us stream server-rendered MDX to the client without shipping + // the compiled MDX (or mdx-bundler's `new Function`, blocked on Workers). + rsc: { enabled: true }, router: { entry: 'entry.router.ts', codeSplittingOptions: { @@ -58,7 +68,8 @@ const config = defineConfig({ }, }, }), - viteReact(), + rsc(), + viteReact({ include: /\.(jsx|js|mdx|md|tsx|ts)$/ }), babel({ presets: [reactCompilerPreset()] }), paraglideVitePlugin({ project: './project.inlang', @@ -66,16 +77,18 @@ const config = defineConfig({ cookieName: 'LOCALE', outputStructure: 'message-modules', strategy: ['url', 'cookie', 'preferredLanguage', 'baseLocale'], - // DisableAsyncLocalStorage should ONLY be used in serverless environments like Cloudflare Workers. - disableAsyncLocalStorage: true, routeStrategies: [ { match: '/health', exclude: true }, { match: '/api/:path(.*)?', exclude: true }, { match: '/rpc/:path(.*)?', strategy: ['cookie', 'baseLocale'] }, { match: '/assets/:path(.*)?', exclude: true }, ], - urlPatterns: translatedPathnames, + urlPatterns: createLocaleUrlPatterns(), }), + // Cloudflare (via Alchemy) runs last so it can pick up the environments + // configured by TanStack Start + RSC. `childEnvironments: ['rsc']` registers + // the RSC graph as a child of the `ssr` Worker environment. + alchemy({ viteEnvironment: { name: 'ssr', childEnvironments: ['rsc'] } }), ]), }); export default config;