@xingwangzhe/stalux 1.34.0 → 1.35.1
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 +9 -0
- package/package.json +8 -3
- package/src/components/stalux/common/Typewriter.astro +2 -2
- package/src/components/stalux/tags/tagsCloud.astro +1 -0
- package/src/index.ts +15 -1
- package/src/internal/compact-styles.ts +322 -0
- package/src/internal/font-slices.ts +2 -0
- package/src/internal/modern-css.ts +7 -0
- package/src/internal/runtime-cache-key.ts +2 -0
- package/src/internal/style-artifacts.ts +94 -0
- package/src/internal/style-name-lookup.ts +4 -0
- package/src/internal/style-names.generated.ts +353 -0
- package/src/layouts/Stalux.astro +7 -1
- package/src/pages/index.astro +1 -1
- package/src/scripts/background.ts +1 -1
- package/src/scripts/view-transitions.ts +1 -1
- package/src/styles/base.css +54 -59
- package/src/styles/content.css +4 -9
- package/src/styles/generated-article.css +1 -1
- package/src/styles/generated-links.css +1 -1
- package/src/styles/generated-prose.css +1 -1
- package/src/styles/generated-waline.css +1 -1
- package/src/styles/generated-words.css +1 -1
- package/src/styles/generated.css +1 -1
- package/src/styles/integrations.css +44 -45
- package/src/utils/accent-color.ts +5 -2
- package/src/utils/style-name.ts +15 -0
- package/src/utils/view-transitions.ts +4 -3
package/README.md
CHANGED
|
@@ -93,6 +93,7 @@ bun run dev
|
|
|
93
93
|
- 🗺️ **Sitemap** (bundled; `.md` source endpoints auto-filtered)
|
|
94
94
|
- 🖼️ **PhotoSwipe** image lightbox
|
|
95
95
|
- 📊 **Mermaid** diagrams and flowcharts
|
|
96
|
+
Uses Merman through `@xingwangzhe/satteri-mermaid@0.8.1`, with the `dark` theme and a transparent canvas by default. The plugin supports 11 theme presets; see the [configuration reference](https://github.com/xingwangzhe/satteri-mermaid/blob/v0.8.1/docs/configuration.md). Rendering failures throw by default.
|
|
96
97
|
- 📐 **Math formula rendering** (Temml → MathML)
|
|
97
98
|
- 💬 **Waline** comment system
|
|
98
99
|
- 🤖 **LLM discovery files** (llms.txt / llms-full.txt)
|
|
@@ -301,3 +302,11 @@ Article bodies and summary cards are up to 840px wide. Summaries use the existin
|
|
|
301
302
|
Visitor weather is optional: set `weather: { enabled: true }` in `stalux/config/site.yml` to show it as plain text in the footer (with visitor IP and approximate location) and below the desktop article clock. Omitted or `false` disables it. It automatically requests an approximate city from IP.SB (with IPinfo as fallback) and weather from Open-Meteo, only while visible; no browser GPS permission is requested. These external services receive the visitor IP, and Open-Meteo receives rounded coordinates. A 20-minute session cache stores the city, visitor IP and weather, without coordinates. Visitors behind VPNs may see the exit city. Hidden tabs, offscreen cards and Astro navigation stop work; unavailable services show a retry action and retain previous weather. Open-Meteo's free endpoint is for non-commercial use subject to its limits; the card links to its attribution licence. No server endpoint or extra package is needed.
|
|
302
303
|
|
|
303
304
|
Archives keep a transparent, single-column year/month timeline with all articles expanded. Each month gently fades and moves into place once when it enters the viewport (280ms), using native browser APIs without extra dependencies. Reduced motion, printing, missing browser APIs and disabled JavaScript retain the complete static list. Mobile dates sit above wrapping article titles. No new configuration is required. Article titles retain a single hover underline; hovering or keyboard-focusing a title previews its existing `desc` on the right, falling below near viewport edges. Empty descriptions show no preview, and touch navigation stays direct. The preview uses the browser top layer (with a high stacking-level fallback) to avoid clipping or overlap.
|
|
305
|
+
|
|
306
|
+
### Production style compaction (1.35.0)
|
|
307
|
+
|
|
308
|
+
Development keeps readable source names. Production builds compact private Stalux classes, custom properties and keyframes before Astro compilation and asset hashing, using a version-local mapping shipped with the package. Templates, client selectors, inline styles and dynamic server-generated animations share the mapping; its contents and transformer source participate in incremental cache invalidation.
|
|
309
|
+
|
|
310
|
+
Stable `data-*` hooks, article anchors, public font properties (`--font-body`, `--font-code`, plus the legacy override hooks `--accent` and `--foreground-60p`), third-party interfaces and user content keep their meaning. Use your own classes and public component interfaces for extensions; the generated `s*`, `--s*` and `k*` names are reserved for the theme. Existing CSS import paths remain available and consumers need no additional configuration or UnoCSS installation.
|
|
311
|
+
|
|
312
|
+
Theme CSS uses native nesting, modern RGB syntax, `overflow-wrap: anywhere` and clipped one-pixel accessible content with `clip-path`. Lightning CSS targets modern browser engines instead of generating obsolete compatibility expansions. Necessary multiline-clamping and third-party MathML browser adaptations remain. Production source maps are disabled to avoid exposing the original names. Build assertions check semantic CSS/HTML/JS surfaces and resource integrity without modifying article text or code examples; detector results always identify the rule or scanner version used.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xingwangzhe/stalux",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.35.1",
|
|
4
4
|
"description": "A powerful, modern Astro blog theme — use as template or install as plugin",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -91,26 +91,30 @@
|
|
|
91
91
|
"prepare": "git config core.hooksPath scripts/git-hooks",
|
|
92
92
|
"dev:debug": "STALUX_DEBUG=1 astro dev --verbose",
|
|
93
93
|
"build:debug": "STALUX_DEBUG=1 astro build --verbose && node scripts/verify-build.ts",
|
|
94
|
-
"styles:build": "node scripts/build-styles.ts",
|
|
94
|
+
"styles:build": "node scripts/build-styles.ts && node scripts/build-style-names.ts",
|
|
95
95
|
"styles:watch": "node scripts/build-styles.ts --watch"
|
|
96
96
|
},
|
|
97
97
|
"dependencies": {
|
|
98
|
+
"@astrojs/compiler": "2.13.1",
|
|
98
99
|
"@astrojs/markdown-satteri": "0.4.2",
|
|
99
100
|
"@astrojs/partytown": "2.1.8",
|
|
100
101
|
"@astrojs/rss": "^4.0.19",
|
|
101
102
|
"@astrojs/sitemap": "^3.7.4",
|
|
103
|
+
"@babel/parser": "7.29.9",
|
|
102
104
|
"@expressive-code/plugin-line-numbers": "^0.44.2",
|
|
103
105
|
"@lucide/astro": "^1.47.0",
|
|
104
106
|
"@mcp-b/webmcp-polyfill": "^5.1.0",
|
|
105
107
|
"@pagefind/component-ui": "^1.5.2",
|
|
106
108
|
"@waline/client": "3.16.0",
|
|
107
109
|
"@xingwangzhe/cjk-font-split-native": "0.2.0",
|
|
108
|
-
"@xingwangzhe/satteri-mermaid": "0.
|
|
110
|
+
"@xingwangzhe/satteri-mermaid": "0.8.1",
|
|
109
111
|
"@xingwangzhe/satteri-photoswipe": "^0.2.5",
|
|
110
112
|
"@xingwangzhe/tags-cloud": "^1.2.8",
|
|
111
113
|
"astro-expressive-code": "^0.44.2",
|
|
112
114
|
"astro-seo": "^1.2.0",
|
|
113
115
|
"badge-maker": "^6.0.0",
|
|
116
|
+
"css-tree": "3.2.1",
|
|
117
|
+
"magic-string": "0.30.21",
|
|
114
118
|
"pagefind": "^1.5.2",
|
|
115
119
|
"parse5": "8.0.1",
|
|
116
120
|
"photoswipe": "^5.4.4",
|
|
@@ -122,6 +126,7 @@
|
|
|
122
126
|
"devDependencies": {
|
|
123
127
|
"@astrojs/check": "0.9.10",
|
|
124
128
|
"@biomejs/biome": "2.5.14",
|
|
129
|
+
"@types/css-tree": "^3.2.0",
|
|
125
130
|
"@types/node": "^26.6.2",
|
|
126
131
|
"@unocss/astro": "^66.10.5",
|
|
127
132
|
"@vitest/coverage-v8": "5.0.1",
|
|
@@ -33,7 +33,7 @@ registerPageLifecycle("typewriter", mountTypewriters);
|
|
|
33
33
|
padding: 0;
|
|
34
34
|
margin: -1px;
|
|
35
35
|
overflow: hidden;
|
|
36
|
-
clip:
|
|
36
|
+
clip-path: inset(50%);
|
|
37
37
|
white-space: nowrap;
|
|
38
38
|
border: 0;
|
|
39
39
|
}
|
|
@@ -80,7 +80,7 @@ registerPageLifecycle("typewriter", mountTypewriters);
|
|
|
80
80
|
height: auto;
|
|
81
81
|
margin: 0;
|
|
82
82
|
overflow: visible;
|
|
83
|
-
clip:
|
|
83
|
+
clip-path: none;
|
|
84
84
|
white-space: normal;
|
|
85
85
|
}
|
|
86
86
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { compactStylesPlugin } from "./internal/compact-styles.ts";
|
|
2
|
+
import { modernCssTargets } from "./internal/modern-css.ts";
|
|
3
|
+
import { styleNames } from "./internal/style-names.generated.ts";
|
|
1
4
|
/**
|
|
2
5
|
* Stalux — Astro 博客主题集成入口
|
|
3
6
|
*
|
|
@@ -162,6 +165,7 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
162
165
|
updateConfig,
|
|
163
166
|
addDevToolbarApp,
|
|
164
167
|
config,
|
|
168
|
+
command,
|
|
165
169
|
logger,
|
|
166
170
|
}) => {
|
|
167
171
|
const started = performance.now();
|
|
@@ -176,6 +180,8 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
176
180
|
path.join(srcDir, "scripts"),
|
|
177
181
|
path.resolve(srcDir, "../package.json"),
|
|
178
182
|
path.join(srcDir, "styles"),
|
|
183
|
+
JSON.stringify([styleNames, modernCssTargets]) +
|
|
184
|
+
readFileSync(path.join(srcDir, "internal/compact-styles.ts"), "utf8"),
|
|
179
185
|
);
|
|
180
186
|
|
|
181
187
|
// 1. 注入 Vite 别名 + 组件覆盖插件 + Vue 特性标记(Waline 依赖)
|
|
@@ -187,14 +193,21 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
187
193
|
config.build.inlineStylesheets === "always" ? "always" : "never",
|
|
188
194
|
},
|
|
189
195
|
vite: {
|
|
190
|
-
|
|
196
|
+
css: { lightningcss: { targets: modernCssTargets } },
|
|
197
|
+
build: {
|
|
198
|
+
target: config.vite.build?.target ?? "esnext",
|
|
199
|
+
cssMinify: "lightningcss",
|
|
200
|
+
sourcemap: false,
|
|
201
|
+
},
|
|
191
202
|
resolve: {
|
|
192
203
|
alias: createViteAliases(srcDir),
|
|
193
204
|
},
|
|
194
205
|
plugins: [
|
|
206
|
+
...(command === "build" ? [compactStylesPlugin(srcDir)] : []),
|
|
195
207
|
staluxComponentsAlias(opt.components, logger.fork("stalux/components")),
|
|
196
208
|
],
|
|
197
209
|
define: {
|
|
210
|
+
__STALUX_COMPACT_STYLES__: JSON.stringify(command === "build"),
|
|
198
211
|
__STALUX_DEBUG__: JSON.stringify(
|
|
199
212
|
process.env.STALUX_DEBUG === "1" ||
|
|
200
213
|
process.argv.includes("--verbose"),
|
|
@@ -278,6 +291,7 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
278
291
|
mermaidHast({
|
|
279
292
|
responsive: true,
|
|
280
293
|
theme: "dark",
|
|
294
|
+
themeVariables: { background: "transparent" },
|
|
281
295
|
themeOverrides: { clusterBorder: "#cccccc" },
|
|
282
296
|
}),
|
|
283
297
|
seen,
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { parse as parseAstro } from "@astrojs/compiler";
|
|
3
|
+
import { parseExpression, parse as parseScript } from "@babel/parser";
|
|
4
|
+
import { generate, ident, parse, walk } from "css-tree";
|
|
5
|
+
import MagicString from "magic-string";
|
|
6
|
+
import { parseFragment } from "parse5";
|
|
7
|
+
import type { Plugin } from "vite";
|
|
8
|
+
import { styleNameIn } from "./style-name-lookup.ts";
|
|
9
|
+
import { styleNames } from "./style-names.generated.ts";
|
|
10
|
+
|
|
11
|
+
type Names = typeof styleNames;
|
|
12
|
+
|
|
13
|
+
/** Rename parsed CSS tokens, preserving strings, URLs and unrelated third-party names. */
|
|
14
|
+
export function compactCss(css: string, names: Names = styleNames, inline = false): string {
|
|
15
|
+
const ast = parse(css, {
|
|
16
|
+
context: inline ? "declarationList" : "stylesheet",
|
|
17
|
+
parseCustomProperty: true,
|
|
18
|
+
});
|
|
19
|
+
walk(ast, function (node) {
|
|
20
|
+
if (node.type === "ClassSelector")
|
|
21
|
+
node.name = styleNameIn(names.classes, ident.decode(node.name)) ?? node.name;
|
|
22
|
+
if (node.type === "Declaration")
|
|
23
|
+
node.property = styleNameIn(names.variables, node.property) ?? node.property;
|
|
24
|
+
if (node.type === "Identifier") {
|
|
25
|
+
node.name =
|
|
26
|
+
styleNameIn(names.variables, node.name) ??
|
|
27
|
+
(this.atrule?.name === "keyframes" ||
|
|
28
|
+
this.declaration?.property === "animation" ||
|
|
29
|
+
this.declaration?.property === "animation-name"
|
|
30
|
+
? styleNameIn(names.animations, node.name)
|
|
31
|
+
: undefined) ??
|
|
32
|
+
node.name;
|
|
33
|
+
}
|
|
34
|
+
if (node.type === "AttributeSelector" && node.name.name === "class" && node.value) {
|
|
35
|
+
if (node.matcher !== "=" && node.matcher !== "~=")
|
|
36
|
+
throw new Error(
|
|
37
|
+
"Theme class substring selectors must use explicit classes before compaction",
|
|
38
|
+
);
|
|
39
|
+
if (node.value.type === "String")
|
|
40
|
+
node.value.value = compactTokens(node.value.value, names);
|
|
41
|
+
if (node.value.type === "Identifier")
|
|
42
|
+
node.value.name = styleNameIn(names.classes, node.value.name) ?? node.value.name;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return generate(ast);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function compactTokens(value: string, names: Names): string {
|
|
49
|
+
// Selectors have two namespaces: class names are private, data attribute values are stable.
|
|
50
|
+
if (value.includes("[data-")) {
|
|
51
|
+
try {
|
|
52
|
+
const selector = parse(value, { context: "selectorList" });
|
|
53
|
+
walk(selector, (node) => {
|
|
54
|
+
if (node.type === "ClassSelector")
|
|
55
|
+
node.name = styleNameIn(names.classes, ident.decode(node.name)) ?? node.name;
|
|
56
|
+
});
|
|
57
|
+
return generate(selector);
|
|
58
|
+
} catch {
|
|
59
|
+
// A template quasi may end inside an attribute selector. Its data values stay literal.
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return value.replace(
|
|
64
|
+
/--[\w-]+|[a-zA-Z_][\w-]*/gu,
|
|
65
|
+
(name) =>
|
|
66
|
+
styleNameIn(names.classes, name) ??
|
|
67
|
+
styleNameIn(names.variables, name) ??
|
|
68
|
+
styleNameIn(names.animations, name) ??
|
|
69
|
+
name,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type Edit = { start: number; end: number; text: string };
|
|
74
|
+
|
|
75
|
+
/** Parse real markup attributes; leave text and code examples untouched. */
|
|
76
|
+
function markupEdits(value: string, names: Names): Edit[] {
|
|
77
|
+
const edits: Edit[] = [];
|
|
78
|
+
const ast = parseFragment(value, { sourceCodeLocationInfo: true });
|
|
79
|
+
function visit(node: typeof ast | (typeof ast.childNodes)[number]) {
|
|
80
|
+
if ("tagName" in node) {
|
|
81
|
+
if (["pre", "code", "script", "style"].includes(node.tagName)) return;
|
|
82
|
+
for (const attr of node.attrs) {
|
|
83
|
+
const loc = node.sourceCodeLocation?.attrs?.[attr.name];
|
|
84
|
+
if (!loc || !["class", "style", "data-toc-active-class"].includes(attr.name))
|
|
85
|
+
continue;
|
|
86
|
+
const raw = value.slice(loc.startOffset, loc.endOffset);
|
|
87
|
+
const offset = loc.startOffset + raw.indexOf("=") + 1;
|
|
88
|
+
const body = value.slice(offset, loc.endOffset);
|
|
89
|
+
if (attr.name === "style") {
|
|
90
|
+
const leading = body.match(/^\s*(["'])?/u)?.[0] ?? "";
|
|
91
|
+
const quote = leading.at(-1);
|
|
92
|
+
const css = body.slice(
|
|
93
|
+
leading.length,
|
|
94
|
+
quote === '"' || quote === "'" ? -1 : undefined,
|
|
95
|
+
);
|
|
96
|
+
const cssAst = parse(css, {
|
|
97
|
+
context: "declarationList",
|
|
98
|
+
positions: true,
|
|
99
|
+
parseCustomProperty: true,
|
|
100
|
+
});
|
|
101
|
+
walk(cssAst, function (token) {
|
|
102
|
+
if (!token.loc) return;
|
|
103
|
+
let next: string | undefined;
|
|
104
|
+
let end = token.loc.end.offset;
|
|
105
|
+
if (token.type === "Declaration") {
|
|
106
|
+
next = styleNameIn(names.variables, token.property);
|
|
107
|
+
end = token.loc.start.offset + token.property.length;
|
|
108
|
+
}
|
|
109
|
+
if (token.type === "Identifier")
|
|
110
|
+
next =
|
|
111
|
+
styleNameIn(names.variables, token.name) ??
|
|
112
|
+
(["animation", "animation-name"].includes(
|
|
113
|
+
this.declaration?.property ?? "",
|
|
114
|
+
)
|
|
115
|
+
? styleNameIn(names.animations, token.name)
|
|
116
|
+
: undefined);
|
|
117
|
+
if (next)
|
|
118
|
+
edits.push({
|
|
119
|
+
start: offset + leading.length + token.loc.start.offset,
|
|
120
|
+
end: offset + leading.length + end,
|
|
121
|
+
text: next,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
} else
|
|
125
|
+
for (const match of body.matchAll(/[a-zA-Z_][\w-]*/gu)) {
|
|
126
|
+
const next = styleNameIn(names.classes, match[0]);
|
|
127
|
+
if (next)
|
|
128
|
+
edits.push({
|
|
129
|
+
start: offset + match.index,
|
|
130
|
+
end: offset + match.index + match[0].length,
|
|
131
|
+
text: next,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if ("childNodes" in node) for (const child of node.childNodes) visit(child);
|
|
137
|
+
}
|
|
138
|
+
visit(ast);
|
|
139
|
+
return edits;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function compactMarkup(value: string, names: Names): string {
|
|
143
|
+
const output = new MagicString(value);
|
|
144
|
+
for (const edit of markupEdits(value, names)) output.overwrite(edit.start, edit.end, edit.text);
|
|
145
|
+
return output.toString();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function visitSyntax(
|
|
149
|
+
value: unknown,
|
|
150
|
+
callback: (node: Record<string, unknown>) => boolean | undefined,
|
|
151
|
+
): void {
|
|
152
|
+
if (!value || typeof value !== "object") return;
|
|
153
|
+
if (Array.isArray(value)) {
|
|
154
|
+
for (const entry of value) visitSyntax(entry, callback);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const node = value as Record<string, unknown>;
|
|
158
|
+
if (typeof node.type === "string" && callback(node) === false) return;
|
|
159
|
+
for (const [key, child] of Object.entries(node))
|
|
160
|
+
if (key !== "loc" && key !== "extra" && key !== "comments") visitSyntax(child, callback);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Parse source literals; never rewrite generated bundles or consumer modules. */
|
|
164
|
+
export function compactScript(code: string, names: Names = styleNames, expression = false): string {
|
|
165
|
+
const ast = expression
|
|
166
|
+
? parseExpression(code, { plugins: ["typescript"] })
|
|
167
|
+
: parseScript(code, { sourceType: "module", plugins: ["typescript"] });
|
|
168
|
+
const output = new MagicString(code);
|
|
169
|
+
visitSyntax(ast, (node) => {
|
|
170
|
+
if (
|
|
171
|
+
node.type === "ImportDeclaration" ||
|
|
172
|
+
node.type === "ImportExpression" ||
|
|
173
|
+
node.type === "ExportAllDeclaration"
|
|
174
|
+
)
|
|
175
|
+
return false;
|
|
176
|
+
if (
|
|
177
|
+
node.type === "StringLiteral" &&
|
|
178
|
+
typeof node.value === "string" &&
|
|
179
|
+
typeof node.start === "number" &&
|
|
180
|
+
typeof node.end === "number"
|
|
181
|
+
) {
|
|
182
|
+
const next = node.value.includes("<")
|
|
183
|
+
? compactMarkup(node.value, names)
|
|
184
|
+
: compactTokens(node.value, names);
|
|
185
|
+
if (next !== node.value) output.overwrite(node.start, node.end, JSON.stringify(next));
|
|
186
|
+
}
|
|
187
|
+
if (
|
|
188
|
+
node.type === "TemplateLiteral" &&
|
|
189
|
+
typeof node.start === "number" &&
|
|
190
|
+
typeof node.end === "number"
|
|
191
|
+
) {
|
|
192
|
+
const segments = node.quasis as Array<{
|
|
193
|
+
start: number;
|
|
194
|
+
end: number;
|
|
195
|
+
value: { raw: string };
|
|
196
|
+
}>;
|
|
197
|
+
const bodyStart = node.start + 1;
|
|
198
|
+
const expressions = node.expressions as Array<{ start: number; end: number }>;
|
|
199
|
+
const masked = new MagicString(code.slice(bodyStart, node.end - 1));
|
|
200
|
+
for (const expr of expressions)
|
|
201
|
+
masked.overwrite(
|
|
202
|
+
expr.start - 2 - bodyStart,
|
|
203
|
+
expr.end + 1 - bodyStart,
|
|
204
|
+
"x".repeat(expr.end - expr.start + 3),
|
|
205
|
+
);
|
|
206
|
+
const html = masked.toString();
|
|
207
|
+
if (html.includes("<")) {
|
|
208
|
+
for (const edit of markupEdits(html, names)) {
|
|
209
|
+
const start = bodyStart + edit.start;
|
|
210
|
+
const end = bodyStart + edit.end;
|
|
211
|
+
if (!expressions.some((expr) => start < expr.end + 1 && end > expr.start - 2))
|
|
212
|
+
output.overwrite(start, end, edit.text);
|
|
213
|
+
}
|
|
214
|
+
} else
|
|
215
|
+
for (const segment of segments) {
|
|
216
|
+
const next = compactTokens(segment.value.raw, names);
|
|
217
|
+
if (next !== segment.value.raw)
|
|
218
|
+
output.overwrite(segment.start, segment.end, next);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
return output.toString();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function compactAstro(code: string, names: Names = styleNames): Promise<string> {
|
|
226
|
+
const { ast } = await parseAstro(code);
|
|
227
|
+
const output = new MagicString(code);
|
|
228
|
+
const sourceBytes = Buffer.from(code);
|
|
229
|
+
const offsetOf = (offset: number) => sourceBytes.subarray(0, offset).toString("utf8").length;
|
|
230
|
+
function visit(node: typeof ast | (typeof ast.children)[number]) {
|
|
231
|
+
if (node.type === "frontmatter") {
|
|
232
|
+
const start = offsetOf(node.position?.start.offset ?? 0) + 3;
|
|
233
|
+
output.overwrite(start, start + node.value.length, compactScript(node.value, names));
|
|
234
|
+
}
|
|
235
|
+
if ("attributes" in node)
|
|
236
|
+
for (const attr of node.attributes) {
|
|
237
|
+
const byteOffset = attr.position?.start.offset;
|
|
238
|
+
const offset = byteOffset === undefined ? undefined : offsetOf(byteOffset);
|
|
239
|
+
if (offset === undefined || attr.kind === "empty" || attr.kind === "spread")
|
|
240
|
+
continue;
|
|
241
|
+
const equals = code.indexOf("=", offset + attr.name.length);
|
|
242
|
+
const start = equals + 1 + (code.slice(equals + 1).match(/^\s*/u)?.[0].length ?? 0);
|
|
243
|
+
if (
|
|
244
|
+
attr.kind === "expression" &&
|
|
245
|
+
(attr.name === "class" ||
|
|
246
|
+
attr.name === "class:list" ||
|
|
247
|
+
attr.name === "style" ||
|
|
248
|
+
attr.name === "data-toc-active-class")
|
|
249
|
+
) {
|
|
250
|
+
const next = compactScript(attr.value, names, true);
|
|
251
|
+
if (next !== attr.value)
|
|
252
|
+
output.overwrite(start + 1, start + 1 + attr.value.length, next);
|
|
253
|
+
} else if (
|
|
254
|
+
attr.kind === "quoted" &&
|
|
255
|
+
["class", "style", "data-toc-active-class"].includes(attr.name)
|
|
256
|
+
) {
|
|
257
|
+
const next =
|
|
258
|
+
attr.name === "style"
|
|
259
|
+
? compactCss(attr.value, names, true)
|
|
260
|
+
: compactTokens(attr.value, names);
|
|
261
|
+
if (next !== attr.value)
|
|
262
|
+
output.overwrite(
|
|
263
|
+
start,
|
|
264
|
+
start + (attr.raw?.length ?? attr.value.length + 2),
|
|
265
|
+
JSON.stringify(next),
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (
|
|
270
|
+
"name" in node &&
|
|
271
|
+
(node.name === "style" || node.name === "script") &&
|
|
272
|
+
"children" in node
|
|
273
|
+
) {
|
|
274
|
+
for (const child of node.children)
|
|
275
|
+
if (child.type === "text" && child.position?.end) {
|
|
276
|
+
const next =
|
|
277
|
+
node.name === "style"
|
|
278
|
+
? compactCss(child.value, names)
|
|
279
|
+
: compactScript(child.value, names);
|
|
280
|
+
if (next !== child.value)
|
|
281
|
+
output.overwrite(
|
|
282
|
+
offsetOf(child.position.start.offset),
|
|
283
|
+
offsetOf(child.position.end.offset),
|
|
284
|
+
next,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if ("children" in node) for (const child of node.children) visit(child);
|
|
290
|
+
}
|
|
291
|
+
visit(ast);
|
|
292
|
+
return output.toString();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function compactStylesPlugin(srcDir: string): Plugin {
|
|
296
|
+
const root = path.resolve(srcDir) + path.sep;
|
|
297
|
+
return {
|
|
298
|
+
name: "stalux:compact-styles",
|
|
299
|
+
enforce: "pre",
|
|
300
|
+
transform: {
|
|
301
|
+
order: "pre",
|
|
302
|
+
async handler(code, id) {
|
|
303
|
+
const filename = id.split("?")[0] ?? "";
|
|
304
|
+
if (!filename.startsWith(root)) return;
|
|
305
|
+
const relative = filename.slice(root.length).replaceAll(path.sep, "/");
|
|
306
|
+
if (
|
|
307
|
+
!/^(?:components|layouts|pages|scripts|utils|styles)\//u.test(relative) ||
|
|
308
|
+
relative === "utils/style-name.ts" ||
|
|
309
|
+
relative.endsWith(".md.ts")
|
|
310
|
+
)
|
|
311
|
+
return;
|
|
312
|
+
let next: string;
|
|
313
|
+
if (id.includes("astro&type=")) return;
|
|
314
|
+
if (filename.endsWith(".astro")) next = await compactAstro(code);
|
|
315
|
+
else if (filename.endsWith(".css")) next = compactCss(code);
|
|
316
|
+
else if (filename.endsWith(".ts")) next = compactScript(code);
|
|
317
|
+
else return;
|
|
318
|
+
return next === code ? undefined : { code: next, map: null };
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { styleNames } from "./style-names.generated.ts";
|
|
1
2
|
/**
|
|
2
3
|
* Build-time CJK font subsetting engine.
|
|
3
4
|
*
|
|
@@ -128,6 +129,7 @@ type HtmlNode = DefaultTreeAdapterMap["node"];
|
|
|
128
129
|
const NON_RENDERED_ELEMENTS = new Set(["script", "style", "template", "title"]);
|
|
129
130
|
const VISUALLY_HIDDEN_CLASSES = new Set([
|
|
130
131
|
"agent-home-summary",
|
|
132
|
+
styleNames.classes["agent-home-summary"] ?? "agent-home-summary",
|
|
131
133
|
"screen-reader-only",
|
|
132
134
|
"sr-only",
|
|
133
135
|
"visually-hidden",
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Modern CSS output floor for the 1.35 release. Older fallback expansions are unnecessary.
|
|
2
|
+
// Acceptance uses current stable browser engines; targets intentionally retain native nesting.
|
|
3
|
+
export const modernCssTargets = {
|
|
4
|
+
chrome: 154 << 16,
|
|
5
|
+
firefox: 157 << 16,
|
|
6
|
+
safari: (26 << 16) | (6 << 8),
|
|
7
|
+
};
|
|
@@ -29,6 +29,7 @@ export function createRuntimeCacheKey(
|
|
|
29
29
|
scriptsDirectory: string,
|
|
30
30
|
packageJson: string,
|
|
31
31
|
stylesDirectory?: string,
|
|
32
|
+
styleMapping?: string,
|
|
32
33
|
): string {
|
|
33
34
|
return hashRuntimeSources([
|
|
34
35
|
...collectFiles(scriptsDirectory),
|
|
@@ -40,6 +41,7 @@ export function createRuntimeCacheKey(
|
|
|
40
41
|
readFileSync(path.join(stylesDirectory, name), "utf8"),
|
|
41
42
|
])
|
|
42
43
|
: []),
|
|
44
|
+
...(styleMapping ? [["style-names", styleMapping] as const] : []),
|
|
43
45
|
["package.json", readFileSync(packageJson, "utf8")],
|
|
44
46
|
]);
|
|
45
47
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { parse, walk } from "css-tree";
|
|
2
|
+
import { parse as parseHtml } from "parse5";
|
|
3
|
+
import { styleNameIn } from "./style-name-lookup.ts";
|
|
4
|
+
import { styleNames } from "./style-names.generated.ts";
|
|
5
|
+
|
|
6
|
+
// Public enthec/webappanalyzer Tailwind CSS rule snapshot, retrieved 2026-10-04.
|
|
7
|
+
// t.json SHA-256: aa96b868b7ad19eccd6c1337a59e14f4ff692a5bcb531a7a1021a3cfef7ab1e4
|
|
8
|
+
const tailwindMarkers =
|
|
9
|
+
/--tw-[\w-]+|@(?:apply|tailwind)\b|\/\*!\s*tailwindcss\b|(?:cdn\.)?tailwindcss\.(?:com|js)|\/npm\/@tailwindcss\/browser/iu;
|
|
10
|
+
|
|
11
|
+
/** Check semantic style surfaces, leaving article prose and code examples intact. */
|
|
12
|
+
export function styleArtifactProblems(filename: string, source: string): string[] {
|
|
13
|
+
const problems: string[] = [];
|
|
14
|
+
if (filename.endsWith(".css")) {
|
|
15
|
+
if (tailwindMarkers.test(source))
|
|
16
|
+
problems.push("Tailwind CSS fingerprint or uncompiled directive");
|
|
17
|
+
walk(parse(source, { parseCustomProperty: true }), (node) => {
|
|
18
|
+
if (node.type === "ClassSelector" && styleNameIn(styleNames.classes, node.name))
|
|
19
|
+
problems.push(`uncompacted class ${node.name}`);
|
|
20
|
+
if (node.type === "Declaration" && styleNameIn(styleNames.variables, node.property))
|
|
21
|
+
problems.push(`uncompacted variable ${node.property}`);
|
|
22
|
+
if (
|
|
23
|
+
node.type === "Identifier" &&
|
|
24
|
+
(styleNameIn(styleNames.variables, node.name) ||
|
|
25
|
+
styleNameIn(styleNames.animations, node.name))
|
|
26
|
+
)
|
|
27
|
+
problems.push(`uncompacted CSS reference ${node.name}`);
|
|
28
|
+
});
|
|
29
|
+
} else if (filename.endsWith(".html")) {
|
|
30
|
+
const ast = parseHtml(source);
|
|
31
|
+
function visit(node: typeof ast | (typeof ast.childNodes)[number]) {
|
|
32
|
+
if ("tagName" in node) {
|
|
33
|
+
if (node.tagName === "pre" || node.tagName === "code") return;
|
|
34
|
+
for (const attr of node.attrs) {
|
|
35
|
+
if (attr.name === "class")
|
|
36
|
+
for (const name of attr.value.split(/\s+/u)) {
|
|
37
|
+
if (styleNameIn(styleNames.classes, name))
|
|
38
|
+
problems.push(`uncompacted HTML class ${name}`);
|
|
39
|
+
}
|
|
40
|
+
if (attr.name === "style")
|
|
41
|
+
for (const match of attr.value.matchAll(/--[\w-]+/gu)) {
|
|
42
|
+
if (styleNameIn(styleNames.variables, match[0]))
|
|
43
|
+
problems.push(`uncompacted inline variable ${match[0]}`);
|
|
44
|
+
}
|
|
45
|
+
if (
|
|
46
|
+
(attr.name === "src" || attr.name === "href") &&
|
|
47
|
+
tailwindMarkers.test(attr.value)
|
|
48
|
+
)
|
|
49
|
+
problems.push("Tailwind CDN reference");
|
|
50
|
+
}
|
|
51
|
+
if (
|
|
52
|
+
node.tagName === "script" &&
|
|
53
|
+
"childNodes" in node &&
|
|
54
|
+
!node.attrs.some(
|
|
55
|
+
(attr) =>
|
|
56
|
+
attr.name === "type" &&
|
|
57
|
+
!["module", "text/javascript"].includes(attr.value),
|
|
58
|
+
)
|
|
59
|
+
) {
|
|
60
|
+
for (const child of node.childNodes)
|
|
61
|
+
if ("value" in child)
|
|
62
|
+
problems.push(...styleArtifactProblems("inline.js", child.value));
|
|
63
|
+
}
|
|
64
|
+
if (node.tagName === "style" && "childNodes" in node)
|
|
65
|
+
for (const child of node.childNodes) {
|
|
66
|
+
if ("value" in child)
|
|
67
|
+
problems.push(...styleArtifactProblems("inline.css", child.value));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if ("childNodes" in node) for (const child of node.childNodes) visit(child);
|
|
71
|
+
}
|
|
72
|
+
visit(ast);
|
|
73
|
+
} else if (filename.endsWith(".js")) {
|
|
74
|
+
if (tailwindMarkers.test(source)) problems.push("Tailwind script fingerprint");
|
|
75
|
+
const styleReferences = source.replace(/\[data-[^\]]+\]/gu, "");
|
|
76
|
+
for (const match of styleReferences.matchAll(/--[\w-]+|[a-zA-Z_][\w-]*/gu)) {
|
|
77
|
+
if (
|
|
78
|
+
styleNameIn(styleNames.classes, match[0]) ||
|
|
79
|
+
styleNameIn(styleNames.variables, match[0]) ||
|
|
80
|
+
styleNameIn(styleNames.animations, match[0])
|
|
81
|
+
)
|
|
82
|
+
problems.push(`uncompacted script name ${match[0]}`);
|
|
83
|
+
}
|
|
84
|
+
} else if (filename.endsWith(".map")) {
|
|
85
|
+
const map = JSON.parse(source) as { sourcesContent?: string[] };
|
|
86
|
+
if (
|
|
87
|
+
map.sourcesContent?.some((content) =>
|
|
88
|
+
/style-names\.generated|stalux-main|--un-/u.test(content),
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
problems.push("public sourcemap exposes theme names");
|
|
92
|
+
}
|
|
93
|
+
return [...new Set(problems)];
|
|
94
|
+
}
|