@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/dev.js
CHANGED
|
@@ -8,7 +8,9 @@ export const devEventsPath = "/__wd/dev-events";
|
|
|
8
8
|
*/
|
|
9
9
|
export function injectDevClient(html) {
|
|
10
10
|
const script = `<script type="module" src="${devClientPath}"></script>`;
|
|
11
|
-
return html.includes("</body>")
|
|
11
|
+
return html.includes("</body>")
|
|
12
|
+
? html.replace("</body>", `${script}\n</body>`)
|
|
13
|
+
: `${html}\n${script}`;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
/**
|
package/src/headers.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security headers Darkmown emits on every delivery surface — the local
|
|
3
|
+
* `darkmown serve`/preview server (`src/statics.js`), Cloudflare Pages
|
|
4
|
+
* (`dist/_headers`, written by `src/builder.js`), and Vercel (`vercel.json`).
|
|
5
|
+
*
|
|
6
|
+
* `statics.js` and the generated `_headers` derive from the values here, so they
|
|
7
|
+
* never drift. `vercel.json` is the exception: Vercel reads it BEFORE the build
|
|
8
|
+
* runs, so its CSP strings and static-route list are hand-maintained copies, not
|
|
9
|
+
* build output. The `vercel-csp-routes` test cross-checks vercel.json against the
|
|
10
|
+
* built route manifest so a new static route can't silently miss the strict CSP.
|
|
11
|
+
*
|
|
12
|
+
* The CSP is derived from what the framework's own output actually emits
|
|
13
|
+
* (verify against built `dist/` HTML):
|
|
14
|
+
* - External script `/__wd/runtime.js` (same-origin) on reactive pages.
|
|
15
|
+
* - Inline `<script type="application/json" data-wd-state>` (state seed) and
|
|
16
|
+
* inline `<script type="speculationrules">` (prerender) — JSON/speculation
|
|
17
|
+
* blocks can't be nonce'd cleanly, so `script-src 'unsafe-inline'`.
|
|
18
|
+
* - The reactive runtime compiles validated expressions via `new Function`,
|
|
19
|
+
* so reactive pages additionally need `script-src 'unsafe-eval'`.
|
|
20
|
+
* - Inline `<style>` for view transitions — `style-src 'unsafe-inline'`.
|
|
21
|
+
* - `data:` favicon and remote images — `img-src 'self' data: https:`.
|
|
22
|
+
*
|
|
23
|
+
* Static pages (zero framework JS) still emit inline `<style>` and an inline
|
|
24
|
+
* `<script type="speculationrules">`, so they keep `'unsafe-inline'`, but they
|
|
25
|
+
* never call `new Function`, so their CSP drops `'unsafe-eval'`.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `connect-src` for the demo is `'self'`: the framework's own pages only
|
|
30
|
+
* `:fetch` same-origin endpoints. Apps whose `:fetch` targets a remote host
|
|
31
|
+
* must widen this (e.g. to `connect-src 'self' https://api.example.com` or
|
|
32
|
+
* `connect-src *`) by editing their `vercel.json` / `_headers` / serve config.
|
|
33
|
+
* @type {string}
|
|
34
|
+
*/
|
|
35
|
+
const CONNECT_SRC = "connect-src 'self'";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Directives shared by both the static and reactive CSP. Order is cosmetic.
|
|
39
|
+
* @type {string[]}
|
|
40
|
+
*/
|
|
41
|
+
const COMMON_CSP_DIRECTIVES = [
|
|
42
|
+
"default-src 'self'",
|
|
43
|
+
"style-src 'self' 'unsafe-inline'",
|
|
44
|
+
"img-src 'self' data: https:",
|
|
45
|
+
"font-src 'self'",
|
|
46
|
+
CONNECT_SRC,
|
|
47
|
+
"object-src 'none'",
|
|
48
|
+
"base-uri 'self'",
|
|
49
|
+
"frame-ancestors 'self'"
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Relaxed CSP for reactive pages: `new Function` needs `'unsafe-eval'`, the
|
|
54
|
+
* inline state/speculationrules JSON needs `'unsafe-inline'`.
|
|
55
|
+
* @type {string}
|
|
56
|
+
*/
|
|
57
|
+
export const REACTIVE_CSP = [
|
|
58
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
|
|
59
|
+
...COMMON_CSP_DIRECTIVES
|
|
60
|
+
].join("; ");
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Stricter CSP for static pages: no runtime means no `new Function`, so
|
|
64
|
+
* `'unsafe-eval'` is dropped. Inline `<style>`/`<script type=speculationrules>`
|
|
65
|
+
* keep `'unsafe-inline'` on script/style.
|
|
66
|
+
* @type {string}
|
|
67
|
+
*/
|
|
68
|
+
export const STATIC_CSP = ["script-src 'self' 'unsafe-inline'", ...COMMON_CSP_DIRECTIVES].join(
|
|
69
|
+
"; "
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Baseline security headers applied to every HTML response, independent of the
|
|
74
|
+
* CSP. CSP is added per-route (static vs reactive) by the helpers below.
|
|
75
|
+
* @type {Record<string, string>}
|
|
76
|
+
*/
|
|
77
|
+
export const BASE_SECURITY_HEADERS = {
|
|
78
|
+
"X-Content-Type-Options": "nosniff",
|
|
79
|
+
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
80
|
+
"X-Frame-Options": "SAMEORIGIN"
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The full security-header set for one response, including the CSP variant.
|
|
85
|
+
* @param {boolean} reactive Whether the route ships the reactive runtime.
|
|
86
|
+
* @returns {Record<string, string>}
|
|
87
|
+
*/
|
|
88
|
+
export function securityHeaders(reactive) {
|
|
89
|
+
return {
|
|
90
|
+
...BASE_SECURITY_HEADERS,
|
|
91
|
+
"Content-Security-Policy": reactive ? REACTIVE_CSP : STATIC_CSP
|
|
92
|
+
};
|
|
93
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Public API barrel for @zvndev/darkmown.
|
|
2
|
+
// Mirrors the type surface declared in `types/index.d.ts`. Importing the
|
|
3
|
+
// package (`import { buildSite } from "@zvndev/darkmown"`) resolves here via the
|
|
4
|
+
// `exports` map; individual modules stay importable by their own path too.
|
|
5
|
+
|
|
6
|
+
export { buildSite } from "./builder.js";
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
compileDocument,
|
|
10
|
+
compilePage,
|
|
11
|
+
escapeHtml,
|
|
12
|
+
loopKeyOf,
|
|
13
|
+
parseFrontmatter
|
|
14
|
+
} from "./compiler.js";
|
|
15
|
+
|
|
16
|
+
export { createPaths, isHiddenName, pageExtensions } from "./config.js";
|
|
17
|
+
export { devClientScript, injectDevClient } from "./dev.js";
|
|
18
|
+
export { discoverRoutes, outputPathForRoute, routeFromFile } from "./router.js";
|
|
19
|
+
|
|
20
|
+
export { initProject } from "./scaffold.js";
|
|
21
|
+
export { compileSkin } from "./skin.js";
|
|
22
|
+
export { contentType, resolvePublicFile, serve } from "./statics.js";
|
package/src/router.js
CHANGED
|
@@ -43,7 +43,9 @@ export function discoverRoutes(routesRoot) {
|
|
|
43
43
|
const seen = new Map();
|
|
44
44
|
for (const route of routes) {
|
|
45
45
|
if (seen.has(route.route)) {
|
|
46
|
-
throw new Error(
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Duplicate route "${route.route}" from ${seen.get(route.route)} and ${route.file}`
|
|
48
|
+
);
|
|
47
49
|
}
|
|
48
50
|
seen.set(route.route, route.file);
|
|
49
51
|
}
|
package/src/runtime.js
CHANGED
|
@@ -246,9 +246,15 @@ function loopKeyOf(item, counts) {
|
|
|
246
246
|
* @returns {void}
|
|
247
247
|
*/
|
|
248
248
|
function fillItem(node, item, meta = {}) {
|
|
249
|
+
// Nested item-relative loops (data-wd-loop-item) own their descendants' binds;
|
|
250
|
+
// when present, `skip` excludes a node living inside one so the outer row's
|
|
251
|
+
// text/meta/each-if/class passes never overwrite an inner row's values.
|
|
252
|
+
const inners = node.querySelectorAll("[data-wd-loop-item]");
|
|
253
|
+
const skip = inners.length ? (/** @type {Element} */ el) => el.closest("[data-wd-loop-item]") : () => false;
|
|
249
254
|
// querySelectorAll skips <template> content, so this sees only the outermost
|
|
250
255
|
// regions; recursing into each injected branch fills the rest.
|
|
251
256
|
for (const region of node.querySelectorAll("[data-wd-each-if]")) {
|
|
257
|
+
if (skip(region)) continue;
|
|
252
258
|
const expr = region.getAttribute("data-wd-if-expr");
|
|
253
259
|
const m = region.getAttribute("data-wd-meta");
|
|
254
260
|
const value = expr ? evalPredicate(expr, item) : (m ? meta[m] : getPath(item, region.getAttribute("data-wd-path")));
|
|
@@ -262,13 +268,17 @@ function fillItem(node, item, meta = {}) {
|
|
|
262
268
|
? [node, ...node.querySelectorAll("[data-wd-each]")]
|
|
263
269
|
: [...node.querySelectorAll("[data-wd-each]")];
|
|
264
270
|
for (const target of targets) {
|
|
271
|
+
if (skip(target)) continue;
|
|
265
272
|
target.textContent = getPath(item, target.getAttribute("data-wd-path")) ?? "";
|
|
266
273
|
}
|
|
267
274
|
for (const marker of node.querySelectorAll("[data-wd-each-meta]")) {
|
|
275
|
+
if (skip(marker)) continue;
|
|
268
276
|
marker.textContent = meta[marker.getAttribute("data-wd-each-meta") || ""] ?? "";
|
|
269
277
|
}
|
|
270
278
|
const classNodes = node.matches("[data-wd-each-class]") ? [node, ...node.querySelectorAll("[data-wd-each-class]")] : [...node.querySelectorAll("[data-wd-each-class]")];
|
|
271
|
-
for (const el of classNodes) classToggle(el, el.getAttribute("data-wd-each-class"), item);
|
|
279
|
+
for (const el of classNodes) { if (!skip(el)) classToggle(el, el.getAttribute("data-wd-each-class"), item); }
|
|
280
|
+
// Reconcile nested loops last, against the now-settled outer row.
|
|
281
|
+
for (const region of inners) reconcile(region, getPath(item, region.getAttribute("data-wd-loop-item")));
|
|
272
282
|
}
|
|
273
283
|
|
|
274
284
|
/**
|
|
@@ -335,6 +345,75 @@ function renderIf(node) {
|
|
|
335
345
|
for (const nested of output.querySelectorAll("[data-wd-if]")) renderIf(nested);
|
|
336
346
|
}
|
|
337
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Reconcile one loop region (top-level or nested) from a resolved row source:
|
|
350
|
+
* filter (`where`) → pipeline (sort/reverse/offset/limit) → @empty → keyed
|
|
351
|
+
* reconcile. Each surviving row is `fillItem`-ed, which itself recurses into any
|
|
352
|
+
* nested item-relative loops. Shared by the global pass and `fillItem`.
|
|
353
|
+
* @param {Element} region
|
|
354
|
+
* @param {any} rows Source list (state-resolved globally; item-relative inside fillItem).
|
|
355
|
+
* @returns {void}
|
|
356
|
+
*/
|
|
357
|
+
function reconcile(region, rows) {
|
|
358
|
+
const template = /** @type {HTMLTemplateElement | null} */ (region.querySelector("template[data-wd-loop-template]"));
|
|
359
|
+
const out = region.querySelector("[data-wd-loop-out]");
|
|
360
|
+
if (!template || !out) return;
|
|
361
|
+
let list = Array.isArray(rows) ? rows.slice() : [];
|
|
362
|
+
const where = region.getAttribute("data-wd-loop-where");
|
|
363
|
+
if (where) {
|
|
364
|
+
const predicate = loopPredicate(where);
|
|
365
|
+
list = list.filter((/** @type {any} */ item) => {
|
|
366
|
+
try {
|
|
367
|
+
return predicate((/** @type {string | null} */ path) => getPath(item, path), (/** @type {string} */ k, /** @type {string} */ r) => getPath(state[k], r || ""), containsFn);
|
|
368
|
+
} catch (error) {
|
|
369
|
+
warn(where, error);
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
list = pipeline(list, region);
|
|
375
|
+
|
|
376
|
+
// Empty branch: clone the [data-wd-loop-empty] template into the output.
|
|
377
|
+
const emptyTpl = /** @type {HTMLTemplateElement | null} */ (region.querySelector("template[data-wd-loop-empty]"));
|
|
378
|
+
if (!list.length && emptyTpl) {
|
|
379
|
+
if (out.getAttribute("data-wd-empty") !== "1") {
|
|
380
|
+
out.textContent = "";
|
|
381
|
+
for (const child of [...emptyTpl.content.children]) out.appendChild(child.cloneNode(true));
|
|
382
|
+
out.setAttribute("data-wd-empty", "1");
|
|
383
|
+
}
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (out.getAttribute("data-wd-empty") === "1") { out.textContent = ""; out.removeAttribute("data-wd-empty"); }
|
|
387
|
+
|
|
388
|
+
/** @type {Map<string, WdRow>} */
|
|
389
|
+
const existing = new Map();
|
|
390
|
+
for (const child of [...out.children]) {
|
|
391
|
+
existing.set(child.getAttribute("data-wd-loop-key") || "", /** @type {WdRow} */ (child));
|
|
392
|
+
}
|
|
393
|
+
/** @type {Map<string, number>} */
|
|
394
|
+
const counts = new Map();
|
|
395
|
+
/** @type {Set<string>} */
|
|
396
|
+
const used = new Set();
|
|
397
|
+
const count = list.length;
|
|
398
|
+
for (let i = 0; i < count; i++) {
|
|
399
|
+
const item = list[i];
|
|
400
|
+
const key = loopKeyOf(item, counts);
|
|
401
|
+
let node = existing.get(key);
|
|
402
|
+
if (!node || used.has(key)) {
|
|
403
|
+
node = /** @type {WdRow} */ (template.content.firstElementChild?.cloneNode(true));
|
|
404
|
+
if (!node) continue;
|
|
405
|
+
node.setAttribute("data-wd-loop-key", key);
|
|
406
|
+
}
|
|
407
|
+
used.add(key);
|
|
408
|
+
fillItem(node, item, { index: i, number: i + 1, first: i === 0, last: i === count - 1, count });
|
|
409
|
+
node.__wdItem = item; /** let per-row actions resolve which row was clicked */
|
|
410
|
+
out.appendChild(node);
|
|
411
|
+
}
|
|
412
|
+
for (const [key, node] of existing) {
|
|
413
|
+
if (!used.has(key)) node.remove();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
338
417
|
/**
|
|
339
418
|
* Synchronously render the whole document from current `state`:
|
|
340
419
|
* computed → if-regions → keyed loop reconcile → text/input binds.
|
|
@@ -348,69 +427,14 @@ function renderNow() {
|
|
|
348
427
|
// bindings (data-wd-each-class) are handled per item inside fillItem.
|
|
349
428
|
for (const el of document.querySelectorAll("[data-wd-class]")) classToggle(el, el.getAttribute("data-wd-class"), undefined);
|
|
350
429
|
|
|
430
|
+
// Nested item-relative loops use data-wd-loop-item, so this top-level query
|
|
431
|
+
// skips them; fillItem reconciles those against their enclosing row.
|
|
351
432
|
for (const region of document.querySelectorAll("[data-wd-loop]")) {
|
|
352
433
|
const key = region.getAttribute("data-wd-loop");
|
|
353
434
|
const data = region.getAttribute("data-wd-loop-data");
|
|
354
435
|
/** Source may be a dotted path (e.g. team.members) read off state via getPath. */
|
|
355
436
|
const dot = key ? key.indexOf(".") : -1;
|
|
356
|
-
|
|
357
|
-
const template = /** @type {HTMLTemplateElement | null} */ (region.querySelector("template[data-wd-loop-template]"));
|
|
358
|
-
const out = region.querySelector("[data-wd-loop-out]");
|
|
359
|
-
if (!template || !out) continue;
|
|
360
|
-
let list = Array.isArray(rows) ? rows.slice() : [];
|
|
361
|
-
const where = region.getAttribute("data-wd-loop-where");
|
|
362
|
-
if (where) {
|
|
363
|
-
const predicate = loopPredicate(where);
|
|
364
|
-
list = list.filter((/** @type {any} */ item) => {
|
|
365
|
-
try {
|
|
366
|
-
return predicate((/** @type {string | null} */ path) => getPath(item, path), (/** @type {string} */ k, /** @type {string} */ r) => getPath(state[k], r || ""), containsFn);
|
|
367
|
-
} catch (error) {
|
|
368
|
-
warn(where, error);
|
|
369
|
-
return false;
|
|
370
|
-
}
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
list = pipeline(list, region);
|
|
374
|
-
|
|
375
|
-
// Empty branch: clone the [data-wd-loop-empty] template into the output.
|
|
376
|
-
const emptyTpl = /** @type {HTMLTemplateElement | null} */ (region.querySelector("template[data-wd-loop-empty]"));
|
|
377
|
-
if (!list.length && emptyTpl) {
|
|
378
|
-
if (out.getAttribute("data-wd-empty") !== "1") {
|
|
379
|
-
out.textContent = "";
|
|
380
|
-
for (const child of [...emptyTpl.content.children]) out.appendChild(child.cloneNode(true));
|
|
381
|
-
out.setAttribute("data-wd-empty", "1");
|
|
382
|
-
}
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
if (out.getAttribute("data-wd-empty") === "1") { out.textContent = ""; out.removeAttribute("data-wd-empty"); }
|
|
386
|
-
|
|
387
|
-
/** @type {Map<string, WdRow>} */
|
|
388
|
-
const existing = new Map();
|
|
389
|
-
for (const child of [...out.children]) {
|
|
390
|
-
existing.set(child.getAttribute("data-wd-loop-key") || "", /** @type {WdRow} */ (child));
|
|
391
|
-
}
|
|
392
|
-
/** @type {Map<string, number>} */
|
|
393
|
-
const counts = new Map();
|
|
394
|
-
/** @type {Set<string>} */
|
|
395
|
-
const used = new Set();
|
|
396
|
-
const count = list.length;
|
|
397
|
-
for (let i = 0; i < count; i++) {
|
|
398
|
-
const item = list[i];
|
|
399
|
-
const key = loopKeyOf(item, counts);
|
|
400
|
-
let node = existing.get(key);
|
|
401
|
-
if (!node || used.has(key)) {
|
|
402
|
-
node = /** @type {WdRow} */ (template.content.firstElementChild?.cloneNode(true));
|
|
403
|
-
if (!node) continue;
|
|
404
|
-
node.setAttribute("data-wd-loop-key", key);
|
|
405
|
-
}
|
|
406
|
-
used.add(key);
|
|
407
|
-
fillItem(node, item, { index: i, number: i + 1, first: i === 0, last: i === count - 1, count });
|
|
408
|
-
node.__wdItem = item; /** let per-row actions resolve which row was clicked */
|
|
409
|
-
out.appendChild(node);
|
|
410
|
-
}
|
|
411
|
-
for (const [key, node] of existing) {
|
|
412
|
-
if (!used.has(key)) node.remove();
|
|
413
|
-
}
|
|
437
|
+
reconcile(region, key ? (dot < 0 ? state[key] : getPath(state[key.slice(0, dot)], key.slice(dot + 1))) : (data ? JSON.parse(data) : []));
|
|
414
438
|
}
|
|
415
439
|
|
|
416
440
|
for (const node of document.querySelectorAll("[data-wd-bind]")) {
|
package/src/scaffold.js
CHANGED
|
@@ -12,136 +12,164 @@ const darkmownVersion = JSON.parse(
|
|
|
12
12
|
*/
|
|
13
13
|
export function initProject(root) {
|
|
14
14
|
fs.mkdirSync(root, { recursive: true });
|
|
15
|
-
writeNew(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"",
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
"
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
"",
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
"",
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
""
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
"",
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
15
|
+
writeNew(
|
|
16
|
+
root,
|
|
17
|
+
"package.json",
|
|
18
|
+
JSON.stringify(
|
|
19
|
+
{
|
|
20
|
+
name: packageNameFromRoot(root),
|
|
21
|
+
private: true,
|
|
22
|
+
scripts: {
|
|
23
|
+
dev: "darkmown dev",
|
|
24
|
+
build: "darkmown build",
|
|
25
|
+
preview: "darkmown serve"
|
|
26
|
+
},
|
|
27
|
+
devDependencies: {
|
|
28
|
+
"@zvndev/darkmown": `^${darkmownVersion}`
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
null,
|
|
32
|
+
2
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
writeNew(
|
|
36
|
+
root,
|
|
37
|
+
"site/pages/index.wd",
|
|
38
|
+
[
|
|
39
|
+
"---",
|
|
40
|
+
"title: My Darkmown site",
|
|
41
|
+
"---",
|
|
42
|
+
"",
|
|
43
|
+
"@include /nav.wd",
|
|
44
|
+
"",
|
|
45
|
+
"<main>",
|
|
46
|
+
"",
|
|
47
|
+
"# My Darkmown site",
|
|
48
|
+
"",
|
|
49
|
+
"Plain Markdown works. Rename to `.wd` when you want directives.",
|
|
50
|
+
"",
|
|
51
|
+
":state count = 0",
|
|
52
|
+
"",
|
|
53
|
+
"Count: { count }",
|
|
54
|
+
"",
|
|
55
|
+
':button "Increment" -> count++',
|
|
56
|
+
"",
|
|
57
|
+
':state todos = ["Write a page"]',
|
|
58
|
+
"",
|
|
59
|
+
"@loop todos into todo",
|
|
60
|
+
"- { todo }",
|
|
61
|
+
"@endloop",
|
|
62
|
+
"",
|
|
63
|
+
':button "Add" -> todos += "Add a loop"',
|
|
64
|
+
"",
|
|
65
|
+
"</main>"
|
|
66
|
+
].join("\n")
|
|
67
|
+
);
|
|
68
|
+
writeNew(
|
|
69
|
+
root,
|
|
70
|
+
"site/pages/index.skin",
|
|
71
|
+
[
|
|
72
|
+
"tokens",
|
|
73
|
+
" ink #172026",
|
|
74
|
+
" paper #f7f2e8",
|
|
75
|
+
" accent #0f8b8d",
|
|
76
|
+
"",
|
|
77
|
+
"page",
|
|
78
|
+
" margin 0",
|
|
79
|
+
" font ui-sans-serif, system-ui, sans-serif",
|
|
80
|
+
" color $ink",
|
|
81
|
+
" bg $paper",
|
|
82
|
+
"",
|
|
83
|
+
"main",
|
|
84
|
+
" max-width 760px",
|
|
85
|
+
" margin 0 auto",
|
|
86
|
+
" padding 3rem 1.5rem",
|
|
87
|
+
"",
|
|
88
|
+
"nav",
|
|
89
|
+
" display flex",
|
|
90
|
+
" align-items center",
|
|
91
|
+
" justify-content space-between",
|
|
92
|
+
" gap 1rem",
|
|
93
|
+
" max-width 760px",
|
|
94
|
+
" margin 0 auto",
|
|
95
|
+
" padding 1.25rem 1.5rem",
|
|
96
|
+
"",
|
|
97
|
+
"nav strong",
|
|
98
|
+
" font-size 1.1rem",
|
|
99
|
+
"",
|
|
100
|
+
"nav .links",
|
|
101
|
+
" display flex",
|
|
102
|
+
" gap 1.25rem",
|
|
103
|
+
"",
|
|
104
|
+
"nav a",
|
|
105
|
+
" color $ink",
|
|
106
|
+
" text-decoration none",
|
|
107
|
+
" opacity .8",
|
|
108
|
+
"",
|
|
109
|
+
"nav a:hover",
|
|
110
|
+
" opacity 1",
|
|
111
|
+
" color $accent",
|
|
112
|
+
"",
|
|
113
|
+
"button",
|
|
114
|
+
" bg $accent",
|
|
115
|
+
" color white",
|
|
116
|
+
" border 0",
|
|
117
|
+
" padding .75rem 1rem",
|
|
118
|
+
" radius 8px"
|
|
119
|
+
].join("\n")
|
|
120
|
+
);
|
|
121
|
+
writeNew(
|
|
122
|
+
root,
|
|
123
|
+
"site/_/nav.wd",
|
|
124
|
+
[
|
|
125
|
+
"<nav>",
|
|
126
|
+
" <strong>My site</strong>",
|
|
127
|
+
' <span class="links">',
|
|
128
|
+
' <a href="/">Home</a>',
|
|
129
|
+
' <a href="/about/">About</a>',
|
|
130
|
+
" </span>",
|
|
131
|
+
"</nav>"
|
|
132
|
+
].join("\n")
|
|
133
|
+
);
|
|
134
|
+
writeNew(
|
|
135
|
+
root,
|
|
136
|
+
"site/pages/about.md",
|
|
137
|
+
[
|
|
138
|
+
"---",
|
|
139
|
+
"title: About",
|
|
140
|
+
"---",
|
|
141
|
+
"",
|
|
142
|
+
"# About",
|
|
143
|
+
"",
|
|
144
|
+
"This page is plain Markdown (`.md`) — strict CommonMark, zero framework JavaScript.",
|
|
145
|
+
"Rename it to `.wd` when you want directives like state, loops, or forms.",
|
|
146
|
+
"",
|
|
147
|
+
"[Back home](/)"
|
|
148
|
+
].join("\n")
|
|
149
|
+
);
|
|
150
|
+
writeNew(
|
|
151
|
+
root,
|
|
152
|
+
"README.md",
|
|
153
|
+
[
|
|
154
|
+
"# My Darkmown site",
|
|
155
|
+
"",
|
|
156
|
+
"Run `npm install` and `npm run dev` to start the live compiler.",
|
|
157
|
+
"",
|
|
158
|
+
"## Commands",
|
|
159
|
+
"",
|
|
160
|
+
"- `npm run dev` — live compiler on http://localhost:5173 with hot reload.",
|
|
161
|
+
"- `npm run build` — compile the site to `dist/`.",
|
|
162
|
+
"- `npm run preview` — serve the built `dist/` locally.",
|
|
163
|
+
"",
|
|
164
|
+
"## Layout",
|
|
165
|
+
"",
|
|
166
|
+
"- Pages live in `site/pages`.",
|
|
167
|
+
"- Shared includes live in `site/_`.",
|
|
168
|
+
"- Hidden route files start with `.` or `-`.",
|
|
169
|
+
"- Colocated `index.skin` and `index.js` attach automatically.",
|
|
170
|
+
"- Any other file under `site/pages` (images, fonts, …) copies to `dist/` at its own path."
|
|
171
|
+
].join("\n")
|
|
172
|
+
);
|
|
145
173
|
return { root };
|
|
146
174
|
}
|
|
147
175
|
|
|
@@ -159,7 +187,6 @@ function writeNew(root, file, content) {
|
|
|
159
187
|
fs.writeFileSync(target, `${content}\n`);
|
|
160
188
|
}
|
|
161
189
|
|
|
162
|
-
|
|
163
190
|
/**
|
|
164
191
|
* Derive a safe default npm package name from the scaffold target directory.
|
|
165
192
|
* @param {string} root Absolute target directory.
|