(\s*)
(\s*)<\/div>){2,}/g, '
' ); // Remove extra line breaks
25 |
26 | if ( title ) {
27 | html = `
${title}
${html}`;
28 | }
29 |
30 | if ( beautify ) {
31 | html = beautifyHTML ( html );
32 | }
33 |
34 | return html.replace ( /^\s*/, '\n' ) // Ensure it starts with a new line
35 | .replace ( /\s*$/, '\n' ); // Ensure it ends with a new line
36 |
37 | },
38 |
39 | async markdown ( html, title ) {
40 |
41 | html = Content.format.html ( html, title, false );
42 |
43 | html = html.replace ( /
]*?)checked(.*?)>/g, ' [x] ' ) // Replace checked checkbox
44 | .replace ( /
/g, ' [ ] ' ); // Replace unchecked checkbox
45 |
46 | const service = new turndown ({
47 | bulletListMarker: '-',
48 | headingStyle: 'atx',
49 | hr: '---'
50 | });
51 |
52 | service.addRule ( 'alignment', {
53 | filter: node => node.nodeName !== 'TABLE' && ( node.getAttribute ( 'style' ) || '' ).includes ( 'text-align:' ),
54 | replacement: ( str, ele ) => {
55 | str = str.trim ();
56 | if ( !str.length ) return '';
57 | const style = ele.getAttribute ( 'style' );
58 | const alignment = style.match ( /text-align:\s*(\S+);/ );
59 | if ( !alignment ) return `${_.trim ( str )}\n\n`; //TODO: The regex isn't good enough if we reach here
60 | return `
${_.trim ( str )}
\n\n`;
61 | }
62 | });
63 |
64 | service.addRule ( 'code', {
65 | filter: node => node.nodeName === 'DIV' && ( node.getAttribute ( 'style' ) || '' ).includes ( '-en-codeblock' ),
66 | replacement: str => {
67 | str = str.trim ();
68 | if ( !str.length ) return '';
69 | str = _.trim ( str ).replace ( /<(?:.|\n)*?>/gm, '' );
70 | str = str.includes ( '\n' ) ? `\n\n\`\`\`\n${str}\n\`\`\`\n` : `\`${str}\``;
71 | return str;
72 | }
73 | });
74 |
75 | service.addRule ( 'others', {
76 | filter: ['font', 'span'],
77 | replacement: ( str, ele ) => {
78 | if ( !_.trim ( str ) ) return '';
79 | /* STYLE */
80 | const style = ele.getAttribute ( 'style' );
81 | let newStyle = '';
82 | if ( style ) {
83 | /* FORMATTING */
84 | if ( style.match ( /text-decoration: underline/ ) ) { // Underline
85 | str = `
${str}`;
86 | }
87 | if ( style.match ( /text-decoration: line-through/ ) ) { // Strikethrough
88 | str = `
${str}`;
89 | }
90 | if ( style.match ( /font-style: italic/ ) ) { // Italic
91 | str = `_${str}_`;
92 | }
93 | if ( style.match ( /font-weight: bold/ ) ) { // Bold
94 | str = `**${str}**`;
95 | }
96 | /* HEADING */
97 | if ( str.match ( /^[^#]|>#/ ) ) { // Doesn't contain an heading
98 | if ( style.match ( /font-size: (48|64|72)px/ ) ) { // H1
99 | str = `# ${str}`;
100 | }
101 | if ( style.match ( /font-size: 36px/ ) ) { // H2
102 | str = `## ${str}`;
103 | }
104 | if ( style.match ( /font-size: 24px/ ) ) { // H3
105 | str = `### ${str}`;
106 | }
107 | if ( style.match ( /font-size: (12|13)px/ ) ) { // Small
108 | str = `
${str}`;
109 | }
110 | if ( style.match ( /font-size: (9|10|11)px/ ) ) { // Very Small
111 | str = `
${str}`;
112 | }
113 | }
114 | /* BACKGROUND COLOR */
115 | const backgroundColor = style.match ( /background-color: ([^;]+);/ );
116 | if ( backgroundColor && backgroundColor[1] !== 'rgb(255, 255, 255)' ) {
117 | newStyle += backgroundColor[0];
118 | }
119 | }
120 | /* COLOR */
121 | const colorAttr = ele.getAttribute ( 'color' ); // Color
122 | if ( colorAttr && colorAttr !== '#010101' ) {
123 | newStyle += `color: ${colorAttr};`
124 | }
125 | if ( style ) {
126 | const colorStyle = style.match ( /[^-]color: ([^;]+);/ );
127 | if ( colorStyle && colorStyle[1] !== '#010101' ) {
128 | newStyle += `color: ${colorStyle[1]};`;
129 | }
130 | }
131 | /* NEW STYLE */
132 | if ( newStyle ) {
133 | str = `
${str}`;
134 | }
135 | return str;
136 | }
137 | });
138 |
139 | service.keep ([ 'kbd' ]);
140 | service.keep ([ 'b', 'i', 's', 'u' ]); // 😚
141 |
142 | return service.turndown ( html )
143 | .replace ( /\\((-|\*|\+) )/g, '$1' ) // Unescape unordered lists
144 | .replace ( /^(-|\*|\+)\s+/gm, '$1 ' ) // Remove extra whitespace from unordered lists
145 | .replace ( /^((?:-|\*|\+) .*)\n\n(?=(?:-|\*|\+) )/gm, '$1\n' ) // Remove extra whitespace between unordered lists items
146 | .replace ( /^(\d+\.)\s+/gm, '$1 ' ) // Remove extra whitespace from ordered lists
147 | .replace ( /\\\[([^\]]*?)\\\] /g, '[$1] ' ) // Unescape square brackets
148 | // .replace ( /(\s*\n\s*){4,}/g, '\n\n
\n\n' ) // Add line breaks
149 | .replace ( /(\s*\n\s*){3,}/g, '\n\n' ) // Remove extra new lines
150 | .replace ( /\n\n
\n\n(-|\*|\+) /g, '\n\n$1 ' ) // Remove line breaks before lists
151 | .replace ( /^\s*/, '\n' ) // Ensure it starts with a new line
152 | .replace ( /\s*$/, '\n' ); // Ensure it ends with a new line
153 |
154 | }
155 |
156 | },
157 |
158 | metadata: {
159 |
160 | options: {
161 | engines: {
162 | yaml: Matter
163 | }
164 | },
165 |
166 | get ( content ) {
167 |
168 | return matter ( content, Content.metadata.options ).data;
169 |
170 | },
171 |
172 | set ( content, metadata ) {
173 |
174 | content = Content.metadata.remove ( content );
175 |
176 | if ( !_.isEmpty ( metadata ) ) {
177 |
178 | content = matter.stringify ( content, metadata, Content.metadata.options );
179 |
180 | }
181 |
182 | return content;
183 |
184 | },
185 |
186 | remove ( content ) {
187 |
188 | return matter ( content, Content.metadata.options ).content;
189 |
190 | }
191 |
192 | }
193 |
194 | };
195 |
196 | /* EXPORT */
197 |
198 | module.exports = Content;
199 |
--------------------------------------------------------------------------------
/src/dump.js:
--------------------------------------------------------------------------------
1 |
2 | /* IMPORT */
3 |
4 | const _ = require ( 'lodash' ),
5 | Config = require ( './config' ),
6 | Content = require ( './content' ),
7 | File = require ( './file' ),
8 | Parse = require ( './parse' ),
9 | Path = require ( './path' ),
10 | Utils = require ( './utils' );
11 |
12 | /* DUMP */
13 |
14 | const Dump = {
15 |
16 | async _xml2data ( xml ) {
17 |
18 | if ( !xml ) return [];
19 |
20 | return await Promise.all ( _.castArray ( xml['en-export'].note ).map ( async note => {
21 |
22 | const title = Parse.title ( note.title || 'Untitled' ),
23 | created = note.created ? Parse.date ( note.created ) : new Date ();
24 |
25 | return {
26 | title,
27 | content: await Parse.content ( note.content || '', title ),
28 | created,
29 | modified: note.updated ? Parse.date ( note.updated ) : created,
30 | tags: _.castArray ( note.tag || [] ),
31 | attachments: Config.dump.attachments && note.resource && _.castArray ( note.resource ).filter ( resource => resource.data ).map ( resource => ({
32 | buffer: Buffer.from ( resource.data, 'base64' ),
33 | fileName: _.get ( resource, ['resource-attributes', 'file-name'] ) || 'Untitled'
34 | }))
35 | };
36 |
37 | }));
38 |
39 | },
40 |
41 | async enex () {
42 |
43 | const xml = await Promise.all ( Config.path.src.map ( Parse.xml ) ),
44 | datas = await Promise.all ( xml.map ( Dump._xml2data ) ),
45 | data = _.flatten ( datas );
46 |
47 | if ( !data.length ) Utils.throw ( 'Nothing to dump, is the path correct?' );
48 |
49 | await Dump.data ( data );
50 |
51 | },
52 |
53 | async data ( data ) {
54 |
55 | return await Promise.all ( data.map ( async datum => {
56 |
57 | const attachments = Config.dump.attachments ? await Dump.attachments ( datum.attachments ) : [];
58 |
59 | if ( Config.dump.metadata ) {
60 |
61 | let metadata = {
62 | title: datum.title,
63 | tags: [...datum.tags, ...Config.dump.tags],
64 | attachments,
65 | created: datum.created.toISOString (),
66 | modified: datum.modified.toISOString ()
67 | };
68 |
69 | metadata = _.pickBy ( metadata, _.negate ( _.isEmpty ) );
70 |
71 | datum.content = Content.metadata.set ( datum.content, metadata );
72 |
73 | }
74 |
75 | if ( Config.dump.notes ) {
76 |
77 | await Dump.note ( datum );
78 |
79 | }
80 |
81 | }));
82 |
83 | },
84 |
85 | async attachments ( attachments ) {
86 |
87 | if ( !attachments ) return [];
88 |
89 | return await Promise.all ( attachments.map ( async attachment => {
90 |
91 | const {filePath, fileName} = await Path.getAllowedPath ( Config.attachments.path, attachment.fileName );
92 |
93 | await File.write ( filePath, attachment.buffer );
94 |
95 | return fileName;
96 |
97 | }));
98 |
99 | },
100 |
101 | async note ( note ) {
102 |
103 | const {filePath, fileName} = await Path.getAllowedPath ( Config.notes.path, `${note.title}.${Config.dump.extension}` );
104 |
105 | await File.write ( filePath, note.content );
106 |
107 | return fileName;
108 |
109 | }
110 |
111 | };
112 |
113 | /* EXPORT */
114 |
115 | module.exports = Dump;
116 |
--------------------------------------------------------------------------------
/src/file.js:
--------------------------------------------------------------------------------
1 |
2 | /* IMPORT */
3 |
4 | const fs = require ( 'fs' ),
5 | mkdirp = require ( 'mkdirp' ),
6 | path = require ( 'path' ),
7 | pify = require ( 'pify' );
8 |
9 | /* FILE */
10 |
11 | const File = {
12 |
13 | async exists ( filePath ) {
14 |
15 | try {
16 |
17 | await pify ( fs.access )( filePath, fs.constants.F_OK );
18 |
19 | return true;
20 |
21 | } catch ( e ) {
22 |
23 | return false;
24 |
25 | }
26 |
27 | },
28 |
29 | async read ( filePath ) {
30 |
31 | try {
32 |
33 | return ( await pify ( fs.readFile )( filePath, { encoding: 'utf8' } ) ).toString ();
34 |
35 | } catch ( e ) {
36 |
37 | return '';
38 |
39 | }
40 |
41 | },
42 |
43 | async write ( filePath, content ) {
44 |
45 | try {
46 |
47 | return await pify ( fs.writeFile )( filePath, content );
48 |
49 | } catch ( e ) {
50 |
51 | if ( e.code === 'ENOENT' ) {
52 |
53 | try {
54 |
55 | await pify ( mkdirp )( path.dirname ( filePath ) );
56 |
57 | return await pify ( fs.writeFile )( filePath, content );
58 |
59 | } catch ( e ) {}
60 |
61 | }
62 |
63 | }
64 |
65 | }
66 |
67 | };
68 |
69 | /* EXPORT */
70 |
71 | module.exports = File;
72 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 |
2 | /* IMPORT */
3 |
4 | const _ = require ( 'lodash' ),
5 | Config = require ( './config' ),
6 | Dump = require ( './dump' ),
7 | Utils = require ( './utils' );
8 |
9 | /* ENEX DUMP */
10 |
11 | function EnexDump ( options ) {
12 |
13 | _.merge ( Config, options );
14 |
15 | if ( !Config.path.src.length ) Utils.throw ( 'You have to pass at least one src path' );
16 |
17 | if ( !Config.path.dst ) Utils.throw ( 'You have to pass a dst path' );
18 |
19 | if ( !Config.dump.formats.includes ( Config.dump.format ) ) Utils.throw ( `We only support these formats: ${Config.dump.formats.map ( format => `"${format}"` ).join ( ', ' )} ` );
20 |
21 | return Dump.enex ();
22 |
23 | }
24 |
25 | /* EXPORT */
26 |
27 | module.exports = EnexDump;
28 |
--------------------------------------------------------------------------------
/src/matter.js:
--------------------------------------------------------------------------------
1 |
2 | /* IMPORT */
3 |
4 | const yaml = require ( 'js-yaml' );
5 |
6 | /* MATTER */
7 |
8 | const Matter = {
9 |
10 | options: {
11 | flowLevel: 1,
12 | indent: 2,
13 | lineWidth: 8000
14 | },
15 |
16 | parse ( str ) {
17 |
18 | return yaml.safeLoad ( str, Matter.options );
19 |
20 | },
21 |
22 | stringify ( obj ) {
23 |
24 | return yaml.safeDump ( obj, Matter.options );
25 |
26 | }
27 |
28 | };
29 |
30 | /* EXPORT */
31 |
32 | module.exports = Matter;
33 |
--------------------------------------------------------------------------------
/src/parse.js:
--------------------------------------------------------------------------------
1 |
2 | /* IMPORT */
3 |
4 | const {parse: xml2js} = require ( 'fast-xml-parser' ),
5 | entities = require ( 'html-entities' ).AllHtmlEntities,
6 | Config = require ( './config' ),
7 | Content = require ( './content' ),
8 | File = require ( './file' );
9 |
10 | /* PARSE */
11 |
12 | const Parse = {
13 |
14 | date ( date ) { // From the YYYYMMDDTHHMMSSZ format
15 |
16 | date = date.split ( '' );
17 |
18 | date.splice ( 13, 0, ':' );
19 | date.splice ( 11, 0, ':' );
20 | date.splice ( 6, 0, '-' );
21 | date.splice ( 4, 0, '-' );
22 |
23 | date = date.join ( '' );
24 |
25 | return new Date ( date );
26 |
27 | },
28 |
29 | title ( title ) {
30 |
31 | return entities.decode ( title );
32 |
33 | },
34 |
35 | async content ( content, title ) { // From the HTML-ish format
36 |
37 | return await Content.format[Config.dump.format]( content, title );
38 |
39 | },
40 |
41 | async xml ( filePath ) {
42 |
43 | const content = await File.read ( filePath );
44 |
45 | try {
46 |
47 | return xml2js ( content );
48 |
49 | } catch ( e ) {}
50 |
51 | }
52 |
53 | };
54 |
55 | /* EXPORT */
56 |
57 | module.exports = Parse;
58 |
--------------------------------------------------------------------------------
/src/path.js:
--------------------------------------------------------------------------------
1 |
2 | /* IMPORT */
3 |
4 | const path = require ( 'path' ),
5 | filenamify = require ( 'filenamify' ),
6 | File = require ( './file' );
7 |
8 | /* PATH */
9 |
10 | const Path = {
11 |
12 | _allowedPaths: {}, // Map of filePath => timestamp, ensuring we don't return the same path mutliple times within some amount of time, in order to avoid race conditions //UGLY
13 |
14 | _checkAllowedPath ( filePath ) {
15 |
16 | if ( !Path._allowedPaths[filePath] || ( Path._allowedPaths[filePath] + 5000 ) < Date.now () ) {
17 |
18 | Path._allowedPaths[filePath] = Date.now ();
19 |
20 | return true;
21 |
22 | }
23 |
24 | return false;
25 |
26 | },
27 |
28 | async getAllowedPath ( folderPath, baseName ) {
29 |
30 | baseName = baseName
31 | .replace ( /\//g, '∕' ); // Preserving a dash-like character
32 |
33 | let {name, ext} = path.parse ( baseName );
34 |
35 | name = Path.sanitize ( name );
36 |
37 | for ( let i = 1;; i++ ) {
38 |
39 | const suffix = i > 1 ? ` (${i})` : '',
40 | fileName = `${name}${suffix}${ext}`,
41 | filePath = path.join ( folderPath, fileName );
42 |
43 | if ( await File.exists ( filePath ) ) continue;
44 |
45 | if ( !Path._checkAllowedPath ( filePath ) ) continue;
46 |
47 | return { folderPath, filePath, fileName };
48 |
49 | }
50 |
51 | },
52 |
53 | sanitize ( filePath ) {
54 |
55 | return filenamify ( filePath, { replacement: ' ' } ).trim ();
56 |
57 | }
58 |
59 | };
60 |
61 | /* EXPORT */
62 |
63 | module.exports = Path;
64 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 |
2 | /* IMPORT */
3 |
4 | const chalk = require ( 'chalk' );
5 |
6 | /* UTILS */
7 |
8 | const Utils = {
9 |
10 | throw ( msg ) {
11 |
12 | console.error ( chalk.red ( msg ) );
13 |
14 | process.exit ( 1 );
15 |
16 | }
17 |
18 | };
19 |
20 | /* EXPORT */
21 |
22 | module.exports = Utils;
23 |
--------------------------------------------------------------------------------