enex-io 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ghazi (mgks.dev)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # enex-io
2
+
3
+ > High-performance parser and generator for Evernote & Apple Notes (.enex) files.
4
+
5
+ <a href="https://www.npmjs.com/package/enex-io"><img src="https://img.shields.io/npm/v/enex-io.svg?style=flat-square&color=007acc" alt="npm version"></a>
6
+ <a href="https://bundlephobia.com/package/enex-io"><img src="https://img.shields.io/bundlephobia/minzip/enex-io?style=flat-square" alt="size"></a>
7
+ <a href="https://www.npmjs.com/package/enex-io"><img src="https://img.shields.io/npm/dt/enex-io.svg?style=flat-square&color=success" alt="npm downloads"></a>
8
+ <a href="https://github.com/mgks/enex-io/blob/main/LICENSE"><img src="https://img.shields.io/github/license/mgks/enex-io.svg?style=flat-square&color=blue" alt="license"></a>
9
+ <a href="https://github.com/mgks/enex-io/stargazers"><img src="https://img.shields.io/github/stars/mgks/enex-io?style=flat-square&logo=github" alt="stars"></a>
10
+
11
+ A lightweight, zero-dependency Node.js library and CLI tool to convert **.enex** files to JSON and back. Perfect for migrations, backups, and data processing.
12
+
13
+ ## 📦 Installation
14
+
15
+ ```bash
16
+ # Install globally for CLI usage
17
+ npm install -g enex-io
18
+
19
+ # Install as a dependency in your project
20
+ npm install enex-io
21
+ ```
22
+
23
+ ## 💻 CLI Usage
24
+
25
+ **Convert ENEX to JSON**
26
+ ```bash
27
+ enex-io to-json my-notes.enex
28
+ # Output: my-notes.json
29
+ ```
30
+
31
+ **Convert JSON to ENEX**
32
+ ```bash
33
+ enex-io to-enex backup.json
34
+ # Output: backup.enex
35
+ ```
36
+
37
+ **Options**
38
+ ```bash
39
+ -o, --output <path> Specify output file path
40
+ -p, --pretty Pretty print JSON output (default: true)
41
+ --version Show version number
42
+ --help Show help
43
+ ```
44
+
45
+ ## 🔧 API Usage
46
+
47
+ Built for modern Node.js environments (ESM).
48
+
49
+ ```javascript
50
+ import { parseEnex, generateEnex } from 'enex-io';
51
+ import fs from 'fs';
52
+
53
+ // 1. Parse ENEX to Object
54
+ const xml = fs.readFileSync('notes.enex', 'utf-8');
55
+ const notes = parseEnex(xml);
56
+
57
+ console.log(notes[0].title); // "My Note"
58
+
59
+ // 2. Generate ENEX from Object
60
+ const myNotes = [
61
+ {
62
+ title: "Hello World",
63
+ content: "<div>This is a <b>test</b>.</div>",
64
+ tags: ["personal", "test"],
65
+ created: "2023-10-27T10:00:00.000Z",
66
+ updated: "2023-10-27T12:00:00.000Z"
67
+ }
68
+ ];
69
+
70
+ const enexOutput = generateEnex(myNotes);
71
+ fs.writeFileSync('export.enex', enexOutput);
72
+ ```
73
+
74
+ ## 🧩 Type Definition
75
+
76
+ The standard Note object used by the parser and generator:
77
+
78
+ ```typescript
79
+ interface Note {
80
+ title: string;
81
+ content: string; // HTML content string
82
+ tags: string[];
83
+ created: string; // ISO 8601 Date String
84
+ updated: string; // ISO 8601 Date String
85
+ author?: string;
86
+ sourceUrl?: string;
87
+ }
88
+ ```
89
+
90
+ ## License
91
+
92
+ MIT
93
+
94
+ > **{ github.com/mgks }**
95
+ >
96
+ > ![Website Badge](https://img.shields.io/badge/Visit-mgks.dev-blue?style=flat&link=https%3A%2F%2Fmgks.dev) ![Sponsor Badge](https://img.shields.io/badge/%20%20Become%20a%20Sponsor%20%20-red?style=flat&logo=github&link=https%3A%2F%2Fgithub.com%2Fsponsors%2Fmgks)
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import{Command as O}from"commander";import s from"fs";import{createRequire as j}from"module";import*as f from"cheerio";import a from"dayjs";function x(t,e={}){let i=`<?xml version="1.0" encoding="UTF-8"?>
3
+ <!DOCTYPE en-export SYSTEM "http://xml.evernote.com/pub/evernote-export3.dtd">
4
+ <en-export export-date="${a().format("YYYYMMDDTHHmmssZ")}" application="${e.application||"enex-io"}" version="1.0">`;for(let n of t)i+=`
5
+ <note>
6
+ <title>${u(n.title)}</title>
7
+ <content><![CDATA[<?xml version="1.0" encoding="UTF-8" standalone="no"?>
8
+ <!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">
9
+ <en-note>${w(n.content)}</en-note>]]></content>
10
+ <created>${m(n.created)}</created>
11
+ <updated>${m(n.updated)}</updated>
12
+ ${n.tags.map(o=>`<tag>${u(o)}</tag>`).join("")}
13
+ </note>`;return i+=`
14
+ </en-export>`,i}function g(t){let e=f.load(t,{xmlMode:!0}),r=[];return e("note").each((i,n)=>{let o=e(n),$=o.find("title").text()||"Untitled",S=o.find("content").text(),p=/<en-note[^>]*>([\s\S]*)<\/en-note>/.exec(S),y=p&&p[1]?p[1]:"",E=d(o.find("created").text()),Y=d(o.find("updated").text()),l=[];o.find("tag").each((N,v)=>{l.push(e(v).text())}),r.push({title:$,content:y,created:E,updated:Y,tags:l})}),r}function u(t){return t?t.replace(/[<>&'"]/g,e=>{switch(e){case"<":return"&lt;";case">":return"&gt;";case"&":return"&amp;";case"'":return"&apos;";case'"':return"&quot;";default:return e}}):""}function w(t){return t?t.replace(/<br>/g,"<br/>"):""}function m(t){return a(t).isValid()?a(t).format("YYYYMMDDTHHmmssZ"):a().format("YYYYMMDDTHHmmssZ")}function d(t){if(!t)return new Date().toISOString();let e=t.replace(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/,"$1-$2-$3T$4:$5:$6Z");return a(e).isValid()?e:new Date().toISOString()}var D=j(import.meta.url),h=D("../package.json"),c=new O;c.name("enex-io").description(h.description).version(h.version,"-v, --version").helpOption("-h, --help","Display help for command").showHelpAfterError();c.command("to-json").description("Convert an .enex file into a structured .json file").argument("<input>","Input .enex file path").option("-o, --output <path>","Output .json file path").option("-p, --pretty","Pretty print JSON output",!0).action((t,e)=>{try{if(!s.existsSync(t))throw new Error(`Input file not found: ${t}`);console.log(`\u23F3 Parsing ${t}...`);let r=s.readFileSync(t,"utf-8"),i=g(r),n=e.output||t.replace(/\.enex$/i,".json"),o=e.pretty?JSON.stringify(i,null,2):JSON.stringify(i);s.writeFileSync(n,o),console.log(`\u2705 Success! Converted ${i.length} notes to ${n}`)}catch(r){console.error(`\u274C Error: ${r.message}`),process.exit(1)}});c.command("to-enex").description("Convert a .json file into an importable .enex file").argument("<input>","Input .json file path").option("-o, --output <path>","Output .enex file path").option("-t, --title <name>","Application name","enex-io").action((t,e)=>{try{if(!s.existsSync(t))throw new Error(`Input file not found: ${t}`);console.log(`\u23F3 Generating ENEX from ${t}...`);let r=s.readFileSync(t,"utf-8"),i=JSON.parse(r);if(!Array.isArray(i))throw new Error("JSON must be an array of note objects");let n=x(i,{application:e.title}),o=e.output||t.replace(/\.json$/i,".enex");s.writeFileSync(o,n),console.log(`\u2705 Success! Generated .enex file at ${o}`)}catch(r){console.error(`\u274C Error: ${r.message}`),process.exit(1)}});c.parse();
15
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from 'commander';\nimport fs from 'fs';\nimport { createRequire } from 'node:module';\nimport { parseEnex, generateEnex } from './index.js';\nimport { Note } from './types.js';\n\n// 1. Clean Metadata Loading\nconst require = createRequire(import.meta.url);\nconst pkg = require('../package.json');\n\nconst program = new Command();\n\nprogram\n .name('enex-io')\n .description(pkg.description)\n .version(pkg.version, '-v, --version')\n .helpOption('-h, --help', 'Display help for command')\n .showHelpAfterError();\n\n// --- Command: ENEX -> JSON ---\nprogram\n .command('to-json')\n .description('Convert an .enex file into a structured .json file')\n .argument('<input>', 'Input .enex file path')\n .option('-o, --output <path>', 'Output .json file path')\n .option('-p, --pretty', 'Pretty print JSON output', true)\n .action((input, options) => {\n try {\n if (!fs.existsSync(input)) throw new Error(`Input file not found: ${input}`);\n \n console.log(`⏳ Parsing ${input}...`);\n const xml = fs.readFileSync(input, 'utf-8');\n const notes = parseEnex(xml);\n \n const outPath = options.output || input.replace(/\\.enex$/i, '.json');\n const jsonContent = options.pretty ? JSON.stringify(notes, null, 2) : JSON.stringify(notes);\n\n fs.writeFileSync(outPath, jsonContent);\n console.log(`✅ Success! Converted ${notes.length} notes to ${outPath}`);\n } catch (e: any) {\n console.error(`❌ Error: ${e.message}`);\n process.exit(1);\n }\n });\n\n// --- Command: JSON -> ENEX ---\nprogram\n .command('to-enex')\n .description('Convert a .json file into an importable .enex file')\n .argument('<input>', 'Input .json file path')\n .option('-o, --output <path>', 'Output .enex file path')\n .option('-t, --title <name>', 'Application name', 'enex-io')\n .action((input, options) => {\n try {\n if (!fs.existsSync(input)) throw new Error(`Input file not found: ${input}`);\n\n console.log(`⏳ Generating ENEX from ${input}...`);\n const json = fs.readFileSync(input, 'utf-8');\n const notes: Note[] = JSON.parse(json);\n \n if (!Array.isArray(notes)) throw new Error(\"JSON must be an array of note objects\");\n \n const xml = generateEnex(notes, { application: options.title });\n const outPath = options.output || input.replace(/\\.json$/i, '.enex');\n \n fs.writeFileSync(outPath, xml);\n console.log(`✅ Success! Generated .enex file at ${outPath}`);\n } catch (e: any) {\n console.error(`❌ Error: ${e.message}`);\n process.exit(1);\n }\n });\n\nprogram.parse();","import * as cheerio from 'cheerio';\nimport dayjs from 'dayjs';\nimport { Note, EnexOptions } from './types.js'; // Fix: Added .js\n\nexport function generateEnex(notes: Note[], options: EnexOptions = {}): string {\n const dateStr = dayjs().format('YYYYMMDDTHHmmssZ');\n \n let xml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE en-export SYSTEM \"http://xml.evernote.com/pub/evernote-export3.dtd\">\n<en-export export-date=\"${dateStr}\" application=\"${options.application || 'enex-io'}\" version=\"1.0\">`;\n\n for (const note of notes) {\n xml += `\n <note>\n <title>${escapeXml(note.title)}</title>\n <content><![CDATA[<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<!DOCTYPE en-note SYSTEM \"http://xml.evernote.com/pub/enml2.dtd\">\n<en-note>${formatContent(note.content)}</en-note>]]></content>\n <created>${formatDate(note.created)}</created>\n <updated>${formatDate(note.updated)}</updated>\n ${note.tags.map(tag => `<tag>${escapeXml(tag)}</tag>`).join('')}\n </note>`;\n }\n\n xml += `\\n</en-export>`;\n return xml;\n}\n\nexport function parseEnex(xmlContent: string): Note[] {\n const $ = cheerio.load(xmlContent, { xmlMode: true });\n const notes: Note[] = [];\n\n $('note').each((_, el) => {\n const node = $(el);\n const title = node.find('title').text() || 'Untitled';\n const contentRaw = node.find('content').text();\n \n // Extract inner HTML from the CDATA/en-note wrapper\n const contentMatch = /<en-note[^>]*>([\\s\\S]*)<\\/en-note>/.exec(contentRaw);\n \n // Fix: Strict Check. If match exists, use index 1, else empty string.\n // This solves \"Type undefined is not assignable to string\"\n const content = (contentMatch && contentMatch[1]) ? contentMatch[1] : '';\n\n const created = parseEnexDate(node.find('created').text());\n const updated = parseEnexDate(node.find('updated').text());\n \n const tags: string[] = [];\n \n // Fix: Wrapped in block {} to avoid returning 'number' (array length)\n // This solves \"Type number is not assignable to void\"\n node.find('tag').each((_, tag) => {\n tags.push($(tag).text());\n });\n\n notes.push({ title, content, created, updated, tags });\n });\n\n return notes;\n}\n\n// --- Helpers ---\n\nfunction escapeXml(unsafe: string): string {\n if (!unsafe) return '';\n return unsafe.replace(/[<>&'\"]/g, (c) => {\n switch (c) {\n case '<': return '&lt;';\n case '>': return '&gt;';\n case '&': return '&amp;';\n case '\\'': return '&apos;';\n case '\"': return '&quot;';\n default: return c;\n }\n });\n}\n\nfunction formatContent(html: string): string {\n if (!html) return '';\n return html.replace(/<br>/g, '<br/>');\n}\n\nfunction formatDate(isoString: string): string {\n return dayjs(isoString).isValid() \n ? dayjs(isoString).format('YYYYMMDDTHHmmssZ') \n : dayjs().format('YYYYMMDDTHHmmssZ');\n}\n\nfunction parseEnexDate(enexDate: string): string {\n if (!enexDate) return new Date().toISOString();\n const iso = enexDate.replace(/^(\\d{4})(\\d{2})(\\d{2})T(\\d{2})(\\d{2})(\\d{2})Z$/, '$1-$2-$3T$4:$5:$6Z');\n return dayjs(iso).isValid() ? iso : new Date().toISOString();\n}"],"mappings":"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","names":["Command","fs","createRequire","cheerio","dayjs","generateEnex","notes","options","xml","note","escapeXml","formatContent","formatDate","tag","parseEnex","xmlContent","$","_","el","node","title","contentRaw","contentMatch","content","created","parseEnexDate","updated","tags","unsafe","c","html","isoString","enexDate","iso","require","createRequire","pkg","program","Command","input","options","fs","xml","notes","parseEnex","outPath","jsonContent","e","json","generateEnex"]}
@@ -0,0 +1,18 @@
1
+ interface Note {
2
+ title: string;
3
+ content: string;
4
+ tags: string[];
5
+ created: string;
6
+ updated: string;
7
+ author?: string;
8
+ sourceUrl?: string;
9
+ }
10
+ interface EnexOptions {
11
+ version?: string;
12
+ application?: string;
13
+ }
14
+
15
+ declare function generateEnex(notes: Note[], options?: EnexOptions): string;
16
+ declare function parseEnex(xmlContent: string): Note[];
17
+
18
+ export { generateEnex, parseEnex };
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ import*as m from"cheerio";import r from"dayjs";function S(t,e={}){let a=`<?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE en-export SYSTEM "http://xml.evernote.com/pub/evernote-export3.dtd">
3
+ <en-export export-date="${r().format("YYYYMMDDTHHmmssZ")}" application="${e.application||"enex-io"}" version="1.0">`;for(let n of t)a+=`
4
+ <note>
5
+ <title>${d(n.title)}</title>
6
+ <content><![CDATA[<?xml version="1.0" encoding="UTF-8" standalone="no"?>
7
+ <!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">
8
+ <en-note>${D(n.content)}</en-note>]]></content>
9
+ <created>${p(n.created)}</created>
10
+ <updated>${p(n.updated)}</updated>
11
+ ${n.tags.map(o=>`<tag>${d(o)}</tag>`).join("")}
12
+ </note>`;return a+=`
13
+ </en-export>`,a}function h(t){let e=m.load(t,{xmlMode:!0}),i=[];return e("note").each((a,n)=>{let o=e(n),l=o.find("title").text()||"Untitled",f=o.find("content").text(),s=/<en-note[^>]*>([\s\S]*)<\/en-note>/.exec(f),x=s&&s[1]?s[1]:"",g=u(o.find("created").text()),Y=u(o.find("updated").text()),c=[];o.find("tag").each((T,$)=>{c.push(e($).text())}),i.push({title:l,content:x,created:g,updated:Y,tags:c})}),i}function d(t){return t?t.replace(/[<>&'"]/g,e=>{switch(e){case"<":return"&lt;";case">":return"&gt;";case"&":return"&amp;";case"'":return"&apos;";case'"':return"&quot;";default:return e}}):""}function D(t){return t?t.replace(/<br>/g,"<br/>"):""}function p(t){return r(t).isValid()?r(t).format("YYYYMMDDTHHmmssZ"):r().format("YYYYMMDDTHHmmssZ")}function u(t){if(!t)return new Date().toISOString();let e=t.replace(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/,"$1-$2-$3T$4:$5:$6Z");return r(e).isValid()?e:new Date().toISOString()}export{S as generateEnex,h as parseEnex};
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import * as cheerio from 'cheerio';\nimport dayjs from 'dayjs';\nimport { Note, EnexOptions } from './types.js'; // Fix: Added .js\n\nexport function generateEnex(notes: Note[], options: EnexOptions = {}): string {\n const dateStr = dayjs().format('YYYYMMDDTHHmmssZ');\n \n let xml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE en-export SYSTEM \"http://xml.evernote.com/pub/evernote-export3.dtd\">\n<en-export export-date=\"${dateStr}\" application=\"${options.application || 'enex-io'}\" version=\"1.0\">`;\n\n for (const note of notes) {\n xml += `\n <note>\n <title>${escapeXml(note.title)}</title>\n <content><![CDATA[<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<!DOCTYPE en-note SYSTEM \"http://xml.evernote.com/pub/enml2.dtd\">\n<en-note>${formatContent(note.content)}</en-note>]]></content>\n <created>${formatDate(note.created)}</created>\n <updated>${formatDate(note.updated)}</updated>\n ${note.tags.map(tag => `<tag>${escapeXml(tag)}</tag>`).join('')}\n </note>`;\n }\n\n xml += `\\n</en-export>`;\n return xml;\n}\n\nexport function parseEnex(xmlContent: string): Note[] {\n const $ = cheerio.load(xmlContent, { xmlMode: true });\n const notes: Note[] = [];\n\n $('note').each((_, el) => {\n const node = $(el);\n const title = node.find('title').text() || 'Untitled';\n const contentRaw = node.find('content').text();\n \n // Extract inner HTML from the CDATA/en-note wrapper\n const contentMatch = /<en-note[^>]*>([\\s\\S]*)<\\/en-note>/.exec(contentRaw);\n \n // Fix: Strict Check. If match exists, use index 1, else empty string.\n // This solves \"Type undefined is not assignable to string\"\n const content = (contentMatch && contentMatch[1]) ? contentMatch[1] : '';\n\n const created = parseEnexDate(node.find('created').text());\n const updated = parseEnexDate(node.find('updated').text());\n \n const tags: string[] = [];\n \n // Fix: Wrapped in block {} to avoid returning 'number' (array length)\n // This solves \"Type number is not assignable to void\"\n node.find('tag').each((_, tag) => {\n tags.push($(tag).text());\n });\n\n notes.push({ title, content, created, updated, tags });\n });\n\n return notes;\n}\n\n// --- Helpers ---\n\nfunction escapeXml(unsafe: string): string {\n if (!unsafe) return '';\n return unsafe.replace(/[<>&'\"]/g, (c) => {\n switch (c) {\n case '<': return '&lt;';\n case '>': return '&gt;';\n case '&': return '&amp;';\n case '\\'': return '&apos;';\n case '\"': return '&quot;';\n default: return c;\n }\n });\n}\n\nfunction formatContent(html: string): string {\n if (!html) return '';\n return html.replace(/<br>/g, '<br/>');\n}\n\nfunction formatDate(isoString: string): string {\n return dayjs(isoString).isValid() \n ? dayjs(isoString).format('YYYYMMDDTHHmmssZ') \n : dayjs().format('YYYYMMDDTHHmmssZ');\n}\n\nfunction parseEnexDate(enexDate: string): string {\n if (!enexDate) return new Date().toISOString();\n const iso = enexDate.replace(/^(\\d{4})(\\d{2})(\\d{2})T(\\d{2})(\\d{2})(\\d{2})Z$/, '$1-$2-$3T$4:$5:$6Z');\n return dayjs(iso).isValid() ? iso : new Date().toISOString();\n}"],"mappings":"AAAA,UAAYA,MAAa,UACzB,OAAOC,MAAW,QAGX,SAASC,EAAaC,EAAeC,EAAuB,CAAC,EAAW,CAG7E,IAAIC,EAAM;AAAA;AAAA,0BAFMJ,EAAM,EAAE,OAAO,kBAAkB,CAIlB,kBAAkBG,EAAQ,aAAe,SAAS,mBAEjF,QAAWE,KAAQH,EACjBE,GAAO;AAAA;AAAA,aAEEE,EAAUD,EAAK,KAAK,CAAC;AAAA;AAAA;AAAA,WAGvBE,EAAcF,EAAK,OAAO,CAAC;AAAA,eACvBG,EAAWH,EAAK,OAAO,CAAC;AAAA,eACxBG,EAAWH,EAAK,OAAO,CAAC;AAAA,MACjCA,EAAK,KAAK,IAAII,GAAO,QAAQH,EAAUG,CAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC;AAAA,WAIjE,OAAAL,GAAO;AAAA,cACAA,CACT,CAEO,SAASM,EAAUC,EAA4B,CACpD,IAAMC,EAAY,OAAKD,EAAY,CAAE,QAAS,EAAK,CAAC,EAC9CT,EAAgB,CAAC,EAEvB,OAAAU,EAAE,MAAM,EAAE,KAAK,CAACC,EAAGC,IAAO,CACxB,IAAMC,EAAOH,EAAEE,CAAE,EACXE,EAAQD,EAAK,KAAK,OAAO,EAAE,KAAK,GAAK,WACrCE,EAAaF,EAAK,KAAK,SAAS,EAAE,KAAK,EAGvCG,EAAe,qCAAqC,KAAKD,CAAU,EAInEE,EAAWD,GAAgBA,EAAa,CAAC,EAAKA,EAAa,CAAC,EAAI,GAEhEE,EAAUC,EAAcN,EAAK,KAAK,SAAS,EAAE,KAAK,CAAC,EACnDO,EAAUD,EAAcN,EAAK,KAAK,SAAS,EAAE,KAAK,CAAC,EAEnDQ,EAAiB,CAAC,EAIxBR,EAAK,KAAK,KAAK,EAAE,KAAK,CAACF,EAAGJ,IAAQ,CAChCc,EAAK,KAAKX,EAAEH,CAAG,EAAE,KAAK,CAAC,CACzB,CAAC,EAEDP,EAAM,KAAK,CAAE,MAAAc,EAAO,QAAAG,EAAS,QAAAC,EAAS,QAAAE,EAAS,KAAAC,CAAK,CAAC,CACvD,CAAC,EAEMrB,CACT,CAIA,SAASI,EAAUkB,EAAwB,CACzC,OAAKA,EACEA,EAAO,QAAQ,WAAaC,GAAM,CACvC,OAAQA,EAAG,CACT,IAAK,IAAK,MAAO,OACjB,IAAK,IAAK,MAAO,OACjB,IAAK,IAAK,MAAO,QACjB,IAAK,IAAM,MAAO,SAClB,IAAK,IAAK,MAAO,SACjB,QAAS,OAAOA,CAClB,CACF,CAAC,EAVmB,EAWtB,CAEA,SAASlB,EAAcmB,EAAsB,CAC3C,OAAKA,EACEA,EAAK,QAAQ,QAAS,OAAO,EADlB,EAEpB,CAEA,SAASlB,EAAWmB,EAA2B,CAC7C,OAAO3B,EAAM2B,CAAS,EAAE,QAAQ,EAC5B3B,EAAM2B,CAAS,EAAE,OAAO,kBAAkB,EAC1C3B,EAAM,EAAE,OAAO,kBAAkB,CACvC,CAEA,SAASqB,EAAcO,EAA0B,CAC/C,GAAI,CAACA,EAAU,OAAO,IAAI,KAAK,EAAE,YAAY,EAC7C,IAAMC,EAAMD,EAAS,QAAQ,iDAAkD,oBAAoB,EACnG,OAAO5B,EAAM6B,CAAG,EAAE,QAAQ,EAAIA,EAAM,IAAI,KAAK,EAAE,YAAY,CAC7D","names":["cheerio","dayjs","generateEnex","notes","options","xml","note","escapeXml","formatContent","formatDate","tag","parseEnex","xmlContent","$","_","el","node","title","contentRaw","contentMatch","content","created","parseEnexDate","updated","tags","unsafe","c","html","isoString","enexDate","iso"]}
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "enex-io",
3
+ "version": "0.1.0",
4
+ "description": "High-performance parser and generator for Evernote/Apple Notes (.enex) files.",
5
+ "type": "module",
6
+ "bin": {
7
+ "enex-io": "dist/cli.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "exports": {
11
+ ".": "./dist/index.js"
12
+ },
13
+ "types": "./dist/index.d.ts",
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "sideEffects": false,
21
+ "scripts": {
22
+ "test": "node --import=tsx --test test/*.ts",
23
+ "build": "tsup",
24
+ "prepublishOnly": "npm run build && npm test",
25
+ "lint": "xo"
26
+ },
27
+ "keywords": [
28
+ "fs",
29
+ "converter",
30
+ "enex",
31
+ "evernote",
32
+ "apple-notes",
33
+ "parser",
34
+ "xml"
35
+ ],
36
+ "author": {
37
+ "name": "mgks",
38
+ "url": "https://mgks.dev"
39
+ },
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/mgks/enex-io.git"
44
+ },
45
+ "bugs": {
46
+ "url": "https://github.com/mgks/enex-io/issues"
47
+ },
48
+ "homepage": "https://github.com/mgks/enex-io#readme",
49
+ "funding": "https://github.com/sponsors/mgks",
50
+ "dependencies": {
51
+ "cheerio": "^1.1.2",
52
+ "commander": "^11.1.0",
53
+ "dayjs": "^1.11.10"
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^20.0.0",
57
+ "tsup": "^8.0.0",
58
+ "tsx": "^4.0.0",
59
+ "typescript": "^5.0.0",
60
+ "xo": "^0.56.0"
61
+ },
62
+ "xo": {
63
+ "prettier": true,
64
+ "rules": {
65
+ "unicorn/filename-case": "off"
66
+ }
67
+ }
68
+ }