Initial commit

Created from https://vercel.com/new
pull/1/head
Shwetha Jayaraj 2023-08-15 19:32:53 +00:00
commit 7988eb4033
55 changed files with 19610 additions and 0 deletions

500
.eleventy.js 100644
View File

@ -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 dont 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("&amp;", "&");
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,
};
};

1
.eleventyignore 100644
View File

@ -0,0 +1 @@
netlify/functions

4
.env 100644
View File

@ -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

15
.github/dependabot.yml vendored 100644
View File

@ -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.

13
.gitignore vendored 100644
View File

@ -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

9
README.md 100644
View File

@ -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/)

14
netlify.toml 100644
View File

@ -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

5360
package-lock.json generated 100644

File diff suppressed because it is too large Load Diff

46
package.json 100644
View File

@ -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"
}
}

72
plugin-info.json 100644
View File

@ -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"
]
}

View File

@ -0,0 +1,12 @@
exports.ALL_NOTE_SETTINGS= [
"dgHomeLink",
"dgPassFrontmatter",
"dgShowBacklinks",
"dgShowLocalGraph",
"dgShowInlineTitle",
"dgShowFileTree",
"dgEnableSearch",
"dgShowToc",
"dgLinkPreview",
"dgShowTags"
];

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,7 @@
// Put your computations here.
function userComputed(data) {
return {};
}
exports.userComputed = userComputed;

View File

@ -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;

28
src/site/404.njk 100644
View File

@ -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"> &#x1F60E; </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>

View File

@ -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;
};

View File

@ -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)
};

View File

@ -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;
};

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>