diff --git a/frontend/components/MarkdownBody.tsx b/frontend/components/MarkdownBody.tsx
index d00d752..5d18fb0 100644
--- a/frontend/components/MarkdownBody.tsx
+++ b/frontend/components/MarkdownBody.tsx
@@ -1,4 +1,5 @@
-import ReactMarkdown, { type Components } from "react-markdown";
+import type { Components } from "react-markdown";
+import { renderMarkdownToReact } from "@/components/markdown/renderMarkdown";
type MarkdownBodyProps = {
content: string;
@@ -6,15 +7,15 @@ type MarkdownBodyProps = {
components?: Components;
};
-/** 站点统一 Markdown 渲染:详情页 / 发帖预览共用 prose-content */
-export default function MarkdownBody({
+/**
+ * 站点统一 Markdown 渲染(服务端):GFM + Shiki 双主题、图箱、代码块复制/折叠。
+ * 客户端预览请用 MarkdownBodyClient。
+ */
+export default async function MarkdownBody({
content,
className = "prose-content",
components,
}: MarkdownBodyProps) {
- return (
-
- {content || ""}
-
- );
+ const tree = await renderMarkdownToReact(content, components);
+ return
{tree}
;
}
diff --git a/frontend/components/MarkdownBodyClient.tsx b/frontend/components/MarkdownBodyClient.tsx
new file mode 100644
index 0000000..b75d47b
--- /dev/null
+++ b/frontend/components/MarkdownBodyClient.tsx
@@ -0,0 +1,32 @@
+"use client";
+
+import ReactMarkdown, { type Components } from "react-markdown";
+import remarkGfm from "remark-gfm";
+import { mergeMarkdownComponents } from "@/components/markdown/mergeComponents";
+
+type MarkdownBodyClientProps = {
+ content: string;
+ className?: string;
+ components?: Components;
+};
+
+/**
+ * 客户端 Markdown(发帖预览 / 评论):GFM + 图箱 / 代码折叠复制。
+ * 语法高亮在 MdCodeBlock 内按需用 Shiki 补齐。
+ */
+export default function MarkdownBodyClient({
+ content,
+ className = "prose-content",
+ components,
+}: MarkdownBodyClientProps) {
+ return (
+
+
+ {content || ""}
+
+
+ );
+}
diff --git a/frontend/components/MarkdownEditor.tsx b/frontend/components/MarkdownEditor.tsx
index 2c6ef27..39ecc70 100644
--- a/frontend/components/MarkdownEditor.tsx
+++ b/frontend/components/MarkdownEditor.tsx
@@ -17,7 +17,7 @@ import {
} from "lucide-react";
import { apiUploadPostImage } from "@/lib/api";
import { toast } from "@/lib/toast";
-import MarkdownBody from "@/components/MarkdownBody";
+import MarkdownBodyClient from "@/components/MarkdownBodyClient";
type MarkdownEditorProps = {
value: string;
@@ -304,7 +304,7 @@ export default function MarkdownEditor({
) : (
{value.trim() ? (
-
+
) : (
暂无内容可预览
)}
diff --git a/frontend/components/markdown/ImageLightbox.tsx b/frontend/components/markdown/ImageLightbox.tsx
new file mode 100644
index 0000000..340b344
--- /dev/null
+++ b/frontend/components/markdown/ImageLightbox.tsx
@@ -0,0 +1,122 @@
+"use client";
+
+import { useEffect, useCallback } from "react";
+import { createPortal } from "react-dom";
+import { X, ChevronLeft, ChevronRight } from "lucide-react";
+
+export type LightboxItem = {
+ src: string;
+ alt?: string;
+};
+
+type ImageLightboxProps = {
+ items: LightboxItem[];
+ index: number;
+ onClose: () => void;
+ onIndexChange: (index: number) => void;
+};
+
+/** 全屏图箱:Esc 关闭,多图时左右切换 */
+export default function ImageLightbox({
+ items,
+ index,
+ onClose,
+ onIndexChange,
+}: ImageLightboxProps) {
+ const total = items.length;
+ const current = items[index];
+ const hasNav = total > 1;
+
+ const goPrev = useCallback(() => {
+ if (!hasNav) return;
+ onIndexChange((index - 1 + total) % total);
+ }, [hasNav, index, total, onIndexChange]);
+
+ const goNext = useCallback(() => {
+ if (!hasNav) return;
+ onIndexChange((index + 1) % total);
+ }, [hasNav, index, total, onIndexChange]);
+
+ useEffect(() => {
+ const onKey = (e: KeyboardEvent) => {
+ if (e.key === "Escape") onClose();
+ else if (e.key === "ArrowLeft") goPrev();
+ else if (e.key === "ArrowRight") goNext();
+ };
+ window.addEventListener("keydown", onKey);
+ const prev = document.body.style.overflow;
+ document.body.style.overflow = "hidden";
+ return () => {
+ window.removeEventListener("keydown", onKey);
+ document.body.style.overflow = prev;
+ };
+ }, [onClose, goPrev, goNext]);
+
+ if (!current || typeof document === "undefined") return null;
+
+ return createPortal(
+
+
+
+ {hasNav && (
+ <>
+
+
+
e.stopPropagation()}>
+ {index + 1} / {total}
+
+ >
+ )}
+
+
e.stopPropagation()}
+ >
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
+ {current.alt ? (
+ {current.alt}
+ ) : null}
+
+
,
+ document.body
+ );
+}
diff --git a/frontend/components/markdown/MdCodeBlock.tsx b/frontend/components/markdown/MdCodeBlock.tsx
new file mode 100644
index 0000000..2f3d9f7
--- /dev/null
+++ b/frontend/components/markdown/MdCodeBlock.tsx
@@ -0,0 +1,182 @@
+"use client";
+
+import {
+ useMemo,
+ useRef,
+ useState,
+ useEffect,
+ type HTMLAttributes,
+ type ReactNode,
+} from "react";
+import { Check, Copy, ChevronDown, ChevronUp } from "lucide-react";
+import { toast } from "@/lib/toast";
+
+const COLLAPSE_LINE_THRESHOLD = 5;
+/** 约 5 行代码高度(含行高) */
+const COLLAPSED_MAX_HEIGHT = "7.75rem";
+
+type MdCodeBlockProps = HTMLAttributes
& {
+ children?: ReactNode;
+};
+
+function extractText(node: ReactNode): string {
+ if (node == null || typeof node === "boolean") return "";
+ if (typeof node === "string" || typeof node === "number") return String(node);
+ if (Array.isArray(node)) return node.map(extractText).join("");
+ if (typeof node === "object" && "props" in node) {
+ const el = node as { props?: { children?: ReactNode } };
+ return extractText(el.props?.children);
+ }
+ return "";
+}
+
+function countLines(pre: HTMLElement | null, fallbackText: string): number {
+ if (pre) {
+ const lineEls = pre.querySelectorAll(".line, [data-line]");
+ if (lineEls.length > 0) return lineEls.length;
+ }
+ const text = fallbackText.replace(/\n$/, "");
+ if (!text) return 0;
+ return text.split("\n").length;
+}
+
+function readLanguage(
+ el: HTMLElement | null,
+ className?: string,
+ dataLanguage?: string
+): string {
+ const fromData = dataLanguage || el?.getAttribute("data-language") || "";
+ if (fromData) return fromData;
+ const cls = [className, el?.className].filter(Boolean).join(" ");
+ const m = cls.match(/language-([\w+-]+)/);
+ return m?.[1] || "";
+}
+
+/** Markdown 代码块:语言标签、复制、>5 行默认折叠;客户端可补 Shiki 双主题 */
+export default function MdCodeBlock({ children, className, ...rest }: MdCodeBlockProps) {
+ const hostRef = useRef(null);
+
+ const plainText = useMemo(() => extractText(children), [children]);
+ const dataLanguage =
+ typeof (rest as Record)["data-language"] === "string"
+ ? String((rest as Record)["data-language"])
+ : undefined;
+
+ const approxLines = useMemo(() => countLines(null, plainText), [plainText]);
+ const alreadyShiki =
+ typeof className === "string" && className.includes("shiki");
+
+ const [copied, setCopied] = useState(false);
+ const [expanded, setExpanded] = useState(false);
+ const [lineCount, setLineCount] = useState(approxLines);
+ const [language, setLanguage] = useState(() =>
+ readLanguage(null, className, dataLanguage)
+ );
+ const [clientHtml, setClientHtml] = useState(null);
+
+ useEffect(() => {
+ const host = hostRef.current;
+ const pre = host?.querySelector("pre") ?? null;
+ setLineCount(countLines(pre, plainText));
+ setLanguage(readLanguage(pre, className, dataLanguage));
+ }, [plainText, className, dataLanguage, clientHtml]);
+
+ // 服务端已着色时跳过;客户端预览动态补双主题
+ useEffect(() => {
+ if (alreadyShiki || !plainText.trim()) return;
+ const lang = readLanguage(null, className, dataLanguage) || "plaintext";
+ let cancelled = false;
+ (async () => {
+ try {
+ const { codeToHtml } = await import("shiki");
+ const html = await codeToHtml(plainText.replace(/\n$/, ""), {
+ lang: lang === "text" ? "plaintext" : lang,
+ themes: {
+ light: "github-light",
+ dark: "github-dark-dimmed",
+ },
+ defaultColor: false,
+ });
+ if (!cancelled) setClientHtml(html);
+ } catch {
+ // 未知语言:保持纯文本
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [plainText, className, dataLanguage, alreadyShiki]);
+
+ const effectiveLines = lineCount > 0 ? lineCount : approxLines;
+ const canCollapse = effectiveLines > COLLAPSE_LINE_THRESHOLD;
+ const collapsed = canCollapse && !expanded;
+
+ const onCopy = async () => {
+ const pre = hostRef.current?.querySelector("pre");
+ const text =
+ pre?.innerText?.replace(/\n$/, "") || plainText.replace(/\n$/, "");
+ if (!text) return;
+ try {
+ await navigator.clipboard.writeText(text);
+ setCopied(true);
+ window.setTimeout(() => setCopied(false), 1600);
+ } catch {
+ toast("复制失败,请手动选择文本");
+ }
+ };
+
+ const preClass = ["md-code-pre", className].filter(Boolean).join(" ");
+
+ return (
+
+
+ {language || "text"}
+
+
+
+ {clientHtml ? (
+
+ ) : (
+
+ {children}
+
+ )}
+ {collapsed &&
}
+
+ {canCollapse && (
+
+ )}
+
+ );
+}
diff --git a/frontend/components/markdown/MdImage.tsx b/frontend/components/markdown/MdImage.tsx
new file mode 100644
index 0000000..daef85d
--- /dev/null
+++ b/frontend/components/markdown/MdImage.tsx
@@ -0,0 +1,76 @@
+"use client";
+
+import { useState, useCallback, type ImgHTMLAttributes } from "react";
+import ImageLightbox, { type LightboxItem } from "./ImageLightbox";
+
+type MdImageProps = ImgHTMLAttributes;
+
+function collectGallery(from: HTMLImageElement): { items: LightboxItem[]; index: number } {
+ const root = from.closest(".prose-content");
+ const nodes = root
+ ? Array.from(root.querySelectorAll("img.md-content-img[src]"))
+ : [from];
+ const items: LightboxItem[] = nodes
+ .map((el) => ({
+ src: el.currentSrc || el.getAttribute("src") || "",
+ alt: el.getAttribute("alt") || undefined,
+ }))
+ .filter((it) => !!it.src);
+ const selfSrc = from.currentSrc || from.getAttribute("src") || "";
+ let index = items.findIndex((it) => it.src === selfSrc);
+ if (index < 0) index = 0;
+ if (items.length === 0 && selfSrc) {
+ return { items: [{ src: selfSrc, alt: from.alt || undefined }], index: 0 };
+ }
+ return { items, index };
+}
+
+/** Markdown 正文图片:点击进入图箱,同文多图可切换 */
+export default function MdImage({ src, alt, title, className, ...rest }: MdImageProps) {
+ const [broken, setBroken] = useState(false);
+ const [lightbox, setLightbox] = useState<{
+ items: LightboxItem[];
+ index: number;
+ } | null>(null);
+
+ const open = useCallback((el: HTMLImageElement) => {
+ const gallery = collectGallery(el);
+ if (gallery.items.length === 0) return;
+ setLightbox(gallery);
+ }, []);
+
+ if (!src || broken) {
+ return (
+
+ {alt || "图片加载失败"}
+
+ );
+ }
+
+ return (
+ <>
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
open(e.currentTarget)}
+ onError={() => setBroken(true)}
+ />
+ {lightbox && (
+ setLightbox(null)}
+ onIndexChange={(i) =>
+ setLightbox((prev) => (prev ? { ...prev, index: i } : prev))
+ }
+ />
+ )}
+ >
+ );
+}
diff --git a/frontend/components/markdown/MdLink.tsx b/frontend/components/markdown/MdLink.tsx
new file mode 100644
index 0000000..dc12c3f
--- /dev/null
+++ b/frontend/components/markdown/MdLink.tsx
@@ -0,0 +1,38 @@
+"use client";
+
+import type { AnchorHTMLAttributes } from "react";
+
+type MdLinkProps = AnchorHTMLAttributes;
+
+function isExternalHref(href?: string): boolean {
+ if (!href) return false;
+ if (href.startsWith("/") || href.startsWith("#") || href.startsWith("?")) {
+ return false;
+ }
+ if (href.startsWith("mailto:") || href.startsWith("tel:")) return false;
+ try {
+ if (typeof window !== "undefined") {
+ const url = new URL(href, window.location.origin);
+ return url.origin !== window.location.origin;
+ }
+ } catch {
+ /* 相对或非法 URL 当站内 */
+ }
+ return /^https?:\/\//i.test(href);
+}
+
+/** Markdown 链接:外链新开页 */
+export default function MdLink({ href, children, ...rest }: MdLinkProps) {
+ const external = isExternalHref(href);
+ return (
+
+ {children}
+
+ );
+}
diff --git a/frontend/components/markdown/mergeComponents.tsx b/frontend/components/markdown/mergeComponents.tsx
new file mode 100644
index 0000000..91e0738
--- /dev/null
+++ b/frontend/components/markdown/mergeComponents.tsx
@@ -0,0 +1,16 @@
+import type { Components } from "react-markdown";
+import MdImage from "./MdImage";
+import MdCodeBlock from "./MdCodeBlock";
+import MdLink from "./MdLink";
+
+/** 默认自定义标签:图箱 / 代码块 / 外链;可被调用方浅合并覆盖 */
+export function mergeMarkdownComponents(
+ overrides?: Components
+): Components {
+ return {
+ img: MdImage,
+ pre: MdCodeBlock,
+ a: MdLink,
+ ...overrides,
+ };
+}
diff --git a/frontend/components/markdown/renderMarkdown.tsx b/frontend/components/markdown/renderMarkdown.tsx
new file mode 100644
index 0000000..40c3a97
--- /dev/null
+++ b/frontend/components/markdown/renderMarkdown.tsx
@@ -0,0 +1,45 @@
+import type { Components } from "react-markdown";
+import type { ReactElement } from "react";
+import { Fragment, jsx, jsxs } from "react/jsx-runtime";
+import { toJsxRuntime } from "hast-util-to-jsx-runtime";
+import { unified } from "unified";
+import remarkParse from "remark-parse";
+import remarkGfm from "remark-gfm";
+import remarkRehype from "remark-rehype";
+import rehypePrettyCode from "rehype-pretty-code";
+import type { Root } from "hast";
+import { mergeMarkdownComponents } from "./mergeComponents";
+
+export const prettyCodeOptions = {
+ theme: {
+ light: "github-light",
+ dark: "github-dark-dimmed",
+ },
+ keepBackground: false,
+ defaultLang: "plaintext",
+} as const;
+
+/** 服务端异步管线:GFM + Shiki 双主题高亮 → React 树 */
+export async function renderMarkdownToReact(
+ content: string,
+ overrides?: Components
+): Promise {
+ const processor = unified()
+ .use(remarkParse)
+ .use(remarkGfm)
+ .use(remarkRehype)
+ .use(rehypePrettyCode, { ...prettyCodeOptions });
+
+ const mdast = processor.parse(content || "");
+ const hast = (await processor.run(mdast)) as Root;
+
+ return toJsxRuntime(hast, {
+ Fragment,
+ jsx,
+ jsxs,
+ components: mergeMarkdownComponents(overrides) as Record<
+ string,
+ React.ElementType
+ >,
+ }) as ReactElement;
+}
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 262ea83..f9149d6 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -8,12 +8,19 @@
"name": "jiang13-bbs-frontend",
"version": "0.1.0",
"dependencies": {
+ "hast-util-to-jsx-runtime": "^2.3.6",
"lucide-react": "^0.460.0",
"next": "^16.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-easy-crop": "^6.2.3",
- "react-markdown": "^9.0.1"
+ "react-markdown": "^9.0.1",
+ "rehype-pretty-code": "^0.14.5",
+ "remark-gfm": "^4.0.1",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.1.2",
+ "shiki": "^4.4.3",
+ "unified": "^11.0.5"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.0",
@@ -792,6 +799,106 @@
"node": ">= 10"
}
},
+ "node_modules/@shikijs/core": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.4.3.tgz",
+ "integrity": "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/primitive": "4.4.3",
+ "@shikijs/types": "4.4.3",
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "@types/hast": "^3.0.5",
+ "hast-util-to-html": "^9.0.5"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shikijs/engine-javascript": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.4.3.tgz",
+ "integrity": "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "4.4.3",
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "oniguruma-to-es": "^4.3.6"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shikijs/engine-oniguruma": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.4.3.tgz",
+ "integrity": "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "4.4.3",
+ "@shikijs/vscode-textmate": "^10.0.2"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shikijs/langs": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.4.3.tgz",
+ "integrity": "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "4.4.3"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shikijs/primitive": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.4.3.tgz",
+ "integrity": "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "4.4.3",
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "@types/hast": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shikijs/themes": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.4.3.tgz",
+ "integrity": "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "4.4.3"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shikijs/types": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.4.3.tgz",
+ "integrity": "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "@types/hast": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shikijs/vscode-textmate": {
+ "version": "10.0.2",
+ "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
+ "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
+ "license": "MIT"
+ },
"node_modules/@swc/helpers": {
"version": "0.5.23",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
@@ -1363,6 +1470,30 @@
"node": ">=10.13.0"
}
},
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/estree-util-is-identifier-name": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
@@ -1386,6 +1517,80 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/hast-util-from-html": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz",
+ "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "devlop": "^1.1.0",
+ "hast-util-from-parse5": "^8.0.0",
+ "parse5": "^7.0.0",
+ "vfile": "^6.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-from-parse5": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz",
+ "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "devlop": "^1.0.0",
+ "hastscript": "^9.0.0",
+ "property-information": "^7.0.0",
+ "vfile": "^6.0.0",
+ "vfile-location": "^5.0.0",
+ "web-namespaces": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-parse-selector": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
+ "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-to-html": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
+ "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "html-void-elements": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "stringify-entities": "^4.0.0",
+ "zwitch": "^2.0.4"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/hast-util-to-jsx-runtime": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
@@ -1413,6 +1618,19 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/hast-util-to-string": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz",
+ "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/hast-util-whitespace": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
@@ -1426,6 +1644,23 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/hastscript": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz",
+ "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "hast-util-parse-selector": "^4.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/html-url-attributes": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
@@ -1436,6 +1671,16 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/html-void-elements": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
+ "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/inline-style-parser": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
@@ -1810,6 +2055,32 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/markdown-table": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
+ "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/mdast-util-find-and-replace": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
+ "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "escape-string-regexp": "^5.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/mdast-util-from-markdown": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
@@ -1834,6 +2105,107 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/mdast-util-gfm": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
+ "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-gfm-autolink-literal": "^2.0.0",
+ "mdast-util-gfm-footnote": "^2.0.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.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-autolink-literal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
+ "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-find-and-replace": "^3.0.0",
+ "micromark-util-character": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-strikethrough": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
+ "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-table": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
+ "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "markdown-table": "^3.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-task-list-item": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
+ "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/mdast-util-mdx-expression": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
@@ -2032,6 +2404,127 @@
"micromark-util-types": "^2.0.0"
}
},
+ "node_modules/micromark-extension-gfm": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
+ "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-extension-gfm-autolink-literal": "^2.0.0",
+ "micromark-extension-gfm-footnote": "^2.0.0",
+ "micromark-extension-gfm-strikethrough": "^2.0.0",
+ "micromark-extension-gfm-table": "^2.0.0",
+ "micromark-extension-gfm-tagfilter": "^2.0.0",
+ "micromark-extension-gfm-task-list-item": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-autolink-literal": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
+ "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-strikethrough": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
+ "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-table": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz",
+ "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-tagfilter": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
+ "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-task-list-item": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
+ "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/micromark-factory-destination": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
@@ -2516,6 +3009,23 @@
"integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==",
"license": "BSD-3-Clause"
},
+ "node_modules/oniguruma-parser": {
+ "version": "0.12.2",
+ "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz",
+ "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==",
+ "license": "MIT"
+ },
+ "node_modules/oniguruma-to-es": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz",
+ "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==",
+ "license": "MIT",
+ "dependencies": {
+ "oniguruma-parser": "^0.12.2",
+ "regex": "^6.1.0",
+ "regex-recursion": "^6.0.2"
+ }
+ },
"node_modules/parse-entities": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
@@ -2541,6 +3051,24 @@
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"license": "MIT"
},
+ "node_modules/parse-numeric-range": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz",
+ "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==",
+ "license": "ISC"
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -2647,6 +3175,83 @@
"react": ">=18"
}
},
+ "node_modules/regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz",
+ "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==",
+ "license": "MIT",
+ "dependencies": {
+ "regex-utilities": "^2.3.0"
+ }
+ },
+ "node_modules/regex-recursion": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz",
+ "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
+ "license": "MIT",
+ "dependencies": {
+ "regex-utilities": "^2.3.0"
+ }
+ },
+ "node_modules/regex-utilities": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
+ "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
+ "license": "MIT"
+ },
+ "node_modules/rehype-parse": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz",
+ "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-from-html": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/rehype-pretty-code": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/rehype-pretty-code/-/rehype-pretty-code-0.14.5.tgz",
+ "integrity": "sha512-+DhMMNMgOwmE0xu2RmagN63nR0GRObPIQ9TuoNdUYYIDcCr8nWXU1CJUG6yca7vg99Gp5d+62A/+ZvksmsQXjA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.4",
+ "hast-util-to-string": "^3.0.0",
+ "parse-numeric-range": "^1.3.0",
+ "rehype-parse": "^9.0.0",
+ "unified": "^11.0.5",
+ "unist-util-visit": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "shiki": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0"
+ }
+ },
+ "node_modules/remark-gfm": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
+ "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-gfm": "^3.0.0",
+ "micromark-extension-gfm": "^3.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-stringify": "^11.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/remark-parse": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
@@ -2680,6 +3285,21 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/remark-stringify": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
+ "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/scheduler": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
@@ -2749,6 +3369,25 @@
}
}
},
+ "node_modules/shiki": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.4.3.tgz",
+ "integrity": "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/core": "4.4.3",
+ "@shikijs/engine-javascript": "4.4.3",
+ "@shikijs/engine-oniguruma": "4.4.3",
+ "@shikijs/langs": "4.4.3",
+ "@shikijs/themes": "4.4.3",
+ "@shikijs/types": "4.4.3",
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "@types/hast": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -2992,6 +3631,20 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/vfile-location": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz",
+ "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/vfile-message": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
@@ -3006,6 +3659,16 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/web-namespaces": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
+ "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index e3da9ce..ba0e035 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -9,12 +9,19 @@
"lint": "next lint"
},
"dependencies": {
+ "hast-util-to-jsx-runtime": "^2.3.6",
"lucide-react": "^0.460.0",
"next": "^16.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-easy-crop": "^6.2.3",
- "react-markdown": "^9.0.1"
+ "react-markdown": "^9.0.1",
+ "rehype-pretty-code": "^0.14.5",
+ "remark-gfm": "^4.0.1",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.1.2",
+ "shiki": "^4.4.3",
+ "unified": "^11.0.5"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.0",