1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-13 17:56:25 +00:00

add YAML front matter when exporting

This commit is contained in:
Baptiste Augrain
2018-11-15 22:48:14 +01:00
parent 168fe212f5
commit c796b3b30e
18 changed files with 639 additions and 382 deletions

View File

@@ -230,7 +230,7 @@ class MarkdownEditor extends React.Component {
}
render () {
const {className, value, config, storageKey, noteKey} = this.props
const {className, value, config, storageKey, noteKey, getNote} = this.props
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
@@ -308,6 +308,8 @@ class MarkdownEditor extends React.Component {
customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS}
lineThroughCheckbox={config.preview.lineThroughCheckbox}
getNote={getNote}
export={config.export}
/>
</div>
)

View File

@@ -16,144 +16,21 @@ import convertModeName from 'browser/lib/convertModeName'
import copy from 'copy-to-clipboard'
import mdurl from 'mdurl'
import exportNote from 'browser/main/lib/dataApi/exportNote'
import formatMarkdown from 'browser/main/lib/dataApi/formatMarkdown'
import formatHTML, { CSS_FILES, buildStyle, getCodeThemeLink, getStyleParams } from 'browser/main/lib/dataApi/formatHTML'
import { escapeHtmlCharacters } from 'browser/lib/utils'
import yaml from 'js-yaml'
import context from 'browser/lib/context'
import i18n from 'browser/lib/i18n'
import fs from 'fs'
const { remote, shell } = require('electron')
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
const { app } = remote
const path = require('path')
const fileUrl = require('file-url')
import path from 'path'
import uri2path from 'file-uri-to-path'
import { remote, shell } from 'electron'
import attachmentManagement from '../main/lib/dataApi/attachmentManagement'
import filenamify from 'filenamify'
const dialog = remote.dialog
const uri2path = require('file-uri-to-path')
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
const appPath = fileUrl(
process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()
)
const CSS_FILES = [
`${appPath}/node_modules/katex/dist/katex.min.css`,
`${appPath}/node_modules/codemirror/lib/codemirror.css`
]
function buildStyle (
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
) {
return `
@font-face {
font-family: 'Lato';
src: url('${appPath}/resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
url('${appPath}/resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
url('${appPath}/resources/fonts/Lato-Regular.ttf') format('truetype');
font-style: normal;
font-weight: normal;
text-rendering: optimizeLegibility;
}
@font-face {
font-family: 'Lato';
src: url('${appPath}/resources/fonts/Lato-Black.woff2') format('woff2'), /* Modern Browsers */
url('${appPath}/resources/fonts/Lato-Black.woff') format('woff'), /* Modern Browsers */
url('${appPath}/resources/fonts/Lato-Black.ttf') format('truetype');
font-style: normal;
font-weight: 700;
text-rendering: optimizeLegibility;
}
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff2') format('woff2'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
}
${markdownStyle}
body {
font-family: '${fontFamily.join("','")}';
font-size: ${fontSize}px;
${scrollPastEnd && 'padding-bottom: 90vh;'}
}
@media print {
body {
padding-bottom: initial;
}
}
code {
font-family: '${codeBlockFontFamily.join("','")}';
background-color: rgba(0,0,0,0.04);
}
.lineNumber {
${lineNumber && 'display: block !important;'}
font-family: '${codeBlockFontFamily.join("','")}';
}
.clipboardButton {
color: rgba(147,147,149,0.8);;
fill: rgba(147,147,149,1);;
border-radius: 50%;
margin: 0px 10px;
border: none;
background-color: transparent;
outline: none;
height: 15px;
width: 15px;
cursor: pointer;
}
.clipboardButton:hover {
transition: 0.2s;
color: #939395;
fill: #939395;
background-color: rgba(0,0,0,0.1);
}
h1, h2 {
border: none;
}
h1 {
padding-bottom: 4px;
margin: 1em 0 8px;
}
h2 {
padding-bottom: 0.2em;
margin: 1em 0 0.37em;
}
body p {
white-space: normal;
}
@media print {
body[data-theme="${theme}"] {
color: #000;
background-color: #fff;
}
.clipboardButton {
display: none
}
}
${allowCustomCSS ? customCSS : ''}
`
}
const scrollBarStyle = `
::-webkit-scrollbar {
width: 12px;
@@ -173,21 +50,6 @@ const scrollBarDarkStyle = `
}
`
const OSX = global.process.platform === 'darwin'
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
if (!OSX) {
defaultFontFamily.unshift('Microsoft YaHei')
defaultFontFamily.unshift('meiryo')
}
const defaultCodeBlockFontFamily = [
'Monaco',
'Menlo',
'Ubuntu Mono',
'Consolas',
'source-code-pro',
'monospace'
]
export default class MarkdownPreview extends React.Component {
constructor (props) {
super(props)
@@ -286,96 +148,11 @@ export default class MarkdownPreview extends React.Component {
}
handleSaveAsMd () {
this.exportAsDocument('md', (noteContent, exportTasks) => {
let result = noteContent
if (this.props && this.props.storagePath && this.props.noteKey) {
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
noteContent,
this.props.storagePath
)
attachmentsAbsolutePaths.forEach(attachment => {
exportTasks.push({
src: attachment,
dst: attachmentManagement.DESTINATION_FOLDER
})
})
result = attachmentManagement.removeStorageAndNoteReferences(
noteContent,
this.props.noteKey
)
}
return result
})
this.exportAsDocument('md', formatMarkdown(this.props))
}
handleSaveAsHtml () {
this.exportAsDocument('html', (noteContent, exportTasks) => {
const {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
} = this.getStyleParams()
const inlineStyles = buildStyle(
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
)
let body = this.markdown.render(noteContent)
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
noteContent,
this.props.storagePath
)
files.forEach(file => {
if (global.process.platform === 'win32') {
file = file.replace('file:///', '')
} else {
file = file.replace('file://', '')
}
exportTasks.push({
src: file,
dst: 'css'
})
})
attachmentsAbsolutePaths.forEach(attachment => {
exportTasks.push({
src: attachment,
dst: attachmentManagement.DESTINATION_FOLDER
})
})
body = attachmentManagement.removeStorageAndNoteReferences(
body,
this.props.noteKey
)
let styles = ''
files.forEach(file => {
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">`
})
return `<html>
<head>
<meta charset="UTF-8">
<meta name = "viewport" content = "width = device-width, initial-scale = 1, maximum-scale = 1">
<style id="style">${inlineStyles}</style>
${styles}
</head>
<body>${body}</body>
</html>`
})
this.exportAsDocument('html', formatHTML(this.props))
}
handlePrint () {
@@ -383,17 +160,21 @@ export default class MarkdownPreview extends React.Component {
}
exportAsDocument (fileType, contentFormatter) {
const note = this.props.getNote()
const options = {
defaultPath: filenamify(note.title, {
replacement: '_'
}),
filters: [{ name: 'Documents', extensions: [fileType] }],
properties: ['openFile', 'createDirectory']
}
dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
if (filename) {
const content = this.props.value
const storage = this.props.storagePath
const storagePath = this.props.storagePath
exportNote(storage, content, filename, contentFormatter)
exportNote(storagePath, note, filename, contentFormatter)
.then(res => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
@@ -555,44 +336,6 @@ export default class MarkdownPreview extends React.Component {
}
}
getStyleParams () {
const {
fontSize,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
} = this.props
let { fontFamily, codeBlockFontFamily } = this.props
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
? fontFamily
.split(',')
.map(fontName => fontName.trim())
.concat(defaultFontFamily)
: defaultFontFamily
codeBlockFontFamily = _.isString(codeBlockFontFamily) &&
codeBlockFontFamily.trim().length > 0
? codeBlockFontFamily
.split(',')
.map(fontName => fontName.trim())
.concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily
return {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
}
}
applyStyle () {
const {
fontFamily,
@@ -604,11 +347,10 @@ export default class MarkdownPreview extends React.Component {
theme,
allowCustomCSS,
customCSS
} = this.getStyleParams()
} = getStyleParams(this.props)
this.getWindow().document.getElementById('codeTheme').href = getCodeThemeLink(codeBlockTheme)
this.getWindow().document.getElementById(
'codeTheme'
).href = this.GetCodeThemeLink(codeBlockTheme)
this.getWindow().document.getElementById('style').innerHTML = buildStyle(
fontFamily,
fontSize,
@@ -621,16 +363,6 @@ export default class MarkdownPreview extends React.Component {
)
}
GetCodeThemeLink (theme) {
theme = consts.THEMES.some(_theme => _theme === theme) &&
theme !== 'default'
? theme
: 'elegant'
return theme.startsWith('solarized')
? `${appPath}/node_modules/codemirror/theme/solarized.css`
: `${appPath}/node_modules/codemirror/theme/${theme}.css`
}
rewriteIframe () {
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll(

View File

@@ -134,7 +134,7 @@ class MarkdownSplitEditor extends React.Component {
}
render () {
const {config, value, storageKey, noteKey} = this.props
const {config, value, storageKey, noteKey, getTitle} = this.props
const storage = findStorage(storageKey)
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
@@ -199,6 +199,8 @@ class MarkdownSplitEditor extends React.Component {
customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS}
lineThroughCheckbox={config.preview.lineThroughCheckbox}
getTitle={getTitle}
export={config.export}
/>
</div>
)

View File

@@ -49,6 +49,8 @@ class MarkdownNoteDetail extends React.Component {
this.toggleLockButton = this.handleToggleLockButton.bind(this)
this.generateToc = () => this.handleGenerateToc()
this.getNote = this.getNote.bind(this)
}
focus () {
@@ -319,6 +321,10 @@ class MarkdownNoteDetail extends React.Component {
this.updateNote(note)
}
getNote () {
return this.state.note
}
renderEditor () {
const { config, ignorePreviewPointerEvents } = this.props
const { note } = this.state
@@ -333,6 +339,7 @@ class MarkdownNoteDetail extends React.Component {
noteKey={note.key}
onChange={this.handleUpdateContent.bind(this)}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
getNote={this.getNote}
/>
} else {
return <MarkdownSplitEditor
@@ -343,6 +350,7 @@ class MarkdownNoteDetail extends React.Component {
noteKey={note.key}
onChange={this.handleUpdateContent.bind(this)}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
getNote={this.getNote}
/>
}
}

View File

@@ -42,12 +42,16 @@ class StorageItem extends React.Component {
label: i18n.__('Export Storage'),
submenu: [
{
label: i18n.__('Export as txt'),
label: i18n.__('Export as Plain Text (.txt)'),
click: (e) => this.handleExportStorageClick(e, 'txt')
},
{
label: i18n.__('Export as md'),
label: i18n.__('Export as Markdown (.md)'),
click: (e) => this.handleExportStorageClick(e, 'md')
},
{
label: i18n.__('Export as HTML (.html)'),
click: (e) => this.handleExportStorageClick(e, 'html')
}
]
},
@@ -94,16 +98,28 @@ class StorageItem extends React.Component {
dialog.showOpenDialog(remote.getCurrentWindow(), options,
(paths) => {
if (paths && paths.length === 1) {
const { storage, dispatch } = this.props
const { storage, dispatch, config } = this.props
dataApi
.exportStorage(storage.key, fileType, paths[0])
.exportStorage(storage.key, fileType, paths[0], config)
.then(data => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
message: `Exported to ${paths[0]}`
})
dispatch({
type: 'EXPORT_STORAGE',
storage: data.storage,
fileType: data.fileType
})
})
.catch(error => {
dialog.showErrorBox(
'Export error',
error ? error.message || error : 'Unexpected error during export'
)
throw error
})
}
})
}
@@ -157,12 +173,16 @@ class StorageItem extends React.Component {
label: i18n.__('Export Folder'),
submenu: [
{
label: i18n.__('Export as txt'),
label: i18n.__('Export as Plain Text (.txt)'),
click: (e) => this.handleExportFolderClick(e, folder, 'txt')
},
{
label: i18n.__('Export as md'),
label: i18n.__('Export as Markdown (.md)'),
click: (e) => this.handleExportFolderClick(e, folder, 'md')
},
{
label: i18n.__('Export as HTML (.html)'),
click: (e) => this.handleExportFolderClick(e, folder, 'html')
}
]
},
@@ -194,10 +214,15 @@ class StorageItem extends React.Component {
dialog.showOpenDialog(remote.getCurrentWindow(), options,
(paths) => {
if (paths && paths.length === 1) {
const { storage, dispatch } = this.props
const { storage, dispatch, config } = this.props
dataApi
.exportFolder(storage.key, folder.key, fileType, paths[0])
.then((data) => {
.exportFolder(storage.key, folder.key, fileType, paths[0], config)
.then(data => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
message: `Exported to ${paths[0]}`
})
dispatch({
type: 'EXPORT_FOLDER',
storage: data.storage,
@@ -205,6 +230,13 @@ class StorageItem extends React.Component {
fileType: data.fileType
})
})
.catch(error => {
dialog.showErrorBox(
'Export error',
error ? error.message || error : 'Unexpected error during export'
)
throw error
})
}
})
}
@@ -274,7 +306,7 @@ class StorageItem extends React.Component {
const { folderNoteMap, trashedSet } = data
const SortableStorageItemChild = SortableElement(StorageItemChild)
const folderList = storage.folders.map((folder, index) => {
let folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
const folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
const isActive = !!(location.pathname.match(folderRegex))
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)

View File

@@ -347,6 +347,7 @@ class SideNav extends React.Component {
dispatch={dispatch}
onSortEnd={this.onSortEnd.bind(this)(storage)}
useDragHandle
config={config}
/>
})
const style = {}

View File

@@ -82,8 +82,9 @@ export const DEFAULT_CONFIG = {
password: ''
},
export: {
action: 'DONT_EXPORT', // 'DONT_EXPORT', 'MERGE_HEADER', 'MERGE_VARIABLE'
variable: 'metadata'
metadata: 'DONT_EXPORT', // 'DONT_EXPORT', 'MERGE_HEADER', 'MERGE_VARIABLE'
variable: 'boostnote',
prefixAttachmentFolder: false
}
}

View File

@@ -386,6 +386,17 @@ function removeStorageAndNoteReferences (input, noteKey) {
return input.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + noteKey + ')?', 'g'), DESTINATION_FOLDER)
}
/**
* @description replace all :storage references with given destination folder.
* @param input Input in which the references should be deleted
* @param noteKey Key of the current note
* @param destinationFolder Destination folder of the attachements
* @returns {String} Input without the references
*/
function replaceStorageReferences (input, noteKey, destinationFolder) {
return input.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + noteKey + ')?', 'g'), destinationFolder || DESTINATION_FOLDER)
}
/**
* @description Deletes the attachment folder specified by the given storageKey and noteKey
* @param storageKey Key of the storage of the note to be deleted
@@ -542,6 +553,7 @@ module.exports = {
getAttachmentsInMarkdownContent,
getAbsolutePathsOfAttachmentsInContent,
removeStorageAndNoteReferences,
replaceStorageReferences,
deleteAttachmentFolder,
deleteAttachmentsNotPresentInNote,
moveAttachments,

View File

@@ -2,14 +2,17 @@ import { findStorage } from 'browser/lib/findStorage'
import resolveStorageData from './resolveStorageData'
import resolveStorageNotes from './resolveStorageNotes'
import filenamify from 'filenamify'
import * as path from 'path'
import * as fs from 'fs'
import path from 'path'
import exportNote from './exportNote'
import formatMarkdown from './formatMarkdown'
import formatHTML from './formatHTML'
/**
* @param {String} storageKey
* @param {String} folderKey
* @param {String} fileType
* @param {String} exportDir
* @param {Object} config
*
* @return {Object}
* ```
@@ -22,7 +25,7 @@ import * as fs from 'fs'
* ```
*/
function exportFolder (storageKey, folderKey, fileType, exportDir) {
function exportFolder (storageKey, folderKey, fileType, exportDir, config) {
let targetStorage
try {
targetStorage = findStorage(storageKey)
@@ -31,31 +34,50 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) {
}
return resolveStorageData(targetStorage)
.then(function assignNotes (storage) {
return resolveStorageNotes(storage)
.then((notes) => {
return {
storage,
notes
}
})
})
.then(function exportNotes (data) {
const { storage, notes } = data
notes
.filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
.forEach(snippet => {
const notePath = path.join(exportDir, `${filenamify(snippet.title, {replacement: '_'})}.${fileType}`)
fs.writeFileSync(notePath, snippet.content)
})
return {
.then(storage => {
return resolveStorageNotes(storage).then(notes => ({
storage,
folderKey,
fileType,
exportDir
notes: notes.filter(note => note.folder === folderKey && !note.isTrashed && note.type === 'MARKDOWN_NOTE')
}))
})
.then(({ storage, notes }) => {
let contentFormatter = null
if (fileType === 'md') {
contentFormatter = formatMarkdown({
storagePath: storage.path,
export: config.export
})
} else if (fileType === 'html') {
contentFormatter = formatHTML({
theme: config.ui.theme,
fontSize: config.preview.fontSize,
fontFamily: config.preview.fontFamily,
codeBlockTheme: config.preview.codeBlockTheme,
codeBlockFontFamily: config.editor.fontFamily,
lineNumber: config.preview.lineNumber,
scrollPastEnd: config.preview.scrollPastEnd,
smartQuotes: config.preview.smartQuotes,
breaks: config.preview.breaks,
sanitize: config.preview.sanitize,
customCSS: config.preview.customCSS,
allowCustomCSS: config.preview.allowCustomCSS,
storagePath: storage.path,
export: config.export
})
}
return Promise
.all(notes.map(note => {
const targetPath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`)
return exportNote(storage.key, note, targetPath, contentFormatter)
}))
.then(() => ({
storage,
folderKey,
fileType,
exportDir
}))
})
}

View File

@@ -16,7 +16,7 @@ const path = require('path')
* @param {function} outputFormatter
* @return {Promise.<*[]>}
*/
function exportNote (storageKey, noteContent, targetPath, outputFormatter) {
function exportNote (storageKey, note, targetPath, outputFormatter) {
const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path
const exportTasks = []
@@ -24,20 +24,18 @@ function exportNote (storageKey, noteContent, targetPath, outputFormatter) {
throw new Error('Storage path is not found')
}
let exportedData = noteContent
if (outputFormatter) {
exportedData = outputFormatter(exportedData, exportTasks)
}
const exportedData = outputFormatter ? outputFormatter(note, targetPath, exportTasks) : note.content
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
return Promise.all(tasks.map((task) => copyFile(task.src, task.dst)))
return Promise
.all(tasks.map(task => copyFile(task.src, task.dst)))
.then(() => {
return saveToFile(exportedData, targetPath)
}).catch((err) => {
})
.catch(error => {
rollbackExport(tasks)
throw err
throw error
})
}
@@ -57,10 +55,12 @@ function prepareTasks (tasks, storagePath, targetPath) {
function saveToFile (data, filename) {
return new Promise((resolve, reject) => {
fs.writeFile(filename, data, (err) => {
if (err) return reject(err)
resolve(filename)
fs.writeFile(filename, data, error => {
if (error) {
reject(error)
} else {
resolve(filename)
}
})
})
}

View File

@@ -2,13 +2,17 @@ import { findStorage } from 'browser/lib/findStorage'
import resolveStorageData from './resolveStorageData'
import resolveStorageNotes from './resolveStorageNotes'
import filenamify from 'filenamify'
import * as path from 'path'
import * as fs from 'fs'
import path from 'path'
import fs from 'fs'
import exportNote from './exportNote'
import formatMarkdown from './formatMarkdown'
import formatHTML from './formatHTML'
/**
* @param {String} storageKey
* @param {String} fileType
* @param {String} exportDir
* @param {Object} config
*
* @return {Object}
* ```
@@ -20,7 +24,7 @@ import * as fs from 'fs'
* ```
*/
function exportStorage (storageKey, fileType, exportDir) {
function exportStorage (storageKey, fileType, exportDir, config) {
let targetStorage
try {
targetStorage = findStorage(storageKey)
@@ -29,34 +33,61 @@ function exportStorage (storageKey, fileType, exportDir) {
}
return resolveStorageData(targetStorage)
.then(storage => (
resolveStorageNotes(storage).then(notes => ({storage, notes}))
))
.then(function exportNotes (data) {
const { storage, notes } = data
.then(storage => {
return resolveStorageNotes(storage).then(notes => ({
storage,
notes: notes.filter(note => !note.isTrashed && note.type === 'MARKDOWN_NOTE')
}))
})
.then(({ storage, notes }) => {
let contentFormatter = null
if (fileType === 'md') {
contentFormatter = formatMarkdown({
storagePath: storage.path,
export: config.export
})
} else if (fileType === 'html') {
contentFormatter = formatHTML({
theme: config.ui.theme,
fontSize: config.preview.fontSize,
fontFamily: config.preview.fontFamily,
codeBlockTheme: config.preview.codeBlockTheme,
codeBlockFontFamily: config.editor.fontFamily,
lineNumber: config.preview.lineNumber,
scrollPastEnd: config.preview.scrollPastEnd,
smartQuotes: config.preview.smartQuotes,
breaks: config.preview.breaks,
sanitize: config.preview.sanitize,
customCSS: config.preview.customCSS,
allowCustomCSS: config.preview.allowCustomCSS,
storagePath: storage.path,
export: config.export
})
}
const folderNamesMapping = {}
storage.folders.forEach(folder => {
const folderExportedDir = path.join(exportDir, filenamify(folder.name, {replacement: '_'}))
folderNamesMapping[folder.key] = folderExportedDir
// make sure directory exists
try {
fs.mkdirSync(folderExportedDir)
} catch (e) {}
})
notes
.filter(note => !note.isTrashed && note.type === 'MARKDOWN_NOTE')
.forEach(markdownNote => {
const folderExportedDir = folderNamesMapping[markdownNote.folder]
const snippetName = `${filenamify(markdownNote.title, {replacement: '_'})}.${fileType}`
const notePath = path.join(folderExportedDir, snippetName)
fs.writeFileSync(notePath, markdownNote.content)
})
return {
storage,
fileType,
exportDir
}
return Promise
.all(notes.map(note => {
const targetPath = path.join(folderNamesMapping[note.folder], `${filenamify(note.title, {replacement: '_'})}.${fileType}`)
return exportNote(storage.key, note, targetPath, contentFormatter)
}))
.then(() => ({
storage,
fileType,
exportDir
}))
})
}

View File

@@ -0,0 +1,295 @@
import path from 'path'
import fileUrl from 'file-url'
import { remote } from 'electron'
import consts from 'browser/lib/consts'
import Markdown from 'browser/lib/markdown'
import attachmentManagement from './attachmentManagement'
const { app } = remote
const appPath = fileUrl(process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve())
let markdownStyle = ''
try {
markdownStyle = require('!!css!stylus?sourceMap!../../../components/markdown.styl')[0][1]
} catch (e) {}
export const CSS_FILES = [
`${appPath}/node_modules/katex/dist/katex.min.css`,
`${appPath}/node_modules/codemirror/lib/codemirror.css`
]
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
if (global.process.platform !== 'darwin') {
defaultFontFamily.unshift('Microsoft YaHei')
defaultFontFamily.unshift('meiryo')
}
const defaultCodeBlockFontFamily = [
'Monaco',
'Menlo',
'Ubuntu Mono',
'Consolas',
'source-code-pro',
'monospace'
]
/**
* ```
* {
* fontFamily,
* fontSize,
* lineNumber,
* codeBlockFontFamily,
* codeBlockTheme,
* scrollPastEnd,
* theme,
* allowCustomCSS,
* customCSS
* smartQuotes,
* sanitize,
* breaks,
* storagePath,
* export
* }
* ```
*/
export default function formatHTML (props) {
const {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
} = getStyleParams(props)
const inlineStyles = buildStyle(
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
)
const { smartQuotes, sanitize, breaks } = props
const markdown = new Markdown({
typographer: smartQuotes,
sanitize,
breaks
})
const files = [getCodeThemeLink(codeBlockTheme), ...CSS_FILES]
return function (note, targetPath, exportTasks) {
let body = markdown.render(note.content)
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(note.content, props.storagePath)
files.forEach(file => {
if (global.process.platform === 'win32') {
file = file.replace('file:///', '')
} else {
file = file.replace('file://', '')
}
exportTasks.push({
src: file,
dst: 'css'
})
})
const destinationFolder = props.export.prefixAttachmentFolder ? `${path.parse(targetPath).name} - ${attachmentManagement.DESTINATION_FOLDER}` : attachmentManagement.DESTINATION_FOLDER
attachmentsAbsolutePaths.forEach(attachment => {
exportTasks.push({
src: attachment,
dst: destinationFolder
})
})
body = attachmentManagement.replaceStorageReferences(body, note.key, destinationFolder)
let styles = ''
files.forEach(file => {
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">`
})
return `<html>
<head>
<meta charset="UTF-8">
<meta name = "viewport" content = "width = device-width, initial-scale = 1, maximum-scale = 1">
<style id="style">${inlineStyles}</style>
${styles}
</head>
<body>${body}</body>
</html>`
}
}
export function getStyleParams (props) {
const {
fontSize,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
} = props
let { fontFamily, codeBlockFontFamily } = props
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
? fontFamily
.split(',')
.map(fontName => fontName.trim())
.concat(defaultFontFamily)
: defaultFontFamily
codeBlockFontFamily = _.isString(codeBlockFontFamily) &&
codeBlockFontFamily.trim().length > 0
? codeBlockFontFamily
.split(',')
.map(fontName => fontName.trim())
.concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily
return {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
}
}
export function getCodeThemeLink (theme) {
if (consts.THEMES.some(_theme => _theme === theme)) {
theme = theme !== 'default' ? theme : 'elegant'
}
return theme.startsWith('solarized')
? `${appPath}/node_modules/codemirror/theme/solarized.css`
: `${appPath}/node_modules/codemirror/theme/${theme}.css`
}
export function buildStyle (
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
) {
return `
@font-face {
font-family: 'Lato';
src: url('${appPath}/resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
url('${appPath}/resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
url('${appPath}/resources/fonts/Lato-Regular.ttf') format('truetype');
font-style: normal;
font-weight: normal;
text-rendering: optimizeLegibility;
}
@font-face {
font-family: 'Lato';
src: url('${appPath}/resources/fonts/Lato-Black.woff2') format('woff2'), /* Modern Browsers */
url('${appPath}/resources/fonts/Lato-Black.woff') format('woff'), /* Modern Browsers */
url('${appPath}/resources/fonts/Lato-Black.ttf') format('truetype');
font-style: normal;
font-weight: 700;
text-rendering: optimizeLegibility;
}
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff2') format('woff2'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
}
${markdownStyle}
body {
font-family: '${fontFamily.join("','")}';
font-size: ${fontSize}px;
${scrollPastEnd && 'padding-bottom: 90vh;'}
}
@media print {
body {
padding-bottom: initial;
}
}
code {
font-family: '${codeBlockFontFamily.join("','")}';
background-color: rgba(0,0,0,0.04);
}
.lineNumber {
${lineNumber && 'display: block !important;'}
font-family: '${codeBlockFontFamily.join("','")}';
}
.clipboardButton {
color: rgba(147,147,149,0.8);;
fill: rgba(147,147,149,1);;
border-radius: 50%;
margin: 0px 10px;
border: none;
background-color: transparent;
outline: none;
height: 15px;
width: 15px;
cursor: pointer;
}
.clipboardButton:hover {
transition: 0.2s;
color: #939395;
fill: #939395;
background-color: rgba(0,0,0,0.1);
}
h1, h2 {
border: none;
}
h1 {
padding-bottom: 4px;
margin: 1em 0 8px;
}
h2 {
padding-bottom: 0.2em;
margin: 1em 0 0.37em;
}
body p {
white-space: normal;
}
@media print {
body[data-theme="${theme}"] {
color: #000;
background-color: #fff;
}
.clipboardButton {
display: none
}
}
${allowCustomCSS ? customCSS : ''}
`
}

View File

@@ -0,0 +1,94 @@
import attachmentManagement from './attachmentManagement'
import yaml from 'js-yaml'
import path from 'path'
const delimiterRegExp = /^\-{3}/
/**
* ```
* {
* storagePath,
* export
* }
* ```
*/
export default function formatMarkdown (props) {
return function (note, targetPath, exportTasks) {
let result = note.content
if (props.storagePath && note.key) {
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(result, props.storagePath)
const destinationFolder = props.export.prefixAttachmentFolder ? `${path.parse(targetPath).name} - ${attachmentManagement.DESTINATION_FOLDER}` : attachmentManagement.DESTINATION_FOLDER
attachmentsAbsolutePaths.forEach(attachment => {
exportTasks.push({
src: attachment,
dst: destinationFolder
})
})
result = attachmentManagement.replaceStorageReferences(result, note.key, destinationFolder)
}
if (props.export.metadata === 'MERGE_HEADER') {
const metadata = getFrontMatter(result)
const values = Object.assign({}, note)
delete values.content
delete values.isTrashed
for (const key in values) {
metadata[key] = values[key]
}
result = replaceFrontMatter(result, metadata)
} else if (props.export.metadata === 'MERGE_VARIABLE') {
const metadata = getFrontMatter(result)
const values = Object.assign({}, note)
delete values.content
delete values.isTrashed
if (props.export.variable) {
metadata[props.export.variable] = values
} else {
for (const key in values) {
metadata[key] = values[key]
}
}
result = replaceFrontMatter(result, metadata)
}
return result
}
}
function getFrontMatter (markdown) {
const lines = markdown.split('\n')
if (delimiterRegExp.test(lines[0])) {
let line = 0
while (++line < lines.length && delimiterRegExp.test(lines[line])) {
}
return yaml.load(lines.slice(1, line).join('\n')) || {}
} else {
return {}
}
}
function replaceFrontMatter (markdown, metadata) {
const lines = markdown.split('\n')
if (lines[0] === '---') {
let line = 0
while (++line < lines.length && lines[line] !== '---') {
}
return `---\n${yaml.dump(metadata)}---\n${lines.slice(line + 1).join('\n')}`
} else {
return `---\n${yaml.dump(metadata)}---\n\n${markdown}`
}
}

View File

@@ -1,7 +1,6 @@
const resolveStorageData = require('./resolveStorageData')
const _ = require('lodash')
const path = require('path')
const fs = require('fs')
const CSON = require('@rokt33r/season')
const keygen = require('browser/lib/keygen')
const sander = require('sander')

View File

@@ -30,21 +30,19 @@ class ExportTab extends React.Component {
componentDidMount () {
this.handleSettingDone = () => {
this.setState({
ExportAlert: {
type: 'success',
message: i18n.__('Successfully applied!')
}
ExportAlert: {
type: 'success',
message: i18n.__('Successfully applied!')
}
)
})
}
this.handleSettingError = (err) => {
this.setState({
ExportAlert: {
type: 'error',
message: err.message != null ? err.message : i18n.__('An error occurred!')
}
ExportAlert: {
type: 'error',
message: err.message != null ? err.message : i18n.__('An error occurred!')
}
)
})
}
this.oldExport = this.state.config.export
@@ -78,8 +76,9 @@ class ExportTab extends React.Component {
const { config } = this.state
config.export = {
action: this.refs.action.value,
variable: !_.isNil(this.refs.variable) ? this.refs.variable.value : config.export.variable
metadata: this.refs.metadata.value,
variable: !_.isNil(this.refs.variable) ? this.refs.variable.value : config.export.variable,
prefixAttachmentFolder: this.refs.prefixAttachmentFolder.checked
}
this.setState({
@@ -99,7 +98,6 @@ class ExportTab extends React.Component {
render () {
const { config, ExportAlert } = this.state
console.log(config.export)
const ExportAlertElement = ExportAlert != null
? <p className={`alert ${ExportAlert.type}`}>
@@ -114,12 +112,12 @@ class ExportTab extends React.Component {
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Action')}
{i18n.__('Metadata')}
</div>
<div styleName='group-section-control'>
<select value={config.export.action}
<select value={config.export.metadata}
onChange={(e) => this.handleExportChange(e)}
ref='action'
ref='metadata'
>
<option value='DONT_EXPORT'>{i18n.__(`Don't export`)}</option>
<option value='MERGE_HEADER'>{i18n.__('Merge with the header')}</option>
@@ -128,7 +126,7 @@ class ExportTab extends React.Component {
</div>
</div>
{ config.export.action === 'MERGE_VARIABLE' &&
{ config.export.metadata === 'MERGE_VARIABLE' &&
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Variable Name')}</div>
<div styleName='group-section-control'>
@@ -141,6 +139,17 @@ class ExportTab extends React.Component {
</div>
}
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleExportChange(e)}
checked={config.export.prefixAttachmentFolder}
ref='prefixAttachmentFolder'
type='checkbox'
/>&nbsp;
{i18n.__('Prefix attachment folder')}
</label>
</div>
<div styleName='group-control'>
<button styleName='group-control-rightButton'
onClick={(e) => this.handleSaveButtonClick(e)}>{i18n.__('Save')}

View File

@@ -89,7 +89,7 @@ class Preferences extends React.Component {
haveToSave={alert => this.setState({BlogAlert: alert})}
/>
)
case 'EXPORT':
case 'EXPORT':
return (
<ExportTab
dispatch={dispatch}

View File

@@ -47,12 +47,20 @@ test.serial('Export a folder', (t) => {
}
input2.title = 'input2'
const config = {
export: {
metadata: 'DONT_EXPORT',
variable: 'boostnote',
prefixAttachmentFolder: false
}
}
return createNote(storageKey, input1)
.then(function () {
return createNote(storageKey, input2)
})
.then(function () {
return exportFolder(storageKey, folderKey, 'md', storagePath)
return exportFolder(storageKey, folderKey, 'md', storagePath, config)
})
.then(function assert () {
let filePath = path.join(storagePath, 'input1.md')

View File

@@ -31,7 +31,16 @@ test.serial('Export a storage', t => {
acc[folder.key] = folder.name
return acc
}, {})
return exportStorage(storageKey, 'md', exportDir)
const config = {
export: {
metadata: 'DONT_EXPORT',
variable: 'boostnote',
prefixAttachmentFolder: false
}
}
return exportStorage(storageKey, 'md', exportDir, config)
.then(() => {
notes.forEach(note => {
const noteDir = path.join(exportDir, folderKeyToName[note.folder], `${note.title}.md`)