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:
@@ -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`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 ``
|
dst: dstRelativePath
|
||||||
})
|
})
|
||||||
|
|
||||||
exportTasks.push(exportFile(exportedData, targetPath))
|
return ``
|
||||||
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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user