Implement support for backlinks and local graph

This commit is contained in:
Ole Eskild Steensen 2022-10-13 13:38:06 +02:00
parent 825893d52c
commit ad7872d19f
9 changed files with 452 additions and 7 deletions

View File

@ -0,0 +1,117 @@
<script>
const getCssVar = (variable) => getComputedStyle(document.documentElement).getPropertyValue(variable);
function htmlDecode(input) {
var doc = new DOMParser().parseFromString(input, "text/html");
return doc.documentElement.textContent;
}
const backLinks = [
{%- for backlink in backlinks -%}
{
id: {{backlink.id}},
title: "{{backlink.title | safe}}",
url: "{{backlink.url}}"
},
{%- endfor -%}
];
const outbound = [
{%- for out in outbound -%}
{
id: {{out.id}},
title: "{{out.title | safe}}",
url: "{{out.url}}"
},
{%- endfor -%}
].map(x => {
x.id += backLinks.length;
return x;
});
const currentNode = {
title: "{{page.fileSlug}}",
id: 0,
url: "{{page.url}}"
};
const gData = {
nodes: [
currentNode, ...backLinks,
...outbound
],
links: [
...backLinks.map(backlink => ({source: backlink.id, target: 0})),
...outbound.map(outlink => ({source: 0, target: outlink.id}))
]
};
gData
.links
.forEach(link => {
const a = gData.nodes[link.source];
const b = gData.nodes[link.target];
!a.neighbors && (a.neighbors = []);
!b.neighbors && (b.neighbors = []);
a
.neighbors
.push(b);
b
.neighbors
.push(a);
!a.links && (a.links = []);
!b.links && (b.links = []);
a
.links
.push(link);
b
.links
.push(link);
});
let Graph;
function renderGraph(width, height) {
if (Graph) {
Graph
.width(width)
.height(height);
Graph.zoomToFit()
Graph.zoom(3)
return;
}
Graph = ForceGraph()(document.getElementById('link-graph'))
.width(width)
.height(height)
.nodeCanvasObject((node, ctx) => {
const nodeR = Math.min(7, Math.max((node.links.length + node.neighbors.length)/2, 2));
ctx.beginPath();
ctx.arc(node.x, node.y, nodeR, 0, 2 * Math.PI, false);
ctx.fillStyle = getCssVar("--text-accent");
ctx.fill();
const label = htmlDecode(node.title)
const fontSize = 6;
ctx.font = `${fontSize}px Sans-Serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillStyle = getCssVar("--text-accent");
ctx.fillText(label, node.x, node.y + nodeR + 2);
})
.linkColor(() => getCssVar("--text-normal"))
.graphData(gData)
.onNodeClick(node => {
window.location = node.url;
});
Graph.zoomToFit()
Graph.zoom(3)
}
</script>

View File

@ -7,7 +7,14 @@
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/prism.min.js" integrity="sha512-hpZ5pDCF2bRCweL5WoA0/N1elet1KYL5mx3LP555Eg/0ZguaHawxNvEjF6O3rufAChs16HVNhEc6blF/rZoowQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/plugins/autoloader/prism-autoloader.min.js" integrity="sha512-sv0slik/5O0JIPdLBCR2A3XDg/1U3WuDEheZfI/DI5n8Yqc3h5kjrnr46FGBNiUAJF7rE4LHKwQ/SoSLRKAxEA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="//unpkg.com/force-graph"></script>
<script src="//unpkg.com/alpinejs" defer></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/themes/prism-okaidia.min.css" integrity="sha512-mIs9kKbaw6JZFfSuo+MovjU+Ntggfoj8RwAmJbVXQ5mkAX5LlgETQEweFPI18humSPHymTb5iikEOKWF7I8ncQ==" crossorigin="anonymous" referrerpolicy="no-referrer"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
<link href="/styles/digital-garden-base.css" rel="stylesheet">

View File

@ -0,0 +1,68 @@
<div x-init="isDesktop = window.innerWidth>=1100; rerenderGraph()"
x-on:resize.window="isDesktop = (window.innerWidth>=1100) ? true : false;"
x-data="sidebarData">
<div class="sidebar" :style="open && isDesktop ? 'box-shadow: -5px 0 15px rgba(0,0,0,0.3);' : ''">
<div x-show="isDesktop" class="expand-line">
<button style="font-size: 48px; background: transparent;" @click="toggleOpen()">
<i x-show="open" class="fa fa-caret-right"></i>
<i x-show="!open" class="fa fa-caret-left"></i>
</button>
</div>
<div x-show="open || !isDesktop" class="sidebar-container">
{%if dgShowLocalGraph === true%}
<div x-show="isDesktop">
<h3>Graph</h3>
<div id="link-graph" style="margin-bottom:20px; background-color: var(--background-primary); border-radius: 10px; width: fit-content;"></div>
</div>
{%endif%}
{%if dgShowBacklinks === true %}
<h3>Links to this page</h3>
{%- if backlinks.length === 0 -%}
<div class="backlink-card">
No backlinks
</div>
{%- endif -%}
{%- for backlink in backlinks -%}
<div class="backlink-card">
<a href="{{backlink.url}}">{{backlink.title}}</a>
</div>
{%- endfor -%}
{%endif%}
</div>
</div>
{%if dgShowLocalGraph === true %}
{%include "components/graphScript.njk"%}
{% endif %}
<script>
document.addEventListener('alpine:init', () => {
const isDesktop = window.innerWidth >= 1100;
{% if dgShowLocalGraph === true %}
renderGraph(600, 400);
{% endif %}
Alpine.data('sidebarData', () => ({
open: false,
isDesktop,
toggleOpen() {
this.open = !this.open;
if (Graph) {
setTimeout(() => {
Graph.zoomToFit();
Graph.zoom(3);
}, 10);
}
}
}))
})
</script>

View File

@ -16,6 +16,10 @@ permalink: "notes/{{ page.fileSlug | slugify }}/"
<a href="/">🏡 Back Home</a>
{% endif %}
{{ content | link | highlight | safe}}
{% if dgShowBacklinks === true or dgShowLocalGraph === true%}
{%include "components/sidebar.njk"%}
{% endif %}
</div>
</body>
</html>

101
src/site/index.11tydata.js Normal file
View File

@ -0,0 +1,101 @@
const wikilink = /\[\[(.*?\|.*?)\]\]/g
function caselessCompare(a, b) {
return a.toLowerCase() === b.toLowerCase();
}
module.exports = {
eleventyComputed: {
backlinks: (data) => {
const notes = data.collections.note;
if(!notes){
return [];
}
const currentFileSlug = data.page.filePathStem.replace('/notes/', '');
let backlinks = [];
let counter = 1;
for (const otherNote of notes) {
const noteContent = otherNote.template.frontMatter.content;
const outboundLinks = (noteContent.match(wikilink) || []).map(link => (
link.slice(2, -2)
.split("|")[0]
.replace(/.(md|markdown)\s?$/i, "")
.trim()
));
if (outboundLinks.some(link => caselessCompare(link, currentFileSlug))) {
let preview = noteContent.slice(0, 240);
backlinks.push({
url: otherNote.url,
title: otherNote.data.page.fileSlug,
preview,
id: counter++
})
}
}
return backlinks;
},
outbound: (data) => {
const notes = data.collections.note;
if(!notes || notes.length == 0){
return [];
}
const currentNote = data.collections.gardenEntry && data.collections.gardenEntry[0];
if(!currentNote){
return [];
}
let counter = 1;
const noteContent = currentNote.template.frontMatter.content;
const outboundLinks = (noteContent.match(wikilink) || []).map(link => (
link.slice(2, -2)
.split("|")[0]
.replace(/.(md|markdown)\s?$/i, "")
.trim()
));
let outbound = outboundLinks.map(fileslug => {
var outboundNote = notes.find(x => caselessCompare(x.data.page.filePathStem.replace("/notes/", ""), fileslug));
if(!outboundNote){
return null;
}
return {
url: outboundNote.url,
title: outboundNote.data.page.fileSlug,
id: counter++
}
}).filter(x=>x);
return outbound;
},
dgShowLocalGraph: (data) => {
const currentNote = data.collections.gardenEntry && data.collections.gardenEntry[0];
if(currentNote && currentNote.data && currentNote.data.dgShowLocalGraph){
return true;
}
return false;
},
dgShowBacklinks: (data) =>{
const currentnote = data.collections.gardenEntry && data.collections.gardenEntry[0];
if(currentnote && currentnote.data && currentnote.data.dgShowLocalGraph){
return true;
}
return false;
}
}
}

View File

@ -7,9 +7,14 @@
<body class="theme-{{meta.baseTheme}} markdown-preview-view">
{%include "components/notegrowthhistory.njk"%}
<div class="content">
{%- for garden in collections.gardenEntry -%}
{{garden.templateContent | link | highlight | safe }}
{%- endfor -%}
{%if collections.gardenEntry[0].data.dgShowBacklinks === true or collections.gardenEntry[0].data.dgShowLocalGraph === true%}
{%include "components/sidebar.njk" %}
{%endif%}
</div>
</body>
</html>
</html>

View File

@ -0,0 +1,86 @@
const wikilink = /\[\[(.*?\|.*?)\]\]/g
function caselessCompare(a, b) {
return a.toLowerCase() === b.toLowerCase();
}
module.exports = {
eleventyComputed: {
backlinks: (data) => {
const notes = data.collections.note;
if(!notes){
return [];
}
const currentFileSlug = data.page.filePathStem.replace('/notes/', '');
let backlinks = [];
let counter = 1;
for (const otherNote of notes) {
const noteContent = otherNote.template.frontMatter.content;
const outboundLinks = (noteContent.match(wikilink) || []).map(link => (
link.slice(2, -2)
.split("|")[0]
.replace(/.(md|markdown)\s?$/i, "")
.trim()
));
if (outboundLinks.some(link => caselessCompare(link, currentFileSlug))) {
let preview = noteContent.slice(0, 240);
backlinks.push({
url: otherNote.url,
title: otherNote.data.page.fileSlug,
preview,
id: counter++
})
}
}
return backlinks;
},
outbound: (data) => {
const notes = data.collections.note;
const currentFileSlug = data.page.filePathStem.replace('/notes/', '');
if(!notes || notes.length == 0){
return [];
}
const currentNote = notes.find(x =>x.data.page.filePathStem && caselessCompare(x.data.page.filePathStem.replace('/notes/', ''), currentFileSlug));
if(!currentNote){
return [];
}
let counter = 1;
const noteContent = currentNote.template.frontMatter.content;
const outboundLinks = (noteContent.match(wikilink) || []).map(link => (
link.slice(2, -2)
.split("|")[0]
.replace(/.(md|markdown)\s?$/i, "")
.trim()
));
let outbound = outboundLinks.map(fileslug => {
var outboundNote = notes.find(x => caselessCompare(x.data.page.filePathStem.replace("/notes/", ""), fileslug));
if(!outboundNote){
return null;
}
return {
url: outboundNote.url,
title: outboundNote.data.page.fileSlug,
id: counter++
}
}).filter(x=>x);
return outbound;
}
}
}

View File

@ -60,6 +60,49 @@ ul.task-list {
padding-left: 15px;
}
.sidebar {
position: fixed;
top: 50%;
transform: translateY(-50%);
right: 0;
height: 100%;
background-color: var(--background-secondary);
min-width: 25px;
display: flex;
}
.expand-line {
display: flex;
flex-direction: column;
justify-content: center;
}
.sidebar-container {
padding: 20px;
width: 100%;
overflow-y: auto;
}
.backlink-card {
padding: 20px;
border-radius: 4px;
margin-bottom: 20px;
background-color: var(--background-primary);
width: 100%;
}
@media(max-width: 1100px) {
.sidebar {
position: relative;
transform: none;
border-radius: 4px;
}
}
div[class*="language-ad-"],
div[class*="callout-"] {
font-family: 'Roboto', sans-serif;

View File

@ -3,6 +3,14 @@
* MODIFY THE custom-style.scss FILE INSTEAD.
***/
body{
--background-primary: rgb(32, 31, 31);
--background-secondary: rgb(57, 56, 56);
--text-normal: #dcddde;
--text-accent: rgb(97, 186, 245);
}
h1 {
color: #FFEF60;
}
@ -19,6 +27,12 @@ h4 {
color: #72DCFF;
}
button {
border: none;
color: white;
padding: 5px 15px;
}
.centered {
position: absolute;
top: 50%;
@ -28,8 +42,8 @@ h4 {
}
.theme-dark {
background: rgb(32, 31, 31);
color: white;
background: var(--background-primary);
color: var(--text-normal);
font-family: 'Roboto', sans-serif;
}
@ -44,7 +58,7 @@ a.is-unresolved{
}
a {
text-decoration: underline;
color: rgb(97, 186, 245);
color: var(--text-accent)
}
.font-bg {