Compare commits

..

10 Commits

14 changed files with 876 additions and 256 deletions

1
.gitignore vendored
View File

@@ -10,6 +10,7 @@ lerna-debug.log*
node_modules node_modules
dist dist
dist-ssr dist-ssr
dist-vault
*.local *.local
# Editor directories and files # Editor directories and files

View File

@@ -3,14 +3,13 @@ description: an example web application
language: en language: en
vault: vault:
root: public type: file
cleanup: true path: /path/to/vault
view:
index:
notfound:
metadata: metadata:
hidden: hidden:
- example - example
- foo/bar.md - foo/bar.md
routes:
index:
error:

721
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"clean": "rm -rf ./dist", "clean": "rm -rf ./dist*",
"update": "node ./src/scripts/sprachbund.js", "update": "node ./src/scripts/sprachbund.js",
"preview": "vite preview" "preview": "vite preview"
}, },
@@ -14,6 +14,7 @@
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.15",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"front-matter": "^4.0.2", "front-matter": "^4.0.2",
"minio": "^8.0.2",
"mustache": "^4.2.0", "mustache": "^4.2.0",
"postcss": "^8.4.45", "postcss": "^8.4.45",
"tailwindcss": "^3.4.10", "tailwindcss": "^3.4.10",

2
public/.gitignore vendored
View File

@@ -1,2 +0,0 @@
*
!.gitignore

View File

@@ -13,7 +13,7 @@
<body class="antialiased md:subpixel-antialiased"> <body class="antialiased md:subpixel-antialiased">
<div x-data="app" class="relative flex flex-col md:flex-row lg:justify-center justify-normal"> <div x-data="app" class="relative flex flex-col md:flex-row lg:justify-center justify-normal">
<div class="2xl:min-w-80 min-w-72 min-h-12 max-h-screen"> <div class="2xl:min-w-80 min-w-72 min-h-12 max-h-screen">
<div x-data="{ open: false }" x-init="router.on('ready', () => open = false)" <div x-data="{ open: false }" x-init="router.on('active', () => open = false)"
class="flex flex-col fixed 2xl:w-80 md:w-72 size-full md:border-r border-0 pointer-events-none z-20" class="flex flex-col fixed 2xl:w-80 md:w-72 size-full md:border-r border-0 pointer-events-none z-20"
> >
<div class="md:block flex items-center gap-x-4 px-5 md:pt-8 py-3 md:border-0 border-b bg-white pointer-events-auto select-none"> <div class="md:block flex items-center gap-x-4 px-5 md:pt-8 py-3 md:border-0 border-b bg-white pointer-events-auto select-none">

View File

@@ -6,6 +6,7 @@ export const PATH = {};
PATH.CONFIG = 'config.yaml'; PATH.CONFIG = 'config.yaml';
PATH.OUTPUT = 'dist'; PATH.OUTPUT = 'dist';
PATH.OBJECT = `${NATIVE ? PATH.OUTPUT : ''}/.objects`; PATH.S3TEMP = 'dist-vault';
PATH.OBJECT = `${NATIVE ? PATH.OUTPUT : ''}/_site`;
PATH.INDEX = `${PATH.OBJECT}/index`; PATH.INDEX = `${PATH.OBJECT}/index`;
PATH.METADATA = `${PATH.OBJECT}/metadata`; PATH.METADATA = `${PATH.OBJECT}/metadata`;

View File

