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/  
#!/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!

comments powered by Disqus