initial commit
This commit is contained in:
commit
d14e1a9ed1
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
16
config.yaml
Normal file
16
config.yaml
Normal file
@ -0,0 +1,16 @@
|
||||
name: Sprachbund
|
||||
description: an example web application
|
||||
language: en
|
||||
|
||||
vault:
|
||||
root: public
|
||||
cleanup: true
|
||||
|
||||
view:
|
||||
index:
|
||||
notfound:
|
||||
|
||||
metadata:
|
||||
hidden:
|
||||
- example
|
||||
- foo/bar.md
|
||||
11
netlify.toml
Normal file
11
netlify.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[build]
|
||||
publish = "dist/"
|
||||
command = "npm run build"
|
||||
|
||||
[build.processing.html]
|
||||
pretty_urls = true
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
3451
package-lock.json
generated
Normal file
3451
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "sprachbund",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"clean": "rm -rf ./dist",
|
||||
"update": "node ./src/scripts/sprachbund.js",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"ali-oss": "^6.21.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"front-matter": "^4.0.2",
|
||||
"mustache": "^4.2.0",
|
||||
"postcss": "^8.4.45",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"vite": "^5.4.1",
|
||||
"yaml": "^2.5.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"alpinejs": "^3.14.1",
|
||||
"graphology": "^0.25.4",
|
||||
"graphology-layout": "^0.6.1",
|
||||
"graphology-layout-force": "^0.2.4",
|
||||
"marked": "^14.1.1",
|
||||
"minisearch": "^7.1.0",
|
||||
"sigma": "^3.0.0-beta.29"
|
||||
}
|
||||
}
|
||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
2
public/.gitignore
vendored
Normal file
2
public/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
1
src/assets/javascript.svg
Normal file
1
src/assets/javascript.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#F7DF1E" d="M0 0h256v256H0V0Z"></path><path d="m67.312 213.932l19.59-11.856c3.78 6.701 7.218 12.371 15.465 12.371c7.905 0 12.89-3.092 12.89-15.12v-81.798h24.057v82.138c0 24.917-14.606 36.259-35.916 36.259c-19.245 0-30.416-9.967-36.087-21.996m85.07-2.576l19.588-11.341c5.157 8.421 11.859 14.607 23.715 14.607c9.969 0 16.325-4.984 16.325-11.858c0-8.248-6.53-11.17-17.528-15.98l-6.013-2.58c-17.357-7.387-28.87-16.667-28.87-36.257c0-18.044 13.747-31.792 35.228-31.792c15.294 0 26.292 5.328 34.196 19.247l-18.732 12.03c-4.125-7.389-8.591-10.31-15.465-10.31c-7.046 0-11.514 4.468-11.514 10.31c0 7.217 4.468 10.14 14.778 14.608l6.014 2.577c20.45 8.765 31.963 17.7 31.963 37.804c0 21.654-17.012 33.51-39.867 33.51c-22.339 0-36.774-10.654-43.819-24.574"></path></svg>
|
||||
|
After Width: | Height: | Size: 995 B |
3
src/index.css
Normal file
3
src/index.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
43
src/index.html
Normal file
43
src/index.html
Normal file
@ -0,0 +1,43 @@
|
||||
<!doctype html>
|
||||
<html lang="{{ language }}">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link href="/index.css" rel="stylesheet">
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/javascript.svg" />
|
||||
<meta name="application-name" content="{{ name }}" />
|
||||
<meta name="description" content="{{ description }}" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<script defer type="module" src="/index.js"></script>
|
||||
<title>{{ name }}</title>
|
||||
</head>
|
||||
<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)"
|
||||
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">
|
||||
<button class="md:hidden block" @click="open = !open">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
|
||||
<path fill-rule="evenodd" d="M2 4.75A.75.75 0 0 1 2.75 4h14.5a.75.75 0 0 1 0 1.5H2.75A.75.75 0 0 1 2 4.75ZM2 10a.75.75 0 0 1 .75-.75h14.5a.75.75 0 0 1 0 1.5H2.75A.75.75 0 0 1 2 10Zm0 5.25a.75.75 0 0 1 .75-.75h14.5a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1-.75-.75Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="md:text-xl text-lg font-semibold">{{ name }}</div>
|
||||
<div class="italic text-neutral-400 md:block hidden">{{ description }}</div>
|
||||
</div>
|
||||
<div class="bg-white pointer-events-auto -translate-x-full z-10"
|
||||
:class="open ? 'translate-x-0 transition' : 'md:translate-x-0 transition'"
|
||||
><div x-html="search"></div>
|
||||
</div>
|
||||
<div class="bg-white pointer-events-auto -translate-x-full overflow-y-auto overscroll-contain h-full"
|
||||
:class="open ? 'translate-x-0 transition' : 'md:translate-x-0 transition'"
|
||||
><div x-html="navigation"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div x-html="content"
|
||||
class="flex max-w-5xl min-h-screen size-full"
|
||||
></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
8
src/index.js
Normal file
8
src/index.js
Normal file
@ -0,0 +1,8 @@
|
||||
import app from './scripts/app'
|
||||
import Alpine from 'alpinejs'
|
||||
|
||||
|
||||
window.Alpine = Alpine;
|
||||
|
||||
Alpine.data('app', app);
|
||||
Alpine.start();
|
||||
11
src/paths.js
Normal file
11
src/paths.js
Normal file
@ -0,0 +1,11 @@
|
||||
// determine if this code is running locally
|
||||
// (under node.js)
|
||||
const NATIVE = typeof window === 'undefined'
|
||||
// paths config
|
||||
export const PATH = {};
|
||||
|
||||
PATH.CONFIG = 'config.yaml';
|
||||
PATH.OUTPUT = 'dist';
|
||||
PATH.OBJECT = `${NATIVE ? PATH.OUTPUT : ''}/.objects`;
|
||||
PATH.INDEX = `${PATH.OBJECT}/index`;
|
||||
PATH.METADATA = `${PATH.OBJECT}/metadata`;
|
||||
164
src/scripts/app.js
Normal file
164
src/scripts/app.js
Normal file
@ -0,0 +1,164 @@
|
||||
import { Engine } from './search'
|
||||
import { Index, State } from './object'
|
||||
import router, { Routes } from './router'
|
||||
import zettelkasten from './zettelkasten'
|
||||
|
||||
import { marked } from 'marked'
|
||||
|
||||
|
||||
export default () => ({
|
||||
router,
|
||||
index: null,
|
||||
|
||||
navigation: new class extends State {
|
||||
toString() {
|
||||
return `
|
||||
<div class="flex flex-col size-full px-5 pt-3 pb-8 text-neutral-600">
|
||||
${this.value}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
fail() {
|
||||
return `
|
||||
<div class="flex size-full justify-center 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="m5.965 4.904 9.131 9.131a6.5 6.5 0 0 0-9.131-9.131Zm8.07 10.192L4.904 5.965a6.5 6.5 0 0 0 9.131 9.131ZM4.343 4.343a8 8 0 1 1 11.314 11.314A8 8 0 0 1 4.343 4.343Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
|
||||
search: new class extends State {
|
||||
constructor() {
|
||||
super();
|
||||
this.engine = Engine.Default();
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.engine.template('search.engine');
|
||||
}
|
||||
},
|
||||
|
||||
content: new class extends State {
|
||||
init() {
|
||||
return `
|
||||
<div class="flex size-full justify-center self-center">
|
||||
<svg class="animate-spin size-6 text-gray-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
fail() {
|
||||
return `
|
||||
<div class="xl:text-9xl lg:text-8xl md:text-7xl text-6xl text-neutral-200 pt-4 pl-6 select-none">
|
||||
An unexpected error occurred.
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
|
||||
graph: new class extends State {
|
||||
toString() {
|
||||
return `
|
||||
<div class="uppercase text-sm font-semibold mb-3">Interactive graph</div>
|
||||
<div x-data="{ expand: false,
|
||||
anchor: $el.parentElement,
|
||||
destroy: () => $el.remove(),
|
||||
collapse: () => { $data.expand = false;
|
||||
$data.anchor.appendChild($el);
|
||||
$data.toggleGlobal(false)
|
||||
},
|
||||
toggleGlobal: (value) => router.emit('graphview toggle global', { value })
|
||||
}"
|
||||
x-effect="if (expand) document.querySelector('body').firstElementChild.appendChild($el)"
|
||||
:class="expand ? 'fixed flex size-full max-h-screen py-10 px-24 z-50' : '2xl:size-72 size-64'"
|
||||
>
|
||||
<div class="relative rounded-md border size-full p-1 bg-white"
|
||||
x-init="graph.value?.nodeType ? $el.appendChild(graph.value) : $el.innerHTML = graph.value"
|
||||
@click="if (expand && $el.children.length === 1) { destroy(); toggleGlobal(false) }"
|
||||
>
|
||||
<div class="flex items-center gap-x-1 mr-1 mt-1 absolute top-0 right-0 z-20 text-neutral-600">
|
||||
<button x-show="expand" class="hover:text-red-500" @click="collapse()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
|
||||
<path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button x-show="!expand" class="hover:text-red-500" @click="expand = true; toggleGlobal(true)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4">
|
||||
<path d="M13 4.5a2.5 2.5 0 1 1 .702 1.737L6.97 9.604a2.518 2.518 0 0 1 0 .792l6.733 3.367a2.5 2.5 0 1 1-.671 1.341l-6.733-3.367a2.5 2.5 0 1 1 0-3.475l6.733-3.366A2.52 2.52 0 0 1 13 4.5Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button x-show="!expand" class="hover:text-red-500" @click="expand = true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
|
||||
<path fill-rule="evenodd" d="M5.22 14.78a.75.75 0 0 0 1.06 0l7.22-7.22v5.69a.75.75 0 0 0 1.5 0v-7.5a.75.75 0 0 0-.75-.75h-7.5a.75.75 0 0 0 0 1.5h5.69l-7.22 7.22a.75.75 0 0 0 0 1.06Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<template x-if="expand">
|
||||
<div class="absolute inset-0 size-full bg-neutral-400 opacity-25 -z-20" @click="collapse()">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
fail() {
|
||||
return `
|
||||
<div class="flex size-full justify-center 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="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495ZM10 5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 10 5Zm0 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
|
||||
async 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.graph.transition(async () => {
|
||||
const { Engine } = await import('./graph');
|
||||
let engine = Engine.fromIndex(this.index);
|
||||
|
||||
return engine.createInstance();
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Routes.other = path => {
|
||||
this.content.transition(() => {
|
||||
let [,view] = this.index.createView(path);
|
||||
return view.render();
|
||||
});
|
||||
}
|
||||
|
||||
marked.use(zettelkasten());
|
||||
},
|
||||
})
|
||||
204
src/scripts/graph.js
Normal file
204
src/scripts/graph.js
Normal file
@ -0,0 +1,204 @@
|
||||
import router from './router'
|
||||
|
||||
import { Sigma } from 'sigma'
|
||||
import Graph from 'graphology'
|
||||
import random from 'graphology-layout/random'
|
||||
import ForceSupervisor from 'graphology-layout-force/worker'
|
||||
|
||||
|
||||
export class Engine {
|
||||
constructor(graph) {
|
||||
this.graph = graph;
|
||||
this.layout = null;
|
||||
this.instance = null;
|
||||
}
|
||||
|
||||
static fromIndex(index) {
|
||||
let graph = new Graph();
|
||||
let links = Object.entries(index.links);
|
||||
|
||||
for (let [src, targets] of links) {
|
||||
|
||||
let source = index.object[src];
|
||||
|
||||
let label = source?.name ?? src;
|
||||
let path = source?.path ?? src;
|
||||
|
||||
if (! graph.hasNode(src)) {
|
||||
graph.addNode(src, { label, path });
|
||||
}
|
||||
|
||||
for (let dest of targets) {
|
||||
let target = index.object[dest];
|
||||
|
||||
let label = target?.name ?? dest;
|
||||
let path = target?.path ?? dest;
|
||||
|
||||
if (! graph.hasNode(dest)) {
|
||||
graph.addNode(dest, { label, path });
|
||||
}
|
||||
|
||||
if (! graph.hasEdge(src, dest)) {
|
||||
graph.addEdge(src, dest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
random.assign(graph);
|
||||
return new Engine(graph);
|
||||
}
|
||||
|
||||
createInstance() {
|
||||
let settings = { allowInvalidContainer: true };
|
||||
let container = document.createElement('div');
|
||||
|
||||
container.style.width = '100%';
|
||||
container.style.height = '100%';
|
||||
|
||||
this.instance = new Sigma(this.graph, container, settings);
|
||||
this.setupInteraction();
|
||||
this.setupGraphStyle();
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
setupInteraction() {
|
||||
let params = { isNodeFixed: (_, attr) => attr.highlighted };
|
||||
this.layout = new ForceSupervisor(this.graph, params);
|
||||
this.layout.start();
|
||||
|
||||
// State for drag'n'drop
|
||||
let activeNode = null;
|
||||
let isDragging = false;
|
||||
|
||||
let preventHop = false;
|
||||
let timeout = 100;
|
||||
|
||||
// On mouse down on a node
|
||||
// - we enable the drag mode
|
||||
// - save in the dragged node in the state
|
||||
// - highlight the node
|
||||
// - disable the camera so its state is not updated
|
||||
this.instance.on("downNode", (e) => {
|
||||
isDragging = true;
|
||||
activeNode = e.node;
|
||||
this.graph.setNodeAttribute(activeNode, "highlighted", true);
|
||||
|
||||
// prevent hopping to another node
|
||||
// when timeout is reached
|
||||
preventHop = false;
|
||||
setTimeout(() => preventHop = true, timeout);
|
||||
});
|
||||
|
||||
// On mouse move, if the drag mode is enabled, we change the position of the draggedNode
|
||||
this.instance.getMouseCaptor().on("mousemovebody", (e) => {
|
||||
if (!isDragging || !activeNode) return;
|
||||
|
||||
// Get new position of node
|
||||
const pos = this.instance.viewportToGraph(e);
|
||||
|
||||
this.graph.setNodeAttribute(activeNode, "x", pos.x);
|
||||
this.graph.setNodeAttribute(activeNode, "y", pos.y);
|
||||
|
||||
// Prevent sigma to move camera:
|
||||
e.preventSigmaDefault();
|
||||
e.original.preventDefault();
|
||||
e.original.stopPropagation();
|
||||
});
|
||||
|
||||
// On mouse up, we reset the autoscale and the dragging mode
|
||||
this.instance.getMouseCaptor().on("mouseup", () => {
|
||||
if (activeNode) {
|
||||
this.graph.removeNodeAttribute(activeNode, "highlighted");
|
||||
}
|
||||
isDragging = false;
|
||||
activeNode = null;
|
||||
});
|
||||
|
||||
// Disable the autoscale at the first down interaction
|
||||
this.instance.getMouseCaptor().on("mousedown", () => {
|
||||
if (! this.instance.getCustomBBox()) {
|
||||
this.instance.setCustomBBox(this.instance.getBBox())
|
||||
}
|
||||
});
|
||||
|
||||
// hopping - act as an link when not dragged
|
||||
this.instance.on('clickNode', e => {
|
||||
if (! preventHop) {
|
||||
let path = this.graph.getNodeAttribute(e.node, 'path');
|
||||
router.goto(path);
|
||||
}
|
||||
preventHop = false;
|
||||
});
|
||||
}
|
||||
|
||||
setupGraphStyle() {
|
||||
let localNodes = [];
|
||||
let localEdges = [];
|
||||
let showGlobal = false;
|
||||
|
||||
router.on('graphview toggle global', ({ value }) => {
|
||||
showGlobal = value;
|
||||
if (value) localEdges = this.graph.edges();
|
||||
});
|
||||
|
||||
router.on('retrieve', ({ path }) => {
|
||||
let cache = this.instance.nodeDataCache;
|
||||
|
||||
if (!(path in cache)) {
|
||||
localNodes = [];
|
||||
localEdges = [];
|
||||
return;
|
||||
}
|
||||
|
||||
let camera = this.instance.getCamera();
|
||||
let { x, y } = cache[path];
|
||||
|
||||
// panover currently visited node
|
||||
camera.animate({ x, y, ratio: 0.075 }, { easing: "linear", duration: 500 });
|
||||
|
||||
localNodes = this.graph.neighbors(path).concat(path);
|
||||
localEdges = localNodes.reduce((r, n) => r.concat(this.graph.edges(n)), []);
|
||||
});
|
||||
|
||||
let activeNodes = [];
|
||||
let activeEdges = [];
|
||||
|
||||
this.instance.on('enterNode', e => {
|
||||
activeNodes = this.graph.neighbors(e.node) + e.node;
|
||||
activeEdges = this.graph.edges(e.node);
|
||||
|
||||
this.instance.refresh({
|
||||
partialGraph: { edges: activeEdges },
|
||||
skipIndexation: true
|
||||
});
|
||||
});
|
||||
|
||||
this.instance.on('leaveNode', e => {
|
||||
activeNodes = [];
|
||||
activeEdges = [];
|
||||
|
||||
this.instance.refresh({
|
||||
partialGraph: { edges: localEdges },
|
||||
skipIndexation: true
|
||||
});
|
||||
});
|
||||
|
||||
this.instance.setSetting('nodeReducer', (node, data) => {
|
||||
data.hidden = !showGlobal && !localNodes.includes(node);
|
||||
data.color = activeNodes.includes(node) ? '#e9757c' : '#999';
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
this.instance.setSetting('edgeReducer', (edge, data) => {
|
||||
data.color = activeEdges.includes(edge) ? '#e9757c' : '#999';
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.layout.kill();
|
||||
this.instance.kill();
|
||||
}
|
||||
}
|
||||
473
src/scripts/object.js
Normal file
473
src/scripts/object.js
Normal file
@ -0,0 +1,473 @@
|
||||
import { PATH } from '../paths.js';
|
||||
import router from './router.js';
|
||||
|
||||
import { decode } from '@msgpack/msgpack'
|
||||
import { marked } from 'marked'
|
||||
|
||||
|
||||
export class State {
|
||||
constructor() {
|
||||
this.value = this.init();
|
||||
}
|
||||
|
||||
toString() { return this.value }
|
||||
|
||||
init() { return '' }
|
||||
fail() { return '' }
|
||||
|
||||
async transition(val) {
|
||||
try {
|
||||
this.value = this.init();
|
||||
this.value = await val() ?? this.value;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.value = this.fail();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BaseObject {
|
||||
static fromObject(o) {
|
||||
return Object.assign(new this(), o);
|
||||
}
|
||||
}
|
||||
|
||||
class BaseNode extends BaseObject {
|
||||
constructor(type) {
|
||||
super();
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
fullname() { return this.name }
|
||||
|
||||
static fromObject(o) {
|
||||
switch (o?.type) {
|
||||
case 'Document':
|
||||
return Object.assign(new Document(), o);
|
||||
case 'Attachment':
|
||||
return Object.assign(new Attachment(), o);
|
||||
default:
|
||||
throw new Error('invalid type');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Index extends BaseObject {
|
||||
constructor(view, metadata, object, links = {}) {
|
||||
super();
|
||||
this.view = view;
|
||||
this.metadata = metadata;
|
||||
this.object = object;
|
||||
this.links = links;
|
||||
}
|
||||
|
||||
static Metadata(x) {
|
||||
return {
|
||||
hidden: x?.hidden,
|
||||
}
|
||||
}
|
||||
|
||||
static View(x) {
|
||||
return {
|
||||
index: x?.index,
|
||||
notfound: x?.notfound
|
||||
}
|
||||
}
|
||||
|
||||
static async fromPack() {
|
||||
let binary = await fetch(PATH.INDEX);
|
||||
let buffer = await binary.arrayBuffer();
|
||||
let data = decode(buffer);
|
||||
|
||||
return Index.fromObject(data);
|
||||
}
|
||||
|
||||
unflatten() {
|
||||
let result = [], level = { result };
|
||||
|
||||
let searchable = [];
|
||||
let id = 0;
|
||||
|
||||
let buildWith = (entry) => (r, name, i, a) => {
|
||||
if (! r[name]) {
|
||||
r[name] = { result: [] };
|
||||
|
||||
if (i !== a.length - 1) {
|
||||
// if this node is a Folder
|
||||
let children = r[name].result;
|
||||
let node = new Folder(name, children);
|
||||
|
||||
r.result.push(node);
|
||||
} else {
|
||||
let node = BaseNode.fromObject(entry);
|
||||
r.result.push(node);
|
||||
|
||||
if (node.type === 'Document') {
|
||||
node['id'] = id++;
|
||||
searchable.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
return r[name];
|
||||
}
|
||||
|
||||
for (let [path, entry] of Object.entries(this.object)) {
|
||||
path.split('/').reduce(buildWith(entry), level);
|
||||
}
|
||||
|
||||
result.sort((a, b) => {
|
||||
if (a.type === 'Folder' && b.type !== 'Folder') return -1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
return [result, searchable];
|
||||
}
|
||||
|
||||
linkWith(src, target) {
|
||||
if (! this.links[target]) this.links[target] = [];
|
||||
this.links[target].push(src);
|
||||
}
|
||||
|
||||
getObject(path) {
|
||||
if (path.startsWith('/')) path = path.slice(1);
|
||||
if (!(path in this.object)) path = path + '.md';
|
||||
|
||||
try { var o = BaseNode.fromObject(this.object[path]) }
|
||||
catch (e) { return null }
|
||||
|
||||
router.emit('retrieve', { path });
|
||||
return o;
|
||||
}
|
||||
|
||||
createTree() {
|
||||
let [tree, searchable] = this.unflatten();
|
||||
let root = new Folder('root', tree);
|
||||
let metadata = this.metadata;
|
||||
|
||||
for (let path of metadata.hidden) {
|
||||
// find and remove hidden paths
|
||||
root.find(path, (parent, i) => parent.splice(i, 1));
|
||||
}
|
||||
|
||||
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 {
|
||||
constructor(name, children) {
|
||||
super('Folder');
|
||||
this.name = name;
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
find(path, callback) {
|
||||
let name = path.split('/', 1)[0];
|
||||
let next = path.slice(name.length + 1);
|
||||
let index = 0;
|
||||
|
||||
for (let child of this.children) {
|
||||
if (child.fullname() === name) {
|
||||
return child.type === 'Folder' && next
|
||||
? child.find(next, callback)
|
||||
: callback(this.children, index);
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
render(wrap = true) {
|
||||
let html = '';
|
||||
|
||||
for (let node of this.children) {
|
||||
if (node.type === 'Folder') {
|
||||
html += node.render();
|
||||
continue;
|
||||
}
|
||||
|
||||
html += `
|
||||
<div data-path="/${node.path}" data-active="false"
|
||||
class="flex md:ml-3 ml-0 pl-3 *:w-full select-none border-l
|
||||
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)"
|
||||
>${node.name}</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
let base = 24;
|
||||
let maxHeight = base * this.children.length;
|
||||
|
||||
if (wrap) return `
|
||||
<div x-data="{ open: false }" @notify="open = true">
|
||||
<div class="flex items-center gap-1 hover:text-neutral-400 md:justify-normal justify-between md:pr-0 pr-4 select-none cursor-pointer" @click="open = !open">
|
||||
<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>
|
||||
</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]"
|
||||
x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="max-h-0"
|
||||
x-transition:enter-end="max-h-[var(--max)]"
|
||||
x-transition:leave="ease-in duration-100"
|
||||
x-transition:leave-start="max-h-[var(--max)]"
|
||||
x-transition:leave-end="max-h-0"
|
||||
>${html}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return `
|
||||
<div x-data="{ active: null, navigate: false }"
|
||||
x-init="router.on('ready', ({ url }) => {
|
||||
let isFromNav = navigate;
|
||||
active?.setAttribute('data-active', false);
|
||||
active = $el.querySelector(\`[data-path="\${url}"]\`);
|
||||
active?.setAttribute('data-active', true);
|
||||
|
||||
if (isFromNav) return;
|
||||
let node = active;
|
||||
|
||||
if (node) while ((node = node.parentElement) !== $el) {
|
||||
// notify all parent elements on the tree
|
||||
// and cause them to open
|
||||
node.dispatchEvent(new CustomEvent('notify'));
|
||||
}
|
||||
setTimeout(() => active?.scrollIntoView(
|
||||
{ behavior: 'smooth', block: 'center' }), 150);
|
||||
});
|
||||
router.on('navigate', () => {
|
||||
navigate = true;
|
||||
setTimeout(() => navigate = false, 100);
|
||||
})"
|
||||
>${html}</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export class Document extends BaseNode {
|
||||
constructor(name, path, metadata, content) {
|
||||
super('Document');
|
||||
this.name = name;
|
||||
this.path = encodeURI(path);
|
||||
this.metadata = metadata;
|
||||
this.content = content;
|
||||
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';
|
||||
}
|
||||
|
||||
render() {
|
||||
let node = document.querySelector('meta[name="application-name"]');
|
||||
let appName = node.content;
|
||||
|
||||
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>
|
||||
<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 -->
|
||||
<div x-html="graph"></div>
|
||||
<!-- Headings list -->
|
||||
<div x-data="{ headings: [], hash: decodeURI(window.location.hash) }"
|
||||
x-init="if (! hash) window.scroll(0, 0);
|
||||
$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"
|
||||
>
|
||||
<a x-text="heading.innerText"
|
||||
class="text-neutral-500 hover:text-neutral-800"
|
||||
:href="id"
|
||||
@click="scroll()"
|
||||
></a>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
inline(attributes = {}) {
|
||||
let html = marked(this.content);
|
||||
let path = this.path;
|
||||
let hash;
|
||||
|
||||
if (attributes.hash) {
|
||||
path = path + attributes.hash;
|
||||
hash = attributes.hash.slice(1);
|
||||
html = `
|
||||
<div x-init="
|
||||
$nextTick(() => {
|
||||
let outer = $el.parentElement;
|
||||
let block = $el.querySelector("${attributes.hash}");
|
||||
if (block) return outer.replaceWith(block);
|
||||
|
||||
let headings = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'];
|
||||
let list = Array.from($el.children);
|
||||
let head = list.findIndex(e => headings.includes(e.nodeName) && e.innerText === "${hash}");
|
||||
let next = list.slice(head + 1).findIndex(e => headings.includes(e.nodeName));
|
||||
|
||||
if (head === -1) return;
|
||||
let nodes = list.slice(head, next === -1 ? undefined : head + next + 1);
|
||||
|
||||
if (nodes.length !== 0) return outer.replaceWith(...nodes);
|
||||
outer.replaceChildren($el.children);
|
||||
})"
|
||||
>${html}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="relative border-l-4 border-red-400 my-6 pl-4 pb-1 embed" data-type="embed">
|
||||
<div class="absolute top-0 right-0">
|
||||
<button @click="router.goto("${path}")" class="rounded-md p-1 hover:bg-neutral-200">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4">
|
||||
<path d="M12.232 4.232a2.5 2.5 0 0 1 3.536 3.536l-1.225 1.224a.75.75 0 0 0 1.061 1.06l1.224-1.224a4 4 0 0 0-5.656-5.656l-3 3a4 4 0 0 0 .225 5.865.75.75 0 0 0 .977-1.138 2.5 2.5 0 0 1-.142-3.667l3-3Z" />
|
||||
<path d="M11.603 7.963a.75.75 0 0 0-.977 1.138 2.5 2.5 0 0 1 .142 3.667l-3 3a2.5 2.5 0 0 1-3.536-3.536l1.225-1.224a.75.75 0 0 0-1.061-1.06l-1.224 1.224a4 4 0 1 0 5.656 5.656l3-3a4 4 0 0 0-.225-5.865Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div data-type="embed">
|
||||
<div class="font-semibold">
|
||||
${this.name}
|
||||
</div>
|
||||
${html}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export class Attachment extends BaseNode {
|
||||
static IMAGE = ['png', 'svg', 'jpg', 'jpeg', 'jfif', 'pjpeg', 'pjp', 'ico', 'cur'];
|
||||
static AUDIO = ['mp3', 'aac', 'ogg'];
|
||||
static VIDEO = ['mp4', 'webm'];
|
||||
|
||||
constructor(name, hash, path, extension) {
|
||||
super('Attachment');
|
||||
this.name = name;
|
||||
this.hash = hash;
|
||||
this.path = encodeURI(path);
|
||||
this.extension = extension;
|
||||
}
|
||||
|
||||
fullname() {
|
||||
return [this.name, this.extension].join('.');
|
||||
}
|
||||
|
||||
inline(attributes = {}) {
|
||||
let source = `${PATH.OBJECT}/${this.hash}`;
|
||||
let type = this.extension, html = '';
|
||||
|
||||
for (let [key, value] of Object.entries(attributes)) {
|
||||
html += `${key}="${value}" `;
|
||||
}
|
||||
|
||||
if (Attachment.IMAGE.includes(type)) {
|
||||
return `
|
||||
<img src="${source}" ${html}/>
|
||||
`;
|
||||
}
|
||||
|
||||
if (Attachment.AUDIO.includes(type)) {
|
||||
return `
|
||||
<audio src="${source}" type="audio/${type}" ${html}controls></audio>
|
||||
`;
|
||||
}
|
||||
|
||||
if (Attachment.VIDEO.includes(type)) {
|
||||
return `
|
||||
<video src="${source}" type="video/${type}" ${html}controls></video>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let html = this.inline();
|
||||
|
||||
let node = document.querySelector('meta[name="application-name"]');
|
||||
let appName = node.content;
|
||||
|
||||
let title = this.name;
|
||||
document.title = `${title} - ${appName}`;
|
||||
|
||||
if (html) return `
|
||||
<div class="flex h-screen w-full justify-center items-center">
|
||||
<div class="flex w-10/12 justify-center">${html}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return `
|
||||
<div class="xl:text-9xl lg:text-8xl md:text-7xl text-6xl text-neutral-200 pt-4 pl-6 select-none">
|
||||
Unsupported file format.
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
41
src/scripts/router.js
Normal file
41
src/scripts/router.js
Normal file
@ -0,0 +1,41 @@
|
||||
export const Routes = {
|
||||
index() {},
|
||||
other() {}
|
||||
}
|
||||
|
||||
const Events = [
|
||||
/* eventName: [callbacks], */
|
||||
]
|
||||
|
||||
export default {
|
||||
react() {
|
||||
let url = window.location.pathname;
|
||||
if (url === '/') {
|
||||
url = encodeURI(Routes.index());
|
||||
history.replaceState({}, '', url);
|
||||
} else {
|
||||
Routes.other(decodeURI(url));
|
||||
}
|
||||
this.emit('ready', { url });
|
||||
},
|
||||
start() {
|
||||
window.addEventListener("popstate", () => this.react());
|
||||
this.react();
|
||||
},
|
||||
from(event) {
|
||||
event.preventDefault();
|
||||
this.goto(event.target.href, '');
|
||||
},
|
||||
goto(url, base = '/') {
|
||||
if (! url) throw new Error('invalid path: ', url);
|
||||
history.pushState({}, '', base + url);
|
||||
this.react();
|
||||
},
|
||||
emit(event, params) {
|
||||
Events[event]?.forEach(fn => fn(params));
|
||||
},
|
||||
on(event, callback) {
|
||||
if (! Events[event]) Events[event] = [];
|
||||
Events[event]?.push(callback);
|
||||
}
|
||||
}
|
||||
71
src/scripts/search.js
Normal file
71
src/scripts/search.js
Normal file
@ -0,0 +1,71 @@
|
||||
import MiniSearch from 'minisearch'
|
||||
|
||||
|
||||
const Singleton = {};
|
||||
|
||||
export class Engine {
|
||||
constructor(options, queryOptions) {
|
||||
Singleton.instance = new MiniSearch(options);
|
||||
this.queryOptions = queryOptions;
|
||||
}
|
||||
|
||||
static Default() {
|
||||
let options = {
|
||||
fields: ['name', 'content'],
|
||||
storeFields: ['name', 'path']
|
||||
};
|
||||
|
||||
let queryOptions = {
|
||||
prefix: true,
|
||||
fuzzy: 0.2
|
||||
};
|
||||
|
||||
return new Engine(options, queryOptions);
|
||||
}
|
||||
|
||||
template(namespace) {
|
||||
return `
|
||||
<div x-data="{ value: '', active: false }" class="relative mx-5 mt-6" @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...">
|
||||
</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">
|
||||
<template x-for="result in results">
|
||||
<div x-html="${namespace}.generateHTML(result)" class="flex size-full"></div>
|
||||
</template>
|
||||
<template x-if="results.length === 0">
|
||||
<div class="text-center text-sm text-neutral-600 py-2">
|
||||
No results found.
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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"
|
||||
>
|
||||
<span>${result.name}</span>
|
||||
<span class="text-xs text-neutral-500"
|
||||
x-show="${result.path.includes('/')}"
|
||||
>${decodeURI(result.path.split('/').slice(0, -1).join('/'))}</span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
getInstance() {
|
||||
return Singleton.instance;
|
||||
}
|
||||
|
||||
search(text) {
|
||||
return Singleton.instance.search(text, this.queryOptions);
|
||||
}
|
||||
}
|
||||
227
src/scripts/sprachbund.js
Normal file
227
src/scripts/sprachbund.js
Normal file
@ -0,0 +1,227 @@
|
||||
import { PATH } from '../paths.js'
|
||||
import { transform } from './zettelkasten.js'
|
||||
import { Index, Attachment, Document } from './object.js'
|
||||
|
||||
import { createHash } from 'node:crypto'
|
||||
import { encode, decode } from '@msgpack/msgpack'
|
||||
|
||||
import Mustache from 'mustache'
|
||||
import OSS from 'ali-oss'
|
||||
import YAML from 'yaml'
|
||||
import fs from 'node:fs'
|
||||
import fm from'front-matter'
|
||||
|
||||
|
||||
const CONFIG = YAML.parse(
|
||||
fs.readFileSync(PATH.CONFIG, 'utf8'));
|
||||
|
||||
|
||||
function walkSourceDir([root, parent], callback) {
|
||||
let base = [root, parent].filter(Boolean).join('/');
|
||||
let filenames = fs.readdirSync(base);
|
||||
|
||||
for (let name of filenames) {
|
||||
// omit filenames starting with '.'
|
||||
if (name.startsWith(".")) continue;
|
||||
|
||||
let path = [base, name].join('/');
|
||||
let relpath = [parent, name].filter(Boolean).join('/');
|
||||
|
||||
let stat = fs.statSync(path);
|
||||
let timestamp = stat.mtimeMs;
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
walkSourceDir([root, relpath], callback);
|
||||
continue;
|
||||
}
|
||||
|
||||
let [extension, short] = bisect(name);
|
||||
let context = {
|
||||
root,
|
||||
name,
|
||||
path,
|
||||
parent,
|
||||
relpath,
|
||||
extension,
|
||||
short,
|
||||
timestamp
|
||||
};
|
||||
|
||||
if (callback && callback(context)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function bisect(filename) {
|
||||
let parts = filename.split('.');
|
||||
let extension = parts.length > 1
|
||||
? parts.pop().toLowerCase()
|
||||
: '';
|
||||
let short = parts.join('.') || filename;
|
||||
|
||||
return [extension, short];
|
||||
}
|
||||
|
||||
function buildMdDocs(index, ctx) /* always return falsy values */ {
|
||||
let { attributes, body } = fm(ctx.content);
|
||||
let pathname = [ctx.parent, ctx.short].filter(Boolean).join('/');
|
||||
|
||||
let text = transform.wikilink(body, href => {
|
||||
let [path, hash] = href.split('#', 2);
|
||||
let [hasExt,] = bisect(path);
|
||||
let needle = hasExt ? path : path + '.md';
|
||||
|
||||
// if link is pointing to itself
|
||||
// skip linking
|
||||
if (!path || needle === ctx.name || needle === ctx.relpath) {
|
||||
let path = [ctx.parent, ctx.short].filter(Boolean).join('/');
|
||||
let result = [path, hash].filter(Boolean).join('#');
|
||||
|
||||
return [result, ctx.short];
|
||||
}
|
||||
|
||||
let displayName, root;
|
||||
|
||||
// if link is 'shortest'
|
||||
if (! needle.includes('/')) {
|
||||
walkSourceDir([ctx.root], findPath);
|
||||
}
|
||||
|
||||
function findPath({ name, short, parent, relpath }) {
|
||||
// find the path to this (shortest) link
|
||||
// returning true breaks the loop
|
||||
if (needle === name) {
|
||||
path = relpath;
|
||||
root = parent;
|
||||
displayName = short;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needle.endsWith('.md')) {
|
||||
// create link only when
|
||||
// it is pointing to a markdown document
|
||||
index.linkWith(ctx.relpath, path);
|
||||
// truncate link when its referee is found
|
||||
if (root) path = [root, displayName].filter(Boolean).join('/');
|
||||
}
|
||||
|
||||
let result = [path, hash].filter(Boolean).join('#');
|
||||
return [result, displayName];
|
||||
});
|
||||
|
||||
index.object[ctx.relpath] =
|
||||
new Document(ctx.short, pathname, attributes, text);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
let content = fs.readFileSync(ctx.path);
|
||||
let shasum = createHash('sha1');
|
||||
|
||||
shasum.update(content);
|
||||
let hash = shasum.digest('hex');
|
||||
|
||||
index.object[ctx.relpath] =
|
||||
new Attachment(ctx.short, hash, ctx.relpath, ctx.extension);
|
||||
|
||||
// skip writing to file when hash already exists
|
||||
// this procedure prevents duplicate files being indexed
|
||||
// for multiple times
|
||||
let path = `${PATH.OBJECT}/${hash}`;
|
||||
if (fs.existsSync(path)) return;
|
||||
|
||||
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, {});
|
||||
|
||||
if (! fs.existsSync(PATH.INDEX)) {
|
||||
// create index if not found
|
||||
// or reuse it
|
||||
fs.mkdirSync(PATH.OBJECT, { recursive: true });
|
||||
|
||||
walkSourceDir([config.vault.root], 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 => {
|
||||
let path = ctx.path;
|
||||
let entry = legacy.object[path];
|
||||
// if no such legacy entry found, continue
|
||||
// indexing and writing to object file
|
||||
if (! entry) return buildObject(index, ctx);
|
||||
|
||||
// if found, remove the legacy entry
|
||||
// the remaining entries are unnecessary and safe for cleanup
|
||||
delete legacy.object[path];
|
||||
|
||||
// compare the timestamps to detect modification
|
||||
// insert the entry to index if no changes detected
|
||||
if (entry["timestamp"] === ctx.timestamp) {
|
||||
index.object[path] = entry;
|
||||
index.links[path] = legacy.links[path];
|
||||
}
|
||||
});
|
||||
|
||||
for (let [path, entry] of Object.entries(legacy.object)) {
|
||||
if (entry.type === 'Attachment') {
|
||||
let hash = legacy.object[path]["hash"];
|
||||
// cleanup legacy entries
|
||||
fs.rmSync(`${PATH.OBJECT}/${hash}`, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
if (process.argv[1] === import.meta.filename) {
|
||||
// when running under node.js solely
|
||||
sprachbund().writeBundle();
|
||||
}
|
||||
|
||||
export default function sprachbund() {
|
||||
return {
|
||||
name: 'sprachbund',
|
||||
|
||||
transformIndexHtml(html) {
|
||||
return Mustache.render(html, {
|
||||
name: CONFIG?.name,
|
||||
description: CONFIG?.description,
|
||||
language: CONFIG?.language,
|
||||
});
|
||||
},
|
||||
writeBundle() {
|
||||
let index = buildIndex(CONFIG);
|
||||
let data = encode(index);
|
||||
|
||||
fs.writeFileSync(PATH.INDEX, data);
|
||||
},
|
||||
}
|
||||
}
|
||||
341
src/scripts/zettelkasten.js
Normal file
341
src/scripts/zettelkasten.js
Normal file
@ -0,0 +1,341 @@
|
||||
export default () => {
|
||||
return {
|
||||
renderer,
|
||||
extensions: [comment(), block()]
|
||||
}
|
||||
}
|
||||
|
||||
export const renderer = {
|
||||
link({ href, text }) {
|
||||
try { var external = new URL(href).href }
|
||||
catch (e) {}
|
||||
|
||||
if (external) {
|
||||
return `
|
||||
<span class="inline-flex gap-0.5">
|
||||
<a href="${external}" target="_blank" rel="noopener noreferrer">${text}</a>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-3.5 text-neutral-400 mt-2.5">
|
||||
<path fill-rule="evenodd" d="M4.25 5.5a.75.75 0 0 0-.75.75v8.5c0 .414.336.75.75.75h8.5a.75.75 0 0 0 .75-.75v-4a.75.75 0 0 1 1.5 0v4A2.25 2.25 0 0 1 12.75 17h-8.5A2.25 2.25 0 0 1 2 14.75v-8.5A2.25 2.25 0 0 1 4.25 4h5a.75.75 0 0 1 0 1.5h-5Z" clip-rule="evenodd" />
|
||||
<path fill-rule="evenodd" d="M6.194 12.753a.75.75 0 0 0 1.06.053L16.5 4.44v2.81a.75.75 0 0 0 1.5 0v-4.5a.75.75 0 0 0-.75-.75h-4.5a.75.75 0 0 0 0 1.5h2.553l-9.056 8.194a.75.75 0 0 0-.053 1.06Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<a href="${href}" @click="router.from($event)">${text}</a>
|
||||
`;
|
||||
},
|
||||
image({ href, text }) {
|
||||
try { var external = new URL(href).href }
|
||||
catch (e) {}
|
||||
|
||||
let attrs = {};
|
||||
let [width, height] = text.split('x', 2);
|
||||
|
||||
if (! height) height = width;
|
||||
if (! isNaN(parseInt(width))) attrs = { width, height };
|
||||
|
||||
if (external) {
|
||||
return `
|
||||
<img src="${href}"
|
||||
${attrs.width ? 'width="' + width + '"' : ''}
|
||||
${attrs.height ? 'height="' + height + '"' : ''}
|
||||
/>
|
||||
`;
|
||||
}
|
||||
|
||||
let url = new URL(href, window.location.origin);
|
||||
if (url.hash) attrs.hash = url.hash;
|
||||
|
||||
return `
|
||||
<div x-data='{ target: null, attrs: ${JSON.stringify(attrs)} }'
|
||||
x-init="target = index.getObject("${decodeURI(url.pathname)}")"
|
||||
x-html="target?.inline(attrs) ?? $el.innerHTML"
|
||||
>
|
||||
<a href="${url.href}" @click="router.from($event)">${text}</a>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
blockquote({ raw, text, tokens }) {
|
||||
// try matching callouts
|
||||
let rule = /^ {0,3}> ?\[!(.+)\](-?)([^\n]*)/;
|
||||
let match = rule.exec(raw);
|
||||
|
||||
if (! match) {
|
||||
let body = this.parser.parse(tokens);
|
||||
return `<blockquote>\n${body}</blockquote>\n`;
|
||||
}
|
||||
|
||||
let type = match[1].trim().toLowerCase();
|
||||
let foldable = Boolean(match[2]);
|
||||
let title = match[3].trim();
|
||||
let style = getCalloutStyle(type);
|
||||
|
||||
if (tokens.length > 1) {
|
||||
tokens.shift();
|
||||
} else {
|
||||
let trim = text.slice(text.indexOf('\n'));
|
||||
tokens[0].tokens[0].text = trim;
|
||||
}
|
||||
|
||||
if (tokens[0].tokens?.length > 1) {
|
||||
tokens[0].tokens.shift();
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="rounded px-4 py-2 my-6 ${style.background}"
|
||||
x-data="{ open: ${!foldable} }"
|
||||
data-callout="${type}"
|
||||
>
|
||||
<div class="inline-flex items-center w-full gap-x-1.5 px-2
|
||||
${style.text} ${foldable ? 'cursor-pointer select-none' : ''}"
|
||||
@click="if (${foldable}) open = !open"
|
||||
>
|
||||
<span>${style.icon}</span>
|
||||
<span class="font-semibold">${title}</span>
|
||||
<span class="${foldable ? 'transition' : 'hidden'}" :class="open ? 'rotate-90' : ''">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
|
||||
<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>
|
||||
</div>
|
||||
<div x-show="open" class="overflow-hidden transition-[max-height]"
|
||||
x-init="$el.style.setProperty('--max', $el.offsetHeight + 'px')"
|
||||
x-transition:enter="ease-out duration-200"
|
||||
x-transition:enter-start="max-h-0"
|
||||
x-transition:enter-end="max-h-[var(--max)]"
|
||||
x-transition:leave="ease-in duration-200"
|
||||
x-transition:leave-start="max-h-[var(--max)]"
|
||||
x-transition:leave-end="max-h-0"
|
||||
>
|
||||
${this.parser.parse(tokens)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export const transform = {
|
||||
wikilink(src, callback) {
|
||||
let rule = /(!)?\[\[([^\]]+)\]\]/;
|
||||
|
||||
let markdown = '';
|
||||
let match = [];
|
||||
let start = 0;
|
||||
|
||||
while (match = rule.exec(src.slice(start))) {
|
||||
let raw = match[0];
|
||||
let isEmbed = match[1] ?? '';
|
||||
let [href, text] = match[2].trim().split('|', 2);
|
||||
let [path, displayName] = callback(href);
|
||||
|
||||
let bound = start + match.index;
|
||||
let display = text ?? displayName ?? path;
|
||||
|
||||
let head = src.slice(start, bound);
|
||||
let tail = isEmbed + `[${display}](/${encodeURI(path)})`;
|
||||
|
||||
markdown += head + tail;
|
||||
start = bound + raw.length;
|
||||
}
|
||||
|
||||
return markdown + src.slice(start);
|
||||
},
|
||||
}
|
||||
|
||||
function comment() {
|
||||
return {
|
||||
name: 'comment',
|
||||
level: 'inline',
|
||||
|
||||
start(src) {
|
||||
return src.match(/%%/)?.index;
|
||||
},
|
||||
tokenizer(src, tokens) {
|
||||
let rule = /^%%([\s\S]+?)%%/;
|
||||
let match = rule.exec(src);
|
||||
|
||||
if (match) return {
|
||||
type: 'comment',
|
||||
raw: match[0],
|
||||
text: match[1]
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function block() {
|
||||
return {
|
||||
name: 'block',
|
||||
level: 'block',
|
||||
|
||||
start(src) {
|
||||
return src.match(/\^/)?.index;
|
||||
},
|
||||
tokenizer(src, tokens) {
|
||||
let rule = /^\^(\S+)/;
|
||||
let match = rule.exec(src);
|
||||
|
||||
if (match) return {
|
||||
type: 'block',
|
||||
raw: match[0],
|
||||
id: match[1],
|
||||
tokens: [tokens.pop()]
|
||||
}
|
||||
},
|
||||
renderer({ id, tokens }) {
|
||||
let body = this.parser.parse(tokens);
|
||||
return `<div id="${id}">${body}</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getCalloutStyle(type) {
|
||||
let icon = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" />
|
||||
</svg>
|
||||
`;
|
||||
let background = 'bg-sky-100';
|
||||
let text = 'text-blue-500';
|
||||
|
||||
if (type === 'note') {
|
||||
return { icon, background, text };
|
||||
}
|
||||
|
||||
if (type === 'info') {
|
||||
let icon = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
|
||||
</svg>
|
||||
`;
|
||||
return { icon, background, text };
|
||||
}
|
||||
|
||||
if (type === 'todo') {
|
||||
let icon = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
`;
|
||||
return { icon, background, text };
|
||||
}
|
||||
|
||||
|
||||
if (type === 'bug') {
|
||||
let icon = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 12.75c1.148 0 2.278.08 3.383.237 1.037.146 1.866.966 1.866 2.013 0 3.728-2.35 6.75-5.25 6.75S6.75 18.728 6.75 15c0-1.046.83-1.867 1.866-2.013A24.204 24.204 0 0 1 12 12.75Zm0 0c2.883 0 5.647.508 8.207 1.44a23.91 23.91 0 0 1-1.152 6.06M12 12.75c-2.883 0-5.647.508-8.208 1.44.125 2.104.52 4.136 1.153 6.06M12 12.75a2.25 2.25 0 0 0 2.248-2.354M12 12.75a2.25 2.25 0 0 1-2.248-2.354M12 8.25c.995 0 1.971-.08 2.922-.236.403-.066.74-.358.795-.762a3.778 3.778 0 0 0-.399-2.25M12 8.25c-.995 0-1.97-.08-2.922-.236-.402-.066-.74-.358-.795-.762a3.734 3.734 0 0 1 .4-2.253M12 8.25a2.25 2.25 0 0 0-2.248 2.146M12 8.25a2.25 2.25 0 0 1 2.248 2.146M8.683 5a6.032 6.032 0 0 1-1.155-1.002c.07-.63.27-1.222.574-1.747m.581 2.749A3.75 3.75 0 0 1 15.318 5m0 0c.427-.283.815-.62 1.155-.999a4.471 4.471 0 0 0-.575-1.752M4.921 6a24.048 24.048 0 0 0-.392 3.314c1.668.546 3.416.914 5.223 1.082M19.08 6c.205 1.08.337 2.187.392 3.314a23.882 23.882 0 0 1-5.223 1.082" />
|
||||
</svg>
|
||||
`;
|
||||
let background = 'bg-rose-100';
|
||||
let text = 'text-red-500';
|
||||
return { icon, background, text };
|
||||
}
|
||||
|
||||
if (type === 'example') {
|
||||
let icon = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0ZM3.75 12h.007v.008H3.75V12Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm-.375 5.25h.007v.008H3.75v-.008Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
|
||||
</svg>
|
||||
`;
|
||||
let background = 'bg-violet-100';
|
||||
let text = 'text-violet-500';
|
||||
return { icon, background, text };
|
||||
}
|
||||
|
||||
if (['abstract', 'summary', 'tldr'].includes(type)) {
|
||||
let icon = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25ZM6.75 12h.008v.008H6.75V12Zm0 3h.008v.008H6.75V15Zm0 3h.008v.008H6.75V18Z" />
|
||||
</svg>
|
||||
`;
|
||||
let background = 'bg-teal-100';
|
||||
let text = 'text-teal-500';
|
||||
return { icon, background, text };
|
||||
}
|
||||
|
||||
if (['tip', 'hint', 'important'].includes(type)) {
|
||||
let icon = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.362 5.214A8.252 8.252 0 0 1 12 21 8.25 8.25 0 0 1 6.038 7.047 8.287 8.287 0 0 0 9 9.601a8.983 8.983 0 0 1 3.361-6.867 8.21 8.21 0 0 0 3 2.48Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 18a3.75 3.75 0 0 0 .495-7.468 5.99 5.99 0 0 0-1.925 3.547 5.975 5.975 0 0 1-2.133-1.001A3.75 3.75 0 0 0 12 18Z" />
|
||||
</svg>
|
||||
`;
|
||||
let background = 'bg-teal-100';
|
||||
let text = 'text-teal-500';
|
||||
return { icon, background, text };
|
||||
}
|
||||
|
||||
if (['success', 'check', 'done'].includes(type)) {
|
||||
let icon = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
||||
</svg>
|
||||
`;
|
||||
let background = 'bg-green-100';
|
||||
let text = 'text-green-500';
|
||||
return { icon, background, text };
|
||||
}
|
||||
|
||||
if (['question', 'help', 'faq'].includes(type)) {
|
||||
let icon = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z" />
|
||||
</svg>
|
||||
`;
|
||||
let background = 'bg-orange-100';
|
||||
let text = 'text-orange-500';
|
||||
return { icon, background, text };
|
||||
}
|
||||
|
||||
if (['warning', 'caution', 'attention'].includes(type)) {
|
||||
let icon = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
|
||||
</svg>
|
||||
`;
|
||||
let background = 'bg-orange-100';
|
||||
let text = 'text-orange-500';
|
||||
return { icon, background, text };
|
||||
}
|
||||
|
||||
if (['failure', 'fail', 'missing'].includes(type)) {
|
||||
let icon = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
`;
|
||||
let background = 'bg-rose-100';
|
||||
let text = 'text-red-500';
|
||||
return { icon, background, text };
|
||||
}
|
||||
|
||||
if (['danger', 'error'].includes(type)) {
|
||||
let icon = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z" />
|
||||
</svg>
|
||||
`;
|
||||
let background = 'bg-rose-100';
|
||||
let text = 'text-red-500';
|
||||
return { icon, background, text };
|
||||
}
|
||||
|
||||
if (['quote', 'cite'].includes(type)) {
|
||||
let icon = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 0 1 .865-.501 48.172 48.172 0 0 0 3.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z" />
|
||||
</svg>
|
||||
`;
|
||||
let background = 'bg-neutral-100';
|
||||
let text = 'text-neutral-500';
|
||||
return { icon, background, text };
|
||||
}
|
||||
|
||||
return { icon , background, text };
|
||||
}
|
||||
10
tailwind.config.js
Normal file
10
tailwind.config.js
Normal file
@ -0,0 +1,10 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./src/**/*.{html,js}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/typography')
|
||||
],
|
||||
}
|
||||
14
vite.config.js
Normal file
14
vite.config.js
Normal file
@ -0,0 +1,14 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import sprachbund from './src/scripts/sprachbund'
|
||||
|
||||
export default defineConfig({
|
||||
root: 'src',
|
||||
build: {
|
||||
outDir: '../dist',
|
||||
assetsDir: '.assets',
|
||||
emptyOutDir: true
|
||||
},
|
||||
plugins: [
|
||||
sprachbund()
|
||||
]
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user