Obsidian as editor of off-vault markdown notes: obseditor in GitHub

Borrowing some ideas from the script here, I have made some minor modifications for use in Linux. I use CachyOS with GNOME,and Nemo as File Explorer. Thanks @ bvdg

This is the script:

#!/usr/bin/env bash
# Obsieditor.sh
# Shell script that makes Obsidian act as a markdown editor for files
# outside a typical vault. 
# Based on this macOS script: 
#     https://forum.obsidian.md/t/have-obsidian-be-the-handler-of-md-files-add-ability-to-use-obsidian-as-a-markdown-editor-on-files-outside-vault-file-association/314/155?u=msfz751

# CONFIGURATION
# create a default vault with all JS goodies and plugins vault_where_files_must_be_opened="$HOME/obsidian/default/"
subtrees_that_must_be_mirrored_in_vault=(
)


# Get vault names from obsidian.json
all_vaults=$(awk -F':|,|{|}|"' '{for(i=1;i<=NF;i++)if($i=="path")print$(i+3)}'\
   <"$HOME/.config/obsidian/obsidian.json")

default_vault="$(readlink -f "$vault_where_files_must_be_opened")" || \
    default_vault="$(readlink "$vault_where_files_must_be_opened")" || \
    default_vault="$(sed -E 's/.*"path":"([^"]+)",.*"open":true.*/\1/'\
   <"$HOME/.config/obsidian/obsidian.json")"  # currently active vault



get_linked_files() {
	# create symlinks to support files, like images, PDF files to which the Markdown file links
	md_dir="$(dirname "$1")"
	
	# Use mapfile to read all links into an array. 
	# 1st line for links of the kind ![image](assets/note_name/image.png)
	# 2nd line for links <img src="assets/note_name/image.png" alt="image" style="zoom:80%;" />
    mapfile -t links < <(sed -En -e 's/.*!*\[[^]]*\]\(([^)]+)\).*/\1/p' \
                            -e 's/.*<img[^>]* src="([^?"]+)("|\?).*/\1/p' "$1")
	
	for linktext in "${links[@]}"
	do
		# URL decode the path (handle %20 as spaces, %2F, etc.)
		decoded_link=$(printf '%b' "${linktext//%/\\x}")
		
		linked_file="$(readlink -f "$md_dir/$decoded_link" 2>/dev/null || \
			readlink "$md_dir/$decoded_link" 2>/dev/null)"
		
		# is it really a local file, and not higher up in the file tree?
		if [[ -n "$linked_file" && "$linked_file" == "$md_dir"* ]]
		then
			link_dir=$2
			# create subdirs if needed
			abs_dir="$(dirname "$linked_file")"
			if [[ "$md_dir" != "$abs_dir" ]]
			then
				link_dir="$link_dir${abs_dir#$md_dir}"
				mkdir -p "$link_dir"
			fi
			linkpath="$link_dir/$(basename "$linked_file")"
			[[ ! -e "$linkpath" ]] && ln -s "$linked_file" "$linkpath"
		fi
	done
}


open_file() {
	# Thanks, https://stackoverflow.com/a/75300235/7840347
	url_encoded="$(perl -e 'use URI; print URI->new("'"$1"'");')"
	if [[ -z $url_encoded ]]; then url_encoded="$1"; fi   # in case perl returns nothing
	open "obsidian://open?path=$url_encoded"
}


for file in "$@"
do

    # check for existence and readability
	if [[ (! -f "$file") || (! -r "$file") ]]
	then
		logger "OPEN-IN-OBSIDIAN warning: No readable file $file"
		continue
	fi
	abspath=$(readlink -f "$file" || readlink "$file")  # on macOS 10.15 -f is not allowed

	# 1. If the file is inside any vault (in place or linked), just open it
	for v in "${all_vaults[@]}"
	do
		foundpath="$(find -L "$v" -samefile "$abspath" -and ! -path "*/.trash/*")"
		if [[ $foundpath ]]
		then
			open_file "$foundpath"
			continue 2  # next input file
		fi
	done

	# 2. If the file is in one of the folders that should be mirrored,
	#    replicate the folder's internal directory chain in the vault
	#    and put a link to the file in it; then open that
	for asset_folder in "${subtrees_that_must_be_mirrored_in_vault[@]}"
	do
        linkpath="$default_vault/$(basename "$asset_folder")${abspath#$asset_folder}"
		if [[ "$abspath" == "$asset_folder"* ]]
		then
			linkpath="$default_vault/$(basename "$asset_folder")${abspath#$asset_folder}"
			mkdir -p "$(dirname "$linkpath")"
			ln -s "$abspath" "$linkpath"			
			get_linked_files "$abspath" "$(dirname "$linkpath")"
			sleep 1  # delay for Obsidian to notice the new file(s)
			open_file "$linkpath"
			continue 2
		fi
	done    
	
	# 3. In other cases, replicate the note's FULL directory structure in the vault
	# Remove leading / from absolute path
	relative_path="${abspath#/}"

	linkpath="$default_vault/$relative_path"
	mkdir -p "$(dirname "$linkpath")"

	# If the exact symlink already exists, just open it
	if [[ -L "$linkpath" && "$(readlink -f "$linkpath" 2>/dev/null || readlink "$linkpath")" == "$abspath" ]]; then
		open_file "$linkpath"
		continue
	fi

	# If something else exists at this location, create a unique name
	while [[ -e "$linkpath" ]]
	do
		linkpath="${linkpath%.*}_$RANDOM.${linkpath##*.}"
	done

	ln -s "$abspath" "$linkpath"
	get_linked_files "$abspath" "$(dirname "$linkpath")"
	sleep 1
	open_file "$linkpath" 
done

I copy this script to $HOME/.local/share/nemo/scripts. You could do the same in Nautilus. Then, I just select the markdown note that I want to edit in Obsidian from the context menu, and I get the note and its attached files all in an Obsidian vault. I created a dummy vault for this purpose - I called it “default”, although you can bring off-vault notes to the vault of your preference. You set that in vault_where_files_must_be_opened variable at the top.

The trick is creating symbolic links of the original .md file and its linked resources, such as images, PDF files, etc. What you have in Obsidian are all symbolic links. Once you finish your editing (text wrangling, formatting, tables, etc.), you could delete the files from the vault. The original files will persists since they are just inodes.

The main change I made on the original script is that instead of creating a Temp folder inside the vault for the outside notes, I recreate the original folder structure where the note was residing. So, it would look to something like this:

I found that this folder structure is more informative of the note, or notes, I am dealing with, while providing context of location.

The script is ready for selection of multiple MD files; it will loop for file in "$@" through every markdown note in the file explorer creating all the necessary symbolic links.

I know it is not a perfect solution but it works. It can serve other purposes as well, such as note importer, or modified, to be a CLI note editor.

Enjoy!

Notes

  1. I usually start markdown notes with Typora with some bare content. Then, I use this script to bring it to Obsidian, to edit it with Obsidian superpowers: plugins, custom JavaScript scripts, advanced tables, formatting, etc. For instance, Typora only supports live embedding for images, while Obsidian natively supports embedding and viewing PDF files.
  2. To make the transition of images (links) to Obsidian set your Typora preferences to use “relative paths”

I just published the code and template in GitHub:

Now includes a Templater template that makes easier to synchronize changes between Obsidian and Typora (or whatever the markdown editor you use). The template addresses several issues that are common when working simultaneously with Obsidian and an external editor:

  • Spaces in image links and resources (PDF documents). The template will convert the spaces to “%20” so the file and path can be understood in the external editor
  • Conversion of symbolic links to files when editing is completing
  • Create and modify object simultaneously in Obsidian and an external editor
  • Moving images and PDF files to the assets subfolder created by the external editor

I have assigned the template to a hotkey such as Alt+U, which will execute several tasks on the current note. The more laborious part was synchronizing changes made to the file in Obsidian with those that are made in the external editor.

I have also added to the bash script the capability of creating or opening markdown files from the terminal. It works with one file or multiple files.

1 Like