@stnd/modules 0.5.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/Backlinks.astro +56 -0
- package/CHANGELOG.md +60 -0
- package/Comments.astro +115 -0
- package/README.md +404 -0
- package/ReadingTime.astro +60 -0
- package/brand-manual/index.module.js +12 -0
- package/brand-manual/route.astro +341 -0
- package/confetti/Confetti.astro +42 -0
- package/confetti/index.module.js +22 -0
- package/content/index.module.js +19 -0
- package/content/route.astro +59 -0
- package/copy-buttons/index.module.js +9 -0
- package/deep-link/index.module.js +9 -0
- package/deep-link/init.js +14 -0
- package/eink/index.module.js +9 -0
- package/fonts/index.module.js +76 -0
- package/gestures/index.module.js +9 -0
- package/gestures/init.js +13 -0
- package/gsap/client.js +41 -0
- package/gsap/index.module.js +20 -0
- package/headers/index.module.js +16 -0
- package/headers/route.js +93 -0
- package/humans/index.module.js +17 -0
- package/humans/route.js +79 -0
- package/iconify/index.module.js +9 -0
- package/image-zoom/index.module.js +9 -0
- package/keyboard/index.module.js +9 -0
- package/lab/index.module.js +26 -0
- package/launcher/index.module.js +27 -0
- package/maintenance/index.module.js +18 -0
- package/maintenance/middleware.js +34 -0
- package/maintenance/routes/index.astro +45 -0
- package/manifest/index.module.js +16 -0
- package/manifest/route.js +57 -0
- package/math/client.js +98 -0
- package/math/index.module.js +19 -0
- package/math/styles.css +25 -0
- package/mermaid/client.js +125 -0
- package/mermaid/index.module.js +19 -0
- package/mermaid/styles.css +114 -0
- package/p5/client.js +25 -0
- package/p5/index.module.js +24 -0
- package/package.json +35 -0
- package/prism/README.md +13 -0
- package/prism/index.module.js +19 -0
- package/robots/index.module.js +16 -0
- package/robots/route.js +67 -0
- package/rss/index.module.js +18 -0
- package/rss/route.js +163 -0
- package/scroll-wrappers/index.module.js +9 -0
- package/security-txt/index.module.js +17 -0
- package/security-txt/route.js +90 -0
- package/sitemap/index.module.js +22 -0
- package/stripe/client.js +27 -0
- package/stripe/components/MockBuyButton.svelte +38 -0
- package/stripe/index.module.js +33 -0
- package/stripe/routes/mock-checkout.js +64 -0
- package/stripe/views/CheckoutView.svelte +181 -0
- package/styles/index.module.js +11 -0
- package/theme-utils/README.md +74 -0
- package/theme-utils/client.js +79 -0
- package/theme-utils/index.module.js +27 -0
- package/themes/index.module.js +50 -0
- package/toast/index.module.js +17 -0
- package/toast/toast.js +17 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSAP Module — Animation library loaded from CDN on demand.
|
|
3
|
+
*
|
|
4
|
+
* Provides a lazy loader for GSAP core and ScrollTrigger plugin.
|
|
5
|
+
* No bundling — loaded at runtime from jsDelivr.
|
|
6
|
+
*
|
|
7
|
+
* Usage in components:
|
|
8
|
+
* import { loadGsap } from "@stnd/modules/gsap/client.js";
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
id: "gsap",
|
|
13
|
+
name: "GSAP",
|
|
14
|
+
description: "Animation library with ScrollTrigger — loaded from CDN on demand",
|
|
15
|
+
|
|
16
|
+
metadata: {
|
|
17
|
+
version: "1.0.0",
|
|
18
|
+
tags: ["animation", "scroll", "gsap", "scroll-trigger"],
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Headers core module
|
|
3
|
+
*
|
|
4
|
+
* Exposes the /_headers route via the Standard module system.
|
|
5
|
+
*/
|
|
6
|
+
export default {
|
|
7
|
+
id: "stnd-headers",
|
|
8
|
+
name: "Stnd :: Headers",
|
|
9
|
+
description: "Emits security headers (_headers) from site configuration.",
|
|
10
|
+
routes: [
|
|
11
|
+
{
|
|
12
|
+
path: "/_headers",
|
|
13
|
+
entrypoint: "./route.js",
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
};
|
package/headers/route.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import config from "virtual:stnd/config";
|
|
2
|
+
|
|
3
|
+
export async function GET() {
|
|
4
|
+
const security = config.security || {};
|
|
5
|
+
if (security.enabled === false) {
|
|
6
|
+
return new Response(null, { status: 404 });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const defaults = {
|
|
10
|
+
xFrameOptions: "DENY",
|
|
11
|
+
xContentTypeOptions: "nosniff",
|
|
12
|
+
referrerPolicy: "strict-origin-when-cross-origin",
|
|
13
|
+
hsts: "max-age=31536000; includeSubDomains; preload",
|
|
14
|
+
enableCaching: true,
|
|
15
|
+
permissions: {
|
|
16
|
+
geolocation: [],
|
|
17
|
+
camera: [],
|
|
18
|
+
microphone: [],
|
|
19
|
+
payment: [],
|
|
20
|
+
usb: [],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const xFrameOptions = security.xFrameOptions ?? defaults.xFrameOptions;
|
|
25
|
+
const xContentTypeOptions =
|
|
26
|
+
security.xContentTypeOptions ?? defaults.xContentTypeOptions;
|
|
27
|
+
const referrerPolicy = security.referrerPolicy ?? defaults.referrerPolicy;
|
|
28
|
+
const hsts = security.hsts ?? defaults.hsts;
|
|
29
|
+
const enableCaching = security.enableCaching ?? defaults.enableCaching;
|
|
30
|
+
|
|
31
|
+
const permissionsConfig = {
|
|
32
|
+
...defaults.permissions,
|
|
33
|
+
...(security.permissions || {}),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const permissionsStr = Object.entries(permissionsConfig)
|
|
37
|
+
.map(([key, values]) => {
|
|
38
|
+
const origins = values.length > 0 ? values.join(" ") : "";
|
|
39
|
+
return `${key}=(${origins})`;
|
|
40
|
+
})
|
|
41
|
+
.join(", ");
|
|
42
|
+
|
|
43
|
+
// CSP is intentionally omitted — Astro 6 handles Content-Security-Policy
|
|
44
|
+
// natively via security.csp with proper hash-based script-src and style-src.
|
|
45
|
+
// Duplicating it here would conflict with Astro's generated hashes.
|
|
46
|
+
|
|
47
|
+
let content = `# Security Headers
|
|
48
|
+
# Generated by Standard Framework
|
|
49
|
+
|
|
50
|
+
/*
|
|
51
|
+
Strict-Transport-Security: ${hsts}
|
|
52
|
+
X-Frame-Options: ${xFrameOptions}
|
|
53
|
+
X-Content-Type-Options: ${xContentTypeOptions}
|
|
54
|
+
Referrer-Policy: ${referrerPolicy}
|
|
55
|
+
Permissions-Policy: ${permissionsStr}
|
|
56
|
+
X-DNS-Prefetch-Control: on
|
|
57
|
+
Cross-Origin-Opener-Policy: same-origin
|
|
58
|
+
Cross-Origin-Resource-Policy: same-origin
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
if (enableCaching) {
|
|
62
|
+
content += `
|
|
63
|
+
# Cache Headers — immutable hashed assets
|
|
64
|
+
/_astro/*
|
|
65
|
+
Cache-Control: public, max-age=31536000, immutable
|
|
66
|
+
|
|
67
|
+
/*.css
|
|
68
|
+
Cache-Control: public, max-age=31536000, immutable
|
|
69
|
+
|
|
70
|
+
/*.js
|
|
71
|
+
Cache-Control: public, max-age=31536000, immutable
|
|
72
|
+
|
|
73
|
+
/*.woff2
|
|
74
|
+
Cache-Control: public, max-age=31536000, immutable
|
|
75
|
+
|
|
76
|
+
# Cache Headers — static assets (revalidate periodically)
|
|
77
|
+
/assets/*
|
|
78
|
+
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
|
|
79
|
+
|
|
80
|
+
/*.svg
|
|
81
|
+
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
|
|
82
|
+
|
|
83
|
+
/*.ico
|
|
84
|
+
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return new Response(content, {
|
|
89
|
+
headers: {
|
|
90
|
+
"Content-Type": "text/plain",
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* humans.txt core module
|
|
3
|
+
*
|
|
4
|
+
* Exposes the /humans.txt route via the Standard module system.
|
|
5
|
+
* A small signal of craft — who built this, and with what.
|
|
6
|
+
*/
|
|
7
|
+
export default {
|
|
8
|
+
id: "stnd-humans",
|
|
9
|
+
name: "Stnd :: humans.txt",
|
|
10
|
+
description: "Serves humans.txt — the people and tools behind the site.",
|
|
11
|
+
routes: [
|
|
12
|
+
{
|
|
13
|
+
path: "/humans.txt",
|
|
14
|
+
entrypoint: "./route.js",
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
};
|
package/humans/route.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import config from "virtual:stnd/config";
|
|
2
|
+
|
|
3
|
+
export async function GET() {
|
|
4
|
+
const humans = config.humans || {};
|
|
5
|
+
if (humans.enabled === false) {
|
|
6
|
+
return new Response(null, { status: 404 });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const site = config.site || "";
|
|
10
|
+
const title = config.title || "A Standard Framework Site";
|
|
11
|
+
const language = config.language || "en";
|
|
12
|
+
const author = config.author || {};
|
|
13
|
+
|
|
14
|
+
let content = `/* TEAM */
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
if (author.name) {
|
|
18
|
+
content += ` Chef: ${author.name}\n`;
|
|
19
|
+
}
|
|
20
|
+
if (author.url) {
|
|
21
|
+
content += ` Site: ${author.url}\n`;
|
|
22
|
+
}
|
|
23
|
+
if (author.location) {
|
|
24
|
+
content += ` Location: ${author.location}\n`;
|
|
25
|
+
}
|
|
26
|
+
if (author.contact) {
|
|
27
|
+
content += ` Contact: ${author.contact}\n`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Additional team members from config
|
|
31
|
+
if (Array.isArray(humans.team)) {
|
|
32
|
+
for (const member of humans.team) {
|
|
33
|
+
content += `\n`;
|
|
34
|
+
if (member.role) content += ` ${member.role}: ${member.name || ""}\n`;
|
|
35
|
+
else if (member.name) content += ` Name: ${member.name}\n`;
|
|
36
|
+
if (member.site) content += ` Site: ${member.site}\n`;
|
|
37
|
+
if (member.location) content += ` Location: ${member.location}\n`;
|
|
38
|
+
if (member.contact) content += ` Contact: ${member.contact}\n`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
content += `
|
|
43
|
+
/* THANKS */
|
|
44
|
+
Astro — https://astro.build
|
|
45
|
+
Standard Framework — https://stnd.build
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
if (Array.isArray(humans.thanks)) {
|
|
49
|
+
for (const thanks of humans.thanks) {
|
|
50
|
+
if (typeof thanks === "string") {
|
|
51
|
+
content += ` ${thanks}\n`;
|
|
52
|
+
} else if (thanks.name) {
|
|
53
|
+
content += ` ${thanks.name}${thanks.url ? ` — ${thanks.url}` : ""}\n`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
content += `
|
|
59
|
+
/* SITE */
|
|
60
|
+
Name: ${title}
|
|
61
|
+
Language: ${language}
|
|
62
|
+
Standards: HTML5, CSS3
|
|
63
|
+
Software: Astro, Standard Framework
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
if (site) {
|
|
67
|
+
content += ` URL: ${site}\n`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (humans.components) {
|
|
71
|
+
content += ` Components: ${humans.components}\n`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return new Response(content, {
|
|
75
|
+
headers: {
|
|
76
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Zoom Module
|
|
3
|
+
* Client script lives in @stnd/client — manifest registered here.
|
|
4
|
+
*/
|
|
5
|
+
export default {
|
|
6
|
+
id: "stnd-image-zoom",
|
|
7
|
+
name: "Stnd :: Client :: Image Zoom",
|
|
8
|
+
scripts: [{ src: "@stnd/client/image-zoom", init: "initImageZoom", refresh: "rescanImages" }],
|
|
9
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lab Module Manifest
|
|
3
|
+
*
|
|
4
|
+
* Live CSS token inspector and design debug tools.
|
|
5
|
+
* Registered as a Gold Standard module — every @stnd site ships with this.
|
|
6
|
+
*
|
|
7
|
+
* Zone: stnd:client (hydrated Svelte component, needs client-side interactivity)
|
|
8
|
+
*/
|
|
9
|
+
export default {
|
|
10
|
+
id: "stnd-lab",
|
|
11
|
+
name: "Stnd :: Lab",
|
|
12
|
+
description: "Loads the StandardLab bundle for inspector/debug tools.",
|
|
13
|
+
environment: ["dev"],
|
|
14
|
+
scripts: ["@stnd/lab"],
|
|
15
|
+
|
|
16
|
+
hooks: {
|
|
17
|
+
"stnd:client": [
|
|
18
|
+
{
|
|
19
|
+
ui: "@stnd/lab/Lab.svelte",
|
|
20
|
+
meta: {
|
|
21
|
+
"client:load": true,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Launcher Module Manifest
|
|
3
|
+
*
|
|
4
|
+
* Provides the core launcher engine and UI hooks.
|
|
5
|
+
* Registers the Launcher UI in the stnd:client hook zone so it
|
|
6
|
+
* auto-injects into every page via <Hook zone="stnd:client" />.
|
|
7
|
+
*
|
|
8
|
+
* Runtime API (initLauncher, configureLauncher, launcherToggle, etc.)
|
|
9
|
+
* should be imported from "@stnd/launcher/launcher" — not from this file.
|
|
10
|
+
*/
|
|
11
|
+
export default {
|
|
12
|
+
id: "stnd::launcher",
|
|
13
|
+
name: "Stnd :: Launcher",
|
|
14
|
+
description: "Universal Command Palette Engine",
|
|
15
|
+
dependencies: ["@stnd/modules/keyboard", "@stnd/icon/module"],
|
|
16
|
+
|
|
17
|
+
hooks: {
|
|
18
|
+
"stnd:client": [
|
|
19
|
+
{
|
|
20
|
+
ui: "@stnd/launcher/Launcher.svelte",
|
|
21
|
+
meta: {
|
|
22
|
+
"client:load": true,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maintenance Mode core module
|
|
3
|
+
*
|
|
4
|
+
* Displays a maintenance page and restricts access to the site.
|
|
5
|
+
* Disabled by default — load explicitly via moduleLoad when needed.
|
|
6
|
+
*/
|
|
7
|
+
export default {
|
|
8
|
+
id: "stnd-maintenance",
|
|
9
|
+
name: "Stnd :: Mode Maintenance",
|
|
10
|
+
description: "Affiche une page de construction et restreint l'accès au site.",
|
|
11
|
+
routes: [
|
|
12
|
+
{
|
|
13
|
+
path: "/maintenance",
|
|
14
|
+
entrypoint: "./routes/index.astro",
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
middleware: ["./middleware.js"],
|
|
18
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
|
|
2
|
+
import { defineMiddleware } from "astro:middleware";
|
|
3
|
+
|
|
4
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
5
|
+
console.log("🔒 Maintenance Middleware Check:", context.request.url);
|
|
6
|
+
// 1. Check if maintenance mode is enabled via Environment Variable
|
|
7
|
+
// Cloudflare uses context.locals.runtime.env usually, or import.meta.env for build time vars
|
|
8
|
+
// We check both for robustness (dev vs prod)
|
|
9
|
+
|
|
10
|
+
// const env = context.locals?.runtime?.env || import.meta.env;
|
|
11
|
+
// const isMaintenance = env.MAINTENANCE_MODE === "true";
|
|
12
|
+
|
|
13
|
+
// Mode Maintenance "ON" par défaut si ce folio est chargé.
|
|
14
|
+
const isMaintenance = true;
|
|
15
|
+
|
|
16
|
+
// 2. Allow access to the maintenance page itself and assets
|
|
17
|
+
const pathname = context.url.pathname;
|
|
18
|
+
if (
|
|
19
|
+
pathname === "/maintenance" ||
|
|
20
|
+
pathname === "/maintenance/" ||
|
|
21
|
+
pathname.startsWith("/_astro") ||
|
|
22
|
+
pathname.startsWith("/assets") ||
|
|
23
|
+
pathname.startsWith("/favicon")
|
|
24
|
+
) {
|
|
25
|
+
return next();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 3. Redirect if maintenance is active
|
|
29
|
+
if (isMaintenance) {
|
|
30
|
+
return context.redirect("/maintenance", 307); // 307 Temporary Redirect
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return next();
|
|
34
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<!doctype html>
|
|
6
|
+
<html lang="fr">
|
|
7
|
+
<head>
|
|
8
|
+
<meta charset="UTF-8" />
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
10
|
+
<title>En construction</title>
|
|
11
|
+
<style>
|
|
12
|
+
body {
|
|
13
|
+
display: flex;
|
|
14
|
+
height: 100vh;
|
|
15
|
+
margin: 0;
|
|
16
|
+
align-items: center;
|
|
17
|
+
justify-content: center;
|
|
18
|
+
font-family: system-ui, sans-serif;
|
|
19
|
+
background-color: #f5f5f0;
|
|
20
|
+
color: #1a1a1a;
|
|
21
|
+
text-align: center;
|
|
22
|
+
}
|
|
23
|
+
.container {
|
|
24
|
+
max-width: 600px;
|
|
25
|
+
padding: 2rem;
|
|
26
|
+
}
|
|
27
|
+
h1 {
|
|
28
|
+
margin-bottom: 1rem;
|
|
29
|
+
}
|
|
30
|
+
a {
|
|
31
|
+
color: inherit;
|
|
32
|
+
text-decoration: underline;
|
|
33
|
+
}
|
|
34
|
+
</style>
|
|
35
|
+
</head>
|
|
36
|
+
<body>
|
|
37
|
+
<div class="container">
|
|
38
|
+
<h1>Site en construction</h1>
|
|
39
|
+
<p>Nous revenons très bientôt.</p>
|
|
40
|
+
<p>
|
|
41
|
+
<a href="mailto:contact@lartdenseigner.com">Nous contacter</a>
|
|
42
|
+
</p>
|
|
43
|
+
</div>
|
|
44
|
+
</body>
|
|
45
|
+
</html>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Manifest core module
|
|
3
|
+
*
|
|
4
|
+
* Exposes the /site.webmanifest route via the Standard module system.
|
|
5
|
+
*/
|
|
6
|
+
export default {
|
|
7
|
+
id: "stnd-manifest",
|
|
8
|
+
name: "Stnd :: Manifest",
|
|
9
|
+
routes: [
|
|
10
|
+
{
|
|
11
|
+
path: "/site.webmanifest",
|
|
12
|
+
entrypoint: "./route.js",
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
description: "Serves the web app manifest from site configuration.",
|
|
16
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import config from "virtual:stnd/config";
|
|
2
|
+
|
|
3
|
+
export async function GET() {
|
|
4
|
+
const manifestConfig = config.manifest || {};
|
|
5
|
+
if (manifestConfig.enabled === false) {
|
|
6
|
+
return new Response(null, { status: 404 });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const defaults = {
|
|
10
|
+
name: "My Site",
|
|
11
|
+
short_name: "Site",
|
|
12
|
+
description: "A website",
|
|
13
|
+
start_url: "/",
|
|
14
|
+
display: "standalone",
|
|
15
|
+
orientation: "any",
|
|
16
|
+
theme_color: "#000000",
|
|
17
|
+
background_color: "#ffffff",
|
|
18
|
+
icons: [],
|
|
19
|
+
categories: [],
|
|
20
|
+
screenshots: [],
|
|
21
|
+
shortcuts: [],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const manifest = {
|
|
25
|
+
name: manifestConfig.name || config.title || defaults.name,
|
|
26
|
+
short_name:
|
|
27
|
+
manifestConfig.short_name || config.title || defaults.short_name,
|
|
28
|
+
description:
|
|
29
|
+
manifestConfig.description || config.description || defaults.description,
|
|
30
|
+
start_url: manifestConfig.start_url || defaults.start_url,
|
|
31
|
+
display: manifestConfig.display || defaults.display,
|
|
32
|
+
orientation: manifestConfig.orientation || defaults.orientation,
|
|
33
|
+
theme_color: manifestConfig.theme_color || defaults.theme_color,
|
|
34
|
+
background_color:
|
|
35
|
+
manifestConfig.background_color || defaults.background_color,
|
|
36
|
+
icons: manifestConfig.icons || defaults.icons,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
if (manifestConfig.categories)
|
|
40
|
+
manifest.categories = manifestConfig.categories;
|
|
41
|
+
if (manifestConfig.screenshots)
|
|
42
|
+
manifest.screenshots = manifestConfig.screenshots;
|
|
43
|
+
if (manifestConfig.shortcuts) manifest.shortcuts = manifestConfig.shortcuts;
|
|
44
|
+
|
|
45
|
+
if (config.author && config.author.name) {
|
|
46
|
+
manifest.author = config.author.name;
|
|
47
|
+
if (config.author.url) {
|
|
48
|
+
manifest.author_url = config.author.url;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return new Response(JSON.stringify(manifest, null, 2), {
|
|
53
|
+
headers: {
|
|
54
|
+
"Content-Type": "application/manifest+json",
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
package/math/client.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Math Module — KaTeX Client Initializer
|
|
3
|
+
*
|
|
4
|
+
* Standalone module that detects and renders mathematical notation.
|
|
5
|
+
* Decoupled from the markdown renderer.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const KATEX_VERSION = "0.16.32";
|
|
9
|
+
const KATEX_CDN = `https://cdn.jsdelivr.net/npm/katex@${KATEX_VERSION}/dist`;
|
|
10
|
+
|
|
11
|
+
const RENDER_CONFIG = {
|
|
12
|
+
delimiters: [
|
|
13
|
+
{ left: "$$", right: "$$", display: true },
|
|
14
|
+
{ left: "$", right: "$", display: false },
|
|
15
|
+
{ left: "\\[", right: "\\]", display: true },
|
|
16
|
+
{ left: "\\(", right: "\\)", display: false },
|
|
17
|
+
],
|
|
18
|
+
throwOnError: false,
|
|
19
|
+
trust: false,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Quick check — does the page contain anything that looks like math? */
|
|
23
|
+
function pageHasMath() {
|
|
24
|
+
const scope = document.querySelector("main") || document.body;
|
|
25
|
+
const text = scope.textContent || "";
|
|
26
|
+
if (text.includes("$$")) return true;
|
|
27
|
+
if (/\$[^$\n]+\$/.test(text)) return true;
|
|
28
|
+
if (text.includes("\\(") || text.includes("\\[")) return true;
|
|
29
|
+
const markers = ["\\frac", "\\sum", "\\int", "\\sqrt", "\\alpha", "\\beta"];
|
|
30
|
+
return markers.some((m) => text.includes(m));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Inject the KaTeX stylesheet (idempotent) */
|
|
34
|
+
function ensureStylesheet() {
|
|
35
|
+
if (document.querySelector(`link[href*="katex@${KATEX_VERSION}"]`)) return;
|
|
36
|
+
const link = document.createElement("link");
|
|
37
|
+
link.rel = "stylesheet";
|
|
38
|
+
link.href = `${KATEX_CDN}/katex.min.css`;
|
|
39
|
+
document.head.appendChild(link);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Load a script via <script> tag, returns a promise (idempotent) */
|
|
43
|
+
function loadScript(src) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const existing = document.querySelector(`script[src="${src}"]`);
|
|
46
|
+
if (existing) {
|
|
47
|
+
if (existing.dataset.loaded) resolve();
|
|
48
|
+
else {
|
|
49
|
+
existing.addEventListener("load", () => resolve());
|
|
50
|
+
existing.addEventListener("error", () =>
|
|
51
|
+
reject(new Error(`Failed to load ${src}`)),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const s = document.createElement("script");
|
|
57
|
+
s.src = src;
|
|
58
|
+
s.onload = () => {
|
|
59
|
+
s.dataset.loaded = "1";
|
|
60
|
+
resolve();
|
|
61
|
+
};
|
|
62
|
+
s.onerror = () => reject(new Error(`Failed to load ${src}`));
|
|
63
|
+
document.head.appendChild(s);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let loaded = false;
|
|
68
|
+
let isProcessing = false;
|
|
69
|
+
|
|
70
|
+
/** Dynamically load KaTeX + auto-render, then render math in the page. */
|
|
71
|
+
async function renderMath() {
|
|
72
|
+
if (isProcessing || !pageHasMath()) return;
|
|
73
|
+
isProcessing = true;
|
|
74
|
+
|
|
75
|
+
ensureStylesheet();
|
|
76
|
+
|
|
77
|
+
if (!loaded) {
|
|
78
|
+
try {
|
|
79
|
+
await loadScript(`${KATEX_CDN}/katex.min.js`);
|
|
80
|
+
await loadScript(`${KATEX_CDN}/contrib/auto-render.min.js`);
|
|
81
|
+
loaded = true;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error("[Math] Failed to load KaTeX:", err);
|
|
84
|
+
isProcessing = false; // Ensure processing flag is reset on error
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const render = window.renderMathInElement;
|
|
90
|
+
if (typeof render === "function") {
|
|
91
|
+
const scope = document.querySelector("main") || document.body;
|
|
92
|
+
render(scope, RENDER_CONFIG);
|
|
93
|
+
}
|
|
94
|
+
isProcessing = false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const init = renderMath;
|
|
98
|
+
export const refresh = renderMath;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Math Module — Standalone mathematical notation rendering.
|
|
3
|
+
*
|
|
4
|
+
* Scans the page for LaTeX-style markers ($$, $) and renders them via KaTeX.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
id: "math",
|
|
9
|
+
name: "Math (KaTeX)",
|
|
10
|
+
description: "Mathematical notation rendering with KaTeX",
|
|
11
|
+
|
|
12
|
+
styles: ["./styles.css"],
|
|
13
|
+
scripts: [{ src: "./client.js", init: "init", refresh: "refresh" }],
|
|
14
|
+
|
|
15
|
+
metadata: {
|
|
16
|
+
version: "1.0.0",
|
|
17
|
+
tags: ["math", "katex", "latex"],
|
|
18
|
+
},
|
|
19
|
+
};
|
package/math/styles.css
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.katex,
|
|
2
|
+
.katex-display {
|
|
3
|
+
text-box-trim: none !important;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
Fix Chromium "Aw, Snap! Error code 11"
|
|
8
|
+
Chromium crashes when a block-level element is injected inside a <p> tag
|
|
9
|
+
that has text-box-trim applied.
|
|
10
|
+
1. We turn off text-box-trim on the parent paragraph.
|
|
11
|
+
2. We force the display math to be inline-block instead of block, preventing the illegal DOM structure.
|
|
12
|
+
*/
|
|
13
|
+
p:has(.katex-display) {
|
|
14
|
+
padding-block: var(--space-2);
|
|
15
|
+
font-size: var(--size-lg);
|
|
16
|
+
* {
|
|
17
|
+
text-box-trim: none !important;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.katex-display {
|
|
22
|
+
display: inline-block !important;
|
|
23
|
+
width: 100%;
|
|
24
|
+
text-align: center;
|
|
25
|
+
}
|