diff --git a/.gitignore b/.gitignore
index 1a35b77..54f682b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,4 @@ vendor/
# macOS
.DS_Store
+/_includes/notes_graph.json
diff --git a/_notes/test.md b/_notes/test.md
index c3f2bd7..d1e1dd7 100644
--- a/_notes/test.md
+++ b/_notes/test.md
@@ -4,3 +4,5 @@ layout: default
---
Hello World
+
+==This is some highlighted text==
diff --git a/_plugins/bidirectional_links_generator.rb b/_plugins/bidirectional_links_generator.rb
new file mode 100644
index 0000000..78184b5
--- /dev/null
+++ b/_plugins/bidirectional_links_generator.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+class BidirectionalLinksGenerator < Jekyll::Generator
+ def generate(site)
+ graph_nodes = []
+ graph_edges = []
+
+ all_notes = site.collections['notes'].docs
+ all_pages = site.pages
+
+ all_docs = all_notes + all_pages
+
+ link_extension = !!site.config["use_html_extension"] ? '.html' : ''
+
+ # Convert all Wiki/Roam-style double-bracket link syntax to plain HTML
+ # anchor tag elements () with "internal-link" CSS class
+ all_docs.each do |current_note|
+ all_docs.each do |note_potentially_linked_to|
+ note_title_regexp_pattern = Regexp.escape(
+ File.basename(
+ note_potentially_linked_to.basename,
+ File.extname(note_potentially_linked_to.basename)
+ )
+ ).gsub('\_', '[ _]').gsub('\-', '[ -]').capitalize
+
+ title_from_data = note_potentially_linked_to.data['title']
+ if title_from_data
+ title_from_data = Regexp.escape(title_from_data)
+ end
+
+ new_href = "#{site.baseurl}#{note_potentially_linked_to.url}#{link_extension}"
+ anchor_tag = "\\1"
+
+ # Replace double-bracketed links with label using note title
+ # [[A note about cats|this is a link to the note about cats]]
+ current_note.content.gsub!(
+ /\[\[#{note_title_regexp_pattern}\|(.+?)(?=\])\]\]/i,
+ anchor_tag
+ )
+
+ # Replace double-bracketed links with label using note filename
+ # [[cats|this is a link to the note about cats]]
+ current_note.content.gsub!(
+ /\[\[#{title_from_data}\|(.+?)(?=\])\]\]/i,
+ anchor_tag
+ )
+
+ # Replace double-bracketed links using note title
+ # [[a note about cats]]
+ current_note.content.gsub!(
+ /\[\[(#{title_from_data})\]\]/i,
+ anchor_tag
+ )
+
+ # Replace double-bracketed links using note filename
+ # [[cats]]
+ current_note.content.gsub!(
+ /\[\[(#{note_title_regexp_pattern})\]\]/i,
+ anchor_tag
+ )
+ end
+
+ # At this point, all remaining double-bracket-wrapped words are
+ # pointing to non-existing pages, so let's turn them into disabled
+ # links by greying them out and changing the cursor
+ current_note.content = current_note.content.gsub(
+ /\[\[([^\]]+)\]\]/i, # match on the remaining double-bracket links
+ <<~HTML.delete("\n") # replace with this HTML (\\1 is what was inside the brackets)
+
+ [[
+ \\1
+ ]]
+ HTML
+ )
+ end
+
+ # Identify note backlinks and add them to each note
+ all_notes.each do |current_note|
+ # Nodes: Jekyll
+ notes_linking_to_current_note = all_notes.filter do |e|
+ e.url != current_note.url && e.content.include?(current_note.url)
+ end
+
+ # Nodes: Graph
+ graph_nodes << {
+ id: note_id_from_note(current_note),
+ path: "#{site.baseurl}#{current_note.url}#{link_extension}",
+ label: current_note.data['title'],
+ } unless current_note.path.include?('_notes/index.html')
+
+ # Edges: Jekyll
+ current_note.data['backlinks'] = notes_linking_to_current_note
+
+ # Edges: Graph
+ notes_linking_to_current_note.each do |n|
+ graph_edges << {
+ source: note_id_from_note(n),
+ target: note_id_from_note(current_note),
+ }
+ end
+ end
+
+ File.write('_includes/notes_graph.json', JSON.dump({
+ edges: graph_edges,
+ nodes: graph_nodes,
+ }))
+ end
+
+ def note_id_from_note(note)
+ note.data['title'].bytes.join
+ end
+end
diff --git a/_plugins/empty_front_matter_note_injector.rb b/_plugins/empty_front_matter_note_injector.rb
new file mode 100644
index 0000000..83fd4b2
--- /dev/null
+++ b/_plugins/empty_front_matter_note_injector.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+EMPTY_FRONT_MATTER = <<~JEKYLL
+ ---
+ ---
+
+JEKYLL
+
+# Inject empty front matter in notes that don't have any
+Jekyll::Hooks.register :site, :after_init do |site|
+ Dir.glob(site.collections['notes'].relative_directory + '/**/*.md').each do |filename|
+ raw_note_content = File.read(filename)
+ unless raw_note_content.start_with?('---')
+ raw_note_content.prepend(EMPTY_FRONT_MATTER)
+ File.write(filename, raw_note_content)
+ end
+ end
+end
diff --git a/_plugins/last_modified_at_generator.rb b/_plugins/last_modified_at_generator.rb
new file mode 100644
index 0000000..ea9b1f4
--- /dev/null
+++ b/_plugins/last_modified_at_generator.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'fileutils'
+require 'pathname'
+require 'jekyll-last-modified-at'
+
+module Recents
+ # Generate change information for all markdown pages
+ class Generator < Jekyll::Generator
+ def generate(site)
+ items = site.collections['notes'].docs
+ items.each do |page|
+ timestamp = Jekyll::LastModifiedAt::Determinator.new(site.source, page.path, '%FT%T%:z').to_s
+ page.data['last_modified_at_timestamp'] = timestamp
+ end
+ end
+ end
+end
diff --git a/_plugins/markdown-highlighter.rb b/_plugins/markdown-highlighter.rb
new file mode 100644
index 0000000..2ef933c
--- /dev/null
+++ b/_plugins/markdown-highlighter.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+# Turns ==something== in Markdown to something in output HTML
+
+Jekyll::Hooks.register [:notes], :pre_render do |doc|
+ replace(doc)
+end
+
+Jekyll::Hooks.register [:pages], :pre_render do |doc|
+ # jekyll considers anything at the root as a page,
+ # we only want to consider actual pages
+ next unless doc.path.start_with?('_pages/')
+ replace(doc)
+end
+
+def replace(doc)
+ doc.content.gsub!(/==+([^ ](.*?)?[^ .=])==+/, "\\1")
+end
diff --git a/_plugins/open_external_links_in_new_tab.rb b/_plugins/open_external_links_in_new_tab.rb
new file mode 100644
index 0000000..8f966c5
--- /dev/null
+++ b/_plugins/open_external_links_in_new_tab.rb
@@ -0,0 +1,28 @@
+# If the configuration sets `open_external_links_in_new_tab` to a truthy value,
+# add 'target=_blank' to anchor tags that don't have `internal-link` class
+
+# frozen_string_literal: true
+require 'nokogiri'
+
+Jekyll::Hooks.register [:notes], :post_convert do |doc|
+ convert_links(doc)
+end
+
+Jekyll::Hooks.register [:pages], :post_convert do |doc|
+ # jekyll considers anything at the root as a page,
+ # we only want to consider actual pages
+ next unless doc.path.start_with?('_pages/')
+ convert_links(doc)
+end
+
+def convert_links(doc)
+ open_external_links_in_new_tab = !!doc.site.config["open_external_links_in_new_tab"]
+
+ if open_external_links_in_new_tab
+ parsed_doc = Nokogiri::HTML::DocumentFragment.parse(doc.content)
+ parsed_doc.css("a:not(.internal-link):not(.footnote):not(.reversefootnote)").each do |link|
+ link.set_attribute('target', '_blank')
+ end
+ doc.content = parsed_doc.inner_html
+ end
+end