hd-garden 0.3.13 → 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/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/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 +48 -5
- 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
package/scripts/check-post.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { parse as parseYaml } from "yaml";
|
|
|
5
5
|
import { canonicalSiteDate } from "./site-date.mjs";
|
|
6
6
|
import { gardenSiteRoot } from "./garden-paths.mjs";
|
|
7
7
|
import { checkUiBlock } from "../ui/core/validate.mjs";
|
|
8
|
+
import { checkGardenMarkdown } from "../ui/garden/check.mjs";
|
|
8
9
|
|
|
9
10
|
const siteRoot = gardenSiteRoot();
|
|
10
11
|
const calloutTypes = new Set([
|
|
@@ -18,7 +19,7 @@ const calloutTypes = new Set([
|
|
|
18
19
|
"DANGER",
|
|
19
20
|
"FAILURE",
|
|
20
21
|
]);
|
|
21
|
-
const authorDate = /^(?:\d{4}-\d{2}-\d{2}
|
|
22
|
+
const authorDate = /^(?:\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?)$/u;
|
|
22
23
|
const matterOptions = {
|
|
23
24
|
engines: {
|
|
24
25
|
yaml: {
|
|
@@ -202,7 +203,7 @@ const checkUi = (errors, warnings, body, markdownPath, sourcePath, line) => {
|
|
|
202
203
|
|
|
203
204
|
const scanBody = (errors, content, markdownPath, sourcePath, { warnings = [], draft = false } = {}) => {
|
|
204
205
|
let fence = null;
|
|
205
|
-
let previousDepth =
|
|
206
|
+
let previousDepth = 2;
|
|
206
207
|
const lines = content.split(/\r?\n/u);
|
|
207
208
|
lines.forEach((line, index) => {
|
|
208
209
|
const lineNumber = index + 1;
|
|
@@ -277,8 +278,11 @@ const checkFile = (filePath) => {
|
|
|
277
278
|
if (data.slug !== undefined) {
|
|
278
279
|
error(errors, "unexpected-slug", 1, `${sourcePath}: 不要写 slug,文件名就是标识`);
|
|
279
280
|
}
|
|
280
|
-
|
|
281
|
-
|
|
281
|
+
const dateText = data.date instanceof Date
|
|
282
|
+
? (Number.isNaN(data.date.getTime()) ? "" : data.date.toISOString())
|
|
283
|
+
: typeof data.date === "string" ? data.date.trim() : "";
|
|
284
|
+
if (!authorDate.test(dateText) || !canonicalSiteDate(data.date)) {
|
|
285
|
+
error(errors, "date-format", 1, `${sourcePath}: date 必须是 2026-09-18、2026-09-18 14:39:00,或带时区的 ISO 时间`);
|
|
282
286
|
}
|
|
283
287
|
if (typeof data.cover === "string" && data.cover.trim() && !isExternalAsset(data.cover.trim())) {
|
|
284
288
|
const coverPath = path.join(siteRoot, "content", "covers", data.cover.trim());
|
|
@@ -289,6 +293,16 @@ const checkFile = (filePath) => {
|
|
|
289
293
|
checkReferences(data, sourcePath, errors);
|
|
290
294
|
const draft = data.draft === true || (typeof data.publishAt === "string" && Date.parse(canonicalSiteDate(data.publishAt) ?? "") > Date.now());
|
|
291
295
|
scanBody(errors, parsed.content, absolute, sourcePath, { warnings, draft });
|
|
296
|
+
const raw = fs.readFileSync(absolute, "utf8");
|
|
297
|
+
const titleLine = Math.max(1, raw.split(/\r?\n/u).findIndex((line) => /^title:/u.test(line)) + 1);
|
|
298
|
+
for (const item of checkGardenMarkdown(raw, {
|
|
299
|
+
draft,
|
|
300
|
+
title: typeof data.title === "string" ? data.title : "",
|
|
301
|
+
titleLine,
|
|
302
|
+
})) {
|
|
303
|
+
const target = item.severity === "warning" ? warnings : errors;
|
|
304
|
+
target.push({ code: item.code, line: item.line, message: `${sourcePath}: ${item.message} 建议:${item.suggestion}` });
|
|
305
|
+
}
|
|
292
306
|
return { file: sourcePath, errors, warnings };
|
|
293
307
|
};
|
|
294
308
|
|
package/scripts/new-post.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { lstat, mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { formatAuthorDate } from "./site-date.mjs";
|
|
5
|
+
import { cheatSheet } from "../ui/garden/catalog.mjs";
|
|
5
6
|
|
|
6
7
|
const root = path.resolve(process.env.GARDEN_SITE || process.cwd());
|
|
7
8
|
const option = (name) => (process.env[`GARDEN_NEW_${name}`] ?? "").trim();
|
|
@@ -51,6 +52,8 @@ async function main() {
|
|
|
51
52
|
"",
|
|
52
53
|
"从这里开始写正文。",
|
|
53
54
|
"",
|
|
55
|
+
cheatSheet(),
|
|
56
|
+
"",
|
|
54
57
|
].join("\n");
|
|
55
58
|
try {
|
|
56
59
|
await writeFile(destination, markdown, { flag: "wx" });
|
package/site.config.yaml
CHANGED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Garden 块模板
|
|
2
|
+
|
|
3
|
+
从这里复制。触发词和 VS Code 片段、macOS 文本替换是同一份目录生成的。
|
|
4
|
+
|
|
5
|
+
## 想法 · 想法
|
|
6
|
+
|
|
7
|
+
还不确定用哪个组件时,写一句想让读者看出什么。
|
|
8
|
+
|
|
9
|
+
不该用:发布前必须编译掉。草稿里可以留着。
|
|
10
|
+
|
|
11
|
+
```garden
|
|
12
|
+
让读者……;想让读者看出……
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## card · 资源卡
|
|
16
|
+
|
|
17
|
+
一篇论文、一个仓库或一本书,读者需要标题、链接和一句说明。
|
|
18
|
+
|
|
19
|
+
不该用:正文里已经有这句话时,不要再包一张卡。
|
|
20
|
+
|
|
21
|
+
```garden card
|
|
22
|
+
url: https://arxiv.org/abs/2309.06180
|
|
23
|
+
title: PagedAttention
|
|
24
|
+
description: vLLM 的分页 KV 管理。
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## stat · 指标
|
|
28
|
+
|
|
29
|
+
一个短指标:标签、数字、可选的一句说明和来源。
|
|
30
|
+
|
|
31
|
+
不该用:数字需要读者自己代入参数时,写成想法,不要做静态指标。
|
|
32
|
+
|
|
33
|
+
```garden stat
|
|
34
|
+
items:
|
|
35
|
+
- label: Llama-2-13B · 4K 上下文
|
|
36
|
+
value: 3.2
|
|
37
|
+
unit: GiB
|
|
38
|
+
source: https://arxiv.org/abs/2309.06180
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## compare · 对比
|
|
42
|
+
|
|
43
|
+
两三件东西并排,每格大约 60 字以内。
|
|
44
|
+
|
|
45
|
+
不该用:任何一格超过约 60 字时,用原文的列表,不要并排。
|
|
46
|
+
|
|
47
|
+
```garden compare
|
|
48
|
+
items:
|
|
49
|
+
- title: 自旋锁
|
|
50
|
+
text: 临界区极短时忙等,不进入休眠。
|
|
51
|
+
- title: 互斥锁
|
|
52
|
+
text: 争用久就休眠,避免空转占满 CPU。
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## timeline · 时间线
|
|
56
|
+
|
|
57
|
+
版本演进或一段历史,每条有日期和一句说明。
|
|
58
|
+
|
|
59
|
+
不该用:没有时间顺序时,用普通列表。
|
|
60
|
+
|
|
61
|
+
```garden timeline
|
|
62
|
+
items:
|
|
63
|
+
- date: 2015
|
|
64
|
+
title: 并发标记
|
|
65
|
+
text: 暂停时间降下来。
|
|
66
|
+
- date: 2017
|
|
67
|
+
title: 栈重扫移出 STW
|
|
68
|
+
text: 停顿通常低于 100 微秒。
|
|
69
|
+
- date: 2022
|
|
70
|
+
title: 软内存上限
|
|
71
|
+
text: 运行时可以设 GOMEMLIMIT。
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## chart · 图表
|
|
75
|
+
|
|
76
|
+
一组已经算好的数字,读者不需要拧参数。
|
|
77
|
+
|
|
78
|
+
不该用:数字会随读者的选择变化时,写成想法,编译成 interactive。
|
|
79
|
+
|
|
80
|
+
```garden chart
|
|
81
|
+
type: bar
|
|
82
|
+
title: 示例延迟
|
|
83
|
+
x: 方案
|
|
84
|
+
series:
|
|
85
|
+
- name: 延迟
|
|
86
|
+
data: [120, 85]
|
|
87
|
+
unit: ms
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## figure · 结构图
|
|
91
|
+
|
|
92
|
+
一张结构图,读者点一个部分就看到对应的一句说明。
|
|
93
|
+
|
|
94
|
+
不该用:作者已经用 mermaid 画了整张流程时,保留 mermaid,不要拆开重画。
|
|
95
|
+
|
|
96
|
+
```garden figure
|
|
97
|
+
image: ./figure.svg
|
|
98
|
+
parts:
|
|
99
|
+
- name: 全部
|
|
100
|
+
text: 整张图。
|
|
101
|
+
- name: 标记
|
|
102
|
+
text: 这一步只做标记。
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## html · 自定义 HTML
|
|
106
|
+
|
|
107
|
+
目录里的组件画不出来的示意图或小工具,写在同名目录的 HTML 里。
|
|
108
|
+
|
|
109
|
+
不该用:能用 card、chart、figure 或正文说清时,不要写 HTML。
|
|
110
|
+
|
|
111
|
+
```garden html
|
|
112
|
+
src: ./demo.html
|
|
113
|
+
title: 示意图
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## interactive · 交互
|
|
117
|
+
|
|
118
|
+
读者需要拧参数或翻状态才能看懂,由想法编译而来。
|
|
119
|
+
|
|
120
|
+
不该用:一次乘法、把散文切成步骤,都不要做成交互。
|
|
121
|
+
|
|
122
|
+
```garden interactive #kv-memory
|
|
123
|
+
caption: 显存与 batch、序列长度都成正比。
|
|
124
|
+
intent: |
|
|
125
|
+
让读者拧 batch,看 KV 显存。
|
|
126
|
+
---
|
|
127
|
+
<State batch={1} />
|
|
128
|
+
```
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<array>
|
|
5
|
+
<dict>
|
|
6
|
+
<key>phrase</key>
|
|
7
|
+
<string>```garden
|
|
8
|
+
让读者……;想让读者看出……
|
|
9
|
+
```</string>
|
|
10
|
+
<key>shortcut</key>
|
|
11
|
+
<string>;garden想法</string>
|
|
12
|
+
</dict>
|
|
13
|
+
<dict>
|
|
14
|
+
<key>phrase</key>
|
|
15
|
+
<string>```garden
|
|
16
|
+
让读者……;想让读者看出……
|
|
17
|
+
```</string>
|
|
18
|
+
<key>shortcut</key>
|
|
19
|
+
<string>;gidea</string>
|
|
20
|
+
</dict>
|
|
21
|
+
<dict>
|
|
22
|
+
<key>phrase</key>
|
|
23
|
+
<string>```garden card
|
|
24
|
+
url: https://arxiv.org/abs/2309.06180
|
|
25
|
+
title: PagedAttention
|
|
26
|
+
description: vLLM 的分页 KV 管理。
|
|
27
|
+
```</string>
|
|
28
|
+
<key>shortcut</key>
|
|
29
|
+
<string>;gcard</string>
|
|
30
|
+
</dict>
|
|
31
|
+
<dict>
|
|
32
|
+
<key>phrase</key>
|
|
33
|
+
<string>```garden stat
|
|
34
|
+
items:
|
|
35
|
+
- label: Llama-2-13B · 4K 上下文
|
|
36
|
+
value: 3.2
|
|
37
|
+
unit: GiB
|
|
38
|
+
source: https://arxiv.org/abs/2309.06180
|
|
39
|
+
```</string>
|
|
40
|
+
<key>shortcut</key>
|
|
41
|
+
<string>;gstat</string>
|
|
42
|
+
</dict>
|
|
43
|
+
<dict>
|
|
44
|
+
<key>phrase</key>
|
|
45
|
+
<string>```garden compare
|
|
46
|
+
items:
|
|
47
|
+
- title: 自旋锁
|
|
48
|
+
text: 临界区极短时忙等,不进入休眠。
|
|
49
|
+
- title: 互斥锁
|
|
50
|
+
text: 争用久就休眠,避免空转占满 CPU。
|
|
51
|
+
```</string>
|
|
52
|
+
<key>shortcut</key>
|
|
53
|
+
<string>;gcompare</string>
|
|
54
|
+
</dict>
|
|
55
|
+
<dict>
|
|
56
|
+
<key>phrase</key>
|
|
57
|
+
<string>```garden timeline
|
|
58
|
+
items:
|
|
59
|
+
- date: 2015
|
|
60
|
+
title: 并发标记
|
|
61
|
+
text: 暂停时间降下来。
|
|
62
|
+
- date: 2017
|
|
63
|
+
title: 栈重扫移出 STW
|
|
64
|
+
text: 停顿通常低于 100 微秒。
|
|
65
|
+
- date: 2022
|
|
66
|
+
title: 软内存上限
|
|
67
|
+
text: 运行时可以设 GOMEMLIMIT。
|
|
68
|
+
```</string>
|
|
69
|
+
<key>shortcut</key>
|
|
70
|
+
<string>;gtimeline</string>
|
|
71
|
+
</dict>
|
|
72
|
+
<dict>
|
|
73
|
+
<key>phrase</key>
|
|
74
|
+
<string>```garden chart
|
|
75
|
+
type: bar
|
|
76
|
+
title: 示例延迟
|
|
77
|
+
x: 方案
|
|
78
|
+
series:
|
|
79
|
+
- name: 延迟
|
|
80
|
+
data: [120, 85]
|
|
81
|
+
unit: ms
|
|
82
|
+
```</string>
|
|
83
|
+
<key>shortcut</key>
|
|
84
|
+
<string>;gchart</string>
|
|
85
|
+
</dict>
|
|
86
|
+
<dict>
|
|
87
|
+
<key>phrase</key>
|
|
88
|
+
<string>```garden figure
|
|
89
|
+
image: ./figure.svg
|
|
90
|
+
parts:
|
|
91
|
+
- name: 全部
|
|
92
|
+
text: 整张图。
|
|
93
|
+
- name: 标记
|
|
94
|
+
text: 这一步只做标记。
|
|
95
|
+
```</string>
|
|
96
|
+
<key>shortcut</key>
|
|
97
|
+
<string>;gfigure</string>
|
|
98
|
+
</dict>
|
|
99
|
+
<dict>
|
|
100
|
+
<key>phrase</key>
|
|
101
|
+
<string>```garden html
|
|
102
|
+
src: ./demo.html
|
|
103
|
+
title: 示意图
|
|
104
|
+
```</string>
|
|
105
|
+
<key>shortcut</key>
|
|
106
|
+
<string>;ghtml</string>
|
|
107
|
+
</dict>
|
|
108
|
+
<dict>
|
|
109
|
+
<key>phrase</key>
|
|
110
|
+
<string>```garden interactive #kv-memory
|
|
111
|
+
caption: 显存与 batch、序列长度都成正比。
|
|
112
|
+
intent: |
|
|
113
|
+
让读者拧 batch,看 KV 显存。
|
|
114
|
+
---
|
|
115
|
+
<State batch={1} />
|
|
116
|
+
```</string>
|
|
117
|
+
<key>shortcut</key>
|
|
118
|
+
<string>;ginteractive</string>
|
|
119
|
+
</dict>
|
|
120
|
+
</array>
|
|
121
|
+
</plist>
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Author-facing Garden blocks. One catalog feeds the parser, checker,
|
|
3
|
+
* snippets, Typora templates and the component list.
|
|
4
|
+
* Names describe what the reader sees. English only.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const GARDEN_NAMES = ["card", "stat", "compare", "timeline", "chart", "figure", "html", "interactive"];
|
|
8
|
+
|
|
9
|
+
export const LEGACY_FENCES = {
|
|
10
|
+
embed: { name: "card", suggestion: "`embed` 已改为 `garden card`" },
|
|
11
|
+
widget: { name: "html", suggestion: "`widget` 已改为 `garden html`" },
|
|
12
|
+
intent: { name: "", suggestion: "`intent` 已改为 `garden`(想法,发布前编译)" },
|
|
13
|
+
ui: { name: "interactive", suggestion: "`ui` 已改为 `garden interactive`" },
|
|
14
|
+
"ui-error": { name: "interactive", suggestion: "`ui` 已改为 `garden interactive`" },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const CATALOG = {
|
|
18
|
+
card: {
|
|
19
|
+
label: "资源卡",
|
|
20
|
+
when: "一篇论文、一个仓库或一本书,读者需要标题、链接和一句说明。",
|
|
21
|
+
whenNot: "正文里已经有这句话时,不要再包一张卡。",
|
|
22
|
+
keys: ["url", "title", "description", "image", "alt", "points", "meta", "text", "source", "site", "items"],
|
|
23
|
+
example: `\`\`\`garden card
|
|
24
|
+
url: https://arxiv.org/abs/2309.06180
|
|
25
|
+
title: PagedAttention
|
|
26
|
+
description: vLLM 的分页 KV 管理。
|
|
27
|
+
\`\`\``,
|
|
28
|
+
triggers: ["gcard"],
|
|
29
|
+
},
|
|
30
|
+
stat: {
|
|
31
|
+
label: "指标",
|
|
32
|
+
when: "一个短指标:标签、数字、可选的一句说明和来源。",
|
|
33
|
+
whenNot: "数字需要读者自己代入参数时,写成想法,不要做静态指标。",
|
|
34
|
+
keys: ["items", "label", "value", "unit", "caption", "source", "text"],
|
|
35
|
+
example: `\`\`\`garden stat
|
|
36
|
+
items:
|
|
37
|
+
- label: Llama-2-13B · 4K 上下文
|
|
38
|
+
value: 3.2
|
|
39
|
+
unit: GiB
|
|
40
|
+
source: https://arxiv.org/abs/2309.06180
|
|
41
|
+
\`\`\``,
|
|
42
|
+
triggers: ["gstat"],
|
|
43
|
+
},
|
|
44
|
+
compare: {
|
|
45
|
+
label: "对比",
|
|
46
|
+
when: "两三件东西并排,每格大约 60 字以内。",
|
|
47
|
+
whenNot: "任何一格超过约 60 字时,用原文的列表,不要并排。",
|
|
48
|
+
keys: ["items"],
|
|
49
|
+
itemKeys: ["title", "text"],
|
|
50
|
+
example: `\`\`\`garden compare
|
|
51
|
+
items:
|
|
52
|
+
- title: 自旋锁
|
|
53
|
+
text: 临界区极短时忙等,不进入休眠。
|
|
54
|
+
- title: 互斥锁
|
|
55
|
+
text: 争用久就休眠,避免空转占满 CPU。
|
|
56
|
+
\`\`\``,
|
|
57
|
+
triggers: ["gcompare"],
|
|
58
|
+
},
|
|
59
|
+
timeline: {
|
|
60
|
+
label: "时间线",
|
|
61
|
+
when: "版本演进或一段历史,每条有日期和一句说明。",
|
|
62
|
+
whenNot: "没有时间顺序时,用普通列表。",
|
|
63
|
+
keys: ["title", "items"],
|
|
64
|
+
itemKeys: ["date", "title", "text", "source"],
|
|
65
|
+
example: `\`\`\`garden timeline
|
|
66
|
+
items:
|
|
67
|
+
- date: 2015
|
|
68
|
+
title: 并发标记
|
|
69
|
+
text: 暂停时间降下来。
|
|
70
|
+
- date: 2017
|
|
71
|
+
title: 栈重扫移出 STW
|
|
72
|
+
text: 停顿通常低于 100 微秒。
|
|
73
|
+
- date: 2022
|
|
74
|
+
title: 软内存上限
|
|
75
|
+
text: 运行时可以设 GOMEMLIMIT。
|
|
76
|
+
\`\`\``,
|
|
77
|
+
triggers: ["gtimeline"],
|
|
78
|
+
},
|
|
79
|
+
chart: {
|
|
80
|
+
label: "图表",
|
|
81
|
+
when: "一组已经算好的数字,读者不需要拧参数。",
|
|
82
|
+
whenNot: "数字会随读者的选择变化时,写成想法,编译成 interactive。",
|
|
83
|
+
keys: ["type", "title", "x", "series", "data", "unit", "caption", "xLabel", "yLabel"],
|
|
84
|
+
example: `\`\`\`garden chart
|
|
85
|
+
type: bar
|
|
86
|
+
title: 示例延迟
|
|
87
|
+
x: 方案
|
|
88
|
+
series:
|
|
89
|
+
- name: 延迟
|
|
90
|
+
data: [120, 85]
|
|
91
|
+
unit: ms
|
|
92
|
+
\`\`\``,
|
|
93
|
+
triggers: ["gchart"],
|
|
94
|
+
},
|
|
95
|
+
figure: {
|
|
96
|
+
label: "结构图",
|
|
97
|
+
when: "一张结构图,读者点一个部分就看到对应的一句说明。",
|
|
98
|
+
whenNot: "作者已经用 mermaid 画了整张流程时,保留 mermaid,不要拆开重画。",
|
|
99
|
+
keys: ["image", "alt", "mermaid", "svg", "parts", "caption"],
|
|
100
|
+
example: `\`\`\`garden figure
|
|
101
|
+
image: ./figure.svg
|
|
102
|
+
parts:
|
|
103
|
+
- name: 全部
|
|
104
|
+
text: 整张图。
|
|
105
|
+
- name: 标记
|
|
106
|
+
text: 这一步只做标记。
|
|
107
|
+
\`\`\``,
|
|
108
|
+
triggers: ["gfigure"],
|
|
109
|
+
},
|
|
110
|
+
html: {
|
|
111
|
+
label: "自定义 HTML",
|
|
112
|
+
when: "目录里的组件画不出来的示意图或小工具,写在同名目录的 HTML 里。",
|
|
113
|
+
whenNot: "能用 card、chart、figure 或正文说清时,不要写 HTML。",
|
|
114
|
+
keys: ["src", "code", "title", "height"],
|
|
115
|
+
example: `\`\`\`garden html
|
|
116
|
+
src: ./demo.html
|
|
117
|
+
title: 示意图
|
|
118
|
+
\`\`\``,
|
|
119
|
+
triggers: ["ghtml"],
|
|
120
|
+
},
|
|
121
|
+
interactive: {
|
|
122
|
+
label: "交互",
|
|
123
|
+
when: "读者需要拧参数或翻状态才能看懂,由想法编译而来。",
|
|
124
|
+
whenNot: "一次乘法、把散文切成步骤,都不要做成交互。",
|
|
125
|
+
keys: ["id", "caption", "intent", "expect"],
|
|
126
|
+
example: `\`\`\`garden interactive #kv-memory
|
|
127
|
+
caption: 显存与 batch、序列长度都成正比。
|
|
128
|
+
intent: |
|
|
129
|
+
让读者拧 batch,看 KV 显存。
|
|
130
|
+
---
|
|
131
|
+
<State batch={1} />
|
|
132
|
+
\`\`\``,
|
|
133
|
+
triggers: ["ginteractive"],
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const IDEA = {
|
|
138
|
+
label: "想法",
|
|
139
|
+
when: "还不确定用哪个组件时,写一句想让读者看出什么。",
|
|
140
|
+
whenNot: "发布前必须编译掉。草稿里可以留着。",
|
|
141
|
+
example: `\`\`\`garden
|
|
142
|
+
让读者……;想让读者看出……
|
|
143
|
+
\`\`\``,
|
|
144
|
+
triggers: ["garden想法", "gidea"],
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export function legacyOf(lang) {
|
|
148
|
+
const key = String(lang || "").toLowerCase();
|
|
149
|
+
if (LEGACY_FENCES[key]) return { lang: key, ...LEGACY_FENCES[key] };
|
|
150
|
+
if (key.startsWith("garden:")) {
|
|
151
|
+
const name = key.slice("garden:".length);
|
|
152
|
+
return { lang: key, name, suggestion: `\`${key}\` 已改为 \`garden ${name}\`` };
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function knownName(name) {
|
|
158
|
+
return GARDEN_NAMES.includes(name);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function distance(left, right) {
|
|
162
|
+
const a = [...left];
|
|
163
|
+
const b = [...right];
|
|
164
|
+
const row = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
165
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
166
|
+
let previous = i;
|
|
167
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
168
|
+
const next = a[i - 1] === b[j - 1] ? row[j - 1] : Math.min(row[j - 1], row[j], previous) + 1;
|
|
169
|
+
row[j - 1] = previous;
|
|
170
|
+
previous = next;
|
|
171
|
+
}
|
|
172
|
+
row[b.length] = previous;
|
|
173
|
+
}
|
|
174
|
+
return row[b.length];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function suggestName(name) {
|
|
178
|
+
let best = null;
|
|
179
|
+
let score = 3;
|
|
180
|
+
for (const known of GARDEN_NAMES) {
|
|
181
|
+
const gap = distance(String(name || ""), known);
|
|
182
|
+
if (gap < score) {
|
|
183
|
+
score = gap;
|
|
184
|
+
best = known;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return best;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function cheatSheet() {
|
|
191
|
+
const blocks = [
|
|
192
|
+
IDEA.example,
|
|
193
|
+
...GARDEN_NAMES.map((name) => CATALOG[name].example),
|
|
194
|
+
];
|
|
195
|
+
return ["<!-- 组件速查。复制需要的一块,删掉这段注释。", ...blocks.flatMap((example) => ["", example]), "-->"].join("\n");
|
|
196
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { CATALOG, suggestName } from "./catalog.mjs";
|
|
2
|
+
import { isProseSteps, isTrivialInteractive } from "./enhance.mjs";
|
|
3
|
+
import { blanksBetween, classifyFence, isFullCodeFence, scanFences } from "./parse.mjs";
|
|
4
|
+
import { splitTitle } from "./title.mjs";
|
|
5
|
+
|
|
6
|
+
const COMPARE_LIMIT = 60;
|
|
7
|
+
|
|
8
|
+
function cellText(item) {
|
|
9
|
+
if (!item || typeof item !== "object") return "";
|
|
10
|
+
const parts = [];
|
|
11
|
+
if (typeof item.text === "string") parts.push(item.text);
|
|
12
|
+
if (Array.isArray(item.text)) parts.push(...item.text.map(String));
|
|
13
|
+
for (const [key, value] of Object.entries(item)) {
|
|
14
|
+
if (key === "title" || key === "text") continue;
|
|
15
|
+
if (typeof value === "string") parts.push(value);
|
|
16
|
+
}
|
|
17
|
+
return parts.join("");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function compareTooLong(props) {
|
|
21
|
+
const items = Array.isArray(props?.items) ? props.items : [];
|
|
22
|
+
return items.some((item) => [...cellText(item).trim()].length > COMPARE_LIMIT);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Diagnostics for one Markdown file.
|
|
27
|
+
* `line` is a 1-based line in `markdown` (front matter included when `title` is passed with `titleLine`).
|
|
28
|
+
*/
|
|
29
|
+
export function checkGardenMarkdown(markdown, { draft = false, title = "", titleLine = 1 } = {}) {
|
|
30
|
+
const diagnostics = [];
|
|
31
|
+
const push = (line, code, message, suggestion, severity = "error") => {
|
|
32
|
+
diagnostics.push({ line, code, message, suggestion, severity });
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (title) {
|
|
36
|
+
const split = splitTitle(title);
|
|
37
|
+
// Titles stay exactly as written. splitTitle already keeps one line when it cannot split.
|
|
38
|
+
if (split.issue === "multiple") {
|
|
39
|
+
push(titleLine, "title-split", "标题里有不止一个分隔符,无法分成正好两行。", "只留一个 丨、| 或 \" | \"。", "warning");
|
|
40
|
+
} else if (split.issue === "empty") {
|
|
41
|
+
push(titleLine, "title-split", "分隔符两侧都要有文字。", "写成「主题丨副题」。", "warning");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const fences = scanFences(markdown);
|
|
46
|
+
fences.forEach((fence, index) => {
|
|
47
|
+
const kind = classifyFence(fence);
|
|
48
|
+
if (kind.kind === "legacy") {
|
|
49
|
+
push(fence.line, "legacy-fence", kind.suggestion, kind.suggestion);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (kind.kind === "diff") {
|
|
53
|
+
const previous = fences[index - 1];
|
|
54
|
+
const previousKind = previous ? classifyFence(previous) : null;
|
|
55
|
+
const adjacent = previous && blanksBetween(markdown, previous.endLine, fence.line) && isFullCodeFence(previousKind);
|
|
56
|
+
if (!adjacent) {
|
|
57
|
+
push(fence.line, "diff-orphan", "这段 diff 前面没有紧挨着的完整代码块。", "先放完整代码,下一围栏再写它的 diff。");
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (kind.kind !== "garden" || !kind.block) return;
|
|
62
|
+
const block = kind.block;
|
|
63
|
+
if (block.legacy) {
|
|
64
|
+
const suggestion = `\`${block.legacyLang}\` 已改为 \`garden ${block.name}\``;
|
|
65
|
+
push(fence.line, "legacy-fence", suggestion, suggestion);
|
|
66
|
+
}
|
|
67
|
+
if (!block.name) {
|
|
68
|
+
if (!draft) {
|
|
69
|
+
push(fence.line, "garden-idea", "非草稿里还有未编译的想法。", "编译成 `garden interactive`,或把这句话写回正文。");
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (block.error?.code === "garden-name") {
|
|
74
|
+
const guess = suggestName(block.name);
|
|
75
|
+
const suggestion = guess ? `你是不是想用 ${guess}` : `可用的名字:${Object.keys(CATALOG).join("、")}`;
|
|
76
|
+
push(fence.line, "garden-name", block.error.message, suggestion);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (block.error?.code === "garden-yaml") {
|
|
80
|
+
const allowed = block.error.allowed ?? CATALOG[block.name]?.keys ?? [];
|
|
81
|
+
push(fence.line, "garden-yaml", block.error.message, allowed.length ? `可用的键:${allowed.join("、")}` : "检查缩进和冒号。");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (block.name === "compare" && compareTooLong(block.props)) {
|
|
85
|
+
push(fence.line, "compare-long", "对比格超过约 60 字。", "这一格改回原文的列表或段落。");
|
|
86
|
+
}
|
|
87
|
+
if (block.name === "interactive") {
|
|
88
|
+
const source = `${fence.body}\n${block.children}`;
|
|
89
|
+
if (isTrivialInteractive(source)) {
|
|
90
|
+
push(fence.line, "interactive-trivial", "只有一次乘法,却做成了交互。", "把这句算式留在正文里。");
|
|
91
|
+
}
|
|
92
|
+
if (isProseSteps(source)) {
|
|
93
|
+
push(fence.line, "steps-prose", "步骤只是散文。", "连续的段落不要拆成步骤;有状态变化时才用 interactive。");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
return diagnostics;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function formatDiagnostic(file, diagnostic) {
|
|
101
|
+
const where = file ? `${file}:${diagnostic.line}` : String(diagnostic.line);
|
|
102
|
+
return `${where} ${diagnostic.code} ${diagnostic.message} 建议:${diagnostic.suggestion}`;
|
|
103
|
+
}
|