From 9bdd4437f22e3311adb7ae13f3fc400be0779749 Mon Sep 17 00:00:00 2001 From: azivner Date: Mon, 3 Sep 2018 09:40:22 +0200 Subject: [PATCH] export single note as markdown, #166 --- src/entities/branch.js | 1 + src/entities/note.js | 7 +++ src/public/javascripts/services/bootstrap.js | 2 + .../javascripts/services/tree_context_menu.js | 6 +- src/routes/api/export.js | 57 ++++++++++++------- src/routes/api/import.js | 7 ++- src/routes/routes.js | 2 +- src/services/repository.js | 5 ++ src/views/index.ejs | 1 + 9 files changed, 61 insertions(+), 27 deletions(-) diff --git a/src/entities/branch.js b/src/entities/branch.js index b4d509eee..6019fe99a 100644 --- a/src/entities/branch.js +++ b/src/entities/branch.js @@ -34,6 +34,7 @@ class Branch extends Entity { this.origParentNoteId = this.parentNoteId; } + /** @returns {Note|null} */ async getNote() { return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]); } diff --git a/src/entities/note.js b/src/entities/note.js index 4c2c09aec..005ef1ad8 100644 --- a/src/entities/note.js +++ b/src/entities/note.js @@ -482,6 +482,13 @@ class Note extends Entity { 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} child notes of this note */ diff --git a/src/public/javascripts/services/bootstrap.js b/src/public/javascripts/services/bootstrap.js index 5064937bf..3fc7b0a68 100644 --- a/src/public/javascripts/services/bootstrap.js +++ b/src/public/javascripts/services/bootstrap.js @@ -98,6 +98,8 @@ if (utils.isElectron()) { }); } +$("#export-note-to-markdown-button").click(() => exportService.exportSubtree(noteDetailService.getCurrentNoteId(), 'markdown-single')); + treeService.showTree(); entrypoints.registerEntrypoints(); diff --git a/src/public/javascripts/services/tree_context_menu.js b/src/public/javascripts/services/tree_context_menu.js index 60f81f22d..24a4b3d0a 100644 --- a/src/public/javascripts/services/tree_context_menu.js +++ b/src/public/javascripts/services/tree_context_menu.js @@ -168,13 +168,13 @@ const contextMenuOptions = { treeChangesService.deleteNodes(treeService.getSelectedNodes(true)); } else if (ui.cmd === "exportSubtreeToTar") { - exportService.exportSubtree(node.data.noteId, 'tar'); + exportService.exportSubtree(node.data.branchId, 'tar'); } else if (ui.cmd === "exportSubtreeToOpml") { - exportService.exportSubtree(node.data.noteId, 'opml'); + exportService.exportSubtree(node.data.branchId, 'opml'); } else if (ui.cmd === "exportSubtreeToMarkdown") { - exportService.exportSubtree(node.data.noteId, 'markdown'); + exportService.exportSubtree(node.data.branchId, 'markdown'); } else if (ui.cmd === "importIntoNote") { exportService.importIntoNote(node.data.noteId); diff --git a/src/routes/api/export.js b/src/routes/api/export.js index f3c64cd8c..c73cfabfc 100644 --- a/src/routes/api/export.js +++ b/src/routes/api/export.js @@ -1,28 +1,29 @@ "use strict"; -const sql = require('../../services/sql'); const html = require('html'); const tar = require('tar-stream'); const sanitize = require("sanitize-filename"); const repository = require("../../services/repository"); const utils = require('../../services/utils'); -const commonmark = require('commonmark'); const TurndownService = require('turndown'); 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 branchId = await sql.getValue('SELECT branchId FROM branches WHERE noteId = ?', [noteId]); - if (format === 'tar') { - await exportToTar(branchId, res); + await exportToTar(await repository.getBranch(entityId), res); } else if (format === 'opml') { - await exportToOpml(branchId, res); + await exportToOpml(await repository.getBranch(entityId), res); } 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 { return [404, "Unrecognized export format " + format]; @@ -48,8 +49,7 @@ function prepareText(text) { return escaped.replace(/\n/g, ' '); } -async function exportToOpml(branchId, res) { - const branch = await repository.getBranch(branchId); +async function exportToOpml(branch, res) { const note = await branch.getNote(); const title = (branch.prefix ? (branch.prefix + ' - ') : '') + note.title; const sanitizedTitle = sanitize(title); @@ -81,18 +81,18 @@ async function exportToOpml(branchId, res) { `); - await exportNoteInner(branchId); + await exportNoteInner(branch.branchId); res.write(` `); res.end(); } -async function exportToTar(branchId, res) { +async function exportToTar(branch, res) { const pack = tar.pack(); const exportedNoteIds = []; - const name = await exportNoteInner(branchId, ''); + const name = await exportNoteInner(branch.branchId, ''); async function exportNoteInner(branchId, directory) { const branch = await repository.getBranch(branchId); @@ -165,14 +165,20 @@ async function exportToTar(branchId, 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 pack = tar.pack(); - const name = await exportNoteInner(branchId, ''); + const name = await exportNoteInner(note, ''); - async function exportNoteInner(branchId, directory) { - const branch = await repository.getBranch(branchId); - const note = await branch.getNote(); + async function exportNoteInner(note, directory) { const childFileName = directory + sanitize(note.title); if (await note.hasLabel('excludeFromExport')) { @@ -181,8 +187,8 @@ async function exportToMarkdown(branchId, res) { saveDataFile(childFileName, note); - for (const child of await note.getChildBranches()) { - await exportNoteInner(child.branchId, childFileName + "/"); + for (const childNote of await note.getChildNotes()) { + await exportNoteInner(childNote, childFileName + "/"); } return childFileName; @@ -221,6 +227,17 @@ async function exportToMarkdown(branchId, 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 = { exportNote }; \ No newline at end of file diff --git a/src/routes/api/import.js b/src/routes/api/import.js index 3909ea2a0..7bced9ffd 100644 --- a/src/routes/api/import.js +++ b/src/routes/api/import.js @@ -8,6 +8,7 @@ const tar = require('tar-stream'); const stream = require('stream'); const path = require('path'); const parseString = require('xml2js').parseString; +const commonmark = require('commonmark'); async function importToBranch(req) { const parentNoteId = req.params.parentNoteId; @@ -178,11 +179,11 @@ async function parseImportFile(file) { async function importNotes(files, parentNoteId, noteIdMap, attributes) { 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); } - if (file.meta.clone) { + if (file.meta && file.meta.clone) { await new Branch({ parentNoteId: parentNoteId, noteId: noteIdMap[file.meta.noteId], @@ -192,7 +193,7 @@ async function importNotes(files, parentNoteId, noteIdMap, attributes) { return; } - if (file.meta.type !== 'file') { + if (!file.meta || file.meta.type !== 'file') { file.data = file.data.toString("UTF-8"); } diff --git a/src/routes/routes.js b/src/routes/routes.js index c3a9356e7..e7eac308f 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -124,7 +124,7 @@ function register(app) { apiRoute(PUT, '/api/notes/:noteId/clone-to/:parentNoteId', cloningApiRoute.cloneNoteToParent); 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/upload', [auth.checkApiAuthOrElectron, uploadMiddleware], diff --git a/src/services/repository.js b/src/services/repository.js index 2bd1ca453..7021bbe09 100644 --- a/src/services/repository.js +++ b/src/services/repository.js @@ -36,22 +36,27 @@ async function getEntity(query, params = []) { return entityConstructor.createEntityFromRow(row); } +/** @returns {Note|null} */ async function getNote(noteId) { return await getEntity("SELECT * FROM notes WHERE noteId = ?", [noteId]); } +/** @returns {Branch|null} */ async function getBranch(branchId) { return await getEntity("SELECT * FROM branches WHERE branchId = ?", [branchId]); } +/** @returns {Image|null} */ async function getImage(imageId) { return await getEntity("SELECT * FROM images WHERE imageId = ?", [imageId]); } +/** @returns {Attribute|null} */ async function getAttribute(attributeId) { return await getEntity("SELECT * FROM attributes WHERE attributeId = ?", [attributeId]); } +/** @returns {Option|null} */ async function getOption(name) { return await getEntity("SELECT * FROM options WHERE name = ?", [name]); } diff --git a/src/views/index.ejs b/src/views/index.ejs index 88ad66f98..db647dd6b 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -167,6 +167,7 @@
  • Alt+A Attributes
  • HTML source
  • Upload file
  • +
  • Export as markdown