feat: init
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { Component, TFile } from "obsidian";
|
||||
import { mount } from "svelte";
|
||||
import App from "../src/ui/app.svelte";
|
||||
import { DEFAULT_SETTINGS } from "../src/settings";
|
||||
import { BoardStore } from "../src/vault/store.svelte";
|
||||
|
||||
const BOARDS = ["personal", "work", "reading", "home"];
|
||||
const FOLDER = "Boards";
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const files = new Map<string, string>();
|
||||
await Promise.all(
|
||||
BOARDS.map(async (name) => {
|
||||
const response = await fetch(`/tests/fixtures/${name}.md`);
|
||||
files.set(`${FOLDER}/${name}.md`, await response.text());
|
||||
}),
|
||||
);
|
||||
const handles = new Map(
|
||||
[...files.keys()].map((path) => [path, new TFile(path)]),
|
||||
);
|
||||
|
||||
const app = {
|
||||
vault: {
|
||||
getMarkdownFiles: () => [...handles.values()],
|
||||
getAbstractFileByPath: (path: string) => handles.get(path) ?? null,
|
||||
cachedRead: async (file: TFile) => files.get(file.path) ?? "",
|
||||
read: async (file: TFile) => files.get(file.path) ?? "",
|
||||
process: async (file: TFile, fn: (data: string) => string) => {
|
||||
const next = fn(files.get(file.path) ?? "");
|
||||
files.set(file.path, next);
|
||||
store.queueReload(file.path);
|
||||
return next;
|
||||
},
|
||||
on: () => ({}),
|
||||
},
|
||||
workspace: { getLeaf: () => ({}), activeEditor: null },
|
||||
plugins: { plugins: {} },
|
||||
};
|
||||
|
||||
const settings = { ...DEFAULT_SETTINGS, boardsFolder: FOLDER };
|
||||
const store = new BoardStore(app as never, settings, () => {});
|
||||
await store.reloadAll();
|
||||
|
||||
mount(App, {
|
||||
target: document.getElementById("app") as HTMLElement,
|
||||
props: {
|
||||
app: app as never,
|
||||
store,
|
||||
settings,
|
||||
component: new Component() as never,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
void main();
|
||||
@@ -0,0 +1,36 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>beaver-calendar preview</title>
|
||||
<link rel="stylesheet" href="/app.css" />
|
||||
<link rel="stylesheet" href="/theme.css" />
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
<style>
|
||||
body.theme-dark,
|
||||
body.theme-light {
|
||||
--accent-h: 254;
|
||||
--accent-s: 80%;
|
||||
--accent-l: 68%;
|
||||
--font-interface:
|
||||
-apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto,
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: var(--background-primary);
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="theme-dark">
|
||||
<div id="app"></div>
|
||||
<script src="/preview/bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,118 @@
|
||||
import {
|
||||
createReadStream,
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
readdirSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { createServer } from "node:http";
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
import { extname, join, normalize } from "node:path";
|
||||
import { extractFile } from "@electron/asar";
|
||||
import esbuild from "esbuild";
|
||||
import sveltePlugin from "esbuild-svelte";
|
||||
|
||||
const root = new URL("..", import.meta.url).pathname;
|
||||
const port = Number(process.env.PORT ?? 4173);
|
||||
|
||||
const ASAR_CANDIDATES = [
|
||||
process.env.OBSIDIAN_ASAR,
|
||||
"/Applications/Obsidian.app/Contents/Resources/obsidian.asar",
|
||||
join(homedir(), "Applications/Obsidian.app/Contents/Resources/obsidian.asar"),
|
||||
"/opt/Obsidian/resources/obsidian.asar",
|
||||
"/usr/lib/obsidian/resources/obsidian.asar",
|
||||
].filter(Boolean);
|
||||
|
||||
function findAppCss() {
|
||||
for (const candidate of ASAR_CANDIDATES) {
|
||||
if (!existsSync(candidate)) continue;
|
||||
try {
|
||||
const path = join(mkdtempSync(join(tmpdir(), "bcal-app-")), "app.css");
|
||||
writeFileSync(path, extractFile(candidate, "app.css"));
|
||||
return path;
|
||||
} catch {
|
||||
/* try the next candidate */
|
||||
}
|
||||
}
|
||||
console.warn(
|
||||
"preview: could not read app.css from an Obsidian install. Set " +
|
||||
"OBSIDIAN_ASAR to point at obsidian.asar. Without it, button and input " +
|
||||
"chrome will not match the real app.",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
function findTheme() {
|
||||
if (process.env.THEME) return process.env.THEME;
|
||||
const vault = process.env.VAULT;
|
||||
if (!vault) return null;
|
||||
const themes = join(vault, ".obsidian/themes");
|
||||
if (!existsSync(themes)) return null;
|
||||
for (const name of readdirSync(themes)) {
|
||||
const css = join(themes, name, "theme.css");
|
||||
if (existsSync(css)) return css;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const appCss = findAppCss();
|
||||
const theme = findTheme();
|
||||
if (!theme) {
|
||||
console.warn(
|
||||
"preview: no community theme loaded. Set VAULT to a vault path, or " +
|
||||
"THEME to a theme.css, to preview against one.",
|
||||
);
|
||||
}
|
||||
|
||||
const ctx = await esbuild.context({
|
||||
entryPoints: [join(root, "preview/entry.ts")],
|
||||
bundle: true,
|
||||
format: "iife",
|
||||
target: "es2022",
|
||||
outfile: join(root, "preview/bundle.js"),
|
||||
logLevel: "info",
|
||||
sourcemap: "inline",
|
||||
conditions: ["svelte", "browser"],
|
||||
mainFields: ["svelte", "browser", "module", "main"],
|
||||
alias: { obsidian: join(root, "tests/obsidian-stub.ts") },
|
||||
plugins: [sveltePlugin({ compilerOptions: { css: "injected", runes: true } })],
|
||||
});
|
||||
await ctx.rebuild();
|
||||
await ctx.watch();
|
||||
|
||||
const TYPES = {
|
||||
".css": "text/css; charset=utf-8",
|
||||
".html": "text/html; charset=utf-8",
|
||||
".js": "text/javascript; charset=utf-8",
|
||||
".md": "text/plain; charset=utf-8",
|
||||
};
|
||||
|
||||
createServer((request, response) => {
|
||||
const url = decodeURIComponent((request.url ?? "/").split("?")[0]);
|
||||
const external = { "/app.css": appCss, "/theme.css": theme };
|
||||
|
||||
if (url in external) {
|
||||
const file = external[url];
|
||||
if (!file) {
|
||||
response.writeHead(200, { "content-type": TYPES[".css"] }).end("");
|
||||
return;
|
||||
}
|
||||
response.writeHead(200, { "content-type": TYPES[".css"], "cache-control": "no-store" });
|
||||
createReadStream(file).pipe(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const file = join(root, normalize(url === "/" ? "/preview/index.html" : url));
|
||||
if (!file.startsWith(root) || !existsSync(file) || statSync(file).isDirectory()) {
|
||||
response.writeHead(404).end("not found");
|
||||
return;
|
||||
}
|
||||
response.writeHead(200, {
|
||||
"content-type": TYPES[extname(file)] ?? "application/octet-stream",
|
||||
"cache-control": "no-store",
|
||||
});
|
||||
createReadStream(file).pipe(response);
|
||||
}).listen(port, () => {
|
||||
console.log(`preview: http://localhost:${port}/`);
|
||||
});
|
||||
Reference in New Issue
Block a user