commit
7988eb4033
|
@ -0,0 +1,500 @@
|
|||
const slugify = require("@sindresorhus/slugify");
|
||||
const markdownIt = require("markdown-it");
|
||||
const fs = require("fs");
|
||||
const matter = require("gray-matter");
|
||||
const faviconsPlugin = require("eleventy-plugin-gen-favicons");
|
||||
const tocPlugin = require("eleventy-plugin-nesting-toc");
|
||||
const { parse } = require("node-html-parser");
|
||||
const htmlMinifier = require("html-minifier");
|
||||
const pluginRss = require("@11ty/eleventy-plugin-rss");
|
||||
|
||||
const { headerToId, namedHeadingsFilter } = require("./src/helpers/utils");
|
||||
const {
|
||||
userMarkdownSetup,
|
||||
userEleventySetup,
|
||||
} = require("./src/helpers/userSetup");
|
||||
|
||||
const Image = require("@11ty/eleventy-img");
|
||||
function transformImage(src, cls, alt, sizes, widths = ["500", "700", "auto"]) {
|
||||
let options = {
|
||||
widths: widths,
|
||||
formats: ["webp", "jpeg"],
|
||||
outputDir: "./dist/img/optimized",
|
||||
urlPath: "/img/optimized",
|
||||
};
|
||||
|
||||
// generate images, while this is async we don’t wait
|
||||
Image(src, options);
|
||||
let metadata = Image.statsSync(src, options);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
const tagRegex = /(^|\s|\>)(#[^\s!@#$%^&*()=+\.,\[{\]};:'"?><]+)(?!([^<]*>))/g;
|
||||
|
||||
module.exports = function (eleventyConfig) {
|
||||
eleventyConfig.setLiquidOptions({
|
||||
dynamicPartials: true,
|
||||
});
|
||||
let markdownLib = markdownIt({
|
||||
breaks: true,
|
||||
html: true,
|
||||
linkify: true,
|
||||
})
|
||||
.use(require("markdown-it-anchor"), {
|
||||
slugify: headerToId,
|
||||
})
|
||||
.use(require("markdown-it-mark"))
|
||||
.use(require("markdown-it-footnote"))
|
||||
.use(function (md) {
|
||||
md.renderer.rules.hashtag_open = function (tokens, idx) {
|
||||
return '<a class="tag" onclick="toggleTagSearch(this)">';
|
||||
};
|
||||
})
|
||||
.use(require("markdown-it-mathjax3"), {
|
||||
tex: {
|
||||
inlineMath: [["$", "$"]],
|
||||
},
|
||||
options: {
|
||||
skipHtmlTags: { "[-]": ["pre"] },
|
||||
},
|
||||
})
|
||||
.use(require("markdown-it-attrs"))
|
||||
.use(require("markdown-it-task-checkbox"), {
|
||||
disabled: true,
|
||||
divWrap: false,
|
||||
divClass: "checkbox",
|
||||
idPrefix: "cbx_",
|
||||
ulClass: "task-list",
|
||||
liClass: "task-list-item",
|
||||
})
|
||||
.use(require("markdown-it-plantuml"), {
|
||||
openMarker: "```plantuml",
|
||||
closeMarker: "```",
|
||||
})
|
||||
.use(namedHeadingsFilter)
|
||||
.use(function (md) {
|
||||
//https://github.com/DCsunset/markdown-it-mermaid-plugin
|
||||
const origFenceRule =
|
||||
md.renderer.rules.fence ||
|
||||
function (tokens, idx, options, env, self) {
|
||||
return self.renderToken(tokens, idx, options, env, self);
|
||||
};
|
||||
md.renderer.rules.fence = (tokens, idx, options, env, slf) => {
|
||||
const token = tokens[idx];
|
||||
if (token.info === "mermaid") {
|
||||
const code = token.content.trim();
|
||||
return `<pre class="mermaid">${code}</pre>`;
|
||||
}
|
||||
if (token.info === "transclusion") {
|
||||
const code = token.content.trim();
|
||||
return `<div class="transclusion">${md.render(code)}</div>`;
|
||||
}
|
||||
if (token.info.startsWith("ad-")) {
|
||||
const code = token.content.trim();
|
||||
const parts = code.split("\n")
|
||||
let titleLine;
|
||||
let collapse;
|
||||
let collapsible = false
|
||||
let collapsed = true
|
||||
let icon;
|
||||
let color;
|
||||
let nbLinesToSkip = 0
|
||||
for (let i = 0; i<4; i++) {
|
||||
if (parts[i] && parts[i].trim()) {
|
||||
let line = parts[i] && parts[i].trim().toLowerCase()
|
||||
if (line.startsWith("title:")) {
|
||||
titleLine = line.substring(6);
|
||||
nbLinesToSkip ++;
|
||||
} else if (line.startsWith("icon:")) {
|
||||
icon = line.substring(5);
|
||||
nbLinesToSkip ++;
|
||||
} else if (line.startsWith("collapse:")) {
|
||||
collapsible = true
|
||||
collapse = line.substring(9);
|
||||
if (collapse && collapse.trim().toLowerCase() == 'open') {
|
||||
collapsed = false
|
||||
}
|
||||
nbLinesToSkip ++;
|
||||
} else if (line.startsWith("color:")) {
|
||||
color = line.substring(6);
|
||||
nbLinesToSkip ++;
|
||||
}
|
||||
}
|
||||
}
|
||||
const foldDiv = collapsible ? `<div class="callout-fold">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-chevron-down">
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
</div>` : "";
|
||||
const titleDiv = titleLine
|
||||
? `<div class="callout-title"><div class="callout-title-inner">${titleLine}</div>${foldDiv}</div>`
|
||||
: "";
|
||||
let collapseClasses = titleLine && collapsible ? 'is-collapsible' : ''
|
||||
if (collapsible && collapsed) {
|
||||
collapseClasses += " is-collapsed"
|
||||
}
|
||||
|
||||
let res = `<div data-callout-metadata class="callout ${collapseClasses}" data-callout="${
|
||||
token.info.substring(3)
|
||||
}">${titleDiv}\n<div class="callout-content">${md.render(
|
||||
parts.slice(nbLinesToSkip).join("\n")
|
||||
)}</div></div>`;
|
||||
return res
|
||||
}
|
||||
|
||||
// Other languages
|
||||
return origFenceRule(tokens, idx, options, env, slf);
|
||||
};
|
||||
|
||||
const defaultImageRule =
|
||||
md.renderer.rules.image ||
|
||||
function (tokens, idx, options, env, self) {
|
||||
return self.renderToken(tokens, idx, options, env, self);
|
||||
};
|
||||
md.renderer.rules.image = (tokens, idx, options, env, self) => {
|
||||
const imageName = tokens[idx].content;
|
||||
const [fileName, width] = imageName.split("|");
|
||||
if (width) {
|
||||
const widthIndex = tokens[idx].attrIndex("width");
|
||||
const widthAttr = `${width}px`;
|
||||
if (widthIndex < 0) {
|
||||
tokens[idx].attrPush(["width", widthAttr]);
|
||||
} else {
|
||||
tokens[idx].attrs[widthIndex][1] = widthAttr;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultImageRule(tokens, idx, options, env, self);
|
||||
};
|
||||
|
||||
const defaultLinkRule =
|
||||
md.renderer.rules.link_open ||
|
||||
function (tokens, idx, options, env, self) {
|
||||
return self.renderToken(tokens, idx, options, env, self);
|
||||
};
|
||||
md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
|
||||
const aIndex = tokens[idx].attrIndex("target");
|
||||
const classIndex = tokens[idx].attrIndex("class");
|
||||
|
||||
if (aIndex < 0) {
|
||||
tokens[idx].attrPush(["target", "_blank"]);
|
||||
} else {
|
||||
tokens[idx].attrs[aIndex][1] = "_blank";
|
||||
}
|
||||
|
||||
if (classIndex < 0) {
|
||||
tokens[idx].attrPush(["class", "external-link"]);
|
||||
} else {
|
||||
tokens[idx].attrs[classIndex][1] = "external-link";
|
||||
}
|
||||
|
||||
return defaultLinkRule(tokens, idx, options, env, self);
|
||||
};
|
||||
})
|
||||
.use(userMarkdownSetup);
|
||||
|
||||
eleventyConfig.setLibrary("md", markdownLib);
|
||||
|
||||
eleventyConfig.addFilter("isoDate", function (date) {
|
||||
return date && date.toISOString();
|
||||
});
|
||||
|
||||
|
||||
eleventyConfig.addFilter("link", function (str) {
|
||||
return (
|
||||
str &&
|
||||
str.replace(/\[\[(.*?\|.*?)\]\]/g, function (match, p1) {
|
||||
//Check if it is an embedded excalidraw drawing or mathjax javascript
|
||||
if (p1.indexOf("],[") > -1 || p1.indexOf('"$"') > -1) {
|
||||
return match;
|
||||
}
|
||||
const [fileLink, linkTitle] = p1.split("|");
|
||||
|
||||
let fileName = fileLink.replaceAll("&", "&");
|
||||
let header = "";
|
||||
let headerLinkPath = "";
|
||||
if (fileLink.includes("#")) {
|
||||
[fileName, header] = fileLink.split("#");
|
||||
headerLinkPath = `#${headerToId(header)}`;
|
||||
}
|
||||
|
||||
let permalink = `/notes/${slugify(fileName)}`;
|
||||
let noteIcon = process.env.NOTE_ICON_DEFAULT;
|
||||
const title = linkTitle ? linkTitle : fileName;
|
||||
let deadLink = false;
|
||||
|
||||
try {
|
||||
const startPath = "./src/site/notes/";
|
||||
const fullPath = fileName.endsWith(".md")
|
||||
? `${startPath}${fileName}`
|
||||
: `${startPath}${fileName}.md`;
|
||||
const file = fs.readFileSync(fullPath, "utf8");
|
||||
const frontMatter = matter(file);
|
||||
if (frontMatter.data.permalink) {
|
||||
permalink = frontMatter.data.permalink;
|
||||
}
|
||||
if (
|
||||
frontMatter.data.tags &&
|
||||
frontMatter.data.tags.indexOf("gardenEntry") != -1
|
||||
) {
|
||||
permalink = "/";
|
||||
}
|
||||
if (frontMatter.data.noteIcon) {
|
||||
noteIcon = frontMatter.data.noteIcon;
|
||||
}
|
||||
} catch {
|
||||
deadLink = true;
|
||||
}
|
||||
|
||||
if(deadLink){
|
||||
return `<a class="internal-link is-unresolved" href="/404">${title}</a>`;
|
||||
}
|
||||
return `<a class="internal-link" data-note-icon="${noteIcon}" href="${permalink}${headerLinkPath}">${title}</a>`;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
eleventyConfig.addFilter("taggify", function (str) {
|
||||
return (
|
||||
str &&
|
||||
str.replace(tagRegex, function (match, precede, tag) {
|
||||
return `${precede}<a class="tag" onclick="toggleTagSearch(this)" data-content="${tag}">${tag}</a>`;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
eleventyConfig.addFilter("searchableTags", function (str) {
|
||||
let tags;
|
||||
let match = str && str.match(tagRegex);
|
||||
if (match) {
|
||||
tags = match
|
||||
.map((m) => {
|
||||
return `"${m.split("#")[1]}"`;
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
if (tags) {
|
||||
return `${tags},`;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
eleventyConfig.addFilter("hideDataview", function (str) {
|
||||
return (
|
||||
str &&
|
||||
str.replace(/\(\S+\:\:(.*)\)/g, function (_, value) {
|
||||
return value.trim();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
eleventyConfig.addTransform("callout-block", function (str) {
|
||||
const parsed = parse(str);
|
||||
|
||||
const transformCalloutBlocks = (
|
||||
blockquotes = parsed.querySelectorAll("blockquote")
|
||||
) => {
|
||||
for (const blockquote of blockquotes) {
|
||||
transformCalloutBlocks(blockquote.querySelectorAll("blockquote"));
|
||||
|
||||
let content = blockquote.innerHTML;
|
||||
|
||||
let titleDiv = "";
|
||||
let calloutType = "";
|
||||
let isCollapsable;
|
||||
let isCollapsed;
|
||||
const calloutMeta = /\[!([\w-]*)\](\+|\-){0,1}(\s?.*)/;
|
||||
if (!content.match(calloutMeta)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
content = content.replace(
|
||||
calloutMeta,
|
||||
function (metaInfoMatch, callout, collapse, title) {
|
||||
isCollapsable = Boolean(collapse);
|
||||
isCollapsed = collapse === "-";
|
||||
const titleText = title.replace(/(<\/{0,1}\w+>)/, "")
|
||||
? title
|
||||
: `${callout.charAt(0).toUpperCase()}${callout
|
||||
.substring(1)
|
||||
.toLowerCase()}`;
|
||||
const fold = isCollapsable
|
||||
? `<div class="callout-fold"><i icon-name="chevron-down"></i></div>`
|
||||
: ``;
|
||||
|
||||
calloutType = callout;
|
||||
titleDiv = `<div class="callout-title"><div class="callout-title-inner">${titleText}</div>${fold}</div>`;
|
||||
return "";
|
||||
}
|
||||
);
|
||||
|
||||
blockquote.tagName = "div";
|
||||
blockquote.classList.add("callout");
|
||||
blockquote.classList.add(isCollapsable ? "is-collapsible" : "");
|
||||
blockquote.classList.add(isCollapsed ? "is-collapsed" : "");
|
||||
blockquote.setAttribute("data-callout", calloutType.toLowerCase());
|
||||
blockquote.innerHTML = `${titleDiv}\n<div class="callout-content">${content}</div>`;
|
||||
}
|
||||
};
|
||||
|
||||
transformCalloutBlocks();
|
||||
|
||||
return str && parsed.innerHTML;
|
||||
});
|
||||
|
||||
function fillPictureSourceSets(src, cls, alt, meta, width, imageTag) {
|
||||
imageTag.tagName = "picture";
|
||||
let html = `<source
|
||||
media="(max-width:480px)"
|
||||
srcset="${meta.webp[0].url}"
|
||||
type="image/webp"
|
||||
/>
|
||||
<source
|
||||
media="(max-width:480px)"
|
||||
srcset="${meta.jpeg[0].url}"
|
||||
/>
|
||||
`
|
||||
if (meta.webp && meta.webp[1] && meta.webp[1].url) {
|
||||
html += `<source
|
||||
media="(max-width:1920px)"
|
||||
srcset="${meta.webp[1].url}"
|
||||
type="image/webp"
|
||||
/>`
|
||||
}
|
||||
if (meta.jpeg && meta.jpeg[1] && meta.jpeg[1].url) {
|
||||
html += `<source
|
||||
media="(max-width:1920px)"
|
||||
srcset="${meta.jpeg[1].url}"
|
||||
/>`
|
||||
}
|
||||
html += `<img
|
||||
class="${cls.toString()}"
|
||||
src="${src}"
|
||||
alt="${alt}"
|
||||
width="${width}"
|
||||
/>`;
|
||||
imageTag.innerHTML = html;
|
||||
}
|
||||
|
||||
|
||||
eleventyConfig.addTransform("picture", function (str) {
|
||||
const parsed = parse(str);
|
||||
for (const imageTag of parsed.querySelectorAll(".cm-s-obsidian img")) {
|
||||
const src = imageTag.getAttribute("src");
|
||||
if (src && src.startsWith("/") && !src.endsWith(".svg")) {
|
||||
const cls = imageTag.classList.value;
|
||||
const alt = imageTag.getAttribute("alt");
|
||||
const width = imageTag.getAttribute("width") || '';
|
||||
|
||||
try {
|
||||
const meta = transformImage(
|
||||
"./src/site" + decodeURI(imageTag.getAttribute("src")),
|
||||
cls.toString(),
|
||||
alt,
|
||||
["(max-width: 480px)", "(max-width: 1024px)"]
|
||||
);
|
||||
|
||||
if (meta) {
|
||||
fillPictureSourceSets(src, cls, alt, meta, width, imageTag);
|
||||
}
|
||||
} catch {
|
||||
// Make it fault tolarent.
|
||||
}
|
||||
}
|
||||
}
|
||||
return str && parsed.innerHTML;
|
||||
});
|
||||
|
||||
eleventyConfig.addTransform("table", function (str) {
|
||||
const parsed = parse(str);
|
||||
for (const t of parsed.querySelectorAll(".cm-s-obsidian > table")) {
|
||||
let inner = t.innerHTML;
|
||||
t.tagName = "div";
|
||||
t.classList.add("table-wrapper");
|
||||
t.innerHTML = `<table>${inner}</table>`;
|
||||
}
|
||||
|
||||
for (const t of parsed.querySelectorAll(
|
||||
".cm-s-obsidian > .block-language-dataview > table"
|
||||
)) {
|
||||
t.classList.add("dataview");
|
||||
t.classList.add("table-view-table");
|
||||
t.querySelector("thead")?.classList.add("table-view-thead");
|
||||
t.querySelector("tbody")?.classList.add("table-view-tbody");
|
||||
t.querySelectorAll("thead > tr")?.forEach((tr) => {
|
||||
tr.classList.add("table-view-tr-header");
|
||||
});
|
||||
t.querySelectorAll("thead > tr > th")?.forEach((th) => {
|
||||
th.classList.add("table-view-th");
|
||||
});
|
||||
}
|
||||
return str && parsed.innerHTML;
|
||||
});
|
||||
|
||||
eleventyConfig.addTransform("htmlMinifier", (content, outputPath) => {
|
||||
if (
|
||||
process.env.NODE_ENV === "production" &&
|
||||
outputPath &&
|
||||
outputPath.endsWith(".html")
|
||||
) {
|
||||
return htmlMinifier.minify(content, {
|
||||
useShortDoctype: true,
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
minifyCSS: true,
|
||||
minifyJS: true,
|
||||
keepClosingSlash: true,
|
||||
});
|
||||
}
|
||||
return content;
|
||||
});
|
||||
|
||||
eleventyConfig.addPassthroughCopy("src/site/img");
|
||||
eleventyConfig.addPassthroughCopy("src/site/scripts");
|
||||
eleventyConfig.addPassthroughCopy("src/site/styles/_theme.*.css");
|
||||
eleventyConfig.addPlugin(faviconsPlugin, { outputDir: "dist" });
|
||||
eleventyConfig.addPlugin(tocPlugin, {
|
||||
ul: true,
|
||||
tags: ["h1", "h2", "h3", "h4", "h5", "h6"],
|
||||
});
|
||||
|
||||
|
||||
eleventyConfig.addFilter("dateToZulu", function (date) {
|
||||
if (!date) return "";
|
||||
return new Date(date).toISOString("dd-MM-yyyyTHH:mm:ssZ");
|
||||
});
|
||||
eleventyConfig.addFilter("jsonify", function (variable) {
|
||||
return JSON.stringify(variable) || '""';
|
||||
});
|
||||
|
||||
eleventyConfig.addFilter("validJson", function (variable) {
|
||||
if (Array.isArray(variable)) {
|
||||
return variable.map((x) => x.replaceAll("\\", "\\\\")).join(",");
|
||||
} else if (typeof variable === "string") {
|
||||
return variable.replaceAll("\\", "\\\\");
|
||||
}
|
||||
return variable;
|
||||
});
|
||||
|
||||
eleventyConfig.addPlugin(pluginRss, {
|
||||
posthtmlRenderOptions: {
|
||||
closingSingleTag: "slash",
|
||||
singleTags: ["link"],
|
||||
},
|
||||
});
|
||||
|
||||
userEleventySetup(eleventyConfig);
|
||||
|
||||
return {
|
||||
dir: {
|
||||
input: "src/site",
|
||||
output: "dist",
|
||||
data: `_data`,
|
||||
},
|
||||
templateFormats: ["njk", "md", "11ty.js"],
|
||||
htmlTemplateEngine: "njk",
|
||||
markdownTemplateEngine: "njk",
|
||||
passthroughFileCopy: true,
|
||||
};
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
netlify/functions
|
|
@ -0,0 +1,4 @@
|
|||
#THEME=https://raw.githubusercontent.com/colineckert/obsidian-things/main/obsidian.css
|
||||
#THEME=https://github.com/kepano/obsidian-minimal/blob/master/obsidian.css
|
||||
#BASE_THEME=light
|
||||
dgHomeLink=true
|
|
@ -0,0 +1,15 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
ignore:
|
||||
- dependency-name: "@sindresorhus/slugify"
|
||||
# For slugify, ignore all updates.
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
node_modules
|
||||
dist
|
||||
netlify/functions/search/data.json
|
||||
netlify/functions/search/index.json
|
||||
src/site/styles/theme.*.css
|
||||
src/site/styles/_theme.*.css
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
.idea/
|
||||
.vercel
|
||||
.cache
|
||||
_site/
|
||||
**/.DS_Store
|
|
@ -0,0 +1,9 @@
|
|||
# Digital Obsidian Garden
|
||||
This is the template to be used together with the [Digital Garden Obsidian Plugin](https://github.com/oleeskild/Obsidian-Digital-Garden).
|
||||
See the README in the plugin repo for information on how to set it up.
|
||||
|
||||
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/oleeskild/digitalgarden)
|
||||
|
||||
---
|
||||
## Docs
|
||||
Docs are available at [dg-docs.ole.dev](https://dg-docs.ole.dev/)
|
|
@ -0,0 +1,14 @@
|
|||
[build]
|
||||
publish = "dist"
|
||||
command = "npm install && npm run build"
|
||||
|
||||
[[redirects]]
|
||||
from = "/api/*"
|
||||
to = "/.netlify/functions/:splat"
|
||||
status = 200
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/404"
|
||||
status = 404
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"name": "web",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "npm-run-all get-theme build:sass --parallel watch:*",
|
||||
"watch:sass": "sass --watch src/site/styles:dist/styles",
|
||||
"watch:eleventy": "cross-env ELEVENTY_ENV=dev eleventy --serve",
|
||||
"build:eleventy": "cross-env ELEVENTY_ENV=prod NODE_OPTIONS=--max-old-space-size=4096 eleventy",
|
||||
"build:sass": "sass src/site/styles:dist/styles --style compressed",
|
||||
"get-theme": "node src/site/get-theme.js",
|
||||
"build": "npm-run-all get-theme build:*"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@11ty/eleventy": "^2.0.1",
|
||||
"@11ty/eleventy-plugin-rss": "^1.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"html-minifier": "^4.0.0",
|
||||
"node-html-parser": "^6.1.4",
|
||||
"sass": "^1.49.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@11ty/eleventy-img": "^3.0.0",
|
||||
"@sindresorhus/slugify": "^1.1.0",
|
||||
"axios": "^1.2.2",
|
||||
"dotenv": "^16.0.3",
|
||||
"eleventy-plugin-gen-favicons": "^1.1.2",
|
||||
"eleventy-plugin-nesting-toc": "^1.3.0",
|
||||
"fs-file-tree": "^1.1.1",
|
||||
"glob": "^10.2.1",
|
||||
"gray-matter": "^4.0.3",
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-anchor": "^8.6.7",
|
||||
"markdown-it-attrs": "^4.1.6",
|
||||
"markdown-it-footnote": "^3.0.3",
|
||||
"markdown-it-mark": "^3.0.1",
|
||||
"markdown-it-mathjax3": "^4.3.1",
|
||||
"markdown-it-plantuml": "^1.4.1",
|
||||
"markdown-it-task-checkbox": "^1.0.6",
|
||||
"npm-run-all": "^4.1.5"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
"filesToDelete": [
|
||||
"src/site/styles/style.css",
|
||||
"src/site/index.njk",
|
||||
"src/site/index.11tydata.js",
|
||||
"src/site/_data/filetree.js",
|
||||
"api/search.js",
|
||||
"netlify/functions/search/search.js",
|
||||
"src/site/versionednote.njk",
|
||||
"src/site/_includes/layouts/versionednote.njk",
|
||||
"src/site/lunr-index.js",
|
||||
"src/site/_data/versionednotes.js",
|
||||
"src/site/lunr.njk"
|
||||
|
||||
],
|
||||
"filesToAdd": [
|
||||
"src/site/styles/custom-style.scss",
|
||||
".env",
|
||||
"src/site/favicon.svg",
|
||||
"src/site/img/default-note-icon.svg",
|
||||
"src/site/img/tree-1.svg",
|
||||
"src/site/img/tree-2.svg",
|
||||
"src/site/img/tree-3.svg",
|
||||
"src/helpers/userUtils.js",
|
||||
"src/helpers/userSetup.js",
|
||||
"vercel.json",
|
||||
"netlify.toml"
|
||||
],
|
||||
"filesToModify": [
|
||||
".eleventy.js",
|
||||
".eleventyignore",
|
||||
"README.md",
|
||||
"package-lock.json",
|
||||
"package.json",
|
||||
"src/site/404.njk",
|
||||
"src/site/sitemap.njk",
|
||||
"src/site/feed.njk",
|
||||
"src/site/styles/style.scss",
|
||||
"src/site/styles/digital-garden-base.scss",
|
||||
"src/site/styles/obsidian-base.scss",
|
||||
"src/site/notes/notes.json",
|
||||
"src/site/notes/notes.11tydata.js",
|
||||
"src/site/_includes/layouts/note.njk",
|
||||
"src/site/_includes/layouts/index.njk",
|
||||
"src/site/_includes/components/notegrowthhistory.njk",
|
||||
"src/site/_includes/components/pageheader.njk",
|
||||
"src/site/_includes/components/linkPreview.njk",
|
||||
"src/site/_includes/components/references.njk",
|
||||
"src/site/_includes/components/sidebar.njk",
|
||||
"src/site/_includes/components/graphScript.njk",
|
||||
"src/site/_includes/components/filetree.njk",
|
||||
"src/site/_includes/components/filetreeNavbar.njk",
|
||||
"src/site/_includes/components/navbar.njk",
|
||||
"src/site/_includes/components/searchButton.njk",
|
||||
"src/site/_includes/components/searchContainer.njk",
|
||||
"src/site/_includes/components/searchScript.njk",
|
||||
"src/site/_includes/components/calloutScript.njk",
|
||||
"src/site/_includes/components/lucideIcons.njk",
|
||||
"src/site/_includes/components/timestamps.njk",
|
||||
"src/site/_data/meta.js",
|
||||
"src/site/_data/dynamics.js",
|
||||
"src/site/img/outgoing.svg",
|
||||
"src/helpers/constants.js",
|
||||
"src/helpers/utils.js",
|
||||
"src/helpers/filetreeUtils.js",
|
||||
"src/helpers/linkUtils.js",
|
||||
"src/site/get-theme.js",
|
||||
"src/site/_data/eleventyComputed.js",
|
||||
"src/site/graph.njk",
|
||||
"src/site/search-index.njk"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
exports.ALL_NOTE_SETTINGS= [
|
||||
"dgHomeLink",
|
||||
"dgPassFrontmatter",
|
||||
"dgShowBacklinks",
|
||||
"dgShowLocalGraph",
|
||||
"dgShowInlineTitle",
|
||||
"dgShowFileTree",
|
||||
"dgEnableSearch",
|
||||
"dgShowToc",
|
||||
"dgLinkPreview",
|
||||
"dgShowTags"
|
||||
];
|
|
@ -0,0 +1,114 @@
|
|||
const sortTree = (unsorted) => {
|
||||
//Sort by folder before file, then by name
|
||||
const orderedTree = Object.keys(unsorted)
|
||||
.sort((a, b) => {
|
||||
|
||||
let a_pinned = unsorted[a].pinned || false;
|
||||
let b_pinned = unsorted[b].pinned || false;
|
||||
if (a_pinned != b_pinned) {
|
||||
if (a_pinned) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
const a_is_note = a.indexOf(".md") > -1;
|
||||
const b_is_note = b.indexOf(".md") > -1;
|
||||
|
||||
if (a_is_note && !b_is_note) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!a_is_note && b_is_note) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.toLowerCase() > b.toLowerCase()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
})
|
||||
.reduce((obj, key) => {
|
||||
obj[key] = unsorted[key];
|
||||
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
for (const key of Object.keys(orderedTree)) {
|
||||
if (orderedTree[key].isFolder) {
|
||||
orderedTree[key] = sortTree(orderedTree[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return orderedTree;
|
||||
};
|
||||
|
||||
function getPermalinkMeta(note, key) {
|
||||
let permalink = "/";
|
||||
let parts = note.filePathStem.split("/");
|
||||
let name = parts[parts.length - 1];
|
||||
let noteIcon = process.env.NOTE_ICON_DEFAULT;
|
||||
let hide = false;
|
||||
let pinned = false;
|
||||
let folders = null;
|
||||
try {
|
||||
if (note.data.permalink) {
|
||||
permalink = note.data.permalink;
|
||||
}
|
||||
if (note.data.tags && note.data.tags.indexOf("gardenEntry") != -1) {
|
||||
permalink = "/";
|
||||
}
|
||||
if (note.data.title) {
|
||||
name = note.data.title;
|
||||
}
|
||||
if (note.data.noteIcon) {
|
||||
noteIcon = note.data.noteIcon;
|
||||
}
|
||||
// Reason for adding the hide flag instead of removing completely from file tree is to
|
||||
// allow users to use the filetree data elsewhere without the fear of losing any data.
|
||||
if (note.data.hide) {
|
||||
hide = note.data.hide;
|
||||
}
|
||||
if (note.data.pinned) {
|
||||
pinned = note.data.pinned;
|
||||
}
|
||||
if (note.data["dg-path"]) {
|
||||
folders = note.data["dg-path"].split("/");
|
||||
} else {
|
||||
folders = note.filePathStem
|
||||
.split("notes/")[1]
|
||||
.split("/");
|
||||
}
|
||||
folders[folders.length - 1]+= ".md";
|
||||
} catch {
|
||||
//ignore
|
||||
}
|
||||
|
||||
return [{ permalink, name, noteIcon, hide, pinned }, folders];
|
||||
}
|
||||
|
||||
function assignNested(obj, keyPath, value) {
|
||||
lastKeyIndex = keyPath.length - 1;
|
||||
for (var i = 0; i < lastKeyIndex; ++i) {
|
||||
key = keyPath[i];
|
||||
if (!(key in obj)) {
|
||||
obj[key] = { isFolder: true };
|
||||
}
|
||||
obj = obj[key];
|
||||
}
|
||||
obj[keyPath[lastKeyIndex]] = value;
|
||||
}
|
||||
|
||||
function getFileTree(data) {
|
||||
const tree = {};
|
||||
(data.collections.note || []).forEach((note) => {
|
||||
const [meta, folders] = getPermalinkMeta(note);
|
||||
assignNested(tree, folders, { isNote: true, ...meta });
|
||||
});
|
||||
const fileTree = sortTree(tree);
|
||||
return fileTree;
|
||||
}
|
||||
|
||||
exports.getFileTree = getFileTree;
|
|
@ -0,0 +1,100 @@
|
|||
const wikiLinkRegex = /\[\[(.*?\|.*?)\]\]/g;
|
||||
const internalLinkRegex = /href="\/(.*?)"/g;
|
||||
|
||||
function caselessCompare(a, b) {
|
||||
return a.toLowerCase() === b.toLowerCase();
|
||||
}
|
||||
|
||||
function extractLinks(content) {
|
||||
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 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];
|
||||
}
|
||||
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(),
|
||||
noteIcon: v.data.noteIcon || process.env.NOTE_ICON_DEFAULT,
|
||||
hide: v.data.hideInGraph || false,
|
||||
};
|
||||
stemURLs[fpath] = v.url;
|
||||
if (
|
||||
v.data["dg-home"] ||
|
||||
(v.data.tags && v.data.tags.indexOf("gardenEntry") > -1)
|
||||
) {
|
||||
homeAlias = v.url;
|
||||
}
|
||||
});
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
exports.wikiLinkRegex = wikiLinkRegex;
|
||||
exports.internalLinkRegex = internalLinkRegex;
|
||||
exports.extractLinks = extractLinks;
|
||||
exports.getGraph = getGraph;
|
|
@ -0,0 +1,10 @@
|
|||
function userMarkdownSetup(md) {
|
||||
// The md parameter stands for the markdown-it instance used throughout the site generator.
|
||||
// Feel free to add any plugin you want here instead of /.eleventy.js
|
||||
}
|
||||
function userEleventySetup(eleventyConfig) {
|
||||
// The eleventyConfig parameter stands for the the config instantiated in /.eleventy.js.
|
||||
// Feel free to add any plugin you want here instead of /.eleventy.js
|
||||
}
|
||||
exports.userMarkdownSetup = userMarkdownSetup;
|
||||
exports.userEleventySetup = userEleventySetup;
|
|
@ -0,0 +1,7 @@
|
|||
// Put your computations here.
|
||||
|
||||
function userComputed(data) {
|
||||
return {};
|
||||
}
|
||||
|
||||
exports.userComputed = userComputed;
|
|
@ -0,0 +1,51 @@
|
|||
const slugify = require("@sindresorhus/slugify");
|
||||
|
||||
function headerToId(heading) {
|
||||
var slugifiedHeader = slugify(heading);
|
||||
if(!slugifiedHeader){
|
||||
return heading;
|
||||
}
|
||||
return slugifiedHeader;
|
||||
}
|
||||
|
||||
function namedHeadings(md, state) {
|
||||
|
||||
var ids = {}
|
||||
|
||||
state.tokens.forEach(function(token, i) {
|
||||
if (token.type === 'heading_open') {
|
||||
var text = md.renderer.render(state.tokens[i + 1].children, md.options)
|
||||
var id = headerToId(text);
|
||||
var uniqId = uncollide(ids, id)
|
||||
ids[uniqId] = true
|
||||
setAttr(token, 'id', uniqId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function uncollide(ids, id) {
|
||||
if (!ids[id]) return id
|
||||
var i = 1
|
||||
while (ids[id + '-' + i]) { i++ }
|
||||
return id + '-' + i
|
||||
}
|
||||
|
||||
function setAttr(token, attr, value, options) {
|
||||
var idx = token.attrIndex(attr)
|
||||
|
||||
if (idx === -1) {
|
||||
token.attrPush([attr, value])
|
||||
} else if (options && options.append) {
|
||||
token.attrs[idx][1] =
|
||||
token.attrs[idx][1] + ' ' + value
|
||||
} else {
|
||||
token.attrs[idx][1] = value
|
||||
}
|
||||
}
|
||||
|
||||
//https://github.com/rstacruz/markdown-it-named-headings/blob/master/index.js
|
||||
exports.namedHeadingsFilter = function (md, options) {
|
||||
md.core.ruler.push('named_headings', namedHeadings.bind(null, md));
|
||||
}
|
||||
|
||||
exports.headerToId = headerToId;
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Nothing here</title>
|
||||
<link href="/styles/digital-garden-base.css" rel="stylesheet">
|
||||
{%-if meta.themeStyle%}
|
||||
<link href="/styles/obsidian-base.css" rel="stylesheet">
|
||||
<link href="{{meta.themeStyle}}" rel="stylesheet">
|
||||
{% else %}
|
||||
<link href="/styles/style.css" rel="stylesheet">
|
||||
{%endif%}
|
||||
|
||||
<link href="/styles/custom-style.css" rel="stylesheet">
|
||||
|
||||
</head>
|
||||
<body class="theme-{{meta.baseTheme}} markdown-preview-view">
|
||||
<div class="content centered">
|
||||
|
||||
{%-if not meta.themeStyle%}
|
||||
<div class="font-bg"> 😎 </div>
|
||||
{%endif%}
|
||||
<h1>There is nothing here</h1>
|
||||
<p>If you got here from a link, this note is probably not made public</p>
|
||||
<a href="/">Go back home</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,60 @@
|
|||
const fsFileTree = require("fs-file-tree");
|
||||
|
||||
const BASE_PATH = "src/site/_includes/components/user";
|
||||
const STYLE_PATH = "src/site/styles/user";
|
||||
const NAMESPACES = ["index", "notes", "common"];
|
||||
const SLOTS = ["head", "header", "beforeContent", "afterContent", "footer"];
|
||||
const FILE_TREE_NAMESPACE = "filetree";
|
||||
const FILE_TREE_SLOTS = ["beforeTitle", "afterTitle"];
|
||||
const SIDEBAR_NAMESPACE = "sidebar";
|
||||
const SIDEBAR_SLOTS = ["top", "bottom"];
|
||||
const STYLES_NAMESPACE = "styles";
|
||||
|
||||
const generateComponentPaths = async (namespace, slots) => {
|
||||
const data = {};
|
||||
for (let index = 0; index < slots.length; index++) {
|
||||
const slot = slots[index];
|
||||
try {
|
||||
const tree = await fsFileTree(`${BASE_PATH}/${namespace}/${slot}`);
|
||||
let comps = Object.keys(tree)
|
||||
.filter((p) => p.indexOf(".njk") != -1)
|
||||
.map((p) => `components/user/${namespace}/${slot}/${p}`);
|
||||
comps.sort();
|
||||
data[slot] = comps;
|
||||
} catch {
|
||||
data[slot] = [];
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const generateStylesPaths = async () => {
|
||||
try {
|
||||
const tree = await fsFileTree(`${STYLE_PATH}`);
|
||||
let comps = Object.keys(tree).map((p) =>
|
||||
`/styles/user/${p}`.replace(".scss", ".css")
|
||||
);
|
||||
comps.sort();
|
||||
return comps;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = async () => {
|
||||
const data = {};
|
||||
for (let index = 0; index < NAMESPACES.length; index++) {
|
||||
const ns = NAMESPACES[index];
|
||||
data[ns] = await generateComponentPaths(ns, SLOTS);
|
||||
}
|
||||
data[FILE_TREE_NAMESPACE] = await generateComponentPaths(
|
||||
FILE_TREE_NAMESPACE,
|
||||
FILE_TREE_SLOTS
|
||||
);
|
||||
data[SIDEBAR_NAMESPACE] = await generateComponentPaths(
|
||||
SIDEBAR_NAMESPACE,
|
||||
SIDEBAR_SLOTS
|
||||
);
|
||||
data[STYLES_NAMESPACE] = await generateStylesPaths();
|
||||
return data;
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
const { getGraph } = require("../../helpers/linkUtils");
|
||||
const { getFileTree } = require("../../helpers/filetreeUtils");
|
||||
const { userComputed } = require("../../helpers/userUtils");
|
||||
|
||||
module.exports = {
|
||||
graph: (data) => getGraph(data),
|
||||
filetree: (data) => getFileTree(data),
|
||||
userComputed: (data) => userComputed(data)
|
||||
};
|
|
@ -0,0 +1,75 @@
|
|||
require("dotenv").config();
|
||||
const axios = require("axios");
|
||||
const fs = require("fs");
|
||||
const crypto = require("crypto");
|
||||
const { globSync } = require("glob");
|
||||
|
||||
module.exports = async (data) => {
|
||||
let baseUrl = process.env.SITE_BASE_URL || "";
|
||||
if (baseUrl && !baseUrl.startsWith("http")) {
|
||||
baseUrl = "https://" + baseUrl;
|
||||
}
|
||||
let themeStyle = globSync("src/site/styles/_theme.*.css")[0] || "";
|
||||
if (themeStyle) {
|
||||
themeStyle = themeStyle.split("site")[1];
|
||||
}
|
||||
let bodyClasses = [];
|
||||
let noteIconsSettings = {
|
||||
filetree: false,
|
||||
links: false,
|
||||
title: false,
|
||||
default: process.env.NOTE_ICON_DEFAULT,
|
||||
};
|
||||
|
||||
const styleSettingsCss = process.env.STYLE_SETTINGS_CSS || "";
|
||||
|
||||
if (process.env.NOTE_ICON_TITLE && process.env.NOTE_ICON_TITLE == "true") {
|
||||
bodyClasses.push("title-note-icon");
|
||||
noteIconsSettings.title = true;
|
||||
}
|
||||
if (
|
||||
process.env.NOTE_ICON_FILETREE &&
|
||||
process.env.NOTE_ICON_FILETREE == "true"
|
||||
) {
|
||||
bodyClasses.push("filetree-note-icon");
|
||||
noteIconsSettings.filetree = true;
|
||||
}
|
||||
if (
|
||||
process.env.NOTE_ICON_INTERNAL_LINKS &&
|
||||
process.env.NOTE_ICON_INTERNAL_LINKS == "true"
|
||||
) {
|
||||
bodyClasses.push("links-note-icon");
|
||||
noteIconsSettings.links = true;
|
||||
}
|
||||
if (
|
||||
process.env.NOTE_ICON_BACK_LINKS &&
|
||||
process.env.NOTE_ICON_BACK_LINKS == "true"
|
||||
) {
|
||||
bodyClasses.push("backlinks-note-icon");
|
||||
noteIconsSettings.backlinks = true;
|
||||
}
|
||||
if(styleSettingsCss){
|
||||
bodyClasses.push("css-settings-manager");
|
||||
}
|
||||
|
||||
let timestampSettings = {
|
||||
timestampFormat: process.env.TIMESTAMP_FORMAT || "MMM dd, yyyy h:mm a",
|
||||
showCreated: process.env.SHOW_CREATED_TIMESTAMP == "true",
|
||||
showUpdated: process.env.SHOW_UPDATED_TIMESTAMP == "true",
|
||||
};
|
||||
const meta = {
|
||||
env: process.env.ELEVENTY_ENV,
|
||||
theme: process.env.THEME,
|
||||
themeStyle,
|
||||
bodyClasses: bodyClasses.join(" "),
|
||||
noteIconsSettings,
|
||||
timestampSettings,
|
||||
baseTheme: process.env.BASE_THEME || "dark",
|
||||
siteName: process.env.SITE_NAME_HEADER || "Digital Garden",
|
||||
siteBaseUrl: baseUrl,
|
||||
styleSettingsCss,
|
||||
buildDate: new Date(),
|
||||
};
|
||||
|
||||
return meta;
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
<script src="https://cdn.jsdelivr.net/npm/lucide@0.115.0/dist/umd/lucide.min.js"></script>
|
||||
<script>
|
||||
// Create callout icons
|
||||
window.addEventListener("load", () => {
|
||||
document.querySelectorAll(".callout").forEach((elem) => {
|
||||
const icon = getComputedStyle(elem).getPropertyValue('--callout-icon');
|
||||
const iconName = icon && icon.trim().replace(/^lucide-/, "");
|
||||
|
||||
if (iconName) {
|
||||
const calloutTitle = elem.querySelector(".callout-title");
|
||||
|
||||
if (calloutTitle) {
|
||||
const calloutIconContainer = document.createElement("div");
|
||||
const calloutIcon = document.createElement("i");
|
||||
|
||||
calloutIconContainer.appendChild(calloutIcon);
|
||||
calloutIcon.setAttribute("icon-name", iconName);
|
||||
calloutIconContainer.setAttribute("class", "callout-icon");
|
||||
calloutTitle.insertBefore(calloutIconContainer, calloutTitle.firstChild);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
lucide.createIcons();
|
||||
|
||||
// Collapse callouts
|
||||
Array.from(document.querySelectorAll(".callout.is-collapsible")).forEach((elem) => {
|
||||
elem.querySelector('.callout-title').addEventListener("click", (event) => {
|
||||
if (elem.classList.contains("is-collapsed")) {
|
||||
elem.classList.remove("is-collapsed");
|
||||
} else {
|
||||
elem.classList.add("is-collapsed");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,56 @@
|
|||
{% macro menuItem(fileOrFolderName, fileOrFolder, step, currentPath) %}
|
||||
{%if fileOrFolder.isNote or fileOrFolder.isFolder%}
|
||||
<div x-show="isOpen" style="display:none" class="{{'filelist' if step>0}}">
|
||||
{%if fileOrFolder.isNote and not fileOrFolder.hide %}
|
||||
<div @click.stop class="notelink {{ 'active-note' if fileOrFolder.permalink === permalink}}">
|
||||
{%- if not meta.noteIconsSettings.filetree -%}<i icon-name="sticky-note" aria-hidden="true"></i>{%- endif -%}
|
||||
<a data-note-icon="{{fileOrFolder.noteIcon}}" style="text-decoration: none;" class="filename" href="{{fileOrFolder.permalink}}">{{fileOrFolder.name}} </a>
|
||||
</div>
|
||||
{% elif fileOrFolder.isFolder%}
|
||||
<div class="folder inner-folder" x-data="{isOpen: $persist(false).as('{{currentPath}}')}" @click.stop="isOpen=!isOpen">
|
||||
<div class="foldername-wrapper align-icon">
|
||||
<i x-show="isOpen" style="display: none;" icon-name="chevron-down"></i>
|
||||
<i x-show="!isOpen" icon-name="chevron-right"></i>
|
||||
<span class="foldername">{{fileOrFolderName}}</span>
|
||||
</div>
|
||||
{% for fileOrFolderName, child in fileOrFolder %}
|
||||
{{menuItem(fileOrFolderName, child, step+1, (currentPath+"/"+fileOrFolderName))}}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{%endif%}
|
||||
{% endmacro %}
|
||||
|
||||
<div x-init="isDesktop = (window.innerWidth>=1400) ? true: false;"
|
||||
x-on:resize.window="isDesktop = (window.innerWidth>=1400) ? true : false;"
|
||||
x-data="{isDesktop: true, showFilesMobile: false}">
|
||||
|
||||
<div x-show.important="!isDesktop" style="display: none;">
|
||||
{%include "components/filetreeNavbar.njk"%}
|
||||
</div>
|
||||
|
||||
<div x-show="showFilesMobile && !isDesktop" @click="showFilesMobile = false" style="display:none;" class="fullpage-overlay"></div>
|
||||
|
||||
<nav class="filetree-sidebar" x-show.important="isDesktop || showFilesMobile" style="display: none;">
|
||||
{% for imp in dynamics.filetree.beforeTitle %}
|
||||
{% include imp %}
|
||||
{% endfor %}
|
||||
<a href="/" style="text-decoration: none;">
|
||||
<h1 style="text-align:center;">{{meta.siteName}}</h1>
|
||||
</a>
|
||||
{% for imp in dynamics.filetree.afterTitle %}
|
||||
{% include imp %}
|
||||
{% endfor %}
|
||||
{% if settings.dgEnableSearch === true%}
|
||||
<div style="display: flex; justify-content: center;">
|
||||
{% include "components/searchButton.njk" %}
|
||||
</div>
|
||||
{%endif%}
|
||||
<div class="folder" x-data="{isOpen: true}">
|
||||
{%- for fileOrFolderName, fileOrFolder in filetree -%}
|
||||
{{menuItem(fileOrFolderName, fileOrFolder, 0, fileOrFolderName)}}
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
|
@ -0,0 +1,18 @@
|
|||
<nav class="navbar">
|
||||
<div class="navbar-inner">
|
||||
<span style="font-size: 1.5rem; margin-right: 10px;" @click="showFilesMobile=!showFilesMobile"><i icon-name="menu"></i></span>
|
||||
{% for imp in dynamics.filetree.beforeTitle %}
|
||||
{% include imp %}
|
||||
{% endfor %}
|
||||
<a href="/" style="text-decoration: none;">
|
||||
<h1 style="margin: 15px !important;">{{meta.siteName}}</h1>
|
||||
</a>
|
||||
{% for imp in dynamics.filetree.afterTitle %}
|
||||
{% include imp %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if settings.dgEnableSearch === true%}
|
||||
{% include "components/searchButton.njk" %}
|
||||
{%endif%}
|
||||
</nav>
|
|
@ -0,0 +1,214 @@
|
|||
<script>
|
||||
async function fetchGraphData() {
|
||||
const graphData = await fetch('/graph.json').then(res => res.json());
|
||||
const fullGraphData = filterFullGraphData(graphData);
|
||||
return {graphData, fullGraphData}
|
||||
}
|
||||
|
||||
function getNextLevelNeighbours(existing, remaining) {
|
||||
const keys = Object.values(existing).map((n) => n.neighbors).flat();
|
||||
const n_remaining = Object.keys(remaining).reduce((acc, key) => {
|
||||
if (keys.indexOf(key) != -1) {
|
||||
if (!remaining[key].hide) {
|
||||
existing[key] = remaining[key];
|
||||
}
|
||||
} else {
|
||||
acc[key] = remaining[key];
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
return existing, n_remaining;
|
||||
}
|
||||
|
||||
function filterLocalGraphData(graphData, depth) {
|
||||
if (graphData == null) {
|
||||
return null;
|
||||
}
|
||||
let remaining = JSON.parse(JSON.stringify(graphData.nodes));
|
||||
let links = JSON.parse(JSON.stringify(graphData.links));
|
||||
let currentLink = decodeURI(window.location.pathname);
|
||||
let currentNode = remaining[currentLink] || Object.values(remaining).find((v) => v.home);
|
||||
delete remaining[currentNode.url];
|
||||
if (!currentNode.home) {
|
||||
let home = Object.values(remaining).find((v) => v.home);
|
||||
delete remaining[home.url];
|
||||
}
|
||||
currentNode.current = true;
|
||||
let existing = {};
|
||||
existing[currentNode.url] = currentNode;
|
||||
for (let i = 0; i < depth; i++) {
|
||||
existing, remaining = getNextLevelNeighbours(existing, remaining);
|
||||
}
|
||||
nodes = Object.values(existing);
|
||||
if (!currentNode.home) {
|
||||
nodes = nodes.filter(n => !n.home);
|
||||
}
|
||||
let ids = nodes.map((n) => n.id);
|
||||
return {
|
||||
nodes,
|
||||
links: links.filter(function (con) {
|
||||
return ids.indexOf(con.target) > -1 && ids.indexOf(con.source) > -1;
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
function getCssVar(variable) {return getComputedStyle(document.body).getPropertyValue(variable)}
|
||||
|
||||
function htmlDecode(input) {
|
||||
var doc = new DOMParser().parseFromString(input, "text/html");
|
||||
return doc.documentElement.textContent;
|
||||
}
|
||||
|
||||
function renderGraph(graphData, id, delay, fullScreen) {
|
||||
if (graphData == null) {
|
||||
return;
|
||||
}
|
||||
const el = document.getElementById(id);
|
||||
width = el.offsetWidth;
|
||||
height = el.offsetHeight;
|
||||
const highlightNodes = new Set();
|
||||
let hoverNode = null;
|
||||
const color = getCssVar("--graph-main");
|
||||
const mutedColor = getCssVar("--graph-muted");
|
||||
let Graph = ForceGraph()
|
||||
(el)
|
||||
.graphData(graphData)
|
||||
.nodeId('id')
|
||||
.nodeLabel('title')
|
||||
.linkSource('source')
|
||||
.linkTarget('target')
|
||||
.d3AlphaDecay(0.10)
|
||||
.width(width)
|
||||
.height(height)
|
||||
.linkDirectionalArrowLength(2)
|
||||
.linkDirectionalArrowRelPos(0.5)
|
||||
.autoPauseRedraw(false)
|
||||
.linkColor((link) => {
|
||||
if (hoverNode == null) {
|
||||
return color;
|
||||
}
|
||||
if (link.source.id == hoverNode.id || link.target.id == hoverNode.id) {
|
||||
return color;
|
||||
} else {
|
||||
return mutedColor;
|
||||
}
|
||||
|
||||
})
|
||||
.nodeCanvasObject((node, ctx) => {
|
||||
const numberOfNeighbours = (node.neighbors && node.neighbors.length) || 2;
|
||||
const nodeR = Math.min(7, Math.max(numberOfNeighbours / 2, 2));
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, nodeR, 0, 2 * Math.PI, false);
|
||||
if (hoverNode == null) {
|
||||
ctx.fillStyle = color;
|
||||
} else {
|
||||
if (node == hoverNode || highlightNodes.has(node.url)) {
|
||||
ctx.fillStyle = color;
|
||||
} else {
|
||||
ctx.fillStyle = mutedColor;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.fill();
|
||||
|
||||
if (node.current) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, nodeR + 1, 0, 2 * Math.PI, false);
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
const label = htmlDecode(node.title)
|
||||
const fontSize = 3.5;
|
||||
ctx.font = `${fontSize}px Sans-Serif`;
|
||||
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(label, node.x, node.y + nodeR + 2);
|
||||
})
|
||||
.onNodeClick(node => {
|
||||
window.location = node.url;
|
||||
})
|
||||
.onNodeHover(node => {
|
||||
highlightNodes.clear();
|
||||
if (node) {
|
||||
highlightNodes.add(node);
|
||||
node.neighbors.forEach(neighbor => highlightNodes.add(neighbor));
|
||||
}
|
||||
hoverNode = node || null;
|
||||
|
||||
});
|
||||
if (fullScreen || (delay != null && graphData.nodes.length > 4)) {
|
||||
setTimeout(() => {
|
||||
Graph.zoomToFit(5, 75);
|
||||
}, delay || 200);
|
||||
}
|
||||
return Graph;
|
||||
}
|
||||
|
||||
function renderLocalGraph(graphData, depth, fullScreen) {
|
||||
if (window.graph){
|
||||
window.graph._destructor();
|
||||
}
|
||||
const data = filterLocalGraphData(graphData, depth);
|
||||
return renderGraph(data, 'link-graph', null, fullScreen);
|
||||
}
|
||||
|
||||
function filterFullGraphData(graphData) {
|
||||
if (graphData == null) {
|
||||
return null;
|
||||
}
|
||||
graphData = JSON.parse(JSON.stringify(graphData));
|
||||
const hiddens = Object.values(graphData.nodes).filter((n) => n.hide).map((n) => n.id);
|
||||
const data = {
|
||||
links: JSON.parse(JSON.stringify(graphData.links)).filter((l) => hiddens.indexOf(l.source) == -1 && hiddens.indexOf(l.target) == -1),
|
||||
nodes: [...Object.values(graphData.nodes).filter((n) => !n.hide)]
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
function openFullGraph(fullGraphData) {
|
||||
lucide.createIcons({
|
||||
attrs: {
|
||||
class: ["svg-icon"]
|
||||
}
|
||||
});
|
||||
return renderGraph(fullGraphData, "full-graph-container", 200, false);;
|
||||
}
|
||||
|
||||
function closefullGraph(fullGraph) {
|
||||
if (fullGraph) {
|
||||
fullGraph._destructor();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
<div x-init="{graphData, fullGraphData} = await fetchGraphData();" x-data="{ graphData: null, depth: 1, graph: null, fullGraph: null, showFullGraph: false, fullScreen: false, fullGraphData: null}" id="graph-component" x-bind:class="fullScreen ? 'graph graph-fs' : 'graph'" v-scope>
|
||||
<div class="graph-title-container">
|
||||
<div class="graph-title">Connected Pages</div>
|
||||
<div id="graph-controls">
|
||||
<div class="depth-control">
|
||||
<label for="graph-depth">Depth</label>
|
||||
<div class="slider">
|
||||
<input x-model.number="depth" name="graph-depth" list="depthmarkers" type="range" step="1" min="1" max="3" id="graph-depth"/>
|
||||
<datalist id="depthmarkers">
|
||||
<option value="1" label="1"></option>
|
||||
<option value="2" label="2"></option>
|
||||
<option value="3" label="3"></option>
|
||||
</datalist>
|
||||
</div>
|
||||
<span id="depth-display" x-text="depth"></span>
|
||||
</div>
|
||||
<div class="ctrl-right">
|
||||
<span id="global-graph-btn" x-on:click="showFullGraph = true; setTimeout(() => {fullGraph = openFullGraph(fullGraphData)}, 100)"><i icon-name="globe" aria-hidden="true"></i></span>
|
||||
<span id="graph-fs-btn" x-on:click="fullScreen = !fullScreen"><i icon-name="expand" aria-hidden="true"></i></span>
|
||||