diff --git a/src/helpers/linkUtils.js b/src/helpers/linkUtils.js index 6f6943d..27bffa4 100644 --- a/src/helpers/linkUtils.js +++ b/src/helpers/linkUtils.js @@ -1,108 +1,92 @@ -const wikilink = /\[\[(.*?\|.*?)\]\]/g +const wikiLinkRegex = /\[\[(.*?\|.*?)\]\]/g; const internalLinkRegex = /href="\/(.*?)"/g; - function caselessCompare(a, b) { - return a.toLowerCase() === b.toLowerCase(); + return a.toLowerCase() === b.toLowerCase(); } function extractLinks(content) { - return[...(content.match(wikilink) || []).map(link => ( - link.slice(2, -2) - .split("|")[0] - .replace(/.(md|markdown)\s?$/i, "") - .replace("\\", "") - .trim() - )), ...(content.match(internalLinkRegex) || []).map( - (link) => - link - .slice(6, -1) - .split("|")[0] - .replace(/.(md|markdown)\s?$/i, "") - .replace("\\", "") - .trim() - )]; + return [ + ...(content.match(wikiLinkRegex) || []).map( + (link) => + link + .slice(2, -2) + .split("|")[0] + .replace(/.(md|markdown)\s?$/i, "") + .replace("\\", "") + .trim() + .split("#")[0] + ), + ...(content.match(internalLinkRegex) || []).map( + (link) => + link + .slice(6, -1) + .split("|")[0] + .replace(/.(md|markdown)\s?$/i, "") + .replace("\\", "") + .trim() + .split("#")[0] + ), + ]; } -function getBacklinks(data) { - const notes = data.collections.note; - if (!notes) { - return []; +function getGraph(data) { + let nodes = {}; + let links = []; + let stemURLs = {}; + let homeAlias = "/"; + data.collections.note.forEach((v, idx) => { + let fpath = v.filePathStem.replace("/notes/", ""); + let parts = fpath.split("/"); + let group = "none"; + if (parts.length >= 3) { + group = parts[parts.length - 2]; } - const currentFileSlug = data.page.filePathStem.replace('/notes/', ''); - const currentURL = data.page.url; - - let backlinks = []; - let uniqueLinks = new Set(); - let counter = 1; - - for (const otherNote of notes) { - const noteContent = otherNote.template.frontMatter.content; - const backLinks = extractLinks(noteContent); - - if (!uniqueLinks.has(otherNote.url) && backLinks.some(link => caselessCompare(link, currentFileSlug) || - currentURL == link.split("#")[0])) { - let preview = noteContent.slice(0, 240); - backlinks.push({ - url: otherNote.url, - title: otherNote.data.title || otherNote.data.page.fileSlug, - preview, - id: counter++ - }) - uniqueLinks.add(otherNote.url); - } + nodes[v.url] = { + id: idx, + title: v.data.title || v.fileSlug, + url: v.url, + group, + home: v.data["dg-home"] || (v.data.tags && v.data.tags.indexOf("gardenEntry") > -1)|| false, + outBound: extractLinks(v.template.frontMatter.content), + neighbors: new Set(), + backLinks: new Set(), + }; + stemURLs[fpath] = v.url; + if (v.data["dg-home"] || (v.data.tags && v.data.tags.indexOf("gardenEntry") > -1)) { + homeAlias = v.url; } - return backlinks; + }); + Object.values(nodes).forEach((node) => { + let outBound = new Set(); + node.outBound.forEach((olink) => { + let link = (stemURLs[olink] || olink).split("#")[0]; + outBound.add(link); + }); + node.outBound = Array.from(outBound); + node.outBound.forEach((link) => { + let n = nodes[link]; + if (n) { + n.neighbors.add(node.url); + n.backLinks.add(node.url); + node.neighbors.add(n.url); + links.push({ source: node.id, target: n.id }); + } + }); + }); + Object.keys(nodes).map((k) => { + nodes[k].neighbors = Array.from(nodes[k].neighbors); + nodes[k].backLinks = Array.from(nodes[k].backLinks); + nodes[k].size = nodes[k].neighbors.length; + }); + return { + homeAlias, + nodes, + links, + }; } -function getOutboundLinks(data, isHome=false){ - const notes = data.collections.note; - - - - if (!notes || notes.length == 0) { - return []; - } - - let currentNote; - if (isHome) { - currentNote = data.collections.gardenEntry && data.collections.gardenEntry[0]; - } else { - const currentFileSlug = data.page.filePathStem.replace('/notes/', ''); - currentNote = notes.find(x => x.data.page.filePathStem && caselessCompare(x.data.page.filePathStem.replace('/notes/', ''), currentFileSlug)); - } - - if (!currentNote) { - return []; - } - - let counter = 1; - let uniqueLinks = new Set(); - - const outboundLinks = extractLinks(currentNote.template.frontMatter.content); - let outbound = outboundLinks.map(fileslug => { - var outboundNote = notes.find(x => caselessCompare(x.data.page.filePathStem.replace("/notes/", ""), fileslug) || x.data.page.url == fileslug.split("#")[0]); - if (!outboundNote) { - return null; - } - if (!uniqueLinks.has(outboundNote.url)) { - - uniqueLinks.add(outboundNote.url); - return { - url: outboundNote.url, - title: outboundNote.data.title || outboundNote.data.page.fileSlug, - id: counter++, - }; - } else { - return null; - } - }).filter(x => x); - return outbound; -} - -exports.wikilink = wikilink; +exports.wikiLinkRegex = wikiLinkRegex; exports.internalLinkRegex = internalLinkRegex; -exports.getBacklinks = getBacklinks; -exports.getOutboundLinks = getOutboundLinks; -exports.caselessCompare = caselessCompare; -exports.extractLinks = extractLinks; \ No newline at end of file +exports.extractLinks = extractLinks; +exports.getGraph = getGraph; diff --git a/src/site/_data/eleventyComputed.js b/src/site/_data/eleventyComputed.js new file mode 100644 index 0000000..d19e5e1 --- /dev/null +++ b/src/site/_data/eleventyComputed.js @@ -0,0 +1,5 @@ +const { getGraph } = require("../../helpers/linkUtils"); + +module.exports = { + graph: (data) => getGraph(data), +} \ No newline at end of file diff --git a/src/site/_includes/components/graphScript.njk b/src/site/_includes/components/graphScript.njk index 831886b..4272bdf 100644 --- a/src/site/_includes/components/graphScript.njk +++ b/src/site/_includes/components/graphScript.njk @@ -1,113 +1,86 @@ \ No newline at end of file + function fetchGraphData() { + fetch('/graph.json').then(res => res.json()).then(data => { + window.graphData = data; + Graph = renderGraph(filterToDepth(JSON.parse(JSON.stringify(data))), "link-graph", 320, 320, 1); + }); + } + + fetchGraphData(); + window.document.getElementById('graph-depth').value = window.maxGraphDepth; + window.document.getElementById('depth-display').innerText = window.maxGraphDepth; + window.document.getElementById('graph-depth').addEventListener('input', (evt) => { + + window.maxGraphDepth = evt.target.value; + window.document.getElementById('depth-display').innerText = window.maxGraphDepth; + if (Graph != null) { + Graph._destructor(); + Graph = null; + } + renderGraph(filterToDepth(JSON.parse(JSON.stringify(window.graphData))), "link-graph", 330, 330, 1); + }) + + window.fullGraph = null; + function renderFullGraph() { + if (!window.fullGraph) { + const graphData = { + links: JSON.parse(JSON.stringify(window.graphData.links)), + nodes: [...Object.values(window.graphData.nodes)] + } + + let g = document.createElement('div'); + g.id = 'full-graph'; + g.classList.add('show'); + document.body.appendChild(g); + g.innerHTML = '
'; + window.fullGraph = renderGraph(graphData, "full-graph-container", g.offsetWidth, g.offsetHeight, 200); + document.getElementById('full-graph-close').addEventListener('click', (evt) => { + g.classList.remove('show'); + window.fullGraph._destructor(); + window.fullGraph = null; + document.getElementById('full-graph').remove() + }); + } + + } + + document.getElementById('graph-full-btn').addEventListener('click', (evt) => { + if (!fullGraph) { + renderFullGraph(); + } + }); + diff --git a/src/site/_includes/components/pageheader.njk b/src/site/_includes/components/pageheader.njk index 0d400ba..4e3fe30 100644 --- a/src/site/_includes/components/pageheader.njk +++ b/src/site/_includes/components/pageheader.njk @@ -17,7 +17,7 @@ - + diff --git a/src/site/_includes/components/sidebar.njk b/src/site/_includes/components/sidebar.njk index b3cfd48..82da48b 100644 --- a/src/site/_includes/components/sidebar.njk +++ b/src/site/_includes/components/sidebar.njk @@ -6,6 +6,21 @@
Connected Pages
+
+
+ +
+ + + + + + +
+ +
+ +
@@ -29,25 +44,41 @@ {%endif%} {%if settings.dgShowBacklinks === true %} + {%if settings.dgShowBacklinks === true %} + {%endif%} + {%endif%} diff --git a/src/site/graph.njk b/src/site/graph.njk new file mode 100644 index 0000000..eb7abd0 --- /dev/null +++ b/src/site/graph.njk @@ -0,0 +1,5 @@ +--- +permalink: /graph.json +eleventyExcludeFromCollections: true +--- +{{ graph | jsonify | safe }} \ No newline at end of file diff --git a/src/site/index.11tydata.js b/src/site/index.11tydata.js index 396fe9f..b38ca19 100644 --- a/src/site/index.11tydata.js +++ b/src/site/index.11tydata.js @@ -1,55 +1,59 @@ - require("dotenv").config(); const settings = require("../helpers/constants"); const markdownIt = require("markdown-it"); -const { getBacklinks, getOutboundLinks } = require("../helpers/linkUtils"); const md = markdownIt({ - html: true, + html: true, }).use(require("../helpers/utils").namedHeadingsFilter); - const allSettings = settings.ALL_NOTE_SETTINGS; module.exports = { - eleventyComputed: { - backlinks: (data) => getBacklinks(data), - outbound: (data) => getOutboundLinks(data, true), - settings: (data) => { - const currentnote = data.collections.gardenEntry && data.collections.gardenEntry[0]; - if (currentnote && currentnote.data) { - const noteSettings = {}; - allSettings.forEach(setting => { - let noteSetting = currentnote.data[setting]; - let globalSetting = process.env[setting]; + eleventyComputed: { + settings: (data) => { + const currentnote = + data.collections.gardenEntry && data.collections.gardenEntry[0]; + if (currentnote && currentnote.data) { + const noteSettings = {}; + allSettings.forEach((setting) => { + let noteSetting = currentnote.data[setting]; + let globalSetting = process.env[setting]; - let settingValue = (noteSetting || (globalSetting === 'true' && noteSetting !== false)); - noteSettings[setting] = settingValue; - }); - return noteSettings; - - } - return {}; - }, - noteTitle: (data) => { - const currentnote = data.collections.gardenEntry && data.collections.gardenEntry[0]; - if (currentnote && currentnote.data) { - return currentnote.data.title || currentnote.data.page.fileSlug; - } - return ""; - }, - tags: (data) => { - const currentnote = data.collections.gardenEntry && data.collections.gardenEntry[0]; - if (currentnote && currentnote.data) { - return currentnote.data.tags; - } - return []; - }, - content: (data) => { - const currentnote = data.collections.gardenEntry && data.collections.gardenEntry[0]; - if (currentnote && currentnote.template && currentnote.template.frontMatter && currentnote.template.frontMatter.content) { - return md.render(currentnote.template.frontMatter.content); - } - return ""; - } - } -} \ No newline at end of file + let settingValue = + noteSetting || (globalSetting === "true" && noteSetting !== false); + noteSettings[setting] = settingValue; + }); + return noteSettings; + } + return {}; + }, + noteTitle: (data) => { + const currentnote = + data.collections.gardenEntry && data.collections.gardenEntry[0]; + if (currentnote && currentnote.data) { + return currentnote.data.title || currentnote.data.page.fileSlug; + } + return ""; + }, + tags: (data) => { + const currentnote = + data.collections.gardenEntry && data.collections.gardenEntry[0]; + if (currentnote && currentnote.data) { + return currentnote.data.tags; + } + return []; + }, + content: (data) => { + const currentnote = + data.collections.gardenEntry && data.collections.gardenEntry[0]; + if ( + currentnote && + currentnote.template && + currentnote.template.frontMatter && + currentnote.template.frontMatter.content + ) { + return md.render(currentnote.template.frontMatter.content); + } + return ""; + }, + }, +}; diff --git a/src/site/notes/notes.11tydata.js b/src/site/notes/notes.11tydata.js index 004846b..c036a74 100644 --- a/src/site/notes/notes.11tydata.js +++ b/src/site/notes/notes.11tydata.js @@ -1,24 +1,21 @@ - require("dotenv").config(); const settings = require("../../helpers/constants"); -const { getBacklinks, getOutboundLinks } = require("../../helpers/linkUtils"); -const allSettings = settings.ALL_NOTE_SETTINGS; +const allSettings = settings.ALL_NOTE_SETTINGS; module.exports = { - eleventyComputed: { - backlinks: (data) => getBacklinks(data), - outbound: (data) => getOutboundLinks(data), - settings: (data) => { - const noteSettings = {}; - allSettings.forEach(setting => { - let noteSetting = data[setting]; - let globalSetting = process.env[setting]; + eleventyComputed: { + settings: (data) => { + const noteSettings = {}; + allSettings.forEach((setting) => { + let noteSetting = data[setting]; + let globalSetting = process.env[setting]; - let settingValue = (noteSetting || (globalSetting === 'true' && noteSetting !== false)); - noteSettings[setting] = settingValue; - }); - return noteSettings; - } - } -} \ No newline at end of file + let settingValue = + noteSetting || (globalSetting === "true" && noteSetting !== false); + noteSettings[setting] = settingValue; + }); + return noteSettings; + }, + }, +}; diff --git a/src/site/styles/digital-garden-base.scss b/src/site/styles/digital-garden-base.scss index 252573c..72a23e9 100644 --- a/src/site/styles/digital-garden-base.scss +++ b/src/site/styles/digital-garden-base.scss @@ -520,3 +520,96 @@ ul.task-list { .callout-fold .lucide { transition: transform 100ms ease-in-out; } + + + + +// Graph Controls +.graph-title-container { + position: relative; +} + +#full-graph { + position: fixed; + top: 50%; + left: 50%; + height: 60vh; + width: 60vw; + min-height: 400px; + min-width: 400px; + transform: translate(-50%, -50%); + z-index: 9999; + display: none; + background-color: var(--background-secondary); + + #full-graph-close { + position: absolute; + top: 10px; + right: 10px; + cursor: pointer; + z-index: 9; + } +} + +#graph-full-btn { + margin-right: 10px; +} + +#full-graph.show { + display: block; +} + +#graph-controls { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 5px; + position: absolute; + top: 115%; + cursor: pointer; + right: 0px; + left: 10px; + color: var(--text-accent); + z-index: 9; + + .depth-control { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 7px; + + .slider { + datalist { + display: flex; + flex-direction: row; + justify-content: space-between; + font-size: 0.6rem; + // padding: 2px 0px; + // width: 200px; + } + + option { + padding: 0; + } + } + + #depth-display { + background-color: var(--text-accent); + color: white; + width: 1rem; + height: 1rem; + font-size: 0.8rem; + display: flex; + justify-content: center; + align-items: center; + margin-top: 0.3rem; + border-radius: 50%; + } + } +} + +input[type=range]::-webkit-slider-thumb { + -webkit-appearance: none; + margin-top: -10px; +} diff --git a/src/site/styles/obsidian-base.scss b/src/site/styles/obsidian-base.scss index 328387a..563409a 100644 --- a/src/site/styles/obsidian-base.scss +++ b/src/site/styles/obsidian-base.scss @@ -7943,6 +7943,8 @@ body { .graph-control-section.mod-display .setting-item:not(.mod-slider):last-child .setting-item-info { display: none; } + + .workspace-leaf-content[data-type='outline'] .view-content { padding: 0; } @@ -10632,4 +10634,3 @@ body { width: 100%; } - \ No newline at end of file