export single note as markdown, #166

This commit is contained in:
azivner 2018-09-03 09:40:22 +02:00
parent 467ad79129
commit 9bdd4437f2
9 changed files with 61 additions and 27 deletions

View File

@ -34,6 +34,7 @@ class Branch extends Entity {
this.origParentNoteId = this.parentNoteId; this.origParentNoteId = this.parentNoteId;
} }
/** @returns {Note|null} */
async getNote() { async getNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]); return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
} }

View File

@ -482,6 +482,13 @@ class Note extends Entity {
return await repository.getEntities("SELECT * FROM branches WHERE isDeleted = 0 AND noteId = ?", [this.noteId]); return await repository.getEntities("SELECT * FROM branches WHERE isDeleted = 0 AND noteId = ?", [this.noteId]);
} }
/**
* @returns {boolean} - true if note has children
*/
async hasChildren() {
return (await this.getChildNotes()).length > 0;
}
/** /**
* @returns {Promise<Note[]>} child notes of this note * @returns {Promise<Note[]>} child notes of this note
*/ */

View File

@ -98,6 +98,8 @@ if (utils.isElectron()) {
}); });
} }
$("#export-note-to-markdown-button").click(() => exportService.exportSubtree(noteDetailService.getCurrentNoteId(), 'markdown-single'));
treeService.showTree(); treeService.showTree();
entrypoints.registerEntrypoints(); entrypoints.registerEntrypoints();

View File

