diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js
index f3bb92bb..9e5e8f53 100644
--- a/browser/components/MarkdownEditor.js
+++ b/browser/components/MarkdownEditor.js
@@ -230,7 +230,7 @@ class MarkdownEditor extends React.Component {
}
render () {
- const {className, value, config, storageKey, noteKey} = this.props
+ const {className, value, config, storageKey, noteKey, getNote} = this.props
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
@@ -308,6 +308,8 @@ class MarkdownEditor extends React.Component {
customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS}
lineThroughCheckbox={config.preview.lineThroughCheckbox}
+ getNote={getNote}
+ export={config.export}
/>
)
diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js
index d9ff7074..84053c4c 100755
--- a/browser/components/MarkdownPreview.js
+++ b/browser/components/MarkdownPreview.js
@@ -16,144 +16,21 @@ import convertModeName from 'browser/lib/convertModeName'
import copy from 'copy-to-clipboard'
import mdurl from 'mdurl'
import exportNote from 'browser/main/lib/dataApi/exportNote'
+import formatMarkdown from 'browser/main/lib/dataApi/formatMarkdown'
+import formatHTML, { CSS_FILES, buildStyle, getCodeThemeLink, getStyleParams } from 'browser/main/lib/dataApi/formatHTML'
import { escapeHtmlCharacters } from 'browser/lib/utils'
import yaml from 'js-yaml'
import context from 'browser/lib/context'
import i18n from 'browser/lib/i18n'
import fs from 'fs'
-
-const { remote, shell } = require('electron')
-const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
-
-const { app } = remote
-const path = require('path')
-const fileUrl = require('file-url')
+import path from 'path'
+import uri2path from 'file-uri-to-path'
+import { remote, shell } from 'electron'
+import attachmentManagement from '../main/lib/dataApi/attachmentManagement'
+import filenamify from 'filenamify'
const dialog = remote.dialog
-const uri2path = require('file-uri-to-path')
-
-const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
-const appPath = fileUrl(
- 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,
- theme,
- allowCustomCSS,
- customCSS
-) {
- return `
-@font-face {
- font-family: 'Lato';
- src: url('${appPath}/resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
- url('${appPath}/resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
- url('${appPath}/resources/fonts/Lato-Regular.ttf') format('truetype');
- font-style: normal;
- font-weight: normal;
- text-rendering: optimizeLegibility;
-}
-@font-face {
- font-family: 'Lato';
- src: url('${appPath}/resources/fonts/Lato-Black.woff2') format('woff2'), /* Modern Browsers */
- url('${appPath}/resources/fonts/Lato-Black.woff') format('woff'), /* Modern Browsers */
- url('${appPath}/resources/fonts/Lato-Black.ttf') format('truetype');
- font-style: normal;
- font-weight: 700;
- text-rendering: optimizeLegibility;
-}
-@font-face {
- font-family: 'Material Icons';
- font-style: normal;
- font-weight: 400;
- src: local('Material Icons'),
- local('MaterialIcons-Regular'),
- url('${appPath}/resources/fonts/MaterialIcons-Regular.woff2') format('woff2'),
- url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
- url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
-}
-${markdownStyle}
-
-body {
- font-family: '${fontFamily.join("','")}';
- font-size: ${fontSize}px;
- ${scrollPastEnd && 'padding-bottom: 90vh;'}
-}
-@media print {
- body {
- padding-bottom: initial;
- }
-}
-code {
- font-family: '${codeBlockFontFamily.join("','")}';
- background-color: rgba(0,0,0,0.04);
-}
-.lineNumber {
- ${lineNumber && 'display: block !important;'}
- font-family: '${codeBlockFontFamily.join("','")}';
-}
-
-.clipboardButton {
- color: rgba(147,147,149,0.8);;
- fill: rgba(147,147,149,1);;
- border-radius: 50%;
- margin: 0px 10px;
- border: none;
- background-color: transparent;
- outline: none;
- height: 15px;
- width: 15px;
- cursor: pointer;
-}
-
-.clipboardButton:hover {
- transition: 0.2s;
- color: #939395;
- fill: #939395;
- background-color: rgba(0,0,0,0.1);
-}
-
-h1, h2 {
- border: none;
-}
-
-h1 {
- padding-bottom: 4px;
- margin: 1em 0 8px;
-}
-
-h2 {
- padding-bottom: 0.2em;
- margin: 1em 0 0.37em;
-}
-
-body p {
- white-space: normal;
-}
-
-@media print {
- body[data-theme="${theme}"] {
- color: #000;
- background-color: #fff;
- }
- .clipboardButton {
- display: none
- }
-}
-
-${allowCustomCSS ? customCSS : ''}
-`
-}
-
const scrollBarStyle = `
::-webkit-scrollbar {
width: 12px;
@@ -173,21 +50,6 @@ const scrollBarDarkStyle = `
}
`
-const OSX = global.process.platform === 'darwin'
-
-const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
-if (!OSX) {
- defaultFontFamily.unshift('Microsoft YaHei')
- defaultFontFamily.unshift('meiryo')
-}
-const defaultCodeBlockFontFamily = [
- 'Monaco',
- 'Menlo',
- 'Ubuntu Mono',
- 'Consolas',
- 'source-code-pro',
- 'monospace'
-]
export default class MarkdownPreview extends React.Component {
constructor (props) {
super(props)
@@ -286,96 +148,11 @@ export default class MarkdownPreview extends React.Component {
}
handleSaveAsMd () {
- this.exportAsDocument('md', (noteContent, exportTasks) => {
- let result = noteContent
- if (this.props && this.props.storagePath && this.props.noteKey) {
- const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
- noteContent,
- this.props.storagePath
- )
- attachmentsAbsolutePaths.forEach(attachment => {
- exportTasks.push({
- src: attachment,
- dst: attachmentManagement.DESTINATION_FOLDER
- })
- })
- result = attachmentManagement.removeStorageAndNoteReferences(
- noteContent,
- this.props.noteKey
- )
- }
- return result
- })
+ this.exportAsDocument('md', formatMarkdown(this.props))
}
handleSaveAsHtml () {
- this.exportAsDocument('html', (noteContent, exportTasks) => {
- const {
- fontFamily,
- fontSize,
- codeBlockFontFamily,
- lineNumber,
- codeBlockTheme,
- scrollPastEnd,
- theme,
- allowCustomCSS,
- customCSS
- } = this.getStyleParams()
-
- const inlineStyles = buildStyle(
- fontFamily,
- fontSize,
- codeBlockFontFamily,
- lineNumber,
- scrollPastEnd,
- theme,
- allowCustomCSS,
- customCSS
- )
- let body = this.markdown.render(noteContent)
- const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
- const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
- noteContent,
- this.props.storagePath
- )
-
- files.forEach(file => {
- if (global.process.platform === 'win32') {
- file = file.replace('file:///', '')
- } else {
- file = file.replace('file://', '')
- }
- exportTasks.push({
- src: file,
- dst: 'css'
- })
- })
- attachmentsAbsolutePaths.forEach(attachment => {
- exportTasks.push({
- src: attachment,
- dst: attachmentManagement.DESTINATION_FOLDER
- })
- })
- body = attachmentManagement.removeStorageAndNoteReferences(
- body,
- this.props.noteKey
- )
-
- let styles = ''
- files.forEach(file => {
- styles += ``
- })
-
- return `
-
-
-
-
- ${styles}
-
- ${body}
- `
- })
+ this.exportAsDocument('html', formatHTML(this.props))
}
handlePrint () {
@@ -383,17 +160,21 @@ export default class MarkdownPreview extends React.Component {
}
exportAsDocument (fileType, contentFormatter) {
+ const note = this.props.getNote()
+
const options = {
+ defaultPath: filenamify(note.title, {
+ replacement: '_'
+ }),
filters: [{ name: 'Documents', extensions: [fileType] }],
properties: ['openFile', 'createDirectory']
}
dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
if (filename) {
- const content = this.props.value
- const storage = this.props.storagePath
+ const storagePath = this.props.storagePath
- exportNote(storage, content, filename, contentFormatter)
+ exportNote(storagePath, note, filename, contentFormatter)
.then(res => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
@@ -555,44 +336,6 @@ export default class MarkdownPreview extends React.Component {
}
}
- getStyleParams () {
- const {
- fontSize,
- lineNumber,
- codeBlockTheme,
- scrollPastEnd,
- theme,
- allowCustomCSS,
- customCSS
- } = this.props
- let { fontFamily, codeBlockFontFamily } = this.props
- fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
- ? 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
-
- return {
- fontFamily,
- fontSize,
- codeBlockFontFamily,
- lineNumber,
- codeBlockTheme,
- scrollPastEnd,
- theme,
- allowCustomCSS,
- customCSS
- }
- }
-
applyStyle () {
const {
fontFamily,
@@ -604,11 +347,10 @@ export default class MarkdownPreview extends React.Component {
theme,
allowCustomCSS,
customCSS
- } = this.getStyleParams()
+ } = getStyleParams(this.props)
+
+ this.getWindow().document.getElementById('codeTheme').href = getCodeThemeLink(codeBlockTheme)
- this.getWindow().document.getElementById(
- 'codeTheme'
- ).href = this.GetCodeThemeLink(codeBlockTheme)
this.getWindow().document.getElementById('style').innerHTML = buildStyle(
fontFamily,
fontSize,
@@ -621,16 +363,6 @@ export default class MarkdownPreview extends React.Component {
)
}
- GetCodeThemeLink (theme) {
- theme = consts.THEMES.some(_theme => _theme === theme) &&
- theme !== 'default'
- ? theme
- : 'elegant'
- return theme.startsWith('solarized')
- ? `${appPath}/node_modules/codemirror/theme/solarized.css`
- : `${appPath}/node_modules/codemirror/theme/${theme}.css`
- }
-
rewriteIframe () {
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll(
diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js
index 4a08bf12..b9600877 100644
--- a/browser/components/MarkdownSplitEditor.js
+++ b/browser/components/MarkdownSplitEditor.js
@@ -134,7 +134,7 @@ class MarkdownSplitEditor extends React.Component {
}
render () {
- const {config, value, storageKey, noteKey} = this.props
+ const {config, value, storageKey, noteKey, getTitle} = this.props
const storage = findStorage(storageKey)
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
@@ -199,6 +199,8 @@ class MarkdownSplitEditor extends React.Component {
customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS}
lineThroughCheckbox={config.preview.lineThroughCheckbox}
+ getTitle={getTitle}
+ export={config.export}
/>
)
diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js
index 116fdec0..a14c389a 100755
--- a/browser/main/Detail/MarkdownNoteDetail.js
+++ b/browser/main/Detail/MarkdownNoteDetail.js
@@ -49,6 +49,8 @@ class MarkdownNoteDetail extends React.Component {
this.toggleLockButton = this.handleToggleLockButton.bind(this)
this.generateToc = () => this.handleGenerateToc()
+
+ this.getNote = this.getNote.bind(this)
}
focus () {
@@ -319,6 +321,10 @@ class MarkdownNoteDetail extends React.Component {
this.updateNote(note)
}
+ getNote () {
+ return this.state.note
+ }
+
renderEditor () {
const { config, ignorePreviewPointerEvents } = this.props
const { note } = this.state
@@ -333,6 +339,7 @@ class MarkdownNoteDetail extends React.Component {
noteKey={note.key}
onChange={this.handleUpdateContent.bind(this)}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
+ getNote={this.getNote}
/>
} else {
return
}
}
diff --git a/browser/main/SideNav/StorageItem.js b/browser/main/SideNav/StorageItem.js
index d17314b3..7c9bdcb0 100644
--- a/browser/main/SideNav/StorageItem.js
+++ b/browser/main/SideNav/StorageItem.js
@@ -42,12 +42,16 @@ class StorageItem extends React.Component {
label: i18n.__('Export Storage'),
submenu: [
{
- label: i18n.__('Export as txt'),
+ label: i18n.__('Export as Plain Text (.txt)'),
click: (e) => this.handleExportStorageClick(e, 'txt')
},
{
- label: i18n.__('Export as md'),
+ label: i18n.__('Export as Markdown (.md)'),
click: (e) => this.handleExportStorageClick(e, 'md')
+ },
+ {
+ label: i18n.__('Export as HTML (.html)'),
+ click: (e) => this.handleExportStorageClick(e, 'html')
}
]
},
@@ -94,16 +98,28 @@ class StorageItem extends React.Component {
dialog.showOpenDialog(remote.getCurrentWindow(), options,
(paths) => {
if (paths && paths.length === 1) {
- const { storage, dispatch } = this.props
+ const { storage, dispatch, config } = this.props
dataApi
- .exportStorage(storage.key, fileType, paths[0])
+ .exportStorage(storage.key, fileType, paths[0], config)
.then(data => {
+ dialog.showMessageBox(remote.getCurrentWindow(), {
+ type: 'info',
+ message: `Exported to ${paths[0]}`
+ })
+
dispatch({
type: 'EXPORT_STORAGE',
storage: data.storage,
fileType: data.fileType
})
})
+ .catch(error => {
+ dialog.showErrorBox(
+ 'Export error',
+ error ? error.message || error : 'Unexpected error during export'
+ )
+ throw error
+ })
}
})
}
@@ -157,12 +173,16 @@ class StorageItem extends React.Component {
label: i18n.__('Export Folder'),
submenu: [
{
- label: i18n.__('Export as txt'),
+ label: i18n.__('Export as Plain Text (.txt)'),
click: (e) => this.handleExportFolderClick(e, folder, 'txt')
},
{
- label: i18n.__('Export as md'),
+ label: i18n.__('Export as Markdown (.md)'),
click: (e) => this.handleExportFolderClick(e, folder, 'md')
+ },
+ {
+ label: i18n.__('Export as HTML (.html)'),
+ click: (e) => this.handleExportFolderClick(e, folder, 'html')
}
]
},
@@ -194,10 +214,15 @@ class StorageItem extends React.Component {
dialog.showOpenDialog(remote.getCurrentWindow(), options,
(paths) => {
if (paths && paths.length === 1) {
- const { storage, dispatch } = this.props
+ const { storage, dispatch, config } = this.props
dataApi
- .exportFolder(storage.key, folder.key, fileType, paths[0])
- .then((data) => {
+ .exportFolder(storage.key, folder.key, fileType, paths[0], config)
+ .then(data => {
+ dialog.showMessageBox(remote.getCurrentWindow(), {
+ type: 'info',
+ message: `Exported to ${paths[0]}`
+ })
+
dispatch({
type: 'EXPORT_FOLDER',
storage: data.storage,
@@ -205,6 +230,13 @@ class StorageItem extends React.Component {
fileType: data.fileType
})
})
+ .catch(error => {
+ dialog.showErrorBox(
+ 'Export error',
+ error ? error.message || error : 'Unexpected error during export'
+ )
+ throw error
+ })
}
})
}
@@ -274,7 +306,7 @@ class StorageItem extends React.Component {
const { folderNoteMap, trashedSet } = data
const SortableStorageItemChild = SortableElement(StorageItemChild)
const folderList = storage.folders.map((folder, index) => {
- let folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
+ const folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
const isActive = !!(location.pathname.match(folderRegex))
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js
index b98d859d..fe304347 100644
--- a/browser/main/SideNav/index.js
+++ b/browser/main/SideNav/index.js
@@ -347,6 +347,7 @@ class SideNav extends React.Component {
dispatch={dispatch}
onSortEnd={this.onSortEnd.bind(this)(storage)}
useDragHandle
+ config={config}
/>
})
const style = {}
diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js
index b662a892..e85b049c 100644
--- a/browser/main/lib/ConfigManager.js
+++ b/browser/main/lib/ConfigManager.js
@@ -82,8 +82,9 @@ export const DEFAULT_CONFIG = {
password: ''
},
export: {
- action: 'DONT_EXPORT', // 'DONT_EXPORT', 'MERGE_HEADER', 'MERGE_VARIABLE'
- variable: 'metadata'
+ metadata: 'DONT_EXPORT', // 'DONT_EXPORT', 'MERGE_HEADER', 'MERGE_VARIABLE'
+ variable: 'boostnote',
+ prefixAttachmentFolder: false
}
}
diff --git a/browser/main/lib/dataApi/attachmentManagement.js b/browser/main/lib/dataApi/attachmentManagement.js
index c193eaf2..980bf859 100644
--- a/browser/main/lib/dataApi/attachmentManagement.js
+++ b/browser/main/lib/dataApi/attachmentManagement.js
@@ -386,6 +386,17 @@ function removeStorageAndNoteReferences (input, noteKey) {
return input.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + noteKey + ')?', 'g'), DESTINATION_FOLDER)
}
+/**
+ * @description replace all :storage references with given destination folder.
+ * @param input Input in which the references should be deleted
+ * @param noteKey Key of the current note
+ * @param destinationFolder Destination folder of the attachements
+ * @returns {String} Input without the references
+ */
+function replaceStorageReferences (input, noteKey, destinationFolder) {
+ return input.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + noteKey + ')?', 'g'), destinationFolder || DESTINATION_FOLDER)
+}
+
/**
* @description Deletes the attachment folder specified by the given storageKey and noteKey
* @param storageKey Key of the storage of the note to be deleted
@@ -542,6 +553,7 @@ module.exports = {
getAttachmentsInMarkdownContent,
getAbsolutePathsOfAttachmentsInContent,
removeStorageAndNoteReferences,
+ replaceStorageReferences,
deleteAttachmentFolder,
deleteAttachmentsNotPresentInNote,
moveAttachments,
diff --git a/browser/main/lib/dataApi/exportFolder.js b/browser/main/lib/dataApi/exportFolder.js
index 3e998f15..aef7bd8e 100644
--- a/browser/main/lib/dataApi/exportFolder.js
+++ b/browser/main/lib/dataApi/exportFolder.js
@@ -2,14 +2,17 @@ import { findStorage } from 'browser/lib/findStorage'
import resolveStorageData from './resolveStorageData'
import resolveStorageNotes from './resolveStorageNotes'
import filenamify from 'filenamify'
-import * as path from 'path'
-import * as fs from 'fs'
+import path from 'path'
+import exportNote from './exportNote'
+import formatMarkdown from './formatMarkdown'
+import formatHTML from './formatHTML'
/**
* @param {String} storageKey
* @param {String} folderKey
* @param {String} fileType
* @param {String} exportDir
+ * @param {Object} config
*
* @return {Object}
* ```
@@ -22,7 +25,7 @@ import * as fs from 'fs'
* ```
*/
-function exportFolder (storageKey, folderKey, fileType, exportDir) {
+function exportFolder (storageKey, folderKey, fileType, exportDir, config) {
let targetStorage
try {
targetStorage = findStorage(storageKey)
@@ -31,31 +34,50 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) {
}
return resolveStorageData(targetStorage)
- .then(function assignNotes (storage) {
- return resolveStorageNotes(storage)
- .then((notes) => {
- return {
- storage,
- notes
- }
- })
- })
- .then(function exportNotes (data) {
- const { storage, notes } = data
-
- notes
- .filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
- .forEach(snippet => {
- const notePath = path.join(exportDir, `${filenamify(snippet.title, {replacement: '_'})}.${fileType}`)
- fs.writeFileSync(notePath, snippet.content)
- })
-
- return {
+ .then(storage => {
+ return resolveStorageNotes(storage).then(notes => ({
storage,
- folderKey,
- fileType,
- exportDir
+ notes: notes.filter(note => note.folder === folderKey && !note.isTrashed && note.type === 'MARKDOWN_NOTE')
+ }))
+ })
+ .then(({ storage, notes }) => {
+ let contentFormatter = null
+ if (fileType === 'md') {
+ contentFormatter = formatMarkdown({
+ storagePath: storage.path,
+ export: config.export
+ })
+ } else if (fileType === 'html') {
+ contentFormatter = formatHTML({
+ theme: config.ui.theme,
+ fontSize: config.preview.fontSize,
+ fontFamily: config.preview.fontFamily,
+ codeBlockTheme: config.preview.codeBlockTheme,
+ codeBlockFontFamily: config.editor.fontFamily,
+ lineNumber: config.preview.lineNumber,
+ scrollPastEnd: config.preview.scrollPastEnd,
+ smartQuotes: config.preview.smartQuotes,
+ breaks: config.preview.breaks,
+ sanitize: config.preview.sanitize,
+ customCSS: config.preview.customCSS,
+ allowCustomCSS: config.preview.allowCustomCSS,
+ storagePath: storage.path,
+ export: config.export
+ })
}
+
+ return Promise
+ .all(notes.map(note => {
+ const targetPath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`)
+
+ return exportNote(storage.key, note, targetPath, contentFormatter)
+ }))
+ .then(() => ({
+ storage,
+ folderKey,
+ fileType,
+ exportDir
+ }))
})
}
diff --git a/browser/main/lib/dataApi/exportNote.js b/browser/main/lib/dataApi/exportNote.js
index e4fec5f4..970528db 100755
--- a/browser/main/lib/dataApi/exportNote.js
+++ b/browser/main/lib/dataApi/exportNote.js
@@ -16,7 +16,7 @@ const path = require('path')
* @param {function} outputFormatter
* @return {Promise.<*[]>}
*/
-function exportNote (storageKey, noteContent, targetPath, outputFormatter) {
+function exportNote (storageKey, note, targetPath, outputFormatter) {
const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path
const exportTasks = []
@@ -24,20 +24,18 @@ function exportNote (storageKey, noteContent, targetPath, outputFormatter) {
throw new Error('Storage path is not found')
}
- let exportedData = noteContent
-
- if (outputFormatter) {
- exportedData = outputFormatter(exportedData, exportTasks)
- }
+ const exportedData = outputFormatter ? outputFormatter(note, targetPath, exportTasks) : note.content
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
- return Promise.all(tasks.map((task) => copyFile(task.src, task.dst)))
+ return Promise
+ .all(tasks.map(task => copyFile(task.src, task.dst)))
.then(() => {
return saveToFile(exportedData, targetPath)
- }).catch((err) => {
+ })
+ .catch(error => {
rollbackExport(tasks)
- throw err
+ throw error
})
}
@@ -57,10 +55,12 @@ function prepareTasks (tasks, storagePath, targetPath) {
function saveToFile (data, filename) {
return new Promise((resolve, reject) => {
- fs.writeFile(filename, data, (err) => {
- if (err) return reject(err)
-
- resolve(filename)
+ fs.writeFile(filename, data, error => {
+ if (error) {
+ reject(error)
+ } else {
+ resolve(filename)
+ }
})
})
}
diff --git a/browser/main/lib/dataApi/exportStorage.js b/browser/main/lib/dataApi/exportStorage.js
index ce2c4573..1bcacd51 100644
--- a/browser/main/lib/dataApi/exportStorage.js
+++ b/browser/main/lib/dataApi/exportStorage.js
@@ -2,13 +2,17 @@ import { findStorage } from 'browser/lib/findStorage'
import resolveStorageData from './resolveStorageData'
import resolveStorageNotes from './resolveStorageNotes'
import filenamify from 'filenamify'
-import * as path from 'path'
-import * as fs from 'fs'
+import path from 'path'
+import fs from 'fs'
+import exportNote from './exportNote'
+import formatMarkdown from './formatMarkdown'
+import formatHTML from './formatHTML'
/**
* @param {String} storageKey
* @param {String} fileType
* @param {String} exportDir
+ * @param {Object} config
*
* @return {Object}
* ```
@@ -20,7 +24,7 @@ import * as fs from 'fs'
* ```
*/
-function exportStorage (storageKey, fileType, exportDir) {
+function exportStorage (storageKey, fileType, exportDir, config) {
let targetStorage
try {
targetStorage = findStorage(storageKey)
@@ -29,34 +33,61 @@ function exportStorage (storageKey, fileType, exportDir) {
}
return resolveStorageData(targetStorage)
- .then(storage => (
- resolveStorageNotes(storage).then(notes => ({storage, notes}))
- ))
- .then(function exportNotes (data) {
- const { storage, notes } = data
+ .then(storage => {
+ return resolveStorageNotes(storage).then(notes => ({
+ storage,
+ notes: notes.filter(note => !note.isTrashed && note.type === 'MARKDOWN_NOTE')
+ }))
+ })
+ .then(({ storage, notes }) => {
+ let contentFormatter = null
+ if (fileType === 'md') {
+ contentFormatter = formatMarkdown({
+ storagePath: storage.path,
+ export: config.export
+ })
+ } else if (fileType === 'html') {
+ contentFormatter = formatHTML({
+ theme: config.ui.theme,
+ fontSize: config.preview.fontSize,
+ fontFamily: config.preview.fontFamily,
+ codeBlockTheme: config.preview.codeBlockTheme,
+ codeBlockFontFamily: config.editor.fontFamily,
+ lineNumber: config.preview.lineNumber,
+ scrollPastEnd: config.preview.scrollPastEnd,
+ smartQuotes: config.preview.smartQuotes,
+ breaks: config.preview.breaks,
+ sanitize: config.preview.sanitize,
+ customCSS: config.preview.customCSS,
+ allowCustomCSS: config.preview.allowCustomCSS,
+ storagePath: storage.path,
+ export: config.export
+ })
+ }
+
const folderNamesMapping = {}
storage.folders.forEach(folder => {
const folderExportedDir = path.join(exportDir, filenamify(folder.name, {replacement: '_'}))
+
folderNamesMapping[folder.key] = folderExportedDir
+
// make sure directory exists
try {
fs.mkdirSync(folderExportedDir)
} catch (e) {}
})
- notes
- .filter(note => !note.isTrashed && note.type === 'MARKDOWN_NOTE')
- .forEach(markdownNote => {
- const folderExportedDir = folderNamesMapping[markdownNote.folder]
- const snippetName = `${filenamify(markdownNote.title, {replacement: '_'})}.${fileType}`
- const notePath = path.join(folderExportedDir, snippetName)
- fs.writeFileSync(notePath, markdownNote.content)
- })
- return {
- storage,
- fileType,
- exportDir
- }
+ return Promise
+ .all(notes.map(note => {
+ const targetPath = path.join(folderNamesMapping[note.folder], `${filenamify(note.title, {replacement: '_'})}.${fileType}`)
+
+ return exportNote(storage.key, note, targetPath, contentFormatter)
+ }))
+ .then(() => ({
+ storage,
+ fileType,
+ exportDir
+ }))
})
}
diff --git a/browser/main/lib/dataApi/formatHTML.js b/browser/main/lib/dataApi/formatHTML.js
new file mode 100644
index 00000000..de99421f
--- /dev/null
+++ b/browser/main/lib/dataApi/formatHTML.js
@@ -0,0 +1,295 @@
+import path from 'path'
+import fileUrl from 'file-url'
+import { remote } from 'electron'
+import consts from 'browser/lib/consts'
+import Markdown from 'browser/lib/markdown'
+import attachmentManagement from './attachmentManagement'
+
+const { app } = remote
+const appPath = fileUrl(process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve())
+
+let markdownStyle = ''
+try {
+ markdownStyle = require('!!css!stylus?sourceMap!../../../components/markdown.styl')[0][1]
+} catch (e) {}
+
+export const CSS_FILES = [
+ `${appPath}/node_modules/katex/dist/katex.min.css`,
+ `${appPath}/node_modules/codemirror/lib/codemirror.css`
+]
+
+const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
+if (global.process.platform !== 'darwin') {
+ defaultFontFamily.unshift('Microsoft YaHei')
+ defaultFontFamily.unshift('meiryo')
+}
+
+const defaultCodeBlockFontFamily = [
+ 'Monaco',
+ 'Menlo',
+ 'Ubuntu Mono',
+ 'Consolas',
+ 'source-code-pro',
+ 'monospace'
+]
+
+/**
+ * ```
+ * {
+ * fontFamily,
+ * fontSize,
+ * lineNumber,
+ * codeBlockFontFamily,
+ * codeBlockTheme,
+ * scrollPastEnd,
+ * theme,
+ * allowCustomCSS,
+ * customCSS
+ * smartQuotes,
+ * sanitize,
+ * breaks,
+ * storagePath,
+ * export
+ * }
+ * ```
+ */
+export default function formatHTML (props) {
+ const {
+ fontFamily,
+ fontSize,
+ codeBlockFontFamily,
+ lineNumber,
+ codeBlockTheme,
+ scrollPastEnd,
+ theme,
+ allowCustomCSS,
+ customCSS
+ } = getStyleParams(props)
+
+ const inlineStyles = buildStyle(
+ fontFamily,
+ fontSize,
+ codeBlockFontFamily,
+ lineNumber,
+ scrollPastEnd,
+ theme,
+ allowCustomCSS,
+ customCSS
+ )
+ const { smartQuotes, sanitize, breaks } = props
+
+ const markdown = new Markdown({
+ typographer: smartQuotes,
+ sanitize,
+ breaks
+ })
+
+ const files = [getCodeThemeLink(codeBlockTheme), ...CSS_FILES]
+
+ return function (note, targetPath, exportTasks) {
+ let body = markdown.render(note.content)
+
+ const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(note.content, props.storagePath)
+
+ files.forEach(file => {
+ if (global.process.platform === 'win32') {
+ file = file.replace('file:///', '')
+ } else {
+ file = file.replace('file://', '')
+ }
+ exportTasks.push({
+ src: file,
+ dst: 'css'
+ })
+ })
+
+ const destinationFolder = props.export.prefixAttachmentFolder ? `${path.parse(targetPath).name} - ${attachmentManagement.DESTINATION_FOLDER}` : attachmentManagement.DESTINATION_FOLDER
+
+ attachmentsAbsolutePaths.forEach(attachment => {
+ exportTasks.push({
+ src: attachment,
+ dst: destinationFolder
+ })
+ })
+
+ body = attachmentManagement.replaceStorageReferences(body, note.key, destinationFolder)
+
+ let styles = ''
+ files.forEach(file => {
+ styles += ``
+ })
+
+ return `
+
+
+
+
+ ${styles}
+
+ ${body}
+ `
+ }
+}
+
+export function getStyleParams (props) {
+ const {
+ fontSize,
+ lineNumber,
+ codeBlockTheme,
+ scrollPastEnd,
+ theme,
+ allowCustomCSS,
+ customCSS
+ } = props
+
+ let { fontFamily, codeBlockFontFamily } = props
+
+ fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
+ ? 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
+
+ return {
+ fontFamily,
+ fontSize,
+ codeBlockFontFamily,
+ lineNumber,
+ codeBlockTheme,
+ scrollPastEnd,
+ theme,
+ allowCustomCSS,
+ customCSS
+ }
+}
+
+export function getCodeThemeLink (theme) {
+ if (consts.THEMES.some(_theme => _theme === theme)) {
+ theme = theme !== 'default' ? theme : 'elegant'
+ }
+
+ return theme.startsWith('solarized')
+ ? `${appPath}/node_modules/codemirror/theme/solarized.css`
+ : `${appPath}/node_modules/codemirror/theme/${theme}.css`
+}
+
+export function buildStyle (
+ fontFamily,
+ fontSize,
+ codeBlockFontFamily,
+ lineNumber,
+ scrollPastEnd,
+ theme,
+ allowCustomCSS,
+ customCSS
+) {
+ return `
+@font-face {
+ font-family: 'Lato';
+ src: url('${appPath}/resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
+ url('${appPath}/resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
+ url('${appPath}/resources/fonts/Lato-Regular.ttf') format('truetype');
+ font-style: normal;
+ font-weight: normal;
+ text-rendering: optimizeLegibility;
+}
+@font-face {
+ font-family: 'Lato';
+ src: url('${appPath}/resources/fonts/Lato-Black.woff2') format('woff2'), /* Modern Browsers */
+ url('${appPath}/resources/fonts/Lato-Black.woff') format('woff'), /* Modern Browsers */
+ url('${appPath}/resources/fonts/Lato-Black.ttf') format('truetype');
+ font-style: normal;
+ font-weight: 700;
+ text-rendering: optimizeLegibility;
+}
+@font-face {
+ font-family: 'Material Icons';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Material Icons'),
+ local('MaterialIcons-Regular'),
+ url('${appPath}/resources/fonts/MaterialIcons-Regular.woff2') format('woff2'),
+ url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
+ url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
+}
+${markdownStyle}
+
+body {
+ font-family: '${fontFamily.join("','")}';
+ font-size: ${fontSize}px;
+ ${scrollPastEnd && 'padding-bottom: 90vh;'}
+}
+@media print {
+ body {
+ padding-bottom: initial;
+ }
+}
+code {
+ font-family: '${codeBlockFontFamily.join("','")}';
+ background-color: rgba(0,0,0,0.04);
+}
+.lineNumber {
+ ${lineNumber && 'display: block !important;'}
+ font-family: '${codeBlockFontFamily.join("','")}';
+}
+
+.clipboardButton {
+ color: rgba(147,147,149,0.8);;
+ fill: rgba(147,147,149,1);;
+ border-radius: 50%;
+ margin: 0px 10px;
+ border: none;
+ background-color: transparent;
+ outline: none;
+ height: 15px;
+ width: 15px;
+ cursor: pointer;
+}
+
+.clipboardButton:hover {
+ transition: 0.2s;
+ color: #939395;
+ fill: #939395;
+ background-color: rgba(0,0,0,0.1);
+}
+
+h1, h2 {
+ border: none;
+}
+
+h1 {
+ padding-bottom: 4px;
+ margin: 1em 0 8px;
+}
+
+h2 {
+ padding-bottom: 0.2em;
+ margin: 1em 0 0.37em;
+}
+
+body p {
+ white-space: normal;
+}
+
+@media print {
+ body[data-theme="${theme}"] {
+ color: #000;
+ background-color: #fff;
+ }
+ .clipboardButton {
+ display: none
+ }
+}
+
+${allowCustomCSS ? customCSS : ''}
+`
+}
diff --git a/browser/main/lib/dataApi/formatMarkdown.js b/browser/main/lib/dataApi/formatMarkdown.js
new file mode 100644
index 00000000..6ba00eac
--- /dev/null
+++ b/browser/main/lib/dataApi/formatMarkdown.js
@@ -0,0 +1,94 @@
+import attachmentManagement from './attachmentManagement'
+import yaml from 'js-yaml'
+import path from 'path'
+
+const delimiterRegExp = /^\-{3}/
+
+/**
+ * ```
+ * {
+ * storagePath,
+ * export
+ * }
+ * ```
+ */
+export default function formatMarkdown (props) {
+ return function (note, targetPath, exportTasks) {
+ let result = note.content
+
+ if (props.storagePath && note.key) {
+ const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(result, props.storagePath)
+
+ const destinationFolder = props.export.prefixAttachmentFolder ? `${path.parse(targetPath).name} - ${attachmentManagement.DESTINATION_FOLDER}` : attachmentManagement.DESTINATION_FOLDER
+
+ attachmentsAbsolutePaths.forEach(attachment => {
+ exportTasks.push({
+ src: attachment,
+ dst: destinationFolder
+ })
+ })
+
+ result = attachmentManagement.replaceStorageReferences(result, note.key, destinationFolder)
+ }
+
+ if (props.export.metadata === 'MERGE_HEADER') {
+ const metadata = getFrontMatter(result)
+
+ const values = Object.assign({}, note)
+ delete values.content
+ delete values.isTrashed
+
+ for (const key in values) {
+ metadata[key] = values[key]
+ }
+
+ result = replaceFrontMatter(result, metadata)
+ } else if (props.export.metadata === 'MERGE_VARIABLE') {
+ const metadata = getFrontMatter(result)
+
+ const values = Object.assign({}, note)
+ delete values.content
+ delete values.isTrashed
+
+ if (props.export.variable) {
+ metadata[props.export.variable] = values
+ } else {
+ for (const key in values) {
+ metadata[key] = values[key]
+ }
+ }
+
+ result = replaceFrontMatter(result, metadata)
+ }
+
+ return result
+ }
+}
+
+function getFrontMatter (markdown) {
+ const lines = markdown.split('\n')
+
+ if (delimiterRegExp.test(lines[0])) {
+ let line = 0
+ while (++line < lines.length && delimiterRegExp.test(lines[line])) {
+ }
+
+ return yaml.load(lines.slice(1, line).join('\n')) || {}
+ } else {
+ return {}
+ }
+}
+
+function replaceFrontMatter (markdown, metadata) {
+ const lines = markdown.split('\n')
+
+ if (lines[0] === '---') {
+ let line = 0
+ while (++line < lines.length && lines[line] !== '---') {
+ }
+
+ return `---\n${yaml.dump(metadata)}---\n${lines.slice(line + 1).join('\n')}`
+ } else {
+ return `---\n${yaml.dump(metadata)}---\n\n${markdown}`
+ }
+}
diff --git a/browser/main/lib/dataApi/moveNote.js b/browser/main/lib/dataApi/moveNote.js
index 2d306cdf..c38968cb 100644
--- a/browser/main/lib/dataApi/moveNote.js
+++ b/browser/main/lib/dataApi/moveNote.js
@@ -1,7 +1,6 @@
const resolveStorageData = require('./resolveStorageData')
const _ = require('lodash')
const path = require('path')
-const fs = require('fs')
const CSON = require('@rokt33r/season')
const keygen = require('browser/lib/keygen')
const sander = require('sander')
diff --git a/browser/main/modals/PreferencesModal/ExportTab.js b/browser/main/modals/PreferencesModal/ExportTab.js
index b2036646..b7813cf7 100644
--- a/browser/main/modals/PreferencesModal/ExportTab.js
+++ b/browser/main/modals/PreferencesModal/ExportTab.js
@@ -30,21 +30,19 @@ class ExportTab extends React.Component {
componentDidMount () {
this.handleSettingDone = () => {
this.setState({
- ExportAlert: {
- type: 'success',
- message: i18n.__('Successfully applied!')
- }
+ ExportAlert: {
+ type: 'success',
+ message: i18n.__('Successfully applied!')
}
- )
+ })
}
this.handleSettingError = (err) => {
this.setState({
- ExportAlert: {
- type: 'error',
- message: err.message != null ? err.message : i18n.__('An error occurred!')
- }
+ ExportAlert: {
+ type: 'error',
+ message: err.message != null ? err.message : i18n.__('An error occurred!')
}
- )
+ })
}
this.oldExport = this.state.config.export
@@ -78,8 +76,9 @@ class ExportTab extends React.Component {
const { config } = this.state
config.export = {
- action: this.refs.action.value,
- variable: !_.isNil(this.refs.variable) ? this.refs.variable.value : config.export.variable
+ metadata: this.refs.metadata.value,
+ variable: !_.isNil(this.refs.variable) ? this.refs.variable.value : config.export.variable,
+ prefixAttachmentFolder: this.refs.prefixAttachmentFolder.checked
}
this.setState({
@@ -99,7 +98,6 @@ class ExportTab extends React.Component {
render () {
const { config, ExportAlert } = this.state
- console.log(config.export)
const ExportAlertElement = ExportAlert != null
?
@@ -114,12 +112,12 @@ class ExportTab extends React.Component {
- {i18n.__('Action')}
+ {i18n.__('Metadata')}
-
- { config.export.action === 'MERGE_VARIABLE' &&
+ { config.export.metadata === 'MERGE_VARIABLE' &&
{i18n.__('Variable Name')}
@@ -141,6 +139,17 @@ class ExportTab extends React.Component {
}
+
+
+
+