@wizzlethorpe/vaults 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -15
- package/dist/build.js +80 -71
- package/dist/build.js.map +1 -1
- package/dist/commands/build.js +3 -4
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/init.js +13 -10
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/password.js +2 -0
- package/dist/commands/password.js.map +1 -1
- package/dist/commands/patreon.js +6 -0
- package/dist/commands/patreon.js.map +1 -1
- package/dist/commands/preview.js +6 -5
- package/dist/commands/preview.js.map +1 -1
- package/dist/commands/push.js +5 -2
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/role.js +5 -0
- package/dist/commands/role.js.map +1 -1
- package/dist/config.js +9 -5
- package/dist/config.js.map +1 -1
- package/dist/index.js +34 -2
- package/dist/index.js.map +1 -1
- package/dist/migrate/0.6-legacy-auth-settings.js +98 -0
- package/dist/migrate/0.6-legacy-auth-settings.js.map +1 -0
- package/dist/migrate/0.7-vaults-dir.js +70 -0
- package/dist/migrate/0.7-vaults-dir.js.map +1 -0
- package/dist/migrate/registry.js +12 -0
- package/dist/migrate/registry.js.map +1 -0
- package/dist/migrate/run.js +38 -0
- package/dist/migrate/run.js.map +1 -0
- package/dist/migrate/types.js +8 -0
- package/dist/migrate/types.js.map +1 -0
- package/dist/paths.js +51 -0
- package/dist/paths.js.map +1 -0
- package/dist/render/auth-template.js +3 -0
- package/dist/render/auth-template.js.map +1 -1
- package/dist/render/footer.js +37 -0
- package/dist/render/footer.js.map +1 -0
- package/dist/render/handlers/assets.js +90 -0
- package/dist/render/handlers/assets.js.map +1 -0
- package/dist/render/handlers/builtin/dice.js +78 -0
- package/dist/render/handlers/builtin/dice.js.map +1 -0
- package/dist/render/handlers/builtin/fm.js +89 -0
- package/dist/render/handlers/builtin/fm.js.map +1 -0
- package/dist/render/handlers/builtin/index.js +9 -0
- package/dist/render/handlers/builtin/index.js.map +1 -0
- package/dist/render/handlers/builtin/statblock.js +351 -0
- package/dist/render/handlers/builtin/statblock.js.map +1 -0
- package/dist/render/handlers/dispatch.js +187 -0
- package/dist/render/handlers/dispatch.js.map +1 -0
- package/dist/render/handlers/loader.js +90 -0
- package/dist/render/handlers/loader.js.map +1 -0
- package/dist/render/handlers/types.js +40 -0
- package/dist/render/handlers/types.js.map +1 -0
- package/dist/render/layout.js +2 -0
- package/dist/render/layout.js.map +1 -1
- package/dist/render/pipeline.js +16 -0
- package/dist/render/pipeline.js.map +1 -1
- package/dist/render/styles.js +30 -1
- package/dist/render/styles.js.map +1 -1
- package/dist/scan.js +1 -1
- package/dist/scan.js.map +1 -1
- package/dist/settings.js +9 -3
- package/dist/settings.js.map +1 -1
- package/package.json +11 -12
- package/dist/api.js +0 -42
- package/dist/api.js.map +0 -1
- package/dist/render/mcp-template.js +0 -239
- package/dist/render/mcp-template.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# vaults
|
|
2
2
|
|
|
3
|
-
Sync an Obsidian vault to a Cloudflare-hosted wiki. The CLI renders your notes locally to HTML and deploys them to your own Cloudflare Pages account. Supports role-based access (public, patron, dm, …) so different parts of the same vault can be visible to different audiences.
|
|
3
|
+
Sync an Obsidian vault to a Cloudflare-hosted wiki. The CLI renders your notes locally to HTML and deploys them to your own Cloudflare Pages account. Supports role-based access (public, patron, dm, …) so different parts of the same vault can be visible to different audiences. Patrons can be authenticated by password or by Patreon OAuth (linking roles to specific Patreon tier IDs).
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -28,6 +28,8 @@ vaults push # render + deploy to Cloudflare Pages
|
|
|
28
28
|
|
|
29
29
|
The first push prompts for a Pages project name and runs `wrangler login` if you aren't authenticated. After that it just renders and deploys.
|
|
30
30
|
|
|
31
|
+
If you'd rather not `cd` into the vault every time, set `VAULT_PATH=~/Documents/MyVault` in your shell rc and run `vaults` from anywhere.
|
|
32
|
+
|
|
31
33
|
## How it works
|
|
32
34
|
|
|
33
35
|
```
|
|
@@ -41,24 +43,49 @@ Cloudflare Pages ← per-user, your account
|
|
|
41
43
|
```
|
|
42
44
|
|
|
43
45
|
- **Per-tier deploys.** A page tagged `role: dm` in its frontmatter only ships to the dm variant. Public visitors *cannot* fetch it; the file structurally doesn't exist in their variant.
|
|
44
|
-
- **
|
|
45
|
-
- **
|
|
46
|
+
- **Inline gating with callouts.** Drop a `> [!dm]` callout in an otherwise public page; the entire block is stripped from the public deploy. Same for any other configured role.
|
|
47
|
+
- **Images and media are gated too.** Only images, audio, video, PDFs, and EPUBs embedded by visible pages are copied into a given variant. Unknown extensions are skipped by default (toggle `include_unknown_files`).
|
|
48
|
+
- **Incremental sync.** External clients (the [Foundry VTT module](https://github.com/wizzlethorpe/vaults)) probe `/_manifest.json` to discover the deploy's name, auth requirements, and role order, then pull `/_batch` (text) and `/_batch-images` (binary) for changed content. Manifest hashes fold in per-page frontmatter, so a role flip or title rename triggers a sync without a body diff.
|
|
49
|
+
- **Bases support.** `.base` files render as cards / table / list inside the wiki, just like inside Obsidian.
|
|
50
|
+
- **Social meta.** OG / Twitter card tags are auto-generated. Pages without an explicit `image:` frontmatter use the first body embed (toggle with `auto_image`).
|
|
46
51
|
|
|
47
52
|
## Commands
|
|
48
53
|
|
|
54
|
+
### Build / deploy
|
|
55
|
+
|
|
49
56
|
| Command | What it does |
|
|
50
57
|
|---|---|
|
|
51
58
|
| `vaults init` | Write a `settings.md` with sensible defaults. |
|
|
52
59
|
| `vaults build` | Render the vault to a local directory (no deploy). |
|
|
53
60
|
| `vaults preview` | Render + serve locally via `wrangler pages dev` so you can click around with auth working. |
|
|
54
61
|
| `vaults push` | Render + deploy to Cloudflare Pages. |
|
|
62
|
+
| `vaults push --dry-run` | Render without deploying. |
|
|
63
|
+
| `vaults push --rotate-secret` | Generate a fresh `SESSION_SECRET`, invalidating every issued auth token at once. |
|
|
64
|
+
| `vaults push --all-warnings` / `vaults build --all-warnings` | Don't truncate the broken-link / missing-image report. |
|
|
65
|
+
|
|
66
|
+
### Roles and passwords
|
|
67
|
+
|
|
68
|
+
| Command | What it does |
|
|
69
|
+
|---|---|
|
|
55
70
|
| `vaults role add <name>` | Add an access tier. The first role becomes the default (no password). |
|
|
56
71
|
| `vaults role remove <name>` | Remove an access tier. |
|
|
57
72
|
| `vaults role list` | List configured roles. |
|
|
58
73
|
| `vaults role promote <name>` / `demote <name>` | Reorder tiers. |
|
|
59
74
|
| `vaults password <role>` | Set or change a role's password (PBKDF2-SHA256). |
|
|
60
|
-
|
|
61
|
-
|
|
75
|
+
|
|
76
|
+
### Patreon OAuth (optional)
|
|
77
|
+
|
|
78
|
+
Link roles to Patreon tier IDs, so any patron at that tier can sign in with Patreon and pick up the corresponding role. Coexists with passwords; either grants the role.
|
|
79
|
+
|
|
80
|
+
| Command | What it does |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `vaults patreon configure` | Prompts for Patreon OAuth client credentials and walks you through picking a campaign. The client secret is stored as a Wrangler secret on next push. |
|
|
83
|
+
| `vaults patreon link <role> <tier-id>` | Map a role to a numeric Patreon tier ID. |
|
|
84
|
+
| `vaults patreon unlink <role>` | Remove a role's tier mapping (password access stays). |
|
|
85
|
+
| `vaults patreon status` | Show current configuration and tier mappings. |
|
|
86
|
+
| `vaults patreon clear` | Remove the entire Patreon configuration. |
|
|
87
|
+
|
|
88
|
+
You'll need to register a Patreon OAuth client at <https://www.patreon.com/portal/registration> and add `https://your-deploy-url/auth/patreon/callback` as a redirect URI before running `configure`.
|
|
62
89
|
|
|
63
90
|
Run any command with `--help` for the full flag list.
|
|
64
91
|
|
|
@@ -71,18 +98,20 @@ Run any command with `--help` for the full flag list.
|
|
|
71
98
|
vault_name: My Wiki
|
|
72
99
|
default_role: public
|
|
73
100
|
accent_color: "#7a4a8c"
|
|
74
|
-
|
|
101
|
+
bg_color: "#1a1a2e"
|
|
75
102
|
favicon: assets/icons/wiki.png
|
|
76
103
|
inline_title: true
|
|
77
|
-
default_image_width:
|
|
104
|
+
default_image_width: 300px
|
|
78
105
|
center_images: true
|
|
106
|
+
auto_image: true
|
|
107
|
+
include_unknown_files: false
|
|
79
108
|
ignore:
|
|
80
109
|
- Templates/**
|
|
81
110
|
- "*.draft.md"
|
|
82
111
|
---
|
|
83
112
|
```
|
|
84
113
|
|
|
85
|
-
Open it in Obsidian; the frontmatter shows up as a Properties form.
|
|
114
|
+
Open it in Obsidian; the frontmatter shows up as a Properties form. Unknown keys are warned about and stripped on next push, so the file stays canonical. Auth config (roles, passwords, Patreon credentials) is **not** in `settings.md`; it lives in `.vaultrc.json` and is managed by the CLI.
|
|
86
115
|
|
|
87
116
|
## Page frontmatter
|
|
88
117
|
|
|
@@ -95,30 +124,118 @@ title: Optional override # default: filename or first H1
|
|
|
95
124
|
aliases: # extra names that resolve to this page from wikilinks
|
|
96
125
|
- Pale Mountains
|
|
97
126
|
- The Pale Mountains
|
|
127
|
+
image: assets/banner.webp # optional cover image (OG / Twitter / Bases / Foundry)
|
|
128
|
+
foundry_base: Compendium.dnd5e.monsters.Actor.bandit # optional Foundry doc to clone
|
|
129
|
+
foundry: # optional doc overrides
|
|
130
|
+
system.attributes.hp.value: 22
|
|
98
131
|
---
|
|
99
132
|
```
|
|
100
133
|
|
|
101
134
|
Wikilinks (`[[Page]]`, `[[Page|alias]]`, `[[NPCs/Page#section]]`), image embeds (`![[image.png]]`), transclusions (`![[Page]]`), and Obsidian callouts all render the same way they do in Obsidian.
|
|
102
135
|
|
|
136
|
+
## Custom handlers
|
|
137
|
+
|
|
138
|
+
Vault authors can extend the renderer with custom inline-code and code-block transforms. Drop a Node ESM module into `.vaults/handlers/<name>.mjs` that exports a `handler` (or `handlers: Handler[]`); vaults-cli loads them at build time and runs them over every page.
|
|
139
|
+
|
|
140
|
+
- **Inline handler**: matches inline code like `` `prefix: content` ``.
|
|
141
|
+
- **Code-block handler**: matches fenced ` ```language ` blocks.
|
|
142
|
+
|
|
143
|
+
Both return either `{ markdown }` (re-processed through the rest of the pipeline) or `{ html }` (sanitized and inserted as-is).
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
// .vaults/handlers/seealso.mjs
|
|
147
|
+
export const handler = {
|
|
148
|
+
codeBlock: "seealso",
|
|
149
|
+
render: (content) => ({
|
|
150
|
+
markdown: content
|
|
151
|
+
.split(/\r?\n/)
|
|
152
|
+
.filter(Boolean)
|
|
153
|
+
.map((p) => `- [[${p.trim()}]]`)
|
|
154
|
+
.join("\n"),
|
|
155
|
+
}),
|
|
156
|
+
};
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Browser-side assets
|
|
160
|
+
|
|
161
|
+
If your handler needs to ship browser-side JavaScript or CSS, declare them with the `assets` field. Paths are relative to the handler file. Every declared asset across all handlers is concatenated into one `_handlers.js` and one `_handlers.css` at the deploy root, deduped by absolute path so a shared utility file is only included once.
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
// .vaults/handlers/widget.mjs
|
|
165
|
+
export const handler = {
|
|
166
|
+
codeBlock: "widget",
|
|
167
|
+
assets: {
|
|
168
|
+
scripts: ["./widget.runtime.js"],
|
|
169
|
+
styles: ["./widget.css"],
|
|
170
|
+
},
|
|
171
|
+
render: (content) => ({
|
|
172
|
+
html: `<div class="widget" data-config="${content}"></div>`,
|
|
173
|
+
}),
|
|
174
|
+
};
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
// .vaults/handlers/widget.runtime.js
|
|
179
|
+
(function () {
|
|
180
|
+
document.querySelectorAll('.widget').forEach((el) => {
|
|
181
|
+
// wire up el.dataset.config into something interactive
|
|
182
|
+
});
|
|
183
|
+
})();
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The deployed page references `/_handlers.js` (deferred) and `/_handlers.css`; the runtime then finds and hydrates the handler's HTML. Wrap your runtime in an IIFE to avoid global pollution.
|
|
187
|
+
|
|
188
|
+
**File-naming convention.** Handler module files end in `.mjs`. Browser-side runtime / CSS files end in `.js` / `.css`. The loader only treats `.mjs` files as handler modules; `.js` files in the same directory are picked up only if a handler's `assets.scripts` references them.
|
|
189
|
+
|
|
190
|
+
### Built-ins
|
|
191
|
+
|
|
192
|
+
- **`dice:` (inline)** — `` `dice: 1d20+5` `` renders as a clickable button on the deploy that re-rolls on click. Mirrors [Obsidian Dice Roller](https://github.com/javalent/dice-roller) syntax.
|
|
193
|
+
|
|
194
|
+
User handlers can override built-ins of the same name. Trust model: handlers run with the same permissions as the rest of the build, so only run `vaults push` on vaults whose contents you trust.
|
|
195
|
+
|
|
103
196
|
## Auth
|
|
104
197
|
|
|
105
198
|
Multi-role deploys ship with a small Cloudflare Pages Function (`_middleware.js`) that:
|
|
106
199
|
|
|
107
200
|
- **Gates per-role variants** via a signed cookie (`SameSite=None; Secure; Partitioned`).
|
|
108
|
-
- **Issues bearer tokens** through an OAuth-style `/connect` flow used by the [Foundry module](https://github.com/wizzlethorpe/vaults
|
|
201
|
+
- **Issues bearer tokens** through an OAuth-style `/connect` flow used by the [Foundry module](https://github.com/wizzlethorpe/vaults).
|
|
202
|
+
- **Handles Patreon login** at `/auth/patreon/login` and `/auth/patreon/callback` when configured.
|
|
109
203
|
- **Exposes** `/_batch` (text) and `/_batch-images` (binary) for bulk content sync.
|
|
204
|
+
- **Publishes** `/_manifest.json` with the deploy's name, role order, and auth requirements so external clients can probe the deploy before picking an auth flow.
|
|
110
205
|
|
|
111
206
|
Tokens are stateless HMAC-signed JWTs; revocation = rotate `SESSION_SECRET` via `vaults push --rotate-secret`.
|
|
112
207
|
|
|
208
|
+
Single-role (public-only) deploys skip the middleware entirely; everything serves as plain static assets.
|
|
209
|
+
|
|
113
210
|
## Files this CLI manages locally
|
|
114
211
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
212
|
+
```
|
|
213
|
+
MyVault/
|
|
214
|
+
├── settings.md ← user-editable settings (Obsidian Properties UI)
|
|
215
|
+
├── …content…
|
|
216
|
+
├── .env ← secrets only (SESSION_SECRET, PATREON_CLIENT_SECRET) — gitignored
|
|
217
|
+
└── .vaults/ ← all vaults-cli internal state lives here
|
|
218
|
+
├── .gitignore ← keeps cache + config out of git automatically
|
|
219
|
+
├── config.json ← CLI-managed: roles, password hashes, project name, Patreon config
|
|
220
|
+
├── cache/ ← build cache (rendered HTML, image webp cache)
|
|
221
|
+
└── handlers/ ← optional: custom inline / code-block handlers
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`settings.md` lives at the vault root (and only there) so Obsidian renders it as an editable Properties form. Everything else is internal and tucked under `.vaults/`. `vaults init` writes `.vaults/.gitignore` automatically; if your vault is a git repo, this is enough to keep the cache + secrets-bearing config from being tracked.
|
|
225
|
+
|
|
226
|
+
## Migrations
|
|
227
|
+
|
|
228
|
+
vaults-cli runs schema and layout migrations automatically before every `build` / `push` / `preview`. They're idempotent: already-migrated vaults pay only the cost of a few `stat()` calls.
|
|
229
|
+
|
|
230
|
+
To run them manually or inspect what would change:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
vaults migrate --list # show all known migrations
|
|
234
|
+
vaults migrate --dry-run # show what would apply on this vault
|
|
235
|
+
vaults migrate # apply pending migrations
|
|
236
|
+
```
|
|
120
237
|
|
|
121
|
-
|
|
238
|
+
If you're upgrading from a pre-0.7 vault, the first run of any command will move `.vaultrc.json` → `.vaults/config.json` and `.vault-cache/` → `.vaults/cache/` and write a `.vaults/.gitignore`. Renames are atomic on the same filesystem so even large caches migrate instantly.
|
|
122
239
|
|
|
123
240
|
## License
|
|
124
241
|
|
package/dist/build.js
CHANGED
|
@@ -24,9 +24,16 @@ import { resolvePageImage } from "./render/cover.js";
|
|
|
24
24
|
import { DEFAULT_CSS, renderThemeOverride } from "./render/styles.js";
|
|
25
25
|
import { loadObsidianSnippets } from "./obsidian.js";
|
|
26
26
|
import { loadSettings, writeSettings, SETTINGS_FILE } from "./settings.js";
|
|
27
|
-
import { loadConfig
|
|
27
|
+
import { loadConfig } from "./config.js";
|
|
28
28
|
import matter from "gray-matter";
|
|
29
29
|
import { renderAuthMiddleware, LOGIN_HTML } from "./render/auth-template.js";
|
|
30
|
+
import { renderFooterHtml } from "./render/footer.js";
|
|
31
|
+
import { buildRegistry } from "./render/handlers/types.js";
|
|
32
|
+
import { loadUserHandlers } from "./render/handlers/loader.js";
|
|
33
|
+
import { BUILTIN_HANDLERS } from "./render/handlers/builtin/index.js";
|
|
34
|
+
import { bundleHandlerAssets } from "./render/handlers/assets.js";
|
|
35
|
+
import { runMigrations } from "./migrate/run.js";
|
|
36
|
+
import { cacheDir } from "./paths.js";
|
|
30
37
|
import { formatDuration, pMap, Progress } from "./util.js";
|
|
31
38
|
/**
|
|
32
39
|
* Output layout when there are multiple roles:
|
|
@@ -48,10 +55,10 @@ import { formatDuration, pMap, Progress } from "./util.js";
|
|
|
48
55
|
export async function buildSite(opts) {
|
|
49
56
|
const start = Date.now();
|
|
50
57
|
const concurrency = Math.max(2, availableParallelism());
|
|
51
|
-
//
|
|
52
|
-
// .
|
|
53
|
-
//
|
|
54
|
-
await
|
|
58
|
+
// Run any pending schema / layout migrations before reading anything
|
|
59
|
+
// else. The framework is idempotent: already-migrated vaults pay only
|
|
60
|
+
// the cost of a few stat() calls. See cli/src/migrate/.
|
|
61
|
+
await runMigrations(opts.vaultPath);
|
|
55
62
|
// ── Settings (user-editable) ─────────────────────────────────────────────
|
|
56
63
|
const settings = await loadSettings(opts.vaultPath);
|
|
57
64
|
for (const w of settings.warnings)
|
|
@@ -66,6 +73,29 @@ export async function buildSite(opts) {
|
|
|
66
73
|
imageQuality: opts.imageQuality === 85 ? settings.values.image_quality : opts.imageQuality,
|
|
67
74
|
maxFileBytes: opts.maxFileBytes === 25 * 1024 * 1024 ? settings.values.max_file_bytes : opts.maxFileBytes,
|
|
68
75
|
};
|
|
76
|
+
// ── Custom handlers ──────────────────────────────────────────────────────
|
|
77
|
+
// Built-ins ship with the CLI; user handlers live in `.vaults/handlers/`
|
|
78
|
+
// and can override built-in names (last-registered wins). One registry
|
|
79
|
+
// is built once and shared across every variant render.
|
|
80
|
+
const userHandlers = await loadUserHandlers(opts.vaultPath);
|
|
81
|
+
const handlerRegistry = buildRegistry([
|
|
82
|
+
...BUILTIN_HANDLERS,
|
|
83
|
+
...userHandlers.map((h) => h.handler),
|
|
84
|
+
]);
|
|
85
|
+
if (userHandlers.length > 0) {
|
|
86
|
+
console.log(` loaded ${userHandlers.length} custom handler(s) from .vaults/handlers/`);
|
|
87
|
+
}
|
|
88
|
+
// Concatenate browser-side assets declared by built-in and user handlers
|
|
89
|
+
// into a single _handlers.js / _handlers.css emitted at the deploy root.
|
|
90
|
+
// Each unique source is included once, regardless of invocation count.
|
|
91
|
+
// Two independent flags so a deploy with only-JS or only-CSS doesn't
|
|
92
|
+
// reference a file that wasn't written.
|
|
93
|
+
const handlerAssets = await bundleHandlerAssets(userHandlers, BUILTIN_HANDLERS, opts.vaultPath);
|
|
94
|
+
const hasHandlerJs = handlerAssets.js.length > 0;
|
|
95
|
+
const hasHandlerCss = handlerAssets.css.length > 0;
|
|
96
|
+
// Footer markdown rendered once per build; the resulting HTML is
|
|
97
|
+
// embedded verbatim in every page's layout. Empty string = no footer.
|
|
98
|
+
const footerHtml = await renderFooterHtml(settings.values.footer);
|
|
69
99
|
// ── CLI-managed state (auth) ─────────────────────────────────────────────
|
|
70
100
|
const cfg = await loadConfig(opts.vaultPath, {});
|
|
71
101
|
const roles = cfg.roles.length > 0 ? cfg.roles : ["public"];
|
|
@@ -183,8 +213,8 @@ export async function buildSite(opts) {
|
|
|
183
213
|
const imageStagingDir = join(opts.outputDir, ".image-staging");
|
|
184
214
|
const imageIndex = new Map();
|
|
185
215
|
if (imageFiles.length > 0) {
|
|
186
|
-
const
|
|
187
|
-
await mkdir(
|
|
216
|
+
const cacheImageDir = join(cacheDir(opts.vaultPath), "images", `q${opts.imageQuality}`);
|
|
217
|
+
await mkdir(cacheImageDir, { recursive: true });
|
|
188
218
|
let cacheHits = 0;
|
|
189
219
|
const progress = new Progress("Images");
|
|
190
220
|
progress.update(0, imageFiles.length);
|
|
@@ -192,7 +222,7 @@ export async function buildSite(opts) {
|
|
|
192
222
|
// SVGs / non-compressible images pass through; everything else gets
|
|
193
223
|
// recoded to webp for size. Either way they land in the staging dir.
|
|
194
224
|
const compressed = opts.imageQuality > 0 && COMPRESSIBLE_EXT_RE.test(f.path)
|
|
195
|
-
? await compressImageCached(f, opts.imageQuality,
|
|
225
|
+
? await compressImageCached(f, opts.imageQuality, cacheImageDir, () => { cacheHits++; })
|
|
196
226
|
: { body: await readFile(f.absolute), outputPath: f.path };
|
|
197
227
|
const dest = join(imageStagingDir, compressed.outputPath);
|
|
198
228
|
await mkdir(dirname(dest), { recursive: true });
|
|
@@ -240,6 +270,13 @@ export async function buildSite(opts) {
|
|
|
240
270
|
await writeFile(join(opts.outputDir, "user.css"), userCss);
|
|
241
271
|
if (userCss)
|
|
242
272
|
console.log(` loaded user.css from .obsidian/snippets/`);
|
|
273
|
+
// Browser-side handler assets (built-in + user) concatenated into a
|
|
274
|
+
// single deploy-root JS and CSS file. Skipped entirely if no handler
|
|
275
|
+
// declared any assets (purely declarative handlers stay overhead-free).
|
|
276
|
+
if (hasHandlerJs)
|
|
277
|
+
await writeFile(join(opts.outputDir, "_handlers.js"), handlerAssets.js);
|
|
278
|
+
if (hasHandlerCss)
|
|
279
|
+
await writeFile(join(opts.outputDir, "_handlers.css"), handlerAssets.css);
|
|
243
280
|
// Favicon; either user-supplied via settings.favicon, or a generated
|
|
244
281
|
// default with the vault's first letter in accent on the theme background.
|
|
245
282
|
try {
|
|
@@ -296,6 +333,10 @@ export async function buildSite(opts) {
|
|
|
296
333
|
passthroughStagingDir: otherStagingDir,
|
|
297
334
|
settings: settings.values,
|
|
298
335
|
authConfigured: roles.length > 1,
|
|
336
|
+
handlerRegistry,
|
|
337
|
+
hasHandlerJs,
|
|
338
|
+
hasHandlerCss,
|
|
339
|
+
footerHtml,
|
|
299
340
|
concurrency,
|
|
300
341
|
allWarnings: opts.allWarnings,
|
|
301
342
|
});
|
|
@@ -374,7 +415,7 @@ async function buildVariant(a) {
|
|
|
374
415
|
visibleSources.set(m.path, a.sources.get(m.path));
|
|
375
416
|
}
|
|
376
417
|
// Synthesize folder indexes from the visible set only.
|
|
377
|
-
const folderIndexes = generateFolderIndexes(visibleMetas, a.role);
|
|
418
|
+
const folderIndexes = generateFolderIndexes(visibleMetas, a.role, a.settings.inline_title);
|
|
378
419
|
for (const fi of folderIndexes) {
|
|
379
420
|
visibleMetas.push({ path: fi.path, title: fi.title, role: a.role });
|
|
380
421
|
visibleSources.set(fi.path, fi.markdown);
|
|
@@ -406,6 +447,7 @@ async function buildVariant(a) {
|
|
|
406
447
|
bases: a.baseSources,
|
|
407
448
|
defaultImageWidth: a.settings.default_image_width,
|
|
408
449
|
redactRoles: a.redactRoles,
|
|
450
|
+
handlers: a.handlerRegistry,
|
|
409
451
|
};
|
|
410
452
|
const rendered = new Map();
|
|
411
453
|
const progress = new Progress(`Pages (${a.role})`);
|
|
@@ -453,6 +495,9 @@ async function buildVariant(a) {
|
|
|
453
495
|
centerImages: a.settings.center_images,
|
|
454
496
|
backlinks,
|
|
455
497
|
authConfigured: a.authConfigured,
|
|
498
|
+
hasHandlerJs: a.hasHandlerJs,
|
|
499
|
+
hasHandlerCss: a.hasHandlerCss,
|
|
500
|
+
footerHtml: a.footerHtml,
|
|
456
501
|
...(p.mtime != null ? { mtime: p.mtime } : {}),
|
|
457
502
|
...(p.birthtime != null ? { birthtime: p.birthtime } : {}),
|
|
458
503
|
...(p.coverImage ? { coverImage: p.coverImage } : {}),
|
|
@@ -482,6 +527,9 @@ async function buildVariant(a) {
|
|
|
482
527
|
defaultImageWidth: a.settings.default_image_width,
|
|
483
528
|
centerImages: a.settings.center_images,
|
|
484
529
|
authConfigured: a.authConfigured,
|
|
530
|
+
hasHandlerJs: a.hasHandlerJs,
|
|
531
|
+
hasHandlerCss: a.hasHandlerCss,
|
|
532
|
+
footerHtml: a.footerHtml,
|
|
485
533
|
}));
|
|
486
534
|
// Per-variant search index. `text` is the page's RENDERED HTML body
|
|
487
535
|
// collapsed to plain text (tags stripped, entities decoded), so search
|
|
@@ -624,9 +672,11 @@ async function copyReferencedPassthroughs(visibleSources, passthroughIndex, stag
|
|
|
624
672
|
}
|
|
625
673
|
/**
|
|
626
674
|
* Build synthesised index.md for any folder (including the root) that has
|
|
627
|
-
* pages but no existing index.md.
|
|
675
|
+
* pages but no existing index.md. When `inlineTitle` is true, the layout
|
|
676
|
+
* already injects an <h1> from the page's title, so the synthesised body
|
|
677
|
+
* skips its own `# Title` heading to avoid the duplicate.
|
|
628
678
|
*/
|
|
629
|
-
function generateFolderIndexes(existing, _role) {
|
|
679
|
+
function generateFolderIndexes(existing, _role, inlineTitle) {
|
|
630
680
|
const existingPaths = new Set(existing.map((p) => p.path));
|
|
631
681
|
const folders = new Map();
|
|
632
682
|
folders.set("", { folders: new Set(), pages: [] });
|
|
@@ -675,11 +725,28 @@ function generateFolderIndexes(existing, _role) {
|
|
|
675
725
|
const propsBlock = propsYaml ? `properties:\n${propsYaml}\n` : "";
|
|
676
726
|
sections.push(`## Pages\n\n\`\`\`base\n${filtersBlock}\n${propsBlock}views:\n - type: table\n name: Contents\n order:\n${orderYaml}\n\`\`\``);
|
|
677
727
|
}
|
|
678
|
-
|
|
679
|
-
|
|
728
|
+
// With inline_title on, the layout injects an <h1> from the page's
|
|
729
|
+
// title — which it learns from the markdown's title source. We can
|
|
730
|
+
// either author the title as a `# Heading` (off-mode) or as YAML
|
|
731
|
+
// frontmatter (on-mode); the latter avoids the duplicated <h1> while
|
|
732
|
+
// still letting the renderer surface the right title.
|
|
733
|
+
const displayTitle = title || "Home";
|
|
734
|
+
const heading = inlineTitle ? "" : (title ? `# ${title}\n\n` : "");
|
|
735
|
+
const frontmatter = inlineTitle ? `---\ntitle: ${yamlString(displayTitle)}\n---\n\n` : "";
|
|
736
|
+
out.push({
|
|
737
|
+
path: indexPath,
|
|
738
|
+
title: displayTitle,
|
|
739
|
+
markdown: `${frontmatter}${heading}${sections.join("\n\n")}\n`,
|
|
740
|
+
});
|
|
680
741
|
}
|
|
681
742
|
return out;
|
|
682
743
|
}
|
|
744
|
+
/** YAML-quote a string only when needed (special chars or ambiguous flow). */
|
|
745
|
+
function yamlString(s) {
|
|
746
|
+
if (/^[A-Za-z0-9_ .-]+$/.test(s) && !/^(true|false|null|yes|no)$/i.test(s))
|
|
747
|
+
return s;
|
|
748
|
+
return JSON.stringify(s);
|
|
749
|
+
}
|
|
683
750
|
/**
|
|
684
751
|
* Pick a small set of columns for an auto-generated folder index based on
|
|
685
752
|
* what frontmatter the pages in that folder actually have. The first
|
|
@@ -970,64 +1037,6 @@ function contentTypeForExt(filename) {
|
|
|
970
1037
|
};
|
|
971
1038
|
return map[ext] ?? "application/octet-stream";
|
|
972
1039
|
}
|
|
973
|
-
/**
|
|
974
|
-
* Pre-canonicalisation migration. Earlier versions stored roles, auth_type,
|
|
975
|
-
* and role_passwords in settings.md frontmatter; they now live in
|
|
976
|
-
* .vaultrc.json. If we still see them in settings.md (legacy vault), copy
|
|
977
|
-
* over what's missing in .vaultrc.json so the imminent canonicaliser doesn't
|
|
978
|
-
* silently drop them.
|
|
979
|
-
*
|
|
980
|
-
* Idempotent: returns true and logs only if it actually moved something.
|
|
981
|
-
*/
|
|
982
|
-
async function migrateLegacyAuthFromSettings(vaultPath) {
|
|
983
|
-
const settingsPath = join(vaultPath, SETTINGS_FILE);
|
|
984
|
-
let raw;
|
|
985
|
-
try {
|
|
986
|
-
raw = await readFile(settingsPath, "utf8");
|
|
987
|
-
}
|
|
988
|
-
catch {
|
|
989
|
-
return false;
|
|
990
|
-
}
|
|
991
|
-
const fm = (matter(raw).data ?? {});
|
|
992
|
-
const hasLegacy = "roles" in fm || "auth_type" in fm || "role_passwords" in fm;
|
|
993
|
-
if (!hasLegacy)
|
|
994
|
-
return false;
|
|
995
|
-
const cfg = await loadConfig(vaultPath, {});
|
|
996
|
-
const moved = [];
|
|
997
|
-
// roles: only migrate if cfg is still at the default ["public"].
|
|
998
|
-
if (Array.isArray(fm.roles)) {
|
|
999
|
-
const list = fm.roles.filter((r) => typeof r === "string");
|
|
1000
|
-
const isDefault = cfg.roles.length === 0 || (cfg.roles.length === 1 && cfg.roles[0] === "public");
|
|
1001
|
-
if (list.length > 0 && isDefault && !arraysEqual(list, ["public"])) {
|
|
1002
|
-
cfg.roles = list;
|
|
1003
|
-
moved.push("roles");
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
if (typeof fm.auth_type === "string" && cfg.authType === "password" && fm.auth_type !== "password") {
|
|
1007
|
-
cfg.authType = fm.auth_type;
|
|
1008
|
-
moved.push("auth_type");
|
|
1009
|
-
}
|
|
1010
|
-
if (fm.role_passwords && typeof fm.role_passwords === "object" && !Array.isArray(fm.role_passwords)
|
|
1011
|
-
&& Object.keys(cfg.rolePasswords).length === 0) {
|
|
1012
|
-
const map = fm.role_passwords;
|
|
1013
|
-
const cleaned = {};
|
|
1014
|
-
for (const [k, v] of Object.entries(map))
|
|
1015
|
-
if (typeof v === "string")
|
|
1016
|
-
cleaned[k] = v;
|
|
1017
|
-
if (Object.keys(cleaned).length > 0) {
|
|
1018
|
-
cfg.rolePasswords = cleaned;
|
|
1019
|
-
moved.push("role_passwords");
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
if (moved.length === 0)
|
|
1023
|
-
return false;
|
|
1024
|
-
await saveConfig(vaultPath, cfg);
|
|
1025
|
-
console.log(` migrated ${moved.join(", ")} from settings.md → .vaultrc.json`);
|
|
1026
|
-
return true;
|
|
1027
|
-
}
|
|
1028
|
-
function arraysEqual(a, b) {
|
|
1029
|
-
return a.length === b.length && a.every((x, i) => x === b[i]);
|
|
1030
|
-
}
|
|
1031
1040
|
function extractPlainText(source, max) {
|
|
1032
1041
|
return source
|
|
1033
1042
|
.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "")
|