Compare commits
10 Commits
535b4d1cfd
...
3a1d28a3c5
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a1d28a3c5 | |||
| 92d42b0f69 | |||
| 3eb27c48ea | |||
| ed12a9e0f2 | |||
| 520040a6bf | |||
| 48fa39ed64 | |||
| 4db1bc8244 | |||
| 23c1cc6b72 | |||
| 24a026c58e | |||
| c7bae4a2b5 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@ lerna-debug.log*
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
dist-vault
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
|
||||
11
config.yaml
11
config.yaml
@@ -3,14 +3,13 @@ description: an example web application
|
||||
language: en
|
||||
|
||||
vault:
|
||||
root: public
|
||||
cleanup: true
|
||||
|
||||
view:
|
||||
index:
|
||||
notfound:
|
||||
type: file
|
||||
path: /path/to/vault
|
||||
|
||||
metadata:
|
||||
hidden:
|
||||
- example
|
||||
- foo/bar.md
|
||||
routes:
|
||||
index:
|
||||
error:
|
||||
|
||||
721
package-lock.json
generated
721
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"clean": "rm -rf ./dist",
|
||||
"clean": "rm -rf ./dist*",
|
||||
"update": "node ./src/scripts/sprachbund.js",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
@@ -14,6 +14,7 @@
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"front-matter": "^4.0.2",
|
||||
"minio": "^8.0.2",
|
||||
"mustache": "^4.2.0",
|
||||
"postcss": "^8.4.45",
|
||||
"tailwindcss": "^3.4.10",
|
||||
|
||||
2
public/.gitignore
vendored
2
public/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -13,7 +13,7 @@
|
||||
<body class="antialiased md:subpixel-antialiased">
|
||||
<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 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"
|
||||
>
|
||||
<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">
|
||||
|
||||
@@ -6,6 +6,7 @@ export const PATH = {};
|
||||
|
||||
PATH.CONFIG = 'config.yaml';
|
||||
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.METADATA = `${PATH.OBJECT}/metadata`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Engine } from './search'
|
||||
import { Index, State } from './object'
|
||||
import router, { Routes } from './router'
|
||||
import router from './router'
|
||||
import zettelkasten from './zettelkasten'
|
||||
|
||||
import { marked } from 'marked'
|
||||
@@ -32,8 +32,8 @@ export default () => ({
|
||||
|
||||
search: new class extends State {
|
||||
constructor() {
|
||||
super();
|
||||
this.engine = Engine.Default();
|
||||
super();
|
||||
this.engine = Engine.Default();
|
||||
}
|
||||
|
||||
toString() {
|
||||
@@ -60,6 +60,21 @@ export default () => ({
|
||||
</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 {
|
||||
@@ -68,10 +83,10 @@ export default () => ({
|
||||
<div class="uppercase text-sm font-semibold mb-3">Interactive graph</div>
|
||||
<div x-data="{ expand: false,
|
||||
anchor: $el.parentElement,
|
||||
destroy: () => $el.remove(),
|
||||
remove: () => $el.remove(),
|
||||
collapse: () => { $data.expand = false;
|
||||
$data.anchor.appendChild($el);
|
||||
$data.toggleGlobal(false)
|
||||
$data.toggleGlobal(false);
|
||||
},
|
||||
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"
|
||||
x-data="{ ready: false }"
|
||||
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">
|
||||
<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>
|
||||
</template>
|
||||
<template x-if="!ready">
|
||||
<div x-html="graph.init()" class="size-full">
|
||||
<div x-html="graph.value" class="size-full">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -137,46 +152,42 @@ export default () => ({
|
||||
}
|
||||
},
|
||||
|
||||
async init() {
|
||||
init() {
|
||||
this.content.transition(async () => {
|
||||
this.index = await Index.fromPack();
|
||||
this.router.start();
|
||||
|
||||
console.log(this.index);
|
||||
});
|
||||
|
||||
Routes.index = () => {
|
||||
let [url, view] = this.index.createView();
|
||||
this.content.value = ''; // clear init state
|
||||
|
||||
this.graph.transition(async () => {
|
||||
const { Engine } = await import('./graph');
|
||||
|
||||
let engine = Engine.fromIndex(this.index);
|
||||
|
||||
return engine.createInstance(url);
|
||||
let node = await engine.createInstance();
|
||||
|
||||
return node;
|
||||
});
|
||||
|
||||
|
||||
this.navigation.transition(() => {
|
||||
let [tree, searchable] = this.index.createTree();
|
||||
let search = this.search.engine.getInstance();
|
||||
|
||||
|
||||
search.addAllAsync(searchable);
|
||||
return tree.render(false);
|
||||
});
|
||||
|
||||
this.content.transition(() => {
|
||||
return view?.render() ?? '';
|
||||
});
|
||||
this.router.start(this.index.metadata.routes);
|
||||
});
|
||||
|
||||
return url;
|
||||
this.router.route = (path) => {
|
||||
let view = this.index.getObject(path);
|
||||
|
||||
this.content.value = view?.render();
|
||||
return Boolean(view);
|
||||
}
|
||||
|
||||
Routes.other = path => {
|
||||
this.content.transition(() => {
|
||||
let [,view] = this.index.createView(path);
|
||||
return view.render();
|
||||
});
|
||||
}
|
||||
this.router.on('error', () => {
|
||||
this.content.fallback();
|
||||
});
|
||||
|
||||
marked.use(zettelkasten());
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -22,7 +22,7 @@ export class Engine {
|
||||
let source = index.object[src];
|
||||
|
||||
let label = source?.name ?? src;
|
||||
let path = source?.path ?? src;
|
||||
let path = source?.path ?? src;
|
||||
|
||||
if (! graph.hasNode(src)) {
|
||||
graph.addNode(src, { label, path });
|
||||
@@ -32,7 +32,7 @@ export class Engine {
|
||||
let target = index.object[dest];
|
||||
|
||||
let label = target?.name ?? dest;
|
||||
let path = target?.path ?? dest;
|
||||
let path = target?.path ?? dest;
|
||||
|
||||
if (! graph.hasNode(dest)) {
|
||||
graph.addNode(dest, { label, path });
|
||||
@@ -48,20 +48,23 @@ export class Engine {
|
||||
return new Engine(graph);
|
||||
}
|
||||
|
||||
createInstance(url) {
|
||||
async createInstance() {
|
||||
let settings = { allowInvalidContainer: true };
|
||||
let container = document.createElement('div');
|
||||
|
||||
container.style.width = '100%';
|
||||
container.style.height = '100%';
|
||||
|
||||
let nodes = this.graph.nodes().length;
|
||||
let delay = nodes * 20;
|
||||
|
||||
this.instance = new Sigma(this.graph, container, settings);
|
||||
this.setupInteraction();
|
||||
this.setupGraphStyle();
|
||||
this.setupGraphStyle(delay);
|
||||
|
||||
let camera = this.instance.getCamera();
|
||||
router.emit('retrieve', { path: url });
|
||||
camera.animate({ ratio: 1 }, { easing: "linear", duration: 200 });
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(() => resolve(), delay);
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
@@ -136,7 +139,7 @@ export class Engine {
|
||||
});
|
||||
}
|
||||
|
||||
setupGraphStyle() {
|
||||
setupGraphStyle(delay) {
|
||||
let localNodes = [];
|
||||
let localEdges = [];
|
||||
let showGlobal = false;
|
||||
@@ -146,7 +149,7 @@ export class Engine {
|
||||
if (value) localEdges = this.graph.edges();
|
||||
});
|
||||
|
||||
router.on('retrieve', ({ path }) => {
|
||||
let event = router.on('retrieve', ({ path }) => {
|
||||
let cache = this.instance.nodeDataCache;
|
||||
|
||||
if (!(path in cache)) {
|
||||
@@ -165,6 +168,8 @@ export class Engine {
|
||||
localEdges = localNodes.reduce((r, n) => r.concat(this.graph.edges(n)), []);
|
||||
});
|
||||
|
||||
setTimeout(() => event.recall(), delay);
|
||||
|
||||
let activeNodes = [];
|
||||
let activeEdges = [];
|
||||
|
||||
|
||||
@@ -53,9 +53,8 @@ class BaseNode extends BaseObject {
|
||||
}
|
||||
|
||||
export class Index extends BaseObject {
|
||||
constructor(view, metadata, object, links = {}) {
|
||||
constructor(metadata, object, links = {}) {
|
||||
super();
|
||||
this.view = view;
|
||||
this.metadata = metadata;
|
||||
this.object = object;
|
||||
this.links = links;
|
||||
@@ -64,13 +63,7 @@ export class Index extends BaseObject {
|
||||
static Metadata(x) {
|
||||
return {
|
||||
hidden: x?.hidden,
|
||||
}
|
||||
}
|
||||
|
||||
static View(x) {
|
||||
return {
|
||||
index: x?.index,
|
||||
notfound: x?.notfound
|
||||
routes: x?.routes,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,27 +144,6 @@ export class Index extends BaseObject {
|
||||
|
||||
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 {
|
||||
@@ -211,8 +183,8 @@ export class Folder extends BaseNode {
|
||||
data-[active=true]:text-red-600 hover:text-neutral-950
|
||||
data-[active=true]:border-red-600 hover:border-neutral-800"
|
||||
>
|
||||
<a class="truncate" href="/${node.path}"
|
||||
@click="router.emit('navigate'); router.from($event)"
|
||||
<a class="truncate md:text-base text-lg" href="/${node.path}"
|
||||
@click="navigate = true; router.from($event)"
|
||||
>${node.name}</a>
|
||||
</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' : ''">
|
||||
<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>
|
||||
<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 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-start="max-h-0"
|
||||
x-transition:enter-end="max-h-[var(--max)]"
|
||||
@@ -242,13 +216,14 @@ export class Folder extends BaseNode {
|
||||
|
||||
return `
|
||||
<div x-data="{ active: null, navigate: false }"
|
||||
x-init="router.on('ready', ({ url }) => {
|
||||
let isFromNav = navigate;
|
||||
x-init="let react = () => {
|
||||
let path = window.location.pathname;
|
||||
|
||||
active?.setAttribute('data-active', false);
|
||||
active = $el.querySelector(\`[data-path="\${url}"]\`);
|
||||
active = $el.querySelector(\`[data-path="\${path}"]\`);
|
||||
active?.setAttribute('data-active', true);
|
||||
|
||||
if (isFromNav) return;
|
||||
if (navigate) return navigate = false;
|
||||
let node = active;
|
||||
|
||||
if (node) while ((node = node.parentElement) !== $el) {
|
||||
@@ -258,11 +233,11 @@ export class Folder extends BaseNode {
|
||||
}
|
||||
setTimeout(() => active?.scrollIntoView(
|
||||
{ behavior: 'smooth', block: 'center' }), 150);
|
||||
};
|
||||
router.on('ready', () => {
|
||||
$nextTick(() => react());
|
||||
});
|
||||
router.on('navigate', () => {
|
||||
navigate = true;
|
||||
setTimeout(() => navigate = false, 100);
|
||||
})"
|
||||
$nextTick(() => react())"
|
||||
>${html}</div>
|
||||
`;
|
||||
}
|
||||
@@ -278,29 +253,6 @@ export class Document extends BaseNode {
|
||||
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() {
|
||||
return this.name + '.md';
|
||||
}
|
||||
@@ -312,13 +264,7 @@ export class Document extends BaseNode {
|
||||
let title = this.metadata.title ?? this.name;
|
||||
document.title = `${title} - ${appName}`;
|
||||
|
||||
return `
|
||||
<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>
|
||||
let sidebar = () => `
|
||||
<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">
|
||||
<!-- 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)'))"
|
||||
x-show="headings.length > 0"
|
||||
class="mr-8"
|
||||
>
|
||||
<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">
|
||||
<template x-for="heading in headings">
|
||||
<div :data-level="heading.tagName"
|
||||
x-data="{ id: '#' + heading.innerText, scroll: () => heading.scrollIntoView() }"
|
||||
x-init="if (hash === id) scroll()"
|
||||
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]
|
||||
pl-3 border-l"
|
||||
>
|
||||
<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">
|
||||
<template x-for="heading in headings">
|
||||
<div :data-level="heading.tagName"
|
||||
x-data="{ id: '#' + heading.innerText, scroll: () => heading.scrollIntoView() }"
|
||||
x-init="if (hash === id) scroll()"
|
||||
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]
|
||||
pl-3 border-l"
|
||||
>
|
||||
<a x-text="heading.innerText"
|
||||
class="text-neutral-500 hover:text-neutral-800"
|
||||
@@ -352,6 +298,20 @@ export class Document extends BaseNode {
|
||||
</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 = {}) {
|
||||
|
||||
@@ -1,24 +1,34 @@
|
||||
export const Routes = {
|
||||
index() {},
|
||||
other() {}
|
||||
const Routes = {
|
||||
index: null,
|
||||
error: null
|
||||
}
|
||||
|
||||
const Events = [
|
||||
/* eventName: [callbacks], */
|
||||
]
|
||||
const Events = {
|
||||
entries: { /* eventName: [callbacks], */ },
|
||||
history: [ /* { eventName, params, }, */ ],
|
||||
}
|
||||
|
||||
export default {
|
||||
react() {
|
||||
let url = window.location.pathname;
|
||||
if (url === '/') {
|
||||
url = encodeURI(Routes.index());
|
||||
history.replaceState({}, '', url);
|
||||
} else {
|
||||
Routes.other(decodeURI(url));
|
||||
let path = decodeURI(window.location.pathname);
|
||||
if (path === '/') {
|
||||
if (! Routes.index) return;
|
||||
history.replaceState({}, '', Routes.index);
|
||||
return this.react();
|
||||
}
|
||||
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());
|
||||
this.react();
|
||||
},
|
||||
@@ -31,11 +41,26 @@ export default {
|
||||
history.pushState({}, '', base + url);
|
||||
this.react();
|
||||
},
|
||||
emit(event, params) {
|
||||
Events[event]?.forEach(fn => fn(params));
|
||||
emit(eventName, 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) {
|
||||
if (! Events[event]) Events[event] = [];
|
||||
Events[event]?.push(callback);
|
||||
on(eventName, callback) {
|
||||
if (! Events.entries[eventName]) Events.entries[eventName] = [];
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,19 @@ export class Engine {
|
||||
|
||||
template(namespace) {
|
||||
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">
|
||||
<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" />
|
||||
</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 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">
|
||||
@@ -51,7 +58,7 @@ export class Engine {
|
||||
generateHTML(result) {
|
||||
return `
|
||||
<button class="flex flex-col size-full gap-y-1.5 text-start rounded-md hover:bg-neutral-200 p-2"
|
||||
@click="router.goto("${result.path}"); active = false"
|
||||
@click="$nextTick(() => active = false); router.goto("${result.path}")"
|
||||
>
|
||||
<span>${result.name}</span>
|
||||
<span class="text-xs text-neutral-500"
|
||||
|
||||
@@ -2,12 +2,13 @@ import { PATH } from '../paths.js'
|
||||
import { transform } from './zettelkasten.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 Mustache from 'mustache'
|
||||
import YAML from 'yaml'
|
||||
import fs from 'node:fs'
|
||||
import fs from 'fs'
|
||||
import fm from'front-matter'
|
||||
|
||||
|
||||
@@ -20,8 +21,8 @@ function walkSourceDir([root, parent], callback) {
|
||||
let filenames = fs.readdirSync(base);
|
||||
|
||||
for (let name of filenames) {
|
||||
// omit filenames starting with '.'
|
||||
if (name.startsWith(".")) continue;
|
||||
// omit filenames starting with '.' or '_'
|
||||
if (name.startsWith(".") || name.startsWith('_')) continue;
|
||||
|
||||
let path = [base, name].join('/');
|
||||
let relpath = [parent, name].filter(Boolean).join('/');
|
||||
@@ -63,7 +64,9 @@ function bisect(filename) {
|
||||
}
|
||||
|
||||
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 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 */ {
|
||||
if (ctx.extension === 'md') {
|
||||
let content = fs.readFileSync(ctx.path, 'utf8');
|
||||
let context = { content, ...ctx };
|
||||
|
||||
return buildMdDocs(index, context);
|
||||
return buildMdDocs(index, ctx);
|
||||
}
|
||||
|
||||
let content = fs.readFileSync(ctx.path);
|
||||
@@ -140,37 +140,25 @@ function buildObject(index, ctx) /* always return falsy values */ {
|
||||
fs.writeFileSync(path, content);
|
||||
}
|
||||
|
||||
function buildIndex(config, vault) {
|
||||
let metadata = Index.Metadata(config["metadata"]);
|
||||
let view = Index.View(config["view"]);
|
||||
|
||||
let index = new Index(view, metadata, {});
|
||||
function buildIndex(metadata, vault) {
|
||||
let index = new Index(metadata, {});
|
||||
|
||||
if (! fs.existsSync(PATH.INDEX)) {
|
||||
// create index if not found
|
||||
// or reuse it
|
||||
fs.mkdirSync(PATH.OBJECT, { recursive: true });
|
||||
|
||||
walkSourceDir([config.vault.root], ctx => {
|
||||
walkSourceDir([vault], 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;
|
||||
}
|
||||
|
||||
let buffer = fs.readFileSync(PATH.INDEX);
|
||||
let legacy = decode(buffer);
|
||||
|
||||
walkSourceDir([config.vault.root], ctx => {
|
||||
walkSourceDir([vault], ctx => {
|
||||
let path = ctx.path;
|
||||
let entry = legacy.object[path];
|
||||
// if no such legacy entry found, continue
|
||||
@@ -200,6 +188,51 @@ function buildIndex(config, vault) {
|
||||
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) {
|
||||
// when running under node.js solely
|
||||
sprachbund().writeBundle();
|
||||
@@ -216,11 +249,11 @@ export default function sprachbund() {
|
||||
language: CONFIG?.language,
|
||||
});
|
||||
},
|
||||
writeBundle() {
|
||||
let index = buildIndex(CONFIG);
|
||||
let data = encode(index);
|
||||
|
||||
fs.writeFileSync(PATH.INDEX, data);
|
||||
async writeBundle() {
|
||||
let vault = await prepareVault(CONFIG.vault);
|
||||
let index = buildIndex(CONFIG.metadata, vault);
|
||||
|
||||
fs.writeFileSync(PATH.INDEX, encode(index));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ export default defineConfig({
|
||||
root: 'src',
|
||||
build: {
|
||||
outDir: '../dist',
|
||||
assetsDir: '.assets',
|
||||
assetsDir: '_assets',
|
||||
emptyOutDir: true
|
||||
},
|
||||
plugins: [
|
||||
|
||||
Reference in New Issue
Block a user