mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 17:56:25 +00:00
add YAML front matter when exporting
This commit is contained in:
@@ -230,7 +230,7 @@ class MarkdownEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
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)
|
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||||
@@ -308,6 +308,8 @@ class MarkdownEditor extends React.Component {
|
|||||||
customCSS={config.preview.customCSS}
|
customCSS={config.preview.customCSS}
|
||||||
allowCustomCSS={config.preview.allowCustomCSS}
|
allowCustomCSS={config.preview.allowCustomCSS}
|
||||||
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
||||||
|
getNote={getNote}
|
||||||
|
export={config.export}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,144 +16,21 @@ import convertModeName from 'browser/lib/convertModeName'
|
|||||||
import copy from 'copy-to-clipboard'
|
import copy from 'copy-to-clipboard'
|
||||||
import mdurl from 'mdurl'
|
import mdurl from 'mdurl'
|
||||||
import exportNote from 'browser/main/lib/dataApi/exportNote'
|
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 { escapeHtmlCharacters } from 'browser/lib/utils'
|
||||||
import yaml from 'js-yaml'
|
import yaml from 'js-yaml'
|
||||||
import context from 'browser/lib/context'
|
import context from 'browser/lib/context'
|
||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
const { remote, shell } = require('electron')
|
import uri2path from 'file-uri-to-path'
|
||||||
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
|
import { remote, shell } from 'electron'
|
||||||
|
import attachmentManagement from '../main/lib/dataApi/attachmentManagement'
|
||||||
const { app } = remote
|
import filenamify from 'filenamify'
|
||||||
const path = require('path')
|
|
||||||
const fileUrl = require('file-url')
|
|
||||||
|
|
||||||
const dialog = remote.dialog
|
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 = `
|
const scrollBarStyle = `
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 12px;
|
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 {
|
export default class MarkdownPreview extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
@@ -286,96 +148,11 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSaveAsMd () {
|
handleSaveAsMd () {
|
||||||
this.exportAsDocument('md', (noteContent, exportTasks) => {
|
this.exportAsDocument('md', formatMarkdown(this.props))
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSaveAsHtml () {
|
handleSaveAsHtml () {
|
||||||
this.exportAsDocument('html', (noteContent, exportTasks) => {
|
this.exportAsDocument('html', formatHTML(this.props))
|
||||||
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 += `<link rel="stylesheet" href="css/${path.basename(file)}">`
|
|
||||||
})
|
|
||||||
|
|
||||||
return `<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name = "viewport" content = "width = device-width, initial-scale = 1, maximum-scale = 1">
|
|
||||||
<style id="style">${inlineStyles}</style>
|
|
||||||
${styles}
|
|
||||||
</head>
|
|
||||||
<body>${body}</body>
|
|
||||||
</html>`
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePrint () {
|
handlePrint () {
|
||||||
@@ -383,17 +160,21 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exportAsDocument (fileType, contentFormatter) {
|
exportAsDocument (fileType, contentFormatter) {
|
||||||
|
const note = this.props.getNote()
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
|
defaultPath: filenamify(note.title, {
|
||||||
|
replacement: '_'
|
||||||
|
}),
|
||||||
filters: [{ name: 'Documents', extensions: [fileType] }],
|
filters: [{ name: 'Documents', extensions: [fileType] }],
|
||||||
properties: ['openFile', 'createDirectory']
|
properties: ['openFile', 'createDirectory']
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
|
dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
|
||||||
if (filename) {
|
if (filename) {
|
||||||
const content = this.props.value
|
const storagePath = this.props.storagePath
|
||||||
const storage = this.props.storagePath
|
|
||||||
|
|
||||||
exportNote(storage, content, filename, contentFormatter)
|
exportNote(storagePath, note, filename, contentFormatter)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
type: 'info',
|
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 () {
|
applyStyle () {
|
||||||
const {
|
const {
|
||||||
fontFamily,
|
fontFamily,
|
||||||
@@ -604,11 +347,10 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
theme,
|
theme,
|
||||||
allowCustomCSS,
|
allowCustomCSS,
|
||||||
customCSS
|
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(
|
this.getWindow().document.getElementById('style').innerHTML = buildStyle(
|
||||||
fontFamily,
|
fontFamily,
|
||||||
fontSize,
|
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 () {
|
rewriteIframe () {
|
||||||
_.forEach(
|
_.forEach(
|
||||||
this.refs.root.contentWindow.document.querySelectorAll(
|
this.refs.root.contentWindow.document.querySelectorAll(
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {config, value, storageKey, noteKey} = this.props
|
const {config, value, storageKey, noteKey, getTitle} = this.props
|
||||||
const storage = findStorage(storageKey)
|
const storage = findStorage(storageKey)
|
||||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||||
@@ -199,6 +199,8 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
customCSS={config.preview.customCSS}
|
customCSS={config.preview.customCSS}
|
||||||
allowCustomCSS={config.preview.allowCustomCSS}
|
allowCustomCSS={config.preview.allowCustomCSS}
|
||||||
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
||||||
|
getTitle={getTitle}
|
||||||
|
export={config.export}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
|
|
||||||
this.toggleLockButton = this.handleToggleLockButton.bind(this)
|
this.toggleLockButton = this.handleToggleLockButton.bind(this)
|
||||||
this.generateToc = () => this.handleGenerateToc()
|
this.generateToc = () => this.handleGenerateToc()
|
||||||
|
|
||||||
|
this.getNote = this.getNote.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
focus () {
|
focus () {
|
||||||
@@ -319,6 +321,10 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
this.updateNote(note)
|
this.updateNote(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getNote () {
|
||||||
|
return this.state.note
|
||||||
|
}
|
||||||
|
|
||||||
renderEditor () {
|
renderEditor () {
|
||||||
const { config, ignorePreviewPointerEvents } = this.props
|
const { config, ignorePreviewPointerEvents } = this.props
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
@@ -333,6 +339,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
noteKey={note.key}
|
noteKey={note.key}
|
||||||
onChange={this.handleUpdateContent.bind(this)}
|
onChange={this.handleUpdateContent.bind(this)}
|
||||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||||
|
getNote={this.getNote}
|
||||||
/>
|
/>
|
||||||
} else {
|
} else {
|
||||||
return <MarkdownSplitEditor
|
return <MarkdownSplitEditor
|
||||||
@@ -343,6 +350,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
noteKey={note.key}
|
noteKey={note.key}
|
||||||
onChange={this.handleUpdateContent.bind(this)}
|
onChange={this.handleUpdateContent.bind(this)}
|
||||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||||
|
getNote={this.getNote}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,12 +42,16 @@ class StorageItem extends React.Component {
|
|||||||
label: i18n.__('Export Storage'),
|
label: i18n.__('Export Storage'),
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: i18n.__('Export as txt'),
|
label: i18n.__('Export as Plain Text (.txt)'),
|
||||||
click: (e) => this.handleExportStorageClick(e, 'txt')
|
click: (e) => this.handleExportStorageClick(e, 'txt')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: i18n.__('Export as md'),
|
label: i18n.__('Export as Markdown (.md)'),
|
||||||
click: (e) => this.handleExportStorageClick(e, '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,
|
dialog.showOpenDialog(remote.getCurrentWindow(), options,
|
||||||
(paths) => {
|
(paths) => {
|
||||||
if (paths && paths.length === 1) {
|
if (paths && paths.length === 1) {
|
||||||
const { storage, dispatch } = this.props
|
const { storage, dispatch, config } = this.props
|
||||||
dataApi
|
dataApi
|
||||||
.exportStorage(storage.key, fileType, paths[0])
|
.exportStorage(storage.key, fileType, paths[0], config)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
|
type: 'info',
|
||||||
|
message: `Exported to ${paths[0]}`
|
||||||
|
})
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'EXPORT_STORAGE',
|
type: 'EXPORT_STORAGE',
|
||||||
storage: data.storage,
|
storage: data.storage,
|
||||||
fileType: data.fileType
|
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'),
|
label: i18n.__('Export Folder'),
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: i18n.__('Export as txt'),
|
label: i18n.__('Export as Plain Text (.txt)'),
|
||||||
click: (e) => this.handleExportFolderClick(e, folder, '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')
|
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,
|
dialog.showOpenDialog(remote.getCurrentWindow(), options,
|
||||||
(paths) => {
|
(paths) => {
|
||||||
if (paths && paths.length === 1) {
|
if (paths && paths.length === 1) {
|
||||||
const { storage, dispatch } = this.props
|
const { storage, dispatch, config } = this.props
|
||||||
dataApi
|
dataApi
|
||||||
.exportFolder(storage.key, folder.key, fileType, paths[0])
|
.exportFolder(storage.key, folder.key, fileType, paths[0], config)
|
||||||
.then((data) => {
|
.then(data => {
|
||||||
|
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
|
type: 'info',
|
||||||
|
message: `Exported to ${paths[0]}`
|
||||||
|
})
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'EXPORT_FOLDER',
|
type: 'EXPORT_FOLDER',
|
||||||
storage: data.storage,
|
storage: data.storage,
|
||||||
@@ -205,6 +230,13 @@ class StorageItem extends React.Component {
|
|||||||
fileType: data.fileType
|
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 { folderNoteMap, trashedSet } = data
|
||||||
const SortableStorageItemChild = SortableElement(StorageItemChild)
|
const SortableStorageItemChild = SortableElement(StorageItemChild)
|
||||||
const folderList = storage.folders.map((folder, index) => {
|
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 isActive = !!(location.pathname.match(folderRegex))
|
||||||
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
|
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
|
||||||
|
|
||||||
|
|||||||
@@ -347,6 +347,7 @@ class SideNav extends React.Component {
|
|||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
onSortEnd={this.onSortEnd.bind(this)(storage)}
|
onSortEnd={this.onSortEnd.bind(this)(storage)}
|
||||||
useDragHandle
|
useDragHandle
|
||||||
|
config={config}
|
||||||
/>
|
/>
|
||||||
})
|
})
|
||||||
const style = {}
|
const style = {}
|
||||||
|
|||||||
@@ -82,8 +82,9 @@ export const DEFAULT_CONFIG = {
|
|||||||
password: ''
|
password: ''
|
||||||
},
|
},
|
||||||
export: {
|
export: {
|
||||||
action: 'DONT_EXPORT', // 'DONT_EXPORT', 'MERGE_HEADER', 'MERGE_VARIABLE'
|
metadata: 'DONT_EXPORT', // 'DONT_EXPORT', 'MERGE_HEADER', 'MERGE_VARIABLE'
|
||||||
variable: 'metadata'
|
variable: 'boostnote',
|
||||||
|
prefixAttachmentFolder: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
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
|
* @description Deletes the attachment folder specified by the given storageKey and noteKey
|
||||||
* @param storageKey Key of the storage of the note to be deleted
|
* @param storageKey Key of the storage of the note to be deleted
|
||||||
@@ -542,6 +553,7 @@ module.exports = {
|
|||||||
getAttachmentsInMarkdownContent,
|
getAttachmentsInMarkdownContent,
|
||||||
getAbsolutePathsOfAttachmentsInContent,
|
getAbsolutePathsOfAttachmentsInContent,
|
||||||
removeStorageAndNoteReferences,
|
removeStorageAndNoteReferences,
|
||||||
|
replaceStorageReferences,
|
||||||
deleteAttachmentFolder,
|
deleteAttachmentFolder,
|
||||||
deleteAttachmentsNotPresentInNote,
|
deleteAttachmentsNotPresentInNote,
|
||||||
moveAttachments,
|
moveAttachments,
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ import { findStorage } from 'browser/lib/findStorage'
|
|||||||
import resolveStorageData from './resolveStorageData'
|
import resolveStorageData from './resolveStorageData'
|
||||||
import resolveStorageNotes from './resolveStorageNotes'
|
import resolveStorageNotes from './resolveStorageNotes'
|
||||||
import filenamify from 'filenamify'
|
import filenamify from 'filenamify'
|
||||||
import * as path from 'path'
|
import path from 'path'
|
||||||
import * as fs from 'fs'
|
import exportNote from './exportNote'
|
||||||
|
import formatMarkdown from './formatMarkdown'
|
||||||
|
import formatHTML from './formatHTML'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {String} storageKey
|
* @param {String} storageKey
|
||||||
* @param {String} folderKey
|
* @param {String} folderKey
|
||||||
* @param {String} fileType
|
* @param {String} fileType
|
||||||
* @param {String} exportDir
|
* @param {String} exportDir
|
||||||
|
* @param {Object} config
|
||||||
*
|
*
|
||||||
* @return {Object}
|
* @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
|
let targetStorage
|
||||||
try {
|
try {
|
||||||
targetStorage = findStorage(storageKey)
|
targetStorage = findStorage(storageKey)
|
||||||
@@ -31,31 +34,50 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return resolveStorageData(targetStorage)
|
return resolveStorageData(targetStorage)
|
||||||
.then(function assignNotes (storage) {
|
.then(storage => {
|
||||||
return resolveStorageNotes(storage)
|
return resolveStorageNotes(storage).then(notes => ({
|
||||||
.then((notes) => {
|
|
||||||
return {
|
|
||||||
storage,
|
storage,
|
||||||
notes
|
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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
|
||||||
})
|
|
||||||
.then(function exportNotes (data) {
|
|
||||||
const { storage, notes } = data
|
|
||||||
|
|
||||||
notes
|
return Promise
|
||||||
.filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
|
.all(notes.map(note => {
|
||||||
.forEach(snippet => {
|
const targetPath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`)
|
||||||
const notePath = path.join(exportDir, `${filenamify(snippet.title, {replacement: '_'})}.${fileType}`)
|
|
||||||
fs.writeFileSync(notePath, snippet.content)
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return exportNote(storage.key, note, targetPath, contentFormatter)
|
||||||
|
}))
|
||||||
|
.then(() => ({
|
||||||
storage,
|
storage,
|
||||||
folderKey,
|
folderKey,
|
||||||
fileType,
|
fileType,
|
||||||
exportDir
|
exportDir
|
||||||
}
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const path = require('path')
|
|||||||
* @param {function} outputFormatter
|
* @param {function} outputFormatter
|
||||||
* @return {Promise.<*[]>}
|
* @return {Promise.<*[]>}
|
||||||
*/
|
*/
|
||||||
function exportNote (storageKey, noteContent, targetPath, outputFormatter) {
|
function exportNote (storageKey, note, targetPath, outputFormatter) {
|
||||||
const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path
|
const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path
|
||||||
const exportTasks = []
|
const exportTasks = []
|
||||||
|
|
||||||
@@ -24,20 +24,18 @@ function exportNote (storageKey, noteContent, targetPath, outputFormatter) {
|
|||||||
throw new Error('Storage path is not found')
|
throw new Error('Storage path is not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
let exportedData = noteContent
|
const exportedData = outputFormatter ? outputFormatter(note, targetPath, exportTasks) : note.content
|
||||||
|
|
||||||
if (outputFormatter) {
|
|
||||||
exportedData = outputFormatter(exportedData, exportTasks)
|
|
||||||
}
|
|
||||||
|
|
||||||
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
|
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(() => {
|
.then(() => {
|
||||||
return saveToFile(exportedData, targetPath)
|
return saveToFile(exportedData, targetPath)
|
||||||
}).catch((err) => {
|
})
|
||||||
|
.catch(error => {
|
||||||
rollbackExport(tasks)
|
rollbackExport(tasks)
|
||||||
throw err
|
throw error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,10 +55,12 @@ function prepareTasks (tasks, storagePath, targetPath) {
|
|||||||
|
|
||||||
function saveToFile (data, filename) {
|
function saveToFile (data, filename) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
fs.writeFile(filename, data, (err) => {
|
fs.writeFile(filename, data, error => {
|
||||||
if (err) return reject(err)
|
if (error) {
|
||||||
|
reject(error)
|
||||||
|
} else {
|
||||||
resolve(filename)
|
resolve(filename)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,17 @@ import { findStorage } from 'browser/lib/findStorage'
|
|||||||
import resolveStorageData from './resolveStorageData'
|
import resolveStorageData from './resolveStorageData'
|
||||||
import resolveStorageNotes from './resolveStorageNotes'
|
import resolveStorageNotes from './resolveStorageNotes'
|
||||||
import filenamify from 'filenamify'
|
import filenamify from 'filenamify'
|
||||||
import * as path from 'path'
|
import path from 'path'
|
||||||
import * as fs from 'fs'
|
import fs from 'fs'
|
||||||
|
import exportNote from './exportNote'
|
||||||
|
import formatMarkdown from './formatMarkdown'
|
||||||
|
import formatHTML from './formatHTML'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {String} storageKey
|
* @param {String} storageKey
|
||||||
* @param {String} fileType
|
* @param {String} fileType
|
||||||
* @param {String} exportDir
|
* @param {String} exportDir
|
||||||
|
* @param {Object} config
|
||||||
*
|
*
|
||||||
* @return {Object}
|
* @return {Object}
|
||||||
* ```
|
* ```
|
||||||
@@ -20,7 +24,7 @@ import * as fs from 'fs'
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function exportStorage (storageKey, fileType, exportDir) {
|
function exportStorage (storageKey, fileType, exportDir, config) {
|
||||||
let targetStorage
|
let targetStorage
|
||||||
try {
|
try {
|
||||||
targetStorage = findStorage(storageKey)
|
targetStorage = findStorage(storageKey)
|
||||||
@@ -29,34 +33,61 @@ function exportStorage (storageKey, fileType, exportDir) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return resolveStorageData(targetStorage)
|
return resolveStorageData(targetStorage)
|
||||||
.then(storage => (
|
.then(storage => {
|
||||||
resolveStorageNotes(storage).then(notes => ({storage, notes}))
|
return resolveStorageNotes(storage).then(notes => ({
|
||||||
))
|
storage,
|
||||||
.then(function exportNotes (data) {
|
notes: notes.filter(note => !note.isTrashed && note.type === 'MARKDOWN_NOTE')
|
||||||
const { storage, notes } = data
|
}))
|
||||||
|
})
|
||||||
|
.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 = {}
|
const folderNamesMapping = {}
|
||||||
storage.folders.forEach(folder => {
|
storage.folders.forEach(folder => {
|
||||||
const folderExportedDir = path.join(exportDir, filenamify(folder.name, {replacement: '_'}))
|
const folderExportedDir = path.join(exportDir, filenamify(folder.name, {replacement: '_'}))
|
||||||
|
|
||||||
folderNamesMapping[folder.key] = folderExportedDir
|
folderNamesMapping[folder.key] = folderExportedDir
|
||||||
|
|
||||||
// make sure directory exists
|
// make sure directory exists
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(folderExportedDir)
|
fs.mkdirSync(folderExportedDir)
|
||||||
} catch (e) {}
|
} 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 {
|
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,
|
storage,
|
||||||
fileType,
|
fileType,
|
||||||
exportDir
|
exportDir
|
||||||
}
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
295
browser/main/lib/dataApi/formatHTML.js
Normal file
295
browser/main/lib/dataApi/formatHTML.js
Normal file
@@ -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 += `<link rel="stylesheet" href="css/${path.basename(file)}">`
|
||||||
|
})
|
||||||
|
|
||||||
|
return `<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name = "viewport" content = "width = device-width, initial-scale = 1, maximum-scale = 1">
|
||||||
|
<style id="style">${inlineStyles}</style>
|
||||||
|
${styles}
|
||||||
|
</head>
|
||||||
|
<body>${body}</body>
|
||||||
|
</html>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 : ''}
|
||||||
|
`
|
||||||
|
}
|
||||||
94
browser/main/lib/dataApi/formatMarkdown.js
Normal file
94
browser/main/lib/dataApi/formatMarkdown.js
Normal file
@@ -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}`
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
const resolveStorageData = require('./resolveStorageData')
|
const resolveStorageData = require('./resolveStorageData')
|
||||||
const _ = require('lodash')
|
const _ = require('lodash')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const fs = require('fs')
|
|
||||||
const CSON = require('@rokt33r/season')
|
const CSON = require('@rokt33r/season')
|
||||||
const keygen = require('browser/lib/keygen')
|
const keygen = require('browser/lib/keygen')
|
||||||
const sander = require('sander')
|
const sander = require('sander')
|
||||||
|
|||||||
@@ -34,8 +34,7 @@ class ExportTab extends React.Component {
|
|||||||
type: 'success',
|
type: 'success',
|
||||||
message: i18n.__('Successfully applied!')
|
message: i18n.__('Successfully applied!')
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
this.handleSettingError = (err) => {
|
this.handleSettingError = (err) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -43,8 +42,7 @@ class ExportTab extends React.Component {
|
|||||||
type: 'error',
|
type: 'error',
|
||||||
message: err.message != null ? err.message : i18n.__('An error occurred!')
|
message: err.message != null ? err.message : i18n.__('An error occurred!')
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.oldExport = this.state.config.export
|
this.oldExport = this.state.config.export
|
||||||
@@ -78,8 +76,9 @@ class ExportTab extends React.Component {
|
|||||||
const { config } = this.state
|
const { config } = this.state
|
||||||
|
|
||||||
config.export = {
|
config.export = {
|
||||||
action: this.refs.action.value,
|
metadata: this.refs.metadata.value,
|
||||||
variable: !_.isNil(this.refs.variable) ? this.refs.variable.value : config.export.variable
|
variable: !_.isNil(this.refs.variable) ? this.refs.variable.value : config.export.variable,
|
||||||
|
prefixAttachmentFolder: this.refs.prefixAttachmentFolder.checked
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -99,7 +98,6 @@ class ExportTab extends React.Component {
|
|||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { config, ExportAlert } = this.state
|
const { config, ExportAlert } = this.state
|
||||||
console.log(config.export)
|
|
||||||
|
|
||||||
const ExportAlertElement = ExportAlert != null
|
const ExportAlertElement = ExportAlert != null
|
||||||
? <p className={`alert ${ExportAlert.type}`}>
|
? <p className={`alert ${ExportAlert.type}`}>
|
||||||
@@ -114,12 +112,12 @@ class ExportTab extends React.Component {
|
|||||||
|
|
||||||
<div styleName='group-section'>
|
<div styleName='group-section'>
|
||||||
<div styleName='group-section-label'>
|
<div styleName='group-section-label'>
|
||||||
{i18n.__('Action')}
|
{i18n.__('Metadata')}
|
||||||
</div>
|
</div>
|
||||||
<div styleName='group-section-control'>
|
<div styleName='group-section-control'>
|
||||||
<select value={config.export.action}
|
<select value={config.export.metadata}
|
||||||
onChange={(e) => this.handleExportChange(e)}
|
onChange={(e) => this.handleExportChange(e)}
|
||||||
ref='action'
|
ref='metadata'
|
||||||
>
|
>
|
||||||
<option value='DONT_EXPORT'>{i18n.__(`Don't export`)}</option>
|
<option value='DONT_EXPORT'>{i18n.__(`Don't export`)}</option>
|
||||||
<option value='MERGE_HEADER'>{i18n.__('Merge with the header')}</option>
|
<option value='MERGE_HEADER'>{i18n.__('Merge with the header')}</option>
|
||||||
@@ -128,7 +126,7 @@ class ExportTab extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ config.export.action === 'MERGE_VARIABLE' &&
|
{ config.export.metadata === 'MERGE_VARIABLE' &&
|
||||||
<div styleName='group-section'>
|
<div styleName='group-section'>
|
||||||
<div styleName='group-section-label'>{i18n.__('Variable Name')}</div>
|
<div styleName='group-section-label'>{i18n.__('Variable Name')}</div>
|
||||||
<div styleName='group-section-control'>
|
<div styleName='group-section-control'>
|
||||||
@@ -141,6 +139,17 @@ class ExportTab extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<div styleName='group-checkBoxSection'>
|
||||||
|
<label>
|
||||||
|
<input onChange={(e) => this.handleExportChange(e)}
|
||||||
|
checked={config.export.prefixAttachmentFolder}
|
||||||
|
ref='prefixAttachmentFolder'
|
||||||
|
type='checkbox'
|
||||||
|
/>
|
||||||
|
{i18n.__('Prefix attachment folder')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div styleName='group-control'>
|
<div styleName='group-control'>
|
||||||
<button styleName='group-control-rightButton'
|
<button styleName='group-control-rightButton'
|
||||||
onClick={(e) => this.handleSaveButtonClick(e)}>{i18n.__('Save')}
|
onClick={(e) => this.handleSaveButtonClick(e)}>{i18n.__('Save')}
|
||||||
|
|||||||
@@ -47,12 +47,20 @@ test.serial('Export a folder', (t) => {
|
|||||||
}
|
}
|
||||||
input2.title = 'input2'
|
input2.title = 'input2'
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
export: {
|
||||||
|
metadata: 'DONT_EXPORT',
|
||||||
|
variable: 'boostnote',
|
||||||
|
prefixAttachmentFolder: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return createNote(storageKey, input1)
|
return createNote(storageKey, input1)
|
||||||
.then(function () {
|
.then(function () {
|
||||||
return createNote(storageKey, input2)
|
return createNote(storageKey, input2)
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
return exportFolder(storageKey, folderKey, 'md', storagePath)
|
return exportFolder(storageKey, folderKey, 'md', storagePath, config)
|
||||||
})
|
})
|
||||||
.then(function assert () {
|
.then(function assert () {
|
||||||
let filePath = path.join(storagePath, 'input1.md')
|
let filePath = path.join(storagePath, 'input1.md')
|
||||||
|
|||||||
@@ -31,7 +31,16 @@ test.serial('Export a storage', t => {
|
|||||||
acc[folder.key] = folder.name
|
acc[folder.key] = folder.name
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
return exportStorage(storageKey, 'md', exportDir)
|
|
||||||
|
const config = {
|
||||||
|
export: {
|
||||||
|
metadata: 'DONT_EXPORT',
|
||||||
|
variable: 'boostnote',
|
||||||
|
prefixAttachmentFolder: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return exportStorage(storageKey, 'md', exportDir, config)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notes.forEach(note => {
|
notes.forEach(note => {
|
||||||
const noteDir = path.join(exportDir, folderKeyToName[note.folder], `${note.title}.md`)
|
const noteDir = path.join(exportDir, folderKeyToName[note.folder], `${note.title}.md`)
|
||||||
|
|||||||
Reference in New Issue
Block a user