fix: router initialization

This commit is contained in:
Break27 2024-09-20 18:13:52 +08:00
parent c7bae4a2b5
commit 24a026c58e
6 changed files with 129 additions and 142 deletions

View File

@ -4,13 +4,12 @@ language: en
vault:
root: public
cleanup: true
view:
index:
notfound:
cleanup: false
metadata:
hidden:
- example
- foo/bar.md
routes:
index:
error:

View File

@ -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'
@ -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 {
@ -80,8 +95,8 @@ 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) }"
x-init="if (graph.value?.nodeType) { ready = true; $el.appendChild(graph.value) } else ready = false;"
@click="if (expand && !ready) { destroy(); 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>
@ -140,43 +155,41 @@ export default () => ({
async init() {
this.content.transition(async () => {
this.index = await Index.fromPack();
this.router.start();
console.log(this.index);
this.content.value = ''; // clear init state
this.load.apply(this);
this.router.start(this.index.metadata.routes);
});
Routes.index = () => {
let [url, view] = this.index.createView();
this.router.route = (path) => {
let view = this.index.getObject(path);
this.graph.transition(async () => {
const { Engine } = await import('./graph');
let engine = Engine.fromIndex(this.index);
return engine.createInstance(url);
});
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() ?? '';
});
return url;
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());
},
async load() {
this.graph.transition(async () => {
const { Engine } = await import('./graph');
let engine = Engine.fromIndex(this.index);
let node = 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);
});
}
})

View File

@ -48,7 +48,7 @@ export class Engine {
return new Engine(graph);
}
createInstance(url) {
createInstance() {
let settings = { allowInvalidContainer: true };
let container = document.createElement('div');
@ -59,9 +59,8 @@ export class Engine {
this.setupInteraction();
this.setupGraphStyle();
let camera = this.instance.getCamera();
router.emit('retrieve', { path: url });
camera.animate({ ratio: 1 }, { easing: "linear", duration: 200 });
let path = router.last('retrieve')?.params?.path;
setTimeout(() => router.emit('retrieve', { path }), 500);
return container;
}
@ -201,6 +200,12 @@ export class Engine {
});
}
panover(path) {
let camera = this.instance.getCamera();
router.emit('graphview update', { path });
camera.animate({ ratio: 0.5 }, { easing: "linear", duration: 200 });
}
dispose() {
this.layout.kill();
this.instance.kill();

View File

@ -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 {
@ -242,10 +214,12 @@ export class Folder extends BaseNode {
return `
<div x-data="{ active: null, navigate: false }"
x-init="router.on('ready', ({ url }) => {
x-init="router.on('ready', () => {
let isFromNav = navigate;
let path = window.location.pathname;
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);
if (isFromNav) return;
@ -278,29 +252,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 +263,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 +274,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 +297,18 @@ export class Document extends BaseNode {
</div>
</div>
`;
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 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 = {}) {

View File

@ -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');
return this.goto(Routes.error);
}
this.emit('ready');
},
start() {
route(path) {
return true;
},
start(routes) {
Object.assign(Routes, routes);
window.addEventListener("popstate", () => this.react());
this.react();
},
@ -32,10 +42,16 @@ export default {
this.react();
},
emit(event, params) {
Events[event]?.forEach(fn => fn(params));
Events.history.unshift({ event, params });
Events.history.length = Math.min(Events.history.length, 50);
Events.entries[event]?.forEach(fn => fn(params));
},
last(event) {
return Events.history.find(e => e.event === event);
},
on(event, callback) {
if (! Events[event]) Events[event] = [];
Events[event]?.push(callback);
if (! Events.entries[event]) Events.entries[event] = [];
Events.entries[event]?.push(callback);
}
}

View File

@ -20,7 +20,7 @@ function walkSourceDir([root, parent], callback) {
let filenames = fs.readdirSync(base);
for (let name of filenames) {
// omit filenames starting with '.' / '_'
// omit filenames starting with '.' or '_'
if (name.startsWith(".") || name.startsWith('_')) continue;
let path = [base, name].join('/');
@ -63,7 +63,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 +118,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,11 +139,9 @@ function buildObject(index, ctx) /* always return falsy values */ {
fs.writeFileSync(path, content);
}
function buildIndex(config, vault) {
function buildIndex(config) {
let metadata = Index.Metadata(config["metadata"]);
let view = Index.View(config["view"]);
let index = new Index(view, metadata, {});
let index = new Index(metadata, {});
if (! fs.existsSync(PATH.INDEX)) {
// create index if not found