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
|
@@ -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:
|
|
2141
|
-
height:
|
|
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"] {
|
|
2146
|
-
.g-compose-icon[data-size="lg"] {
|
|
2147
|
-
.g-compose-icon[data-size="xl"] {
|
|
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; }
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
package/scripts/dev-server.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/scripts/fetch-embed.mjs
CHANGED
|
@@ -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://
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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,
|
|
168
|
-
if (
|
|
189
|
+
const imageUrl = new URL(image, finalUrl);
|
|
190
|
+
if (["http:", "https:"].includes(imageUrl.protocol)) hints.push(`image: ${imageUrl.toString()}`);
|
|
169
191
|
} catch {
|
|
170
|
-
|
|
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) => {
|