diff --git a/src/public/javascripts/services/context_menu.js b/src/public/javascripts/services/context_menu.js index dd01e8e70..0fbde248e 100644 --- a/src/public/javascripts/services/context_menu.js +++ b/src/public/javascripts/services/context_menu.js @@ -94,13 +94,15 @@ const contextMenuOptions = { {title: "Paste into Ctrl+V", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"}, {title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"}, {title: "----"}, - {title: "Export branch", cmd: "exportBranch", uiIcon: " ui-icon-arrowthick-1-ne"}, + {title: "Export branch", cmd: "exportBranch", uiIcon: " ui-icon-arrowthick-1-ne", children: [ + {title: "Native Tar", cmd: "exportBranchToTar"}, + {title: "OPML", cmd: "exportBranchToOpml"} + ]}, {title: "Import into branch", cmd: "importBranch", uiIcon: "ui-icon-arrowthick-1-sw"}, {title: "----"}, {title: "Collapse branch Alt+-", cmd: "collapseBranch", uiIcon: "ui-icon-minus"}, {title: "Force note sync", cmd: "forceNoteSync", uiIcon: "ui-icon-refresh"}, {title: "Sort alphabetically Alt+S", cmd: "sortAlphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"} - ], beforeOpen: async (event, ui) => { const node = $.ui.fancytree.getNode(ui.target); @@ -163,8 +165,11 @@ const contextMenuOptions = { else if (ui.cmd === "delete") { treeChangesService.deleteNodes(treeService.getSelectedNodes(true)); } - else if (ui.cmd === "exportBranch") { - exportService.exportBranch(node.data.noteId); + else if (ui.cmd === "exportBranchToTar") { + exportService.exportBranch(node.data.noteId, 'tar'); + } + else if (ui.cmd === "exportBranchToOpml") { + exportService.exportBranch(node.data.noteId, 'opml'); } else if (ui.cmd === "importBranch") { exportService.importBranch(node.data.noteId); diff --git a/src/public/javascripts/services/export.js b/src/public/javascripts/services/export.js index 8cd954a3c..de4c795f7 100644 --- a/src/public/javascripts/services/export.js +++ b/src/public/javascripts/services/export.js @@ -3,9 +3,9 @@ import protectedSessionHolder from './protected_session_holder.js'; import utils from './utils.js'; import server from './server.js'; -function exportBranch(noteId) { - const url = utils.getHost() + "/api/notes/" + noteId + "/export?protectedSessionId=" - + encodeURIComponent(protectedSessionHolder.getProtectedSessionId()); +function exportBranch(noteId, format) { + const url = utils.getHost() + "/api/notes/" + noteId + "/export/" + format + + "?protectedSessionId=" + encodeURIComponent(protectedSessionHolder.getProtectedSessionId()); utils.download(url); } diff --git a/src/public/javascripts/services/tree.js b/src/public/javascripts/services/tree.js index 14969eec5..ada3c78f8 100644 --- a/src/public/javascripts/services/tree.js +++ b/src/public/javascripts/services/tree.js @@ -86,6 +86,10 @@ async function expandToNote(notePath, expandOpts) { for (const childNoteId of runPath) { const node = getNodesByNoteId(childNoteId).find(node => node.data.parentNoteId === parentNoteId); + if (!node) { + console.log(`Can't find node for noteId=${childNoteId} with parentNoteId=${parentNoteId}`); + } + if (childNoteId === noteId) { return node; } @@ -154,6 +158,8 @@ async function getRunPath(notePath) { for (const noteId of pathToRoot) { effectivePath.push(noteId); } + + effectivePath.push('root'); } break; diff --git a/src/routes/api/export.js b/src/routes/api/export.js index 414fbf6a7..9fb1217bb 100644 --- a/src/routes/api/export.js +++ b/src/routes/api/export.js @@ -5,12 +5,85 @@ const html = require('html'); const tar = require('tar-stream'); const sanitize = require("sanitize-filename"); const repository = require("../../services/repository"); +const utils = require('../../services/utils'); async function exportNote(req, res) { const noteId = req.params.noteId; + const format = req.params.format; const branchId = await sql.getValue('SELECT branchId FROM branches WHERE noteId = ?', [noteId]); + if (format === 'tar') { + await exportToTar(branchId, res); + } + else if (format === 'opml') { + await exportToOpml(branchId, res); + } + else { + return [404, "Unrecognized export format " + format]; + } +} + +function escapeXmlAttribute(text) { + return text.replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function prepareText(text) { + const newLines = text.replace(/(
]*>|
)/g, '\n')
+ .replace(/ /g, ' '); // nbsp isn't in XML standard (only HTML)
+
+ const stripped = utils.stripTags(newLines);
+
+ const escaped = escapeXmlAttribute(stripped);
+
+ return escaped.replace(/\n/g, '
');
+}
+
+async function exportToOpml(branchId, res) {
+ const branch = await repository.getBranch(branchId);
+ const note = await branch.getNote();
+ const title = (branch.prefix ? (branch.prefix + ' - ') : '') + note.title;
+ const sanitizedTitle = sanitize(title);
+
+ async function exportNoteInner(branchId) {
+ const branch = await repository.getBranch(branchId);
+ const note = await branch.getNote();
+ const title = (branch.prefix ? (branch.prefix + ' - ') : '') + note.title;
+
+ const preparedTitle = prepareText(title);
+ const preparedContent = prepareText(note.content);
+
+ res.write(`