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

Export images together with document

This commit is contained in:
Nikolay Lopin
2018-02-05 02:08:33 +03:00
parent 3da4bb69ce
commit f678a17505
3 changed files with 153 additions and 71 deletions

View File

@@ -9,10 +9,10 @@ import Raphael from 'raphael'
import flowchart from 'flowchart' import flowchart from 'flowchart'
import SequenceDiagram from 'js-sequence-diagrams' import SequenceDiagram from 'js-sequence-diagrams'
import eventEmitter from 'browser/main/lib/eventEmitter' import eventEmitter from 'browser/main/lib/eventEmitter'
import fs from 'fs'
import htmlTextHelper from 'browser/lib/htmlTextHelper' import htmlTextHelper from 'browser/lib/htmlTextHelper'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import mdurl from 'mdurl' import mdurl from 'mdurl'
import exportNote from 'browser/main/lib/dataApi/exportNote';
const { remote } = require('electron') const { remote } = require('electron')
const { app } = remote const { app } = remote
@@ -24,6 +24,11 @@ const appPath = 'file://' + (process.env.NODE_ENV === 'production'
? app.getAppPath() ? app.getAppPath()
: path.resolve()) : path.resolve())
const CSS_FILES = [
`${appPath}/node_modules/katex/dist/katex.min.css`,
`${appPath}/node_modules/codemirror/lib/codemirror.css`
]
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber) { function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber) {
return ` return `
@font-face { @font-face {
@@ -146,12 +151,10 @@ export default class MarkdownPreview extends React.Component {
} }
handleContextMenu (e) { handleContextMenu (e) {
if (!this.props.onContextMenu) return
this.props.onContextMenu(e) this.props.onContextMenu(e)
} }
handleMouseDown (e) { handleMouseDown (e) {
if (!this.props.onMouseDown) return
if (e.target != null) { if (e.target != null) {
switch (e.target.tagName) { switch (e.target.tagName) {
case 'A': case 'A':
@@ -179,8 +182,33 @@ export default class MarkdownPreview extends React.Component {
} }
handleSaveAsHtml () { handleSaveAsHtml () {
this.exportAsDocument('html', (value) => { this.exportAsDocument('html', (noteContent, exportTasks) => {
return this.refs.root.contentWindow.document.documentElement.outerHTML const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme} = this.getStyleParams()
const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber)
const body = markdown.render(noteContent)
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
files.forEach((file) => {
file = file.replace('file://', '')
exportTasks.push({
src: file,
dst: 'css'
})
})
let styles = ''
files.forEach((file) => {
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">`
})
return `<html>
<head>
<style id="style">${inlineStyles}</style>
${styles}
</head>
<body>${body}</body>
</html>`
}) })
} }
@@ -188,20 +216,26 @@ export default class MarkdownPreview extends React.Component {
this.refs.root.contentWindow.print() this.refs.root.contentWindow.print()
} }
exportAsDocument (fileType, formatter) { exportAsDocument (fileType, contentFormatter) {
const options = { const options = {
filters: [ filters: [
{name: 'Documents', extensions: [fileType]} {name: 'Documents', extensions: [fileType]}
], ],
properties: ['openFile', 'createDirectory'] properties: ['openFile', 'createDirectory']
} }
const value = formatter ? formatter.call(this, this.props.value) : this.props.value
dialog.showSaveDialog(remote.getCurrentWindow(), options, dialog.showSaveDialog(remote.getCurrentWindow(), options,
(filename) => { (filename) => {
if (filename) { if (filename) {
fs.writeFile(filename, value, (err) => { const content = this.props.value
if (err) throw err const storage = this.props.storagePath
exportNote(storage, content, filename, contentFormatter)
.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
}) })
} }
}) })
@@ -221,12 +255,16 @@ export default class MarkdownPreview extends React.Component {
this.refs.root.setAttribute('sandbox', 'allow-scripts') this.refs.root.setAttribute('sandbox', 'allow-scripts')
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler) this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
this.refs.root.contentWindow.document.head.innerHTML = ` let styles = `
<style id='style'></style> <style id='style'></style>
<link rel="stylesheet" href="${appPath}/node_modules/katex/dist/katex.min.css">
<link rel="stylesheet" href="${appPath}/node_modules/codemirror/lib/codemirror.css">
<link rel="stylesheet" id="codeTheme"> <link rel="stylesheet" id="codeTheme">
` `
CSS_FILES.forEach((file) => {
styles += `<link rel="stylesheet" href="${file}">`
})
this.refs.root.contentWindow.document.head.innerHTML = styles
this.rewriteIframe() this.rewriteIframe()
this.applyStyle() this.applyStyle()
@@ -266,7 +304,7 @@ export default class MarkdownPreview extends React.Component {
} }
} }
applyStyle () { getStyleParams () {
const {fontSize, lineNumber, codeBlockTheme} = this.props const {fontSize, lineNumber, codeBlockTheme} = this.props
let {fontFamily, codeBlockFontFamily} = this.props let {fontFamily, codeBlockFontFamily} = this.props
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0 fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
@@ -276,15 +314,21 @@ export default class MarkdownPreview extends React.Component {
? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily) ? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily : defaultCodeBlockFontFamily
this.setCodeTheme(codeBlockTheme) return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme}
}
applyStyle () {
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme} = this.getStyleParams()
this.getWindow().document.getElementById('codeTheme').href = this.GetCodeThemeLink(codeBlockTheme)
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber) this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber)
} }
setCodeTheme (theme) { GetCodeThemeLink (theme) {
theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default' theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default'
? theme ? theme
: 'elegant' : 'elegant'
this.getWindow().document.getElementById('codeTheme').href = theme.startsWith('solarized') return theme.startsWith('solarized')
? `${appPath}/node_modules/codemirror/theme/solarized.css` ? `${appPath}/node_modules/codemirror/theme/solarized.css`
: `${appPath}/node_modules/codemirror/theme/${theme}.css` : `${appPath}/node_modules/codemirror/theme/${theme}.css`
} }

