Node module that turns Evernote .ENEX documents into pretty web pages. This module is particularly useful for writing simple static site generators where you can write up your site content with Evernote (which is what I do).
If all you want to do is convert Evernote documents to HTML you can do the following:
var evernoteTranslator = require('evernote-translator');
evernoteTranslator.translate({
inputFile: '/file/name/of/input/document.enex',
outputFile: '/file/name/of/output/index.html',
resourceDir: '/file/name/of/output/resources/',
onFailure: function() {},
onSuccess: function() {}
});
inputFile
is the file name of the .ENEX document to be read.
outputFile
is the file name of the resulting HTML document.
resourceDir
is the directory where you want the embedded resources
to be stored.
Keep in mind, though, that the resulting HTML documents will be no different from exporting HTML documents straight out of Evernote in the first place. The next section covers how you can manipulate the output data.
For simple changes you might want to consider the naive approach, which would be to manipulate the output string:
evernoteTranslator.translate({
/* ... */
outputDir: '/file/name/of/output/',
/* ... */
onSuccess: function(outputString) {
var i = outputString.indexOf('</head>');
var customStyles = '<style>body { font-family: \'Helvetica Neue\', sans-serif; }</style>';
var newString = outputString.substring(0, i) + customStyles + outputString.substring(i);
fs.writeFile('/file/name/of/output/index.html', newString, 'utf8');
}
});
Note that you have to specify an outputDir
instead of an outputFile
when doing the writing to disk yourself. It is necessary to tell the
translator about the output directory so that it can generate correct
URLs for embedded resources.
If you're going to make more complex changes to the output document,
you should start using the ProcessorPipeline
. After the document
has been parsed and its content converted into a plain JS object
structure, it is passed through the processor pipeline.
The default pipeline looks a bit like this:
mediaTranslator -> cryptTranslator -> todoTranslator -> ... -> htmlWrapper
mediaTranslator
: Converts<en-media/>
tags into correct HTML.cryptTranslator
: Replaces<en-crypt/>
tags with[Encrypted in Evernote]
.todoTranslator
: Replaces<en-todo/>
tags with checkboxes.insertProcessor
: Finds inserts present in the file (explained below).annotationReader
: Finds annotations present in the file (explained below).annotationRemover
: Removes all annotations found.htmlWrapper
: Wraps the document with<html>
and<body>
tags and such.
In order to inject our custom CSS like before, we have to add a new processor to the processor pipeline:
var customPipeline = new evernoteTranslator.ProcessorPipeline();
customPipeline.insertAfter('htmlWrapper', {
name: 'cssInjector',
fn: function(dom) {
dom[0].tag === '!DOCTYPE'; // true
dom[1].tag === 'html'; // true
dom[1].children[0].tag === 'head'; // true
var headTag = dom[1].children[0];
headTag.children.push({
tag: 'style',
children: ['body { font-family: \'Helvetica Neue\', sans-serif; }']
});
return dom;
}
});
evernoteTranslator.translate({
/* ... */
processorPipeline: customPipeline,
/* ... */
});
We use the .insertAfter()
because we want to apply our changes to the DOM
after htmlWrapper
has done its work. The order of processors in the pipeline
is important. For example, the annotationReader
processor parses annotations
and attaches them as metadata to DOM nodes whilst the very next processor,
annotationRemover
, removes the metadata so that it doesn't show up in the
output file. If you want to make use of the metadata, you'll have to place
your processor between the annotationReader
and annotationRemover
.
If you specify a resourceDir
then embedded resources will be dealt with
automatically. All resources will be placed in the specified directory with
this naming convention:
./(md5 checksum of file).(extension)
For more versatility, you can pass a nameResource
function to the translator
instead of a resourceDir
.
evernoteTranslator.translate({
/* ... */
nameResource: function(fileName, mimeType, checksum) {
if (mimeType === 'image/jpeg') {
return checksum + '.jpg';
} else {
return false; // you can conditionally ignore certain resources
}
},
/* ... */
});
For maximum versatility, you can pass a handleResource
function to the translator
instead of a nameResource
function.
evernoteTranslator.translate({
/* ... */
handleResource: function(fileName, mimeType, buffer, checksum) {
if (mimeType === 'image/jpeg') {
fs.writeFile(checksum + '.jpg', buffer);
return checksum + '.jpg';
} else {
return false;
}
},
/* ... */
});
With this last option you have the responsibility of writing the resources
to the file system yourself. You must return the URL for the resource relative
to the HTML document (or false
, if you've chosen to ignore the resource).
The annotation system is a powerful tool when processing documents and making
them fit for the web. You can annotate a certain line/paragraph/object in your
Evernote document by writing something of the form ![ ]
on the line above it.
The following Evernote note,
Lorem ipsum
![hidden message: Hello there!]
Dolor sit amet
Consectetur adipiscing elit
...translates to this HTML code:
<div>Lorem ipsum</div>
<div><b>![hidden message: Hello, there!]</b></div>
<div>Dolor sit amet</div>
<div>Consectetur adipiscing elit</div>
And after passing through the annotationReader
it
looks like this:
<div>Lorem ipsum</div>
<div annotations={'hidden message': 'Hello, there!'}>Dolor sit amet</div>
<div>Consectetur adipiscing elit</div>
Technically, it won't be HTML, but rather this JS object representing the HTML:
[{
tag: 'div',
children: ['Lorem ipsum']
},{
tag: 'div',
children: ['Doler sit amet'],
annotations: { 'hidden message': 'Hello, there!' }
},{
tag: 'div',
children: ['Consectetur adipiscing elit']
}]
Now, let's write a small processor that uses this annotation to manipulate the DOM:
var customPipeline = new evernoteTranslator.ProcessorPipeline();
customPipeline.insertAfter('annotationReader', {
name: 'hiddenMessageProcessor',
fn: function(dom) {
dom.forEach(function(node) {
if (node.annotations && node.annotations['hidden message']) {
var message = node.annotations['hidden message'];
node.onclick = 'alert(\'' + message + '\')';
}
});
return dom;
}
});
evernoteTranslator.translate({
/* ... */
processorPipeline: customPipeline,
/* ... */
});
The output file will look like this:
<!DOCTYPE html>
<html>
<head>
<title>Note Title</title>
</head>
<body>
<div>Lorem ipsum</div>
<div onclick="alert('Hello, there!')">Dolor sit amet</div>
<div>Consectetur adipiscing elit</div>
</body>
</html>
Annotations are an incredibly powerful tool for processing documents.
Annotations are textual ques that apply to line directly below the annotation.
Inserts look exactly like annotations, but instead of applying to the line
below it, it acts as a que for some other node to be inserted. In other
words, you want to replace the line where your ![ ]
insert is written
with some other element.
We want some footer on our webpages, but not on all of them. So we can use an insert on the pages where we want to insert a footer.
This will be our Evernote note:
Lorem ipsum
Dolor sit amet
![footer]
This is how we can program in our footer insert:
function footerInsert() {
return {
tag: 'footer',
style: 'color: #aaa; font-size: 12px;'
children: ['Footer content!']
};
}
evernoteTranslator.translate({
/* ... */
inserts: {
'footer': footerInsert
},
/* ... */
});
With this code, an input like this:
This note will have the following HTML code:
<div>Lorem ipsum</div>
<div>Dolor sit amet</div>
<div><b>![footer]</b></div>
...is turned into this...
<div>Lorem ipsum</div>
<div>Dolor sit amet</div>
<footer style="color: #aaa; font-size: 12px;">Footer content!</footer>
Note that the entire <div>
that contained the insert has
been replaced with the footer, not just the insert text.
Optionally, you can accept a single string argument:
Lorem ipsum
Dolor sit amet
![footer: small]
The argument will be passed to your insert function.
function footerInsert(arg) {
if (arg === 'regular') {
/* ... regular footer ... */
} else if (arg === 'small') {
/* ... small footer ... */
} else {
return null;
/* When nothing is returned, the div
* that had the insert is removed instead
* of replaced. */
}
}
evernoteTranslator.translate({
/* ... */
inserts: {
'footer': footerInsert
},
/* ... */
});