@ -168,13 +168,13 @@ const contextMenuOptions = {
treeChangesService.deleteNodes(treeService.getSelectedNodes(true)); treeChangesService.deleteNodes(treeService.getSelectedNodes(true));
} }
else if (ui.cmd === "exportSubtreeToTar") { else if (ui.cmd === "exportSubtreeToTar") {
exportService.exportSubtree(node.data.noteId, 'tar'); exportService.exportSubtree(node.data.branchId, 'tar');
} }
else if (ui.cmd === "exportSubtreeToOpml") { else if (ui.cmd === "exportSubtreeToOpml") {
exportService.exportSubtree(node.data.noteId, 'opml'); exportService.exportSubtree(node.data.branchId, 'opml');
} }
else if (ui.cmd === "exportSubtreeToMarkdown") { else if (ui.cmd === "exportSubtreeToMarkdown") {
exportService.exportSubtree(node.data.noteId, 'markdown'); exportService.exportSubtree(node.data.branchId, 'markdown');
} }
else if (ui.cmd === "importIntoNote") { else if (ui.cmd === "importIntoNote") {
exportService.importIntoNote(node.data.noteId); exportService.importIntoNote(node.data.noteId);

View File

@ -1,28 +1,29 @@
"use strict"; "use strict";
const sql = require('../../services/sql');
const html = require('html'); const html = require('html');
const tar = require('tar-stream'); const tar = require('tar-stream');
const sanitize = require("sanitize-filename"); const sanitize = require("sanitize-filename");
const repository = require("../../services/repository"); const repository = require("../../services/repository");
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const commonmark = require('commonmark');
const TurndownService = require('turndown'); const TurndownService = require('turndown');
async function exportNote(req, res) { async function exportNote(req, res) {
const noteId = req.params.noteId; // entityId maybe either noteId or branchId depending on format
const entityId = req.params.entityId;
const format = req.params.format; const format = req.params.format;
const branchId = await sql.getValue('SELECT branchId FROM branches WHERE noteId = ?', [noteId]);
if (format === 'tar') { if (format === 'tar') {
await exportToTar(branchId, res); await exportToTar(await repository.getBranch(entityId), res);
} }
else if (format === 'opml') { else if (format === 'opml') {
await exportToOpml(branchId, res); await exportToOpml(await repository.getBranch(entityId), res);
} }
else if (format === 'markdown') { else if (format === 'markdown') {
await exportToMarkdown(branchId, res); await exportToMarkdown(await repository.getBranch(entityId), res);
}
// export single note without subtree
else if (format === 'markdown-single') {
await exportSingleMarkdown(await repository.getNote(entityId), res);
} }
else { else {
return [404, "Unrecognized export format " + format]; return [404, "Unrecognized export format " + format];
@ -48,8 +49,7 @@ function prepareText(text) {
return escaped.replace(/\n/g, '&#10;'); return escaped.replace(/\n/g, '&#10;');
} }
async function exportToOpml(branchId, res) { async function exportToOpml(branch, res) {
const branch = await repository.getBranch(branchId);
const note = await branch.getNote(); const note = await branch.getNote();
const title = (branch.prefix ? (branch.prefix + ' - ') : '') + note.title; const title = (branch.prefix ? (branch.prefix + ' - ') : '') + note.title;
const sanitizedTitle = sanitize(title); const sanitizedTitle = sanitize(title);
@ -81,18 +81,18 @@ async function exportToOpml(branchId, res) {
</head> </head>
<body>`); <body>`);
await exportNoteInner(branchId); await exportNoteInner(branch.branchId);
res.write(`</body> res.write(`</body>
</opml>`); </opml>`);
res.end(); res.end();
} }
async function exportToTar(branchId, res) { async function exportToTar(branch, res) {
const pack = tar.pack(); const pack = tar.pack();
const exportedNoteIds = []; const exportedNoteIds = [];
const name = await exportNoteInner(branchId, ''); const name = await exportNoteInner(branch.branchId, '');
async function exportNoteInner(branchId, directory) { async function exportNoteInner(branchId, directory) {
const branch = await repository.getBranch(branchId); const branch = await repository.getBranch(branchId);
@ -165,14 +165,20 @@ async function exportToTar(branchId, res) {
pack.pipe(res); pack.pipe(res);
} }
async function exportToMarkdown(branchId, res) { async function exportToMarkdown(branch, res) {
const note = await branch.getNote();
if (!await note.hasChildren()) {
await exportSingleMarkdown(note, res);
return;
}
const turndownService = new TurndownService(); const turndownService = new TurndownService();
const pack = tar.pack(); const pack = tar.pack();
const name = await exportNoteInner(branchId, ''); const name = await exportNoteInner(note, '');
async function exportNoteInner(branchId, directory) { async function exportNoteInner(note, directory) {
const branch = await repository.getBranch(branchId);
const note = await branch.getNote();
const childFileName = directory + sanitize(note.title); const childFileName = directory + sanitize(note.title);
if (await note.hasLabel('excludeFromExport')) { if (await note.hasLabel('excludeFromExport')) {
@ -181,8 +187,8 @@ async function exportToMarkdown(branchId, res) {
saveDataFile(childFileName, note); saveDataFile(childFileName, note);
for (const child of await note.getChildBranches()) { for (const childNote of await note.getChildNotes()) {
await exportNoteInner(child.branchId, childFileName + "/"); await exportNoteInner(childNote, childFileName + "/");
} }
return childFileName; return childFileName;
@ -221,6 +227,17 @@ async function exportToMarkdown(branchId, res) {
pack.pipe(res); pack.pipe(res);
} }
async function exportSingleMarkdown(note, res) {
const turndownService = new TurndownService();
const markdown = turndownService.turndown(note.content);
const name = sanitize(note.title);
res.setHeader('Content-Disposition', 'file; filename="' + name + '.md"');
res.setHeader('Content-Type', 'text/markdown; charset=UTF-8');
res.send(markdown);
}
module.exports = { module.exports = {
exportNote exportNote
}; };

View File

@ -8,6 +8,7 @@ const tar = require('tar-stream');
const stream = require('stream'); const stream = require('stream');
const path = require('path'); const path = require('path');
const parseString = require('xml2js').parseString; const parseString = require('xml2js').parseString;
const commonmark = require('commonmark');
async function importToBranch(req) { async function importToBranch(req) {
const parentNoteId = req.params.parentNoteId; const parentNoteId = req.params.parentNoteId;
@ -178,11 +179,11 @@ async function parseImportFile(file) {
async function importNotes(files, parentNoteId, noteIdMap, attributes) { async function importNotes(files, parentNoteId, noteIdMap, attributes) {
for (const file of files) { for (const file of files) {
if (file.meta.version !== 1) { if (file.meta && file.meta.version !== 1) {
throw new Error("Can't read meta data version " + file.meta.version); throw new Error("Can't read meta data version " + file.meta.version);
} }
if (file.meta.clone) { if (file.meta && file.meta.clone) {
await new Branch({ await new Branch({
parentNoteId: parentNoteId, parentNoteId: parentNoteId,
noteId: noteIdMap[file.meta.noteId], noteId: noteIdMap[file.meta.noteId],
@ -192,7 +193,7 @@ async function importNotes(files, parentNoteId, noteIdMap, attributes) {
return; return;
} }
if (file.meta.type !== 'file') { if (!file.meta || file.meta.type !== 'file') {
file.data = file.data.toString("UTF-8"); file.data = file.data.toString("UTF-8");
} }

View File

@ -124,7 +124,7 @@ function register(app) {
apiRoute(PUT, '/api/notes/:noteId/clone-to/:parentNoteId', cloningApiRoute.cloneNoteToParent); apiRoute(PUT, '/api/notes/:noteId/clone-to/:parentNoteId', cloningApiRoute.cloneNoteToParent);
apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter); apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter);
route(GET, '/api/notes/:noteId/export/:format', [auth.checkApiAuthOrElectron], exportRoute.exportNote); route(GET, '/api/notes/:entityId/export/:format', [auth.checkApiAuthOrElectron], exportRoute.exportNote);
route(POST, '/api/notes/:parentNoteId/import', [auth.checkApiAuthOrElectron, uploadMiddleware], importRoute.importToBranch, apiResultHandler); route(POST, '/api/notes/:parentNoteId/import', [auth.checkApiAuthOrElectron, uploadMiddleware], importRoute.importToBranch, apiResultHandler);
route(POST, '/api/notes/:parentNoteId/upload', [auth.checkApiAuthOrElectron, uploadMiddleware], route(POST, '/api/notes/:parentNoteId/upload', [auth.checkApiAuthOrElectron, uploadMiddleware],

View File

@ -36,22 +36,27 @@ async function getEntity(query, params = []) {
return entityConstructor.createEntityFromRow(row); return entityConstructor.createEntityFromRow(row);
} }
/** @returns {Note|null} */
async function getNote(noteId) { async function getNote(noteId) {
return await getEntity("SELECT * FROM notes WHERE noteId = ?", [noteId]); return await getEntity("SELECT * FROM notes WHERE noteId = ?", [noteId]);
} }
/** @returns {Branch|null} */
async function getBranch(branchId) { async function getBranch(branchId) {
return await getEntity("SELECT * FROM branches WHERE branchId = ?", [branchId]); return await getEntity("SELECT * FROM branches WHERE branchId = ?", [branchId]);
} }
/** @returns {Image|null} */
async function getImage(imageId) { async function getImage(imageId) {
return await getEntity("SELECT * FROM images WHERE imageId = ?", [imageId]); return await getEntity("SELECT * FROM images WHERE imageId = ?", [imageId]);
} }
/** @returns {Attribute|null} */
async function getAttribute(attributeId) { async function getAttribute(attributeId) {
return await getEntity("SELECT * FROM attributes WHERE attributeId = ?", [attributeId]); return await getEntity("SELECT * FROM attributes WHERE attributeId = ?", [attributeId]);
} }
/** @returns {Option|null} */
async function getOption(name) { async function getOption(name) {
return await getEntity("SELECT * FROM options WHERE name = ?", [name]); return await getEntity("SELECT * FROM options WHERE name = ?", [name]);
} }

View File

@ -167,6 +167,7 @@
<li><a class="show-attributes-button"><kbd>Alt+A</kbd> Attributes</a></li> <li><a class="show-attributes-button"><kbd>Alt+A</kbd> Attributes</a></li>
<li><a id="show-source-button">HTML source</a></li> <li><a id="show-source-button">HTML source</a></li>
<li><a id="upload-file-button">Upload file</a></li> <li><a id="upload-file-button">Upload file</a></li>
<li><a id="export-note-to-markdown-button">Export as markdown</a></li>
</ul> </ul>
</div> </div>
</div> </div>