@xingwangzhe/stalux 1.35.2 → 1.35.5
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/LICENSE +4 -0
- package/README.md +6 -0
- package/package.json +4 -3
- package/public/fonts/GUST-FONT-LICENSE.txt +30 -0
- package/public/fonts/Google-Sans-Code-OFL.txt +93 -0
- package/public/fonts/LPPL-1.3c.txt +416 -0
- package/public/fonts/LXGW-WenKai-OFL.txt +94 -0
- package/public/fonts/StaluxMathAux-OFL.txt +93 -0
- package/public/fonts/StaluxMathAux.woff2 +0 -0
- package/public/fonts/latinmodern-math.woff2 +0 -0
- package/src/index.ts +114 -20
- package/src/internal/font-slices.ts +56 -6
- package/src/internal/html-images.ts +23 -2
- package/src/internal/image-dimensions.ts +60 -33
- package/src/internal/page-output-cache.ts +78 -0
- package/src/plugins/feature-flags.ts +5 -17
- package/src/styles/generated-prose.css +1 -1
- package/src/styles/math.css +5 -5
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Copyright 2021-2026 LXGW (https://github.com/lxgw/LxgwWenKai), with Reserved Font Name '霞鹜', '霞鶩', '落霞孤鹜', '落霞孤鶩' and 'LXGW'. [ADDITIONAL PERMISSION] The Reserved Font Names '霞鹜', '霞鶩', '落霞孤鹜', '落霞孤鶩' and 'LXGW' may continue to be used in Modified Versions recompiled from the Original Version, without modifications to the font source code; or in Modified Versions subsetted or converted to other formats (e.g., WOFF/WOFF2) solely for web font delivery, provided such Modified Versions are not made available as installable desktop fonts (e.g., on mainstream platforms like Google Fonts, or third-party non-commercial platforms recognized by the author @lxgw; other web font platforms please contact the author @lxgw for confirmation).
|
|
2
|
+
Copyright 2020 The Klee Project Authors (https://github.com/fontworks-fonts/Klee)
|
|
3
|
+
|
|
4
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
5
|
+
This license is copied below, and is also available with a FAQ at:
|
|
6
|
+
https://openfontlicense.org
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
-----------------------------------------------------------
|
|
10
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
11
|
+
-----------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
PREAMBLE
|
|
14
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
15
|
+
development of collaborative font projects, to support the font creation
|
|
16
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
17
|
+
open framework in which fonts may be shared and improved in partnership
|
|
18
|
+
with others.
|
|
19
|
+
|
|
20
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
21
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
22
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
23
|
+
redistributed and/or sold with any software provided that any reserved
|
|
24
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
25
|
+
however, cannot be released under any other type of license. The
|
|
26
|
+
requirement for fonts to remain under this license does not apply
|
|
27
|
+
to any document created using the fonts or their derivatives.
|
|
28
|
+
|
|
29
|
+
DEFINITIONS
|
|
30
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
31
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
32
|
+
include source files, build scripts and documentation.
|
|
33
|
+
|
|
34
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
35
|
+
copyright statement(s).
|
|
36
|
+
|
|
37
|
+
"Original Version" refers to the collection of Font Software components as
|
|
38
|
+
distributed by the Copyright Holder(s).
|
|
39
|
+
|
|
40
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
41
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
42
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
43
|
+
new environment.
|
|
44
|
+
|
|
45
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
46
|
+
writer or other person who contributed to the Font Software.
|
|
47
|
+
|
|
48
|
+
PERMISSION & CONDITIONS
|
|
49
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
50
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
51
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
52
|
+
Software, subject to the following conditions:
|
|
53
|
+
|
|
54
|
+
1) Neither the Font Software nor any of its individual components,
|
|
55
|
+
in Original or Modified Versions, may be sold by itself.
|
|
56
|
+
|
|
57
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
58
|
+
redistributed and/or sold with any software, provided that each copy
|
|
59
|
+
contains the above copyright notice and this license. These can be
|
|
60
|
+
included either as stand-alone text files, human-readable headers or
|
|
61
|
+
in the appropriate machine-readable metadata fields within text or
|
|
62
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
63
|
+
|
|
64
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
65
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
66
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
67
|
+
presented to the users.
|
|
68
|
+
|
|
69
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
70
|
+
Software shall not be used to promote, endorse or advertise any
|
|
71
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
72
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
73
|
+
permission.
|
|
74
|
+
|
|
75
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
76
|
+
must be distributed entirely under this license, and must not be
|
|
77
|
+
distributed under any other license. The requirement for fonts to
|
|
78
|
+
remain under this license does not apply to any document created
|
|
79
|
+
using the Font Software.
|
|
80
|
+
|
|
81
|
+
TERMINATION
|
|
82
|
+
This license becomes null and void if any of the above conditions are
|
|
83
|
+
not met.
|
|
84
|
+
|
|
85
|
+
DISCLAIMER
|
|
86
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
87
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
88
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
89
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
90
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
91
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
92
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
93
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
94
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright (c) 2009-2010 Design Science, Inc.
|
|
2
|
+
Copyright (c) 2014-2018 Khan Academy
|
|
3
|
+
Original fonts have Reserved Font Names KaTeX_Main and KaTeX_Script.
|
|
4
|
+
Modified font: Stalux Math Aux. Copyright (c) 2026 xingwangzhe.
|
|
5
|
+
Renamed, script codepoints remapped, primes scaled/repeated/reflected;
|
|
6
|
+
rebuilt with FontTools without FontCreator or its generated binaries.
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Binary file
|
|
Binary file
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
readdirSync,
|
|
20
20
|
readFileSync,
|
|
21
21
|
} from "node:fs";
|
|
22
|
+
import { createRequire } from "node:module";
|
|
22
23
|
import path from "node:path";
|
|
23
24
|
import { fileURLToPath } from "node:url";
|
|
24
25
|
import partytown from "@astrojs/partytown";
|
|
@@ -30,6 +31,7 @@ import { fontProviders } from "astro/config";
|
|
|
30
31
|
// pagefind 是 ESM-only 包,需要在模块顶层导入
|
|
31
32
|
// 因为 astro:build:done 钩子中 Vite module runner 已关闭,无法动态 import
|
|
32
33
|
import { createIndex as pagefindCreateIndex } from "pagefind";
|
|
34
|
+
import { parse } from "parse5";
|
|
33
35
|
import { parse as parseYaml } from "yaml";
|
|
34
36
|
import type { StaluxOptions } from "./config";
|
|
35
37
|
import { expressiveCode } from "./expressive-code";
|
|
@@ -38,10 +40,12 @@ import {
|
|
|
38
40
|
clearPageFontSubsets,
|
|
39
41
|
readLinkedStylesheetText,
|
|
40
42
|
resolveFontInputs,
|
|
41
|
-
|
|
43
|
+
writePageFontSubsetAsync,
|
|
42
44
|
} from "./internal/font-slices";
|
|
43
|
-
import {
|
|
45
|
+
import { findHtmlImages } from "./internal/html-images";
|
|
46
|
+
import { addLocalImageDimensions, type ImageDimensions } from "./internal/image-dimensions";
|
|
44
47
|
import { createInjectedRoutes } from "./internal/injected-routes";
|
|
48
|
+
import { createPageOutputCache } from "./internal/page-output-cache";
|
|
45
49
|
import { createRuntimeCacheKey } from "./internal/runtime-cache-key";
|
|
46
50
|
import {
|
|
47
51
|
appendUniquePlugin,
|
|
@@ -49,11 +53,7 @@ import {
|
|
|
49
53
|
prepareSatteriProcessor,
|
|
50
54
|
} from "./internal/satteri-config";
|
|
51
55
|
import { createViteAliases } from "./internal/vite-aliases";
|
|
52
|
-
import {
|
|
53
|
-
applyHtmlImageLoadingPolicy,
|
|
54
|
-
featureFlagsHast,
|
|
55
|
-
featureFlagsMdast,
|
|
56
|
-
} from "./plugins/feature-flags";
|
|
56
|
+
import { featureFlagsHast, featureFlagsMdast } from "./plugins/feature-flags";
|
|
57
57
|
import { temml } from "./plugins/satteri-temml";
|
|
58
58
|
import { describeError } from "./utils/diagnostics";
|
|
59
59
|
|
|
@@ -123,6 +123,21 @@ function syncBackgroundSvgs(srcDir: string): { copied: number; skipped: number }
|
|
|
123
123
|
return { copied, skipped };
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/** Sync packaged math fonts and licensing notices; Astro does not merge integration public directories. */
|
|
127
|
+
function syncFontAssets(srcDir: string): void {
|
|
128
|
+
const source = path.join(srcDir, "../public/fonts");
|
|
129
|
+
if (!existsSync(source)) return;
|
|
130
|
+
const destination = path.resolve(process.cwd(), "public/fonts");
|
|
131
|
+
mkdirSync(destination, { recursive: true });
|
|
132
|
+
for (const name of readdirSync(source)) {
|
|
133
|
+
if (!/\.(?:woff2|txt)$/u.test(name)) continue;
|
|
134
|
+
const from = path.join(source, name);
|
|
135
|
+
const to = path.join(destination, name);
|
|
136
|
+
if (existsSync(to) && readFileSync(from).equals(readFileSync(to))) continue;
|
|
137
|
+
copyFileSync(from, to);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
126
141
|
// ---------------------------------------------------------------------------
|
|
127
142
|
// 集成入口
|
|
128
143
|
// ---------------------------------------------------------------------------
|
|
@@ -230,6 +245,7 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
230
245
|
// 破坏增量构建依赖图 hash)。Astro 不会合并集成的 public 目录,这里显式同步一次。
|
|
231
246
|
// 判别式:内容一致跳过(幂等),内容不同才覆盖,用户新增文件保留。
|
|
232
247
|
try {
|
|
248
|
+
syncFontAssets(srcDir);
|
|
233
249
|
const { copied, skipped } = syncBackgroundSvgs(srcDir);
|
|
234
250
|
if (copied > 0) {
|
|
235
251
|
assetsLogger.info(`同步背景 SVG ${copied} 个(跳过 ${skipped} 个不变)`);
|
|
@@ -386,53 +402,127 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
386
402
|
// Astro's Markdown image transform drops arbitrary lazy-loading hints
|
|
387
403
|
// added during HAST processing. Reapply the policy to emitted HTML.
|
|
388
404
|
const htmlFiles = await fs.readdir(outDir, { recursive: true });
|
|
405
|
+
const sourceDir = fileURLToPath(new URL(".", import.meta.url));
|
|
406
|
+
const resolveModule = createRequire(import.meta.url).resolve;
|
|
407
|
+
const parserEntry = resolveModule("parse5");
|
|
408
|
+
const nativeEntry = resolveModule("@xingwangzhe/cjk-font-split-native");
|
|
409
|
+
const outputCache = await createPageOutputCache(
|
|
410
|
+
path.resolve("node_modules/.astro/stalux-page-output"),
|
|
411
|
+
[
|
|
412
|
+
...[
|
|
413
|
+
"index.ts",
|
|
414
|
+
"internal/font-slices.ts",
|
|
415
|
+
"internal/image-dimensions.ts",
|
|
416
|
+
"internal/html-images.ts",
|
|
417
|
+
"internal/page-output-cache.ts",
|
|
418
|
+
"internal/style-names.generated.ts",
|
|
419
|
+
"plugins/feature-flags.ts",
|
|
420
|
+
"../package.json",
|
|
421
|
+
].map((entry) => path.join(sourceDir, entry)),
|
|
422
|
+
path.join(path.dirname(nativeEntry), "package.json"),
|
|
423
|
+
path.resolve(path.dirname(parserEntry), "../package.json"),
|
|
424
|
+
],
|
|
425
|
+
pageFontData?.fontBuffer,
|
|
426
|
+
htmlFiles
|
|
427
|
+
.filter((entry) => /\.(?:css|png|jpe?g|gif|webp)$/iu.test(entry))
|
|
428
|
+
.map((entry) => path.join(outDir, entry)),
|
|
429
|
+
);
|
|
430
|
+
let cacheHits = 0;
|
|
389
431
|
let optimizedImagePages = 0;
|
|
390
432
|
let pageFontPages = 0;
|
|
391
433
|
if (pageFontData) await fs.mkdir(pageFontData.outputDir, { recursive: true });
|
|
392
434
|
if (pageFontData) await fs.mkdir(pageFontData.cacheDir, { recursive: true });
|
|
393
435
|
const referencedPageFonts = new Set<string>();
|
|
394
436
|
const linkedCssTextCache = new Map<string, string>();
|
|
395
|
-
|
|
396
|
-
|
|
437
|
+
const dimensionsCache = new Map<string, Promise<ImageDimensions | undefined>>();
|
|
438
|
+
const pageEntries = htmlFiles.filter((entry) => entry.endsWith(".html"));
|
|
439
|
+
const processPage = async (entry: string) => {
|
|
397
440
|
const output = path.join(outDir, entry);
|
|
398
441
|
const before = await fs.readFile(output, "utf8");
|
|
399
442
|
const withoutPageFont = before.replace(
|
|
400
443
|
/<style data-stalux-page-font>[\s\S]*?<\/style>/gi,
|
|
401
444
|
"",
|
|
402
445
|
);
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
446
|
+
const cached = await outputCache.get(entry, withoutPageFont);
|
|
447
|
+
if (cached) {
|
|
448
|
+
if (cached.font && pageFontData) {
|
|
449
|
+
if (!referencedPageFonts.has(cached.font.filename)) {
|
|
450
|
+
copyFileSync(
|
|
451
|
+
cached.font.sourcePath,
|
|
452
|
+
path.join(pageFontData.outputDir, cached.font.filename),
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
referencedPageFonts.add(cached.font.filename);
|
|
456
|
+
pageFontPages++;
|
|
457
|
+
}
|
|
458
|
+
if (cached.html !== before) {
|
|
459
|
+
await fs.writeFile(output, cached.html);
|
|
460
|
+
optimizedImagePages++;
|
|
461
|
+
}
|
|
462
|
+
cacheHits++;
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
let cachedFont: { filename: string; sourcePath: string } | undefined;
|
|
466
|
+
const dependencies = new Set<string>();
|
|
467
|
+
const document = parse(withoutPageFont, { sourceCodeLocationInfo: true });
|
|
468
|
+
const images = findHtmlImages(withoutPageFont, document);
|
|
469
|
+
let after = await addLocalImageDimensions(withoutPageFont, outDir, {
|
|
470
|
+
images,
|
|
471
|
+
cache: dimensionsCache,
|
|
472
|
+
loadingPolicy: true,
|
|
473
|
+
dependencies,
|
|
474
|
+
});
|
|
408
475
|
if (pageFontData) {
|
|
409
476
|
const linkedStylesheetText = readLinkedStylesheetText(
|
|
410
477
|
after,
|
|
411
478
|
output,
|
|
412
479
|
outDir,
|
|
413
480
|
linkedCssTextCache,
|
|
481
|
+
document,
|
|
482
|
+
dependencies,
|
|
414
483
|
);
|
|
415
|
-
const pageFont =
|
|
484
|
+
const pageFont = await writePageFontSubsetAsync(
|
|
416
485
|
after,
|
|
417
486
|
pageFontData.fontBuffer,
|
|
418
487
|
pageFontData.cacheDir,
|
|
419
488
|
"/_astro/fonts/",
|
|
420
489
|
linkedStylesheetText,
|
|
490
|
+
"",
|
|
491
|
+
document,
|
|
421
492
|
);
|
|
422
493
|
if (pageFont) {
|
|
423
494
|
after = pageFont.html;
|
|
424
495
|
pageFontPages++;
|
|
496
|
+
cachedFont = {
|
|
497
|
+
filename: pageFont.filename,
|
|
498
|
+
sourcePath: pageFont.sourcePath,
|
|
499
|
+
};
|
|
500
|
+
if (!referencedPageFonts.has(pageFont.filename)) {
|
|
501
|
+
copyFileSync(
|
|
502
|
+
pageFont.sourcePath,
|
|
503
|
+
path.join(pageFontData.outputDir, pageFont.filename),
|
|
504
|
+
);
|
|
505
|
+
}
|
|
425
506
|
referencedPageFonts.add(pageFont.filename);
|
|
426
|
-
copyFileSync(
|
|
427
|
-
pageFont.sourcePath,
|
|
428
|
-
path.join(pageFontData.outputDir, pageFont.filename),
|
|
429
|
-
);
|
|
430
507
|
}
|
|
431
508
|
}
|
|
432
509
|
if (after !== before) {
|
|
433
510
|
await fs.writeFile(output, after);
|
|
434
511
|
optimizedImagePages++;
|
|
435
512
|
}
|
|
513
|
+
await outputCache.set(
|
|
514
|
+
entry,
|
|
515
|
+
withoutPageFont,
|
|
516
|
+
{
|
|
517
|
+
html: after,
|
|
518
|
+
font: cachedFont,
|
|
519
|
+
},
|
|
520
|
+
[...dependencies],
|
|
521
|
+
);
|
|
522
|
+
};
|
|
523
|
+
// Bound I/O concurrency without retaining the entire site's HTML in memory.
|
|
524
|
+
for (let offset = 0; offset < pageEntries.length; offset += 4) {
|
|
525
|
+
await Promise.all(pageEntries.slice(offset, offset + 4).map(processPage));
|
|
436
526
|
}
|
|
437
527
|
if (pageFontData) {
|
|
438
528
|
clearPageFontSubsets(pageFontData.outputDir, referencedPageFonts);
|
|
@@ -441,11 +531,15 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
441
531
|
logger.info(
|
|
442
532
|
`generated page-specific body font subsets for ${pageFontPages} HTML pages`,
|
|
443
533
|
);
|
|
534
|
+
logger.info(
|
|
535
|
+
`HTML postprocessing ${(performance.now() - started).toFixed(1)}ms; cache ${cacheHits}/${htmlFiles.filter((entry) => entry.endsWith(".html")).length}`,
|
|
536
|
+
);
|
|
444
537
|
|
|
445
538
|
// 后处理:Pagefind 搜索索引。启用时任何失败都必须让构建失败,
|
|
446
539
|
// 避免发布一个页面正常但搜索已损坏的产物。
|
|
447
540
|
if (opt.pagefind) {
|
|
448
541
|
logger.info("Running Pagefind indexer...");
|
|
542
|
+
const pagefindStarted = performance.now();
|
|
449
543
|
|
|
450
544
|
try {
|
|
451
545
|
const { index, errors } = await pagefindCreateIndex();
|
|
@@ -466,7 +560,7 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
466
560
|
});
|
|
467
561
|
|
|
468
562
|
logger.info(
|
|
469
|
-
`indexed ${page_count} pages → ${outputPath} (${(performance.now() -
|
|
563
|
+
`indexed ${page_count} pages → ${outputPath} (${(performance.now() - pagefindStarted).toFixed(1)}ms)`,
|
|
470
564
|
);
|
|
471
565
|
} catch (error) {
|
|
472
566
|
logger.error(`Pagefind indexing failed: ${describeError(error)}`);
|
|
@@ -18,7 +18,7 @@ import { styleNames } from "./style-names.generated.ts";
|
|
|
18
18
|
import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
19
19
|
import { dirname, join, resolve, sep } from "node:path";
|
|
20
20
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
21
|
-
import {
|
|
21
|
+
import { FontSubsetter } from "@xingwangzhe/cjk-font-split-native";
|
|
22
22
|
import type { AstroIntegrationLogger } from "astro";
|
|
23
23
|
import { type DefaultTreeAdapterMap, parse } from "parse5";
|
|
24
24
|
|
|
@@ -46,6 +46,8 @@ const PAGE_SLICE_OUT_DIR = "node_modules/.astro/stalux-page-fonts";
|
|
|
46
46
|
*/
|
|
47
47
|
export const RESERVED_GLYPHS = "0123456789";
|
|
48
48
|
|
|
49
|
+
const preparedFonts = new WeakMap<Buffer, FontSubsetter>();
|
|
50
|
+
|
|
49
51
|
// ---------------------------------------------------------------------------
|
|
50
52
|
// Types
|
|
51
53
|
// ---------------------------------------------------------------------------
|
|
@@ -164,10 +166,12 @@ export function readLinkedStylesheetText(
|
|
|
164
166
|
pagePath: string,
|
|
165
167
|
outputDir: string,
|
|
166
168
|
cache = new Map<string, string>(),
|
|
169
|
+
document = parse(html),
|
|
170
|
+
dependencies?: Set<string>,
|
|
167
171
|
): string {
|
|
168
172
|
const root = resolve(outputDir);
|
|
169
173
|
const links: string[] = [];
|
|
170
|
-
collectStylesheetLinks(
|
|
174
|
+
collectStylesheetLinks(document, links);
|
|
171
175
|
const content: string[] = [];
|
|
172
176
|
for (const href of links) {
|
|
173
177
|
if (/^(?:[a-z][a-z\d+.-]*:|\/\/|data:)/iu.test(href)) continue;
|
|
@@ -180,14 +184,15 @@ export function readLinkedStylesheetText(
|
|
|
180
184
|
continue;
|
|
181
185
|
}
|
|
182
186
|
if (cssPath !== root && !cssPath.startsWith(`${root}${sep}`)) continue;
|
|
187
|
+
dependencies?.add(cssPath);
|
|
183
188
|
if (!cache.has(cssPath)) {
|
|
184
189
|
try {
|
|
185
|
-
cache.set(cssPath, readFileSync(cssPath, "utf8"));
|
|
190
|
+
cache.set(cssPath, cssGeneratedText(readFileSync(cssPath, "utf8")));
|
|
186
191
|
} catch {
|
|
187
192
|
cache.set(cssPath, "");
|
|
188
193
|
}
|
|
189
194
|
}
|
|
190
|
-
content.push(
|
|
195
|
+
content.push(cache.get(cssPath) ?? "");
|
|
191
196
|
}
|
|
192
197
|
return content.join(" ");
|
|
193
198
|
}
|
|
@@ -256,8 +261,40 @@ export function writePageFontSubset(
|
|
|
256
261
|
publicUrlPrefix = "/_astro/fonts/",
|
|
257
262
|
linkedStylesheetText = "",
|
|
258
263
|
extraChars = "",
|
|
264
|
+
document = parse(html),
|
|
259
265
|
): PageFontSubset | undefined {
|
|
260
|
-
const
|
|
266
|
+
const input = preparePageFont(html, fontBuffer, linkedStylesheetText, extraChars, document);
|
|
267
|
+
if (!input) return undefined;
|
|
268
|
+
return renderPageFont(
|
|
269
|
+
html,
|
|
270
|
+
publicUrlPrefix,
|
|
271
|
+
input.chars,
|
|
272
|
+
input.prepared.subset(input.chars.join(""), cacheDir),
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function writePageFontSubsetAsync(
|
|
277
|
+
html: string,
|
|
278
|
+
fontBuffer: Buffer,
|
|
279
|
+
cacheDir: string,
|
|
280
|
+
publicUrlPrefix = "/_astro/fonts/",
|
|
281
|
+
linkedStylesheetText = "",
|
|
282
|
+
extraChars = "",
|
|
283
|
+
document = parse(html),
|
|
284
|
+
): Promise<PageFontSubset | undefined> {
|
|
285
|
+
const input = preparePageFont(html, fontBuffer, linkedStylesheetText, extraChars, document);
|
|
286
|
+
if (!input) return undefined;
|
|
287
|
+
const result = await input.prepared.subsetAsync(input.chars.join(""), cacheDir);
|
|
288
|
+
return renderPageFont(html, publicUrlPrefix, input.chars, result);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function preparePageFont(
|
|
292
|
+
html: string,
|
|
293
|
+
fontBuffer: Buffer,
|
|
294
|
+
linkedStylesheetText: string,
|
|
295
|
+
extraChars: string,
|
|
296
|
+
document: DefaultTreeAdapterMap["document"],
|
|
297
|
+
) {
|
|
261
298
|
const body = findBody(document);
|
|
262
299
|
if (!body) return undefined;
|
|
263
300
|
|
|
@@ -274,7 +311,20 @@ export function writePageFontSubset(
|
|
|
274
311
|
);
|
|
275
312
|
if (chars.length === 0) return undefined;
|
|
276
313
|
|
|
277
|
-
|
|
314
|
+
let prepared = preparedFonts.get(fontBuffer);
|
|
315
|
+
if (!prepared) {
|
|
316
|
+
prepared = new FontSubsetter(fontBuffer);
|
|
317
|
+
preparedFonts.set(fontBuffer, prepared);
|
|
318
|
+
}
|
|
319
|
+
return { chars, prepared };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function renderPageFont(
|
|
323
|
+
html: string,
|
|
324
|
+
publicUrlPrefix: string,
|
|
325
|
+
chars: string[],
|
|
326
|
+
result: { hash: string; path: string; cacheHit: boolean },
|
|
327
|
+
): PageFontSubset {
|
|
278
328
|
const filename = `page-${result.hash}.woff2`;
|
|
279
329
|
|
|
280
330
|
const rel = `${publicUrlPrefix}${filename}`;
|
|
@@ -10,7 +10,10 @@ export interface HtmlImage {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/** Source ranges preserve scripts and code-copy attributes byte-for-byte. */
|
|
13
|
-
export function findHtmlImages(
|
|
13
|
+
export function findHtmlImages(
|
|
14
|
+
html: string,
|
|
15
|
+
document = parse(html, { sourceCodeLocationInfo: true }),
|
|
16
|
+
): HtmlImage[] {
|
|
14
17
|
const images: HtmlImage[] = [];
|
|
15
18
|
function visit(node: DefaultTreeAdapterMap["node"], body?: number) {
|
|
16
19
|
if ("tagName" in node) {
|
|
@@ -35,10 +38,28 @@ export function findHtmlImages(html: string): HtmlImage[] {
|
|
|
35
38
|
}
|
|
36
39
|
if ("childNodes" in node) for (const child of node.childNodes) visit(child, body);
|
|
37
40
|
}
|
|
38
|
-
visit(
|
|
41
|
+
visit(document);
|
|
39
42
|
return images.sort((left, right) => left.start - right.start);
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
export function imageLoadingUpdates(images: HtmlImage[]): Map<HtmlImage, Record<string, string>> {
|
|
46
|
+
const firstSeen = new Set<number>();
|
|
47
|
+
const changes = new Map<HtmlImage, Record<string, string>>();
|
|
48
|
+
for (const image of images) {
|
|
49
|
+
if (image.body === undefined) continue;
|
|
50
|
+
const first = !firstSeen.has(image.body);
|
|
51
|
+
firstSeen.add(image.body);
|
|
52
|
+
const updates: Record<string, string> = {
|
|
53
|
+
decoding: image.attributes.get("decoding") || "async",
|
|
54
|
+
loading: first ? "eager" : "lazy",
|
|
55
|
+
};
|
|
56
|
+
if (first) updates.fetchpriority = "high";
|
|
57
|
+
else if (image.attributes.get("fetchpriority") === "high") updates.fetchpriority = "auto";
|
|
58
|
+
changes.set(image, updates);
|
|
59
|
+
}
|
|
60
|
+
return changes;
|
|
61
|
+
}
|
|
62
|
+
|
|
42
63
|
export function updateImageTag(image: HtmlImage, updates: Record<string, string>): string {
|
|
43
64
|
let tag = image.tag;
|
|
44
65
|
const edits: Array<{ start: number; end: number; value: string }> = [];
|