feat(frontend): Markdown 支持 GFM、代码高亮与图片灯箱
统一 SSR/客户端渲染路径,评论与预览共用增强组件。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { ChevronRight, Megaphone } from "lucide-react";
|
||||
import { fetchAnnouncementDetail } from "@/lib/api";
|
||||
import { formatDate } from "@/lib/format";
|
||||
import MarkdownBody from "@/components/MarkdownBody";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -77,9 +77,7 @@ export default async function AnnouncementPage({ params }: PageProps) {
|
||||
className="mt-5 pt-5"
|
||||
style={{ borderTop: "1px solid var(--line)" }}
|
||||
>
|
||||
<div className="prose-content">
|
||||
<ReactMarkdown>{a.content}</ReactMarkdown>
|
||||
</div>
|
||||
<MarkdownBody content={a.content} />
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
||||
@@ -1702,15 +1702,28 @@ a.j13-mention-token:hover {
|
||||
border-radius: 6px;
|
||||
font-size: 0.86em;
|
||||
}
|
||||
.prose-content pre {
|
||||
background: var(--panel-2) !important;
|
||||
border: 1px solid var(--line);
|
||||
padding: 1.05rem 1.15rem;
|
||||
border-radius: var(--r-sm);
|
||||
.prose-content pre,
|
||||
.prose-content .md-code-pre,
|
||||
.prose-content .md-code-shiki-host pre {
|
||||
background: transparent !important;
|
||||
border: none;
|
||||
padding: 0.85rem 1.05rem;
|
||||
border-radius: 0;
|
||||
overflow-x: auto;
|
||||
margin: 1.15rem 0;
|
||||
margin: 0;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.84em;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.prose-content pre code,
|
||||
.prose-content .md-code-pre code,
|
||||
.prose-content .md-code-shiki-host code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
.prose-content pre code { background: none; padding: 0; border-radius: 0; }
|
||||
.prose-content blockquote {
|
||||
border-left: 3px solid var(--accent);
|
||||
background: var(--accent-soft);
|
||||
@@ -1725,19 +1738,249 @@ a.j13-mention-token:hover {
|
||||
text-underline-offset: 3px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.prose-content img {
|
||||
.prose-content img,
|
||||
.prose-content .md-content-img {
|
||||
max-width: 100%;
|
||||
border-radius: var(--r-sm);
|
||||
border: 1px solid var(--line);
|
||||
cursor: zoom-in;
|
||||
transition: opacity 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
.prose-content .md-content-img:hover {
|
||||
opacity: 0.92;
|
||||
box-shadow: 0 4px 18px color-mix(in srgb, var(--ink) 12%, transparent);
|
||||
}
|
||||
.prose-content .md-img-fallback {
|
||||
display: inline-block;
|
||||
padding: 0.55rem 0.85rem;
|
||||
border-radius: var(--r-sm);
|
||||
border: 1px dashed var(--line);
|
||||
background: var(--panel-2);
|
||||
color: var(--ink-2);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.prose-content hr { border: none; border-top: 1px solid var(--line); margin: 2.2rem 0; }
|
||||
.prose-content strong { font-weight: 700; }
|
||||
.prose-content del { color: var(--ink-2); text-decoration: line-through; }
|
||||
|
||||
/* GFM 表格 */
|
||||
.prose-content table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1.2rem 0;
|
||||
font-size: 0.95em;
|
||||
overflow-x: auto;
|
||||
display: block;
|
||||
}
|
||||
.prose-content thead { background: var(--panel-2); }
|
||||
.prose-content th,
|
||||
.prose-content td {
|
||||
border: 1px solid var(--line);
|
||||
padding: 0.55rem 0.85rem;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
.prose-content th { font-weight: 700; color: var(--ink); }
|
||||
.prose-content tr:nth-child(even) td { background: color-mix(in srgb, var(--tint) 55%, transparent); }
|
||||
|
||||
/* GFM 任务列表 */
|
||||
.prose-content ul.contains-task-list { list-style: none; padding-left: 0.25rem; }
|
||||
.prose-content li.task-list-item { display: flex; align-items: flex-start; gap: 0.5rem; }
|
||||
.prose-content li.task-list-item input[type="checkbox"] {
|
||||
margin-top: 0.4em;
|
||||
accent-color: var(--accent);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* rehype-pretty-code figure 外壳 */
|
||||
.prose-content figure[data-rehype-pretty-code-figure] {
|
||||
margin: 1.15rem 0;
|
||||
}
|
||||
.prose-content figure[data-rehype-pretty-code-figure] > .md-code-block {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 代码块壳:工具栏 + 折叠 */
|
||||
.prose-content .md-code-block {
|
||||
margin: 1.15rem 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--r-sm);
|
||||
background: var(--panel-2);
|
||||
overflow: hidden;
|
||||
}
|
||||
.prose-content .md-code-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 0.4rem 0.75rem;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: color-mix(in srgb, var(--panel) 70%, var(--panel-2));
|
||||
}
|
||||
.prose-content .md-code-lang {
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: lowercase;
|
||||
color: var(--ink-2);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.prose-content .md-code-copy {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
padding: 0.2rem 0.55rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: var(--ink-2);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
.prose-content .md-code-copy:hover {
|
||||
color: var(--ink);
|
||||
background: var(--tint);
|
||||
border-color: var(--line);
|
||||
}
|
||||
.prose-content .md-code-body {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.prose-content .md-code-body:not(.md-code-collapsed) {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.prose-content .md-code-collapsed {
|
||||
overflow: hidden;
|
||||
}
|
||||
.prose-content .md-code-fade {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 2.75rem;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(to bottom, transparent, var(--panel-2));
|
||||
}
|
||||
.prose-content .md-code-fold {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.45rem 0.75rem;
|
||||
border: none;
|
||||
border-top: 1px solid var(--line);
|
||||
background: color-mix(in srgb, var(--panel) 55%, var(--panel-2));
|
||||
color: var(--accent);
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.prose-content .md-code-fold:hover {
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
|
||||
/* Shiki 双主题:跟随 html.dark */
|
||||
.prose-content .shiki,
|
||||
.prose-content .shiki span {
|
||||
color: var(--shiki-light);
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.prose-content .shiki {
|
||||
background: transparent !important;
|
||||
}
|
||||
html.dark .prose-content .shiki,
|
||||
html.dark .prose-content .shiki span {
|
||||
color: var(--shiki-dark) !important;
|
||||
font-style: var(--shiki-dark-font-style) !important;
|
||||
font-weight: var(--shiki-dark-font-weight) !important;
|
||||
text-decoration: var(--shiki-dark-text-decoration) !important;
|
||||
}
|
||||
|
||||
/* 图箱 */
|
||||
.md-lightbox {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1200;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
background: rgba(0, 0, 0, 0.82);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
.md-lightbox-figure {
|
||||
margin: 0;
|
||||
max-width: min(96vw, 1200px);
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.md-lightbox-img {
|
||||
max-width: 100%;
|
||||
max-height: calc(90vh - 3rem);
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
|
||||
user-select: none;
|
||||
}
|
||||
.md-lightbox-caption {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
max-width: 40rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.md-lightbox-close,
|
||||
.md-lightbox-nav {
|
||||
position: absolute;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
.md-lightbox-close:hover,
|
||||
.md-lightbox-nav:hover {
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
.md-lightbox-close {
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
.md-lightbox-nav {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
}
|
||||
.md-lightbox-prev { left: 1rem; }
|
||||
.md-lightbox-next { right: 1rem; }
|
||||
.md-lightbox-counter {
|
||||
position: absolute;
|
||||
bottom: 1.25rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 13px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* 评论区的 Markdown 紧凑排版:继承 prose-content 全部规则,仅收紧行距与间距 */
|
||||
.prose-comment { font-size: 14.5px; line-height: 1.7; }
|
||||
.prose-comment p { margin: 0.35rem 0; }
|
||||
.prose-comment blockquote { margin: 0.5rem 0; padding: 0.45rem 0.9rem; }
|
||||
.prose-comment pre { margin: 0.6rem 0; }
|
||||
.prose-comment .md-code-block { margin: 0.6rem 0; }
|
||||
.prose-comment h1, .prose-comment h2, .prose-comment h3, .prose-comment h4 {
|
||||
font-size: 1em;
|
||||
margin: 0.6rem 0 0.3rem;
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Trash2, MessagesSquare, ChevronRight, ChevronDown, BadgeCheck } from "lucide-react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import {
|
||||
apiCreateComment,
|
||||
apiDeleteComment,
|
||||
@@ -18,6 +17,7 @@ import { canModerateBoard } from "@/lib/roles";
|
||||
import { toast } from "@/lib/toast";
|
||||
import Avatar from "./Avatar";
|
||||
import Pagination from "./Pagination";
|
||||
import MarkdownBodyClient from "@/components/MarkdownBodyClient";
|
||||
|
||||
interface CommentSectionProps {
|
||||
postId: string;
|
||||
@@ -294,8 +294,11 @@ export default function CommentSection({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="prose-content prose-comment mt-0.5">
|
||||
<ReactMarkdown>{c.content}</ReactMarkdown>
|
||||
<div className="mt-0.5">
|
||||
<MarkdownBodyClient
|
||||
content={c.content}
|
||||
className="prose-content prose-comment"
|
||||
/>
|
||||
</div>
|
||||
{/* 底部行:时间 · 回复(左),删除(右) */}
|
||||
<div className="flex items-center justify-between mt-1.5">
|
||||
@@ -398,9 +401,10 @@ export default function CommentSection({
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="prose-content prose-comment">
|
||||
<ReactMarkdown>{c.content}</ReactMarkdown>
|
||||
</div>
|
||||
<MarkdownBodyClient
|
||||
content={c.content}
|
||||
className="prose-content prose-comment"
|
||||
/>
|
||||
{/* 底部行:时间 · 回复(左),删除(右) */}
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
@@ -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 (
|
||||
<div className={className}>
|
||||
<ReactMarkdown components={components}>{content || ""}</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
const tree = await renderMarkdownToReact(content, components);
|
||||
return <div className={className}>{tree}</div>;
|
||||
}
|
||||
|
||||
32
frontend/components/MarkdownBodyClient.tsx
Normal file
32
frontend/components/MarkdownBodyClient.tsx
Normal file
@@ -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 (
|
||||
<div className={className}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={mergeMarkdownComponents(components)}
|
||||
>
|
||||
{content || ""}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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({
|
||||
) : (
|
||||
<div className="j13-md-preview">
|
||||
{value.trim() ? (
|
||||
<MarkdownBody content={value} />
|
||||
<MarkdownBodyClient content={value} />
|
||||
) : (
|
||||
<p className="meta text-center py-16">暂无内容可预览</p>
|
||||
)}
|
||||
|
||||
122
frontend/components/markdown/ImageLightbox.tsx
Normal file
122
frontend/components/markdown/ImageLightbox.tsx
Normal file
@@ -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(
|
||||
<div
|
||||
className="md-lightbox"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="图片预览"
|
||||
onClick={onClose}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="md-lightbox-close"
|
||||
aria-label="关闭"
|
||||
onClick={onClose}
|
||||
>
|
||||
<X size={22} />
|
||||
</button>
|
||||
|
||||
{hasNav && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="md-lightbox-nav md-lightbox-prev"
|
||||
aria-label="上一张"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
goPrev();
|
||||
}}
|
||||
>
|
||||
<ChevronLeft size={28} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="md-lightbox-nav md-lightbox-next"
|
||||
aria-label="下一张"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
goNext();
|
||||
}}
|
||||
>
|
||||
<ChevronRight size={28} />
|
||||
</button>
|
||||
<div className="md-lightbox-counter" onClick={(e) => e.stopPropagation()}>
|
||||
{index + 1} / {total}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<figure
|
||||
className="md-lightbox-figure"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={current.src}
|
||||
alt={current.alt || ""}
|
||||
className="md-lightbox-img"
|
||||
draggable={false}
|
||||
/>
|
||||
{current.alt ? (
|
||||
<figcaption className="md-lightbox-caption">{current.alt}</figcaption>
|
||||
) : null}
|
||||
</figure>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
182
frontend/components/markdown/MdCodeBlock.tsx
Normal file
182
frontend/components/markdown/MdCodeBlock.tsx
Normal file
@@ -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<HTMLPreElement> & {
|
||||
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<HTMLDivElement>(null);
|
||||
|
||||
const plainText = useMemo(() => extractText(children), [children]);
|
||||
const dataLanguage =
|
||||
typeof (rest as Record<string, unknown>)["data-language"] === "string"
|
||||
? String((rest as Record<string, unknown>)["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<string | null>(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 (
|
||||
<div className="md-code-block">
|
||||
<div className="md-code-toolbar">
|
||||
<span className="md-code-lang">{language || "text"}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="md-code-copy"
|
||||
onClick={onCopy}
|
||||
aria-label={copied ? "已复制" : "复制代码"}
|
||||
>
|
||||
{copied ? <Check size={14} /> : <Copy size={14} />}
|
||||
<span>{copied ? "已复制" : "复制"}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
ref={hostRef}
|
||||
className={`md-code-body${collapsed ? " md-code-collapsed" : ""}`}
|
||||
style={collapsed ? { maxHeight: COLLAPSED_MAX_HEIGHT } : undefined}
|
||||
>
|
||||
{clientHtml ? (
|
||||
<div
|
||||
className="md-code-shiki-host"
|
||||
dangerouslySetInnerHTML={{ __html: clientHtml }}
|
||||
/>
|
||||
) : (
|
||||
<pre className={preClass} {...rest}>
|
||||
{children}
|
||||
</pre>
|
||||
)}
|
||||
{collapsed && <div className="md-code-fade" aria-hidden="true" />}
|
||||
</div>
|
||||
{canCollapse && (
|
||||
<button
|
||||
type="button"
|
||||
className="md-code-fold"
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
>
|
||||
{expanded ? (
|
||||
<>
|
||||
<ChevronUp size={14} />
|
||||
收起
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown size={14} />
|
||||
展开全部({effectiveLines} 行)
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
76
frontend/components/markdown/MdImage.tsx
Normal file
76
frontend/components/markdown/MdImage.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, type ImgHTMLAttributes } from "react";
|
||||
import ImageLightbox, { type LightboxItem } from "./ImageLightbox";
|
||||
|
||||
type MdImageProps = ImgHTMLAttributes<HTMLImageElement>;
|
||||
|
||||
function collectGallery(from: HTMLImageElement): { items: LightboxItem[]; index: number } {
|
||||
const root = from.closest(".prose-content");
|
||||
const nodes = root
|
||||
? Array.from(root.querySelectorAll<HTMLImageElement>("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 (
|
||||
<span className="md-img-fallback" role="img" aria-label={alt || "图片加载失败"}>
|
||||
{alt || "图片加载失败"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
{...rest}
|
||||
src={src}
|
||||
alt={alt || ""}
|
||||
title={title}
|
||||
className={`md-content-img${className ? ` ${className}` : ""}`}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
onClick={(e) => open(e.currentTarget)}
|
||||
onError={() => setBroken(true)}
|
||||
/>
|
||||
{lightbox && (
|
||||
<ImageLightbox
|
||||
items={lightbox.items}
|
||||
index={lightbox.index}
|
||||
onClose={() => setLightbox(null)}
|
||||
onIndexChange={(i) =>
|
||||
setLightbox((prev) => (prev ? { ...prev, index: i } : prev))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
38
frontend/components/markdown/MdLink.tsx
Normal file
38
frontend/components/markdown/MdLink.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import type { AnchorHTMLAttributes } from "react";
|
||||
|
||||
type MdLinkProps = AnchorHTMLAttributes<HTMLAnchorElement>;
|
||||
|
||||
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 (
|
||||
<a
|
||||
{...rest}
|
||||
href={href}
|
||||
{...(external
|
||||
? { target: "_blank", rel: "noopener noreferrer" }
|
||||
: {})}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
16
frontend/components/markdown/mergeComponents.tsx
Normal file
16
frontend/components/markdown/mergeComponents.tsx
Normal file
@@ -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,
|
||||
};
|
||||
}
|
||||
45
frontend/components/markdown/renderMarkdown.tsx
Normal file
45
frontend/components/markdown/renderMarkdown.tsx
Normal file
@@ -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<ReactElement> {
|
||||
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;
|
||||
}
|
||||
665
frontend/package-lock.json
generated
665
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user