NESTOR
The following python script transforms notes into dynamic nodes within a digital mind map. Primarily designed for a daily notes outliner workflow (Roam Research like) in Obsidian, Nestor provides a simple, automated way to organize and interconnect ideas recorded in Markdown notes, based on their indentation level and bi-directional links. By doing so, it transforms a flat collection of notes into a structured, easily navigable "mind map" of interlinked ideas, turning the notes into a dynamic knowledge base easily navigable in the graph view.
Basically, it allows you to link your notes together simply by indenting them in a bullet list as follows :
- If you start writing about [[Note 1]]
- Then if you indent [[Note 2]] under it.
- And [[Note 3]] & [[Note 4]] under them.
- But you leave out [[Note 5]] of the indentation.
Then [[Note 1]] will be the parent of [[Note 2]], [[Note 3]] & [[Note 4]] while them will be it's children.
[[Note 2]] will be the child of [[Note 2]] & the parent of [[Note 3]] & [[Note 4]].
[[Note 3]] & [[Note 4]] will be siblings and have as parents both [[Note 2]] & [[Note 1]].
[[Note 5]] will have no relations.
For exemple here is the output written on top of [[Note 1]] :
%%
Parent::
Sibling::
Child:: [[Note 2]] - [[Note 4]] - [[Note 3]]
%%
---
import os
import re
import subprocess
from collections import defaultdict
directory = 'PATH_TO_YOUR_OBISIDAN_VAULT'
exclude_dirs = ['PATH_TO_EXCLUDED_DIRECTORY_1',
'PATH_TO_EXCLUDED_DIRECTORY_2']
link_regex = re.compile(r'\[\[(.*?)\]\]')
relation_regex = re.compile(r'(Parent|Sibling|Child):: (.*?)\n')
parent_notes = defaultdict(set)
child_notes = defaultdict(set)
sibling_notes = defaultdict(set)
# FIRST LOOP: Process all files and clean up existing relation lines.
for root, dirs, files in os.walk(directory):
for filename in files:
filepath = os.path.join(root, filename)
if filename.endswith('.md'):
with open(filepath, 'r') as file:
content = file.readlines()
new_content = []
for line in content:
if not (line.startswith(('Parent::', 'Child::', 'Sibling::')) or line.strip() == '---'):
if line.strip() == '' and (not new_content or new_content[-1].strip() != ''):
new_content.append(line)
elif line.strip() != '':
new_content.append(line.strip('%%'))
with open(filepath, 'w') as file:
file.writelines(new_content)
# SECOND LOOP: Parse relationships and build new relationship maps.
for root, dirs, files in os.walk(directory):
for filename in files:
filepath = os.path.join(root, filename)
if filename.endswith('.md'):
with open(filepath, 'r') as file:
content = file.readlines()
if content:
previous_links = [[] for _ in range(len(content))]
relations = relation_regex.findall(''.join(content))
for relation in relations:
if relation[0] == 'Parent':
parent_notes[filename[:-3]].update(relation[1].split(' - '))
elif relation[0] == 'Child':
child_notes[filename[:-3]].update(relation[1].split(' - '))
elif relation[0] == 'Sibling' and line.strip().startswith('-'):
sibling_notes[filename[:-3]].update(relation[1].split(' - '))
# THIRD LOOP: Use new relationship maps to create updated content, and write it back to the files.
for root, dirs, files in os.walk(directory):
for filename in files:
filepath = os.path.join(root, filename)
if filename.endswith('.md'):
with open(filepath, 'r') as file:
content = file.readlines()
new_content = []
for line in content:
if not line.startswith(('Parent::', 'Child::', 'Sibling::')):
new_content.append(line)
parent_links = ' - '.join([f'[[{note}]]' for note in parent_notes[filename[:-3]]])
sibling_links = ' - '.join([f'[[{note}]]' for note in sibling_notes[filename[:-3]]])
child_links = ' - '.join([f'[[{note}]]' for note in child_notes[filename[:-3]]])
new_content = ["%%\n"
f"Parent:: {parent_links}\n",
f"Sibling:: {sibling_links}\n",
f"Child:: {child_links}\n",
"%%\n",
"\n",
"---\n"] + new_content
if root not in exclude_dirs:
with open(filepath, 'w') as file:
file.writelines(new_content)
title = "Script Notification"
text = "Nestor is done!"
sound = "/System/Library/Sounds/Glass.aiff"
subprocess.run(['osascript', '-e', f'display notification "{text}" with title "{title}"'])
subprocess.run(['afplay', sound])
CODE
WARNING
The script will modify all the markdown files in your vault. Make sure to back-up your vault before using it.
Also, the script doesn’t recognize YAML frontmatter yet, so it will currently NOT work with it and will actually erase it or make unusable because it is designed to output the relationships on top of the files. I’m trying to figure out a better way but for now that’s the way it is.
Collab
If your interested in helping me turn this into a plugin or if you have any suggestions please reach out via the Contact Page.
Disable Nesting Your Ideas Script
This script will remove all the nesting from your files.
Keep in mind that this script does not restore any other changes that might have been made to your .md files outside of adding these specific lines. It is always a good practice to keep a backup of your original files before running scripts that modify them, just in case you need to revert any changes.
import os
# Import the os module to work with files and directories
directory = 'YOUR_VAULT_DIRECTORY_HERE' # Replace with your vault directory
# Define the directory where your vault files are stored
for root, dirs, files in os.walk(directory):
# Loop through all the files and subdirectories in the vault directory
for filename in files:
# Loop through each file in the current subdirectory
filepath = os.path.join(root, filename)
# Get the full path of the file
if filename.endswith('.md'):
# Check if the file is a markdown file
with open(filepath, 'r') as file:
# Open the file in read mode
content = file.readlines()
# Read all the lines of the file into a list
new_content = []
# Create an empty list to store the new content
for line in content:
# Loop through each line in the original content
if not (line.startswith(('Parent::', 'Child::', 'Sibling::')) or line.strip() == '---'):
# Check if the line is not a parent, child or sibling link or a separator
new_content.append(line)
# Add the line to the new content list
# Write the cleaned-up content back into the file
with open(filepath, 'w') as file:
# Open the file in write mode
file.writelines(new_content)
# Write the new content list into the file