hd-garden 0.8.4 → 0.9.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/app/bookshelf/load.mjs +96 -0
- package/app/bookshelf/load.ts +16 -0
- package/app/bookshelf/page.tsx +52 -0
- package/app/components/SiteChrome.tsx +17 -3
- package/app/components/SiteHeader.tsx +3 -3
- package/app/components/bookshelf/Bookshelf.tsx +117 -0
- package/app/components/bookshelf/bookshelf.css +1288 -0
- package/app/components/bookshelf/catalog.mjs +116 -0
- package/app/components/bookshelf/catalog.ts +23 -0
- package/app/components/bookshelf/demo-data.ts +93 -0
- package/app/components/bookshelf/density.ts +32 -0
- package/app/components/bookshelf/engine.ts +2690 -0
- package/app/components/bookshelf/index.ts +12 -0
- package/app/components/bookshelf/model.ts +16 -0
- package/app/components/bookshelf/schema.mjs +86 -0
- package/app/components/bookshelf/schema.ts +68 -0
- package/app/components/bookshelf/types.ts +55 -0
- package/app/garden-modules.d.ts +6 -0
- package/app/layout.tsx +2 -1
- package/app/lib/i18n.ts +2 -2
- package/app/sitemap.ts +3 -0
- package/docs/handbook/config.md +41 -2
- package/docs/handbook/en/config.md +41 -2
- package/package.json +2 -1
- package/public/covers/accelerate.jpg +0 -0
- package/public/covers/clean-architecture.jpg +0 -0
- package/public/covers/clean-code.jpg +0 -0
- package/public/covers/continuous-delivery.jpg +0 -0
- package/public/covers/ddd.jpg +0 -0
- package/public/covers/ddia.jpg +0 -0
- package/public/covers/design-patterns.jpg +0 -0
- package/public/covers/eaa.jpg +0 -0
- package/public/covers/fundamentals-sa.jpg +0 -0
- package/public/covers/implementing-ddd.jpg +0 -0
- package/public/covers/lean-startup.jpg +0 -0
- package/public/covers/microservices.jpg +0 -0
- package/public/covers/mythical-man-month.jpg +0 -0
- package/public/covers/phoenix-project.jpg +0 -0
- package/public/covers/pragmatic-programmer.jpg +0 -0
- package/public/covers/refactoring.jpg +0 -0
- package/public/covers/release-it.jpg +0 -0
- package/public/covers/sre.jpg +0 -0
- package/public/covers/systems-performance.jpg +0 -0
- package/public/covers/tdd.jpg +0 -0
- package/public/covers/team-topologies.jpg +0 -0
- package/scripts/build-content.mjs +17 -1
- package/scripts/handbook-facts.mjs +2 -1
- package/ui/garden/site-nav.mjs +12 -0
- package/vite.config.ts +12 -3
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { marked } from "marked";
|
|
2
|
+
import { BookshelfConfigError } from "./schema.mjs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Site shelf for `<Bookshelf catalog notes />`.
|
|
6
|
+
* Built only from `content/bookshelf.yaml` and the posts it names.
|
|
7
|
+
* The package demo is a separate path: this module never reads it.
|
|
8
|
+
*
|
|
9
|
+
* `post` is a `content/posts` path without `.md`. Its front matter description
|
|
10
|
+
* leads the note HTML so the open book's right page is that summary.
|
|
11
|
+
* A cover file name is published at `/media/covers/<file>` (same stem cleanup
|
|
12
|
+
* as content sync). A full URL or a path starting with `/` is kept.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const WIDGET_FENCE = /```(?:garden|ui-error|ui|intent|widget)(?:[ \t]+[^\n]*)?\r?\n[\s\S]*?```/gu;
|
|
16
|
+
|
|
17
|
+
export function resolveBookshelfCover(cover, where = "cover") {
|
|
18
|
+
if (!cover?.trim()) return undefined;
|
|
19
|
+
const value = cover.trim();
|
|
20
|
+
if (/^(?:[a-z][a-z0-9+.-]*:)?\/\//iu.test(value) || value.startsWith("/") || value.startsWith("data:")) {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
let relative = value.replace(/^\.\//u, "");
|
|
24
|
+
if (relative.startsWith("covers/")) relative = relative.slice("covers/".length);
|
|
25
|
+
const parts = relative.split("/");
|
|
26
|
+
if (!relative || parts.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
27
|
+
throw new BookshelfConfigError("cover", `${where} 的 cover 必须是文件名或完整 URL。`);
|
|
28
|
+
}
|
|
29
|
+
return `/media/covers/${parts.map(publishCoverSegment).join("/")}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `content/posts` path, without `.md`. Rejects anything that leaves that directory. */
|
|
33
|
+
export function normalizeBookshelfPostPath(value) {
|
|
34
|
+
const relative = value
|
|
35
|
+
.trim()
|
|
36
|
+
.replaceAll("\\", "/")
|
|
37
|
+
.replace(/^(?:content\/)?posts\//u, "")
|
|
38
|
+
.replace(/\.md$/u, "");
|
|
39
|
+
if (
|
|
40
|
+
!relative
|
|
41
|
+
|| relative.startsWith("/")
|
|
42
|
+
|| relative.split("/").some((segment) => segment === "" || segment === "." || segment === "..")
|
|
43
|
+
) {
|
|
44
|
+
throw new BookshelfConfigError("post", `post「${value}」必须是 content/posts 下的路径,不带 .md。`);
|
|
45
|
+
}
|
|
46
|
+
return relative;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function bookshelfMountFromFile(file, posts) {
|
|
50
|
+
const byPath = new Map(posts.map((post) => [normalizeBookshelfPostPath(post.path), post]));
|
|
51
|
+
const notes = {};
|
|
52
|
+
const catalog = file.shelves.map((shelf) => ({
|
|
53
|
+
id: shelf.id,
|
|
54
|
+
name: shelf.name,
|
|
55
|
+
books: shelf.books.map((book) => toCatalogBook(book, byPath, notes)),
|
|
56
|
+
}));
|
|
57
|
+
return {
|
|
58
|
+
title: file.title,
|
|
59
|
+
description: file.description,
|
|
60
|
+
eyebrow: file.eyebrow,
|
|
61
|
+
catalog,
|
|
62
|
+
notes,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function toCatalogBook(book, byPath, notes) {
|
|
67
|
+
const post = book.post ? byPath.get(normalizeBookshelfPostPath(book.post)) : undefined;
|
|
68
|
+
if (book.post && !post) {
|
|
69
|
+
throw new BookshelfConfigError("post", `书架引用的博文 ${book.post} 没有读到。`);
|
|
70
|
+
}
|
|
71
|
+
const cover = resolveBookshelfCover(book.cover, book.id) ?? (post ? resolveBookshelfCover(post.cover, book.id) : undefined);
|
|
72
|
+
const body = post
|
|
73
|
+
? renderNoteHtml(post.content, post.description)
|
|
74
|
+
: book.note
|
|
75
|
+
? renderNoteHtml(book.note)
|
|
76
|
+
: "";
|
|
77
|
+
const note = body ? book.id : null;
|
|
78
|
+
if (note) notes[book.id] = { title: book.title, body, summary: post?.description };
|
|
79
|
+
return {
|
|
80
|
+
id: book.id,
|
|
81
|
+
title: book.title,
|
|
82
|
+
author: book.author ?? "",
|
|
83
|
+
spine: book.spine ?? book.title,
|
|
84
|
+
size: book.size ?? (book.filler ? "thin" : "s-mid"),
|
|
85
|
+
cover: cover ?? false,
|
|
86
|
+
color: book.color,
|
|
87
|
+
filler: book.filler,
|
|
88
|
+
post: book.post ? normalizeBookshelfPostPath(book.post) : undefined,
|
|
89
|
+
note,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function renderNoteHtml(markdown, description) {
|
|
94
|
+
const prose = markdown.replace(WIDGET_FENCE, "\n").trim();
|
|
95
|
+
const parsed = prose ? marked.parse(prose, { async: false }).trim() : "";
|
|
96
|
+
const summary = description?.trim();
|
|
97
|
+
if (!summary || prose.includes(summary)) return parsed;
|
|
98
|
+
return `<p>${escapeHtml(summary)}</p>${parsed ? `\n${parsed}` : ""}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function publishCoverSegment(segment, index, all) {
|
|
102
|
+
const isFile = index === all.length - 1;
|
|
103
|
+
if (!isFile) return encodeURIComponent(segment.replace(/[^\w\u3400-\u9fff.-]+/gu, "-"));
|
|
104
|
+
const dot = segment.lastIndexOf(".");
|
|
105
|
+
const ext = dot >= 0 ? segment.slice(dot).toLowerCase() : "";
|
|
106
|
+
const stem = dot >= 0 ? segment.slice(0, dot) : segment;
|
|
107
|
+
return encodeURIComponent(`${stem.replace(/[^\w\u3400-\u9fff.-]+/gu, "-")}${ext}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function escapeHtml(value) {
|
|
111
|
+
return value
|
|
112
|
+
.replaceAll("&", "&")
|
|
113
|
+
.replaceAll("<", "<")
|
|
114
|
+
.replaceAll(">", ">")
|
|
115
|
+
.replaceAll('"', """);
|
|
116
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { BookshelfNote, BookshelfShelf } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Site shelf ready for `<Bookshelf catalog notes />`.
|
|
5
|
+
* The runtime lives in `catalog.mjs` (node tests and the page share it).
|
|
6
|
+
* The package demo is not an input.
|
|
7
|
+
*/
|
|
8
|
+
export type BookshelfMount = {
|
|
9
|
+
title: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
eyebrow?: string;
|
|
12
|
+
catalog: BookshelfShelf[];
|
|
13
|
+
notes: Record<string, BookshelfNote>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** A post the shelf can turn into a note. `path` is relative to `content/posts`, without `.md`. */
|
|
17
|
+
export type BookshelfPostSource = {
|
|
18
|
+
path: string;
|
|
19
|
+
title?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
cover?: string;
|
|
22
|
+
content: string;
|
|
23
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { BookshelfNote, BookshelfShelf } from "./types";
|
|
2
|
+
|
|
3
|
+
export const DEMO_CATALOG: BookshelfShelf[] = [
|
|
4
|
+
{
|
|
5
|
+
id: "tools",
|
|
6
|
+
name: "工具与基础",
|
|
7
|
+
books: [
|
|
8
|
+
{ id: "clean-code", title: "代码整洁之道", author: "Robert C. Martin", spine: "代码整洁之道", size: "s-tall", cover: true, note: "clean-code" },
|
|
9
|
+
{ id: "refactoring", title: "重构", author: "Martin Fowler", spine: "重构", size: "s-mid", cover: true, note: null },
|
|
10
|
+
{ id: "design-patterns", title: "设计模式", author: "GoF", spine: "设计模式", size: "s-tall", cover: true, note: null },
|
|
11
|
+
{ id: "mythical-man-month", title: "人月神话", author: "Frederick Brooks", spine: "人月神话", size: "s-mid", cover: true, note: null },
|
|
12
|
+
{ id: "zaji-1", title: "札记", author: "", spine: "札记", size: "thin", cover: false, filler: "sand", note: null },
|
|
13
|
+
{ id: "biji-1", title: "笔记", author: "", spine: "笔记", size: "thin", cover: false, filler: "cream", note: null },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: "eng",
|
|
18
|
+
name: "工程实践",
|
|
19
|
+
books: [
|
|
20
|
+
{ id: "continuous-delivery", title: "持续交付", author: "Jez Humble", spine: "持续交付", size: "s-tall", cover: true, note: null },
|
|
21
|
+
{ id: "phoenix-project", title: "凤凰项目", author: "Gene Kim", spine: "凤凰项目", size: "s-mid", cover: true, note: null },
|
|
22
|
+
{ id: "pragmatic-programmer", title: "程序员修炼之道", author: "David Thomas & Andrew Hunt", spine: "程序员修炼之道", size: "s-mid", cover: true, note: "pragmatic-programmer" },
|
|
23
|
+
{ id: "tdd", title: "测试驱动开发", author: "Kent Beck", spine: "测试驱动开发", size: "s-tall", cover: true, note: null },
|
|
24
|
+
{ id: "lean-startup", title: "精益创业", author: "Eric Ries", spine: "精益创业", size: "s-mid", cover: true, note: null },
|
|
25
|
+
{ id: "release-it", title: "发布!", author: "Michael Nygard", spine: "发布!", size: "s-short", cover: true, note: null },
|
|
26
|
+
{ id: "sre", title: "SRE 实践", author: "Google", spine: "SRE 实践", size: "s-mid", cover: true, note: null },
|
|
27
|
+
{ id: "zaji-2", title: "札记", author: "", spine: "札记", size: "thin", cover: false, filler: "sand", note: null },
|
|
28
|
+
{ id: "team-topologies", title: "团队拓扑", author: "Matthew Skelton", spine: "团队拓扑", size: "s-mid", cover: true, note: null },
|
|
29
|
+
{ id: "accelerate", title: "加速", author: "Nicole Forsgren", spine: "加速", size: "s-tall", cover: true, note: null },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "arch",
|
|
34
|
+
name: "领域与架构",
|
|
35
|
+
books: [
|
|
36
|
+
{ id: "ddd", title: "领域驱动设计", author: "Eric Evans", spine: "领域驱动设计", size: "s-tall", cover: true, note: null },
|
|
37
|
+
{ id: "microservices", title: "微服务设计", author: "Sam Newman", spine: "微服务设计", size: "s-mid", cover: true, note: null },
|
|
38
|
+
{ id: "eaa", title: "企业应用架构模式", author: "Martin Fowler", spine: "企业应用架构模式", size: "s-mid", cover: true, note: null },
|
|
39
|
+
{ id: "ddia", title: "数据密集型应用", author: "Martin Kleppmann", spine: "数据密集型应用", size: "s-tall", cover: true, note: "ddia" },
|
|
40
|
+
{ id: "fundamentals-sa", title: "软件架构基础", author: "Mark Richards", spine: "软件架构基础", size: "s-short", cover: true, note: null },
|
|
41
|
+
{ id: "implementing-ddd", title: "实现领域驱动设计", author: "Vaughn Vernon", spine: "实现领域驱动设计", size: "s-mid", cover: true, note: null },
|
|
42
|
+
{ id: "biji-2", title: "笔记", author: "", spine: "笔记", size: "thin", cover: false, filler: "cream", note: null },
|
|
43
|
+
{ id: "systems-performance", title: "性能之巅", author: "Brendan Gregg", spine: "性能之巅", size: "s-mid", cover: true, note: null },
|
|
44
|
+
{ id: "clean-architecture", title: "架构整洁之道", author: "Robert C. Martin", spine: "架构整洁之道", size: "s-tall", cover: true, note: null },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
export const DEMO_NOTES: Record<string, BookshelfNote> = {
|
|
50
|
+
"pragmatic-programmer": {
|
|
51
|
+
title: "程序员修炼之道",
|
|
52
|
+
body: `
|
|
53
|
+
<h2>为什么还在读这本</h2>
|
|
54
|
+
<p>不是为了清单打卡,是为了把「匠人感」拉回日常:小步验证、可逆决定、别把偶然当设计。</p>
|
|
55
|
+
<h2>几条带走的</h2>
|
|
56
|
+
<ul>
|
|
57
|
+
<li>DRY 不是少敲几行字,是别让同一条知识活在两处。</li>
|
|
58
|
+
<li>正交:改一处不该惊动无关处。</li>
|
|
59
|
+
<li>抛出异常要够具体,吞掉异常等于撒谎。</li>
|
|
60
|
+
</ul>
|
|
61
|
+
<p>合上后仍会问自己:今天写的代码,半年后的我还认得出意图吗?</p>
|
|
62
|
+
`,
|
|
63
|
+
},
|
|
64
|
+
"clean-code": {
|
|
65
|
+
title: "代码整洁之道",
|
|
66
|
+
body: `
|
|
67
|
+
<h2>整洁不是洁癖</h2>
|
|
68
|
+
<p>可读性是团队带宽。命名、函数长度、副作用边界,比任何风格指南都更直接决定协作成本。</p>
|
|
69
|
+
<h2>记下的几条</h2>
|
|
70
|
+
<ul>
|
|
71
|
+
<li>函数做一件事,并且把它做完。</li>
|
|
72
|
+
<li>注释解释「为什么」,别复述「做了什么」。</li>
|
|
73
|
+
<li>坏味道先闻到,再谈重构顺序。</li>
|
|
74
|
+
</ul>
|
|
75
|
+
<p>Demo 笔记:正式站会换成完整读书笔记正文。</p>
|
|
76
|
+
`,
|
|
77
|
+
},
|
|
78
|
+
ddia: {
|
|
79
|
+
title: "数据密集型应用系统设计",
|
|
80
|
+
body: `
|
|
81
|
+
<h2>为什么这本难跳过</h2>
|
|
82
|
+
<p>把复制、分区、事务、流处理放进同一张地图里。读完后「CAP」不再是面试口号,而是取舍清单。</p>
|
|
83
|
+
<h2>带走的框架</h2>
|
|
84
|
+
<ul>
|
|
85
|
+
<li>可靠性 / 可扩展性 / 可维护性:先问系统要哪一项。</li>
|
|
86
|
+
<li>写放大与读放大:每加一层缓存都要付账。</li>
|
|
87
|
+
<li>一致性不是开关,是光谱。</li>
|
|
88
|
+
</ul>
|
|
89
|
+
<p>合上书,仍会用它的词汇表去审自己的服务边界。</p>
|
|
90
|
+
`,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { BookshelfShelf } from "./types";
|
|
2
|
+
|
|
3
|
+
export const MID_W = 132;
|
|
4
|
+
export const MID_H = 198;
|
|
5
|
+
|
|
6
|
+
/** Places `fromRect` onto `toRect`. `extra` is the demo's rotateZ term. */
|
|
7
|
+
export function flipTransform(
|
|
8
|
+
fromRect: { left: number; top: number; width: number; height: number },
|
|
9
|
+
toRect: { left: number; top: number; width: number; height: number },
|
|
10
|
+
extra = "",
|
|
11
|
+
): string {
|
|
12
|
+
const sx = fromRect.width / toRect.width;
|
|
13
|
+
const sy = fromRect.height / toRect.height;
|
|
14
|
+
const tx = fromRect.left - toRect.left;
|
|
15
|
+
const ty = fromRect.top - toRect.top;
|
|
16
|
+
return `translate(${tx}px, ${ty}px) scale(${sx}, ${sy})${extra ? " " + extra : ""}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Page-level density from the approved demo.
|
|
21
|
+
* If any row's covers cannot fit the text column, every row is spines.
|
|
22
|
+
*/
|
|
23
|
+
export function pageDensity(catalog: readonly Pick<BookshelfShelf, "books">[], viewportWidth: number): "covers" | "spines" {
|
|
24
|
+
const col = Math.min(viewportWidth - 32, 740);
|
|
25
|
+
const coverW = viewportWidth <= 640 ? 102 + 10 : 118 + 14;
|
|
26
|
+
for (const cat of catalog) {
|
|
27
|
+
const n = cat.books.filter((b) => b.cover).length;
|
|
28
|
+
const need = n * coverW;
|
|
29
|
+
if (need > col - 8) return "spines";
|
|
30
|
+
}
|
|
31
|
+
return "covers";
|
|
32
|
+
}
|