mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-14 18:26:26 +00:00
Compare commits
1 Commits
master
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6315d1c38 |
@@ -63,7 +63,7 @@ export default class CodeEditor extends React.Component {
|
||||
this.focusHandler = () => {
|
||||
ipcRenderer.send('editor:focused', true)
|
||||
}
|
||||
this.debouncedDeletionOfAttachments = _.debounce(
|
||||
const debouncedDeletionOfAttachments = _.debounce(
|
||||
attachmentManagement.deleteAttachmentsNotPresentInNote,
|
||||
30000
|
||||
)
|
||||
@@ -80,7 +80,7 @@ export default class CodeEditor extends React.Component {
|
||||
this.props.onBlur != null && this.props.onBlur(e)
|
||||
const { storageKey, noteKey } = this.props
|
||||
if (this.props.deleteUnusedAttachments === true) {
|
||||
this.debouncedDeletionOfAttachments(
|
||||
debouncedDeletionOfAttachments(
|
||||
this.editor.getValue(),
|
||||
storageKey,
|
||||
noteKey
|
||||
@@ -170,10 +170,6 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
handleEditorActivity() {
|
||||
if (this.props.onCursorActivity) {
|
||||
this.props.onCursorActivity(this.editor)
|
||||
}
|
||||
|
||||
if (!this.textEditorInterface.transaction) {
|
||||
this.updateTableEditorState()
|
||||
}
|
||||
@@ -235,19 +231,11 @@ export default class CodeEditor extends React.Component {
|
||||
},
|
||||
[translateHotkey(hotkey.insertDate)]: function(cm) {
|
||||
const dateNow = new Date()
|
||||
if (self.props.dateFormatISO8601) {
|
||||
cm.replaceSelection(dateNow.toISOString().split('T')[0])
|
||||
} else {
|
||||
cm.replaceSelection(dateNow.toLocaleDateString())
|
||||
}
|
||||
cm.replaceSelection(dateNow.toLocaleDateString())
|
||||
},
|
||||
[translateHotkey(hotkey.insertDateTime)]: function(cm) {
|
||||
const dateNow = new Date()
|
||||
if (self.props.dateFormatISO8601) {
|
||||
cm.replaceSelection(dateNow.toISOString())
|
||||
} else {
|
||||
cm.replaceSelection(dateNow.toLocaleString())
|
||||
}
|
||||
cm.replaceSelection(dateNow.toLocaleString())
|
||||
},
|
||||
Enter: 'boostNewLineAndIndentContinueMarkdownList',
|
||||
'Ctrl-C': cm => {
|
||||
@@ -345,18 +333,10 @@ export default class CodeEditor extends React.Component {
|
||||
'CodeMirror-lint-markers'
|
||||
],
|
||||
autoCloseBrackets: {
|
||||
codeBlock: {
|
||||
pairs: this.props.codeBlockMatchingPairs,
|
||||
closeBefore: this.props.codeBlockMatchingCloseBefore,
|
||||
triples: this.props.codeBlockMatchingTriples,
|
||||
explode: this.props.codeBlockExplodingPairs
|
||||
},
|
||||
markdown: {
|
||||
pairs: this.props.matchingPairs,
|
||||
closeBefore: this.props.matchingCloseBefore,
|
||||
triples: this.props.matchingTriples,
|
||||
explode: this.props.explodingPairs
|
||||
}
|
||||
pairs: this.props.matchingPairs,
|
||||
triples: this.props.matchingTriples,
|
||||
explode: this.props.explodingPairs,
|
||||
override: true
|
||||
},
|
||||
extraKeys: this.defaultKeyMap,
|
||||
prettierConfig: this.props.prettierConfig
|
||||
@@ -384,7 +364,6 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
eventEmitter.emit('code:init')
|
||||
this.editor.on('scroll', this.scrollHandler)
|
||||
this.editor.on('cursorActivity', this.editorActivityHandler)
|
||||
|
||||
const editorTheme = document.getElementById('editorTheme')
|
||||
editorTheme.addEventListener('load', this.loadStyleHandler)
|
||||
@@ -522,6 +501,7 @@ export default class CodeEditor extends React.Component {
|
||||
})
|
||||
|
||||
if (this.props.enableTableEditor) {
|
||||
this.editor.on('cursorActivity', this.editorActivityHandler)
|
||||
this.editor.on('changes', this.editorActivityHandler)
|
||||
}
|
||||
|
||||
@@ -580,18 +560,12 @@ export default class CodeEditor extends React.Component {
|
||||
this.editor.off('paste', this.pasteHandler)
|
||||
eventEmitter.off('top:search', this.searchHandler)
|
||||
this.editor.off('scroll', this.scrollHandler)
|
||||
this.editor.off('cursorActivity', this.editorActivityHandler)
|
||||
this.editor.off('contextmenu', this.contextMenuHandler)
|
||||
|
||||
const editorTheme = document.getElementById('editorTheme')
|
||||
editorTheme.removeEventListener('load', this.loadStyleHandler)
|
||||
|
||||
spellcheck.setLanguage(null, spellcheck.SPELLCHECK_DISABLED)
|
||||
eventEmitter.off('code:format-table', this.formatTable)
|
||||
|
||||
if (this.props.enableTableEditor) {
|
||||
this.editor.off('changes', this.editorActivityHandler)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
@@ -667,32 +641,16 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
if (
|
||||
prevProps.matchingPairs !== this.props.matchingPairs ||
|
||||
prevProps.matchingCloseBefore !== this.props.matchingCloseBefore ||
|
||||
prevProps.matchingTriples !== this.props.matchingTriples ||
|
||||
prevProps.explodingPairs !== this.props.explodingPairs ||
|
||||
prevProps.codeBlockMatchingPairs !== this.props.codeBlockMatchingPairs ||
|
||||
prevProps.codeBlockMatchingCloseBefore !==
|
||||
this.props.codeBlockMatchingCloseBefore ||
|
||||
prevProps.codeBlockMatchingTriples !==
|
||||
this.props.codeBlockMatchingTriples ||
|
||||
prevProps.codeBlockExplodingPairs !== this.props.codeBlockExplodingPairs
|
||||
prevProps.explodingPairs !== this.props.explodingPairs
|
||||
) {
|
||||
const autoCloseBrackets = {
|
||||
codeBlock: {
|
||||
pairs: this.props.codeBlockMatchingPairs,
|
||||
closeBefore: this.props.codeBlockMatchingCloseBefore,
|
||||
triples: this.props.codeBlockMatchingTriples,
|
||||
explode: this.props.codeBlockExplodingPairs
|
||||
},
|
||||
markdown: {
|
||||
pairs: this.props.matchingPairs,
|
||||
closeBefore: this.props.matchingCloseBefore,
|
||||
triples: this.props.matchingTriples,
|
||||
explode: this.props.explodingPairs
|
||||
}
|
||||
const bracketObject = {
|
||||
pairs: this.props.matchingPairs,
|
||||
triples: this.props.matchingTriples,
|
||||
explode: this.props.explodingPairs,
|
||||
override: true
|
||||
}
|
||||
|
||||
this.editor.setOption('autoCloseBrackets', autoCloseBrackets)
|
||||
this.editor.setOption('autoCloseBrackets', bracketObject)
|
||||
}
|
||||
|
||||
if (prevProps.enableTableEditor !== this.props.enableTableEditor) {
|
||||
@@ -810,8 +768,6 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
handleChange(editor, changeObject) {
|
||||
this.debouncedDeletionOfAttachments.cancel()
|
||||
|
||||
spellcheck.handleChange(editor, changeObject)
|
||||
|
||||
// The current note contains an toc. We'll check for changes on headlines.
|
||||
|
||||
@@ -139,7 +139,7 @@ class MarkdownEditor extends React.Component {
|
||||
},
|
||||
() => {
|
||||
this.previewRef.current.focus()
|
||||
this.previewRef.current.scrollToLine(cursorPosition.line)
|
||||
this.previewRef.current.scrollToRow(cursorPosition.line)
|
||||
}
|
||||
)
|
||||
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||
@@ -323,7 +323,6 @@ class MarkdownEditor extends React.Component {
|
||||
storageKey,
|
||||
noteKey,
|
||||
linesHighlighted,
|
||||
getNote,
|
||||
RTL
|
||||
} = this.props
|
||||
|
||||
@@ -366,15 +365,8 @@ class MarkdownEditor extends React.Component {
|
||||
displayLineNumbers={config.editor.displayLineNumbers}
|
||||
lineWrapping
|
||||
matchingPairs={config.editor.matchingPairs}
|
||||
matchingCloseBefore={config.editor.matchingCloseBefore}
|
||||
matchingTriples={config.editor.matchingTriples}
|
||||
explodingPairs={config.editor.explodingPairs}
|
||||
codeBlockMatchingPairs={config.editor.codeBlockMatchingPairs}
|
||||
codeBlockMatchingCloseBefore={
|
||||
config.editor.codeBlockMatchingCloseBefore
|
||||
}
|
||||
codeBlockMatchingTriples={config.editor.codeBlockMatchingTriples}
|
||||
codeBlockExplodingPairs={config.editor.codeBlockExplodingPairs}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
storageKey={storageKey}
|
||||
noteKey={noteKey}
|
||||
@@ -389,7 +381,6 @@ class MarkdownEditor extends React.Component {
|
||||
switchPreview={config.editor.switchPreview}
|
||||
enableMarkdownLint={config.editor.enableMarkdownLint}
|
||||
customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
|
||||
dateFormatISO8601={config.editor.dateFormatISO8601}
|
||||
prettierConfig={config.editor.prettierConfig}
|
||||
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
|
||||
RTL={RTL}
|
||||
@@ -427,8 +418,6 @@ class MarkdownEditor extends React.Component {
|
||||
customCSS={config.preview.customCSS}
|
||||
allowCustomCSS={config.preview.allowCustomCSS}
|
||||
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
||||
getNote={getNote}
|
||||
export={config.export}
|
||||
onDrop={e => this.handleDropImage(e)}
|
||||
RTL={RTL}
|
||||
/>
|
||||
|
||||
@@ -18,30 +18,258 @@ 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,
|
||||
escapeHtmlCharactersInCodeTag
|
||||
} from 'browser/main/lib/dataApi/formatHTML'
|
||||
import formatPDF from 'browser/main/lib/dataApi/formatPDF'
|
||||
import { escapeHtmlCharacters } from 'browser/lib/utils'
|
||||
import yaml from 'js-yaml'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import path from 'path'
|
||||
import { remote, shell } from 'electron'
|
||||
import attachmentManagement from '../main/lib/dataApi/attachmentManagement'
|
||||
import filenamify from 'filenamify'
|
||||
import { render } from 'react-dom'
|
||||
import Carousel from 'react-image-carousel'
|
||||
import { push } from 'connected-react-router'
|
||||
import ConfigManager from '../main/lib/ConfigManager'
|
||||
import uiThemes from 'browser/lib/ui-themes'
|
||||
import { buildMarkdownPreviewContextMenu } from 'browser/lib/contextMenuBuilder'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
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 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 = `
|
||||
::-webkit-scrollbar {
|
||||
${config.get().ui.showScrollBar ? '' : 'display: none;'}
|
||||
@@ -73,6 +301,22 @@ 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 -1 if the line is not found
|
||||
function getSourceLineNumberByElement(element) {
|
||||
@@ -186,15 +430,94 @@ class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
handleSaveAsMd() {
|
||||
this.exportAsDocument('md', formatMarkdown(this.props))
|
||||
this.exportAsDocument('md')
|
||||
}
|
||||
|
||||
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() {
|
||||
this.exportAsDocument('html', formatHTML(this.props))
|
||||
this.exportAsDocument('html', (noteContent, exportTasks, targetDir) =>
|
||||
Promise.resolve(
|
||||
this.htmlContentFormatter(noteContent, exportTasks, targetDir)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
handleSaveAsPdf() {
|
||||
this.exportAsDocument('pdf', formatPDF(this.props))
|
||||
this.exportAsDocument('pdf', (noteContent, exportTasks, targetDir) => {
|
||||
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() {
|
||||
@@ -202,21 +525,18 @@ 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 storagePath = this.props.storagePath
|
||||
const content = this.props.value
|
||||
const storage = this.props.storagePath
|
||||
const nodeKey = this.props.noteKey
|
||||
|
||||
exportNote(storagePath, note, filename, contentFormatter)
|
||||
exportNote(nodeKey, storage, content, filename, contentFormatter)
|
||||
.then(res => {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'info',
|
||||
@@ -247,6 +567,32 @@ class MarkdownPreview extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Convert special characters between three ```
|
||||
* @param {string[]} splitWithCodeTag Array of HTML strings separated by three ```
|
||||
* @returns {string} HTML in which special characters between three ``` have been converted
|
||||
*/
|
||||
escapeHtmlCharactersInCodeTag(splitWithCodeTag) {
|
||||
for (let index = 0; index < splitWithCodeTag.length; index++) {
|
||||
const codeTagRequired =
|
||||
splitWithCodeTag[index] !== '```' && index < splitWithCodeTag.length - 1
|
||||
if (codeTagRequired) {
|
||||
splitWithCodeTag.splice(index + 1, 0, '```')
|
||||
}
|
||||
}
|
||||
let inCodeTag = false
|
||||
let result = ''
|
||||
for (let content of splitWithCodeTag) {
|
||||
if (content === '```') {
|
||||
inCodeTag = !inCodeTag
|
||||
} else if (inCodeTag) {
|
||||
content = escapeHtmlCharacters(content)
|
||||
}
|
||||
result += content
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
getScrollBarStyle() {
|
||||
const { theme } = this.props
|
||||
|
||||
@@ -397,6 +743,47 @@ 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() {
|
||||
const {
|
||||
fontFamily,
|
||||
@@ -409,13 +796,12 @@ class MarkdownPreview extends React.Component {
|
||||
allowCustomCSS,
|
||||
customCSS,
|
||||
RTL
|
||||
} = getStyleParams(this.props)
|
||||
} = this.getStyleParams()
|
||||
|
||||
this.getWindow().document.getElementById(
|
||||
'codeTheme'
|
||||
).href = getCodeThemeLink(codeBlockTheme)
|
||||
|
||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle(
|
||||
).href = this.getCodeThemeLink(codeBlockTheme)
|
||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle({
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
@@ -425,7 +811,15 @@ class MarkdownPreview extends React.Component {
|
||||
allowCustomCSS,
|
||||
customCSS,
|
||||
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() {
|
||||
@@ -459,7 +853,7 @@ class MarkdownPreview extends React.Component {
|
||||
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
|
||||
if (sanitize === 'NONE') {
|
||||
const splitWithCodeTag = value.split('```')
|
||||
value = escapeHtmlCharactersInCodeTag(splitWithCodeTag)
|
||||
value = this.escapeHtmlCharactersInCodeTag(splitWithCodeTag)
|
||||
}
|
||||
const renderedHTML = this.markdown.render(value)
|
||||
attachmentManagement.migrateAttachments(value, storagePath, noteKey)
|
||||
@@ -522,9 +916,13 @@ class MarkdownPreview extends React.Component {
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const opts = {}
|
||||
|
||||
// if (this.props.theme === 'dark') {
|
||||
// opts['font-color'] = '#DDD'
|
||||
// opts['line-color'] = '#DDD'
|
||||
// opts['element-color'] = '#DDD'
|
||||
// opts['fill'] = '#3A404C'
|
||||
// }
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll('.flowchart'),
|
||||
el => {
|
||||
@@ -747,18 +1145,17 @@ class MarkdownPreview extends React.Component {
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @param {Number} targetLine
|
||||
* @param {Number} targetRow
|
||||
*/
|
||||
scrollToLine(targetLine) {
|
||||
scrollToRow(targetRow) {
|
||||
const blocks = this.getWindow().document.querySelectorAll(
|
||||
'body [data-line]'
|
||||
'body>[data-line]'
|
||||
)
|
||||
|
||||
for (let index = 0; index < blocks.length; index++) {
|
||||
let block = blocks[index]
|
||||
const line = parseInt(block.getAttribute('data-line'))
|
||||
|
||||
if (line > targetLine || index === blocks.length - 1) {
|
||||
const row = parseInt(block.getAttribute('data-line'))
|
||||
if (row > targetRow || index === blocks.length - 1) {
|
||||
block = blocks[index - 1]
|
||||
block != null && this.scrollTo(0, block.offsetTop)
|
||||
break
|
||||
|
||||
@@ -13,7 +13,7 @@ class MarkdownSplitEditor extends React.Component {
|
||||
this.value = props.value
|
||||
this.focus = () => this.refs.code.focus()
|
||||
this.reload = () => this.refs.code.reload()
|
||||
this.userScroll = props.config.preview.scrollSync
|
||||
this.userScroll = true
|
||||
this.state = {
|
||||
isSliderFocused: false,
|
||||
codeEditorWidthInPercent: 50,
|
||||
@@ -21,72 +21,6 @@ class MarkdownSplitEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
this.props.config.preview.scrollSync !==
|
||||
prevProps.config.preview.scrollSync
|
||||
) {
|
||||
this.userScroll = this.props.config.preview.scrollSync
|
||||
}
|
||||
}
|
||||
|
||||
handleCursorActivity(editor) {
|
||||
if (this.userScroll) {
|
||||
const previewDoc = _.get(
|
||||
this,
|
||||
'refs.preview.refs.root.contentWindow.document'
|
||||
)
|
||||
const previewTop = _.get(previewDoc, 'body.scrollTop')
|
||||
|
||||
const line = editor.doc.getCursor().line
|
||||
let top
|
||||
if (line === 0) {
|
||||
top = 0
|
||||
} else {
|
||||
const blockElements = previewDoc.querySelectorAll('body [data-line]')
|
||||
const blocks = []
|
||||
for (const block of blockElements) {
|
||||
const l = parseInt(block.getAttribute('data-line'))
|
||||
|
||||
blocks.push({
|
||||
line: l,
|
||||
top: block.offsetTop
|
||||
})
|
||||
|
||||
if (l > line) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (blocks.length === 1) {
|
||||
const block = blockElements[blockElements.length - 1]
|
||||
|
||||
blocks.push({
|
||||
line: editor.doc.size,
|
||||
top: block.offsetTop + block.offsetHeight
|
||||
})
|
||||
}
|
||||
|
||||
const i = blocks.length - 1
|
||||
|
||||
const ratio =
|
||||
(blocks[i].top - blocks[i - 1].top) /
|
||||
(blocks[i].line - blocks[i - 1].line)
|
||||
|
||||
const delta = Math.floor(_.get(previewDoc, 'body.clientHeight') / 3)
|
||||
|
||||
top =
|
||||
blocks[i - 1].top +
|
||||
Math.floor((line - blocks[i - 1].line) * ratio) -
|
||||
delta
|
||||
}
|
||||
|
||||
this.scrollTo(previewTop, top, y =>
|
||||
_.set(previewDoc, 'body.scrollTop', y)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
setValue(value) {
|
||||
this.refs.code.setValue(value)
|
||||
}
|
||||
@@ -96,125 +30,59 @@ class MarkdownSplitEditor extends React.Component {
|
||||
this.props.onChange(e)
|
||||
}
|
||||
|
||||
handleEditorScroll(e) {
|
||||
handleScroll(e) {
|
||||
if (!this.props.config.preview.scrollSync) return
|
||||
|
||||
const previewDoc = _.get(
|
||||
this,
|
||||
'refs.preview.refs.root.contentWindow.document'
|
||||
)
|
||||
const codeDoc = _.get(this, 'refs.code.editor.doc')
|
||||
let srcTop, srcHeight, targetTop, targetHeight
|
||||
|
||||
if (this.userScroll) {
|
||||
const previewDoc = _.get(
|
||||
this,
|
||||
'refs.preview.refs.root.contentWindow.document'
|
||||
)
|
||||
const codeDoc = _.get(this, 'refs.code.editor.doc')
|
||||
|
||||
const from = codeDoc.cm.coordsChar({ left: 0, top: 0 }).line
|
||||
const to = codeDoc.cm.coordsChar({
|
||||
left: 0,
|
||||
top: codeDoc.cm.display.lastWrapHeight * 1.125
|
||||
}).line
|
||||
const previewTop = _.get(previewDoc, 'body.scrollTop')
|
||||
|
||||
let top
|
||||
if (from === 0) {
|
||||
top = 0
|
||||
} else if (to === codeDoc.lastLine()) {
|
||||
top =
|
||||
_.get(previewDoc, 'body.scrollHeight') -
|
||||
_.get(previewDoc, 'body.clientHeight')
|
||||
if (e.doc) {
|
||||
srcTop = _.get(e, 'doc.scrollTop')
|
||||
srcHeight = _.get(e, 'doc.height')
|
||||
targetTop = _.get(previewDoc, 'body.scrollTop')
|
||||
targetHeight = _.get(previewDoc, 'body.scrollHeight')
|
||||
} else {
|
||||
const line = from + Math.floor((to - from) / 3)
|
||||
|
||||
const blockElements = previewDoc.querySelectorAll('body [data-line]')
|
||||
const blocks = []
|
||||
for (const block of blockElements) {
|
||||
const l = parseInt(block.getAttribute('data-line'))
|
||||
|
||||
blocks.push({
|
||||
line: l,
|
||||
top: block.offsetTop
|
||||
})
|
||||
|
||||
if (l > line) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (blocks.length === 1) {
|
||||
const block = blockElements[blockElements.length - 1]
|
||||
|
||||
blocks.push({
|
||||
line: codeDoc.size,
|
||||
top: block.offsetTop + block.offsetHeight
|
||||
})
|
||||
}
|
||||
|
||||
const i = blocks.length - 1
|
||||
|
||||
const ratio =
|
||||
(blocks[i].top - blocks[i - 1].top) /
|
||||
(blocks[i].line - blocks[i - 1].line)
|
||||
|
||||
top =
|
||||
blocks[i - 1].top + Math.floor((line - blocks[i - 1].line) * ratio)
|
||||
srcTop = _.get(previewDoc, 'body.scrollTop')
|
||||
srcHeight = _.get(previewDoc, 'body.scrollHeight')
|
||||
targetTop = _.get(codeDoc, 'scrollTop')
|
||||
targetHeight = _.get(codeDoc, 'height')
|
||||
}
|
||||
|
||||
this.scrollTo(previewTop, top, y =>
|
||||
_.set(previewDoc, 'body.scrollTop', y)
|
||||
)
|
||||
}
|
||||
}
|
||||
const distance = (targetHeight * srcTop) / srcHeight - targetTop
|
||||
const framerate = 1000 / 60
|
||||
const frames = 20
|
||||
const refractory = frames * framerate
|
||||
|
||||
handlePreviewScroll(e) {
|
||||
if (this.userScroll) {
|
||||
const previewDoc = _.get(
|
||||
this,
|
||||
'refs.preview.refs.root.contentWindow.document'
|
||||
)
|
||||
const codeDoc = _.get(this, 'refs.code.editor.doc')
|
||||
this.userScroll = false
|
||||
|
||||
const srcTop = _.get(previewDoc, 'body.scrollTop')
|
||||
const editorTop = _.get(codeDoc, 'scrollTop')
|
||||
|
||||
let top
|
||||
if (srcTop === 0) {
|
||||
top = 0
|
||||
} else {
|
||||
const delta = Math.floor(_.get(previewDoc, 'body.clientHeight') / 3)
|
||||
const previewTop = srcTop + delta
|
||||
|
||||
const blockElements = previewDoc.querySelectorAll('body [data-line]')
|
||||
const blocks = []
|
||||
for (const block of blockElements) {
|
||||
const top = block.offsetTop
|
||||
|
||||
blocks.push({
|
||||
line: parseInt(block.getAttribute('data-line')),
|
||||
top
|
||||
})
|
||||
|
||||
if (top > previewTop) {
|
||||
break
|
||||
}
|
||||
let frame = 0
|
||||
let scrollPos, time
|
||||
const timer = setInterval(() => {
|
||||
time = frame / frames
|
||||
scrollPos =
|
||||
time < 0.5
|
||||
? 2 * time * time // ease in
|
||||
: -1 + (4 - 2 * time) * time // ease out
|
||||
if (e.doc)
|
||||
_.set(previewDoc, 'body.scrollTop', targetTop + scrollPos * distance)
|
||||
else
|
||||
_.get(this, 'refs.code.editor').scrollTo(
|
||||
0,
|
||||
targetTop + scrollPos * distance
|
||||
)
|
||||
if (frame >= frames) {
|
||||
clearInterval(timer)
|
||||
setTimeout(() => {
|
||||
this.userScroll = true
|
||||
}, refractory)
|
||||
}
|
||||
|
||||
if (blocks.length === 1) {
|
||||
const block = blockElements[blockElements.length - 1]
|
||||
|
||||
blocks.push({
|
||||
line: codeDoc.size,
|
||||
top: block.offsetTop + block.offsetHeight
|
||||
})
|
||||
}
|
||||
|
||||
const i = blocks.length - 1
|
||||
|
||||
const from = codeDoc.cm.heightAtLine(blocks[i - 1].line, 'local')
|
||||
const to = codeDoc.cm.heightAtLine(blocks[i].line, 'local')
|
||||
|
||||
const ratio =
|
||||
(previewTop - blocks[i - 1].top) / (blocks[i].top - blocks[i - 1].top)
|
||||
|
||||
top = from + Math.floor((to - from) * ratio) - delta
|
||||
}
|
||||
|
||||
this.scrollTo(editorTop, top, y => codeDoc.cm.scrollTo(0, y))
|
||||
frame++
|
||||
}, framerate)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,35 +168,6 @@ class MarkdownSplitEditor extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
scrollTo(from, to, scroller) {
|
||||
const distance = to - from
|
||||
const framerate = 1000 / 60
|
||||
const frames = 20
|
||||
const refractory = frames * framerate
|
||||
|
||||
this.userScroll = false
|
||||
|
||||
let frame = 0
|
||||
let scrollPos, time
|
||||
const timer = setInterval(() => {
|
||||
time = frame / frames
|
||||
scrollPos =
|
||||
time < 0.5
|
||||
? 2 * time * time // ease in
|
||||
: -1 + (4 - 2 * time) * time // ease out
|
||||
|
||||
scroller(from + scrollPos * distance)
|
||||
|
||||
if (frame >= frames) {
|
||||
clearInterval(timer)
|
||||
setTimeout(() => {
|
||||
this.userScroll = true
|
||||
}, refractory)
|
||||
}
|
||||
frame++
|
||||
}, framerate)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
config,
|
||||
@@ -336,7 +175,6 @@ class MarkdownSplitEditor extends React.Component {
|
||||
storageKey,
|
||||
noteKey,
|
||||
linesHighlighted,
|
||||
getNote,
|
||||
isStacking,
|
||||
RTL
|
||||
} = this.props
|
||||
@@ -422,15 +260,8 @@ class MarkdownSplitEditor extends React.Component {
|
||||
displayLineNumbers={config.editor.displayLineNumbers}
|
||||
lineWrapping
|
||||
matchingPairs={config.editor.matchingPairs}
|
||||
matchingCloseBefore={config.editor.matchingCloseBefore}
|
||||
matchingTriples={config.editor.matchingTriples}
|
||||
explodingPairs={config.editor.explodingPairs}
|
||||
codeBlockMatchingPairs={config.editor.codeBlockMatchingPairs}
|
||||
codeBlockMatchingCloseBefore={
|
||||
config.editor.codeBlockMatchingCloseBefore
|
||||
}
|
||||
codeBlockMatchingTriples={config.editor.codeBlockMatchingTriples}
|
||||
codeBlockExplodingPairs={config.editor.codeBlockExplodingPairs}
|
||||
indentType={config.editor.indentType}
|
||||
indentSize={editorStyle.indentSize}
|
||||
enableRulers={config.editor.enableRulers}
|
||||
@@ -442,15 +273,13 @@ class MarkdownSplitEditor extends React.Component {
|
||||
noteKey={noteKey}
|
||||
linesHighlighted={linesHighlighted}
|
||||
onChange={e => this.handleOnChange(e)}
|
||||
onScroll={e => this.handleEditorScroll(e)}
|
||||
onCursorActivity={e => this.handleCursorActivity(e)}
|
||||
onScroll={this.handleScroll.bind(this)}
|
||||
spellCheck={config.editor.spellcheck}
|
||||
enableSmartPaste={config.editor.enableSmartPaste}
|
||||
hotkey={config.hotkey}
|
||||
switchPreview={config.editor.switchPreview}
|
||||
enableMarkdownLint={config.editor.enableMarkdownLint}
|
||||
customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
|
||||
dateFormatISO8601={config.editor.dateFormatISO8601}
|
||||
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
|
||||
RTL={RTL}
|
||||
/>
|
||||
@@ -471,7 +300,6 @@ class MarkdownSplitEditor extends React.Component {
|
||||
codeBlockTheme={config.preview.codeBlockTheme}
|
||||
codeBlockFontFamily={config.editor.fontFamily}
|
||||
lineNumber={config.preview.lineNumber}
|
||||
indentSize={editorIndentSize}
|
||||
scrollPastEnd={config.preview.scrollPastEnd}
|
||||
smartQuotes={config.preview.smartQuotes}
|
||||
smartArrows={config.preview.smartArrows}
|
||||
@@ -481,15 +309,13 @@ class MarkdownSplitEditor extends React.Component {
|
||||
tabInde='0'
|
||||
value={value}
|
||||
onCheckboxClick={e => this.handleCheckboxClick(e)}
|
||||
onScroll={e => this.handlePreviewScroll(e)}
|
||||
onScroll={this.handleScroll.bind(this)}
|
||||
showCopyNotification={config.ui.showCopyNotification}
|
||||
storagePath={storage.path}
|
||||
noteKey={noteKey}
|
||||
customCSS={config.preview.customCSS}
|
||||
allowCustomCSS={config.preview.allowCustomCSS}
|
||||
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
||||
getNote={getNote}
|
||||
export={config.export}
|
||||
RTL={RTL}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ import markdownItTocAndAnchor from '@hikerpig/markdown-it-toc-and-anchor'
|
||||
import _ from 'lodash'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import katex from 'katex'
|
||||
import { escapeHtmlCharacters, lastFindInArray } from './utils'
|
||||
import { lastFindInArray } from './utils'
|
||||
|
||||
function createGutter(str, firstLineNumber) {
|
||||
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
|
||||
@@ -31,8 +31,7 @@ class Markdown {
|
||||
html: true,
|
||||
xhtmlOut: true,
|
||||
breaks: config.preview.breaks,
|
||||
sanitize: 'STRICT',
|
||||
onFence: () => {}
|
||||
sanitize: 'STRICT'
|
||||
}
|
||||
|
||||
const updatedOptions = Object.assign(defaultOptions, options)
|
||||
@@ -267,26 +266,22 @@ class Markdown {
|
||||
token.parameters.format = 'yaml'
|
||||
}
|
||||
|
||||
updatedOptions.onFence('chart', token.parameters.format)
|
||||
|
||||
return `<pre class="fence" data-line="${token.map[0]}">
|
||||
<span class="filename">${token.fileName}</span>
|
||||
<div class="chart" data-height="${
|
||||
token.parameters.height
|
||||
}" data-format="${token.parameters.format || 'json'}">${
|
||||
<span class="filename">${token.fileName}</span>
|
||||
<div class="chart" data-height="${
|
||||
token.parameters.height
|
||||
}" data-format="${token.parameters.format || 'json'}">${
|
||||
token.content
|
||||
}</div>
|
||||
</pre>`
|
||||
</pre>`
|
||||
},
|
||||
flowchart: token => {
|
||||
updatedOptions.onFence('flowchart')
|
||||
|
||||
return `<pre class="fence" data-line="${token.map[0]}">
|
||||
<span class="filename">${token.fileName}</span>
|
||||
<div class="flowchart" data-height="${token.parameters.height}">${
|
||||
<span class="filename">${token.fileName}</span>
|
||||
<div class="flowchart" data-height="${token.parameters.height}">${
|
||||
token.content
|
||||
}</div>
|
||||
</pre>`
|
||||
</pre>`
|
||||
},
|
||||
gallery: token => {
|
||||
const content = token.content
|
||||
@@ -303,41 +298,35 @@ class Markdown {
|
||||
.join('\n')
|
||||
|
||||
return `<pre class="fence" data-line="${token.map[0]}">
|
||||
<span class="filename">${token.fileName}</span>
|
||||
<div class="gallery" data-autoplay="${
|
||||
token.parameters.autoplay
|
||||
}" data-height="${token.parameters.height}">${content}</div>
|
||||
</pre>`
|
||||
<span class="filename">${token.fileName}</span>
|
||||
<div class="gallery" data-autoplay="${
|
||||
token.parameters.autoplay
|
||||
}" data-height="${token.parameters.height}">${content}</div>
|
||||
</pre>`
|
||||
},
|
||||
mermaid: token => {
|
||||
updatedOptions.onFence('mermaid')
|
||||
|
||||
return `<pre class="fence" data-line="${token.map[0]}">
|
||||
<span class="filename">${token.fileName}</span>
|
||||
<div class="mermaid" data-height="${token.parameters.height}">${
|
||||
<span class="filename">${token.fileName}</span>
|
||||
<div class="mermaid" data-height="${token.parameters.height}">${
|
||||
token.content
|
||||
}</div>
|
||||
</pre>`
|
||||
</pre>`
|
||||
},
|
||||
sequence: token => {
|
||||
updatedOptions.onFence('sequence')
|
||||
|
||||
return `<pre class="fence" data-line="${token.map[0]}">
|
||||
<span class="filename">${token.fileName}</span>
|
||||
<div class="sequence" data-height="${token.parameters.height}">${
|
||||
<span class="filename">${token.fileName}</span>
|
||||
<div class="sequence" data-height="${token.parameters.height}">${
|
||||
token.content
|
||||
}</div>
|
||||
</pre>`
|
||||
</pre>`
|
||||
}
|
||||
},
|
||||
token => {
|
||||
updatedOptions.onFence('code', token.langType)
|
||||
|
||||
return `<pre class="code CodeMirror" data-line="${token.map[0]}">
|
||||
<span class="filename">${token.fileName}</span>
|
||||
${createGutter(token.content, token.firstLineNumber)}
|
||||
<code class="${token.langType}">${token.content}</code>
|
||||
</pre>`
|
||||
<span class="filename">${token.fileName}</span>
|
||||
${createGutter(token.content, token.firstLineNumber)}
|
||||
<code class="${token.langType}">${token.content}</code>
|
||||
</pre>`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -479,16 +468,6 @@ class Markdown {
|
||||
return true
|
||||
})
|
||||
|
||||
this.md.renderer.rules.code_inline = function(tokens, idx) {
|
||||
const token = tokens[idx]
|
||||
|
||||
return (
|
||||
'<code class="inline">' +
|
||||
escapeHtmlCharacters(token.content) +
|
||||
'</code>'
|
||||
)
|
||||
}
|
||||
|
||||
if (config.preview.smartArrows) {
|
||||
this.md.use(smartArrows)
|
||||
}
|
||||
|
||||
@@ -57,11 +57,10 @@ class MarkdownNoteDetail extends React.Component {
|
||||
|
||||
this.dispatchTimer = null
|
||||
|
||||
this.generateToc = this.handleGenerateToc.bind(this)
|
||||
this.toggleLockButton = this.handleToggleLockButton.bind(this)
|
||||
this.generateToc = this.handleGenerateToc.bind(this)
|
||||
this.handleUpdateContent = this.handleUpdateContent.bind(this)
|
||||
this.handleSwitchStackDirection = this.handleSwitchStackDirection.bind(this)
|
||||
this.getNote = this.getNote.bind(this)
|
||||
}
|
||||
|
||||
focus() {
|
||||
@@ -442,10 +441,6 @@ class MarkdownNoteDetail extends React.Component {
|
||||
this.updateNote(note)
|
||||
}
|
||||
|
||||
getNote() {
|
||||
return this.state.note
|
||||
}
|
||||
|
||||
renderEditor() {
|
||||
const { config, ignorePreviewPointerEvents } = this.props
|
||||
const { note, isStacking } = this.state
|
||||
@@ -461,8 +456,8 @@ class MarkdownNoteDetail extends React.Component {
|
||||
noteKey={note.key}
|
||||
linesHighlighted={note.linesHighlighted}
|
||||
onChange={this.handleUpdateContent}
|
||||
isLocked={this.state.isLocked}
|
||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||
getNote={this.getNote}
|
||||
RTL={config.editor.rtlEnabled && this.state.RTL}
|
||||
/>
|
||||
)
|
||||
@@ -478,7 +473,6 @@ class MarkdownNoteDetail extends React.Component {
|
||||
linesHighlighted={note.linesHighlighted}
|
||||
onChange={this.handleUpdateContent}
|
||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||
getNote={this.getNote}
|
||||
RTL={config.editor.rtlEnabled && this.state.RTL}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -859,15 +859,8 @@ class SnippetNoteDetail extends React.Component {
|
||||
indentSize={editorIndentSize}
|
||||
displayLineNumbers={config.editor.displayLineNumbers}
|
||||
matchingPairs={config.editor.matchingPairs}
|
||||
matchingCloseBefore={config.editor.matchingCloseBefore}
|
||||
matchingTriples={config.editor.matchingTriples}
|
||||
explodingPairs={config.editor.explodingPairs}
|
||||
codeBlockMatchingPairs={config.editor.codeBlockMatchingPairs}
|
||||
codeBlockMatchingCloseBefore={
|
||||
config.editor.codeBlockMatchingCloseBefore
|
||||
}
|
||||
codeBlockMatchingTriples={config.editor.codeBlockMatchingTriples}
|
||||
codeBlockExplodingPairs={config.editor.codeBlockExplodingPairs}
|
||||
keyMap={config.editor.keyMap}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||
@@ -877,7 +870,6 @@ class SnippetNoteDetail extends React.Component {
|
||||
enableSmartPaste={config.editor.enableSmartPaste}
|
||||
hotkey={config.hotkey}
|
||||
autoDetect={autoDetect}
|
||||
dateFormatISO8601={config.editor.dateFormatISO8601}
|
||||
storageKey={storageKey}
|
||||
noteKey={note.key}
|
||||
/>
|
||||
|
||||
@@ -18,7 +18,6 @@ import { getLocales } from 'browser/lib/Languages'
|
||||
import applyShortcuts from 'browser/main/lib/shortcutManager'
|
||||
import { chooseTheme, applyTheme } from 'browser/main/lib/ThemeManager'
|
||||
import { push } from 'connected-react-router'
|
||||
import { ipcRenderer } from 'electron'
|
||||
|
||||
const path = require('path')
|
||||
const electron = require('electron')
|
||||
@@ -185,7 +184,6 @@ class Main extends React.Component {
|
||||
this.toggleMenuBarVisible.bind(this)
|
||||
)
|
||||
eventEmitter.on('dispatch:push', this.changeRoutePush.bind(this))
|
||||
eventEmitter.on('update', () => ipcRenderer.send('update-check', 'manual'))
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
||||
@@ -21,7 +21,6 @@ import Markdown from '../../lib/markdown'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||
import context from 'browser/lib/context'
|
||||
import filenamify from 'filenamify'
|
||||
import queryString from 'query-string'
|
||||
|
||||
const { remote } = require('electron')
|
||||
@@ -635,38 +634,6 @@ class NoteList extends React.Component {
|
||||
this.selectNextNote()
|
||||
}
|
||||
|
||||
handleExportClick(e, note, fileType) {
|
||||
const options = {
|
||||
defaultPath: filenamify(note.title, {
|
||||
replacement: '_'
|
||||
}),
|
||||
filters: [{ name: 'Documents', extensions: [fileType] }],
|
||||
properties: ['openFile', 'createDirectory']
|
||||
}
|
||||
|
||||
dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
|
||||
if (filename) {
|
||||
const { config } = this.props
|
||||
|
||||
dataApi
|
||||
.exportNoteAs(note, filename, fileType, config)
|
||||
.then(res => {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'info',
|
||||
message: `Exported to ${filename}`
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
dialog.showErrorBox(
|
||||
'Export error',
|
||||
err ? err.message || err : 'Unexpected error during export'
|
||||
)
|
||||
throw err
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleNoteContextMenu(e, uniqueKey) {
|
||||
const { location } = this.props
|
||||
const { selectedNoteKeys } = this.state
|
||||
@@ -722,40 +689,9 @@ class NoteList extends React.Component {
|
||||
click: this.copyNoteLink.bind(this, 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) {
|
||||
templates.push(
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: updateLabel,
|
||||
click: this.publishMarkdown.bind(this)
|
||||
@@ -766,15 +702,10 @@ class NoteList extends React.Component {
|
||||
}
|
||||
)
|
||||
} else {
|
||||
templates.push(
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: publishLabel,
|
||||
click: this.publishMarkdown.bind(this)
|
||||
}
|
||||
)
|
||||
templates.push({
|
||||
label: publishLabel,
|
||||
click: this.publishMarkdown.bind(this)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,20 +43,12 @@ class StorageItem extends React.Component {
|
||||
label: i18n.__('Export Storage'),
|
||||
submenu: [
|
||||
{
|
||||
label: i18n.__('Export as Plain Text (.txt)'),
|
||||
label: i18n.__('Export as txt'),
|
||||
click: e => this.handleExportStorageClick(e, 'txt')
|
||||
},
|
||||
{
|
||||
label: i18n.__('Export as Markdown (.md)'),
|
||||
label: i18n.__('Export as 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')
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -105,28 +97,14 @@ class StorageItem extends React.Component {
|
||||
}
|
||||
dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
|
||||
if (paths && paths.length === 1) {
|
||||
const { storage, dispatch, config } = this.props
|
||||
dataApi
|
||||
.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
|
||||
const { storage, dispatch } = this.props
|
||||
dataApi.exportStorage(storage.key, fileType, paths[0]).then(data => {
|
||||
dispatch({
|
||||
type: 'EXPORT_STORAGE',
|
||||
storage: data.storage,
|
||||
fileType: data.fileType
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -188,20 +166,12 @@ class StorageItem extends React.Component {
|
||||
label: i18n.__('Export Folder'),
|
||||
submenu: [
|
||||
{
|
||||
label: i18n.__('Export as Plain Text (.txt)'),
|
||||
label: i18n.__('Export as txt'),
|
||||
click: e => this.handleExportFolderClick(e, folder, 'txt')
|
||||
},
|
||||
{
|
||||
label: i18n.__('Export as Markdown (.md)'),
|
||||
label: i18n.__('Export as 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')
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -232,28 +202,30 @@ class StorageItem extends React.Component {
|
||||
}
|
||||
dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
|
||||
if (paths && paths.length === 1) {
|
||||
const { storage, dispatch, config } = this.props
|
||||
const { storage, dispatch } = this.props
|
||||
dataApi
|
||||
.exportFolder(storage.key, folder.key, fileType, paths[0], config)
|
||||
.exportFolder(storage.key, folder.key, fileType, paths[0])
|
||||
.then(data => {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'info',
|
||||
message: `Exported to ${paths[0]}`
|
||||
})
|
||||
|
||||
dispatch({
|
||||
type: 'EXPORT_FOLDER',
|
||||
storage: data.storage,
|
||||
folderKey: data.folderKey,
|
||||
fileType: data.fileType
|
||||
})
|
||||
return data
|
||||
})
|
||||
.catch(error => {
|
||||
.then(data => {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'info',
|
||||
message: 'Exported to "' + data.exportDir + '"'
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
dialog.showErrorBox(
|
||||
'Export error',
|
||||
error ? error.message || error : 'Unexpected error during export'
|
||||
err ? err.message || err : 'Unexpected error during export'
|
||||
)
|
||||
throw error
|
||||
throw err
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -26,8 +26,6 @@ import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||
import ColorPicker from 'browser/components/ColorPicker'
|
||||
import { every, sortBy } from 'lodash'
|
||||
|
||||
const { dialog } = remote
|
||||
|
||||
function matchActiveTags(tags, activeTags) {
|
||||
return every(activeTags, v => tags.indexOf(v) >= 0)
|
||||
}
|
||||
@@ -65,12 +63,15 @@ class SideNav extends React.Component {
|
||||
}
|
||||
|
||||
deleteTag(tag) {
|
||||
const selectedButton = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: i18n.__('Confirm tag deletion'),
|
||||
detail: i18n.__('This will permanently remove this tag.'),
|
||||
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||
})
|
||||
const selectedButton = remote.dialog.showMessageBox(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
type: 'warning',
|
||||
message: i18n.__('Confirm tag deletion'),
|
||||
detail: i18n.__('This will permanently remove this tag.'),
|
||||
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||
}
|
||||
)
|
||||
|
||||
if (selectedButton === 0) {
|
||||
const {
|
||||
@@ -154,80 +155,28 @@ class SideNav extends React.Component {
|
||||
}
|
||||
|
||||
handleTagContextMenu(e, tag) {
|
||||
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)
|
||||
}
|
||||
])
|
||||
}
|
||||
const menu = []
|
||||
|
||||
handleExportTagClick(e, tag, fileType) {
|
||||
const options = {
|
||||
properties: ['openDirectory', 'createDirectory'],
|
||||
buttonLabel: i18n.__('Select directory'),
|
||||
title: i18n.__('Select a folder to export the files to'),
|
||||
multiSelections: false
|
||||
}
|
||||
dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
|
||||
if (paths && paths.length === 1) {
|
||||
const { data, config } = this.props
|
||||
dataApi
|
||||
.exportTag(data, tag, fileType, paths[0], config)
|
||||
.then(data => {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'info',
|
||||
message: `Exported to ${paths[0]}`
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
dialog.showErrorBox(
|
||||
'Export error',
|
||||
error ? error.message || error : 'Unexpected error during export'
|
||||
)
|
||||
throw error
|
||||
})
|
||||
}
|
||||
menu.push({
|
||||
label: i18n.__('Delete Tag'),
|
||||
click: this.deleteTag.bind(this, tag)
|
||||
})
|
||||
|
||||
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() {
|
||||
@@ -381,7 +330,6 @@ class SideNav extends React.Component {
|
||||
dispatch={dispatch}
|
||||
onSortEnd={this.onSortEnd.bind(this)(storage)}
|
||||
useDragHandle
|
||||
config={config}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -12,7 +12,6 @@ import DevTools from './DevTools'
|
||||
require('./lib/ipcClient')
|
||||
require('../lib/customMeta')
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import ConfigManager from './lib/ConfigManager'
|
||||
|
||||
const electron = require('electron')
|
||||
|
||||
@@ -108,22 +107,6 @@ function updateApp() {
|
||||
}
|
||||
}
|
||||
|
||||
function downloadUpdate() {
|
||||
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: i18n.__('Update Boostnote'),
|
||||
detail: i18n.__('New Boostnote is ready to be downloaded.'),
|
||||
buttons: [i18n.__('Download now'), i18n.__('Ignore updates')]
|
||||
})
|
||||
|
||||
if (index === 0) {
|
||||
ipcRenderer.send('update-download-confirm')
|
||||
} else if (index === 1) {
|
||||
ipcRenderer.send('update-cancel')
|
||||
ConfigManager.set({ autoUpdateEnabled: false })
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
@@ -164,12 +147,8 @@ ReactDOM.render(
|
||||
})
|
||||
|
||||
ipcRenderer.on('update-found', function() {
|
||||
downloadUpdate()
|
||||
})
|
||||
|
||||
ipcRenderer.on('update-not-found', function(_, msg) {
|
||||
notify('Update not found!', {
|
||||
body: msg
|
||||
notify('Update found!', {
|
||||
body: 'Preparing to update...'
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ const DEFAULT_MARKDOWN_LINT_CONFIG = `{
|
||||
|
||||
const DEFAULT_CSS_CONFIG = `
|
||||
/* Drop Your Custom CSS Code Here */
|
||||
[data-theme="default"] p code.inline,
|
||||
[data-theme="default"] li code.inline,
|
||||
[data-theme="default"] td code.inline
|
||||
[data-theme="default"] p code,
|
||||
[data-theme="default"] li code,
|
||||
[data-theme="default"] td code
|
||||
{
|
||||
padding: 2px;
|
||||
border-width: 1px;
|
||||
@@ -86,13 +86,8 @@ export const DEFAULT_CONFIG = {
|
||||
rulers: [80, 120],
|
||||
displayLineNumbers: true,
|
||||
matchingPairs: '()[]{}\'\'""$$**``~~__',
|
||||
matchingCloseBefore: ')]}\'":;>',
|
||||
matchingTriples: '```"""\'\'\'',
|
||||
explodingPairs: '[]{}``$$',
|
||||
codeBlockMatchingPairs: '()[]{}\'\'""``',
|
||||
codeBlockMatchingCloseBefore: ')]}\'":;>',
|
||||
codeBlockMatchingTriples: '',
|
||||
codeBlockExplodingPairs: '[]{}``',
|
||||
switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK'
|
||||
delfaultStatus: 'PREVIEW', // 'PREVIEW', 'CODE'
|
||||
scrollPastEnd: false,
|
||||
@@ -105,7 +100,6 @@ export const DEFAULT_CONFIG = {
|
||||
enableSmartPaste: false,
|
||||
enableMarkdownLint: false,
|
||||
customMarkdownLintConfig: DEFAULT_MARKDOWN_LINT_CONFIG,
|
||||
dateFormatISO8601: false,
|
||||
prettierConfig: `{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
@@ -144,11 +138,6 @@ export const DEFAULT_CONFIG = {
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
export: {
|
||||
metadata: 'DONT_EXPORT', // 'DONT_EXPORT', 'MERGE_HEADER', 'MERGE_VARIABLE'
|
||||
variable: 'boostnote',
|
||||
prefixAttachmentFolder: false
|
||||
},
|
||||
coloredTags: {},
|
||||
wakatime: {
|
||||
key: null
|
||||
|
||||
@@ -706,15 +706,14 @@ function replaceNoteKeyWithNewNoteKey(noteContent, oldNoteKey, newNoteKey) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @description replace all :storage references with given destination folder.
|
||||
* @param input Input in which the references should be replaced
|
||||
* @description Deletes all :storage and noteKey references from the given input.
|
||||
* @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) {
|
||||
function removeStorageAndNoteReferences(input, noteKey) {
|
||||
return input.replace(
|
||||
new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '[^"\\)<\\s]+', 'g'),
|
||||
new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?("|\\))', 'g'),
|
||||
function(match) {
|
||||
return match
|
||||
.replace(new RegExp(mdurl.encode(path.win32.sep), 'g'), path.posix.sep)
|
||||
@@ -736,7 +735,7 @@ function replaceStorageReferences(input, noteKey, destinationFolder) {
|
||||
')?',
|
||||
'g'
|
||||
),
|
||||
destinationFolder
|
||||
DESTINATION_FOLDER
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -1102,8 +1101,8 @@ module.exports = {
|
||||
getAttachmentsInMarkdownContent,
|
||||
getAbsolutePathsOfAttachmentsInContent,
|
||||
importAttachments,
|
||||
removeStorageAndNoteReferences,
|
||||
removeAttachmentsByPaths,
|
||||
replaceStorageReferences,
|
||||
deleteAttachmentFolder,
|
||||
deleteAttachmentsNotPresentInNote,
|
||||
getAttachmentsPathAndStatus,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import fs from 'fs'
|
||||
import fx from 'fs-extra'
|
||||
import path from 'path'
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
/**
|
||||
* @description Copy a file from source to destination
|
||||
@@ -15,8 +14,7 @@ function copyFile(srcPath, dstPath) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const dstFolder = path.dirname(dstPath)
|
||||
|
||||
fx.ensureDirSync(dstFolder)
|
||||
if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder)
|
||||
|
||||
const input = fs.createReadStream(decodeURI(srcPath))
|
||||
const output = fs.createWriteStream(dstPath)
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
import resolveStorageData from './resolveStorageData'
|
||||
import resolveStorageNotes from './resolveStorageNotes'
|
||||
import getFilename from './getFilename'
|
||||
import exportNote from './exportNote'
|
||||
import getContentFormatter from './getContentFormatter'
|
||||
import filenamify from 'filenamify'
|
||||
import * as path from 'path'
|
||||
|
||||
/**
|
||||
* @param {String} storageKey
|
||||
* @param {String} folderKey
|
||||
* @param {String} fileType
|
||||
* @param {String} exportDir
|
||||
* @param {Object} config
|
||||
*
|
||||
* @return {Object}
|
||||
* ```
|
||||
@@ -23,7 +22,7 @@ import getContentFormatter from './getContentFormatter'
|
||||
* ```
|
||||
*/
|
||||
|
||||
function exportFolder(storageKey, folderKey, fileType, exportDir, config) {
|
||||
function exportFolder(storageKey, folderKey, fileType, exportDir) {
|
||||
let targetStorage
|
||||
try {
|
||||
targetStorage = findStorage(storageKey)
|
||||
@@ -31,34 +30,39 @@ function exportFolder(storageKey, folderKey, fileType, exportDir, config) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
const deduplicator = {}
|
||||
|
||||
return resolveStorageData(targetStorage)
|
||||
.then(storage => {
|
||||
return resolveStorageNotes(storage).then(notes => ({
|
||||
storage,
|
||||
notes: notes.filter(
|
||||
note =>
|
||||
note.folder === folderKey &&
|
||||
!note.isTrashed &&
|
||||
note.type === 'MARKDOWN_NOTE'
|
||||
)
|
||||
}))
|
||||
.then(function assignNotes(storage) {
|
||||
return resolveStorageNotes(storage).then(notes => {
|
||||
return {
|
||||
storage,
|
||||
notes
|
||||
}
|
||||
})
|
||||
})
|
||||
.then(({ storage, notes }) => {
|
||||
const contentFormatter = getContentFormatter(storage, fileType, config)
|
||||
.then(function exportNotes(data) {
|
||||
const { storage, notes } = data
|
||||
|
||||
return Promise.all(
|
||||
notes.map(note => {
|
||||
const targetPath = getFilename(
|
||||
note,
|
||||
fileType,
|
||||
exportDir,
|
||||
deduplicator
|
||||
notes
|
||||
.filter(
|
||||
note =>
|
||||
note.folder === folderKey &&
|
||||
note.isTrashed === false &&
|
||||
note.type === 'MARKDOWN_NOTE'
|
||||
)
|
||||
|
||||
return exportNote(storage.key, note, targetPath, contentFormatter)
|
||||
})
|
||||
.map(note => {
|
||||
const notePath = path.join(
|
||||
exportDir,
|
||||
`${filenamify(note.title, { replacement: '_' })}.${fileType}`
|
||||
)
|
||||
return exportNote(
|
||||
note.key,
|
||||
storage.path,
|
||||
note.content,
|
||||
notePath,
|
||||
null
|
||||
)
|
||||
})
|
||||
).then(() => ({
|
||||
storage,
|
||||
folderKey,
|
||||
|
||||
@@ -4,34 +4,57 @@ import { findStorage } from 'browser/lib/findStorage'
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const attachmentManagement = require('./attachmentManagement')
|
||||
|
||||
/**
|
||||
* Export note together with attachments
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* @param {String} nodeKey key of the node that should be exported
|
||||
* @param {String} storageKey or storage path
|
||||
* @param {Object} note Note to export
|
||||
* @param {String} noteContent Content to export
|
||||
* @param {String} targetPath Path to exported file
|
||||
* @param {function} outputFormatter
|
||||
* @return {Promise.<*[]>}
|
||||
*/
|
||||
function exportNote(storageKey, note, targetPath, outputFormatter) {
|
||||
function exportNote(
|
||||
nodeKey,
|
||||
storageKey,
|
||||
noteContent,
|
||||
targetPath,
|
||||
outputFormatter
|
||||
) {
|
||||
const storagePath = path.isAbsolute(storageKey)
|
||||
? storageKey
|
||||
: findStorage(storageKey).path
|
||||
|
||||
const exportTasks = []
|
||||
|
||||
if (!storagePath) {
|
||||
throw new Error('Storage path is not found')
|
||||
}
|
||||
|
||||
const exportedData = Promise.resolve(
|
||||
outputFormatter
|
||||
? outputFormatter(note, targetPath, exportTasks)
|
||||
: note.content
|
||||
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
|
||||
noteContent,
|
||||
storagePath
|
||||
)
|
||||
attachmentsAbsolutePaths.forEach(attachment => {
|
||||
exportTasks.push({
|
||||
src: attachment,
|
||||
dst: attachmentManagement.DESTINATION_FOLDER
|
||||
})
|
||||
})
|
||||
|
||||
let exportedData = attachmentManagement.removeStorageAndNoteReferences(
|
||||
noteContent,
|
||||
nodeKey
|
||||
)
|
||||
|
||||
if (outputFormatter) {
|
||||
exportedData = outputFormatter(exportedData, exportTasks, targetPath)
|
||||
} else {
|
||||
exportedData = Promise.resolve(exportedData)
|
||||
}
|
||||
|
||||
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
|
||||
|
||||
@@ -40,9 +63,9 @@ function exportNote(storageKey, note, targetPath, outputFormatter) {
|
||||
.then(data => {
|
||||
return saveToFile(data, targetPath)
|
||||
})
|
||||
.catch(error => {
|
||||
.catch(err => {
|
||||
rollbackExport(tasks)
|
||||
throw error
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
@@ -84,14 +107,14 @@ function rollbackExport(tasks) {
|
||||
}
|
||||
|
||||
if (fs.existsSync(fullpath)) {
|
||||
fs.unlinkSync(fullpath)
|
||||
fs.unlink(fullpath)
|
||||
folders.add(path.dirname(fullpath))
|
||||
}
|
||||
})
|
||||
|
||||
folders.forEach(folder => {
|
||||
if (fs.readdirSync(folder).length === 0) {
|
||||
fs.rmdirSync(folder)
|
||||
fs.rmdir(folder)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
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,17 +2,13 @@ import { findStorage } from 'browser/lib/findStorage'
|
||||
import resolveStorageData from './resolveStorageData'
|
||||
import resolveStorageNotes from './resolveStorageNotes'
|
||||
import filenamify from 'filenamify'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import exportNote from './exportNote'
|
||||
import getContentFormatter from './getContentFormatter'
|
||||
import getFilename from './getFilename'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
|
||||
/**
|
||||
* @param {String} storageKey
|
||||
* @param {String} fileType
|
||||
* @param {String} exportDir
|
||||
* @param {Object} config
|
||||
*
|
||||
* @return {Object}
|
||||
* ```
|
||||
@@ -24,7 +20,7 @@ import getFilename from './getFilename'
|
||||
* ```
|
||||
*/
|
||||
|
||||
function exportStorage(storageKey, fileType, exportDir, config) {
|
||||
function exportStorage(storageKey, fileType, exportDir) {
|
||||
let targetStorage
|
||||
try {
|
||||
targetStorage = findStorage(storageKey)
|
||||
@@ -33,52 +29,39 @@ function exportStorage(storageKey, fileType, exportDir, config) {
|
||||
}
|
||||
|
||||
return resolveStorageData(targetStorage)
|
||||
.then(storage => {
|
||||
return resolveStorageNotes(storage).then(notes => ({
|
||||
storage,
|
||||
notes: notes.filter(
|
||||
note => !note.isTrashed && note.type === 'MARKDOWN_NOTE'
|
||||
)
|
||||
}))
|
||||
})
|
||||
.then(({ storage, notes }) => {
|
||||
const contentFormatter = getContentFormatter(storage, fileType, config)
|
||||
|
||||
.then(storage =>
|
||||
resolveStorageNotes(storage).then(notes => ({ storage, notes }))
|
||||
)
|
||||
.then(function exportNotes(data) {
|
||||
const { storage, notes } = data
|
||||
const folderNamesMapping = {}
|
||||
const deduplicators = {}
|
||||
|
||||
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) {}
|
||||
|
||||
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)
|
||||
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)
|
||||
})
|
||||
).then(() => ({
|
||||
|
||||
return {
|
||||
storage,
|
||||
fileType,
|
||||
exportDir
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
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
|
||||
@@ -1,796 +0,0 @@
|
||||
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.inline,
|
||||
li code.inline,
|
||||
td code.inline
|
||||
{
|
||||
padding: 2px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: 5px;
|
||||
}
|
||||
[data-theme="default"] p code.inline,
|
||||
[data-theme="default"] li code.inline,
|
||||
[data-theme="default"] td code.inline
|
||||
{
|
||||
background-color: #F4F4F4;
|
||||
border-color: #d9d9d9;
|
||||
color: inherit;
|
||||
}
|
||||
[data-theme="white"] p code.inline,
|
||||
[data-theme="white"] li code.inline,
|
||||
[data-theme="white"] td code.inline
|
||||
{
|
||||
background-color: #F4F4F4;
|
||||
border-color: #d9d9d9;
|
||||
color: inherit;
|
||||
}
|
||||
[data-theme="dark"] p code.inline,
|
||||
[data-theme="dark"] li code.inline,
|
||||
[data-theme="dark"] td code.inline
|
||||
{
|
||||
background-color: #444444;
|
||||
border-color: #555;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
[data-theme="dracula"] p code.inline,
|
||||
[data-theme="dracula"] li code.inline,
|
||||
[data-theme="dracula"] td code.inline
|
||||
{
|
||||
background-color: #444444;
|
||||
border-color: #555;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
[data-theme="monokai"] p code.inline,
|
||||
[data-theme="monokai"] li code.inline,
|
||||
[data-theme="monokai"] td code.inline
|
||||
{
|
||||
background-color: #444444;
|
||||
border-color: #555;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
[data-theme="nord"] p code.inline,
|
||||
[data-theme="nord"] li code.inline,
|
||||
[data-theme="nord"] td code.inline
|
||||
{
|
||||
background-color: #444444;
|
||||
border-color: #555;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
[data-theme="solarized-dark"] p code.inline,
|
||||
[data-theme="solarized-dark"] li code.inline,
|
||||
[data-theme="solarized-dark"] td code.inline
|
||||
{
|
||||
background-color: #444444;
|
||||
border-color: #555;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
[data-theme="vulcan"] p code.inline,
|
||||
[data-theme="vulcan"] li code.inline,
|
||||
[data-theme="vulcan"] td code.inline
|
||||
{
|
||||
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
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
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}`
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
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,14 +15,11 @@ const dataApi = {
|
||||
updateNote: require('./updateNote'),
|
||||
deleteNote: require('./deleteNote'),
|
||||
moveNote: require('./moveNote'),
|
||||
exportNoteAs: require('./exportNoteAs'),
|
||||
migrateFromV5Storage: require('./migrateFromV5Storage'),
|
||||
createSnippet: require('./createSnippet'),
|
||||
deleteSnippet: require('./deleteSnippet'),
|
||||
updateSnippet: require('./updateSnippet'),
|
||||
fetchSnippet: require('./fetchSnippet'),
|
||||
exportTag: require('./exportTag'),
|
||||
getFilename: require('./getFilename'),
|
||||
|
||||
_migrateFromV6Storage: require('./migrateFromV6Storage'),
|
||||
_resolveStorageData: require('./resolveStorageData'),
|
||||
|
||||
@@ -139,13 +139,6 @@ div[id^="firstRow"]
|
||||
margin-right 10px
|
||||
font-size 14px
|
||||
|
||||
.group-section-label-right
|
||||
width 200px
|
||||
text-align right
|
||||
margin-right 10px
|
||||
font-size 14px
|
||||
padding-right 1.5rem
|
||||
|
||||
.group-section-control
|
||||
flex 1
|
||||
margin-left 5px
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
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)
|
||||
@@ -16,78 +16,25 @@ class InfoTab extends React.Component {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
config: this.props.config,
|
||||
subscriptionFormStatus: 'idle',
|
||||
subscriptionFormErrorMessage: null,
|
||||
subscriptionFormEmail: ''
|
||||
config: this.props.config
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { autoUpdateEnabled, amaEnabled } = ConfigManager.get()
|
||||
|
||||
this.setState({ config: { autoUpdateEnabled, amaEnabled } })
|
||||
}
|
||||
|
||||
handleLinkClick(e) {
|
||||
shell.openExternal(e.currentTarget.href)
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
handleConfigChange(e) {
|
||||
const newConfig = {
|
||||
amaEnabled: this.refs.amaEnabled.checked,
|
||||
autoUpdateEnabled: this.refs.autoUpdateEnabled.checked
|
||||
}
|
||||
const newConfig = { amaEnabled: this.refs.amaEnabled.checked }
|
||||
|
||||
this.setState({ config: newConfig })
|
||||
return newConfig
|
||||
}
|
||||
|
||||
handleSubscriptionFormSubmit(e) {
|
||||
e.preventDefault()
|
||||
this.setState({
|
||||
subscriptionFormStatus: 'sending',
|
||||
subscriptionFormErrorMessage: null
|
||||
})
|
||||
|
||||
fetch(
|
||||
'https://boostmails.boostio.co/api/public/lists/5f434dccd05f3160b41c0d49/subscriptions',
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email: this.state.subscriptionFormEmail })
|
||||
}
|
||||
)
|
||||
.then(response => {
|
||||
if (response.status >= 400) {
|
||||
return response.text().then(text => {
|
||||
throw new Error(text)
|
||||
})
|
||||
}
|
||||
this.setState({
|
||||
subscriptionFormStatus: 'done'
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({
|
||||
subscriptionFormStatus: 'idle',
|
||||
subscriptionFormErrorMessage: error.message
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
handleSubscriptionFormEmailChange(e) {
|
||||
this.setState({
|
||||
subscriptionFormEmail: e.target.value
|
||||
})
|
||||
}
|
||||
|
||||
handleSaveButtonClick(e) {
|
||||
const newConfig = this.state.config
|
||||
const newConfig = {
|
||||
amaEnabled: this.state.config.amaEnabled
|
||||
}
|
||||
|
||||
if (!newConfig.amaEnabled) {
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('DISABLE_AMA')
|
||||
@@ -114,17 +61,20 @@ class InfoTab extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
toggleAutoUpdate() {
|
||||
const newConfig = {
|
||||
autoUpdateEnabled: !this.state.config.autoUpdateEnabled
|
||||
}
|
||||
|
||||
this.setState({ config: newConfig })
|
||||
ConfigManager.set(newConfig)
|
||||
}
|
||||
|
||||
infoMessage() {
|
||||
const { amaMessage } = this.state
|
||||
return amaMessage ? <p styleName='policy-confirm'>{amaMessage}</p> : null
|
||||
}
|
||||
|
||||
handleAutoUpdateChange() {
|
||||
const { autoUpdateEnabled } = this.handleConfigChange()
|
||||
|
||||
ConfigManager.set({ autoUpdateEnabled })
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div styleName='root'>
|
||||
@@ -184,40 +134,6 @@ class InfoTab extends React.Component {
|
||||
|
||||
<hr />
|
||||
|
||||
<div styleName='group-header--sub'>Subscribe Update Notes</div>
|
||||
{this.state.subscriptionFormStatus === 'done' ? (
|
||||
<div>
|
||||
<blockquote color={{ color: 'green' }}>
|
||||
Thanks for the subscription!
|
||||
</blockquote>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{this.state.subscriptionFormErrorMessage != null && (
|
||||
<blockquote style={{ color: 'red' }}>
|
||||
{this.state.subscriptionFormErrorMessage}
|
||||
</blockquote>
|
||||
)}
|
||||
<form onSubmit={e => this.handleSubscriptionFormSubmit(e)}>
|
||||
<input
|
||||
styleName='subscription-email-input'
|
||||
placeholder='E-mail'
|
||||
type='email'
|
||||
onChange={e => this.handleSubscriptionFormEmailChange(e)}
|
||||
disabled={this.state.subscriptionFormStatus === 'sending'}
|
||||
/>
|
||||
<button
|
||||
styleName='subscription-submit-button'
|
||||
type='submit'
|
||||
disabled={this.state.subscriptionFormStatus === 'sending'}
|
||||
>
|
||||
Subscribe
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
<hr />
|
||||
|
||||
<div styleName='group-header--sub'>{i18n.__('About')}</div>
|
||||
|
||||
<div styleName='top'>
|
||||
@@ -265,8 +181,7 @@ class InfoTab extends React.Component {
|
||||
<label>
|
||||
<input
|
||||
type='checkbox'
|
||||
ref='autoUpdateEnabled'
|
||||
onChange={() => this.handleAutoUpdateChange()}
|
||||
onChange={this.toggleAutoUpdate.bind(this)}
|
||||
checked={this.state.config.autoUpdateEnabled}
|
||||
/>
|
||||
{i18n.__('Enable Auto Update')}
|
||||
|
||||
@@ -33,35 +33,6 @@
|
||||
.separate-line
|
||||
margin 40px 0
|
||||
|
||||
.subscription-email-input
|
||||
height 35px
|
||||
vertical-align middle
|
||||
width 200px
|
||||
font-size $tab--button-font-size
|
||||
border solid 1px $border-color
|
||||
border-radius 2px
|
||||
padding 0 5px
|
||||
margin-right 5px
|
||||
outline none
|
||||
&:disabled
|
||||
background-color $ui-input--disabled-backgroundColor
|
||||
|
||||
.subscription-submit-button
|
||||
margin-top 10px
|
||||
height 35px
|
||||
border-radius 2px
|
||||
border none
|
||||
background-color alpha(#1EC38B, 90%)
|
||||
padding-left 20px
|
||||
padding-right 20px
|
||||
text-decoration none
|
||||
color white
|
||||
font-weight 600
|
||||
font-size 16px
|
||||
&:hover
|
||||
background-color #1EC38B
|
||||
transition 0.2s
|
||||
|
||||
.policy-submit
|
||||
margin-top 10px
|
||||
height 35px
|
||||
|
||||
@@ -35,18 +35,10 @@ class SnippetEditor extends React.Component {
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
autoCloseBrackets: {
|
||||
codeBlock: {
|
||||
pairs: this.props.codeBlockMatchingPairs,
|
||||
closeBefore: this.props.codeBlockMatchingCloseBefore,
|
||||
triples: this.props.codeBlockMatchingTriples,
|
||||
explode: this.props.codeBlockExplodingPairs
|
||||
},
|
||||
markdown: {
|
||||
pairs: this.props.matchingPairs,
|
||||
closeBefore: this.props.matchingCloseBefore,
|
||||
triples: this.props.matchingTriples,
|
||||
explode: this.props.explodingPairs
|
||||
}
|
||||
pairs: this.props.matchingPairs,
|
||||
triples: this.props.matchingTriples,
|
||||
explode: this.props.explodingPairs,
|
||||
override: true
|
||||
},
|
||||
mode: 'null'
|
||||
})
|
||||
|
||||
@@ -152,15 +152,8 @@ class SnippetTab extends React.Component {
|
||||
rulers={config.editor.rulers}
|
||||
displayLineNumbers={config.editor.displayLineNumbers}
|
||||
matchingPairs={config.editor.matchingPairs}
|
||||
matchingCloseBefore={config.editor.matchingCloseBefore}
|
||||
matchingTriples={config.editor.matchingTriples}
|
||||
explodingPairs={config.editor.explodingPairs}
|
||||
codeBlockMatchingPairs={config.editor.codeBlockMatchingPairs}
|
||||
codeBlockMatchingCloseBefore={
|
||||
config.editor.codeBlockMatchingCloseBefore
|
||||
}
|
||||
codeBlockMatchingTriples={config.editor.codeBlockMatchingTriples}
|
||||
codeBlockExplodingPairs={config.editor.codeBlockExplodingPairs}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
onRef={ref => {
|
||||
this.snippetEditor = ref
|
||||
|
||||
@@ -124,21 +124,14 @@ class UiTab extends React.Component {
|
||||
enableFrontMatterTitle: this.refs.enableFrontMatterTitle.checked,
|
||||
frontMatterTitleField: this.refs.frontMatterTitleField.value,
|
||||
matchingPairs: this.refs.matchingPairs.value,
|
||||
matchingCloseBefore: this.refs.matchingCloseBefore.value,
|
||||
matchingTriples: this.refs.matchingTriples.value,
|
||||
explodingPairs: this.refs.explodingPairs.value,
|
||||
codeBlockMatchingPairs: this.refs.codeBlockMatchingPairs.value,
|
||||
codeBlockMatchingCloseBefore: this.refs.codeBlockMatchingCloseBefore
|
||||
.value,
|
||||
codeBlockMatchingTriples: this.refs.codeBlockMatchingTriples.value,
|
||||
codeBlockExplodingPairs: this.refs.codeBlockExplodingPairs.value,
|
||||
spellcheck: this.refs.spellcheck.checked,
|
||||
enableSmartPaste: this.refs.enableSmartPaste.checked,
|
||||
enableMarkdownLint: this.refs.enableMarkdownLint.checked,
|
||||
customMarkdownLintConfig: this.customMarkdownLintConfigCM
|
||||
.getCodeMirror()
|
||||
.getValue(),
|
||||
dateFormatISO8601: this.refs.dateFormatISO8601.checked,
|
||||
prettierConfig: this.prettierConfigCM.getCodeMirror().getValue(),
|
||||
deleteUnusedAttachments: this.refs.deleteUnusedAttachments.checked,
|
||||
rtlEnabled: this.refs.rtlEnabled.checked
|
||||
@@ -752,126 +745,6 @@ class UiTab extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Matching character pairs')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input
|
||||
styleName='group-section-control-input'
|
||||
value={this.state.config.editor.matchingPairs}
|
||||
ref='matchingPairs'
|
||||
onChange={e => this.handleUIChange(e)}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label-right'>
|
||||
{i18n.__('in code blocks')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input
|
||||
styleName='group-section-control-input'
|
||||
value={this.state.config.editor.codeBlockMatchingPairs}
|
||||
ref='codeBlockMatchingPairs'
|
||||
onChange={e => this.handleUIChange(e)}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Close pairs before')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input
|
||||
styleName='group-section-control-input'
|
||||
value={this.state.config.editor.matchingCloseBefore}
|
||||
ref='matchingCloseBefore'
|
||||
onChange={e => this.handleUIChange(e)}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label-right'>
|
||||
{i18n.__('in code blocks')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input
|
||||
styleName='group-section-control-input'
|
||||
value={this.state.config.editor.codeBlockMatchingCloseBefore}
|
||||
ref='codeBlockMatchingCloseBefore'
|
||||
onChange={e => this.handleUIChange(e)}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Matching character triples')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input
|
||||
styleName='group-section-control-input'
|
||||
value={this.state.config.editor.matchingTriples}
|
||||
ref='matchingTriples'
|
||||
onChange={e => this.handleUIChange(e)}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label-right'>
|
||||
{i18n.__('in code blocks')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input
|
||||
styleName='group-section-control-input'
|
||||
value={this.state.config.editor.codeBlockMatchingTriples}
|
||||
ref='codeBlockMatchingTriples'
|
||||
onChange={e => this.handleUIChange(e)}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Exploding character pairs')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input
|
||||
styleName='group-section-control-input'
|
||||
value={this.state.config.editor.explodingPairs}
|
||||
ref='explodingPairs'
|
||||
onChange={e => this.handleUIChange(e)}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label-right'>
|
||||
{i18n.__('in code blocks')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input
|
||||
styleName='group-section-control-input'
|
||||
value={this.state.config.editor.codeBlockExplodingPairs}
|
||||
ref='codeBlockExplodingPairs'
|
||||
onChange={e => this.handleUIChange(e)}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input
|
||||
@@ -1002,19 +875,50 @@ class UiTab extends React.Component {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Matching character pairs')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input
|
||||
styleName='group-section-control-input'
|
||||
value={this.state.config.editor.matchingPairs}
|
||||
ref='matchingPairs'
|
||||
onChange={e => this.handleUIChange(e)}
|
||||
checked={this.state.config.editor.dateFormatISO8601}
|
||||
ref='dateFormatISO8601'
|
||||
type='checkbox'
|
||||
type='text'
|
||||
/>
|
||||
|
||||
{i18n.__('Date shortcut use iso 8601 format')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Matching character triples')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input
|
||||
styleName='group-section-control-input'
|
||||
value={this.state.config.editor.matchingTriples}
|
||||
ref='matchingTriples'
|
||||
onChange={e => this.handleUIChange(e)}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Exploding character pairs')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input
|
||||
styleName='group-section-control-input'
|
||||
value={this.state.config.editor.explodingPairs}
|
||||
ref='explodingPairs'
|
||||
onChange={e => this.handleUIChange(e)}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Custom MarkdownLint Rules')}
|
||||
|
||||
@@ -6,7 +6,6 @@ import UiTab from './UiTab'
|
||||
import InfoTab from './InfoTab'
|
||||
import Crowdfunding from './Crowdfunding'
|
||||
import StoragesTab from './StoragesTab'
|
||||
import ExportTab from './ExportTab'
|
||||
import SnippetTab from './SnippetTab'
|
||||
import PluginsTab from './PluginsTab'
|
||||
import Blog from './Blog'
|
||||
@@ -25,8 +24,7 @@ class Preferences extends React.Component {
|
||||
currentTab: 'STORAGES',
|
||||
UIAlert: '',
|
||||
HotkeyAlert: '',
|
||||
BlogAlert: '',
|
||||
ExportAlert: ''
|
||||
BlogAlert: ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,15 +81,6 @@ class Preferences extends React.Component {
|
||||
haveToSave={alert => this.setState({ BlogAlert: alert })}
|
||||
/>
|
||||
)
|
||||
case 'EXPORT':
|
||||
return (
|
||||
<ExportTab
|
||||
dispatch={dispatch}
|
||||
config={config}
|
||||
data={data}
|
||||
haveToSave={alert => this.setState({ ExportAlert: alert })}
|
||||
/>
|
||||
)
|
||||
case 'SNIPPET':
|
||||
return <SnippetTab dispatch={dispatch} config={config} data={data} />
|
||||
case 'PLUGINS':
|
||||
@@ -142,11 +131,6 @@ class Preferences extends React.Component {
|
||||
{ target: 'INFO', label: i18n.__('About') },
|
||||
{ target: 'CROWDFUNDING', label: i18n.__('Crowdfunding') },
|
||||
{ target: 'BLOG', label: i18n.__('Blog'), Blog: this.state.BlogAlert },
|
||||
{
|
||||
target: 'EXPORT',
|
||||
label: i18n.__('Export'),
|
||||
Export: this.state.ExportAlert
|
||||
},
|
||||
{ target: 'SNIPPET', label: i18n.__('Snippets') },
|
||||
{ target: 'PLUGINS', label: i18n.__('Plugins') }
|
||||
]
|
||||
|
||||
196
extra_scripts/codemirror/addon/edit/closebrackets.js
vendored
196
extra_scripts/codemirror/addon/edit/closebrackets.js
vendored
@@ -1,196 +0,0 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
var defaults = {
|
||||
pairs: "()[]{}''\"\"",
|
||||
closeBefore: ")]}'\":;>",
|
||||
triples: "",
|
||||
explode: "[]{}"
|
||||
};
|
||||
|
||||
var Pos = CodeMirror.Pos;
|
||||
|
||||
CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) {
|
||||
if (old && old != CodeMirror.Init) {
|
||||
cm.removeKeyMap(keyMap);
|
||||
cm.state.closeBrackets = null;
|
||||
}
|
||||
if (val) {
|
||||
ensureBound(getOption(val.markdown, "pairs"))
|
||||
cm.state.closeBrackets = val;
|
||||
cm.addKeyMap(keyMap);
|
||||
}
|
||||
});
|
||||
|
||||
function getOption(conf, name) {
|
||||
if (name == "pairs" && typeof conf == "string") return conf;
|
||||
if (typeof conf == "object" && conf[name] != null) return conf[name];
|
||||
return defaults[name];
|
||||
}
|
||||
|
||||
var keyMap = {Backspace: handleBackspace, Enter: handleEnter};
|
||||
function ensureBound(chars) {
|
||||
for (var i = 0; i < chars.length; i++) {
|
||||
var ch = chars.charAt(i), key = "'" + ch + "'"
|
||||
if (!keyMap[key]) keyMap[key] = handler(ch)
|
||||
}
|
||||
}
|
||||
ensureBound(defaults.pairs + "`")
|
||||
|
||||
function handler(ch) {
|
||||
return function(cm) { return handleChar(cm, ch); };
|
||||
}
|
||||
|
||||
function getConfig(cm) {
|
||||
var cursor = cm.getCursor();
|
||||
var token = cm.getTokenAt(cursor);
|
||||
var inCodeBlock = !!token.state.fencedEndRE;
|
||||
|
||||
if (inCodeBlock) {
|
||||
return cm.state.closeBrackets.codeBlock
|
||||
} else {
|
||||
return cm.state.closeBrackets.markdown
|
||||
}
|
||||
}
|
||||
|
||||
function handleBackspace(cm) {
|
||||
var conf = getConfig(cm);
|
||||
if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
|
||||
|
||||
var pairs = getOption(conf, "pairs");
|
||||
var ranges = cm.listSelections();
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
if (!ranges[i].empty()) return CodeMirror.Pass;
|
||||
var around = charsAround(cm, ranges[i].head);
|
||||
if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
|
||||
}
|
||||
for (var i = ranges.length - 1; i >= 0; i--) {
|
||||
var cur = ranges[i].head;
|
||||
cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete");
|
||||
}
|
||||
}
|
||||
|
||||
function handleEnter(cm) {
|
||||
var conf = getConfig(cm);
|
||||
var explode = conf && getOption(conf, "explode");
|
||||
if (!explode || cm.getOption("disableInput")) return CodeMirror.Pass;
|
||||
|
||||
var ranges = cm.listSelections();
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
if (!ranges[i].empty()) return CodeMirror.Pass;
|
||||
var around = charsAround(cm, ranges[i].head);
|
||||
if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass;
|
||||
}
|
||||
cm.operation(function() {
|
||||
var linesep = cm.lineSeparator() || "\n";
|
||||
cm.replaceSelection(linesep + linesep, null);
|
||||
cm.execCommand("goCharLeft");
|
||||
ranges = cm.listSelections();
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var line = ranges[i].head.line;
|
||||
cm.indentLine(line, null, true);
|
||||
cm.indentLine(line + 1, null, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function contractSelection(sel) {
|
||||
var inverted = CodeMirror.cmpPos(sel.anchor, sel.head) > 0;
|
||||
return {anchor: new Pos(sel.anchor.line, sel.anchor.ch + (inverted ? -1 : 1)),
|
||||
head: new Pos(sel.head.line, sel.head.ch + (inverted ? 1 : -1))};
|
||||
}
|
||||
|
||||
function handleChar(cm, ch) {
|
||||
var conf = getConfig(cm);
|
||||
if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
|
||||
|
||||
var pairs = getOption(conf, "pairs");
|
||||
var pos = pairs.indexOf(ch);
|
||||
if (pos == -1) return CodeMirror.Pass;
|
||||
|
||||
var closeBefore = getOption(conf,"closeBefore");
|
||||
|
||||
var triples = getOption(conf, "triples");
|
||||
|
||||
var identical = pairs.charAt(pos + 1) == ch;
|
||||
var ranges = cm.listSelections();
|
||||
var opening = pos % 2 == 0;
|
||||
|
||||
var type;
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var range = ranges[i], cur = range.head, curType;
|
||||
var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1));
|
||||
if (opening && !range.empty()) {
|
||||
curType = "surround";
|
||||
} else if ((identical || !opening) && next == ch) {
|
||||
if (identical && stringStartsAfter(cm, cur))
|
||||
curType = "both";
|
||||
else if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch)
|
||||
curType = "skipThree";
|
||||
else
|
||||
curType = "skip";
|
||||
} else if (identical && cur.ch > 1 && triples.indexOf(ch) >= 0 &&
|
||||
cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch) {
|
||||
if (cur.ch > 2 && /\bstring/.test(cm.getTokenTypeAt(Pos(cur.line, cur.ch - 2)))) return CodeMirror.Pass;
|
||||
curType = "addFour";
|
||||
} else if (identical) {
|
||||
var prev = cur.ch == 0 ? " " : cm.getRange(Pos(cur.line, cur.ch - 1), cur)
|
||||
if (!CodeMirror.isWordChar(next) && prev != ch && !CodeMirror.isWordChar(prev)) curType = "both";
|
||||
else return CodeMirror.Pass;
|
||||
} else if (opening && (next.length === 0 || /\s/.test(next) || closeBefore.indexOf(next) > -1)) {
|
||||
curType = "both";
|
||||
} else {
|
||||
return CodeMirror.Pass;
|
||||
}
|
||||
if (!type) type = curType;
|
||||
else if (type != curType) return CodeMirror.Pass;
|
||||
}
|
||||
|
||||
var left = pos % 2 ? pairs.charAt(pos - 1) : ch;
|
||||
var right = pos % 2 ? ch : pairs.charAt(pos + 1);
|
||||
cm.operation(function() {
|
||||
if (type == "skip") {
|
||||
cm.execCommand("goCharRight");
|
||||
} else if (type == "skipThree") {
|
||||
for (var i = 0; i < 3; i++)
|
||||
cm.execCommand("goCharRight");
|
||||
} else if (type == "surround") {
|
||||
var sels = cm.getSelections();
|
||||
for (var i = 0; i < sels.length; i++)
|
||||
sels[i] = left + sels[i] + right;
|
||||
cm.replaceSelections(sels, "around");
|
||||
sels = cm.listSelections().slice();
|
||||
for (var i = 0; i < sels.length; i++)
|
||||
sels[i] = contractSelection(sels[i]);
|
||||
cm.setSelections(sels);
|
||||
} else if (type == "both") {
|
||||
cm.replaceSelection(left + right, null);
|
||||
cm.triggerElectric(left + right);
|
||||
cm.execCommand("goCharLeft");
|
||||
} else if (type == "addFour") {
|
||||
cm.replaceSelection(left + left + left + left, "before");
|
||||
cm.execCommand("goCharRight");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function charsAround(cm, pos) {
|
||||
var str = cm.getRange(Pos(pos.line, pos.ch - 1),
|
||||
Pos(pos.line, pos.ch + 1));
|
||||
return str.length == 2 ? str : null;
|
||||
}
|
||||
|
||||
function stringStartsAfter(cm, pos) {
|
||||
var token = cm.getTokenAt(Pos(pos.line, pos.ch + 1))
|
||||
return /\bstring/.test(token.type) && token.start == pos.ch &&
|
||||
(pos.ch == 0 || !/\bstring/.test(cm.getTokenTypeAt(pos)))
|
||||
}
|
||||
});
|
||||
292
extra_scripts/codemirror/mode/bfm/bfm.js
vendored
292
extra_scripts/codemirror/mode/bfm/bfm.js
vendored
@@ -55,168 +55,202 @@
|
||||
}
|
||||
}
|
||||
|
||||
CodeMirror.defineMode('bfm', function (config, baseConfig) {
|
||||
baseConfig.name = 'yaml-frontmatter'
|
||||
const baseMode = CodeMirror.getMode(config, baseConfig)
|
||||
CodeMirror.defineMode(
|
||||
'bfm',
|
||||
function(config, baseConfig) {
|
||||
baseConfig.name = 'yaml-frontmatter'
|
||||
const baseMode = CodeMirror.getMode(config, baseConfig)
|
||||
|
||||
return {
|
||||
startState: function() {
|
||||
return {
|
||||
baseState: CodeMirror.startState(baseMode),
|
||||
return {
|
||||
startState: function() {
|
||||
return {
|
||||
baseState: CodeMirror.startState(baseMode),
|
||||
|
||||
basePos: 0,
|
||||
baseCur: null,
|
||||
overlayPos: 0,
|
||||
overlayCur: null,
|
||||
streamSeen: null,
|
||||
basePos: 0,
|
||||
baseCur: null,
|
||||
overlayPos: 0,
|
||||
overlayCur: null,
|
||||
streamSeen: null,
|
||||
|
||||
fencedEndRE: null,
|
||||
fencedEndRE: null,
|
||||
|
||||
inTable: false,
|
||||
rowIndex: 0
|
||||
}
|
||||
},
|
||||
copyState: function(s) {
|
||||
return {
|
||||
baseState: CodeMirror.copyState(baseMode, s.baseState),
|
||||
inTable: false,
|
||||
rowIndex: 0
|
||||
}
|
||||
},
|
||||
copyState: function(s) {
|
||||
return {
|
||||
baseState: CodeMirror.copyState(baseMode, s.baseState),
|
||||
|
||||
basePos: s.basePos,
|
||||
baseCur: null,
|
||||
overlayPos: s.overlayPos,
|
||||
overlayCur: null,
|
||||
basePos: s.basePos,
|
||||
baseCur: null,
|
||||
overlayPos: s.overlayPos,
|
||||
overlayCur: null,
|
||||
|
||||
fencedMode: s.fencedMode,
|
||||
fencedState: s.fencedMode ? CodeMirror.copyState(s.fencedMode, s.fencedState) : null,
|
||||
fencedMode: s.fencedMode,
|
||||
fencedState: s.fencedMode
|
||||
? CodeMirror.copyState(s.fencedMode, s.fencedState)
|
||||
: null,
|
||||
|
||||
fencedEndRE: s.fencedEndRE,
|
||||
fencedEndRE: s.fencedEndRE,
|
||||
|
||||
inTable: s.inTable,
|
||||
rowIndex: s.rowIndex
|
||||
}
|
||||
},
|
||||
token: function(stream, state) {
|
||||
const initialPos = stream.pos
|
||||
inTable: s.inTable,
|
||||
rowIndex: s.rowIndex
|
||||
}
|
||||
},
|
||||
token: function(stream, state) {
|
||||
const initialPos = stream.pos
|
||||
|
||||
if (state.fencedEndRE) {
|
||||
if (stream.match(state.fencedEndRE)) {
|
||||
if (state.fencedEndRE && stream.match(state.fencedEndRE)) {
|
||||
state.fencedEndRE = null
|
||||
state.fencedMode = null
|
||||
state.fencedState = null
|
||||
|
||||
stream.pos = initialPos
|
||||
} else if (state.fencedMode) {
|
||||
return state.fencedMode.token(stream, state.fencedState)
|
||||
} else {
|
||||
if (state.fencedMode) {
|
||||
return state.fencedMode.token(stream, state.fencedState)
|
||||
}
|
||||
|
||||
const match = stream.match(fencedCodeRE, true)
|
||||
if (match) {
|
||||
state.fencedEndRE = new RegExp(match[1] + '+ *$')
|
||||
|
||||
state.fencedMode = getMode(
|
||||
match[2],
|
||||
match[3],
|
||||
config,
|
||||
stream.lineOracle.doc.cm
|
||||
)
|
||||
if (state.fencedMode) {
|
||||
state.fencedState = CodeMirror.startState(state.fencedMode)
|
||||
}
|
||||
|
||||
stream.pos = initialPos
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
stream != state.streamSeen ||
|
||||
Math.min(state.basePos, state.overlayPos) < stream.start
|
||||
) {
|
||||
state.streamSeen = stream
|
||||
state.basePos = state.overlayPos = stream.start
|
||||
}
|
||||
|
||||
if (stream.start == state.basePos) {
|
||||
state.baseCur = baseMode.token(stream, state.baseState)
|
||||
state.basePos = stream.pos
|
||||
}
|
||||
if (stream.start == state.overlayPos) {
|
||||
stream.pos = stream.start
|
||||
state.overlayCur = this.overlayToken(stream, state)
|
||||
state.overlayPos = stream.pos
|
||||
}
|
||||
stream.pos = Math.min(state.basePos, state.overlayPos)
|
||||
|
||||
if (state.overlayCur == null) {
|
||||
return state.baseCur
|
||||
} else if (state.baseCur != null && state.combineTokens) {
|
||||
return state.baseCur + ' ' + state.overlayCur
|
||||
} else {
|
||||
return state.overlayCur
|
||||
}
|
||||
}
|
||||
else {
|
||||
},
|
||||
overlayToken: function(stream, state) {
|
||||
state.combineTokens = false
|
||||
|
||||
if (state.fencedEndRE && stream.match(state.fencedEndRE)) {
|
||||
state.fencedEndRE = null
|
||||
state.localMode = null
|
||||
state.localState = null
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
if (state.localMode) {
|
||||
return state.localMode.token(stream, state.localState) || ''
|
||||
}
|
||||
|
||||
const match = stream.match(fencedCodeRE, true)
|
||||
if (match) {
|
||||
state.fencedEndRE = new RegExp(match[1] + '+ *$')
|
||||
|
||||
state.fencedMode = getMode(match[2], match[3], config, stream.lineOracle.doc.cm)
|
||||
if (state.fencedMode) {
|
||||
state.fencedState = CodeMirror.startState(state.fencedMode)
|
||||
state.localMode = getMode(
|
||||
match[2],
|
||||
match[3],
|
||||
config,
|
||||
stream.lineOracle.doc.cm
|
||||
)
|
||||
if (state.localMode) {
|
||||
state.localState = CodeMirror.startState(state.localMode)
|
||||
}
|
||||
|
||||
stream.pos = initialPos
|
||||
}
|
||||
}
|
||||
|
||||
if (stream != state.streamSeen || Math.min(state.basePos, state.overlayPos) < stream.start) {
|
||||
state.streamSeen = stream
|
||||
state.basePos = state.overlayPos = stream.start
|
||||
}
|
||||
|
||||
if (stream.start == state.basePos) {
|
||||
state.baseCur = baseMode.token(stream, state.baseState)
|
||||
state.basePos = stream.pos
|
||||
}
|
||||
if (stream.start == state.overlayPos) {
|
||||
stream.pos = stream.start
|
||||
state.overlayCur = this.overlayToken(stream, state)
|
||||
state.overlayPos = stream.pos
|
||||
}
|
||||
stream.pos = Math.min(state.basePos, state.overlayPos)
|
||||
|
||||
if (state.overlayCur == null) {
|
||||
return state.baseCur
|
||||
}
|
||||
else if (state.baseCur != null && state.combineTokens) {
|
||||
return state.baseCur + ' ' + state.overlayCur
|
||||
}
|
||||
else {
|
||||
return state.overlayCur
|
||||
}
|
||||
},
|
||||
overlayToken: function(stream, state) {
|
||||
state.combineTokens = false
|
||||
|
||||
if (state.localMode) {
|
||||
return state.localMode.token(stream, state.localState) || ''
|
||||
}
|
||||
|
||||
state.combineTokens = true
|
||||
|
||||
if (state.inTable) {
|
||||
if (stream.match(/^\|/)) {
|
||||
++state.rowIndex
|
||||
|
||||
stream.skipToEnd()
|
||||
|
||||
if (state.rowIndex === 1) {
|
||||
return 'table table-separator'
|
||||
} else if (state.rowIndex % 2 === 0) {
|
||||
return 'table table-row table-row-even'
|
||||
} else {
|
||||
return 'table table-row table-row-odd'
|
||||
}
|
||||
} else {
|
||||
state.inTable = false
|
||||
|
||||
stream.skipToEnd()
|
||||
return null
|
||||
}
|
||||
} else if (stream.match(/^\|/)) {
|
||||
state.inTable = true
|
||||
state.rowIndex = 0
|
||||
|
||||
state.combineTokens = true
|
||||
|
||||
if (state.inTable) {
|
||||
if (stream.match(/^\|/)) {
|
||||
++state.rowIndex
|
||||
|
||||
stream.skipToEnd()
|
||||
|
||||
if (state.rowIndex === 1) {
|
||||
return 'table table-separator'
|
||||
} else if (state.rowIndex % 2 === 0) {
|
||||
return 'table table-row table-row-even'
|
||||
} else {
|
||||
return 'table table-row table-row-odd'
|
||||
}
|
||||
} else {
|
||||
state.inTable = false
|
||||
|
||||
stream.skipToEnd()
|
||||
return null
|
||||
}
|
||||
} else if (stream.match(/^\|/)) {
|
||||
state.inTable = true
|
||||
state.rowIndex = 0
|
||||
|
||||
stream.skipToEnd()
|
||||
return 'table table-header'
|
||||
}
|
||||
|
||||
stream.skipToEnd()
|
||||
return 'table table-header'
|
||||
}
|
||||
|
||||
stream.skipToEnd()
|
||||
return null
|
||||
},
|
||||
electricChars: baseMode.electricChars,
|
||||
innerMode: function(state) {
|
||||
if (state.fencedMode) {
|
||||
return {
|
||||
mode: state.fencedMode,
|
||||
state: state.fencedState
|
||||
return null
|
||||
},
|
||||
electricChars: baseMode.electricChars,
|
||||
innerMode: function(state) {
|
||||
if (state.fencedMode) {
|
||||
return {
|
||||
mode: state.fencedMode,
|
||||
state: state.fencedState
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
mode: baseMode,
|
||||
state: state.baseState
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
mode: baseMode,
|
||||
state: state.baseState
|
||||
}
|
||||
}
|
||||
},
|
||||
blankLine: function(state) {
|
||||
state.inTable = false
|
||||
},
|
||||
blankLine: function(state) {
|
||||
state.inTable = false
|
||||
|
||||
if (state.fencedMode) {
|
||||
return state.fencedMode.blankLine && state.fencedMode.blankLine(state.fencedState)
|
||||
} else {
|
||||
return baseMode.blankLine(state.baseState)
|
||||
if (state.fencedMode) {
|
||||
return (
|
||||
state.fencedMode.blankLine &&
|
||||
state.fencedMode.blankLine(state.fencedState)
|
||||
)
|
||||
} else {
|
||||
return baseMode.blankLine(state.baseState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 'yaml-frontmatter')
|
||||
},
|
||||
'yaml-frontmatter'
|
||||
)
|
||||
|
||||
CodeMirror.defineMIME('text/x-bfm', 'bfm')
|
||||
|
||||
@@ -225,4 +259,4 @@
|
||||
mime: 'text/x-bfm',
|
||||
mode: 'bfm'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -187,7 +187,7 @@ module.exports = function(grunt) {
|
||||
}
|
||||
|
||||
ChildProcess.exec(
|
||||
`codesign --verbose --deep --force --timestamp=none --sign \"${OSX_COMMON_NAME}\" dist/Boostnote-darwin-x64/Boostnote.app`,
|
||||
`codesign --verbose --deep --force --sign \"${OSX_COMMON_NAME}\" dist/Boostnote-darwin-x64/Boostnote.app`,
|
||||
function(err, stdout, stderr) {
|
||||
grunt.log.writeln(stdout)
|
||||
if (err) {
|
||||
|
||||
@@ -26,7 +26,6 @@ if (!singleInstance) {
|
||||
}
|
||||
|
||||
var isUpdateReady = false
|
||||
let updateFound = false
|
||||
|
||||
var ghReleasesOpts = {
|
||||
repo: 'BoostIO/boost-releases',
|
||||
@@ -37,33 +36,25 @@ const updater = new GhReleases(ghReleasesOpts)
|
||||
|
||||
// Check for updates
|
||||
// `status` returns true if there is a new update available
|
||||
function checkUpdate(manualTriggered = false) {
|
||||
function checkUpdate() {
|
||||
if (!isPackaged) {
|
||||
// Prevents app from attempting to update when in dev mode.
|
||||
console.log('Updates are disabled in Development mode, see main-app.js')
|
||||
return true
|
||||
}
|
||||
|
||||
// End if auto updates disabled and it is an automatic check
|
||||
if (!electronConfig.get('autoUpdateEnabled', true) && !manualTriggered) return
|
||||
|
||||
if (process.platform === 'linux' || isUpdateReady || updateFound) {
|
||||
if (!electronConfig.get('autoUpdateEnabled', true)) return
|
||||
if (process.platform === 'linux' || isUpdateReady) {
|
||||
return true
|
||||
}
|
||||
|
||||
updater.check((err, status) => {
|
||||
if (err) {
|
||||
var isLatest = err.message === 'There is no newer version.'
|
||||
if (!isLatest) console.error('Updater error! %s', err.message)
|
||||
mainWindow.webContents.send(
|
||||
'update-not-found',
|
||||
isLatest ? 'There is no newer version.' : 'Updater error'
|
||||
)
|
||||
return
|
||||
}
|
||||
if (status) {
|
||||
mainWindow.webContents.send('update-found', 'Update available!')
|
||||
updateFound = true
|
||||
updater.download()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -72,7 +63,6 @@ updater.on('update-downloaded', info => {
|
||||
if (mainWindow != null) {
|
||||
mainWindow.webContents.send('update-ready', 'Update available!')
|
||||
isUpdateReady = true
|
||||
updateFound = false
|
||||
}
|
||||
})
|
||||
|
||||
@@ -87,14 +77,6 @@ ipc.on('update-app-confirm', function(event, msg) {
|
||||
}
|
||||
})
|
||||
|
||||
ipc.on('update-cancel', () => {
|
||||
updateFound = false
|
||||
})
|
||||
|
||||
ipc.on('update-download-confirm', () => {
|
||||
updater.download()
|
||||
})
|
||||
|
||||
app.on('window-all-closed', function() {
|
||||
app.quit()
|
||||
})
|
||||
@@ -131,7 +113,7 @@ app.on('ready', function() {
|
||||
if (isUpdateReady) {
|
||||
mainWindow.webContents.send('update-ready', 'Update available!')
|
||||
} else {
|
||||
checkUpdate(msg === 'manual')
|
||||
checkUpdate()
|
||||
}
|
||||
})
|
||||
}, 10 * 1000)
|
||||
|
||||
@@ -178,18 +178,6 @@ const file = {
|
||||
mainWindow.webContents.send('list:isMarkdownNote', 'print')
|
||||
mainWindow.webContents.send('print')
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Update',
|
||||
click() {
|
||||
mainWindow.webContents.send('update')
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -484,21 +472,9 @@ const help = {
|
||||
]
|
||||
}
|
||||
|
||||
const team = {
|
||||
label: 'For Team',
|
||||
submenu: [
|
||||
{
|
||||
label: 'BoostHub',
|
||||
click: async () => {
|
||||
shell.openExternal('https://boosthub.io/')
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
module.exports =
|
||||
process.platform === 'darwin'
|
||||
? [boost, file, edit, view, window, team, help]
|
||||
? [boost, file, edit, view, window, help]
|
||||
: process.platform === 'win32'
|
||||
? [boost, file, view, team, help]
|
||||
: [file, view, team, help]
|
||||
? [boost, file, view, help]
|
||||
: [file, view, help]
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
border-left-color: rgba(142, 142, 142, 0.5);
|
||||
mix-blend-mode: difference;
|
||||
}
|
||||
|
||||
|
||||
.CodeMirror-scroll {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
@@ -116,7 +116,7 @@
|
||||
<script src="../extra_scripts/codemirror/mode/gfm/gfm.js"></script>
|
||||
<script src="../extra_scripts/codemirror/addon/hyperlink/hyperlink.js"></script>
|
||||
|
||||
<script src="../extra_scripts/codemirror/addon/edit/closebrackets.js"></script>
|
||||
<script src="../node_modules/codemirror/addon/edit/closebrackets.js"></script>
|
||||
<script src="../node_modules/codemirror/addon/edit/matchbrackets.js"></script>
|
||||
|
||||
<script src="../node_modules/codemirror/addon/search/search.js"></script>
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
<script src="../extra_scripts/codemirror/mode/gfm/gfm.js"></script>
|
||||
<script src="../extra_scripts/codemirror/addon/hyperlink/hyperlink.js"></script>
|
||||
|
||||
<script src="../extra_scripts/codemirror/addon/edit/closebrackets.js"></script>
|
||||
<script src="../node_modules/codemirror/addon/edit/closebrackets.js"></script>
|
||||
<script src="../node_modules/codemirror/addon/edit/matchbrackets.js"></script>
|
||||
|
||||
<script src="../node_modules/codemirror/addon/search/search.js"></script>
|
||||
|
||||
@@ -202,6 +202,7 @@
|
||||
"Create new folder": "Ordner erstellen",
|
||||
"Folder name": "Ordnername",
|
||||
"Create": "Erstellen",
|
||||
"Untitled": "Neuer Ordner",
|
||||
"Unlink Storage": "Speicherverknüpfung aufheben",
|
||||
"Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.": "Die Verknüpfung des Speichers mit Boostnote wird entfernt. Es werden keine Daten gelöscht. Um die Daten dauerhaft zu löschen musst du den Ordner auf der Festplatte manuell entfernen.",
|
||||
"Empty note": "Leere Notiz",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "boost",
|
||||
"productName": "Boostnote",
|
||||
"version": "0.16.1",
|
||||
"version": "0.15.3",
|
||||
"main": "index.js",
|
||||
"description": "Boostnote",
|
||||
"license": "GPL-3.0",
|
||||
@@ -186,7 +186,7 @@
|
||||
"stylus": "^0.52.4",
|
||||
"stylus-loader": "^2.3.1",
|
||||
"webpack": "^1.12.2",
|
||||
"webpack-dev-server": "^1.12.0"
|
||||
"webpack-dev-server": "^3.1.11"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"grunt-electron-installer-debian": "^0.5.0",
|
||||
|
||||
66
readme.md
66
readme.md
@@ -1,26 +1,12 @@
|
||||
> # New Boost Note app is available!
|
||||
>
|
||||
> We've launched a new Boost Note app which supports real-time collaborative writing.
|
||||
>
|
||||
> And it is open sourced too! Please check it out! https://github.com/BoostIO/BoostNote-App
|
||||
>
|
||||
> ## 📦 Download App
|
||||
>
|
||||
> ### 🖥 Desktop
|
||||
>
|
||||
> - [🌎 Web App (boostnote.io)](https://boostnote.io)
|
||||
> - [🍎 macOS (.dmg)](https://github.com/BoostIO/BoostNote-App/releases/latest/download/boost-note-mac.dmg)
|
||||
> - [:framed_picture: Windows (.exe NSIS)](https://github.com/BoostIO/BoostNote-App/releases/latest/download/boost-note-win.exe)
|
||||
> - [🐧 Linux (.deb)](https://github.com/BoostIO/BoostNote-App/releases/latest/download/boost-note-linux.deb)
|
||||
> - [🐧 Linux (.rpm)](https://github.com/BoostIO/BoostNote-App/releases/latest/download/boost-note-linux.rpm)
|
||||
>
|
||||
> ### 📱 Mobile
|
||||
>
|
||||
> - [🌎 Mobile Web App (m.boostnote.io)](https://m.boostnote.io)
|
||||
> - [🍏 iOS (Apple App Store)](https://apps.apple.com/gb/app/boost-note-mobile/id1576176505)
|
||||
> - [🤖 Android (Google Play Store)](https://play.google.com/store/apps/details?id=com.boostio.boostnote2021)
|
||||
> [We've launched desktop and mobile app of the new Boost Note now.](https://github.com/BoostIO/BoostNote.next)
|
||||
|
||||
<h1 align="center">BoostNote-Legacy</h1>
|
||||
> ### [Boost Note for Teams](https://boosthub.io/)
|
||||
>
|
||||
> We've developed a collaborative workspace app called "Boost Hub" for developer teams.
|
||||
>
|
||||
> It's customizable and easy to optimize for your team like rego blocks and even lets you edit documents together in real-time!
|
||||
|
||||

|
||||
|
||||
<h4 align="center">Note-taking app for programmers. </h4>
|
||||
<h5 align="center">Apps available for Mac, Windows and Linux.</h5>
|
||||
@@ -35,6 +21,42 @@
|
||||
|
||||
[Find the latest release of Boostnote here!](https://github.com/BoostIO/boost-releases/releases/)
|
||||
|
||||
## Roadmap
|
||||
|
||||
[Boost Note Roadmap 2020](https://medium.com/boostnote/boost-note-roadmap-2020-9f06a642f5f1)
|
||||
|
||||
## Authors & Maintainers
|
||||
|
||||
- [Rokt33r](https://github.com/rokt33r)
|
||||
- [KZ](https://github.com/kazup01)
|
||||
- [ZeroX-DG](https://github.com/ZeroX-DG)
|
||||
|
||||
## Contributors
|
||||
|
||||
Thank you to all the people who have contributed to Boostnote!
|
||||
|
||||
<a href="https://github.com/BoostIO/Boostnote/graphs/contributors"><img src="https://opencollective.com/boostnoteio/contributors.svg?width=890" /></a>
|
||||
|
||||
## Supporting Boostnote
|
||||
|
||||
Boostnote is an open source project. It's an independent project with its ongoing development made possible thanks to the support by our amazing backers.
|
||||
|
||||
Issues on Boostnote can be funded by anyone and the money will be distributed to contributors and maintainers. If you use Boostnote please consider becoming a backer:
|
||||
|
||||
[](https://issuehunt.io/repos/53266139)
|
||||
|
||||
## Community
|
||||
|
||||
- [Facebook Group](https://www.facebook.com/groups/boostnote/)
|
||||
- [Twitter](https://twitter.com/boostnoteapp)
|
||||
- [Slack Group](https://join.slack.com/t/boostnote-group/shared_invite/zt-cun7pas3-WwkaezxHBB1lCbUHrwQLXw)
|
||||
- [Blog](https://medium.com/boostnote)
|
||||
- [Reddit](https://www.reddit.com/r/Boostnote/)
|
||||
|
||||
### Boostnote mobile
|
||||
A community project developing a mobile cross-platform version of boostnote for iOS and Android can be found here: [NoteApp](https://github.com/T0M0F/NoteApp)
|
||||
|
||||
|
||||
#### More Information
|
||||
|
||||
- Website: https://boostnote.io
|
||||
|
||||
@@ -702,15 +702,14 @@ it('should remove the all ":storage" and noteKey references', function() {
|
||||
' </p>\n' +
|
||||
' </body>\n' +
|
||||
'</html>'
|
||||
const actual = systemUnderTest.replaceStorageReferences(
|
||||
const actual = systemUnderTest.removeStorageAndNoteReferences(
|
||||
testInput,
|
||||
noteKey,
|
||||
systemUnderTest.DESTINATION_FOLDER
|
||||
noteKey
|
||||
)
|
||||
expect(actual).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
it('should make sure that "replaceStorageReferences" works with markdown content as well', function() {
|
||||
it('should make sure that "removeStorageAndNoteReferences" works with markdown content as well', function() {
|
||||
const noteKey = 'noteKey'
|
||||
const testInput =
|
||||
'Test input' +
|
||||
@@ -737,113 +736,9 @@ it('should make sure that "replaceStorageReferences" works with markdown content
|
||||
systemUnderTest.DESTINATION_FOLDER +
|
||||
path.posix.sep +
|
||||
'pdf.pdf)'
|
||||
const actual = systemUnderTest.replaceStorageReferences(
|
||||
const actual = systemUnderTest.removeStorageAndNoteReferences(
|
||||
testInput,
|
||||
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
|
||||
noteKey
|
||||
)
|
||||
expect(actual).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
@@ -52,20 +52,12 @@ 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, config)
|
||||
return exportFolder(storageKey, folderKey, 'md', storagePath)
|
||||
})
|
||||
.then(function assert() {
|
||||
let filePath = path.join(storagePath, 'input1.md')
|
||||
|
||||
@@ -35,16 +35,7 @@ test.serial('Export a storage', t => {
|
||||
acc[folder.key] = folder.name
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const config = {
|
||||
export: {
|
||||
metadata: 'DONT_EXPORT',
|
||||
variable: 'boostnote',
|
||||
prefixAttachmentFolder: false
|
||||
}
|
||||
}
|
||||
|
||||
return exportStorage(storageKey, 'md', exportDir, config).then(() => {
|
||||
return exportStorage(storageKey, 'md', exportDir).then(() => {
|
||||
notes.forEach(note => {
|
||||
const noteDir = path.join(
|
||||
exportDir,
|
||||
|
||||
@@ -98,11 +98,11 @@ exports[`Markdown.render() should renders checkboxes 1`] = `
|
||||
|
||||
exports[`Markdown.render() should renders codeblock correctly 1`] = `
|
||||
"<pre class=\\"code CodeMirror\\" data-line=\\"1\\">
|
||||
<span class=\\"filename\\">filename.js</span>
|
||||
<span class=\\"lineNumber CodeMirror-gutters\\"><span class=\\"CodeMirror-linenumber\\">2</span></span>
|
||||
<code class=\\"js\\">var project = 'boostnote';
|
||||
<span class=\\"filename\\">filename.js</span>
|
||||
<span class=\\"lineNumber CodeMirror-gutters\\"><span class=\\"CodeMirror-linenumber\\">2</span></span>
|
||||
<code class=\\"js\\">var project = 'boostnote';
|
||||
</code>
|
||||
</pre>"
|
||||
</pre>"
|
||||
`;
|
||||
|
||||
exports[`Markdown.render() should renders definition lists correctly 1`] = `
|
||||
|
||||
Reference in New Issue
Block a user