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.
Files changed (118) hide show
  1. package/Makefile +1 -1
  2. package/app/api/admin/distribution/route.ts +4 -2
  3. package/app/blog/[...path]/page.tsx +4 -2
  4. package/app/columns/[column]/[...path]/page.tsx +4 -2
  5. package/app/components/GardenMediaGuide.tsx +11 -23
  6. package/app/components/GardenPlayer.tsx +25 -0
  7. package/app/components/HandbookView.tsx +5 -3
  8. package/app/components/MarkdownArticle.tsx +7 -2
  9. package/app/components/PostReadingView.tsx +3 -3
  10. package/app/garden-modules.d.ts +10 -2
  11. package/app/globals.css +58 -1
  12. package/app/lib/content-body.ts +20 -0
  13. package/app/lib/garden-docs-meta.ts +2 -0
  14. package/app/lib/markdown-plugins.ts +5 -0
  15. package/app/lib/remark-video-player.d.mts +10 -0
  16. package/app/lib/remark-video-player.mjs +70 -0
  17. package/app/rss.xml/route.ts +3 -2
  18. package/bin/garden.mjs +18 -78
  19. package/docs/handbook/components/compose.md +3 -1
  20. package/docs/handbook/components/diagrams/chart.md +38 -0
  21. package/docs/handbook/components/diagrams/code-walkthrough.md +38 -0
  22. package/docs/handbook/components/diagrams/columns-join.md +38 -0
  23. package/docs/handbook/components/diagrams/compare.md +38 -0
  24. package/docs/handbook/components/diagrams/converter.md +38 -0
  25. package/docs/handbook/components/diagrams/facets.md +38 -0
  26. package/docs/handbook/components/diagrams/fan-out.md +38 -0
  27. package/docs/handbook/components/diagrams/icon-rail.md +38 -0
  28. package/docs/handbook/components/diagrams/inspect-grid.md +38 -0
  29. package/docs/handbook/components/diagrams/lanes.md +38 -0
  30. package/docs/handbook/components/diagrams/layers.md +38 -0
  31. package/docs/handbook/components/diagrams/other.md +37 -0
  32. package/docs/handbook/components/diagrams/plan.md +38 -0
  33. package/docs/handbook/components/diagrams/previews/chart.svg +7 -0
  34. package/docs/handbook/components/diagrams/previews/code-walkthrough.svg +5 -0
  35. package/docs/handbook/components/diagrams/previews/columns-join.svg +6 -0
  36. package/docs/handbook/components/diagrams/previews/compare.svg +5 -0
  37. package/docs/handbook/components/diagrams/previews/converter.svg +6 -0
  38. package/docs/handbook/components/diagrams/previews/facets.svg +6 -0
  39. package/docs/handbook/components/diagrams/previews/fan-out.svg +8 -0
  40. package/docs/handbook/components/diagrams/previews/icon-rail.svg +5 -0
  41. package/docs/handbook/components/diagrams/previews/inspect-grid.svg +5 -0
  42. package/docs/handbook/components/diagrams/previews/lanes.svg +5 -0
  43. package/docs/handbook/components/diagrams/previews/layers.svg +6 -0
  44. package/docs/handbook/components/diagrams/previews/other.svg +6 -0
  45. package/docs/handbook/components/diagrams/previews/plan.svg +5 -0
  46. package/docs/handbook/components/diagrams/previews/sequence.svg +7 -0
  47. package/docs/handbook/components/diagrams/previews/stepper.svg +5 -0
  48. package/docs/handbook/components/diagrams/previews/steps.svg +7 -0
  49. package/docs/handbook/components/diagrams/previews/tabs.svg +6 -0
  50. package/docs/handbook/components/diagrams/previews/timeline.svg +5 -0
  51. package/docs/handbook/components/diagrams/previews/trace.svg +7 -0
  52. package/docs/handbook/components/diagrams/previews/tunable.svg +4 -0
  53. package/docs/handbook/components/diagrams/previews/two-row.svg +4 -0
  54. package/docs/handbook/components/diagrams/sequence.md +38 -0
  55. package/docs/handbook/components/diagrams/stepper.md +38 -0
  56. package/docs/handbook/components/diagrams/steps.md +38 -0
  57. package/docs/handbook/components/diagrams/tabs.md +38 -0
  58. package/docs/handbook/components/diagrams/timeline.md +38 -0
  59. package/docs/handbook/components/diagrams/trace.md +38 -0
  60. package/docs/handbook/components/diagrams/tunable.md +38 -0
  61. package/docs/handbook/components/diagrams/two-row.md +38 -0
  62. package/docs/handbook/components/diagrams.md +100 -0
  63. package/docs/handbook/components/link-card.md +1 -1
  64. package/docs/handbook/components.md +1 -0
  65. package/docs/handbook/en/components/compose.md +3 -1
  66. package/docs/handbook/en/components/diagrams/chart.md +40 -0
  67. package/docs/handbook/en/components/diagrams/code-walkthrough.md +40 -0
  68. package/docs/handbook/en/components/diagrams/columns-join.md +40 -0
  69. package/docs/handbook/en/components/diagrams/compare.md +40 -0
  70. package/docs/handbook/en/components/diagrams/converter.md +40 -0
  71. package/docs/handbook/en/components/diagrams/facets.md +40 -0
  72. package/docs/handbook/en/components/diagrams/fan-out.md +40 -0
  73. package/docs/handbook/en/components/diagrams/icon-rail.md +40 -0
  74. package/docs/handbook/en/components/diagrams/inspect-grid.md +40 -0
  75. package/docs/handbook/en/components/diagrams/lanes.md +40 -0
  76. package/docs/handbook/en/components/diagrams/layers.md +40 -0
  77. package/docs/handbook/en/components/diagrams/other.md +39 -0
  78. package/docs/handbook/en/components/diagrams/plan.md +40 -0
  79. package/docs/handbook/en/components/diagrams/sequence.md +40 -0
  80. package/docs/handbook/en/components/diagrams/stepper.md +40 -0
  81. package/docs/handbook/en/components/diagrams/steps.md +40 -0
  82. package/docs/handbook/en/components/diagrams/tabs.md +40 -0
  83. package/docs/handbook/en/components/diagrams/timeline.md +40 -0
  84. package/docs/handbook/en/components/diagrams/trace.md +40 -0
  85. package/docs/handbook/en/components/diagrams/tunable.md +40 -0
  86. package/docs/handbook/en/components/diagrams/two-row.md +40 -0
  87. package/docs/handbook/en/components/diagrams.md +100 -0
  88. package/docs/handbook/en/components/link-card.md +1 -1
  89. package/docs/handbook/en/components.md +1 -0
  90. package/docs/handbook/en/start.md +1 -1
  91. package/docs/handbook/en/workflow.md +2 -2
  92. package/docs/handbook/en/writing.md +82 -1
  93. package/docs/handbook/start.md +1 -1
  94. package/docs/handbook/workflow.md +2 -2
  95. package/docs/handbook/writing.md +82 -1
  96. package/editors/dist/{garden-typora-0.14.12.zip → garden-typora-0.14.13.zip} +0 -0
  97. package/editors/dist/typora/main.js +175 -175
  98. package/editors/dist/typora/manifest.json +1 -1
  99. package/editors/dist/typora/style.css +44 -6
  100. package/editors/dist/vscode/{garden-ui-0.14.12.vsix → garden-ui-0.14.13.vsix} +0 -0
  101. package/package.json +1 -1
  102. package/scripts/build-content.mjs +23 -8
  103. package/scripts/dev-server.mjs +30 -2
  104. package/scripts/fetch-embed.mjs +39 -27
  105. package/scripts/handbook-diagrams.mjs +434 -0
  106. package/scripts/link-assets.mjs +6 -0
  107. package/scripts/vinext-runtime.mjs +80 -0
  108. package/templates/site/Makefile +1 -1
  109. package/templates/site/Makefile.en +1 -1
  110. package/ui/compose/compose.css +43 -5
  111. package/ui/garden/markdown-links.mjs +8 -5
  112. package/ui/garden/picker-placement.mjs +106 -2
  113. package/ui/garden/player.mjs +286 -0
  114. package/ui/garden/rehype-keep-punctuation.mjs +87 -18
  115. package/ui/preview/dist/garden-ui.css +43 -5
  116. package/ui/preview/dist/garden-ui.esm.js +4 -4
  117. package/ui/preview/dist/garden-ui.iife.js +4 -4
  118. package/vite.config.ts +84 -23
