@xingwangzhe/stalux 1.27.0 → 1.28.0
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/README.md +6 -6
- package/package.json +3 -2
- package/src/components/stalux/archives/archivesList.astro +34 -42
- package/src/components/stalux/categories/categoriesList.astro +4 -4
- package/src/components/stalux/common/textType.astro +1 -3
- package/src/components/stalux/footer/FooterBadges.astro +28 -24
- package/src/components/stalux/footer/FooterBeian.astro +22 -26
- package/src/components/stalux/footer/FooterStats.astro +28 -27
- package/src/components/stalux/footer/FooterThemeInfo.astro +24 -28
- package/src/components/stalux/layout/footer.astro +5 -14
- package/src/components/stalux/layout/head.astro +5 -9
- package/src/components/stalux/layout/navs.astro +16 -20
- package/src/components/stalux/links/linkList.astro +4 -4
- package/src/components/stalux/links/socialMedia.astro +34 -34
- package/src/components/stalux/posts/PostTaxonomy.astro +14 -14
- package/src/components/stalux/posts/WalineComment.astro +20 -464
- package/src/components/stalux/posts/postCC.astro +7 -7
- package/src/components/stalux/posts/postCard.astro +41 -41
- package/src/components/stalux/posts/postContent.astro +46 -41
- package/src/components/stalux/posts/postList.astro +4 -4
- package/src/components/stalux/posts/postNavigation.astro +107 -119
- package/src/components/stalux/posts/postToc.astro +20 -20
- package/src/components/stalux/tags/tagsCloud.astro +15 -15
- package/src/components/stalux/words/wordCard.astro +27 -32
- package/src/components/stalux/words/wordList.astro +18 -18
- package/src/i18n/en.json +1 -0
- package/src/i18n/zh-CN.json +1 -0
- package/src/index.ts +82 -34
- package/src/internal/font-slices.ts +151 -136
- package/src/layouts/PostLayout.astro +6 -6
- package/src/layouts/Stalux.astro +4 -8
- package/src/pages/about.astro +6 -6
- package/src/pages/categories/[category].astro +5 -1
- package/src/pages/posts/[post].astro +2 -2
- package/src/plugins/feature-flags.ts +60 -8
- package/src/scripts/waline.ts +32 -11
- package/src/styles/base/init.css +2 -1
- package/src/styles/base/math.css +1 -1
- package/src/styles/components/posts/waline.css +440 -0
- package/src/utils/i18n.ts +16 -5
package/src/index.ts
CHANGED
|
@@ -8,7 +8,14 @@
|
|
|
8
8
|
* 基于 Astro 7.1.3 Integration API(astro:config:setup / injectRoute / injectScript)
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
copyFileSync,
|
|
13
|
+
existsSync,
|
|
14
|
+
promises as fs,
|
|
15
|
+
mkdirSync,
|
|
16
|
+
readdirSync,
|
|
17
|
+
readFileSync,
|
|
18
|
+
} from "node:fs";
|
|
12
19
|
import path from "node:path";
|
|
13
20
|
import { fileURLToPath } from "node:url";
|
|
14
21
|
|
|
@@ -23,7 +30,11 @@ import { createIndex as pagefindCreateIndex } from "pagefind";
|
|
|
23
30
|
import type { StaluxOptions } from "./config";
|
|
24
31
|
import { expressiveCode } from "./expressive-code";
|
|
25
32
|
import { staluxComponentsAlias } from "./internal/components-plugin";
|
|
26
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
clearPageFontSubsets,
|
|
35
|
+
resolveFontInputs,
|
|
36
|
+
writePageFontSubset,
|
|
37
|
+
} from "./internal/font-slices";
|
|
27
38
|
import { createInjectedRoutes } from "./internal/injected-routes";
|
|
28
39
|
import { createRuntimeCacheKey } from "./internal/runtime-cache-key";
|
|
29
40
|
import {
|
|
@@ -32,36 +43,23 @@ import {
|
|
|
32
43
|
prepareSatteriProcessor,
|
|
33
44
|
} from "./internal/satteri-config";
|
|
34
45
|
import { createViteAliases } from "./internal/vite-aliases";
|
|
35
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
applyHtmlImageLoadingPolicy,
|
|
48
|
+
featureFlagsHast,
|
|
49
|
+
featureFlagsMdast,
|
|
50
|
+
} from "./plugins/feature-flags";
|
|
36
51
|
import { temml } from "./plugins/satteri-temml";
|
|
37
52
|
import { describeError } from "./utils/diagnostics";
|
|
38
53
|
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
// 7.2.2 已修复 Fonts API 随机端口污染 incrementalBuild dependencyHash 的问题(PR #17659)。
|
|
42
|
-
function buildFontsConfig(slices: FontSlice[], codeNormal: string, codeItalic?: string) {
|
|
54
|
+
// 代码字体通过 Astro Fonts API 注入;正文字体由 native N-API 按页面生成子集。
|
|
55
|
+
function buildFontsConfig(codeNormal: string, codeItalic?: string) {
|
|
43
56
|
return [
|
|
44
|
-
{
|
|
45
|
-
// 正文:LXGW WenKai 按 unicode-range 分片,浏览器只下载命中区间的分片
|
|
46
|
-
provider: fontProviders.local(),
|
|
47
|
-
name: "LXGW WenKai",
|
|
48
|
-
cssVariable: "--font-body",
|
|
49
|
-
fallbacks: ["Noto Sans SC", "Noto Sans CJK SC", "system-ui", "sans-serif"],
|
|
50
|
-
optimizedFallbacks: true,
|
|
51
|
-
options: {
|
|
52
|
-
variants: slices.map((slice) => ({
|
|
53
|
-
weight: 400,
|
|
54
|
-
style: "normal",
|
|
55
|
-
src: [slice.src],
|
|
56
|
-
unicodeRange: slice.unicodeRange,
|
|
57
|
-
})),
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
57
|
{
|
|
61
58
|
// 代码:Google Sans Code 可变字体,原样注册,不切片
|
|
62
59
|
provider: fontProviders.local(),
|
|
63
60
|
name: "Google Sans Code",
|
|
64
61
|
cssVariable: "--font-code",
|
|
62
|
+
display: "optional" as const,
|
|
65
63
|
fallbacks: ["JetBrains Mono", "Fira Code", "Consolas", "Courier New", "monospace"],
|
|
66
64
|
optimizedFallbacks: true,
|
|
67
65
|
options: {
|
|
@@ -150,6 +148,7 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
150
148
|
// 探测是否为插件模式(通过 npm 安装)vs 源码开发模式
|
|
151
149
|
// 安装在 node_modules 中时,页面的文件路由不可用,需要通过 injectRoute 注入
|
|
152
150
|
const isPluginMode = import.meta.url.includes("node_modules");
|
|
151
|
+
let pageFontData: { fontBuffer: Buffer; outputDir: string; cacheDir: string } | undefined;
|
|
153
152
|
|
|
154
153
|
const coreIntegration: AstroIntegration = {
|
|
155
154
|
name: "stalux",
|
|
@@ -330,20 +329,20 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
330
329
|
}
|
|
331
330
|
}
|
|
332
331
|
|
|
333
|
-
// 7.
|
|
334
|
-
// (config:setup 阶段执行,保证 dev/build 都能生成;local provider 纯本地读文件)
|
|
332
|
+
// 7. 字体:解析本地字体输入;正文字体由 native N-API 在 build:done 阶段逐页生成。
|
|
335
333
|
try {
|
|
336
|
-
const sliced = await
|
|
334
|
+
const sliced = await resolveFontInputs(process.cwd(), fontsLogger);
|
|
337
335
|
if (sliced) {
|
|
336
|
+
pageFontData = {
|
|
337
|
+
fontBuffer: sliced.fontBuffer,
|
|
338
|
+
outputDir: path.join(fileURLToPath(config.outDir), "_astro", "fonts"),
|
|
339
|
+
cacheDir: sliced.pageSubsetDir,
|
|
340
|
+
};
|
|
338
341
|
updateConfig({
|
|
339
|
-
fonts: buildFontsConfig(
|
|
340
|
-
sliced.body,
|
|
341
|
-
sliced.codeNormal,
|
|
342
|
-
sliced.codeItalic,
|
|
343
|
-
),
|
|
342
|
+
fonts: buildFontsConfig(sliced.codeNormal, sliced.codeItalic),
|
|
344
343
|
});
|
|
345
344
|
fontsLogger.debug(
|
|
346
|
-
|
|
345
|
+
"injected code font via Astro Fonts API; page body subsets use native N-API",
|
|
347
346
|
);
|
|
348
347
|
}
|
|
349
348
|
} catch (error) {
|
|
@@ -353,9 +352,58 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
353
352
|
},
|
|
354
353
|
|
|
355
354
|
"astro:build:done": async ({ dir, logger: rootLogger }) => {
|
|
356
|
-
const logger = rootLogger.fork("stalux/
|
|
355
|
+
const logger = rootLogger.fork("stalux/build");
|
|
357
356
|
const started = performance.now();
|
|
358
|
-
const outDir =
|
|
357
|
+
const outDir = pageFontData
|
|
358
|
+
? path.dirname(path.dirname(pageFontData.outputDir))
|
|
359
|
+
: fileURLToPath(new URL(String(dir)));
|
|
360
|
+
|
|
361
|
+
// Astro's Markdown image transform drops arbitrary lazy-loading hints
|
|
362
|
+
// added during HAST processing. Reapply the policy to emitted HTML.
|
|
363
|
+
const htmlFiles = await fs.readdir(outDir, { recursive: true });
|
|
364
|
+
let optimizedImagePages = 0;
|
|
365
|
+
let pageFontPages = 0;
|
|
366
|
+
if (pageFontData) await fs.mkdir(pageFontData.outputDir, { recursive: true });
|
|
367
|
+
if (pageFontData) await fs.mkdir(pageFontData.cacheDir, { recursive: true });
|
|
368
|
+
const referencedPageFonts = new Set<string>();
|
|
369
|
+
for (const entry of htmlFiles) {
|
|
370
|
+
if (typeof entry !== "string" || !entry.endsWith(".html")) continue;
|
|
371
|
+
const output = path.join(outDir, entry);
|
|
372
|
+
const before = await fs.readFile(output, "utf8");
|
|
373
|
+
const withoutPageFont = before.replace(
|
|
374
|
+
/<style data-stalux-page-font>[\s\S]*?<\/style>/gi,
|
|
375
|
+
"",
|
|
376
|
+
);
|
|
377
|
+
let after = applyHtmlImageLoadingPolicy(withoutPageFont);
|
|
378
|
+
if (pageFontData) {
|
|
379
|
+
const pageFont = writePageFontSubset(
|
|
380
|
+
after,
|
|
381
|
+
pageFontData.fontBuffer,
|
|
382
|
+
pageFontData.cacheDir,
|
|
383
|
+
"/_astro/fonts/",
|
|
384
|
+
);
|
|
385
|
+
if (pageFont) {
|
|
386
|
+
after = pageFont.html;
|
|
387
|
+
pageFontPages++;
|
|
388
|
+
referencedPageFonts.add(pageFont.filename);
|
|
389
|
+
copyFileSync(
|
|
390
|
+
pageFont.sourcePath,
|
|
391
|
+
path.join(pageFontData.outputDir, pageFont.filename),
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (after !== before) {
|
|
396
|
+
await fs.writeFile(output, after);
|
|
397
|
+
optimizedImagePages++;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (pageFontData) {
|
|
401
|
+
clearPageFontSubsets(pageFontData.outputDir, referencedPageFonts);
|
|
402
|
+
}
|
|
403
|
+
logger.info(`applied image loading hints to ${optimizedImagePages} HTML pages`);
|
|
404
|
+
logger.info(
|
|
405
|
+
`generated page-specific body font subsets for ${pageFontPages} HTML pages`,
|
|
406
|
+
);
|
|
359
407
|
|
|
360
408
|
// 后处理:Pagefind 搜索索引。启用时任何失败都必须让构建失败,
|
|
361
409
|
// 避免发布一个页面正常但搜索已损坏的产物。
|
|
@@ -1,29 +1,25 @@
|
|
|
1
|
-
import { describeError } from "../utils/diagnostics";
|
|
2
1
|
/**
|
|
3
|
-
* Build-time CJK font
|
|
2
|
+
* Build-time CJK font subsetting engine.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* pipeline (`updateConfig({ fonts })`). The browser downloads only the chunks
|
|
8
|
-
* whose unicode-range matches characters on the page.
|
|
4
|
+
* LXGW WenKai is subset per rendered page with the native N-API package. Only
|
|
5
|
+
* the independent Google Sans Code font uses Astro's local Fonts API.
|
|
9
6
|
*
|
|
10
7
|
* Astro's local provider reads font files from disk (`readFile`), so no
|
|
11
8
|
* network access is involved at build time — identical behavior on GitHub
|
|
12
|
-
* Actions and CN mirrors.
|
|
13
|
-
*
|
|
9
|
+
* Actions and CN mirrors. Page subset cache is emitted to `node_modules/.astro/`
|
|
10
|
+
* so it stays out of the repo and out of `dist/fonts`.
|
|
14
11
|
*
|
|
15
12
|
* Output layout:
|
|
16
|
-
* node_modules/.astro/stalux-fonts/
|
|
17
|
-
*
|
|
13
|
+
* node_modules/.astro/stalux-page-fonts/
|
|
14
|
+
* page-{hash}.woff2 — page-specific content-addressed CJK subset
|
|
18
15
|
*/
|
|
19
16
|
|
|
20
|
-
import {
|
|
21
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
22
18
|
import { dirname, join, resolve } from "node:path";
|
|
23
19
|
import { fileURLToPath } from "node:url";
|
|
24
|
-
|
|
20
|
+
import { subsetFont } from "@xingwangzhe/cjk-font-split-native";
|
|
25
21
|
import type { AstroIntegrationLogger } from "astro";
|
|
26
|
-
import
|
|
22
|
+
import { type DefaultTreeAdapterMap, parse } from "parse5";
|
|
27
23
|
|
|
28
24
|
// ---------------------------------------------------------------------------
|
|
29
25
|
// Configuration
|
|
@@ -36,71 +32,22 @@ const FONT_INPUT = "src/assets/fonts/LXGWWenKai-Regular.ttf";
|
|
|
36
32
|
const CODE_FONT_INPUT = "src/assets/fonts/GoogleSansCode.woff2";
|
|
37
33
|
const CODE_FONT_ITALIC_INPUT = "src/assets/fonts/GoogleSansCode-Italic.woff2";
|
|
38
34
|
|
|
39
|
-
|
|
40
|
-
const SLICE_OUT_DIR = "node_modules/.astro/stalux-fonts";
|
|
41
|
-
|
|
42
|
-
/** Chunk size (code points) for the CJK Unified Ideographs block */
|
|
43
|
-
const CJK_CHUNK_SIZE = 1050;
|
|
35
|
+
const PAGE_SLICE_OUT_DIR = "node_modules/.astro/stalux-page-fonts";
|
|
44
36
|
|
|
45
37
|
// ---------------------------------------------------------------------------
|
|
46
38
|
// Types
|
|
47
39
|
// ---------------------------------------------------------------------------
|
|
48
40
|
|
|
49
|
-
export interface
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
unicodeRange: string[];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface SlicedFonts {
|
|
57
|
-
/** Body font slices (LXGW WenKai), one variant per chunk */
|
|
58
|
-
body: FontSlice[];
|
|
41
|
+
export interface FontInputs {
|
|
42
|
+
/** Build-time per-page subset cache, outside dist so incremental builds can reuse it. */
|
|
43
|
+
pageSubsetDir: string;
|
|
44
|
+
fontBuffer: Buffer;
|
|
59
45
|
/** Absolute path to the code font woff2 */
|
|
60
46
|
codeNormal: string;
|
|
61
47
|
/** Absolute path to the code italic woff2, if present */
|
|
62
48
|
codeItalic?: string;
|
|
63
49
|
}
|
|
64
50
|
|
|
65
|
-
interface SliceDef {
|
|
66
|
-
/** Ordinal used in the output filename */
|
|
67
|
-
id: number;
|
|
68
|
-
/** [start, end] code-point ranges covered by this slice */
|
|
69
|
-
ranges: Array<[number, number]>;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
|
-
// Slice definitions
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* CJK Unified Ideographs (U+4E00–U+9FFF) split into fixed-size chunks.
|
|
78
|
-
* Chunk boundaries are content-independent, so slices are deterministic
|
|
79
|
-
* across builds (no per-page character scanning).
|
|
80
|
-
*/
|
|
81
|
-
function buildCjkChunks(start = 0x4e00, end = 0x9fff, size = CJK_CHUNK_SIZE): SliceDef[] {
|
|
82
|
-
const chunks: SliceDef[] = [];
|
|
83
|
-
for (let s = start, id = 2; s <= end; s += size, id++) {
|
|
84
|
-
chunks.push({ id, ranges: [[s, Math.min(s + size - 1, end)]] });
|
|
85
|
-
}
|
|
86
|
-
return chunks;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const SLICES: SliceDef[] = [
|
|
90
|
-
// ASCII printable + Latin-1 supplement
|
|
91
|
-
{ id: 0, ranges: [[0x20, 0x7e]] },
|
|
92
|
-
// CJK punctuation / fullwidth forms / general punctuation
|
|
93
|
-
{
|
|
94
|
-
id: 1,
|
|
95
|
-
ranges: [
|
|
96
|
-
[0x2000, 0x206f],
|
|
97
|
-
[0x3000, 0x303f],
|
|
98
|
-
[0xff00, 0xffef],
|
|
99
|
-
],
|
|
100
|
-
},
|
|
101
|
-
...buildCjkChunks(),
|
|
102
|
-
];
|
|
103
|
-
|
|
104
51
|
// ---------------------------------------------------------------------------
|
|
105
52
|
// Helpers
|
|
106
53
|
// ---------------------------------------------------------------------------
|
|
@@ -115,43 +62,22 @@ function findFont(projectRoot: string, rel: string): string | undefined {
|
|
|
115
62
|
return candidates.find((p) => existsSync(p));
|
|
116
63
|
}
|
|
117
64
|
|
|
118
|
-
/** Render a [start, end] range as a CSS unicode-range value. */
|
|
119
|
-
export function toUnicodeRange(ranges: Array<[number, number]>): string[] {
|
|
120
|
-
const hex = (codePoint: number) => codePoint.toString(16).toUpperCase().padStart(4, "0");
|
|
121
|
-
return ranges.map(([start, end]) =>
|
|
122
|
-
start === end ? `U+${hex(start)}` : `U+${hex(start)}-${hex(end)}`,
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Collect every code point in the ranges as a deduped, sorted string. */
|
|
127
|
-
function rangesToChars(ranges: Array<[number, number]>): string {
|
|
128
|
-
const set = new Set<string>();
|
|
129
|
-
for (const [s, e] of ranges) {
|
|
130
|
-
for (let cp = s; cp <= e; cp++) {
|
|
131
|
-
set.add(String.fromCodePoint(cp));
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return [...set].sort().join("");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
65
|
// ---------------------------------------------------------------------------
|
|
138
66
|
// Main entry
|
|
139
67
|
// ---------------------------------------------------------------------------
|
|
140
68
|
|
|
141
69
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
70
|
+
* Resolve the body-font source for native page subsetting and code fonts for
|
|
71
|
+
* Astro's local provider. Returns `null` when a required font is missing so
|
|
144
72
|
* the caller can skip font injection (pages fall back to system fonts).
|
|
145
73
|
*
|
|
146
74
|
* Called from the `astro:config:setup` hook before `updateConfig({ fonts })`.
|
|
147
75
|
*/
|
|
148
|
-
export async function
|
|
76
|
+
export async function resolveFontInputs(
|
|
149
77
|
projectRoot: string,
|
|
150
78
|
logger: AstroIntegrationLogger,
|
|
151
|
-
): Promise<
|
|
79
|
+
): Promise<FontInputs | null> {
|
|
152
80
|
const started = performance.now();
|
|
153
|
-
let cached = 0;
|
|
154
|
-
let generated = 0;
|
|
155
81
|
logger.debug("locating font inputs");
|
|
156
82
|
// 1. Locate fonts (check project root first, then stalux package dir)
|
|
157
83
|
const fontPath = findFont(projectRoot, FONT_INPUT);
|
|
@@ -167,55 +93,144 @@ export async function runFontSlicing(
|
|
|
167
93
|
const codeItalic = findFont(projectRoot, CODE_FONT_ITALIC_INPUT);
|
|
168
94
|
|
|
169
95
|
const fontBuffer = readFileSync(fontPath);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
} else {
|
|
189
|
-
try {
|
|
190
|
-
const data = await subsetFont(fontBuffer, chars, { targetFormat: "woff2" });
|
|
191
|
-
if (data.length > 0) {
|
|
192
|
-
writeFileSync(outPath, data);
|
|
193
|
-
generated++;
|
|
194
|
-
logger.debug(
|
|
195
|
-
` body slice ${def.id}: ${(data.length / 1024).toFixed(1)} KB → ${filename}`,
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
} catch (error) {
|
|
199
|
-
logger.warn(` body slice ${def.id} failed: ${describeError(error)}`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
96
|
+
logger.debug(`font inputs located in ${(performance.now() - started).toFixed(1)}ms`);
|
|
97
|
+
logger.info(
|
|
98
|
+
`Native page-font source ready (LXGW ${(fontBuffer.length / 1024 / 1024).toFixed(1)} MB)`,
|
|
99
|
+
);
|
|
100
|
+
return {
|
|
101
|
+
pageSubsetDir: resolve(projectRoot, PAGE_SLICE_OUT_DIR),
|
|
102
|
+
fontBuffer,
|
|
103
|
+
codeNormal,
|
|
104
|
+
codeItalic,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface PageFontSubset {
|
|
109
|
+
html: string;
|
|
110
|
+
filename: string;
|
|
111
|
+
sourcePath: string;
|
|
112
|
+
cacheHit: boolean;
|
|
113
|
+
}
|
|
202
114
|
|
|
203
|
-
|
|
204
|
-
|
|
115
|
+
type HtmlNode = DefaultTreeAdapterMap["node"];
|
|
116
|
+
|
|
117
|
+
const NON_RENDERED_ELEMENTS = new Set(["script", "style", "template", "title"]);
|
|
118
|
+
const VISUALLY_HIDDEN_CLASSES = new Set([
|
|
119
|
+
"agent-home-summary",
|
|
120
|
+
"screen-reader-only",
|
|
121
|
+
"sr-only",
|
|
122
|
+
"visually-hidden",
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
function findBody(node: HtmlNode): HtmlNode | undefined {
|
|
126
|
+
if ("tagName" in node && node.tagName === "body") return node;
|
|
127
|
+
if ("childNodes" in node) {
|
|
128
|
+
for (const child of node.childNodes) {
|
|
129
|
+
const body = findBody(child);
|
|
130
|
+
if (body) return body;
|
|
205
131
|
}
|
|
206
132
|
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
207
135
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
136
|
+
function isHiddenElement(attrs: Array<{ name: string; value: string }>): boolean {
|
|
137
|
+
const attributes = new Map(attrs.map(({ name, value }) => [name, value]));
|
|
138
|
+
if (attributes.has("hidden") || attributes.has("inert")) return true;
|
|
139
|
+
|
|
140
|
+
const classes = (attributes.get("class") ?? "").split(/\s+/u);
|
|
141
|
+
if (classes.some((name) => VISUALLY_HIDDEN_CLASSES.has(name))) return true;
|
|
142
|
+
|
|
143
|
+
const style = attributes.get("style") ?? "";
|
|
144
|
+
return /(?:^|;)\s*(?:display\s*:\s*none|visibility\s*:\s*hidden|content-visibility\s*:\s*hidden)\s*(?:;|$)/iu.test(
|
|
145
|
+
style,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Collect renderable body text nodes, including content inside collapsed controls. */
|
|
150
|
+
function collectBodyText(node: HtmlNode, output: string[]): void {
|
|
151
|
+
if (node.nodeName === "#text" && "value" in node) {
|
|
152
|
+
output.push(node.value);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!("childNodes" in node)) return;
|
|
157
|
+
if (
|
|
158
|
+
"tagName" in node &&
|
|
159
|
+
(NON_RENDERED_ELEMENTS.has(node.tagName) || isHiddenElement(node.attrs))
|
|
160
|
+
) {
|
|
161
|
+
return;
|
|
211
162
|
}
|
|
212
163
|
|
|
213
|
-
|
|
214
|
-
|
|
164
|
+
for (const child of node.childNodes) collectBodyText(child, output);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Create a cached body-font subset containing precisely the page's CJK code points. */
|
|
168
|
+
export function writePageFontSubset(
|
|
169
|
+
html: string,
|
|
170
|
+
fontBuffer: Buffer,
|
|
171
|
+
cacheDir: string,
|
|
172
|
+
publicUrlPrefix = "/_astro/fonts/",
|
|
173
|
+
): PageFontSubset | undefined {
|
|
174
|
+
const document = parse(html);
|
|
175
|
+
const body = findBody(document);
|
|
176
|
+
if (!body) return undefined;
|
|
177
|
+
|
|
178
|
+
const bodyText: string[] = [];
|
|
179
|
+
collectBodyText(body, bodyText);
|
|
180
|
+
const chars = [
|
|
181
|
+
...new Set(
|
|
182
|
+
[...bodyText.join("")].filter((char) =>
|
|
183
|
+
/[\u2000-\u206f\u3000-\u303f\u4e00-\u9fff\uff00-\uffef]/u.test(char),
|
|
184
|
+
),
|
|
185
|
+
),
|
|
186
|
+
]
|
|
187
|
+
.sort()
|
|
188
|
+
.join("");
|
|
189
|
+
if (!chars) return undefined;
|
|
190
|
+
|
|
191
|
+
const result = subsetFont(fontBuffer, chars, cacheDir);
|
|
192
|
+
const filename = `page-${result.hash}.woff2`;
|
|
193
|
+
|
|
194
|
+
const rel = `${publicUrlPrefix}${filename}`;
|
|
195
|
+
// Keep Astro's broad unicode-range font out of the fallback stack; otherwise
|
|
196
|
+
// a character outside this page's subset would download multiple 300 KB slices.
|
|
197
|
+
const originalFontStack = html.match(/--font-body:\s*([^;}]+)/u)?.[1]?.trim();
|
|
198
|
+
const fallbackStack =
|
|
199
|
+
originalFontStack || '"Noto Sans SC","Noto Sans CJK SC",system-ui,sans-serif';
|
|
200
|
+
const originalCodeStack = html.match(/--font-code:\s*([^;}]+)/u)?.[1]?.trim();
|
|
201
|
+
const codeStack = originalCodeStack
|
|
202
|
+
? `${originalCodeStack},"LXGW WenKai-Page Subset"`
|
|
203
|
+
: `"LXGW WenKai-Page Subset",${fallbackStack}`;
|
|
204
|
+
const css = `@font-face{font-family:"LXGW WenKai-Page Subset";src:url("${rel}") format("woff2");font-style:normal;font-weight:400;font-display:swap;unicode-range:${[...chars].map((c) => `U+${c.codePointAt(0)?.toString(16).toUpperCase()}`).join(",")}}:root{--font-body:"LXGW WenKai-Page Subset",${fallbackStack};--font-code:${codeStack}}`;
|
|
205
|
+
const withoutBroadBodyFonts = html.replace(
|
|
206
|
+
/@font-face\{[^}]*font-family:"LXGW WenKai-[^"]+"[^}]*\}/g,
|
|
207
|
+
(face) => {
|
|
208
|
+
const unicodeRange = face.match(/unicode-range:([^;}]+)/i)?.[1] ?? "";
|
|
209
|
+
const hasCjkRange = unicodeRange.split(",").some((range) => {
|
|
210
|
+
const start = range.match(/U\+([\da-f]+)/i)?.[1];
|
|
211
|
+
if (!start) return false;
|
|
212
|
+
const codePoint = Number.parseInt(start, 16);
|
|
213
|
+
return codePoint >= 0x4e00 && codePoint <= 0x9fff;
|
|
214
|
+
});
|
|
215
|
+
return hasCjkRange ? "" : face;
|
|
216
|
+
},
|
|
215
217
|
);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
218
|
+
const outputHtml = withoutBroadBodyFonts.replace(
|
|
219
|
+
/<\/head\s*>/i,
|
|
220
|
+
`<style data-stalux-page-font>${css}</style></head>`,
|
|
219
221
|
);
|
|
220
|
-
return {
|
|
222
|
+
return { html: outputHtml, filename, sourcePath: result.path, cacheHit: result.cacheHit };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Remove stale page subsets so deploy artifacts only contain the current build's referenced files. */
|
|
226
|
+
export function clearPageFontSubsets(
|
|
227
|
+
outputDir: string,
|
|
228
|
+
referencedFiles: Set<string> = new Set(),
|
|
229
|
+
): void {
|
|
230
|
+
if (!existsSync(outputDir)) return;
|
|
231
|
+
for (const name of readdirSync(outputDir)) {
|
|
232
|
+
if (name.startsWith("page-") && name.endsWith(".woff2") && !referencedFiles.has(name)) {
|
|
233
|
+
unlinkSync(join(outputDir, name));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
221
236
|
}
|
|
@@ -67,12 +67,12 @@ const walineConfig = walineSection?.serverURL
|
|
|
67
67
|
<PostCC title={props.title} author={author} url={Astro.url.href} cc={props.cc} />
|
|
68
68
|
<PostNavigation prev={props.prev} next={props.next} />
|
|
69
69
|
{commentEnabled && walineConfig?.serverURL && (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
<WalineComment
|
|
71
|
+
{...buildWalineProps(walineConfig)}
|
|
72
|
+
path={Astro.url.pathname}
|
|
73
|
+
title={props.title}
|
|
74
|
+
/>
|
|
75
|
+
)}
|
|
76
76
|
</PostContent>
|
|
77
77
|
|
|
78
78
|
<!-- Right Sidebar -->
|
package/src/layouts/Stalux.astro
CHANGED
|
@@ -73,11 +73,7 @@ const routeAnimation = getRouteAnimation(routeKind);
|
|
|
73
73
|
import "../scripts/background.ts";
|
|
74
74
|
import "../scripts/view-transitions.ts";
|
|
75
75
|
</script>
|
|
76
|
-
{aiConfig?.conformance !== "disabled" &&
|
|
77
|
-
<script>
|
|
78
|
-
import "../scripts/webmcp.ts";
|
|
79
|
-
</script>
|
|
80
|
-
)}
|
|
76
|
+
{aiConfig?.conformance !== "disabled" && <script>import "../scripts/webmcp.ts";</script>}
|
|
81
77
|
<body
|
|
82
78
|
class="stalux-root dark"
|
|
83
79
|
data-stalux-bg-layer="a"
|
|
@@ -90,10 +86,10 @@ const routeAnimation = getRouteAnimation(routeKind);
|
|
|
90
86
|
data-stalux-clarity-id={headConfig?.bingClarityId}
|
|
91
87
|
>
|
|
92
88
|
{llmPromote && (
|
|
93
|
-
|
|
89
|
+
<section class="stalux-ai-prompt" data-stalux-ai-prompt aria-label="AI site context">
|
|
94
90
|
<p>{llmPromote}</p>
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
</section>
|
|
92
|
+
)}
|
|
97
93
|
<div
|
|
98
94
|
class="bg-layer bg-a"
|
|
99
95
|
style="opacity: 1;"
|
package/src/pages/about.astro
CHANGED
|
@@ -47,12 +47,12 @@ const author = authorConfig.name;
|
|
|
47
47
|
cc="CC-BY-NC-SA-4.0"
|
|
48
48
|
/>
|
|
49
49
|
{commentEnabled && walineConfig?.serverURL && (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
<WalineComment
|
|
51
|
+
{...buildWalineProps(walineConfig)}
|
|
52
|
+
path={Astro.url.pathname}
|
|
53
|
+
title={aboutPage.data.title}
|
|
54
|
+
/>
|
|
55
|
+
)}
|
|
56
56
|
</PostContent>
|
|
57
57
|
</div>
|
|
58
58
|
</Stalux>
|
|
@@ -27,7 +27,11 @@ const props = Astro.props as Props;
|
|
|
27
27
|
|
|
28
28
|
<Stalux
|
|
29
29
|
title={`${t("taxonomy.category")}: ${props.category} | ${siteConfig.title}`}
|
|
30
|
-
description={t("taxonomy.browseCategory", {
|
|
30
|
+
description={t("taxonomy.browseCategory", {
|
|
31
|
+
title: siteConfig.title,
|
|
32
|
+
category: props.category,
|
|
33
|
+
count: props.count,
|
|
34
|
+
})}
|
|
31
35
|
>
|
|
32
36
|
<div style="height: 3em;"></div>
|
|
33
37
|
<Navs />
|
|
@@ -72,8 +72,8 @@ export async function getStaticPaths() {
|
|
|
72
72
|
date={props.post.data.date}
|
|
73
73
|
updated={props.post.data.updated}
|
|
74
74
|
readingMinutes={flags.readingMinutes}
|
|
75
|
-
tags={props.post.data.tags||[]}
|
|
76
|
-
categories={props.post.data.categories||[]}
|
|
75
|
+
tags={props.post.data.tags || []}
|
|
76
|
+
categories={props.post.data.categories || []}
|
|
77
77
|
headings={headings}
|
|
78
78
|
author={authorConfig.name}
|
|
79
79
|
authorAvatar={authorConfig.avatar}
|