@xingwangzhe/stalux 1.35.15 → 1.36.0
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 +7 -5
- package/package.json +2 -2
- package/src/components/stalux/archives/archivesList.astro +2 -13
- package/src/components/stalux/layout/head.astro +3 -3
- package/src/components/stalux/posts/postContent.astro +7 -1
- package/src/config.ts +1 -1
- package/src/env.d.ts +9 -0
- package/src/expressive-code.ts +9 -2
- package/src/index.ts +22 -14
- package/src/internal/html-output.ts +4 -1
- package/src/internal/html-styles.ts +39 -0
- package/src/internal/offscreen-layout.ts +117 -0
- package/src/internal/project-mode.ts +7 -0
- package/src/internal/search-style.ts +29 -0
- package/src/internal/style-names.generated.ts +13 -14
- package/src/internal/webmcp-mutations.ts +37 -0
- package/src/plugins/image-captions.ts +117 -0
- package/src/scripts/analytics-runtime.ts +94 -103
- package/src/scripts/archive-descriptions.ts +25 -4
- package/src/scripts/background.ts +40 -12
- package/src/scripts/code-accessibility.ts +31 -0
- package/src/scripts/navigation-font.ts +36 -10
- package/src/scripts/webmcp-mutations.ts +27 -0
- package/src/styles/base.css +10 -0
- package/src/styles/content.css +8 -0
- package/src/styles/generated-archives.css +1 -1
- package/src/styles/generated-article.css +1 -1
- package/src/styles/generated-prose.css +1 -1
- package/src/styles/generated.css +1 -1
- package/src/styles/motion.css +0 -11
package/README.md
CHANGED
|
@@ -14,6 +14,10 @@ All HTML routes include a circular back-to-top button in the footer. It appears
|
|
|
14
14
|
|
|
15
15
|
The homepage typewriter is built into Stalux and continues to use the `items` string list in `stalux/config/typetexts.yml`, with no new configuration. Defaults are 100ms per typed grapheme, 33ms per deleted grapheme, a 1500ms hold, and a 500ms pause after deletion. It pauses outside the viewport or in background tabs and cleans up on soft navigation. Reduced motion displays whole messages without a blinking cursor. Without JavaScript, the first message remains visible; screen readers and print receive the complete list.
|
|
16
16
|
|
|
17
|
+
Standalone images written as `` receive a `figure` and `figcaption` through a build-time satteri HAST plugin. Captions appear as small, centered gray text below the image, preserving image alt text and the PhotoSwipe lightbox. No configuration or browser script is required. Empty descriptions, inline images, multiple images in one paragraph, and existing figures are left unchanged. Captions are plain text with automatic escaping.
|
|
18
|
+
|
|
19
|
+
Code blocks default to the single `dark-plus` theme, with system color-scheme switching and theme selectors disabled. To opt into another theme or switching behavior, explicitly override `themes`, `useDarkModeMediaQuery`, and `themeCssSelector` through `expressiveCode`. This only changes generated code styles and adds no telemetry.
|
|
20
|
+
|
|
17
21
|
---
|
|
18
22
|
|
|
19
23
|
## 🚀 Quick Start
|
|
@@ -57,7 +61,7 @@ integrations: [
|
|
|
57
61
|
stalux({
|
|
58
62
|
contentDir: "stalux",
|
|
59
63
|
sitemap: { filter: (page) => page.startsWith("https://example.com/posts/") },
|
|
60
|
-
expressiveCode: { themes: ["dark-plus",
|
|
64
|
+
expressiveCode: { themes: ["dark-plus"], useDarkModeMediaQuery: false },
|
|
61
65
|
}),
|
|
62
66
|
];
|
|
63
67
|
```
|
|
@@ -183,13 +187,11 @@ id: head
|
|
|
183
187
|
bingClarityId: "YOUR_CLARITY_PROJECT_ID"
|
|
184
188
|
```
|
|
185
189
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
`stalux()` includes Partytown. The Google Analytics, Clarity, and Umami scripts run in a Web Worker without an additional consumer integration. Page tracking follows `astro:page-load`; browsers with `Element.moveBefore()` preserve the worker during navigation. Vercount loads asynchronously in the main window, avoiding worker startup and DOM bridge delays for visible counters. It runs once after each `astro:page-load` to bind incoming page counters and use the current URL and first-party visitor cookie.
|
|
190
|
+
Google Analytics, Clarity, and Umami use low-priority native asynchronous scripts. Each SDK loads once per session, and failures do not block reading or navigation. Vercount rebinds visible counters after each `astro:page-load`. GA sends explicit pageviews. Umami disables automatic pageviews while retaining click tracking, capturing each page's URL, title, and referrer before the SDK resolves so rapid navigation cannot mix visits. An existing legacy Partytown session performs one full reload when it encounters the new runtime to prevent duplicate listeners and tracking.
|
|
189
191
|
|
|
190
192
|
For Google Analytics, turn off **Page changes based on browser history events** under **Admin → Data streams → Enhanced measurement → Page views → Advanced settings**. Stalux sets `send_page_view: false` and sends the URL and title for each navigation explicitly. GA's history tracking must also be disabled in the property to prevent duplicate soft-navigation pageviews. Other enhanced measurement options can remain enabled. See [Google's pageview documentation](https://developers.google.com/analytics/devguides/collection/ga4/views).
|
|
191
193
|
|
|
192
|
-
`bingClarityId` is the historical Stalux field name for a Microsoft Clarity Project ID. Get the ID from the Clarity project under **Settings → Setup → Get tracking code**. Stalux loads the tracking code
|
|
194
|
+
`bingClarityId` is the historical Stalux field name for a Microsoft Clarity Project ID. Get the ID from the Clarity project under **Settings → Setup → Get tracking code**. Stalux loads the tracking code asynchronously and preserves one loader across soft navigation. The SDK handles history navigation itself. Do not install the same project again through `anyhead`, a tag manager, or another plugin.
|
|
193
195
|
|
|
194
196
|
The Project ID is a public browser identifier. Never put a Clarity Data Export API token in this YAML or in client-side code. If the site uses a strict CSP, consent banner, or CMP, configure those host-site policies and signals separately; this theme does not make legal compliance decisions for the site.
|
|
195
197
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xingwangzhe/stalux",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.36.0",
|
|
4
4
|
"description": "A powerful, modern Astro blog theme — use as template or install as plugin",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"stalux": ".release/cli.mjs"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
|
+
"src/env.d.ts",
|
|
29
30
|
"src/index.ts",
|
|
30
31
|
"src/config.ts",
|
|
31
32
|
"src/expressive-code.ts",
|
|
@@ -101,7 +102,6 @@
|
|
|
101
102
|
"dependencies": {
|
|
102
103
|
"@astrojs/compiler": "^4.0.0",
|
|
103
104
|
"@astrojs/markdown-satteri": "^0.4.3",
|
|
104
|
-
"@astrojs/partytown": "^2.1.8",
|
|
105
105
|
"@astrojs/rss": "^4.0.19",
|
|
106
106
|
"@astrojs/sitemap": "^3.7.4",
|
|
107
107
|
"@expressive-code/plugin-line-numbers": "^0.44.2",
|
|
@@ -66,13 +66,7 @@ const archiveYears = [...archivesByYearMonth.entries()]
|
|
|
66
66
|
</div>
|
|
67
67
|
</div>
|
|
68
68
|
{archiveYear.months.map(({ month, posts: monthPosts }) => (
|
|
69
|
-
<div
|
|
70
|
-
class="stalux-archive-month-section"
|
|
71
|
-
data-archive-month
|
|
72
|
-
style={{
|
|
73
|
-
"--archive-estimate": `${Math.ceil(monthPosts.length / 12) * 12 * 48 + 40}px`,
|
|
74
|
-
}}
|
|
75
|
-
>
|
|
69
|
+
<div class="stalux-archive-month-section" data-archive-month>
|
|
76
70
|
<h3 class="stalux-archive-month-title">
|
|
77
71
|
{t("archives.month", { n: month })}
|
|
78
72
|
</h3>
|
|
@@ -80,12 +74,7 @@ const archiveYears = [...archivesByYearMonth.entries()]
|
|
|
80
74
|
{ length: Math.ceil(monthPosts.length / 12) },
|
|
81
75
|
(_, index) => monthPosts.slice(index * 12, (index + 1) * 12),
|
|
82
76
|
).map((group) => (
|
|
83
|
-
<ul
|
|
84
|
-
class="stalux-archive-post-list"
|
|
85
|
-
style={{
|
|
86
|
-
"--archive-group-estimate": `${group.length * 48}px`,
|
|
87
|
-
}}
|
|
88
|
-
>
|
|
77
|
+
<ul class="stalux-archive-post-list">
|
|
89
78
|
{group.map((post) => (
|
|
90
79
|
<li class="stalux-archive-post-item">
|
|
91
80
|
<time
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
import { Font } from "astro:assets";
|
|
3
3
|
import { getCollection } from "astro:content";
|
|
4
4
|
import { ClientRouter } from "astro:transitions";
|
|
5
|
+
// Emit a stable asset even while the search component JavaScript stays lazy.
|
|
6
|
+
// The closed search dialog does not need to block the page's first paint.
|
|
7
|
+
import pagefindStyles from "virtual:stalux-search-style";
|
|
5
8
|
import BingClarity from "@components/stalux/analytics/clarity.astro";
|
|
6
9
|
import GoogleAnalytics from "@components/stalux/analytics/google.astro";
|
|
7
10
|
import Umami from "@components/stalux/analytics/umami.astro";
|
|
8
11
|
import VerCount from "@components/stalux/common/verCount.astro";
|
|
9
|
-
// Emit a stable asset even while the search component JavaScript stays lazy.
|
|
10
|
-
// The closed search dialog does not need to block the page's first paint.
|
|
11
|
-
import pagefindStyles from "@pagefind/component-ui/css?url";
|
|
12
12
|
import {
|
|
13
13
|
getAuthorData,
|
|
14
14
|
getHeadData,
|
|
@@ -118,4 +118,10 @@ const mdSourceUrl = exportMd ? `${Astro.url.pathname.replace(/\/$/, "")}.md` : "
|
|
|
118
118
|
</section>
|
|
119
119
|
</article>
|
|
120
120
|
|
|
121
|
-
<script
|
|
121
|
+
<script>
|
|
122
|
+
import "../../../scripts/photoswipe";
|
|
123
|
+
import { mountCodeAccessibility } from "../../../scripts/code-accessibility";
|
|
124
|
+
import { registerPageLifecycle } from "../../../scripts/page-runtime";
|
|
125
|
+
|
|
126
|
+
registerPageLifecycle("code-accessibility", mountCodeAccessibility);
|
|
127
|
+
</script>
|
package/src/config.ts
CHANGED
|
@@ -66,7 +66,7 @@ export interface StaluxOptions {
|
|
|
66
66
|
sitemap?: boolean | SitemapOptions;
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
* 是否自动打包 Expressive Code(默认 true
|
|
69
|
+
* 是否自动打包 Expressive Code(默认 true,带代码块行号插件,固定 dark-plus 暗色主题)。
|
|
70
70
|
* 传 false 关闭;传对象自定义主题/样式等选项。
|
|
71
71
|
*/
|
|
72
72
|
expressiveCode?: boolean | ExpressiveCodeOptions;
|
package/src/env.d.ts
ADDED
package/src/expressive-code.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers";
|
|
18
18
|
import expressiveCodeBase from "astro-expressive-code";
|
|
19
|
+
import { codeContainmentPlugin } from "./internal/offscreen-layout";
|
|
19
20
|
|
|
20
21
|
export type ExpressiveCodeOptions = NonNullable<Parameters<typeof expressiveCodeBase>[0]>;
|
|
21
22
|
|
|
@@ -24,8 +25,14 @@ export type ExpressiveCodeOptions = NonNullable<Parameters<typeof expressiveCode
|
|
|
24
25
|
* @param options - 透传 astro-expressive-code 的选项;用户传入的 plugins 会追加在行号插件之后
|
|
25
26
|
*/
|
|
26
27
|
export function expressiveCode(options: ExpressiveCodeOptions = {}) {
|
|
27
|
-
const plugins = [pluginLineNumbers(), ...(options.plugins ?? [])];
|
|
28
|
-
return expressiveCodeBase({
|
|
28
|
+
const plugins = [pluginLineNumbers(), ...(options.plugins ?? []), codeContainmentPlugin()];
|
|
29
|
+
return expressiveCodeBase({
|
|
30
|
+
themes: ["dark-plus"],
|
|
31
|
+
useDarkModeMediaQuery: false,
|
|
32
|
+
themeCssSelector: false,
|
|
33
|
+
...options,
|
|
34
|
+
plugins,
|
|
35
|
+
});
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
/** @alias Public default-import form of the named integration export. */
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference path="./env.d.ts" />
|
|
1
2
|
import { forEachBounded } from "./internal/bounded-tasks.ts";
|
|
2
3
|
import { compactStylesPlugin } from "./internal/compact-styles.ts";
|
|
3
4
|
import { modernCssTargets } from "./internal/modern-css.ts";
|
|
@@ -16,7 +17,6 @@ import { copyFileSync, existsSync, promises as fs, readdirSync, readFileSync } f
|
|
|
16
17
|
import { createRequire } from "node:module";
|
|
17
18
|
import path from "node:path";
|
|
18
19
|
import { fileURLToPath } from "node:url";
|
|
19
|
-
import partytown from "@astrojs/partytown";
|
|
20
20
|
import sitemap from "@astrojs/sitemap";
|
|
21
21
|
import { mermaidHast, mermaidMdast } from "@xingwangzhe/satteri-mermaid";
|
|
22
22
|
import { photoswipe } from "@xingwangzhe/satteri-photoswipe";
|
|
@@ -39,10 +39,13 @@ import {
|
|
|
39
39
|
} from "./internal/font-slices";
|
|
40
40
|
import { findHtmlImages } from "./internal/html-images";
|
|
41
41
|
import { createHtmlMutationEmitter } from "./internal/html-output";
|
|
42
|
+
import { promoteCodeStyles } from "./internal/html-styles";
|
|
42
43
|
import { type ImageDimensions, updateLocalImageDimensions } from "./internal/image-dimensions";
|
|
43
44
|
import { createInjectedRoutes } from "./internal/injected-routes";
|
|
44
45
|
import { markdownPropagationPlugin } from "./internal/markdown-propagation";
|
|
46
|
+
import { containMermaidSvgs } from "./internal/offscreen-layout";
|
|
45
47
|
import { createPageOutputCache } from "./internal/page-output-cache";
|
|
48
|
+
import { isPluginProject } from "./internal/project-mode";
|
|
46
49
|
import { syncBackgroundSvgs, syncFontAssets } from "./internal/public-assets";
|
|
47
50
|
import { createRuntimeCacheKey } from "./internal/runtime-cache-key";
|
|
48
51
|
import {
|
|
@@ -50,8 +53,11 @@ import {
|
|
|
50
53
|
collectPluginNames,
|
|
51
54
|
prepareSatteriProcessor,
|
|
52
55
|
} from "./internal/satteri-config";
|
|
56
|
+
import { searchStylePlugin } from "./internal/search-style";
|
|
53
57
|
import { createViteAliases } from "./internal/vite-aliases";
|
|
58
|
+
import { webMcpMutationsPlugin } from "./internal/webmcp-mutations";
|
|
54
59
|
import { featureFlagsHast, featureFlagsMdast } from "./plugins/feature-flags";
|
|
60
|
+
import { imageCaptions } from "./plugins/image-captions";
|
|
55
61
|
import { temml } from "./plugins/satteri-temml";
|
|
56
62
|
import { describeError } from "./utils/diagnostics";
|
|
57
63
|
|
|
@@ -110,9 +116,6 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
110
116
|
...options,
|
|
111
117
|
};
|
|
112
118
|
|
|
113
|
-
// 探测是否为插件模式(通过 npm 安装)vs 源码开发模式
|
|
114
|
-
// 安装在 node_modules 中时,页面的文件路由不可用,需要通过 injectRoute 注入
|
|
115
|
-
const isPluginMode = import.meta.url.includes("node_modules");
|
|
116
119
|
let pageFontData: { fontBuffer: Buffer; outputDir: string; cacheDir: string } | undefined;
|
|
117
120
|
|
|
118
121
|
const coreIntegration: AstroIntegration = {
|
|
@@ -128,6 +131,7 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
128
131
|
logger,
|
|
129
132
|
}) => {
|
|
130
133
|
const started = performance.now();
|
|
134
|
+
const isPluginMode = isPluginProject(config.root, new URL("../", import.meta.url));
|
|
131
135
|
const configLogger = logger.fork("stalux/config");
|
|
132
136
|
const routesLogger = logger.fork("stalux/routes");
|
|
133
137
|
const markdownLogger = logger.fork("stalux/markdown");
|
|
@@ -139,7 +143,8 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
139
143
|
path.resolve(srcDir, "../package.json"),
|
|
140
144
|
path.join(srcDir, "styles"),
|
|
141
145
|
JSON.stringify([styleNames, modernCssTargets]) +
|
|
142
|
-
readFileSync(path.join(srcDir, "internal/compact-styles.ts"), "utf8")
|
|
146
|
+
readFileSync(path.join(srcDir, "internal/compact-styles.ts"), "utf8") +
|
|
147
|
+
readFileSync(path.join(srcDir, "internal/offscreen-layout.ts"), "utf8"),
|
|
143
148
|
path.resolve(fileURLToPath(config.root), opt.contentDir ?? "stalux", "config"),
|
|
144
149
|
);
|
|
145
150
|
|
|
@@ -152,6 +157,7 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
152
157
|
config.build.inlineStylesheets === "always" ? "always" : "never",
|
|
153
158
|
},
|
|
154
159
|
vite: {
|
|
160
|
+
optimizeDeps: { exclude: ["@mcp-b/webmcp-polyfill"] },
|
|
155
161
|
css: { lightningcss: { targets: modernCssTargets } },
|
|
156
162
|
build: {
|
|
157
163
|
target: config.vite.build?.target ?? "esnext",
|
|
@@ -162,6 +168,8 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
162
168
|
alias: createViteAliases(srcDir),
|
|
163
169
|
},
|
|
164
170
|
plugins: [
|
|
171
|
+
searchStylePlugin(command === "dev", config.base),
|
|
172
|
+
webMcpMutationsPlugin(),
|
|
165
173
|
...(command === "build"
|
|
166
174
|
? [markdownPropagationPlugin(), compactStylesPlugin(srcDir)]
|
|
167
175
|
: []),
|
|
@@ -218,7 +226,7 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
218
226
|
// 消费方即使完全不配置 processor,这里也会对默认 processor 的 options 做幂等合并:
|
|
219
227
|
// - features:默认开启 math / frontmatter / gfm / smartPunctuation(用户显式 false 则尊重)
|
|
220
228
|
// - mdastPlugins:mermaidMdast + temml(数学公式)+ featureFlagsMdast(字数统计)
|
|
221
|
-
// - hastPlugins:photoswipe(图片灯箱)+ mermaidHast + featureFlagsHast
|
|
229
|
+
// - hastPlugins:photoswipe(图片灯箱)+ imageCaptions(图注)+ mermaidHast + featureFlagsHast
|
|
222
230
|
// 去重是必须的:重复注册 Mermaid 或 featureFlagsMdast 会导致渲染异常或字数统计翻倍。
|
|
223
231
|
try {
|
|
224
232
|
const processorOptions = prepareSatteriProcessor(config.markdown?.processor);
|
|
@@ -242,9 +250,10 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
242
250
|
seen,
|
|
243
251
|
);
|
|
244
252
|
appendUniquePlugin(processorOptions.hastPlugins, photoswipe(), seen);
|
|
253
|
+
appendUniquePlugin(processorOptions.hastPlugins, imageCaptions, seen);
|
|
245
254
|
appendUniquePlugin(processorOptions.hastPlugins, featureFlagsHast, seen);
|
|
246
255
|
markdownLogger.debug(
|
|
247
|
-
"injected satteri plugins (mermaid/temml/photoswipe/feature-flags)",
|
|
256
|
+
"injected satteri plugins (mermaid/temml/photoswipe/image-captions/feature-flags)",
|
|
248
257
|
);
|
|
249
258
|
} else {
|
|
250
259
|
markdownLogger.warn(
|
|
@@ -371,6 +380,8 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
371
380
|
"internal/image-dimensions.ts",
|
|
372
381
|
"internal/html-images.ts",
|
|
373
382
|
"internal/html-output.ts",
|
|
383
|
+
"internal/html-styles.ts",
|
|
384
|
+
"internal/offscreen-layout.ts",
|
|
374
385
|
"internal/page-output-cache.ts",
|
|
375
386
|
"internal/style-names.generated.ts",
|
|
376
387
|
"plugins/feature-flags.ts",
|
|
@@ -419,6 +430,8 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
419
430
|
const dependencies = new Set<string>();
|
|
420
431
|
const document = parse(before, { sourceCodeLocationInfo: true });
|
|
421
432
|
const emitHtml = createHtmlMutationEmitter(before, document);
|
|
433
|
+
promoteCodeStyles(document);
|
|
434
|
+
containMermaidSvgs(document);
|
|
422
435
|
removePageFontStyles(document);
|
|
423
436
|
const images = findHtmlImages(before, document);
|
|
424
437
|
await updateLocalImageDimensions(images, outDir, {
|
|
@@ -524,13 +537,8 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
524
537
|
};
|
|
525
538
|
|
|
526
539
|
// 打包内置集成:Astro 的 integrations 配置支持嵌套数组并自动展平(schema 里 val.flat(Infinity)),
|
|
527
|
-
// 因此这里返回 [coreIntegration,
|
|
528
|
-
const bundled: AstroIntegration[] = [
|
|
529
|
-
coreIntegration,
|
|
530
|
-
partytown({
|
|
531
|
-
config: { forward: ["dataLayer.push", "gtag"], mainWindowAccessors: ["CSS"] },
|
|
532
|
-
}),
|
|
533
|
-
];
|
|
540
|
+
// 因此这里返回 [coreIntegration, sitemap, expressiveCode],让消费方一行 `integrations: [stalux()]` 即可完成配置。
|
|
541
|
+
const bundled: AstroIntegration[] = [coreIntegration];
|
|
534
542
|
|
|
535
543
|
if (opt.sitemap !== false) {
|
|
536
544
|
const userSitemap = opt.sitemap === true ? undefined : opt.sitemap;
|
|
@@ -24,7 +24,10 @@ export function createHtmlMutationEmitter(
|
|
|
24
24
|
if (node.tagName === "head") head = node;
|
|
25
25
|
const location = node.sourceCodeLocation;
|
|
26
26
|
if (
|
|
27
|
-
(node.tagName === "img" ||
|
|
27
|
+
(node.tagName === "img" ||
|
|
28
|
+
node.tagName === "style" ||
|
|
29
|
+
node.tagName === "link" ||
|
|
30
|
+
node.tagName === "svg") &&
|
|
28
31
|
location
|
|
29
32
|
) {
|
|
30
33
|
snapshots.push({
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type DefaultTreeAdapterMap, defaultTreeAdapter } from "parse5";
|
|
2
|
+
|
|
3
|
+
type Element = DefaultTreeAdapterMap["element"];
|
|
4
|
+
|
|
5
|
+
/** Astro's router preloads head styles only; EC emits its shared CSS inside the first block. */
|
|
6
|
+
export function promoteCodeStyles(document: DefaultTreeAdapterMap["document"]): number {
|
|
7
|
+
let head: Element | undefined;
|
|
8
|
+
const links: Element[] = [];
|
|
9
|
+
function visit(node: DefaultTreeAdapterMap["node"], inCode = false) {
|
|
10
|
+
if ("tagName" in node) {
|
|
11
|
+
if (node.tagName === "template" || node.tagName === "noscript") return;
|
|
12
|
+
if (node.tagName === "head") head = node;
|
|
13
|
+
inCode ||= (
|
|
14
|
+
node.attrs.find((a) => a.name === "class")?.value.split(/\s+/) ?? []
|
|
15
|
+
).includes("expressive-code");
|
|
16
|
+
if (
|
|
17
|
+
inCode &&
|
|
18
|
+
node.tagName === "link" &&
|
|
19
|
+
node.attrs.some((a) => a.name === "rel" && a.value === "stylesheet")
|
|
20
|
+
)
|
|
21
|
+
links.push(node);
|
|
22
|
+
}
|
|
23
|
+
if ("childNodes" in node) for (const child of node.childNodes) visit(child, inCode);
|
|
24
|
+
}
|
|
25
|
+
visit(document);
|
|
26
|
+
if (!head) return 0;
|
|
27
|
+
for (const link of links) {
|
|
28
|
+
// Clone metadata without source ranges so the shared mutation emitter can
|
|
29
|
+
// remove the original bytes and insert the reference without rewriting prose.
|
|
30
|
+
const promoted = defaultTreeAdapter.createElement(
|
|
31
|
+
"link",
|
|
32
|
+
link.namespaceURI,
|
|
33
|
+
link.attrs.map((a) => ({ ...a })),
|
|
34
|
+
);
|
|
35
|
+
defaultTreeAdapter.detachNode(link);
|
|
36
|
+
defaultTreeAdapter.appendChild(head, promoted);
|
|
37
|
+
}
|
|
38
|
+
return links.length;
|
|
39
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { ExpressiveCodePlugin } from "astro-expressive-code";
|
|
2
|
+
import { parse as parseCss, walk as walkCss } from "css-tree";
|
|
3
|
+
import type { DefaultTreeAdapterMap } from "parse5";
|
|
4
|
+
|
|
5
|
+
interface AstNode {
|
|
6
|
+
type: string;
|
|
7
|
+
tagName?: string;
|
|
8
|
+
properties?: Record<string, unknown>;
|
|
9
|
+
children?: AstNode[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function styleProperties(value: unknown): { text: string; names: Set<string> } | undefined {
|
|
13
|
+
if (value !== undefined && typeof value !== "string") return;
|
|
14
|
+
const text = value ?? "";
|
|
15
|
+
const names = new Set<string>();
|
|
16
|
+
try {
|
|
17
|
+
walkCss(parseCss(text, { context: "declarationList" }), {
|
|
18
|
+
visit: "Declaration",
|
|
19
|
+
enter(node) {
|
|
20
|
+
names.add(node.property.toLowerCase());
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
} catch {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
return { text, names };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const classes = (node: AstNode) => {
|
|
30
|
+
const value = node.properties?.className;
|
|
31
|
+
return Array.isArray(value) ? value.map(String) : String(value ?? "").split(/\s+/u);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Non-wrapping EC rows are exactly one inherited line-height each, including empty rows. */
|
|
35
|
+
export function containCodeBlocks(node: AstNode): number {
|
|
36
|
+
let changed = 0;
|
|
37
|
+
if (node.type === "element" && node.tagName === "pre" && !classes(node).includes("wrap")) {
|
|
38
|
+
const code = node.children?.find(
|
|
39
|
+
(child) => child.type === "element" && child.tagName === "code",
|
|
40
|
+
);
|
|
41
|
+
const lines = code?.children ?? [];
|
|
42
|
+
if (
|
|
43
|
+
code &&
|
|
44
|
+
lines.length &&
|
|
45
|
+
lines.every((line) => line.type === "element" && classes(line).includes("ec-line"))
|
|
46
|
+
) {
|
|
47
|
+
code.properties ??= {};
|
|
48
|
+
const properties = code.properties;
|
|
49
|
+
const style = styleProperties(properties.style);
|
|
50
|
+
if (
|
|
51
|
+
style &&
|
|
52
|
+
!style.names.has("content-visibility") &&
|
|
53
|
+
!style.names.has("contain-intrinsic-block-size") &&
|
|
54
|
+
!["height", "block-size", "min-height", "max-height"].some((name) =>
|
|
55
|
+
style.names.has(name),
|
|
56
|
+
)
|
|
57
|
+
) {
|
|
58
|
+
properties.style = `${style.text};content-visibility:auto;contain-intrinsic-block-size:auto calc(${lines.length} * 1lh)`;
|
|
59
|
+
properties["data-stalux-offscreen-code"] = "";
|
|
60
|
+
changed++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const child of node.children ?? []) changed += containCodeBlocks(child);
|
|
65
|
+
return changed;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function codeContainmentPlugin(): ExpressiveCodePlugin {
|
|
69
|
+
return {
|
|
70
|
+
name: "stalux-offscreen-code",
|
|
71
|
+
hooks: {
|
|
72
|
+
postprocessRenderedBlockGroup({ renderData }) {
|
|
73
|
+
containCodeBlocks(renderData.groupAst);
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Mermaid SVGs have an exact viewBox ratio; skipping offscreen paint need not estimate their size. */
|
|
80
|
+
export function containMermaidSvgs(document: DefaultTreeAdapterMap["document"]): number {
|
|
81
|
+
let changed = 0;
|
|
82
|
+
function visit(node: DefaultTreeAdapterMap["node"]) {
|
|
83
|
+
if ("tagName" in node) {
|
|
84
|
+
if (node.tagName === "template" || node.tagName === "noscript") return;
|
|
85
|
+
const attribute = (name: string) =>
|
|
86
|
+
node.attrs.find((a) => a.name.toLowerCase() === name);
|
|
87
|
+
if (node.tagName === "svg" && attribute("id")?.value.startsWith("satteri-mermaid-")) {
|
|
88
|
+
const box = attribute("viewbox")
|
|
89
|
+
?.value.trim()
|
|
90
|
+
.split(/[\s,]+/u)
|
|
91
|
+
.map(Number);
|
|
92
|
+
const style = styleProperties(attribute("style")?.value);
|
|
93
|
+
if (
|
|
94
|
+
box?.length === 4 &&
|
|
95
|
+
box.every(Number.isFinite) &&
|
|
96
|
+
(box[2] ?? 0) > 0 &&
|
|
97
|
+
(box[3] ?? 0) > 0 &&
|
|
98
|
+
style &&
|
|
99
|
+
!style.names.has("content-visibility")
|
|
100
|
+
) {
|
|
101
|
+
const ratio = style.names.has("aspect-ratio")
|
|
102
|
+
? ""
|
|
103
|
+
: `;aspect-ratio:${box[2]}/${box[3]}`;
|
|
104
|
+
const value = `${style.text};content-visibility:auto${ratio}`;
|
|
105
|
+
const existing = attribute("style");
|
|
106
|
+
if (existing) existing.value = value;
|
|
107
|
+
else node.attrs.push({ name: "style", value });
|
|
108
|
+
node.attrs.push({ name: "data-stalux-offscreen-svg", value: "" });
|
|
109
|
+
changed++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if ("childNodes" in node) for (const child of node.childNodes) visit(child);
|
|
114
|
+
}
|
|
115
|
+
visit(document);
|
|
116
|
+
return changed;
|
|
117
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
/** Linked consumers still need injected routes; only the theme's own root has file routes. */
|
|
5
|
+
export function isPluginProject(projectRoot: URL, themeRoot: URL): boolean {
|
|
6
|
+
return realpathSync(fileURLToPath(projectRoot)) !== realpathSync(fileURLToPath(themeRoot));
|
|
7
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import type { Plugin } from "vite";
|
|
3
|
+
|
|
4
|
+
const publicId = "virtual:stalux-search-style";
|
|
5
|
+
const virtualId = `\0${publicId}`;
|
|
6
|
+
|
|
7
|
+
/** Astro dev treats a direct CSS ?url import as a module script; keep it a URL-only leaf. */
|
|
8
|
+
export function searchStylePlugin(development: boolean, base: string): Plugin {
|
|
9
|
+
const stylesheet = createRequire(import.meta.url)
|
|
10
|
+
.resolve("@pagefind/component-ui/css")
|
|
11
|
+
.replaceAll("\\", "/");
|
|
12
|
+
return {
|
|
13
|
+
name: "stalux:search-style",
|
|
14
|
+
configureServer(server) {
|
|
15
|
+
// A linked package can be outside the consumer's workspace root.
|
|
16
|
+
server.config.server.fs.allow.push(stylesheet);
|
|
17
|
+
},
|
|
18
|
+
async resolveId(id, importer) {
|
|
19
|
+
if (id !== publicId) return;
|
|
20
|
+
return development
|
|
21
|
+
? virtualId
|
|
22
|
+
: this.resolve(`${stylesheet}?url`, importer, { skipSelf: true });
|
|
23
|
+
},
|
|
24
|
+
load(id) {
|
|
25
|
+
if (id !== virtualId) return;
|
|
26
|
+
return `export default ${JSON.stringify(`${base.replace(/\/$/, "")}/@fs${stylesheet}`)};`;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -336,19 +336,18 @@ export const styleNames: { classes: Record<string, string>; variables: Record<st
|
|
|
336
336
|
"stalux-page-enter": "k6",
|
|
337
337
|
"stalux-page-enter-back": "k7",
|
|
338
338
|
"stalux-page-exit": "k8",
|
|
339
|
-
"stalux-
|
|
340
|
-
"stalux-
|
|
341
|
-
"stalux-sidebar-left-back-
|
|
342
|
-
"stalux-sidebar-left-
|
|
343
|
-
"stalux-sidebar-left-
|
|
344
|
-
"stalux-sidebar-
|
|
345
|
-
"stalux-sidebar-right-back-
|
|
346
|
-
"stalux-sidebar-right-
|
|
347
|
-
"stalux-sidebar-right-
|
|
348
|
-
"stalux-
|
|
349
|
-
"stalux-
|
|
350
|
-
"stalux-taxonomy-enter": "kk",
|
|
351
|
-
"stalux-
|
|
352
|
-
"stalux-word-word-glow": "km"
|
|
339
|
+
"stalux-random-spin": "k9",
|
|
340
|
+
"stalux-sidebar-left-back-in": "ka",
|
|
341
|
+
"stalux-sidebar-left-back-out": "kb",
|
|
342
|
+
"stalux-sidebar-left-in": "kc",
|
|
343
|
+
"stalux-sidebar-left-out": "kd",
|
|
344
|
+
"stalux-sidebar-right-back-in": "ke",
|
|
345
|
+
"stalux-sidebar-right-back-out": "kf",
|
|
346
|
+
"stalux-sidebar-right-in": "kg",
|
|
347
|
+
"stalux-sidebar-right-out": "kh",
|
|
348
|
+
"stalux-social-stalux-social-arrive": "ki",
|
|
349
|
+
"stalux-taxonomy-enter": "kj",
|
|
350
|
+
"stalux-taxonomy-enter-back": "kk",
|
|
351
|
+
"stalux-word-word-glow": "kl"
|
|
353
352
|
}
|
|
354
353
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import type { Plugin } from "vite";
|
|
5
|
+
import { affectsDeclarativeTools } from "../scripts/webmcp-mutations";
|
|
6
|
+
|
|
7
|
+
const observer = "const observer = new MutationObserver(sync);";
|
|
8
|
+
|
|
9
|
+
/** Upstream 5.1.0 rescans every element even for unrelated clock/background mutations. */
|
|
10
|
+
export function optimizeWebMcpMutations(code: string, version: string): string | undefined {
|
|
11
|
+
if (
|
|
12
|
+
version !== "5.1.0" ||
|
|
13
|
+
!code.includes("function installDeclarativeForms(document, context)") ||
|
|
14
|
+
code.split(observer).length !== 2
|
|
15
|
+
)
|
|
16
|
+
return;
|
|
17
|
+
return code.replace(
|
|
18
|
+
observer,
|
|
19
|
+
`const relevant = ${affectsDeclarativeTools.toString()};\nconst observer = new MutationObserver((records) => { if (relevant(records)) sync(); });`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function webMcpMutationsPlugin(): Plugin {
|
|
24
|
+
const entry = createRequire(import.meta.url).resolve("@mcp-b/webmcp-polyfill");
|
|
25
|
+
const version = JSON.parse(
|
|
26
|
+
readFileSync(new URL("../package.json", pathToFileURL(entry)), "utf8"),
|
|
27
|
+
).version;
|
|
28
|
+
return {
|
|
29
|
+
name: "stalux:webmcp-mutations",
|
|
30
|
+
enforce: "pre",
|
|
31
|
+
transform(code, id) {
|
|
32
|
+
if (!id.replaceAll("\\", "/").endsWith("/@mcp-b/webmcp-polyfill/dist/index.js")) return;
|
|
33
|
+
const optimized = optimizeWebMcpMutations(code, version);
|
|
34
|
+
if (optimized) return { code: optimized, map: null };
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|