@@ -5,7 +5,7 @@
5
5
  "author": "hedon",
6
6
  "authorUrl": "https://github.com/hedon954",
7
7
  "repo": "hedon954/garden",
8
- "version": "0.14.12",
8
+ "version": "0.14.13",
9
9
  "minAppVersion": "1.5.0",
10
10
  "minCoreVersion": "2.10.52",
11
11
  "platforms": [
@@ -2135,16 +2135,51 @@
2135
2135
  .g-compose-grid-item[data-span="4"] { grid-column: span 4; }
2136
2136
 
2137
2137
  .g-compose-icon {
2138
+ --gc-icon-size: 15px;
2138
2139
  display: inline-block;
2139
2140
  flex: 0 0 auto;
2140
- width: 15px;
2141
- height: 15px;
2141
+ width: var(--gc-icon-size);
2142
+ height: var(--gc-icon-size);
2142
2143
  color: var(--ink);
2143
2144
  vertical-align: -2px;
2144
2145
  }
2145
- .g-compose-icon[data-size="sm"] { width: 13px; height: 13px; }
2146
- .g-compose-icon[data-size="lg"] { width: 18px; height: 18px; }
2147
- .g-compose-icon[data-size="xl"] { width: 22px; height: 22px; }
2146
+ .g-compose-icon[data-size="sm"] { --gc-icon-size: 13px; }
2147
+ .g-compose-icon[data-size="lg"] { --gc-icon-size: 18px; }
2148
+ .g-compose-icon[data-size="xl"] { --gc-icon-size: 22px; }
2149
+ /* Align row-leading icons with the first line of adjacent text/headings rather than the top of half-leading. */
2150
+ .g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:has(+ .g-compose-text:not([data-size="sm"]):not([data-size="lg"]):not([data-size="xl"])),
2151
+ .g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:has(+ .g-compose-group > .g-compose-text:first-child:not([data-size="sm"]):not([data-size="lg"]):not([data-size="xl"])),
2152
+ .g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:has(+ .g-compose-group > .g-compose-content:first-child) {
2153
+ margin-top: max(0px, calc((1lh - var(--gc-icon-size)) / 2));
2154
+ }
2155
+ .g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:has(+ .g-compose-text[data-size="sm"]),
2156
+ .g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:has(+ .g-compose-group > .g-compose-text[data-size="sm"]:first-child) {
2157
+ margin-top: max(0px, calc((1.3125em - var(--gc-icon-size)) / 2));
2158
+ }
2159
+ .g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:has(+ .g-compose-text[data-size="lg"]),
2160
+ .g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:has(+ .g-compose-group > .g-compose-text[data-size="lg"]:first-child) {
2161
+ margin-top: max(0px, calc((1.125 * 1lh - var(--gc-icon-size)) / 2));
2162
+ }
2163
+ .g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:has(+ .g-compose-text[data-size="xl"]),
2164
+ .g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:has(+ .g-compose-group > .g-compose-text[data-size="xl"]:first-child) {
2165
+ margin-top: max(0px, calc((1.25 * 1lh - var(--gc-icon-size)) / 2));
2166
+ }
2167
+ .g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:not([data-size="xl"]):has(+ .g-compose-h:not([data-size="sm"]):not([data-size="lg"]):not([data-size="xl"])),
2168
+ .g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:not([data-size="xl"]):has(+ .g-compose-group > .g-compose-h:first-child:not([data-size="sm"]):not([data-size="lg"]):not([data-size="xl"])) {
2169
+ margin-top: max(0px, calc((1.4em - var(--gc-icon-size)) / 2));
2170
+ }
2171
+ .g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:has(+ .g-compose-h[data-size="sm"]),
2172
+ .g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:has(+ .g-compose-group > .g-compose-h[data-size="sm"]:first-child) {
2173
+ margin-top: max(0px, calc((1.1375em - var(--gc-icon-size)) / 2));
2174
+ }
2175
+ .g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:has(+ .g-compose-h[data-size="lg"]),
2176
+ .g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:has(+ .g-compose-group > .g-compose-h[data-size="lg"]:first-child) {
2177
+ margin-top: max(0px, calc((1.575em - var(--gc-icon-size)) / 2));
2178
+ }
2179
+ .g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:has(+ .g-compose-h[data-size="xl"]),
2180
+ .g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:has(+ .g-compose-group > .g-compose-h[data-size="xl"]:first-child) {
2181
+ margin-top: max(0px, calc((1.75em - var(--gc-icon-size)) / 2));
2182
+ }
2148
2183
  .g-compose-card[data-size="sm"] > .g-compose-icon {
2149
2184
  width: 14px;
2150
2185
  height: 14px;
@@ -2531,6 +2566,9 @@
2531
2566
  width: 100%;
2532
2567
  min-width: 0;
2533
2568
  }
2569
+ .g-compose-row:not([data-wrap="nowrap"]) > .g-compose-icon {
2570
+ margin-top: 0;
2571
+ }
2534
2572
 
2535
2573
  /* Columnized centered rows stretch labels to 100% width, so align-items
2536
2574
  can center arrows but not text. Center direct labels only; explicit
@@ -5537,6 +5575,6 @@ html[data-theme="dark"] {
5537
5575
  .garden-suggest-label { font-size: 13px; line-height: 1.3; }
5538
5576
  .garden-suggest-hint { opacity: 0.65; font-size: 12px; }
5539
5577
  .garden-suggest-copy { display: flex; flex-direction: column; min-width: 12em; gap: 1px; }
5540
- .auto-suggest-container.garden-picker-open { overflow-x: hidden; overflow-y: auto; box-sizing: border-box; }
5578
+ #ty-auto-suggest.garden-picker-open, .auto-suggest-container.garden-picker-open { overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; box-sizing: border-box; }
5541
5579
  .garden-picker-open .garden-suggest-thumb { width: 72px; height: 48px; min-width: 72px; flex: none; }
5542
5580
  .garden-picker-open .garden-suggest-label, .garden-picker-open .garden-suggest-hint { overflow-wrap: anywhere; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hd-garden",
3
- "version": "0.14.12",
3
+ "version": "0.14.13",
4
4
  "private": false,
5
5
  "description": "用 Markdown 写作、用 GitHub Pages 发布的个人博客引擎。",
6
6
  "license": "MIT",
@@ -71,7 +71,10 @@ const siteConfig = parseYaml(fs.readFileSync(siteConfigPath, "utf8"));
71
71
  const generatedMediaRoot = path.join(root, "public", "media");
72
72
  const gardenDir = path.join(root, ".garden");
73
73
  const output = path.join(gardenDir, "generated-content.ts");
74
+ const postBodiesOutput = path.join(gardenDir, "generated-post-bodies.ts");
75
+ const handbookBodiesOutput = path.join(gardenDir, "generated-handbook-bodies.ts");
74
76
  const siteConfigOutput = path.join(gardenDir, "site-config.ts");
77
+ const omitContent = ({ content, ...rest }) => rest;
75
78
  const includeDrafts = process.env.CONTENT_INCLUDE_DRAFTS === "1";
76
79
  // Local preview shows broken ui blocks as error cards; public builds fail on them.
77
80
  const uiMode = includeDrafts || process.env.GARDEN_CONTENT_MODE === "preview" ? "preview" : "publish";
@@ -726,7 +729,7 @@ export type ContentEntry = {
726
729
  readingTime?: string;
727
730
  kind: "post" | "column";
728
731
  sourcePath: string;
729
- content: string;
732
+ content?: string;
730
733
  column?: string;
731
734
  columnTitle?: string;
732
735
  columnDescription?: string;
@@ -747,19 +750,19 @@ export type GardenDocEntry = {
747
750
  order: number;
748
751
  badge?: string;
749
752
  references?: { title: string; url?: string; domain?: string }[];
750
- content: string;
753
+ content?: string;
751
754
  };
752
755
 
753
756
  export type LinkCardMeta = { title: string; description?: string; siteName?: string; author?: string; publishedTime?: string; image?: string };
754
757
  export type LinkAssets = { favicons: Record<string, string>; cards: Record<string, LinkCardMeta> };
755
758
 
756
759
  export const contentHash = "${contentHash}";
757
- export const posts = ${JSON.stringify(posts, null, 2)} as ContentEntry[];
758
- export const columns = ${JSON.stringify(columns, null, 2)} as ContentEntry[];
759
- export const gardenDocs = ${JSON.stringify(gardenDocs, null, 2)} as GardenDocEntry[];
760
- export const gardenIndexDoc = ${JSON.stringify(gardenIndexDoc, null, 2)} as GardenDocEntry | null;
761
- export const gardenDocsEn = ${JSON.stringify(gardenDocsEn, null, 2)} as GardenDocEntry[];
762
- export const gardenIndexDocEn = ${JSON.stringify(gardenIndexDocEn, null, 2)} as GardenDocEntry | null;
760
+ export const posts = ${JSON.stringify(posts.map(omitContent), null, 2)} as ContentEntry[];
761
+ export const columns = ${JSON.stringify(columns.map(omitContent), null, 2)} as ContentEntry[];
762
+ export const gardenDocs = ${JSON.stringify(gardenDocs.map(omitContent), null, 2)} as GardenDocEntry[];
763
+ export const gardenIndexDoc = ${JSON.stringify(gardenIndexDoc ? omitContent(gardenIndexDoc) : null, null, 2)} as GardenDocEntry | null;
764
+ export const gardenDocsEn = ${JSON.stringify(gardenDocsEn.map(omitContent), null, 2)} as GardenDocEntry[];
765
+ export const gardenIndexDocEn = ${JSON.stringify(gardenIndexDocEn ? omitContent(gardenIndexDocEn) : null, null, 2)} as GardenDocEntry | null;
763
766
  export const a2WordCounts = ${JSON.stringify(a2WordCounts, null, 2)} as Record<string, number>;
764
767
  export const linkAssets = ${JSON.stringify(linkAssetsForRender(root, basePath), null, 2)} as LinkAssets;
765
768
  export const imageSizes: Record<string, { width: number; height: number }> = ${JSON.stringify(imageSizes)};
@@ -767,6 +770,18 @@ export const imageSizes: Record<string, { width: number; height: number }> = ${J
767
770
 
768
771
  fs.mkdirSync(path.dirname(output), { recursive: true });
769
772
  writeChanged(output, source);
773
+ writeChanged(
774
+ postBodiesOutput,
775
+ `// Generated by garden sync. Edit files in content/ instead.
776
+ export const postBodies = ${JSON.stringify(Object.fromEntries(posts.map((post) => [post.path, post.content])), null, 2)} as Record<string, string>;
777
+ `,
778
+ );
779
+ writeChanged(
780
+ handbookBodiesOutput,
781
+ `// Generated by garden sync. Edit files in docs/handbook/ instead.
782
+ export const handbookBodies = ${JSON.stringify(Object.fromEntries(allHandbookDocs.map((doc) => [`${doc.lang}:${doc.path}`, doc.content])), null, 2)} as Record<string, string>;
783
+ `,
784
+ );
770
785
  const fullIndex = buildFullIndex({ posts, columns });
771
786
  writeChanged(
772
787
  siteConfigOutput,
@@ -13,6 +13,34 @@ const read = (file) => {
13
13
  catch (error) { if (error.code === "ENOENT") return null; throw error; }
14
14
  };
15
15
  const ps = (pid, field) => execFileSync("ps", ["-p", String(pid), "-o", `${field}=`], { encoding: "utf8", env: { ...process.env, LC_ALL: "C" } }).trim();
16
+ function childPids(pid) {
17
+ try {
18
+ const out = execFileSync("pgrep", ["-P", String(pid)], {
19
+ encoding: "utf8",
20
+ env: { ...process.env, LC_ALL: "C" },
21
+ }).trim();
22
+ return out ? out.split("\n").map(Number).filter((child) => Number.isInteger(child) && child > 1) : [];
23
+ } catch {
24
+ return [];
25
+ }
26
+ }
27
+
28
+ function descendants(pid) {
29
+ return childPids(pid).flatMap((child) => [child, ...descendants(child)]);
30
+ }
31
+
32
+ function stopProven(pid) {
33
+ const tree = descendants(pid);
34
+ process.kill(pid, "SIGTERM");
35
+ for (const child of tree) {
36
+ try {
37
+ if (alive(child)) process.kill(child, "SIGTERM");
38
+ } catch (error) {
39
+ if (error.code !== "ESRCH") throw error;
40
+ }
41
+ }
42
+ }
43
+
16
44
  function identity(pid, executable) {
17
45
  if (process.platform !== "linux" && process.platform !== "darwin") throw new Error("Automatic dev replacement requires Linux or macOS process inspection.");
18
46
  if (Number(ps(pid, "uid")) !== process.getuid()) throw new Error("Dev process belongs to another user; nothing was stopped.");
@@ -78,8 +106,8 @@ export async function startDev({ cwd, executable, args = [], env = process.env,
78
106
  // Recheck identity immediately before signalling to reduce PID-reuse races.
79
107
  const current = identity(lock.pid, executable);
80
108
  if (current.start !== previous.start || current.cwd !== cwd || read(lockPath)?.startedAt !== lock.startedAt) throw new Error("Dev owner changed during replacement; nothing was stopped.");
81
- process.kill(lock.pid, "SIGTERM");
82
- while (alive(lock.pid) || !(await portFree(lock))) {
109
+ stopProven(lock.pid);
110
+ while (alive(lock.pid) || descendants(lock.pid).some(alive) || !(await portFree(lock))) {
83
111
  if (Date.now() >= deadline) throw new Error("Previous dev server did not release its process/port in time; no force-kill attempted.");
84
112
  await delay(100);
85
113
  }
@@ -1,5 +1,6 @@
1
1
  import { lookup } from "node:dns/promises";
2
2
  import { isIP } from "node:net";
3
+ import { authorPasteUrl, resolvePlayer } from "../ui/garden/player.mjs";
3
4
 
4
5
  const timeoutMs = 8000;
5
6
  const maxBytes = 1_000_000;
@@ -98,10 +99,6 @@ function pageTitle(html) {
98
99
  return match ? decodeEntities(match.replaceAll(/\s+/gu, " ")) : undefined;
99
100
  }
100
101
 
101
- function yamlScalar(value) {
102
- return JSON.stringify(value);
103
- }
104
-
105
102
  async function readLimited(response) {
106
103
  const type = response.headers.get("content-type") ?? "";
107
104
  if (!/text\/html|application\/xhtml\+xml/iu.test(type) && type !== "") {
@@ -143,7 +140,7 @@ async function fetchPublic(url, redirectsLeft) {
143
140
  return fetchPublic(next, redirectsLeft - 1);
144
141
  }
145
142
  if (!response.ok) fail(`抓取失败:HTTP ${response.status}。`);
146
- return readLimited(response);
143
+ return { url, html: await readLimited(response) };
147
144
  } catch (error) {
148
145
  if (error?.name === "AbortError") fail("抓取超时。");
149
146
  throw error;
@@ -154,33 +151,48 @@ async function fetchPublic(url, redirectsLeft) {
154
151
 
155
152
  async function main() {
156
153
  const raw = option("URL") || process.argv[2] || "";
157
- if (!raw) fail('用法:make embed URL="https://example.com/article"');
158
-
159
- const url = await assertPublicHttpUrl(raw);
160
- const html = await fetchPublic(url, maxRedirects);
161
- const title = metaContent(html, ["og:title", "twitter:title"]) ?? pageTitle(html);
162
- const description = metaContent(html, ["og:description", "twitter:description", "description"]);
163
- const image = metaContent(html, ["og:image", "twitter:image", "twitter:image:src"]);
164
-
154
+ if (!raw) fail('用法:make embed URL="https://www.youtube.com/watch?v=..." 或一篇网页地址。YouTube / Bilibili 会打印可直接粘贴的一行链接。');
155
+
156
+ const starting = await assertPublicHttpUrl(raw);
157
+ const alreadyPlayable = resolvePlayer(starting);
158
+ let finalUrl = starting;
159
+ let title;
160
+ let description;
161
+ let image;
162
+ try {
163
+ const snapshot = await fetchPublic(starting, maxRedirects);
164
+ finalUrl = snapshot.url;
165
+ title = metaContent(snapshot.html, ["og:title", "twitter:title"]) ?? pageTitle(snapshot.html);
166
+ description = metaContent(snapshot.html, ["og:description", "twitter:description", "description"]);
167
+ image = metaContent(snapshot.html, ["og:image", "twitter:image", "twitter:image:src"]);
168
+ } catch (error) {
169
+ if (!alreadyPlayable.ok) throw error;
170
+ }
171
+ const player = resolvePlayer(finalUrl);
172
+ const paste = (player.ok ? player.pageUrl : authorPasteUrl(finalUrl)) || finalUrl.toString();
173
+
174
+ process.stdout.write(`${paste}\n`);
175
+ const hints = [];
176
+ if (player.ok) {
177
+ hints.push(`已规范化为可粘贴的 ${player.label} 地址:单独一行写入文章即可在站内播放。`);
178
+ } else if (alreadyPlayable.reason === "short-link") {
179
+ hints.push("这是需要跳转的短链。文章渲染不会在构建时解析短链;请把跳转后的完整视频链接单独一行贴进文章。");
180
+ } else if (player.reason === "unsupported-youtube-url" || player.reason === "unsupported-bilibili-url") {
181
+ hints.push("这不是可嵌入的单个视频地址,单独一行时会显示为链接卡片,而不是站内播放器。");
182
+ } else {
183
+ hints.push("这不是 YouTube / Bilibili 视频。单独一行会显示链接卡片;若要在句中链接后显示卡片,在链接后紧跟 {card}。");
184
+ }
185
+ if (title) hints.push(`title: ${title}`);
186
+ if (description) hints.push(`description: ${description}`);
165
187
  if (image) {
166
188
  try {
167
- const imageUrl = new URL(image, url);
168
- if (!["http:", "https:"].includes(imageUrl.protocol)) fail("封面地址不是 http(s)。");
189
+ const imageUrl = new URL(image, finalUrl);
190
+ if (["http:", "https:"].includes(imageUrl.protocol)) hints.push(`image: ${imageUrl.toString()}`);
169
191
  } catch {
170
- fail("封面地址无效。");
192
+ // Hint-only; ignore a malformed image URL.
171
193
  }
172
194
  }
173
-
174
- const lines = ["```embed", `url: ${url.toString()}`];
175
- if (title) lines.push(`title: ${yamlScalar(title)}`);
176
- if (description) lines.push(`description: ${yamlScalar(description)}`);
177
- if (image) lines.push(`image: ${yamlScalar(new URL(image, url).toString())}`);
178
- lines.push("```");
179
-
180
- process.stdout.write(`${lines.join("\n")}\n`);
181
- if (!title && !description) {
182
- process.stderr.write("已生成围栏,但页面没有标题或摘要,请手工补全。\n");
183
- }
195
+ if (hints.length) process.stderr.write(`${hints.join("\n")}\n`);
184
196
  }
185
197
 
186
198
  main().catch((error) => {