View File

@@ -2,35 +2,30 @@ const fs = require('fs')
const path = require('path') const path = require('path')
/** /**
* @description Export a file * @description Copy a file from source to destination
* @param {String} storagePath * @param {String} srcPath
* @param {String} srcFilename
* @param {String} dstPath * @param {String} dstPath
* @param {String} dstFilename if not present, destination filename will be equal to srcFilename
* @return {Promise} an image path * @return {Promise} an image path
*/ */
function exportFile (storagePath, srcFilename, dstPath, dstFilename = '') { function copyFile (srcPath, dstPath) {
dstFilename = dstFilename || srcFilename if (!path.extname(dstPath)) {
dstPath = path.join(dstPath, path.basename(srcPath))
const src = path.join(storagePath, 'images', srcFilename)
if (!path.extname(dstFilename)) {
dstFilename += path.extname(srcFilename)
} }
const dst = path.join(dstPath, dstFilename)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!fs.existsSync(dstPath)) fs.mkdirSync(dstPath) const dstFolder = path.dirname(dstPath)
if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder)
const input = fs.createReadStream(src) const input = fs.createReadStream(srcPath)
const output = fs.createWriteStream(dst) const output = fs.createWriteStream(dstPath)
output.on('error', reject) output.on('error', reject)
input.on('error', reject) input.on('error', reject)
input.on('end', resolve, dst) input.on('end', () => {
resolve(dstPath)
})
input.pipe(output) input.pipe(output)
}) })
} }
module.exports = exportFile module.exports = copyFile

View File

