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
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate handbook pages and preview SVGs from ui/garden/diagram-recipes.mjs.
|
|
3
|
+
* The Typora and VS Code pickers both read that same list.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/handbook-diagrams.mjs
|
|
7
|
+
* node scripts/handbook-diagrams.mjs --check
|
|
8
|
+
*/
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { DIAGRAM_RECIPES } from "../ui/garden/diagram-recipes.mjs";
|
|
13
|
+
|
|
14
|
+
const ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
15
|
+
const PREVIEW_DIR = path.join(ROOT, "docs/handbook/components/diagrams/previews");
|
|
16
|
+
const ZH_DIR = path.join(ROOT, "docs/handbook/components/diagrams");
|
|
17
|
+
const EN_DIR = path.join(ROOT, "docs/handbook/en/components/diagrams");
|
|
18
|
+
const ZH_INDEX = path.join(ROOT, "docs/handbook/components/diagrams.md");
|
|
19
|
+
const EN_INDEX = path.join(ROOT, "docs/handbook/en/components/diagrams.md");
|
|
20
|
+
const STALE_FILES = [
|
|
21
|
+
path.join(ZH_DIR, "index.md"),
|
|
22
|
+
path.join(EN_DIR, "index.md"),
|
|
23
|
+
];
|
|
24
|
+
const MARKER = "<!-- Generated by scripts/handbook-diagrams.mjs. Do not edit by hand. -->\n";
|
|
25
|
+
|
|
26
|
+
/** English copy for the handbook only. The picker still inserts the Chinese brief. */
|
|
27
|
+
const EN = {
|
|
28
|
+
"columns-join": {
|
|
29
|
+
label: "Join diagram",
|
|
30
|
+
hint: "Use this to show how two sides connect through a middle item",
|
|
31
|
+
when: "Two sides connect through a middle item",
|
|
32
|
+
whenNot: "If there is no middle connection, do not force one",
|
|
33
|
+
routeHint: "First name the two sides, how the middle item joins them, and which block is the focus",
|
|
34
|
+
},
|
|
35
|
+
"fan-out": {
|
|
36
|
+
label: "Fan-out diagram",
|
|
37
|
+
hint: "Use this to show a first stop that then branches to several places at once",
|
|
38
|
+
when: "A first stop, then several places at once",
|
|
39
|
+
whenNot: "If there is no real first stop and then a branch, do not draw a fan-out",
|
|
40
|
+
routeHint: "First say who reaches whom, then which places are reached at once, and which place is the focus",
|
|
41
|
+
},
|
|
42
|
+
layers: {
|
|
43
|
+
label: "Layer diagram",
|
|
44
|
+
hint: "Use this to show top-down layers that do not have to be walked in order",
|
|
45
|
+
when: "Top-down layers",
|
|
46
|
+
whenNot: "Layers are not automatically a flow that must be walked in order",
|
|
47
|
+
routeHint: "Name each layer, say which layers are the focus, and whether every request follows this order",
|
|
48
|
+
},
|
|
49
|
+
facets: {
|
|
50
|
+
label: "Aspect diagram",
|
|
51
|
+
hint: "Use this to show several aspects of one thing, with no required order",
|
|
52
|
+
when: "Several aspects of one thing, with no required order",
|
|
53
|
+
whenNot: "If there is no order, do not draw a flow. If you are not comparing options under the same conditions, do not use a comparison diagram",
|
|
54
|
+
routeHint: "List a short name and a note for each aspect, and say whether the blocks have a strict order",
|
|
55
|
+
},
|
|
56
|
+
"two-row": {
|
|
57
|
+
label: "Two-row contrast",
|
|
58
|
+
hint: "Use this to contrast a background process with the process you want to emphasize, on two rows",
|
|
59
|
+
when: "The upper row is background; the lower row is the process you want to explain",
|
|
60
|
+
whenNot: "If you are not contrasting two processes, or you are comparing options under the same conditions, do not use this two-row layout",
|
|
61
|
+
routeHint: "Name the stages of the background process above and the focus process below, and say which row is the focus",
|
|
62
|
+
},
|
|
63
|
+
"icon-rail": {
|
|
64
|
+
label: "Illustrated list",
|
|
65
|
+
hint: "Use this to explain independent items one by one",
|
|
66
|
+
when: "Independent notes, each with an icon, a title, and body text",
|
|
67
|
+
whenNot: "If the items have no order dependence, do not draw a flow, and do not write them as a checklist to follow",
|
|
68
|
+
routeHint: "Lead each independent note with an icon, and keep the title and body together. This is not a flow, so do not add sequence arrows",
|
|
69
|
+
},
|
|
70
|
+
sequence: {
|
|
71
|
+
label: "Flow diagram",
|
|
72
|
+
hint: "Use this to show the order in which something is done",
|
|
73
|
+
when: "The order in which something is done",
|
|
74
|
+
whenNot: "Do not add arrows to unordered items, and do not shrink type to force a horizontal row on a narrow screen",
|
|
75
|
+
routeHint: "Use arrows only when the source text has an order. Short stages can run left to right; long stages or a narrow screen should stack top to bottom",
|
|
76
|
+
},
|
|
77
|
+
steps: {
|
|
78
|
+
label: "Step diagram",
|
|
79
|
+
hint: "Use this to explain something step by step, with room for detail inside a step",
|
|
80
|
+
when: "A step-by-step explanation, with room for detail in a step",
|
|
81
|
+
whenNot: "This is not a time-scaled bar, not a checklist to follow, and not a stepper for watching state change",
|
|
82
|
+
routeHint: "Use a step diagram to make the order clear, and expand detail inside a step when needed. Do not turn it into a to-do list or a stepper unless the material really is that",
|
|
83
|
+
},
|
|
84
|
+
compare: {
|
|
85
|
+
label: "Comparison diagram",
|
|
86
|
+
hint: "Use this to compare two or three options under the same conditions",
|
|
87
|
+
when: "Two or three options compared under the same conditions",
|
|
88
|
+
whenNot: "If you are only listing traits and there is no shared condition, use an ordinary table or an aspect diagram",
|
|
89
|
+
routeHint: "Compare only when there is a real shared condition. Keep each side's facts, and do not invent a winner or a ranking",
|
|
90
|
+
},
|
|
91
|
+
timeline: {
|
|
92
|
+
label: "Timeline",
|
|
93
|
+
hint: "Use this to arrange events by date or time",
|
|
94
|
+
when: "Events arranged by date or time",
|
|
95
|
+
whenNot: "If the material is not a duration-scaled schedule, do not draw a Gantt chart with bar lengths",
|
|
96
|
+
routeHint: "Arrange events by the dates or times in the source, and keep that order. Do not turn it into a duration-scaled progress bar",
|
|
97
|
+
},
|
|
98
|
+
lanes: {
|
|
99
|
+
label: "Message sequence",
|
|
100
|
+
hint: "Use this to show who sent what to whom, and when",
|
|
101
|
+
when: "Who sent what to whom, and when",
|
|
102
|
+
whenNot: "This is not a general modeling language, and it does not support arbitrary branches, loops, or automatic routing",
|
|
103
|
+
routeHint: "Name the participants and the direction and order of their messages from the source. Do not add branches or loops that are not in the source",
|
|
104
|
+
},
|
|
105
|
+
tabs: {
|
|
106
|
+
label: "Tabs",
|
|
107
|
+
hint: "Use this when several groups of notes are independent and the reader should see one group at a time",
|
|
108
|
+
when: "Independent groups of notes, one group visible at a time",
|
|
109
|
+
whenNot: "Do not hide facts that must be compared at the same time. This is not app navigation",
|
|
110
|
+
routeHint: "Use tabs only when the groups can be read separately. Do not hide content that must be compared side by side",
|
|
111
|
+
},
|
|
112
|
+
chart: {
|
|
113
|
+
label: "Chart",
|
|
114
|
+
hint: "Use this to turn numbers you already have into bars, lines, or shares",
|
|
115
|
+
when: "Existing numbers drawn as bars, lines, or shares",
|
|
116
|
+
whenNot: "Do not invent data. If the result should change with an input, prefer a tunable chart",
|
|
117
|
+
routeHint: "Chart only real numbers from the material, and label the axes and units. Do not invent values",
|
|
118
|
+
},
|
|
119
|
+
tunable: {
|
|
120
|
+
label: "Tunable chart",
|
|
121
|
+
hint: "Use this when the reader should change one or two inputs and watch the result",
|
|
122
|
+
when: "One or two inputs the reader can change, and a result that follows",
|
|
123
|
+
whenNot: "If there is only one fixed example, or too many inputs, split it first. If one input should drive several outputs, use a conversion diagram",
|
|
124
|
+
routeHint: "Name the inputs and the conclusion that changes with them. The default state should already make sense. Do not ask the author to write a program",
|
|
125
|
+
},
|
|
126
|
+
converter: {
|
|
127
|
+
label: "Conversion diagram",
|
|
128
|
+
hint: "Use this when one input should produce several results at once",
|
|
129
|
+
when: "One input converted into several results at once",
|
|
130
|
+
whenNot: "If there is only one result, use a tunable chart. If each result needs its own control, split them first",
|
|
131
|
+
routeHint: "Keep the formulas and units for one input that yields several results. Do not invent conversions that are not in the material",
|
|
132
|
+
},
|
|
133
|
+
"code-walkthrough": {
|
|
134
|
+
label: "Code walkthrough",
|
|
135
|
+
hint: "Use this to walk through code line by line: what runs, and what the variables are",
|
|
136
|
+
when: "Line by line, what runs and what the variables are",
|
|
137
|
+
whenNot: "If you only need to show the code, use an ordinary code block. If the code is long, cut it to the key stretch first",
|
|
138
|
+
routeHint: "Walk a short, important stretch of code. Say which line is running and what the variables are at that moment. Do not change facts in the original code",
|
|
139
|
+
},
|
|
140
|
+
stepper: {
|
|
141
|
+
label: "Stepper",
|
|
142
|
+
hint: "Use this when the reader should pause on a step and see how state changes",
|
|
143
|
+
when: "The reader pauses on a step and sees how state changes",
|
|
144
|
+
whenNot: "If there are only two or three static stages, use a step diagram or a flow diagram",
|
|
145
|
+
routeHint: "Let the reader step through how state changes. If the stages are only static, use a step diagram instead",
|
|
146
|
+
},
|
|
147
|
+
trace: {
|
|
148
|
+
label: "Path callout",
|
|
149
|
+
hint: "Use this to mark how far you have walked on an existing flow or state diagram",
|
|
150
|
+
when: "A mark for how far you have walked on an existing flow or state diagram",
|
|
151
|
+
whenNot: "If you only need the structure, or there are too many nodes and edges, split it first",
|
|
152
|
+
routeHint: "Mark the current step on an existing state or flow diagram. Do not invent nodes or transitions that are not in the source",
|
|
153
|
+
},
|
|
154
|
+
plan: {
|
|
155
|
+
label: "Checklist",
|
|
156
|
+
hint: "Use this when the reader should follow items one by one and come back to check progress",
|
|
157
|
+
when: "A list the reader can follow and return to",
|
|
158
|
+
whenNot: "If these are only conceptual stages, use a list or a step diagram. If the reader should watch state change between steps, use a stepper",
|
|
159
|
+
routeHint: "List the steps in the order the reader will follow, and keep the wording from the source. Do not force conceptual stages into to-dos",
|
|
160
|
+
},
|
|
161
|
+
"inspect-grid": {
|
|
162
|
+
label: "Grid diagram",
|
|
163
|
+
hint: "Use this to mark which cells in a two-dimensional structure matter, and why",
|
|
164
|
+
when: "A two-dimensional structure, with certain cells called out",
|
|
165
|
+
whenNot: "If this is an ordinary data table and no cell needs a special note, use an ordinary table",
|
|
166
|
+
routeHint: "Present the two-dimensional structure as a grid, and point out the cells that need a special note and why. Do not force an ordinary table into a grid diagram",
|
|
167
|
+
},
|
|
168
|
+
other: {
|
|
169
|
+
label: "Other",
|
|
170
|
+
hint: "Use this when no built-in figure fits, and you need to combine text and graphics freely",
|
|
171
|
+
when: "No built-in figure fits, or you need to combine text and graphics freely",
|
|
172
|
+
whenNot: "Do not treat the built-in list as a closed allowlist, and do not learn a new syntax for this",
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
function authorPromptEn(item, en) {
|
|
177
|
+
if (item.open) {
|
|
178
|
+
return [
|
|
179
|
+
"Please combine text and graphics freely in one garden compose fence, from the material and the communication goal I provide. When no built-in figure fits, or several kinds of content need to sit together, use ordinary Markdown / HTML and the existing building blocks.",
|
|
180
|
+
"",
|
|
181
|
+
"My material:",
|
|
182
|
+
"- (paste the content, relationships, and source wording)",
|
|
183
|
+
"",
|
|
184
|
+
"My goal:",
|
|
185
|
+
"- (what the reader should understand, and what the focus is)",
|
|
186
|
+
"",
|
|
187
|
+
"Please:",
|
|
188
|
+
"- Organize only from the material; do not invent facts or conclusions",
|
|
189
|
+
"- Handle the graphic syntax, layout, and mobile reading yourself; do not ask me to learn a new syntax or write CSS",
|
|
190
|
+
"- The built-in figures in the list are a starting point, not a closed allowlist; if no recipe fits, extract the real relationships and combine building blocks",
|
|
191
|
+
"- Do not limit the result to one ready-made template; mix ordinary Markdown, safe HTML, and basic blocks such as cards, rows, columns, or steps when needed",
|
|
192
|
+
].join("\n");
|
|
193
|
+
}
|
|
194
|
+
return [
|
|
195
|
+
`Please make a "${en.label}" diagram in garden compose from the material and the communication goal I provide.`,
|
|
196
|
+
"",
|
|
197
|
+
"My material:",
|
|
198
|
+
"- (paste the original items and wording to keep)",
|
|
199
|
+
"",
|
|
200
|
+
"My goal:",
|
|
201
|
+
"- (what the reader should see at a glance)",
|
|
202
|
+
"",
|
|
203
|
+
"Please:",
|
|
204
|
+
"- Organize only from the material; do not invent facts or conclusions",
|
|
205
|
+
"- Handle the graphic syntax, layout, and mobile reading yourself; do not ask me to learn tags or write CSS",
|
|
206
|
+
`- "${en.label}" is the recommended starting point: ${en.routeHint}`,
|
|
207
|
+
"- A recommended figure is not the only valid form; you may still combine necessary Markdown / HTML building blocks in the same compose fence",
|
|
208
|
+
"- Do not start a different author fence or a new syntax; use only garden compose",
|
|
209
|
+
].join("\n");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function handbookEnglish(item) {
|
|
213
|
+
const en = EN[item.name];
|
|
214
|
+
if (!en) throw new Error(`Missing English handbook copy for ${item.name}`);
|
|
215
|
+
return { ...en, prompt: authorPromptEn(item, en) };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function previewSvgMarkup(item) {
|
|
219
|
+
const prefix = "data:image/svg+xml,";
|
|
220
|
+
if (!item.preview?.startsWith(prefix)) throw new Error(`Missing svg preview for ${item.name}`);
|
|
221
|
+
const svg = decodeURIComponent(item.preview.slice(prefix.length));
|
|
222
|
+
if (!svg.includes('width="72"') || !svg.includes('height="48"') || !svg.includes('viewBox="0 0 72 48"')) {
|
|
223
|
+
throw new Error(`Unexpected preview markup for ${item.name}`);
|
|
224
|
+
}
|
|
225
|
+
return svg.replace('width="72"', 'width="216"').replace('height="48"', 'height="144"');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function generatedPaths() {
|
|
229
|
+
const files = [
|
|
230
|
+
ZH_INDEX,
|
|
231
|
+
EN_INDEX,
|
|
232
|
+
];
|
|
233
|
+
for (const item of DIAGRAM_RECIPES) {
|
|
234
|
+
files.push(path.join(PREVIEW_DIR, `${item.name}.svg`));
|
|
235
|
+
files.push(path.join(ZH_DIR, `${item.name}.md`));
|
|
236
|
+
files.push(path.join(EN_DIR, `${item.name}.md`));
|
|
237
|
+
}
|
|
238
|
+
return files;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function yamlText(value) {
|
|
242
|
+
return JSON.stringify(String(value));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function zhPage(item, order) {
|
|
246
|
+
const fence = item.body.trim();
|
|
247
|
+
const aside = item.open
|
|
248
|
+
? "「其他」不是又一种现成模板。现成图示无法表达、或需要把几种内容放在一起时,在同一个 `garden compose` 里用任意 Markdown、安全 HTML,以及卡片、行列、步骤等基础块自由组合。推荐说明草稿不是语法课,也不要把列表当成合法白名单。"
|
|
249
|
+
: "推荐说明草稿是起点,不是语法课,也不是唯一合法写法。需要时仍可在同一个 `garden compose` 里搭配 Markdown 或安全 HTML 基础块。";
|
|
250
|
+
return `---
|
|
251
|
+
title: ${yamlText(item.label)}
|
|
252
|
+
navTitle: ${yamlText(item.label)}
|
|
253
|
+
description: ${yamlText(item.hint)}
|
|
254
|
+
section: diagrams
|
|
255
|
+
order: ${order}
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
${MARKER}
|
|
259
|
+

|
|
260
|
+
|
|
261
|
+
${item.hint}。
|
|
262
|
+
|
|
263
|
+
${item.whenNot}。
|
|
264
|
+
|
|
265
|
+
Typora 和 VS Code / Cursor 共用同一份推荐列表。在 \`/garden\` 里,这一项的名称和适用说明与上面相同。选中后会插入下面的 \`garden compose\` 说明草稿。补全材料和表达目标,再交给 AI 编译。
|
|
266
|
+
|
|
267
|
+
${aside}
|
|
268
|
+
|
|
269
|
+
\`\`\`\`markdown
|
|
270
|
+
${fence}
|
|
271
|
+
\`\`\`\`
|
|
272
|
+
`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function enPage(item, order) {
|
|
276
|
+
const en = handbookEnglish(item);
|
|
277
|
+
const fence = "```garden compose\n" + en.prompt + "\n```";
|
|
278
|
+
const aside = item.open
|
|
279
|
+
? "Other is not another ready-made template. When no built-in figure fits, or several kinds of content need to sit together, combine ordinary Markdown, safe HTML, and basic blocks such as cards, rows, columns, or steps in the same `garden compose` fence. The recommended brief is not a syntax lesson, and the list is not a closed allowlist."
|
|
280
|
+
: "The recommended brief is a starting point, not a syntax lesson, and not the only valid form. You may still combine Markdown or safe HTML building blocks in the same `garden compose` fence.";
|
|
281
|
+
return `---
|
|
282
|
+
title: ${yamlText(en.label)}
|
|
283
|
+
navTitle: ${yamlText(en.label)}
|
|
284
|
+
description: ${yamlText(en.hint)}
|
|
285
|
+
section: diagrams
|
|
286
|
+
order: ${order}
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
${MARKER}
|
|
290
|
+

|
|
291
|
+
|
|
292
|
+
${en.hint}.
|
|
293
|
+
|
|
294
|
+
${en.whenNot}.
|
|
295
|
+
|
|
296
|
+
Typora and VS Code / Cursor share this list. In the \`/garden\` picker, this row is the same figure and the same use case. Choosing it inserts a \`garden compose\` brief. Fill in your material and goal, then ask the AI to compile the post.
|
|
297
|
+
|
|
298
|
+
The built-in picker currently inserts that brief in Chinese. The English wording below is the same recommended brief, written for the English handbook so you can copy it when you write in English.
|
|
299
|
+
|
|
300
|
+
${aside}
|
|
301
|
+
|
|
302
|
+
\`\`\`\`markdown
|
|
303
|
+
${fence}
|
|
304
|
+
\`\`\`\`
|
|
305
|
+
`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function zhIndex() {
|
|
309
|
+
const count = DIAGRAM_RECIPES.length;
|
|
310
|
+
const named = DIAGRAM_RECIPES.filter((item) => !item.open).length;
|
|
311
|
+
const cards = DIAGRAM_RECIPES.map((item) => {
|
|
312
|
+
return `
|
|
313
|
+
|
|
314
|
+
**[${item.label}](/garden/components/diagrams/${item.name}/)** ${item.hint}。`;
|
|
315
|
+
}).join("\n\n");
|
|
316
|
+
return `---
|
|
317
|
+
title: "推荐图示"
|
|
318
|
+
navTitle: "推荐图示"
|
|
319
|
+
description: "Typora 和 VS Code 内置推荐图示的名称、示意图、适用场景和推荐说明草稿。"
|
|
320
|
+
section: diagrams
|
|
321
|
+
order: 1
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
${MARKER}
|
|
325
|
+
|
|
326
|
+
Typora 和 VS Code / Cursor 的 \`/garden\` 列表来自同一份内置推荐列表,目前共 ${count} 项:${named} 种常用图示,最后一项是「其他」。选中一项会插入统一的 \`garden compose\` 说明草稿,不是另一种作者围栏。推荐说明草稿用来写清材料和表达目标,不是语法课。
|
|
327
|
+
|
|
328
|
+
「其他」用于现成图示无法表达、或需要自由组合图文的内容。不必把下面的列表当成合法白名单。
|
|
329
|
+
|
|
330
|
+
入口和编译步骤见 [Compose](/garden/components/compose) 与 [语法检查与快捷输入](/garden/workflow)。
|
|
331
|
+
|
|
332
|
+
${cards}
|
|
333
|
+
`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function enIndex() {
|
|
337
|
+
const count = DIAGRAM_RECIPES.length;
|
|
338
|
+
const named = DIAGRAM_RECIPES.filter((item) => !item.open).length;
|
|
339
|
+
const cards = DIAGRAM_RECIPES.map((item) => {
|
|
340
|
+
const en = handbookEnglish(item);
|
|
341
|
+
return `
|
|
342
|
+
|
|
343
|
+
**[${en.label}](/garden/en/components/diagrams/${item.name}/)** ${en.hint}.`;
|
|
344
|
+
}).join("\n\n");
|
|
345
|
+
return `---
|
|
346
|
+
title: "Recommended figures"
|
|
347
|
+
navTitle: "Recommended figures"
|
|
348
|
+
description: "Names, schematics, use cases, and recommended briefs for the built-in Typora and VS Code figure list."
|
|
349
|
+
section: diagrams
|
|
350
|
+
order: 1
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
${MARKER}
|
|
354
|
+
|
|
355
|
+
The \`/garden\` list in Typora and in VS Code / Cursor comes from the same built-in list. There are currently ${count} items: ${named} common figures, and Other as the last item. Choosing one inserts a single \`garden compose\` brief, not a different author fence. That brief is for your material and goal, not a syntax lesson.
|
|
356
|
+
|
|
357
|
+
Use Other when no built-in figure fits, or when you need to combine text and graphics freely. Do not treat the list below as a closed allowlist.
|
|
358
|
+
|
|
359
|
+
For the entry and compile steps, see [Compose](/garden/en/components/compose) and [Checks & Shortcuts](/garden/en/workflow).
|
|
360
|
+
|
|
361
|
+
${cards}
|
|
362
|
+
`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function plannedFiles() {
|
|
366
|
+
const files = new Map();
|
|
367
|
+
files.set(path.join(PREVIEW_DIR, ".gitkeep"), "");
|
|
368
|
+
for (const item of DIAGRAM_RECIPES) {
|
|
369
|
+
files.set(path.join(PREVIEW_DIR, `${item.name}.svg`), `${previewSvgMarkup(item)}\n`);
|
|
370
|
+
}
|
|
371
|
+
files.set(ZH_INDEX, zhIndex());
|
|
372
|
+
files.set(EN_INDEX, enIndex());
|
|
373
|
+
DIAGRAM_RECIPES.forEach((item, index) => {
|
|
374
|
+
const order = (index + 1) * 10 + 1;
|
|
375
|
+
files.set(path.join(ZH_DIR, `${item.name}.md`), zhPage(item, order));
|
|
376
|
+
files.set(path.join(EN_DIR, `${item.name}.md`), enPage(item, order));
|
|
377
|
+
});
|
|
378
|
+
return files;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function checkGenerated(root = ROOT) {
|
|
382
|
+
const issues = [];
|
|
383
|
+
const files = plannedFiles();
|
|
384
|
+
for (const [file, body] of files) {
|
|
385
|
+
if (file.endsWith(".gitkeep") && !body) continue;
|
|
386
|
+
const rel = path.relative(ROOT, file).split(path.sep).join("/");
|
|
387
|
+
const actualPath = path.join(root, rel);
|
|
388
|
+
if (!fs.existsSync(actualPath)) {
|
|
389
|
+
issues.push({ file: rel, message: "missing generated handbook file; run node scripts/handbook-diagrams.mjs" });
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const actual = fs.readFileSync(actualPath, "utf8");
|
|
393
|
+
if (actual !== body) {
|
|
394
|
+
issues.push({ file: rel, message: "generated handbook file is stale; run node scripts/handbook-diagrams.mjs" });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
for (const file of STALE_FILES) {
|
|
398
|
+
const rel = path.relative(ROOT, file).split(path.sep).join("/");
|
|
399
|
+
const actualPath = path.join(root, rel);
|
|
400
|
+
if (fs.existsSync(actualPath)) {
|
|
401
|
+
issues.push({ file: rel, message: "stale generated handbook file should be removed; run node scripts/handbook-diagrams.mjs" });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return issues;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function writeGenerated() {
|
|
408
|
+
fs.mkdirSync(PREVIEW_DIR, { recursive: true });
|
|
409
|
+
fs.mkdirSync(ZH_DIR, { recursive: true });
|
|
410
|
+
fs.mkdirSync(EN_DIR, { recursive: true });
|
|
411
|
+
for (const [file, body] of plannedFiles()) {
|
|
412
|
+
if (file.endsWith(".gitkeep") && !body) continue;
|
|
413
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
414
|
+
fs.writeFileSync(file, body);
|
|
415
|
+
}
|
|
416
|
+
for (const file of STALE_FILES) {
|
|
417
|
+
if (fs.existsSync(file)) fs.rmSync(file);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const isCli = path.basename(process.argv[1] ?? "") === "handbook-diagrams.mjs";
|
|
422
|
+
if (isCli) {
|
|
423
|
+
const check = process.argv.includes("--check");
|
|
424
|
+
if (check) {
|
|
425
|
+
const issues = checkGenerated();
|
|
426
|
+
if (issues.length) {
|
|
427
|
+
for (const issue of issues) process.stderr.write(`${issue.file}: ${issue.message}\n`);
|
|
428
|
+
process.exitCode = 1;
|
|
429
|
+
}
|
|
430
|
+
} else {
|
|
431
|
+
writeGenerated();
|
|
432
|
+
process.stdout.write(`Wrote ${DIAGRAM_RECIPES.length} figure pages and previews from diagram-recipes.\n`);
|
|
433
|
+
}
|
|
434
|
+
}
|
package/scripts/link-assets.mjs
CHANGED
|
@@ -300,6 +300,12 @@ export async function refreshDevLinkAssets({ siteRoot, files, ...options }) {
|
|
|
300
300
|
} catch { selected = Object.keys(current); }
|
|
301
301
|
}
|
|
302
302
|
const changed = [...new Set(selected)].filter(file => Object.hasOwn(current, file)).map(file => path.join(siteRoot, file));
|
|
303
|
+
if (changed.length === 0 && previous) {
|
|
304
|
+
const cache = readLinkCache(siteRoot);
|
|
305
|
+
const cached = Object.keys(cache.favicons).length;
|
|
306
|
+
options.log?.(`链接图标:新取 0,沿用缓存 ${cached},没有图标 0`);
|
|
307
|
+
return { cache, stats: { fetched: 0, cached, missing: 0 } };
|
|
308
|
+
}
|
|
303
309
|
const result = await refreshLinkAssets({ siteRoot, ...options, files: changed });
|
|
304
310
|
fs.mkdirSync(path.dirname(stateFile), { recursive: true });
|
|
305
311
|
fs.writeFileSync(stateFile, JSON.stringify(current));
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function samePath(left, right) {
|
|
5
|
+
return path.resolve(left) === path.resolve(right);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function resolveRuntimeNodeModules(siteRoot, gardenPackageRoot) {
|
|
9
|
+
const packageModules = path.join(gardenPackageRoot, "node_modules");
|
|
10
|
+
const siteModules = path.join(siteRoot, "node_modules");
|
|
11
|
+
if (fs.existsSync(path.join(packageModules, "vinext"))) return packageModules;
|
|
12
|
+
if (fs.existsSync(path.join(siteModules, "vinext"))) return siteModules;
|
|
13
|
+
return packageModules;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ensureLink(from, to) {
|
|
17
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
18
|
+
try {
|
|
19
|
+
const stat = fs.lstatSync(to);
|
|
20
|
+
if (stat.isSymbolicLink() && fs.readlinkSync(to) === from) return;
|
|
21
|
+
if (stat.isSymbolicLink()) fs.unlinkSync(to);
|
|
22
|
+
else return;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error.code !== "ENOENT") throw error;
|
|
25
|
+
}
|
|
26
|
+
fs.symlinkSync(from, to);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Overlay Vinext's cwd so file: and npm installs share one layout. */
|
|
30
|
+
export function ensureVinextRuntime({ siteRoot, gardenPackageRoot, lang = "zh" }) {
|
|
31
|
+
if (samePath(siteRoot, gardenPackageRoot)) return gardenPackageRoot;
|
|
32
|
+
|
|
33
|
+
const runtime = path.join(siteRoot, ".garden", "runtime");
|
|
34
|
+
fs.mkdirSync(runtime, { recursive: true });
|
|
35
|
+
|
|
36
|
+
const links = [
|
|
37
|
+
["app", path.join(gardenPackageRoot, "app")],
|
|
38
|
+
["ui", path.join(gardenPackageRoot, "ui")],
|
|
39
|
+
["scripts", path.join(gardenPackageRoot, "scripts")],
|
|
40
|
+
["docs", path.join(gardenPackageRoot, "docs")],
|
|
41
|
+
["build", path.join(gardenPackageRoot, "build")],
|
|
42
|
+
["worker", path.join(gardenPackageRoot, "worker")],
|
|
43
|
+
[".openai", path.join(gardenPackageRoot, ".openai")],
|
|
44
|
+
["vite.config.ts", path.join(gardenPackageRoot, "vite.config.ts")],
|
|
45
|
+
["next.config.ts", path.join(gardenPackageRoot, "next.config.ts")],
|
|
46
|
+
["postcss.config.mjs", path.join(gardenPackageRoot, "postcss.config.mjs")],
|
|
47
|
+
["tsconfig.json", path.join(gardenPackageRoot, "tsconfig.json")],
|
|
48
|
+
["package.json", path.join(gardenPackageRoot, "package.json")],
|
|
49
|
+
["node_modules", resolveRuntimeNodeModules(siteRoot, gardenPackageRoot)],
|
|
50
|
+
["public", path.join(siteRoot, "public")],
|
|
51
|
+
["content", path.join(siteRoot, "content")],
|
|
52
|
+
["site.config.yaml", path.join(siteRoot, "site.config.yaml")],
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
for (const name of [".env", ".env.local"]) {
|
|
56
|
+
const from = path.join(siteRoot, name);
|
|
57
|
+
if (fs.existsSync(from)) links.push([name, from]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const [name, from] of links) {
|
|
61
|
+
if (fs.existsSync(from) || name === "public" || name === "content") {
|
|
62
|
+
if ((name === "public" || name === "content") && !fs.existsSync(from)) {
|
|
63
|
+
fs.mkdirSync(from, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
ensureLink(from, path.join(runtime, name));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// English sites: the runtime manifest is a copy with the English description, not a link.
|
|
70
|
+
// Remove the link first so the write never goes through it into the engine's own package.json.
|
|
71
|
+
if (lang === "en") {
|
|
72
|
+
const target = path.join(runtime, "package.json");
|
|
73
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(gardenPackageRoot, "package.json"), "utf8"));
|
|
74
|
+
manifest.description = "A personal blog engine: write in Markdown, publish with GitHub Pages.";
|
|
75
|
+
fs.rmSync(target, { force: true });
|
|
76
|
+
fs.writeFileSync(target, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return runtime;
|
|
80
|
+
}
|
package/templates/site/Makefile
CHANGED
|
@@ -12,7 +12,7 @@ export GARDEN_EMBED_URL := $(value URL)
|
|
|
12
12
|
export GARDEN_CHECK_FILE := $(value FILE)
|
|
13
13
|
|
|
14
14
|
help:
|
|
15
|
-
@printf "%s\n" "Garden 常用命令:" " make new 用引擎创建文章草稿" ' make new TITLE="文章标题" SLUG=my-post DIR=writing' ' make embed URL="https://
|
|
15
|
+
@printf "%s\n" "Garden 常用命令:" " make new 用引擎创建文章草稿" ' make new TITLE="文章标题" SLUG=my-post DIR=writing' ' make embed URL="https://www.youtube.com/watch?v=..." 打印可粘贴的规范化视频链接或网页地址' " make skill 安装可交互图表 skill" " make dev 安装依赖并本地预览" " make check 全量严格预检后 sync/build;FILE=路径 只限制预检" " make check diff 只检查 Git 改动的 Markdown,不构建/跑全套测试" " make build 构建公开站点" " make update 检查、提交并触发自动发布"
|
|
16
16
|
|
|
17
17
|
new: prepare
|
|
18
18
|
npx garden new
|
|
@@ -12,7 +12,7 @@ export GARDEN_EMBED_URL := $(value URL)
|
|
|
12
12
|
export GARDEN_CHECK_FILE := $(value FILE)
|
|
13
13
|
|
|
14
14
|
help:
|
|
15
|
-
@printf "%s\n" "Common Garden commands:" " make new Create a post draft with the engine" ' make new TITLE="Post title" SLUG=my-post DIR=writing' ' make embed URL="https://
|
|
15
|
+
@printf "%s\n" "Common Garden commands:" " make new Create a post draft with the engine" ' make new TITLE="Post title" SLUG=my-post DIR=writing' ' make embed URL="https://www.youtube.com/watch?v=..." Print a paste-ready video or page URL' " make skill Install interactive chart skills" " make dev Install dependencies and preview locally" " make check Strict all-content preflight, then sync/build (FILE=path for one file preflight)" " make check diff Check only changed Markdown; no build or tests" " make build Build the public site" " make update Check, commit, and push to publish"
|
|
16
16
|
|
|
17
17
|
new: prepare
|
|
18
18
|
npx garden new
|
package/ui/compose/compose.css
CHANGED
|
@@ -865,16 +865,51 @@
|
|
|
865
865
|
.g-compose-grid-item[data-span="4"] { grid-column: span 4; }
|
|
866
866
|
|
|
867
867
|
.g-compose-icon {
|
|
868
|
+
--gc-icon-size: 15px;
|
|
868
869
|
display: inline-block;
|
|
869
870
|
flex: 0 0 auto;
|
|
870
|
-
width:
|
|
871
|
-
height:
|
|
871
|
+
width: var(--gc-icon-size);
|
|
872
|
+
height: var(--gc-icon-size);
|
|
872
873
|
color: var(--ink);
|
|
873
874
|
vertical-align: -2px;
|
|
874
875
|
}
|
|
875
|
-
.g-compose-icon[data-size="sm"] {
|
|
876
|
-
.g-compose-icon[data-size="lg"] {
|
|
877
|
-
.g-compose-icon[data-size="xl"] {
|
|
876
|
+
.g-compose-icon[data-size="sm"] { --gc-icon-size: 13px; }
|
|
877
|
+
.g-compose-icon[data-size="lg"] { --gc-icon-size: 18px; }
|
|
878
|
+
.g-compose-icon[data-size="xl"] { --gc-icon-size: 22px; }
|
|
879
|
+
/* Align row-leading icons with the first line of adjacent text/headings rather than the top of half-leading. */
|
|
880
|
+
.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"])),
|
|
881
|
+
.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"])),
|
|
882
|
+
.g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:has(+ .g-compose-group > .g-compose-content:first-child) {
|
|
883
|
+
margin-top: max(0px, calc((1lh - var(--gc-icon-size)) / 2));
|
|
884
|
+
}
|
|
885
|
+
.g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:has(+ .g-compose-text[data-size="sm"]),
|
|
886
|
+
.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) {
|
|
887
|
+
margin-top: max(0px, calc((1.3125em - var(--gc-icon-size)) / 2));
|
|
888
|
+
}
|
|
889
|
+
.g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:has(+ .g-compose-text[data-size="lg"]),
|
|
890
|
+
.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) {
|
|
891
|
+
margin-top: max(0px, calc((1.125 * 1lh - var(--gc-icon-size)) / 2));
|
|
892
|
+
}
|
|
893
|
+
.g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:has(+ .g-compose-text[data-size="xl"]),
|
|
894
|
+
.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) {
|
|
895
|
+
margin-top: max(0px, calc((1.25 * 1lh - var(--gc-icon-size)) / 2));
|
|
896
|
+
}
|
|
897
|
+
.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"])),
|
|
898
|
+
.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"])) {
|
|
899
|
+
margin-top: max(0px, calc((1.4em - var(--gc-icon-size)) / 2));
|
|
900
|
+
}
|
|
901
|
+
.g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:has(+ .g-compose-h[data-size="sm"]),
|
|
902
|
+
.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) {
|
|
903
|
+
margin-top: max(0px, calc((1.1375em - var(--gc-icon-size)) / 2));
|
|
904
|
+
}
|
|
905
|
+
.g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:has(+ .g-compose-h[data-size="lg"]),
|
|
906
|
+
.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) {
|
|
907
|
+
margin-top: max(0px, calc((1.575em - var(--gc-icon-size)) / 2));
|
|
908
|
+
}
|
|
909
|
+
.g-compose-row:not([data-align="center"]):not([data-align="end"]) > .g-compose-icon:has(+ .g-compose-h[data-size="xl"]),
|
|
910
|
+
.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) {
|
|
911
|
+
margin-top: max(0px, calc((1.75em - var(--gc-icon-size)) / 2));
|
|
912
|
+
}
|
|
878
913
|
.g-compose-card[data-size="sm"] > .g-compose-icon {
|
|
879
914
|
width: 14px;
|
|
880
915
|
height: 14px;
|
|
@@ -1261,6 +1296,9 @@
|
|
|
1261
1296
|
width: 100%;
|
|
1262
1297
|
min-width: 0;
|
|
1263
1298
|
}
|
|
1299
|
+
.g-compose-row:not([data-wrap="nowrap"]) > .g-compose-icon {
|
|
1300
|
+
margin-top: 0;
|
|
1301
|
+
}
|
|
1264
1302
|
|
|
1265
1303
|
/* Columnized centered rows stretch labels to 100% width, so align-items
|
|
1266
1304
|
can center arrows but not text. Center direct labels only; explicit
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* and inline spans) is skipped, so a URL in a snippet is never a link.
|
|
4
4
|
*/
|
|
5
5
|
import { INLINE_LINK_SOURCE } from "./markdown-destination.mjs";
|
|
6
|
+
import { isPlayablePlayerUrl } from "./player.mjs";
|
|
6
7
|
|
|
7
8
|
const FENCE = /^ {0,3}(`{3,}|~{3,})/u;
|
|
8
9
|
|
|
@@ -65,8 +66,10 @@ export const SOURCE_MARKER = "{source}";
|
|
|
65
66
|
|
|
66
67
|
/**
|
|
67
68
|
* A single-line paragraph whose only content is a web URL: bare, <autolink>,
|
|
68
|
-
* or `[url](url)` (optional `{card}`).
|
|
69
|
-
* the OG fetch path with `{card}` markers.
|
|
69
|
+
* or `[url](url)` (optional `{card}`). Ordinary sites render as link cards and
|
|
70
|
+
* share the OG fetch path with `{card}` markers. A playable YouTube or
|
|
71
|
+
* Bilibili URL on its own line becomes a player instead, unless the author
|
|
72
|
+
* wrote `{card}`.
|
|
70
73
|
*/
|
|
71
74
|
function standaloneUrl(line) {
|
|
72
75
|
const trimmed = String(line ?? "").trim();
|
|
@@ -97,15 +100,15 @@ export function markdownLinks(markdown) {
|
|
|
97
100
|
for (const match of text.matchAll(LINK)) {
|
|
98
101
|
if (match[1] !== "!" && isWebUrl(linkUrl(match))) {
|
|
99
102
|
const url = linkUrl(match);
|
|
100
|
-
links.push({ url, card: match[6] === CARD_MARKER || standalone.has(url) });
|
|
103
|
+
links.push({ url, card: match[6] === CARD_MARKER || (standalone.has(url) && !isPlayablePlayerUrl(url)) });
|
|
101
104
|
}
|
|
102
105
|
}
|
|
103
106
|
const rest = text.replace(LINK, " ");
|
|
104
107
|
for (const match of rest.matchAll(AUTOLINK)) {
|
|
105
|
-
links.push({ url: match[1], card: standalone.has(match[1]) });
|
|
108
|
+
links.push({ url: match[1], card: standalone.has(match[1]) && !isPlayablePlayerUrl(match[1]) });
|
|
106
109
|
}
|
|
107
110
|
for (const match of rest.replace(AUTOLINK, " ").matchAll(BARE)) {
|
|
108
|
-
links.push({ url: match[2], card: standalone.has(match[2]) });
|
|
111
|
+
links.push({ url: match[2], card: standalone.has(match[2]) && !isPlayablePlayerUrl(match[2]) });
|
|
109
112
|
}
|
|
110
113
|
return text;
|
|
111
114
|
});
|