From 83da07a9417e9c7eb3db3959b798c37f1a7dbd83 Mon Sep 17 00:00:00 2001 From: Nikolay Lopin Date: Sun, 17 Dec 2017 21:39:34 +0300 Subject: [PATCH] Export note with local images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Looks through the note and searches for local images. Copies them to ‘images’ folder in the export path and replaces affected links. #1261 --- browser/components/MarkdownPreview.js | 32 ----------- browser/main/Detail/MarkdownNoteDetail.js | 39 +++++++++++++ browser/main/lib/dataApi/exportImage.js | 37 +++++++++++++ browser/main/lib/dataApi/exportNote.js | 67 +++++++++++++++++++++++ 4 files changed, 143 insertions(+), 32 deletions(-) mode change 100644 => 100755 browser/components/MarkdownPreview.js mode change 100644 => 100755 browser/main/Detail/MarkdownNoteDetail.js create mode 100755 browser/main/lib/dataApi/exportImage.js create mode 100755 browser/main/lib/dataApi/exportNote.js diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js old mode 100644 new mode 100755 index a3e7bb93..c2693312 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -8,7 +8,6 @@ import Raphael from 'raphael' import flowchart from 'flowchart' import SequenceDiagram from 'js-sequence-diagrams' import eventEmitter from 'browser/main/lib/eventEmitter' -import fs from 'fs' import htmlTextHelper from 'browser/lib/htmlTextHelper' import copy from 'copy-to-clipboard' import mdurl from 'mdurl' @@ -116,8 +115,6 @@ export default class MarkdownPreview extends React.Component { this.mouseUpHandler = (e) => this.handleMouseUp(e) this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e) this.checkboxClickHandler = (e) => this.handleCheckboxClick(e) - this.saveAsTextHandler = () => this.handleSaveAsText() - this.saveAsMdHandler = () => this.handleSaveAsMd() this.printHandler = () => this.handlePrint() this.linkClickHandler = this.handlelinkClick.bind(this) @@ -165,35 +162,10 @@ export default class MarkdownPreview extends React.Component { if (this.props.onMouseUp != null) this.props.onMouseUp(e) } - handleSaveAsText () { - this.exportAsDocument('txt') - } - - handleSaveAsMd () { - this.exportAsDocument('md') - } - handlePrint () { this.refs.root.contentWindow.print() } - exportAsDocument (fileType) { - const options = { - filters: [ - { name: 'Documents', extensions: [fileType] } - ], - properties: ['openFile', 'createDirectory'] - } - dialog.showSaveDialog(remote.getCurrentWindow(), options, - (filename) => { - if (filename) { - fs.writeFile(filename, this.props.value, (err) => { - if (err) throw err - }) - } - }) - } - fixDecodedURI (node) { if (node && node.children.length === 1 && typeof node.children[0] === 'string') { const { innerText, href } = node @@ -221,8 +193,6 @@ export default class MarkdownPreview extends React.Component { this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler) this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler) this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler) - eventEmitter.on('export:save-text', this.saveAsTextHandler) - eventEmitter.on('export:save-md', this.saveAsMdHandler) eventEmitter.on('print', this.printHandler) } @@ -232,8 +202,6 @@ export default class MarkdownPreview extends React.Component { this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler) this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler) this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler) - eventEmitter.off('export:save-text', this.saveAsTextHandler) - eventEmitter.off('export:save-md', this.saveAsMdHandler) eventEmitter.off('print', this.printHandler) } diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js old mode 100644 new mode 100755 index 25c993d0..956efb63 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -23,6 +23,7 @@ import InfoPanelTrashed from './InfoPanelTrashed' import { formatDate } from 'browser/lib/date-formatter' import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus' import striptags from 'striptags' +import exportNote from 'browser/main/lib/dataApi/exportNote' const electron = require('electron') const { remote } = electron @@ -44,6 +45,8 @@ class MarkdownNoteDetail extends React.Component { this.dispatchTimer = null this.toggleLockButton = this.handleToggleLockButton.bind(this) + this.saveAsText = this.handleSaveAsText.bind(this) + this.saveAsMd = this.handleSaveAsMd.bind(this) } focus () { @@ -52,6 +55,8 @@ class MarkdownNoteDetail extends React.Component { componentDidMount () { ee.on('topbar:togglelockbutton', this.toggleLockButton) + ee.on('export:save-text', this.saveAsText) + ee.on('export:save-md', this.saveAsMd) } componentWillReceiveProps (nextProps) { @@ -72,6 +77,8 @@ class MarkdownNoteDetail extends React.Component { componentDidUnmount () { ee.off('topbar:togglelockbutton', this.toggleLockButton) + ee.off('export:save-text', this.saveAsTextHandler) + ee.off('export:save-md', this.saveAsMdHandler) } handleChange (e) { @@ -170,6 +177,30 @@ class MarkdownNoteDetail extends React.Component { ee.emit('export:save-text') } + exportAsDocument (fileType) { + const options = { + filters: [ + { name: 'Documents', extensions: [fileType] } + ], + properties: ['openFile', 'createDirectory'] + } + + dialog.showSaveDialog(remote.getCurrentWindow(), options, + (filename) => { + if (filename) { + const note = this.props.note + + exportNote(note.storage, note.content, filename) + .then((res) => { + dialog.showMessageBox(remote.getCurrentWindow(), {type: 'info', message: `Exported to ${filename}`}) + }).catch((err) => { + dialog.showErrorBox('Export error', err ? err.message || err : 'Unexpected error during export') + throw err + }) + } + }) + } + handleTrashButtonClick (e) { const { note } = this.state const { isTrashed } = note @@ -207,6 +238,14 @@ class MarkdownNoteDetail extends React.Component { ee.emit('list:next') } + handleSaveAsText () { + this.exportAsDocument('txt') + } + + handleSaveAsMd () { + this.exportAsDocument('md') + } + handleUndoButtonClick (e) { const { note } = this.state diff --git a/browser/main/lib/dataApi/exportImage.js b/browser/main/lib/dataApi/exportImage.js new file mode 100755 index 00000000..a1c84390 --- /dev/null +++ b/browser/main/lib/dataApi/exportImage.js @@ -0,0 +1,37 @@ +const fs = require('fs') +const path = require('path') + +/** + * @description Export an image + * @param {String} storagePath + * @param {String} srcFilename + * @param {String} dstPath + * @param {String} dstFilename if not present, destination filename will be equal to srcFilename + * @return {Promise} an image path + */ +function exportImage (storagePath, srcFilename, dstPath, dstFilename = '') { + dstFilename = dstFilename || srcFilename + + const src = path.join(storagePath, 'images', srcFilename) + + if (!path.extname(dstFilename)) { + dstFilename += path.extname(srcFilename) + } + + const dstImagesFolder = path.join(dstPath, 'images') + const dst = path.join(dstImagesFolder, dstFilename) + + return new Promise((resolve, reject) => { + if (!fs.existsSync(dstImagesFolder)) fs.mkdirSync(dstImagesFolder) + + const input = fs.createReadStream(src) + const output = fs.createWriteStream(dst) + + output.on('error', reject) + input.on('error', reject) + input.on('end', resolve) + input.pipe(output) + }) +} + +module.exports = exportImage diff --git a/browser/main/lib/dataApi/exportNote.js b/browser/main/lib/dataApi/exportNote.js new file mode 100755 index 00000000..368b43ca --- /dev/null +++ b/browser/main/lib/dataApi/exportNote.js @@ -0,0 +1,67 @@ +import exportImage from 'browser/main/lib/dataApi/exportImage' +import {findStorage} from 'browser/lib/findStorage' + +const fs = require('fs') +const path = require('path') + +/** + * Export note together with images + * + * If images is stored in the storage, creates 'images' subfolder in target directory + * and copies images to it. Changes links to images in the content of the note + * + * @param {String} storageKey + * @param {String} noteContent Content to export + * @param {String} targetPath Path to exported file + * @return {Promise.<*[]>} + */ +function exportNote (storageKey, noteContent, targetPath) { + const targetStorage = findStorage(storageKey) + const storagedImagesRe = /!\[(.*?)\]\(\s*?\/:storage\/(.*\.\S*?)\)/gi + const exportTasks = [] + const images = [] + + const exportedData = noteContent.replace(storagedImagesRe, (match, dstFilename, srcFilename) => { + if (!path.extname(dstFilename)) { + dstFilename += path.extname(srcFilename) + } + const imagePath = path.join('images', dstFilename) + + exportTasks.push( + exportImage(targetStorage.path, srcFilename, path.dirname(targetPath), dstFilename) + ) + images.push(imagePath) + return `![${dstFilename}](${imagePath})` + }) + + exportTasks.push(exportFile(exportedData, targetPath)) + return Promise.all(exportTasks) + .catch((err) => { + rollbackExport(images) + throw err + }) +} + +function exportFile (data, filename) { + return new Promise((resolve, reject) => { + fs.writeFile(filename, data, (err) => { + if (err) throw err + + resolve(filename) + }) + }) +} + +/** + * Remove exported images + * @param imagesPaths + */ +function rollbackExport (imagesPaths) { + imagesPaths.forEach((path) => { + if (fs.existsSync(path)) { + fs.unlink(path) + } + }) +} + +export default exportNote