From 168fe212f55a490322a970482b8538022aa2420c Mon Sep 17 00:00:00 2001 From: Baptiste Augrain Date: Thu, 15 Nov 2018 03:15:11 +0100 Subject: [PATCH 01/18] add preference tab --- browser/main/lib/ConfigManager.js | 4 + .../main/modals/PreferencesModal/ExportTab.js | 161 ++++++++++++++++++ browser/main/modals/PreferencesModal/index.js | 14 +- yarn.lock | 6 +- 4 files changed, 181 insertions(+), 4 deletions(-) create mode 100644 browser/main/modals/PreferencesModal/ExportTab.js diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index 4cbe80a7..b662a892 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -80,6 +80,10 @@ export const DEFAULT_CONFIG = { token: '', username: '', password: '' + }, + export: { + action: 'DONT_EXPORT', // 'DONT_EXPORT', 'MERGE_HEADER', 'MERGE_VARIABLE' + variable: 'metadata' } } diff --git a/browser/main/modals/PreferencesModal/ExportTab.js b/browser/main/modals/PreferencesModal/ExportTab.js new file mode 100644 index 00000000..b2036646 --- /dev/null +++ b/browser/main/modals/PreferencesModal/ExportTab.js @@ -0,0 +1,161 @@ +import PropTypes from 'prop-types' +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './ConfigTab.styl' +import ConfigManager from 'browser/main/lib/ConfigManager' +import store from 'browser/main/store' +import _ from 'lodash' +import i18n from 'browser/lib/i18n' + +const electron = require('electron') +const ipc = electron.ipcRenderer + +class ExportTab extends React.Component { + constructor (props) { + super(props) + + this.state = { + config: props.config + } + } + + clearMessage () { + _.debounce(() => { + this.setState({ + ExportAlert: null + }) + }, 2000)() + } + + componentDidMount () { + this.handleSettingDone = () => { + this.setState({ + 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!') + } + } + ) + } + + this.oldExport = this.state.config.export + + ipc.addListener('APP_SETTING_DONE', this.handleSettingDone) + ipc.addListener('APP_SETTING_ERROR', this.handleSettingError) + } + + componentWillUnmount () { + ipc.removeListener('APP_SETTING_DONE', this.handleSettingDone) + ipc.removeListener('APP_SETTING_ERROR', this.handleSettingError) + } + + handleSaveButtonClick (e) { + const newConfig = { + export: this.state.config.export + } + + ConfigManager.set(newConfig) + + store.dispatch({ + type: 'SET_UI', + config: newConfig + }) + + this.clearMessage() + this.props.haveToSave() + } + + handleExportChange (e) { + const { config } = this.state + + config.export = { + action: this.refs.action.value, + variable: !_.isNil(this.refs.variable) ? this.refs.variable.value : config.export.variable + } + + this.setState({ + config + }) + + if (_.isEqual(this.oldExport, config.export)) { + this.props.haveToSave() + } else { + this.props.haveToSave({ + tab: 'Export', + type: 'warning', + message: i18n.__('Unsaved Changes!') + }) + } + } + + render () { + const { config, ExportAlert } = this.state + console.log(config.export) + + const ExportAlertElement = ExportAlert != null + ?

+ {ExportAlert.message} +

+ : null + + return ( +
+
+
{i18n.__('Export')}
+ +
+
+ {i18n.__('Action')} +
+
+ +
+
+ + { config.export.action === 'MERGE_VARIABLE' && +
+
{i18n.__('Variable Name')}
+
+ this.handleExportChange(e)} + ref='variable' + value={config.export.variable} + type='text' /> +
+
+ } + +
+ + {ExportAlertElement} +
+
+
+ ) + } +} + +ExportTab.propTypes = { + dispatch: PropTypes.func, + haveToSave: PropTypes.func +} + +export default CSSModules(ExportTab, styles) diff --git a/browser/main/modals/PreferencesModal/index.js b/browser/main/modals/PreferencesModal/index.js index f3fc3751..257d50c7 100644 --- a/browser/main/modals/PreferencesModal/index.js +++ b/browser/main/modals/PreferencesModal/index.js @@ -6,6 +6,7 @@ import UiTab from './UiTab' import InfoTab from './InfoTab' import Crowdfunding from './Crowdfunding' import StoragesTab from './StoragesTab' +import ExportTab from './ExportTab' import SnippetTab from './SnippetTab' import Blog from './Blog' import ModalEscButton from 'browser/components/ModalEscButton' @@ -23,7 +24,8 @@ class Preferences extends React.Component { currentTab: 'STORAGES', UIAlert: '', HotkeyAlert: '', - BlogAlert: '' + BlogAlert: '', + ExportAlert: '' } } @@ -87,6 +89,15 @@ class Preferences extends React.Component { haveToSave={alert => this.setState({BlogAlert: alert})} /> ) + case 'EXPORT': + return ( + this.setState({ExportAlert: alert})} + /> + ) case 'SNIPPET': return ( Date: Thu, 15 Nov 2018 22:48:14 +0100 Subject: [PATCH 02/18] add YAML front matter when exporting --- browser/components/MarkdownEditor.js | 4 +- browser/components/MarkdownPreview.js | 306 ++---------------- browser/components/MarkdownSplitEditor.js | 4 +- browser/main/Detail/MarkdownNoteDetail.js | 8 + browser/main/SideNav/StorageItem.js | 52 ++- browser/main/SideNav/index.js | 1 + browser/main/lib/ConfigManager.js | 5 +- .../main/lib/dataApi/attachmentManagement.js | 12 + browser/main/lib/dataApi/exportFolder.js | 74 +++-- browser/main/lib/dataApi/exportNote.js | 26 +- browser/main/lib/dataApi/exportStorage.js | 73 +++-- browser/main/lib/dataApi/formatHTML.js | 295 +++++++++++++++++ browser/main/lib/dataApi/formatMarkdown.js | 94 ++++++ browser/main/lib/dataApi/moveNote.js | 1 - .../main/modals/PreferencesModal/ExportTab.js | 43 ++- browser/main/modals/PreferencesModal/index.js | 2 +- tests/dataApi/exportFolder-test.js | 10 +- tests/dataApi/exportStorage-test.js | 11 +- 18 files changed, 639 insertions(+), 382 deletions(-) create mode 100644 browser/main/lib/dataApi/formatHTML.js create mode 100644 browser/main/lib/dataApi/formatMarkdown.js 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')}
- this.handleExportChange(e)} + checked={config.export.prefixAttachmentFolder} + ref='prefixAttachmentFolder' + type='checkbox' + />  + {i18n.__('Prefix attachment folder')} + +
+
From d76b7235db4ee61e957b8ff8061e37a6f2fb9981 Mon Sep 17 00:00:00 2001 From: Baptiste Augrain Date: Sat, 15 Dec 2018 12:42:39 +0100 Subject: [PATCH 06/18] export styles of code blocks --- browser/components/MarkdownSplitEditor.js | 1 + browser/lib/markdown.js | 13 +- browser/main/SideNav/StorageItem.js | 14 -- browser/main/lib/dataApi/copyFile.js | 8 +- browser/main/lib/dataApi/exportFolder.js | 1 + browser/main/lib/dataApi/exportNote.js | 4 +- browser/main/lib/dataApi/exportStorage.js | 1 + browser/main/lib/dataApi/formatHTML.js | 183 +++++++++++++++++++--- 8 files changed, 179 insertions(+), 46 deletions(-) diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js index 5eb476ff..79fb0aa7 100644 --- a/browser/components/MarkdownSplitEditor.js +++ b/browser/components/MarkdownSplitEditor.js @@ -187,6 +187,7 @@ class MarkdownSplitEditor extends React.Component { codeBlockTheme={config.preview.codeBlockTheme} codeBlockFontFamily={config.editor.fontFamily} lineNumber={config.preview.lineNumber} + indentSize={editorIndentSize} scrollPastEnd={config.preview.scrollPastEnd} smartQuotes={config.preview.smartQuotes} smartArrows={config.preview.smartArrows} diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index 2a7b66b0..a7de2abc 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -28,7 +28,8 @@ class Markdown { html: true, xhtmlOut: true, breaks: config.preview.breaks, - sanitize: 'STRICT' + sanitize: 'STRICT', + onFence: () => {} } const updatedOptions = Object.assign(defaultOptions, options) @@ -141,30 +142,40 @@ class Markdown { token.parameters.format = 'yaml' } + updatedOptions.onFence('chart', token.parameters.format) + return `
           ${token.fileName}
           
${token.content}
` }, flowchart: token => { + updatedOptions.onFence('flowchart') + return `
           ${token.fileName}
           
${token.content}
` }, mermaid: token => { + updatedOptions.onFence('mermaid') + return `
           ${token.fileName}
           
${token.content}
` }, sequence: token => { + updatedOptions.onFence('sequence') + return `
           ${token.fileName}
           
${token.content}
` } }, token => { + updatedOptions.onFence('code', token.langType) + return `
         ${token.fileName}
         ${createGutter(token.content, token.firstLineNumber)}
diff --git a/browser/main/SideNav/StorageItem.js b/browser/main/SideNav/StorageItem.js
index 595efd85..ae86e5be 100644
--- a/browser/main/SideNav/StorageItem.js
+++ b/browser/main/SideNav/StorageItem.js
@@ -230,20 +230,6 @@ class StorageItem extends React.Component {
               folderKey: data.folderKey,
               fileType: data.fileType
             })
-            return data
-          })
-          .then(data => {
-            dialog.showMessageBox(remote.getCurrentWindow(), {
-              type: 'info',
-              message: 'Exported to "' + data.exportDir + '"'
-            })
-          })
-          .catch(err => {
-            dialog.showErrorBox(
-              'Export error',
-              err ? err.message || err : 'Unexpected error during export'
-            )
-            throw err
           })
           .catch(error => {
             dialog.showErrorBox(
diff --git a/browser/main/lib/dataApi/copyFile.js b/browser/main/lib/dataApi/copyFile.js
index 6f23aae2..f079c7db 100755
--- a/browser/main/lib/dataApi/copyFile.js
+++ b/browser/main/lib/dataApi/copyFile.js
@@ -1,5 +1,6 @@
-const fs = require('fs')
-const path = require('path')
+import fs from 'fs'
+import fx from 'fs-extra'
+import path from 'path'
 
 /**
  * @description Copy a file from source to destination
@@ -14,7 +15,8 @@ function copyFile (srcPath, dstPath) {
 
   return new Promise((resolve, reject) => {
     const dstFolder = path.dirname(dstPath)
-    if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder)
+
+    fx.ensureDirSync(dstFolder)
 
     const input = fs.createReadStream(decodeURI(srcPath))
     const output = fs.createWriteStream(dstPath)
diff --git a/browser/main/lib/dataApi/exportFolder.js b/browser/main/lib/dataApi/exportFolder.js
index aef7bd8e..63b5c3a1 100644
--- a/browser/main/lib/dataApi/exportFolder.js
+++ b/browser/main/lib/dataApi/exportFolder.js
@@ -55,6 +55,7 @@ function exportFolder (storageKey, folderKey, fileType, exportDir, config) {
           codeBlockTheme: config.preview.codeBlockTheme,
           codeBlockFontFamily: config.editor.fontFamily,
           lineNumber: config.preview.lineNumber,
+          indentSize: config.editor.indentSize,
           scrollPastEnd: config.preview.scrollPastEnd,
           smartQuotes: config.preview.smartQuotes,
           breaks: config.preview.breaks,
diff --git a/browser/main/lib/dataApi/exportNote.js b/browser/main/lib/dataApi/exportNote.js
index 2130e94d..f17e06c4 100755
--- a/browser/main/lib/dataApi/exportNote.js
+++ b/browser/main/lib/dataApi/exportNote.js
@@ -81,14 +81,14 @@ function rollbackExport (tasks) {
     }
 
     if (fs.existsSync(fullpath)) {
-      fs.unlink(fullpath)
+      fs.unlinkSync(fullpath)
       folders.add(path.dirname(fullpath))
     }
   })
 
   folders.forEach((folder) => {
     if (fs.readdirSync(folder).length === 0) {
-      fs.rmdir(folder)
+      fs.rmdirSync(folder)
     }
   })
 }
diff --git a/browser/main/lib/dataApi/exportStorage.js b/browser/main/lib/dataApi/exportStorage.js
index 1bcacd51..fab3aee2 100644
--- a/browser/main/lib/dataApi/exportStorage.js
+++ b/browser/main/lib/dataApi/exportStorage.js
@@ -54,6 +54,7 @@ function exportStorage (storageKey, fileType, exportDir, config) {
           codeBlockTheme: config.preview.codeBlockTheme,
           codeBlockFontFamily: config.editor.fontFamily,
           lineNumber: config.preview.lineNumber,
+          indentSize: config.editor.indentSize,
           scrollPastEnd: config.preview.scrollPastEnd,
           smartQuotes: config.preview.smartQuotes,
           breaks: config.preview.breaks,
diff --git a/browser/main/lib/dataApi/formatHTML.js b/browser/main/lib/dataApi/formatHTML.js
index de99421f..eb44e9ce 100644
--- a/browser/main/lib/dataApi/formatHTML.js
+++ b/browser/main/lib/dataApi/formatHTML.js
@@ -1,5 +1,6 @@
 import path from 'path'
 import fileUrl from 'file-url'
+import fs from 'fs'
 import { remote } from 'electron'
 import consts from 'browser/lib/consts'
 import Markdown from 'browser/lib/markdown'
@@ -33,6 +34,14 @@ const defaultCodeBlockFontFamily = [
   'monospace'
 ]
 
+function unprefix (file) {
+  if (global.process.platform === 'win32') {
+    return file.replace('file:///', '')
+  } else {
+    return file.replace('file://', '')
+  }
+}
+
 /**
  * ```
  * {
@@ -49,7 +58,8 @@ const defaultCodeBlockFontFamily = [
  *   sanitize,
  *   breaks,
  *   storagePath,
- *   export
+ *   export,
+ *   indentSize
  * }
  * ```
  */
@@ -76,29 +86,149 @@ export default function formatHTML (props) {
     allowCustomCSS,
     customCSS
   )
+
   const { smartQuotes, sanitize, breaks } = props
 
-  const markdown = new Markdown({
-    typographer: smartQuotes,
-    sanitize,
-    breaks
-  })
+  let indentSize = parseInt(props.indentSize, 10)
+  if (!(indentSize > 0 && indentSize < 132)) {
+    indentSize = 4
+  }
 
   const files = [getCodeThemeLink(codeBlockTheme), ...CSS_FILES]
 
   return function (note, targetPath, exportTasks) {
+    let styles = files.map(file => ``).join('\n')
+
+    let inlineScripts = ''
+    let scripts = ''
+
+    let lodash = false
+    function addLodash () {
+      if (lodash) {
+        return
+      }
+
+      lodash = true
+
+      exportTasks.push({
+        src: unprefix(`${appPath}/node_modules/lodash/lodash.min.js`),
+        dst: 'js'
+      })
+
+      scripts += ``
+    }
+
+    let codemirror = false
+    function addCodeMirror () {
+      if (codemirror) {
+        return
+      }
+
+      codemirror = true
+
+      addLodash()
+
+      exportTasks.push({
+        src: unprefix(`${appPath}/node_modules/codemirror/lib/codemirror.js`),
+        dst: 'js/codemirror'
+      }, {
+        src: unprefix(`${appPath}/node_modules/codemirror/mode/meta.js`),
+        dst: 'js/codemirror/mode'
+      }, {
+        src: unprefix(`${appPath}/node_modules/codemirror/addon/mode/loadmode.js`),
+        dst: 'js/codemirror/addon/mode'
+      }, {
+        src: unprefix(`${appPath}/node_modules/codemirror/addon/runmode/runmode.js`),
+        dst: 'js/codemirror/addon/runmode'
+      })
+
+      scripts += `
+
+
+
+
+`
+
+      let className = `cm-s-${codeBlockTheme}`
+      if (codeBlockTheme.indexOf('solarized') === 0) {
+        const [refThema, color] = codeBlockTheme.split(' ')
+        className = `cm-s-${refThema} cm-s-${color}`
+      }
+
+      inlineScripts += `
+CodeMirror.modeURL = 'js/codemirror/mode/%N/%N.js';
+
+function decodeEntities (text) {
+  var entities = [
+    ['apos', '\\''],
+    ['amp', '&'],
+    ['lt', '<'],
+    ['gt', '>'],
+    ['#63', '\\?'],
+    ['#36', '\\$']
+  ]
+
+  for (var i = 0, max = entities.length; i < max; ++i) {
+    text = text.replace(new RegExp(\`&\${entities[i][0]};\`, 'g'), entities[i][1])
+  }
+
+  return text
+}
+
+function displayCodeBlocks () {
+  _.forEach(
+    document.querySelectorAll('.code code'),
+    el => {
+      let syntax = CodeMirror.findModeByName(el.className)
+      if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
+      CodeMirror.requireMode(syntax.mode, () => {
+        const content = decodeEntities(el.innerHTML)
+        el.innerHTML = ''
+        el.parentNode.className += ' ${className}'
+        CodeMirror.runMode(content, syntax.mime, el, {
+          tabSize: ${indentSize}
+        })
+      })
+    }
+  )
+}
+
+document.addEventListener('DOMContentLoaded', displayCodeBlocks);
+`
+    }
+
+    const modes = {}
+    const markdown = new Markdown({
+      typographer: smartQuotes,
+      sanitize,
+      breaks,
+      onFence (type, mode) {
+        if (type === 'code') {
+          addCodeMirror()
+
+          if (mode && modes[mode] !== true) {
+            const file = unprefix(`${appPath}/node_modules/codemirror/mode/${mode}/${mode}.js`)
+
+            if (fs.existsSync(file)) {
+              exportTasks.push({
+                src: file,
+                dst: `js/codemirror/mode/${mode}`
+              })
+
+              modes[mode] = true
+            }
+          }
+        }
+      }
+    })
+
     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,
+        src: unprefix(file),
         dst: 'css'
       })
     })
@@ -114,20 +244,21 @@ export default function formatHTML (props) {
 
     body = attachmentManagement.replaceStorageReferences(body, note.key, destinationFolder)
 
-    let styles = ''
-    files.forEach(file => {
-      styles += ``
-    })
-
-    return `
-                
-                  
-                  
-                  
-                  ${styles}
-                
-                ${body}
-            `
+    return `
+
+
+  
+  
+  
+  ${styles}
+  ${scripts}
+  
+
+
+${body}
+
+
+`
   }
 }
 

From 9813412c8e642e55a176cd5a7a2b7e4ef06ac5d4 Mon Sep 17 00:00:00 2001
From: Baptiste Augrain 
Date: Sat, 15 Dec 2018 14:55:13 +0100
Subject: [PATCH 07/18] add export menu in note list's context menu

---
 browser/main/NoteList/index.js           | 55 ++++++++++++++++++++++++
 browser/main/lib/dataApi/exportNoteAs.js | 45 +++++++++++++++++++
 browser/main/lib/dataApi/index.js        |  1 +
 3 files changed, 101 insertions(+)
 create mode 100644 browser/main/lib/dataApi/exportNoteAs.js

diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js
index d1c8d14a..78748a7f 100644
--- a/browser/main/NoteList/index.js
+++ b/browser/main/NoteList/index.js
@@ -22,6 +22,7 @@ import Markdown from '../../lib/markdown'
 import i18n from 'browser/lib/i18n'
 import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
 import context from 'browser/lib/context'
+import filenamify from 'filenamify'
 
 const { remote } = require('electron')
 const { dialog } = remote
@@ -527,6 +528,38 @@ class NoteList extends React.Component {
     this.selectNextNote()
   }
 
+  handleExportClick (e, note, fileType) {
+    const options = {
+      defaultPath: filenamify(note.title, {
+        replacement: '_'
+      }),
+      filters: [{ name: 'Documents', extensions: [fileType] }],
+      properties: ['openFile', 'createDirectory']
+    }
+
+    dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
+      if (filename) {
+        const { config } = this.props
+
+        dataApi
+          .exportNoteAs(note, filename, fileType, config)
+          .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
+          })
+      }
+    })
+  }
+
   handleNoteContextMenu (e, uniqueKey) {
     const { location } = this.props
     const { selectedNoteKeys } = this.state
@@ -572,10 +605,30 @@ class NoteList extends React.Component {
       }, {
         label: copyNoteLink,
         click: this.copyNoteLink.bind(this, note)
+      }, {
+        type: 'separator'
+      }, {
+        label: i18n.__('Export Note'),
+        submenu: [
+          {
+            label: i18n.__('Export as Plain Text (.txt)'),
+            click: e => this.handleExportClick(e, note, 'txt')
+          },
+          {
+            label: i18n.__('Export as Markdown (.md)'),
+            click: e => this.handleExportClick(e, note, 'md')
+          },
+          {
+            label: i18n.__('Export as HTML (.html)'),
+            click: e => this.handleExportClick(e, note, 'html')
+          }
+        ]
       })
       if (note.type === 'MARKDOWN_NOTE') {
         if (note.blog && note.blog.blogLink && note.blog.blogId) {
           templates.push({
+            type: 'separator'
+          }, {
             label: updateLabel,
             click: this.publishMarkdown.bind(this)
           }, {
@@ -584,6 +637,8 @@ class NoteList extends React.Component {
           })
         } else {
           templates.push({
+            type: 'separator'
+          }, {
             label: publishLabel,
             click: this.publishMarkdown.bind(this)
           })
diff --git a/browser/main/lib/dataApi/exportNoteAs.js b/browser/main/lib/dataApi/exportNoteAs.js
new file mode 100644
index 00000000..87c7f117
--- /dev/null
+++ b/browser/main/lib/dataApi/exportNoteAs.js
@@ -0,0 +1,45 @@
+import { findStorage } from 'browser/lib/findStorage'
+import exportNote from './exportNote'
+import formatMarkdown from './formatMarkdown'
+import formatHTML from './formatHTML'
+
+/**
+ * @param {Object} note
+ * @param {String} filename
+ * @param {String} fileType
+ * @param {Object} config
+ */
+
+function exportNoteAs (note, filename, fileType, config) {
+  const storage = findStorage(note.storage)
+
+  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,
+      indentSize: config.editor.indentSize,
+      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 exportNote(storage.key, note, filename, contentFormatter)
+}
+
+module.exports = exportNoteAs
diff --git a/browser/main/lib/dataApi/index.js b/browser/main/lib/dataApi/index.js
index 92be6b93..b6e53078 100644
--- a/browser/main/lib/dataApi/index.js
+++ b/browser/main/lib/dataApi/index.js
@@ -14,6 +14,7 @@ const dataApi = {
   updateNote: require('./updateNote'),
   deleteNote: require('./deleteNote'),
   moveNote: require('./moveNote'),
+  exportNoteAs: require('./exportNoteAs'),
   migrateFromV5Storage: require('./migrateFromV5Storage'),
   createSnippet: require('./createSnippet'),
   deleteSnippet: require('./deleteSnippet'),

From d6a54b8a2615f5610d8f5268a984c90fec833872 Mon Sep 17 00:00:00 2001
From: Baptiste Augrain 
Date: Sat, 15 Dec 2018 15:53:49 +0100
Subject: [PATCH 08/18] export diagrams

---
 browser/main/lib/dataApi/formatHTML.js | 261 +++++++++++++++++++++++--
 1 file changed, 243 insertions(+), 18 deletions(-)

diff --git a/browser/main/lib/dataApi/formatHTML.js b/browser/main/lib/dataApi/formatHTML.js
index eb44e9ce..c879f4cd 100644
--- a/browser/main/lib/dataApi/formatHTML.js
+++ b/browser/main/lib/dataApi/formatHTML.js
@@ -102,6 +102,33 @@ export default function formatHTML (props) {
     let inlineScripts = ''
     let scripts = ''
 
+    let decodeEntities = false
+    function addDecodeEntities () {
+      if (decodeEntities) {
+        return
+      }
+
+      decodeEntities = true
+
+      inlineScripts += `
+function decodeEntities (text) {
+  var entities = [
+    ['apos', '\\''],
+    ['amp', '&'],
+    ['lt', '<'],
+    ['gt', '>'],
+    ['#63', '\\?'],
+    ['#36', '\\$']
+  ]
+
+  for (var i = 0, max = entities.length; i < max; ++i) {
+    text = text.replace(new RegExp(\`&\${entities[i][0]};\`, 'g'), entities[i][1])
+  }
+
+  return text
+}`
+    }
+
     let lodash = false
     function addLodash () {
       if (lodash) {
@@ -118,6 +145,87 @@ export default function formatHTML (props) {
       scripts += ``
     }
 
+    let raphael = false
+    function addRaphael () {
+      if (raphael) {
+        return
+      }
+
+      raphael = true
+
+      exportTasks.push({
+        src: unprefix(`${appPath}/node_modules/raphael/raphael.min.js`),
+        dst: 'js'
+      })
+
+      scripts += ``
+    }
+
+    let yaml = false
+    function addYAML () {
+      if (yaml) {
+        return
+      }
+
+      yaml = true
+
+      exportTasks.push({
+        src: unprefix(`${appPath}/node_modules/js-yaml/dist/js-yaml.min.js`),
+        dst: 'js'
+      })
+
+      scripts += ``
+    }
+
+    let chart = false
+    function addChart () {
+      if (chart) {
+        return
+      }
+
+      chart = true
+
+      addLodash()
+
+      exportTasks.push({
+        src: unprefix(`${appPath}/node_modules/chart.js/dist/Chart.min.js`),
+        dst: 'js'
+      })
+
+      scripts += ``
+
+      inlineScripts += `
+function displayCharts () {
+  _.forEach(
+    document.querySelectorAll('.chart'),
+    el => {
+      try {
+        const format = el.attributes.getNamedItem('data-format').value
+        const chartConfig = format === 'yaml' ? jsyaml.load(el.innerHTML) : JSON.parse(el.innerHTML)
+        el.innerHTML = ''
+
+        const canvas = document.createElement('canvas')
+        el.appendChild(canvas)
+
+        const height = el.attributes.getNamedItem('data-height')
+        if (height && height.value !== 'undefined') {
+          el.style.height = height.value + 'vh'
+          canvas.height = height.value + 'vh'
+        }
+
+        const chart = new Chart(canvas, chartConfig)
+      } catch (e) {
+        el.className = 'chart-error'
+        el.innerHTML = 'chartjs diagram parse error: ' + e.message
+      }
+    }
+  )
+}
+
+document.addEventListener('DOMContentLoaded', displayCharts);
+`
+    }
+
     let codemirror = false
     function addCodeMirror () {
       if (codemirror) {
@@ -126,6 +234,7 @@ export default function formatHTML (props) {
 
       codemirror = true
 
+      addDecodeEntities()
       addLodash()
 
       exportTasks.push({
@@ -158,23 +267,6 @@ export default function formatHTML (props) {
       inlineScripts += `
 CodeMirror.modeURL = 'js/codemirror/mode/%N/%N.js';
 
-function decodeEntities (text) {
-  var entities = [
-    ['apos', '\\''],
-    ['amp', '&'],
-    ['lt', '<'],
-    ['gt', '>'],
-    ['#63', '\\?'],
-    ['#36', '\\$']
-  ]
-
-  for (var i = 0, max = entities.length; i < max; ++i) {
-    text = text.replace(new RegExp(\`&\${entities[i][0]};\`, 'g'), entities[i][1])
-  }
-
-  return text
-}
-
 function displayCodeBlocks () {
   _.forEach(
     document.querySelectorAll('.code code'),
@@ -197,13 +289,140 @@ document.addEventListener('DOMContentLoaded', displayCodeBlocks);
 `
     }
 
+    let flowchart = false
+    function addFlowchart () {
+      if (flowchart) {
+        return
+      }
+
+      flowchart = true
+
+      addDecodeEntities()
+      addLodash()
+      addRaphael()
+
+      exportTasks.push({
+        src: unprefix(`${appPath}/node_modules/flowchart.js/release/flowchart.min.js`),
+        dst: 'js'
+      })
+
+      scripts += ``
+
+      inlineScripts += `
+function displayFlowcharts () {
+  _.forEach(
+    document.querySelectorAll('.flowchart'),
+    el => {
+      try {
+        const diagram = flowchart.parse(
+          decodeEntities(el.innerHTML)
+        )
+        el.innerHTML = ''
+        diagram.drawSVG(el)
+      } catch (e) {
+        el.className = 'flowchart-error'
+        el.innerHTML = 'Flowchart parse error: ' + e.message
+      }
+    }
+  )
+}
+
+document.addEventListener('DOMContentLoaded', displayFlowcharts);
+`
+    }
+
+    let mermaid = false
+    function addMermaid () {
+      if (mermaid) {
+        return
+      }
+
+      mermaid = true
+
+      addLodash()
+
+      exportTasks.push({
+        src: unprefix(`${appPath}/node_modules/mermaid/dist/mermaid.min.js`),
+        dst: 'js'
+      })
+
+      scripts += ``
+
+      const isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai' || theme === 'dracula'
+
+      inlineScripts += `
+function displayMermaids () {
+  _.forEach(
+    document.querySelectorAll('.mermaid'),
+    el => {
+      const height = el.attributes.getNamedItem('data-height')
+      if (height && height.value !== 'undefined') {
+        el.style.height = height.value + 'vh'
+      }
+    }
+  )
+}
+
+document.addEventListener('DOMContentLoaded', displayMermaids);
+`
+    }
+
+    let sequence = false
+    function addSequence () {
+      if (sequence) {
+        return
+      }
+
+      sequence = true
+
+      addDecodeEntities()
+      addLodash()
+      addRaphael()
+
+      exportTasks.push({
+        src: unprefix(`${appPath}/node_modules/js-sequence-diagrams/fucknpm/sequence-diagram-min.js`),
+        dst: 'js'
+      })
+
+      scripts += ``
+
+      inlineScripts += `
+function displaySequences () {
+  _.forEach(
+    document.querySelectorAll('.sequence'),
+    el => {
+       try {
+        const diagram = Diagram.parse(
+          decodeEntities(el.innerHTML)
+        )
+        el.innerHTML = ''
+        diagram.drawSVG(el, { theme: 'simple' })
+      } catch (e) {
+        el.className = 'sequence-error'
+        el.innerHTML = 'Sequence diagram parse error: ' + e.message
+      }
+    }
+  )
+}
+
+document.addEventListener('DOMContentLoaded', displaySequences);
+`
+    }
+
     const modes = {}
     const markdown = new Markdown({
       typographer: smartQuotes,
       sanitize,
       breaks,
       onFence (type, mode) {
-        if (type === 'code') {
+        if (type === 'chart') {
+          addChart()
+
+          if (mode === 'yaml') {
+            addYAML()
+          }
+        }
+        else if (type === 'code') {
           addCodeMirror()
 
           if (mode && modes[mode] !== true) {
@@ -218,6 +437,12 @@ document.addEventListener('DOMContentLoaded', displayCodeBlocks);
               modes[mode] = true
             }
           }
+        } else if (type === 'flowchart') {
+          addFlowchart()
+        } else if (type === 'mermaid') {
+          addMermaid()
+        } else if (type === 'sequence') {
+          addSequence()
         }
       }
     })

From fa157f6f76c6a9913db1fd5dd1d7a0b1bce9916c Mon Sep 17 00:00:00 2001
From: Baptiste Augrain 
Date: Sat, 15 Dec 2018 15:57:37 +0100
Subject: [PATCH 09/18] fix lint error

---
 browser/main/lib/dataApi/formatHTML.js | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/browser/main/lib/dataApi/formatHTML.js b/browser/main/lib/dataApi/formatHTML.js
index c879f4cd..c4fce8e3 100644
--- a/browser/main/lib/dataApi/formatHTML.js
+++ b/browser/main/lib/dataApi/formatHTML.js
@@ -421,8 +421,7 @@ document.addEventListener('DOMContentLoaded', displaySequences);
           if (mode === 'yaml') {
             addYAML()
           }
-        }
-        else if (type === 'code') {
+        } else if (type === 'code') {
           addCodeMirror()
 
           if (mode && modes[mode] !== true) {

From 5414fe338472169b8a0a32722c2bc6a6ac95fea6 Mon Sep 17 00:00:00 2001
From: Baptiste Augrain 
Date: Fri, 12 Jun 2020 15:53:23 +0200
Subject: [PATCH 10/18] export tag

---
 browser/main/SideNav/index.js         | 109 +++++++++++++++++++-------
 browser/main/lib/dataApi/exportTag.js |  30 +++++++
 browser/main/lib/dataApi/index.js     |   1 +
 3 files changed, 111 insertions(+), 29 deletions(-)
 create mode 100644 browser/main/lib/dataApi/exportTag.js

diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js
index 7211a85c..a8f0489d 100644
--- a/browser/main/SideNav/index.js
+++ b/browser/main/SideNav/index.js
@@ -26,6 +26,8 @@ import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
 import ColorPicker from 'browser/components/ColorPicker'
 import { every, sortBy } from 'lodash'
 
+const { dialog } = remote
+
 function matchActiveTags(tags, activeTags) {
   return every(activeTags, v => tags.indexOf(v) >= 0)
 }
@@ -63,15 +65,12 @@ class SideNav extends React.Component {
   }
 
   deleteTag(tag) {
-    const selectedButton = remote.dialog.showMessageBox(
-      remote.getCurrentWindow(),
-      {
-        type: 'warning',
-        message: i18n.__('Confirm tag deletion'),
-        detail: i18n.__('This will permanently remove this tag.'),
-        buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
-      }
-    )
+    const selectedButton = dialog.showMessageBox(remote.getCurrentWindow(), {
+      type: 'warning',
+      message: i18n.__('Confirm tag deletion'),
+      detail: i18n.__('This will permanently remove this tag.'),
+      buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
+    })
 
     if (selectedButton === 0) {
       const {
@@ -155,28 +154,80 @@ class SideNav extends React.Component {
   }
 
   handleTagContextMenu(e, tag) {
-    const menu = []
+    context.popup([
+      {
+        label: i18n.__('Rename Tag'),
+        click: this.handleRenameTagClick.bind(this, tag)
+      },
+      {
+        label: i18n.__('Customize Color'),
+        click: this.displayColorPicker.bind(
+          this,
+          tag,
+          e.target.getBoundingClientRect()
+        )
+      },
+      {
+        type: 'separator'
+      },
+      {
+        label: i18n.__('Export Tag'),
+        submenu: [
+          {
+            label: i18n.__('Export as Plain Text (.txt)'),
+            click: e => this.handleExportTagClick(e, tag, 'txt')
+          },
+          {
+            label: i18n.__('Export as Markdown (.md)'),
+            click: e => this.handleExportTagClick(e, tag, 'md')
+          },
+          {
+            label: i18n.__('Export as HTML (.html)'),
+            click: e => this.handleExportTagClick(e, tag, 'html')
+          },
+          {
+            label: i18n.__('Export as PDF (.pdf)'),
+            click: e => this.handleExportTagClick(e, tag, 'pdf')
+          }
+        ]
+      },
+      {
+        type: 'separator'
+      },
+      {
+        label: i18n.__('Delete Tag'),
+        click: this.deleteTag.bind(this, tag)
+      }
+    ])
+  }
 
-    menu.push({
-      label: i18n.__('Delete Tag'),
-      click: this.deleteTag.bind(this, tag)
+  handleExportTagClick(e, tag, fileType) {
+    const options = {
+      properties: ['openDirectory', 'createDirectory'],
+      buttonLabel: i18n.__('Select directory'),
+      title: i18n.__('Select a folder to export the files to'),
+      multiSelections: false
+    }
+    dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
+      if (paths && paths.length === 1) {
+        const { data, config } = this.props
+        dataApi
+          .exportTag(data, tag, fileType, paths[0], config)
+          .then(data => {
+            dialog.showMessageBox(remote.getCurrentWindow(), {
+              type: 'info',
+              message: `Exported to ${paths[0]}`
+            })
+          })
+          .catch(error => {
+            dialog.showErrorBox(
+              'Export error',
+              error ? error.message || error : 'Unexpected error during export'
+            )
+            throw error
+          })
+      }
     })
-
-    menu.push({
-      label: i18n.__('Customize Color'),
-      click: this.displayColorPicker.bind(
-        this,
-        tag,
-        e.target.getBoundingClientRect()
-      )
-    })
-
-    menu.push({
-      label: i18n.__('Rename Tag'),
-      click: this.handleRenameTagClick.bind(this, tag)
-    })
-
-    context.popup(menu)
   }
 
   dismissColorPicker() {
diff --git a/browser/main/lib/dataApi/exportTag.js b/browser/main/lib/dataApi/exportTag.js
new file mode 100644
index 00000000..a4a0cd2a
--- /dev/null
+++ b/browser/main/lib/dataApi/exportTag.js
@@ -0,0 +1,30 @@
+import exportNoteAs from './exportNoteAs'
+import filenamify from 'filenamify'
+import path from 'path'
+
+/**
+ * @param {Object} data
+ * @param {String} tag
+ * @param {String} fileType
+ * @param {String} exportDir
+ * @param {Object} config
+ */
+
+function exportTag(data, tag, fileType, exportDir, config) {
+  const notes = data.noteMap
+    .map(note => note)
+    .filter(note => note.tags.indexOf(tag) !== -1)
+
+  return Promise.all(
+    notes.map(note => {
+      const filename = path.join(
+        exportDir,
+        `${filenamify(note.title, { replacement: '_' })}.${fileType}`
+      )
+
+      return exportNoteAs(note, filename, fileType, config)
+    })
+  )
+}
+
+module.exports = exportTag
diff --git a/browser/main/lib/dataApi/index.js b/browser/main/lib/dataApi/index.js
index 60eef9c3..de7e42a9 100644
--- a/browser/main/lib/dataApi/index.js
+++ b/browser/main/lib/dataApi/index.js
@@ -21,6 +21,7 @@ const dataApi = {
   deleteSnippet: require('./deleteSnippet'),
   updateSnippet: require('./updateSnippet'),
   fetchSnippet: require('./fetchSnippet'),
+  exportTag: require('./exportTag'),
 
   _migrateFromV6Storage: require('./migrateFromV6Storage'),
   _resolveStorageData: require('./resolveStorageData'),

From 80b89484333f0866517cd84b6f8228d2d04ea043 Mon Sep 17 00:00:00 2001
From: Baptiste Augrain 
Date: Fri, 12 Jun 2020 16:18:27 +0200
Subject: [PATCH 11/18] - export untitled notes as 'Untitled' based on the
 language - export notes with duplicate title as ' (<index>)'

---
 browser/main/lib/dataApi/exportFolder.js  | 11 ++++---
 browser/main/lib/dataApi/exportStorage.js | 11 +++++--
 browser/main/lib/dataApi/exportTag.js     | 10 +++---
 browser/main/lib/dataApi/getFilename.js   | 37 +++++++++++++++++++++++
 browser/main/lib/dataApi/index.js         |  1 +
 locales/de.json                           |  1 -
 6 files changed, 57 insertions(+), 14 deletions(-)
 create mode 100644 browser/main/lib/dataApi/getFilename.js

diff --git a/browser/main/lib/dataApi/exportFolder.js b/browser/main/lib/dataApi/exportFolder.js
index 51df1dc4..6ac56abf 100644
--- a/browser/main/lib/dataApi/exportFolder.js
+++ b/browser/main/lib/dataApi/exportFolder.js
@@ -1,8 +1,7 @@
 import { findStorage } from 'browser/lib/findStorage'
 import resolveStorageData from './resolveStorageData'
 import resolveStorageNotes from './resolveStorageNotes'
-import filenamify from 'filenamify'
-import path from 'path'
+import getFilename from './getFilename'
 import exportNote from './exportNote'
 import getContentFormatter from './getContentFormatter'
 
@@ -32,6 +31,8 @@ function exportFolder(storageKey, folderKey, fileType, exportDir, config) {
     return Promise.reject(e)
   }
 
+  const deduplicator = {}
+
   return resolveStorageData(targetStorage)
     .then(storage => {
       return resolveStorageNotes(storage).then(notes => ({
@@ -49,9 +50,11 @@ function exportFolder(storageKey, folderKey, fileType, exportDir, config) {
 
       return Promise.all(
         notes.map(note => {
-          const targetPath = path.join(
+          const targetPath = getFilename(
+            note,
+            fileType,
             exportDir,
-            `${filenamify(note.title, { replacement: '_' })}.${fileType}`
+            deduplicator
           )
 
           return exportNote(storage.key, note, targetPath, contentFormatter)
diff --git a/browser/main/lib/dataApi/exportStorage.js b/browser/main/lib/dataApi/exportStorage.js
index 0cbbcc5b..14f581dd 100644
--- a/browser/main/lib/dataApi/exportStorage.js
+++ b/browser/main/lib/dataApi/exportStorage.js
@@ -7,6 +7,7 @@ import fs from 'fs'
 import exportNote from './exportNote'
 import formatMarkdown from './formatMarkdown'
 import formatHTML from './formatHTML'
+import getFilename from './getFilename'
 
 /**
  * @param {String} storageKey
@@ -83,11 +84,15 @@ function exportStorage(storageKey, fileType, exportDir, config) {
         } catch (e) {}
       })
 
+      const deduplicator = {}
+
       return Promise.all(
         notes.map(note => {
-          const targetPath = path.join(
-            folderNamesMapping[note.folder],
-            `${filenamify(note.title, { replacement: '_' })}.${fileType}`
+          const targetPath = getFilename(
+            note,
+            fileType,
+            exportDir,
+            deduplicator
           )
 
           return exportNote(storage.key, note, targetPath, contentFormatter)
diff --git a/browser/main/lib/dataApi/exportTag.js b/browser/main/lib/dataApi/exportTag.js
index a4a0cd2a..0bef11b9 100644
--- a/browser/main/lib/dataApi/exportTag.js
+++ b/browser/main/lib/dataApi/exportTag.js
@@ -1,6 +1,5 @@
 import exportNoteAs from './exportNoteAs'
-import filenamify from 'filenamify'
-import path from 'path'
+import getFilename from './getFilename'
 
 /**
  * @param {Object} data
@@ -15,12 +14,11 @@ function exportTag(data, tag, fileType, exportDir, config) {
     .map(note => note)
     .filter(note => note.tags.indexOf(tag) !== -1)
 
+  const deduplicator = {}
+
   return Promise.all(
     notes.map(note => {
-      const filename = path.join(
-        exportDir,
-        `${filenamify(note.title, { replacement: '_' })}.${fileType}`
-      )
+      const filename = getFilename(note, fileType, exportDir, deduplicator)
 
       return exportNoteAs(note, filename, fileType, config)
     })
diff --git a/browser/main/lib/dataApi/getFilename.js b/browser/main/lib/dataApi/getFilename.js
new file mode 100644
index 00000000..59947954
--- /dev/null
+++ b/browser/main/lib/dataApi/getFilename.js
@@ -0,0 +1,37 @@
+import filenamify from 'filenamify'
+import i18n from 'browser/lib/i18n'
+import path from 'path'
+
+/**
+ * @param {Object} note
+ * @param {String} fileType
+ * @param {String} directory
+ * @param {Object} deduplicator
+ *
+ * @return {String}
+ */
+
+function getFilename(note, fileType, directory, deduplicator) {
+  const basename = note.title
+    ? filenamify(note.title, { replacement: '_' })
+    : i18n.__('Untitled')
+
+  if (deduplicator) {
+    if (deduplicator[basename]) {
+      const filename = path.join(
+        directory,
+        `${basename} (${deduplicator[basename]}).${fileType}`
+      )
+
+      ++deduplicator[basename]
+
+      return filename
+    } else {
+      deduplicator[basename] = 1
+    }
+  }
+
+  return path.join(directory, `${basename}.${fileType}`)
+}
+
+module.exports = getFilename
diff --git a/browser/main/lib/dataApi/index.js b/browser/main/lib/dataApi/index.js
index de7e42a9..5bc85126 100644
--- a/browser/main/lib/dataApi/index.js
+++ b/browser/main/lib/dataApi/index.js
@@ -22,6 +22,7 @@ const dataApi = {
   updateSnippet: require('./updateSnippet'),
   fetchSnippet: require('./fetchSnippet'),
   exportTag: require('./exportTag'),
+  getFilename: require('./getFilename'),
 
   _migrateFromV6Storage: require('./migrateFromV6Storage'),
   _resolveStorageData: require('./resolveStorageData'),
diff --git a/locales/de.json b/locales/de.json
index 22f15957..1d857c90 100644
--- a/locales/de.json
+++ b/locales/de.json
@@ -202,7 +202,6 @@
   "Create new folder": "Ordner erstellen",
   "Folder name": "Ordnername",
   "Create": "Erstellen",
-  "Untitled": "Neuer Ordner",
   "Unlink Storage": "Speicherverknüpfung aufheben",
   "Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.": "Die Verknüpfung des Speichers mit Boostnote wird entfernt. Es werden keine Daten gelöscht. Um die Daten dauerhaft zu löschen musst du den Ordner auf der Festplatte manuell entfernen.",
   "Empty note": "Leere Notiz",

From b678c3bd89f6e5beb257acc3499ba763a710b714 Mon Sep 17 00:00:00 2001
From: Baptiste Augrain <daiyam@zokugun.org>
Date: Fri, 12 Jun 2020 16:40:16 +0200
Subject: [PATCH 12/18] export html is escaping html characters as the preview

---
 browser/components/MarkdownPreview.js  | 32 +++--------------------
 browser/main/lib/dataApi/formatHTML.js | 35 +++++++++++++++++++++++++-
 2 files changed, 37 insertions(+), 30 deletions(-)

diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js
index 41868165..7871704e 100755
--- a/browser/components/MarkdownPreview.js
+++ b/browser/components/MarkdownPreview.js
@@ -23,10 +23,10 @@ import formatHTML, {
   CSS_FILES,
   buildStyle,
   getCodeThemeLink,
-  getStyleParams
+  getStyleParams,
+  escapeHtmlCharactersInCodeTag
 } from 'browser/main/lib/dataApi/formatHTML'
 import formatPDF from 'browser/main/lib/dataApi/formatPDF'
-import { escapeHtmlCharacters } from 'browser/lib/utils'
 import yaml from 'js-yaml'
 import i18n from 'browser/lib/i18n'
 import path from 'path'
@@ -247,32 +247,6 @@ class MarkdownPreview extends React.Component {
     }
   }
 
-  /**
-   * @description Convert special characters between three ```
-   * @param {string[]} splitWithCodeTag Array of HTML strings separated by three ```
-   * @returns {string} HTML in which special characters between three ``` have been converted
-   */
-  escapeHtmlCharactersInCodeTag(splitWithCodeTag) {
-    for (let index = 0; index < splitWithCodeTag.length; index++) {
-      const codeTagRequired =
-        splitWithCodeTag[index] !== '```' && index < splitWithCodeTag.length - 1
-      if (codeTagRequired) {
-        splitWithCodeTag.splice(index + 1, 0, '```')
-      }
-    }
-    let inCodeTag = false
-    let result = ''
-    for (let content of splitWithCodeTag) {
-      if (content === '```') {
-        inCodeTag = !inCodeTag
-      } else if (inCodeTag) {
-        content = escapeHtmlCharacters(content)
-      }
-      result += content
-    }
-    return result
-  }
-
   getScrollBarStyle() {
     const { theme } = this.props
 
@@ -485,7 +459,7 @@ class MarkdownPreview extends React.Component {
     this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
     if (sanitize === 'NONE') {
       const splitWithCodeTag = value.split('```')
-      value = this.escapeHtmlCharactersInCodeTag(splitWithCodeTag)
+      value = escapeHtmlCharactersInCodeTag(splitWithCodeTag)
     }
     const renderedHTML = this.markdown.render(value)
     attachmentManagement.migrateAttachments(value, storagePath, noteKey)
diff --git a/browser/main/lib/dataApi/formatHTML.js b/browser/main/lib/dataApi/formatHTML.js
index 1ab14945..37c3756c 100644
--- a/browser/main/lib/dataApi/formatHTML.js
+++ b/browser/main/lib/dataApi/formatHTML.js
@@ -6,6 +6,7 @@ import consts from 'browser/lib/consts'
 import Markdown from 'browser/lib/markdown'
 import attachmentManagement from './attachmentManagement'
 import { version as codemirrorVersion } from 'codemirror/package.json'
+import { escapeHtmlCharacters } from 'browser/lib/utils'
 
 const { app } = remote
 const appPath = fileUrl(
@@ -467,7 +468,13 @@ document.addEventListener('DOMContentLoaded', displaySequences);
       }
     })
 
-    let body = markdown.render(note.content)
+    let body = note.content
+
+    if (sanitize === 'NONE') {
+      body = escapeHtmlCharactersInCodeTag(body.split('```'))
+    }
+
+    body = markdown.render(note.content)
 
     const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
       note.content,
@@ -761,3 +768,29 @@ body p {
 ${allowCustomCSS ? customCSS : ''}
 `
 }
+
+/**
+ * @description Convert special characters between three ```
+ * @param {string[]} splitWithCodeTag Array of HTML strings separated by three ```
+ * @returns {string} HTML in which special characters between three ``` have been converted
+ */
+export function escapeHtmlCharactersInCodeTag(splitWithCodeTag) {
+  for (let index = 0; index < splitWithCodeTag.length; index++) {
+    const codeTagRequired =
+      splitWithCodeTag[index] !== '```' && index < splitWithCodeTag.length - 1
+    if (codeTagRequired) {
+      splitWithCodeTag.splice(index + 1, 0, '```')
+    }
+  }
+  let inCodeTag = false
+  let result = ''
+  for (let content of splitWithCodeTag) {
+    if (content === '```') {
+      inCodeTag = !inCodeTag
+    } else if (inCodeTag) {
+      content = escapeHtmlCharacters(content)
+    }
+    result += content
+  }
+  return result
+}

From febc98c10150cbd167839904ce88a6c5af88dadc Mon Sep 17 00:00:00 2001
From: Baptiste Augrain <daiyam@zokugun.org>
Date: Fri, 12 Jun 2020 16:51:35 +0200
Subject: [PATCH 13/18] fix exporting storage's notes as PDFs

---
 browser/main/lib/dataApi/exportStorage.js     | 29 ++-----------------
 .../main/lib/dataApi/getContentFormatter.js   |  2 +-
 2 files changed, 3 insertions(+), 28 deletions(-)

diff --git a/browser/main/lib/dataApi/exportStorage.js b/browser/main/lib/dataApi/exportStorage.js
index 14f581dd..6cd80565 100644
--- a/browser/main/lib/dataApi/exportStorage.js
+++ b/browser/main/lib/dataApi/exportStorage.js
@@ -5,8 +5,7 @@ import filenamify from 'filenamify'
 import path from 'path'
 import fs from 'fs'
 import exportNote from './exportNote'
-import formatMarkdown from './formatMarkdown'
-import formatHTML from './formatHTML'
+import getContentFormatter from './getContentFormatter'
 import getFilename from './getFilename'
 
 /**
@@ -43,31 +42,7 @@ function exportStorage(storageKey, fileType, exportDir, config) {
       }))
     })
     .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,
-          indentSize: config.editor.indentSize,
-          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 contentFormatter = getContentFormatter(storage, fileType, config)
 
       const folderNamesMapping = {}
       storage.folders.forEach(folder => {
diff --git a/browser/main/lib/dataApi/getContentFormatter.js b/browser/main/lib/dataApi/getContentFormatter.js
index afc1662e..b39b681c 100644
--- a/browser/main/lib/dataApi/getContentFormatter.js
+++ b/browser/main/lib/dataApi/getContentFormatter.js
@@ -8,7 +8,7 @@ import formatPDF from './formatPDF'
  * @param {Object} config
  */
 
-export default function getContentFormatterr(storage, fileType, config) {
+export default function getContentFormatter(storage, fileType, config) {
   if (fileType === 'md') {
     return formatMarkdown({
       storagePath: storage.path,

From aa8b5895697a740e94701ae79e084591009efa08 Mon Sep 17 00:00:00 2001
From: Baptiste Augrain <daiyam@zokugun.org>
Date: Fri, 12 Jun 2020 16:56:07 +0200
Subject: [PATCH 14/18] don't show export menu to snippet notes

---
 browser/main/NoteList/index.js | 52 ++++++++++++++++++----------------
 1 file changed, 28 insertions(+), 24 deletions(-)

diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js
index 6f8cc1a2..51dfe0d8 100644
--- a/browser/main/NoteList/index.js
+++ b/browser/main/NoteList/index.js
@@ -720,33 +720,37 @@ class NoteList extends React.Component {
         {
           label: copyNoteLink,
           click: this.copyNoteLink.bind(this, note)
-        },
-        {
-          type: 'separator'
-        },
-        {
-          label: i18n.__('Export Note'),
-          submenu: [
-            {
-              label: i18n.__('Export as Plain Text (.txt)'),
-              click: e => this.handleExportClick(e, note, 'txt')
-            },
-            {
-              label: i18n.__('Export as Markdown (.md)'),
-              click: e => this.handleExportClick(e, note, 'md')
-            },
-            {
-              label: i18n.__('Export as HTML (.html)'),
-              click: e => this.handleExportClick(e, note, 'html')
-            },
-            {
-              label: i18n.__('Export as PDF (.pdf)'),
-              click: e => this.handleExportClick(e, note, 'pdf')
-            }
-          ]
         }
       )
+
       if (note.type === 'MARKDOWN_NOTE') {
+        templates.push(
+          {
+            type: 'separator'
+          },
+          {
+            label: i18n.__('Export Note'),
+            submenu: [
+              {
+                label: i18n.__('Export as Plain Text (.txt)'),
+                click: e => this.handleExportClick(e, note, 'txt')
+              },
+              {
+                label: i18n.__('Export as Markdown (.md)'),
+                click: e => this.handleExportClick(e, note, 'md')
+              },
+              {
+                label: i18n.__('Export as HTML (.html)'),
+                click: e => this.handleExportClick(e, note, 'html')
+              },
+              {
+                label: i18n.__('Export as PDF (.pdf)'),
+                click: e => this.handleExportClick(e, note, 'pdf')
+              }
+            ]
+          }
+        )
+
         if (note.blog && note.blog.blogLink && note.blog.blogId) {
           templates.push(
             {

From f4259bb4d09aaa59bc013143b94b962169a69b62 Mon Sep 17 00:00:00 2001
From: Baptiste Augrain <daiyam@zokugun.org>
Date: Fri, 12 Jun 2020 17:36:46 +0200
Subject: [PATCH 15/18] fix exporting storage's notes into their own folders

---
 browser/main/lib/dataApi/exportStorage.js | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/browser/main/lib/dataApi/exportStorage.js b/browser/main/lib/dataApi/exportStorage.js
index 6cd80565..525e13cf 100644
--- a/browser/main/lib/dataApi/exportStorage.js
+++ b/browser/main/lib/dataApi/exportStorage.js
@@ -45,6 +45,8 @@ function exportStorage(storageKey, fileType, exportDir, config) {
       const contentFormatter = getContentFormatter(storage, fileType, config)
 
       const folderNamesMapping = {}
+      const deduplicators = {}
+
       storage.folders.forEach(folder => {
         const folderExportedDir = path.join(
           exportDir,
@@ -57,17 +59,17 @@ function exportStorage(storageKey, fileType, exportDir, config) {
         try {
           fs.mkdirSync(folderExportedDir)
         } catch (e) {}
-      })
 
-      const deduplicator = {}
+        deduplicators[folder.key] = {}
+      })
 
       return Promise.all(
         notes.map(note => {
           const targetPath = getFilename(
             note,
             fileType,
-            exportDir,
-            deduplicator
+            folderNamesMapping[note.folder],
+            deduplicators[note.folder]
           )
 
           return exportNote(storage.key, note, targetPath, contentFormatter)

From 87a530612f92ed063e76837ace129368b1cf24b5 Mon Sep 17 00:00:00 2001
From: Baptiste Augrain <daiyam@zokugun.org>
Date: Thu, 25 Jun 2020 22:50:08 +0200
Subject: [PATCH 16/18] - use newest test - remove useless binding - regroup
 function

---
 browser/main/Detail/MarkdownNoteDetail.js     |  4 +-
 .../main/lib/dataApi/attachmentManagement.js  | 33 +++---------
 tests/dataApi/attachmentManagement.test.js    | 52 +++++++++++--------
 3 files changed, 39 insertions(+), 50 deletions(-)

diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js
index 794358d5..856f266b 100755
--- a/browser/main/Detail/MarkdownNoteDetail.js
+++ b/browser/main/Detail/MarkdownNoteDetail.js
@@ -460,7 +460,7 @@ class MarkdownNoteDetail extends React.Component {
           storageKey={note.storage}
           noteKey={note.key}
           linesHighlighted={note.linesHighlighted}
-          onChange={this.handleUpdateContent.bind(this)}
+          onChange={this.handleUpdateContent}
           ignorePreviewPointerEvents={ignorePreviewPointerEvents}
           getNote={this.getNote}
           RTL={config.editor.rtlEnabled && this.state.RTL}
@@ -475,7 +475,7 @@ class MarkdownNoteDetail extends React.Component {
           storageKey={note.storage}
           noteKey={note.key}
           linesHighlighted={note.linesHighlighted}
-          onChange={this.handleUpdateContent.bind(this)}
+          onChange={this.handleUpdateContent}
           ignorePreviewPointerEvents={ignorePreviewPointerEvents}
           getNote={this.getNote}
           RTL={config.editor.rtlEnabled && this.state.RTL}
diff --git a/browser/main/lib/dataApi/attachmentManagement.js b/browser/main/lib/dataApi/attachmentManagement.js
index cd7c8db8..33e00b0b 100644
--- a/browser/main/lib/dataApi/attachmentManagement.js
+++ b/browser/main/lib/dataApi/attachmentManagement.js
@@ -706,14 +706,15 @@ function replaceNoteKeyWithNewNoteKey(noteContent, oldNoteKey, newNoteKey) {
 }
 
 /**
- * @description Deletes all :storage and noteKey references from the given input.
- * @param input Input in which the references should be deleted
+ * @description replace all :storage references with given destination folder.
+ * @param input Input in which the references should be replaced
  * @param noteKey Key of the current note
+ * @param destinationFolder Destination folder of the attachements
  * @returns {String} Input without the references
  */
-function removeStorageAndNoteReferences(input, noteKey) {
+function replaceStorageReferences(input, noteKey, destinationFolder) {
   return input.replace(
-    new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?("|\\))', 'g'),
+    new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '[^"\\)<\\s]+', 'g'),
     function(match) {
       return match
         .replace(new RegExp(mdurl.encode(path.win32.sep), 'g'), path.posix.sep)
@@ -735,33 +736,12 @@ function removeStorageAndNoteReferences(input, noteKey) {
               ')?',
             'g'
           ),
-          DESTINATION_FOLDER
+          destinationFolder
         )
     }
   )
 }
 
-/**
- * @description replace all :storage references with given destination folder.
- * @param input Input in which the references should be replaced
- * @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
@@ -1122,7 +1102,6 @@ module.exports = {
   getAttachmentsInMarkdownContent,
   getAbsolutePathsOfAttachmentsInContent,
   importAttachments,
-  removeStorageAndNoteReferences,
   removeAttachmentsByPaths,
   replaceStorageReferences,
   deleteAttachmentFolder,
diff --git a/tests/dataApi/attachmentManagement.test.js b/tests/dataApi/attachmentManagement.test.js
index c3b40eac..6adee614 100644
--- a/tests/dataApi/attachmentManagement.test.js
+++ b/tests/dataApi/attachmentManagement.test.js
@@ -702,14 +702,15 @@ it('should remove the all ":storage" and noteKey references', function() {
     '        </p>\n' +
     '    </body>\n' +
     '</html>'
-  const actual = systemUnderTest.removeStorageAndNoteReferences(
+  const actual = systemUnderTest.replaceStorageReferences(
     testInput,
-    noteKey
+    noteKey,
+    systemUnderTest.DESTINATION_FOLDER
   )
   expect(actual).toEqual(expectedOutput)
 })
 
-it('should make sure that "removeStorageAndNoteReferences" works with markdown content as well', function() {
+it('should make sure that "replaceStorageReferences" works with markdown content as well', function() {
   const noteKey = 'noteKey'
   const testInput =
     'Test input' +
@@ -736,9 +737,10 @@ it('should make sure that "removeStorageAndNoteReferences" works with markdown c
     systemUnderTest.DESTINATION_FOLDER +
     path.posix.sep +
     'pdf.pdf)'
-  const actual = systemUnderTest.removeStorageAndNoteReferences(
+  const actual = systemUnderTest.replaceStorageReferences(
     testInput,
-    noteKey
+    noteKey,
+    systemUnderTest.DESTINATION_FOLDER
   )
   expect(actual).toEqual(expectedOutput)
 })
@@ -803,7 +805,11 @@ it('should replace the all ":storage" references', function() {
     '        </p>\n' +
     '    </body>\n' +
     '</html>'
-  const actual = systemUnderTest.replaceStorageReferences(testInput, noteKey)
+  const actual = systemUnderTest.replaceStorageReferences(
+    testInput,
+    noteKey,
+    systemUnderTest.DESTINATION_FOLDER
+  )
   expect(actual).toEqual(expectedOutput)
 })
 
@@ -811,30 +817,34 @@ it('should make sure that "replaceStorageReferences" works with markdown content
   const noteKey = 'noteKey'
   const testInput =
     'Test input' +
-    '![' +
+    '![imageName](' +
     systemUnderTest.STORAGE_FOLDER_PLACEHOLDER +
-    path.sep +
+    path.win32.sep +
     noteKey +
-    path.sep +
-    'image.jpg](imageName}) \n' +
-    '[' +
+    path.win32.sep +
+    'image.jpg) \n' +
+    '[pdf](' +
     systemUnderTest.STORAGE_FOLDER_PLACEHOLDER +
-    path.sep +
+    path.posix.sep +
     noteKey +
-    path.sep +
-    'pdf.pdf](pdf})'
+    path.posix.sep +
+    'pdf.pdf)'
 
   const expectedOutput =
     'Test input' +
-    '![' +
+    '![imageName](' +
     systemUnderTest.DESTINATION_FOLDER +
-    path.sep +
-    'image.jpg](imageName}) \n' +
-    '[' +
+    path.posix.sep +
+    'image.jpg) \n' +
+    '[pdf](' +
     systemUnderTest.DESTINATION_FOLDER +
-    path.sep +
-    'pdf.pdf](pdf})'
-  const actual = systemUnderTest.replaceStorageReferences(testInput, noteKey)
+    path.posix.sep +
+    'pdf.pdf)'
+  const actual = systemUnderTest.replaceStorageReferences(
+    testInput,
+    noteKey,
+    systemUnderTest.DESTINATION_FOLDER
+  )
   expect(actual).toEqual(expectedOutput)
 })
 

From 0ca18d8ca5e7354ea6061d57052e9011b6f08de7 Mon Sep 17 00:00:00 2001
From: Baptiste Augrain <daiyam@zokugun.org>
Date: Fri, 26 Jun 2020 03:07:25 +0200
Subject: [PATCH 17/18] re-add missing isStacking flag

---
 browser/main/Detail/MarkdownNoteDetail.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js
index 856f266b..b1e0dd95 100755
--- a/browser/main/Detail/MarkdownNoteDetail.js
+++ b/browser/main/Detail/MarkdownNoteDetail.js
@@ -474,6 +474,7 @@ class MarkdownNoteDetail extends React.Component {
           value={note.content}
           storageKey={note.storage}
           noteKey={note.key}
+          isStacking={isStacking}
           linesHighlighted={note.linesHighlighted}
           onChange={this.handleUpdateContent}
           ignorePreviewPointerEvents={ignorePreviewPointerEvents}

From bd9b1306b11b9c426a8d34f7d876604658a2c2dc Mon Sep 17 00:00:00 2001
From: Baptiste Augrain <daiyam@zokugun.org>
Date: Fri, 26 Jun 2020 17:55:16 +0200
Subject: [PATCH 18/18] fix missing isStacking flag

---
 browser/main/Detail/MarkdownNoteDetail.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js
index b1e0dd95..be76e35c 100755
--- a/browser/main/Detail/MarkdownNoteDetail.js
+++ b/browser/main/Detail/MarkdownNoteDetail.js
@@ -448,7 +448,7 @@ class MarkdownNoteDetail extends React.Component {
 
   renderEditor() {
     const { config, ignorePreviewPointerEvents } = this.props
-    const { note } = this.state
+    const { note, isStacking } = this.state
 
     if (this.state.editorType === 'EDITOR_PREVIEW') {
       return (