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
|
node_modules
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
|
dist-vault
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
|
|||||||
11
config.yaml
11
config.yaml
@@ -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
721
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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
2
public/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
*
|
|
||||||
!.gitignore
|
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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`;
|
||||||
|
|||||||
@@ -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());
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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 = [];
|
||||||
|
|
||||||
|
|||||||
@@ -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="\${url}"]\`);
|
active = $el.querySelector(\`[data-path="\${path}"]\`);
|
||||||
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 = {}) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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("${result.path}"); active = false"
|
@click="$nextTick(() => active = false); router.goto("${result.path}")"
|
||||||
>
|
>
|
||||||
<span>${result.name}</span>
|
<span>${result.name}</span>
|
||||||
<span class="text-xs text-neutral-500"
|
<span class="text-xs text-neutral-500"
|
||||||
|
|||||||
@@ -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));
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
Reference in New Issue
Block a user