Quick and Dirty Notable to Obsidian Conversion Script

I’ve been a long time user of Notable, which I still think is a great piece of open source software. Using Notable as more of a live wiki where I link between and organize sets of documents is where I find it lacking. Recently I’ve been toying around with Obsidian and finally decided to convert everything over. Obsidian markets itself as *”a powerful knowledge base on top of

a local folder of plain text Markdown files”*, and so far I’m really enjoying it.

There are conversion/importers for many Markdown-like softwares, but I was not able to find one for Notable. So I threw together a quick and dirty version that works for my needs.

Hopefully this is useful to someone else out there! You can run it like such: node import.js /path/to/notable/notes /path/to/obsidian/my_vault/. The source is below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#!/usr/bin/env node

const path = require('path');
const fs = require('fs').promises;

async function readNotable(notableDir, results = new Map()) {
//
// Read Notable entries, extract some metadata, and cache
//
let files = await fs.readdir(notableDir, { withFileTypes : true } );
for (let f of files) {
let fullPath = path.join(notableDir, f.name);
if (f.isDirectory()) {
await readNotable(fullPath, results);
} else {
console.info(`* Caching ${fullPath}...`);

const data = await fs.readFile(fullPath, 'utf-8' );

const metaRe = /---((?:.|[\r\n])*)---/g;

let title;
let tags = [];
let match = metaRe.exec(data);
if (match) {
const metadata = match[1];

const titleRe = /title: ([^\n]+)/;
match = metadata.match(titleRe);
title = match && match[1];

const tagsRe = /tags: \[([^\]]+)\]\n/;
match = metadata.match(tagsRe);
if (match) {
tags = match[1].split(',').map(s => s.trim());
}
}

const entry = {
fullPath,
data,
title,
tags,
};

results.set(title, entry);
}
}
return results;
}

async function migrateTo(entriesCache, notableDir, obsidianVaultDir) {
//
// Re-read Notable entries, converting to Obsidian
// - Internal links of [Title](@note/foo.md) -> [[Title]]
//
for (const [title, entry] of entriesCache) {
// Example:
// From: c:\path\to\notable\notes\sub\foo.md
// To: c:\path\to\obsidian\myVault\sub\foo.md
const vaultRelPath = entry.fullPath.replace(notableDir, '');
let vaultFullPath = path.dirname(path.join(obsidianVaultDir, vaultRelPath));
vaultFullPath = path.join(vaultFullPath, `${path.basename(entry.fullPath)}`);

console.info(`* Processing "${title}" @ ${entry.fullPath} -> ${vaultFullPath}...`);

const noteRe = /\[([\w\s]+)\]\(@note\/([\w\s\/\-_\+\.\:]+)\.md\)/g;
let newData = entry.data;
let match;
while ((match = noteRe.exec(entry.data))) {
const base = path.dirname(vaultRelPath).substr(1);
let newLink = path.join(base, path.basename(match[2], '.md'));
const linkTitle = match[1] === newLink ? '' : `\\|${match[1]}`;
newLink = `[[${newLink}${linkTitle}]]`;
console.log(` > Internal link: -> ${newLink}`);
newData = newData.replace(match[0], newLink);
console.log(match[0])
}

await fs.mkdir(path.dirname(vaultFullPath), { recursive: true } );
await fs.writeFile(vaultFullPath, newData, { encoding : 'utf-8'} );
}
}

async function validDirectories(paths) {
for (const path of paths) {
if (!path) {
return false;
}

const stats = await fs.stat(path);
if (!stats.isDirectory()) {
return false;
}
}

return true;
}

async function main() {
//
// This isn't the most optimized approach in the world,
// but this should be a one time migrate, so oh well...
//
const notableDir = process.argv[2];
const obsidianVaultDir = process.argv[3];

const validDirs = await validDirectories([notableDir, obsidianVaultDir]);
if (!validDirs) {
return console.error('Usage: notable_to_obsidian.js <notableDir> <vaultDir>');
}

const entriesCache = await readNotable(process.argv[2])
await migrateTo(entriesCache, notableDir, obsidianVaultDir);

return 0;
}

main().then().catch(console.error);

Enjoy!