@zvndev/darkmown 1.2.1 → 1.4.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/AGENTS.md +2 -0
- package/CHANGELOG.md +18 -0
- package/README.md +129 -1
- package/docs/spec-alignment.md +24 -0
- package/package.json +2 -1
- package/site/_/base.skin +14 -0
- package/site/_/nav.wd +1 -0
- package/site/_/scoped-promo.skin +43 -0
- package/site/_/scoped-promo.wd +10 -0
- package/site/_/scoped-stat.skin +32 -0
- package/site/_/scoped-stat.wd +9 -0
- package/site/pages/blog/drafts-that-never-leak.md +16 -0
- package/site/pages/blog/index.md +13 -0
- package/site/pages/blog/shipping-feeds.md +17 -0
- package/site/pages/blog/unfinished-thoughts.md +13 -0
- package/site/pages/docs/index.wd +70 -0
- package/site/pages/highlight/index.skin +78 -0
- package/site/pages/highlight/index.wd +142 -0
- package/site/pages/index.wd +1 -0
- package/site/pages/scoped/index.skin +23 -0
- package/site/pages/scoped/index.wd +47 -0
- package/src/builder.js +239 -10
- package/src/cli.js +51 -7
- package/src/compiler/context.js +10 -1
- package/src/compiler/directives.js +11 -5
- package/src/compiler/includes.js +48 -1
- package/src/compiler/index.js +1 -1
- package/src/compiler/markdown.js +7 -2
- package/src/compiler/page.js +58 -7
- package/src/compiler.js +2 -1
- package/src/dev.js +34 -5
- package/src/feeds.js +276 -0
- package/src/highlight.js +126 -0
- package/src/router.js +35 -3
- package/src/skin.js +147 -3
- package/src/templates/blog/site/pages/index.wd +1 -0
- package/src/templates/blog/site/pages/posts/hello-darkmown.md +2 -0
- package/src/templates/blog/site/pages/posts/markdown-native.md +2 -0
package/AGENTS.md
CHANGED
|
@@ -253,6 +253,8 @@ input:focus-visible
|
|
|
253
253
|
```
|
|
254
254
|
Always include: a tokens block, a real type scale, generous spacing, a `max-width` centered `main`, styled `button`/`input` with `:hover`/`:focus-visible` states, and a responsive `@media` rule (`.skin` supports `@media` — its indented rules are wrapped automatically). The `font` property is the CSS shorthand when it leads with a size (`font 16px/1.6 system-ui`) and `font-family` otherwise. Prefer `.skin`; a plain `<style>` block in a `.wd` also works.
|
|
255
255
|
|
|
256
|
+
**Scoped styles — opt in with `scoped`.** A `.skin` is **global** by default (style site-wide; the right default for your design system). To stop a reusable component's classes from clashing with the rest of the page, make the **first line** of its `.skin` the word `scoped` — its selectors then only match the component it's colocated with (a page skin scopes that page; an include skin scopes just that include's subtree). You still write `class="card"`; the build stamps a `data-wd-scope` attribute and rewrites the selectors — zero runtime, a static page stays static. **Tokens stay global** (a `tokens`/`tokens dark` block still emits `:root` vars, so `$accent` and dark mode keep working). Escape a single rule with `:global(.x)` (whole-selector only). `page`/`*`/`html`/`body`/`::selection` are a compile error in a scoped skin — those belong in a global skin.
|
|
257
|
+
|
|
256
258
|
**Layout gotcha — CTAs and link rows.** Markdown wraps adjacent links/buttons on consecutive lines into a single `<p>`, so putting `display:flex` on their container won't space them. To lay out a row of buttons or nav links, wrap them in explicit raw HTML and style that:
|
|
257
259
|
```wd
|
|
258
260
|
<div class="actions">
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Darkmown are documented here. Versions follow [semver](https://semver.org); pre-1.0 minor versions may contain breaking changes.
|
|
4
4
|
|
|
5
|
+
## 1.4.0 — 2026-06-28
|
|
6
|
+
|
|
7
|
+
Scoped styles — the long-standing "`.skin` selectors are global" gap, closed. Opt-in, compile-time, and **zero runtime** (the shared runtime is still 7459 B gzipped; nothing new ships to the client but one HTML attribute and one attribute selector).
|
|
8
|
+
|
|
9
|
+
- **`scoped` skins.** Put `scoped` as the first line of a `.skin` file and its selectors apply only to the page or include it ships with — so two components can reuse the same class name (`.card`) without colliding. The compiler appends a per-file `[data-wd-scope="wd-xxxx"]` attribute to each selector's subject and stamps the matching subtree's HTML; the scope id is a stable hash of the skin's path. Selectors compose as you'd expect: descendant selectors scope only the subject, comma lists scope each branch, `@media`/`@supports` wrappers pass through, and `&`-nesting/pseudo-classes resolve before the attribute is attached.
|
|
10
|
+
- **Tokens stay global.** A `tokens` / `tokens dark` block in a `scoped` skin still emits global `:root` custom properties, so `var(--…)`, dark mode, `:theme`, and the `$code-*` highlight tokens keep working across the whole site — only your component's selector rules are scoped.
|
|
11
|
+
- **`:global(...)` escape hatch** for a whole selector that must stay global (e.g. a class you portal elsewhere). Page-level selectors (`page`, `*`, `html`, `body`, `::selection`) are rejected in a scoped skin with a corrective hint — those belong in a global design-system skin.
|
|
12
|
+
- **Fully backward compatible.** A `.skin` without the `scoped` marker is unchanged, byte-for-byte (covered by a golden test). Existing global skins — `base.skin`, `nav.skin`, every scaffolded project — keep styling the whole site exactly as before. Unused scoped selectors produce a build *warning* (never silently removed, since a colocated `.js` behavior may add the class at runtime).
|
|
13
|
+
|
|
14
|
+
## 1.3.0 — 2026-06-28
|
|
15
|
+
|
|
16
|
+
The "table-stakes batteries" release — the build-time, zero-JS features a markdown site is expected to ship. The runtime is unchanged (still 7459 B gzipped, well under the 8 KB budget); everything here is compile-time HTML/CSS or build output.
|
|
17
|
+
|
|
18
|
+
- **Syntax highlighting.** Fenced code blocks with a known language are highlighted at compile time (via `highlight.js` through markdown-it) — HTML plus a small stylesheet, **zero client JS**. Colors are driven by a new `$code-*` skin-token set (`code-bg`, `code-fg`, `code-keyword`, `code-string`, `code-comment`, `code-function`, `code-number`, `code-punctuation`), so code **dark-modes for free** through the existing `tokens dark` / `:theme` system — override any token in your own `.skin`. The stylesheet (`/__wd/highlight.css`) ships **only on pages that contain a highlighted block** (pay-for-what-you-use). Unknown/absent languages degrade to plain escaped `<code>`; inline code and line numbers are intentionally excluded; a prose+code page stays `runtime: false`.
|
|
19
|
+
- **RSS + sitemap.xml + robots.txt.** `darkmown build` now emits `sitemap.xml`, `rss.xml`, and `robots.txt` at build time. Site identity lives in **one reserved home-page frontmatter field, `site_url`** (absolute origin) — which reuses the home `title`/`description` as the RSS channel and acts as the trigger; no config file is introduced. The sitemap lists every published route with a `<lastmod>` (frontmatter `date` → git last-commit date → file mtime). Any page with a `date:` becomes an RSS item (newest-first, capped; description from `excerpt:` → `description:` → first paragraph of a `.md`); an `<link rel="alternate" type="application/rss+xml">` is injected into every page `<head>`. With no `site_url`, `robots.txt` still emits (without the `Sitemap:` line) and the feeds are skipped with a build hint — never a crash.
|
|
20
|
+
- **Drafts.** `draft: true` frontmatter excludes a page from a production build — gone from `dist`, `routes.json`, the sitemap, and the feed (even if it has a `date:`). `darkmown dev` still builds and serves drafts with a dev-only banner; `darkmown build --drafts` includes them (for staging). Drafts are filtered at route discovery, so a feed can never leak one. This is distinct from the permanent `.`/`-`/`_` filename-prefix hiding: use a prefix to hide a file forever, `draft: true` to hide a page you intend to publish later.
|
|
21
|
+
- **New public frontmatter:** `site_url` (home page), `draft`, `date`, `excerpt`. New build flag: `--drafts`. The `blog` starter template now ships working feeds out of the box.
|
|
22
|
+
|
|
5
23
|
## 1.2.1 — 2026-06-27
|
|
6
24
|
|
|
7
25
|
Repository hygiene and documentation polish from a gold-standard audit (50/50 — every dimension at React-tier). No runtime, API, or behavior change; the shipped package is byte-identical except for one source comment.
|
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ npm run dev # live demo site — the same site that runs darkmown.com
|
|
|
52
52
|
|
|
53
53
|
- `darkmown init [dir] [--template <name>]` scaffolds a new site from a template (`starter`, `blog`, `store`, `dashboard`, `landing`).
|
|
54
54
|
- `darkmown dev` starts the live compiler with browser reload, an in-browser error overlay, and a local runner for `api/*` functions.
|
|
55
|
-
- `darkmown build [--target cloudflare]` writes static output to `dist
|
|
55
|
+
- `darkmown build [--target cloudflare] [--drafts]` writes static output to `dist` (plus `sitemap.xml`/`rss.xml`/`robots.txt`); `--drafts` includes `draft: true` pages for staging.
|
|
56
56
|
- `darkmown deploy <vercel|cloudflare> [--prod]` builds and deploys via the platform CLI.
|
|
57
57
|
- `darkmown serve` previews the built `dist` locally.
|
|
58
58
|
- `darkmown version` prints the installed package version.
|
|
@@ -113,6 +113,8 @@ tags: [sales, revenue, "q1, q2"]
|
|
|
113
113
|
|
|
114
114
|
Three keys also drive the document `<head>`: `title` sets `<title>`, `description` adds the meta description plus Open Graph / Twitter tags, and `image` (an absolute URL) sets the social-share preview (`og:image` / `twitter:image` and a `summary_large_image` card).
|
|
115
115
|
|
|
116
|
+
A few reserved keys drive **drafts and feeds** (see [SEO & feeds](#seo--feeds-sitemap-rss-robots) below): `draft: true` excludes a page from production builds; `site_url` on the home page turns on `sitemap.xml` + `rss.xml`; `date:` marks a page as a blog post (it lands in `rss.xml` and sets the page's `<lastmod>`); `excerpt:` is the RSS summary.
|
|
117
|
+
|
|
116
118
|
`transitions: true` opts a page into **instant, flash-free navigation** — zero JavaScript, all declarative. It emits a directional fade+slide **view transition** for the page swap (old lifts up and out, new rises up and in — replacing the default cross-fade, which left both pages ghosted at ~50 % opacity mid-navigation), plus a **`<script type="speculationrules">` prerender** hint that renders the next same-origin page on hover/pointerdown so the click activates an already-painted page (no white render-gap flash). It honors `prefers-reduced-motion`. Only same-origin pages that both opt in transition; browsers without support — or with page-preloading disabled — navigate normally. Off by default; opt out with `transitions: false`. Mark a link `{.no-prefetch}` to exclude it from prerendering. (Chrome disables prerendering while DevTools is open, so test the built site with DevTools closed.)
|
|
117
119
|
|
|
118
120
|
## Static assets
|
|
@@ -135,6 +137,55 @@ Hidden/private page paths stay private: any `site/pages` asset below a path segm
|
|
|
135
137
|
|
|
136
138
|
Use the shelf for assets shared across many pages; colocate the ones that belong to a single page or section. Both ship untouched with the correct content-type.
|
|
137
139
|
|
|
140
|
+
## SEO & feeds (sitemap, RSS, robots)
|
|
141
|
+
|
|
142
|
+
`darkmown build` emits the crawler files a site needs — all build-time, zero client JS — driven by a little frontmatter. There is no config file: the **home page** (`site/pages/index.md` or `.wd`) carries the site's identity.
|
|
143
|
+
|
|
144
|
+
```wd
|
|
145
|
+
---
|
|
146
|
+
title: My Blog
|
|
147
|
+
description: Notes on shipping Markdown that runs.
|
|
148
|
+
site_url: https://example.com
|
|
149
|
+
---
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- **`site_url`** (absolute origin, **no trailing slash**) turns on `sitemap.xml` and `rss.xml` and is the absolute prefix for every URL in them. The home `title`/`description` become the RSS channel title/description.
|
|
153
|
+
- **`robots.txt`** is *always* emitted (`User-agent: * / Allow: /`); the `Sitemap:` line is added only when `site_url` is set.
|
|
154
|
+
- **`sitemap.xml`** lists every built page (reactive pages included — they're indexable HTML). Each `<lastmod>` is the page's frontmatter `date:` if set, else its git last-commit date, else the file's mtime. No `<priority>`/`<changefreq>`.
|
|
155
|
+
- **`rss.xml`** syndicates your **posts** — any page with a `date:` in its frontmatter. Newest first, capped at the 20 most recent. Each item's `<description>` is the page's `excerpt:`, else its `description:`, else (for a plain `.md` post) its first paragraph. Every page links the feed with `<link rel="alternate" type="application/rss+xml">` so readers can autodiscover it.
|
|
156
|
+
|
|
157
|
+
```md
|
|
158
|
+
---
|
|
159
|
+
title: Hello, Darkmown
|
|
160
|
+
date: 2026-01-15
|
|
161
|
+
excerpt: Why I rewrote my blog as plain Markdown files.
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
# Hello, Darkmown
|
|
165
|
+
…
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Without `site_url`, `robots.txt` still emits and the build prints a one-line hint telling you which field to set; it never crashes.
|
|
169
|
+
|
|
170
|
+
### Drafts
|
|
171
|
+
|
|
172
|
+
Mark any page `draft: true` to keep it out of production:
|
|
173
|
+
|
|
174
|
+
```md
|
|
175
|
+
---
|
|
176
|
+
title: Work in progress
|
|
177
|
+
draft: true
|
|
178
|
+
---
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
- **`darkmown build`** excludes drafts everywhere — no HTML in `dist`, no entry in `routes.json`, `sitemap.xml`, or `rss.xml` (even a draft that *also* has a `date:` never reaches a feed).
|
|
182
|
+
- **`darkmown dev`** builds and serves drafts so you can preview them, with a visible "DRAFT" banner that exists only in the dev server — never in any shipped HTML.
|
|
183
|
+
- **`darkmown build --drafts`** includes drafts everywhere — for a staging deploy.
|
|
184
|
+
|
|
185
|
+
Drafts are filtered at one point — route discovery — so nothing downstream ever sees them. This is **separate from** the permanent `.`/`-`/`_` filename hiding: a hidden name (`-notes.wd`) is private forever; `draft:` is a toggle you flip when the page is ready to ship.
|
|
186
|
+
|
|
187
|
+
The `build` summary reports what it emitted: `Built 14 routes, sitemap (14 urls), rss (6 posts) into dist`.
|
|
188
|
+
|
|
138
189
|
## Loops
|
|
139
190
|
|
|
140
191
|
`@loop <things> into <thing>` is the only loop. The source decides the behavior:
|
|
@@ -692,6 +743,39 @@ The block must follow the element with no space, and works on links, images, emp
|
|
|
692
743
|
|
|
693
744
|
Darkmown's shipped CSP pre-authorizes exactly the two embed origins (`youtube-nocookie.com`, `player.vimeo.com`) and `media-src 'self' https:`, so embeds and remote media work out of the box on the bundled server, Cloudflare `_headers`, and Vercel.
|
|
694
745
|
|
|
746
|
+
## Syntax highlighting
|
|
747
|
+
|
|
748
|
+
*New in 1.3.* Fenced code blocks with a language are highlighted at **build time** — HTML and CSS only, no client JavaScript. Tag the fence with a language and it just works:
|
|
749
|
+
|
|
750
|
+
````md
|
|
751
|
+
```js
|
|
752
|
+
const greeting = "Darkmown"; // highlighted at build time
|
|
753
|
+
```
|
|
754
|
+
````
|
|
755
|
+
|
|
756
|
+
The highlighter is [highlight.js](https://highlightjs.org/) and is not configurable (one closed default, like the rest of the framework). Its token classes map onto your skin's `$code-*` tokens, so highlighted code **dark-modes for free** through the same [`tokens dark` / `:theme`](#dark-mode--tokens-dark) system below — no extra wiring. Tune the palette (or rely on the built-in default set) in your `.skin`:
|
|
757
|
+
|
|
758
|
+
```skin
|
|
759
|
+
tokens
|
|
760
|
+
code-bg #1b2420
|
|
761
|
+
code-fg #e9efe7
|
|
762
|
+
code-keyword #d9a8e0
|
|
763
|
+
code-string #97d892
|
|
764
|
+
code-comment #859289
|
|
765
|
+
code-function #88c4ee
|
|
766
|
+
code-number #ecae78
|
|
767
|
+
code-punctuation #c1ccc6
|
|
768
|
+
tokens dark
|
|
769
|
+
code-bg #100d0a
|
|
770
|
+
code-keyword #e2b9e8
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
- **Pay-for-what-you-use.** The stylesheet (`/__wd/highlight.css`) is emitted and linked **only** on pages that actually contain a highlighted block — a page with no code ships nothing extra.
|
|
774
|
+
- **Zero runtime.** Highlighting is build-time output, so a page of prose plus code stays `runtime: false` (no `/__wd/runtime.js`). It never pulls in the reactive runtime.
|
|
775
|
+
- **Graceful degradation.** A fence with an unknown or absent language renders as plain escaped `<code>` (no highlighting, no error). Inline `` `code` `` is never highlighted, and there are deliberately **no line numbers** (they break copy-paste).
|
|
776
|
+
|
|
777
|
+
See it recolor live on the [Syntax highlighting demo](https://darkmown.com/highlight/) — flip the theme toggle and every block recolors at once.
|
|
778
|
+
|
|
695
779
|
## Dark mode — `tokens dark`
|
|
696
780
|
|
|
697
781
|
*New in 0.19.0.* A colocated `.skin` file already declares its palette in a `tokens` block (`name value` pairs referenced elsewhere as `$name`). Add a second `tokens dark` block to override any of those tokens under the visitor's OS dark preference — it compiles to a `@media (prefers-color-scheme: dark) :root { … }` rule, so the page follows the system theme with **zero JavaScript**.
|
|
@@ -725,6 +809,50 @@ A single `tokens dark` block powers **both** theming paths: it compiles to `:roo
|
|
|
725
809
|
|
|
726
810
|
Because `tokens dark` already emits the `[data-theme="dark"]` rule, **no extra skin block is needed** — the same palette drives the toggle. `"light"` forces the light palette even under OS dark; `"auto"` clears the attribute and follows the OS again; `theme` persists across reloads and tabs like any `:store`. (`:theme name = "light"` renames the store and seeds a different default. The older `tokens [data-theme=dark]` block — a manual-only override — still works for bespoke setups.)
|
|
727
811
|
|
|
812
|
+
## Scoped styles — `scoped`
|
|
813
|
+
|
|
814
|
+
*New in 1.4.0.* By default a colocated `.skin` is **global** — its selectors match the whole page, exactly like a stylesheet. That's the right default for a design system. But when two components both want a class called `.card`, global CSS makes them fight. Opt a skin into **scoping** so its selectors only ever match the component it ships with: make the **first line** of the `.skin` file the word `scoped`.
|
|
815
|
+
|
|
816
|
+
```skin
|
|
817
|
+
scoped
|
|
818
|
+
|
|
819
|
+
.card
|
|
820
|
+
padding 1.5rem
|
|
821
|
+
bg $panel
|
|
822
|
+
radius $radius
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
Scoping is **pure compile time** — a short, path-derived id (e.g. `wd-7c21`) is stamped onto the component's HTML (`data-wd-scope="wd-7c21"`) and appended to each of the skin's selectors:
|
|
826
|
+
|
|
827
|
+
```css
|
|
828
|
+
.card[data-wd-scope="wd-7c21"] { padding: 1.5rem; }
|
|
829
|
+
.card[data-wd-scope="wd-7c21"] { background: var(--panel); }
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
A second component's `.card` gets a **different** id, so the two never collide. There is **no runtime cost and no class renaming in your markup** — you still write `class="card"`; the framework adds the attribute during the build. A static page with a scoped skin stays **zero-JS**.
|
|
833
|
+
|
|
834
|
+
**What scopes, what stays global:**
|
|
835
|
+
|
|
836
|
+
| In a `scoped` skin | Result |
|
|
837
|
+
|---|---|
|
|
838
|
+
| A selector rule (`.card`, `.card:hover`, `.card .title`, `h2, h3`) | Scoped — the attribute lands on the **subject** (rightmost) selector, before any `:hover`/`::before` so it stays valid CSS |
|
|
839
|
+
| A descendant selector (`.card .title`) | Only `.title` (the subject) is scoped; `.card` matches inside the subtree |
|
|
840
|
+
| `tokens` / `tokens dark` / `tokens [data-theme=…]` | **Global** — design tokens always emit on `:root`, so `$accent` and dark mode keep working site-wide |
|
|
841
|
+
| `:global(.toast)` (whole selector) | **Opts back out** — emits a plain, unscoped `.toast { … }` |
|
|
842
|
+
| `@media` / `@supports` wrappers | Untouched; only the inner rule's selector is scoped |
|
|
843
|
+
| `page` / `*` / `html` / `body` / `::selection` | **Compile error** — page-level styles belong in a global skin, not a scoped one |
|
|
844
|
+
|
|
845
|
+
Where the scope applies depends on **where the skin lives**:
|
|
846
|
+
|
|
847
|
+
- A skin colocated with a **page** (`about.skin` next to `about.wd`) scopes that page's body.
|
|
848
|
+
- A skin colocated with an **include** (`card.skin` next to `card.wd`) scopes just that include's subtree wherever it's `@include`d — so the same scoped component can appear many times on a page without leaking into its neighbours.
|
|
849
|
+
|
|
850
|
+
**Caveats (honest limits this release):**
|
|
851
|
+
|
|
852
|
+
- **Whole-selector `:global()` only.** A descendant `:global` (`.card :global(.x)`) is **not** supported yet — use a whole-selector `:global(.x)`.
|
|
853
|
+
- **Unused selectors warn, they aren't removed.** If a scoped selector's class/element/id never appears in the stamped subtree, the build prints `hint: scoped selector ".badge" in card.skin matches no element` — but the rule is **kept** (a colocated `.js` may add the class at runtime). It's a typo nudge, not a dead-code remover.
|
|
854
|
+
- **Scoping is opt-in.** Every existing `.skin` (no `scoped` marker) is byte-for-byte unchanged.
|
|
855
|
+
|
|
728
856
|
## The escape hatch
|
|
729
857
|
|
|
730
858
|
Reactive pages expose `window.wd` so a colocated `.js` file can do anything the directives deliberately don't — keyboard, drag/touch, canvas, charts, maps:
|
package/docs/spec-alignment.md
CHANGED
|
@@ -134,6 +134,30 @@ Four progressive-disclosure capabilities plus an assets and a security change. T
|
|
|
134
134
|
- **Page-colocated static assets:** any non-page file under `site/pages/` (anything but `.md`/`.wd` routes and colocated `.skin`/`.js`) is copied to `dist/` at its path (`site/pages/logo.svg` → `dist/logo.svg`). The shelf (`site/_/` → `/__wd/media`, `/__wd/data`) is unchanged; a name clash with an emitted route is skipped with a warning. As of v0.14.1, hidden path segments (`.`, `-`, `_`) and symlinked page assets are skipped. `.avif` was added to the served content-types.
|
|
135
135
|
- **`:fetch` URL scheme hardening (security):** `:fetch`/`refresh=` URLs are validated at compile time — relative, `http(s)://`, or a leading `{ state }` interpolation are allowed; protocol-relative `//host` and non-http(s) schemes (`file:`, `data:`, `javascript:`, …) are rejected with a corrective hint. **This is scheme validation, not a host allowlist** — SSRF to arbitrary http(s) hosts remains the author's responsibility (see the trust boundary).
|
|
136
136
|
|
|
137
|
+
## 1.4.0 additions — compile-time scoped styles (2026-06-28)
|
|
138
|
+
|
|
139
|
+
The 1.4.0 cycle ships one feature: **opt-in scoped styles** for colocated `.skin` files, closing the "no CSS scoping" gap an audit flagged (two components couldn't reuse a class name without collision). **Pure compile time, zero runtime** — `src/runtime.js` and the 8 KB budget are untouched, and a static page with a scoped skin stays `runtime: false`.
|
|
140
|
+
|
|
141
|
+
- **Authoring surface — the `scoped` marker:** a `.skin` scopes its selectors **only** when its first non-comment, non-blank, non-divider line is exactly `scoped`; colocation alone changes nothing. Every existing `.skin` (no marker) stays **global** and emits **byte-identical CSS** to before (golden-tested against `site/_/base.skin`). `scoped` anywhere but the first line is a compile error (`"scoped" must be the first line of a .skin file`). The only other new surface is `:global()` inside a scoped skin — no new `.wd` syntax.
|
|
142
|
+
|
|
143
|
+
- **Mechanism — id + selector rewrite + HTML stamp.** The scope id is a short, deterministic hash of the skin's **project-relative path** (e.g. `wd-7c21`) — path-based (stable across content edits, unique per file), computed in `src/builder.js` (the coordination point) and re-derived identically by the HTML stamp from the same path, so `src/skin.js` stays a pure string→CSS function (`compileSkin(src, { scope })`). In scoped mode `compileSkin` appends `[data-wd-scope="<id>"]` to the **subject** (rightmost compound) of each comma-separated selector, **after** `&`-nesting resolves and **before** any trailing pseudo-class/element (`.card[data-wd-scope="…"]:hover`, never `.card:hover[…]`; `::before` likewise) — so descendant selectors scope only the subject (`.card .title` → `.title` gets the attribute) and `@media`/`@supports` wrappers are untouched. The HTML half (`stampScope` in `src/compiler/page.js`, an `enhanceImages`-style regex post-pass) stamps `data-wd-scope="<id>"` onto every opening tag of the scoped subtree that doesn't already carry one (first stamp wins, so a nested include keeps its own id). A **page**-colocated scoped skin stamps the page body; an **include**-colocated scoped skin is stamped in `handleInclude` over just that include's returned subtree — so the same scoped component repeats on a page without its scope leaking to siblings.
|
|
144
|
+
|
|
145
|
+
- **Tokens stay global.** A `tokens` / `tokens dark` / `tokens [attr]` block in a scoped skin still emits a global `:root { --name: … }` (and the dark/manual-toggle variants) — never scoped — so `var(--x)`, dark mode, `:theme`, and the `$code-*` highlight tokens keep resolving site-wide. Enforced by only touching the selector-rule construction, leaving the token/`:root` path identical.
|
|
146
|
+
|
|
147
|
+
- **`:global()` escape + disallowed selectors.** A **whole-selector** `:global(.toast)` emits a plain, unscoped `.toast { … }`. Page-level selectors in a scoped skin — `page` (→ body), bare top-level `*`/`html`/`body`/`::selection`, and any bare top-level declaration that would land on `:root` — are a **compile error** with a hint pointing to a global skin (chosen over silently auto-`:global`-ing them).
|
|
148
|
+
|
|
149
|
+
- **Unused-selector handling — warn, never remove.** The builder has both the scoped subject set (collected by `compileSkin`) and the stamped subtree HTML, so it warns (`hint: scoped selector ".badge" in card.skin matches no element`) when a scoped subject's class/element/id never appears in the stamped subtree — but **keeps the rule** (a colocated `.js` may add the class at runtime). A typo nudge, not a dead-code remover.
|
|
150
|
+
|
|
151
|
+
- **Honest caveats:** only **whole-selector** `:global()` this release — descendant `:global` (`.card :global(.x)`) is deferred; unused selectors **warn, are not removed**. Demonstrated end-to-end by the [`/scoped/`](/scoped/) demo (two components reuse `.card` with no collision; a global skin still works alongside).
|
|
152
|
+
|
|
153
|
+
## 1.3.0 additions — table-stakes batteries (2026-06-28)
|
|
154
|
+
|
|
155
|
+
The 1.3.0 cycle closes table-stakes gaps a new user expects out of the box. Tracks land in parallel; this entry covers the **syntax-highlighting** and **drafts + feeds** tracks.
|
|
156
|
+
|
|
157
|
+
- **Drafts + sitemap.xml + rss.xml + robots.txt:** three coupled SEO/feed capabilities sharing one draft-exclusion gate. Site identity lives in the **home page frontmatter** (no config loader this cycle): one reserved field `site_url` (absolute origin, no trailing slash) reuses the home `title`/`description` as the RSS channel fields and is the absolute prefix for — and the trigger for emitting — `sitemap.xml` + `rss.xml`. **Drafts:** `draft: true` excludes a page from a production build everywhere (not in `dist`, `routes.json`, sitemap, or rss — even a draft that *also* has a `date:`); `darkmown dev` builds/serves drafts with a dev-only visible banner injected at serve time (never in shipped HTML, like the SSE client); `darkmown build --drafts` includes them for staging. Drafts are filtered at the single route-discovery **chokepoint** (`discoverRoutes`), which now reads each file's frontmatter **once** and threads it out as `route.meta` (one source of truth for "is this a draft", and the data the feeds consume — no second parse). This is distinct from the permanent `.`/`-`/`_` filename hiding (private forever vs a publish-when-ready toggle). **sitemap.xml** (emitted whenever `site_url` is set) lists one `<url>` per built non-404 route — reactive pages included (indexable HTML) — with `<lastmod>` = frontmatter `date:` → git last-commit date of the source file → file mtime (git wrapped in try/catch so a scaffolded app or extracted tarball with no `.git` degrades to mtime, never crashes); no `<priority>`/`<changefreq>`. **rss.xml** (emitted only with `site_url` *and* at least one dated post) syndicates the pages with a `date:` (the "this is a post" signal), newest-first, capped at 20; `<description>` resolves `excerpt:` → `description:` → first paragraph of rendered prose (plain `.md` only — `.wd` directive markup is not stripped back to prose); `<pubDate>` is RFC-822; every page advertises the feed with `<link rel="alternate" type="application/rss+xml">`. **robots.txt** is *always* emitted (`User-agent: * / Allow: /`); the `Sitemap:` line appears only with `site_url`. **No `site_url`:** robots still emits (without the Sitemap line), sitemap + rss are skipped with a loud, actionable build hint, and the build never crashes. String/XML building lives in a new `src/feeds.js` (mirroring how `src/highlight.js` keeps `builder.js` lean); the `build` summary reports counts (`Built 14 routes, sitemap (14 urls), rss (6 posts) into dist`). **Zero runtime impact** — all build-time; `src/runtime.js` and the 8 KB budget are untouched. **The leak contract** (a `draft: true` page with a `date:` produces zero sitemap entries and zero rss items; `/404/` never appears in either) is regression-tested on both the production-exclusion and `--drafts`-inclusion paths, plus the git-date vs mtime-fallback paths.
|
|
158
|
+
|
|
159
|
+
- **Build-time syntax highlighting:** fenced code blocks with a known language are highlighted at compile time via [highlight.js](https://highlightjs.org/), wired onto both markdown-it instances (the default `html: true` and the lazy `html: false` strict variant) through markdown-it's `highlight` option in `src/compiler/markdown.js`. Output is **class-based** (`hljs-*` token spans, escaped) — no inline colors — and the framework ships a small stylesheet (`/__wd/highlight.css`) that maps the common token classes onto a `$code-*` skin-token set (`code-bg`, `code-fg`, `code-keyword`, `code-string`, `code-comment`, `code-function`, `code-number`, `code-punctuation`), each `var(--code-*, fallback)`. Because those tokens carry a `tokens dark` override in the base skin, **highlighted code dark-modes for free** through the existing `tokens dark` / `:theme` system with zero JavaScript. The highlighter is **not configurable** (closed-whitelist philosophy). Emission/injection is **pay-for-what-you-use**: a per-page `assets.hasCode` flag (set when the rendered HTML carries the `hljs` marker) emits and links the stylesheet **only** on pages that contain a highlighted block, mirroring the behavior modules — a page with no code ships nothing extra. **Zero runtime impact:** highlighting is build-time HTML + CSS only (`src/runtime.js` untouched, the 8 KB runtime budget unchanged), so a page of prose plus a code block stays `runtime: false`. **Graceful degradation:** a fence with an unknown or absent language renders as plain escaped `<code>` (no throw, no highlighting); inline `` `code` `` is never highlighted; there are deliberately **no line numbers** (they break copy-paste). **Honest caveat:** the stylesheet maps a curated common subset of highlight.js classes — any token class outside that subset inherits the base `--code-fg` color, so it stays legible but is not individually themed.
|
|
160
|
+
|
|
137
161
|
## 1.2.0 additions — backends, deploy & interactions (2026-06-27)
|
|
138
162
|
|
|
139
163
|
- **Serverless `api/` model:** backend endpoints are plain-JS Web-standard handlers — `export default (request, context) => Response` — in a top-level `api/` directory (`api/echo.js` → `/api/echo`; `api/users/[id].js` → `/api/users/:id` with `context.params.id`). Darkmown owns no server; it *adapts* to the host. The build stays 100% static. On Vercel the `api/` functions run natively (Edge); `darkmown deploy cloudflare` (or `build --target cloudflare`) emits `dist/_worker.js` that routes `/api/*` to the same functions and serves everything else from `env.ASSETS`. `darkmown dev` runs a local runner so `/api/*` behaves identically before deploy. No `.wd` backend DSL — minimal new learning, zero lock-in. `form-action 'self'` was added to both CSP variants (and `vercel.json`) so a native `:form action="/api/…"` POST is allowed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zvndev/darkmown",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"node": ">=20"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
+
"highlight.js": "^11.11.1",
|
|
60
61
|
"image-size": "^1.2.1",
|
|
61
62
|
"markdown-it": "^14.2.0"
|
|
62
63
|
},
|
package/site/_/base.skin
CHANGED
|
@@ -26,6 +26,13 @@ tokens
|
|
|
26
26
|
code-bg #1b2420
|
|
27
27
|
code-ink #e9efe7
|
|
28
28
|
code-line #2c3b34
|
|
29
|
+
code-fg #e9efe7
|
|
30
|
+
code-keyword #d9a8e0
|
|
31
|
+
code-string #97d892
|
|
32
|
+
code-comment #859289
|
|
33
|
+
code-function #88c4ee
|
|
34
|
+
code-number #ecae78
|
|
35
|
+
code-punctuation #c1ccc6
|
|
29
36
|
ring #0f6b5e
|
|
30
37
|
radius 12px
|
|
31
38
|
radius-sm 8px
|
|
@@ -51,6 +58,13 @@ tokens dark
|
|
|
51
58
|
code-bg #100d0a
|
|
52
59
|
code-ink #e4ebe2
|
|
53
60
|
code-line #2a231b
|
|
61
|
+
code-fg #e4ebe2
|
|
62
|
+
code-keyword #e2b9e8
|
|
63
|
+
code-string #a6e0a1
|
|
64
|
+
code-comment #7c8983
|
|
65
|
+
code-function #9bd0f4
|
|
66
|
+
code-number #f2bb8a
|
|
67
|
+
code-punctuation #b6c2bc
|
|
54
68
|
ring #5ccab4
|
|
55
69
|
|
|
56
70
|
/* ── Reset & base ───────────────────────────────────────────────── */
|
package/site/_/nav.wd
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
scoped
|
|
2
|
+
|
|
3
|
+
/* Component A — a warm promo card. Same class names as scoped-stat.skin
|
|
4
|
+
(.card / .title / .body), but scoped, so neither leaks. Tokens stay global,
|
|
5
|
+
so $accent / $warm / $radius still resolve from base.skin's :root. */
|
|
6
|
+
|
|
7
|
+
.card
|
|
8
|
+
display grid
|
|
9
|
+
gap .6rem
|
|
10
|
+
padding 1.5rem
|
|
11
|
+
bg $warm-soft
|
|
12
|
+
border 1px solid $warm
|
|
13
|
+
border-left 4px solid $warm
|
|
14
|
+
radius $radius
|
|
15
|
+
|
|
16
|
+
.badge
|
|
17
|
+
justify-self start
|
|
18
|
+
font-size .7rem
|
|
19
|
+
font-weight 700
|
|
20
|
+
text-transform uppercase
|
|
21
|
+
letter-spacing .08em
|
|
22
|
+
color #fff
|
|
23
|
+
bg $warm
|
|
24
|
+
padding .2rem .55rem
|
|
25
|
+
radius 999px
|
|
26
|
+
|
|
27
|
+
.title
|
|
28
|
+
margin 0
|
|
29
|
+
font-size 1.25rem
|
|
30
|
+
color $ink
|
|
31
|
+
|
|
32
|
+
.body
|
|
33
|
+
margin 0
|
|
34
|
+
color $ink-soft
|
|
35
|
+
max-width none
|
|
36
|
+
|
|
37
|
+
.cta
|
|
38
|
+
justify-self start
|
|
39
|
+
font-weight 600
|
|
40
|
+
color $warm
|
|
41
|
+
|
|
42
|
+
.cta:hover
|
|
43
|
+
color $ink
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<!-- Scoped component A. Its colocated scoped-promo.skin opts into scoping, so the
|
|
2
|
+
`.card` here gets its OWN look — a warm, accent-bordered promo — without
|
|
3
|
+
clashing with the identically-named `.card` in scoped-stat.wd. handleInclude
|
|
4
|
+
stamps just this returned subtree with the component's scope id. -->
|
|
5
|
+
<article class="card">
|
|
6
|
+
<span class="badge">Promo</span>
|
|
7
|
+
<h3 class="title">Same class name, zero collision</h3>
|
|
8
|
+
<p class="body">This <code>.card</code> is a warm promo panel. The component beside it also calls its root <code>.card</code> — and they do not fight, because each scoped skin only matches inside its own stamped subtree.</p>
|
|
9
|
+
<a class="cta" href="/docs/">How scoping works →</a>
|
|
10
|
+
</article>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
scoped
|
|
2
|
+
|
|
3
|
+
/* Component B — a dark stat tile. Identical class names to scoped-promo.skin,
|
|
4
|
+
but a completely different look. Scoping keeps them apart; tokens are global
|
|
5
|
+
so $accent-deep / $accent / $radius-lg come straight from base.skin's :root. */
|
|
6
|
+
|
|
7
|
+
.card
|
|
8
|
+
display grid
|
|
9
|
+
gap .35rem
|
|
10
|
+
justify-items center
|
|
11
|
+
text-align center
|
|
12
|
+
padding 2rem 1.5rem
|
|
13
|
+
bg $accent-deep
|
|
14
|
+
border 1px solid $accent-deep
|
|
15
|
+
radius $radius-lg
|
|
16
|
+
|
|
17
|
+
.title
|
|
18
|
+
font-size 3rem
|
|
19
|
+
font-weight 700
|
|
20
|
+
line-height 1
|
|
21
|
+
color #fff
|
|
22
|
+
|
|
23
|
+
.body
|
|
24
|
+
margin 0
|
|
25
|
+
max-width none
|
|
26
|
+
color $accent-soft
|
|
27
|
+
|
|
28
|
+
.badge
|
|
29
|
+
font-size .68rem
|
|
30
|
+
text-transform uppercase
|
|
31
|
+
letter-spacing .1em
|
|
32
|
+
color $accent
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<!-- Scoped component B. Reuses the SAME class names as scoped-promo.wd
|
|
2
|
+
(.card / .title / .body) but its colocated scoped-stat.skin scopes them to a
|
|
3
|
+
dark, centered stat tile. Proof the leak is gone: two `.card` definitions on
|
|
4
|
+
one page, each only styling its own subtree. -->
|
|
5
|
+
<article class="card">
|
|
6
|
+
<span class="title">99.9%</span>
|
|
7
|
+
<p class="body">uptime, because there is no client framework to crash</p>
|
|
8
|
+
<span class="badge">measured</span>
|
|
9
|
+
</article>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Drafts that never leak
|
|
3
|
+
description: draft true excludes a page everywhere in a production build — dist, routes.json, sitemap, and rss.
|
|
4
|
+
date: 2026-06-27
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Drafts that never leak
|
|
8
|
+
|
|
9
|
+
Set `draft: true` on any page and it disappears from a production build — no
|
|
10
|
+
HTML in `dist`, no entry in `routes.json`, and crucially no entry in
|
|
11
|
+
`sitemap.xml` or `rss.xml`, even if the draft also carries a `date:`.
|
|
12
|
+
|
|
13
|
+
The filter lives at one chokepoint — route discovery — so every downstream
|
|
14
|
+
consumer only ever sees published pages. `darkmown dev` still builds and serves
|
|
15
|
+
your drafts (with a visible banner), and `darkmown build --drafts` includes them
|
|
16
|
+
for a staging preview.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: The Darkmown Blog
|
|
3
|
+
description: Notes on shipping Markdown that runs — feeds, drafts, and zero-JS releases.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# The Darkmown Blog
|
|
7
|
+
|
|
8
|
+
Short notes on the framework. Every post here is a plain `.md` file with a
|
|
9
|
+
`date:` in its frontmatter — that `date:` is what lands a page in `rss.xml` and
|
|
10
|
+
stamps its `<lastmod>` in `sitemap.xml`.
|
|
11
|
+
|
|
12
|
+
- [Shipping feeds without a config file](/blog/shipping-feeds/)
|
|
13
|
+
- [Drafts that never leak](/blog/drafts-that-never-leak/)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Shipping feeds without a config file
|
|
3
|
+
description: Site identity lives in the home page frontmatter — one site_url field turns on sitemap.xml and rss.xml.
|
|
4
|
+
date: 2026-06-26
|
|
5
|
+
excerpt: One reserved frontmatter field on the home page — site_url — turns on the whole feeds story. No config loader, no plugin.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Shipping feeds without a config file
|
|
9
|
+
|
|
10
|
+
Darkmown has no config file, so the site's public origin lives where the rest of
|
|
11
|
+
its identity already does: the home page frontmatter. Add `site_url` to
|
|
12
|
+
`site/pages/index` and the build emits `sitemap.xml` and `rss.xml`, reusing the
|
|
13
|
+
home `title` and `description` as the RSS channel fields.
|
|
14
|
+
|
|
15
|
+
Every page picks up a `<link rel="alternate" type="application/rss+xml">` so
|
|
16
|
+
feed readers can autodiscover the feed, and `robots.txt` points crawlers at the
|
|
17
|
+
sitemap. All build-time — not a byte of client JavaScript.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Unfinished thoughts (do not ship)
|
|
3
|
+
description: A work-in-progress post that demonstrates draft exclusion — it carries a date but never reaches a production feed.
|
|
4
|
+
date: 2026-06-28
|
|
5
|
+
draft: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Unfinished thoughts
|
|
9
|
+
|
|
10
|
+
This post is marked `draft: true`. It also has a `date:`, which would normally
|
|
11
|
+
land it in `rss.xml` and `sitemap.xml` — but because it is a draft, a production
|
|
12
|
+
build excludes it from both, and from `dist` and `routes.json` entirely. You are
|
|
13
|
+
only reading this in `darkmown dev` or a `build --drafts` staging build.
|
package/site/pages/docs/index.wd
CHANGED
|
@@ -176,6 +176,24 @@ tags: [sales, revenue, "q1, q2"]
|
|
|
176
176
|
|
|
177
177
|
`{ meta.title }` prints a scalar and `{ meta.tags }` joins an array with `, `. You can loop a frontmatter array at build time — `@loop meta.tags into tag` — and the page stays static. Arrays are inline flow only (`[a, b]`); quoted items keep their commas.
|
|
178
178
|
|
|
179
|
+
## SEO & feeds
|
|
180
|
+
|
|
181
|
+
`darkmown build` writes `sitemap.xml`, `rss.xml`, and `robots.txt` — all build-time, zero client JS. There's no config file: the **home page** frontmatter carries the site's identity. Add `site_url` (absolute origin, no trailing slash) and the sitemap and feed switch on, prefixed with that origin and titled with the home `title`/`description`.
|
|
182
|
+
|
|
183
|
+
```wd
|
|
184
|
+
---
|
|
185
|
+
title: My Blog
|
|
186
|
+
description: Notes
|
|
187
|
+
site_url: https://example.com
|
|
188
|
+
---
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
A page becomes an **RSS post** by carrying a `date:`; `excerpt:` is its feed summary (else `description:`, else — for a plain `.md` — its first paragraph). The sitemap's `<lastmod>` is the `date:` if set, else the file's git/last-modified date. `robots.txt` is always written; the `Sitemap:` line only appears with a `site_url`. Without `site_url`, robots still emits and the build prints a hint — it never breaks.
|
|
192
|
+
|
|
193
|
+
## Drafts
|
|
194
|
+
|
|
195
|
+
Mark a page `draft: true` and it stays out of production: `darkmown build` leaves it out of `dist`, `routes.json`, the sitemap, and the feed — even if it has a `date:`. `darkmown dev` still builds and serves it (with a "DRAFT" banner you only see in dev), and `darkmown build --drafts` includes drafts for a staging deploy. This is separate from the permanent `.`/`-`/`_` filename hiding — a hidden name is private forever; `draft:` is a switch you flip when the page is ready.
|
|
196
|
+
|
|
179
197
|
## Instant navigation
|
|
180
198
|
|
|
181
199
|
Add `transitions: true` to a page's frontmatter for **instant, flash-free navigation** — zero JavaScript. It emits a directional fade+slide view transition for the page swap and a `<script type="speculationrules">` prerender hint, so the browser renders the next same-origin page on hover/pointerdown and the click activates an already-painted page (no white render-gap). It honors `prefers-reduced-motion`, and only same-origin pages that both opt in transition. Off by default; opt out with `transitions: false`, or exclude a single link from prerendering with `{.no-prefetch}`. (Chrome turns prerendering off while DevTools is open — test the built site with DevTools closed.)
|
|
@@ -396,6 +414,29 @@ For an explicit light/dark switch alongside the OS preference, declare `:theme`
|
|
|
396
414
|
|
|
397
415
|
A single `tokens dark` block in your `.skin` already powers both the OS `prefers-color-scheme` query and this toggle — no extra block needed. `"auto"` clears the attribute and follows the OS again. See it on the [Ledger demo](/ledger/).
|
|
398
416
|
|
|
417
|
+
## Scoped styles — `scoped`
|
|
418
|
+
|
|
419
|
+
A colocated `.skin` is **global** by default. Make its **first line** `scoped` and its selectors only ever match the component it ships with — so two components can both use `.card` without colliding. It's pure compile time: a path-derived id (e.g. `wd-7c21`) is stamped onto the HTML and appended to each selector.
|
|
420
|
+
|
|
421
|
+
```skin
|
|
422
|
+
scoped
|
|
423
|
+
|
|
424
|
+
.card
|
|
425
|
+
padding 1.5rem
|
|
426
|
+
bg $panel
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
compiles to `.card[data-wd-scope="wd-7c21"] { … }`, and the component's markup is stamped `data-wd-scope="wd-7c21"`. You still write `class="card"` — no renaming, no runtime, **zero JS** on a static page. A page skin scopes the page body; an **include** skin scopes just that include's subtree wherever it's used.
|
|
430
|
+
|
|
431
|
+
| In a `scoped` skin | Result |
|
|
432
|
+
|---|---|
|
|
433
|
+
| A selector rule | Scoped — attribute on the subject, before any `:hover`/`::before` |
|
|
434
|
+
| `tokens` / `tokens dark` | **Global** on `:root` — `$vars` and dark mode keep working site-wide |
|
|
435
|
+
| `:global(.toast)` (whole selector) | Opts back out — plain unscoped `.toast { … }` |
|
|
436
|
+
| `page` / `*` / `html` / `body` / `::selection` | Compile error — page-level styles belong in a global skin |
|
|
437
|
+
|
|
438
|
+
Honest limits this release: only **whole-selector** `:global()` (no `.card :global(.x)` yet), and an **unused** scoped selector **warns but is not removed** (a colocated `.js` may add the class). See the side-by-side [Scoped demo](/scoped/).
|
|
439
|
+
|
|
399
440
|
## Media — `:video`, `:audio`, `:embed`
|
|
400
441
|
|
|
401
442
|
Three compile-time-only directives (they emit no `data-wd-*`, so a media page stays zero-JS):
|
|
@@ -408,6 +449,35 @@ Three compile-time-only directives (they emit no `data-wd-*`, so a media page st
|
|
|
408
449
|
|
|
409
450
|
`:video`/`:audio` emit a hardened HTML5 player — `preload="metadata"` and `controls` by default, whitelisted flags (`autoplay` implies `muted`), and the `:fetch` URL scheme guard. `:embed` rewrites a YouTube/Vimeo link to its no-cookie/player form in a lazy, responsive `16/9` iframe. See the zero-JS [Media demo](/media/).
|
|
410
451
|
|
|
452
|
+
## Syntax highlighting
|
|
453
|
+
|
|
454
|
+
Fenced code blocks with a language are highlighted at **build time** — HTML and CSS only, no client JavaScript. Tag the fence with a language:
|
|
455
|
+
|
|
456
|
+
````md
|
|
457
|
+
```js
|
|
458
|
+
const greeting = "Darkmown"; // highlighted at build time
|
|
459
|
+
```
|
|
460
|
+
````
|
|
461
|
+
|
|
462
|
+
The highlighter is [highlight.js](https://highlightjs.org/) and isn't configurable (one closed default, like everything else). It maps onto your skin's `$code-*` tokens, so highlighted code **dark-modes for free** through the same `tokens dark` / `:theme` system — no extra wiring. Define the palette once:
|
|
463
|
+
|
|
464
|
+
```
|
|
465
|
+
tokens
|
|
466
|
+
code-bg #1b2420
|
|
467
|
+
code-fg #e9efe7
|
|
468
|
+
code-keyword #d9a8e0
|
|
469
|
+
code-string #97d892
|
|
470
|
+
code-comment #859289
|
|
471
|
+
code-function #88c4ee
|
|
472
|
+
code-number #ecae78
|
|
473
|
+
code-punctuation #c1ccc6
|
|
474
|
+
tokens dark
|
|
475
|
+
code-bg #100d0a
|
|
476
|
+
code-keyword #e2b9e8
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
The framework ships a small default `$code-*` set, so highlighting looks right out of the box. The stylesheet is **pay-for-what-you-use**: it's emitted and linked only on pages that actually contain a highlighted block — a page with no code ships nothing extra. A fence with an unknown or absent language renders as plain escaped `<code>` (no highlighting, no error); inline `` `code` `` is never highlighted, and there are deliberately no line numbers (they break copy-paste). See the live [Syntax highlighting demo](/highlight/).
|
|
480
|
+
|
|
411
481
|
## Trust boundary
|
|
412
482
|
|
|
413
483
|
Darkmown is a trusted-author site generator: you compile content you wrote, the way you trust your own source. Three things define that boundary.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
main.app-highlight
|
|
2
|
+
max-width 1000px
|
|
3
|
+
margin 0 auto
|
|
4
|
+
padding 2.5rem 1.5rem
|
|
5
|
+
|
|
6
|
+
.hl-eyebrow
|
|
7
|
+
text-transform uppercase
|
|
8
|
+
letter-spacing .08em
|
|
9
|
+
font-size .8rem
|
|
10
|
+
opacity .7
|
|
11
|
+
|
|
12
|
+
.hl-tagline
|
|
13
|
+
font-size 1.05rem
|
|
14
|
+
opacity .85
|
|
15
|
+
margin-top .5rem
|
|
16
|
+
max-width 60ch
|
|
17
|
+
|
|
18
|
+
/* Theme toggle — reuse the ledger's pill-button switch so flipping the theme
|
|
19
|
+
recolors every highlighted block at once. */
|
|
20
|
+
.app-highlight .theme-switch
|
|
21
|
+
display inline-flex
|
|
22
|
+
align-items center
|
|
23
|
+
gap .4rem
|
|
24
|
+
font-size .75rem
|
|
25
|
+
font-weight 600
|
|
26
|
+
letter-spacing .04em
|
|
27
|
+
text-transform uppercase
|
|
28
|
+
color $muted
|
|
29
|
+
margin 1.5rem 0 .5rem
|
|
30
|
+
|
|
31
|
+
.app-highlight .theme-switch button[data-wd-action]
|
|
32
|
+
border 1px solid $line
|
|
33
|
+
bg $panel
|
|
34
|
+
color $ink-soft
|
|
35
|
+
font-size .8rem
|
|
36
|
+
font-weight 600
|
|
37
|
+
padding .4rem .8rem
|
|
38
|
+
radius 999px
|
|
39
|
+
cursor pointer
|
|
40
|
+
transition color .14s ease, border-color .14s ease, background .14s ease
|
|
41
|
+
|
|
42
|
+
.app-highlight .theme-switch button[data-wd-action]:hover
|
|
43
|
+
color $ink
|
|
44
|
+
border-color $accent
|
|
45
|
+
|
|
46
|
+
.hl-grid
|
|
47
|
+
margin 1rem 0 1.5rem
|
|
48
|
+
|
|
49
|
+
.hl-panel
|
|
50
|
+
padding 1.4rem 1.5rem
|
|
51
|
+
|
|
52
|
+
.hl-panel h3
|
|
53
|
+
margin-top 0
|
|
54
|
+
margin-bottom .6rem
|
|
55
|
+
font-size 1rem
|
|
56
|
+
color $muted
|
|
57
|
+
text-transform uppercase
|
|
58
|
+
letter-spacing .04em
|
|
59
|
+
|
|
60
|
+
.hl-panel pre
|
|
61
|
+
margin 0
|
|
62
|
+
|
|
63
|
+
.hl-note
|
|
64
|
+
margin-top 1.5rem
|
|
65
|
+
padding 1rem 1.25rem
|
|
66
|
+
border 1px solid $line
|
|
67
|
+
border-left 3px solid $accent
|
|
68
|
+
bg $panel
|
|
69
|
+
radius $radius-sm
|
|
70
|
+
color $ink-soft
|
|
71
|
+
font-size .95rem
|
|
72
|
+
|
|
73
|
+
.hl-degrade
|
|
74
|
+
border-left-color $muted
|
|
75
|
+
|
|
76
|
+
@media (max-width: 760px)
|
|
77
|
+
.hl-grid
|
|
78
|
+
grid-template-columns 1fr
|