Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 68 additions & 25 deletions browser/components/MarkdownPreview.js
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ 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'
import exportNote from 'browser/main/lib/dataApi/exportNote'

const { remote } = require('electron')
const { app } = remote
Expand All @@ -23,6 +23,10 @@ const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
const appPath = 'file://' + (process.env.NODE_ENV === 'production'
? app.getAppPath()
: 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, scrollPastEnd) {
return `
Expand Down Expand Up @@ -147,12 +151,10 @@ export default class MarkdownPreview extends React.Component {
}

handleContextMenu (e) {
if (!this.props.onContextMenu) return
this.props.onContextMenu(e)
}

handleMouseDown (e) {
if (!this.props.onMouseDown) return
if (e.target != null) {
switch (e.target.tagName) {
case 'A':
Expand Down Expand Up @@ -180,32 +182,63 @@ export default class MarkdownPreview extends React.Component {
}

handleSaveAsHtml () {
this.exportAsDocument('html', (value) => {
return this.refs.root.contentWindow.document.documentElement.outerHTML
this.exportAsDocument('html', (noteContent, exportTasks) => {
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>`
})
}

handlePrint () {
this.refs.root.contentWindow.print()
}

exportAsDocument (fileType, formatter) {
exportAsDocument (fileType, contentFormatter) {
const options = {
filters: [
{ name: 'Documents', extensions: [fileType] }
{name: 'Documents', extensions: [fileType]}
],
properties: ['openFile', 'createDirectory']
}
const value = formatter ? formatter.call(this, this.props.value) : this.props.value

dialog.showSaveDialog(remote.getCurrentWindow(), options,
(filename) => {
if (filename) {
fs.writeFile(filename, value, (err) => {
if (err) throw err
(filename) => {
if (filename) {
const content = this.props.value
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
})
}
})
}
})
}

fixDecodedURI (node) {
Expand All @@ -222,13 +255,17 @@ export default class MarkdownPreview extends React.Component {
this.refs.root.setAttribute('sandbox', 'allow-scripts')
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)

this.refs.root.contentWindow.document.head.innerHTML = `
let styles = `
<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">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
`

CSS_FILES.forEach((file) => {
styles += `<link rel="stylesheet" href="${file}">`
})

this.refs.root.contentWindow.document.head.innerHTML = styles
this.rewriteIframe()
this.applyStyle()

Expand Down Expand Up @@ -269,25 +306,31 @@ export default class MarkdownPreview extends React.Component {
}
}

applyStyle () {
getStyleParams () {
const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd } = this.props
let { fontFamily, codeBlockFontFamily } = this.props
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily)
: defaultFontFamily
? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily)
: defaultFontFamily
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily
? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily

return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd}
}

applyStyle () {
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd} = this.getStyleParams()

this.setCodeTheme(codeBlockTheme)
this.getWindow().document.getElementById('codeTheme').href = this.GetCodeThemeLink(codeBlockTheme)
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd)
}

setCodeTheme (theme) {
GetCodeThemeLink (theme) {
theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default'
? theme
: '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/${theme}.css`
}
Expand Down
2 changes: 1 addition & 1 deletion browser/components/markdown.styl
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ body
justify-content left
li
label.taskListItem
margin-left -2em
margin-left -1.8em
&.checked
text-decoration line-through
opacity 0.5
Expand Down
Empty file modified browser/main/Detail/MarkdownNoteDetail.js
100644 → 100755
Empty file.
31 changes: 31 additions & 0 deletions browser/main/lib/dataApi/copyFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const fs = require('fs')
const path = require('path')

/**
* @description Copy a file from source to destination
* @param {String} srcPath
* @param {String} dstPath
* @return {Promise} an image path
*/
function copyFile (srcPath, dstPath) {
if (!path.extname(dstPath)) {
dstPath = path.join(dstPath, path.basename(srcPath))
}

return new Promise((resolve, reject) => {
const dstFolder = path.dirname(dstPath)
if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder)

const input = fs.createReadStream(srcPath)
const output = fs.createWriteStream(dstPath)

output.on('error', reject)
input.on('error', reject)
input.on('end', () => {
resolve(dstPath)
})
input.pipe(output)
})
}

module.exports = copyFile
110 changes: 110 additions & 0 deletions browser/main/lib/dataApi/exportNote.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import copyFile from 'browser/main/lib/dataApi/copyFile'
import {findStorage} from 'browser/lib/findStorage'

const fs = require('fs')
const path = require('path')

const LOCAL_STORED_REGEX = /!\[(.*?)\]\(\s*?\/:storage\/(.*\.\S*?)\)/gi
const IMAGES_FOLDER_NAME = 'images'

/**
* 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 or storage path
* @param {String} noteContent Content to export
* @param {String} targetPath Path to exported file
* @param {function} outputFormatter
* @return {Promise.<*[]>}
*/
function exportNote (storageKey, noteContent, targetPath, outputFormatter) {
const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path
const exportTasks = []

if (!storagePath) {
throw new Error('Storage path is not found')
}

let exportedData = noteContent.replace(LOCAL_STORED_REGEX, (match, dstFilename, srcFilename) => {
if (!path.extname(dstFilename)) {
dstFilename += path.extname(srcFilename)
}

const dstRelativePath = path.join(IMAGES_FOLDER_NAME, dstFilename)

exportTasks.push({
src: path.join(IMAGES_FOLDER_NAME, srcFilename),
dst: dstRelativePath
})

return `![${dstFilename}](${dstRelativePath})`
})

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

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) => {
fs.writeFile(filename, data, (err) => {
if (err) return reject(err)

resolve(filename)
})
})
}

/**
* Remove exported files
* @param tasks Array of copy task objects. Object consists of two mandatory fields – `src` and `dst`
*/
function rollbackExport (tasks) {
const folders = new Set()
tasks.forEach((task) => {
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)
}
})
}

export default exportNote