@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 +2 -2
- package/src/index.ts +9 -0
- package/src/internal/font-slices.ts +87 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xingwangzhe/stalux",
|
|
3
|
-
"version": "1.28.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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:${
|
|
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) => {
|