From f47ae12019f6f53e3302c6c582aee04038153350 Mon Sep 17 00:00:00 2001 From: azivner Date: Sun, 27 May 2018 12:26:34 -0400 Subject: [PATCH] OPML export support (issue #78), import missing for now --- .../javascripts/services/context_menu.js | 13 +++- src/public/javascripts/services/export.js | 6 +- src/public/javascripts/services/tree.js | 6 ++ src/routes/api/export.js | 73 +++++++++++++++++++ src/routes/routes.js | 2 +- src/services/utils.js | 7 +- 6 files changed, 98 insertions(+), 9 deletions(-) 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(`\n`); + + for (const child of await note.getChildBranches()) { + await exportNoteInner(child.branchId); + } + + res.write(''); + } + + res.setHeader('Content-Disposition', 'file; filename="' + sanitizedTitle + '.opml"'); + res.setHeader('Content-Type', 'text/x-opml'); + + res.write(` + + +Trilium export + +`); + + await exportNoteInner(branchId); + + res.write(` +`); + res.end(); +} + +async function exportToTar(branchId, res) { const pack = tar.pack(); const exportedNoteIds = []; diff --git a/src/routes/routes.js b/src/routes/routes.js index a3f79be16..0204f9f5e 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -122,7 +122,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', [auth.checkApiAuthOrElectron], exportRoute.exportNote); + route(GET, '/api/notes/:noteId/export/:format', [auth.checkApiAuthOrElectron], exportRoute.exportNote); route(POST, '/api/notes/:parentNoteId/import', [auth.checkApiAuthOrElectron, uploadMiddleware], importRoute.importTar, apiResultHandler); route(POST, '/api/notes/:parentNoteId/upload', [auth.checkApiAuthOrElectron, uploadMiddleware], diff --git a/src/services/utils.js b/src/services/utils.js index 148361e26..d2739ef48 100644 --- a/src/services/utils.js +++ b/src/services/utils.js @@ -75,6 +75,10 @@ function toObject(array, fn) { return obj; } +function stripTags(text) { + return text.replace(/<(?:.|\n)*?>/gm, ''); +} + module.exports = { randomSecureToken, randomString, @@ -88,5 +92,6 @@ module.exports = { sanitizeSql, stopWatch, unescapeHtml, - toObject + toObject, + stripTags }; \ No newline at end of file