@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.
@@ -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
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
- writePageFontSubset,
43
+ writePageFontSubsetAsync,
42
44
  } from "./internal/font-slices";
43
- import { addLocalImageDimensions } from "./internal/image-dimensions";
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
- for (const entry of htmlFiles) {
396
- if (typeof entry !== "string" || !entry.endsWith(".html")) continue;
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 withImageDimensions = await addLocalImageDimensions(
404
- withoutPageFont,
405
- outDir,
406
- );
407
- let after = applyHtmlImageLoadingPolicy(withImageDimensions);
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 = writePageFontSubset(
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() - started).toFixed(1)}ms)`,
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 { subsetFont } from "@xingwangzhe/cjk-font-split-native";
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(parse(html), links);
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(cssGeneratedText(cache.get(cssPath) ?? ""));
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 document = parse(html);
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
- const result = subsetFont(fontBuffer, chars.join(""), cacheDir);
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(html: string): HtmlImage[] {
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(parse(html, { sourceCodeLocationInfo: true }));
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 }> = [];