Fork Notesium source and restructure into Go package layout
Initial fork of github.com/alonswartz/notesium into librenotes: - Source moved to internal/notesium/ (package notesium) - Thin entry point at cmd/librenotes/main.go - Module renamed to git.librete.ch/public/librenotes - main() exposed as notesium.Run() - LICENSE preserved (MIT), NOTICE added with attribution - Web assets and completion.bash co-located with embedding code to satisfy go:embed path constraints Closes #3, #34, #35. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Notesium Graph</title>
|
||||
<!--
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>tailwind.config = { darkMode: 'class' }</script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
|
||||
-->
|
||||
<link href="tailwind.css" rel="stylesheet">
|
||||
<script src="d3.min.js"></script>
|
||||
<script src="forcegraph.js"></script>
|
||||
<style>
|
||||
#forcegraph { margin: auto; width: 97vw; height: 97vh; }
|
||||
#forcegraph .node { fill: #ff7f0e; cursor: pointer; }
|
||||
#forcegraph .node-oneword { fill: #1f77b4; }
|
||||
#forcegraph .node-ghost { fill: #a855f7; }
|
||||
#forcegraph .node-dangling { fill-opacity: 0.3; }
|
||||
#forcegraph .link { stroke-width: 0.5; }
|
||||
#forcegraph .label { fill: #475569; font-size: 4px; cursor: pointer; }
|
||||
#forcegraph .label:hover { text-decoration: underline; }
|
||||
.dark #forcegraph .label { fill: #94a3b8; }
|
||||
#forcegraph-filter-results li { margin-bottom: 3px; }
|
||||
#forcegraph-filter-results a:hover { text-decoration: underline; }
|
||||
#preview-panel-content a { color: #2563eb; }
|
||||
#preview-panel-content a:hover { text-decoration: underline; }
|
||||
#preview-panel-content .heading { color: #ff7f0e; font-weight: bold;}
|
||||
#preview-panel-content-body pre { background-color: #f8fafc; padding: 5px;}
|
||||
.dark #preview-panel-content a { color: #60a5fa; }
|
||||
.dark #preview-panel-content hr { border-top: 1px solid #475569; }
|
||||
.dark #preview-panel-content-body pre { background-color: #0f172a; padding: 5px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="h-screen bg-slate-50 dark:bg-slate-800 dark:text-white">
|
||||
|
||||
<div id="preview-panel" hidden class="relative z-10" role="dialog" aria-modal="true">
|
||||
<div class="absolute inset-0 overflow-hidden">
|
||||
<div class="fixed inset-y-0 right-0 flex max-w-full pl-10">
|
||||
<div class="w-screen max-w-xl">
|
||||
<div class="flex h-full flex-col overflow-y-scroll bg-white py-3 shadow-xl dark:text-slate-300 dark:shadow-slate-700 dark:bg-slate-800">
|
||||
<div class="absolute top-0 right-2 px-4 py-2 flex">
|
||||
<a id="preview-panel-editlink" href="#" class="p-1 cursor-pointer text-gray-200 hover:text-gray-400" tabindex="-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
||||
</svg>
|
||||
</a>
|
||||
<div onclick="toggleDiv('#preview-panel')" class="p-1 cursor-pointer text-gray-200 hover:text-gray-400">
|
||||
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div id="preview-panel-content" class="relative flex-1 mt-4 px-4 sm:px-6 text-xs leading-normal">
|
||||
<pre id="preview-panel-content-body"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul id="forcegraph-filter-results" class="absolute top-0 left-44 max-h-screen overflow-y-hidden pt-6 p-4 w-max text-gray-600 text-xs backdrop-blur-sm bg-slate-50/10 dark:text-slate-400 dark:bg-slate-800/10"></ul>
|
||||
|
||||
<div class="absolute w-44 space-y-2 text-xs text-gray-600 border border-gray-200 rounded-lg px-2 py-1.5 m-2 backdrop-blur-sm bg-white/30
|
||||
dark:text-slate-400 dark:border-slate-700 dark:bg-slate-900/50">
|
||||
<div class="flex items-center justify-between pt-1 pb-3">
|
||||
<input id="forcegraph-filter" placeholder="filter..." type="text" autocomplete="off" spellcheck="false"
|
||||
class="w-full px-2 py-1 border focus:outline-none rounded-md dark:bg-slate-900 dark:border-slate-700">
|
||||
<div class="flex h-5 items-center">
|
||||
<input id="forcegraph-filter-results-toggle" type="checkbox" checked class="h-4 w-4 ml-2 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div onclick="toggleDiv('#settings-display')" class="flex items-center justify-between py-1 cursor-pointer">
|
||||
<p class="text-sm text-gray-900 dark:text-slate-200">display</p>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<ul id="settings-display" hidden="true" role="list" class="my-4 divide-y divide-gray-200 dark:divide-slate-600">
|
||||
<li class="flex items-center justify-between py-3">
|
||||
<span>dark mode</span>
|
||||
<div class="flex h-5 items-center">
|
||||
<input id="darkmode" type="checkbox" class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex items-center justify-between py-3">
|
||||
<span>show labels</span>
|
||||
<div class="flex h-5 items-center">
|
||||
<input id="forcegraph-labels" type="checkbox" checked class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex items-center justify-between py-3">
|
||||
<span>auto-scale labels</span>
|
||||
<div class="flex h-5 items-center">
|
||||
<input id="forcegraph-scale-labels" type="checkbox" checked class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex items-center justify-between py-3">
|
||||
<span>size nodes per links</span>
|
||||
<div class="flex h-5 items-center">
|
||||
<input id="forcegraph-dynamic-radius" type="checkbox" class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div onclick="toggleDiv('#settings-cluster')" class="flex items-center justify-between py-1 cursor-pointer">
|
||||
<p class="text-sm text-gray-900 dark:text-slate-200">cluster</p>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<ul id="settings-cluster" hidden="true" role="list" class="my-4 divide-y divide-gray-200 dark:divide-slate-600">
|
||||
<li class="flex items-center justify-between py-3">
|
||||
<span>default</span>
|
||||
<div class="flex h-5 items-center">
|
||||
<input name="cluster" type="radio" onchange="clusterDefault()" checked class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex items-center justify-between py-3">
|
||||
<span>infer from titles</span>
|
||||
<div class="flex h-5 items-center">
|
||||
<input name="cluster" type="radio" onchange="clusterTitles()" class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex items-center justify-between py-3">
|
||||
<span>creation day</span>
|
||||
<div class="flex h-5 items-center">
|
||||
<input name="cluster" type="radio" onchange="clusterDates(format='YYYY-MM-DD')" class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex items-center justify-between py-3">
|
||||
<span>creation week</span>
|
||||
<div class="flex h-5 items-center">
|
||||
<input name="cluster" type="radio" onchange="clusterDates(format='YYYY/WeekXX')" class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex items-center justify-between py-3">
|
||||
<span>creation month</span>
|
||||
<div class="flex h-5 items-center">
|
||||
<input name="cluster" type="radio" onchange="clusterDates(format='YYYY-MM')" class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div onclick="toggleDiv('#settings-forces')" class="flex items-center justify-between py-1 cursor-pointer">
|
||||
<p class="text-sm text-gray-900 dark:text-slate-200">forces</p>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<ul id="settings-forces" hidden="true" role="list" class="my-4 divide-y divide-gray-200 dark:divide-slate-600">
|
||||
<li class="py-3 space-y-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>repel force</span>
|
||||
<span id="forcegraph-force-strength-value">-30</span>
|
||||
</div>
|
||||
<input class="w-full" id="forcegraph-force-strength" type="range" value="-30" min="-100" max="0" step="1">
|
||||
</li>
|
||||
<li class="py-3 space-y-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>collide radius</span>
|
||||
<span id="forcegraph-collide-radius-value">1</span>
|
||||
</div>
|
||||
<input class="w-full" id="forcegraph-collide-radius" type="range" value="1" min="1" max="50" step="1">
|
||||
</li>
|
||||
<li class="py-3 space-y-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>collide strength</span>
|
||||
<span id="forcegraph-collide-strength-value">0.5</span>
|
||||
</div>
|
||||
<input class="w-full" id="forcegraph-collide-strength" type="range" value="0.5" min="0" max="1" step="0.05">
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="forcegraph" class="text-gray-300/70 dark:text-slate-700"></div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
function sendHeartbeat() {
|
||||
fetch("/api/heartbeat")
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error("HTTP status " + response.status);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log("Failed to send heartbeat: " + error.message);
|
||||
clearInterval(heartbeatInterval);
|
||||
});
|
||||
}
|
||||
var heartbeatInterval = setInterval(sendHeartbeat, 5000);
|
||||
|
||||
var originalData = null;
|
||||
|
||||
function clusterDefault() {
|
||||
document.getElementById("forcegraph").innerHTML = "";
|
||||
initialize_forcegraph(originalData, '#forcegraph');
|
||||
}
|
||||
|
||||
function clusterTitles() {
|
||||
const data = {
|
||||
"href": originalData.href,
|
||||
"nodes": [ ...originalData.nodes ],
|
||||
"links": []
|
||||
};
|
||||
const clusters = new Set();
|
||||
data.nodes.forEach(node => {
|
||||
const match = node.title.match(/^(\w+):/);
|
||||
if (match) {
|
||||
clusters.add(match[1]);
|
||||
data.links.push({'source': node.id, 'target': match[1]});
|
||||
}
|
||||
});
|
||||
clusters.forEach(c => data.nodes.push({'id': c, 'title': c, 'type': 'ghost'}));
|
||||
|
||||
document.getElementById("forcegraph").innerHTML = "";
|
||||
initialize_forcegraph(data, '#forcegraph');
|
||||
}
|
||||
|
||||
function clusterDates(format="YYYY-MM-DD") {
|
||||
const data = {
|
||||
"href": originalData.href,
|
||||
"nodes": [ ...originalData.nodes ],
|
||||
"links": []
|
||||
};
|
||||
|
||||
const clusters = new Set();
|
||||
data.nodes.forEach(node => {
|
||||
let epoch = parseInt(node.id.slice(0, -3), 16);
|
||||
let dateTime = new Date(epoch * 1000);
|
||||
let year = dateTime.getFullYear();
|
||||
switch (format) {
|
||||
case 'YYYY-MM-DD':
|
||||
month = String(dateTime.getMonth() + 1).padStart(2, '0');
|
||||
day = String(dateTime.getDate()).padStart(2, '0');
|
||||
formattedDate = `${year}-${month}-${day}`;
|
||||
break;
|
||||
case 'YYYY-MM':
|
||||
month = String(dateTime.getMonth() + 1).padStart(2, '0');
|
||||
formattedDate = `${year}-${month}`;
|
||||
break;
|
||||
case 'YYYY/WeekXX':
|
||||
onejan = new Date(year, 0, 1);
|
||||
week = Math.ceil(((dateTime - onejan) / 86400000 + onejan.getDay() + 1) / 7);
|
||||
formattedDate = `${year}/Week${week}`;
|
||||
}
|
||||
clusters.add(formattedDate);
|
||||
data.links.push({'source': node.id, 'target': formattedDate});
|
||||
})
|
||||
clusters.forEach(c => data.nodes.push({'id': c, 'title': c, 'type': 'ghost'}));
|
||||
|
||||
document.getElementById("forcegraph").innerHTML = "";
|
||||
initialize_forcegraph(data, '#forcegraph');
|
||||
}
|
||||
|
||||
// toggle settings section
|
||||
function toggleDiv(settingsDiv) {
|
||||
d3.select(settingsDiv).attr("hidden", function() {
|
||||
return this.hasAttribute("hidden") ? null : true;
|
||||
});
|
||||
}
|
||||
|
||||
// toggle darkmode
|
||||
d3.select("#darkmode").on("change", function() {
|
||||
d3.select("body").classed("dark", this.checked);
|
||||
});
|
||||
|
||||
// toggle filtered results list
|
||||
d3.select("#forcegraph-filter-results-toggle").on("change", function() {
|
||||
d3.select("#forcegraph-filter-results").classed("hidden", !this.checked);
|
||||
});
|
||||
|
||||
// capture keypress events
|
||||
d3.select("body").on('keyup', function() {
|
||||
if (event.code === 'Slash' && document.activeElement.id != 'forcegraph-filter') {
|
||||
d3.select("#forcegraph-filter").node().focus();
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (event.code === 'Escape' && document.activeElement.id === 'forcegraph-filter') {
|
||||
d3.select("#forcegraph-filter").node().value = '';
|
||||
d3.select("#forcegraph-filter").node().blur();
|
||||
d3.select("#forcegraph-filter").dispatch("input");
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// capture click events
|
||||
d3.select("body").on('click', function() {
|
||||
if (! event.target.attributes.href ) return;
|
||||
if (! event.target.attributes.href.value ) return;
|
||||
if (! event.target.attributes.href.value.endsWith('.md') ) return;
|
||||
if (! graphData.nodes.map(n => n.id).includes(event.target.attributes.href.value) ) return;
|
||||
event.preventDefault();
|
||||
getNote(event.target.attributes.href.value);
|
||||
});
|
||||
|
||||
// markdownify
|
||||
function markdownify(s) {
|
||||
const blocks = [];
|
||||
|
||||
s = s.replace(/(```)([^`]*?)(```)/gs, function(_, start, content, end) {
|
||||
var div = document.createElement("div");
|
||||
div.textContent = content;
|
||||
blocks.push('<pre>' + div.innerHTML + '</pre>');
|
||||
return '%%BLOCK' + (blocks.length - 1) + '%%';
|
||||
});
|
||||
|
||||
s = s
|
||||
.replace(/(^|\s)\[(.*?)\]\((http[s]?:\/\/.*?)\)/g, '$1<a href="$3" target="_blank">$2➜</a>')
|
||||
.replace(/(^|\s)\[(.*?)\]\((?!http[s]?:\/\/)(?!.*\.md$)(.*?)\)/g, '$1<a href="$3">$2</a>')
|
||||
.replace(/(^|\n)(#+\s.*?)(?=\n|$)/g, '$1<span class="heading">$2</span>')
|
||||
.replace(/\*\*(.*?)\*\*/gs, '<b>$1</b>')
|
||||
.replace(/`([^`]*)`/g, '<b><i>`$1`</i></b>')
|
||||
.trimEnd();
|
||||
|
||||
blocks.forEach((block, index) => {
|
||||
s = s.replace('%%BLOCK' + index + '%%', block);
|
||||
});
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
// preview panel
|
||||
function getNote(id) {
|
||||
fetch("/api/notes/" + id)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById("preview-panel").hidden = false;
|
||||
document.getElementById("preview-panel-editlink").href = 'notesium://' + data.Path;
|
||||
document.getElementById("preview-panel-editlink").hidden = hideEditLink;
|
||||
document.getElementById("preview-panel-content-body").innerHTML = markdownify(data.Content);
|
||||
|
||||
var backlinks = ""
|
||||
if (data.IncomingLinks) {
|
||||
backlinks += "\n\n<hr/>\n\n## backlinks\n\n";
|
||||
|
||||
var linksWithTitles = data.IncomingLinks.map(link => {
|
||||
var node = graphData.nodes.find(node => node.id === link.Filename);
|
||||
var title = node ? node.title : link.Filename;
|
||||
return { link: link, title: title };
|
||||
});
|
||||
|
||||
linksWithTitles.sort((a, b) => a.title.localeCompare(b.title));
|
||||
linksWithTitles.forEach(item => {
|
||||
backlinks += `[${item.title}](${item.link.Filename})\n`;
|
||||
});
|
||||
|
||||
document.getElementById("preview-panel-content-body").innerHTML += markdownify(backlinks);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
const hideEditLink = new URLSearchParams(window.location.search).has('noxdg');
|
||||
|
||||
const graphData = {
|
||||
"nodes": [],
|
||||
"links": [],
|
||||
}
|
||||
|
||||
fetch("/api/notes")
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
for (const key in data) {
|
||||
const note = data[key];
|
||||
graphData.nodes.push({'id': note.Filename, 'title': note.Title});
|
||||
|
||||
if (note.OutgoingLinks) {
|
||||
note.OutgoingLinks.forEach(link => {
|
||||
graphData.links.push({'source': note.Filename, 'target': link.Filename});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// dangling links
|
||||
const nodes = graphData.nodes.map(n => n.id);
|
||||
const targets = graphData.links.map(l => l.target);
|
||||
const dangling = targets.filter(id => !nodes.includes(id));
|
||||
dangling.forEach(id => graphData.nodes.push({'id': id, 'title': 'dangling link', 'type': 'ghost'}));
|
||||
|
||||
originalData = graphData;
|
||||
clusterDefault();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user