hd-garden 0.14.12 → 0.14.13
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/Makefile +1 -1
- package/app/api/admin/distribution/route.ts +4 -2
- package/app/blog/[...path]/page.tsx +4 -2
- package/app/columns/[column]/[...path]/page.tsx +4 -2
- package/app/components/GardenMediaGuide.tsx +11 -23
- package/app/components/GardenPlayer.tsx +25 -0
- package/app/components/HandbookView.tsx +5 -3
- package/app/components/MarkdownArticle.tsx +7 -2
- package/app/components/PostReadingView.tsx +3 -3
- package/app/garden-modules.d.ts +10 -2
- package/app/globals.css +58 -1
- package/app/lib/content-body.ts +20 -0
- package/app/lib/garden-docs-meta.ts +2 -0
- package/app/lib/markdown-plugins.ts +5 -0
- package/app/lib/remark-video-player.d.mts +10 -0
- package/app/lib/remark-video-player.mjs +70 -0
- package/app/rss.xml/route.ts +3 -2
- package/bin/garden.mjs +18 -78
- package/docs/handbook/components/compose.md +3 -1
- package/docs/handbook/components/diagrams/chart.md +38 -0
- package/docs/handbook/components/diagrams/code-walkthrough.md +38 -0
- package/docs/handbook/components/diagrams/columns-join.md +38 -0
- package/docs/handbook/components/diagrams/compare.md +38 -0
- package/docs/handbook/components/diagrams/converter.md +38 -0
- package/docs/handbook/components/diagrams/facets.md +38 -0
- package/docs/handbook/components/diagrams/fan-out.md +38 -0
- package/docs/handbook/components/diagrams/icon-rail.md +38 -0
- package/docs/handbook/components/diagrams/inspect-grid.md +38 -0
- package/docs/handbook/components/diagrams/lanes.md +38 -0
- package/docs/handbook/components/diagrams/layers.md +38 -0
- package/docs/handbook/components/diagrams/other.md +37 -0
- package/docs/handbook/components/diagrams/plan.md +38 -0
- package/docs/handbook/components/diagrams/previews/chart.svg +7 -0
- package/docs/handbook/components/diagrams/previews/code-walkthrough.svg +5 -0
- package/docs/handbook/components/diagrams/previews/columns-join.svg +6 -0
- package/docs/handbook/components/diagrams/previews/compare.svg +5 -0
- package/docs/handbook/components/diagrams/previews/converter.svg +6 -0
- package/docs/handbook/components/diagrams/previews/facets.svg +6 -0
- package/docs/handbook/components/diagrams/previews/fan-out.svg +8 -0
- package/docs/handbook/components/diagrams/previews/icon-rail.svg +5 -0
- package/docs/handbook/components/diagrams/previews/inspect-grid.svg +5 -0
- package/docs/handbook/components/diagrams/previews/lanes.svg +5 -0
- package/docs/handbook/components/diagrams/previews/layers.svg +6 -0
- package/docs/handbook/components/diagrams/previews/other.svg +6 -0
- package/docs/handbook/components/diagrams/previews/plan.svg +5 -0
- package/docs/handbook/components/diagrams/previews/sequence.svg +7 -0
- package/docs/handbook/components/diagrams/previews/stepper.svg +5 -0
- package/docs/handbook/components/diagrams/previews/steps.svg +7 -0
- package/docs/handbook/components/diagrams/previews/tabs.svg +6 -0
- package/docs/handbook/components/diagrams/previews/timeline.svg +5 -0
- package/docs/handbook/components/diagrams/previews/trace.svg +7 -0
- package/docs/handbook/components/diagrams/previews/tunable.svg +4 -0
- package/docs/handbook/components/diagrams/previews/two-row.svg +4 -0
- package/docs/handbook/components/diagrams/sequence.md +38 -0
- package/docs/handbook/components/diagrams/stepper.md +38 -0
- package/docs/handbook/components/diagrams/steps.md +38 -0
- package/docs/handbook/components/diagrams/tabs.md +38 -0
- package/docs/handbook/components/diagrams/timeline.md +38 -0
- package/docs/handbook/components/diagrams/trace.md +38 -0
- package/docs/handbook/components/diagrams/tunable.md +38 -0
- package/docs/handbook/components/diagrams/two-row.md +38 -0
- package/docs/handbook/components/diagrams.md +100 -0
- package/docs/handbook/components/link-card.md +1 -1
- package/docs/handbook/components.md +1 -0
- package/docs/handbook/en/components/compose.md +3 -1
- package/docs/handbook/en/components/diagrams/chart.md +40 -0
- package/docs/handbook/en/components/diagrams/code-walkthrough.md +40 -0
- package/docs/handbook/en/components/diagrams/columns-join.md +40 -0
- package/docs/handbook/en/components/diagrams/compare.md +40 -0
- package/docs/handbook/en/components/diagrams/converter.md +40 -0
- package/docs/handbook/en/components/diagrams/facets.md +40 -0
- package/docs/handbook/en/components/diagrams/fan-out.md +40 -0
- package/docs/handbook/en/components/diagrams/icon-rail.md +40 -0
- package/docs/handbook/en/components/diagrams/inspect-grid.md +40 -0
- package/docs/handbook/en/components/diagrams/lanes.md +40 -0
- package/docs/handbook/en/components/diagrams/layers.md +40 -0
- package/docs/handbook/en/components/diagrams/other.md +39 -0
- package/docs/handbook/en/components/diagrams/plan.md +40 -0
- package/docs/handbook/en/components/diagrams/sequence.md +40 -0
- package/docs/handbook/en/components/diagrams/stepper.md +40 -0
- package/docs/handbook/en/components/diagrams/steps.md +40 -0
- package/docs/handbook/en/components/diagrams/tabs.md +40 -0
- package/docs/handbook/en/components/diagrams/timeline.md +40 -0
- package/docs/handbook/en/components/diagrams/trace.md +40 -0
- package/docs/handbook/en/components/diagrams/tunable.md +40 -0
- package/docs/handbook/en/components/diagrams/two-row.md +40 -0
- package/docs/handbook/en/components/diagrams.md +100 -0
- package/docs/handbook/en/components/link-card.md +1 -1
- package/docs/handbook/en/components.md +1 -0
- package/docs/handbook/en/start.md +1 -1
- package/docs/handbook/en/workflow.md +2 -2
- package/docs/handbook/en/writing.md +82 -1
- package/docs/handbook/start.md +1 -1
- package/docs/handbook/workflow.md +2 -2
- package/docs/handbook/writing.md +82 -1
- package/editors/dist/{garden-typora-0.14.12.zip → garden-typora-0.14.13.zip} +0 -0
- package/editors/dist/typora/main.js +175 -175
- package/editors/dist/typora/manifest.json +1 -1
- package/editors/dist/typora/style.css +44 -6
- package/editors/dist/vscode/{garden-ui-0.14.12.vsix → garden-ui-0.14.13.vsix} +0 -0
- package/package.json +1 -1
- package/scripts/build-content.mjs +23 -8
- package/scripts/dev-server.mjs +30 -2
- package/scripts/fetch-embed.mjs +39 -27
- package/scripts/handbook-diagrams.mjs +434 -0
- package/scripts/link-assets.mjs +6 -0
- package/scripts/vinext-runtime.mjs +80 -0
- package/templates/site/Makefile +1 -1
- package/templates/site/Makefile.en +1 -1
- package/ui/compose/compose.css +43 -5
- package/ui/garden/markdown-links.mjs +8 -5
- package/ui/garden/picker-placement.mjs +106 -2
- package/ui/garden/player.mjs +286 -0
- package/ui/garden/rehype-keep-punctuation.mjs +87 -18
- package/ui/preview/dist/garden-ui.css +43 -5
- package/ui/preview/dist/garden-ui.esm.js +4 -4
- package/ui/preview/dist/garden-ui.iife.js +4 -4
- package/vite.config.ts +84 -23
package/Makefile
CHANGED
|
@@ -13,7 +13,7 @@ export GARDEN_EMBED_URL := $(value URL)
|
|
|
13
13
|
export GARDEN_CHECK_FILE := $(value FILE)
|
|
14
14
|
|
|
15
15
|
help:
|
|
16
|
-
@printf "%s\n" "Garden 常用命令:" " make new 创建文章草稿(无需安装 npm 依赖)" ' make new TITLE="文章标题" SLUG=my-post DIR=writing' ' make embed URL="https://
|
|
16
|
+
@printf "%s\n" "Garden 常用命令:" " make new 创建文章草稿(无需安装 npm 依赖)" ' make new TITLE="文章标题" SLUG=my-post DIR=writing' ' make embed URL="https://www.youtube.com/watch?v=..." 打印可粘贴的规范化视频链接或网页地址' " make skill 安装可交互图表 skill" " make dev 首次准备并本地预览" " make check 全量严格预检后 lint/测试;FILE=路径 只限制预检" " make check diff 只检查 Git 改动的 Markdown,不构建/跑全套测试" " make build 构建公开站点" " make update 检查、提交并触发自动发布" " make update MESSAGE=\"新增一篇文章\" 自定义提交说明"
|
|
17
17
|
|
|
18
18
|
new:
|
|
19
19
|
@node scripts/new-post.mjs
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { requireAdminApi, requireAdminMutation } from "../../../lib/admin-auth";
|
|
2
2
|
import { findColumnEntry, findPost } from "../../../lib/content";
|
|
3
|
+
import { postWithBody } from "../../../lib/content-body";
|
|
3
4
|
import { absoluteUrl, getSiteUrl } from "../../../lib/site";
|
|
4
5
|
import { contentForDistribution } from "../../../../ui/core/transform.mjs";
|
|
5
6
|
|
|
@@ -42,8 +43,9 @@ export async function POST(request: Request) {
|
|
|
42
43
|
const entry = kind === "post" ? findPost(path) : undefined;
|
|
43
44
|
const column = typeof payload.column === "string" ? payload.column : "";
|
|
44
45
|
const columnEntry = kind === "column" ? findColumnEntry(column, path) : undefined;
|
|
45
|
-
const
|
|
46
|
-
if (!
|
|
46
|
+
const found = entry ?? columnEntry;
|
|
47
|
+
if (!found) return Response.json({ error: "未找到对应内容。" }, { status: 404 });
|
|
48
|
+
const source = postWithBody(found);
|
|
47
49
|
|
|
48
50
|
const publicPath = kind === "post" ? `/blog/${source.path}` : `/columns/${source.column}/${source.path}`;
|
|
49
51
|
const siteUrl = await getSiteUrl();
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
postHref,
|
|
7
7
|
posts,
|
|
8
8
|
} from "../../lib/content";
|
|
9
|
+
import { postWithBody } from "../../lib/content-body";
|
|
9
10
|
import { PostReadingView } from "../../components/PostReadingView";
|
|
10
11
|
import { adjacentByDate } from "../../../ui/garden/adjacent.mjs";
|
|
11
12
|
import {
|
|
@@ -59,11 +60,12 @@ export default async function PostPage({
|
|
|
59
60
|
params: Promise<{ path: string[] }>;
|
|
60
61
|
}) {
|
|
61
62
|
const { path } = await params;
|
|
62
|
-
const
|
|
63
|
-
if (!
|
|
63
|
+
const found = findPost(path.join("/"));
|
|
64
|
+
if (!found) {
|
|
64
65
|
if (isStaticPlaceholder(path)) return null;
|
|
65
66
|
notFound();
|
|
66
67
|
}
|
|
68
|
+
const post = postWithBody(found);
|
|
67
69
|
const baseUrl = await getSiteUrl();
|
|
68
70
|
const pathname = postHref(post);
|
|
69
71
|
const canonicalUrl = absoluteUrl(pathname, baseUrl);
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
columnHref,
|
|
8
8
|
columnEntryPath,
|
|
9
9
|
} from "../../../lib/content";
|
|
10
|
+
import { postWithBody } from "../../../lib/content-body";
|
|
10
11
|
import { PostReadingView } from "../../../components/PostReadingView";
|
|
11
12
|
import {
|
|
12
13
|
absoluteUrl,
|
|
@@ -69,11 +70,12 @@ export default async function ColumnEntryPage({
|
|
|
69
70
|
params: Promise<{ column: string; path: string[] }>;
|
|
70
71
|
}) {
|
|
71
72
|
const { column, path } = await params;
|
|
72
|
-
const
|
|
73
|
-
if (!
|
|
73
|
+
const found = findColumnEntry(column, path.join("/"));
|
|
74
|
+
if (!found) {
|
|
74
75
|
if (isStaticPlaceholder(column)) return null;
|
|
75
76
|
notFound();
|
|
76
77
|
}
|
|
78
|
+
const entry = postWithBody(found);
|
|
77
79
|
const series = columns.filter((item) => item.column === column);
|
|
78
80
|
const listed = [...series].sort((left, right) => Date.parse(right.date) - Date.parse(left.date));
|
|
79
81
|
const currentIndex = series.findIndex((item) => item.path === entry.path);
|
|
@@ -1,28 +1,17 @@
|
|
|
1
|
-
const
|
|
1
|
+
const mediaTypes = [
|
|
2
2
|
{
|
|
3
3
|
title: "YouTube / Bilibili",
|
|
4
|
-
description: "
|
|
5
|
-
},
|
|
6
|
-
{
|
|
7
|
-
title: "X / Twitter",
|
|
8
|
-
description: "显示成一张带出处的卡片。",
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
title: "微信公众号",
|
|
12
|
-
description: "留下标题、摘要和原文链接。微信不允许嵌进页面时,卡片仍然能点开。",
|
|
4
|
+
description: "把可播放的视频链接单独占一行,页面里就能播放,也能回到原站。",
|
|
13
5
|
},
|
|
14
6
|
{
|
|
15
7
|
title: "其他网站",
|
|
16
|
-
description: "
|
|
8
|
+
description: "单独一行的网址显示为链接卡片;句子里的链接在后面紧跟 {card} 才会变成卡片。",
|
|
17
9
|
},
|
|
18
10
|
];
|
|
19
11
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
description: 给读者一个值得点开的理由。
|
|
24
|
-
image: https://example.com/cover.jpg
|
|
25
|
-
\`\`\``;
|
|
12
|
+
const mediaSource = `https://www.youtube.com/watch?v=VIDEO_ID
|
|
13
|
+
|
|
14
|
+
https://www.bilibili.com/video/BV1xx411c7mD`;
|
|
26
15
|
|
|
27
16
|
export function GardenMediaGuide() {
|
|
28
17
|
return (
|
|
@@ -30,9 +19,8 @@ export function GardenMediaGuide() {
|
|
|
30
19
|
<header>
|
|
31
20
|
<h2 id="garden-media-title">视频和链接</h2>
|
|
32
21
|
<p>
|
|
33
|
-
自己的图片、音频和视频用 Markdown
|
|
34
|
-
|
|
35
|
-
围栏。能播的直接播,不能播的显示成一张卡片。
|
|
22
|
+
自己的图片、音频和视频用 Markdown 或 <code>audio</code> / <code>video</code> 标签。
|
|
23
|
+
YouTube / Bilibili 只要把链接单独一行。其他网站用链接卡片,不要写任意 iframe。
|
|
36
24
|
</p>
|
|
37
25
|
</header>
|
|
38
26
|
|
|
@@ -40,11 +28,11 @@ export function GardenMediaGuide() {
|
|
|
40
28
|
<div>
|
|
41
29
|
<span>content/posts/example.md</span>
|
|
42
30
|
</div>
|
|
43
|
-
<pre><code>{
|
|
31
|
+
<pre><code>{mediaSource}</code></pre>
|
|
44
32
|
</div>
|
|
45
33
|
|
|
46
34
|
<ul className="garden-media-providers">
|
|
47
|
-
{
|
|
35
|
+
{mediaTypes.map(({ title, description }) => (
|
|
48
36
|
<li key={title}>
|
|
49
37
|
<strong>{title}</strong>
|
|
50
38
|
<p>{description}</p>
|
|
@@ -53,7 +41,7 @@ export function GardenMediaGuide() {
|
|
|
53
41
|
</ul>
|
|
54
42
|
|
|
55
43
|
<p className="garden-media-boundary">
|
|
56
|
-
|
|
44
|
+
需要规范化短链或核对标题时,用 <code>npx garden embed URL</code> 打印一行可粘贴的地址,看过再贴进文章。
|
|
57
45
|
</p>
|
|
58
46
|
</section>
|
|
59
47
|
);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { resolvePlayer } from "../../ui/garden/player.mjs";
|
|
2
|
+
|
|
3
|
+
export function GardenPlayer({ href, title }: { href: string; title?: string }) {
|
|
4
|
+
const player = resolvePlayer(href);
|
|
5
|
+
if (!player.ok) return null;
|
|
6
|
+
const open = player.provider === "youtube" ? "在 YouTube 打开" : "在 Bilibili 打开";
|
|
7
|
+
const frameTitle = title?.trim() || `${player.label} video`;
|
|
8
|
+
return (
|
|
9
|
+
<figure className={`g-player g-player-${player.provider}`}>
|
|
10
|
+
<div className="g-player-frame">
|
|
11
|
+
<iframe
|
|
12
|
+
src={player.playerUrl}
|
|
13
|
+
title={frameTitle}
|
|
14
|
+
loading="lazy"
|
|
15
|
+
allow="fullscreen; picture-in-picture; encrypted-media"
|
|
16
|
+
referrerPolicy="strict-origin-when-cross-origin"
|
|
17
|
+
allowFullScreen
|
|
18
|
+
/>
|
|
19
|
+
</div>
|
|
20
|
+
<figcaption>
|
|
21
|
+
<a className="ext" href={player.pageUrl} target="_blank" rel="noopener noreferrer">{open}</a>
|
|
22
|
+
</figcaption>
|
|
23
|
+
</figure>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -9,6 +9,7 @@ import { HandbookLangRedirect } from "./HandbookLangRedirect";
|
|
|
9
9
|
import { MarkdownArticle } from "./MarkdownArticle";
|
|
10
10
|
import { TableOfContents } from "./TableOfContents";
|
|
11
11
|
import { extractHeadings, handbookDocs, handbookIndexDoc, hasHandbookPage, type GardenDocEntry } from "../lib/content";
|
|
12
|
+
import { docWithBody } from "../lib/content-body";
|
|
12
13
|
import { pageAlternates } from "../lib/page-alternates";
|
|
13
14
|
import { absoluteUrl, getSiteUrl } from "../lib/site";
|
|
14
15
|
import { siteHref } from "../lib/site-href";
|
|
@@ -143,7 +144,7 @@ export async function handbookDocMetadata(lang: HandbookLang, doc: GardenDocEntr
|
|
|
143
144
|
export function HandbookOverview({ lang }: { lang: HandbookLang }) {
|
|
144
145
|
const t = HANDBOOK_UI[lang];
|
|
145
146
|
const docs = handbookDocs(lang);
|
|
146
|
-
const indexDoc = handbookIndexDoc(lang);
|
|
147
|
+
const indexDoc = docWithBody(handbookIndexDoc(lang), lang);
|
|
147
148
|
const navItems = toHandbookNavItems(docs);
|
|
148
149
|
const firstDoc = orderedHandbookDocs(docs)[0] ?? null;
|
|
149
150
|
const content = indexDoc?.content ?? "";
|
|
@@ -197,13 +198,14 @@ export function HandbookOverview({ lang }: { lang: HandbookLang }) {
|
|
|
197
198
|
|
|
198
199
|
export function HandbookDoc({ lang, doc }: { lang: HandbookLang; doc: GardenDocEntry }) {
|
|
199
200
|
const t = HANDBOOK_UI[lang];
|
|
201
|
+
const resolved = docWithBody(doc, lang);
|
|
200
202
|
const docs = handbookDocs(lang);
|
|
201
203
|
const ordered = orderedHandbookDocs(docs);
|
|
202
204
|
const navItems = toHandbookNavItems(docs);
|
|
203
205
|
const index = ordered.findIndex((item) => item.path === doc.path);
|
|
204
206
|
const prev = index > 0 ? ordered[index - 1] : null;
|
|
205
207
|
const next = index >= 0 && index < ordered.length - 1 ? ordered[index + 1] : null;
|
|
206
|
-
const headings = extractHeadings(
|
|
208
|
+
const headings = extractHeadings(resolved.content);
|
|
207
209
|
const sectionMeta = HANDBOOK_SECTIONS.find((sec) => sec.id === doc.section);
|
|
208
210
|
const componentBadge = COMPONENT_BADGE[doc.path];
|
|
209
211
|
|
|
@@ -229,7 +231,7 @@ export function HandbookDoc({ lang, doc }: { lang: HandbookLang; doc: GardenDocE
|
|
|
229
231
|
<ArticleMeasure />
|
|
230
232
|
|
|
231
233
|
<div className="e-content">
|
|
232
|
-
<MarkdownArticle content={
|
|
234
|
+
<MarkdownArticle content={resolved.content} lang={lang} fieldTables handbook refDemos={isReferencePath(doc.path)} />
|
|
233
235
|
</div>
|
|
234
236
|
|
|
235
237
|
<ArticleReferences references={doc.references} lang={lang} />
|
|
@@ -38,6 +38,8 @@ import { LinkCardRefresh } from "./LinkCardRefresh";
|
|
|
38
38
|
import { linkHost } from "../../scripts/link-meta-core.mjs";
|
|
39
39
|
import { FaviconProvider, SourceMark } from "./garden/patterns";
|
|
40
40
|
import { rehypeFieldTables } from "../lib/rehype-field-tables.mjs";
|
|
41
|
+
import { GardenPlayer } from "./GardenPlayer";
|
|
42
|
+
import { resolvePlayer } from "../../ui/garden/player.mjs";
|
|
41
43
|
import { cardDate } from "../../ui/garden/card-refresh.mjs";
|
|
42
44
|
import { siteHref } from "../lib/site-href";
|
|
43
45
|
import { codeLanguageLabel, fenceLanguage } from "../../ui/garden/code-language.mjs";
|
|
@@ -271,8 +273,11 @@ export function MarkdownArticle({ content, lang, fieldTables, refDemos, handbook
|
|
|
271
273
|
const only = nodes.length === 1 && isValidElement<{ href?: string; children?: ReactNode }>(nodes[0]) ? nodes[0] : null;
|
|
272
274
|
const href = only?.props.href;
|
|
273
275
|
const text = only ? headingText(only.props.children).trim() : "";
|
|
274
|
-
// Bare http(s) URL alone in a paragraph →
|
|
275
|
-
if (href && text === href && /^https?:\/\//iu.test(href))
|
|
276
|
+
// Bare http(s) URL alone in a paragraph → player for YouTube / Bilibili, otherwise a link card.
|
|
277
|
+
if (href && text === href && /^https?:\/\//iu.test(href)) {
|
|
278
|
+
if (resolvePlayer(href).ok) return <GardenPlayer href={href} />;
|
|
279
|
+
return <LinkCard href={href} />;
|
|
280
|
+
}
|
|
276
281
|
return <p {...domProps}>{children}</p>;
|
|
277
282
|
},
|
|
278
283
|
a: ({ href: written, children, ...props }) => {
|
|
@@ -53,8 +53,8 @@ export function PostReadingView({
|
|
|
53
53
|
}) {
|
|
54
54
|
const displayTitle = title ?? post.title;
|
|
55
55
|
const numbering = (post as ContentEntry & { numbering?: boolean }).numbering;
|
|
56
|
-
const headings = extractHeadings(post.content, { numbering });
|
|
57
|
-
const wordCount = estimateWordCount(post.content);
|
|
56
|
+
const headings = extractHeadings(post.content ?? "", { numbering });
|
|
57
|
+
const wordCount = estimateWordCount(post.content ?? "");
|
|
58
58
|
const t = ui();
|
|
59
59
|
|
|
60
60
|
return (
|
|
@@ -116,7 +116,7 @@ export function PostReadingView({
|
|
|
116
116
|
<ArticleMeasure />
|
|
117
117
|
<div className="e-content">
|
|
118
118
|
{post.description ? <p className="lead quote-plain p-summary">{post.description}</p> : null}
|
|
119
|
-
<MarkdownArticle content={post.content} numbering={numbering} />
|
|
119
|
+
<MarkdownArticle content={post.content ?? ""} numbering={numbering} />
|
|
120
120
|
</div>
|
|
121
121
|
<ArticleReferences references={post.references} />
|
|
122
122
|
<Webmentions target={canonicalUrl} endpoint={webmentionApiEndpoint()} />
|
package/app/garden-modules.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ declare module "@garden/generated-content" {
|
|
|
14
14
|
readingTime?: string;
|
|
15
15
|
kind: "post" | "column";
|
|
16
16
|
sourcePath: string;
|
|
17
|
-
content
|
|
17
|
+
content?: string;
|
|
18
18
|
column?: string;
|
|
19
19
|
columnTitle?: string;
|
|
20
20
|
columnDescription?: string;
|
|
@@ -42,7 +42,7 @@ declare module "@garden/generated-content" {
|
|
|
42
42
|
readingTime?: string;
|
|
43
43
|
references?: { title: string; url?: string; domain?: string }[];
|
|
44
44
|
sourcePath: string;
|
|
45
|
-
content
|
|
45
|
+
content?: string;
|
|
46
46
|
lang?: "zh" | "en";
|
|
47
47
|
};
|
|
48
48
|
export const gardenDocs: GardenDocEntry[];
|
|
@@ -64,6 +64,14 @@ declare module "@garden/generated-content" {
|
|
|
64
64
|
export const imageSizes: Record<string, { width: number; height: number }>;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
declare module "@garden/generated-post-bodies" {
|
|
68
|
+
export const postBodies: Record<string, string>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
declare module "@garden/generated-handbook-bodies" {
|
|
72
|
+
export const handbookBodies: Record<string, string>;
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
declare module "@garden/site-config" {
|
|
68
76
|
export const searchIndexUrl: string;
|
|
69
77
|
/** The full-text index file and the hash its contents are cached under. */
|
package/app/globals.css
CHANGED
|
@@ -2141,7 +2141,8 @@ button {
|
|
|
2141
2141
|
-webkit-line-clamp: 2;
|
|
2142
2142
|
}
|
|
2143
2143
|
|
|
2144
|
-
.external-embed-error
|
|
2144
|
+
.external-embed-error,
|
|
2145
|
+
.g-player-error {
|
|
2145
2146
|
margin: 32px 0;
|
|
2146
2147
|
padding: 18px;
|
|
2147
2148
|
border-left: 3px solid var(--accent);
|
|
@@ -2150,6 +2151,62 @@ button {
|
|
|
2150
2151
|
font-size: 12px;
|
|
2151
2152
|
}
|
|
2152
2153
|
|
|
2154
|
+
.markdown-body .g-player,
|
|
2155
|
+
.markdown-body .g-player-fallback {
|
|
2156
|
+
width: 100%;
|
|
2157
|
+
max-width: 100%;
|
|
2158
|
+
margin: 36px 0;
|
|
2159
|
+
background: transparent;
|
|
2160
|
+
border: 0;
|
|
2161
|
+
box-shadow: none;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
/* The video uses the article radius token --r (12px). Standalone body
|
|
2165
|
+
images have no border-radius; covers use --r-lg; figure-card photos
|
|
2166
|
+
use 8px inside a padded card shell we are not copying. */
|
|
2167
|
+
.g-player-frame {
|
|
2168
|
+
width: 100%;
|
|
2169
|
+
max-width: 100%;
|
|
2170
|
+
aspect-ratio: 16 / 9;
|
|
2171
|
+
border-radius: var(--r);
|
|
2172
|
+
overflow: hidden;
|
|
2173
|
+
background: #111;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
@supports (overflow-clip-margin: 4px) {
|
|
2177
|
+
.g-player-frame {
|
|
2178
|
+
overflow: clip;
|
|
2179
|
+
overflow-clip-margin: 4px;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
.g-player-frame:focus-within {
|
|
2184
|
+
outline: 2px solid var(--blue);
|
|
2185
|
+
outline-offset: 2px;
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
.g-player-frame iframe {
|
|
2189
|
+
width: 100%;
|
|
2190
|
+
max-width: 100%;
|
|
2191
|
+
height: 100%;
|
|
2192
|
+
display: block;
|
|
2193
|
+
border: 0;
|
|
2194
|
+
border-radius: inherit;
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
/* Same typography as .markdown-body .fig figcaption: a quiet line, not a bar. */
|
|
2198
|
+
.markdown-body .g-player figcaption {
|
|
2199
|
+
margin-top: 12px;
|
|
2200
|
+
font-size: var(--fs-small);
|
|
2201
|
+
line-height: 1.6;
|
|
2202
|
+
color: var(--muted);
|
|
2203
|
+
text-align: center;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
.markdown-body .g-player figcaption a {
|
|
2207
|
+
color: var(--muted);
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2153
2210
|
.markdown-body .garden-chart {
|
|
2154
2211
|
margin: 36px 0;
|
|
2155
2212
|
border: 1px solid var(--line-strong);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { handbookBodies } from "@garden/generated-handbook-bodies";
|
|
2
|
+
import { postBodies } from "@garden/generated-post-bodies";
|
|
3
|
+
import type { ContentEntry, GardenDocEntry } from "./content";
|
|
4
|
+
|
|
5
|
+
export function postWithBody<T extends Pick<ContentEntry, "path">>(entry: T): T & { content: string } {
|
|
6
|
+
return { ...entry, content: postBodies[entry.path] ?? "" };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function docWithBody<T extends Pick<GardenDocEntry, "path">>(
|
|
10
|
+
doc: T,
|
|
11
|
+
lang: "zh" | "en",
|
|
12
|
+
): T & { content: string };
|
|
13
|
+
export function docWithBody(
|
|
14
|
+
doc: GardenDocEntry | null | undefined,
|
|
15
|
+
lang: "zh" | "en",
|
|
16
|
+
): (GardenDocEntry & { content: string }) | null | undefined;
|
|
17
|
+
export function docWithBody(doc: GardenDocEntry | null | undefined, lang: "zh" | "en") {
|
|
18
|
+
if (!doc) return doc;
|
|
19
|
+
return { ...doc, content: handbookBodies[`${lang}:${doc.path}`] ?? "" };
|
|
20
|
+
}
|
|
@@ -69,6 +69,7 @@ export const HANDBOOK_SECTIONS: HandbookSectionMeta[] = [
|
|
|
69
69
|
{ id: "getting-started", title: "快速上手", titleEn: "Getting Started" },
|
|
70
70
|
{ id: "writing-and-reading", title: "写作与排版", titleEn: "Writing & Layout" },
|
|
71
71
|
{ id: "components", title: "内置组件", titleEn: "Built-in Components" },
|
|
72
|
+
{ id: "diagrams", title: "推荐图示", titleEn: "Recommended figures" },
|
|
72
73
|
{ id: "workflow", title: "检查与补全", titleEn: "Checks & Shortcuts" },
|
|
73
74
|
{ id: "reference", title: "组件参考", titleEn: "Component reference" },
|
|
74
75
|
];
|
|
@@ -129,6 +130,7 @@ export const NAV_SHORT_TITLE: Record<string, string> = {
|
|
|
129
130
|
"components/link-card": "链接卡片",
|
|
130
131
|
"components/source": "来源胶囊",
|
|
131
132
|
"components/image": "本地图片",
|
|
133
|
+
"components/diagrams": "推荐图示",
|
|
132
134
|
workflow: "语法检查与快捷输入",
|
|
133
135
|
reference: "总览",
|
|
134
136
|
};
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { PluggableList } from "unified";
|
|
2
2
|
import { markdownSyntaxPlugins } from "../../ui/garden/markdown-syntax.mjs";
|
|
3
3
|
import { remarkGarden } from "./remark-garden.mjs";
|
|
4
|
+
import { remarkVideoPlayer } from "./remark-video-player.mjs";
|
|
4
5
|
|
|
5
6
|
// Keep article rendering and heading extraction on the same Markdown syntax.
|
|
7
|
+
// The spread + remarkGarden list already fails PluggableList checking on HEAD.
|
|
8
|
+
// Adding another plugin in that literal would report a new slot error even when
|
|
9
|
+
// the plugin itself is a Plugin; append it as a typed Pluggable instead.
|
|
6
10
|
export const markdownPlugins: PluggableList = [...markdownSyntaxPlugins, remarkGarden];
|
|
11
|
+
markdownPlugins.push(remarkVideoPlayer);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Root } from "mdast";
|
|
2
|
+
import type { Plugin } from "unified";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Remark plugin: a paragraph that is only a YouTube or Bilibili video URL
|
|
6
|
+
* becomes an in-article player. This is the attacher itself (Plugin), not a
|
|
7
|
+
* factory that returns a Plugin.
|
|
8
|
+
*/
|
|
9
|
+
declare const remarkVideoPlayer: Plugin<[], Root>;
|
|
10
|
+
export { remarkVideoPlayer };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A paragraph whose only content is one YouTube / Bilibili video link becomes
|
|
3
|
+
* an in-article player. `{card}` keeps a link card. In-sentence links, lists
|
|
4
|
+
* of several links, and code stay unchanged. Historical ```embed fences with
|
|
5
|
+
* a playable URL still render, so older drafts are not left as dead code.
|
|
6
|
+
*/
|
|
7
|
+
import { toString } from "mdast-util-to-string";
|
|
8
|
+
import { renderLegacyEmbed, renderPlayerHtml, resolvePlayer } from "../../ui/garden/player.mjs";
|
|
9
|
+
|
|
10
|
+
const CARD_MARKER = "{card}";
|
|
11
|
+
|
|
12
|
+
const meaningful = (children) =>
|
|
13
|
+
(children ?? []).filter((child) => !(child.type === "text" && !child.value.trim()) && child.type !== "break");
|
|
14
|
+
|
|
15
|
+
function htmlNode(value) {
|
|
16
|
+
return { type: "html", value };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function asksForCard(node) {
|
|
20
|
+
const children = node.children ?? [];
|
|
21
|
+
for (let index = 0; index < children.length - 1; index += 1) {
|
|
22
|
+
const link = children[index];
|
|
23
|
+
const next = children[index + 1];
|
|
24
|
+
if (link.type === "link" && next.type === "text" && next.value.startsWith(CARD_MARKER)) return true;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function exclusiveLink(node) {
|
|
30
|
+
const rest = meaningful(node.children);
|
|
31
|
+
return rest.length === 1 && rest[0].type === "link" ? rest[0] : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function replaceParagraph(parent, index, node) {
|
|
35
|
+
if (asksForCard(node)) return false;
|
|
36
|
+
const link = exclusiveLink(node);
|
|
37
|
+
if (!link) return false;
|
|
38
|
+
const player = resolvePlayer(link.url);
|
|
39
|
+
if (!player.ok) return false;
|
|
40
|
+
const title = toString(link).trim();
|
|
41
|
+
parent.children[index] = htmlNode(renderPlayerHtml(player, {
|
|
42
|
+
title: title && title !== link.url ? title : undefined,
|
|
43
|
+
}));
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function walk(parent) {
|
|
48
|
+
const children = parent.children ?? [];
|
|
49
|
+
for (let index = 0; index < children.length; index += 1) {
|
|
50
|
+
const node = children[index];
|
|
51
|
+
if (node.type === "inlineCode" || node.type === "html") continue;
|
|
52
|
+
if (node.type === "code") {
|
|
53
|
+
if (String(node.lang ?? "").toLowerCase() === "embed") {
|
|
54
|
+
children[index] = htmlNode(renderLegacyEmbed(node.value));
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (node.type === "paragraph") {
|
|
59
|
+
replaceParagraph(parent, index, node);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (node.children) walk(node);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function remarkVideoPlayer() {
|
|
67
|
+
return (tree) => {
|
|
68
|
+
walk(tree);
|
|
69
|
+
};
|
|
70
|
+
}
|
package/app/rss.xml/route.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { marked } from "marked";
|
|
2
2
|
import { columnHref, columns, postHref, posts } from "../lib/content";
|
|
3
|
+
import { postWithBody } from "../lib/content-body";
|
|
3
4
|
import { configuredSiteUrl, siteDescription, siteName } from "../lib/site";
|
|
4
5
|
import { contentForFeed } from "../../ui/core/transform.mjs";
|
|
5
6
|
import { withTrailingSlash } from "../../ui/garden/site-href.mjs";
|
|
@@ -20,9 +21,9 @@ const cdata = (value: string) => value.replaceAll("]]>", "]]]]><![CDATA[>");
|
|
|
20
21
|
export async function GET() {
|
|
21
22
|
const origin = configuredSiteUrl();
|
|
22
23
|
const entries = [
|
|
23
|
-
...posts.map((post) => ({ ...post, path: postHref(post) })),
|
|
24
|
+
...posts.map((post) => ({ ...postWithBody(post), path: postHref(post) })),
|
|
24
25
|
...columns.map((entry) => ({
|
|
25
|
-
...entry,
|
|
26
|
+
...postWithBody(entry),
|
|
26
27
|
path: columnHref(entry),
|
|
27
28
|
})),
|
|
28
29
|
].sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
|