mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	OPML export support (issue #78), import missing for now
This commit is contained in:
		| @@ -94,13 +94,15 @@ const contextMenuOptions = { | |||||||
|         {title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"}, |         {title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"}, | ||||||
|         {title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"}, |         {title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"}, | ||||||
|         {title: "----"}, |         {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: "Import into branch", cmd: "importBranch", uiIcon: "ui-icon-arrowthick-1-sw"}, | ||||||
|         {title: "----"}, |         {title: "----"}, | ||||||
|         {title: "Collapse branch <kbd>Alt+-</kbd>", cmd: "collapseBranch", uiIcon: "ui-icon-minus"}, |         {title: "Collapse branch <kbd>Alt+-</kbd>", cmd: "collapseBranch", uiIcon: "ui-icon-minus"}, | ||||||
|         {title: "Force note sync", cmd: "forceNoteSync", uiIcon: "ui-icon-refresh"}, |         {title: "Force note sync", cmd: "forceNoteSync", uiIcon: "ui-icon-refresh"}, | ||||||
|         {title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sortAlphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"} |         {title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sortAlphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"} | ||||||
|  |  | ||||||
|     ], |     ], | ||||||
|     beforeOpen: async (event, ui) => { |     beforeOpen: async (event, ui) => { | ||||||
|         const node = $.ui.fancytree.getNode(ui.target); |         const node = $.ui.fancytree.getNode(ui.target); | ||||||
| @@ -163,8 +165,11 @@ const contextMenuOptions = { | |||||||
|         else if (ui.cmd === "delete") { |         else if (ui.cmd === "delete") { | ||||||
|             treeChangesService.deleteNodes(treeService.getSelectedNodes(true)); |             treeChangesService.deleteNodes(treeService.getSelectedNodes(true)); | ||||||
|         } |         } | ||||||
|         else if (ui.cmd === "exportBranch") { |         else if (ui.cmd === "exportBranchToTar") { | ||||||
|             exportService.exportBranch(node.data.noteId); |             exportService.exportBranch(node.data.noteId, 'tar'); | ||||||
|  |         } | ||||||
|  |         else if (ui.cmd === "exportBranchToOpml") { | ||||||
|  |             exportService.exportBranch(node.data.noteId, 'opml'); | ||||||
|         } |         } | ||||||
|         else if (ui.cmd === "importBranch") { |         else if (ui.cmd === "importBranch") { | ||||||
|             exportService.importBranch(node.data.noteId); |             exportService.importBranch(node.data.noteId); | ||||||
|   | |||||||
| @@ -3,9 +3,9 @@ import protectedSessionHolder from './protected_session_holder.js'; | |||||||
| import utils from './utils.js'; | import utils from './utils.js'; | ||||||
| import server from './server.js'; | import server from './server.js'; | ||||||
|  |  | ||||||
| function exportBranch(noteId) { | function exportBranch(noteId, format) { | ||||||
|     const url = utils.getHost() + "/api/notes/" + noteId + "/export?protectedSessionId=" |     const url = utils.getHost() + "/api/notes/" + noteId + "/export/" + format + | ||||||
|         + encodeURIComponent(protectedSessionHolder.getProtectedSessionId()); |         "?protectedSessionId=" + encodeURIComponent(protectedSessionHolder.getProtectedSessionId()); | ||||||
|  |  | ||||||
|     utils.download(url); |     utils.download(url); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -86,6 +86,10 @@ async function expandToNote(notePath, expandOpts) { | |||||||
|     for (const childNoteId of runPath) { |     for (const childNoteId of runPath) { | ||||||
|         const node = getNodesByNoteId(childNoteId).find(node => node.data.parentNoteId === parentNoteId); |         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) { |         if (childNoteId === noteId) { | ||||||
|             return node; |             return node; | ||||||
|         } |         } | ||||||
| @@ -154,6 +158,8 @@ async function getRunPath(notePath) { | |||||||
|                         for (const noteId of pathToRoot) { |                         for (const noteId of pathToRoot) { | ||||||
|                             effectivePath.push(noteId); |                             effectivePath.push(noteId); | ||||||
|                         } |                         } | ||||||
|  |  | ||||||
|  |                         effectivePath.push('root'); | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|                     break; |                     break; | ||||||
|   | |||||||
| @@ -5,12 +5,85 @@ 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'); | ||||||
|  |  | ||||||
| async function exportNote(req, res) { | async function exportNote(req, res) { | ||||||
|     const noteId = req.params.noteId; |     const noteId = req.params.noteId; | ||||||
|  |     const format = req.params.format; | ||||||
|  |  | ||||||
|     const branchId = await sql.getValue('SELECT branchId FROM branches WHERE noteId = ?', [noteId]); |     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, '"') | ||||||
|  |         .replace(/'/g, '''); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function prepareText(text) { | ||||||
|  |     const newLines = text.replace(/(<p[^>]*>|<br\s*\/?>)/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(`<outline title="${preparedTitle}" text="${preparedContent}">\n`); | ||||||
|  |  | ||||||
|  |         for (const child of await note.getChildBranches()) { | ||||||
|  |             await exportNoteInner(child.branchId); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         res.write('</outline>'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     res.setHeader('Content-Disposition', 'file; filename="' + sanitizedTitle + '.opml"'); | ||||||
|  |     res.setHeader('Content-Type', 'text/x-opml'); | ||||||
|  |  | ||||||
|  |     res.write(`<?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <opml version="1.0"> | ||||||
|  | <head> | ||||||
|  | <title>Trilium export</title> | ||||||
|  | </head> | ||||||
|  | <body>`); | ||||||
|  |  | ||||||
|  |     await exportNoteInner(branchId); | ||||||
|  |  | ||||||
|  |     res.write(`</body> | ||||||
|  | </opml>`); | ||||||
|  |     res.end(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function exportToTar(branchId, res) { | ||||||
|     const pack = tar.pack(); |     const pack = tar.pack(); | ||||||
|  |  | ||||||
|     const exportedNoteIds = []; |     const exportedNoteIds = []; | ||||||
|   | |||||||
| @@ -122,7 +122,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', [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/import', [auth.checkApiAuthOrElectron, uploadMiddleware], importRoute.importTar, apiResultHandler); | ||||||
|  |  | ||||||
|     route(POST, '/api/notes/:parentNoteId/upload', [auth.checkApiAuthOrElectron, uploadMiddleware], |     route(POST, '/api/notes/:parentNoteId/upload', [auth.checkApiAuthOrElectron, uploadMiddleware], | ||||||
|   | |||||||
| @@ -75,6 +75,10 @@ function toObject(array, fn) { | |||||||
|     return obj; |     return obj; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function stripTags(text) { | ||||||
|  |     return text.replace(/<(?:.|\n)*?>/gm, ''); | ||||||
|  | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     randomSecureToken, |     randomSecureToken, | ||||||
|     randomString, |     randomString, | ||||||
| @@ -88,5 +92,6 @@ module.exports = { | |||||||
|     sanitizeSql, |     sanitizeSql, | ||||||
|     stopWatch, |     stopWatch, | ||||||
|     unescapeHtml, |     unescapeHtml, | ||||||
|     toObject |     toObject, | ||||||
|  |     stripTags | ||||||
| }; | }; | ||||||
		Reference in New Issue
	
	Block a user