@@ -1,6 +1,6 @@
import { Engine } from './search' import { Engine } from './search'
import { Index, State } from './object' import { Index, State } from './object'
import router, { Routes } from './router' import router from './router'
import zettelkasten from './zettelkasten' import zettelkasten from './zettelkasten'
import { marked } from 'marked' import { marked } from 'marked'
@@ -32,8 +32,8 @@ export default () => ({
search: new class extends State { search: new class extends State {
constructor() { constructor() {
super(); super();
this.engine = Engine.Default(); this.engine = Engine.Default();
} }
toString() { toString() {
@@ -60,6 +60,21 @@ export default () => ({
</div> </div>
`; `;
} }
fallback() {
this.value = `
<div class="flex size-full justify-center self-center">
<div class="flex flex-col items-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-24 text-neutral-200">
<path fill-rule="evenodd" d="M4.5 2A1.5 1.5 0 0 0 3 3.5v13A1.5 1.5 0 0 0 4.5 18h11a1.5 1.5 0 0 0 1.5-1.5V7.621a1.5 1.5 0 0 0-.44-1.06l-4.12-4.122A1.5 1.5 0 0 0 11.378 2H4.5Zm2.25 8.5a.75.75 0 0 0 0 1.5h6.5a.75.75 0 0 0 0-1.5h-6.5Zm0 3a.75.75 0 0 0 0 1.5h6.5a.75.75 0 0 0 0-1.5h-6.5Z" clip-rule="evenodd" />
</svg>
<div class="font-semibold">
NOT FOUND
</div>
</div>
</div>
`;
}
}, },
graph: new class extends State { graph: new class extends State {
@@ -68,10 +83,10 @@ export default () => ({
<div class="uppercase text-sm font-semibold mb-3">Interactive graph</div> <div class="uppercase text-sm font-semibold mb-3">Interactive graph</div>
<div x-data="{ expand: false, <div x-data="{ expand: false,
anchor: $el.parentElement, anchor: $el.parentElement,
destroy: () => $el.remove(), remove: () => $el.remove(),
collapse: () => { $data.expand = false; collapse: () => { $data.expand = false;
$data.anchor.appendChild($el); $data.anchor.appendChild($el);
$data.toggleGlobal(false) $data.toggleGlobal(false);
}, },
toggleGlobal: (value) => router.emit('graphview toggle global', { value }) toggleGlobal: (value) => router.emit('graphview toggle global', { value })
}" }"
@@ -81,7 +96,7 @@ export default () => ({
<div class="relative rounded-md border size-full p-1 bg-white" <div class="relative rounded-md border size-full p-1 bg-white"
x-data="{ ready: false }" x-data="{ ready: false }"
x-init="if (graph.value?.nodeType) { ready = true; $el.appendChild(graph.value) }" x-init="if (graph.value?.nodeType) { ready = true; $el.appendChild(graph.value) }"
@click="if (expand && $el.children.length === 1) { destroy(); toggleGlobal(false) }" @click="if ($el.children.length < 4) { remove(); toggleGlobal(false) }"
> >
<template x-if="ready"> <template x-if="ready">
<div class="flex items-center gap-x-1 mr-1 mt-1 absolute top-0 right-0 z-20 text-neutral-600"> <div class="flex items-center gap-x-1 mr-1 mt-1 absolute top-0 right-0 z-20 text-neutral-600">
@@ -103,7 +118,7 @@ export default () => ({
</div> </div>
</template> </template>
<template x-if="!ready"> <template x-if="!ready">
<div x-html="graph.init()" class="size-full"> <div x-html="graph.value" class="size-full">
</div> </div>
</template> </template>
</div> </div>
@@ -137,22 +152,18 @@ export default () => ({
} }
}, },
async init() { init() {
this.content.transition(async () => { this.content.transition(async () => {
this.index = await Index.fromPack(); this.index = await Index.fromPack();
this.router.start(); this.content.value = ''; // clear init state
console.log(this.index);
});
Routes.index = () => {
let [url, view] = this.index.createView();
this.graph.transition(async () => { this.graph.transition(async () => {
const { Engine } = await import('./graph'); const { Engine } = await import('./graph');
let engine = Engine.fromIndex(this.index);
return engine.createInstance(url); let engine = Engine.fromIndex(this.index);
let node = await engine.createInstance();
return node;
}); });
this.navigation.transition(() => { this.navigation.transition(() => {
@@ -163,20 +174,20 @@ export default () => ({
return tree.render(false); return tree.render(false);
}); });
this.content.transition(() => { this.router.start(this.index.metadata.routes);
return view?.render() ?? ''; });
});
return url; this.router.route = (path) => {
let view = this.index.getObject(path);
this.content.value = view?.render();
return Boolean(view);
} }
Routes.other = path => { this.router.on('error', () => {
this.content.transition(() => { this.content.fallback();
let [,view] = this.index.createView(path); });
return view.render();
});
}
marked.use(zettelkasten()); marked.use(zettelkasten());
}, }
}) })

View File

@@ -22,7 +22,7 @@ export class Engine {
let source = index.object[src]; let source = index.object[src];
let label = source?.name ?? src; let label = source?.name ?? src;
let path = source?.path ?? src; let path = source?.path ?? src;
if (! graph.hasNode(src)) { if (! graph.hasNode(src)) {
graph.addNode(src, { label, path }); graph.addNode(src, { label, path });
@@ -32,7 +32,7 @@ export class Engine {
let target = index.object[dest]; let target = index.object[dest];
let label = target?.name ?? dest; let label = target?.name ?? dest;
let path = target?.path ?? dest; let path = target?.path ?? dest;
if (! graph.hasNode(dest)) { if (! graph.hasNode(dest)) {
graph.addNode(dest, { label, path }); graph.addNode(dest, { label, path });
@@ -48,20 +48,23 @@ export class Engine {
return new Engine(graph); return new Engine(graph);
} }
createInstance(url) { async createInstance() {
let settings = { allowInvalidContainer: true }; let settings = { allowInvalidContainer: true };
let container = document.createElement('div'); let container = document.createElement('div');
container.style.width = '100%'; container.style.width = '100%';
container.style.height = '100%'; container.style.height = '100%';
let nodes = this.graph.nodes().length;
let delay = nodes * 20;
this.instance = new Sigma(this.graph, container, settings); this.instance = new Sigma(this.graph, container, settings);
this.setupInteraction(); this.setupInteraction();
this.setupGraphStyle(); this.setupGraphStyle(delay);
let camera = this.instance.getCamera(); await new Promise((resolve) => {
router.emit('retrieve', { path: url }); setTimeout(() => resolve(), delay);
camera.animate({ ratio: 1 }, { easing: "linear", duration: 200 }); });
return container; return container;
} }
@@ -136,7 +139,7 @@ export class Engine {
}); });
} }
setupGraphStyle() { setupGraphStyle(delay) {
let localNodes = []; let localNodes = [];
let localEdges = []; let localEdges = [];
let showGlobal = false; let showGlobal = false;
@@ -146,7 +149,7 @@ export class Engine {
if (value) localEdges = this.graph.edges(); if (value) localEdges = this.graph.edges();
}); });
router.on('retrieve', ({ path }) => { let event = router.on('retrieve', ({ path }) => {
let cache = this.instance.nodeDataCache; let cache = this.instance.nodeDataCache;
if (!(path in cache)) { if (!(path in cache)) {
@@ -165,6 +168,8 @@ export class Engine {
localEdges = localNodes.reduce((r, n) => r.concat(this.graph.edges(n)), []); localEdges = localNodes.reduce((r, n) => r.concat(this.graph.edges(n)), []);
}); });
setTimeout(() => event.recall(), delay);
let activeNodes = []; let activeNodes = [];
let activeEdges = []; let activeEdges = [];

View File

@@ -53,9 +53,8 @@ class BaseNode extends BaseObject {
} }
export class Index extends BaseObject { export class Index extends BaseObject {
constructor(view, metadata, object, links = {}) { constructor(metadata, object, links = {}) {
super(); super();
this.view = view;
this.metadata = metadata; this.metadata = metadata;
this.object = object; this.object = object;
this.links = links; this.links = links;
@@ -64,13 +63,7 @@ export class Index extends BaseObject {
static Metadata(x) { static Metadata(x) {
return { return {
hidden: x?.hidden, hidden: x?.hidden,
} routes: x?.routes,
}
static View(x) {
return {
index: x?.index,
notfound: x?.notfound
} }
} }
@@ -151,27 +144,6 @@ export class Index extends BaseObject {
return [root, searchable]; return [root, searchable];
} }
createView(path = '') {
let entry = this.getObject(path);
let view = this.view;
if (! path) {
path = view.index ?? '';
entry = this.getObject(path);
if (!entry) return [path, Document.Blank()];
}
if (! entry) {
path = view.notfound ?? '';
entry = this.getObject(path);
if (!entry) return [path, Document.NotFound()];
}
return [path, entry];
}
} }
export class Folder extends BaseNode { export class Folder extends BaseNode {
@@ -211,8 +183,8 @@ export class Folder extends BaseNode {
data-[active=true]:text-red-600 hover:text-neutral-950 data-[active=true]:text-red-600 hover:text-neutral-950
data-[active=true]:border-red-600 hover:border-neutral-800" data-[active=true]:border-red-600 hover:border-neutral-800"
> >
<a class="truncate" href="/${node.path}" <a class="truncate md:text-base text-lg" href="/${node.path}"
@click="router.emit('navigate'); router.from($event)" @click="navigate = true; router.from($event)"
>${node.name}</a> >${node.name}</a>
</div> </div>
`; `;
@@ -227,9 +199,11 @@ export class Folder extends BaseNode {
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5" :class="open ? 'rotate-90' : ''"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5" :class="open ? 'rotate-90' : ''">
<path fill-rule="evenodd" d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
</svg> </svg>
<span class="md:order-last order-first">${this.name}</span> <span class="md:order-last order-first md:text-base text-lg">
${this.name}
</span>
</div> </div>
<div x-show="open" style="--max: ${maxHeight}px" class="flex flex-col md:ml-4 ml-1 mb-1 md:pr-0 pr-8 overflow-hidden transition-[max-height]" <div x-show="open" style="--max: ${maxHeight}px" class="flex flex-col md:ml-4 ml-1 mb-1 md:pr-0 pr-1.5 overflow-hidden transition-[max-height]"
x-transition:enter="ease-out duration-100" x-transition:enter="ease-out duration-100"
x-transition:enter-start="max-h-0" x-transition:enter-start="max-h-0"
x-transition:enter-end="max-h-[var(--max)]" x-transition:enter-end="max-h-[var(--max)]"
@@ -242,13 +216,14 @@ export class Folder extends BaseNode {
return ` return `
<div x-data="{ active: null, navigate: false }" <div x-data="{ active: null, navigate: false }"
x-init="router.on('ready', ({ url }) => { x-init="let react = () => {
let isFromNav = navigate; let path = window.location.pathname;
active?.setAttribute('data-active', false); active?.setAttribute('data-active', false);
active = $el.querySelector(\`[data-path=&quot;\${url}&quot;]\`); active = $el.querySelector(\`[data-path=&quot;\${path}&quot;]\`);
active?.setAttribute('data-active', true); active?.setAttribute('data-active', true);
if (isFromNav) return; if (navigate) return navigate = false;
let node = active; let node = active;
if (node) while ((node = node.parentElement) !== $el) { if (node) while ((node = node.parentElement) !== $el) {
@@ -258,11 +233,11 @@ export class Folder extends BaseNode {
} }
setTimeout(() => active?.scrollIntoView( setTimeout(() => active?.scrollIntoView(
{ behavior: 'smooth', block: 'center' }), 150); { behavior: 'smooth', block: 'center' }), 150);
};
router.on('ready', () => {
$nextTick(() => react());
}); });
router.on('navigate', () => { $nextTick(() => react())"
navigate = true;
setTimeout(() => navigate = false, 100);
})"
>${html}</div> >${html}</div>
`; `;
} }
@@ -278,29 +253,6 @@ export class Document extends BaseNode {
this.links = []; this.links = [];
} }
static NotFound() {
return {
render: () => `
<div class="flex size-full justify-center self-center">
<div class="flex flex-col items-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-24 text-neutral-200">
<path fill-rule="evenodd" d="M4.5 2A1.5 1.5 0 0 0 3 3.5v13A1.5 1.5 0 0 0 4.5 18h11a1.5 1.5 0 0 0 1.5-1.5V7.621a1.5 1.5 0 0 0-.44-1.06l-4.12-4.122A1.5 1.5 0 0 0 11.378 2H4.5Zm2.25 8.5a.75.75 0 0 0 0 1.5h6.5a.75.75 0 0 0 0-1.5h-6.5Zm0 3a.75.75 0 0 0 0 1.5h6.5a.75.75 0 0 0 0-1.5h-6.5Z" clip-rule="evenodd" />
</svg>
<div class="font-semibold">
NOT FOUND
</div>
</div>
</div>
`
}
}
static Blank() {
return {
render: () => ''
}
}
fullname() { fullname() {
return this.name + '.md'; return this.name + '.md';
} }
@@ -312,13 +264,7 @@ export class Document extends BaseNode {
let title = this.metadata.title ?? this.name; let title = this.metadata.title ?? this.name;
document.title = `${title} - ${appName}`; document.title = `${title} - ${appName}`;
return ` let sidebar = () => `
<div class="flex flex-col w-full md:mx-12 m-6 md:mt-8">
<div class="font-bold text-4xl mb-6">${title}</div>
<div x-ref="page" class="prose prose-neutral md:max-w-prose max-w-none w-full">
${marked(this.content)}
</div>
</div>
<div class="lg:block hidden 2xl:min-w-80 max-w-72 w-full max-h-screen"> <div class="lg:block hidden 2xl:min-w-80 max-w-72 w-full max-h-screen">
<div class="flex flex-col fixed gap-y-8 pt-6 2xl:w-80 w-72 select-none"> <div class="flex flex-col fixed gap-y-8 pt-6 2xl:w-80 w-72 select-none">
<!-- Interactive Graph --> <!-- Interactive Graph -->
@@ -329,16 +275,16 @@ export class Document extends BaseNode {
$nextTick(() => headings = $refs.page.querySelectorAll(':not([data-type]) > :is(h1, h2, h3, h4, h5, h6)'))" $nextTick(() => headings = $refs.page.querySelectorAll(':not([data-type]) > :is(h1, h2, h3, h4, h5, h6)'))"
x-show="headings.length > 0" x-show="headings.length > 0"
class="mr-8" class="mr-8"
> >
<div class="uppercase text-sm font-semibold mb-3">On this page</div> <div class="uppercase text-sm font-semibold mb-3">On this page</div>
<div class="overflow-y-auto overscroll-contain 2xl:max-h-96 max-h-64"> <div class="overflow-y-auto overscroll-contain 2xl:max-h-96 max-h-64">
<template x-for="heading in headings"> <template x-for="heading in headings">
<div :data-level="heading.tagName" <div :data-level="heading.tagName"
x-data="{ id: '#' + heading.innerText, scroll: () => heading.scrollIntoView() }" x-data="{ id: '#' + heading.innerText, scroll: () => heading.scrollIntoView() }"
x-init="if (hash === id) scroll()" x-init="if (hash === id) scroll()"
class="data-[level=H1]:pl-0 data-[level=H1]:border-0 data-[level=H1]:ml-0 class="data-[level=H1]:pl-0 data-[level=H1]:border-0 data-[level=H1]:ml-0
data-[level=H2]:ml-1 data-[level=H3]:ml-5 data-[level=H4]:ml-9 data-[level=H5]:ml-[3.25rem] ml-[4.25rem] data-[level=H2]:ml-1 data-[level=H3]:ml-5 data-[level=H4]:ml-9 data-[level=H5]:ml-[3.25rem] ml-[4.25rem]
pl-3 border-l" pl-3 border-l"
> >
<a x-text="heading.innerText" <a x-text="heading.innerText"
class="text-neutral-500 hover:text-neutral-800" class="text-neutral-500 hover:text-neutral-800"
@@ -352,6 +298,20 @@ export class Document extends BaseNode {
</div> </div>
</div> </div>
`; `;
return `
<div x-init="$nextTick(() => router.emit('ready'))"
class="flex flex-col w-full md:mx-12 m-6 md:mt-8"
>
<div class="font-bold text-4xl mb-6">${title}</div>
<div x-ref="page" class="prose prose-neutral max-w-none w-full
${this.metadata['no_sidebar'] ? '' : 'md:max-w-prose'}"
>
${marked(this.content)}
</div>
</div>
${this.metadata['no_sidebar'] ? '' : sidebar()}
`;
} }
inline(attributes = {}) { inline(attributes = {}) {

View File

@@ -1,24 +1,34 @@
export const Routes = { const Routes = {
index() {}, index: null,
other() {} error: null
} }
const Events = [ const Events = {
/* eventName: [callbacks], */ entries: { /* eventName: [callbacks], */ },
] history: [ /* { eventName, params, }, */ ],
}
export default { export default {
react() { react() {
let url = window.location.pathname; let path = decodeURI(window.location.pathname);
if (url === '/') { if (path === '/') {
url = encodeURI(Routes.index()); if (! Routes.index) return;
history.replaceState({}, '', url); history.replaceState({}, '', Routes.index);
} else { return this.react();
Routes.other(decodeURI(url));
} }
this.emit('ready', { url });
if (! this.route(path)) {
if (! Routes.error) this.emit('error');
else this.goto(Routes.error);
}
this.emit('active');
}, },
start() { route(path) {
return true;
},
start(routes) {
Object.assign(Routes, routes);
window.addEventListener("popstate", () => this.react()); window.addEventListener("popstate", () => this.react());
this.react(); this.react();
}, },
@@ -31,11 +41,26 @@ export default {
history.pushState({}, '', base + url); history.pushState({}, '', base + url);
this.react(); this.react();
}, },
emit(event, params) { emit(eventName, params) {
Events[event]?.forEach(fn => fn(params)); Events.history.unshift({ eventName, params });
Events.history.length = Math.min(Events.history.length, 50);
Events.entries[eventName]?.forEach(fn => fn(params));
}, },
on(event, callback) { on(eventName, callback) {
if (! Events[event]) Events[event] = []; if (! Events.entries[eventName]) Events.entries[eventName] = [];
Events[event]?.push(callback); Events.entries[eventName]?.push(callback);
return {
recall: () => {
for (let e of Events.history)
if (e.eventName === eventName)
return callback(e.params);
},
cancel: () => {
let callbacks = Events.entries[eventName];
this.on(eventName, () => Events.entries[eventName] = callbacks);
}
};
} }
} }

View File

@@ -25,12 +25,19 @@ export class Engine {
template(namespace) { template(namespace) {
return ` return `
<div x-data="{ value: '', active: false }" class="relative mx-5 mt-6" @focusin="active = true" @focusout="active = false"> <div class="relative mx-5 mt-6 md:text-base text-xl"
x-data="{ value: '', active: false }"
@focusin="active = true" @focusout="active = false"
>
<div class="flex focus-within:ring-2 ring-neutral-300 transition w-full items-center rounded-md border pl-1 py-0.5"> <div class="flex focus-within:ring-2 ring-neutral-300 transition w-full items-center rounded-md border pl-1 py-0.5">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5 text-neutral-400"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5 text-neutral-400">
<path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11ZM2 9a7 7 0 1 1 12.452 4.391l3.328 3.329a.75.75 0 1 1-1.06 1.06l-3.329-3.328A7 7 0 0 1 2 9Z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11ZM2 9a7 7 0 1 1 12.452 4.391l3.328 3.329a.75.75 0 1 1-1.06 1.06l-3.329-3.328A7 7 0 0 1 2 9Z" clip-rule="evenodd" />
</svg> </svg>
<input type="text" x-model="value" class="w-full px-2 placeholder:text-neutral-300 focus:outline-none" placeholder="Search page or heading..."> <input x-model="value"
class="w-full px-2 placeholder:text-neutral-300 placeholder:text-sm focus:outline-none"
type="text"
placeholder="Search page or heading..."
>
</div> </div>
<div x-show="active && value" class="absolute rounded-md border bg-white left-0 top-0 mt-9 min-w-48 max-h-96 shadow-xl overflow-y-auto overscroll-contain"> <div x-show="active && value" class="absolute rounded-md border bg-white left-0 top-0 mt-9 min-w-48 max-h-96 shadow-xl overflow-y-auto overscroll-contain">
<div x-data="{ results: [] }" x-effect="results = ${namespace}.search(value)" class="empty:hidden flex flex-col p-1.5"> <div x-data="{ results: [] }" x-effect="results = ${namespace}.search(value)" class="empty:hidden flex flex-col p-1.5">
@@ -51,7 +58,7 @@ export class Engine {
generateHTML(result) { generateHTML(result) {
return ` return `
<button class="flex flex-col size-full gap-y-1.5 text-start rounded-md hover:bg-neutral-200 p-2" <button class="flex flex-col size-full gap-y-1.5 text-start rounded-md hover:bg-neutral-200 p-2"
@click="router.goto(&quot;${result.path}&quot;); active = false" @click="$nextTick(() => active = false); router.goto(&quot;${result.path}&quot;)"
> >
<span>${result.name}</span> <span>${result.name}</span>
<span class="text-xs text-neutral-500" <span class="text-xs text-neutral-500"

View File

@@ -2,12 +2,13 @@ import { PATH } from '../paths.js'
import { transform } from './zettelkasten.js' import { transform } from './zettelkasten.js'
import { Index, Attachment, Document } from './object.js' import { Index, Attachment, Document } from './object.js'
import { createHash } from 'node:crypto' import { Client } from 'minio'
import { createHash } from 'crypto'
import { encode, decode } from '@msgpack/msgpack' import { encode, decode } from '@msgpack/msgpack'
import Mustache from 'mustache' import Mustache from 'mustache'
import YAML from 'yaml' import YAML from 'yaml'
import fs from 'node:fs' import fs from 'fs'
import fm from'front-matter' import fm from'front-matter'
@@ -20,8 +21,8 @@ function walkSourceDir([root, parent], callback) {
let filenames = fs.readdirSync(base); let filenames = fs.readdirSync(base);
for (let name of filenames) { for (let name of filenames) {
// omit filenames starting with '.' // omit filenames starting with '.' or '_'
if (name.startsWith(".")) continue; if (name.startsWith(".") || name.startsWith('_')) continue;
let path = [base, name].join('/'); let path = [base, name].join('/');
let relpath = [parent, name].filter(Boolean).join('/'); let relpath = [parent, name].filter(Boolean).join('/');
@@ -63,7 +64,9 @@ function bisect(filename) {
} }
function buildMdDocs(index, ctx) /* always return falsy values */ { function buildMdDocs(index, ctx) /* always return falsy values */ {
let { attributes, body } = fm(ctx.content); let content = fs.readFileSync(ctx.path, 'utf8');
let { attributes, body } = fm(content);
let pathname = [ctx.parent, ctx.short].filter(Boolean).join('/'); let pathname = [ctx.parent, ctx.short].filter(Boolean).join('/');
let text = transform.wikilink(body, href => { let text = transform.wikilink(body, href => {
@@ -116,10 +119,7 @@ function buildMdDocs(index, ctx) /* always return falsy values */ {
function buildObject(index, ctx) /* always return falsy values */ { function buildObject(index, ctx) /* always return falsy values */ {
if (ctx.extension === 'md') { if (ctx.extension === 'md') {
let content = fs.readFileSync(ctx.path, 'utf8'); return buildMdDocs(index, ctx);
let context = { content, ...ctx };
return buildMdDocs(index, context);
} }
let content = fs.readFileSync(ctx.path); let content = fs.readFileSync(ctx.path);
@@ -140,37 +140,25 @@ function buildObject(index, ctx) /* always return falsy values */ {
fs.writeFileSync(path, content); fs.writeFileSync(path, content);
} }
function buildIndex(config, vault) { function buildIndex(metadata, vault) {
let metadata = Index.Metadata(config["metadata"]); let index = new Index(metadata, {});
let view = Index.View(config["view"]);
let index = new Index(view, metadata, {});
if (! fs.existsSync(PATH.INDEX)) { if (! fs.existsSync(PATH.INDEX)) {
// create index if not found // create index if not found
// or reuse it // or reuse it
fs.mkdirSync(PATH.OBJECT, { recursive: true }); fs.mkdirSync(PATH.OBJECT, { recursive: true });
walkSourceDir([config.vault.root], ctx => { walkSourceDir([vault], ctx => {
buildObject(index, ctx); buildObject(index, ctx);
}); });
if (config.vault.cleanup) {
fs.readdirSync(config.vault.root).forEach(name => {
if (name !== '.gitignore') {
let path = [config.vault.root, name].join('/');
fs.rmSync(path, { recursive: true, force: true });
}
});
}
return index; return index;
} }
let buffer = fs.readFileSync(PATH.INDEX); let buffer = fs.readFileSync(PATH.INDEX);
let legacy = decode(buffer); let legacy = decode(buffer);
walkSourceDir([config.vault.root], ctx => { walkSourceDir([vault], ctx => {
let path = ctx.path; let path = ctx.path;
let entry = legacy.object[path]; let entry = legacy.object[path];
// if no such legacy entry found, continue // if no such legacy entry found, continue
@@ -200,6 +188,51 @@ function buildIndex(config, vault) {
return index; return index;
} }
function prepareVault(config) {
switch (config["type"]) {
case "minio":
fs.rmSync(PATH.S3TEMP, { recursive: true, force: true });
fs.mkdirSync(PATH.S3TEMP);
let bucket = config["bucket"];
let prefix = config["prefix"];
let client = new Client(config["client"]);
let stream = client.listObjectsV2(bucket, prefix, true);
stream.on('data', item => {
let name = item.name.replace(prefix, '');
let path = `${PATH.S3TEMP}/${name}`;
if (name.endsWith('/')) {
if (! fs.existsSync(path)) fs.mkdirSync(path);
return;
}
stream.pause();
console.log(item.name);
client.fGetObject(bucket, item.name, path)
.catch(err => { throw err })
.finally(() => stream.resume());
});
return new Promise((resolve, reject) => {
stream.on('close', () => resolve(PATH.S3TEMP));
stream.on('error', er => reject(er));
});
case "file":
let path = config["path"];
if (fs.lstatSync(path).isDirectory()) {
return new Promise((resolve) => resolve(path));
}
throw new Error(path + ": not a valid vault");
default:
throw new Error("invalid vault type");
}
}
if (process.argv[1] === import.meta.filename) { if (process.argv[1] === import.meta.filename) {
// when running under node.js solely // when running under node.js solely
sprachbund().writeBundle(); sprachbund().writeBundle();
@@ -216,11 +249,11 @@ export default function sprachbund() {
language: CONFIG?.language, language: CONFIG?.language,
}); });
}, },
writeBundle() { async writeBundle() {
let index = buildIndex(CONFIG); let vault = await prepareVault(CONFIG.vault);
let data = encode(index); let index = buildIndex(CONFIG.metadata, vault);
fs.writeFileSync(PATH.INDEX, data); fs.writeFileSync(PATH.INDEX, encode(index));
}, },
} }
} }

View File

@@ -5,7 +5,7 @@ export default defineConfig({
root: 'src', root: 'src',
build: { build: {
outDir: '../dist', outDir: '../dist',
assetsDir: '.assets', assetsDir: '_assets',
emptyOutDir: true emptyOutDir: true
}, },
plugins: [ plugins: [