hd-garden 0.3.12 → 0.4.0
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/app/a2/[slug]/page.tsx +61 -0
- package/app/a2/page.tsx +27 -0
- package/app/admin/page.tsx +11 -15
- package/app/admin/ui/page.tsx +1 -1
- package/app/blog/[...path]/page.tsx +33 -22
- package/app/blog/page.tsx +6 -7
- package/app/columns/[column]/[...path]/page.tsx +33 -25
- package/app/columns/page.tsx +38 -52
- package/app/components/AdminDashboard.tsx +24 -34
- package/app/components/AdminUiPanels.tsx +5 -5
- package/app/components/AdminUiShell.tsx +7 -7
- package/app/components/ArticleMeasure.tsx +95 -0
- package/app/components/ArticleTitle.tsx +20 -0
- package/app/components/BlogArchive.tsx +106 -102
- package/app/components/GardenAdminGuide.tsx +38 -45
- package/app/components/GardenContentGuide.tsx +22 -106
- package/app/components/GardenMediaGuide.tsx +24 -45
- package/app/components/MarkdownArticle.tsx +145 -66
- package/app/components/MermaidDiagram.tsx +44 -71
- package/app/components/PageIntro.tsx +2 -2
- 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/UiStudio.tsx +2 -2
- 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/page.tsx +85 -223
- package/app/garden-modules.d.ts +2 -0
- package/app/globals.css +947 -88
- 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 +48 -5
- package/app/lib/fallback-site-config.ts +1 -1
- 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 +2 -2
- package/public/images/garden/admin-studio.png +0 -0
- 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 +2 -2
- package/templates/garden-blocks.md +128 -0
- package/templates/garden-snippets.plist +121 -0
- package/templates/site/site.config.yaml +1 -1
- 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
package/app/layout.tsx
CHANGED
|
@@ -2,6 +2,9 @@ import type { Metadata } from "next";
|
|
|
2
2
|
import { Geist_Mono } from "next/font/google";
|
|
3
3
|
import "katex/dist/katex.min.css";
|
|
4
4
|
import "./globals.css";
|
|
5
|
+
import "./styles/tokens.css";
|
|
6
|
+
import "./styles/a2.css";
|
|
7
|
+
import "./styles/shell.css";
|
|
5
8
|
import { SiteChrome } from "./components/SiteChrome";
|
|
6
9
|
import {
|
|
7
10
|
getSiteUrl,
|
|
@@ -82,6 +85,12 @@ export default function RootLayout({
|
|
|
82
85
|
return (
|
|
83
86
|
<html lang="zh-CN" suppressHydrationWarning>
|
|
84
87
|
<head>
|
|
88
|
+
<script
|
|
89
|
+
dangerouslySetInnerHTML={{
|
|
90
|
+
__html:
|
|
91
|
+
'(function(){try{var t=localStorage.getItem("theme");var d=t==="dark"||(!t&&matchMedia("(prefers-color-scheme: dark)").matches);document.documentElement.dataset.theme=d?"dark":"light";}catch(e){}})();',
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
85
94
|
{mentionEndpoint && <link rel="webmention" href={mentionEndpoint} />}
|
|
86
95
|
{analyticsDomain && (
|
|
87
96
|
<script
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const shanghaiOffsetMs = 8 * 60 * 60 * 1000;
|
|
2
|
+
|
|
3
|
+
function shanghaiDate(value: string) {
|
|
4
|
+
const date = new Date(value);
|
|
5
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
6
|
+
return new Date(date.getTime() + shanghaiOffsetMs);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Calendar date in Asia/Shanghai, the zone the demo uses for 「2026 年 9 月 18 日」. */
|
|
10
|
+
export function demoDateLabel(value: string) {
|
|
11
|
+
const date = shanghaiDate(value);
|
|
12
|
+
if (!date) return value;
|
|
13
|
+
return `${date.getUTCFullYear()} 年 ${date.getUTCMonth() + 1} 月 ${date.getUTCDate()} 日`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function demoDateTime(value: string) {
|
|
17
|
+
const date = shanghaiDate(value);
|
|
18
|
+
if (!date) return value.slice(0, 10);
|
|
19
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
20
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
21
|
+
return `${date.getUTCFullYear()}-${month}-${day}`;
|
|
22
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
|
|
5
|
+
const dir = path.join(process.cwd(), "parity", "articles");
|
|
6
|
+
|
|
7
|
+
const originals: Record<string, string> = {
|
|
8
|
+
"ai-agent-react-vs-workflow": "https://hedon.top/blog/ai-agent/ai-agent-react-vs-workflow/",
|
|
9
|
+
"rust-action-spinlock": "https://hedon.top/blog/rust-action-spinlock/",
|
|
10
|
+
"claude-code-source": "https://hedon.top/blog/claude-code-source/",
|
|
11
|
+
"go-gc-tri-color-marking": "https://hedon.top/blog/go/go-gc-tri-color-marking/",
|
|
12
|
+
"kv-cache-interactive": "https://hedon.top/blog/llm-kv-cache/",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type A2Article = {
|
|
16
|
+
slug: string;
|
|
17
|
+
title: string;
|
|
18
|
+
description: string;
|
|
19
|
+
date: string;
|
|
20
|
+
topic: string;
|
|
21
|
+
cover: string;
|
|
22
|
+
original: string;
|
|
23
|
+
content: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function a2Slugs() {
|
|
27
|
+
if (!fs.existsSync(dir)) return [];
|
|
28
|
+
return fs.readdirSync(dir).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -3)).sort();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function readA2Article(slug: string): A2Article | null {
|
|
32
|
+
const file = path.join(dir, `${slug}.md`);
|
|
33
|
+
if (!fs.existsSync(file)) return null;
|
|
34
|
+
const parsed = matter(fs.readFileSync(file, "utf8"));
|
|
35
|
+
const data = parsed.data ?? {};
|
|
36
|
+
const cover = typeof data.cover === "string" ? data.cover : "";
|
|
37
|
+
const date = data.date instanceof Date && !Number.isNaN(data.date.getTime())
|
|
38
|
+
? data.date.toISOString()
|
|
39
|
+
: typeof data.date === "string" ? data.date : "";
|
|
40
|
+
return {
|
|
41
|
+
slug,
|
|
42
|
+
title: String(data.title ?? slug),
|
|
43
|
+
description: String(data.description ?? ""),
|
|
44
|
+
date,
|
|
45
|
+
topic: String(data.topic ?? ""),
|
|
46
|
+
cover: cover && !/^https?:/iu.test(cover) ? `https://hedon.top/media/covers/${cover}` : cover,
|
|
47
|
+
original: originals[slug] ?? "",
|
|
48
|
+
content: parsed.content,
|
|
49
|
+
};
|
|
50
|
+
}
|
package/app/lib/content.ts
CHANGED
|
@@ -38,22 +38,65 @@ export const formatDate = (value: string, withTime = false) => {
|
|
|
38
38
|
}).format(date);
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
const NUMBERED_ARTICLE = /^#{2,3}[ \t]+\d/m;
|
|
42
|
+
|
|
43
|
+
/** An article that already writes its own heading numbers is not auto-numbered. */
|
|
44
|
+
export const articleUsesHeadingNumbers = (markdown: string) => NUMBERED_ARTICLE.test(markdown);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Prototype render: an article with no `##` treats `###` as the top section.
|
|
48
|
+
* Display level is `depth - base + 2`, and anything past h3 stays an h4.
|
|
49
|
+
*/
|
|
50
|
+
export const articleHeadingBase = (markdown: string) => (/^##[ \t]/m.test(markdown) ? 2 : 3);
|
|
51
|
+
|
|
52
|
+
export const displayHeadingLevel = (depth: number, base: number) => {
|
|
53
|
+
if (depth < 2 || depth > 4) return depth;
|
|
54
|
+
const level = depth - base + 2;
|
|
55
|
+
return level > 3 ? 4 : level;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Pull a leading `2.1` into the section marker and leave the heading sentence. */
|
|
59
|
+
export const splitHeadingNumber = (text: string) => {
|
|
60
|
+
const match = /^(\d+(?:\.\d+)*)\.?[ \t]+([\s\S]+)$/u.exec(text.trim());
|
|
61
|
+
return match ? { no: match[1], text: match[2] } : null;
|
|
62
|
+
};
|
|
63
|
+
|
|
41
64
|
/**
|
|
42
65
|
* Parses the same Markdown heading text that ReactMarkdown sends to
|
|
43
66
|
* rehype-slug. Every heading depth advances the slugger so headings outside
|
|
44
67
|
* the H2-H5 TOC range cannot shift the IDs shown in the directory.
|
|
68
|
+
* `no` follows the prototype: keep an author-written number, otherwise
|
|
69
|
+
* auto-number h2/h3 only when the article has no numbered headings.
|
|
45
70
|
*/
|
|
46
71
|
export const extractHeadings = (markdown: string) => {
|
|
47
72
|
const slugger = new GithubSlugger();
|
|
48
|
-
const
|
|
73
|
+
const numbered = articleUsesHeadingNumbers(markdown);
|
|
74
|
+
const base = articleHeadingBase(markdown);
|
|
75
|
+
const headings: Array<{ depth: number; text: string; id: string; no?: string }> = [];
|
|
49
76
|
const tree = unified().use(remarkParse).use(markdownPlugins).parse(markdown);
|
|
77
|
+
let h2 = 0;
|
|
78
|
+
let h3 = 0;
|
|
50
79
|
|
|
51
80
|
visit(tree, "heading", (node) => {
|
|
52
|
-
const
|
|
53
|
-
const id = slugger.slug(
|
|
54
|
-
if (
|
|
55
|
-
|
|
81
|
+
const raw = toString(node).trim();
|
|
82
|
+
const id = slugger.slug(raw);
|
|
83
|
+
if (!(raw && node.depth >= 2 && node.depth <= 5)) return;
|
|
84
|
+
const level = displayHeadingLevel(node.depth, base);
|
|
85
|
+
let text = raw;
|
|
86
|
+
let no: string | undefined;
|
|
87
|
+
if (level === 2 || level === 3) {
|
|
88
|
+
const split = splitHeadingNumber(raw);
|
|
89
|
+
if (split) {
|
|
90
|
+
no = split.no;
|
|
91
|
+
text = split.text;
|
|
92
|
+
if (level === 2) { h2 += 1; h3 = 0; }
|
|
93
|
+
else h3 += 1;
|
|
94
|
+
} else if (!numbered) {
|
|
95
|
+
if (level === 2) { h2 += 1; h3 = 0; no = String(h2); }
|
|
96
|
+
else { h3 += 1; no = h2 ? `${h2}.${h3}` : String(h3); }
|
|
97
|
+
}
|
|
56
98
|
}
|
|
99
|
+
headings.push({ depth: level, text, id, ...(no ? { no } : {}) });
|
|
57
100
|
});
|
|
58
101
|
|
|
59
102
|
return headings;
|
|
@@ -3,6 +3,7 @@ import remarkCjkFriendly from "remark-cjk-friendly";
|
|
|
3
3
|
import remarkBreaks from "remark-breaks";
|
|
4
4
|
import remarkMath from "remark-math";
|
|
5
5
|
import type { PluggableList } from "unified";
|
|
6
|
+
import { remarkGarden } from "./remark-garden.mjs";
|
|
6
7
|
|
|
7
8
|
// Keep article rendering and heading extraction on the same Markdown syntax.
|
|
8
9
|
export const markdownPlugins: PluggableList = [
|
|
@@ -10,4 +11,5 @@ export const markdownPlugins: PluggableList = [
|
|
|
10
11
|
remarkCjkFriendly,
|
|
11
12
|
remarkBreaks,
|
|
12
13
|
remarkMath,
|
|
14
|
+
remarkGarden,
|
|
13
15
|
];
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { toString } from "mdast-util-to-string";
|
|
2
|
+
|
|
3
|
+
const ALERT_TITLE = {
|
|
4
|
+
note: "提醒",
|
|
5
|
+
tip: "建议",
|
|
6
|
+
important: "重要",
|
|
7
|
+
warning: "警告",
|
|
8
|
+
caution: "注意",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const LEGACY_KIND = {
|
|
12
|
+
info: "note",
|
|
13
|
+
success: "tip",
|
|
14
|
+
warning: "warning",
|
|
15
|
+
danger: "caution",
|
|
16
|
+
note: "note",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const ALERT_ICON = {
|
|
20
|
+
note: '<svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="6.25"/><path d="M8 7.2v4"/><path d="M8 4.8v.1" stroke-width="2"/></svg>',
|
|
21
|
+
tip: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6 12.5h4M6.5 14.5h3M8 1.75a4.5 4.5 0 0 0-2.6 8.2c.4.3.6.7.6 1.2v.35h4v-.35c0-.5.2-.9.6-1.2A4.5 4.5 0 0 0 8 1.75Z"/></svg>',
|
|
22
|
+
important: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2.25 3.25a1 1 0 0 1 1-1h9.5a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7l-3 2.5v-2.5h-.75a1 1 0 0 1-1-1Z"/><path d="M8 4.75v3"/><path d="M8 9.4v.1" stroke-width="2"/></svg>',
|
|
23
|
+
warning: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 2 1.75 13.25h12.5Z"/><path d="M8 6.5v3.25"/><path d="M8 11.6v.1" stroke-width="2"/></svg>',
|
|
24
|
+
caution: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5.3 1.75h5.4l3.55 3.55v5.4l-3.55 3.55H5.3l-3.55-3.55V5.3Z"/><path d="M8 4.75v3.75"/><path d="M8 10.9v.1" stroke-width="2"/></svg>',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function meaningfulChildren(node) {
|
|
28
|
+
return (node.children ?? []).filter((child) => !(child.type === "text" && !String(child.value).trim()));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function visibleText(node) {
|
|
32
|
+
const parts = [];
|
|
33
|
+
const visit = (current) => {
|
|
34
|
+
if (!current || current.type === "html") return;
|
|
35
|
+
if (current.type === "text") {
|
|
36
|
+
parts.push(current.value);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
for (const child of current.children ?? []) visit(child);
|
|
40
|
+
};
|
|
41
|
+
visit(node);
|
|
42
|
+
return parts.join("").trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isBlankParagraph(node) {
|
|
46
|
+
return node?.type === "paragraph" && toString(node).trim() === "" && meaningfulChildren(node).length === 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function alertTitle(kind, label) {
|
|
50
|
+
return {
|
|
51
|
+
type: "paragraph",
|
|
52
|
+
data: { hName: "p", hProperties: { className: ["markdown-alert-title"] } },
|
|
53
|
+
children: [
|
|
54
|
+
{ type: "html", value: ALERT_ICON[kind] },
|
|
55
|
+
{ type: "text", value: label || ALERT_TITLE[kind] },
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const ALERT_KIND = {
|
|
61
|
+
NOTE: "note",
|
|
62
|
+
INFO: "note",
|
|
63
|
+
TIP: "tip",
|
|
64
|
+
SUCCESS: "tip",
|
|
65
|
+
IMPORTANT: "important",
|
|
66
|
+
WARNING: "warning",
|
|
67
|
+
CAUTION: "caution",
|
|
68
|
+
DANGER: "caution",
|
|
69
|
+
FAILURE: "caution",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function paragraphFirstLine(node) {
|
|
73
|
+
let line = "";
|
|
74
|
+
for (const child of node.children ?? []) {
|
|
75
|
+
if (child.type === "break") break;
|
|
76
|
+
if (child.type === "text") {
|
|
77
|
+
const newline = child.value.indexOf("\n");
|
|
78
|
+
line += newline >= 0 ? child.value.slice(0, newline) : child.value;
|
|
79
|
+
if (newline >= 0) break;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
line += toString(child);
|
|
83
|
+
}
|
|
84
|
+
return line;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function withoutMarkerLine(node) {
|
|
88
|
+
const children = [];
|
|
89
|
+
let skipping = true;
|
|
90
|
+
for (const child of node.children ?? []) {
|
|
91
|
+
if (!skipping) {
|
|
92
|
+
children.push(child);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (child.type === "break") {
|
|
96
|
+
skipping = false;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (child.type === "text") {
|
|
100
|
+
const newline = child.value.indexOf("\n");
|
|
101
|
+
if (newline >= 0) {
|
|
102
|
+
skipping = false;
|
|
103
|
+
const rest = child.value.slice(newline + 1);
|
|
104
|
+
if (rest) children.push({ type: "text", value: rest });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!children.some((child) => child.type !== "text" || child.value.trim())) return null;
|
|
109
|
+
return { type: "paragraph", children };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function gfmAlert(node) {
|
|
113
|
+
if (node?.type !== "paragraph") return null;
|
|
114
|
+
const match = /^\[!(NOTE|INFO|TIP|SUCCESS|IMPORTANT|WARNING|CAUTION|DANGER|FAILURE)\][ \t]*(.*)$/iu.exec(paragraphFirstLine(node).trim());
|
|
115
|
+
if (!match) return null;
|
|
116
|
+
return {
|
|
117
|
+
kind: ALERT_KIND[match[1].toUpperCase()],
|
|
118
|
+
label: match[2].trim(),
|
|
119
|
+
body: withoutMarkerLine(node),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function legacyAlert(node) {
|
|
124
|
+
if (node?.type !== "paragraph" || visibleText(node)) return null;
|
|
125
|
+
const html = (node.children ?? []).filter((child) => child.type === "html").map((child) => child.value).join("");
|
|
126
|
+
const match = /garden-callout-marker--(info|success|warning|danger|note)/u.exec(html);
|
|
127
|
+
if (!match) return null;
|
|
128
|
+
return LEGACY_KIND[match[1]] || "note";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function strongOnlyText(node) {
|
|
132
|
+
if (node?.type !== "paragraph") return "";
|
|
133
|
+
const children = meaningfulChildren(node);
|
|
134
|
+
if (children.length !== 1 || children[0].type !== "strong") return "";
|
|
135
|
+
return toString(children[0]).trim();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function rewriteAlert(node) {
|
|
139
|
+
const children = node.children ?? [];
|
|
140
|
+
const gfm = gfmAlert(children[0]);
|
|
141
|
+
const legacy = gfm ? null : legacyAlert(children[0]);
|
|
142
|
+
if (!gfm && !legacy) return;
|
|
143
|
+
const kind = gfm ? gfm.kind : legacy;
|
|
144
|
+
let label = gfm ? gfm.label : "";
|
|
145
|
+
let rest = gfm?.body ? [gfm.body, ...children.slice(1)] : children.slice(1);
|
|
146
|
+
if (!label) {
|
|
147
|
+
const custom = strongOnlyText(rest.find((child) => !isBlankParagraph(child)));
|
|
148
|
+
if (custom) {
|
|
149
|
+
label = custom;
|
|
150
|
+
const index = rest.findIndex((child) => !isBlankParagraph(child));
|
|
151
|
+
rest = rest.filter((_, childIndex) => childIndex !== index);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
while (rest.length && isBlankParagraph(rest[0])) rest.shift();
|
|
155
|
+
node.children = [alertTitle(kind, label), ...rest];
|
|
156
|
+
node.data ??= {};
|
|
157
|
+
node.data.hProperties ??= {};
|
|
158
|
+
const existing = node.data.hProperties.className;
|
|
159
|
+
const classes = Array.isArray(existing) ? existing : existing ? [existing] : [];
|
|
160
|
+
node.data.hProperties.className = [...classes, "markdown-alert", `markdown-alert-${kind}`];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function figureCaption(image) {
|
|
164
|
+
const title = String(image.title ?? "").trim();
|
|
165
|
+
const alt = String(image.alt ?? "").trim();
|
|
166
|
+
if (title) return title;
|
|
167
|
+
if (alt && !/^image|^截图|^img/iu.test(alt)) return alt;
|
|
168
|
+
return "";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function asFigure(node) {
|
|
172
|
+
if (node?.type !== "paragraph" || node.data?.hName) return null;
|
|
173
|
+
const children = meaningfulChildren(node);
|
|
174
|
+
if (children.length !== 1 || children[0].type !== "image") return null;
|
|
175
|
+
const image = children[0];
|
|
176
|
+
const caption = figureCaption(image);
|
|
177
|
+
const card = {
|
|
178
|
+
type: "paragraph",
|
|
179
|
+
data: { hName: "div", hProperties: { className: ["fig-card"] } },
|
|
180
|
+
children: [image],
|
|
181
|
+
};
|
|
182
|
+
const figureChildren = caption
|
|
183
|
+
? [card, {
|
|
184
|
+
type: "paragraph",
|
|
185
|
+
data: { hName: "figcaption" },
|
|
186
|
+
children: [{ type: "text", value: caption }],
|
|
187
|
+
}]
|
|
188
|
+
: [card];
|
|
189
|
+
return {
|
|
190
|
+
type: "paragraph",
|
|
191
|
+
data: { hName: "figure", hProperties: { className: ["fig"] } },
|
|
192
|
+
children: figureChildren,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function walk(node) {
|
|
197
|
+
if (!node || typeof node !== "object" || !Array.isArray(node.children)) return;
|
|
198
|
+
if (node.type === "blockquote") rewriteAlert(node);
|
|
199
|
+
node.children = node.children.map((child) => asFigure(child) ?? child);
|
|
200
|
+
for (const child of node.children) walk(child);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Demo alert cards and captioned figures. Article rendering only. */
|
|
204
|
+
export function remarkA2() {
|
|
205
|
+
return (tree) => {
|
|
206
|
+
walk(tree);
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { diffToNewCode, protectMathIdentifiers } from "../../ui/garden/enhance.mjs";
|
|
2
|
+
import { classifyFence } from "../../ui/garden/parse.mjs";
|
|
3
|
+
|
|
4
|
+
function gardenElement(payload) {
|
|
5
|
+
return {
|
|
6
|
+
type: "gardenBlock",
|
|
7
|
+
data: {
|
|
8
|
+
hName: "garden-block",
|
|
9
|
+
hProperties: { dataPayload: encodeURIComponent(JSON.stringify(payload)) },
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function codeDiffElement(code, diff) {
|
|
15
|
+
return {
|
|
16
|
+
type: "gardenCodeDiff",
|
|
17
|
+
data: {
|
|
18
|
+
hName: "garden-codediff",
|
|
19
|
+
hProperties: {
|
|
20
|
+
dataNew: encodeURIComponent(diffToNewCode(diff.value)),
|
|
21
|
+
dataDiff: encodeURIComponent(diff.value),
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
children: [code],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fenceNodeToRecord(node) {
|
|
29
|
+
return { lang: String(node.lang || "").toLowerCase(), meta: String(node.meta || ""), info: `${node.lang || ""} ${node.meta || ""}`.trim() };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function rewriteChildren(node) {
|
|
33
|
+
if (!Array.isArray(node.children)) return;
|
|
34
|
+
const next = [];
|
|
35
|
+
for (let index = 0; index < node.children.length; index += 1) {
|
|
36
|
+
const child = node.children[index];
|
|
37
|
+
const following = node.children[index + 1];
|
|
38
|
+
if (child?.type === "code" && following?.type === "code" && String(following.lang || "").toLowerCase() === "diff") {
|
|
39
|
+
const current = fenceNodeToRecord(child);
|
|
40
|
+
const kind = classifyFence({ lang: current.lang, meta: current.meta, info: current.info });
|
|
41
|
+
if (kind.kind === "code") {
|
|
42
|
+
next.push(codeDiffElement(child, following));
|
|
43
|
+
index += 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (child?.type === "code") {
|
|
48
|
+
const record = fenceNodeToRecord(child);
|
|
49
|
+
const kind = classifyFence({ ...record, body: child.value, line: 1, endLine: 1 });
|
|
50
|
+
if (kind.kind === "legacy") {
|
|
51
|
+
next.push(gardenElement({ legacy: true, from: kind.lang || record.lang, suggestion: kind.suggestion }));
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (kind.kind === "garden" && kind.block) {
|
|
55
|
+
next.push(gardenElement(kind.block));
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
next.push(child);
|
|
60
|
+
}
|
|
61
|
+
node.children = next;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function walk(node) {
|
|
65
|
+
if (!node || typeof node !== "object") return;
|
|
66
|
+
if (node.type === "math" || node.type === "inlineMath") {
|
|
67
|
+
node.value = protectMathIdentifiers(node.value);
|
|
68
|
+
}
|
|
69
|
+
if (Array.isArray(node.children)) {
|
|
70
|
+
for (const child of node.children) walk(child);
|
|
71
|
+
rewriteChildren(node);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function remarkGarden() {
|
|
76
|
+
return (tree) => {
|
|
77
|
+
walk(tree);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function readGardenPayload(value) {
|
|
82
|
+
if (!value) return null;
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(decodeURIComponent(String(value)));
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|