mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-12 17:26:17 +00:00
@@ -323,6 +323,7 @@ class MarkdownEditor extends React.Component {
|
|||||||
storageKey,
|
storageKey,
|
||||||
noteKey,
|
noteKey,
|
||||||
linesHighlighted,
|
linesHighlighted,
|
||||||
|
getNote,
|
||||||
RTL
|
RTL
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
@@ -426,6 +427,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}
|
||||||
onDrop={e => this.handleDropImage(e)}
|
onDrop={e => this.handleDropImage(e)}
|
||||||
RTL={RTL}
|
RTL={RTL}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,258 +18,30 @@ 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 { escapeHtmlCharacters } from 'browser/lib/utils'
|
import formatMarkdown from 'browser/main/lib/dataApi/formatMarkdown'
|
||||||
|
import formatHTML, {
|
||||||
|
CSS_FILES,
|
||||||
|
buildStyle,
|
||||||
|
getCodeThemeLink,
|
||||||
|
getStyleParams,
|
||||||
|
escapeHtmlCharactersInCodeTag
|
||||||
|
} from 'browser/main/lib/dataApi/formatHTML'
|
||||||
|
import formatPDF from 'browser/main/lib/dataApi/formatPDF'
|
||||||
import yaml from 'js-yaml'
|
import yaml from 'js-yaml'
|
||||||
|
import i18n from 'browser/lib/i18n'
|
||||||
|
import path from 'path'
|
||||||
|
import { remote, shell } from 'electron'
|
||||||
|
import attachmentManagement from '../main/lib/dataApi/attachmentManagement'
|
||||||
|
import filenamify from 'filenamify'
|
||||||
import { render } from 'react-dom'
|
import { render } from 'react-dom'
|
||||||
import Carousel from 'react-image-carousel'
|
import Carousel from 'react-image-carousel'
|
||||||
import { push } from 'connected-react-router'
|
import { push } from 'connected-react-router'
|
||||||
import ConfigManager from '../main/lib/ConfigManager'
|
import ConfigManager from '../main/lib/ConfigManager'
|
||||||
import uiThemes from 'browser/lib/ui-themes'
|
import uiThemes from 'browser/lib/ui-themes'
|
||||||
import i18n from 'browser/lib/i18n'
|
import { buildMarkdownPreviewContextMenu } from 'browser/lib/contextMenuBuilder'
|
||||||
|
|
||||||
const { remote, shell } = require('electron')
|
|
||||||
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
|
|
||||||
const buildMarkdownPreviewContextMenu = require('browser/lib/contextMenuBuilder')
|
|
||||||
.buildMarkdownPreviewContextMenu
|
|
||||||
|
|
||||||
const { app } = remote
|
|
||||||
const path = require('path')
|
|
||||||
const fileUrl = require('file-url')
|
|
||||||
|
|
||||||
const dialog = remote.dialog
|
const dialog = remote.dialog
|
||||||
|
|
||||||
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`,
|
|
||||||
`${appPath}/node_modules/react-image-carousel/lib/css/main.min.css`
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Object} opts
|
|
||||||
* @param {String} opts.fontFamily
|
|
||||||
* @param {Numberl} opts.fontSize
|
|
||||||
* @param {String} opts.codeBlockFontFamily
|
|
||||||
* @param {String} opts.theme
|
|
||||||
* @param {Boolean} [opts.lineNumber] Should show line number
|
|
||||||
* @param {Boolean} [opts.scrollPastEnd]
|
|
||||||
* @param {Boolean} [opts.allowCustomCSS] Should add custom css
|
|
||||||
* @param {String} [opts.customCSS] Will be added to bottom, only if `opts.allowCustomCSS` is truthy
|
|
||||||
* @returns {String}
|
|
||||||
*/
|
|
||||||
function buildStyle(opts) {
|
|
||||||
const {
|
|
||||||
fontFamily,
|
|
||||||
fontSize,
|
|
||||||
codeBlockFontFamily,
|
|
||||||
lineNumber,
|
|
||||||
scrollPastEnd,
|
|
||||||
theme,
|
|
||||||
allowCustomCSS,
|
|
||||||
customCSS,
|
|
||||||
RTL
|
|
||||||
} = opts
|
|
||||||
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;
|
|
||||||
box-sizing: border-box;
|
|
||||||
`
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
${RTL ? 'direction: rtl;' : ''}
|
|
||||||
${RTL ? 'text-align: right;' : ''}
|
|
||||||
}
|
|
||||||
@media print {
|
|
||||||
body {
|
|
||||||
padding-bottom: initial;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
code {
|
|
||||||
font-family: '${codeBlockFontFamily.join("','")}';
|
|
||||||
background-color: rgba(0,0,0,0.04);
|
|
||||||
text-align: left;
|
|
||||||
direction: ltr;
|
|
||||||
}
|
|
||||||
|
|
||||||
p code,
|
|
||||||
li code,
|
|
||||||
td code
|
|
||||||
{
|
|
||||||
padding: 2px;
|
|
||||||
border-width: 1px;
|
|
||||||
border-style: solid;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
[data-theme="default"] p code,
|
|
||||||
[data-theme="default"] li code,
|
|
||||||
[data-theme="default"] td code
|
|
||||||
{
|
|
||||||
background-color: #F4F4F4;
|
|
||||||
border-color: #d9d9d9;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
[data-theme="white"] p code,
|
|
||||||
[data-theme="white"] li code,
|
|
||||||
[data-theme="white"] td code
|
|
||||||
{
|
|
||||||
background-color: #F4F4F4;
|
|
||||||
border-color: #d9d9d9;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
[data-theme="dark"] p code,
|
|
||||||
[data-theme="dark"] li code,
|
|
||||||
[data-theme="dark"] td code
|
|
||||||
{
|
|
||||||
background-color: #444444;
|
|
||||||
border-color: #555;
|
|
||||||
color: #FFFFFF;
|
|
||||||
}
|
|
||||||
[data-theme="dracula"] p code,
|
|
||||||
[data-theme="dracula"] li code,
|
|
||||||
[data-theme="dracula"] td code
|
|
||||||
{
|
|
||||||
background-color: #444444;
|
|
||||||
border-color: #555;
|
|
||||||
color: #FFFFFF;
|
|
||||||
}
|
|
||||||
[data-theme="monokai"] p code,
|
|
||||||
[data-theme="monokai"] li code,
|
|
||||||
[data-theme="monokai"] td code
|
|
||||||
{
|
|
||||||
background-color: #444444;
|
|
||||||
border-color: #555;
|
|
||||||
color: #FFFFFF;
|
|
||||||
}
|
|
||||||
[data-theme="nord"] p code,
|
|
||||||
[data-theme="nord"] li code,
|
|
||||||
[data-theme="nord"] td code
|
|
||||||
{
|
|
||||||
background-color: #444444;
|
|
||||||
border-color: #555;
|
|
||||||
color: #FFFFFF;
|
|
||||||
}
|
|
||||||
[data-theme="solarized-dark"] p code,
|
|
||||||
[data-theme="solarized-dark"] li code,
|
|
||||||
[data-theme="solarized-dark"] td code
|
|
||||||
{
|
|
||||||
background-color: #444444;
|
|
||||||
border-color: #555;
|
|
||||||
color: #FFFFFF;
|
|
||||||
}
|
|
||||||
[data-theme="vulcan"] p code,
|
|
||||||
[data-theme="vulcan"] li code,
|
|
||||||
[data-theme="vulcan"] td code
|
|
||||||
{
|
|
||||||
background-color: #444444;
|
|
||||||
border-color: #555;
|
|
||||||
color: #FFFFFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 1em 0 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4, h5, h6 {
|
|
||||||
margin: 1.1em 0 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
padding: 0.2em 0 0.2em;
|
|
||||||
margin: 1em 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
padding: 0.2em 0 0.2em;
|
|
||||||
margin: 1em 0 0.7em;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
||||||
${config.get().ui.showScrollBar ? '' : 'display: none;'}
|
${config.get().ui.showScrollBar ? '' : 'display: none;'}
|
||||||
@@ -301,22 +73,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'
|
|
||||||
]
|
|
||||||
|
|
||||||
// return the line number of the line that used to generate the specified element
|
// return the line number of the line that used to generate the specified element
|
||||||
// return -1 if the line is not found
|
// return -1 if the line is not found
|
||||||
function getSourceLineNumberByElement(element) {
|
function getSourceLineNumberByElement(element) {
|
||||||
@@ -430,94 +186,15 @@ class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSaveAsMd() {
|
handleSaveAsMd() {
|
||||||
this.exportAsDocument('md')
|
this.exportAsDocument('md', formatMarkdown(this.props))
|
||||||
}
|
|
||||||
|
|
||||||
htmlContentFormatter(noteContent, exportTasks, targetDir) {
|
|
||||||
const {
|
|
||||||
fontFamily,
|
|
||||||
fontSize,
|
|
||||||
codeBlockFontFamily,
|
|
||||||
lineNumber,
|
|
||||||
codeBlockTheme,
|
|
||||||
scrollPastEnd,
|
|
||||||
theme,
|
|
||||||
allowCustomCSS,
|
|
||||||
customCSS,
|
|
||||||
RTL
|
|
||||||
} = this.getStyleParams()
|
|
||||||
|
|
||||||
const inlineStyles = buildStyle({
|
|
||||||
fontFamily,
|
|
||||||
fontSize,
|
|
||||||
codeBlockFontFamily,
|
|
||||||
lineNumber,
|
|
||||||
scrollPastEnd,
|
|
||||||
theme,
|
|
||||||
allowCustomCSS,
|
|
||||||
customCSS,
|
|
||||||
RTL
|
|
||||||
})
|
|
||||||
let body = this.refs.root.contentWindow.document.body.innerHTML
|
|
||||||
body = attachmentManagement.fixLocalURLS(body, this.props.storagePath)
|
|
||||||
const files = [this.getCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
|
||||||
files.forEach(file => {
|
|
||||||
if (global.process.platform === 'win32') {
|
|
||||||
file = file.replace('file:///', '')
|
|
||||||
} else {
|
|
||||||
file = file.replace('file://', '')
|
|
||||||
}
|
|
||||||
exportTasks.push({
|
|
||||||
src: file,
|
|
||||||
dst: 'css'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
let styles = ''
|
|
||||||
files.forEach(file => {
|
|
||||||
styles += `<link rel="stylesheet" href="../css/${path.basename(file)}">`
|
|
||||||
})
|
|
||||||
|
|
||||||
return `<html>
|
|
||||||
<head>
|
|
||||||
<base href="file://${targetDir}/">
|
|
||||||
<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>`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSaveAsHtml() {
|
handleSaveAsHtml() {
|
||||||
this.exportAsDocument('html', (noteContent, exportTasks, targetDir) =>
|
this.exportAsDocument('html', formatHTML(this.props))
|
||||||
Promise.resolve(
|
|
||||||
this.htmlContentFormatter(noteContent, exportTasks, targetDir)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSaveAsPdf() {
|
handleSaveAsPdf() {
|
||||||
this.exportAsDocument('pdf', (noteContent, exportTasks, targetDir) => {
|
this.exportAsDocument('pdf', formatPDF(this.props))
|
||||||
const printout = new remote.BrowserWindow({
|
|
||||||
show: false,
|
|
||||||
webPreferences: { webSecurity: false, javascript: false }
|
|
||||||
})
|
|
||||||
printout.loadURL(
|
|
||||||
'data:text/html;charset=UTF-8,' +
|
|
||||||
this.htmlContentFormatter(noteContent, exportTasks, targetDir)
|
|
||||||
)
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
printout.webContents.on('did-finish-load', () => {
|
|
||||||
printout.webContents.printToPDF({}, (err, data) => {
|
|
||||||
if (err) reject(err)
|
|
||||||
else resolve(data)
|
|
||||||
printout.destroy()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePrint() {
|
handlePrint() {
|
||||||
@@ -525,18 +202,21 @@ 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
|
|
||||||
const nodeKey = this.props.noteKey
|
|
||||||
|
|
||||||
exportNote(nodeKey, 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',
|
||||||
@@ -567,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() {
|
getScrollBarStyle() {
|
||||||
const { theme } = this.props
|
const { theme } = this.props
|
||||||
|
|
||||||
@@ -743,47 +397,6 @@ class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getStyleParams() {
|
|
||||||
const {
|
|
||||||
fontSize,
|
|
||||||
lineNumber,
|
|
||||||
codeBlockTheme,
|
|
||||||
scrollPastEnd,
|
|
||||||
theme,
|
|
||||||
allowCustomCSS,
|
|
||||||
customCSS,
|
|
||||||
RTL
|
|
||||||
} = 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,
|
|
||||||
RTL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
applyStyle() {
|
applyStyle() {
|
||||||
const {
|
const {
|
||||||
fontFamily,
|
fontFamily,
|
||||||
@@ -796,12 +409,13 @@ class MarkdownPreview extends React.Component {
|
|||||||
allowCustomCSS,
|
allowCustomCSS,
|
||||||
customCSS,
|
customCSS,
|
||||||
RTL
|
RTL
|
||||||
} = this.getStyleParams()
|
} = getStyleParams(this.props)
|
||||||
|
|
||||||
this.getWindow().document.getElementById(
|
this.getWindow().document.getElementById(
|
||||||
'codeTheme'
|
'codeTheme'
|
||||||
).href = this.getCodeThemeLink(codeBlockTheme)
|
).href = getCodeThemeLink(codeBlockTheme)
|
||||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle({
|
|
||||||
|
this.getWindow().document.getElementById('style').innerHTML = buildStyle(
|
||||||
fontFamily,
|
fontFamily,
|
||||||
fontSize,
|
fontSize,
|
||||||
codeBlockFontFamily,
|
codeBlockFontFamily,
|
||||||
@@ -811,15 +425,7 @@ class MarkdownPreview extends React.Component {
|
|||||||
allowCustomCSS,
|
allowCustomCSS,
|
||||||
customCSS,
|
customCSS,
|
||||||
RTL
|
RTL
|
||||||
})
|
)
|
||||||
}
|
|
||||||
|
|
||||||
getCodeThemeLink(name) {
|
|
||||||
const theme = consts.THEMES.find(theme => theme.name === name)
|
|
||||||
|
|
||||||
return theme != null
|
|
||||||
? theme.path
|
|
||||||
: `${appPath}/node_modules/codemirror/theme/elegant.css`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rewriteIframe() {
|
rewriteIframe() {
|
||||||
@@ -853,7 +459,7 @@ class MarkdownPreview extends React.Component {
|
|||||||
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
|
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
|
||||||
if (sanitize === 'NONE') {
|
if (sanitize === 'NONE') {
|
||||||
const splitWithCodeTag = value.split('```')
|
const splitWithCodeTag = value.split('```')
|
||||||
value = this.escapeHtmlCharactersInCodeTag(splitWithCodeTag)
|
value = escapeHtmlCharactersInCodeTag(splitWithCodeTag)
|
||||||
}
|
}
|
||||||
const renderedHTML = this.markdown.render(value)
|
const renderedHTML = this.markdown.render(value)
|
||||||
attachmentManagement.migrateAttachments(value, storagePath, noteKey)
|
attachmentManagement.migrateAttachments(value, storagePath, noteKey)
|
||||||
@@ -916,13 +522,9 @@ class MarkdownPreview extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const opts = {}
|
const opts = {}
|
||||||
// if (this.props.theme === 'dark') {
|
|
||||||
// opts['font-color'] = '#DDD'
|
|
||||||
// opts['line-color'] = '#DDD'
|
|
||||||
// opts['element-color'] = '#DDD'
|
|
||||||
// opts['fill'] = '#3A404C'
|
|
||||||
// }
|
|
||||||
_.forEach(
|
_.forEach(
|
||||||
this.refs.root.contentWindow.document.querySelectorAll('.flowchart'),
|
this.refs.root.contentWindow.document.querySelectorAll('.flowchart'),
|
||||||
el => {
|
el => {
|
||||||
|
|||||||
@@ -336,6 +336,7 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
storageKey,
|
storageKey,
|
||||||
noteKey,
|
noteKey,
|
||||||
linesHighlighted,
|
linesHighlighted,
|
||||||
|
getNote,
|
||||||
isStacking,
|
isStacking,
|
||||||
RTL
|
RTL
|
||||||
} = this.props
|
} = this.props
|
||||||
@@ -470,6 +471,7 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
codeBlockTheme={config.preview.codeBlockTheme}
|
codeBlockTheme={config.preview.codeBlockTheme}
|
||||||
codeBlockFontFamily={config.editor.fontFamily}
|
codeBlockFontFamily={config.editor.fontFamily}
|
||||||
lineNumber={config.preview.lineNumber}
|
lineNumber={config.preview.lineNumber}
|
||||||
|
indentSize={editorIndentSize}
|
||||||
scrollPastEnd={config.preview.scrollPastEnd}
|
scrollPastEnd={config.preview.scrollPastEnd}
|
||||||
smartQuotes={config.preview.smartQuotes}
|
smartQuotes={config.preview.smartQuotes}
|
||||||
smartArrows={config.preview.smartArrows}
|
smartArrows={config.preview.smartArrows}
|
||||||
@@ -486,6 +488,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}
|
||||||
|
getNote={getNote}
|
||||||
|
export={config.export}
|
||||||
RTL={RTL}
|
RTL={RTL}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ class Markdown {
|
|||||||
html: true,
|
html: true,
|
||||||
xhtmlOut: true,
|
xhtmlOut: true,
|
||||||
breaks: config.preview.breaks,
|
breaks: config.preview.breaks,
|
||||||
sanitize: 'STRICT'
|
sanitize: 'STRICT',
|
||||||
|
onFence: () => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedOptions = Object.assign(defaultOptions, options)
|
const updatedOptions = Object.assign(defaultOptions, options)
|
||||||
@@ -266,22 +267,26 @@ class Markdown {
|
|||||||
token.parameters.format = 'yaml'
|
token.parameters.format = 'yaml'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updatedOptions.onFence('chart', token.parameters.format)
|
||||||
|
|
||||||
return `<pre class="fence" data-line="${token.map[0]}">
|
return `<pre class="fence" data-line="${token.map[0]}">
|
||||||
<span class="filename">${token.fileName}</span>
|
<span class="filename">${token.fileName}</span>
|
||||||
<div class="chart" data-height="${
|
<div class="chart" data-height="${
|
||||||
token.parameters.height
|
token.parameters.height
|
||||||
}" data-format="${token.parameters.format || 'json'}">${
|
}" data-format="${token.parameters.format || 'json'}">${
|
||||||
token.content
|
token.content
|
||||||
}</div>
|
}</div>
|
||||||
</pre>`
|
</pre>`
|
||||||
},
|
},
|
||||||
flowchart: token => {
|
flowchart: token => {
|
||||||
|
updatedOptions.onFence('flowchart')
|
||||||
|
|
||||||
return `<pre class="fence" data-line="${token.map[0]}">
|
return `<pre class="fence" data-line="${token.map[0]}">
|
||||||
<span class="filename">${token.fileName}</span>
|
<span class="filename">${token.fileName}</span>
|
||||||
<div class="flowchart" data-height="${token.parameters.height}">${
|
<div class="flowchart" data-height="${token.parameters.height}">${
|
||||||
token.content
|
token.content
|
||||||
}</div>
|
}</div>
|
||||||
</pre>`
|
</pre>`
|
||||||
},
|
},
|
||||||
gallery: token => {
|
gallery: token => {
|
||||||
const content = token.content
|
const content = token.content
|
||||||
@@ -298,35 +303,41 @@ class Markdown {
|
|||||||
.join('\n')
|
.join('\n')
|
||||||
|
|
||||||
return `<pre class="fence" data-line="${token.map[0]}">
|
return `<pre class="fence" data-line="${token.map[0]}">
|
||||||
<span class="filename">${token.fileName}</span>
|
<span class="filename">${token.fileName}</span>
|
||||||
<div class="gallery" data-autoplay="${
|
<div class="gallery" data-autoplay="${
|
||||||
token.parameters.autoplay
|
token.parameters.autoplay
|
||||||
}" data-height="${token.parameters.height}">${content}</div>
|
}" data-height="${token.parameters.height}">${content}</div>
|
||||||
</pre>`
|
</pre>`
|
||||||
},
|
},
|
||||||
mermaid: token => {
|
mermaid: token => {
|
||||||
|
updatedOptions.onFence('mermaid')
|
||||||
|
|
||||||
return `<pre class="fence" data-line="${token.map[0]}">
|
return `<pre class="fence" data-line="${token.map[0]}">
|
||||||
<span class="filename">${token.fileName}</span>
|
<span class="filename">${token.fileName}</span>
|
||||||
<div class="mermaid" data-height="${token.parameters.height}">${
|
<div class="mermaid" data-height="${token.parameters.height}">${
|
||||||
token.content
|
token.content
|
||||||
}</div>
|
}</div>
|
||||||
</pre>`
|
</pre>`
|
||||||
},
|
},
|
||||||
sequence: token => {
|
sequence: token => {
|
||||||
|
updatedOptions.onFence('sequence')
|
||||||
|
|
||||||
return `<pre class="fence" data-line="${token.map[0]}">
|
return `<pre class="fence" data-line="${token.map[0]}">
|
||||||
<span class="filename">${token.fileName}</span>
|
<span class="filename">${token.fileName}</span>
|
||||||
<div class="sequence" data-height="${token.parameters.height}">${
|
<div class="sequence" data-height="${token.parameters.height}">${
|
||||||
token.content
|
token.content
|
||||||
}</div>
|
}</div>
|
||||||
</pre>`
|
</pre>`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
token => {
|
token => {
|
||||||
|
updatedOptions.onFence('code', token.langType)
|
||||||
|
|
||||||
return `<pre class="code CodeMirror" data-line="${token.map[0]}">
|
return `<pre class="code CodeMirror" data-line="${token.map[0]}">
|
||||||
<span class="filename">${token.fileName}</span>
|
<span class="filename">${token.fileName}</span>
|
||||||
${createGutter(token.content, token.firstLineNumber)}
|
${createGutter(token.content, token.firstLineNumber)}
|
||||||
<code class="${token.langType}">${token.content}</code>
|
<code class="${token.langType}">${token.content}</code>
|
||||||
</pre>`
|
</pre>`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -57,10 +57,11 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
|
|
||||||
this.dispatchTimer = null
|
this.dispatchTimer = null
|
||||||
|
|
||||||
this.toggleLockButton = this.handleToggleLockButton.bind(this)
|
|
||||||
this.generateToc = this.handleGenerateToc.bind(this)
|
this.generateToc = this.handleGenerateToc.bind(this)
|
||||||
|
this.toggleLockButton = this.handleToggleLockButton.bind(this)
|
||||||
this.handleUpdateContent = this.handleUpdateContent.bind(this)
|
this.handleUpdateContent = this.handleUpdateContent.bind(this)
|
||||||
this.handleSwitchStackDirection = this.handleSwitchStackDirection.bind(this)
|
this.handleSwitchStackDirection = this.handleSwitchStackDirection.bind(this)
|
||||||
|
this.getNote = this.getNote.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
@@ -441,6 +442,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, isStacking } = this.state
|
const { note, isStacking } = this.state
|
||||||
@@ -456,8 +461,8 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
noteKey={note.key}
|
noteKey={note.key}
|
||||||
linesHighlighted={note.linesHighlighted}
|
linesHighlighted={note.linesHighlighted}
|
||||||
onChange={this.handleUpdateContent}
|
onChange={this.handleUpdateContent}
|
||||||
isLocked={this.state.isLocked}
|
|
||||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||||
|
getNote={this.getNote}
|
||||||
RTL={config.editor.rtlEnabled && this.state.RTL}
|
RTL={config.editor.rtlEnabled && this.state.RTL}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -473,6 +478,7 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
linesHighlighted={note.linesHighlighted}
|
linesHighlighted={note.linesHighlighted}
|
||||||
onChange={this.handleUpdateContent}
|
onChange={this.handleUpdateContent}
|
||||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||||
|
getNote={this.getNote}
|
||||||
RTL={config.editor.rtlEnabled && this.state.RTL}
|
RTL={config.editor.rtlEnabled && this.state.RTL}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import Markdown from '../../lib/markdown'
|
|||||||
import i18n from 'browser/lib/i18n'
|
import i18n from 'browser/lib/i18n'
|
||||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||||
import context from 'browser/lib/context'
|
import context from 'browser/lib/context'
|
||||||
|
import filenamify from 'filenamify'
|
||||||
import queryString from 'query-string'
|
import queryString from 'query-string'
|
||||||
|
|
||||||
const { remote } = require('electron')
|
const { remote } = require('electron')
|
||||||
@@ -634,6 +635,38 @@ class NoteList extends React.Component {
|
|||||||
this.selectNextNote()
|
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) {
|
handleNoteContextMenu(e, uniqueKey) {
|
||||||
const { location } = this.props
|
const { location } = this.props
|
||||||
const { selectedNoteKeys } = this.state
|
const { selectedNoteKeys } = this.state
|
||||||
@@ -689,9 +722,40 @@ class NoteList extends React.Component {
|
|||||||
click: this.copyNoteLink.bind(this, note)
|
click: this.copyNoteLink.bind(this, note)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (note.type === 'MARKDOWN_NOTE') {
|
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) {
|
if (note.blog && note.blog.blogLink && note.blog.blogId) {
|
||||||
templates.push(
|
templates.push(
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: updateLabel,
|
label: updateLabel,
|
||||||
click: this.publishMarkdown.bind(this)
|
click: this.publishMarkdown.bind(this)
|
||||||
@@ -702,10 +766,15 @@ class NoteList extends React.Component {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
templates.push({
|
templates.push(
|
||||||
label: publishLabel,
|
{
|
||||||
click: this.publishMarkdown.bind(this)
|
type: 'separator'
|
||||||
})
|
},
|
||||||
|
{
|
||||||
|
label: publishLabel,
|
||||||
|
click: this.publishMarkdown.bind(this)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,12 +43,20 @@ 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')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.__('Export as PDF (.pdf)'),
|
||||||
|
click: e => this.handleExportStorageClick(e, 'pdf')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -97,14 +105,28 @@ class StorageItem extends React.Component {
|
|||||||
}
|
}
|
||||||
dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
|
dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
|
||||||
if (paths && paths.length === 1) {
|
if (paths && paths.length === 1) {
|
||||||
const { storage, dispatch } = this.props
|
const { storage, dispatch, config } = this.props
|
||||||
dataApi.exportStorage(storage.key, fileType, paths[0]).then(data => {
|
dataApi
|
||||||
dispatch({
|
.exportStorage(storage.key, fileType, paths[0], config)
|
||||||
type: 'EXPORT_STORAGE',
|
.then(data => {
|
||||||
storage: data.storage,
|
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
fileType: data.fileType
|
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
|
||||||
})
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -166,12 +188,20 @@ 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')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.__('Export as PDF (.pdf)'),
|
||||||
|
click: e => this.handleExportFolderClick(e, folder, 'pdf')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -202,30 +232,28 @@ class StorageItem extends React.Component {
|
|||||||
}
|
}
|
||||||
dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
|
dialog.showOpenDialog(remote.getCurrentWindow(), options, 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,
|
||||||
folderKey: data.folderKey,
|
folderKey: data.folderKey,
|
||||||
fileType: data.fileType
|
fileType: data.fileType
|
||||||
})
|
})
|
||||||
return data
|
|
||||||
})
|
})
|
||||||
.then(data => {
|
.catch(error => {
|
||||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
|
||||||
type: 'info',
|
|
||||||
message: 'Exported to "' + data.exportDir + '"'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
dialog.showErrorBox(
|
dialog.showErrorBox(
|
||||||
'Export error',
|
'Export error',
|
||||||
err ? err.message || err : 'Unexpected error during export'
|
error ? error.message || error : 'Unexpected error during export'
|
||||||
)
|
)
|
||||||
throw err
|
throw error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
|||||||
import ColorPicker from 'browser/components/ColorPicker'
|
import ColorPicker from 'browser/components/ColorPicker'
|
||||||
import { every, sortBy } from 'lodash'
|
import { every, sortBy } from 'lodash'
|
||||||
|
|
||||||
|
const { dialog } = remote
|
||||||
|
|
||||||
function matchActiveTags(tags, activeTags) {
|
function matchActiveTags(tags, activeTags) {
|
||||||
return every(activeTags, v => tags.indexOf(v) >= 0)
|
return every(activeTags, v => tags.indexOf(v) >= 0)
|
||||||
}
|
}
|
||||||
@@ -63,15 +65,12 @@ class SideNav extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteTag(tag) {
|
deleteTag(tag) {
|
||||||
const selectedButton = remote.dialog.showMessageBox(
|
const selectedButton = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
remote.getCurrentWindow(),
|
type: 'warning',
|
||||||
{
|
message: i18n.__('Confirm tag deletion'),
|
||||||
type: 'warning',
|
detail: i18n.__('This will permanently remove this tag.'),
|
||||||
message: i18n.__('Confirm tag deletion'),
|
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||||
detail: i18n.__('This will permanently remove this tag.'),
|
})
|
||||||
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (selectedButton === 0) {
|
if (selectedButton === 0) {
|
||||||
const {
|
const {
|
||||||
@@ -155,28 +154,80 @@ class SideNav extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleTagContextMenu(e, tag) {
|
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({
|
handleExportTagClick(e, tag, fileType) {
|
||||||
label: i18n.__('Delete Tag'),
|
const options = {
|
||||||
click: this.deleteTag.bind(this, tag)
|
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() {
|
dismissColorPicker() {
|
||||||
@@ -330,6 +381,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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -144,6 +144,11 @@ export const DEFAULT_CONFIG = {
|
|||||||
username: '',
|
username: '',
|
||||||
password: ''
|
password: ''
|
||||||
},
|
},
|
||||||
|
export: {
|
||||||
|
metadata: 'DONT_EXPORT', // 'DONT_EXPORT', 'MERGE_HEADER', 'MERGE_VARIABLE'
|
||||||
|
variable: 'boostnote',
|
||||||
|
prefixAttachmentFolder: false
|
||||||
|
},
|
||||||
coloredTags: {},
|
coloredTags: {},
|
||||||
wakatime: {
|
wakatime: {
|
||||||
key: null
|
key: null
|
||||||
|
|||||||
@@ -706,14 +706,15 @@ function replaceNoteKeyWithNewNoteKey(noteContent, oldNoteKey, newNoteKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Deletes all :storage and noteKey references from the given input.
|
* @description replace all :storage references with given destination folder.
|
||||||
* @param input Input in which the references should be deleted
|
* @param input Input in which the references should be replaced
|
||||||
* @param noteKey Key of the current note
|
* @param noteKey Key of the current note
|
||||||
|
* @param destinationFolder Destination folder of the attachements
|
||||||
* @returns {String} Input without the references
|
* @returns {String} Input without the references
|
||||||
*/
|
*/
|
||||||
function removeStorageAndNoteReferences(input, noteKey) {
|
function replaceStorageReferences(input, noteKey, destinationFolder) {
|
||||||
return input.replace(
|
return input.replace(
|
||||||
new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?("|\\))', 'g'),
|
new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '[^"\\)<\\s]+', 'g'),
|
||||||
function(match) {
|
function(match) {
|
||||||
return match
|
return match
|
||||||
.replace(new RegExp(mdurl.encode(path.win32.sep), 'g'), path.posix.sep)
|
.replace(new RegExp(mdurl.encode(path.win32.sep), 'g'), path.posix.sep)
|
||||||
@@ -735,7 +736,7 @@ function removeStorageAndNoteReferences(input, noteKey) {
|
|||||||
')?',
|
')?',
|
||||||
'g'
|
'g'
|
||||||
),
|
),
|
||||||
DESTINATION_FOLDER
|
destinationFolder
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1101,8 +1102,8 @@ module.exports = {
|
|||||||
getAttachmentsInMarkdownContent,
|
getAttachmentsInMarkdownContent,
|
||||||
getAbsolutePathsOfAttachmentsInContent,
|
getAbsolutePathsOfAttachmentsInContent,
|
||||||
importAttachments,
|
importAttachments,
|
||||||
removeStorageAndNoteReferences,
|
|
||||||
removeAttachmentsByPaths,
|
removeAttachmentsByPaths,
|
||||||
|
replaceStorageReferences,
|
||||||
deleteAttachmentFolder,
|
deleteAttachmentFolder,
|
||||||
deleteAttachmentsNotPresentInNote,
|
deleteAttachmentsNotPresentInNote,
|
||||||
getAttachmentsPathAndStatus,
|
getAttachmentsPathAndStatus,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const fs = require('fs')
|
import fs from 'fs'
|
||||||
const path = require('path')
|
import fx from 'fs-extra'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Copy a file from source to destination
|
* @description Copy a file from source to destination
|
||||||
@@ -14,7 +15,8 @@ function copyFile(srcPath, dstPath) {
|
|||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const dstFolder = path.dirname(dstPath)
|
const dstFolder = path.dirname(dstPath)
|
||||||
if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder)
|
|
||||||
|
fx.ensureDirSync(dstFolder)
|
||||||
|
|
||||||
const input = fs.createReadStream(decodeURI(srcPath))
|
const input = fs.createReadStream(decodeURI(srcPath))
|
||||||
const output = fs.createWriteStream(dstPath)
|
const output = fs.createWriteStream(dstPath)
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { findStorage } from 'browser/lib/findStorage'
|
import { findStorage } from 'browser/lib/findStorage'
|
||||||
import resolveStorageData from './resolveStorageData'
|
import resolveStorageData from './resolveStorageData'
|
||||||
import resolveStorageNotes from './resolveStorageNotes'
|
import resolveStorageNotes from './resolveStorageNotes'
|
||||||
|
import getFilename from './getFilename'
|
||||||
import exportNote from './exportNote'
|
import exportNote from './exportNote'
|
||||||
import filenamify from 'filenamify'
|
import getContentFormatter from './getContentFormatter'
|
||||||
import * as path from 'path'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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 +23,7 @@ import * as path from 'path'
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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)
|
||||||
@@ -30,39 +31,34 @@ function exportFolder(storageKey, folderKey, fileType, exportDir) {
|
|||||||
return Promise.reject(e)
|
return Promise.reject(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deduplicator = {}
|
||||||
|
|
||||||
return resolveStorageData(targetStorage)
|
return resolveStorageData(targetStorage)
|
||||||
.then(function assignNotes(storage) {
|
.then(storage => {
|
||||||
return resolveStorageNotes(storage).then(notes => {
|
return resolveStorageNotes(storage).then(notes => ({
|
||||||
return {
|
storage,
|
||||||
storage,
|
notes: notes.filter(
|
||||||
notes
|
note =>
|
||||||
}
|
note.folder === folderKey &&
|
||||||
})
|
!note.isTrashed &&
|
||||||
|
note.type === 'MARKDOWN_NOTE'
|
||||||
|
)
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
.then(function exportNotes(data) {
|
.then(({ storage, notes }) => {
|
||||||
const { storage, notes } = data
|
const contentFormatter = getContentFormatter(storage, fileType, config)
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
notes
|
notes.map(note => {
|
||||||
.filter(
|
const targetPath = getFilename(
|
||||||
note =>
|
note,
|
||||||
note.folder === folderKey &&
|
fileType,
|
||||||
note.isTrashed === false &&
|
exportDir,
|
||||||
note.type === 'MARKDOWN_NOTE'
|
deduplicator
|
||||||
)
|
)
|
||||||
.map(note => {
|
|
||||||
const notePath = path.join(
|
return exportNote(storage.key, note, targetPath, contentFormatter)
|
||||||
exportDir,
|
})
|
||||||
`${filenamify(note.title, { replacement: '_' })}.${fileType}`
|
|
||||||
)
|
|
||||||
return exportNote(
|
|
||||||
note.key,
|
|
||||||
storage.path,
|
|
||||||
note.content,
|
|
||||||
notePath,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
})
|
|
||||||
).then(() => ({
|
).then(() => ({
|
||||||
storage,
|
storage,
|
||||||
folderKey,
|
folderKey,
|
||||||
|
|||||||
@@ -4,58 +4,35 @@ import { findStorage } from 'browser/lib/findStorage'
|
|||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
const attachmentManagement = require('./attachmentManagement')
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export note together with attachments
|
* Export note together with attachments
|
||||||
*
|
*
|
||||||
* If attachments are stored in the storage, creates 'attachments' subfolder in target directory
|
* If attachments are stored in the storage, creates 'attachments' subfolder in target directory
|
||||||
* and copies attachments to it. Changes links to images in the content of the note
|
* and copies attachments to it. Changes links to images in the content of the note
|
||||||
*
|
*
|
||||||
* @param {String} nodeKey key of the node that should be exported
|
|
||||||
* @param {String} storageKey or storage path
|
* @param {String} storageKey or storage path
|
||||||
* @param {String} noteContent Content to export
|
* @param {Object} note Note to export
|
||||||
* @param {String} targetPath Path to exported file
|
* @param {String} targetPath Path to exported file
|
||||||
* @param {function} outputFormatter
|
* @param {function} outputFormatter
|
||||||
* @return {Promise.<*[]>}
|
* @return {Promise.<*[]>}
|
||||||
*/
|
*/
|
||||||
function exportNote(
|
function exportNote(storageKey, note, targetPath, outputFormatter) {
|
||||||
nodeKey,
|
|
||||||
storageKey,
|
|
||||||
noteContent,
|
|
||||||
targetPath,
|
|
||||||
outputFormatter
|
|
||||||
) {
|
|
||||||
const storagePath = path.isAbsolute(storageKey)
|
const storagePath = path.isAbsolute(storageKey)
|
||||||
? storageKey
|
? storageKey
|
||||||
: findStorage(storageKey).path
|
: findStorage(storageKey).path
|
||||||
|
|
||||||
const exportTasks = []
|
const exportTasks = []
|
||||||
|
|
||||||
if (!storagePath) {
|
if (!storagePath) {
|
||||||
throw new Error('Storage path is not found')
|
throw new Error('Storage path is not found')
|
||||||
}
|
}
|
||||||
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
|
|
||||||
noteContent,
|
|
||||||
storagePath
|
|
||||||
)
|
|
||||||
attachmentsAbsolutePaths.forEach(attachment => {
|
|
||||||
exportTasks.push({
|
|
||||||
src: attachment,
|
|
||||||
dst: attachmentManagement.DESTINATION_FOLDER
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
let exportedData = attachmentManagement.removeStorageAndNoteReferences(
|
const exportedData = Promise.resolve(
|
||||||
noteContent,
|
outputFormatter
|
||||||
nodeKey
|
? outputFormatter(note, targetPath, exportTasks)
|
||||||
|
: note.content
|
||||||
)
|
)
|
||||||
|
|
||||||
if (outputFormatter) {
|
|
||||||
exportedData = outputFormatter(exportedData, exportTasks, targetPath)
|
|
||||||
} else {
|
|
||||||
exportedData = Promise.resolve(exportedData)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)))
|
||||||
@@ -63,9 +40,9 @@ function exportNote(
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
return saveToFile(data, targetPath)
|
return saveToFile(data, targetPath)
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(error => {
|
||||||
rollbackExport(tasks)
|
rollbackExport(tasks)
|
||||||
throw err
|
throw error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,14 +84,14 @@ function rollbackExport(tasks) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fs.existsSync(fullpath)) {
|
if (fs.existsSync(fullpath)) {
|
||||||
fs.unlink(fullpath)
|
fs.unlinkSync(fullpath)
|
||||||
folders.add(path.dirname(fullpath))
|
folders.add(path.dirname(fullpath))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
folders.forEach(folder => {
|
folders.forEach(folder => {
|
||||||
if (fs.readdirSync(folder).length === 0) {
|
if (fs.readdirSync(folder).length === 0) {
|
||||||
fs.rmdir(folder)
|
fs.rmdirSync(folder)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
19
browser/main/lib/dataApi/exportNoteAs.js
Normal file
19
browser/main/lib/dataApi/exportNoteAs.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { findStorage } from 'browser/lib/findStorage'
|
||||||
|
import exportNote from './exportNote'
|
||||||
|
import getContentFormatter from './getContentFormatter'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} note
|
||||||
|
* @param {String} filename
|
||||||
|
* @param {String} fileType
|
||||||
|
* @param {Object} config
|
||||||
|
*/
|
||||||
|
|
||||||
|
function exportNoteAs(note, filename, fileType, config) {
|
||||||
|
const storage = findStorage(note.storage)
|
||||||
|
const contentFormatter = getContentFormatter(storage, fileType, config)
|
||||||
|
|
||||||
|
return exportNote(storage.key, note, filename, contentFormatter)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exportNoteAs
|
||||||
@@ -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 getContentFormatter from './getContentFormatter'
|
||||||
|
import getFilename from './getFilename'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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,39 +33,52 @@ 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(
|
||||||
const { storage, notes } = data
|
note => !note.isTrashed && note.type === 'MARKDOWN_NOTE'
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.then(({ storage, notes }) => {
|
||||||
|
const contentFormatter = getContentFormatter(storage, fileType, config)
|
||||||
|
|
||||||
const folderNamesMapping = {}
|
const folderNamesMapping = {}
|
||||||
|
const deduplicators = {}
|
||||||
|
|
||||||
storage.folders.forEach(folder => {
|
storage.folders.forEach(folder => {
|
||||||
const folderExportedDir = path.join(
|
const folderExportedDir = path.join(
|
||||||
exportDir,
|
exportDir,
|
||||||
filenamify(folder.name, { replacement: '_' })
|
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 {
|
deduplicators[folder.key] = {}
|
||||||
|
})
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
notes.map(note => {
|
||||||
|
const targetPath = getFilename(
|
||||||
|
note,
|
||||||
|
fileType,
|
||||||
|
folderNamesMapping[note.folder],
|
||||||
|
deduplicators[note.folder]
|
||||||
|
)
|
||||||
|
|
||||||
|
return exportNote(storage.key, note, targetPath, contentFormatter)
|
||||||
|
})
|
||||||
|
).then(() => ({
|
||||||
storage,
|
storage,
|
||||||
fileType,
|
fileType,
|
||||||
exportDir
|
exportDir
|
||||||
}
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
28
browser/main/lib/dataApi/exportTag.js
Normal file
28
browser/main/lib/dataApi/exportTag.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import exportNoteAs from './exportNoteAs'
|
||||||
|
import getFilename from './getFilename'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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)
|
||||||
|
|
||||||
|
const deduplicator = {}
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
notes.map(note => {
|
||||||
|
const filename = getFilename(note, fileType, exportDir, deduplicator)
|
||||||
|
|
||||||
|
return exportNoteAs(note, filename, fileType, config)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exportTag
|
||||||
796
browser/main/lib/dataApi/formatHTML.js
Normal file
796
browser/main/lib/dataApi/formatHTML.js
Normal file
@@ -0,0 +1,796 @@
|
|||||||
|
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'
|
||||||
|
import attachmentManagement from './attachmentManagement'
|
||||||
|
import { version as codemirrorVersion } from 'codemirror/package.json'
|
||||||
|
import { escapeHtmlCharacters } from 'browser/lib/utils'
|
||||||
|
|
||||||
|
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`,
|
||||||
|
`${appPath}/node_modules/react-image-carousel/lib/css/main.min.css`
|
||||||
|
]
|
||||||
|
|
||||||
|
const macos = global.process.platform === 'darwin'
|
||||||
|
|
||||||
|
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
|
||||||
|
if (!macos) {
|
||||||
|
defaultFontFamily.unshift('Microsoft YaHei')
|
||||||
|
defaultFontFamily.unshift('meiryo')
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultCodeBlockFontFamily = [
|
||||||
|
'Monaco',
|
||||||
|
'Menlo',
|
||||||
|
'Ubuntu Mono',
|
||||||
|
'Consolas',
|
||||||
|
'source-code-pro',
|
||||||
|
'monospace'
|
||||||
|
]
|
||||||
|
|
||||||
|
function unprefix(file) {
|
||||||
|
if (global.process.platform === 'win32') {
|
||||||
|
return file.replace('file:///', '')
|
||||||
|
} else {
|
||||||
|
return file.replace('file://', '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ```
|
||||||
|
* {
|
||||||
|
* fontFamily,
|
||||||
|
* fontSize,
|
||||||
|
* lineNumber,
|
||||||
|
* codeBlockFontFamily,
|
||||||
|
* codeBlockTheme,
|
||||||
|
* scrollPastEnd,
|
||||||
|
* theme,
|
||||||
|
* allowCustomCSS,
|
||||||
|
* customCSS
|
||||||
|
* smartQuotes,
|
||||||
|
* sanitize,
|
||||||
|
* breaks,
|
||||||
|
* storagePath,
|
||||||
|
* export,
|
||||||
|
* indentSize
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
|
||||||
|
let indentSize = parseInt(props.indentSize, 10)
|
||||||
|
if (!(indentSize > 0 && indentSize < 132)) {
|
||||||
|
indentSize = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = [getCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
||||||
|
|
||||||
|
return function(note, targetPath, exportTasks) {
|
||||||
|
const styles = files
|
||||||
|
.map(file => `<link rel="stylesheet" href="css/${path.basename(file)}">`)
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lodash = true
|
||||||
|
|
||||||
|
exportTasks.push({
|
||||||
|
src: unprefix(`${appPath}/node_modules/lodash/lodash.min.js`),
|
||||||
|
dst: 'js'
|
||||||
|
})
|
||||||
|
|
||||||
|
scripts += `<script src="js/lodash.min.js"></script>`
|
||||||
|
}
|
||||||
|
|
||||||
|
let raphael = false
|
||||||
|
function addRaphael() {
|
||||||
|
if (raphael) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
raphael = true
|
||||||
|
|
||||||
|
exportTasks.push({
|
||||||
|
src: unprefix(`${appPath}/node_modules/raphael/raphael.min.js`),
|
||||||
|
dst: 'js'
|
||||||
|
})
|
||||||
|
|
||||||
|
scripts += `<script src="js/raphael.min.js"></script>`
|
||||||
|
}
|
||||||
|
|
||||||
|
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 += `<script src="js/js-yaml.min.js"></script>`
|
||||||
|
}
|
||||||
|
|
||||||
|
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 += `<script src="js/Chart.min.js"></script>`
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
codemirror = true
|
||||||
|
|
||||||
|
addDecodeEntities()
|
||||||
|
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 += `
|
||||||
|
<script src="js/codemirror/codemirror.js"></script>
|
||||||
|
<script src="js/codemirror/mode/meta.js"></script>
|
||||||
|
<script src="js/codemirror/addon/mode/loadmode.js"></script>
|
||||||
|
<script src="js/codemirror/addon/runmode/runmode.js"></script>
|
||||||
|
`
|
||||||
|
|
||||||
|
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 = 'https://cdn.jsdelivr.net/npm/codemirror@${codemirrorVersion}/mode/%N/%N.js';
|
||||||
|
|
||||||
|
function displayCodeBlocks() {
|
||||||
|
_.forEach(
|
||||||
|
document.querySelectorAll('.code code'),
|
||||||
|
el => {
|
||||||
|
el.parentNode.className += ' ${className}'
|
||||||
|
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 = ''
|
||||||
|
CodeMirror.runMode(content, syntax.mime, el, {
|
||||||
|
tabSize: ${indentSize}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 += `<script src="js/flowchart.min.js"></script>`
|
||||||
|
|
||||||
|
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 += `<script src="js/mermaid.min.js"></script>`
|
||||||
|
|
||||||
|
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/@rokt33r/js-sequence-diagrams/dist/sequence-diagram-min.js`
|
||||||
|
),
|
||||||
|
dst: 'js'
|
||||||
|
})
|
||||||
|
|
||||||
|
scripts += `<script src="js/sequence-diagram-min.js"></script>`
|
||||||
|
|
||||||
|
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 === 'chart') {
|
||||||
|
addChart()
|
||||||
|
|
||||||
|
if (mode === 'yaml') {
|
||||||
|
addYAML()
|
||||||
|
}
|
||||||
|
} else 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (type === 'flowchart') {
|
||||||
|
addFlowchart()
|
||||||
|
} else if (type === 'mermaid') {
|
||||||
|
addMermaid()
|
||||||
|
} else if (type === 'sequence') {
|
||||||
|
addSequence()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let body = note.content
|
||||||
|
|
||||||
|
if (sanitize === 'NONE') {
|
||||||
|
body = escapeHtmlCharactersInCodeTag(body.split('```'))
|
||||||
|
}
|
||||||
|
|
||||||
|
body = markdown.render(note.content)
|
||||||
|
|
||||||
|
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
|
||||||
|
note.content,
|
||||||
|
props.storagePath
|
||||||
|
)
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
exportTasks.push({
|
||||||
|
src: unprefix(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
|
||||||
|
)
|
||||||
|
|
||||||
|
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}
|
||||||
|
${scripts}
|
||||||
|
<script>${inlineScripts}</script>
|
||||||
|
</head>
|
||||||
|
<body data-theme="${theme}">
|
||||||
|
${body}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStyleParams(props) {
|
||||||
|
const {
|
||||||
|
fontSize,
|
||||||
|
lineNumber,
|
||||||
|
codeBlockTheme,
|
||||||
|
scrollPastEnd,
|
||||||
|
theme,
|
||||||
|
allowCustomCSS,
|
||||||
|
customCSS,
|
||||||
|
RTL
|
||||||
|
} = 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,
|
||||||
|
RTL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCodeThemeLink(name) {
|
||||||
|
const theme = consts.THEMES.find(theme => theme.name === name)
|
||||||
|
|
||||||
|
return theme != null
|
||||||
|
? theme.path
|
||||||
|
: `${appPath}/node_modules/codemirror/theme/elegant.css`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildStyle(
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
codeBlockFontFamily,
|
||||||
|
lineNumber,
|
||||||
|
scrollPastEnd,
|
||||||
|
theme,
|
||||||
|
allowCustomCSS,
|
||||||
|
customCSS,
|
||||||
|
RTL
|
||||||
|
) {
|
||||||
|
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;box-sizing: border-box;'}
|
||||||
|
${RTL && 'direction: rtl;text-align: right;'}
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
padding-bottom: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: '${codeBlockFontFamily.join("','")}';
|
||||||
|
background-color: rgba(0,0,0,0.04);
|
||||||
|
text-align: left;
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
|
||||||
|
p code,
|
||||||
|
li code,
|
||||||
|
td code
|
||||||
|
{
|
||||||
|
padding: 2px;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
[data-theme="default"] p code,
|
||||||
|
[data-theme="default"] li code,
|
||||||
|
[data-theme="default"] td code
|
||||||
|
{
|
||||||
|
background-color: #F4F4F4;
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
[data-theme="white"] p code,
|
||||||
|
[data-theme="white"] li code,
|
||||||
|
[data-theme="white"] td code
|
||||||
|
{
|
||||||
|
background-color: #F4F4F4;
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] p code,
|
||||||
|
[data-theme="dark"] li code,
|
||||||
|
[data-theme="dark"] td code
|
||||||
|
{
|
||||||
|
background-color: #444444;
|
||||||
|
border-color: #555;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
[data-theme="dracula"] p code,
|
||||||
|
[data-theme="dracula"] li code,
|
||||||
|
[data-theme="dracula"] td code
|
||||||
|
{
|
||||||
|
background-color: #444444;
|
||||||
|
border-color: #555;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
[data-theme="monokai"] p code,
|
||||||
|
[data-theme="monokai"] li code,
|
||||||
|
[data-theme="monokai"] td code
|
||||||
|
{
|
||||||
|
background-color: #444444;
|
||||||
|
border-color: #555;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
[data-theme="nord"] p code,
|
||||||
|
[data-theme="nord"] li code,
|
||||||
|
[data-theme="nord"] td code
|
||||||
|
{
|
||||||
|
background-color: #444444;
|
||||||
|
border-color: #555;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
[data-theme="solarized-dark"] p code,
|
||||||
|
[data-theme="solarized-dark"] li code,
|
||||||
|
[data-theme="solarized-dark"] td code
|
||||||
|
{
|
||||||
|
background-color: #444444;
|
||||||
|
border-color: #555;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
[data-theme="vulcan"] p code,
|
||||||
|
[data-theme="vulcan"] li code,
|
||||||
|
[data-theme="vulcan"] td code
|
||||||
|
{
|
||||||
|
background-color: #444444;
|
||||||
|
border-color: #555;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 : ''}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
}
|
||||||
103
browser/main/lib/dataApi/formatMarkdown.js
Normal file
103
browser/main/lib/dataApi/formatMarkdown.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
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 (delimiterRegExp.test(lines[0])) {
|
||||||
|
let line = 0
|
||||||
|
while (++line < lines.length && !delimiterRegExp.test(lines[line])) {}
|
||||||
|
|
||||||
|
return `---\n${yaml.dump(metadata)}---\n${lines.slice(line + 1).join('\n')}`
|
||||||
|
} else {
|
||||||
|
return `---\n${yaml.dump(metadata)}---\n\n${markdown}`
|
||||||
|
}
|
||||||
|
}
|
||||||
26
browser/main/lib/dataApi/formatPDF.js
Normal file
26
browser/main/lib/dataApi/formatPDF.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import formatHTML from './formatHTML'
|
||||||
|
import { remote } from 'electron'
|
||||||
|
|
||||||
|
export default function formatPDF(props) {
|
||||||
|
return function(note, targetPath, exportTasks) {
|
||||||
|
const printout = new remote.BrowserWindow({
|
||||||
|
show: false,
|
||||||
|
webPreferences: { webSecurity: false, javascript: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
printout.loadURL(
|
||||||
|
'data:text/html;charset=UTF-8,' +
|
||||||
|
formatHTML(props)(note, targetPath, exportTasks)
|
||||||
|
)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
printout.webContents.on('did-finish-load', () => {
|
||||||
|
printout.webContents.printToPDF({}, (err, data) => {
|
||||||
|
if (err) reject(err)
|
||||||
|
else resolve(data)
|
||||||
|
printout.destroy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
58
browser/main/lib/dataApi/getContentFormatter.js
Normal file
58
browser/main/lib/dataApi/getContentFormatter.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import formatMarkdown from './formatMarkdown'
|
||||||
|
import formatHTML from './formatHTML'
|
||||||
|
import formatPDF from './formatPDF'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} storage
|
||||||
|
* @param {String} fileType
|
||||||
|
* @param {Object} config
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function getContentFormatter(storage, fileType, config) {
|
||||||
|
if (fileType === 'md') {
|
||||||
|
return formatMarkdown({
|
||||||
|
storagePath: storage.path,
|
||||||
|
export: config.export
|
||||||
|
})
|
||||||
|
} else if (fileType === 'html') {
|
||||||
|
return 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,
|
||||||
|
RTL: config.editor.rtlEnabled /* && this.state.RTL */
|
||||||
|
})
|
||||||
|
} else if (fileType === 'pdf') {
|
||||||
|
return formatPDF({
|
||||||
|
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,
|
||||||
|
RTL: config.editor.rtlEnabled /* && this.state.RTL */
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
37
browser/main/lib/dataApi/getFilename.js
Normal file
37
browser/main/lib/dataApi/getFilename.js
Normal file
@@ -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
|
||||||
@@ -15,11 +15,14 @@ const dataApi = {
|
|||||||
updateNote: require('./updateNote'),
|
updateNote: require('./updateNote'),
|
||||||
deleteNote: require('./deleteNote'),
|
deleteNote: require('./deleteNote'),
|
||||||
moveNote: require('./moveNote'),
|
moveNote: require('./moveNote'),
|
||||||
|
exportNoteAs: require('./exportNoteAs'),
|
||||||
migrateFromV5Storage: require('./migrateFromV5Storage'),
|
migrateFromV5Storage: require('./migrateFromV5Storage'),
|
||||||
createSnippet: require('./createSnippet'),
|
createSnippet: require('./createSnippet'),
|
||||||
deleteSnippet: require('./deleteSnippet'),
|
deleteSnippet: require('./deleteSnippet'),
|
||||||
updateSnippet: require('./updateSnippet'),
|
updateSnippet: require('./updateSnippet'),
|
||||||
fetchSnippet: require('./fetchSnippet'),
|
fetchSnippet: require('./fetchSnippet'),
|
||||||
|
exportTag: require('./exportTag'),
|
||||||
|
getFilename: require('./getFilename'),
|
||||||
|
|
||||||
_migrateFromV6Storage: require('./migrateFromV6Storage'),
|
_migrateFromV6Storage: require('./migrateFromV6Storage'),
|
||||||
_resolveStorageData: require('./resolveStorageData'),
|
_resolveStorageData: require('./resolveStorageData'),
|
||||||
|
|||||||
184
browser/main/modals/PreferencesModal/ExportTab.js
Normal file
184
browser/main/modals/PreferencesModal/ExportTab.js
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
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 = {
|
||||||
|
metadata: this.refs.metadata.value,
|
||||||
|
variable: !_.isNil(this.refs.variable)
|
||||||
|
? this.refs.variable.value
|
||||||
|
: config.export.variable,
|
||||||
|
prefixAttachmentFolder: this.refs.prefixAttachmentFolder.checked
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
const ExportAlertElement =
|
||||||
|
ExportAlert != null ? (
|
||||||
|
<p className={`alert ${ExportAlert.type}`}>{ExportAlert.message}</p>
|
||||||
|
) : null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div styleName='root'>
|
||||||
|
<div styleName='group'>
|
||||||
|
<div styleName='group-header'>{i18n.__('Export')}</div>
|
||||||
|
|
||||||
|
<div styleName='group-section'>
|
||||||
|
<div styleName='group-section-label'>{i18n.__('Metadata')}</div>
|
||||||
|
<div styleName='group-section-control'>
|
||||||
|
<select
|
||||||
|
value={config.export.metadata}
|
||||||
|
onChange={e => this.handleExportChange(e)}
|
||||||
|
ref='metadata'
|
||||||
|
>
|
||||||
|
<option value='DONT_EXPORT'>{i18n.__(`Don't export`)}</option>
|
||||||
|
<option value='MERGE_HEADER'>
|
||||||
|
{i18n.__('Merge with the header')}
|
||||||
|
</option>
|
||||||
|
<option value='MERGE_VARIABLE'>
|
||||||
|
{i18n.__('Merge with a variable')}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{config.export.metadata === 'MERGE_VARIABLE' && (
|
||||||
|
<div styleName='group-section'>
|
||||||
|
<div styleName='group-section-label'>
|
||||||
|
{i18n.__('Variable Name')}
|
||||||
|
</div>
|
||||||
|
<div styleName='group-section-control'>
|
||||||
|
<input
|
||||||
|
styleName='group-section-control-input'
|
||||||
|
onChange={e => this.handleExportChange(e)}
|
||||||
|
ref='variable'
|
||||||
|
value={config.export.variable}
|
||||||
|
type='text'
|
||||||
|
/>
|
||||||
|
</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'>
|
||||||
|
<button
|
||||||
|
styleName='group-control-rightButton'
|
||||||
|
onClick={e => this.handleSaveButtonClick(e)}
|
||||||
|
>
|
||||||
|
{i18n.__('Save')}
|
||||||
|
</button>
|
||||||
|
{ExportAlertElement}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ExportTab.propTypes = {
|
||||||
|
dispatch: PropTypes.func,
|
||||||
|
haveToSave: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(ExportTab, styles)
|
||||||
@@ -6,6 +6,7 @@ import UiTab from './UiTab'
|
|||||||
import InfoTab from './InfoTab'
|
import InfoTab from './InfoTab'
|
||||||
import Crowdfunding from './Crowdfunding'
|
import Crowdfunding from './Crowdfunding'
|
||||||
import StoragesTab from './StoragesTab'
|
import StoragesTab from './StoragesTab'
|
||||||
|
import ExportTab from './ExportTab'
|
||||||
import SnippetTab from './SnippetTab'
|
import SnippetTab from './SnippetTab'
|
||||||
import PluginsTab from './PluginsTab'
|
import PluginsTab from './PluginsTab'
|
||||||
import Blog from './Blog'
|
import Blog from './Blog'
|
||||||
@@ -24,7 +25,8 @@ class Preferences extends React.Component {
|
|||||||
currentTab: 'STORAGES',
|
currentTab: 'STORAGES',
|
||||||
UIAlert: '',
|
UIAlert: '',
|
||||||
HotkeyAlert: '',
|
HotkeyAlert: '',
|
||||||
BlogAlert: ''
|
BlogAlert: '',
|
||||||
|
ExportAlert: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +83,15 @@ class Preferences extends React.Component {
|
|||||||
haveToSave={alert => this.setState({ BlogAlert: alert })}
|
haveToSave={alert => this.setState({ BlogAlert: alert })}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
case 'EXPORT':
|
||||||
|
return (
|
||||||
|
<ExportTab
|
||||||
|
dispatch={dispatch}
|
||||||
|
config={config}
|
||||||
|
data={data}
|
||||||
|
haveToSave={alert => this.setState({ ExportAlert: alert })}
|
||||||
|
/>
|
||||||
|
)
|
||||||
case 'SNIPPET':
|
case 'SNIPPET':
|
||||||
return <SnippetTab dispatch={dispatch} config={config} data={data} />
|
return <SnippetTab dispatch={dispatch} config={config} data={data} />
|
||||||
case 'PLUGINS':
|
case 'PLUGINS':
|
||||||
@@ -131,6 +142,11 @@ class Preferences extends React.Component {
|
|||||||
{ target: 'INFO', label: i18n.__('About') },
|
{ target: 'INFO', label: i18n.__('About') },
|
||||||
{ target: 'CROWDFUNDING', label: i18n.__('Crowdfunding') },
|
{ target: 'CROWDFUNDING', label: i18n.__('Crowdfunding') },
|
||||||
{ target: 'BLOG', label: i18n.__('Blog'), Blog: this.state.BlogAlert },
|
{ target: 'BLOG', label: i18n.__('Blog'), Blog: this.state.BlogAlert },
|
||||||
|
{
|
||||||
|
target: 'EXPORT',
|
||||||
|
label: i18n.__('Export'),
|
||||||
|
Export: this.state.ExportAlert
|
||||||
|
},
|
||||||
{ target: 'SNIPPET', label: i18n.__('Snippets') },
|
{ target: 'SNIPPET', label: i18n.__('Snippets') },
|
||||||
{ target: 'PLUGINS', label: i18n.__('Plugins') }
|
{ target: 'PLUGINS', label: i18n.__('Plugins') }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -202,7 +202,6 @@
|
|||||||
"Create new folder": "Ordner erstellen",
|
"Create new folder": "Ordner erstellen",
|
||||||
"Folder name": "Ordnername",
|
"Folder name": "Ordnername",
|
||||||
"Create": "Erstellen",
|
"Create": "Erstellen",
|
||||||
"Untitled": "Neuer Ordner",
|
|
||||||
"Unlink Storage": "Speicherverknüpfung aufheben",
|
"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.",
|
"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",
|
"Empty note": "Leere Notiz",
|
||||||
|
|||||||
@@ -702,14 +702,15 @@ it('should remove the all ":storage" and noteKey references', function() {
|
|||||||
' </p>\n' +
|
' </p>\n' +
|
||||||
' </body>\n' +
|
' </body>\n' +
|
||||||
'</html>'
|
'</html>'
|
||||||
const actual = systemUnderTest.removeStorageAndNoteReferences(
|
const actual = systemUnderTest.replaceStorageReferences(
|
||||||
testInput,
|
testInput,
|
||||||
noteKey
|
noteKey,
|
||||||
|
systemUnderTest.DESTINATION_FOLDER
|
||||||
)
|
)
|
||||||
expect(actual).toEqual(expectedOutput)
|
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 noteKey = 'noteKey'
|
||||||
const testInput =
|
const testInput =
|
||||||
'Test input' +
|
'Test input' +
|
||||||
@@ -736,9 +737,113 @@ it('should make sure that "removeStorageAndNoteReferences" works with markdown c
|
|||||||
systemUnderTest.DESTINATION_FOLDER +
|
systemUnderTest.DESTINATION_FOLDER +
|
||||||
path.posix.sep +
|
path.posix.sep +
|
||||||
'pdf.pdf)'
|
'pdf.pdf)'
|
||||||
const actual = systemUnderTest.removeStorageAndNoteReferences(
|
const actual = systemUnderTest.replaceStorageReferences(
|
||||||
testInput,
|
testInput,
|
||||||
noteKey
|
noteKey,
|
||||||
|
systemUnderTest.DESTINATION_FOLDER
|
||||||
|
)
|
||||||
|
expect(actual).toEqual(expectedOutput)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should replace the all ":storage" references', function() {
|
||||||
|
const storageFolder = systemUnderTest.DESTINATION_FOLDER
|
||||||
|
const noteKey = 'noteKey'
|
||||||
|
const testInput =
|
||||||
|
'<html>\n' +
|
||||||
|
' <head>\n' +
|
||||||
|
' //header\n' +
|
||||||
|
' </head>\n' +
|
||||||
|
' <body data-theme="default">\n' +
|
||||||
|
' <h2 data-line="0" id="Headline">Headline</h2>\n' +
|
||||||
|
' <p data-line="2">\n' +
|
||||||
|
' <img src=":storage' +
|
||||||
|
mdurl.encode(path.sep) +
|
||||||
|
noteKey +
|
||||||
|
mdurl.encode(path.sep) +
|
||||||
|
'0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
|
||||||
|
' </p>\n' +
|
||||||
|
' <p data-line="4">\n' +
|
||||||
|
' <a href=":storage' +
|
||||||
|
mdurl.encode(path.sep) +
|
||||||
|
noteKey +
|
||||||
|
mdurl.encode(path.sep) +
|
||||||
|
'0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
|
||||||
|
' </p>\n' +
|
||||||
|
' <p data-line="6">\n' +
|
||||||
|
' <img src=":storage' +
|
||||||
|
mdurl.encode(path.sep) +
|
||||||
|
noteKey +
|
||||||
|
mdurl.encode(path.sep) +
|
||||||
|
'd6c5ee92.jpg" alt="dummyImage2.jpg">\n' +
|
||||||
|
' </p>\n' +
|
||||||
|
' </body>\n' +
|
||||||
|
'</html>'
|
||||||
|
const expectedOutput =
|
||||||
|
'<html>\n' +
|
||||||
|
' <head>\n' +
|
||||||
|
' //header\n' +
|
||||||
|
' </head>\n' +
|
||||||
|
' <body data-theme="default">\n' +
|
||||||
|
' <h2 data-line="0" id="Headline">Headline</h2>\n' +
|
||||||
|
' <p data-line="2">\n' +
|
||||||
|
' <img src="' +
|
||||||
|
storageFolder +
|
||||||
|
path.sep +
|
||||||
|
'0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
|
||||||
|
' </p>\n' +
|
||||||
|
' <p data-line="4">\n' +
|
||||||
|
' <a href="' +
|
||||||
|
storageFolder +
|
||||||
|
path.sep +
|
||||||
|
'0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
|
||||||
|
' </p>\n' +
|
||||||
|
' <p data-line="6">\n' +
|
||||||
|
' <img src="' +
|
||||||
|
storageFolder +
|
||||||
|
path.sep +
|
||||||
|
'd6c5ee92.jpg" alt="dummyImage2.jpg">\n' +
|
||||||
|
' </p>\n' +
|
||||||
|
' </body>\n' +
|
||||||
|
'</html>'
|
||||||
|
const actual = systemUnderTest.replaceStorageReferences(
|
||||||
|
testInput,
|
||||||
|
noteKey,
|
||||||
|
systemUnderTest.DESTINATION_FOLDER
|
||||||
|
)
|
||||||
|
expect(actual).toEqual(expectedOutput)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should make sure that "replaceStorageReferences" works with markdown content as well', function() {
|
||||||
|
const noteKey = 'noteKey'
|
||||||
|
const testInput =
|
||||||
|
'Test input' +
|
||||||
|
' \n' +
|
||||||
|
'[pdf](' +
|
||||||
|
systemUnderTest.STORAGE_FOLDER_PLACEHOLDER +
|
||||||
|
path.posix.sep +
|
||||||
|
noteKey +
|
||||||
|
path.posix.sep +
|
||||||
|
'pdf.pdf)'
|
||||||
|
|
||||||
|
const expectedOutput =
|
||||||
|
'Test input' +
|
||||||
|
' \n' +
|
||||||
|
'[pdf](' +
|
||||||
|
systemUnderTest.DESTINATION_FOLDER +
|
||||||
|
path.posix.sep +
|
||||||
|
'pdf.pdf)'
|
||||||
|
const actual = systemUnderTest.replaceStorageReferences(
|
||||||
|
testInput,
|
||||||
|
noteKey,
|
||||||
|
systemUnderTest.DESTINATION_FOLDER
|
||||||
)
|
)
|
||||||
expect(actual).toEqual(expectedOutput)
|
expect(actual).toEqual(expectedOutput)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -52,12 +52,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')
|
||||||
|
|||||||
@@ -35,7 +35,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).then(() => {
|
|
||||||
|
const config = {
|
||||||
|
export: {
|
||||||
|
metadata: 'DONT_EXPORT',
|
||||||
|
variable: 'boostnote',
|
||||||
|
prefixAttachmentFolder: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return exportStorage(storageKey, 'md', exportDir, config).then(() => {
|
||||||
notes.forEach(note => {
|
notes.forEach(note => {
|
||||||
const noteDir = path.join(
|
const noteDir = path.join(
|
||||||
exportDir,
|
exportDir,
|
||||||
|
|||||||
@@ -98,11 +98,11 @@ exports[`Markdown.render() should renders checkboxes 1`] = `
|
|||||||
|
|
||||||
exports[`Markdown.render() should renders codeblock correctly 1`] = `
|
exports[`Markdown.render() should renders codeblock correctly 1`] = `
|
||||||
"<pre class=\\"code CodeMirror\\" data-line=\\"1\\">
|
"<pre class=\\"code CodeMirror\\" data-line=\\"1\\">
|
||||||
<span class=\\"filename\\">filename.js</span>
|
<span class=\\"filename\\">filename.js</span>
|
||||||
<span class=\\"lineNumber CodeMirror-gutters\\"><span class=\\"CodeMirror-linenumber\\">2</span></span>
|
<span class=\\"lineNumber CodeMirror-gutters\\"><span class=\\"CodeMirror-linenumber\\">2</span></span>
|
||||||
<code class=\\"js\\">var project = 'boostnote';
|
<code class=\\"js\\">var project = 'boostnote';
|
||||||
</code>
|
</code>
|
||||||
</pre>"
|
</pre>"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Markdown.render() should renders definition lists correctly 1`] = `
|
exports[`Markdown.render() should renders definition lists correctly 1`] = `
|
||||||
|
|||||||
Reference in New Issue
Block a user