@xingwangzhe/stalux 1.28.1 → 1.28.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xingwangzhe/stalux",
3
- "version": "1.28.1",
3
+ "version": "1.28.2",
4
4
  "description": "A powerful, modern Astro blog theme — use as template or install as plugin",
5
5
  "keywords": [
6
6
  "astro",
@@ -101,7 +101,7 @@
101
101
  "@mcp-b/webmcp-polyfill": "^5.1.0",
102
102
  "@pagefind/component-ui": "^1.5.2",
103
103
  "@waline/client": "^3.15.2",
104
- "@xingwangzhe/cjk-font-split-native": "0.1.0",
104
+ "@xingwangzhe/cjk-font-split-native": "0.2.0",
105
105
  "@xingwangzhe/satteri-mermaid": "0.7.8",
106
106
  "@xingwangzhe/satteri-photoswipe": "^0.2.5",
107
107
  "@xingwangzhe/tags-cloud": "^1.2.8",
package/src/index.ts CHANGED
@@ -32,6 +32,7 @@ import { expressiveCode } from "./expressive-code";
32
32
  import { staluxComponentsAlias } from "./internal/components-plugin";
33
33
  import {
34
34
  clearPageFontSubsets,
35
+ readLinkedStylesheetText,
35
36
  resolveFontInputs,
36
37
  writePageFontSubset,
37
38
  } from "./internal/font-slices";
@@ -366,6 +367,7 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
366
367
  if (pageFontData) await fs.mkdir(pageFontData.outputDir, { recursive: true });
367
368
  if (pageFontData) await fs.mkdir(pageFontData.cacheDir, { recursive: true });
368
369
  const referencedPageFonts = new Set<string>();
370
+ const linkedCssTextCache = new Map<string, string>();
369
371
  for (const entry of htmlFiles) {
370
372
  if (typeof entry !== "string" || !entry.endsWith(".html")) continue;
371
373
  const output = path.join(outDir, entry);
@@ -376,11 +378,18 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
376
378
  );
377
379
  let after = applyHtmlImageLoadingPolicy(withoutPageFont);
378
380
  if (pageFontData) {
381
+ const linkedStylesheetText = readLinkedStylesheetText(
382
+ after,
383
+ output,
384
+ outDir,
385
+ linkedCssTextCache,
386
+ );
379
387
  const pageFont = writePageFontSubset(
380
388
  after,
381
389
  pageFontData.fontBuffer,
382
390
  pageFontData.cacheDir,
383
391
  "/_astro/fonts/",
392
+ linkedStylesheetText,
384
393
  );
385
394
  if (pageFont) {
386
395
  after = pageFont.html;
@@ -15,8 +15,8 @@
15
15
  */
16
16
 
17
17
  import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
18
- import { dirname, join, resolve } from "node:path";
19
- import { fileURLToPath } from "node:url";
18
+ import { dirname, join, resolve, sep } from "node:path";
19
+ import { fileURLToPath, pathToFileURL } from "node:url";
20
20
  import { subsetFont } from "@xingwangzhe/cjk-font-split-native";
21
21
  import type { AstroIntegrationLogger } from "astro";
22
22
  import { type DefaultTreeAdapterMap, parse } from "parse5";
@@ -122,6 +122,63 @@ const VISUALLY_HIDDEN_CLASSES = new Set([
122
122
  "visually-hidden",
123
123
  ]);
124
124
 
125
+ function cssGeneratedText(css: string): string {
126
+ return [...css.matchAll(/\bcontent\s*:\s*(["'])(.*?)\1\s*(?:!important\s*)?(?:;|})/gis)]
127
+ .map(([, , value]) =>
128
+ (value ?? "").replace(/\\([\da-f]{1,6})\s?|\\(.)/gi, (_, hex, char) =>
129
+ hex ? String.fromCodePoint(Number.parseInt(hex, 16)) : char,
130
+ ),
131
+ )
132
+ .join(" ");
133
+ }
134
+
135
+ function collectStylesheetLinks(node: HtmlNode, output: string[]): void {
136
+ if ("tagName" in node && node.tagName === "link") {
137
+ const attrs = new Map(node.attrs.map(({ name, value }) => [name, value]));
138
+ if (attrs.get("rel")?.split(/\s+/u).includes("stylesheet")) {
139
+ const href = attrs.get("href");
140
+ if (href) output.push(href);
141
+ }
142
+ }
143
+ if ("childNodes" in node) {
144
+ for (const child of node.childNodes) collectStylesheetLinks(child, output);
145
+ }
146
+ }
147
+
148
+ /** Read local CSS files linked from an emitted page and return generated text strings. */
149
+ export function readLinkedStylesheetText(
150
+ html: string,
151
+ pagePath: string,
152
+ outputDir: string,
153
+ cache = new Map<string, string>(),
154
+ ): string {
155
+ const root = resolve(outputDir);
156
+ const links: string[] = [];
157
+ collectStylesheetLinks(parse(html), links);
158
+ const content: string[] = [];
159
+ for (const href of links) {
160
+ if (/^(?:[a-z][a-z\d+.-]*:|\/\/|data:)/iu.test(href)) continue;
161
+ let cssPath: string;
162
+ try {
163
+ cssPath = href.startsWith("/")
164
+ ? resolve(root, `.${new URL(href, "https://stalux.local").pathname}`)
165
+ : fileURLToPath(new URL(href, pathToFileURL(pagePath)));
166
+ } catch {
167
+ continue;
168
+ }
169
+ if (cssPath !== root && !cssPath.startsWith(`${root}${sep}`)) continue;
170
+ if (!cache.has(cssPath)) {
171
+ try {
172
+ cache.set(cssPath, readFileSync(cssPath, "utf8"));
173
+ } catch {
174
+ cache.set(cssPath, "");
175
+ }
176
+ }
177
+ content.push(cssGeneratedText(cache.get(cssPath) ?? ""));
178
+ }
179
+ return content.join(" ");
180
+ }
181
+
125
182
  function findBody(node: HtmlNode): HtmlNode | undefined {
126
183
  if ("tagName" in node && node.tagName === "body") return node;
127
184
  if ("childNodes" in node) {
@@ -164,31 +221,45 @@ function collectBodyText(node: HtmlNode, output: string[]): void {
164
221
  for (const child of node.childNodes) collectBodyText(child, output);
165
222
  }
166
223
 
167
- /** Create a cached body-font subset containing precisely the page's CJK code points. */
224
+ function formatUnicodeRange(chars: string[]): string {
225
+ const points = chars.map((char) => char.codePointAt(0) as number).sort((a, b) => a - b);
226
+ const ranges: Array<[number, number]> = [];
227
+ for (const point of points) {
228
+ const last = ranges.at(-1);
229
+ if (last && point === last[1] + 1) last[1] = point;
230
+ else ranges.push([point, point]);
231
+ }
232
+ const format = (point: number) => point.toString(16).toUpperCase().padStart(4, "0");
233
+ return ranges
234
+ .map(([start, end]) => `U+${format(start)}${start === end ? "" : `-${format(end)}`}`)
235
+ .join(",");
236
+ }
237
+
238
+ /** Create a cached body-font subset containing the page's visible text characters. */
168
239
  export function writePageFontSubset(
169
240
  html: string,
170
241
  fontBuffer: Buffer,
171
242
  cacheDir: string,
172
243
  publicUrlPrefix = "/_astro/fonts/",
244
+ linkedStylesheetText = "",
173
245
  ): PageFontSubset | undefined {
174
246
  const document = parse(html);
175
247
  const body = findBody(document);
176
248
  if (!body) return undefined;
177
249
 
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
- ),
250
+ const bodyText: string[] = [
251
+ ...[...html.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi)].map(([, css]) =>
252
+ cssGeneratedText(css ?? ""),
185
253
  ),
186
- ]
187
- .sort()
188
- .join("");
189
- if (!chars) return undefined;
254
+ linkedStylesheetText,
255
+ ];
256
+ collectBodyText(body, bodyText);
257
+ const chars = [...new Set([...bodyText.join("")].filter((char) => !/\p{C}/u.test(char)))].sort(
258
+ (a, b) => (a.codePointAt(0) as number) - (b.codePointAt(0) as number),
259
+ );
260
+ if (chars.length === 0) return undefined;
190
261
 
191
- const result = subsetFont(fontBuffer, chars, cacheDir);
262
+ const result = subsetFont(fontBuffer, chars.join(""), cacheDir);
192
263
  const filename = `page-${result.hash}.woff2`;
193
264
 
194
265
  const rel = `${publicUrlPrefix}${filename}`;
@@ -203,7 +274,7 @@ export function writePageFontSubset(
203
274
  : `"LXGW WenKai-Page Subset",${fallbackStack}`;
204
275
  // The source font is regular; advertise a weight range so browsers synthesize
205
276
  // bold weights from these same glyphs instead of falling back to system CJK.
206
- const css = `@font-face{font-family:"LXGW WenKai-Page Subset";src:url("${rel}") format("woff2");font-style:normal;font-weight:100 900;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}}body{font-family:var(--font-body),${fallbackStack}}`;
277
+ const css = `@font-face{font-family:"LXGW WenKai-Page Subset";src:url("${rel}") format("woff2");font-style:normal;font-weight:100 900;font-display:swap;unicode-range:${formatUnicodeRange(chars)}}:root{--font-body:"LXGW WenKai-Page Subset",${fallbackStack};--font-code:${codeStack}}body{font-family:var(--font-body),${fallbackStack}}`;
207
278
  const withoutBroadBodyFonts = html.replace(
208
279
  /@font-face\{[^}]*font-family:"LXGW WenKai-[^"]+"[^}]*\}/g,
209
280
  (face) => {