Evernote is my note-taking application by choice, I have used it for many years and have never really had an issue with it. I’m not the biggest fan of the latest UI but the option’s there to use the version I’m more familiar with, if need be.
Over the years I have massed thousands of notes across multiple notebooks. Notes from near every stage of my programming career. How-tos, what-not-to-dos, explanations, ramblings, ideas and problems.
If I were to lose my notes, I’d be devastated. Endless hours of nostalgia-fuelled reading at the click of a button.
Why Markdown?
I love writing in Markdown. My only gripe with Evernote is that you can’t write in Markdown. I even built my own Markdown note-taking application, to try soothe this pain. As with many side-projects though, I lost interest. My fickle brain — much more suited to the haphazardness of blogging — made the minimum viable product and then found something more interesting.
The goal
My goal is to convert my existing notes to markdown and then have them stored in a private repository on GitHub.
For two reasons:
- Have a backup, you never know, Evernote might go out of business.
- Have the notes stored in a universally accepted format, to make potential transitioning simple
Yarle
Yarle, or “Yet another rope ladder from Evernote” can do a lot of the heavy lifting for us. It can perform the conversion from the .enex files to markdown — no need to reinvent the wheel. Yarle is mostly a fancy UI where you can select your .enex files and it’ll spit out the markdown. I want to leverage this in code so that I can simply run a script.
Using npx I can use the power of Yarle without installing the package:
npx -p yarle-evernote-to-md@latest yarle --configFile ./config.json
My Yarle configuration file looks like this:
{
"enexSources": [
"C:\\Users\\lukeg\\Desktop\\MarkdownNotes\\AllNotebooks\\Angular.enex"
],
"outputDir": "C:\\Users\\lukeg\\Desktop\\MarkdownNotes\\Markdown",
"isZettelkastenNeeded": false,
"plainTextNotesOnly": false,
"skipWebClips": false,
"useHashTags": true,
"outputFormat": "StandardMD",
"urlEncodeFileNamesAndLinks": false,
"skipEnexFileNameFromOutputPath": false,
"monospaceIsCodeBlock": false,
"keepMDCharactersOfENNotes": false,
"keepOriginalAmountOfNewlines": false,
"addExtensionToInternalLinks": true,
"nestedTags": {
"separatorInEN": "_",
"replaceSeparatorWith": "/",
"replaceSpaceWith": "-"
},
"resourcesDir": "resources",
"turndownOptions": {
"headingStyle": "atx"
},
"dateFormat": "YYYY-MM-DD",
"haveEnexLevelResources": true,
"haveGlobalResources": false,
"logseqSettings":{
"journalNotes": false
},
"obsidianSettings": {
"omitLinkDisplayName": false
}
}
Running this puts the notes in the output directory specified.
The plan
When you export notebooks into .enex files, all the notes are placed within this one file, it is possible — like myself — to have stacked notebooks, where a folder will contain multiple.
notebooks
│ Artificial Intelligence.enex
│ Java.enex
│
└───Second year university
│ │ Architectures and operating systems.enex
│ │ Data Structures.enex
│
└───Third year university
│ Audio visual processing.enex
│ Embedded systems.enex
Luckily in the config.json, we can specify an array of sources, such that we can include these stacks as their own sources:
"enexSources": [
"C:\\Users\\lukeg\\Desktop\\MarkdownNotes\\AllNotebooks",
"C:\\Users\\lukeg\\Desktop\\MarkdownNotes\\AllNotebooks\\Second year university",
"C:\\Users\\lukeg\\Desktop\\MarkdownNotes\\AllNotebooks\\Third year university"
],
So given the directory, I just need to get all the folders that contain .enex files.
const fs = require('fs');
const inputDir = 'C:\\Users\\lukeg\\Desktop\\MarkdownNotes\\notebooks';
let enexSources = [];
enexSources.push(inputDir);
fs.readdirSync(inputDir).forEach(file => {
const absolutePath = `${inputDir}\\${file}`;
if (fs.existsSync(absolutePath) && fs.lstatSync(absolutePath).isDirectory()) {
if (fs.readdirSync(absolutePath).some(f => f.endsWith('enex'))) {
enexSources.push(absolutePath);
}
}
});
const rawConfig = fs.readFileSync('./config.json');
let config = JSON.parse(rawConfig);
config.enexSources = enexSources;
fs.writeFileSync('config.json', JSON.stringify(config), 'utf8');
So I’m simply reading all the folders in the input directory — where my notes are — checking if they’re a notebook stack by peaking into the directory and determining whether there are any .enex files. If there are I then add the absolute path to the enexSources array. I’m then reading in the current config file, updating it with the new array and then overwriting it.
So now the config would look something like:
{
"enexSources": [
"C:\\Users\\lukeg\\Desktop\\MarkdownNotes\\notebooks",
"C:\\Users\\lukeg\\Desktop\\MarkdownNotes\\notebooks\\Java",
"C:\\Users\\lukeg\\Desktop\\MarkdownNotes\\notebooks\\Second year",
"C:\\Users\\lukeg\\Desktop\\MarkdownNotes\\notebooks\\Third year"
],
"outputDir": "C:\\Users\\lukeg\\Desktop\\MarkdownNotes\\Markdown",
"isZettelkastenNeeded": false,
"plainTextNotesOnly": false,
"skipWebClips": false,
"useHashTags": true,
"outputFormat": "StandardMD",
"urlEncodeFileNamesAndLinks": false,
"skipEnexFileNameFromOutputPath": false,
"monospaceIsCodeBlock": false,
"keepMDCharactersOfENNotes": false,
"keepOriginalAmountOfNewlines": false,
"addExtensionToInternalLinks": true,
"nestedTags": {
"separatorInEN": "_",
"replaceSeparatorWith": "/",
"replaceSpaceWith": "-"
},
"resourcesDir": "resources",
"turndownOptions": {
"headingStyle": "atx"
},
"dateFormat": "YYYY-MM-DD",
"haveEnexLevelResources": true,
"haveGlobalResources": false,
"logseqSettings": {
"journalNotes": false
},
"obsidianSettings": {
"omitLinkDisplayName": false
}
}
As I’m using node I can execute the script used previously from the command-line in code, using exec:
exec('npx -p yarle-evernote-to-md@latest yarle --configFile ./config.json');
So when I execute the following code, all of my notebooks are converted into markdown:
const { exec } = require('child_process');
const fs = require('fs');
const inputDir = 'C:\\Users\\lukeg\\Desktop\\MarkdownNotes\\notebooks';
let enexSources = [];
enexSources.push(inputDir);
fs.readdirSync(inputDir).forEach(file => {
const absolutePath = `${inputDir}\\${file}`;
if (fs.existsSync(absolutePath) && fs.lstatSync(absolutePath).isDirectory()) {
if (fs.readdirSync(absolutePath).some(f => f.endsWith('enex'))) {
enexSources.push(absolutePath);
}
}
});
const rawConfig = fs.readFileSync('./config.json');
let config = JSON.parse(rawConfig);
config.enexSources = enexSources;
fs.writeFileSync('config.json', JSON.stringify(config), 'utf8');
exec('npx -p yarle-evernote-to-md@latest yarle --configFile ./config.json');
To my private repo
Brilliant, now I just need to get it into my Github repository. Every time I run the script, I could just manually commit it, I’m far too lazy for that. Let’s add the following into our package.json
under scripts:
"git": "git add . && git commit -m",
"postgit": "git push --all
So now we can write this command and it’ll store in the repo I’ve set up:
exec(`npm run git -- ${new Date().toLocaleDateString()}`);
Note the postgit, is a hook that is called after git is executed.
In order to execute this after the conversion has happened, I need to use execSync
rather than exec
— for the conversion:
const { exec, execSync } = require('child_process');
...
execSync('npx -p yarle-evernote-to-md@latest yarle --configFile ./config.json');
exec(`npm run git -- ${new Date().toLocaleDateString()}`);
And that’s it!
Conclusion
Now all I have to do if I want to backup my notes is export them, then run this script, brilliant.
A potential improvement to this would be to use Evernotes api, but unfortunately I had to create a whole new account to use it, which defeated the purpose. I’m sure there’s avenues such as subscribing to their webhooks to keep data sources in sync, or creating a job to periodically check for new updates. Maybe I’ll investigate this in a future blog, but for now, If I want to try a different note-taking system, it should be as simple as importing my markdown files.