@@ -1,48 +1,77 @@
import exportImage from 'browser/main/lib/dataApi/exportImage' import copyFile from 'browser/main/lib/dataApi/copyFile'
import {findStorage} from 'browser/lib/findStorage' import {findStorage} from 'browser/lib/findStorage'
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const LOCAL_STORED_REGEX = /!\[(.*?)\]\(\s*?\/:storage\/(.*\.\S*?)\)/gi
const IMAGES_FOLDER_NAME = 'images'
/** /**
* Export note together with images * Export note together with images
* *
* If images is stored in the storage, creates 'images' subfolder in target directory * 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 * and copies images to it. Changes links to images in the content of the note
* *
* @param {String} storageKey * @param {String} storageKey or storage path
* @param {String} noteContent Content to export * @param {String} noteContent Content to export
* @param {String} targetPath Path to exported file * @param {String} targetPath Path to exported file
* @param {function} outputFormatter
* @return {Promise.<*[]>} * @return {Promise.<*[]>}
*/ */
function exportNote (storageKey, noteContent, targetPath) { function exportNote (storageKey, noteContent, targetPath, outputFormatter) {
const targetStorage = findStorage(storageKey) const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path
const storagedImagesRe = /!\[(.*?)\]\(\s*?\/:storage\/(.*\.\S*?)\)/gi
const exportTasks = [] const exportTasks = []
const images = []
const exportedData = noteContent.replace(storagedImagesRe, (match, dstFilename, srcFilename) => { if (!storagePath) {
throw new Error('Storage path is not found')
}
let exportedData = noteContent.replace(LOCAL_STORED_REGEX, (match, dstFilename, srcFilename) => {
if (!path.extname(dstFilename)) { if (!path.extname(dstFilename)) {
dstFilename += path.extname(srcFilename) dstFilename += path.extname(srcFilename)
} }
const imagePath = path.join('images', dstFilename)
exportTasks.push( const dstRelativePath = path.join(IMAGES_FOLDER_NAME, dstFilename)
exportImage(targetStorage.path, srcFilename, path.dirname(targetPath), dstFilename)
) exportTasks.push({
images.push(imagePath) src: path.join(IMAGES_FOLDER_NAME, srcFilename),
return `![${dstFilename}](${imagePath})` dst: dstRelativePath
}) })
exportTasks.push(exportFile(exportedData, targetPath)) return `![${dstFilename}](${dstRelativePath})`
return Promise.all(exportTasks) })
.catch((err) => {
rollbackExport(images) if (outputFormatter) {
exportedData = outputFormatter(exportedData, exportTasks)
}
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
return Promise.all(tasks.map((task) => copyFile(task.src, task.dst)))
.then(() => {
return saveToFile(exportedData, targetPath)
}).catch((err) => {
rollbackExport(tasks)
throw err throw err
}) })
} }
function exportFile (data, filename) { function prepareTasks (tasks, storagePath, targetPath) {
return tasks.map((task) => {
if (!path.isAbsolute(task.src)) {
task.src = path.join(storagePath, task.src)
}
if (!path.isAbsolute(task.dst)) {
task.dst = path.join(targetPath, task.dst)
}
return task
})
}
function saveToFile (data, filename) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.writeFile(filename, data, (err) => { fs.writeFile(filename, data, (err) => {
if (err) throw err if (err) throw err
@@ -53,13 +82,27 @@ function exportFile (data, filename) {
} }
/** /**
* Remove exported images * Remove exported files
* @param imagesPaths * @param tasks Array of copy task objects. Object consists of two mandatory fields `src` and `dst`
*/ */
function rollbackExport (imagesPaths) { function rollbackExport (tasks) {
imagesPaths.forEach((path) => { const folders = new Set()
if (fs.existsSync(path)) { tasks.forEach((task) => {
fs.unlink(path) let fullpath = task.dst
if (!path.extname(task.dst)) {
fullpath = path.join(task.dst, path.basename(task.src))
}
if (fs.existsSync(fullpath)) {
fs.unlink(fullpath)
folders.add(path.dirname(fullpath))
}
})
folders.forEach((folder) => {
if (fs.readdirSync(folder).length === 0) {
fs.rmdir(folder)
} }
}) })
} }