1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-13 09:46:22 +00:00

Export note with local images

Looks through the note and searches for local images. Copies them to ‘images’ folder in the export path and replaces affected links.

#1261
This commit is contained in:
Nikolay Lopin
2017-12-17 21:39:34 +03:00
parent e72a7ceaea
commit 83da07a941
4 changed files with 143 additions and 32 deletions

32
browser/components/MarkdownPreview.js Normal file → Executable file
View File

@@ -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)
}

39
browser/main/Detail/MarkdownNoteDetail.js Normal file → Executable file
View File

@@ -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

View File

@@ -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

View File

@@ -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