hd-garden 0.3.13 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/garden-interactive-chart/SKILL.md +6 -6
- package/.agents/skills/garden-ui/SKILL.md +4 -4
- package/app/a2/[slug]/page.tsx +61 -0
- package/app/a2/page.tsx +27 -0
- package/app/blog/[...path]/page.tsx +32 -22
- package/app/blog/page.tsx +6 -7
- package/app/columns/[column]/[...path]/page.tsx +32 -25
- package/app/columns/page.tsx +38 -52
- package/app/components/ArticleMeasure.tsx +95 -0
- package/app/components/ArticleTitle.tsx +20 -0
- package/app/components/BlogArchive.tsx +106 -102
- package/app/components/MarkdownArticle.tsx +145 -66
- package/app/components/MermaidDiagram.tsx +44 -71
- package/app/components/SiteFooter.tsx +3 -7
- package/app/components/SiteHeader.tsx +55 -41
- package/app/components/TableOfContents.tsx +3 -2
- package/app/components/TagIndex.tsx +97 -0
- package/app/components/garden/GardenBlock.tsx +45 -0
- package/app/components/garden/blocks.tsx +530 -0
- package/app/components/garden/chrome.tsx +350 -0
- package/app/components/garden/sandbox-message.ts +16 -0
- package/app/garden-modules.d.ts +2 -0
- package/app/globals.css +17 -12
- package/app/layout.tsx +9 -0
- package/app/lib/a2-meta.ts +22 -0
- package/app/lib/a2-preview.ts +50 -0
- package/app/lib/content.ts +52 -6
- package/app/lib/markdown-plugins.ts +2 -0
- package/app/lib/remark-a2.mjs +208 -0
- package/app/lib/remark-garden.mjs +88 -0
- package/app/page.tsx +122 -93
- package/app/styles/a2.css +1383 -0
- package/app/styles/shell.css +438 -0
- package/app/styles/tokens.css +111 -0
- package/app/tags/page.tsx +28 -0
- package/app/ui-parity/page.tsx +3 -2
- package/package.json +1 -1
- package/scripts/build-content.mjs +11 -1
- package/scripts/check-post.mjs +18 -4
- package/scripts/new-post.mjs +3 -0
- package/site.config.yaml +1 -1
- package/templates/garden-blocks.md +128 -0
- package/templates/garden-snippets.plist +121 -0
- package/ui/garden/catalog.mjs +196 -0
- package/ui/garden/check.mjs +103 -0
- package/ui/garden/enhance.mjs +89 -0
- package/ui/garden/parse.mjs +198 -0
- package/ui/garden/snippets.mjs +109 -0
- package/ui/garden/title.mjs +37 -0
- package/ui/garden/word-count.mjs +266 -0
- package/ui/host.mjs +45 -28
- package/ui/parity-fixtures.mjs +1 -1
- package/ui/preview/dist/garden-ui.css +81 -77
- package/ui/preview/dist/garden-ui.esm.js +124 -89
- package/ui/preview/dist/garden-ui.iife.js +124 -89
- package/ui/prompt/rules.md +17 -1
- package/ui/react/CatalogPage.tsx +12 -0
- package/ui/react/components/display.tsx +1 -1
- package/ui/react/components/media.tsx +2 -2
- package/ui/react/ui.css +81 -77
|
@@ -2,124 +2,128 @@
|
|
|
2
2
|
|
|
3
3
|
import Link from "next/link";
|
|
4
4
|
import { useSearchParams } from "next/navigation";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { columns, postHref, posts, topics } from "../lib/content";
|
|
7
7
|
import { ContentEmptyState } from "./ContentEmptyState";
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
export const archiveHref = (topic?: string, page?: number) => {
|
|
9
|
+
export const archiveHref = (topic?: string) => {
|
|
12
10
|
const params = new URLSearchParams();
|
|
13
11
|
if (topic) params.set("topic", topic);
|
|
14
|
-
if (page && page > 1) params.set("page", String(page));
|
|
15
12
|
const query = params.toString();
|
|
16
13
|
return query ? `/blog?${query}` : "/blog";
|
|
17
14
|
};
|
|
18
15
|
|
|
16
|
+
function day(value: string) {
|
|
17
|
+
const date = new Date(value);
|
|
18
|
+
if (Number.isNaN(date.getTime())) return value.slice(0, 10);
|
|
19
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
20
|
+
timeZone: "Asia/Shanghai",
|
|
21
|
+
year: "numeric",
|
|
22
|
+
month: "2-digit",
|
|
23
|
+
day: "2-digit",
|
|
24
|
+
}).format(date);
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
export function BlogArchive() {
|
|
20
28
|
const searchParams = useSearchParams();
|
|
29
|
+
const [compact, setCompact] = useState(false);
|
|
21
30
|
const requestedTopic = searchParams.get("topic") ?? "";
|
|
22
31
|
const selectedTopic = topics.includes(requestedTopic) ? requestedTopic : undefined;
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
const filtered = (selectedTopic ? posts.filter((post) => post.topic === selectedTopic) : posts)
|
|
33
|
+
.slice()
|
|
34
|
+
.sort((left, right) => Date.parse(right.date) - Date.parse(left.date));
|
|
35
|
+
const years = new Map<string, typeof filtered>();
|
|
36
|
+
for (const post of filtered) {
|
|
37
|
+
const year = day(post.date).slice(0, 4) || "未知";
|
|
38
|
+
const list = years.get(year);
|
|
39
|
+
if (list) list.push(post);
|
|
40
|
+
else years.set(year, [post]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const series = Array.from(
|
|
44
|
+
columns.reduce((result, entry) => {
|
|
45
|
+
const key = entry.column ?? "";
|
|
46
|
+
const list = result.get(key);
|
|
47
|
+
if (list) list.push(entry);
|
|
48
|
+
else result.set(key, [entry]);
|
|
49
|
+
return result;
|
|
50
|
+
}, new Map<string, typeof columns>()),
|
|
35
51
|
);
|
|
36
52
|
|
|
37
53
|
return (
|
|
38
|
-
|
|
39
|
-
<div
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
>
|
|
45
|
-
全部 · {posts.length}
|
|
46
|
-
</Link>
|
|
47
|
-
{topics.map((topic) => (
|
|
48
|
-
<Link
|
|
49
|
-
href={archiveHref(topic)}
|
|
50
|
-
className={selectedTopic === topic ? "active" : ""}
|
|
51
|
-
aria-current={selectedTopic === topic ? "page" : undefined}
|
|
52
|
-
key={topic}
|
|
53
|
-
>
|
|
54
|
-
{topic} · {posts.filter((post) => post.topic === topic).length}
|
|
55
|
-
</Link>
|
|
56
|
-
))}
|
|
57
|
-
</div>
|
|
58
|
-
|
|
59
|
-
{visiblePosts.length > 0 ? (
|
|
60
|
-
<section className="archive-list" aria-label="全部博文">
|
|
61
|
-
{visiblePosts.map((post) => (
|
|
62
|
-
<Link
|
|
63
|
-
key={post.path}
|
|
64
|
-
href={postHref(post)}
|
|
65
|
-
className={`archive-row${post.cover ? " with-cover" : ""}`}
|
|
66
|
-
>
|
|
67
|
-
{post.cover && (
|
|
68
|
-
// eslint-disable-next-line @next/next/no-img-element
|
|
69
|
-
<img
|
|
70
|
-
className="archive-cover"
|
|
71
|
-
src={post.cover}
|
|
72
|
-
alt={`${post.title} 封面`}
|
|
73
|
-
loading="lazy"
|
|
74
|
-
/>
|
|
75
|
-
)}
|
|
76
|
-
<div className="archive-copy">
|
|
77
|
-
<div className="archive-meta">
|
|
78
|
-
<time>{formatDate(post.date)}</time>
|
|
79
|
-
<span className="archive-topic">
|
|
80
|
-
{post.topic}
|
|
81
|
-
{post.pinned && (
|
|
82
|
-
<span className="pinned-badge">
|
|
83
|
-
<PushPin size={12} weight="fill" />
|
|
84
|
-
置顶
|
|
85
|
-
</span>
|
|
86
|
-
)}
|
|
87
|
-
</span>
|
|
88
|
-
</div>
|
|
89
|
-
<h2>{post.title}</h2>
|
|
90
|
-
<p>{post.description}</p>
|
|
91
|
-
</div>
|
|
92
|
-
<ArrowRight size={19} />
|
|
54
|
+
<div className="sh-listgrid">
|
|
55
|
+
<div>
|
|
56
|
+
<ul className="sh-filters" aria-label="按分类筛选">
|
|
57
|
+
<li>
|
|
58
|
+
<Link className="sh-chip" href="/blog" aria-current={selectedTopic ? undefined : "true"}>
|
|
59
|
+
全部<span className="n">{posts.length}</span>
|
|
93
60
|
</Link>
|
|
61
|
+
</li>
|
|
62
|
+
{topics.map((topic) => (
|
|
63
|
+
<li key={topic}>
|
|
64
|
+
<Link className="sh-chip" href={archiveHref(topic)} aria-current={selectedTopic === topic ? "true" : undefined}>
|
|
65
|
+
{topic}<span className="n">{posts.filter((post) => post.topic === topic).length}</span>
|
|
66
|
+
</Link>
|
|
67
|
+
</li>
|
|
94
68
|
))}
|
|
95
|
-
</
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
69
|
+
</ul>
|
|
70
|
+
<div className="sh-status">
|
|
71
|
+
<span aria-live="polite">{selectedTopic ? `${selectedTopic} ${filtered.length} 篇` : `全部 ${posts.length} 篇`}</span>
|
|
72
|
+
{selectedTopic ? <Link href="/blog">清除筛选</Link> : null}
|
|
73
|
+
<div className="sh-view" role="group" aria-label="列表密度">
|
|
74
|
+
<button type="button" aria-pressed={!compact} onClick={() => setCompact(false)}>摘要</button>
|
|
75
|
+
<button type="button" aria-pressed={compact} onClick={() => setCompact(true)}>紧凑</button>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
<div id="sh-list" className={compact ? "is-compact" : undefined}>
|
|
79
|
+
{filtered.length ? [...years.entries()].map(([year, entries]) => (
|
|
80
|
+
<section className="sh-year" id={`y${year}`} key={year} aria-labelledby={`yh${year}`}>
|
|
81
|
+
<h2 id={`yh${year}`}>{year}<span className="n">{entries.length} 篇</span></h2>
|
|
82
|
+
<ol className="sh-rows">
|
|
83
|
+
{entries.map((post) => (
|
|
84
|
+
<li className="sh-row" key={post.path}>
|
|
85
|
+
<time className="archive-meta" dateTime={day(post.date)}>{day(post.date).slice(5)}</time>
|
|
86
|
+
<div className="archive-copy">
|
|
87
|
+
{post.cover ? (
|
|
88
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
89
|
+
<img className="archive-cover" src={post.cover} alt="" />
|
|
90
|
+
) : null}
|
|
91
|
+
<h3>
|
|
92
|
+
<Link href={postHref(post)}>{post.title}</Link>
|
|
93
|
+
{post.pinned ? <span className="pin">置顶</span> : null}
|
|
94
|
+
</h3>
|
|
95
|
+
{post.description ? <p>{post.description}</p> : null}
|
|
96
|
+
</div>
|
|
97
|
+
{post.topic ? <Link className="cat" href={archiveHref(post.topic)}>{post.topic}</Link> : null}
|
|
98
|
+
</li>
|
|
99
|
+
))}
|
|
100
|
+
</ol>
|
|
101
|
+
</section>
|
|
102
|
+
)) : <ContentEmptyState kind="posts" />}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
<aside className="sh-rail" aria-label="按年份跳转">
|
|
106
|
+
<h2>按年份</h2>
|
|
107
|
+
<ol>
|
|
108
|
+
{[...years.keys()].map((year) => (
|
|
109
|
+
<li key={year}>
|
|
110
|
+
<a href={`#y${year}`}><span>{year}</span><span className="n num">{years.get(year)?.length ?? 0}</span></a>
|
|
111
|
+
</li>
|
|
112
|
+
))}
|
|
113
|
+
</ol>
|
|
114
|
+
{series.length ? (
|
|
115
|
+
<>
|
|
116
|
+
<h2>专栏</h2>
|
|
117
|
+
<ol>
|
|
118
|
+
{series.map(([slug, entries]) => (
|
|
119
|
+
<li key={slug}>
|
|
120
|
+
<Link href={`/columns#${slug}`}><span>{entries[0]?.columnTitle ?? slug}</span><span className="n num">{entries.length}</span></Link>
|
|
121
|
+
</li>
|
|
122
|
+
))}
|
|
123
|
+
</ol>
|
|
124
|
+
</>
|
|
125
|
+
) : null}
|
|
126
|
+
</aside>
|
|
127
|
+
</div>
|
|
124
128
|
);
|
|
125
129
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { markdownPlugins } from "../lib/markdown-plugins";
|
|
2
|
+
import { remarkA2 } from "../lib/remark-a2.mjs";
|
|
2
3
|
import {
|
|
3
4
|
Children,
|
|
5
|
+
cloneElement,
|
|
4
6
|
isValidElement,
|
|
5
7
|
type HTMLAttributes,
|
|
8
|
+
type ReactNode,
|
|
6
9
|
} from "react";
|
|
10
|
+
import { articleHeadingBase, articleUsesHeadingNumbers, displayHeadingLevel, splitHeadingNumber } from "../lib/content";
|
|
7
11
|
import ReactMarkdown from "react-markdown";
|
|
8
12
|
import rehypeRaw from "rehype-raw";
|
|
9
13
|
import rehypeKatex from "rehype-katex";
|
|
@@ -12,40 +16,32 @@ import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
|
|
12
16
|
import rehypeHighlight from "rehype-highlight";
|
|
13
17
|
import { MermaidDiagram } from "./MermaidDiagram";
|
|
14
18
|
import { MarkdownImage } from "./MarkdownImage";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
19
|
+
import { CodeCopy } from "./garden/chrome";
|
|
20
|
+
import { CodeDiff } from "./garden/chrome";
|
|
21
|
+
import { GardenBlock } from "./garden/GardenBlock";
|
|
18
22
|
|
|
19
|
-
const BLOCK_LANGUAGES = ["language-mermaid"
|
|
23
|
+
const BLOCK_LANGUAGES = ["language-mermaid"];
|
|
20
24
|
const isBlockLanguage = (className?: string) =>
|
|
21
25
|
Boolean(className) && BLOCK_LANGUAGES.some((language) => className?.split(/\s+/u).includes(language));
|
|
22
26
|
|
|
27
|
+
function codeText(node: unknown): string {
|
|
28
|
+
if (!node || typeof node !== "object") return "";
|
|
29
|
+
const record = node as { type?: string; value?: string; children?: unknown[] };
|
|
30
|
+
if (typeof record.value === "string" && record.type === "text") return record.value;
|
|
31
|
+
return (record.children ?? []).map((child) => codeText(child)).join("");
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
function CodeBlock({
|
|
24
35
|
className,
|
|
25
36
|
children,
|
|
37
|
+
node,
|
|
26
38
|
...props
|
|
27
39
|
}: HTMLAttributes<HTMLElement> & { node?: unknown }) {
|
|
28
40
|
if (className?.includes("language-mermaid")) {
|
|
29
|
-
return <MermaidDiagram source={String(children).trim()} />;
|
|
30
|
-
}
|
|
31
|
-
if (className?.includes("language-embed")) {
|
|
32
|
-
return <ExternalEmbed source={String(children).trim()} />;
|
|
33
|
-
}
|
|
34
|
-
if (className?.includes("language-widget")) {
|
|
35
|
-
return <WidgetChart source={String(children).trim()} />;
|
|
36
|
-
}
|
|
37
|
-
if (className?.split(/\s+/u).includes("language-ui")) {
|
|
38
|
-
return <UiBlock source={String(children)} />;
|
|
39
|
-
}
|
|
40
|
-
if (className?.split(/\s+/u).includes("language-ui-error")) {
|
|
41
|
-
return <UiErrorBlock source={String(children)} />;
|
|
42
|
-
}
|
|
43
|
-
if (className?.split(/\s+/u).includes("language-intent")) {
|
|
44
|
-
return <IntentBlock source={String(children)} />;
|
|
41
|
+
return <MermaidDiagram source={codeText(node) || String(children ?? "").trim()} />;
|
|
45
42
|
}
|
|
46
43
|
|
|
47
44
|
const codeProps = { ...props };
|
|
48
|
-
delete codeProps.node;
|
|
49
45
|
|
|
50
46
|
return (
|
|
51
47
|
<code className={className} {...codeProps}>
|
|
@@ -54,51 +50,113 @@ function CodeBlock({
|
|
|
54
50
|
);
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const marker = line.match(/^\s*(`{3,}|~{3,})/)?.[1];
|
|
74
|
-
if (marker) { if (!fence) fence = marker[0]; else if (marker[0] === fence) fence = null; return [line]; }
|
|
75
|
-
if (fence) return [line];
|
|
76
|
-
const legacyMarker = line.match(/garden-callout-marker--(info|success|warning|danger)/u)?.[1];
|
|
77
|
-
if (legacyMarker) {
|
|
78
|
-
pendingLegacyLabel = true;
|
|
79
|
-
return [line];
|
|
53
|
+
function stripPrefix(children: ReactNode, prefix: string): ReactNode {
|
|
54
|
+
let rest = prefix;
|
|
55
|
+
const walk = (node: ReactNode): ReactNode => {
|
|
56
|
+
if (!rest) return node;
|
|
57
|
+
if (typeof node === "string" || typeof node === "number") {
|
|
58
|
+
const value = String(node);
|
|
59
|
+
if (value.startsWith(rest)) {
|
|
60
|
+
const kept = value.slice(rest.length);
|
|
61
|
+
rest = "";
|
|
62
|
+
return kept;
|
|
63
|
+
}
|
|
64
|
+
if (rest.startsWith(value)) {
|
|
65
|
+
rest = rest.slice(value.length);
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
80
69
|
}
|
|
81
|
-
if (
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
const label = line.match(/^>\s*\*\*(.+?)\*\*\s*$/u)?.[1].trim();
|
|
85
|
-
if (label && automaticCalloutLabels.has(label)) return [];
|
|
70
|
+
if (Array.isArray(node)) return node.map(walk);
|
|
71
|
+
if (isValidElement<{ children?: ReactNode }>(node)) {
|
|
72
|
+
return cloneElement(node, undefined, walk(node.props.children));
|
|
86
73
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const title = match?.[2].trim();
|
|
91
|
-
const lines = [`> <span class="garden-callout-marker garden-callout-marker--${callout.tone}" aria-hidden="true"></span>`, ">"];
|
|
92
|
-
if (title && !automaticCalloutLabels.has(title)) lines.push(`> **${title}**`, ">");
|
|
93
|
-
return lines;
|
|
94
|
-
}).join("\n");
|
|
74
|
+
return node;
|
|
75
|
+
};
|
|
76
|
+
return walk(children);
|
|
95
77
|
}
|
|
96
78
|
|
|
79
|
+
function LinkCard({ href }: { href: string }) {
|
|
80
|
+
let host = href;
|
|
81
|
+
try { host = new URL(href).hostname.replace(/^www\./u, ""); } catch { /* keep the raw url */ }
|
|
82
|
+
return (
|
|
83
|
+
<ul className="link-cards">
|
|
84
|
+
<li>
|
|
85
|
+
<a className="link-card ext" href={href} target="_blank" rel="noopener noreferrer">
|
|
86
|
+
<span className="lc-body">
|
|
87
|
+
<span className="lc-site">
|
|
88
|
+
<span className="lc-sitename">{host}</span>
|
|
89
|
+
<span className="lc-host">{host}</span>
|
|
90
|
+
<span className="lc-arrow" aria-hidden="true" />
|
|
91
|
+
</span>
|
|
92
|
+
<span className="lc-title">{href}</span>
|
|
93
|
+
</span>
|
|
94
|
+
</a>
|
|
95
|
+
</li>
|
|
96
|
+
</ul>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const headingText = (children: ReactNode) => {
|
|
101
|
+
const parts: string[] = [];
|
|
102
|
+
const walk = (node: ReactNode) => {
|
|
103
|
+
if (typeof node === "string" || typeof node === "number") parts.push(String(node));
|
|
104
|
+
else if (Array.isArray(node)) node.forEach(walk);
|
|
105
|
+
else if (isValidElement<{ children?: React.ReactNode }>(node)) walk(node.props.children);
|
|
106
|
+
};
|
|
107
|
+
walk(children);
|
|
108
|
+
return parts.join("");
|
|
109
|
+
};
|
|
110
|
+
|
|
97
111
|
export function MarkdownArticle({ content }: { content: string }) {
|
|
112
|
+
const numbers = { h2: 0, h3: 0 };
|
|
113
|
+
const numberedArticle = articleUsesHeadingNumbers(content);
|
|
114
|
+
const headingBase = articleHeadingBase(content);
|
|
115
|
+
const section = (depth: 2 | 3, children: ReactNode) => {
|
|
116
|
+
const raw = headingText(children).trim();
|
|
117
|
+
const split = splitHeadingNumber(raw);
|
|
118
|
+
if (split) {
|
|
119
|
+
if (depth === 2) { numbers.h2 += 1; numbers.h3 = 0; }
|
|
120
|
+
else numbers.h3 += 1;
|
|
121
|
+
const prefix = raw.slice(0, raw.length - split.text.length);
|
|
122
|
+
return { no: split.no, children: stripPrefix(children, prefix) };
|
|
123
|
+
}
|
|
124
|
+
if (numberedArticle) return { no: "", children };
|
|
125
|
+
if (depth === 2) { numbers.h2 += 1; numbers.h3 = 0; return { no: String(numbers.h2), children }; }
|
|
126
|
+
numbers.h3 += 1;
|
|
127
|
+
return { no: numbers.h2 ? `${numbers.h2}.${numbers.h3}` : String(numbers.h3), children };
|
|
128
|
+
};
|
|
129
|
+
const heading = (source: 2 | 3 | 4, children: ReactNode, props: HTMLAttributes<HTMLHeadingElement> & { node?: unknown }) => {
|
|
130
|
+
const level = displayHeadingLevel(source, headingBase);
|
|
131
|
+
const Tag = level === 2 ? "h2" : level === 3 ? "h3" : "h4";
|
|
132
|
+
const domProps = { ...props };
|
|
133
|
+
Reflect.deleteProperty(domProps, "node");
|
|
134
|
+
if (level > 3) return <Tag {...domProps}>{children}</Tag>;
|
|
135
|
+
const sectioned = section(level as 2 | 3, children);
|
|
136
|
+
return <Tag {...domProps}>{sectioned.no ? <span className="sec-no">{sectioned.no}</span> : null}{sectioned.children}</Tag>;
|
|
137
|
+
};
|
|
138
|
+
const gardenComponents = {
|
|
139
|
+
"garden-block": (props: { dataPayload?: string; "data-payload"?: string }) => (
|
|
140
|
+
<GardenBlock dataPayload={props.dataPayload ?? props["data-payload"]} />
|
|
141
|
+
),
|
|
142
|
+
"garden-codediff": (props: {
|
|
143
|
+
children?: ReactNode;
|
|
144
|
+
dataDiff?: string;
|
|
145
|
+
dataNew?: string;
|
|
146
|
+
"data-diff"?: string;
|
|
147
|
+
"data-new"?: string;
|
|
148
|
+
}) => (
|
|
149
|
+
<CodeDiff
|
|
150
|
+
code={props.children}
|
|
151
|
+
diff={decodeURIComponent(props.dataDiff ?? props["data-diff"] ?? "")}
|
|
152
|
+
next={decodeURIComponent(props.dataNew ?? props["data-new"] ?? "")}
|
|
153
|
+
/>
|
|
154
|
+
),
|
|
155
|
+
};
|
|
98
156
|
return (
|
|
99
|
-
<div className="markdown-body">
|
|
157
|
+
<div className="markdown-body prose">
|
|
100
158
|
<ReactMarkdown
|
|
101
|
-
remarkPlugins={markdownPlugins}
|
|
159
|
+
remarkPlugins={[...markdownPlugins, remarkA2]}
|
|
102
160
|
rehypePlugins={[
|
|
103
161
|
rehypeRaw,
|
|
104
162
|
rehypeKatex,
|
|
@@ -107,14 +165,24 @@ export function MarkdownArticle({ content }: { content: string }) {
|
|
|
107
165
|
rehypeHighlight,
|
|
108
166
|
]}
|
|
109
167
|
components={{
|
|
168
|
+
p: ({ children, ...props }) => {
|
|
169
|
+
const domProps = { ...props };
|
|
170
|
+
Reflect.deleteProperty(domProps, "node");
|
|
171
|
+
const nodes = Children.toArray(children).filter((node) => !(typeof node === "string" && !node.trim()));
|
|
172
|
+
const only = nodes.length === 1 && isValidElement<{ href?: string; children?: ReactNode }>(nodes[0]) ? nodes[0] : null;
|
|
173
|
+
const href = only?.props.href;
|
|
174
|
+
const text = only ? headingText(only.props.children).trim() : "";
|
|
175
|
+
if (href && text === href && /^https?:\/\//iu.test(href)) return <LinkCard href={href} />;
|
|
176
|
+
return <p {...domProps}>{children}</p>;
|
|
177
|
+
},
|
|
110
178
|
a: ({ href, children, ...props }) => {
|
|
111
|
-
const external = href
|
|
179
|
+
const external = Boolean(href && /^(https?:)?\/\//iu.test(href));
|
|
112
180
|
return (
|
|
113
181
|
<a
|
|
114
182
|
href={href}
|
|
115
183
|
{...props}
|
|
116
184
|
{...(external
|
|
117
|
-
? { target: "_blank", rel: "noopener noreferrer" }
|
|
185
|
+
? { className: "ext", target: "_blank", rel: "noopener noreferrer" }
|
|
118
186
|
: {})}
|
|
119
187
|
>
|
|
120
188
|
{children}
|
|
@@ -122,9 +190,18 @@ export function MarkdownArticle({ content }: { content: string }) {
|
|
|
122
190
|
);
|
|
123
191
|
},
|
|
124
192
|
img: MarkdownImage,
|
|
193
|
+
table: ({ children, ...props }) => (
|
|
194
|
+
<div className="table-wrap" role="region" aria-label="表格,可横向滚动" tabIndex={0}>
|
|
195
|
+
<table {...props}>{children}</table>
|
|
196
|
+
</div>
|
|
197
|
+
),
|
|
198
|
+
h2: ({ children, ...props }) => heading(2, children, props),
|
|
199
|
+
h3: ({ children, ...props }) => heading(3, children, props),
|
|
200
|
+
h4: ({ children, ...props }) => heading(4, children, props),
|
|
125
201
|
code: CodeBlock,
|
|
202
|
+
...gardenComponents,
|
|
126
203
|
pre: ({ children, ...props }) => {
|
|
127
|
-
const child = Children.
|
|
204
|
+
const child = Children.toArray(children)[0];
|
|
128
205
|
const className = isValidElement<{ className?: string }>(child)
|
|
129
206
|
? child.props.className
|
|
130
207
|
: undefined;
|
|
@@ -137,14 +214,16 @@ export function MarkdownArticle({ content }: { content: string }) {
|
|
|
137
214
|
}
|
|
138
215
|
|
|
139
216
|
return (
|
|
140
|
-
<
|
|
141
|
-
{
|
|
142
|
-
|
|
217
|
+
<CodeCopy>
|
|
218
|
+
<pre className="code" {...preProps} data-language={language?.toUpperCase()}>
|
|
219
|
+
{children}
|
|
220
|
+
</pre>
|
|
221
|
+
</CodeCopy>
|
|
143
222
|
);
|
|
144
223
|
},
|
|
145
224
|
}}
|
|
146
225
|
>
|
|
147
|
-
{
|
|
226
|
+
{content}
|
|
148
227
|
</ReactMarkdown>
|
|
149
228
|
</div>
|
|
150
229
|
);
|