hd-garden 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/blog/[...path]/page.tsx +1 -1
- package/app/columns/[column]/[...path]/page.tsx +1 -1
- package/app/components/ArticleMeasure.tsx +15 -6
- package/app/components/MarkdownArticle.tsx +16 -7
- package/app/components/MarkdownImage.tsx +4 -3
- package/app/components/SiteHeader.tsx +7 -3
- package/app/garden-modules.d.ts +8 -0
- package/app/globals.css +21 -18
- package/app/layout.tsx +3 -1
- package/app/lib/remark-a2.mjs +19 -11
- package/app/lib/remark-link-card.mjs +11 -9
- package/app/styles/a2.css +27 -21
- package/app/styles/shell.css +13 -6
- package/app/styles/tokens.css +1 -1
- package/bin/garden.mjs +9 -1
- package/package.json +3 -1
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon.svg +1 -4
- package/scripts/brand.mjs +59 -0
- package/scripts/build-content.mjs +46 -1
- package/scripts/image-sizes.mjs +206 -0
- package/scripts/link-assets.mjs +17 -4
- package/scripts/redirects.mjs +128 -0
- package/templates/site/site.config.yaml +7 -0
- package/ui/garden/enhance.mjs +30 -0
- package/ui/garden/image-dims.mjs +43 -0
- package/ui/garden/same-site.mjs +63 -0
|
@@ -111,7 +111,7 @@ export default async function PostPage({
|
|
|
111
111
|
<ArticleTags tags={post.tags} />
|
|
112
112
|
{post.cover && (
|
|
113
113
|
<figure className="article-hero article-cover cover-card cover-img">
|
|
114
|
-
<CoverImage className="u-photo" src={post.cover} image={post.coverImage} alt={`${post.title} 封面`} sizes="(max-width:
|
|
114
|
+
<CoverImage className="u-photo" src={post.cover} image={post.coverImage} alt={`${post.title} 封面`} sizes="(max-width: 852px) calc(100vw - 40px), 812px" priority />
|
|
115
115
|
</figure>
|
|
116
116
|
)}
|
|
117
117
|
</header>
|
|
@@ -131,7 +131,7 @@ export default async function ColumnEntryPage({
|
|
|
131
131
|
<ArticleTags tags={entry.tags} />
|
|
132
132
|
{entry.cover && (
|
|
133
133
|
<figure className="article-hero article-cover cover-img">
|
|
134
|
-
<CoverImage className="u-photo" src={entry.cover} image={entry.coverImage} alt={`${entry.title} 封面`} sizes="(max-width:
|
|
134
|
+
<CoverImage className="u-photo" src={entry.cover} image={entry.coverImage} alt={`${entry.title} 封面`} sizes="(max-width: 852px) calc(100vw - 40px), 812px" priority />
|
|
135
135
|
</figure>
|
|
136
136
|
)}
|
|
137
137
|
</header>
|
|
@@ -101,23 +101,32 @@ export function ArticleMeasure() {
|
|
|
101
101
|
article.querySelectorAll<HTMLElement>(".fig").forEach((figure) => {
|
|
102
102
|
const image = figure.querySelector("img");
|
|
103
103
|
resetWide(figure);
|
|
104
|
+
// From here on the browser decides; the build-time breakout CSS steps aside.
|
|
105
|
+
figure.classList.remove("fig-wide");
|
|
104
106
|
if (!image) return;
|
|
105
|
-
// Back to
|
|
106
|
-
image.
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
// Back to the unwidened size: the build-time width when the size is known
|
|
108
|
+
// (so an image that has not loaded keeps its box), else CSS sizing.
|
|
109
|
+
const baseWidth = Number(image.dataset.bw) || 0;
|
|
110
|
+
if (baseWidth) image.style.width = `${baseWidth}px`;
|
|
111
|
+
else image.style.removeProperty("width");
|
|
112
|
+
// Sizes read at build time let this run before the file arrives; otherwise wait for load.
|
|
113
|
+
const known = Number(image.dataset.nw) || 0;
|
|
114
|
+
const pixels = image.naturalWidth || known;
|
|
115
|
+
if (!pixels) return;
|
|
116
|
+
// width="" written by the build is the pixel size, not an author's choice.
|
|
117
|
+
const attrWidth = known ? Number(image.dataset.aw) || 0 : Number(image.getAttribute("width")) || 0;
|
|
109
118
|
const breakout = figureBreakout({
|
|
110
119
|
viewport,
|
|
111
120
|
articleLeft: articleBox.left,
|
|
112
121
|
articleRight: articleBox.right,
|
|
113
122
|
figureClient: figure.clientWidth,
|
|
114
123
|
imageClient: image.clientWidth,
|
|
115
|
-
naturalWidth:
|
|
124
|
+
naturalWidth: pixels,
|
|
116
125
|
attrWidth,
|
|
117
126
|
});
|
|
118
127
|
widen(figure, breakout);
|
|
119
128
|
// Only a widened image, or one with a width attribute, gets an inline width.
|
|
120
|
-
const width = figureImageWidth({ naturalWidth:
|
|
129
|
+
const width = figureImageWidth({ naturalWidth: pixels, attrWidth, widened: breakout.left > 0 });
|
|
121
130
|
if (width) image.style.width = `${width}px`;
|
|
122
131
|
});
|
|
123
132
|
placeToc();
|
|
@@ -20,12 +20,17 @@ import { MarkdownImage } from "./MarkdownImage";
|
|
|
20
20
|
import { CodeCopy } from "./garden/chrome";
|
|
21
21
|
import { CodeDiff } from "./garden/chrome";
|
|
22
22
|
import { GardenBlock } from "./garden/GardenBlock";
|
|
23
|
-
import { linkAssets } from "@garden/generated-content";
|
|
24
|
-
import {
|
|
23
|
+
import { imageSizes, linkAssets } from "@garden/generated-content";
|
|
24
|
+
import { imageDimensions, lookupImageSize } from "../../ui/garden/image-dims.mjs";
|
|
25
|
+
import { siteConfig, siteLinks } from "@garden/site-config";
|
|
26
|
+
import { articleLink, sameSiteMatcher } from "../../ui/garden/same-site.mjs";
|
|
25
27
|
import { LinkCardRefresh } from "./LinkCardRefresh";
|
|
26
28
|
import { linkHost } from "../../scripts/link-meta-core.mjs";
|
|
27
29
|
import { cardDate } from "../../ui/garden/card-refresh.mjs";
|
|
28
30
|
|
|
31
|
+
// Full URLs back to this site are internal links, written as root-relative paths.
|
|
32
|
+
const sameSite = sameSiteMatcher(siteLinks?.address ?? "", siteLinks?.basePath ?? "");
|
|
33
|
+
|
|
29
34
|
/** The site's favicon, downloaded at build time; nothing when there is none. */
|
|
30
35
|
function Favicon({ href }: { href: string }) {
|
|
31
36
|
const src = linkAssets?.favicons?.[linkHost(href)];
|
|
@@ -221,7 +226,7 @@ export function MarkdownArticle({ content }: { content: string }) {
|
|
|
221
226
|
return (
|
|
222
227
|
<div className="markdown-body prose">
|
|
223
228
|
<ReactMarkdown
|
|
224
|
-
remarkPlugins={[...markdownPlugins, remarkLinkCard, remarkA2]}
|
|
229
|
+
remarkPlugins={[...markdownPlugins, [remarkLinkCard, { sameSite }], [remarkA2, { imageSize: (src: string) => lookupImageSize(imageSizes, src) }]]}
|
|
225
230
|
rehypePlugins={[
|
|
226
231
|
rehypeRaw,
|
|
227
232
|
rehypeKatex,
|
|
@@ -237,11 +242,11 @@ export function MarkdownArticle({ content }: { content: string }) {
|
|
|
237
242
|
const only = nodes.length === 1 && isValidElement<{ href?: string; children?: ReactNode }>(nodes[0]) ? nodes[0] : null;
|
|
238
243
|
const href = only?.props.href;
|
|
239
244
|
const text = only ? headingText(only.props.children).trim() : "";
|
|
240
|
-
if (href && text === href && /^https?:\/\//iu.test(href)) return <LinkCard href={href} />;
|
|
245
|
+
if (href && text === href && /^https?:\/\//iu.test(href) && !sameSite(href)) return <LinkCard href={href} />;
|
|
241
246
|
return <p {...domProps}>{children}</p>;
|
|
242
247
|
},
|
|
243
|
-
a: ({ href, children, ...props }) => {
|
|
244
|
-
const external =
|
|
248
|
+
a: ({ href: written, children, ...props }) => {
|
|
249
|
+
const { href, external } = articleLink(written, sameSite);
|
|
245
250
|
const domProps = { ...props };
|
|
246
251
|
Reflect.deleteProperty(domProps, "node");
|
|
247
252
|
return (
|
|
@@ -256,7 +261,11 @@ export function MarkdownArticle({ content }: { content: string }) {
|
|
|
256
261
|
</a>
|
|
257
262
|
);
|
|
258
263
|
},
|
|
259
|
-
img:
|
|
264
|
+
img: (props) => {
|
|
265
|
+
const size = lookupImageSize(imageSizes, typeof props.src === "string" ? props.src : undefined);
|
|
266
|
+
const dims = imageDimensions(props, size);
|
|
267
|
+
return <MarkdownImage {...props} {...dims} style={{ ...props.style, ...dims.style }} />;
|
|
268
|
+
},
|
|
260
269
|
table: ({ children, ...props }) => (
|
|
261
270
|
<div className="table-wrap" role="region" aria-label="表格,可横向滚动" tabIndex={0}>
|
|
262
271
|
<table {...props}>{children}</table>
|
|
@@ -6,7 +6,7 @@ import { useEffect, useState, type ComponentPropsWithoutRef } from "react";
|
|
|
6
6
|
type MarkdownImageProps = ComponentPropsWithoutRef<"img"> & { node?: unknown };
|
|
7
7
|
|
|
8
8
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
9
|
-
export function MarkdownImage({ alt = "", src, node, ...props }: MarkdownImageProps) {
|
|
9
|
+
export function MarkdownImage({ alt = "", src, node, style, ...props }: MarkdownImageProps) {
|
|
10
10
|
const [open, setOpen] = useState(false);
|
|
11
11
|
const description = alt || "查看原图";
|
|
12
12
|
|
|
@@ -37,7 +37,7 @@ export function MarkdownImage({ alt = "", src, node, ...props }: MarkdownImagePr
|
|
|
37
37
|
>
|
|
38
38
|
{/* Native images preserve Typora-authored relative paths. */}
|
|
39
39
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
40
|
-
<img loading="lazy" alt={alt} src={src} {...props} />
|
|
40
|
+
<img loading="lazy" alt={alt} src={src} {...props} style={style} />
|
|
41
41
|
</button>
|
|
42
42
|
|
|
43
43
|
{open ? (
|
|
@@ -49,8 +49,9 @@ export function MarkdownImage({ alt = "", src, node, ...props }: MarkdownImagePr
|
|
|
49
49
|
onClick={() => setOpen(false)}
|
|
50
50
|
>
|
|
51
51
|
<div className="markdown-image-lightbox-content" onClick={(event) => event.stopPropagation()}>
|
|
52
|
+
{/* The preview sizes itself; the article's inline width stays on the article image. */}
|
|
52
53
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
53
|
-
<img src={src} alt={alt} {
|
|
54
|
+
<img src={src} alt={alt} width={props.width} height={props.height} />
|
|
54
55
|
<button
|
|
55
56
|
type="button"
|
|
56
57
|
className="markdown-image-lightbox-close"
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "react";
|
|
13
13
|
import type Fuse from "fuse.js";
|
|
14
14
|
import { MagnifyingGlass, X } from "@phosphor-icons/react";
|
|
15
|
-
import { searchFull, searchIndexUrl } from "@garden/site-config";
|
|
15
|
+
import { brand, searchFull, searchIndexUrl } from "@garden/site-config";
|
|
16
16
|
import { createSearch } from "../../ui/garden/search-match.mjs";
|
|
17
17
|
import { createIdbStore, loadFullIndex, saveDataOn, whenIdleAfterLoad } from "../../ui/garden/search-full.mjs";
|
|
18
18
|
import { siteConfig } from "../site.config";
|
|
@@ -358,7 +358,11 @@ export function SiteHeader() {
|
|
|
358
358
|
<>
|
|
359
359
|
<header className="sh-header">
|
|
360
360
|
<div className="sh-bar">
|
|
361
|
-
<Link href="/" className="sh-word">
|
|
361
|
+
<Link href="/" className="sh-word">
|
|
362
|
+
{/* Build-time SVG from brand.logo, cleaned of scripts and handlers. */}
|
|
363
|
+
{brand.logo ? <span className="sh-mark" aria-hidden="true" dangerouslySetInnerHTML={{ __html: brand.logo }} /> : null}
|
|
364
|
+
{siteConfig.name}
|
|
365
|
+
</Link>
|
|
362
366
|
<nav className="sh-nav" aria-label="站点导航">
|
|
363
367
|
{nav.map((item) => (
|
|
364
368
|
<Link key={item.href} href={item.href} aria-current={current(item.href) ? "page" : undefined}>
|
|
@@ -372,7 +376,7 @@ export function SiteHeader() {
|
|
|
372
376
|
<circle cx="9" cy="9" r="5.8" />
|
|
373
377
|
<path d="M13.3 13.3 17 17" />
|
|
374
378
|
</svg>
|
|
375
|
-
<span className="lbl"
|
|
379
|
+
<span className="lbl">搜索</span>
|
|
376
380
|
<kbd><span className="kbd-mod kbd-mac">⌘</span><span className="kbd-mod kbd-other">Ctrl</span>{" "}K</kbd>
|
|
377
381
|
</button>
|
|
378
382
|
<button className="sh-icon sh-search-m" type="button" onClick={openSearch} onPointerDown={prefetchSearch} aria-label="搜索文章">
|
package/app/garden-modules.d.ts
CHANGED
|
@@ -59,12 +59,18 @@ declare module "@garden/generated-content" {
|
|
|
59
59
|
};
|
|
60
60
|
export type LinkAssets = { favicons: Record<string, string>; cards: Record<string, LinkCardMeta> };
|
|
61
61
|
export const linkAssets: LinkAssets;
|
|
62
|
+
/** Intrinsic sizes of body images by src, read at build time. */
|
|
63
|
+
export const imageSizes: Record<string, { width: number; height: number }>;
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
declare module "@garden/site-config" {
|
|
65
67
|
export const searchIndexUrl: string;
|
|
66
68
|
/** The full-text index file and the hash its contents are cached under. */
|
|
67
69
|
export const searchFull: { url: string; hash: string };
|
|
70
|
+
/** The site address (site.config.yaml `url`, else SITE_URL) and base path, for spotting links back to the site. */
|
|
71
|
+
export const siteLinks: { address: string; basePath: string };
|
|
72
|
+
/** Header logo as inline SVG ("" when hidden) and icon URLs, from brand in site.config.yaml. */
|
|
73
|
+
export const brand: { logo: string; favicon: string; appleTouchIcon: string };
|
|
68
74
|
export const siteConfig: {
|
|
69
75
|
name: string;
|
|
70
76
|
tagline: string;
|
|
@@ -74,6 +80,8 @@ declare module "@garden/site-config" {
|
|
|
74
80
|
pages: Record<string, Record<string, unknown>>;
|
|
75
81
|
footer: string;
|
|
76
82
|
linkPreview?: { worker?: string };
|
|
83
|
+
url?: string;
|
|
84
|
+
brand?: { logo?: string | false; favicon?: string; appleTouchIcon?: string | false };
|
|
77
85
|
};
|
|
78
86
|
export const githubUrl: string;
|
|
79
87
|
}
|
package/app/globals.css
CHANGED
|
@@ -126,15 +126,6 @@ button {
|
|
|
126
126
|
width: max-content;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
.brand-mark {
|
|
130
|
-
width: 43px;
|
|
131
|
-
height: 50px;
|
|
132
|
-
display: grid;
|
|
133
|
-
place-items: center;
|
|
134
|
-
color: var(--faint);
|
|
135
|
-
border-right: 1px solid var(--line-strong);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
129
|
.brand strong {
|
|
139
130
|
display: block;
|
|
140
131
|
font-size: 20px;
|
|
@@ -2061,15 +2052,32 @@ button {
|
|
|
2061
2052
|
text-decoration-thickness: 2px;
|
|
2062
2053
|
}
|
|
2063
2054
|
|
|
2064
|
-
/*
|
|
2065
|
-
.markdown-body u
|
|
2055
|
+
/* Highlighter for key claims, as in the A2 demo: a soft stroke under the lower part of the line. */
|
|
2056
|
+
.markdown-body u,
|
|
2057
|
+
.markdown-body mark,
|
|
2058
|
+
.markdown-body .hl {
|
|
2059
|
+
color: var(--ink);
|
|
2060
|
+
font-weight: 500;
|
|
2066
2061
|
text-decoration: none;
|
|
2067
|
-
border-bottom:
|
|
2068
|
-
|
|
2062
|
+
border-bottom: 0;
|
|
2063
|
+
background: linear-gradient(to bottom, transparent 58%, var(--hl) 58%, var(--hl) 92%, transparent 92%);
|
|
2064
|
+
padding: 0 0.08em;
|
|
2069
2065
|
-webkit-box-decoration-break: clone;
|
|
2070
2066
|
box-decoration-break: clone;
|
|
2071
2067
|
}
|
|
2072
2068
|
|
|
2069
|
+
.markdown-body u strong,
|
|
2070
|
+
.markdown-body u u,
|
|
2071
|
+
.markdown-body mark strong,
|
|
2072
|
+
.markdown-body mark u,
|
|
2073
|
+
.markdown-body .hl strong,
|
|
2074
|
+
.markdown-body .hl u {
|
|
2075
|
+
font-weight: 600;
|
|
2076
|
+
text-decoration: none;
|
|
2077
|
+
background: none;
|
|
2078
|
+
padding: 0;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2073
2081
|
.markdown-body strong {
|
|
2074
2082
|
color: var(--ink);
|
|
2075
2083
|
}
|
|
@@ -4098,11 +4106,6 @@ html[data-theme="dark"] .article-references {
|
|
|
4098
4106
|
display: none;
|
|
4099
4107
|
}
|
|
4100
4108
|
|
|
4101
|
-
.brand-mark {
|
|
4102
|
-
width: 36px;
|
|
4103
|
-
height: 44px;
|
|
4104
|
-
}
|
|
4105
|
-
|
|
4106
4109
|
.brand strong {
|
|
4107
4110
|
font-size: 17px;
|
|
4108
4111
|
}
|
package/app/layout.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
webmentionEndpoint,
|
|
12
12
|
} from "./lib/site";
|
|
13
13
|
import { githubUrl, siteConfig } from "./site.config";
|
|
14
|
+
import { brand } from "@garden/site-config";
|
|
14
15
|
|
|
15
16
|
export async function generateMetadata(): Promise<Metadata> {
|
|
16
17
|
const siteUrl = await getSiteUrl();
|
|
@@ -29,7 +30,8 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
29
30
|
},
|
|
30
31
|
},
|
|
31
32
|
icons: {
|
|
32
|
-
icon:
|
|
33
|
+
icon: [{ url: brand.favicon, type: brand.favicon.endsWith(".svg") ? "image/svg+xml" : undefined }],
|
|
34
|
+
...(brand.appleTouchIcon ? { apple: [{ url: brand.appleTouchIcon, sizes: "180x180" }] } : {}),
|
|
33
35
|
},
|
|
34
36
|
robots: {
|
|
35
37
|
index: true,
|
package/app/lib/remark-a2.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { figureLayout } from "../../ui/garden/enhance.mjs";
|
|
1
2
|
import { toString } from "mdast-util-to-string";
|
|
2
3
|
|
|
3
4
|
const ALERT_TITLE = {
|
|
@@ -18,10 +19,10 @@ const LEGACY_KIND = {
|
|
|
18
19
|
|
|
19
20
|
const ALERT_ICON = {
|
|
20
21
|
note: '<svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="6.25"/><path d="M8 7.2v4"/><path d="M8 4.8v.1" stroke-width="2"/></svg>',
|
|
21
|
-
tip: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="
|
|
22
|
-
important: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2.25 3.25a1 1 0 0 1 1-
|
|
23
|
-
warning: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="
|
|
24
|
-
caution: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5.3 1.75h5.4l3.55 3.55v5.4l-3.55 3.55H5.
|
|
22
|
+
tip: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 1.75a4.25 4.25 0 0 0-2.5 7.7c.4.3.6.7.6 1.2v.6h3.8v-.6c0-.5.2-.9.6-1.2A4.25 4.25 0 0 0 8 1.75z"/><path d="M6.2 13.6h3.6"/></svg>',
|
|
23
|
+
important: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2.25 3.25a1.5 1.5 0 0 1 1.5-1.5h8.5a1.5 1.5 0 0 1 1.5 1.5v6.5a1.5 1.5 0 0 1-1.5 1.5H7.5l-3 2.75v-2.75h-.75a1.5 1.5 0 0 1-1.5-1.5z"/><path d="M8 4.5v3"/><path d="M8 9.4v.1" stroke-width="2"/></svg>',
|
|
24
|
+
warning: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M7.13 2.4a1 1 0 0 1 1.74 0l5.6 9.9a1 1 0 0 1-.87 1.5H2.4a1 1 0 0 1-.87-1.5z"/><path d="M8 6.3v3.1"/><path d="M8 11.4v.1" stroke-width="2"/></svg>',
|
|
25
|
+
caution: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5.3 1.75h5.4l3.55 3.55v5.4l-3.55 3.55H5.3L1.75 10.7V5.3z"/><path d="M8 4.8v3.6"/><path d="M8 10.6v.1" stroke-width="2"/></svg>',
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
function meaningfulChildren(node) {
|
|
@@ -168,7 +169,7 @@ function figureCaption(image) {
|
|
|
168
169
|
return "";
|
|
169
170
|
}
|
|
170
171
|
|
|
171
|
-
function asFigure(node) {
|
|
172
|
+
function asFigure(node, imageSize) {
|
|
172
173
|
if (node?.type !== "paragraph" || node.data?.hName) return null;
|
|
173
174
|
const children = meaningfulChildren(node);
|
|
174
175
|
if (children.length !== 1 || children[0].type !== "image") return null;
|
|
@@ -186,23 +187,30 @@ function asFigure(node) {
|
|
|
186
187
|
children: [{ type: "text", value: caption }],
|
|
187
188
|
}]
|
|
188
189
|
: [card];
|
|
190
|
+
// A known size decides the breakout now, so the figure does not jump when the image loads.
|
|
191
|
+
const size = imageSize(image.url);
|
|
192
|
+
const side = size ? figureLayout({ naturalWidth: size.width }).side : 0;
|
|
193
|
+
const hProperties = side > 0
|
|
194
|
+
? { className: ["fig", "fig-wide"], style: `--wl:${side}px;--wr:${side}px` }
|
|
195
|
+
: { className: ["fig"] };
|
|
189
196
|
return {
|
|
190
197
|
type: "paragraph",
|
|
191
|
-
data: { hName: "figure", hProperties
|
|
198
|
+
data: { hName: "figure", hProperties },
|
|
192
199
|
children: figureChildren,
|
|
193
200
|
};
|
|
194
201
|
}
|
|
195
202
|
|
|
196
|
-
function walk(node) {
|
|
203
|
+
function walk(node, imageSize) {
|
|
197
204
|
if (!node || typeof node !== "object" || !Array.isArray(node.children)) return;
|
|
198
205
|
if (node.type === "blockquote") rewriteAlert(node);
|
|
199
|
-
node.children = node.children.map((child) => asFigure(child) ?? child);
|
|
200
|
-
for (const child of node.children) walk(child);
|
|
206
|
+
node.children = node.children.map((child) => asFigure(child, imageSize) ?? child);
|
|
207
|
+
for (const child of node.children) walk(child, imageSize);
|
|
201
208
|
}
|
|
202
209
|
|
|
203
210
|
/** Demo alert cards and captioned figures. Article rendering only. */
|
|
204
|
-
export function remarkA2() {
|
|
211
|
+
export function remarkA2(options = {}) {
|
|
212
|
+
const imageSize = typeof options?.imageSize === "function" ? options.imageSize : () => null;
|
|
205
213
|
return (tree) => {
|
|
206
|
-
walk(tree);
|
|
214
|
+
walk(tree, imageSize);
|
|
207
215
|
};
|
|
208
216
|
}
|
|
@@ -32,7 +32,7 @@ const meaningful = (children) =>
|
|
|
32
32
|
children.filter((child) => !(child.type === "text" && !child.value.trim()) && child.type !== "break");
|
|
33
33
|
|
|
34
34
|
/** Take the markers out of one parent's inline children; return the marked links. */
|
|
35
|
-
function takeMarkers(parent, source) {
|
|
35
|
+
function takeMarkers(parent, source, sameSite) {
|
|
36
36
|
const marked = [];
|
|
37
37
|
const children = parent.children ?? [];
|
|
38
38
|
for (let index = 0; index < children.length - 1; index += 1) {
|
|
@@ -41,13 +41,14 @@ function takeMarkers(parent, source) {
|
|
|
41
41
|
if (link.type !== "link" || next.type !== "text" || !next.value.startsWith(MARKER)) continue;
|
|
42
42
|
if (!isHttp(link.url) || !writtenAsLink(link, source)) continue;
|
|
43
43
|
next.value = next.value.slice(MARKER.length);
|
|
44
|
-
|
|
44
|
+
// A link back to this site is an ordinary internal link: the marker goes, no card.
|
|
45
|
+
if (!sameSite(link.url)) marked.push(link);
|
|
45
46
|
}
|
|
46
47
|
parent.children = children.filter((child) => !(child.type === "text" && child.value === ""));
|
|
47
48
|
return marked;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
function walk(parent, source) {
|
|
51
|
+
function walk(parent, source, sameSite) {
|
|
51
52
|
const children = parent.children ?? [];
|
|
52
53
|
for (let index = 0; index < children.length; index += 1) {
|
|
53
54
|
const node = children[index];
|
|
@@ -55,14 +56,14 @@ function walk(parent, source) {
|
|
|
55
56
|
if (node.type !== "paragraph") {
|
|
56
57
|
if (node.children) {
|
|
57
58
|
// Headings, table cells and the like: drop the marker, no card.
|
|
58
|
-
if (node.type === "heading" || node.type === "tableCell") takeMarkers(node, source);
|
|
59
|
-
walk(node, source);
|
|
59
|
+
if (node.type === "heading" || node.type === "tableCell") takeMarkers(node, source, sameSite);
|
|
60
|
+
walk(node, source, sameSite);
|
|
60
61
|
}
|
|
61
62
|
continue;
|
|
62
63
|
}
|
|
63
64
|
// Links nested in emphasis count too.
|
|
64
|
-
const marked = takeMarkers(node, source);
|
|
65
|
-
for (const child of node.children) if (child.children && child.type !== "link") marked.push(...takeMarkers(child, source));
|
|
65
|
+
const marked = takeMarkers(node, source, sameSite);
|
|
66
|
+
for (const child of node.children) if (child.children && child.type !== "link") marked.push(...takeMarkers(child, source, sameSite));
|
|
66
67
|
if (!marked.length) continue;
|
|
67
68
|
const rest = meaningful(node.children);
|
|
68
69
|
if (marked.length === 1 && rest.length === 1 && rest[0] === marked[0]) {
|
|
@@ -75,9 +76,10 @@ function walk(parent, source) {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
export function remarkLinkCard() {
|
|
79
|
+
export function remarkLinkCard(options = {}) {
|
|
80
|
+
const sameSite = typeof options?.sameSite === "function" ? options.sameSite : () => null;
|
|
79
81
|
return (tree, file) => {
|
|
80
82
|
const source = typeof file?.value === "string" ? file.value : String(file?.value ?? "");
|
|
81
|
-
walk(tree, source);
|
|
83
|
+
walk(tree, source, sameSite);
|
|
82
84
|
};
|
|
83
85
|
}
|
package/app/styles/a2.css
CHANGED
|
@@ -27,7 +27,6 @@ body {
|
|
|
27
27
|
color: var(--ink);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
.brand-mark,
|
|
31
30
|
.brand small {
|
|
32
31
|
display: none;
|
|
33
32
|
}
|
|
@@ -244,7 +243,8 @@ body {
|
|
|
244
243
|
.cover-card.cover-img,
|
|
245
244
|
.article-hero.article-cover {
|
|
246
245
|
width: 100%;
|
|
247
|
-
|
|
246
|
+
/* Column plus half a gutter each side: centred, and clear of the TOC rail. */
|
|
247
|
+
max-width: calc(var(--col) + 72px);
|
|
248
248
|
margin: 48px auto 0;
|
|
249
249
|
aspect-ratio: auto;
|
|
250
250
|
background: none;
|
|
@@ -466,15 +466,15 @@ html[data-theme="dark"] .markdown-body img.fav {
|
|
|
466
466
|
}
|
|
467
467
|
|
|
468
468
|
.markdown-body.prose {
|
|
469
|
-
--al-note: #
|
|
470
|
-
--al-note-bg: #
|
|
471
|
-
--al-note-line: rgba(
|
|
469
|
+
--al-note: #2c55b4;
|
|
470
|
+
--al-note-bg: #eef3fc;
|
|
471
|
+
--al-note-line: rgba(44, 85, 180, 0.22);
|
|
472
472
|
--al-tip: #2d6a3e;
|
|
473
473
|
--al-tip-bg: #edf5ef;
|
|
474
474
|
--al-tip-line: rgba(45, 106, 62, 0.22);
|
|
475
|
-
--al-important: #
|
|
476
|
-
--al-important-bg: #
|
|
477
|
-
--al-important-line: rgba(
|
|
475
|
+
--al-important: #6a3fb5;
|
|
476
|
+
--al-important-bg: #f3effa;
|
|
477
|
+
--al-important-line: rgba(106, 63, 181, 0.22);
|
|
478
478
|
--al-warning: #8a5300;
|
|
479
479
|
--al-warning-bg: #fbf3e5;
|
|
480
480
|
--al-warning-line: rgba(176, 108, 0, 0.26);
|
|
@@ -487,15 +487,15 @@ html[data-theme="dark"] .markdown-body img.fav {
|
|
|
487
487
|
:root[data-theme="dark"] .prose,
|
|
488
488
|
html[data-theme="dark"] .markdown-body.prose,
|
|
489
489
|
html[data-theme="dark"] .prose {
|
|
490
|
-
--al-note: #
|
|
491
|
-
--al-note-bg: rgba(
|
|
492
|
-
--al-note-line: rgba(
|
|
490
|
+
--al-note: #93b2f4;
|
|
491
|
+
--al-note-bg: rgba(147, 178, 244, 0.09);
|
|
492
|
+
--al-note-line: rgba(147, 178, 244, 0.28);
|
|
493
493
|
--al-tip: #8fcb9f;
|
|
494
494
|
--al-tip-bg: rgba(143, 203, 159, 0.08);
|
|
495
495
|
--al-tip-line: rgba(143, 203, 159, 0.26);
|
|
496
|
-
--al-important: #
|
|
497
|
-
--al-important-bg:
|
|
498
|
-
--al-important-line: rgba(
|
|
496
|
+
--al-important: #bfa5f0;
|
|
497
|
+
--al-important-bg: rgba(191, 165, 240, 0.09);
|
|
498
|
+
--al-important-line: rgba(191, 165, 240, 0.28);
|
|
499
499
|
--al-warning: #e6b566;
|
|
500
500
|
--al-warning-bg: rgba(230, 181, 102, 0.08);
|
|
501
501
|
--al-warning-line: rgba(230, 181, 102, 0.28);
|
|
@@ -535,17 +535,10 @@ html[data-theme="dark"] .prose {
|
|
|
535
535
|
--al-line: var(--al-tip-line);
|
|
536
536
|
}
|
|
537
537
|
|
|
538
|
-
/* IMPORTANT reads in ink: a dark bar on the left and a bold title. */
|
|
539
538
|
.markdown-body .markdown-alert-important {
|
|
540
539
|
--al: var(--al-important);
|
|
541
540
|
--al-bg: var(--al-important-bg);
|
|
542
541
|
--al-line: var(--al-important-line);
|
|
543
|
-
border-left: 3px solid var(--al-important);
|
|
544
|
-
padding-left: 18px;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
.markdown-body .markdown-alert-important .markdown-alert-title {
|
|
548
|
-
font-weight: 700;
|
|
549
542
|
}
|
|
550
543
|
|
|
551
544
|
.markdown-body .markdown-alert-warning {
|
|
@@ -737,6 +730,19 @@ html[data-theme="dark"] .markdown-body .markdown-alert code {
|
|
|
737
730
|
}
|
|
738
731
|
}
|
|
739
732
|
|
|
733
|
+
/* A breakout decided at build time from the image's known size. It holds the
|
|
734
|
+
figure at its final width before the image loads and before the script
|
|
735
|
+
measures; the script then takes over (and removes .fig-wide). Only where the
|
|
736
|
+
article column is a full 740px with room on both sides. */
|
|
737
|
+
@media (min-width: 1348px) {
|
|
738
|
+
.post-reading-page .markdown-body .fig-wide {
|
|
739
|
+
margin-left: calc(-1 * var(--wl, 0px));
|
|
740
|
+
margin-right: calc(-1 * var(--wr, 0px));
|
|
741
|
+
max-width: none;
|
|
742
|
+
width: auto;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
740
746
|
.toc {
|
|
741
747
|
font-size: var(--fs-small);
|
|
742
748
|
line-height: 1.5;
|
package/app/styles/shell.css
CHANGED
|
@@ -37,21 +37,28 @@ img,svg{max-width:100%}
|
|
|
37
37
|
.sh-header{position:sticky;top:0;z-index:40;background:var(--paper);border-bottom:1px solid transparent;transition:border-color .2s ease}
|
|
38
38
|
.sh-header.is-scrolled{border-bottom-color:var(--rule)}
|
|
39
39
|
.sh-bar{max-width:var(--shell);margin:0 auto;height:var(--hdr);padding:0 32px;display:flex;align-items:center;gap:28px}
|
|
40
|
-
.sh-word{font:600 var(--fs-lead)/1 var(--serif);color:var(--ink);text-decoration:none;white-space:nowrap;padding:8px 0}
|
|
40
|
+
.sh-word{display:inline-flex;align-items:center;gap:10px;font:600 var(--fs-lead)/1 var(--serif);color:var(--ink);text-decoration:none;white-space:nowrap;padding:8px 0}
|
|
41
|
+
.sh-mark{display:inline-flex;flex:none}
|
|
42
|
+
.sh-mark svg{display:block;height:26px;width:auto}
|
|
41
43
|
.sh-nav{display:flex;gap:4px;margin-left:auto}
|
|
42
44
|
.sh-nav a{font-size:15px;color:var(--soft);text-decoration:none;padding:8px 12px;border-radius:var(--r);transition:color .15s ease,background-color .15s ease}
|
|
43
45
|
.sh-nav a:hover{color:var(--ink);background:var(--tint)}
|
|
44
46
|
.sh-nav a[aria-current="page"],.sh-nav a[aria-current="true"]{color:var(--blue);font-weight:600}
|
|
45
47
|
.sh-tools{display:flex;align-items:center;gap:8px}
|
|
46
|
-
.sh-search{display:inline-flex;align-items:center;gap:
|
|
47
|
-
.sh-search:hover{
|
|
48
|
+
.sh-search{display:inline-flex;align-items:center;gap:6px;height:38px;padding:8px 12px;border:0;background:transparent;color:var(--soft);border-radius:var(--r);font:400 15px/1 var(--sans);cursor:pointer;appearance:none;transition:color .15s ease,background-color .15s ease}
|
|
49
|
+
.sh-search:hover{color:var(--ink);background:var(--tint)}
|
|
50
|
+
.sh-search:focus-visible{outline:2px solid var(--blue);outline-offset:2px}
|
|
48
51
|
.sh-search svg,.sh-icon svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;flex:none}
|
|
49
|
-
.sh-search
|
|
52
|
+
.sh-search svg{width:16px;height:16px}
|
|
53
|
+
.sh-search .lbl{text-align:left}
|
|
54
|
+
/* The shortcut hint only where there is a keyboard and a precise pointer. */
|
|
55
|
+
.sh-search kbd{display:none;margin-left:4px;border:0;padding:0;background:none;font:400 12px/1 var(--sans);color:var(--muted);opacity:.7}
|
|
56
|
+
@media (hover:hover) and (pointer:fine){.sh-search kbd{display:inline}}
|
|
50
57
|
kbd{font:500 12px/1 var(--sans);color:var(--muted);border:1px solid var(--rule);border-bottom-width:2px;border-radius:6px;padding:3px 6px;background:var(--paper)}
|
|
51
|
-
.sh-search .kbd-mod{display:inline
|
|
58
|
+
.sh-search .kbd-mod{display:inline}
|
|
52
59
|
.sh-search .kbd-other{display:none}
|
|
53
60
|
html[data-mod="other"] .sh-search .kbd-mac{display:none}
|
|
54
|
-
html[data-mod="other"] .sh-search .kbd-other{display:inline
|
|
61
|
+
html[data-mod="other"] .sh-search .kbd-other{display:inline}
|
|
55
62
|
.sh-search-m{color:var(--soft);opacity:1}
|
|
56
63
|
.sh-search-m svg{opacity:1}
|
|
57
64
|
.sh-icon{display:inline-grid;place-items:center;width:40px;height:40px;border:0;background:transparent;color:var(--soft);border-radius:var(--r);cursor:pointer;transition:background-color .15s ease,color .15s ease}
|
package/app/styles/tokens.css
CHANGED
package/bin/garden.mjs
CHANGED
|
@@ -77,7 +77,7 @@ function ensureLink(from, to) {
|
|
|
77
77
|
function ensurePublic() {
|
|
78
78
|
const dest = path.join(siteRoot, "public");
|
|
79
79
|
fs.mkdirSync(dest, { recursive: true });
|
|
80
|
-
for (const name of ["favicon.svg", "og.png"]) {
|
|
80
|
+
for (const name of ["favicon.svg", "apple-touch-icon.png", "og.png"]) {
|
|
81
81
|
const from = path.join(gardenPackageRoot, "public", name);
|
|
82
82
|
const to = path.join(dest, name);
|
|
83
83
|
if (fs.existsSync(from) && !fs.existsSync(to)) fs.copyFileSync(from, to);
|
|
@@ -355,6 +355,7 @@ function help() {
|
|
|
355
355
|
garden new 创建文章草稿
|
|
356
356
|
garden embed 拉取外链摘要
|
|
357
357
|
garden links 重新拉取外链图标(build 时会自动做)
|
|
358
|
+
garden images 读取新的远程图片尺寸,写入 image-sizes.json(build 时会自动做)
|
|
358
359
|
garden init [目录] 创建只含稿和配置的新站点
|
|
359
360
|
garden check <文章.md> 检查一篇博文的硬性结构
|
|
360
361
|
garden ui <子命令> Intelligent UI:scan、check、apply、prompt、schema、catalog
|
|
@@ -382,11 +383,18 @@ switch (command) {
|
|
|
382
383
|
case "build":
|
|
383
384
|
// Favicons are refetched on every build; failures fall back to link-assets.json.
|
|
384
385
|
if (process.env.GARDEN_OFFLINE !== "1") runNode("scripts/link-assets.mjs");
|
|
386
|
+
// Remote image sizes: only new images are probed; the rest come from image-sizes.json.
|
|
387
|
+
if (process.env.GARDEN_OFFLINE !== "1") runNode("scripts/image-sizes.mjs");
|
|
385
388
|
runVinext(["build", ...rest], { copyDist: true });
|
|
389
|
+
// Pages for old addresses listed under redirects in site.config.yaml (static export only).
|
|
390
|
+
if (process.env.STATIC_EXPORT === "1") runNode("scripts/redirects.mjs");
|
|
386
391
|
break;
|
|
387
392
|
case "links":
|
|
388
393
|
runNode("scripts/link-assets.mjs");
|
|
389
394
|
break;
|
|
395
|
+
case "images":
|
|
396
|
+
runNode("scripts/image-sizes.mjs");
|
|
397
|
+
break;
|
|
390
398
|
case "start":
|
|
391
399
|
runVinext(["start", ...rest]);
|
|
392
400
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hd-garden",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "用 Markdown 写作、用 GitHub Pages 发布的个人博客引擎。",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"bin",
|
|
28
28
|
"build",
|
|
29
29
|
"public/favicon.svg",
|
|
30
|
+
"public/apple-touch-icon.png",
|
|
30
31
|
"public/og.png",
|
|
31
32
|
"public/images",
|
|
32
33
|
"scripts",
|
|
@@ -81,6 +82,7 @@
|
|
|
81
82
|
"fuse.js": "^7.5.0",
|
|
82
83
|
"github-slugger": "^2.0.0",
|
|
83
84
|
"gray-matter": "^4.0.3",
|
|
85
|
+
"image-size": "^2.0.4",
|
|
84
86
|
"katex": "^0.16.47",
|
|
85
87
|
"marked": "^16.4.2",
|
|
86
88
|
"mdast-util-from-markdown": "^2.1.0",
|
|
Binary file
|
package/public/favicon.svg
CHANGED
|
@@ -1,4 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="
|
|
2
|
-
<rect width="256" height="256" fill="#f8f8f6"/>
|
|
3
|
-
<path fill="#202124" d="M216,208H168V184h4a68.07,68.07,0,0,0,68-68,28,28,0,0,0-56,0,12,12,0,0,1-12,12h-4V56a40,40,0,0,0-80,0V88H84A12,12,0,0,1,72,76a28,28,0,0,0-56,0,68.07,68.07,0,0,0,68,68h4v64H40a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16ZM96,128H84A52.06,52.06,0,0,1,32,76a12,12,0,0,1,24,0,28,28,0,0,0,28,28H96a8,8,0,0,0,8-8V56a24,24,0,0,1,48,0v80a8,8,0,0,0,8,8h12a28,28,0,0,0,28-28,12,12,0,0,1,24,0,52.06,52.06,0,0,1-52,52H160a8,8,0,0,0-8,8v32H104V136A8,8,0,0,0,96,128Z"/>
|
|
4
|
-
</svg>
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="1.5 1.25 28 28" fill="none" stroke="#1B1A18" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><style>@media (prefers-color-scheme: dark){svg{stroke:#EEECE7}}</style><path d="M10 27V10"/><path d="M10 10C10 5.5 13.5 3.5 18 3.5C18 7.5 15 10 10 10Z"/><path d="M10 19C10 15.5 12.5 13.5 15.5 13.5S21 15.5 21 19V27"/></svg>
|