@zvndev/darkmown 0.18.0 → 0.20.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/CHANGELOG.md +22 -0
- package/README.md +60 -3
- package/SECURITY.md +39 -4
- package/docs/spec-alignment.md +9 -0
- package/package.json +23 -5
- package/site/_/nav.wd +1 -0
- package/site/pages/loops.wd +22 -0
- package/site/pages/theme.skin +53 -0
- package/site/pages/theme.wd +58 -0
- package/src/builder.js +66 -10
- package/src/cli.js +216 -77
- package/src/compiler/body.js +432 -0
- package/src/compiler/context.js +160 -0
- package/src/compiler/directives.js +1149 -0
- package/src/compiler/frontmatter.js +117 -0
- package/src/compiler/includes.js +116 -0
- package/src/compiler/index.js +22 -0
- package/src/compiler/interpolation.js +189 -0
- package/src/compiler/loops.js +644 -0
- package/src/compiler/markdown.js +233 -0
- package/src/compiler/page.js +267 -0
- package/src/compiler/predicates.js +281 -0
- package/src/compiler.js +26 -2753
- package/src/dev.js +3 -1
- package/src/headers.js +93 -0
- package/src/index.js +22 -0
- package/src/router.js +3 -1
- package/src/runtime.js +83 -59
- package/src/scaffold.js +158 -131
- package/src/skin.js +62 -3
- package/src/statics.js +19 -2
- package/site/_/og.png +0 -0
package/src/skin.js
CHANGED
|
@@ -14,7 +14,8 @@ const aliases = new Map([
|
|
|
14
14
|
* @returns {string}
|
|
15
15
|
*/
|
|
16
16
|
function resolveProp(prop, rest) {
|
|
17
|
-
if (prop === "font" && (/^[\d.]/.test(rest[0] || "") || rest.join(" ").includes("/")))
|
|
17
|
+
if (prop === "font" && (/^[\d.]/.test(rest[0] || "") || rest.join(" ").includes("/")))
|
|
18
|
+
return "font";
|
|
18
19
|
return aliases.get(prop) || prop;
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -53,7 +54,16 @@ export function compileSkin(source) {
|
|
|
53
54
|
/** @type {number | null} */
|
|
54
55
|
let tokenIndent = null;
|
|
55
56
|
/** @type {string[]} */
|
|
57
|
+
let tokenVars = [];
|
|
58
|
+
/** @type {string[]} */
|
|
56
59
|
const rootVars = [];
|
|
60
|
+
/** @type {string[]} */
|
|
61
|
+
const darkVars = [];
|
|
62
|
+
// Manual-toggle variants: `tokens [data-theme=dark]` collects custom
|
|
63
|
+
// properties under an attribute-scoped `:root` so a `:store theme` toggle can
|
|
64
|
+
// flip them with no JS beyond writing the attribute.
|
|
65
|
+
/** @type {Map<string, string[]>} */
|
|
66
|
+
const attrVars = new Map();
|
|
57
67
|
|
|
58
68
|
for (let i = 0; i < lines.length; i++) {
|
|
59
69
|
const { indent, text } = lines[i];
|
|
@@ -61,14 +71,41 @@ export function compileSkin(source) {
|
|
|
61
71
|
// the next line is indented deeper. Everything else is a declaration.
|
|
62
72
|
const opensBlock = i + 1 < lines.length && lines[i + 1].indent > indent;
|
|
63
73
|
|
|
64
|
-
|
|
74
|
+
// `tokens` opens a design-token block emitting `:root { --name: value }`.
|
|
75
|
+
// An optional modifier picks a variant: `tokens dark` collects into a
|
|
76
|
+
// `prefers-color-scheme: dark` override (OS-driven), while a bracketed
|
|
77
|
+
// `tokens [data-theme=dark]` scopes the override to a `:root` attribute for
|
|
78
|
+
// manual toggling. Both reuse the same `--name` properties.
|
|
79
|
+
// Capture the whole modifier (`.+`, not `\S+`) so a malformed bracketed
|
|
80
|
+
// modifier with internal spaces (`[data-theme=dark extra]`) is still routed
|
|
81
|
+
// to normalizeAttr's null fail-safe rather than slipping through as a bogus
|
|
82
|
+
// `tokens …` selector.
|
|
83
|
+
const tokenMatch = /^tokens(?:\s+(.+))?$/.exec(text);
|
|
84
|
+
if (tokenMatch && opensBlock) {
|
|
65
85
|
tokenIndent = indent;
|
|
86
|
+
const modifier = tokenMatch[1];
|
|
87
|
+
if (!modifier) {
|
|
88
|
+
tokenVars = rootVars;
|
|
89
|
+
} else if (modifier === "dark") {
|
|
90
|
+
tokenVars = darkVars;
|
|
91
|
+
} else if (modifier.startsWith("[") && modifier.endsWith("]")) {
|
|
92
|
+
const attr = normalizeAttr(modifier);
|
|
93
|
+
if (attr) {
|
|
94
|
+
const selector = `:root${attr}`;
|
|
95
|
+
tokenVars = attrVars.get(selector) ?? [];
|
|
96
|
+
attrVars.set(selector, tokenVars);
|
|
97
|
+
} else {
|
|
98
|
+
tokenVars = rootVars; // malformed `[…]` modifier → fall back to default :root tokens
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
tokenVars = rootVars;
|
|
102
|
+
}
|
|
66
103
|
continue;
|
|
67
104
|
}
|
|
68
105
|
|
|
69
106
|
if (tokenIndent !== null && indent > tokenIndent) {
|
|
70
107
|
const [name, ...rest] = text.split(/\s+/);
|
|
71
|
-
|
|
108
|
+
tokenVars.push(` --${name}: ${rest.join(" ")};`);
|
|
72
109
|
continue;
|
|
73
110
|
}
|
|
74
111
|
|
|
@@ -96,10 +133,32 @@ export function compileSkin(source) {
|
|
|
96
133
|
css.push(media ? `${media} { ${rule} }` : rule);
|
|
97
134
|
}
|
|
98
135
|
|
|
136
|
+
// Prepend tokens ahead of selector rules. `unshift` reverses insertion order,
|
|
137
|
+
// so push the overrides first to land them after the `:root` defaults.
|
|
138
|
+
for (const [selector, vars] of attrVars) {
|
|
139
|
+
if (vars.length) css.unshift(`${selector} {\n${vars.join("\n")}\n}`);
|
|
140
|
+
}
|
|
141
|
+
if (darkVars.length) {
|
|
142
|
+
css.unshift(`@media (prefers-color-scheme: dark) {\n:root {\n${darkVars.join("\n")}\n}\n}`);
|
|
143
|
+
}
|
|
99
144
|
if (rootVars.length) css.unshift(`:root {\n${rootVars.join("\n")}\n}`);
|
|
100
145
|
return css.join("\n");
|
|
101
146
|
}
|
|
102
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Normalize a bracketed token modifier into a valid attribute selector,
|
|
150
|
+
* quoting the value so `[data-theme=dark]` becomes `[data-theme="dark"]`.
|
|
151
|
+
* Returns null for a malformed modifier (`[]`, `[a=b c]`, injection-shaped
|
|
152
|
+
* input) so the caller can fall back to default `:root` tokens instead of
|
|
153
|
+
* emitting a junk selector.
|
|
154
|
+
* @param {string} bracketed
|
|
155
|
+
* @returns {string | null}
|
|
156
|
+
*/
|
|
157
|
+
function normalizeAttr(bracketed) {
|
|
158
|
+
const m = bracketed.match(/^\[\s*([^\]=\s]+)\s*=\s*"?([^\]"]*)"?\s*\]$/);
|
|
159
|
+
return m ? `[${m[1]}="${m[2]}"]` : null;
|
|
160
|
+
}
|
|
161
|
+
|
|
103
162
|
/**
|
|
104
163
|
* Combine a selector with its parent (handles `&` nesting and comma lists).
|
|
105
164
|
* @param {string} text
|
package/src/statics.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import http from "node:http";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { BASE_SECURITY_HEADERS, REACTIVE_CSP } from "./headers.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Security headers for an HTML response from the local server. The relaxed
|
|
8
|
+
* (reactive) CSP is a superset that satisfies both static and reactive pages,
|
|
9
|
+
* so the runtime server uses it for every HTML response; the build-time
|
|
10
|
+
* outputs (`vercel.json`, `dist/_headers`) tighten static routes per-path.
|
|
11
|
+
* @type {Record<string, string>}
|
|
12
|
+
*/
|
|
13
|
+
const HTML_SECURITY_HEADERS = {
|
|
14
|
+
...BASE_SECURITY_HEADERS,
|
|
15
|
+
"Content-Security-Policy": REACTIVE_CSP
|
|
16
|
+
};
|
|
4
17
|
|
|
5
18
|
/**
|
|
6
19
|
* Serve a built file for a request URL out of `distRoot`, or a 404 page.
|
|
@@ -12,7 +25,7 @@ import path from "node:path";
|
|
|
12
25
|
export function serve(distRoot, url, res) {
|
|
13
26
|
const file = resolvePublicFile(distRoot, url || "/");
|
|
14
27
|
if (!file || !fs.existsSync(file)) {
|
|
15
|
-
res.writeHead(404, { "content-type": "text/html; charset=utf-8" });
|
|
28
|
+
res.writeHead(404, { "content-type": "text/html; charset=utf-8", ...HTML_SECURITY_HEADERS });
|
|
16
29
|
const notFound = path.join(distRoot, "404.html");
|
|
17
30
|
if (fs.existsSync(notFound)) {
|
|
18
31
|
res.end(fs.readFileSync(notFound));
|
|
@@ -21,7 +34,11 @@ export function serve(distRoot, url, res) {
|
|
|
21
34
|
res.end("<h1>Not found</h1><p>This route is hidden or has not been created.</p>");
|
|
22
35
|
return;
|
|
23
36
|
}
|
|
24
|
-
|
|
37
|
+
const type = contentType(file);
|
|
38
|
+
const headers = type.startsWith("text/html")
|
|
39
|
+
? { "content-type": type, ...HTML_SECURITY_HEADERS }
|
|
40
|
+
: { "content-type": type };
|
|
41
|
+
res.writeHead(200, headers);
|
|
25
42
|
fs.createReadStream(file).pipe(res);
|
|
26
43
|
}
|
|
27
44
|
|
package/site/_/og.png
DELETED
|
Binary file
|